Skip to content

Commit c28bc5f

Browse files
thunpisitclaude
andauthored
fix: lint errors + FTS5 external-content migration (matches upstream PR codustry#49 + PR codustry#50) (#15)
Cherry-picks both upstream fixes: - PR codustry#49 (closes codustry#47) — 7 ESLint errors blocking CI on fresh install - PR codustry#50 (closes codustry#48) — switch articles_fts from contentless to external-content FTS5; UPDATE/DELETE on article_localizations no longer throws SQLITE_ERROR Migration 0011 already applied to live D1 (same database serves both repos). Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a05dcd2 commit c28bc5f

8 files changed

Lines changed: 99 additions & 5 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
-- Fix #48: switch articles_fts from a contentless FTS5 table to an
2+
-- external-content table (content='article_localizations',
3+
-- content_rowid='rowid').
4+
--
5+
-- The contentless pattern from migration 0002 requires the
6+
-- ('delete', old.rowid, …all column values…) tuple to **exactly
7+
-- match** what's stored in the index. Any drift — different
8+
-- normalization, trailing newline, prior failed REPLACE — makes the
9+
-- delete-trigger fail with SQLITE_ERROR. UPDATE inherits that fault.
10+
-- After-effect on the CMS: editors couldn't update or delete an
11+
-- article_localizations row once any drift had occurred.
12+
--
13+
-- The external-content pattern lets the trigger reference rowid
14+
-- alone:
15+
-- INSERT INTO articles_fts(articles_fts, rowid) VALUES('delete', old.rowid);
16+
-- — no column values to match against, so drift becomes a non-issue.
17+
-- And `rebuild` can recover from any inconsistency without dropping
18+
-- the table.
19+
--
20+
-- Migration steps:
21+
-- 1. Drop the old triggers (must happen before dropping the table).
22+
-- 2. Drop the old virtual table.
23+
-- 3. Recreate as external-content.
24+
-- 4. Rebuild the index from article_localizations in one shot.
25+
-- 5. Recreate triggers using the simpler delete pattern.
26+
27+
DROP TRIGGER IF EXISTS articles_fts_ai;
28+
--> statement-breakpoint
29+
DROP TRIGGER IF EXISTS articles_fts_ad;
30+
--> statement-breakpoint
31+
DROP TRIGGER IF EXISTS articles_fts_au;
32+
--> statement-breakpoint
33+
DROP TABLE IF EXISTS articles_fts;
34+
--> statement-breakpoint
35+
36+
CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts USING fts5(
37+
title,
38+
excerpt,
39+
body,
40+
locale UNINDEXED,
41+
article_id UNINDEXED,
42+
content = 'article_localizations',
43+
content_rowid = 'rowid',
44+
tokenize = 'unicode61 remove_diacritics 2'
45+
);
46+
--> statement-breakpoint
47+
48+
-- Build the index from current state. With external-content, this
49+
-- reads from article_localizations directly (the `content =` link
50+
-- above tells FTS5 where to look).
51+
INSERT INTO articles_fts(articles_fts) VALUES('rebuild');
52+
--> statement-breakpoint
53+
54+
-- Triggers — simpler than before because external-content tables
55+
-- only need rowid for delete (the source-of-truth values come from
56+
-- the linked table itself).
57+
58+
CREATE TRIGGER IF NOT EXISTS articles_fts_ai AFTER INSERT ON article_localizations BEGIN
59+
INSERT INTO articles_fts(rowid, title, excerpt, body, locale, article_id)
60+
VALUES (new.rowid, new.title, COALESCE(new.excerpt, ''), new.body, new.locale, new.article_id);
61+
END;
62+
--> statement-breakpoint
63+
64+
CREATE TRIGGER IF NOT EXISTS articles_fts_ad AFTER DELETE ON article_localizations BEGIN
65+
INSERT INTO articles_fts(articles_fts, rowid) VALUES('delete', old.rowid);
66+
END;
67+
--> statement-breakpoint
68+
69+
CREATE TRIGGER IF NOT EXISTS articles_fts_au AFTER UPDATE ON article_localizations BEGIN
70+
INSERT INTO articles_fts(articles_fts, rowid) VALUES('delete', old.rowid);
71+
INSERT INTO articles_fts(rowid, title, excerpt, body, locale, article_id)
72+
VALUES (new.rowid, new.title, COALESCE(new.excerpt, ''), new.body, new.locale, new.article_id);
73+
END;

drizzle/meta/_journal.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@
7878
"when": 1777709468165,
7979
"tag": "0010_windy_carmella_unuscione",
8080
"breakpoints": true
81+
},
82+
{
83+
"idx": 11,
84+
"version": "6",
85+
"when": 1778060000000,
86+
"tag": "0011_fts5_external_content",
87+
"breakpoints": true
8188
}
8289
]
8390
}

