@jant/core 0.3.23 → 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 +4 -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 +3 -3
- 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 +61 -72
- 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/themes/threads/ThreadsSiteLayout.js +172 -0
- package/dist/themes/threads/index.js +81 -0
- package/dist/themes/{minimal → threads}/pages/ArchivePage.js +32 -47
- package/dist/themes/threads/pages/CollectionPage.js +65 -0
- package/dist/themes/{minimal → threads}/pages/HomePage.js +3 -3
- package/dist/themes/{minimal → threads}/pages/PostPage.js +12 -9
- package/dist/themes/{minimal → threads}/pages/SearchPage.js +13 -14
- package/dist/themes/{minimal → threads}/pages/SinglePage.js +4 -4
- package/dist/themes/threads/timeline/LinkCard.js +68 -0
- package/dist/themes/threads/timeline/NoteCard.js +53 -0
- package/dist/themes/threads/timeline/QuoteCard.js +59 -0
- package/dist/themes/{minimal → threads}/timeline/ThreadPreview.js +17 -13
- package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
- package/dist/themes/{minimal → threads}/timeline/TimelineItem.js +8 -16
- 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 +4 -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 +28 -12
- 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 +199 -51
- 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 +80 -82
- 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/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/themes/threads/ThreadsSiteLayout.tsx +194 -0
- package/src/themes/{minimal → threads}/index.ts +30 -13
- package/src/themes/{minimal → threads}/pages/ArchivePage.tsx +53 -53
- package/src/themes/threads/pages/CollectionPage.tsx +61 -0
- package/src/themes/{minimal → threads}/pages/HomePage.tsx +3 -3
- package/src/themes/{minimal → threads}/pages/PostPage.tsx +12 -8
- package/src/themes/{minimal → threads}/pages/SearchPage.tsx +15 -13
- package/src/themes/{minimal → threads}/pages/SinglePage.tsx +4 -4
- package/src/themes/threads/style.css +336 -0
- package/src/themes/threads/timeline/LinkCard.tsx +67 -0
- package/src/themes/threads/timeline/NoteCard.tsx +58 -0
- package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
- package/src/themes/{minimal → threads}/timeline/ThreadPreview.tsx +15 -13
- package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
- package/src/themes/{minimal → threads}/timeline/TimelineItem.tsx +9 -17
- 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/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/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/QuoteCard.js +0 -48
- package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
- package/src/routes/api/timeline.tsx +0 -159
- package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
- package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
- 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/TimelineFeed.tsx +0 -57
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import type { Bindings } from "../../types.js";
|
|
7
7
|
import type { AppVariables } from "../../app.js";
|
|
8
|
-
import { PostPage as DefaultPostPage } from "../../themes/
|
|
8
|
+
import { PostPage as DefaultPostPage } from "../../themes/threads/pages/PostPage.js";
|
|
9
9
|
import * as sqid from "../../lib/sqid.js";
|
|
10
10
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
11
11
|
import { renderPublicPage } from "../../lib/render.js";
|
|
@@ -19,24 +19,15 @@ export const postRoutes = new Hono<Env>();
|
|
|
19
19
|
postRoutes.get("/:id", async (c) => {
|
|
20
20
|
const paramId = c.req.param("id");
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// If not a valid sqid, try to find by path
|
|
26
|
-
if (!id) {
|
|
27
|
-
const post = await c.var.services.posts.getByPath(paramId);
|
|
28
|
-
if (post) {
|
|
29
|
-
id = post.id;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
22
|
+
// Decode sqid to numeric ID
|
|
23
|
+
const id = sqid.decode(paramId);
|
|
33
24
|
if (!id) return c.notFound();
|
|
34
25
|
|
|
35
26
|
const post = await c.var.services.posts.getById(id);
|
|
36
27
|
if (!post) return c.notFound();
|
|
37
28
|
|
|
38
29
|
// Don't show drafts on public site
|
|
39
|
-
if (post.
|
|
30
|
+
if (post.status === "draft") {
|
|
40
31
|
return c.notFound();
|
|
41
32
|
}
|
|
42
33
|
|
|
@@ -64,7 +55,7 @@ postRoutes.get("/:id", async (c) => {
|
|
|
64
55
|
|
|
65
56
|
return renderPublicPage(c, {
|
|
66
57
|
title,
|
|
67
|
-
description: post.
|
|
58
|
+
description: post.body?.slice(0, 160),
|
|
68
59
|
navData,
|
|
69
60
|
content: <Page post={postView} theme={components} />,
|
|
70
61
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import type { Bindings, SearchResult } from "../../types.js";
|
|
7
7
|
import type { AppVariables } from "../../app.js";
|
|
8
|
-
import { SearchPage as DefaultSearchPage } from "../../themes/
|
|
8
|
+
import { SearchPage as DefaultSearchPage } from "../../themes/threads/pages/SearchPage.js";
|
|
9
9
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
10
10
|
import { renderPublicPage } from "../../lib/render.js";
|
|
11
11
|
import { createMediaContext, toSearchResultViews } from "../../lib/view.js";
|
|
@@ -34,7 +34,7 @@ searchRoutes.get("/", async (c) => {
|
|
|
34
34
|
results = await c.var.services.search.search(query, {
|
|
35
35
|
limit: PAGE_SIZE + 1,
|
|
36
36
|
offset: (page - 1) * PAGE_SIZE,
|
|
37
|
-
|
|
37
|
+
status: ["published"],
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
hasMore = results.length > PAGE_SIZE;
|
|
@@ -19,44 +19,80 @@ describe("CollectionService", () => {
|
|
|
19
19
|
describe("create", () => {
|
|
20
20
|
it("creates a collection with required fields", async () => {
|
|
21
21
|
const collection = await collectionService.create({
|
|
22
|
+
slug: "my-collection",
|
|
22
23
|
title: "My Collection",
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
expect(collection.id).toBe(1);
|
|
27
|
+
expect(collection.slug).toBe("my-collection");
|
|
26
28
|
expect(collection.title).toBe("My Collection");
|
|
27
|
-
expect(collection.path).toBeNull();
|
|
28
29
|
expect(collection.description).toBeNull();
|
|
30
|
+
expect(collection.icon).toBeNull();
|
|
31
|
+
expect(collection.sortOrder).toBe("newest");
|
|
32
|
+
expect(collection.showDivider).toBe(0);
|
|
29
33
|
});
|
|
30
34
|
|
|
31
35
|
it("creates a collection with all fields", async () => {
|
|
32
36
|
const collection = await collectionService.create({
|
|
37
|
+
slug: "tech",
|
|
33
38
|
title: "Tech Posts",
|
|
34
|
-
path: "tech",
|
|
35
39
|
description: "Posts about technology",
|
|
40
|
+
icon: "laptop",
|
|
41
|
+
sortOrder: "oldest",
|
|
42
|
+
position: 5,
|
|
43
|
+
showDivider: true,
|
|
36
44
|
});
|
|
37
45
|
|
|
46
|
+
expect(collection.slug).toBe("tech");
|
|
38
47
|
expect(collection.title).toBe("Tech Posts");
|
|
39
|
-
expect(collection.path).toBe("tech");
|
|
40
48
|
expect(collection.description).toBe("Posts about technology");
|
|
49
|
+
expect(collection.icon).toBe("laptop");
|
|
50
|
+
expect(collection.sortOrder).toBe("oldest");
|
|
51
|
+
expect(collection.position).toBe(5);
|
|
52
|
+
expect(collection.showDivider).toBe(1);
|
|
41
53
|
});
|
|
42
54
|
|
|
43
55
|
it("sets timestamps", async () => {
|
|
44
56
|
const collection = await collectionService.create({
|
|
57
|
+
slug: "test",
|
|
45
58
|
title: "Test",
|
|
46
59
|
});
|
|
47
60
|
|
|
48
61
|
expect(collection.createdAt).toBeGreaterThan(0);
|
|
49
62
|
expect(collection.updatedAt).toBeGreaterThan(0);
|
|
50
63
|
});
|
|
64
|
+
|
|
65
|
+
it("auto-assigns position when not provided", async () => {
|
|
66
|
+
const first = await collectionService.create({
|
|
67
|
+
slug: "first",
|
|
68
|
+
title: "First",
|
|
69
|
+
});
|
|
70
|
+
const second = await collectionService.create({
|
|
71
|
+
slug: "second",
|
|
72
|
+
title: "Second",
|
|
73
|
+
});
|
|
74
|
+
const third = await collectionService.create({
|
|
75
|
+
slug: "third",
|
|
76
|
+
title: "Third",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(first.position).toBe(0);
|
|
80
|
+
expect(second.position).toBe(1);
|
|
81
|
+
expect(third.position).toBe(2);
|
|
82
|
+
});
|
|
51
83
|
});
|
|
52
84
|
|
|
53
85
|
describe("getById", () => {
|
|
54
86
|
it("returns a collection by ID", async () => {
|
|
55
|
-
const created = await collectionService.create({
|
|
87
|
+
const created = await collectionService.create({
|
|
88
|
+
slug: "test",
|
|
89
|
+
title: "Test",
|
|
90
|
+
});
|
|
56
91
|
|
|
57
92
|
const found = await collectionService.getById(created.id);
|
|
58
93
|
expect(found).not.toBeNull();
|
|
59
94
|
expect(found?.title).toBe("Test");
|
|
95
|
+
expect(found?.slug).toBe("test");
|
|
60
96
|
});
|
|
61
97
|
|
|
62
98
|
it("returns null for non-existent ID", async () => {
|
|
@@ -65,17 +101,18 @@ describe("CollectionService", () => {
|
|
|
65
101
|
});
|
|
66
102
|
});
|
|
67
103
|
|
|
68
|
-
describe("
|
|
69
|
-
it("returns a collection by
|
|
70
|
-
await collectionService.create({
|
|
104
|
+
describe("getBySlug", () => {
|
|
105
|
+
it("returns a collection by slug", async () => {
|
|
106
|
+
await collectionService.create({ slug: "tech", title: "Tech" });
|
|
71
107
|
|
|
72
|
-
const found = await collectionService.
|
|
108
|
+
const found = await collectionService.getBySlug("tech");
|
|
73
109
|
expect(found).not.toBeNull();
|
|
74
110
|
expect(found?.title).toBe("Tech");
|
|
111
|
+
expect(found?.slug).toBe("tech");
|
|
75
112
|
});
|
|
76
113
|
|
|
77
|
-
it("returns null for non-existent
|
|
78
|
-
const found = await collectionService.
|
|
114
|
+
it("returns null for non-existent slug", async () => {
|
|
115
|
+
const found = await collectionService.getBySlug("nonexistent");
|
|
79
116
|
expect(found).toBeNull();
|
|
80
117
|
});
|
|
81
118
|
});
|
|
@@ -87,18 +124,44 @@ describe("CollectionService", () => {
|
|
|
87
124
|
});
|
|
88
125
|
|
|
89
126
|
it("returns all collections", async () => {
|
|
90
|
-
await collectionService.create({ title: "First" });
|
|
91
|
-
await collectionService.create({ title: "Second" });
|
|
92
|
-
await collectionService.create({ title: "Third" });
|
|
127
|
+
await collectionService.create({ slug: "first", title: "First" });
|
|
128
|
+
await collectionService.create({ slug: "second", title: "Second" });
|
|
129
|
+
await collectionService.create({ slug: "third", title: "Third" });
|
|
93
130
|
|
|
94
131
|
const list = await collectionService.list();
|
|
95
132
|
expect(list).toHaveLength(3);
|
|
96
133
|
});
|
|
134
|
+
|
|
135
|
+
it("orders by position ASC, then createdAt DESC", async () => {
|
|
136
|
+
const a = await collectionService.create({
|
|
137
|
+
slug: "a",
|
|
138
|
+
title: "A",
|
|
139
|
+
position: 2,
|
|
140
|
+
});
|
|
141
|
+
const b = await collectionService.create({
|
|
142
|
+
slug: "b",
|
|
143
|
+
title: "B",
|
|
144
|
+
position: 0,
|
|
145
|
+
});
|
|
146
|
+
const c = await collectionService.create({
|
|
147
|
+
slug: "c",
|
|
148
|
+
title: "C",
|
|
149
|
+
position: 1,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const list = await collectionService.list();
|
|
153
|
+
expect(list[0]?.id).toBe(b.id);
|
|
154
|
+
expect(list[1]?.id).toBe(c.id);
|
|
155
|
+
expect(list[2]?.id).toBe(a.id);
|
|
156
|
+
});
|
|
97
157
|
});
|
|
98
158
|
|
|
99
159
|
describe("update", () => {
|
|
100
160
|
it("updates collection title", async () => {
|
|
101
|
-
const collection = await collectionService.create({
|
|
161
|
+
const collection = await collectionService.create({
|
|
162
|
+
slug: "test",
|
|
163
|
+
title: "Old",
|
|
164
|
+
});
|
|
102
165
|
|
|
103
166
|
const updated = await collectionService.update(collection.id, {
|
|
104
167
|
title: "New",
|
|
@@ -107,17 +170,80 @@ describe("CollectionService", () => {
|
|
|
107
170
|
expect(updated?.title).toBe("New");
|
|
108
171
|
});
|
|
109
172
|
|
|
110
|
-
it("updates collection
|
|
173
|
+
it("updates collection slug", async () => {
|
|
174
|
+
const collection = await collectionService.create({
|
|
175
|
+
slug: "old-slug",
|
|
176
|
+
title: "Test",
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const updated = await collectionService.update(collection.id, {
|
|
180
|
+
slug: "new-slug",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(updated?.slug).toBe("new-slug");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("updates description", async () => {
|
|
187
|
+
const collection = await collectionService.create({
|
|
188
|
+
slug: "test",
|
|
189
|
+
title: "Test",
|
|
190
|
+
description: "Old description",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const updated = await collectionService.update(collection.id, {
|
|
194
|
+
description: "New description",
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(updated?.description).toBe("New description");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("clears nullable fields with null", async () => {
|
|
201
|
+
const collection = await collectionService.create({
|
|
202
|
+
slug: "test",
|
|
203
|
+
title: "Test",
|
|
204
|
+
description: "Some desc",
|
|
205
|
+
icon: "star",
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const updated = await collectionService.update(collection.id, {
|
|
209
|
+
description: null,
|
|
210
|
+
icon: null,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(updated?.description).toBeNull();
|
|
214
|
+
expect(updated?.icon).toBeNull();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("updates icon, sortOrder, position, and showDivider", async () => {
|
|
218
|
+
const collection = await collectionService.create({
|
|
219
|
+
slug: "test",
|
|
220
|
+
title: "Test",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const updated = await collectionService.update(collection.id, {
|
|
224
|
+
icon: "rocket",
|
|
225
|
+
sortOrder: "rating_desc",
|
|
226
|
+
position: 10,
|
|
227
|
+
showDivider: true,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(updated?.icon).toBe("rocket");
|
|
231
|
+
expect(updated?.sortOrder).toBe("rating_desc");
|
|
232
|
+
expect(updated?.position).toBe(10);
|
|
233
|
+
expect(updated?.showDivider).toBe(1);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("updates updatedAt timestamp", async () => {
|
|
111
237
|
const collection = await collectionService.create({
|
|
238
|
+
slug: "test",
|
|
112
239
|
title: "Test",
|
|
113
|
-
path: "old-path",
|
|
114
240
|
});
|
|
115
241
|
|
|
116
242
|
const updated = await collectionService.update(collection.id, {
|
|
117
|
-
|
|
243
|
+
title: "Updated",
|
|
118
244
|
});
|
|
119
245
|
|
|
120
|
-
expect(updated?.
|
|
246
|
+
expect(updated?.updatedAt).toBeGreaterThanOrEqual(collection.updatedAt);
|
|
121
247
|
});
|
|
122
248
|
|
|
123
249
|
it("returns null for non-existent collection", async () => {
|
|
@@ -128,7 +254,10 @@ describe("CollectionService", () => {
|
|
|
128
254
|
|
|
129
255
|
describe("delete", () => {
|
|
130
256
|
it("deletes a collection", async () => {
|
|
131
|
-
const collection = await collectionService.create({
|
|
257
|
+
const collection = await collectionService.create({
|
|
258
|
+
slug: "test",
|
|
259
|
+
title: "Test",
|
|
260
|
+
});
|
|
132
261
|
|
|
133
262
|
const result = await collectionService.delete(collection.id);
|
|
134
263
|
expect(result).toBe(true);
|
|
@@ -137,18 +266,23 @@ describe("CollectionService", () => {
|
|
|
137
266
|
expect(found).toBeNull();
|
|
138
267
|
});
|
|
139
268
|
|
|
140
|
-
it("
|
|
141
|
-
const collection = await collectionService.create({
|
|
269
|
+
it("clears collectionId on related posts", async () => {
|
|
270
|
+
const collection = await collectionService.create({
|
|
271
|
+
slug: "test",
|
|
272
|
+
title: "Test",
|
|
273
|
+
});
|
|
142
274
|
const post = await postService.create({
|
|
143
|
-
|
|
144
|
-
|
|
275
|
+
format: "note",
|
|
276
|
+
body: "test post",
|
|
277
|
+
collectionId: collection.id,
|
|
145
278
|
});
|
|
146
279
|
|
|
147
|
-
await collectionService.addPost(collection.id, post.id);
|
|
148
280
|
await collectionService.delete(collection.id);
|
|
149
281
|
|
|
150
|
-
// Post itself should still exist
|
|
151
|
-
|
|
282
|
+
// Post itself should still exist but with null collectionId
|
|
283
|
+
const found = await postService.getById(post.id);
|
|
284
|
+
expect(found).not.toBeNull();
|
|
285
|
+
expect(found?.collectionId).toBeNull();
|
|
152
286
|
});
|
|
153
287
|
|
|
154
288
|
it("returns false for non-existent collection", async () => {
|
|
@@ -157,172 +291,137 @@ describe("CollectionService", () => {
|
|
|
157
291
|
});
|
|
158
292
|
});
|
|
159
293
|
|
|
160
|
-
describe("
|
|
161
|
-
it("
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
content: "test",
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
await collectionService.addPost(collection.id, post.id);
|
|
294
|
+
describe("reorder", () => {
|
|
295
|
+
it("updates positions based on array order", async () => {
|
|
296
|
+
const a = await collectionService.create({ slug: "a", title: "A" });
|
|
297
|
+
const b = await collectionService.create({ slug: "b", title: "B" });
|
|
298
|
+
const c = await collectionService.create({ slug: "c", title: "C" });
|
|
169
299
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
expect(posts[0]?.id).toBe(post.id);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("adding same post twice is idempotent", async () => {
|
|
176
|
-
const collection = await collectionService.create({ title: "Test" });
|
|
177
|
-
const post = await postService.create({
|
|
178
|
-
type: "note",
|
|
179
|
-
content: "test",
|
|
180
|
-
});
|
|
300
|
+
// Reverse the order: C, B, A
|
|
301
|
+
await collectionService.reorder([c.id, b.id, a.id]);
|
|
181
302
|
|
|
182
|
-
await collectionService.
|
|
183
|
-
await collectionService.
|
|
303
|
+
const reorderedC = await collectionService.getById(c.id);
|
|
304
|
+
const reorderedB = await collectionService.getById(b.id);
|
|
305
|
+
const reorderedA = await collectionService.getById(a.id);
|
|
184
306
|
|
|
185
|
-
|
|
186
|
-
expect(
|
|
307
|
+
expect(reorderedC?.position).toBe(0);
|
|
308
|
+
expect(reorderedB?.position).toBe(1);
|
|
309
|
+
expect(reorderedA?.position).toBe(2);
|
|
187
310
|
});
|
|
188
311
|
|
|
189
|
-
it("
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
type: "note",
|
|
193
|
-
content: "test",
|
|
194
|
-
});
|
|
312
|
+
it("updates updatedAt when reordering", async () => {
|
|
313
|
+
const a = await collectionService.create({ slug: "a", title: "A" });
|
|
314
|
+
const b = await collectionService.create({ slug: "b", title: "B" });
|
|
195
315
|
|
|
196
|
-
await collectionService.
|
|
197
|
-
await collectionService.removePost(collection.id, post.id);
|
|
316
|
+
await collectionService.reorder([b.id, a.id]);
|
|
198
317
|
|
|
199
|
-
const
|
|
200
|
-
expect(
|
|
318
|
+
const reorderedA = await collectionService.getById(a.id);
|
|
319
|
+
expect(reorderedA?.updatedAt).toBeGreaterThanOrEqual(a.updatedAt);
|
|
201
320
|
});
|
|
202
321
|
|
|
203
|
-
it("
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
});
|
|
322
|
+
it("handles empty array", async () => {
|
|
323
|
+
await collectionService.reorder([]);
|
|
324
|
+
// Should not throw
|
|
325
|
+
const list = await collectionService.list();
|
|
326
|
+
expect(list).toEqual([]);
|
|
327
|
+
});
|
|
210
328
|
|
|
211
|
-
|
|
212
|
-
await collectionService.
|
|
329
|
+
it("reflects new order in list()", async () => {
|
|
330
|
+
const a = await collectionService.create({ slug: "a", title: "A" });
|
|
331
|
+
const b = await collectionService.create({ slug: "b", title: "B" });
|
|
332
|
+
const c = await collectionService.create({ slug: "c", title: "C" });
|
|
213
333
|
|
|
214
|
-
|
|
215
|
-
post.id,
|
|
216
|
-
);
|
|
217
|
-
expect(collections).toHaveLength(2);
|
|
218
|
-
});
|
|
334
|
+
await collectionService.reorder([c.id, a.id, b.id]);
|
|
219
335
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
expect(
|
|
336
|
+
const list = await collectionService.list();
|
|
337
|
+
expect(list[0]?.id).toBe(c.id);
|
|
338
|
+
expect(list[1]?.id).toBe(a.id);
|
|
339
|
+
expect(list[2]?.id).toBe(b.id);
|
|
224
340
|
});
|
|
225
341
|
});
|
|
226
342
|
|
|
227
|
-
describe("
|
|
228
|
-
it("
|
|
229
|
-
|
|
230
|
-
const col2 = await collectionService.create({ title: "Col 2" });
|
|
231
|
-
const post = await postService.create({
|
|
232
|
-
type: "note",
|
|
233
|
-
content: "test",
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
await collectionService.syncPostCollections(post.id, [col1.id, col2.id]);
|
|
343
|
+
describe("getPostCounts", () => {
|
|
344
|
+
it("returns empty map when no posts exist", async () => {
|
|
345
|
+
await collectionService.create({ slug: "empty", title: "Empty" });
|
|
237
346
|
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
);
|
|
241
|
-
expect(collections).toHaveLength(2);
|
|
242
|
-
expect(collections.map((c) => c.id).sort()).toEqual(
|
|
243
|
-
[col1.id, col2.id].sort(),
|
|
244
|
-
);
|
|
347
|
+
const counts = await collectionService.getPostCounts();
|
|
348
|
+
expect(counts.size).toBe(0);
|
|
245
349
|
});
|
|
246
350
|
|
|
247
|
-
it("
|
|
248
|
-
const col1 = await collectionService.create({
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
351
|
+
it("returns correct counts for collections with posts", async () => {
|
|
352
|
+
const col1 = await collectionService.create({
|
|
353
|
+
slug: "col1",
|
|
354
|
+
title: "Col 1",
|
|
355
|
+
});
|
|
356
|
+
const col2 = await collectionService.create({
|
|
357
|
+
slug: "col2",
|
|
358
|
+
title: "Col 2",
|
|
253
359
|
});
|
|
254
360
|
|
|
255
|
-
await
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
361
|
+
await postService.create({
|
|
362
|
+
format: "note",
|
|
363
|
+
body: "post 1",
|
|
364
|
+
collectionId: col1.id,
|
|
365
|
+
});
|
|
366
|
+
await postService.create({
|
|
367
|
+
format: "note",
|
|
368
|
+
body: "post 2",
|
|
369
|
+
collectionId: col1.id,
|
|
370
|
+
});
|
|
371
|
+
await postService.create({
|
|
372
|
+
format: "note",
|
|
373
|
+
body: "post 3",
|
|
374
|
+
collectionId: col2.id,
|
|
375
|
+
});
|
|
260
376
|
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
);
|
|
264
|
-
expect(collections).toHaveLength(1);
|
|
265
|
-
expect(collections[0]?.id).toBe(col1.id);
|
|
377
|
+
const counts = await collectionService.getPostCounts();
|
|
378
|
+
expect(counts.get(col1.id)).toBe(2);
|
|
379
|
+
expect(counts.get(col2.id)).toBe(1);
|
|
266
380
|
});
|
|
267
381
|
|
|
268
|
-
it("
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const post = await postService.create({
|
|
273
|
-
type: "note",
|
|
274
|
-
content: "test",
|
|
382
|
+
it("does not count posts without a collection", async () => {
|
|
383
|
+
const col = await collectionService.create({
|
|
384
|
+
slug: "col",
|
|
385
|
+
title: "Col",
|
|
275
386
|
});
|
|
276
387
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
await
|
|
388
|
+
await postService.create({
|
|
389
|
+
format: "note",
|
|
390
|
+
body: "with collection",
|
|
391
|
+
collectionId: col.id,
|
|
392
|
+
});
|
|
393
|
+
await postService.create({
|
|
394
|
+
format: "note",
|
|
395
|
+
body: "no collection",
|
|
396
|
+
});
|
|
283
397
|
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
);
|
|
287
|
-
expect(collections).toHaveLength(2);
|
|
288
|
-
expect(collections.map((c) => c.id).sort()).toEqual(
|
|
289
|
-
[col2.id, col3.id].sort(),
|
|
290
|
-
);
|
|
398
|
+
const counts = await collectionService.getPostCounts();
|
|
399
|
+
expect(counts.get(col.id)).toBe(1);
|
|
400
|
+
expect(counts.size).toBe(1);
|
|
291
401
|
});
|
|
292
402
|
|
|
293
|
-
it("
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
content: "test",
|
|
403
|
+
it("does not count soft-deleted posts", async () => {
|
|
404
|
+
const col = await collectionService.create({
|
|
405
|
+
slug: "col",
|
|
406
|
+
title: "Col",
|
|
298
407
|
});
|
|
299
408
|
|
|
300
|
-
await collectionService.addPost(col1.id, post.id);
|
|
301
|
-
|
|
302
|
-
await collectionService.syncPostCollections(post.id, []);
|
|
303
|
-
|
|
304
|
-
const collections = await collectionService.getCollectionsForPost(
|
|
305
|
-
post.id,
|
|
306
|
-
);
|
|
307
|
-
expect(collections).toHaveLength(0);
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it("is a no-op when already in sync", async () => {
|
|
311
|
-
const col1 = await collectionService.create({ title: "Col 1" });
|
|
312
409
|
const post = await postService.create({
|
|
313
|
-
|
|
314
|
-
|
|
410
|
+
format: "note",
|
|
411
|
+
body: "will be deleted",
|
|
412
|
+
collectionId: col.id,
|
|
413
|
+
});
|
|
414
|
+
await postService.create({
|
|
415
|
+
format: "note",
|
|
416
|
+
body: "still alive",
|
|
417
|
+
collectionId: col.id,
|
|
315
418
|
});
|
|
316
419
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
await collectionService.syncPostCollections(post.id, [col1.id]);
|
|
420
|
+
// Soft-delete one post
|
|
421
|
+
await postService.delete(post.id);
|
|
320
422
|
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
);
|
|
324
|
-
expect(collections).toHaveLength(1);
|
|
325
|
-
expect(collections[0]?.id).toBe(col1.id);
|
|
423
|
+
const counts = await collectionService.getPostCounts();
|
|
424
|
+
expect(counts.get(col.id)).toBe(1);
|
|
326
425
|
});
|
|
327
426
|
});
|
|
328
427
|
});
|