@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
|
@@ -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
|
});
|
|
@@ -166,8 +166,8 @@ describe("MediaService", () => {
|
|
|
166
166
|
describe("getByPostId", () => {
|
|
167
167
|
it("returns media ordered by position", async () => {
|
|
168
168
|
const post = await postService.create({
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
format: "note",
|
|
170
|
+
body: "test",
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
const m1 = await mediaService.create({
|
|
@@ -191,8 +191,8 @@ describe("MediaService", () => {
|
|
|
191
191
|
|
|
192
192
|
it("returns empty array for post with no media", async () => {
|
|
193
193
|
const post = await postService.create({
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
format: "note",
|
|
195
|
+
body: "test",
|
|
196
196
|
});
|
|
197
197
|
|
|
198
198
|
const results = await mediaService.getByPostId(post.id);
|
|
@@ -203,12 +203,12 @@ describe("MediaService", () => {
|
|
|
203
203
|
describe("getByPostIds", () => {
|
|
204
204
|
it("returns Map grouped by postId", async () => {
|
|
205
205
|
const post1 = await postService.create({
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
format: "note",
|
|
207
|
+
body: "post 1",
|
|
208
208
|
});
|
|
209
209
|
const post2 = await postService.create({
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
format: "note",
|
|
211
|
+
body: "post 2",
|
|
212
212
|
});
|
|
213
213
|
|
|
214
214
|
const m1 = await mediaService.create({
|
|
@@ -240,8 +240,8 @@ describe("MediaService", () => {
|
|
|
240
240
|
|
|
241
241
|
it("returns ordered by position within each post", async () => {
|
|
242
242
|
const post = await postService.create({
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
format: "note",
|
|
244
|
+
body: "test",
|
|
245
245
|
});
|
|
246
246
|
|
|
247
247
|
const m1 = await mediaService.create({
|
|
@@ -309,8 +309,8 @@ describe("MediaService", () => {
|
|
|
309
309
|
describe("attachToPost", () => {
|
|
310
310
|
it("sets postId and position for each media", async () => {
|
|
311
311
|
const post = await postService.create({
|
|
312
|
-
|
|
313
|
-
|
|
312
|
+
format: "note",
|
|
313
|
+
body: "test",
|
|
314
314
|
});
|
|
315
315
|
|
|
316
316
|
const m1 = await mediaService.create({
|
|
@@ -334,8 +334,8 @@ describe("MediaService", () => {
|
|
|
334
334
|
|
|
335
335
|
it("replaces existing attachments", async () => {
|
|
336
336
|
const post = await postService.create({
|
|
337
|
-
|
|
338
|
-
|
|
337
|
+
format: "note",
|
|
338
|
+
body: "test",
|
|
339
339
|
});
|
|
340
340
|
|
|
341
341
|
const m1 = await mediaService.create({
|
|
@@ -367,8 +367,8 @@ describe("MediaService", () => {
|
|
|
367
367
|
|
|
368
368
|
it("handles empty array by clearing all attachments", async () => {
|
|
369
369
|
const post = await postService.create({
|
|
370
|
-
|
|
371
|
-
|
|
370
|
+
format: "note",
|
|
371
|
+
body: "test",
|
|
372
372
|
});
|
|
373
373
|
|
|
374
374
|
const m1 = await mediaService.create({
|
|
@@ -387,8 +387,8 @@ describe("MediaService", () => {
|
|
|
387
387
|
describe("detachFromPost", () => {
|
|
388
388
|
it("clears postId and resets position", async () => {
|
|
389
389
|
const post = await postService.create({
|
|
390
|
-
|
|
391
|
-
|
|
390
|
+
format: "note",
|
|
391
|
+
body: "test",
|
|
392
392
|
});
|
|
393
393
|
|
|
394
394
|
const m1 = await mediaService.create({
|