@jant/core 0.3.36 → 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/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
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { encode, decode, isValidSqid } from "../sqid.js";
|
|
3
|
-
|
|
4
|
-
describe("encode", () => {
|
|
5
|
-
it("encodes a numeric ID to a string", () => {
|
|
6
|
-
const result = encode(1);
|
|
7
|
-
expect(typeof result).toBe("string");
|
|
8
|
-
expect(result.length).toBeGreaterThanOrEqual(4);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("produces minimum 4-character strings", () => {
|
|
12
|
-
expect(encode(0).length).toBeGreaterThanOrEqual(4);
|
|
13
|
-
expect(encode(1).length).toBeGreaterThanOrEqual(4);
|
|
14
|
-
expect(encode(100).length).toBeGreaterThanOrEqual(4);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("produces different strings for different IDs", () => {
|
|
18
|
-
const a = encode(1);
|
|
19
|
-
const b = encode(2);
|
|
20
|
-
const c = encode(100);
|
|
21
|
-
expect(a).not.toBe(b);
|
|
22
|
-
expect(b).not.toBe(c);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("produces consistent results for the same ID", () => {
|
|
26
|
-
expect(encode(42)).toBe(encode(42));
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("decode", () => {
|
|
31
|
-
it("decodes an encoded string back to the original ID", () => {
|
|
32
|
-
for (const id of [0, 1, 42, 100, 999, 10000]) {
|
|
33
|
-
const encoded = encode(id);
|
|
34
|
-
expect(decode(encoded)).toBe(id);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("returns null for empty string", () => {
|
|
39
|
-
expect(decode("")).toBe(null);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("handles round-trip encoding", () => {
|
|
43
|
-
const original = 12345;
|
|
44
|
-
const sqid = encode(original);
|
|
45
|
-
const decoded = decode(sqid);
|
|
46
|
-
expect(decoded).toBe(original);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("isValidSqid", () => {
|
|
51
|
-
it("returns true for valid encoded sqids", () => {
|
|
52
|
-
const sqid = encode(1);
|
|
53
|
-
expect(isValidSqid(sqid)).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("returns true for various valid sqids", () => {
|
|
57
|
-
for (const id of [0, 1, 100, 999]) {
|
|
58
|
-
expect(isValidSqid(encode(id))).toBe(true);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("returns false for empty string", () => {
|
|
63
|
-
expect(isValidSqid("")).toBe(false);
|
|
64
|
-
});
|
|
65
|
-
});
|
package/src/lib/sqid.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sqids - Short unique IDs for URLs
|
|
3
|
-
*
|
|
4
|
-
* Encodes numeric IDs to short strings like "jR3k"
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import Sqids from "sqids";
|
|
8
|
-
|
|
9
|
-
const sqids = new Sqids({
|
|
10
|
-
minLength: 4,
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Encodes a numeric database ID to a short, URL-friendly string.
|
|
15
|
-
*
|
|
16
|
-
* Uses the Sqids library to generate short unique identifiers with a minimum length of 4 characters.
|
|
17
|
-
* These are used in URLs (e.g., `/p/jR3k`) to obscure sequential integer IDs while maintaining
|
|
18
|
-
* uniqueness and reversibility.
|
|
19
|
-
*
|
|
20
|
-
* @param id - The numeric database ID to encode
|
|
21
|
-
* @returns A short string representation of the ID (minimum 4 characters)
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```ts
|
|
25
|
-
* const sqid = encode(123);
|
|
26
|
-
* // Returns: "jR3k" (or similar short string)
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
export function encode(id: number): string {
|
|
30
|
-
return sqids.encode([id]);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Decodes a sqid string back to the original numeric database ID.
|
|
35
|
-
*
|
|
36
|
-
* Attempts to decode a sqid string generated by the `encode()` function. Returns the original
|
|
37
|
-
* numeric ID if valid, or `null` if the string is not a valid sqid. This is used to extract
|
|
38
|
-
* database IDs from URL parameters.
|
|
39
|
-
*
|
|
40
|
-
* @param str - The sqid string to decode
|
|
41
|
-
* @returns The original numeric ID if valid, or `null` if decoding fails
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* ```ts
|
|
45
|
-
* const id = decode("jR3k");
|
|
46
|
-
* // Returns: 123
|
|
47
|
-
*
|
|
48
|
-
* const invalid = decode("invalid");
|
|
49
|
-
* // Returns: null
|
|
50
|
-
* ```
|
|
51
|
-
*/
|
|
52
|
-
export function decode(str: string): number | null {
|
|
53
|
-
try {
|
|
54
|
-
const ids = sqids.decode(str);
|
|
55
|
-
return ids[0] ?? null;
|
|
56
|
-
} catch {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Checks if a string is a valid sqid that can be decoded.
|
|
63
|
-
*
|
|
64
|
-
* Validates whether a string can be successfully decoded to a numeric ID.
|
|
65
|
-
* Useful for route validation and input sanitization.
|
|
66
|
-
*
|
|
67
|
-
* @param str - The string to validate
|
|
68
|
-
* @returns `true` if the string is a valid sqid, `false` otherwise
|
|
69
|
-
*
|
|
70
|
-
* @example
|
|
71
|
-
* ```ts
|
|
72
|
-
* if (isValidSqid("jR3k")) {
|
|
73
|
-
* // Process the valid sqid
|
|
74
|
-
* }
|
|
75
|
-
* ```
|
|
76
|
-
*/
|
|
77
|
-
export function isValidSqid(str: string): boolean {
|
|
78
|
-
return decode(str) !== null;
|
|
79
|
-
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
3
|
-
import { pagesApiRoutes } from "../pages.js";
|
|
4
|
-
|
|
5
|
-
describe("Pages API Routes", () => {
|
|
6
|
-
describe("GET /api/pages", () => {
|
|
7
|
-
it("returns empty list when no pages exist", async () => {
|
|
8
|
-
const { app } = createTestApp();
|
|
9
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
10
|
-
|
|
11
|
-
const res = await app.request("/api/pages");
|
|
12
|
-
expect(res.status).toBe(200);
|
|
13
|
-
|
|
14
|
-
const body = await res.json();
|
|
15
|
-
expect(body.pages).toEqual([]);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("returns pages list", async () => {
|
|
19
|
-
const { app, services } = createTestApp();
|
|
20
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
21
|
-
|
|
22
|
-
await services.pages.create({ slug: "about", title: "About" });
|
|
23
|
-
await services.pages.create({ slug: "contact", title: "Contact" });
|
|
24
|
-
|
|
25
|
-
const res = await app.request("/api/pages");
|
|
26
|
-
const body = await res.json();
|
|
27
|
-
|
|
28
|
-
expect(body.pages).toHaveLength(2);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("GET /api/pages/:id", () => {
|
|
33
|
-
it("returns a page by id", async () => {
|
|
34
|
-
const { app, services } = createTestApp();
|
|
35
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
36
|
-
|
|
37
|
-
const page = await services.pages.create({
|
|
38
|
-
slug: "about",
|
|
39
|
-
title: "About Us",
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const res = await app.request(`/api/pages/${page.id}`);
|
|
43
|
-
expect(res.status).toBe(200);
|
|
44
|
-
|
|
45
|
-
const body = await res.json();
|
|
46
|
-
expect(body.title).toBe("About Us");
|
|
47
|
-
expect(body.slug).toBe("about");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("returns 400 for invalid id", async () => {
|
|
51
|
-
const { app } = createTestApp();
|
|
52
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
53
|
-
|
|
54
|
-
const res = await app.request("/api/pages/abc");
|
|
55
|
-
expect(res.status).toBe(400);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("returns 404 for non-existent page", async () => {
|
|
59
|
-
const { app } = createTestApp();
|
|
60
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
61
|
-
|
|
62
|
-
const res = await app.request("/api/pages/9999");
|
|
63
|
-
expect(res.status).toBe(404);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe("POST /api/pages", () => {
|
|
68
|
-
it("returns 401 when not authenticated", async () => {
|
|
69
|
-
const { app } = createTestApp({ authenticated: false });
|
|
70
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
71
|
-
|
|
72
|
-
const res = await app.request("/api/pages", {
|
|
73
|
-
method: "POST",
|
|
74
|
-
headers: { "Content-Type": "application/json" },
|
|
75
|
-
body: JSON.stringify({ slug: "about", title: "About" }),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
expect(res.status).toBe(401);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("creates a page when authenticated", async () => {
|
|
82
|
-
const { app } = createTestApp({ authenticated: true });
|
|
83
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
84
|
-
|
|
85
|
-
const res = await app.request("/api/pages", {
|
|
86
|
-
method: "POST",
|
|
87
|
-
headers: { "Content-Type": "application/json" },
|
|
88
|
-
body: JSON.stringify({
|
|
89
|
-
slug: "about",
|
|
90
|
-
title: "About Us",
|
|
91
|
-
body: "We are Jant.",
|
|
92
|
-
status: "published",
|
|
93
|
-
}),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
expect(res.status).toBe(201);
|
|
97
|
-
const body = await res.json();
|
|
98
|
-
expect(body.slug).toBe("about");
|
|
99
|
-
expect(body.title).toBe("About Us");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("returns 400 for missing slug", async () => {
|
|
103
|
-
const { app } = createTestApp({ authenticated: true });
|
|
104
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
105
|
-
|
|
106
|
-
const res = await app.request("/api/pages", {
|
|
107
|
-
method: "POST",
|
|
108
|
-
headers: { "Content-Type": "application/json" },
|
|
109
|
-
body: JSON.stringify({ title: "No Slug" }),
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
expect(res.status).toBe(400);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe("PUT /api/pages/:id", () => {
|
|
117
|
-
it("returns 401 when not authenticated", async () => {
|
|
118
|
-
const { app, services } = createTestApp({ authenticated: false });
|
|
119
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
120
|
-
|
|
121
|
-
const page = await services.pages.create({
|
|
122
|
-
slug: "about",
|
|
123
|
-
title: "About",
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const res = await app.request(`/api/pages/${page.id}`, {
|
|
127
|
-
method: "PUT",
|
|
128
|
-
headers: { "Content-Type": "application/json" },
|
|
129
|
-
body: JSON.stringify({ title: "Updated" }),
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
expect(res.status).toBe(401);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("updates a page when authenticated", async () => {
|
|
136
|
-
const { app, services } = createTestApp({ authenticated: true });
|
|
137
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
138
|
-
|
|
139
|
-
const page = await services.pages.create({
|
|
140
|
-
slug: "about",
|
|
141
|
-
title: "About",
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const res = await app.request(`/api/pages/${page.id}`, {
|
|
145
|
-
method: "PUT",
|
|
146
|
-
headers: { "Content-Type": "application/json" },
|
|
147
|
-
body: JSON.stringify({ title: "Updated About" }),
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
expect(res.status).toBe(200);
|
|
151
|
-
const body = await res.json();
|
|
152
|
-
expect(body.title).toBe("Updated About");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("returns 404 for non-existent page", async () => {
|
|
156
|
-
const { app } = createTestApp({ authenticated: true });
|
|
157
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
158
|
-
|
|
159
|
-
const res = await app.request("/api/pages/9999", {
|
|
160
|
-
method: "PUT",
|
|
161
|
-
headers: { "Content-Type": "application/json" },
|
|
162
|
-
body: JSON.stringify({ title: "test" }),
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
expect(res.status).toBe(404);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
describe("DELETE /api/pages/:id", () => {
|
|
170
|
-
it("returns 401 when not authenticated", async () => {
|
|
171
|
-
const { app, services } = createTestApp({ authenticated: false });
|
|
172
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
173
|
-
|
|
174
|
-
const page = await services.pages.create({
|
|
175
|
-
slug: "about",
|
|
176
|
-
title: "About",
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
const res = await app.request(`/api/pages/${page.id}`, {
|
|
180
|
-
method: "DELETE",
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
expect(res.status).toBe(401);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("deletes a page when authenticated", async () => {
|
|
187
|
-
const { app, services } = createTestApp({ authenticated: true });
|
|
188
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
189
|
-
|
|
190
|
-
const page = await services.pages.create({
|
|
191
|
-
slug: "about",
|
|
192
|
-
title: "About",
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const res = await app.request(`/api/pages/${page.id}`, {
|
|
196
|
-
method: "DELETE",
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
expect(res.status).toBe(200);
|
|
200
|
-
const body = await res.json();
|
|
201
|
-
expect(body.success).toBe(true);
|
|
202
|
-
|
|
203
|
-
const found = await services.pages.getById(page.id);
|
|
204
|
-
expect(found).toBeNull();
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it("returns 404 for non-existent page", async () => {
|
|
208
|
-
const { app } = createTestApp({ authenticated: true });
|
|
209
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
210
|
-
|
|
211
|
-
const res = await app.request("/api/pages/9999", {
|
|
212
|
-
method: "DELETE",
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
expect(res.status).toBe(404);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
});
|
package/src/routes/api/pages.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pages API Routes
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Hono } from "hono";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
import type { Bindings } from "../../types.js";
|
|
8
|
-
import type { AppVariables } from "../../types/app-context.js";
|
|
9
|
-
import { requireAuthApi } from "../../middleware/auth.js";
|
|
10
|
-
import {
|
|
11
|
-
CreatePageSchema,
|
|
12
|
-
StatusSchema,
|
|
13
|
-
parseValidated,
|
|
14
|
-
} from "../../lib/schemas.js";
|
|
15
|
-
import { assertFound, parseIntParam, NotFoundError } from "../../lib/errors.js";
|
|
16
|
-
|
|
17
|
-
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
18
|
-
|
|
19
|
-
export const pagesApiRoutes = new Hono<Env>();
|
|
20
|
-
|
|
21
|
-
// API update schema extends shared schema with nullable fields for explicit clearing
|
|
22
|
-
const UpdatePageSchema = CreatePageSchema.partial().extend({
|
|
23
|
-
title: z.string().nullable().optional(),
|
|
24
|
-
body: z.string().nullable().optional(),
|
|
25
|
-
status: StatusSchema.optional(),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// List pages
|
|
29
|
-
pagesApiRoutes.get("/", async (c) => {
|
|
30
|
-
const pages = await c.var.services.pages.list();
|
|
31
|
-
return c.json({ pages });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
// Get single page
|
|
35
|
-
pagesApiRoutes.get("/:id", async (c) => {
|
|
36
|
-
const id = parseIntParam(c.req.param("id"));
|
|
37
|
-
const page = assertFound(await c.var.services.pages.getById(id), "Page");
|
|
38
|
-
return c.json(page);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// Create page (requires auth)
|
|
42
|
-
pagesApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
43
|
-
const body = parseValidated(CreatePageSchema, await c.req.json());
|
|
44
|
-
|
|
45
|
-
const page = await c.var.services.pages.create({
|
|
46
|
-
slug: body.slug,
|
|
47
|
-
title: body.title,
|
|
48
|
-
body: body.body,
|
|
49
|
-
status: body.status,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
return c.json(page, 201);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Update page (requires auth)
|
|
56
|
-
pagesApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
57
|
-
const id = parseIntParam(c.req.param("id"));
|
|
58
|
-
const body = parseValidated(UpdatePageSchema, await c.req.json());
|
|
59
|
-
|
|
60
|
-
const page = assertFound(await c.var.services.pages.update(id, body), "Page");
|
|
61
|
-
|
|
62
|
-
return c.json(page);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// Delete page (requires auth)
|
|
66
|
-
pagesApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
|
|
67
|
-
const id = parseIntParam(c.req.param("id"));
|
|
68
|
-
|
|
69
|
-
const success = await c.var.services.pages.delete(id);
|
|
70
|
-
if (!success) throw new NotFoundError("Page");
|
|
71
|
-
|
|
72
|
-
return c.json({ success: true });
|
|
73
|
-
});
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the page/nav management logic used by dashboard pages routes.
|
|
3
|
-
*
|
|
4
|
-
* Note: Route handler tests that import JSX components with @lingui/react/macro
|
|
5
|
-
* cannot run in vitest (requires SWC plugin). These tests verify the service
|
|
6
|
-
* layer operations that the routes orchestrate.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
10
|
-
import { createTestDatabase } from "../../../__tests__/helpers/db.js";
|
|
11
|
-
import { createPageService } from "../../../services/page.js";
|
|
12
|
-
import { createNavItemService } from "../../../services/navigation.js";
|
|
13
|
-
import { createPathRegistryService } from "../../../services/path-registry.js";
|
|
14
|
-
import type { Database } from "../../../db/index.js";
|
|
15
|
-
|
|
16
|
-
describe("Dashboard Pages - Nav Management Logic", () => {
|
|
17
|
-
let db: Database;
|
|
18
|
-
let pageService: ReturnType<typeof createPageService>;
|
|
19
|
-
let navItemService: ReturnType<typeof createNavItemService>;
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
const testDb = createTestDatabase();
|
|
23
|
-
db = testDb.db as unknown as Database;
|
|
24
|
-
pageService = createPageService(db, createPathRegistryService(db));
|
|
25
|
-
navItemService = createNavItemService(db);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe("add page to nav", () => {
|
|
29
|
-
it("creates a page-type nav item for the page", async () => {
|
|
30
|
-
const page = await pageService.create({
|
|
31
|
-
slug: "about",
|
|
32
|
-
title: "About Us",
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// Simulate what the route handler does
|
|
36
|
-
await navItemService.create({
|
|
37
|
-
type: "page",
|
|
38
|
-
label: page.title || page.slug,
|
|
39
|
-
url: `/${page.slug}`,
|
|
40
|
-
pageId: page.id,
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const navItems = await navItemService.list();
|
|
44
|
-
expect(navItems).toHaveLength(1);
|
|
45
|
-
expect(navItems[0]?.type).toBe("page");
|
|
46
|
-
expect(navItems[0]?.label).toBe("About Us");
|
|
47
|
-
expect(navItems[0]?.url).toBe("/about");
|
|
48
|
-
expect(navItems[0]?.pageId).toBe(page.id);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("uses slug as label when page has no title", async () => {
|
|
52
|
-
const page = await pageService.create({ slug: "about" });
|
|
53
|
-
|
|
54
|
-
await navItemService.create({
|
|
55
|
-
type: "page",
|
|
56
|
-
label: page.title || page.slug,
|
|
57
|
-
url: `/${page.slug}`,
|
|
58
|
-
pageId: page.id,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const navItems = await navItemService.list();
|
|
62
|
-
expect(navItems[0]?.label).toBe("about");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("page appears in nav and not in listNotInNav after adding", async () => {
|
|
66
|
-
const page = await pageService.create({
|
|
67
|
-
slug: "about",
|
|
68
|
-
title: "About",
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// Before adding to nav
|
|
72
|
-
let notInNav = await pageService.listNotInNav();
|
|
73
|
-
expect(notInNav).toHaveLength(1);
|
|
74
|
-
|
|
75
|
-
// Add to nav
|
|
76
|
-
await navItemService.create({
|
|
77
|
-
type: "page",
|
|
78
|
-
label: page.title || page.slug,
|
|
79
|
-
url: `/${page.slug}`,
|
|
80
|
-
pageId: page.id,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// After adding to nav
|
|
84
|
-
notInNav = await pageService.listNotInNav();
|
|
85
|
-
expect(notInNav).toHaveLength(0);
|
|
86
|
-
|
|
87
|
-
const navItems = await navItemService.list();
|
|
88
|
-
expect(navItems).toHaveLength(1);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe("remove page from nav", () => {
|
|
93
|
-
it("removes the nav item but keeps the page", async () => {
|
|
94
|
-
const page = await pageService.create({
|
|
95
|
-
slug: "about",
|
|
96
|
-
title: "About",
|
|
97
|
-
});
|
|
98
|
-
await navItemService.create({
|
|
99
|
-
type: "page",
|
|
100
|
-
label: "About",
|
|
101
|
-
url: "/about",
|
|
102
|
-
pageId: page.id,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Simulate what the route handler does: find and delete nav item by pageId
|
|
106
|
-
const allNavItems = await navItemService.list();
|
|
107
|
-
const found = allNavItems.find((item) => item.pageId === page.id);
|
|
108
|
-
expect(found).toBeDefined();
|
|
109
|
-
await navItemService.delete(found?.id as number);
|
|
110
|
-
|
|
111
|
-
// Nav item should be gone
|
|
112
|
-
const navItems = await navItemService.list();
|
|
113
|
-
expect(navItems).toHaveLength(0);
|
|
114
|
-
|
|
115
|
-
// Page should still exist
|
|
116
|
-
const foundPage = await pageService.getById(page.id);
|
|
117
|
-
expect(foundPage).not.toBeNull();
|
|
118
|
-
|
|
119
|
-
// Page should appear in "not in nav" list
|
|
120
|
-
const notInNav = await pageService.listNotInNav();
|
|
121
|
-
expect(notInNav).toHaveLength(1);
|
|
122
|
-
expect(notInNav[0]?.slug).toBe("about");
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe("reorder nav items", () => {
|
|
127
|
-
it("reorders nav items by position", async () => {
|
|
128
|
-
const a = await navItemService.create({
|
|
129
|
-
type: "link",
|
|
130
|
-
label: "A",
|
|
131
|
-
url: "/a",
|
|
132
|
-
});
|
|
133
|
-
const b = await navItemService.create({
|
|
134
|
-
type: "link",
|
|
135
|
-
label: "B",
|
|
136
|
-
url: "/b",
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// Reverse order
|
|
140
|
-
await navItemService.reorder([b.id, a.id]);
|
|
141
|
-
|
|
142
|
-
const items = await navItemService.list();
|
|
143
|
-
expect(items[0]?.label).toBe("B");
|
|
144
|
-
expect(items[1]?.label).toBe("A");
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe("link CRUD", () => {
|
|
149
|
-
it("creates a link nav item", async () => {
|
|
150
|
-
await navItemService.create({
|
|
151
|
-
type: "link",
|
|
152
|
-
label: "Blog",
|
|
153
|
-
url: "/blog",
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const navItems = await navItemService.list();
|
|
157
|
-
expect(navItems).toHaveLength(1);
|
|
158
|
-
expect(navItems[0]?.type).toBe("link");
|
|
159
|
-
expect(navItems[0]?.label).toBe("Blog");
|
|
160
|
-
expect(navItems[0]?.url).toBe("/blog");
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("updates a link nav item", async () => {
|
|
164
|
-
const item = await navItemService.create({
|
|
165
|
-
type: "link",
|
|
166
|
-
label: "Blog",
|
|
167
|
-
url: "/blog",
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
await navItemService.update(item.id, {
|
|
171
|
-
label: "Posts",
|
|
172
|
-
url: "/posts",
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
const updated = await navItemService.getById(item.id);
|
|
176
|
-
expect(updated?.label).toBe("Posts");
|
|
177
|
-
expect(updated?.url).toBe("/posts");
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("deletes a link nav item", async () => {
|
|
181
|
-
const item = await navItemService.create({
|
|
182
|
-
type: "link",
|
|
183
|
-
label: "Blog",
|
|
184
|
-
url: "/blog",
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
await navItemService.delete(item.id);
|
|
188
|
-
|
|
189
|
-
const found = await navItemService.getById(item.id);
|
|
190
|
-
expect(found).toBeNull();
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
describe("unified page listing", () => {
|
|
195
|
-
it("separates pages into nav and non-nav groups", async () => {
|
|
196
|
-
const aboutPage = await pageService.create({
|
|
197
|
-
slug: "about",
|
|
198
|
-
title: "About",
|
|
199
|
-
});
|
|
200
|
-
await pageService.create({ slug: "contact", title: "Contact" });
|
|
201
|
-
|
|
202
|
-
// Add about to nav
|
|
203
|
-
await navItemService.create({
|
|
204
|
-
type: "page",
|
|
205
|
-
label: "About",
|
|
206
|
-
url: "/about",
|
|
207
|
-
pageId: aboutPage.id,
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// Also add a link nav item
|
|
211
|
-
await navItemService.create({
|
|
212
|
-
type: "link",
|
|
213
|
-
label: "External",
|
|
214
|
-
url: "https://example.com",
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// Simulate the unified page view data fetch
|
|
218
|
-
const navItems = await navItemService.list();
|
|
219
|
-
const otherPages = await pageService.listNotInNav();
|
|
220
|
-
|
|
221
|
-
expect(navItems).toHaveLength(2); // page + link
|
|
222
|
-
expect(otherPages).toHaveLength(1); // only contact
|
|
223
|
-
expect(otherPages[0]?.slug).toBe("contact");
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
});
|