@jant/core 0.3.36 → 0.3.38
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/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4012 -3276
- package/dist/index.js +10285 -5809
- package/package.json +11 -3
- package/src/__tests__/helpers/app.ts +9 -9
- package/src/__tests__/helpers/db.ts +91 -93
- package/src/app.tsx +157 -27
- 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/client/avatar-upload.ts +3 -2
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
- package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
- package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +7 -9
- package/src/client/components/compose-types.ts +101 -4
- package/src/client/components/jant-collection-form.ts +43 -7
- package/src/client/components/jant-collection-sidebar.ts +88 -84
- package/src/client/components/jant-compose-dialog.ts +1655 -219
- package/src/client/components/jant-compose-editor.ts +732 -168
- package/src/client/components/jant-compose-fullscreen.ts +23 -78
- package/src/client/components/jant-media-lightbox.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +24 -284
- package/src/client/components/jant-post-form.ts +89 -9
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/client/components/jant-settings-avatar.ts +3 -3
- package/src/client/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/client/components/nav-manager-types.ts +4 -19
- package/src/client/components/post-form-template.ts +107 -12
- package/src/client/components/post-form-types.ts +11 -4
- package/src/client/compose-bridge.ts +410 -109
- package/src/client/image-processor.ts +26 -8
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/client/post-form-bridge.ts +52 -1
- package/src/client/settings-bridge.ts +0 -12
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/create-editor.ts +46 -0
- package/src/client/tiptap/extensions.ts +5 -0
- package/src/client/tiptap/image-node.ts +2 -8
- package/src/client/tiptap/paste-image.ts +2 -13
- package/src/client/tiptap/slash-commands.ts +173 -63
- package/src/client/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +15 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +5 -2
- 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 -145
- 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 +487 -1217
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +613 -996
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +624 -1007
- 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__/schemas.test.ts +181 -63
- 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__/view.test.ts +141 -66
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +885 -68
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +78 -32
- 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 +12 -3
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +20 -2
- package/src/lib/resolve-config.ts +6 -2
- package/src/lib/schemas.ts +224 -55
- 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 +66 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +74 -34
- package/src/lib/tiptap-render.ts +5 -10
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +190 -29
- package/src/lib/url.ts +31 -0
- package/src/lib/view.ts +204 -37
- 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/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +45 -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 +51 -42
- 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 +43 -39
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +85 -19
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/setup.tsx +26 -33
- package/src/routes/auth/signin.tsx +3 -7
- package/src/routes/compose.tsx +10 -55
- package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +304 -232
- package/src/routes/feed/__tests__/rss.test.ts +27 -28
- package/src/routes/feed/rss.ts +6 -4
- 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 +41 -22
- package/src/routes/pages/archive.tsx +175 -39
- package/src/routes/pages/collection.tsx +22 -10
- package/src/routes/pages/collections.tsx +3 -3
- package/src/routes/pages/featured.tsx +28 -4
- package/src/routes/pages/home.tsx +16 -15
- 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 +713 -234
- package/src/services/__tests__/search.test.ts +67 -10
- 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 +687 -154
- package/src/services/search.ts +160 -75
- package/src/styles/components.css +58 -7
- package/src/styles/tokens.css +84 -6
- package/src/styles/ui.css +2964 -457
- package/src/types/bindings.ts +4 -1
- package/src/types/config.ts +12 -4
- package/src/types/constants.ts +15 -3
- package/src/types/entities.ts +74 -35
- package/src/types/operations.ts +11 -24
- package/src/types/props.ts +51 -16
- package/src/types/views.ts +45 -22
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +239 -17
- 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 +3 -1
- package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
- package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
- package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
- package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +3 -57
- 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 +8 -0
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
- 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 +105 -92
- package/src/ui/pages/ArchivePage.tsx +923 -98
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +181 -37
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +47 -37
- package/src/ui/shared/MediaGallery.tsx +445 -149
- 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/dist/client/assets/url-8Dj-5CLW.js +0 -1
- package/src/client/media-upload.ts +0 -161
- package/src/client/page-slug-bridge.ts +0 -42
- 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/0012_add_tiptap_columns.sql +0 -2
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- 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/index.tsx +0 -109
- package/src/routes/dash/media.tsx +0 -135
- package/src/routes/dash/pages.tsx +0 -245
- package/src/routes/dash/posts.tsx +0 -338
- package/src/routes/dash/redirects.tsx +0 -263
- 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 -216
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/ui/dash/PageForm.tsx +0 -187
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/media/MediaListContent.tsx +0 -206
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -75
- package/src/ui/dash/posts/PostForm.tsx +0 -260
- package/src/ui/layouts/DashLayout.tsx +0 -247
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
RedirectTypeSchema,
|
|
6
6
|
CreatePostSchema,
|
|
7
7
|
UpdatePostSchema,
|
|
8
|
+
SetupSchema,
|
|
9
|
+
SigninSchema,
|
|
10
|
+
normalizeEmail,
|
|
8
11
|
parseFormData,
|
|
9
12
|
parseFormDataOptional,
|
|
10
13
|
validateMediaCount,
|
|
@@ -52,6 +55,46 @@ describe("RedirectTypeSchema", () => {
|
|
|
52
55
|
});
|
|
53
56
|
});
|
|
54
57
|
|
|
58
|
+
describe("normalizeEmail", () => {
|
|
59
|
+
it("trims and lowercases email addresses", () => {
|
|
60
|
+
expect(normalizeEmail(" User.Name+tag@Example.COM ")).toBe(
|
|
61
|
+
"user.name+tag@example.com",
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("SetupSchema", () => {
|
|
67
|
+
it("normalizes email before returning parsed data", () => {
|
|
68
|
+
const result = SetupSchema.parse({
|
|
69
|
+
siteName: "Jant",
|
|
70
|
+
email: " Admin@Example.COM ",
|
|
71
|
+
password: "password123",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(result.email).toBe("admin@example.com");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("SigninSchema", () => {
|
|
79
|
+
it("normalizes email before returning parsed data", () => {
|
|
80
|
+
const result = SigninSchema.parse({
|
|
81
|
+
email: " Admin@Example.COM ",
|
|
82
|
+
password: "password123",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(result.email).toBe("admin@example.com");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("rejects invalid email after normalization", () => {
|
|
89
|
+
expect(() =>
|
|
90
|
+
SigninSchema.parse({
|
|
91
|
+
email: " not-an-email ",
|
|
92
|
+
password: "password123",
|
|
93
|
+
}),
|
|
94
|
+
).toThrow();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
55
98
|
describe("CreatePostSchema", () => {
|
|
56
99
|
const validPost = {
|
|
57
100
|
format: "note",
|
|
@@ -67,11 +110,23 @@ describe("CreatePostSchema", () => {
|
|
|
67
110
|
});
|
|
68
111
|
|
|
69
112
|
it("accepts all formats", () => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
expect(() =>
|
|
114
|
+
CreatePostSchema.parse({ ...validPost, format: "note" }),
|
|
115
|
+
).not.toThrow();
|
|
116
|
+
expect(() =>
|
|
117
|
+
CreatePostSchema.parse({
|
|
118
|
+
...validPost,
|
|
119
|
+
format: "link",
|
|
120
|
+
url: "https://example.com",
|
|
121
|
+
}),
|
|
122
|
+
).not.toThrow();
|
|
123
|
+
expect(() =>
|
|
124
|
+
CreatePostSchema.parse({
|
|
125
|
+
...validPost,
|
|
126
|
+
format: "quote",
|
|
127
|
+
quoteText: "A wise person once said...",
|
|
128
|
+
}),
|
|
129
|
+
).not.toThrow();
|
|
75
130
|
});
|
|
76
131
|
|
|
77
132
|
it("accepts optional title", () => {
|
|
@@ -82,88 +137,68 @@ describe("CreatePostSchema", () => {
|
|
|
82
137
|
expect(result.title).toBe("My Post");
|
|
83
138
|
});
|
|
84
139
|
|
|
85
|
-
it("accepts valid
|
|
140
|
+
it("accepts valid slug format", () => {
|
|
86
141
|
const result = CreatePostSchema.parse({
|
|
87
142
|
...validPost,
|
|
88
|
-
|
|
143
|
+
slug: "my-post-slug",
|
|
89
144
|
});
|
|
90
|
-
expect(result.
|
|
145
|
+
expect(result.slug).toBe("my-post-slug");
|
|
91
146
|
});
|
|
92
147
|
|
|
93
|
-
it("accepts single-character
|
|
148
|
+
it("accepts single-character slug", () => {
|
|
94
149
|
const result = CreatePostSchema.parse({
|
|
95
150
|
...validPost,
|
|
96
|
-
|
|
151
|
+
slug: "a",
|
|
97
152
|
});
|
|
98
|
-
expect(result.
|
|
153
|
+
expect(result.slug).toBe("a");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("accepts empty slug (transforms to undefined)", () => {
|
|
157
|
+
const result = CreatePostSchema.parse({ ...validPost, slug: "" });
|
|
158
|
+
expect(result.slug).toBeUndefined();
|
|
99
159
|
});
|
|
100
160
|
|
|
101
|
-
it("
|
|
102
|
-
const result = CreatePostSchema.parse({ ...validPost,
|
|
103
|
-
expect(result.
|
|
161
|
+
it("normalizes uppercase slug", () => {
|
|
162
|
+
const result = CreatePostSchema.parse({ ...validPost, slug: "MyPost" });
|
|
163
|
+
expect(result.slug).toBe("mypost");
|
|
104
164
|
});
|
|
105
165
|
|
|
106
|
-
it("
|
|
166
|
+
it("normalizes special chars in slug", () => {
|
|
107
167
|
const result = CreatePostSchema.parse({
|
|
108
168
|
...validPost,
|
|
109
|
-
|
|
169
|
+
slug: "my post!",
|
|
110
170
|
});
|
|
111
|
-
expect(result.
|
|
171
|
+
expect(result.slug).toBe("my-post");
|
|
112
172
|
});
|
|
113
173
|
|
|
114
|
-
it("
|
|
174
|
+
it("normalizes leading hyphen in slug", () => {
|
|
115
175
|
const result = CreatePostSchema.parse({
|
|
116
176
|
...validPost,
|
|
117
|
-
|
|
177
|
+
slug: "-my-post",
|
|
118
178
|
});
|
|
119
|
-
expect(result.
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("rejects invalid path format (uppercase)", () => {
|
|
123
|
-
expect(() =>
|
|
124
|
-
CreatePostSchema.parse({ ...validPost, path: "MyPost" }),
|
|
125
|
-
).toThrow();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("rejects invalid path format (special chars)", () => {
|
|
129
|
-
expect(() =>
|
|
130
|
-
CreatePostSchema.parse({ ...validPost, path: "my post!" }),
|
|
131
|
-
).toThrow();
|
|
179
|
+
expect(result.slug).toBe("my-post");
|
|
132
180
|
});
|
|
133
181
|
|
|
134
|
-
it("
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
it("rejects path ending with hyphen", () => {
|
|
141
|
-
expect(() =>
|
|
142
|
-
CreatePostSchema.parse({ ...validPost, path: "my-post-" }),
|
|
143
|
-
).toThrow();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("rejects path with leading slash", () => {
|
|
147
|
-
expect(() =>
|
|
148
|
-
CreatePostSchema.parse({ ...validPost, path: "/my-post" }),
|
|
149
|
-
).toThrow();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("rejects path with trailing slash", () => {
|
|
153
|
-
expect(() =>
|
|
154
|
-
CreatePostSchema.parse({ ...validPost, path: "my-post/" }),
|
|
155
|
-
).toThrow();
|
|
182
|
+
it("normalizes trailing hyphen in slug", () => {
|
|
183
|
+
const result = CreatePostSchema.parse({
|
|
184
|
+
...validPost,
|
|
185
|
+
slug: "my-post-",
|
|
186
|
+
});
|
|
187
|
+
expect(result.slug).toBe("my-post");
|
|
156
188
|
});
|
|
157
189
|
|
|
158
|
-
it("
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
190
|
+
it("normalizes slashes in slug", () => {
|
|
191
|
+
const result = CreatePostSchema.parse({
|
|
192
|
+
...validPost,
|
|
193
|
+
slug: "my/post",
|
|
194
|
+
});
|
|
195
|
+
expect(result.slug).toBe("my-post");
|
|
162
196
|
});
|
|
163
197
|
|
|
164
198
|
it("accepts valid url", () => {
|
|
165
199
|
const result = CreatePostSchema.parse({
|
|
166
200
|
...validPost,
|
|
201
|
+
format: "link",
|
|
167
202
|
url: "https://example.com",
|
|
168
203
|
});
|
|
169
204
|
expect(result.url).toBe("https://example.com");
|
|
@@ -232,12 +267,23 @@ describe("CreatePostSchema", () => {
|
|
|
232
267
|
});
|
|
233
268
|
|
|
234
269
|
it("accepts visibility values", () => {
|
|
235
|
-
for (const v of ["
|
|
270
|
+
for (const v of ["public", "unlisted", "private"]) {
|
|
236
271
|
const result = CreatePostSchema.parse({ ...validPost, visibility: v });
|
|
237
272
|
expect(result.visibility).toBe(v);
|
|
238
273
|
}
|
|
239
274
|
});
|
|
240
275
|
|
|
276
|
+
it("accepts featured as boolean", () => {
|
|
277
|
+
const result = CreatePostSchema.parse({ ...validPost, featured: true });
|
|
278
|
+
expect(result.featured).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("rejects featured as non-boolean (other than 'on')", () => {
|
|
282
|
+
expect(() =>
|
|
283
|
+
CreatePostSchema.parse({ ...validPost, featured: "invalid" }),
|
|
284
|
+
).toThrow();
|
|
285
|
+
});
|
|
286
|
+
|
|
241
287
|
it("rejects invalid visibility", () => {
|
|
242
288
|
expect(() =>
|
|
243
289
|
CreatePostSchema.parse({ ...validPost, visibility: "hidden" }),
|
|
@@ -254,14 +300,62 @@ describe("CreatePostSchema", () => {
|
|
|
254
300
|
expect(result.pinned).toBe(true);
|
|
255
301
|
});
|
|
256
302
|
|
|
257
|
-
it("accepts optional quoteText", () => {
|
|
303
|
+
it("accepts optional quoteText for quote posts", () => {
|
|
258
304
|
const result = CreatePostSchema.parse({
|
|
259
305
|
...validPost,
|
|
306
|
+
format: "quote",
|
|
260
307
|
quoteText: "A wise person once said...",
|
|
261
308
|
});
|
|
262
309
|
expect(result.quoteText).toBe("A wise person once said...");
|
|
263
310
|
});
|
|
264
311
|
|
|
312
|
+
it("rejects note posts with a URL", () => {
|
|
313
|
+
expect(() =>
|
|
314
|
+
CreatePostSchema.parse({
|
|
315
|
+
...validPost,
|
|
316
|
+
url: "https://example.com",
|
|
317
|
+
}),
|
|
318
|
+
).toThrow("Notes can't include a URL.");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("rejects note posts with quoted text", () => {
|
|
322
|
+
expect(() =>
|
|
323
|
+
CreatePostSchema.parse({
|
|
324
|
+
...validPost,
|
|
325
|
+
quoteText: "A wise person once said...",
|
|
326
|
+
}),
|
|
327
|
+
).toThrow("Notes can't include quoted text.");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("rejects link posts without a URL", () => {
|
|
331
|
+
expect(() =>
|
|
332
|
+
CreatePostSchema.parse({
|
|
333
|
+
...validPost,
|
|
334
|
+
format: "link",
|
|
335
|
+
}),
|
|
336
|
+
).toThrow("Link posts need a URL.");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("rejects link posts with quoted text", () => {
|
|
340
|
+
expect(() =>
|
|
341
|
+
CreatePostSchema.parse({
|
|
342
|
+
...validPost,
|
|
343
|
+
format: "link",
|
|
344
|
+
url: "https://example.com",
|
|
345
|
+
quoteText: "A wise person once said...",
|
|
346
|
+
}),
|
|
347
|
+
).toThrow("Link posts can't include quoted text.");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("rejects quote posts without quoted text", () => {
|
|
351
|
+
expect(() =>
|
|
352
|
+
CreatePostSchema.parse({
|
|
353
|
+
...validPost,
|
|
354
|
+
format: "quote",
|
|
355
|
+
}),
|
|
356
|
+
).toThrow("Quote posts need quoted text.");
|
|
357
|
+
});
|
|
358
|
+
|
|
265
359
|
it("accepts optional rating (1-5)", () => {
|
|
266
360
|
for (const rating of [1, 2, 3, 4, 5]) {
|
|
267
361
|
const result = CreatePostSchema.parse({ ...validPost, rating });
|
|
@@ -286,12 +380,18 @@ describe("CreatePostSchema", () => {
|
|
|
286
380
|
expect(result.rating).toBeUndefined();
|
|
287
381
|
});
|
|
288
382
|
|
|
289
|
-
it("accepts optional collectionIds as array of
|
|
383
|
+
it("accepts optional collectionIds as array of non-empty strings", () => {
|
|
290
384
|
const result = CreatePostSchema.parse({
|
|
291
385
|
...validPost,
|
|
292
|
-
collectionIds: [1, 2, 3],
|
|
386
|
+
collectionIds: ["col-1", "col-2", "col-3"],
|
|
293
387
|
});
|
|
294
|
-
expect(result.collectionIds).toEqual([1, 2, 3]);
|
|
388
|
+
expect(result.collectionIds).toEqual(["col-1", "col-2", "col-3"]);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("rejects collectionIds with empty strings", () => {
|
|
392
|
+
expect(() =>
|
|
393
|
+
CreatePostSchema.parse({ ...validPost, collectionIds: [""] }),
|
|
394
|
+
).toThrow();
|
|
295
395
|
});
|
|
296
396
|
|
|
297
397
|
it("accepts empty string collectionIds (transforms to undefined)", () => {
|
|
@@ -319,6 +419,24 @@ describe("CreatePostSchema", () => {
|
|
|
319
419
|
expect(() => CreatePostSchema.parse({})).toThrow();
|
|
320
420
|
expect(() => CreatePostSchema.parse({ body: "hello" })).toThrow();
|
|
321
421
|
});
|
|
422
|
+
|
|
423
|
+
it("accepts bodyMarkdown", () => {
|
|
424
|
+
const result = CreatePostSchema.parse({
|
|
425
|
+
format: "note",
|
|
426
|
+
bodyMarkdown: "Hello **world**",
|
|
427
|
+
});
|
|
428
|
+
expect(result.bodyMarkdown).toBe("Hello **world**");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("rejects both body and bodyMarkdown", () => {
|
|
432
|
+
expect(() =>
|
|
433
|
+
CreatePostSchema.parse({
|
|
434
|
+
format: "note",
|
|
435
|
+
body: '{"type":"doc","content":[]}',
|
|
436
|
+
bodyMarkdown: "Hello",
|
|
437
|
+
}),
|
|
438
|
+
).toThrow("Provide either body or bodyMarkdown, not both");
|
|
439
|
+
});
|
|
322
440
|
});
|
|
323
441
|
|
|
324
442
|
describe("UpdatePostSchema", () => {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { generatePostSlug } from "../slug.js";
|
|
3
|
+
|
|
4
|
+
/** Helper: always-available check */
|
|
5
|
+
const alwaysAvailable = async () => true;
|
|
6
|
+
|
|
7
|
+
/** Helper: never-available check */
|
|
8
|
+
const neverAvailable = async () => false;
|
|
9
|
+
|
|
10
|
+
/** Helper: available after N calls */
|
|
11
|
+
function availableAfter(n: number) {
|
|
12
|
+
let calls = 0;
|
|
13
|
+
return async () => {
|
|
14
|
+
calls++;
|
|
15
|
+
return calls > n;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("generatePostSlug", () => {
|
|
20
|
+
describe("user-provided slug", () => {
|
|
21
|
+
it("returns user slug when available", async () => {
|
|
22
|
+
const slug = await generatePostSlug({
|
|
23
|
+
slug: "my-post",
|
|
24
|
+
idLength: 5,
|
|
25
|
+
isAvailable: alwaysAvailable,
|
|
26
|
+
});
|
|
27
|
+
expect(slug).toBe("my-post");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("throws ConflictError when slug is taken", async () => {
|
|
31
|
+
await expect(
|
|
32
|
+
generatePostSlug({
|
|
33
|
+
slug: "taken-slug",
|
|
34
|
+
idLength: 5,
|
|
35
|
+
isAvailable: neverAvailable,
|
|
36
|
+
}),
|
|
37
|
+
).rejects.toThrow("already in use");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("throws ValidationError for reserved slug", async () => {
|
|
41
|
+
await expect(
|
|
42
|
+
generatePostSlug({
|
|
43
|
+
slug: "dash",
|
|
44
|
+
idLength: 5,
|
|
45
|
+
isAvailable: alwaysAvailable,
|
|
46
|
+
}),
|
|
47
|
+
).rejects.toThrow("reserved");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("prioritizes user slug over title", async () => {
|
|
51
|
+
const slug = await generatePostSlug({
|
|
52
|
+
slug: "custom-slug",
|
|
53
|
+
title: "My Title",
|
|
54
|
+
idLength: 5,
|
|
55
|
+
isAvailable: alwaysAvailable,
|
|
56
|
+
});
|
|
57
|
+
expect(slug).toBe("custom-slug");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("title-based slug", () => {
|
|
62
|
+
it("generates slug from title", async () => {
|
|
63
|
+
const slug = await generatePostSlug({
|
|
64
|
+
title: "Hello World",
|
|
65
|
+
idLength: 5,
|
|
66
|
+
isAvailable: alwaysAvailable,
|
|
67
|
+
});
|
|
68
|
+
expect(slug).toBe("hello-world");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("appends random suffix on conflict", async () => {
|
|
72
|
+
const slug = await generatePostSlug({
|
|
73
|
+
title: "Hello World",
|
|
74
|
+
idLength: 5,
|
|
75
|
+
isAvailable: availableAfter(1), // first call (base) fails, second succeeds
|
|
76
|
+
});
|
|
77
|
+
expect(slug).toMatch(/^hello-world-[a-z0-9]{5}$/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("retries with different random suffixes", async () => {
|
|
81
|
+
const slug = await generatePostSlug({
|
|
82
|
+
title: "Test Post",
|
|
83
|
+
idLength: 5,
|
|
84
|
+
isAvailable: availableAfter(3),
|
|
85
|
+
});
|
|
86
|
+
expect(slug).toMatch(/^test-post-[a-z0-9]{5}$/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("throws after exceeding max retries", async () => {
|
|
90
|
+
await expect(
|
|
91
|
+
generatePostSlug({
|
|
92
|
+
title: "Test Post",
|
|
93
|
+
idLength: 5,
|
|
94
|
+
isAvailable: neverAvailable,
|
|
95
|
+
}),
|
|
96
|
+
).rejects.toThrow("Could not generate a unique slug");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("random slug (no title, no slug)", () => {
|
|
101
|
+
it("generates random ID of specified length", async () => {
|
|
102
|
+
const slug = await generatePostSlug({
|
|
103
|
+
idLength: 5,
|
|
104
|
+
isAvailable: alwaysAvailable,
|
|
105
|
+
});
|
|
106
|
+
expect(slug).toMatch(/^[a-z0-9]{5}$/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("retries on conflict", async () => {
|
|
110
|
+
const slug = await generatePostSlug({
|
|
111
|
+
idLength: 8,
|
|
112
|
+
isAvailable: availableAfter(2),
|
|
113
|
+
});
|
|
114
|
+
expect(slug).toMatch(/^[a-z0-9]{8}$/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("throws after exceeding max retries", async () => {
|
|
118
|
+
await expect(
|
|
119
|
+
generatePostSlug({
|
|
120
|
+
idLength: 5,
|
|
121
|
+
isAvailable: neverAvailable,
|
|
122
|
+
}),
|
|
123
|
+
).rejects.toThrow("Could not generate a unique slug");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -3,20 +3,20 @@ import { dsRedirect, dsToast, dsSignals } from "../sse.js";
|
|
|
3
3
|
|
|
4
4
|
describe("dsRedirect", () => {
|
|
5
5
|
it("returns a Response with text/html content-type", () => {
|
|
6
|
-
const res = dsRedirect("/
|
|
6
|
+
const res = dsRedirect("/settings");
|
|
7
7
|
expect(res.headers.get("Content-Type")).toBe("text/html");
|
|
8
8
|
});
|
|
9
9
|
|
|
10
10
|
it("includes Datastar headers for append mode", () => {
|
|
11
|
-
const res = dsRedirect("/
|
|
11
|
+
const res = dsRedirect("/settings");
|
|
12
12
|
expect(res.headers.get("Datastar-Mode")).toBe("append");
|
|
13
13
|
expect(res.headers.get("Datastar-Selector")).toBe("body");
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
it("body contains redirect script with correct URL", async () => {
|
|
17
|
-
const res = dsRedirect("/
|
|
17
|
+
const res = dsRedirect("/settings/general");
|
|
18
18
|
const body = await res.text();
|
|
19
|
-
expect(body).toContain("window.location.href='/
|
|
19
|
+
expect(body).toContain("window.location.href='/settings/general'");
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it("escapes single quotes in URL", async () => {
|
|
@@ -26,7 +26,7 @@ describe("dsRedirect", () => {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
it("merges additional headers from plain object", () => {
|
|
29
|
-
const res = dsRedirect("/
|
|
29
|
+
const res = dsRedirect("/settings", {
|
|
30
30
|
headers: { "Set-Cookie": "session=abc" },
|
|
31
31
|
});
|
|
32
32
|
expect(res.headers.get("Set-Cookie")).toBe("session=abc");
|
|
@@ -37,7 +37,7 @@ describe("dsRedirect", () => {
|
|
|
37
37
|
const headers = new Headers();
|
|
38
38
|
headers.append("set-cookie", "session=abc; Path=/; HttpOnly");
|
|
39
39
|
headers.append("set-cookie", "data=xyz; Path=/; Max-Age=300");
|
|
40
|
-
const res = dsRedirect("/
|
|
40
|
+
const res = dsRedirect("/settings", { headers });
|
|
41
41
|
const cookies = res.headers.getSetCookie();
|
|
42
42
|
expect(cookies).toHaveLength(2);
|
|
43
43
|
expect(cookies[0]).toBe("session=abc; Path=/; HttpOnly");
|