src/lib/components/consent/CookieBanner.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
<p class="font-medium text-foreground mb-1">{m.cookie_banner_title()}</p>
4747
<p>
4848
{m.cookie_banner_body()}
49+
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- privacy href is operator-supplied (always /[locale]/privacy-policy from layout); not a build-time route -->
4950
<a href={privacyHref} class="underline hover:text-foreground">{m.cookie_banner_learn_more()}</a>
5051
</p>
5152
{#if detailsOpen}

src/lib/components/seo/Seo.svelte

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,16 @@
8888
<meta name="twitter:site" content={defaults.twitter} />
8989
{/if}
9090

91-
<!-- JSON-LD: one <script> per entry for maximum tooling compatibility -->
91+
<!-- JSON-LD: one <script> per entry for maximum tooling compatibility.
92+
Using {@html} is required to emit a literal <script> tag (Svelte
93+
strips them in normal markup). The payload is server-built JSON
94+
from our own typed builders ($lib/seo) — never user input — and
95+
we escape any "</script>" sequence before injection so a JSON
96+
value can't break out of the tag. -->
9297
{#each jsonLd as ld, i (i)}
93-
{@html `<script type="application/ld+json">${JSON.stringify(ld)}<\/script>`}
98+
{@const safeJson = JSON.stringify(ld).replace(/<\/script>/gi, '<\\/script>')}
99+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted: server-built JSON-LD with </script> escaped -->
100+
{@html '<script type="application/ld+json">' + safeJson + '<' + '/script>'}
94101
{/each}
95102

96103
<!-- RSS auto-discovery -->

src/lib/server/webhooks/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ async function deliverOne(
7979
for (let attempt = 1; attempt <= MAX_INLINE_ATTEMPTS; attempt++) {
8080
const t0 = Date.now();
8181
let responseStatus: number | null = null;
82-
let responseExcerpt: string | null = null;
82+
// Assigned in every code path below; no initializer to satisfy
83+
// no-useless-assignment.
84+
let responseExcerpt: string | null;
8385
let ok = false;
8486
try {
8587
const ctrl = new AbortController();

src/routes/(cms)/cms/dashboard/+page.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { redirect, error } from "@sveltejs/kit";
22
import { drizzle } from "drizzle-orm/d1";
3-
import { desc, eq, and, gte } from "drizzle-orm";
3+
import { desc, eq, gte } from "drizzle-orm";
44
import * as schema from "$lib/server/content/schema";
55
import { canManageUsers } from "$lib/server/auth/permissions";
66
import { AnalyticsService } from "$lib/server/analytics";

src/routes/(cms)/cms/media/+page.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@
3434
Boolean(data.user && ['super_admin', 'admin', 'editor'].includes(data.user.role)),
3535
);
3636
37-
// Build a parent → children map for the folder tree.
37+
// Build a parent → children map for the folder tree. Local to the
38+
// derived; never read reactively (rebuilt every invocation), so a
39+
// plain Map is correct — no need for SvelteMap's reactivity overhead.
3840
const childrenByParent = $derived.by(() => {
41+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- local lookup, not reactive state
3942
const map = new Map<string | null, MediaFolderRecord[]>();
4043
for (const f of data.folders) {
4144
const parent = f.parentId ?? null;

src/routes/(www)/[locale]/[...slug]/+page.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<h1 class="text-4xl font-bold mb-2">{data.title}</h1>
2323
</header>
2424
<div class="prose prose-neutral dark:prose-invert max-w-none">
25+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted: server-rendered markdown from CMS -->
2526
{@html data.htmlContent}
2627
</div>
2728
</article>

0 commit comments

Comments
 (0)