@jant/core 0.6.14 → 0.6.15
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 +81 -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-BxCOR3Uc.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-B_eGKN5p.css +2 -0
- package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
- package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
- package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +5 -5
- 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__/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 +64 -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 +18 -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__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
- package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
- 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-types.ts +8 -0
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +2 -1
- package/src/client/components/jant-compose-dialog.ts +85 -21
- package/src/client/components/jant-compose-editor.ts +13 -7
- package/src/client/components/jant-compose-fullscreen.ts +24 -4
- 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 +86 -74
- 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 +4 -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/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__/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/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 +8 -6
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -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-collection-migration.test.ts +364 -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/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/_journal.json +28 -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/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/_journal.json +28 -0
- package/src/db/pg/schema.ts +28 -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 +28 -20
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +133 -30
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +128 -25
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +128 -25
- 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__/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 +8 -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__/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__/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/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +10 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/markdown-manager.ts +188 -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 +7 -1
- 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 +98 -122
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +355 -81
- package/src/lib/view.ts +30 -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__/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 +2 -2
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +208 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +41 -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 +2 -1
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +92 -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__/preview.test.ts +174 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +27 -7
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +83 -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 +159 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +109 -30
- package/src/services/__tests__/post.test.ts +567 -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 +226 -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 +91 -80
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/post.ts +874 -271
- package/src/services/search.ts +8 -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 -0
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +14 -9
- package/src/styles/ui.css +829 -29
- 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 +9 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +8 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +16 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +92 -2
- 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/NoteCard.tsx +10 -20
- 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/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 +7 -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 +33 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +3 -1
- 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/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +46 -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
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
import { createTestApp } from "../../../../__tests__/helpers/app.js";
|
|
4
|
+
import { posts, sites } from "../../../../db/schema.js";
|
|
5
|
+
import { POST_BODY_HTML_VERSION } from "../../../../lib/post-body-html.js";
|
|
6
|
+
import { createPostService } from "../../../../services/post.js";
|
|
7
|
+
import { internalPostBodyHtmlRoutes } from "../post-body-html.js";
|
|
8
|
+
import { internalSitesRoutes } from "../sites.js";
|
|
9
|
+
|
|
10
|
+
describe("Internal post body HTML rebuild routes", () => {
|
|
11
|
+
it("hides the current-site endpoint when no internal token is configured", async () => {
|
|
12
|
+
const { app } = createTestApp({ authenticated: false });
|
|
13
|
+
app.route("/api/internal/posts/body-html", internalPostBodyHtmlRoutes);
|
|
14
|
+
|
|
15
|
+
const response = await app.request(
|
|
16
|
+
"/api/internal/posts/body-html/rebuild",
|
|
17
|
+
{ method: "POST" },
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
expect(response.status).toBe(404);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("dry-runs and rebuilds the current site", async () => {
|
|
24
|
+
const { app, services, db } = createTestApp({
|
|
25
|
+
authenticated: false,
|
|
26
|
+
internalAdminToken: "internal-secret",
|
|
27
|
+
});
|
|
28
|
+
app.use("*", async (c, next) => {
|
|
29
|
+
c.set("appConfig", undefined as never);
|
|
30
|
+
await next();
|
|
31
|
+
});
|
|
32
|
+
app.route("/api/internal/posts/body-html", internalPostBodyHtmlRoutes);
|
|
33
|
+
const post = await services.posts.create({
|
|
34
|
+
format: "note",
|
|
35
|
+
bodyMarkdown: "Body[^1]\n\n[^1]: Definition",
|
|
36
|
+
});
|
|
37
|
+
await db
|
|
38
|
+
.update(posts)
|
|
39
|
+
.set({
|
|
40
|
+
bodyHtml: '<span class="sidenote">legacy</span>',
|
|
41
|
+
bodyHtmlVersion: 1,
|
|
42
|
+
})
|
|
43
|
+
.where(eq(posts.id, post.id));
|
|
44
|
+
|
|
45
|
+
const dryRun = await app.request("/api/internal/posts/body-html/rebuild", {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: {
|
|
48
|
+
Authorization: "Bearer internal-secret",
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify({ dryRun: true, limit: 25 }),
|
|
52
|
+
});
|
|
53
|
+
expect(dryRun.status).toBe(200);
|
|
54
|
+
await expect(dryRun.json()).resolves.toMatchObject({
|
|
55
|
+
processed: 1,
|
|
56
|
+
wouldRebuild: 1,
|
|
57
|
+
rebuilt: 0,
|
|
58
|
+
targetVersion: POST_BODY_HTML_VERSION,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const rebuild = await app.request("/api/internal/posts/body-html/rebuild", {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: {
|
|
64
|
+
Authorization: "Bearer internal-secret",
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify({ limit: 25 }),
|
|
68
|
+
});
|
|
69
|
+
expect(rebuild.status).toBe(200);
|
|
70
|
+
await expect(rebuild.json()).resolves.toMatchObject({
|
|
71
|
+
rebuilt: 1,
|
|
72
|
+
failed: 0,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("rebuilds only the explicitly selected managed site", async () => {
|
|
77
|
+
const { app, services, db } = createTestApp({
|
|
78
|
+
authenticated: false,
|
|
79
|
+
internalAdminToken: "internal-secret",
|
|
80
|
+
siteResolutionMode: "host-based",
|
|
81
|
+
});
|
|
82
|
+
app.use("*", async (c, next) => {
|
|
83
|
+
c.set("appConfig", undefined as never);
|
|
84
|
+
await next();
|
|
85
|
+
});
|
|
86
|
+
app.route("/api/internal/sites", internalSitesRoutes);
|
|
87
|
+
|
|
88
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
89
|
+
const secondSiteId = "sit_managed00000000000000000000";
|
|
90
|
+
await db.insert(sites).values({
|
|
91
|
+
id: secondSiteId,
|
|
92
|
+
key: "managed",
|
|
93
|
+
status: "active",
|
|
94
|
+
createdAt: timestamp,
|
|
95
|
+
updatedAt: timestamp,
|
|
96
|
+
});
|
|
97
|
+
const defaultPost = await services.posts.create({
|
|
98
|
+
format: "note",
|
|
99
|
+
bodyMarkdown: "Default",
|
|
100
|
+
});
|
|
101
|
+
const managedService = createPostService(
|
|
102
|
+
db,
|
|
103
|
+
{ slugIdLength: 5 },
|
|
104
|
+
secondSiteId,
|
|
105
|
+
);
|
|
106
|
+
const managedPost = await managedService.create({
|
|
107
|
+
format: "note",
|
|
108
|
+
bodyMarkdown: "Managed[^1]\n\n[^1]: Definition",
|
|
109
|
+
});
|
|
110
|
+
await db
|
|
111
|
+
.update(posts)
|
|
112
|
+
.set({ bodyHtmlVersion: 1 })
|
|
113
|
+
.where(eq(posts.id, defaultPost.id));
|
|
114
|
+
await db
|
|
115
|
+
.update(posts)
|
|
116
|
+
.set({ bodyHtmlVersion: 1 })
|
|
117
|
+
.where(eq(posts.id, managedPost.id));
|
|
118
|
+
|
|
119
|
+
const response = await app.request(
|
|
120
|
+
`/api/internal/sites/${secondSiteId}/posts/body-html/rebuild`,
|
|
121
|
+
{
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: {
|
|
124
|
+
Authorization: "Bearer internal-secret",
|
|
125
|
+
"Content-Type": "application/json",
|
|
126
|
+
},
|
|
127
|
+
body: JSON.stringify({}),
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
expect(response.status).toBe(200);
|
|
131
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
132
|
+
processed: 1,
|
|
133
|
+
rebuilt: 1,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const rows = await db
|
|
137
|
+
.select({ id: posts.id, version: posts.bodyHtmlVersion })
|
|
138
|
+
.from(posts);
|
|
139
|
+
expect(rows.find((row) => row.id === managedPost.id)?.version).toBe(
|
|
140
|
+
POST_BODY_HTML_VERSION,
|
|
141
|
+
);
|
|
142
|
+
expect(rows.find((row) => row.id === defaultPost.id)?.version).toBe(1);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("rejects an unknown explicit site instead of reporting an empty success", async () => {
|
|
146
|
+
const { app } = createTestApp({
|
|
147
|
+
authenticated: false,
|
|
148
|
+
internalAdminToken: "internal-secret",
|
|
149
|
+
siteResolutionMode: "host-based",
|
|
150
|
+
});
|
|
151
|
+
app.route("/api/internal/sites", internalSitesRoutes);
|
|
152
|
+
|
|
153
|
+
const response = await app.request(
|
|
154
|
+
"/api/internal/sites/sit_missing/posts/body-html/rebuild",
|
|
155
|
+
{
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: { Authorization: "Bearer internal-secret" },
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(response.status).toBe(404);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -120,7 +120,7 @@ describe("Internal site admin routes", () => {
|
|
|
120
120
|
primaryHost: "locale-demo.example.com",
|
|
121
121
|
siteName: "Locale Demo",
|
|
122
122
|
siteLanguage: "en-US",
|
|
123
|
-
timeZone: "
|
|
123
|
+
timeZone: "Etc/GMT-8",
|
|
124
124
|
}),
|
|
125
125
|
});
|
|
126
126
|
|
|
@@ -139,10 +139,43 @@ describe("Internal site admin routes", () => {
|
|
|
139
139
|
{ key: "ONBOARDING_STATUS", value: "completed" },
|
|
140
140
|
{ key: "SITE_LANGUAGE", value: "en" },
|
|
141
141
|
{ key: "SITE_NAME", value: "Locale Demo" },
|
|
142
|
-
{ key: "TIME_ZONE", value: "
|
|
142
|
+
{ key: "TIME_ZONE", value: "Etc/GMT-8" },
|
|
143
143
|
]);
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
it("falls back to UTC instead of rejecting invalid browser timezone metadata", async () => {
|
|
147
|
+
const { app, sqlite } = createTestApp({
|
|
148
|
+
authenticated: false,
|
|
149
|
+
internalAdminToken: "internal-secret",
|
|
150
|
+
siteResolutionMode: "host-based",
|
|
151
|
+
});
|
|
152
|
+
app.route("/api/internal/sites", internalSitesRoutes);
|
|
153
|
+
|
|
154
|
+
const res = await app.request("/api/internal/sites", {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: {
|
|
157
|
+
Authorization: "Bearer internal-secret",
|
|
158
|
+
"Content-Type": "application/json",
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
key: "timezone-fallback",
|
|
162
|
+
primaryHost: "timezone-fallback.example.com",
|
|
163
|
+
siteName: "Timezone Fallback",
|
|
164
|
+
timeZone: "Unknown/Zone",
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(res.status).toBe(201);
|
|
169
|
+
const body = (await res.json()) as { siteId: string };
|
|
170
|
+
const timeZone = sqlite
|
|
171
|
+
.prepare(
|
|
172
|
+
'SELECT "value" FROM "site_setting" WHERE "site_id" = ? AND "key" = \'TIME_ZONE\'',
|
|
173
|
+
)
|
|
174
|
+
.get(body.siteId);
|
|
175
|
+
|
|
176
|
+
expect(timeZone).toBeUndefined();
|
|
177
|
+
});
|
|
178
|
+
|
|
146
179
|
it("reports an unused key as available", async () => {
|
|
147
180
|
const { app } = createTestApp({
|
|
148
181
|
authenticated: false,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { requireInternalAdminApi } from "../../../middleware/auth.js";
|
|
4
|
+
import { parseValidated } from "../../../lib/schemas.js";
|
|
5
|
+
import { rebuildPostBodyHtmlWithRuntimeSettings } from "../../../services/post.js";
|
|
6
|
+
import type { Bindings } from "../../../types.js";
|
|
7
|
+
import type { AppVariables } from "../../../types/app-context.js";
|
|
8
|
+
|
|
9
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
10
|
+
|
|
11
|
+
export const RebuildPostBodyHtmlSchema = z.object({
|
|
12
|
+
limit: z.number().int().positive().max(100).optional(),
|
|
13
|
+
cursor: z.string().trim().min(1).optional(),
|
|
14
|
+
dryRun: z.boolean().optional(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const internalPostBodyHtmlRoutes = new Hono<Env>();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Rebuild the current site's materialized post body HTML projection.
|
|
21
|
+
*
|
|
22
|
+
* Callers follow `nextCursor` until `done`. The service owns rendering,
|
|
23
|
+
* compare-and-swap writes, and timestamp preservation; this route only
|
|
24
|
+
* validates the authenticated maintenance request.
|
|
25
|
+
*/
|
|
26
|
+
internalPostBodyHtmlRoutes.post(
|
|
27
|
+
"/rebuild",
|
|
28
|
+
requireInternalAdminApi(),
|
|
29
|
+
async (c) => {
|
|
30
|
+
const contentType = c.req.header("Content-Type") || "";
|
|
31
|
+
const rawBody = contentType.includes("application/json")
|
|
32
|
+
? await c.req.json().catch(() => ({}))
|
|
33
|
+
: {};
|
|
34
|
+
const body = parseValidated(RebuildPostBodyHtmlSchema, rawBody);
|
|
35
|
+
|
|
36
|
+
const result = await rebuildPostBodyHtmlWithRuntimeSettings(
|
|
37
|
+
c.var.services,
|
|
38
|
+
c.env,
|
|
39
|
+
body,
|
|
40
|
+
);
|
|
41
|
+
return c.json(result);
|
|
42
|
+
},
|
|
43
|
+
);
|
|
@@ -3,12 +3,14 @@ import { z } from "zod";
|
|
|
3
3
|
import { requireInternalAdminApi } from "../../../middleware/auth.js";
|
|
4
4
|
import { ConflictError } from "../../../lib/errors.js";
|
|
5
5
|
import { parseValidated } from "../../../lib/schemas.js";
|
|
6
|
+
import { rebuildPostBodyHtmlWithRuntimeSettings } from "../../../services/post.js";
|
|
6
7
|
import {
|
|
7
8
|
getConfiguredStorageDriver,
|
|
8
9
|
getSiteResolutionMode,
|
|
9
10
|
} from "../../../lib/env.js";
|
|
10
11
|
import type { Bindings } from "../../../types.js";
|
|
11
12
|
import type { AppVariables } from "../../../types/app-context.js";
|
|
13
|
+
import { RebuildPostBodyHtmlSchema } from "./post-body-html.js";
|
|
12
14
|
|
|
13
15
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
14
16
|
|
|
@@ -199,6 +201,28 @@ internalSitesRoutes.post(
|
|
|
199
201
|
},
|
|
200
202
|
);
|
|
201
203
|
|
|
204
|
+
// Rebuild one managed site's materialized body HTML projection. Hosted fleet
|
|
205
|
+
// orchestration stays in the control plane; core never silently widens a
|
|
206
|
+
// content-facing operation to every tenant.
|
|
207
|
+
internalSitesRoutes.post(
|
|
208
|
+
"/:siteId/posts/body-html/rebuild",
|
|
209
|
+
requireInternalAdminApi(),
|
|
210
|
+
async (c) => {
|
|
211
|
+
assertHostBasedMode(c.env);
|
|
212
|
+
|
|
213
|
+
const contentType = c.req.header("Content-Type") || "";
|
|
214
|
+
const rawBody = contentType.includes("application/json")
|
|
215
|
+
? await c.req.json().catch(() => ({}))
|
|
216
|
+
: {};
|
|
217
|
+
const body = parseValidated(RebuildPostBodyHtmlSchema, rawBody);
|
|
218
|
+
const services = c.var.servicesForSite(c.req.param("siteId"));
|
|
219
|
+
|
|
220
|
+
return c.json(
|
|
221
|
+
await rebuildPostBodyHtmlWithRuntimeSettings(services, c.env, body),
|
|
222
|
+
);
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
|
|
202
226
|
internalSitesRoutes.get(
|
|
203
227
|
"/:siteId/export",
|
|
204
228
|
requireInternalAdminApi(),
|
|
@@ -8,6 +8,7 @@ import { z } from "zod";
|
|
|
8
8
|
import type { Bindings } from "../../types.js";
|
|
9
9
|
import type { AppVariables } from "../../types/app-context.js";
|
|
10
10
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
11
|
+
import { requirePublicApiAccess } from "../../middleware/public-content-access.js";
|
|
11
12
|
import {
|
|
12
13
|
CreateNavItemSchema,
|
|
13
14
|
NavItemIdSchema,
|
|
@@ -31,6 +32,11 @@ const MoveSchema = z.object({
|
|
|
31
32
|
before: NavItemIdSchema.nullable().optional(),
|
|
32
33
|
});
|
|
33
34
|
|
|
35
|
+
const PageCandidatesQuerySchema = z.object({
|
|
36
|
+
q: z.string().trim().max(200).optional(),
|
|
37
|
+
limit: z.coerce.number().int().min(1).max(50).optional(),
|
|
38
|
+
});
|
|
39
|
+
|
|
34
40
|
async function withSiteHeaderHtml<T extends object>(
|
|
35
41
|
c: Context<Env>,
|
|
36
42
|
body: T,
|
|
@@ -48,11 +54,21 @@ async function withSiteHeaderHtml<T extends object>(
|
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
// List nav items
|
|
51
|
-
navItemsApiRoutes.get("/", async (c) => {
|
|
57
|
+
navItemsApiRoutes.get("/", requirePublicApiAccess(), async (c) => {
|
|
52
58
|
const items = await c.var.services.navItems.list();
|
|
53
59
|
return c.json({ navItems: items });
|
|
54
60
|
});
|
|
55
61
|
|
|
62
|
+
// Search published titled notes that can be added as pages (requires auth)
|
|
63
|
+
navItemsApiRoutes.get("/pages", requireAuthApi(), async (c) => {
|
|
64
|
+
const query = parseValidated(PageCandidatesQuerySchema, c.req.query());
|
|
65
|
+
const pages = await c.var.services.navItems.listPageCandidates({
|
|
66
|
+
query: query.q,
|
|
67
|
+
limit: query.limit,
|
|
68
|
+
});
|
|
69
|
+
return c.json({ pages });
|
|
70
|
+
});
|
|
71
|
+
|
|
56
72
|
// Move nav item (requires auth) — must be before /:id
|
|
57
73
|
navItemsApiRoutes.put("/:id/move", requireAuthApi(), async (c) => {
|
|
58
74
|
const id = parseIdParam(c.req.param("id"), ID_PREFIX.navItem);
|
|
@@ -94,6 +110,13 @@ navItemsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
94
110
|
url: getCollectionPagePath(collection.slug),
|
|
95
111
|
placement: body.placement,
|
|
96
112
|
});
|
|
113
|
+
} else if (body.type === "page") {
|
|
114
|
+
item = await c.var.services.navItems.create({
|
|
115
|
+
type: "page",
|
|
116
|
+
postId: body.postId,
|
|
117
|
+
label: body.label,
|
|
118
|
+
placement: body.placement,
|
|
119
|
+
});
|
|
97
120
|
} else {
|
|
98
121
|
item = await c.var.services.navItems.create({
|
|
99
122
|
type: "link",
|
package/src/routes/api/posts.ts
CHANGED
|
@@ -132,7 +132,7 @@ postsApiRoutes.get("/:id", requireAuthApi(), async (c) => {
|
|
|
132
132
|
const id = parseIdParam(c.req.param("id"), ID_PREFIX.post);
|
|
133
133
|
|
|
134
134
|
// Fetch post, media, and collections in parallel (all keyed by the same id)
|
|
135
|
-
const [post, mediaList,
|
|
135
|
+
const [post, mediaList, threadCollections] = await Promise.all([
|
|
136
136
|
c.var.services.posts.getById(id),
|
|
137
137
|
c.var.services.media.getByPostId(id),
|
|
138
138
|
c.var.services.collections.getCollectionsByPostId(id),
|
|
@@ -145,7 +145,7 @@ postsApiRoutes.get("/:id", requireAuthApi(), async (c) => {
|
|
|
145
145
|
localPublicUrl,
|
|
146
146
|
sitePathPrefix,
|
|
147
147
|
} = c.var.appConfig;
|
|
148
|
-
const collectionIds =
|
|
148
|
+
const collectionIds = threadCollections.map((col) => col.id);
|
|
149
149
|
|
|
150
150
|
return c.json(
|
|
151
151
|
toApiPost(foundPost, {
|
|
@@ -88,6 +88,41 @@ describe("Public Archive API Routes", () => {
|
|
|
88
88
|
expect(nextBody.posts[0].id).not.toBe(body.posts[0].id);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
it("paginates chronologically without promoting pinned posts", async () => {
|
|
92
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
93
|
+
app.route("/api/public/archive", publicArchiveApiRoutes);
|
|
94
|
+
|
|
95
|
+
const olderPinned = await services.posts.create({
|
|
96
|
+
format: "note",
|
|
97
|
+
title: "Older pinned post",
|
|
98
|
+
bodyMarkdown: "older",
|
|
99
|
+
pinned: true,
|
|
100
|
+
publishedAt: Date.UTC(2025, 0, 1) / 1000,
|
|
101
|
+
});
|
|
102
|
+
const newerUnpinned = await services.posts.create({
|
|
103
|
+
format: "note",
|
|
104
|
+
title: "Newer unpinned post",
|
|
105
|
+
bodyMarkdown: "newer",
|
|
106
|
+
publishedAt: Date.UTC(2026, 0, 1) / 1000,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const first = await app.request("/api/public/archive?limit=1");
|
|
110
|
+
expect(first.status).toBe(200);
|
|
111
|
+
const firstBody = await first.json();
|
|
112
|
+
expect(firstBody.posts.map((post: { id: string }) => post.id)).toEqual([
|
|
113
|
+
newerUnpinned.id,
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const second = await app.request(
|
|
117
|
+
`/api/public/archive?limit=1&cursor=${firstBody.nextCursor}`,
|
|
118
|
+
);
|
|
119
|
+
expect(second.status).toBe(200);
|
|
120
|
+
const secondBody = await second.json();
|
|
121
|
+
expect(secondBody.posts.map((post: { id: string }) => post.id)).toEqual([
|
|
122
|
+
olderPinned.id,
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
|
|
91
126
|
it("filters by year using publishedAt", async () => {
|
|
92
127
|
const { app, services } = createTestApp({ authenticated: false });
|
|
93
128
|
app.route("/api/public/archive", publicArchiveApiRoutes);
|
|
@@ -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", () => {
|
|
@@ -100,6 +102,32 @@ describe("Public Posts API Routes", () => {
|
|
|
100
102
|
expect(body.posts[0]).not.toHaveProperty("bodyText");
|
|
101
103
|
});
|
|
102
104
|
|
|
105
|
+
it("returns canonical v3 HTML when the stored projection is stale", async () => {
|
|
106
|
+
const { app, services, db } = createTestApp({ authenticated: false });
|
|
107
|
+
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
108
|
+
const post = await services.posts.create({
|
|
109
|
+
format: "note",
|
|
110
|
+
title: "Footnote API",
|
|
111
|
+
bodyMarkdown: "API body[^1]\n\n[^1]: API definition",
|
|
112
|
+
});
|
|
113
|
+
await db
|
|
114
|
+
.update(posts)
|
|
115
|
+
.set({
|
|
116
|
+
bodyHtml: '<span class="sidenote">legacy</span>',
|
|
117
|
+
bodyHtmlVersion: 1,
|
|
118
|
+
})
|
|
119
|
+
.where(eq(posts.id, post.id));
|
|
120
|
+
|
|
121
|
+
const response = await app.request("/api/public/posts");
|
|
122
|
+
const body = await response.json();
|
|
123
|
+
|
|
124
|
+
expect(response.status).toBe(200);
|
|
125
|
+
expect(body.posts[0].bodyHtml).toContain('role="doc-noteref"');
|
|
126
|
+
expect(body.posts[0].bodyHtml).toMatch(/id="fn-[a-z0-9]{13}-1"/);
|
|
127
|
+
expect(body.posts[0].bodyHtml).not.toContain(post.id);
|
|
128
|
+
expect(body.posts[0].bodyHtml).not.toContain("legacy");
|
|
129
|
+
});
|
|
130
|
+
|
|
103
131
|
it("filters posts by a single collection slug", async () => {
|
|
104
132
|
const { app, services } = createTestApp({ authenticated: false });
|
|
105
133
|
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
@@ -194,12 +222,20 @@ describe("Public Posts API Routes", () => {
|
|
|
194
222
|
title: "Older",
|
|
195
223
|
bodyMarkdown: "first",
|
|
196
224
|
collectionIds: [collection.id],
|
|
225
|
+
publishedAt: 1000,
|
|
197
226
|
});
|
|
198
227
|
const newer = await services.posts.create({
|
|
199
228
|
format: "note",
|
|
200
229
|
title: "Newer",
|
|
201
230
|
bodyMarkdown: "second",
|
|
202
231
|
collectionIds: [collection.id],
|
|
232
|
+
publishedAt: 2000,
|
|
233
|
+
});
|
|
234
|
+
await services.posts.create({
|
|
235
|
+
format: "note",
|
|
236
|
+
bodyMarkdown: "Later reply to the older Thread",
|
|
237
|
+
replyToId: older.id,
|
|
238
|
+
publishedAt: 3000,
|
|
203
239
|
});
|
|
204
240
|
|
|
205
241
|
const res = await app.request(
|
|
@@ -213,6 +249,145 @@ describe("Public Posts API Routes", () => {
|
|
|
213
249
|
expect(body.posts[1].id).toBe(newer.id);
|
|
214
250
|
});
|
|
215
251
|
|
|
252
|
+
it("sorts collection results by Thread activity from Child Posts", async () => {
|
|
253
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
254
|
+
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
255
|
+
|
|
256
|
+
const collection = await services.collections.create({
|
|
257
|
+
slug: "active-threads",
|
|
258
|
+
title: "Active Threads",
|
|
259
|
+
});
|
|
260
|
+
const olderRoot = await services.posts.create({
|
|
261
|
+
format: "note",
|
|
262
|
+
title: "Older root",
|
|
263
|
+
bodyMarkdown: "older",
|
|
264
|
+
collectionIds: [collection.id],
|
|
265
|
+
publishedAt: 1000,
|
|
266
|
+
});
|
|
267
|
+
const newerRoot = await services.posts.create({
|
|
268
|
+
format: "note",
|
|
269
|
+
title: "Newer root",
|
|
270
|
+
bodyMarkdown: "newer",
|
|
271
|
+
collectionIds: [collection.id],
|
|
272
|
+
publishedAt: 2000,
|
|
273
|
+
});
|
|
274
|
+
await services.posts.create({
|
|
275
|
+
format: "note",
|
|
276
|
+
bodyMarkdown: "new activity",
|
|
277
|
+
replyToId: olderRoot.id,
|
|
278
|
+
publishedAt: 3000,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const res = await app.request(
|
|
282
|
+
"/api/public/posts?collection=active-threads&sort=newest",
|
|
283
|
+
);
|
|
284
|
+
expect(res.status).toBe(200);
|
|
285
|
+
|
|
286
|
+
const body = await res.json();
|
|
287
|
+
expect(body.posts.map((post: { id: string }) => post.id)).toEqual([
|
|
288
|
+
olderRoot.id,
|
|
289
|
+
newerRoot.id,
|
|
290
|
+
]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("sorts and cursor-paginates collection Threads by Child ratings", async () => {
|
|
294
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
295
|
+
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
296
|
+
|
|
297
|
+
const collection = await services.collections.create({
|
|
298
|
+
slug: "rated-threads",
|
|
299
|
+
title: "Rated Threads",
|
|
300
|
+
sortOrder: "rating_desc",
|
|
301
|
+
});
|
|
302
|
+
const createRatedThread = async (
|
|
303
|
+
title: string,
|
|
304
|
+
publishedAt: number,
|
|
305
|
+
rating: number,
|
|
306
|
+
) => {
|
|
307
|
+
const root = await services.posts.create({
|
|
308
|
+
format: "note",
|
|
309
|
+
title,
|
|
310
|
+
bodyMarkdown: `${title} root`,
|
|
311
|
+
collectionIds: [collection.id],
|
|
312
|
+
publishedAt,
|
|
313
|
+
});
|
|
314
|
+
await services.posts.create({
|
|
315
|
+
format: "note",
|
|
316
|
+
bodyMarkdown: `${title} rated Child`,
|
|
317
|
+
replyToId: root.id,
|
|
318
|
+
publishedAt: publishedAt + 100,
|
|
319
|
+
rating,
|
|
320
|
+
});
|
|
321
|
+
return root;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const highest = await createRatedThread("Highest", 1000, 5);
|
|
325
|
+
const middle = await createRatedThread("Middle", 2000, 3);
|
|
326
|
+
const lowest = await createRatedThread("Lowest", 3000, 1);
|
|
327
|
+
|
|
328
|
+
const firstRes = await app.request(
|
|
329
|
+
"/api/public/posts?collection=rated-threads&limit=1",
|
|
330
|
+
);
|
|
331
|
+
expect(firstRes.status).toBe(200);
|
|
332
|
+
const first = await firstRes.json();
|
|
333
|
+
expect(first.posts[0].id).toBe(highest.id);
|
|
334
|
+
expect(first.nextCursor).toBe(highest.id);
|
|
335
|
+
|
|
336
|
+
const secondRes = await app.request(
|
|
337
|
+
`/api/public/posts?collection=rated-threads&limit=1&cursor=${first.nextCursor}`,
|
|
338
|
+
);
|
|
339
|
+
expect(secondRes.status).toBe(200);
|
|
340
|
+
const second = await secondRes.json();
|
|
341
|
+
expect(second.posts[0].id).toBe(middle.id);
|
|
342
|
+
expect(second.nextCursor).toBe(middle.id);
|
|
343
|
+
|
|
344
|
+
const thirdRes = await app.request(
|
|
345
|
+
`/api/public/posts?collection=rated-threads&limit=1&cursor=${second.nextCursor}`,
|
|
346
|
+
);
|
|
347
|
+
expect(thirdRes.status).toBe(200);
|
|
348
|
+
const third = await thirdRes.json();
|
|
349
|
+
expect(third.posts[0].id).toBe(lowest.id);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("keeps root format and Latest visibility filters for Collections", async () => {
|
|
353
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
354
|
+
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
355
|
+
|
|
356
|
+
const collection = await services.collections.create({
|
|
357
|
+
slug: "mixed",
|
|
358
|
+
title: "Mixed",
|
|
359
|
+
});
|
|
360
|
+
const note = await services.posts.create({
|
|
361
|
+
format: "note",
|
|
362
|
+
title: "Visible note",
|
|
363
|
+
bodyMarkdown: "note",
|
|
364
|
+
collectionIds: [collection.id],
|
|
365
|
+
});
|
|
366
|
+
await services.posts.create({
|
|
367
|
+
format: "link",
|
|
368
|
+
title: "Visible link",
|
|
369
|
+
url: "https://example.com",
|
|
370
|
+
collectionIds: [collection.id],
|
|
371
|
+
});
|
|
372
|
+
await services.posts.create({
|
|
373
|
+
format: "note",
|
|
374
|
+
title: "Hidden note",
|
|
375
|
+
bodyMarkdown: "hidden",
|
|
376
|
+
visibility: "latest_hidden",
|
|
377
|
+
collectionIds: [collection.id],
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const res = await app.request(
|
|
381
|
+
"/api/public/posts?collection=mixed&format=note",
|
|
382
|
+
);
|
|
383
|
+
expect(res.status).toBe(200);
|
|
384
|
+
|
|
385
|
+
const body = await res.json();
|
|
386
|
+
expect(body.posts.map((post: { id: string }) => post.id)).toEqual([
|
|
387
|
+
note.id,
|
|
388
|
+
]);
|
|
389
|
+
});
|
|
390
|
+
|
|
216
391
|
it("allows overriding collection sort order via sort parameter", async () => {
|
|
217
392
|
const { app, services } = createTestApp({ authenticated: false });
|
|
218
393
|
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
@@ -282,6 +457,39 @@ describe("Public Posts API Routes", () => {
|
|
|
282
457
|
expect(body).not.toHaveProperty("body");
|
|
283
458
|
});
|
|
284
459
|
|
|
460
|
+
it("returns the shared Thread Collections for a child post", async () => {
|
|
461
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
462
|
+
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
463
|
+
|
|
464
|
+
const collection = await services.collections.create({
|
|
465
|
+
slug: "shared",
|
|
466
|
+
title: "Shared",
|
|
467
|
+
});
|
|
468
|
+
const root = await services.posts.create({
|
|
469
|
+
format: "note",
|
|
470
|
+
bodyMarkdown: "root",
|
|
471
|
+
collectionIds: [collection.id],
|
|
472
|
+
});
|
|
473
|
+
const child = await services.posts.create({
|
|
474
|
+
format: "note",
|
|
475
|
+
bodyMarkdown: "child",
|
|
476
|
+
replyToId: root.id,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const res = await app.request(`/api/public/posts/${child.slug}`);
|
|
480
|
+
expect(res.status).toBe(200);
|
|
481
|
+
await expect(res.json()).resolves.toMatchObject({
|
|
482
|
+
id: child.id,
|
|
483
|
+
collections: [
|
|
484
|
+
{
|
|
485
|
+
id: collection.id,
|
|
486
|
+
slug: "shared",
|
|
487
|
+
title: "Shared",
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
285
493
|
it("returns markdown instead of rendered fields when content=markdown", async () => {
|
|
286
494
|
const { app, services } = createTestApp({ authenticated: false });
|
|
287
495
|
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);
|