@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
|
@@ -4,13 +4,16 @@ import {
|
|
|
4
4
|
createTestDatabase,
|
|
5
5
|
DEFAULT_TEST_SITE_ID,
|
|
6
6
|
} from "../../__tests__/helpers/db.js";
|
|
7
|
-
import {
|
|
7
|
+
import { threadCollections, posts, sites } from "../../db/schema.js";
|
|
8
8
|
import { createPostService } from "../post.js";
|
|
9
9
|
import { createMediaService } from "../media.js";
|
|
10
10
|
import { createCollectionService } from "../collection.js";
|
|
11
|
+
import { createNavItemService } from "../navigation.js";
|
|
11
12
|
import type { Database } from "../../db/index.js";
|
|
12
13
|
import { createPathService } from "../path.js";
|
|
13
14
|
import type { MediaService } from "../media.js";
|
|
15
|
+
import { POST_BODY_HTML_VERSION } from "../../lib/post-body-html.js";
|
|
16
|
+
import type BetterSqlite3 from "better-sqlite3";
|
|
14
17
|
|
|
15
18
|
function createMockStorage() {
|
|
16
19
|
const files = new Map<string, { body: Uint8Array; contentType?: string }>();
|
|
@@ -44,12 +47,14 @@ function createMockStorage() {
|
|
|
44
47
|
|
|
45
48
|
describe("PostService", () => {
|
|
46
49
|
let db: Database;
|
|
50
|
+
let sqlite: BetterSqlite3.Database;
|
|
47
51
|
let postService: ReturnType<typeof createPostService>;
|
|
48
52
|
let collectionService: ReturnType<typeof createCollectionService>;
|
|
49
53
|
|
|
50
54
|
beforeEach(() => {
|
|
51
55
|
const testDb = createTestDatabase();
|
|
52
56
|
db = testDb.db as unknown as Database;
|
|
57
|
+
sqlite = testDb.sqlite;
|
|
53
58
|
postService = createPostService(
|
|
54
59
|
db,
|
|
55
60
|
{ slugIdLength: 5 },
|
|
@@ -152,6 +157,26 @@ describe("PostService", () => {
|
|
|
152
157
|
expect(post.bodyHtml).toContain("<strong>bold</strong>");
|
|
153
158
|
});
|
|
154
159
|
|
|
160
|
+
it("writes current namespaced body HTML projection metadata", async () => {
|
|
161
|
+
const post = await postService.create({
|
|
162
|
+
format: "note",
|
|
163
|
+
bodyMarkdown: "Body[^1]\n\n[^1]: Definition",
|
|
164
|
+
});
|
|
165
|
+
const row = await db
|
|
166
|
+
.select({
|
|
167
|
+
bodyHtml: posts.bodyHtml,
|
|
168
|
+
bodyHtmlVersion: posts.bodyHtmlVersion,
|
|
169
|
+
})
|
|
170
|
+
.from(posts)
|
|
171
|
+
.where(eq(posts.id, post.id))
|
|
172
|
+
.limit(1);
|
|
173
|
+
|
|
174
|
+
expect(row[0]?.bodyHtmlVersion).toBe(POST_BODY_HTML_VERSION);
|
|
175
|
+
expect(row[0]?.bodyHtml).toMatch(/id="fn-[a-z0-9]{13}-1"/);
|
|
176
|
+
expect(row[0]?.bodyHtml).not.toContain(post.id);
|
|
177
|
+
expect(post.bodyHtml).toContain('role="doc-endnotes"');
|
|
178
|
+
});
|
|
179
|
+
|
|
155
180
|
it("sets publishedAt and timestamps", async () => {
|
|
156
181
|
const post = await postService.create({
|
|
157
182
|
format: "note",
|
|
@@ -344,6 +369,16 @@ describe("PostService", () => {
|
|
|
344
369
|
).rejects.toThrow("Parent post not found");
|
|
345
370
|
});
|
|
346
371
|
|
|
372
|
+
it("rejects reserved paths that would otherwise become aliases", async () => {
|
|
373
|
+
await expect(
|
|
374
|
+
postService.create({
|
|
375
|
+
format: "note",
|
|
376
|
+
bodyMarkdown: "reserved path",
|
|
377
|
+
path: "skill.md",
|
|
378
|
+
}),
|
|
379
|
+
).rejects.toThrow('Path "skill.md" is reserved and cannot be used');
|
|
380
|
+
});
|
|
381
|
+
|
|
347
382
|
it("rolls back the post insert when slug persistence fails inside the batch", async () => {
|
|
348
383
|
await postService.create({
|
|
349
384
|
format: "note",
|
|
@@ -683,6 +718,55 @@ describe("PostService", () => {
|
|
|
683
718
|
expect(notFeatured[0]?.bodyText).toBe("normal post");
|
|
684
719
|
});
|
|
685
720
|
|
|
721
|
+
it("orders Featured Post lists and cursors by publishedAt", async () => {
|
|
722
|
+
const oldest = await postService.create({
|
|
723
|
+
format: "note",
|
|
724
|
+
bodyMarkdown: "oldest",
|
|
725
|
+
featured: true,
|
|
726
|
+
publishedAt: 1000,
|
|
727
|
+
});
|
|
728
|
+
const middle = await postService.create({
|
|
729
|
+
format: "note",
|
|
730
|
+
bodyMarkdown: "middle",
|
|
731
|
+
featured: true,
|
|
732
|
+
publishedAt: 2000,
|
|
733
|
+
});
|
|
734
|
+
const newest = await postService.create({
|
|
735
|
+
format: "note",
|
|
736
|
+
bodyMarkdown: "newest",
|
|
737
|
+
featured: true,
|
|
738
|
+
publishedAt: 3000,
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
await db
|
|
742
|
+
.update(posts)
|
|
743
|
+
.set({ featuredAt: 9000 })
|
|
744
|
+
.where(eq(posts.id, oldest.id));
|
|
745
|
+
await db
|
|
746
|
+
.update(posts)
|
|
747
|
+
.set({ featuredAt: 8000 })
|
|
748
|
+
.where(eq(posts.id, middle.id));
|
|
749
|
+
await db
|
|
750
|
+
.update(posts)
|
|
751
|
+
.set({ featuredAt: 7000 })
|
|
752
|
+
.where(eq(posts.id, newest.id));
|
|
753
|
+
|
|
754
|
+
const firstPage = await postService.list({
|
|
755
|
+
featured: true,
|
|
756
|
+
ignorePinnedSort: true,
|
|
757
|
+
limit: 2,
|
|
758
|
+
});
|
|
759
|
+
const secondPage = await postService.list({
|
|
760
|
+
featured: true,
|
|
761
|
+
ignorePinnedSort: true,
|
|
762
|
+
cursor: middle.id,
|
|
763
|
+
limit: 2,
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
expect(firstPage.map((post) => post.id)).toEqual([newest.id, middle.id]);
|
|
767
|
+
expect(secondPage.map((post) => post.id)).toEqual([oldest.id]);
|
|
768
|
+
});
|
|
769
|
+
|
|
686
770
|
it("excludes posts hidden from Latest when requested", async () => {
|
|
687
771
|
await postService.create({
|
|
688
772
|
format: "note",
|
|
@@ -1072,9 +1156,9 @@ describe("PostService", () => {
|
|
|
1072
1156
|
rating: i + 1,
|
|
1073
1157
|
});
|
|
1074
1158
|
|
|
1075
|
-
await db.insert(
|
|
1159
|
+
await db.insert(threadCollections).values({
|
|
1076
1160
|
siteId: DEFAULT_TEST_SITE_ID,
|
|
1077
|
-
|
|
1161
|
+
threadId: post.id,
|
|
1078
1162
|
collectionId: collection.id,
|
|
1079
1163
|
createdAt: 100 + i,
|
|
1080
1164
|
});
|
|
@@ -1298,6 +1382,76 @@ describe("PostService", () => {
|
|
|
1298
1382
|
}
|
|
1299
1383
|
});
|
|
1300
1384
|
|
|
1385
|
+
it("updates the shared Thread collections when editing a child", async () => {
|
|
1386
|
+
const firstCollection = await collectionService.create({
|
|
1387
|
+
slug: "first-thread-collection",
|
|
1388
|
+
title: "First",
|
|
1389
|
+
});
|
|
1390
|
+
const secondCollection = await collectionService.create({
|
|
1391
|
+
slug: "second-thread-collection",
|
|
1392
|
+
title: "Second",
|
|
1393
|
+
});
|
|
1394
|
+
const root = await postService.create({
|
|
1395
|
+
format: "note",
|
|
1396
|
+
bodyMarkdown: "root",
|
|
1397
|
+
collectionIds: [firstCollection.id],
|
|
1398
|
+
});
|
|
1399
|
+
const child = await postService.create({
|
|
1400
|
+
format: "note",
|
|
1401
|
+
bodyMarkdown: "child",
|
|
1402
|
+
replyToId: root.id,
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
expect(
|
|
1406
|
+
(await collectionService.getCollectionsByPostId(child.id)).map(
|
|
1407
|
+
(collection) => collection.id,
|
|
1408
|
+
),
|
|
1409
|
+
).toEqual([firstCollection.id]);
|
|
1410
|
+
|
|
1411
|
+
await postService.update(child.id, {
|
|
1412
|
+
collectionIds: [secondCollection.id],
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
const [rootCollections, childCollections, rows] = await Promise.all([
|
|
1416
|
+
collectionService.getCollectionsByPostId(root.id),
|
|
1417
|
+
collectionService.getCollectionsByPostId(child.id),
|
|
1418
|
+
db.select().from(threadCollections),
|
|
1419
|
+
]);
|
|
1420
|
+
expect(rootCollections.map((collection) => collection.id)).toEqual([
|
|
1421
|
+
secondCollection.id,
|
|
1422
|
+
]);
|
|
1423
|
+
expect(childCollections.map((collection) => collection.id)).toEqual([
|
|
1424
|
+
secondCollection.id,
|
|
1425
|
+
]);
|
|
1426
|
+
expect(rows).toEqual([
|
|
1427
|
+
expect.objectContaining({
|
|
1428
|
+
threadId: root.id,
|
|
1429
|
+
collectionId: secondCollection.id,
|
|
1430
|
+
}),
|
|
1431
|
+
]);
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
it("rejects Collection membership while creating a child", async () => {
|
|
1435
|
+
const collection = await collectionService.create({
|
|
1436
|
+
slug: "reply-create-collection",
|
|
1437
|
+
title: "Reply Create Collection",
|
|
1438
|
+
});
|
|
1439
|
+
const root = await postService.create({
|
|
1440
|
+
format: "note",
|
|
1441
|
+
bodyMarkdown: "root",
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
await expect(
|
|
1445
|
+
postService.create({
|
|
1446
|
+
format: "note",
|
|
1447
|
+
bodyMarkdown: "child",
|
|
1448
|
+
replyToId: root.id,
|
|
1449
|
+
collectionIds: [collection.id],
|
|
1450
|
+
}),
|
|
1451
|
+
).rejects.toThrow("Cannot set Collections while creating a Thread reply");
|
|
1452
|
+
expect(await collectionService.getThreadIds(collection.id)).toEqual([]);
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1301
1455
|
it("sets publishedAt when publishing a draft", async () => {
|
|
1302
1456
|
const post = await postService.create({
|
|
1303
1457
|
format: "note",
|
|
@@ -1413,11 +1567,17 @@ describe("PostService", () => {
|
|
|
1413
1567
|
expect(updated?.pinnedAt).toBeTypeOf("number");
|
|
1414
1568
|
});
|
|
1415
1569
|
|
|
1416
|
-
it("updates slug", async () => {
|
|
1570
|
+
it("updates slug and its page navigation URL", async () => {
|
|
1417
1571
|
const post = await postService.create({
|
|
1418
1572
|
format: "note",
|
|
1419
|
-
|
|
1573
|
+
title: "Test page",
|
|
1420
1574
|
slug: "old-slug",
|
|
1575
|
+
visibility: "latest_hidden",
|
|
1576
|
+
});
|
|
1577
|
+
const navItemService = createNavItemService(db, DEFAULT_TEST_SITE_ID);
|
|
1578
|
+
const navItem = await navItemService.create({
|
|
1579
|
+
type: "page",
|
|
1580
|
+
postId: post.id,
|
|
1421
1581
|
});
|
|
1422
1582
|
|
|
1423
1583
|
const updated = await postService.update(post.id, {
|
|
@@ -1425,6 +1585,25 @@ describe("PostService", () => {
|
|
|
1425
1585
|
});
|
|
1426
1586
|
|
|
1427
1587
|
expect(updated?.slug).toBe("new-slug");
|
|
1588
|
+
expect(await navItemService.getById(navItem.id)).toMatchObject({
|
|
1589
|
+
label: "Test page",
|
|
1590
|
+
url: "/new-slug",
|
|
1591
|
+
});
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
it("removes page navigation when the page becomes a draft", async () => {
|
|
1595
|
+
const post = await postService.create({
|
|
1596
|
+
format: "note",
|
|
1597
|
+
title: "Temporary page",
|
|
1598
|
+
slug: "temporary-page",
|
|
1599
|
+
visibility: "latest_hidden",
|
|
1600
|
+
});
|
|
1601
|
+
const navItemService = createNavItemService(db, DEFAULT_TEST_SITE_ID);
|
|
1602
|
+
await navItemService.create({ type: "page", postId: post.id });
|
|
1603
|
+
|
|
1604
|
+
await postService.update(post.id, { status: "draft" });
|
|
1605
|
+
|
|
1606
|
+
expect(await navItemService.list()).toEqual([]);
|
|
1428
1607
|
});
|
|
1429
1608
|
|
|
1430
1609
|
it("updates quoteText and rating", async () => {
|
|
@@ -1601,6 +1780,21 @@ describe("PostService", () => {
|
|
|
1601
1780
|
expect(found).toBeNull();
|
|
1602
1781
|
});
|
|
1603
1782
|
|
|
1783
|
+
it("cascade deletes the post's page navigation item", async () => {
|
|
1784
|
+
const post = await postService.create({
|
|
1785
|
+
format: "note",
|
|
1786
|
+
title: "Disposable page",
|
|
1787
|
+
slug: "disposable-page",
|
|
1788
|
+
visibility: "latest_hidden",
|
|
1789
|
+
});
|
|
1790
|
+
const navItemService = createNavItemService(db, DEFAULT_TEST_SITE_ID);
|
|
1791
|
+
await navItemService.create({ type: "page", postId: post.id });
|
|
1792
|
+
|
|
1793
|
+
await postService.delete(post.id);
|
|
1794
|
+
|
|
1795
|
+
expect(await navItemService.list()).toEqual([]);
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1604
1798
|
it("returns false for non-existent post", async () => {
|
|
1605
1799
|
const result = await postService.delete(9999);
|
|
1606
1800
|
expect(result).toBe(false);
|
|
@@ -2286,6 +2480,374 @@ describe("PostService", () => {
|
|
|
2286
2480
|
});
|
|
2287
2481
|
});
|
|
2288
2482
|
|
|
2483
|
+
describe("rebuildBodyHtml", () => {
|
|
2484
|
+
async function createFootnotePost(label: string) {
|
|
2485
|
+
return postService.create({
|
|
2486
|
+
format: "note",
|
|
2487
|
+
bodyMarkdown: `Body ${label}[^1]\n\n[^1]: Definition ${label}`,
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
function legacyFootnoteBody(): string {
|
|
2492
|
+
return JSON.stringify({
|
|
2493
|
+
type: "doc",
|
|
2494
|
+
content: [
|
|
2495
|
+
{
|
|
2496
|
+
type: "paragraph",
|
|
2497
|
+
content: [
|
|
2498
|
+
{ type: "text", text: "Body " },
|
|
2499
|
+
{
|
|
2500
|
+
type: "text",
|
|
2501
|
+
text: "[1]",
|
|
2502
|
+
marks: [{ type: "link", attrs: { href: "#fn-1" } }],
|
|
2503
|
+
},
|
|
2504
|
+
],
|
|
2505
|
+
},
|
|
2506
|
+
{ type: "horizontalRule" },
|
|
2507
|
+
{
|
|
2508
|
+
type: "orderedList",
|
|
2509
|
+
content: [
|
|
2510
|
+
{
|
|
2511
|
+
type: "listItem",
|
|
2512
|
+
content: [
|
|
2513
|
+
{
|
|
2514
|
+
type: "paragraph",
|
|
2515
|
+
content: [
|
|
2516
|
+
{ type: "text", text: "Definition " },
|
|
2517
|
+
{
|
|
2518
|
+
type: "text",
|
|
2519
|
+
text: "↩︎",
|
|
2520
|
+
marks: [{ type: "link", attrs: { href: "#fnref-1" } }],
|
|
2521
|
+
},
|
|
2522
|
+
],
|
|
2523
|
+
},
|
|
2524
|
+
],
|
|
2525
|
+
},
|
|
2526
|
+
],
|
|
2527
|
+
},
|
|
2528
|
+
],
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
it("resolves stale stored HTML on reads before the rebuild writes", async () => {
|
|
2533
|
+
const post = await createFootnotePost("stale");
|
|
2534
|
+
await db
|
|
2535
|
+
.update(posts)
|
|
2536
|
+
.set({
|
|
2537
|
+
bodyHtml: '<span class="sidenote">legacy</span>',
|
|
2538
|
+
bodyHtmlVersion: 1,
|
|
2539
|
+
})
|
|
2540
|
+
.where(eq(posts.id, post.id));
|
|
2541
|
+
|
|
2542
|
+
const resolved = await postService.getById(post.id);
|
|
2543
|
+
expect(resolved?.bodyHtml).toContain('role="doc-noteref"');
|
|
2544
|
+
expect(resolved?.bodyHtml).toContain('role="doc-endnotes"');
|
|
2545
|
+
expect(resolved?.bodyHtml).not.toContain("legacy");
|
|
2546
|
+
|
|
2547
|
+
const stored = await db
|
|
2548
|
+
.select({ bodyHtml: posts.bodyHtml })
|
|
2549
|
+
.from(posts)
|
|
2550
|
+
.where(eq(posts.id, post.id))
|
|
2551
|
+
.limit(1);
|
|
2552
|
+
expect(stored[0]?.bodyHtml).toContain("legacy");
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
it("supports dry-run, preserves timestamps, and is idempotent", async () => {
|
|
2556
|
+
const post = await createFootnotePost("rebuild");
|
|
2557
|
+
await db
|
|
2558
|
+
.update(posts)
|
|
2559
|
+
.set({
|
|
2560
|
+
bodyHtml: '<span class="sidenote">legacy</span>',
|
|
2561
|
+
bodyHtmlVersion: 1,
|
|
2562
|
+
})
|
|
2563
|
+
.where(eq(posts.id, post.id));
|
|
2564
|
+
|
|
2565
|
+
const before = await db
|
|
2566
|
+
.select({
|
|
2567
|
+
updatedAt: posts.updatedAt,
|
|
2568
|
+
publishedAt: posts.publishedAt,
|
|
2569
|
+
})
|
|
2570
|
+
.from(posts)
|
|
2571
|
+
.where(eq(posts.id, post.id))
|
|
2572
|
+
.limit(1);
|
|
2573
|
+
|
|
2574
|
+
const dryRun = await postService.rebuildBodyHtml({ dryRun: true });
|
|
2575
|
+
expect(dryRun).toMatchObject({
|
|
2576
|
+
processed: 1,
|
|
2577
|
+
wouldRebuild: 1,
|
|
2578
|
+
rebuilt: 0,
|
|
2579
|
+
skipped: 0,
|
|
2580
|
+
conflicted: 0,
|
|
2581
|
+
failed: 0,
|
|
2582
|
+
done: true,
|
|
2583
|
+
targetVersion: POST_BODY_HTML_VERSION,
|
|
2584
|
+
});
|
|
2585
|
+
|
|
2586
|
+
const firstPass = await postService.rebuildBodyHtml();
|
|
2587
|
+
expect(firstPass).toMatchObject({
|
|
2588
|
+
wouldRebuild: 1,
|
|
2589
|
+
rebuilt: 1,
|
|
2590
|
+
failed: 0,
|
|
2591
|
+
});
|
|
2592
|
+
|
|
2593
|
+
const stored = await db
|
|
2594
|
+
.select({
|
|
2595
|
+
bodyHtml: posts.bodyHtml,
|
|
2596
|
+
bodyHtmlVersion: posts.bodyHtmlVersion,
|
|
2597
|
+
updatedAt: posts.updatedAt,
|
|
2598
|
+
publishedAt: posts.publishedAt,
|
|
2599
|
+
})
|
|
2600
|
+
.from(posts)
|
|
2601
|
+
.where(eq(posts.id, post.id))
|
|
2602
|
+
.limit(1);
|
|
2603
|
+
expect(stored[0]?.bodyHtmlVersion).toBe(POST_BODY_HTML_VERSION);
|
|
2604
|
+
expect(stored[0]?.bodyHtml).toContain('role="doc-endnotes"');
|
|
2605
|
+
expect(stored[0]?.updatedAt).toBe(before[0]?.updatedAt);
|
|
2606
|
+
expect(stored[0]?.publishedAt).toBe(before[0]?.publishedAt);
|
|
2607
|
+
|
|
2608
|
+
const secondPass = await postService.rebuildBodyHtml();
|
|
2609
|
+
expect(secondPass).toMatchObject({
|
|
2610
|
+
wouldRebuild: 0,
|
|
2611
|
+
rebuilt: 0,
|
|
2612
|
+
skipped: 1,
|
|
2613
|
+
failed: 0,
|
|
2614
|
+
});
|
|
2615
|
+
});
|
|
2616
|
+
|
|
2617
|
+
it("atomically upgrades historical generic footnotes and their projections", async () => {
|
|
2618
|
+
const post = await postService.create({
|
|
2619
|
+
format: "note",
|
|
2620
|
+
title: "Legacy",
|
|
2621
|
+
bodyMarkdown: "Placeholder",
|
|
2622
|
+
});
|
|
2623
|
+
const legacyBody = legacyFootnoteBody();
|
|
2624
|
+
await db
|
|
2625
|
+
.update(posts)
|
|
2626
|
+
.set({
|
|
2627
|
+
body: legacyBody,
|
|
2628
|
+
bodyHtml:
|
|
2629
|
+
'<p>Body <a href="#fn-1">[1]</a></p><hr><ol><li><p>Definition <a href="#fnref-1">↩︎</a></p></li></ol>',
|
|
2630
|
+
bodyHtmlVersion: 3,
|
|
2631
|
+
bodyText: "Body [1] #fn-1 Definition ↩︎ #fnref-1",
|
|
2632
|
+
summary: "Body [1]\n\nDefinition ↩︎",
|
|
2633
|
+
})
|
|
2634
|
+
.where(eq(posts.id, post.id));
|
|
2635
|
+
const before = await db
|
|
2636
|
+
.select({ updatedAt: posts.updatedAt, body: posts.body })
|
|
2637
|
+
.from(posts)
|
|
2638
|
+
.where(eq(posts.id, post.id))
|
|
2639
|
+
.limit(1);
|
|
2640
|
+
|
|
2641
|
+
const dryRun = await postService.rebuildBodyHtml({
|
|
2642
|
+
dryRun: true,
|
|
2643
|
+
summaryConfig: { maxParagraphs: 5, maxChars: 500 },
|
|
2644
|
+
});
|
|
2645
|
+
expect(dryRun).toMatchObject({
|
|
2646
|
+
wouldRebuild: 1,
|
|
2647
|
+
rebuilt: 0,
|
|
2648
|
+
wouldUpgradeFootnotes: 1,
|
|
2649
|
+
upgradedFootnotes: 0,
|
|
2650
|
+
failed: 0,
|
|
2651
|
+
});
|
|
2652
|
+
expect(
|
|
2653
|
+
await db
|
|
2654
|
+
.select({ body: posts.body })
|
|
2655
|
+
.from(posts)
|
|
2656
|
+
.where(eq(posts.id, post.id))
|
|
2657
|
+
.limit(1),
|
|
2658
|
+
).toEqual([{ body: legacyBody }]);
|
|
2659
|
+
|
|
2660
|
+
const result = await postService.rebuildBodyHtml({
|
|
2661
|
+
summaryConfig: { maxParagraphs: 5, maxChars: 500 },
|
|
2662
|
+
});
|
|
2663
|
+
expect(result).toMatchObject({
|
|
2664
|
+
rebuilt: 1,
|
|
2665
|
+
wouldUpgradeFootnotes: 1,
|
|
2666
|
+
upgradedFootnotes: 1,
|
|
2667
|
+
conflicted: 0,
|
|
2668
|
+
failed: 0,
|
|
2669
|
+
});
|
|
2670
|
+
|
|
2671
|
+
const stored = await db
|
|
2672
|
+
.select({
|
|
2673
|
+
body: posts.body,
|
|
2674
|
+
bodyHtml: posts.bodyHtml,
|
|
2675
|
+
bodyHtmlVersion: posts.bodyHtmlVersion,
|
|
2676
|
+
bodyText: posts.bodyText,
|
|
2677
|
+
summary: posts.summary,
|
|
2678
|
+
updatedAt: posts.updatedAt,
|
|
2679
|
+
})
|
|
2680
|
+
.from(posts)
|
|
2681
|
+
.where(eq(posts.id, post.id))
|
|
2682
|
+
.limit(1);
|
|
2683
|
+
expect(stored[0]?.body).toContain('"type":"footnoteReference"');
|
|
2684
|
+
expect(stored[0]?.body).toContain('"type":"footnoteDefinition"');
|
|
2685
|
+
expect(stored[0]?.bodyHtml).toContain('role="doc-noteref"');
|
|
2686
|
+
expect(stored[0]?.bodyHtml).toContain('role="doc-endnotes"');
|
|
2687
|
+
expect(stored[0]?.bodyText).toBe("Body Definition");
|
|
2688
|
+
expect(stored[0]?.summary).toBe("Body");
|
|
2689
|
+
expect(stored[0]?.bodyHtmlVersion).toBe(POST_BODY_HTML_VERSION);
|
|
2690
|
+
expect(stored[0]?.updatedAt).toBe(before[0]?.updatedAt);
|
|
2691
|
+
|
|
2692
|
+
await expect(postService.rebuildBodyHtml()).resolves.toMatchObject({
|
|
2693
|
+
wouldRebuild: 0,
|
|
2694
|
+
wouldUpgradeFootnotes: 0,
|
|
2695
|
+
upgradedFootnotes: 0,
|
|
2696
|
+
skipped: 1,
|
|
2697
|
+
});
|
|
2698
|
+
});
|
|
2699
|
+
|
|
2700
|
+
it("reports malformed canonical bodies without marking them current", async () => {
|
|
2701
|
+
const post = await createFootnotePost("invalid");
|
|
2702
|
+
await db
|
|
2703
|
+
.update(posts)
|
|
2704
|
+
.set({
|
|
2705
|
+
body: "not json",
|
|
2706
|
+
bodyHtml: "<p>legacy fallback</p>",
|
|
2707
|
+
bodyHtmlVersion: 1,
|
|
2708
|
+
})
|
|
2709
|
+
.where(eq(posts.id, post.id));
|
|
2710
|
+
|
|
2711
|
+
const result = await postService.rebuildBodyHtml();
|
|
2712
|
+
expect(result.failed).toBe(1);
|
|
2713
|
+
expect(result.failures[0]?.postId).toBe(post.id);
|
|
2714
|
+
expect(result.rebuilt).toBe(0);
|
|
2715
|
+
|
|
2716
|
+
const stored = await db
|
|
2717
|
+
.select({
|
|
2718
|
+
bodyHtml: posts.bodyHtml,
|
|
2719
|
+
bodyHtmlVersion: posts.bodyHtmlVersion,
|
|
2720
|
+
})
|
|
2721
|
+
.from(posts)
|
|
2722
|
+
.where(eq(posts.id, post.id))
|
|
2723
|
+
.limit(1);
|
|
2724
|
+
expect(stored[0]).toEqual({
|
|
2725
|
+
bodyHtml: "<p>legacy fallback</p>",
|
|
2726
|
+
bodyHtmlVersion: 1,
|
|
2727
|
+
});
|
|
2728
|
+
expect((await postService.getById(post.id))?.bodyHtml).toBe(
|
|
2729
|
+
"<p>legacy fallback</p>",
|
|
2730
|
+
);
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2733
|
+
it("paginates deterministically", async () => {
|
|
2734
|
+
const created = [];
|
|
2735
|
+
for (const label of ["one", "two", "three"]) {
|
|
2736
|
+
created.push(await createFootnotePost(label));
|
|
2737
|
+
}
|
|
2738
|
+
await db
|
|
2739
|
+
.update(posts)
|
|
2740
|
+
.set({ bodyHtmlVersion: 1 })
|
|
2741
|
+
.where(eq(posts.siteId, DEFAULT_TEST_SITE_ID));
|
|
2742
|
+
|
|
2743
|
+
const first = await postService.rebuildBodyHtml({ limit: 2 });
|
|
2744
|
+
expect(first.processed).toBe(2);
|
|
2745
|
+
expect(first.done).toBe(false);
|
|
2746
|
+
expect(first.nextCursor).not.toBeNull();
|
|
2747
|
+
|
|
2748
|
+
const second = await postService.rebuildBodyHtml({
|
|
2749
|
+
limit: 2,
|
|
2750
|
+
cursor: first.nextCursor ?? undefined,
|
|
2751
|
+
});
|
|
2752
|
+
expect(second.processed).toBe(1);
|
|
2753
|
+
expect(second.done).toBe(true);
|
|
2754
|
+
expect(first.rebuilt + second.rebuilt).toBe(created.length);
|
|
2755
|
+
});
|
|
2756
|
+
|
|
2757
|
+
it("lets a concurrent canonical-body edit win the compare-and-swap", async () => {
|
|
2758
|
+
const post = await createFootnotePost("conflict");
|
|
2759
|
+
await db
|
|
2760
|
+
.update(posts)
|
|
2761
|
+
.set({ bodyHtmlVersion: 1 })
|
|
2762
|
+
.where(eq(posts.id, post.id));
|
|
2763
|
+
|
|
2764
|
+
const concurrentBody = JSON.stringify({
|
|
2765
|
+
type: "doc",
|
|
2766
|
+
content: [
|
|
2767
|
+
{
|
|
2768
|
+
type: "paragraph",
|
|
2769
|
+
content: [{ type: "text", text: "Concurrent edit" }],
|
|
2770
|
+
},
|
|
2771
|
+
],
|
|
2772
|
+
});
|
|
2773
|
+
let injected = false;
|
|
2774
|
+
const conflictDb = new Proxy(db, {
|
|
2775
|
+
get(target, property, receiver) {
|
|
2776
|
+
if (property === "update") {
|
|
2777
|
+
return (...args: Parameters<Database["update"]>) => {
|
|
2778
|
+
if (!injected) {
|
|
2779
|
+
injected = true;
|
|
2780
|
+
sqlite
|
|
2781
|
+
.prepare("UPDATE post SET body = ? WHERE id = ?")
|
|
2782
|
+
.run(concurrentBody, post.id);
|
|
2783
|
+
}
|
|
2784
|
+
return target.update(...args);
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
const value = Reflect.get(target, property, receiver) as unknown;
|
|
2789
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
2790
|
+
},
|
|
2791
|
+
});
|
|
2792
|
+
const conflictService = createPostService(
|
|
2793
|
+
conflictDb,
|
|
2794
|
+
{ slugIdLength: 5 },
|
|
2795
|
+
DEFAULT_TEST_SITE_ID,
|
|
2796
|
+
);
|
|
2797
|
+
|
|
2798
|
+
const result = await conflictService.rebuildBodyHtml();
|
|
2799
|
+
|
|
2800
|
+
expect(result).toMatchObject({ rebuilt: 0, conflicted: 1 });
|
|
2801
|
+
const stored = await db
|
|
2802
|
+
.select({ body: posts.body, version: posts.bodyHtmlVersion })
|
|
2803
|
+
.from(posts)
|
|
2804
|
+
.where(eq(posts.id, post.id))
|
|
2805
|
+
.limit(1);
|
|
2806
|
+
expect(stored[0]).toEqual({ body: concurrentBody, version: 1 });
|
|
2807
|
+
});
|
|
2808
|
+
|
|
2809
|
+
it("never widens a rebuild to another site", async () => {
|
|
2810
|
+
const defaultPost = await createFootnotePost("default");
|
|
2811
|
+
const secondSiteId = "sit_second000000000000000000000";
|
|
2812
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
2813
|
+
await db.insert(sites).values({
|
|
2814
|
+
id: secondSiteId,
|
|
2815
|
+
key: "second",
|
|
2816
|
+
status: "active",
|
|
2817
|
+
createdAt: timestamp,
|
|
2818
|
+
updatedAt: timestamp,
|
|
2819
|
+
});
|
|
2820
|
+
const secondService = createPostService(
|
|
2821
|
+
db,
|
|
2822
|
+
{ slugIdLength: 5 },
|
|
2823
|
+
secondSiteId,
|
|
2824
|
+
);
|
|
2825
|
+
const secondPost = await secondService.create({
|
|
2826
|
+
format: "note",
|
|
2827
|
+
bodyMarkdown: "Second[^1]\n\n[^1]: Other site",
|
|
2828
|
+
});
|
|
2829
|
+
await db
|
|
2830
|
+
.update(posts)
|
|
2831
|
+
.set({ bodyHtmlVersion: 1 })
|
|
2832
|
+
.where(eq(posts.id, defaultPost.id));
|
|
2833
|
+
await db
|
|
2834
|
+
.update(posts)
|
|
2835
|
+
.set({ bodyHtmlVersion: 1 })
|
|
2836
|
+
.where(eq(posts.id, secondPost.id));
|
|
2837
|
+
|
|
2838
|
+
const result = await postService.rebuildBodyHtml();
|
|
2839
|
+
expect(result.processed).toBe(1);
|
|
2840
|
+
|
|
2841
|
+
const rows = await db
|
|
2842
|
+
.select({ id: posts.id, version: posts.bodyHtmlVersion })
|
|
2843
|
+
.from(posts);
|
|
2844
|
+
expect(rows.find((row) => row.id === defaultPost.id)?.version).toBe(
|
|
2845
|
+
POST_BODY_HTML_VERSION,
|
|
2846
|
+
);
|
|
2847
|
+
expect(rows.find((row) => row.id === secondPost.id)?.version).toBe(1);
|
|
2848
|
+
});
|
|
2849
|
+
});
|
|
2850
|
+
|
|
2289
2851
|
describe("listForSitemap", () => {
|
|
2290
2852
|
it("returns published non-reply non-private non-deleted posts", async () => {
|
|
2291
2853
|
const root = await postService.create({
|