@jant/core 0.6.14 → 0.6.16
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/import-site.js +88 -13
- package/bin/commands/migrate.js +55 -0
- package/bin/commands/posts/rebuild-html.js +184 -0
- package/bin/commands/site/snapshot/import.js +14 -10
- package/bin/lib/d1-query.js +23 -5
- package/bin/lib/migration-runner.js +29 -7
- package/bin/lib/node-database.js +32 -0
- package/bin/lib/site-snapshot.js +347 -46
- package/bin/lib/sql-export.js +1 -1
- package/bin/lib/thread-collection-migration.js +326 -0
- package/dist/app-C75Zc8dD.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +7 -7
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- package/package.json +11 -10
- package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
- package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
- package/src/__tests__/export-hugo-build.test.ts +63 -4
- package/src/__tests__/export-import-roundtrip.test.ts +6 -4
- package/src/__tests__/export-service.test.ts +142 -9
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +2 -0
- package/src/__tests__/import-site-command.test.ts +65 -0
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/__tests__/vite-config.test.ts +45 -0
- package/src/app.tsx +16 -3
- package/src/client/__tests__/collection-created-notice.test.ts +36 -0
- package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
- package/src/client/__tests__/collection-page-actions.test.ts +71 -2
- package/src/client/__tests__/compose-bridge.test.ts +316 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
- package/src/client/__tests__/feed-video-player.test.ts +16 -1
- package/src/client/__tests__/footnote-rail.test.ts +472 -0
- package/src/client/__tests__/hydrate-partial.test.ts +3 -0
- package/src/client/__tests__/toast.test.ts +82 -1
- package/src/client/collection-created-notice.ts +66 -0
- package/src/client/collection-form-bridge.ts +17 -0
- package/src/client/collection-navigation.ts +44 -0
- package/src/client/collection-page-actions.ts +57 -1
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
- package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
- package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
- package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
- package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
- 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-manager-types.ts +9 -3
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +55 -3
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +14 -7
- package/src/client/components/jant-compose-dialog.ts +1542 -694
- package/src/client/components/jant-compose-editor.ts +237 -99
- package/src/client/components/jant-compose-fullscreen.ts +27 -9
- package/src/client/components/jant-config-editor.ts +722 -0
- package/src/client/components/jant-media-lightbox.ts +100 -18
- package/src/client/components/jant-nav-manager.ts +1283 -54
- package/src/client/components/jant-post-form.ts +3 -3
- package/src/client/components/jant-post-menu.ts +177 -95
- package/src/client/components/nav-manager-types.ts +51 -2
- package/src/client/components/post-form-types.ts +1 -1
- package/src/client/compose-bridge.ts +147 -123
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -1
- package/src/client/feed-video-player.ts +62 -2
- package/src/client/footnote-rail.ts +478 -0
- package/src/client/hydrate-partial.ts +2 -0
- package/src/client/media-lightbox-events.ts +63 -0
- package/src/client/post-refresh.ts +135 -0
- package/src/client/site-header-fragment.ts +38 -0
- package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
- package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
- package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
- package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
- package/src/client/tiptap/bubble-menu.ts +88 -10
- package/src/client/tiptap/continuous-lists.ts +102 -0
- package/src/client/tiptap/create-editor.ts +3 -0
- package/src/client/tiptap/extensions.ts +14 -5
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/tiptap/markdown-clipboard.ts +608 -11
- package/src/client/tiptap/slash-commands.ts +8 -1
- package/src/client/tiptap/structural-keymap.ts +211 -0
- package/src/client/tiptap/tab-indent.ts +8 -22
- package/src/client/tiptap/table-control-labels.ts +30 -0
- package/src/client/tiptap/table-controls.ts +537 -0
- package/src/client/tiptap/table-size-picker.ts +254 -0
- package/src/client/toast.ts +133 -39
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/d1-query.test.ts +43 -1
- package/src/db/__tests__/migration-runner.test.ts +63 -0
- package/src/db/__tests__/migrations.test.ts +469 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/migrations/0027_old_blue_blade.sql +39 -0
- package/src/db/migrations/0028_superb_post.sql +2 -0
- package/src/db/migrations/0029_boring_magma.sql +1 -0
- package/src/db/migrations/0030_typical_vivisector.sql +50 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/meta/0027_snapshot.json +2511 -0
- package/src/db/migrations/meta/0028_snapshot.json +2511 -0
- package/src/db/migrations/meta/0029_snapshot.json +2519 -0
- package/src/db/migrations/meta/0030_snapshot.json +2541 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +35 -0
- package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
- package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
- package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
- package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
- package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +35 -0
- package/src/db/pg/schema.ts +46 -20
- package/src/db/rehearsal-fixtures/demo-current.json +39 -4
- package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
- package/src/db/schema.ts +48 -20
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +222 -51
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +216 -45
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +216 -45
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +513 -10
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +515 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +515 -12
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/api-settings.test.ts +299 -0
- package/src/lib/__tests__/constants.test.ts +5 -0
- package/src/lib/__tests__/feed-policy.test.ts +25 -0
- package/src/lib/__tests__/feed.test.ts +168 -0
- package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
- package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
- package/src/lib/__tests__/markdown.test.ts +24 -6
- package/src/lib/__tests__/navigation.test.ts +59 -2
- package/src/lib/__tests__/post-body-html.test.ts +176 -0
- package/src/lib/__tests__/post-display.test.ts +64 -0
- package/src/lib/__tests__/resolve-config.test.ts +113 -1
- package/src/lib/__tests__/slug.test.ts +18 -0
- package/src/lib/__tests__/summary.test.ts +82 -0
- package/src/lib/__tests__/timeline.test.ts +109 -70
- package/src/lib/__tests__/timezones.test.ts +31 -3
- package/src/lib/__tests__/tiptap-render.test.ts +268 -9
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -0
- package/src/lib/__tests__/view.test.ts +53 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
- package/src/lib/api-settings.ts +239 -12
- package/src/lib/collection-sort.ts +7 -5
- package/src/lib/constants.ts +2 -0
- package/src/lib/feed-path.ts +25 -0
- package/src/lib/feed-policy.ts +64 -0
- package/src/lib/feed.ts +113 -11
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +15 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +193 -5
- package/src/lib/markdown.ts +10 -3
- package/src/lib/navigation.ts +16 -3
- package/src/lib/post-body-html.ts +131 -0
- package/src/lib/post-display.ts +47 -8
- package/src/lib/render.tsx +9 -2
- package/src/lib/resolve-config.ts +100 -16
- package/src/lib/schemas.ts +133 -1
- package/src/lib/settings-paths.ts +5 -0
- package/src/lib/site-skill.ts +78 -0
- package/src/lib/summary.ts +16 -5
- package/src/lib/timeline.ts +137 -131
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +359 -83
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +58 -20
- package/src/lib/worker-response-cache.ts +11 -18
- package/src/middleware/__tests__/public-content-access.test.ts +91 -0
- package/src/middleware/config.ts +2 -2
- package/src/middleware/public-content-access.ts +87 -0
- package/src/node/__tests__/cli-export.test.ts +14 -0
- package/src/node/__tests__/cli-migrate.test.ts +6 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
- package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
- package/src/node/index.ts +2 -1
- package/src/preset.css +16 -2
- package/src/routes/__tests__/compose.test.ts +51 -0
- package/src/routes/__tests__/site-skill.test.ts +112 -0
- package/src/routes/api/__tests__/collections.test.ts +91 -19
- package/src/routes/api/__tests__/mcp.test.ts +65 -1
- package/src/routes/api/__tests__/nav-items.test.ts +91 -0
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/__tests__/public-access.test.ts +81 -0
- package/src/routes/api/__tests__/settings.test.ts +217 -0
- package/src/routes/api/collections.ts +22 -26
- package/src/routes/api/export.ts +2 -0
- package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
- package/src/routes/api/internal/post-body-html.ts +43 -0
- package/src/routes/api/internal/sites.ts +24 -0
- package/src/routes/api/nav-items.ts +24 -1
- package/src/routes/api/posts.ts +14 -10
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +246 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +48 -24
- package/src/routes/api/search.ts +3 -0
- package/src/routes/api/settings.ts +33 -0
- package/src/routes/auth/__tests__/setup.test.ts +7 -4
- package/src/routes/compose.tsx +8 -3
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +384 -3
- package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
- package/src/routes/pages/__tests__/collections.test.ts +9 -9
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +176 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +90 -22
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +86 -11
- package/src/routes/site-skill.ts +18 -0
- package/src/services/__tests__/auth.test.ts +7 -5
- package/src/services/__tests__/collection.test.ts +190 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +249 -39
- package/src/services/__tests__/post.test.ts +844 -5
- package/src/services/__tests__/search.test.ts +48 -1
- package/src/services/__tests__/settings.test.ts +8 -0
- package/src/services/__tests__/site-admin.test.ts +121 -1
- package/src/services/auth.ts +2 -2
- package/src/services/bootstrap.ts +3 -2
- package/src/services/collection.ts +224 -167
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +46 -25
- package/src/services/export-theme/layouts/_default/list.html +18 -4
- package/src/services/export-theme/layouts/_default/rss.xml +8 -12
- package/src/services/export-theme/layouts/featured/list.html +5 -4
- package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
- package/src/services/export-theme/styles/main.css +30 -2
- package/src/services/export.ts +94 -85
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +1142 -342
- package/src/services/search.ts +17 -2
- package/src/services/settings.ts +2 -10
- package/src/services/site-admin.ts +63 -43
- package/src/style-cjk-jp.css +1 -1
- package/src/style-cjk-kr.css +1 -1
- package/src/style-cjk-tc.css +1 -1
- package/src/style-cjk.css +1 -1
- package/src/styles/components.css +540 -14
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +76 -10
- package/src/styles/ui.css +2227 -1347
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +243 -12
- package/src/types/constants.ts +22 -1
- package/src/types/entities.ts +14 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +20 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +22 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +109 -19
- package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
- package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
- package/src/ui/dash/settings/GeneralContent.tsx +15 -5
- package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
- package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
- package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
- package/src/ui/feed/LinkCard.tsx +2 -12
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- package/src/ui/feed/QuoteCard.tsx +2 -12
- package/src/ui/feed/ThreadPreview.tsx +26 -34
- package/src/ui/feed/TimelineFeed.tsx +2 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
- package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
- package/src/ui/feed/thread-preview-state.ts +5 -7
- package/src/ui/font-themes.ts +74 -26
- package/src/ui/layouts/BaseLayout.tsx +22 -8
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
- package/src/ui/pages/ArchivePage.tsx +131 -24
- package/src/ui/pages/CollectionPage.tsx +101 -22
- package/src/ui/pages/CollectionsPage.tsx +3 -1
- package/src/ui/pages/PostPage.tsx +39 -12
- package/src/ui/pages/SearchPage.tsx +6 -14
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +70 -0
- package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -9
- package/src/ui/shared/CollectionsManager.tsx +17 -7
- package/src/ui/shared/DraftPreviewBar.tsx +49 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +38 -13
- package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
- package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
- package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
- package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
- package/src/ui/shared/collection-management-labels.ts +33 -2
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +52 -0
- package/dist/app-DZDlmh7C.js +0 -6
- package/dist/client/_assets/client-Cevfpm8H.css +0 -2
- package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
- package/src/client/tiptap/wrapping-input-rules.ts +0 -102
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
2
3
|
import { createTestApp } from "../../../../__tests__/helpers/app.js";
|
|
4
|
+
import { posts } from "../../../../db/schema.js";
|
|
3
5
|
import { publicPostsApiRoutes } from "../posts.js";
|
|
4
6
|
|
|
5
7
|
describe("Public Posts API Routes", () => {
|
|
@@ -51,6 +53,44 @@ describe("Public Posts API Routes", () => {
|
|
|
51
53
|
expect(body.posts[0]).not.toHaveProperty("body");
|
|
52
54
|
});
|
|
53
55
|
|
|
56
|
+
// Consumers need both axes: `lastActivityAt` answers "when was this
|
|
57
|
+
// announced", `threadUpdatedAt` answers "when did it actually change".
|
|
58
|
+
// Exposing only the first left no way to tell a quiet addition happened.
|
|
59
|
+
it("reports both thread activity timestamps", async () => {
|
|
60
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
61
|
+
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
62
|
+
|
|
63
|
+
const root = await services.posts.create({
|
|
64
|
+
format: "note",
|
|
65
|
+
title: "Quietly extended",
|
|
66
|
+
bodyMarkdown: "root",
|
|
67
|
+
publishedAt: 1000,
|
|
68
|
+
});
|
|
69
|
+
await services.posts.create({
|
|
70
|
+
format: "note",
|
|
71
|
+
bodyMarkdown: "quiet addendum",
|
|
72
|
+
replyToId: root.id,
|
|
73
|
+
publishedAt: 3000,
|
|
74
|
+
quietReply: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const res = await app.request("/api/public/posts");
|
|
78
|
+
expect(res.status).toBe(200);
|
|
79
|
+
const body = (await res.json()) as {
|
|
80
|
+
posts: {
|
|
81
|
+
title: string | null;
|
|
82
|
+
quietReply: boolean;
|
|
83
|
+
lastActivityAt: number;
|
|
84
|
+
threadUpdatedAt: number;
|
|
85
|
+
}[];
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const entry = body.posts.find((p) => p.title === "Quietly extended");
|
|
89
|
+
expect(entry?.lastActivityAt).toBe(1000);
|
|
90
|
+
expect(entry?.threadUpdatedAt).toBe(3000);
|
|
91
|
+
expect(entry?.quietReply).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
54
94
|
it("supports format and limit filters", async () => {
|
|
55
95
|
const { app, services } = createTestApp({ authenticated: false });
|
|
56
96
|
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
@@ -100,6 +140,32 @@ describe("Public Posts API Routes", () => {
|
|
|
100
140
|
expect(body.posts[0]).not.toHaveProperty("bodyText");
|
|
101
141
|
});
|
|
102
142
|
|
|
143
|
+
it("returns canonical v3 HTML when the stored projection is stale", async () => {
|
|
144
|
+
const { app, services, db } = createTestApp({ authenticated: false });
|
|
145
|
+
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
146
|
+
const post = await services.posts.create({
|
|
147
|
+
format: "note",
|
|
148
|
+
title: "Footnote API",
|
|
149
|
+
bodyMarkdown: "API body[^1]\n\n[^1]: API definition",
|
|
150
|
+
});
|
|
151
|
+
await db
|
|
152
|
+
.update(posts)
|
|
153
|
+
.set({
|
|
154
|
+
bodyHtml: '<span class="sidenote">legacy</span>',
|
|
155
|
+
bodyHtmlVersion: 1,
|
|
156
|
+
})
|
|
157
|
+
.where(eq(posts.id, post.id));
|
|
158
|
+
|
|
159
|
+
const response = await app.request("/api/public/posts");
|
|
160
|
+
const body = await response.json();
|
|
161
|
+
|
|
162
|
+
expect(response.status).toBe(200);
|
|
163
|
+
expect(body.posts[0].bodyHtml).toContain('role="doc-noteref"');
|
|
164
|
+
expect(body.posts[0].bodyHtml).toMatch(/id="fn-[a-z0-9]{13}-1"/);
|
|
165
|
+
expect(body.posts[0].bodyHtml).not.toContain(post.id);
|
|
166
|
+
expect(body.posts[0].bodyHtml).not.toContain("legacy");
|
|
167
|
+
});
|
|
168
|
+
|
|
103
169
|
it("filters posts by a single collection slug", async () => {
|
|
104
170
|
const { app, services } = createTestApp({ authenticated: false });
|
|
105
171
|
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
@@ -194,12 +260,20 @@ describe("Public Posts API Routes", () => {
|
|
|
194
260
|
title: "Older",
|
|
195
261
|
bodyMarkdown: "first",
|
|
196
262
|
collectionIds: [collection.id],
|
|
263
|
+
publishedAt: 1000,
|
|
197
264
|
});
|
|
198
265
|
const newer = await services.posts.create({
|
|
199
266
|
format: "note",
|
|
200
267
|
title: "Newer",
|
|
201
268
|
bodyMarkdown: "second",
|
|
202
269
|
collectionIds: [collection.id],
|
|
270
|
+
publishedAt: 2000,
|
|
271
|
+
});
|
|
272
|
+
await services.posts.create({
|
|
273
|
+
format: "note",
|
|
274
|
+
bodyMarkdown: "Later reply to the older Thread",
|
|
275
|
+
replyToId: older.id,
|
|
276
|
+
publishedAt: 3000,
|
|
203
277
|
});
|
|
204
278
|
|
|
205
279
|
const res = await app.request(
|
|
@@ -213,6 +287,145 @@ describe("Public Posts API Routes", () => {
|
|
|
213
287
|
expect(body.posts[1].id).toBe(newer.id);
|
|
214
288
|
});
|
|
215
289
|
|
|
290
|
+
it("sorts collection results by Thread activity from Child Posts", async () => {
|
|
291
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
292
|
+
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
293
|
+
|
|
294
|
+
const collection = await services.collections.create({
|
|
295
|
+
slug: "active-threads",
|
|
296
|
+
title: "Active Threads",
|
|
297
|
+
});
|
|
298
|
+
const olderRoot = await services.posts.create({
|
|
299
|
+
format: "note",
|
|
300
|
+
title: "Older root",
|
|
301
|
+
bodyMarkdown: "older",
|
|
302
|
+
collectionIds: [collection.id],
|
|
303
|
+
publishedAt: 1000,
|
|
304
|
+
});
|
|
305
|
+
const newerRoot = await services.posts.create({
|
|
306
|
+
format: "note",
|
|
307
|
+
title: "Newer root",
|
|
308
|
+
bodyMarkdown: "newer",
|
|
309
|
+
collectionIds: [collection.id],
|
|
310
|
+
publishedAt: 2000,
|
|
311
|
+
});
|
|
312
|
+
await services.posts.create({
|
|
313
|
+
format: "note",
|
|
314
|
+
bodyMarkdown: "new activity",
|
|
315
|
+
replyToId: olderRoot.id,
|
|
316
|
+
publishedAt: 3000,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const res = await app.request(
|
|
320
|
+
"/api/public/posts?collection=active-threads&sort=newest",
|
|
321
|
+
);
|
|
322
|
+
expect(res.status).toBe(200);
|
|
323
|
+
|
|
324
|
+
const body = await res.json();
|
|
325
|
+
expect(body.posts.map((post: { id: string }) => post.id)).toEqual([
|
|
326
|
+
olderRoot.id,
|
|
327
|
+
newerRoot.id,
|
|
328
|
+
]);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("sorts and cursor-paginates collection Threads by Child ratings", async () => {
|
|
332
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
333
|
+
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
334
|
+
|
|
335
|
+
const collection = await services.collections.create({
|
|
336
|
+
slug: "rated-threads",
|
|
337
|
+
title: "Rated Threads",
|
|
338
|
+
sortOrder: "rating_desc",
|
|
339
|
+
});
|
|
340
|
+
const createRatedThread = async (
|
|
341
|
+
title: string,
|
|
342
|
+
publishedAt: number,
|
|
343
|
+
rating: number,
|
|
344
|
+
) => {
|
|
345
|
+
const root = await services.posts.create({
|
|
346
|
+
format: "note",
|
|
347
|
+
title,
|
|
348
|
+
bodyMarkdown: `${title} root`,
|
|
349
|
+
collectionIds: [collection.id],
|
|
350
|
+
publishedAt,
|
|
351
|
+
});
|
|
352
|
+
await services.posts.create({
|
|
353
|
+
format: "note",
|
|
354
|
+
bodyMarkdown: `${title} rated Child`,
|
|
355
|
+
replyToId: root.id,
|
|
356
|
+
publishedAt: publishedAt + 100,
|
|
357
|
+
rating,
|
|
358
|
+
});
|
|
359
|
+
return root;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const highest = await createRatedThread("Highest", 1000, 5);
|
|
363
|
+
const middle = await createRatedThread("Middle", 2000, 3);
|
|
364
|
+
const lowest = await createRatedThread("Lowest", 3000, 1);
|
|
365
|
+
|
|
366
|
+
const firstRes = await app.request(
|
|
367
|
+
"/api/public/posts?collection=rated-threads&limit=1",
|
|
368
|
+
);
|
|
369
|
+
expect(firstRes.status).toBe(200);
|
|
370
|
+
const first = await firstRes.json();
|
|
371
|
+
expect(first.posts[0].id).toBe(highest.id);
|
|
372
|
+
expect(first.nextCursor).toBe(highest.id);
|
|
373
|
+
|
|
374
|
+
const secondRes = await app.request(
|
|
375
|
+
`/api/public/posts?collection=rated-threads&limit=1&cursor=${first.nextCursor}`,
|
|
376
|
+
);
|
|
377
|
+
expect(secondRes.status).toBe(200);
|
|
378
|
+
const second = await secondRes.json();
|
|
379
|
+
expect(second.posts[0].id).toBe(middle.id);
|
|
380
|
+
expect(second.nextCursor).toBe(middle.id);
|
|
381
|
+
|
|
382
|
+
const thirdRes = await app.request(
|
|
383
|
+
`/api/public/posts?collection=rated-threads&limit=1&cursor=${second.nextCursor}`,
|
|
384
|
+
);
|
|
385
|
+
expect(thirdRes.status).toBe(200);
|
|
386
|
+
const third = await thirdRes.json();
|
|
387
|
+
expect(third.posts[0].id).toBe(lowest.id);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("keeps root format and Latest visibility filters for Collections", async () => {
|
|
391
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
392
|
+
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
393
|
+
|
|
394
|
+
const collection = await services.collections.create({
|
|
395
|
+
slug: "mixed",
|
|
396
|
+
title: "Mixed",
|
|
397
|
+
});
|
|
398
|
+
const note = await services.posts.create({
|
|
399
|
+
format: "note",
|
|
400
|
+
title: "Visible note",
|
|
401
|
+
bodyMarkdown: "note",
|
|
402
|
+
collectionIds: [collection.id],
|
|
403
|
+
});
|
|
404
|
+
await services.posts.create({
|
|
405
|
+
format: "link",
|
|
406
|
+
title: "Visible link",
|
|
407
|
+
url: "https://example.com",
|
|
408
|
+
collectionIds: [collection.id],
|
|
409
|
+
});
|
|
410
|
+
await services.posts.create({
|
|
411
|
+
format: "note",
|
|
412
|
+
title: "Hidden note",
|
|
413
|
+
bodyMarkdown: "hidden",
|
|
414
|
+
visibility: "latest_hidden",
|
|
415
|
+
collectionIds: [collection.id],
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const res = await app.request(
|
|
419
|
+
"/api/public/posts?collection=mixed&format=note",
|
|
420
|
+
);
|
|
421
|
+
expect(res.status).toBe(200);
|
|
422
|
+
|
|
423
|
+
const body = await res.json();
|
|
424
|
+
expect(body.posts.map((post: { id: string }) => post.id)).toEqual([
|
|
425
|
+
note.id,
|
|
426
|
+
]);
|
|
427
|
+
});
|
|
428
|
+
|
|
216
429
|
it("allows overriding collection sort order via sort parameter", async () => {
|
|
217
430
|
const { app, services } = createTestApp({ authenticated: false });
|
|
218
431
|
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
@@ -282,6 +495,39 @@ describe("Public Posts API Routes", () => {
|
|
|
282
495
|
expect(body).not.toHaveProperty("body");
|
|
283
496
|
});
|
|
284
497
|
|
|
498
|
+
it("returns the shared Thread Collections for a child post", async () => {
|
|
499
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
500
|
+
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
501
|
+
|
|
502
|
+
const collection = await services.collections.create({
|
|
503
|
+
slug: "shared",
|
|
504
|
+
title: "Shared",
|
|
505
|
+
});
|
|
506
|
+
const root = await services.posts.create({
|
|
507
|
+
format: "note",
|
|
508
|
+
bodyMarkdown: "root",
|
|
509
|
+
collectionIds: [collection.id],
|
|
510
|
+
});
|
|
511
|
+
const child = await services.posts.create({
|
|
512
|
+
format: "note",
|
|
513
|
+
bodyMarkdown: "child",
|
|
514
|
+
replyToId: root.id,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const res = await app.request(`/api/public/posts/${child.slug}`);
|
|
518
|
+
expect(res.status).toBe(200);
|
|
519
|
+
await expect(res.json()).resolves.toMatchObject({
|
|
520
|
+
id: child.id,
|
|
521
|
+
collections: [
|
|
522
|
+
{
|
|
523
|
+
id: collection.id,
|
|
524
|
+
slug: "shared",
|
|
525
|
+
title: "Shared",
|
|
526
|
+
},
|
|
527
|
+
],
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
285
531
|
it("returns markdown instead of rendered fields when content=markdown", async () => {
|
|
286
532
|
const { app, services } = createTestApp({ authenticated: false });
|
|
287
533
|
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
@@ -5,11 +5,14 @@ import type { AppVariables } from "../../../types/app-context.js";
|
|
|
5
5
|
import { MEDIA_KINDS } from "../../../types.js";
|
|
6
6
|
import { FormatSchema, parseValidated } from "../../../lib/schemas.js";
|
|
7
7
|
import { toPublicPost } from "./posts.js";
|
|
8
|
+
import { requirePublicApiEnabled } from "../../../middleware/public-content-access.js";
|
|
8
9
|
|
|
9
10
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
10
11
|
|
|
11
12
|
export const publicArchiveApiRoutes = new Hono<Env>();
|
|
12
13
|
|
|
14
|
+
publicArchiveApiRoutes.use("*", requirePublicApiEnabled());
|
|
15
|
+
|
|
13
16
|
const MediaKindSchema = z.enum(MEDIA_KINDS);
|
|
14
17
|
const MEDIA_KIND_LIST = MEDIA_KINDS.join(", ");
|
|
15
18
|
const INVALID_MEDIA_MESSAGE =
|
|
@@ -108,6 +111,7 @@ publicArchiveApiRoutes.get("/", async (c) => {
|
|
|
108
111
|
hasMedia: mediaPresence ?? hasMedia,
|
|
109
112
|
hasTitle: title ?? hasTitle,
|
|
110
113
|
hasReplies: replies,
|
|
114
|
+
ignorePinnedSort: true,
|
|
111
115
|
});
|
|
112
116
|
|
|
113
117
|
const postIds = posts.map((post) => post.id);
|
|
@@ -21,11 +21,14 @@ import {
|
|
|
21
21
|
resolveCollectionSortOrder,
|
|
22
22
|
supportsCollectionRatingSort,
|
|
23
23
|
} from "../../../lib/collection-sort.js";
|
|
24
|
+
import { requirePublicApiEnabled } from "../../../middleware/public-content-access.js";
|
|
24
25
|
|
|
25
26
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
26
27
|
|
|
27
28
|
export const publicPostsApiRoutes = new Hono<Env>();
|
|
28
29
|
|
|
30
|
+
publicPostsApiRoutes.use("*", requirePublicApiEnabled());
|
|
31
|
+
|
|
29
32
|
const ListPublicPostsQuerySchema = z.object({
|
|
30
33
|
format: FormatSchema.optional(),
|
|
31
34
|
collection: z.string().optional(),
|
|
@@ -58,10 +61,15 @@ export type PublicPostBaseResponse = {
|
|
|
58
61
|
previewImageUrl: string | null;
|
|
59
62
|
replyToId: string | null;
|
|
60
63
|
threadId: string;
|
|
64
|
+
/** Reply published without announcing its Thread. Always false on roots. */
|
|
65
|
+
quietReply: boolean;
|
|
61
66
|
pinnedAt: number | null;
|
|
62
67
|
featuredAt: number | null;
|
|
63
68
|
publishedAt: number | null;
|
|
69
|
+
/** Root only: newest post in the Thread, quiet replies excluded. */
|
|
64
70
|
lastActivityAt: number;
|
|
71
|
+
/** Root only: newest post in the Thread, quiet replies included. */
|
|
72
|
+
threadUpdatedAt: number;
|
|
65
73
|
createdAt: number;
|
|
66
74
|
updatedAt: number;
|
|
67
75
|
attachments: ReturnType<typeof toApiAttachment>[];
|
|
@@ -97,7 +105,7 @@ function isPublicDetailVisible(post: Post | null): post is Post {
|
|
|
97
105
|
export function toPublicPost(
|
|
98
106
|
post: Post,
|
|
99
107
|
mediaList: Media[],
|
|
100
|
-
|
|
108
|
+
threadCollections: Collection[],
|
|
101
109
|
appConfig: AppVariables["appConfig"],
|
|
102
110
|
options?: { content?: "markdown" },
|
|
103
111
|
): PublicPostResponse {
|
|
@@ -143,10 +151,12 @@ export function toPublicPost(
|
|
|
143
151
|
previewImageUrl,
|
|
144
152
|
replyToId: post.replyToId,
|
|
145
153
|
threadId: post.threadId,
|
|
154
|
+
quietReply: post.quietReply,
|
|
146
155
|
pinnedAt: post.pinnedAt,
|
|
147
156
|
featuredAt: post.featuredAt,
|
|
148
157
|
publishedAt: post.publishedAt,
|
|
149
158
|
lastActivityAt: post.lastActivityAt,
|
|
159
|
+
threadUpdatedAt: post.threadUpdatedAt,
|
|
150
160
|
createdAt: post.createdAt,
|
|
151
161
|
updatedAt: post.updatedAt,
|
|
152
162
|
attachments: mediaList.map((media) =>
|
|
@@ -159,7 +169,7 @@ export function toPublicPost(
|
|
|
159
169
|
sitePathPrefix,
|
|
160
170
|
),
|
|
161
171
|
),
|
|
162
|
-
collections:
|
|
172
|
+
collections: threadCollections.map((collection) => ({
|
|
163
173
|
id: collection.id,
|
|
164
174
|
slug: collection.slug,
|
|
165
175
|
title: collection.title,
|
|
@@ -226,16 +236,19 @@ publicPostsApiRoutes.get("/", async (c) => {
|
|
|
226
236
|
? "newest"
|
|
227
237
|
: primaryCollection.sortOrder;
|
|
228
238
|
|
|
229
|
-
const
|
|
230
|
-
|
|
239
|
+
const ratedThreadCount =
|
|
240
|
+
await c.var.services.posts.countCollectionThreadRootsUpToForCollections(
|
|
231
241
|
collectionIds,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
242
|
+
{
|
|
243
|
+
status: "published",
|
|
244
|
+
excludePrivate: true,
|
|
245
|
+
excludeLatestHidden: true,
|
|
246
|
+
rootFormat: format,
|
|
247
|
+
hasRating: true,
|
|
248
|
+
},
|
|
249
|
+
2,
|
|
250
|
+
);
|
|
251
|
+
const showRatingSort = supportsCollectionRatingSort(ratedThreadCount);
|
|
239
252
|
const defaultSort = resolveCollectionSortOrder(
|
|
240
253
|
undefined,
|
|
241
254
|
requestedDefaultSort,
|
|
@@ -244,17 +257,28 @@ publicPostsApiRoutes.get("/", async (c) => {
|
|
|
244
257
|
sortOrder = resolveCollectionSortOrder(sort, defaultSort, showRatingSort);
|
|
245
258
|
}
|
|
246
259
|
|
|
247
|
-
const posts =
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
260
|
+
const posts = collectionIds
|
|
261
|
+
? await c.var.services.posts.listCollectionThreadRootsForCollections(
|
|
262
|
+
collectionIds,
|
|
263
|
+
{
|
|
264
|
+
status: "published",
|
|
265
|
+
excludePrivate: true,
|
|
266
|
+
excludeLatestHidden: true,
|
|
267
|
+
rootFormat: format,
|
|
268
|
+
sortOrder,
|
|
269
|
+
cursor: cursor ?? undefined,
|
|
270
|
+
limit,
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
: await c.var.services.posts.list({
|
|
274
|
+
format,
|
|
275
|
+
status: "published",
|
|
276
|
+
cursor: cursor ?? undefined,
|
|
277
|
+
limit,
|
|
278
|
+
excludePrivate: true,
|
|
279
|
+
excludeLatestHidden: true,
|
|
280
|
+
excludeReplies: true,
|
|
281
|
+
});
|
|
258
282
|
|
|
259
283
|
const postIds = posts.map((post) => post.id);
|
|
260
284
|
const [mediaMap, collectionsMap] = await Promise.all([
|
|
@@ -289,13 +313,13 @@ publicPostsApiRoutes.get("/:slug", async (c) => {
|
|
|
289
313
|
throw new NotFoundError("Post");
|
|
290
314
|
}
|
|
291
315
|
|
|
292
|
-
const [mediaList,
|
|
316
|
+
const [mediaList, threadCollections] = await Promise.all([
|
|
293
317
|
c.var.services.media.getByPostId(post.id),
|
|
294
318
|
c.var.services.collections.getCollectionsByPostId(post.id),
|
|
295
319
|
]);
|
|
296
320
|
|
|
297
321
|
return c.json(
|
|
298
|
-
toPublicPost(post, mediaList,
|
|
322
|
+
toPublicPost(post, mediaList, threadCollections, c.var.appConfig, {
|
|
299
323
|
content,
|
|
300
324
|
}),
|
|
301
325
|
);
|
package/src/routes/api/search.ts
CHANGED
|
@@ -8,11 +8,14 @@ import type { AppVariables } from "../../types/app-context.js";
|
|
|
8
8
|
import { ValidationError, ExternalServiceError } from "../../lib/errors.js";
|
|
9
9
|
import { toSearchApiResult } from "../../lib/api-search.js";
|
|
10
10
|
import { rateLimit } from "../../middleware/rate-limit.js";
|
|
11
|
+
import { requirePublicApiAccess } from "../../middleware/public-content-access.js";
|
|
11
12
|
|
|
12
13
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
13
14
|
|
|
14
15
|
export const searchApiRoutes = new Hono<Env>();
|
|
15
16
|
|
|
17
|
+
searchApiRoutes.use("*", requirePublicApiAccess());
|
|
18
|
+
|
|
16
19
|
// Per-IP rate limit. The request-time wrapper is needed because the
|
|
17
20
|
// per-minute cap is pulled from `appConfig` which is only available on
|
|
18
21
|
// `c.var`; constructing the middleware once at module load would capture
|
|
@@ -13,7 +13,10 @@ import { parseValidated } from "../../lib/schemas.js";
|
|
|
13
13
|
import { ValidationError } from "../../lib/errors.js";
|
|
14
14
|
import { syncHostedControlPlaneSiteAvatar } from "../../lib/hosted-control-plane-sync.js";
|
|
15
15
|
import {
|
|
16
|
+
buildConfigEditorFields,
|
|
16
17
|
buildEditableSettingsResponse,
|
|
18
|
+
demoLockedSettingKeys,
|
|
19
|
+
isResettableConfigEditorKey,
|
|
17
20
|
partitionEditableSettingUpdates,
|
|
18
21
|
partitionImportableSettingUpdates,
|
|
19
22
|
} from "../../lib/api-settings.js";
|
|
@@ -31,6 +34,7 @@ settingsApiRoutes.get("/", requireAuthApi(), async (c) => {
|
|
|
31
34
|
settings: buildEditableSettingsResponse(
|
|
32
35
|
allSettings,
|
|
33
36
|
c.var.appConfig.demoMode,
|
|
37
|
+
c.env,
|
|
34
38
|
),
|
|
35
39
|
});
|
|
36
40
|
});
|
|
@@ -61,6 +65,7 @@ settingsApiRoutes.put("/", requireAuthApi(), async (c) => {
|
|
|
61
65
|
settings: buildEditableSettingsResponse(
|
|
62
66
|
allSettings,
|
|
63
67
|
c.var.appConfig.demoMode,
|
|
68
|
+
c.env,
|
|
64
69
|
),
|
|
65
70
|
...(rejectedKeys.length > 0 && { rejectedKeys }),
|
|
66
71
|
});
|
|
@@ -216,3 +221,31 @@ settingsApiRoutes.delete("/avatar", requireAuthApi(), async (c) => {
|
|
|
216
221
|
}
|
|
217
222
|
return c.json({ success: true });
|
|
218
223
|
});
|
|
224
|
+
|
|
225
|
+
// Remove one runtime override so the environment/default fallback applies.
|
|
226
|
+
// Registered after the concrete avatar route so `DELETE /avatar` remains
|
|
227
|
+
// unambiguous.
|
|
228
|
+
settingsApiRoutes.delete("/:key", requireAuthApi(), async (c) => {
|
|
229
|
+
const key = c.req.param("key");
|
|
230
|
+
const demoMode = c.var.appConfig.demoMode;
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
!isResettableConfigEditorKey(key) ||
|
|
234
|
+
(demoMode && demoLockedSettingKeys.has(key))
|
|
235
|
+
) {
|
|
236
|
+
throw new ValidationError("This setting can't be reset.", {
|
|
237
|
+
rejectedKeys: [key],
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
await c.var.services.settings.remove(key);
|
|
242
|
+
const allSettings = await c.var.services.settings.getAll();
|
|
243
|
+
const setting = buildConfigEditorFields(allSettings, c.env, demoMode).find(
|
|
244
|
+
(field) => field.key === key,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
return c.json({
|
|
248
|
+
settings: buildEditableSettingsResponse(allSettings, demoMode, c.env),
|
|
249
|
+
setting,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -38,14 +38,13 @@ describe("Setup bootstrap logic", () => {
|
|
|
38
38
|
};
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
it("creates
|
|
41
|
+
it("creates the five-item default navigation without Latest", async () => {
|
|
42
42
|
await runSetupBootstrap(services);
|
|
43
43
|
|
|
44
44
|
const navItemsList = await services.db.select().from(navItems);
|
|
45
|
-
expect(navItemsList).toHaveLength(
|
|
45
|
+
expect(navItemsList).toHaveLength(5);
|
|
46
46
|
|
|
47
47
|
expect(navItemsList.map((item) => item.systemKey)).toEqual([
|
|
48
|
-
"latest",
|
|
49
48
|
"featured",
|
|
50
49
|
"collections",
|
|
51
50
|
"archive",
|
|
@@ -125,7 +124,11 @@ describe("Setup bootstrap logic", () => {
|
|
|
125
124
|
const navItemsList = await services.db.select().from(navItems);
|
|
126
125
|
const systemItems = navItemsList.filter((item) => item.type === "system");
|
|
127
126
|
|
|
128
|
-
expect(systemItems).toHaveLength(
|
|
127
|
+
expect(systemItems).toHaveLength(5);
|
|
128
|
+
expect(systemItems.map((item) => item.systemKey)).not.toContain("latest");
|
|
129
|
+
expect(
|
|
130
|
+
systemItems.find((item) => item.systemKey === "collections")?.label,
|
|
131
|
+
).toBe("Collections");
|
|
129
132
|
});
|
|
130
133
|
|
|
131
134
|
it("creates a site shell when setup runs after a factory reset", async () => {
|
package/src/routes/compose.tsx
CHANGED
|
@@ -211,7 +211,9 @@ composeRoutes.post("/thread", async (c) => {
|
|
|
211
211
|
postSchemas.map((data, index) => ({
|
|
212
212
|
data: {
|
|
213
213
|
format: data.format,
|
|
214
|
-
|
|
214
|
+
// Every post may carry its own permalink; blank means the service
|
|
215
|
+
// generates one (title-derived for the root, random id for replies).
|
|
216
|
+
slug: data.slug || undefined,
|
|
215
217
|
title:
|
|
216
218
|
data.format === "quote"
|
|
217
219
|
? data.sourceName || undefined
|
|
@@ -226,9 +228,12 @@ composeRoutes.post("/thread", async (c) => {
|
|
|
226
228
|
: data.url || undefined,
|
|
227
229
|
quoteText: data.quoteText || undefined,
|
|
228
230
|
rating: data.rating || undefined,
|
|
229
|
-
collectionIds: index === 0 ? data.collectionIds :
|
|
231
|
+
collectionIds: index === 0 ? data.collectionIds : undefined,
|
|
230
232
|
replyToId: index === 0 ? data.replyToId : undefined,
|
|
231
|
-
|
|
233
|
+
quietReply: data.quietReply,
|
|
234
|
+
// Replies may carry their own date; when they don't, the service
|
|
235
|
+
// inherits the root's rather than stamping "now".
|
|
236
|
+
publishedAt: data.publishedAt,
|
|
232
237
|
},
|
|
233
238
|
attachments: data.attachments,
|
|
234
239
|
})),
|