@jant/core 0.3.27 → 0.3.29
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/reset-password.js +22 -0
- package/dist/client/client.css +1 -0
- package/dist/client/client.js +31561 -0
- package/dist/index.js +15209 -15
- package/package.json +25 -15
- package/src/__tests__/helpers/app.ts +19 -3
- package/src/__tests__/helpers/db.ts +44 -0
- package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
- package/src/app.tsx +111 -174
- package/src/client.ts +13 -0
- package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
- package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
- package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
- package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
- package/src/db/schema.ts +24 -4
- package/src/i18n/locales/en.po +810 -385
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +733 -522
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +733 -522
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +7 -11
- package/src/index.ts +1 -1
- package/src/lib/__tests__/icons.test.ts +178 -0
- package/src/lib/__tests__/resolve-config.test.ts +184 -0
- package/src/lib/__tests__/schemas.test.ts +12 -6
- package/src/lib/__tests__/theme.test.ts +62 -0
- package/src/lib/__tests__/timezones.test.ts +1 -1
- package/src/lib/__tests__/url.test.ts +12 -0
- package/src/lib/__tests__/view.test.ts +1 -5
- package/src/lib/avatar-upload.ts +18 -10
- package/src/lib/collection-form-bridge.ts +52 -0
- package/src/lib/collections-reorder.ts +28 -0
- package/src/lib/compose-bridge.ts +251 -0
- package/src/lib/errors.ts +116 -0
- package/src/lib/excerpt.ts +1 -1
- package/src/lib/favicon.ts +3 -5
- package/src/lib/html.ts +22 -0
- package/src/lib/icon-catalog.ts +181 -0
- package/src/lib/icons.ts +202 -0
- package/src/lib/navigation.ts +18 -33
- package/src/lib/pagination.ts +3 -2
- package/src/lib/post-form-bridge.ts +136 -0
- package/src/lib/render.tsx +11 -4
- package/src/lib/resolve-config.ts +157 -0
- package/src/lib/schemas.ts +76 -12
- package/src/lib/settings-bridge.ts +139 -0
- package/src/lib/storage.ts +37 -16
- package/src/lib/theme.ts +5 -7
- package/src/lib/timeline.ts +4 -8
- package/src/lib/toast.ts +134 -0
- package/src/lib/upload.ts +71 -0
- package/src/lib/url.ts +9 -1
- package/src/lib/version.ts +16 -0
- package/src/lib/view.ts +9 -10
- package/src/middleware/__tests__/auth.test.ts +6 -28
- package/src/middleware/__tests__/onboarding.test.ts +1 -1
- package/src/middleware/auth.ts +6 -12
- package/src/middleware/config.ts +51 -0
- package/src/middleware/error-handler.ts +56 -0
- package/src/middleware/onboarding.ts +1 -1
- package/src/preset.css +6 -0
- package/src/routes/__tests__/compose.test.ts +104 -17
- package/src/routes/api/__tests__/collections.test.ts +93 -2
- package/src/routes/api/__tests__/posts.test.ts +2 -1
- package/src/routes/api/__tests__/settings.test.ts +1 -1
- package/src/routes/api/collections.ts +64 -68
- package/src/routes/api/nav-items.ts +21 -59
- package/src/routes/api/pages.ts +18 -46
- package/src/routes/api/posts.ts +64 -86
- package/src/routes/api/search.ts +6 -4
- package/src/routes/api/settings.ts +8 -24
- package/src/routes/api/upload.ts +55 -53
- package/src/routes/auth/__tests__/setup.test.ts +118 -0
- package/src/routes/auth/reset.tsx +17 -66
- package/src/routes/auth/setup.tsx +67 -11
- package/src/routes/auth/signin.tsx +44 -8
- package/src/routes/compose.tsx +194 -0
- package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
- package/src/routes/dash/__tests__/pages.test.ts +2 -2
- package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
- package/src/routes/dash/appearance.tsx +173 -0
- package/src/routes/dash/collections.tsx +80 -14
- package/src/routes/dash/index.tsx +12 -14
- package/src/routes/dash/media.tsx +46 -49
- package/src/routes/dash/pages.tsx +85 -37
- package/src/routes/dash/posts.tsx +60 -23
- package/src/routes/dash/redirects.tsx +43 -33
- package/src/routes/dash/settings.tsx +234 -214
- package/src/routes/feed/__tests__/rss.test.ts +7 -3
- package/src/routes/feed/rss.ts +11 -16
- package/src/routes/feed/sitemap.ts +15 -9
- package/src/routes/pages/__tests__/collections.test.ts +9 -8
- package/src/routes/pages/archive.tsx +2 -2
- package/src/routes/pages/collection.tsx +76 -9
- package/src/routes/pages/collections.tsx +3 -1
- package/src/routes/pages/featured.tsx +2 -2
- package/src/routes/pages/home.tsx +3 -3
- package/src/routes/pages/latest.tsx +2 -2
- package/src/routes/pages/page.tsx +2 -2
- package/src/routes/pages/post.tsx +2 -2
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +324 -34
- package/src/services/__tests__/media.test.ts +1 -1
- package/src/services/__tests__/page.test.ts +116 -1
- package/src/services/auth.ts +88 -0
- package/src/services/collection.ts +169 -30
- package/src/services/index.ts +8 -3
- package/src/services/media.ts +39 -12
- package/src/services/navigation.ts +17 -5
- package/src/services/page.ts +24 -4
- package/src/services/post.ts +87 -19
- package/src/services/search.ts +0 -1
- package/src/services/settings.ts +21 -13
- package/src/style.css +3 -0
- package/src/styles/components.css +42 -1
- package/src/styles/tokens.css +4 -0
- package/src/styles/ui.css +902 -73
- package/src/types/app-context.ts +25 -0
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +60 -23
- package/src/types/entities.ts +12 -2
- package/src/types/lingui-react-macro.d.ts +3 -3
- package/src/types/operations.ts +2 -4
- package/src/types/views.ts +1 -3
- package/src/ui/__tests__/font-themes.test.ts +27 -8
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
- package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
- package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
- package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
- package/src/ui/components/collection-types.ts +45 -0
- package/src/ui/components/compose-types.ts +75 -0
- package/src/ui/components/jant-collection-form.ts +512 -0
- package/src/ui/components/jant-compose-dialog.ts +494 -0
- package/src/ui/components/jant-compose-editor.ts +799 -0
- package/src/ui/components/jant-post-form.ts +290 -0
- package/src/ui/components/jant-settings-avatar.ts +231 -0
- package/src/ui/components/jant-settings-general.ts +436 -0
- package/src/ui/components/post-form-template.ts +260 -0
- package/src/ui/components/post-form-types.ts +87 -0
- package/src/ui/components/settings-types.ts +62 -0
- package/src/ui/compose/ComposeDialog.tsx +141 -385
- package/src/ui/compose/ComposePrompt.tsx +3 -3
- package/src/ui/dash/PostList.tsx +55 -61
- package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
- package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
- package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
- package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
- package/src/ui/dash/collections/CollectionForm.tsx +130 -117
- package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
- package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
- package/src/ui/dash/index.ts +1 -1
- package/src/ui/dash/posts/PostForm.tsx +248 -0
- package/src/ui/dash/settings/AccountContent.tsx +69 -80
- package/src/ui/dash/settings/GeneralContent.tsx +159 -478
- package/src/ui/dash/settings/SettingsNav.tsx +4 -4
- package/src/ui/font-themes.ts +115 -32
- package/src/ui/layouts/BaseLayout.tsx +49 -19
- package/src/ui/layouts/DashLayout.tsx +14 -9
- package/src/ui/layouts/SiteLayout.tsx +38 -23
- package/src/ui/pages/CollectionPage.tsx +12 -2
- package/src/ui/pages/CollectionsPage.tsx +27 -27
- package/src/ui/pages/HomePage.tsx +15 -6
- package/src/ui/pages/SearchPage.tsx +1 -2
- package/src/ui/shared/CollectionsSidebar.tsx +59 -0
- package/src/ui/shared/Pagination.tsx +2 -2
- package/dist/app.js +0 -267
- package/dist/auth.js +0 -39
- package/dist/client.js +0 -13
- package/dist/db/index.js +0 -10
- package/dist/db/schema.js +0 -224
- package/dist/i18n/Trans.js +0 -24
- package/dist/i18n/context.js +0 -58
- package/dist/i18n/detect.js +0 -26
- package/dist/i18n/i18n.js +0 -49
- package/dist/i18n/index.js +0 -44
- package/dist/i18n/locales/en.js +0 -1
- package/dist/i18n/locales/zh-Hans.js +0 -1
- package/dist/i18n/locales/zh-Hant.js +0 -1
- package/dist/i18n/locales.js +0 -13
- package/dist/i18n/middleware.js +0 -30
- package/dist/lib/avatar-upload.js +0 -134
- package/dist/lib/config.js +0 -143
- package/dist/lib/constants.js +0 -50
- package/dist/lib/excerpt.js +0 -76
- package/dist/lib/favicon.js +0 -102
- package/dist/lib/feed.js +0 -123
- package/dist/lib/image-processor.js +0 -187
- package/dist/lib/image.js +0 -97
- package/dist/lib/index.js +0 -7
- package/dist/lib/markdown.js +0 -83
- package/dist/lib/media-helpers.js +0 -49
- package/dist/lib/media-upload.js +0 -104
- package/dist/lib/nav-reorder.js +0 -27
- package/dist/lib/navigation.js +0 -79
- package/dist/lib/pagination.js +0 -44
- package/dist/lib/render.js +0 -53
- package/dist/lib/schemas.js +0 -174
- package/dist/lib/sqid.js +0 -72
- package/dist/lib/sse.js +0 -218
- package/dist/lib/storage.js +0 -164
- package/dist/lib/theme.js +0 -65
- package/dist/lib/time.js +0 -159
- package/dist/lib/timeline.js +0 -95
- package/dist/lib/timezones.js +0 -388
- package/dist/lib/url.js +0 -89
- package/dist/lib/view.js +0 -217
- package/dist/middleware/auth.js +0 -52
- package/dist/middleware/onboarding.js +0 -41
- package/dist/routes/api/collections.js +0 -124
- package/dist/routes/api/nav-items.js +0 -104
- package/dist/routes/api/pages.js +0 -91
- package/dist/routes/api/posts.js +0 -218
- package/dist/routes/api/search.js +0 -48
- package/dist/routes/api/settings.js +0 -68
- package/dist/routes/api/upload.js +0 -246
- package/dist/routes/auth/reset.js +0 -221
- package/dist/routes/auth/setup.js +0 -194
- package/dist/routes/auth/signin.js +0 -176
- package/dist/routes/compose.js +0 -48
- package/dist/routes/dash/collections.js +0 -115
- package/dist/routes/dash/index.js +0 -118
- package/dist/routes/dash/media.js +0 -106
- package/dist/routes/dash/pages.js +0 -294
- package/dist/routes/dash/posts.js +0 -244
- package/dist/routes/dash/redirects.js +0 -257
- package/dist/routes/dash/settings.js +0 -379
- package/dist/routes/feed/rss.js +0 -62
- package/dist/routes/feed/sitemap.js +0 -49
- package/dist/routes/pages/archive.js +0 -62
- package/dist/routes/pages/collection.js +0 -34
- package/dist/routes/pages/collections.js +0 -28
- package/dist/routes/pages/featured.js +0 -36
- package/dist/routes/pages/home.js +0 -64
- package/dist/routes/pages/latest.js +0 -45
- package/dist/routes/pages/page.js +0 -68
- package/dist/routes/pages/post.js +0 -44
- package/dist/routes/pages/search.js +0 -54
- package/dist/services/collection.js +0 -109
- package/dist/services/index.js +0 -24
- package/dist/services/media.js +0 -117
- package/dist/services/navigation.js +0 -91
- package/dist/services/page.js +0 -84
- package/dist/services/post.js +0 -229
- package/dist/services/redirect.js +0 -48
- package/dist/services/search.js +0 -67
- package/dist/services/settings.js +0 -68
- package/dist/types/bindings.js +0 -3
- package/dist/types/config.js +0 -147
- package/dist/types/constants.js +0 -27
- package/dist/types/entities.js +0 -3
- package/dist/types/lingui-react-macro.d.js +0 -9
- package/dist/types/operations.js +0 -3
- package/dist/types/props.js +0 -3
- package/dist/types/sortablejs.d.js +0 -5
- package/dist/types/views.js +0 -5
- package/dist/types.js +0 -11
- package/dist/ui/color-themes.js +0 -268
- package/dist/ui/compose/ComposeDialog.js +0 -467
- package/dist/ui/compose/ComposePrompt.js +0 -55
- package/dist/ui/dash/ActionButtons.js +0 -46
- package/dist/ui/dash/CrudPageHeader.js +0 -22
- package/dist/ui/dash/DangerZone.js +0 -36
- package/dist/ui/dash/FormatBadge.js +0 -27
- package/dist/ui/dash/ListItemRow.js +0 -21
- package/dist/ui/dash/PageForm.js +0 -195
- package/dist/ui/dash/PostForm.js +0 -395
- package/dist/ui/dash/PostList.js +0 -83
- package/dist/ui/dash/StatusBadge.js +0 -46
- package/dist/ui/dash/collections/CollectionForm.js +0 -152
- package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
- package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
- package/dist/ui/dash/index.js +0 -10
- package/dist/ui/dash/media/MediaListContent.js +0 -166
- package/dist/ui/dash/media/ViewMediaContent.js +0 -212
- package/dist/ui/dash/pages/LinkFormContent.js +0 -130
- package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
- package/dist/ui/dash/settings/AccountContent.js +0 -209
- package/dist/ui/dash/settings/AppearanceContent.js +0 -259
- package/dist/ui/dash/settings/GeneralContent.js +0 -536
- package/dist/ui/dash/settings/SettingsNav.js +0 -41
- package/dist/ui/feed/LinkCard.js +0 -72
- package/dist/ui/feed/NoteCard.js +0 -58
- package/dist/ui/feed/QuoteCard.js +0 -63
- package/dist/ui/feed/ThreadPreview.js +0 -48
- package/dist/ui/feed/TimelineFeed.js +0 -41
- package/dist/ui/feed/TimelineItem.js +0 -27
- package/dist/ui/font-themes.js +0 -36
- package/dist/ui/layouts/BaseLayout.js +0 -153
- package/dist/ui/layouts/DashLayout.js +0 -141
- package/dist/ui/layouts/SiteLayout.js +0 -169
- package/dist/ui/pages/ArchivePage.js +0 -143
- package/dist/ui/pages/CollectionPage.js +0 -70
- package/dist/ui/pages/CollectionsPage.js +0 -76
- package/dist/ui/pages/FeaturedPage.js +0 -24
- package/dist/ui/pages/HomePage.js +0 -24
- package/dist/ui/pages/PostPage.js +0 -55
- package/dist/ui/pages/SearchPage.js +0 -122
- package/dist/ui/pages/SinglePage.js +0 -23
- package/dist/ui/shared/EmptyState.js +0 -27
- package/dist/ui/shared/MediaGallery.js +0 -35
- package/dist/ui/shared/Pagination.js +0 -195
- package/dist/ui/shared/ThreadView.js +0 -108
- package/dist/ui/shared/index.js +0 -5
- package/dist/vendor/datastar.js +0 -1606
- package/src/lib/__tests__/config.test.ts +0 -192
- package/src/lib/config.ts +0 -167
- package/src/routes/compose.ts +0 -63
- package/src/ui/dash/PostForm.tsx +0 -360
- package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
|
@@ -18,7 +18,7 @@ describe("Compose Routes", () => {
|
|
|
18
18
|
expect(res.headers.get("Location")).toBe("/signin");
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
it("creates a note post and returns
|
|
21
|
+
it("creates a note post and returns timeline card via SSE", async () => {
|
|
22
22
|
const { app, services } = createTestApp({ authenticated: true });
|
|
23
23
|
app.route("/compose", composeRoutes);
|
|
24
24
|
|
|
@@ -29,7 +29,13 @@ describe("Compose Routes", () => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
expect(res.status).toBe(200);
|
|
32
|
-
expect(res.headers.get("Content-Type")).toBe("text/
|
|
32
|
+
expect(res.headers.get("Content-Type")).toBe("text/event-stream");
|
|
33
|
+
|
|
34
|
+
const text = await res.text();
|
|
35
|
+
// SSE prepends the card to the timeline
|
|
36
|
+
expect(text).toContain("datastar-patch-elements");
|
|
37
|
+
expect(text).toContain('data-format="note"');
|
|
38
|
+
expect(text).toContain("selector #timeline-items");
|
|
33
39
|
|
|
34
40
|
// Verify post was created
|
|
35
41
|
const posts = await services.posts.list();
|
|
@@ -54,6 +60,10 @@ describe("Compose Routes", () => {
|
|
|
54
60
|
});
|
|
55
61
|
|
|
56
62
|
expect(res.status).toBe(200);
|
|
63
|
+
expect(res.headers.get("Content-Type")).toBe("text/event-stream");
|
|
64
|
+
|
|
65
|
+
const text = await res.text();
|
|
66
|
+
expect(text).toContain('data-format="link"');
|
|
57
67
|
|
|
58
68
|
const posts = await services.posts.list();
|
|
59
69
|
expect(posts).toHaveLength(1);
|
|
@@ -77,6 +87,10 @@ describe("Compose Routes", () => {
|
|
|
77
87
|
});
|
|
78
88
|
|
|
79
89
|
expect(res.status).toBe(200);
|
|
90
|
+
expect(res.headers.get("Content-Type")).toBe("text/event-stream");
|
|
91
|
+
|
|
92
|
+
const text = await res.text();
|
|
93
|
+
expect(text).toContain('data-format="quote"');
|
|
80
94
|
|
|
81
95
|
const posts = await services.posts.list();
|
|
82
96
|
expect(posts).toHaveLength(1);
|
|
@@ -84,7 +98,7 @@ describe("Compose Routes", () => {
|
|
|
84
98
|
expect(posts[0].quoteText).toBe("The original quote");
|
|
85
99
|
});
|
|
86
100
|
|
|
87
|
-
it("creates a draft
|
|
101
|
+
it("creates a draft and closes dialog with toast", async () => {
|
|
88
102
|
const { app, services } = createTestApp({ authenticated: true });
|
|
89
103
|
app.route("/compose", composeRoutes);
|
|
90
104
|
|
|
@@ -99,6 +113,13 @@ describe("Compose Routes", () => {
|
|
|
99
113
|
});
|
|
100
114
|
|
|
101
115
|
expect(res.status).toBe(200);
|
|
116
|
+
expect(res.headers.get("Content-Type")).toBe("text/event-stream");
|
|
117
|
+
|
|
118
|
+
const text = await res.text();
|
|
119
|
+
// Should close dialog and show toast, not prepend to timeline
|
|
120
|
+
expect(text).toContain("compose-dialog");
|
|
121
|
+
expect(text).toContain("Draft saved");
|
|
122
|
+
expect(text).not.toContain("selector #timeline-items");
|
|
102
123
|
|
|
103
124
|
const posts = await services.posts.list({ includeDrafts: true });
|
|
104
125
|
expect(posts).toHaveLength(1);
|
|
@@ -158,27 +179,20 @@ describe("Compose Routes", () => {
|
|
|
158
179
|
expect(attachments[0].id).toBe(media.id);
|
|
159
180
|
});
|
|
160
181
|
|
|
161
|
-
it("
|
|
162
|
-
const { app
|
|
182
|
+
it("resets compose signals after publishing", async () => {
|
|
183
|
+
const { app } = createTestApp({ authenticated: true });
|
|
163
184
|
app.route("/compose", composeRoutes);
|
|
164
185
|
|
|
165
186
|
const res = await app.request("/compose", {
|
|
166
187
|
method: "POST",
|
|
167
188
|
headers: { "Content-Type": "application/json" },
|
|
168
|
-
body: JSON.stringify({
|
|
169
|
-
format: "note",
|
|
170
|
-
body: "Featured and pinned",
|
|
171
|
-
featured: true,
|
|
172
|
-
pinned: true,
|
|
173
|
-
}),
|
|
189
|
+
body: JSON.stringify({ format: "note", body: "Hello" }),
|
|
174
190
|
});
|
|
175
191
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
expect(
|
|
180
|
-
expect(posts[0].featured).toBe(1);
|
|
181
|
-
expect(posts[0].pinned).toBe(1);
|
|
192
|
+
const text = await res.text();
|
|
193
|
+
// SSE should include signal reset
|
|
194
|
+
expect(text).toContain("datastar-patch-signals");
|
|
195
|
+
expect(text).toContain('"_composeLoading":false');
|
|
182
196
|
});
|
|
183
197
|
|
|
184
198
|
it("returns error when format is missing", async () => {
|
|
@@ -196,4 +210,77 @@ describe("Compose Routes", () => {
|
|
|
196
210
|
expect(text).toContain("toast-error");
|
|
197
211
|
});
|
|
198
212
|
});
|
|
213
|
+
|
|
214
|
+
describe("POST /compose (JSON mode)", () => {
|
|
215
|
+
it("returns JSON for published note", async () => {
|
|
216
|
+
const { app, services } = createTestApp({ authenticated: true });
|
|
217
|
+
app.route("/compose", composeRoutes);
|
|
218
|
+
|
|
219
|
+
const res = await app.request("/compose", {
|
|
220
|
+
method: "POST",
|
|
221
|
+
headers: {
|
|
222
|
+
"Content-Type": "application/json",
|
|
223
|
+
Accept: "application/json",
|
|
224
|
+
},
|
|
225
|
+
body: JSON.stringify({ format: "note", body: "Hello JSON" }),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(res.status).toBe(200);
|
|
229
|
+
expect(res.headers.get("Content-Type")).toContain("application/json");
|
|
230
|
+
|
|
231
|
+
const data = await res.json();
|
|
232
|
+
expect(data.status).toBe("published");
|
|
233
|
+
expect(data.cardHtml).toContain('data-format="note"');
|
|
234
|
+
|
|
235
|
+
const posts = await services.posts.list();
|
|
236
|
+
expect(posts).toHaveLength(1);
|
|
237
|
+
expect(posts[0].body).toBe("Hello JSON");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("returns JSON for draft", async () => {
|
|
241
|
+
const { app, services } = createTestApp({ authenticated: true });
|
|
242
|
+
app.route("/compose", composeRoutes);
|
|
243
|
+
|
|
244
|
+
const res = await app.request("/compose", {
|
|
245
|
+
method: "POST",
|
|
246
|
+
headers: {
|
|
247
|
+
"Content-Type": "application/json",
|
|
248
|
+
Accept: "application/json",
|
|
249
|
+
},
|
|
250
|
+
body: JSON.stringify({
|
|
251
|
+
format: "note",
|
|
252
|
+
body: "Draft JSON",
|
|
253
|
+
status: "draft",
|
|
254
|
+
}),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(res.status).toBe(200);
|
|
258
|
+
const data = await res.json();
|
|
259
|
+
expect(data.status).toBe("draft");
|
|
260
|
+
expect(data.toast).toBe("Draft saved.");
|
|
261
|
+
|
|
262
|
+
const posts = await services.posts.list({ includeDrafts: true });
|
|
263
|
+
expect(posts).toHaveLength(1);
|
|
264
|
+
expect(posts[0].status).toBe("draft");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("returns JSON error for invalid input", async () => {
|
|
268
|
+
const { app } = createTestApp({ authenticated: true });
|
|
269
|
+
app.route("/compose", composeRoutes);
|
|
270
|
+
|
|
271
|
+
const res = await app.request("/compose", {
|
|
272
|
+
method: "POST",
|
|
273
|
+
headers: {
|
|
274
|
+
"Content-Type": "application/json",
|
|
275
|
+
Accept: "application/json",
|
|
276
|
+
},
|
|
277
|
+
body: JSON.stringify({ format: "invalid", body: "Hello" }),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(res.status).toBe(422);
|
|
281
|
+
const data = await res.json();
|
|
282
|
+
expect(data.status).toBe("error");
|
|
283
|
+
expect(data.error).toBeDefined();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
199
286
|
});
|
|
@@ -23,11 +23,11 @@ describe("Collections API Routes", () => {
|
|
|
23
23
|
slug: "tech",
|
|
24
24
|
title: "Tech",
|
|
25
25
|
});
|
|
26
|
-
await services.posts.create({
|
|
26
|
+
const post = await services.posts.create({
|
|
27
27
|
format: "note",
|
|
28
28
|
body: "tech post",
|
|
29
|
-
collectionId: col.id,
|
|
30
29
|
});
|
|
30
|
+
await services.collections.addPost(col.id, post.id);
|
|
31
31
|
|
|
32
32
|
const res = await app.request("/api/collections");
|
|
33
33
|
const body = await res.json();
|
|
@@ -246,4 +246,95 @@ describe("Collections API Routes", () => {
|
|
|
246
246
|
expect(res.status).toBe(404);
|
|
247
247
|
});
|
|
248
248
|
});
|
|
249
|
+
|
|
250
|
+
describe("POST /api/collections/:id/posts", () => {
|
|
251
|
+
it("adds a post to a collection", async () => {
|
|
252
|
+
const { app, services } = createTestApp({ authenticated: true });
|
|
253
|
+
app.route("/api/collections", collectionsApiRoutes);
|
|
254
|
+
|
|
255
|
+
const col = await services.collections.create({
|
|
256
|
+
slug: "tech",
|
|
257
|
+
title: "Tech",
|
|
258
|
+
});
|
|
259
|
+
const post = await services.posts.create({
|
|
260
|
+
format: "note",
|
|
261
|
+
body: "test",
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const res = await app.request(`/api/collections/${col.id}/posts`, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: { "Content-Type": "application/json" },
|
|
267
|
+
body: JSON.stringify({ postId: post.id }),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
expect(res.status).toBe(201);
|
|
271
|
+
|
|
272
|
+
const postIds = await services.collections.getPostIds(col.id);
|
|
273
|
+
expect(postIds).toContain(post.id);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("returns 404 for non-existent collection", async () => {
|
|
277
|
+
const { app, services } = createTestApp({ authenticated: true });
|
|
278
|
+
app.route("/api/collections", collectionsApiRoutes);
|
|
279
|
+
|
|
280
|
+
const post = await services.posts.create({
|
|
281
|
+
format: "note",
|
|
282
|
+
body: "test",
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const res = await app.request("/api/collections/9999/posts", {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers: { "Content-Type": "application/json" },
|
|
288
|
+
body: JSON.stringify({ postId: post.id }),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(res.status).toBe(404);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("returns 401 when not authenticated", async () => {
|
|
295
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
296
|
+
app.route("/api/collections", collectionsApiRoutes);
|
|
297
|
+
|
|
298
|
+
const col = await services.collections.create({
|
|
299
|
+
slug: "tech",
|
|
300
|
+
title: "Tech",
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const res = await app.request(`/api/collections/${col.id}/posts`, {
|
|
304
|
+
method: "POST",
|
|
305
|
+
headers: { "Content-Type": "application/json" },
|
|
306
|
+
body: JSON.stringify({ postId: 1 }),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
expect(res.status).toBe(401);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe("DELETE /api/collections/:id/posts/:postId", () => {
|
|
314
|
+
it("removes a post from a collection", async () => {
|
|
315
|
+
const { app, services } = createTestApp({ authenticated: true });
|
|
316
|
+
app.route("/api/collections", collectionsApiRoutes);
|
|
317
|
+
|
|
318
|
+
const col = await services.collections.create({
|
|
319
|
+
slug: "tech",
|
|
320
|
+
title: "Tech",
|
|
321
|
+
});
|
|
322
|
+
const post = await services.posts.create({
|
|
323
|
+
format: "note",
|
|
324
|
+
body: "test",
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
await services.collections.addPost(col.id, post.id);
|
|
328
|
+
|
|
329
|
+
const res = await app.request(
|
|
330
|
+
`/api/collections/${col.id}/posts/${post.id}`,
|
|
331
|
+
{ method: "DELETE" },
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
expect(res.status).toBe(200);
|
|
335
|
+
|
|
336
|
+
const postIds = await services.collections.getPostIds(col.id);
|
|
337
|
+
expect(postIds).not.toContain(post.id);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
249
340
|
});
|
|
@@ -275,7 +275,8 @@ describe("Posts API Routes", () => {
|
|
|
275
275
|
|
|
276
276
|
expect(res.status).toBe(400);
|
|
277
277
|
const body = await res.json();
|
|
278
|
-
expect(body.error).
|
|
278
|
+
expect(body.error).toContain("Invalid");
|
|
279
|
+
expect(body.code).toBe("VALIDATION_ERROR");
|
|
279
280
|
});
|
|
280
281
|
|
|
281
282
|
it("returns 400 for missing required fields", async () => {
|
|
@@ -94,7 +94,7 @@ describe("Settings API Routes", () => {
|
|
|
94
94
|
|
|
95
95
|
expect(res.status).toBe(400);
|
|
96
96
|
const body = await res.json();
|
|
97
|
-
expect(body.rejectedKeys).toContain("AUTH_SECRET");
|
|
97
|
+
expect(body.details.rejectedKeys).toContain("AUTH_SECRET");
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
it("partially applies when mixing editable and env-only keys", async () => {
|
|
@@ -3,40 +3,37 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
+
import { z } from "zod";
|
|
6
7
|
import type { Bindings, SortOrder } from "../../types.js";
|
|
7
|
-
import type { AppVariables } from "../../app.js";
|
|
8
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
8
9
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
9
|
-
import {
|
|
10
|
-
|
|
10
|
+
import {
|
|
11
|
+
CreateCollectionSchema,
|
|
12
|
+
SortOrderSchema,
|
|
13
|
+
parseValidated,
|
|
14
|
+
} from "../../lib/schemas.js";
|
|
15
|
+
import { assertFound, parseIntParam, NotFoundError } from "../../lib/errors.js";
|
|
11
16
|
|
|
12
17
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
13
18
|
|
|
14
19
|
export const collectionsApiRoutes = new Hono<Env>();
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const CreateCollectionSchema = z.object({
|
|
19
|
-
slug: z.string().min(1),
|
|
20
|
-
title: z.string().min(1),
|
|
21
|
-
description: z.string().optional(),
|
|
22
|
-
icon: z.string().optional(),
|
|
23
|
-
sortOrder: SortOrderSchema.optional(),
|
|
24
|
-
position: z.number().int().min(0).optional(),
|
|
25
|
-
showDivider: z.boolean().optional(),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const UpdateCollectionSchema = z.object({
|
|
29
|
-
slug: z.string().min(1).optional(),
|
|
30
|
-
title: z.string().min(1).optional(),
|
|
21
|
+
// API update schema extends shared schema with nullable fields for explicit clearing
|
|
22
|
+
const UpdateCollectionSchema = CreateCollectionSchema.partial().extend({
|
|
31
23
|
description: z.string().nullable().optional(),
|
|
32
24
|
icon: z.string().nullable().optional(),
|
|
33
25
|
sortOrder: SortOrderSchema.optional(),
|
|
34
26
|
position: z.number().int().min(0).optional(),
|
|
35
|
-
showDivider: z.boolean().optional(),
|
|
36
27
|
});
|
|
37
28
|
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
// Route-specific schemas (not shared domain schemas)
|
|
30
|
+
const CollectionReorderSchema = z.object({
|
|
31
|
+
ids: z.array(z.number().int().positive()).optional(),
|
|
32
|
+
items: z.array(z.string().regex(/^[cd]-\d+$/)).optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const PostAssignSchema = z.object({
|
|
36
|
+
postId: z.number().int().positive(),
|
|
40
37
|
});
|
|
41
38
|
|
|
42
39
|
// List collections (includes post counts)
|
|
@@ -54,45 +51,30 @@ collectionsApiRoutes.get("/", async (c) => {
|
|
|
54
51
|
|
|
55
52
|
// Get single collection
|
|
56
53
|
collectionsApiRoutes.get("/:id", async (c) => {
|
|
57
|
-
const id =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
const id = parseIntParam(c.req.param("id"));
|
|
55
|
+
const collection = assertFound(
|
|
56
|
+
await c.var.services.collections.getById(id),
|
|
57
|
+
"Collection",
|
|
58
|
+
);
|
|
63
59
|
return c.json(collection);
|
|
64
60
|
});
|
|
65
61
|
|
|
66
62
|
// Reorder collections (requires auth) — must be before /:id
|
|
67
63
|
collectionsApiRoutes.put("/reorder", requireAuthApi(), async (c) => {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
const parseResult = ReorderSchema.safeParse(rawBody);
|
|
71
|
-
if (!parseResult.success) {
|
|
72
|
-
return c.json(
|
|
73
|
-
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
74
|
-
400,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
64
|
+
const body = parseValidated(CollectionReorderSchema, await c.req.json());
|
|
77
65
|
|
|
78
|
-
|
|
66
|
+
if (body.items) {
|
|
67
|
+
await c.var.services.collections.reorderAll(body.items);
|
|
68
|
+
} else if (body.ids) {
|
|
69
|
+
await c.var.services.collections.reorder(body.ids);
|
|
70
|
+
}
|
|
79
71
|
const collections = await c.var.services.collections.list();
|
|
80
72
|
return c.json({ collections });
|
|
81
73
|
});
|
|
82
74
|
|
|
83
75
|
// Create collection (requires auth)
|
|
84
76
|
collectionsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
const parseResult = CreateCollectionSchema.safeParse(rawBody);
|
|
88
|
-
if (!parseResult.success) {
|
|
89
|
-
return c.json(
|
|
90
|
-
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
91
|
-
400,
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const body = parseResult.data;
|
|
77
|
+
const body = parseValidated(CreateCollectionSchema, await c.req.json());
|
|
96
78
|
|
|
97
79
|
const collection = await c.var.services.collections.create({
|
|
98
80
|
slug: body.slug,
|
|
@@ -101,7 +83,6 @@ collectionsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
101
83
|
icon: body.icon,
|
|
102
84
|
sortOrder: body.sortOrder as SortOrder | undefined,
|
|
103
85
|
position: body.position,
|
|
104
|
-
showDivider: body.showDivider,
|
|
105
86
|
});
|
|
106
87
|
|
|
107
88
|
return c.json(collection, 201);
|
|
@@ -109,35 +90,50 @@ collectionsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
109
90
|
|
|
110
91
|
// Update collection (requires auth)
|
|
111
92
|
collectionsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
112
|
-
const id =
|
|
113
|
-
|
|
93
|
+
const id = parseIntParam(c.req.param("id"));
|
|
94
|
+
const body = parseValidated(UpdateCollectionSchema, await c.req.json());
|
|
114
95
|
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (!parseResult.success) {
|
|
119
|
-
return c.json(
|
|
120
|
-
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
121
|
-
400,
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const collection = await c.var.services.collections.update(
|
|
126
|
-
id,
|
|
127
|
-
parseResult.data,
|
|
96
|
+
const collection = assertFound(
|
|
97
|
+
await c.var.services.collections.update(id, body),
|
|
98
|
+
"Collection",
|
|
128
99
|
);
|
|
129
|
-
if (!collection) return c.json({ error: "Not found" }, 404);
|
|
130
100
|
|
|
131
101
|
return c.json(collection);
|
|
132
102
|
});
|
|
133
103
|
|
|
134
104
|
// Delete collection (requires auth)
|
|
135
105
|
collectionsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
|
|
136
|
-
const id =
|
|
137
|
-
if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
|
|
106
|
+
const id = parseIntParam(c.req.param("id"));
|
|
138
107
|
|
|
139
108
|
const success = await c.var.services.collections.delete(id);
|
|
140
|
-
if (!success)
|
|
109
|
+
if (!success) throw new NotFoundError("Collection");
|
|
141
110
|
|
|
142
111
|
return c.json({ success: true });
|
|
143
112
|
});
|
|
113
|
+
|
|
114
|
+
// Add a post to a collection (requires auth)
|
|
115
|
+
collectionsApiRoutes.post("/:id/posts", requireAuthApi(), async (c) => {
|
|
116
|
+
const id = parseIntParam(c.req.param("id"));
|
|
117
|
+
assertFound(await c.var.services.collections.getById(id), "Collection");
|
|
118
|
+
|
|
119
|
+
const body = parseValidated(PostAssignSchema, await c.req.json());
|
|
120
|
+
assertFound(await c.var.services.posts.getById(body.postId), "Post");
|
|
121
|
+
|
|
122
|
+
await c.var.services.collections.addPost(id, body.postId);
|
|
123
|
+
|
|
124
|
+
return c.json({ success: true }, 201);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Remove a post from a collection (requires auth)
|
|
128
|
+
collectionsApiRoutes.delete(
|
|
129
|
+
"/:id/posts/:postId",
|
|
130
|
+
requireAuthApi(),
|
|
131
|
+
async (c) => {
|
|
132
|
+
const id = parseIntParam(c.req.param("id"));
|
|
133
|
+
const postId = parseIntParam(c.req.param("postId"));
|
|
134
|
+
|
|
135
|
+
await c.var.services.collections.removePost(id, postId);
|
|
136
|
+
|
|
137
|
+
return c.json({ success: true });
|
|
138
|
+
},
|
|
139
|
+
);
|
|
@@ -3,35 +3,24 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
+
import { z } from "zod";
|
|
6
7
|
import type { Bindings, NavItemType } from "../../types.js";
|
|
7
|
-
import type { AppVariables } from "../../app.js";
|
|
8
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
8
9
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
CreateNavItemSchema,
|
|
12
|
+
ReorderSchema,
|
|
13
|
+
parseValidated,
|
|
14
|
+
} from "../../lib/schemas.js";
|
|
15
|
+
import { assertFound, parseIntParam, NotFoundError } from "../../lib/errors.js";
|
|
10
16
|
|
|
11
17
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
12
18
|
|
|
13
19
|
export const navItemsApiRoutes = new Hono<Env>();
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const CreateNavItemSchema = z.object({
|
|
18
|
-
type: NavItemTypeSchema,
|
|
19
|
-
label: z.string().min(1),
|
|
20
|
-
url: z.string().min(1),
|
|
21
|
-
pageId: z.number().int().positive().optional(),
|
|
22
|
-
position: z.number().int().min(0).optional(),
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const UpdateNavItemSchema = z.object({
|
|
26
|
-
type: NavItemTypeSchema.optional(),
|
|
27
|
-
label: z.string().min(1).optional(),
|
|
28
|
-
url: z.string().min(1).optional(),
|
|
21
|
+
// API update schema extends shared schema with nullable pageId for explicit clearing
|
|
22
|
+
const UpdateNavItemSchema = CreateNavItemSchema.partial().extend({
|
|
29
23
|
pageId: z.number().int().positive().nullable().optional(),
|
|
30
|
-
position: z.number().int().min(0).optional(),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const ReorderSchema = z.object({
|
|
34
|
-
ids: z.array(z.number().int().positive()),
|
|
35
24
|
});
|
|
36
25
|
|
|
37
26
|
// List nav items
|
|
@@ -42,34 +31,16 @@ navItemsApiRoutes.get("/", async (c) => {
|
|
|
42
31
|
|
|
43
32
|
// Reorder nav items (requires auth) — must be before /:id
|
|
44
33
|
navItemsApiRoutes.put("/reorder", requireAuthApi(), async (c) => {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
const parseResult = ReorderSchema.safeParse(rawBody);
|
|
48
|
-
if (!parseResult.success) {
|
|
49
|
-
return c.json(
|
|
50
|
-
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
51
|
-
400,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
34
|
+
const body = parseValidated(ReorderSchema, await c.req.json());
|
|
54
35
|
|
|
55
|
-
await c.var.services.navItems.reorder(
|
|
36
|
+
await c.var.services.navItems.reorder(body.ids);
|
|
56
37
|
const items = await c.var.services.navItems.list();
|
|
57
38
|
return c.json({ navItems: items });
|
|
58
39
|
});
|
|
59
40
|
|
|
60
41
|
// Create nav item (requires auth)
|
|
61
42
|
navItemsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
const parseResult = CreateNavItemSchema.safeParse(rawBody);
|
|
65
|
-
if (!parseResult.success) {
|
|
66
|
-
return c.json(
|
|
67
|
-
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
68
|
-
400,
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const body = parseResult.data;
|
|
43
|
+
const body = parseValidated(CreateNavItemSchema, await c.req.json());
|
|
73
44
|
|
|
74
45
|
const item = await c.var.services.navItems.create({
|
|
75
46
|
type: body.type as NavItemType,
|
|
@@ -84,32 +55,23 @@ navItemsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
84
55
|
|
|
85
56
|
// Update nav item (requires auth)
|
|
86
57
|
navItemsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
87
|
-
const id =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const rawBody = await c.req.json();
|
|
91
|
-
|
|
92
|
-
const parseResult = UpdateNavItemSchema.safeParse(rawBody);
|
|
93
|
-
if (!parseResult.success) {
|
|
94
|
-
return c.json(
|
|
95
|
-
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
96
|
-
400,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
58
|
+
const id = parseIntParam(c.req.param("id"));
|
|
59
|
+
const body = parseValidated(UpdateNavItemSchema, await c.req.json());
|
|
99
60
|
|
|
100
|
-
const item =
|
|
101
|
-
|
|
61
|
+
const item = assertFound(
|
|
62
|
+
await c.var.services.navItems.update(id, body),
|
|
63
|
+
"Nav item",
|
|
64
|
+
);
|
|
102
65
|
|
|
103
66
|
return c.json(item);
|
|
104
67
|
});
|
|
105
68
|
|
|
106
69
|
// Delete nav item (requires auth)
|
|
107
70
|
navItemsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
|
|
108
|
-
const id =
|
|
109
|
-
if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
|
|
71
|
+
const id = parseIntParam(c.req.param("id"));
|
|
110
72
|
|
|
111
73
|
const success = await c.var.services.navItems.delete(id);
|
|
112
|
-
if (!success)
|
|
74
|
+
if (!success) throw new NotFoundError("Nav item");
|
|
113
75
|
|
|
114
76
|
return c.json({ success: true });
|
|
115
77
|
});
|