@jant/core 0.3.7 → 0.3.9
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 +11 -4
- package/dist/client.js +1 -0
- package/dist/db/schema.js +15 -1
- 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/lib/image.js +39 -15
- package/dist/lib/media-helpers.js +49 -0
- package/dist/lib/nav-reorder.js +27 -0
- package/dist/lib/navigation.js +35 -0
- package/dist/lib/storage.js +164 -0
- package/dist/lib/theme-components.js +49 -0
- package/dist/routes/api/posts.js +12 -7
- package/dist/routes/api/timeline.js +116 -0
- package/dist/routes/api/upload.js +35 -24
- package/dist/routes/dash/media.js +24 -14
- package/dist/routes/dash/navigation.js +274 -0
- package/dist/routes/dash/posts.js +4 -1
- package/dist/routes/feed/rss.js +3 -2
- package/dist/routes/pages/archive.js +14 -27
- package/dist/routes/pages/collection.js +10 -19
- package/dist/routes/pages/home.js +84 -126
- package/dist/routes/pages/page.js +19 -38
- package/dist/routes/pages/post.js +47 -56
- package/dist/routes/pages/search.js +13 -26
- package/dist/services/index.js +3 -1
- package/dist/services/media.js +8 -6
- package/dist/services/navigation.js +115 -0
- package/dist/services/post.js +26 -1
- package/dist/theme/components/PostForm.js +4 -3
- package/dist/theme/components/PostList.js +5 -0
- package/dist/theme/components/index.js +2 -0
- package/dist/theme/components/timeline/ArticleCard.js +50 -0
- package/dist/theme/components/timeline/ImageCard.js +86 -0
- package/dist/theme/components/timeline/LinkCard.js +62 -0
- package/dist/theme/components/timeline/NoteCard.js +37 -0
- package/dist/theme/components/timeline/QuoteCard.js +51 -0
- package/dist/theme/components/timeline/ThreadPreview.js +52 -0
- package/dist/theme/components/timeline/TimelineFeed.js +43 -0
- package/dist/theme/components/timeline/TimelineItem.js +25 -0
- package/dist/theme/components/timeline/index.js +8 -0
- package/dist/theme/layouts/DashLayout.js +8 -0
- package/dist/theme/layouts/SiteLayout.js +160 -0
- package/dist/theme/layouts/index.js +1 -0
- package/dist/types/sortablejs.d.js +5 -0
- package/dist/types.js +32 -0
- package/package.json +4 -2
- package/src/__tests__/helpers/app.ts +1 -0
- package/src/__tests__/helpers/db.ts +20 -0
- package/src/app.tsx +12 -7
- package/src/client.ts +1 -0
- package/src/db/migrations/0003_add_navigation_links.sql +8 -0
- package/src/db/migrations/0004_add_storage_provider.sql +3 -0
- package/src/db/migrations/meta/0003_snapshot.json +821 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/schema.ts +15 -1
- package/src/i18n/locales/en.po +148 -80
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +150 -103
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +150 -103
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +5 -0
- package/src/lib/__tests__/image.test.ts +96 -0
- package/src/lib/__tests__/storage.test.ts +162 -0
- package/src/lib/__tests__/theme-components.test.ts +107 -0
- package/src/lib/image.ts +46 -16
- package/src/lib/media-helpers.ts +65 -0
- package/src/lib/nav-reorder.ts +26 -0
- package/src/lib/navigation.ts +46 -0
- package/src/lib/storage.ts +236 -0
- package/src/lib/theme-components.ts +76 -0
- package/src/routes/api/__tests__/posts.test.ts +8 -8
- package/src/routes/api/__tests__/timeline.test.ts +242 -0
- package/src/routes/api/posts.ts +20 -6
- package/src/routes/api/timeline.tsx +152 -0
- package/src/routes/api/upload.ts +52 -25
- package/src/routes/dash/media.tsx +40 -8
- package/src/routes/dash/navigation.tsx +306 -0
- package/src/routes/dash/posts.tsx +5 -0
- package/src/routes/feed/rss.ts +3 -2
- package/src/routes/pages/archive.tsx +15 -23
- package/src/routes/pages/collection.tsx +8 -15
- package/src/routes/pages/home.tsx +118 -122
- package/src/routes/pages/page.tsx +17 -30
- package/src/routes/pages/post.tsx +63 -60
- package/src/routes/pages/search.tsx +18 -22
- package/src/services/__tests__/media.test.ts +73 -28
- package/src/services/__tests__/navigation.test.ts +213 -0
- package/src/services/__tests__/post-timeline.test.ts +220 -0
- package/src/services/index.ts +7 -0
- package/src/services/media.ts +12 -8
- package/src/services/navigation.ts +165 -0
- package/src/services/post.ts +48 -1
- package/src/styles/components.css +59 -0
- package/src/theme/components/PostForm.tsx +13 -2
- package/src/theme/components/PostList.tsx +7 -0
- package/src/theme/components/index.ts +12 -0
- package/src/theme/components/timeline/ArticleCard.tsx +57 -0
- package/src/theme/components/timeline/ImageCard.tsx +80 -0
- package/src/theme/components/timeline/LinkCard.tsx +66 -0
- package/src/theme/components/timeline/NoteCard.tsx +41 -0
- package/src/theme/components/timeline/QuoteCard.tsx +55 -0
- package/src/theme/components/timeline/ThreadPreview.tsx +49 -0
- package/src/theme/components/timeline/TimelineFeed.tsx +52 -0
- package/src/theme/components/timeline/TimelineItem.tsx +39 -0
- package/src/theme/components/timeline/index.ts +8 -0
- package/src/theme/layouts/DashLayout.tsx +10 -0
- package/src/theme/layouts/SiteLayout.tsx +184 -0
- package/src/theme/layouts/index.ts +1 -0
- package/src/types/sortablejs.d.ts +23 -0
- package/src/types.ts +102 -1
- package/dist/app.d.ts +0 -38
- package/dist/app.d.ts.map +0 -1
- package/dist/auth.d.ts +0 -25
- package/dist/auth.d.ts.map +0 -1
- package/dist/db/index.d.ts +0 -10
- package/dist/db/index.d.ts.map +0 -1
- package/dist/db/schema.d.ts +0 -1543
- package/dist/db/schema.d.ts.map +0 -1
- package/dist/i18n/Trans.d.ts +0 -25
- package/dist/i18n/Trans.d.ts.map +0 -1
- package/dist/i18n/context.d.ts +0 -69
- package/dist/i18n/context.d.ts.map +0 -1
- package/dist/i18n/detect.d.ts +0 -20
- package/dist/i18n/detect.d.ts.map +0 -1
- package/dist/i18n/i18n.d.ts +0 -32
- package/dist/i18n/i18n.d.ts.map +0 -1
- package/dist/i18n/index.d.ts +0 -41
- package/dist/i18n/index.d.ts.map +0 -1
- package/dist/i18n/locales/en.d.ts +0 -3
- package/dist/i18n/locales/en.d.ts.map +0 -1
- package/dist/i18n/locales/zh-Hans.d.ts +0 -3
- package/dist/i18n/locales/zh-Hans.d.ts.map +0 -1
- package/dist/i18n/locales/zh-Hant.d.ts +0 -3
- package/dist/i18n/locales/zh-Hant.d.ts.map +0 -1
- package/dist/i18n/locales.d.ts +0 -11
- package/dist/i18n/locales.d.ts.map +0 -1
- package/dist/i18n/middleware.d.ts +0 -21
- package/dist/i18n/middleware.d.ts.map +0 -1
- package/dist/index.d.ts +0 -16
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/config.d.ts +0 -83
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/constants.d.ts +0 -37
- package/dist/lib/constants.d.ts.map +0 -1
- package/dist/lib/image.d.ts +0 -73
- package/dist/lib/image.d.ts.map +0 -1
- package/dist/lib/index.d.ts +0 -9
- package/dist/lib/index.d.ts.map +0 -1
- package/dist/lib/markdown.d.ts +0 -60
- package/dist/lib/markdown.d.ts.map +0 -1
- package/dist/lib/schemas.d.ts +0 -130
- package/dist/lib/schemas.d.ts.map +0 -1
- package/dist/lib/sqid.d.ts +0 -60
- package/dist/lib/sqid.d.ts.map +0 -1
- package/dist/lib/sse.d.ts +0 -192
- package/dist/lib/sse.d.ts.map +0 -1
- package/dist/lib/theme.d.ts +0 -44
- package/dist/lib/theme.d.ts.map +0 -1
- package/dist/lib/time.d.ts +0 -90
- package/dist/lib/time.d.ts.map +0 -1
- package/dist/lib/url.d.ts +0 -82
- package/dist/lib/url.d.ts.map +0 -1
- package/dist/middleware/auth.d.ts +0 -24
- package/dist/middleware/auth.d.ts.map +0 -1
- package/dist/middleware/onboarding.d.ts +0 -26
- package/dist/middleware/onboarding.d.ts.map +0 -1
- package/dist/routes/api/posts.d.ts +0 -13
- package/dist/routes/api/posts.d.ts.map +0 -1
- package/dist/routes/api/search.d.ts +0 -13
- package/dist/routes/api/search.d.ts.map +0 -1
- package/dist/routes/api/upload.d.ts +0 -16
- package/dist/routes/api/upload.d.ts.map +0 -1
- package/dist/routes/dash/collections.d.ts +0 -13
- package/dist/routes/dash/collections.d.ts.map +0 -1
- package/dist/routes/dash/index.d.ts +0 -15
- package/dist/routes/dash/index.d.ts.map +0 -1
- package/dist/routes/dash/media.d.ts +0 -16
- package/dist/routes/dash/media.d.ts.map +0 -1
- package/dist/routes/dash/pages.d.ts +0 -15
- package/dist/routes/dash/pages.d.ts.map +0 -1
- package/dist/routes/dash/posts.d.ts +0 -13
- package/dist/routes/dash/posts.d.ts.map +0 -1
- package/dist/routes/dash/redirects.d.ts +0 -13
- package/dist/routes/dash/redirects.d.ts.map +0 -1
- package/dist/routes/dash/settings.d.ts +0 -15
- package/dist/routes/dash/settings.d.ts.map +0 -1
- package/dist/routes/feed/rss.d.ts +0 -13
- package/dist/routes/feed/rss.d.ts.map +0 -1
- package/dist/routes/feed/sitemap.d.ts +0 -13
- package/dist/routes/feed/sitemap.d.ts.map +0 -1
- package/dist/routes/pages/archive.d.ts +0 -15
- package/dist/routes/pages/archive.d.ts.map +0 -1
- package/dist/routes/pages/collection.d.ts +0 -13
- package/dist/routes/pages/collection.d.ts.map +0 -1
- package/dist/routes/pages/home.d.ts +0 -13
- package/dist/routes/pages/home.d.ts.map +0 -1
- package/dist/routes/pages/page.d.ts +0 -15
- package/dist/routes/pages/page.d.ts.map +0 -1
- package/dist/routes/pages/post.d.ts +0 -13
- package/dist/routes/pages/post.d.ts.map +0 -1
- package/dist/routes/pages/search.d.ts +0 -13
- package/dist/routes/pages/search.d.ts.map +0 -1
- package/dist/services/collection.d.ts +0 -32
- package/dist/services/collection.d.ts.map +0 -1
- package/dist/services/index.d.ts +0 -28
- package/dist/services/index.d.ts.map +0 -1
- package/dist/services/media.d.ts +0 -34
- package/dist/services/media.d.ts.map +0 -1
- package/dist/services/post.d.ts +0 -31
- package/dist/services/post.d.ts.map +0 -1
- package/dist/services/redirect.d.ts +0 -15
- package/dist/services/redirect.d.ts.map +0 -1
- package/dist/services/search.d.ts +0 -26
- package/dist/services/search.d.ts.map +0 -1
- package/dist/services/settings.d.ts +0 -18
- package/dist/services/settings.d.ts.map +0 -1
- package/dist/theme/color-themes.d.ts +0 -30
- package/dist/theme/color-themes.d.ts.map +0 -1
- package/dist/theme/components/ActionButtons.d.ts +0 -43
- package/dist/theme/components/ActionButtons.d.ts.map +0 -1
- package/dist/theme/components/CrudPageHeader.d.ts +0 -23
- package/dist/theme/components/CrudPageHeader.d.ts.map +0 -1
- package/dist/theme/components/DangerZone.d.ts +0 -36
- package/dist/theme/components/DangerZone.d.ts.map +0 -1
- package/dist/theme/components/EmptyState.d.ts +0 -27
- package/dist/theme/components/EmptyState.d.ts.map +0 -1
- package/dist/theme/components/ListItemRow.d.ts +0 -15
- package/dist/theme/components/ListItemRow.d.ts.map +0 -1
- package/dist/theme/components/MediaGallery.d.ts +0 -13
- package/dist/theme/components/MediaGallery.d.ts.map +0 -1
- package/dist/theme/components/PageForm.d.ts +0 -14
- package/dist/theme/components/PageForm.d.ts.map +0 -1
- package/dist/theme/components/Pagination.d.ts +0 -46
- package/dist/theme/components/Pagination.d.ts.map +0 -1
- package/dist/theme/components/PostForm.d.ts +0 -16
- package/dist/theme/components/PostForm.d.ts.map +0 -1
- package/dist/theme/components/PostList.d.ts +0 -10
- package/dist/theme/components/PostList.d.ts.map +0 -1
- package/dist/theme/components/ThreadView.d.ts +0 -15
- package/dist/theme/components/ThreadView.d.ts.map +0 -1
- package/dist/theme/components/TypeBadge.d.ts +0 -12
- package/dist/theme/components/TypeBadge.d.ts.map +0 -1
- package/dist/theme/components/VisibilityBadge.d.ts +0 -12
- package/dist/theme/components/VisibilityBadge.d.ts.map +0 -1
- package/dist/theme/components/index.d.ts +0 -14
- package/dist/theme/components/index.d.ts.map +0 -1
- package/dist/theme/index.d.ts +0 -21
- package/dist/theme/index.d.ts.map +0 -1
- package/dist/theme/layouts/BaseLayout.d.ts +0 -23
- package/dist/theme/layouts/BaseLayout.d.ts.map +0 -1
- package/dist/theme/layouts/DashLayout.d.ts +0 -17
- package/dist/theme/layouts/DashLayout.d.ts.map +0 -1
- package/dist/theme/layouts/index.d.ts +0 -3
- package/dist/theme/layouts/index.d.ts.map +0 -1
- package/dist/types.d.ts +0 -237
- package/dist/types.d.ts.map +0 -1
|
@@ -18,11 +18,11 @@ describe("MediaService", () => {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
const sampleMedia = {
|
|
21
|
-
filename: "
|
|
21
|
+
filename: "0192a9f1-a2b7-7c3d-8e4f-5a6b7c8d9e0f.jpg",
|
|
22
22
|
originalName: "photo.jpg",
|
|
23
23
|
mimeType: "image/jpeg",
|
|
24
24
|
size: 102400,
|
|
25
|
-
|
|
25
|
+
storageKey: "media/2025/01/0192a9f1-a2b7-7c3d-8e4f-5a6b7c8d9e0f.jpg",
|
|
26
26
|
width: 1920,
|
|
27
27
|
height: 1080,
|
|
28
28
|
};
|
|
@@ -32,11 +32,14 @@ describe("MediaService", () => {
|
|
|
32
32
|
const media = await mediaService.create(sampleMedia);
|
|
33
33
|
|
|
34
34
|
expect(media.id).toBeTruthy(); // UUIDv7
|
|
35
|
-
expect(media.filename).toBe("
|
|
35
|
+
expect(media.filename).toBe("0192a9f1-a2b7-7c3d-8e4f-5a6b7c8d9e0f.jpg");
|
|
36
36
|
expect(media.originalName).toBe("photo.jpg");
|
|
37
37
|
expect(media.mimeType).toBe("image/jpeg");
|
|
38
38
|
expect(media.size).toBe(102400);
|
|
39
|
-
expect(media.
|
|
39
|
+
expect(media.storageKey).toBe(
|
|
40
|
+
"media/2025/01/0192a9f1-a2b7-7c3d-8e4f-5a6b7c8d9e0f.jpg",
|
|
41
|
+
);
|
|
42
|
+
expect(media.provider).toBe("r2");
|
|
40
43
|
expect(media.width).toBe(1920);
|
|
41
44
|
expect(media.height).toBe(1080);
|
|
42
45
|
expect(media.postId).toBeNull();
|
|
@@ -54,6 +57,20 @@ describe("MediaService", () => {
|
|
|
54
57
|
expect(media.alt).toBe("A beautiful sunset");
|
|
55
58
|
});
|
|
56
59
|
|
|
60
|
+
it("defaults provider to 'r2'", async () => {
|
|
61
|
+
const media = await mediaService.create(sampleMedia);
|
|
62
|
+
expect(media.provider).toBe("r2");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("accepts provider 's3'", async () => {
|
|
66
|
+
const media = await mediaService.create({
|
|
67
|
+
...sampleMedia,
|
|
68
|
+
storageKey: "media/2025/01/s3-upload.jpg",
|
|
69
|
+
provider: "s3",
|
|
70
|
+
});
|
|
71
|
+
expect(media.provider).toBe("s3");
|
|
72
|
+
});
|
|
73
|
+
|
|
57
74
|
it("creates media with position and blurhash", async () => {
|
|
58
75
|
const media = await mediaService.create({
|
|
59
76
|
...sampleMedia,
|
|
@@ -69,13 +86,36 @@ describe("MediaService", () => {
|
|
|
69
86
|
const media1 = await mediaService.create(sampleMedia);
|
|
70
87
|
const media2 = await mediaService.create({
|
|
71
88
|
...sampleMedia,
|
|
72
|
-
|
|
89
|
+
storageKey: "media/2025/01/other.jpg",
|
|
73
90
|
});
|
|
74
91
|
|
|
75
92
|
expect(media1.id).not.toBe(media2.id);
|
|
76
93
|
// UUIDv7 should be sortable — later ID is lexicographically greater
|
|
77
94
|
expect(media2.id > media1.id).toBe(true);
|
|
78
95
|
});
|
|
96
|
+
|
|
97
|
+
it("uses provided id when given", async () => {
|
|
98
|
+
const customId = "0192a9f1-a2b7-7c3d-8e4f-custom000001";
|
|
99
|
+
const media = await mediaService.create({
|
|
100
|
+
...sampleMedia,
|
|
101
|
+
id: customId,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(media.id).toBe(customId);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("auto-generates id when not provided", async () => {
|
|
108
|
+
const media = await mediaService.create({
|
|
109
|
+
...sampleMedia,
|
|
110
|
+
storageKey: "media/2025/01/auto.jpg",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(media.id).toBeTruthy();
|
|
114
|
+
// UUIDv7 format: 8-4-4-4-12 hex chars
|
|
115
|
+
expect(media.id).toMatch(
|
|
116
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
|
117
|
+
);
|
|
118
|
+
});
|
|
79
119
|
});
|
|
80
120
|
|
|
81
121
|
describe("getById", () => {
|
|
@@ -84,7 +124,7 @@ describe("MediaService", () => {
|
|
|
84
124
|
|
|
85
125
|
const found = await mediaService.getById(created.id);
|
|
86
126
|
expect(found).not.toBeNull();
|
|
87
|
-
expect(found?.filename).toBe("
|
|
127
|
+
expect(found?.filename).toBe("0192a9f1-a2b7-7c3d-8e4f-5a6b7c8d9e0f.jpg");
|
|
88
128
|
});
|
|
89
129
|
|
|
90
130
|
it("returns null for non-existent ID", async () => {
|
|
@@ -97,11 +137,11 @@ describe("MediaService", () => {
|
|
|
97
137
|
it("returns media for valid IDs", async () => {
|
|
98
138
|
const m1 = await mediaService.create({
|
|
99
139
|
...sampleMedia,
|
|
100
|
-
|
|
140
|
+
storageKey: "media/a.jpg",
|
|
101
141
|
});
|
|
102
142
|
const m2 = await mediaService.create({
|
|
103
143
|
...sampleMedia,
|
|
104
|
-
|
|
144
|
+
storageKey: "media/b.jpg",
|
|
105
145
|
});
|
|
106
146
|
|
|
107
147
|
const results = await mediaService.getByIds([m1.id, m2.id]);
|
|
@@ -132,11 +172,11 @@ describe("MediaService", () => {
|
|
|
132
172
|
|
|
133
173
|
const m1 = await mediaService.create({
|
|
134
174
|
...sampleMedia,
|
|
135
|
-
|
|
175
|
+
storageKey: "media/a.jpg",
|
|
136
176
|
});
|
|
137
177
|
const m2 = await mediaService.create({
|
|
138
178
|
...sampleMedia,
|
|
139
|
-
|
|
179
|
+
storageKey: "media/b.jpg",
|
|
140
180
|
});
|
|
141
181
|
|
|
142
182
|
await mediaService.attachToPost(post.id, [m2.id, m1.id]);
|
|
@@ -173,15 +213,15 @@ describe("MediaService", () => {
|
|
|
173
213
|
|
|
174
214
|
const m1 = await mediaService.create({
|
|
175
215
|
...sampleMedia,
|
|
176
|
-
|
|
216
|
+
storageKey: "media/a.jpg",
|
|
177
217
|
});
|
|
178
218
|
const m2 = await mediaService.create({
|
|
179
219
|
...sampleMedia,
|
|
180
|
-
|
|
220
|
+
storageKey: "media/b.jpg",
|
|
181
221
|
});
|
|
182
222
|
const m3 = await mediaService.create({
|
|
183
223
|
...sampleMedia,
|
|
184
|
-
|
|
224
|
+
storageKey: "media/c.jpg",
|
|
185
225
|
});
|
|
186
226
|
|
|
187
227
|
await mediaService.attachToPost(post1.id, [m1.id, m2.id]);
|
|
@@ -206,11 +246,11 @@ describe("MediaService", () => {
|
|
|
206
246
|
|
|
207
247
|
const m1 = await mediaService.create({
|
|
208
248
|
...sampleMedia,
|
|
209
|
-
|
|
249
|
+
storageKey: "media/a.jpg",
|
|
210
250
|
});
|
|
211
251
|
const m2 = await mediaService.create({
|
|
212
252
|
...sampleMedia,
|
|
213
|
-
|
|
253
|
+
storageKey: "media/b.jpg",
|
|
214
254
|
});
|
|
215
255
|
|
|
216
256
|
await mediaService.attachToPost(post.id, [m2.id, m1.id]);
|
|
@@ -222,17 +262,19 @@ describe("MediaService", () => {
|
|
|
222
262
|
});
|
|
223
263
|
});
|
|
224
264
|
|
|
225
|
-
describe("
|
|
265
|
+
describe("getByStorageKey", () => {
|
|
226
266
|
it("returns media by R2 key", async () => {
|
|
227
267
|
await mediaService.create(sampleMedia);
|
|
228
268
|
|
|
229
|
-
const found = await mediaService.
|
|
269
|
+
const found = await mediaService.getByStorageKey(
|
|
270
|
+
"media/2025/01/0192a9f1-a2b7-7c3d-8e4f-5a6b7c8d9e0f.jpg",
|
|
271
|
+
);
|
|
230
272
|
expect(found).not.toBeNull();
|
|
231
273
|
expect(found?.originalName).toBe("photo.jpg");
|
|
232
274
|
});
|
|
233
275
|
|
|
234
276
|
it("returns null for non-existent R2 key", async () => {
|
|
235
|
-
const found = await mediaService.
|
|
277
|
+
const found = await mediaService.getByStorageKey("nonexistent");
|
|
236
278
|
expect(found).toBeNull();
|
|
237
279
|
});
|
|
238
280
|
});
|
|
@@ -244,8 +286,8 @@ describe("MediaService", () => {
|
|
|
244
286
|
});
|
|
245
287
|
|
|
246
288
|
it("returns media ordered by createdAt desc", async () => {
|
|
247
|
-
await mediaService.create({ ...sampleMedia,
|
|
248
|
-
await mediaService.create({ ...sampleMedia,
|
|
289
|
+
await mediaService.create({ ...sampleMedia, storageKey: "a.jpg" });
|
|
290
|
+
await mediaService.create({ ...sampleMedia, storageKey: "b.jpg" });
|
|
249
291
|
|
|
250
292
|
const list = await mediaService.list();
|
|
251
293
|
expect(list).toHaveLength(2);
|
|
@@ -253,7 +295,10 @@ describe("MediaService", () => {
|
|
|
253
295
|
|
|
254
296
|
it("respects limit parameter", async () => {
|
|
255
297
|
for (let i = 0; i < 5; i++) {
|
|
256
|
-
await mediaService.create({
|
|
298
|
+
await mediaService.create({
|
|
299
|
+
...sampleMedia,
|
|
300
|
+
storageKey: `img${i}.jpg`,
|
|
301
|
+
});
|
|
257
302
|
}
|
|
258
303
|
|
|
259
304
|
const list = await mediaService.list(2);
|
|
@@ -270,11 +315,11 @@ describe("MediaService", () => {
|
|
|
270
315
|
|
|
271
316
|
const m1 = await mediaService.create({
|
|
272
317
|
...sampleMedia,
|
|
273
|
-
|
|
318
|
+
storageKey: "media/a.jpg",
|
|
274
319
|
});
|
|
275
320
|
const m2 = await mediaService.create({
|
|
276
321
|
...sampleMedia,
|
|
277
|
-
|
|
322
|
+
storageKey: "media/b.jpg",
|
|
278
323
|
});
|
|
279
324
|
|
|
280
325
|
await mediaService.attachToPost(post.id, [m1.id, m2.id]);
|
|
@@ -295,15 +340,15 @@ describe("MediaService", () => {
|
|
|
295
340
|
|
|
296
341
|
const m1 = await mediaService.create({
|
|
297
342
|
...sampleMedia,
|
|
298
|
-
|
|
343
|
+
storageKey: "media/a.jpg",
|
|
299
344
|
});
|
|
300
345
|
const m2 = await mediaService.create({
|
|
301
346
|
...sampleMedia,
|
|
302
|
-
|
|
347
|
+
storageKey: "media/b.jpg",
|
|
303
348
|
});
|
|
304
349
|
const m3 = await mediaService.create({
|
|
305
350
|
...sampleMedia,
|
|
306
|
-
|
|
351
|
+
storageKey: "media/c.jpg",
|
|
307
352
|
});
|
|
308
353
|
|
|
309
354
|
await mediaService.attachToPost(post.id, [m1.id, m2.id]);
|
|
@@ -328,7 +373,7 @@ describe("MediaService", () => {
|
|
|
328
373
|
|
|
329
374
|
const m1 = await mediaService.create({
|
|
330
375
|
...sampleMedia,
|
|
331
|
-
|
|
376
|
+
storageKey: "media/a.jpg",
|
|
332
377
|
});
|
|
333
378
|
|
|
334
379
|
await mediaService.attachToPost(post.id, [m1.id]);
|
|
@@ -348,7 +393,7 @@ describe("MediaService", () => {
|
|
|
348
393
|
|
|
349
394
|
const m1 = await mediaService.create({
|
|
350
395
|
...sampleMedia,
|
|
351
|
-
|
|
396
|
+
storageKey: "media/a.jpg",
|
|
352
397
|
});
|
|
353
398
|
|
|
354
399
|
await mediaService.attachToPost(post.id, [m1.id]);
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
3
|
+
import { createNavigationLinkService } from "../navigation.js";
|
|
4
|
+
import type { Database } from "../../db/index.js";
|
|
5
|
+
|
|
6
|
+
describe("NavigationLinkService", () => {
|
|
7
|
+
let db: Database;
|
|
8
|
+
let navigationService: ReturnType<typeof createNavigationLinkService>;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
const testDb = createTestDatabase();
|
|
12
|
+
db = testDb.db as unknown as Database;
|
|
13
|
+
navigationService = createNavigationLinkService(db);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("create", () => {
|
|
17
|
+
it("creates a navigation link with auto-assigned position", async () => {
|
|
18
|
+
const link = await navigationService.create({
|
|
19
|
+
label: "Home",
|
|
20
|
+
url: "/",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(link.label).toBe("Home");
|
|
24
|
+
expect(link.url).toBe("/");
|
|
25
|
+
expect(link.position).toBe(0);
|
|
26
|
+
expect(link.id).toBe(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("auto-increments position for subsequent links", async () => {
|
|
30
|
+
await navigationService.create({ label: "Home", url: "/" });
|
|
31
|
+
const second = await navigationService.create({
|
|
32
|
+
label: "Archive",
|
|
33
|
+
url: "/archive",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(second.position).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("uses provided position when specified", async () => {
|
|
40
|
+
const link = await navigationService.create({
|
|
41
|
+
label: "Home",
|
|
42
|
+
url: "/",
|
|
43
|
+
position: 5,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(link.position).toBe(5);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("getById", () => {
|
|
51
|
+
it("returns a link by ID", async () => {
|
|
52
|
+
const created = await navigationService.create({
|
|
53
|
+
label: "Home",
|
|
54
|
+
url: "/",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const found = await navigationService.getById(created.id);
|
|
58
|
+
expect(found).not.toBeNull();
|
|
59
|
+
expect(found?.label).toBe("Home");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns null for non-existent ID", async () => {
|
|
63
|
+
const found = await navigationService.getById(9999);
|
|
64
|
+
expect(found).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("list", () => {
|
|
69
|
+
it("returns empty array when no links exist", async () => {
|
|
70
|
+
const links = await navigationService.list();
|
|
71
|
+
expect(links).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("returns links ordered by position", async () => {
|
|
75
|
+
await navigationService.create({
|
|
76
|
+
label: "C",
|
|
77
|
+
url: "/c",
|
|
78
|
+
position: 2,
|
|
79
|
+
});
|
|
80
|
+
await navigationService.create({
|
|
81
|
+
label: "A",
|
|
82
|
+
url: "/a",
|
|
83
|
+
position: 0,
|
|
84
|
+
});
|
|
85
|
+
await navigationService.create({
|
|
86
|
+
label: "B",
|
|
87
|
+
url: "/b",
|
|
88
|
+
position: 1,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const links = await navigationService.list();
|
|
92
|
+
expect(links).toHaveLength(3);
|
|
93
|
+
expect(links[0]?.label).toBe("A");
|
|
94
|
+
expect(links[1]?.label).toBe("B");
|
|
95
|
+
expect(links[2]?.label).toBe("C");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("update", () => {
|
|
100
|
+
it("updates a link's label", async () => {
|
|
101
|
+
const created = await navigationService.create({
|
|
102
|
+
label: "Home",
|
|
103
|
+
url: "/",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const updated = await navigationService.update(created.id, {
|
|
107
|
+
label: "Main Page",
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(updated?.label).toBe("Main Page");
|
|
111
|
+
expect(updated?.url).toBe("/");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("updates a link's url", async () => {
|
|
115
|
+
const created = await navigationService.create({
|
|
116
|
+
label: "Blog",
|
|
117
|
+
url: "/blog",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const updated = await navigationService.update(created.id, {
|
|
121
|
+
url: "/posts",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(updated?.url).toBe("/posts");
|
|
125
|
+
expect(updated?.label).toBe("Blog");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("returns null for non-existent ID", async () => {
|
|
129
|
+
const result = await navigationService.update(9999, { label: "Nope" });
|
|
130
|
+
expect(result).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("delete", () => {
|
|
135
|
+
it("deletes a link by ID", async () => {
|
|
136
|
+
const link = await navigationService.create({
|
|
137
|
+
label: "Home",
|
|
138
|
+
url: "/",
|
|
139
|
+
});
|
|
140
|
+
const result = await navigationService.delete(link.id);
|
|
141
|
+
|
|
142
|
+
expect(result).toBe(true);
|
|
143
|
+
|
|
144
|
+
const found = await navigationService.getById(link.id);
|
|
145
|
+
expect(found).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("returns false for non-existent ID", async () => {
|
|
149
|
+
const result = await navigationService.delete(9999);
|
|
150
|
+
expect(result).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("reorder", () => {
|
|
155
|
+
it("updates positions to match array order", async () => {
|
|
156
|
+
const a = await navigationService.create({
|
|
157
|
+
label: "A",
|
|
158
|
+
url: "/a",
|
|
159
|
+
});
|
|
160
|
+
const b = await navigationService.create({
|
|
161
|
+
label: "B",
|
|
162
|
+
url: "/b",
|
|
163
|
+
});
|
|
164
|
+
const c = await navigationService.create({
|
|
165
|
+
label: "C",
|
|
166
|
+
url: "/c",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Reverse the order
|
|
170
|
+
await navigationService.reorder([c.id, b.id, a.id]);
|
|
171
|
+
|
|
172
|
+
const links = await navigationService.list();
|
|
173
|
+
expect(links[0]?.label).toBe("C");
|
|
174
|
+
expect(links[0]?.position).toBe(0);
|
|
175
|
+
expect(links[1]?.label).toBe("B");
|
|
176
|
+
expect(links[1]?.position).toBe(1);
|
|
177
|
+
expect(links[2]?.label).toBe("A");
|
|
178
|
+
expect(links[2]?.position).toBe(2);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("ensureDefaults", () => {
|
|
183
|
+
it("creates default links when table is empty", async () => {
|
|
184
|
+
const links = await navigationService.ensureDefaults();
|
|
185
|
+
|
|
186
|
+
expect(links).toHaveLength(3);
|
|
187
|
+
expect(links[0]?.label).toBe("Home");
|
|
188
|
+
expect(links[0]?.url).toBe("/");
|
|
189
|
+
expect(links[1]?.label).toBe("Archive");
|
|
190
|
+
expect(links[1]?.url).toBe("/archive");
|
|
191
|
+
expect(links[2]?.label).toBe("RSS");
|
|
192
|
+
expect(links[2]?.url).toBe("/feed");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("returns existing links without creating new ones", async () => {
|
|
196
|
+
await navigationService.create({ label: "Custom", url: "/custom" });
|
|
197
|
+
|
|
198
|
+
const links = await navigationService.ensureDefaults();
|
|
199
|
+
|
|
200
|
+
expect(links).toHaveLength(1);
|
|
201
|
+
expect(links[0]?.label).toBe("Custom");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("is idempotent - calling twice returns same result", async () => {
|
|
205
|
+
const first = await navigationService.ensureDefaults();
|
|
206
|
+
const second = await navigationService.ensureDefaults();
|
|
207
|
+
|
|
208
|
+
expect(first).toHaveLength(3);
|
|
209
|
+
expect(second).toHaveLength(3);
|
|
210
|
+
expect(first[0]?.id).toBe(second[0]?.id);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
3
|
+
import { createPostService } from "../post.js";
|
|
4
|
+
import type { Database } from "../../db/index.js";
|
|
5
|
+
|
|
6
|
+
describe("PostService - Timeline features", () => {
|
|
7
|
+
let db: Database;
|
|
8
|
+
let postService: ReturnType<typeof createPostService>;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
const testDb = createTestDatabase();
|
|
12
|
+
db = testDb.db as unknown as Database;
|
|
13
|
+
postService = createPostService(db);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("excludeTypes filter", () => {
|
|
17
|
+
it("excludes posts of specified types", async () => {
|
|
18
|
+
await postService.create({ type: "note", content: "a note" });
|
|
19
|
+
await postService.create({ type: "page", content: "a page" });
|
|
20
|
+
await postService.create({
|
|
21
|
+
type: "article",
|
|
22
|
+
content: "an article",
|
|
23
|
+
title: "Article",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const posts = await postService.list({ excludeTypes: ["page"] });
|
|
27
|
+
expect(posts).toHaveLength(2);
|
|
28
|
+
expect(posts.every((p) => p.type !== "page")).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("excludes multiple types", async () => {
|
|
32
|
+
await postService.create({ type: "note", content: "a note" });
|
|
33
|
+
await postService.create({ type: "page", content: "a page" });
|
|
34
|
+
await postService.create({
|
|
35
|
+
type: "article",
|
|
36
|
+
content: "an article",
|
|
37
|
+
title: "Article",
|
|
38
|
+
});
|
|
39
|
+
await postService.create({
|
|
40
|
+
type: "link",
|
|
41
|
+
content: "a link",
|
|
42
|
+
sourceUrl: "https://example.com",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const posts = await postService.list({
|
|
46
|
+
excludeTypes: ["page", "link"],
|
|
47
|
+
});
|
|
48
|
+
expect(posts).toHaveLength(2);
|
|
49
|
+
expect(posts.every((p) => p.type !== "page" && p.type !== "link")).toBe(
|
|
50
|
+
true,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns all posts when excludeTypes is empty", async () => {
|
|
55
|
+
await postService.create({ type: "note", content: "a note" });
|
|
56
|
+
await postService.create({ type: "page", content: "a page" });
|
|
57
|
+
|
|
58
|
+
const posts = await postService.list({ excludeTypes: [] });
|
|
59
|
+
expect(posts).toHaveLength(2);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("works combined with other filters", async () => {
|
|
63
|
+
await postService.create({
|
|
64
|
+
type: "note",
|
|
65
|
+
content: "featured note",
|
|
66
|
+
visibility: "featured",
|
|
67
|
+
});
|
|
68
|
+
await postService.create({
|
|
69
|
+
type: "page",
|
|
70
|
+
content: "featured page",
|
|
71
|
+
visibility: "featured",
|
|
72
|
+
});
|
|
73
|
+
await postService.create({
|
|
74
|
+
type: "note",
|
|
75
|
+
content: "draft note",
|
|
76
|
+
visibility: "draft",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const posts = await postService.list({
|
|
80
|
+
excludeTypes: ["page"],
|
|
81
|
+
visibility: "featured",
|
|
82
|
+
});
|
|
83
|
+
expect(posts).toHaveLength(1);
|
|
84
|
+
expect(posts[0]?.type).toBe("note");
|
|
85
|
+
expect(posts[0]?.visibility).toBe("featured");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("getThreadPreviews", () => {
|
|
90
|
+
it("returns empty map for empty input", async () => {
|
|
91
|
+
const previews = await postService.getThreadPreviews([]);
|
|
92
|
+
expect(previews.size).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns preview replies for a thread root", async () => {
|
|
96
|
+
const root = await postService.create({
|
|
97
|
+
type: "note",
|
|
98
|
+
content: "root",
|
|
99
|
+
});
|
|
100
|
+
await postService.create({
|
|
101
|
+
type: "note",
|
|
102
|
+
content: "reply 1",
|
|
103
|
+
replyToId: root.id,
|
|
104
|
+
});
|
|
105
|
+
await postService.create({
|
|
106
|
+
type: "note",
|
|
107
|
+
content: "reply 2",
|
|
108
|
+
replyToId: root.id,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const previews = await postService.getThreadPreviews([root.id]);
|
|
112
|
+
const replies = previews.get(root.id);
|
|
113
|
+
expect(replies).toBeDefined();
|
|
114
|
+
expect(replies).toHaveLength(2);
|
|
115
|
+
expect(replies?.[0]?.content).toBe("reply 1");
|
|
116
|
+
expect(replies?.[1]?.content).toBe("reply 2");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("limits preview replies to previewCount", async () => {
|
|
120
|
+
const root = await postService.create({
|
|
121
|
+
type: "note",
|
|
122
|
+
content: "root",
|
|
123
|
+
});
|
|
124
|
+
for (let i = 0; i < 5; i++) {
|
|
125
|
+
await postService.create({
|
|
126
|
+
type: "note",
|
|
127
|
+
content: `reply ${i}`,
|
|
128
|
+
replyToId: root.id,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const previews = await postService.getThreadPreviews([root.id], 2);
|
|
133
|
+
const replies = previews.get(root.id);
|
|
134
|
+
expect(replies).toHaveLength(2);
|
|
135
|
+
expect(replies?.[0]?.content).toBe("reply 0");
|
|
136
|
+
expect(replies?.[1]?.content).toBe("reply 1");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("defaults to 3 preview replies", async () => {
|
|
140
|
+
const root = await postService.create({
|
|
141
|
+
type: "note",
|
|
142
|
+
content: "root",
|
|
143
|
+
});
|
|
144
|
+
for (let i = 0; i < 5; i++) {
|
|
145
|
+
await postService.create({
|
|
146
|
+
type: "note",
|
|
147
|
+
content: `reply ${i}`,
|
|
148
|
+
replyToId: root.id,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const previews = await postService.getThreadPreviews([root.id]);
|
|
153
|
+
const replies = previews.get(root.id);
|
|
154
|
+
expect(replies).toHaveLength(3);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("handles multiple thread roots", async () => {
|
|
158
|
+
const root1 = await postService.create({
|
|
159
|
+
type: "note",
|
|
160
|
+
content: "root 1",
|
|
161
|
+
});
|
|
162
|
+
const root2 = await postService.create({
|
|
163
|
+
type: "note",
|
|
164
|
+
content: "root 2",
|
|
165
|
+
});
|
|
166
|
+
await postService.create({
|
|
167
|
+
type: "note",
|
|
168
|
+
content: "reply to root 1",
|
|
169
|
+
replyToId: root1.id,
|
|
170
|
+
});
|
|
171
|
+
await postService.create({
|
|
172
|
+
type: "note",
|
|
173
|
+
content: "reply to root 2",
|
|
174
|
+
replyToId: root2.id,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const previews = await postService.getThreadPreviews([
|
|
178
|
+
root1.id,
|
|
179
|
+
root2.id,
|
|
180
|
+
]);
|
|
181
|
+
expect(previews.size).toBe(2);
|
|
182
|
+
expect(previews.get(root1.id)).toHaveLength(1);
|
|
183
|
+
expect(previews.get(root2.id)).toHaveLength(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("excludes deleted replies", async () => {
|
|
187
|
+
const root = await postService.create({
|
|
188
|
+
type: "note",
|
|
189
|
+
content: "root",
|
|
190
|
+
});
|
|
191
|
+
const reply1 = await postService.create({
|
|
192
|
+
type: "note",
|
|
193
|
+
content: "reply 1",
|
|
194
|
+
replyToId: root.id,
|
|
195
|
+
});
|
|
196
|
+
await postService.create({
|
|
197
|
+
type: "note",
|
|
198
|
+
content: "reply 2",
|
|
199
|
+
replyToId: root.id,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await postService.delete(reply1.id);
|
|
203
|
+
|
|
204
|
+
const previews = await postService.getThreadPreviews([root.id]);
|
|
205
|
+
const replies = previews.get(root.id);
|
|
206
|
+
expect(replies).toHaveLength(1);
|
|
207
|
+
expect(replies?.[0]?.content).toBe("reply 2");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("returns empty for roots with no replies", async () => {
|
|
211
|
+
const root = await postService.create({
|
|
212
|
+
type: "note",
|
|
213
|
+
content: "root with no replies",
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const previews = await postService.getThreadPreviews([root.id]);
|
|
217
|
+
expect(previews.get(root.id)).toBeUndefined();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|