@jant/core 0.3.35 → 0.3.37
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/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4564 -3013
- package/dist/index.js +12885 -8161
- package/package.json +23 -6
- package/src/__tests__/helpers/app.ts +10 -10
- package/src/__tests__/helpers/db.ts +91 -87
- package/src/app.tsx +157 -31
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/{lib → client}/avatar-upload.ts +4 -3
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
- package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +43 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/client/components/compose-types.ts +174 -0
- package/src/client/components/jant-collection-form.ts +667 -0
- package/src/client/components/jant-collection-sidebar.ts +805 -0
- package/src/client/components/jant-compose-dialog.ts +2161 -0
- package/src/client/components/jant-compose-editor.ts +1813 -0
- package/src/client/components/jant-compose-fullscreen.ts +283 -0
- package/src/client/components/jant-media-lightbox.ts +259 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
- package/src/{ui → client}/components/jant-post-form.ts +141 -12
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
- package/src/{ui → client}/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/{ui → client}/components/nav-manager-types.ts +6 -18
- package/src/{ui → client}/components/post-form-template.ts +137 -38
- package/src/{ui → client}/components/post-form-types.ts +15 -4
- package/src/client/compose-bridge.ts +583 -0
- package/src/{lib → client}/image-processor.ts +26 -8
- package/src/client/lazy-slugify.ts +51 -0
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/{lib → client}/post-form-bridge.ts +53 -2
- package/src/{lib → client}/settings-bridge.ts +3 -15
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +86 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +65 -0
- package/src/client/tiptap/image-node.ts +482 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +129 -0
- package/src/client/tiptap/slash-commands.ts +438 -0
- package/src/{lib → client}/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +44 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +27 -17
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -140
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +783 -1087
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +867 -812
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +878 -823
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +186 -65
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +140 -65
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +963 -0
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +77 -31
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +22 -12
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +24 -5
- package/src/lib/resolve-config.ts +13 -2
- package/src/lib/schemas.ts +226 -58
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +158 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +76 -34
- package/src/lib/tiptap-render.ts +191 -0
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +263 -14
- package/src/lib/url.ts +37 -22
- package/src/lib/view.ts +236 -55
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/error-handler.ts +3 -3
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +83 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +57 -31
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +81 -62
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +92 -24
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +39 -31
- package/src/routes/auth/signin.tsx +13 -14
- package/src/routes/compose.tsx +27 -63
- package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +475 -99
- package/src/routes/feed/__tests__/rss.test.ts +22 -23
- package/src/routes/feed/rss.ts +6 -2
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +36 -18
- package/src/routes/pages/archive.tsx +177 -37
- package/src/routes/pages/collection.tsx +43 -14
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +27 -3
- package/src/routes/pages/home.tsx +15 -14
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +800 -230
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +764 -172
- package/src/services/search.ts +161 -74
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +293 -62
- package/src/styles/tokens.css +93 -5
- package/src/styles/ui.css +4349 -766
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +34 -4
- package/src/types/constants.ts +17 -2
- package/src/types/entities.ts +83 -37
- package/src/types/operations.ts +20 -27
- package/src/types/props.ts +52 -17
- package/src/types/views.ts +48 -24
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +255 -16
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +12 -2
- package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
- package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +87 -146
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +78 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +116 -103
- package/src/ui/pages/ArchivePage.tsx +923 -95
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +182 -38
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +239 -4
- package/src/ui/shared/MediaGallery.tsx +475 -41
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/collections-reorder.ts +0 -28
- package/src/lib/compose-bridge.ts +0 -280
- package/src/lib/media-upload.ts +0 -148
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/routes/dash/index.tsx +0 -103
- package/src/routes/dash/media.tsx +0 -132
- package/src/routes/dash/pages.tsx +0 -239
- package/src/routes/dash/posts.tsx +0 -334
- package/src/routes/dash/redirects.tsx +0 -257
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -203
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/types/sortablejs.d.ts +0 -29
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
- package/src/ui/components/compose-types.ts +0 -75
- package/src/ui/components/jant-collection-form.ts +0 -512
- package/src/ui/components/jant-compose-dialog.ts +0 -495
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/PageForm.tsx +0 -185
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/media/MediaListContent.tsx +0 -201
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -74
- package/src/ui/dash/posts/PostForm.tsx +0 -248
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- package/src/ui/layouts/DashLayout.tsx +0 -165
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
- /package/src/{ui → client}/components/settings-types.ts +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
3
|
+
import { createCustomUrlService } from "../custom-url.js";
|
|
4
|
+
import { createPostService } from "../post.js";
|
|
5
|
+
import type { Database } from "../../db/index.js";
|
|
6
|
+
|
|
7
|
+
describe("CustomUrlService", () => {
|
|
8
|
+
let db: Database;
|
|
9
|
+
let customUrlService: ReturnType<typeof createCustomUrlService>;
|
|
10
|
+
let postService: ReturnType<typeof createPostService>;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
const testDb = createTestDatabase();
|
|
14
|
+
db = testDb.db as unknown as Database;
|
|
15
|
+
customUrlService = createCustomUrlService(db);
|
|
16
|
+
postService = createPostService(db, { slugIdLength: 5 });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("create", () => {
|
|
20
|
+
it("creates a redirect custom URL", async () => {
|
|
21
|
+
const url = await customUrlService.create({
|
|
22
|
+
path: "old-page",
|
|
23
|
+
targetType: "redirect",
|
|
24
|
+
toPath: "/new-page",
|
|
25
|
+
redirectType: 301,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(url.path).toBe("old-page");
|
|
29
|
+
expect(url.targetType).toBe("redirect");
|
|
30
|
+
expect(url.toPath).toBe("/new-page");
|
|
31
|
+
expect(url.redirectType).toBe(301);
|
|
32
|
+
expect(typeof url.id).toBe("string");
|
|
33
|
+
expect(typeof url.createdAt).toBe("number");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("creates a post custom URL", async () => {
|
|
37
|
+
const post = await postService.create({ format: "note" });
|
|
38
|
+
|
|
39
|
+
const url = await customUrlService.create({
|
|
40
|
+
path: "blog/my-post",
|
|
41
|
+
targetType: "post",
|
|
42
|
+
targetId: post.id,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(url.path).toBe("blog/my-post");
|
|
46
|
+
expect(url.targetType).toBe("post");
|
|
47
|
+
expect(url.targetId).toBe(post.id);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("rejects reserved paths", async () => {
|
|
51
|
+
await expect(
|
|
52
|
+
customUrlService.create({
|
|
53
|
+
path: "dash",
|
|
54
|
+
targetType: "redirect",
|
|
55
|
+
toPath: "/somewhere",
|
|
56
|
+
}),
|
|
57
|
+
).rejects.toThrow("reserved");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("rejects duplicate paths", async () => {
|
|
61
|
+
await customUrlService.create({
|
|
62
|
+
path: "my-path",
|
|
63
|
+
targetType: "redirect",
|
|
64
|
+
toPath: "/target",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await expect(
|
|
68
|
+
customUrlService.create({
|
|
69
|
+
path: "my-path",
|
|
70
|
+
targetType: "redirect",
|
|
71
|
+
toPath: "/other-target",
|
|
72
|
+
}),
|
|
73
|
+
).rejects.toThrow("already in use");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("rejects paths that conflict with post slugs", async () => {
|
|
77
|
+
const post = await postService.create({
|
|
78
|
+
format: "note",
|
|
79
|
+
slug: "my-slug",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await expect(
|
|
83
|
+
customUrlService.create({
|
|
84
|
+
path: post.slug,
|
|
85
|
+
targetType: "redirect",
|
|
86
|
+
toPath: "/somewhere",
|
|
87
|
+
}),
|
|
88
|
+
).rejects.toThrow("conflicts with an existing post slug");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("getByPath", () => {
|
|
93
|
+
it("returns custom URL by path", async () => {
|
|
94
|
+
await customUrlService.create({
|
|
95
|
+
path: "test-path",
|
|
96
|
+
targetType: "redirect",
|
|
97
|
+
toPath: "/target",
|
|
98
|
+
redirectType: 302,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const result = await customUrlService.getByPath("test-path");
|
|
102
|
+
expect(result).not.toBeNull();
|
|
103
|
+
expect(result?.path).toBe("test-path");
|
|
104
|
+
expect(result?.redirectType).toBe(302);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("returns null for non-existent path", async () => {
|
|
108
|
+
const result = await customUrlService.getByPath("nonexistent");
|
|
109
|
+
expect(result).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("getByTarget", () => {
|
|
114
|
+
it("returns custom URL by target", async () => {
|
|
115
|
+
const post = await postService.create({ format: "note" });
|
|
116
|
+
await customUrlService.create({
|
|
117
|
+
path: "custom-path",
|
|
118
|
+
targetType: "post",
|
|
119
|
+
targetId: post.id,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = await customUrlService.getByTarget("post", post.id);
|
|
123
|
+
expect(result).not.toBeNull();
|
|
124
|
+
expect(result?.path).toBe("custom-path");
|
|
125
|
+
expect(result?.targetId).toBe(post.id);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("returns null when no match", async () => {
|
|
129
|
+
const result = await customUrlService.getByTarget(
|
|
130
|
+
"post",
|
|
131
|
+
"nonexistent-id",
|
|
132
|
+
);
|
|
133
|
+
expect(result).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("delete", () => {
|
|
138
|
+
it("deletes a custom URL", async () => {
|
|
139
|
+
const url = await customUrlService.create({
|
|
140
|
+
path: "to-delete",
|
|
141
|
+
targetType: "redirect",
|
|
142
|
+
toPath: "/target",
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const deleted = await customUrlService.delete(url.id);
|
|
146
|
+
expect(deleted).toBe(true);
|
|
147
|
+
|
|
148
|
+
const result = await customUrlService.getByPath("to-delete");
|
|
149
|
+
expect(result).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("returns false for non-existent ID", async () => {
|
|
153
|
+
const deleted = await customUrlService.delete("nonexistent-id");
|
|
154
|
+
expect(deleted).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("list", () => {
|
|
159
|
+
it("returns all custom URLs", async () => {
|
|
160
|
+
await customUrlService.create({
|
|
161
|
+
path: "path-1",
|
|
162
|
+
targetType: "redirect",
|
|
163
|
+
toPath: "/a",
|
|
164
|
+
});
|
|
165
|
+
await customUrlService.create({
|
|
166
|
+
path: "path-2",
|
|
167
|
+
targetType: "redirect",
|
|
168
|
+
toPath: "/b",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const all = await customUrlService.list();
|
|
172
|
+
expect(all).toHaveLength(2);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("returns empty array when none exist", async () => {
|
|
176
|
+
const all = await customUrlService.list();
|
|
177
|
+
expect(all).toEqual([]);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("isPathAvailable", () => {
|
|
182
|
+
it("returns true for available path", async () => {
|
|
183
|
+
const available = await customUrlService.isPathAvailable("free-path");
|
|
184
|
+
expect(available).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("returns false for reserved path", async () => {
|
|
188
|
+
const available = await customUrlService.isPathAvailable("api");
|
|
189
|
+
expect(available).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("returns false for existing custom URL path", async () => {
|
|
193
|
+
await customUrlService.create({
|
|
194
|
+
path: "taken-path",
|
|
195
|
+
targetType: "redirect",
|
|
196
|
+
toPath: "/target",
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const available = await customUrlService.isPathAvailable("taken-path");
|
|
200
|
+
expect(available).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("returns false for existing post slug", async () => {
|
|
204
|
+
const post = await postService.create({
|
|
205
|
+
format: "note",
|
|
206
|
+
slug: "post-slug",
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const available = await customUrlService.isPathAvailable(post.slug);
|
|
210
|
+
expect(available).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -3,7 +3,6 @@ import { describe, it, expect, beforeEach } from "vitest";
|
|
|
3
3
|
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
4
4
|
import { createMediaService } from "../media.js";
|
|
5
5
|
import { createPostService } from "../post.js";
|
|
6
|
-
import { createPathRegistryService } from "../path-registry.js";
|
|
7
6
|
import type { Database } from "../../db/index.js";
|
|
8
7
|
|
|
9
8
|
describe("MediaService", () => {
|
|
@@ -15,7 +14,7 @@ describe("MediaService", () => {
|
|
|
15
14
|
const testDb = createTestDatabase();
|
|
16
15
|
db = testDb.db as unknown as Database;
|
|
17
16
|
mediaService = createMediaService(db);
|
|
18
|
-
postService = createPostService(db,
|
|
17
|
+
postService = createPostService(db, { slugIdLength: 5 });
|
|
19
18
|
});
|
|
20
19
|
|
|
21
20
|
const sampleMedia = {
|
|
@@ -45,8 +44,19 @@ describe("MediaService", () => {
|
|
|
45
44
|
expect(media.height).toBe(1080);
|
|
46
45
|
expect(media.postId).toBeNull();
|
|
47
46
|
expect(media.alt).toBeNull();
|
|
48
|
-
expect(media.position).toBe(
|
|
47
|
+
expect(media.position).toBe("a0");
|
|
49
48
|
expect(media.blurhash).toBeNull();
|
|
49
|
+
expect(media.posterKey).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("creates media with posterKey", async () => {
|
|
53
|
+
const media = await mediaService.create({
|
|
54
|
+
...sampleMedia,
|
|
55
|
+
storageKey: "media/2025/01/video.mp4",
|
|
56
|
+
posterKey: "media/2025/01/video-poster.webp",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(media.posterKey).toBe("media/2025/01/video-poster.webp");
|
|
50
60
|
});
|
|
51
61
|
|
|
52
62
|
it("creates media with optional alt text", async () => {
|
|
@@ -75,14 +85,34 @@ describe("MediaService", () => {
|
|
|
75
85
|
it("creates media with position and blurhash", async () => {
|
|
76
86
|
const media = await mediaService.create({
|
|
77
87
|
...sampleMedia,
|
|
78
|
-
position:
|
|
88
|
+
position: "a3",
|
|
79
89
|
blurhash: "LKO2?U%2Tw=w]~RBVZRi};RPxuwH",
|
|
80
90
|
});
|
|
81
91
|
|
|
82
|
-
expect(media.position).toBe(
|
|
92
|
+
expect(media.position).toBe("a3");
|
|
83
93
|
expect(media.blurhash).toBe("LKO2?U%2Tw=w]~RBVZRi};RPxuwH");
|
|
84
94
|
});
|
|
85
95
|
|
|
96
|
+
it("appends position when creating media already attached to a post", async () => {
|
|
97
|
+
const post = await postService.create({
|
|
98
|
+
format: "note",
|
|
99
|
+
bodyMarkdown: "test",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const media1 = await mediaService.create({
|
|
103
|
+
...sampleMedia,
|
|
104
|
+
postId: post.id,
|
|
105
|
+
});
|
|
106
|
+
const media2 = await mediaService.create({
|
|
107
|
+
...sampleMedia,
|
|
108
|
+
postId: post.id,
|
|
109
|
+
storageKey: "media/2025/01/second.jpg",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(media1.position).toBe("a0");
|
|
113
|
+
expect(media2.position).toBe("a1");
|
|
114
|
+
});
|
|
115
|
+
|
|
86
116
|
it("generates UUIDv7 IDs", async () => {
|
|
87
117
|
const media1 = await mediaService.create(sampleMedia);
|
|
88
118
|
const media2 = await mediaService.create({
|
|
@@ -117,6 +147,46 @@ describe("MediaService", () => {
|
|
|
117
147
|
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
|
118
148
|
);
|
|
119
149
|
});
|
|
150
|
+
|
|
151
|
+
it("rejects non-positive sizes at the database layer", async () => {
|
|
152
|
+
await expect(
|
|
153
|
+
mediaService.create({
|
|
154
|
+
...sampleMedia,
|
|
155
|
+
storageKey: "media/2025/01/invalid-size.jpg",
|
|
156
|
+
size: 0,
|
|
157
|
+
}),
|
|
158
|
+
).rejects.toThrow();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("rejects blank positions at the database layer", async () => {
|
|
162
|
+
await expect(
|
|
163
|
+
mediaService.create({
|
|
164
|
+
...sampleMedia,
|
|
165
|
+
storageKey: "media/2025/01/invalid-position.jpg",
|
|
166
|
+
position: " ",
|
|
167
|
+
}),
|
|
168
|
+
).rejects.toThrow();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("rejects non-positive dimensions at the database layer", async () => {
|
|
172
|
+
await expect(
|
|
173
|
+
mediaService.create({
|
|
174
|
+
...sampleMedia,
|
|
175
|
+
storageKey: "media/2025/01/invalid-dimensions.jpg",
|
|
176
|
+
width: 0,
|
|
177
|
+
}),
|
|
178
|
+
).rejects.toThrow();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("rejects negative extracted text length at the database layer", async () => {
|
|
182
|
+
await expect(
|
|
183
|
+
mediaService.create({
|
|
184
|
+
...sampleMedia,
|
|
185
|
+
storageKey: "media/2025/01/invalid-chars.jpg",
|
|
186
|
+
chars: -1,
|
|
187
|
+
}),
|
|
188
|
+
).rejects.toThrow();
|
|
189
|
+
});
|
|
120
190
|
});
|
|
121
191
|
|
|
122
192
|
describe("getById", () => {
|
|
@@ -168,7 +238,7 @@ describe("MediaService", () => {
|
|
|
168
238
|
it("returns media ordered by position", async () => {
|
|
169
239
|
const post = await postService.create({
|
|
170
240
|
format: "note",
|
|
171
|
-
|
|
241
|
+
bodyMarkdown: "test",
|
|
172
242
|
});
|
|
173
243
|
|
|
174
244
|
const m1 = await mediaService.create({
|
|
@@ -185,15 +255,15 @@ describe("MediaService", () => {
|
|
|
185
255
|
const results = await mediaService.getByPostId(post.id);
|
|
186
256
|
expect(results).toHaveLength(2);
|
|
187
257
|
expect(results[0]!.id).toBe(m2.id);
|
|
188
|
-
expect(results[0]!.position).toBe(
|
|
258
|
+
expect(results[0]!.position).toBe("a0");
|
|
189
259
|
expect(results[1]!.id).toBe(m1.id);
|
|
190
|
-
expect(results[1]!.position).toBe(
|
|
260
|
+
expect(results[1]!.position).toBe("a1");
|
|
191
261
|
});
|
|
192
262
|
|
|
193
263
|
it("returns empty array for post with no media", async () => {
|
|
194
264
|
const post = await postService.create({
|
|
195
265
|
format: "note",
|
|
196
|
-
|
|
266
|
+
bodyMarkdown: "test",
|
|
197
267
|
});
|
|
198
268
|
|
|
199
269
|
const results = await mediaService.getByPostId(post.id);
|
|
@@ -205,11 +275,11 @@ describe("MediaService", () => {
|
|
|
205
275
|
it("returns Map grouped by postId", async () => {
|
|
206
276
|
const post1 = await postService.create({
|
|
207
277
|
format: "note",
|
|
208
|
-
|
|
278
|
+
bodyMarkdown: "post 1",
|
|
209
279
|
});
|
|
210
280
|
const post2 = await postService.create({
|
|
211
281
|
format: "note",
|
|
212
|
-
|
|
282
|
+
bodyMarkdown: "post 2",
|
|
213
283
|
});
|
|
214
284
|
|
|
215
285
|
const m1 = await mediaService.create({
|
|
@@ -242,7 +312,7 @@ describe("MediaService", () => {
|
|
|
242
312
|
it("returns ordered by position within each post", async () => {
|
|
243
313
|
const post = await postService.create({
|
|
244
314
|
format: "note",
|
|
245
|
-
|
|
315
|
+
bodyMarkdown: "test",
|
|
246
316
|
});
|
|
247
317
|
|
|
248
318
|
const m1 = await mediaService.create({
|
|
@@ -314,15 +384,37 @@ describe("MediaService", () => {
|
|
|
314
384
|
|
|
315
385
|
const found = await mediaService.getByStorageKey(
|
|
316
386
|
"media/2025/01/0192a9f1-a2b7-7c3d-8e4f-5a6b7c8d9e0f.jpg",
|
|
387
|
+
"r2",
|
|
317
388
|
);
|
|
318
389
|
expect(found).not.toBeNull();
|
|
319
390
|
expect(found?.originalName).toBe("photo.jpg");
|
|
320
391
|
});
|
|
321
392
|
|
|
322
393
|
it("returns null for non-existent R2 key", async () => {
|
|
323
|
-
const found = await mediaService.getByStorageKey("nonexistent");
|
|
394
|
+
const found = await mediaService.getByStorageKey("nonexistent", "r2");
|
|
324
395
|
expect(found).toBeNull();
|
|
325
396
|
});
|
|
397
|
+
|
|
398
|
+
it("allows the same storage key on different providers", async () => {
|
|
399
|
+
await mediaService.create(sampleMedia);
|
|
400
|
+
await mediaService.create({
|
|
401
|
+
...sampleMedia,
|
|
402
|
+
id: "0192a9f1-a2b7-7c3d-8e4f-5a6b7c8d9e10",
|
|
403
|
+
provider: "s3",
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const r2Media = await mediaService.getByStorageKey(
|
|
407
|
+
sampleMedia.storageKey,
|
|
408
|
+
"r2",
|
|
409
|
+
);
|
|
410
|
+
const s3Media = await mediaService.getByStorageKey(
|
|
411
|
+
sampleMedia.storageKey,
|
|
412
|
+
"s3",
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
expect(r2Media?.provider).toBe("r2");
|
|
416
|
+
expect(s3Media?.provider).toBe("s3");
|
|
417
|
+
});
|
|
326
418
|
});
|
|
327
419
|
|
|
328
420
|
describe("list", () => {
|
|
@@ -356,7 +448,7 @@ describe("MediaService", () => {
|
|
|
356
448
|
it("sets postId and position for each media", async () => {
|
|
357
449
|
const post = await postService.create({
|
|
358
450
|
format: "note",
|
|
359
|
-
|
|
451
|
+
bodyMarkdown: "test",
|
|
360
452
|
});
|
|
361
453
|
|
|
362
454
|
const m1 = await mediaService.create({
|
|
@@ -373,15 +465,15 @@ describe("MediaService", () => {
|
|
|
373
465
|
const attached = await mediaService.getByPostId(post.id);
|
|
374
466
|
expect(attached).toHaveLength(2);
|
|
375
467
|
expect(attached[0]!.id).toBe(m1.id);
|
|
376
|
-
expect(attached[0]!.position).toBe(
|
|
468
|
+
expect(attached[0]!.position).toBe("a0");
|
|
377
469
|
expect(attached[1]!.id).toBe(m2.id);
|
|
378
|
-
expect(attached[1]!.position).toBe(
|
|
470
|
+
expect(attached[1]!.position).toBe("a1");
|
|
379
471
|
});
|
|
380
472
|
|
|
381
473
|
it("replaces existing attachments", async () => {
|
|
382
474
|
const post = await postService.create({
|
|
383
475
|
format: "note",
|
|
384
|
-
|
|
476
|
+
bodyMarkdown: "test",
|
|
385
477
|
});
|
|
386
478
|
|
|
387
479
|
const m1 = await mediaService.create({
|
|
@@ -403,18 +495,18 @@ describe("MediaService", () => {
|
|
|
403
495
|
const attached = await mediaService.getByPostId(post.id);
|
|
404
496
|
expect(attached).toHaveLength(1);
|
|
405
497
|
expect(attached[0]!.id).toBe(m3.id);
|
|
406
|
-
expect(attached[0]!.position).toBe(
|
|
498
|
+
expect(attached[0]!.position).toBe("a0");
|
|
407
499
|
|
|
408
500
|
// Verify old media is detached
|
|
409
501
|
const old1 = await mediaService.getById(m1.id);
|
|
410
502
|
expect(old1!.postId).toBeNull();
|
|
411
|
-
expect(old1!.position).toBe(
|
|
503
|
+
expect(old1!.position).toBe("a0");
|
|
412
504
|
});
|
|
413
505
|
|
|
414
506
|
it("handles empty array by clearing all attachments", async () => {
|
|
415
507
|
const post = await postService.create({
|
|
416
508
|
format: "note",
|
|
417
|
-
|
|
509
|
+
bodyMarkdown: "test",
|
|
418
510
|
});
|
|
419
511
|
|
|
420
512
|
const m1 = await mediaService.create({
|
|
@@ -434,7 +526,7 @@ describe("MediaService", () => {
|
|
|
434
526
|
it("clears postId and resets position", async () => {
|
|
435
527
|
const post = await postService.create({
|
|
436
528
|
format: "note",
|
|
437
|
-
|
|
529
|
+
bodyMarkdown: "test",
|
|
438
530
|
});
|
|
439
531
|
|
|
440
532
|
const m1 = await mediaService.create({
|
|
@@ -450,7 +542,7 @@ describe("MediaService", () => {
|
|
|
450
542
|
|
|
451
543
|
const detached = await mediaService.getById(m1.id);
|
|
452
544
|
expect(detached!.postId).toBeNull();
|
|
453
|
-
expect(detached!.position).toBe(
|
|
545
|
+
expect(detached!.position).toBe("a0");
|
|
454
546
|
});
|
|
455
547
|
});
|
|
456
548
|
|
|
@@ -469,6 +561,27 @@ describe("MediaService", () => {
|
|
|
469
561
|
const result = await mediaService.delete("nonexistent");
|
|
470
562
|
expect(result).toBe(false);
|
|
471
563
|
});
|
|
564
|
+
|
|
565
|
+
it("deletes poster from storage when posterKey exists", async () => {
|
|
566
|
+
const media = await mediaService.create({
|
|
567
|
+
...sampleMedia,
|
|
568
|
+
storageKey: "media/2025/01/vid.mp4",
|
|
569
|
+
posterKey: "media/2025/01/vid-poster.webp",
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const deletedKeys: string[] = [];
|
|
573
|
+
const mockStorage = {
|
|
574
|
+
delete: async (key: string) => {
|
|
575
|
+
deletedKeys.push(key);
|
|
576
|
+
},
|
|
577
|
+
put: async () => {},
|
|
578
|
+
get: async () => null,
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
await mediaService.delete(media.id, mockStorage as never);
|
|
582
|
+
expect(deletedKeys).toContain("media/2025/01/vid.mp4");
|
|
583
|
+
expect(deletedKeys).toContain("media/2025/01/vid-poster.webp");
|
|
584
|
+
});
|
|
472
585
|
});
|
|
473
586
|
|
|
474
587
|
describe("deleteByIds", () => {
|
|
@@ -500,5 +613,32 @@ describe("MediaService", () => {
|
|
|
500
613
|
|
|
501
614
|
expect(await mediaService.getById(m1.id)).not.toBeNull();
|
|
502
615
|
});
|
|
616
|
+
|
|
617
|
+
it("deletes poster keys from storage", async () => {
|
|
618
|
+
const m1 = await mediaService.create({
|
|
619
|
+
...sampleMedia,
|
|
620
|
+
storageKey: "media/a.mp4",
|
|
621
|
+
posterKey: "media/a-poster.webp",
|
|
622
|
+
});
|
|
623
|
+
const m2 = await mediaService.create({
|
|
624
|
+
...sampleMedia,
|
|
625
|
+
storageKey: "media/b.jpg",
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const deletedKeys: string[] = [];
|
|
629
|
+
const mockStorage = {
|
|
630
|
+
delete: async (key: string) => {
|
|
631
|
+
deletedKeys.push(key);
|
|
632
|
+
},
|
|
633
|
+
put: async () => {},
|
|
634
|
+
get: async () => null,
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
await mediaService.deleteByIds([m1.id, m2.id], mockStorage as never);
|
|
638
|
+
expect(deletedKeys).toContain("media/a.mp4");
|
|
639
|
+
expect(deletedKeys).toContain("media/a-poster.webp");
|
|
640
|
+
expect(deletedKeys).toContain("media/b.jpg");
|
|
641
|
+
expect(deletedKeys).toHaveLength(3);
|
|
642
|
+
});
|
|
503
643
|
});
|
|
504
644
|
});
|