Skip to content

fix: correct migration rollback, media ID, MCP taxonomy tools, and FTS escaping#497

Merged
ascorbic merged 4 commits intomainfrom
fix/correctness-bugs
Apr 13, 2026
Merged

fix: correct migration rollback, media ID, MCP taxonomy tools, and FTS escaping#497
ascorbic merged 4 commits intomainfrom
fix/correctness-bugs

Conversation

@ascorbic
Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes five correctness bugs identified in the code quality review (H4, H5, H6, M14, M19).

  • Migration 011 rollback (H4): down() was dropping indexes from migration 015 (idx_content_taxonomies_term, idx_media_mime_type) instead of its own (idx_sections_category, idx_sections_source). Rolling back 011 would destroy unrelated indexes.

  • Plugin media upload ID (H5): createMediaAccessWithWrite.upload() generated a ULID for the storage key prefix but returned it as mediaId. The database record got a different ULID from mediaRepo.create(). Plugins storing the returned mediaId would reference a non-existent record. Fix: return the DB-generated ID.

  • MCP taxonomy tools (H6, M14): taxonomy_list and taxonomy_create_term used raw Kysely queries with as never casts, bypassing type safety. taxonomy_create_term inserted directly without slug validation or duplicate checking. taxonomy_list had unguarded JSON.parse. Rewritten to delegate to handler layer (handleTaxonomyList, handleTermCreate) which provide validation, dedup, and proper error handling. taxonomy_list_terms retains its paginated inline query (handler has no pagination and N+1 queries) but uses the repository for type safety.

  • FTS escapeQuery (M19): The operator detection check (AND, OR, NOT, NEAR) was testing the raw query but returning the escaped string, producing malformed FTS5 when queries contained both quotes and operators. Reordered to escape quotes first, then check for operators and return the escaped result.

Type of change

  • Bug fix

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added a changeset

AI-generated code disclosure

  • This PR includes AI-generated code

…S query escaping

- Migration 011 down(): drop own indexes (idx_sections_*) instead of
  indexes from migration 015 (H4)
- Plugin media upload: return the DB-generated media ID instead of the
  orphaned ULID used only for the storage key prefix (H5)
- MCP taxonomy tools: delegate to handler layer (handleTaxonomyList,
  handleTermList, handleTermCreate) instead of raw SQL with as-never
  casts. Gets slug validation, duplicate checking, and type safety (H6, M14)
- FTS escapeQuery: check for operators before escaping quotes, not after.
  Previously, queries with both quotes and operators produced malformed
  FTS5 syntax (M19)
…gination

Addresses adversarial review:
- escapeQuery: restore quote escaping before returning on operator path.
  The previous fix removed escaping, creating a regression where queries
  with both quotes and operators were sent raw to FTS5.
- MCP taxonomy_list_terms: restore limit/cursor pagination and { items }
  response shape. The handler layer (handleTermList) has no pagination
  and runs N+1 queries, so keep paginated inline query via repository.
Copilot AI review requested due to automatic review settings April 12, 2026 19:36
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 12, 2026

🦋 Changeset detected

Latest commit: 8885917

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 12, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground 8885917 Apr 13 2026, 06:39 PM

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 12, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@497

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@497

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@497

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@497

emdash

npm i https://pkg.pr.new/emdash@497

create-emdash

npm i https://pkg.pr.new/create-emdash@497

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@497

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@497

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@497

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@497

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@497

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@497

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@497

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@497

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@497

commit: 8885917

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes multiple correctness issues across migrations, plugin media upload, MCP taxonomy tools, and FTS query escaping.

Changes:

  • Corrects migration 011_sections rollback to drop the proper indexes.
  • Fixes plugin media upload to return the DB-created media ID (not the storage key ULID).
  • Refactors MCP taxonomy tools to use handler/repository layers and adjusts FTS query escaping behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/core/src/search/query.ts Adjusts FTS5 query escaping logic (operators + quote escaping).
packages/core/src/plugins/context.ts Returns DB media ID from upload flow; separates storage key prefix from media ID.
packages/core/src/mcp/server.ts Delegates taxonomy list/create to handlers; refactors term listing to repository-based access.
packages/core/src/database/migrations/011_sections.ts Fixes rollback to drop the correct indexes for migration 011.
.changeset/slow-flies-leave.md Adds a patch changeset describing the fixes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +381 to +383
// If already quoted, pass through with quotes escaped
if (query.startsWith('"') && query.endsWith('"')) {
return query;
return escaped;
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

escapeQuery() treats already-quoted queries as needing quote escaping and returns escaped, but escaped is produced by replacing all " with "" (including the leading/trailing phrase delimiters). For an input like "hello world", this becomes ""hello world"", which changes/breaks FTS5 phrase syntax. Preserve the outer quotes and only escape interior quotes (e.g., strip the outer quotes first, escape the inner content, then re-wrap), or perform the already-quoted check before global quote escaping.

Copilot uses AI. Check for mistakes.
Comment on lines +1287 to +1294
// Verify taxonomy exists via handler layer
const { handleTaxonomyList } = await import("../api/handlers/taxonomies.js");
const listResult = await handleTaxonomyList(ec.db);
if (!listResult.success) return unwrap(listResult);

const taxonomies = (listResult.data as { taxonomies: Array<{ name: string; id?: string }> })
.taxonomies;
const taxonomy = taxonomies.find((t: { name: string }) => t.name === args.taxonomy);
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

taxonomy_list_terms verifies taxonomy existence by calling handleTaxonomyList() and then scanning the full list. This couples term listing to successfully parsing all taxonomy defs (including collections JSON) and can fail even when the requested taxonomy exists. Prefer a direct lookup (e.g., the requireTaxonomyDef logic used in handlers/taxonomies.ts) so term listing only depends on the requested taxonomy row.

Suggested change
// Verify taxonomy exists via handler layer
const { handleTaxonomyList } = await import("../api/handlers/taxonomies.js");
const listResult = await handleTaxonomyList(ec.db);
if (!listResult.success) return unwrap(listResult);
const taxonomies = (listResult.data as { taxonomies: Array<{ name: string; id?: string }> })
.taxonomies;
const taxonomy = taxonomies.find((t: { name: string }) => t.name === args.taxonomy);
// Verify only the requested taxonomy exists via direct handler lookup.
// This avoids coupling term listing to parsing every taxonomy definition.
const { handleTaxonomyGet } = await import("../api/handlers/taxonomies.js");
const taxonomyResult = await handleTaxonomyGet(ec.db, args.taxonomy);
if (!taxonomyResult.success) return unwrap(taxonomyResult);
const taxonomy = (taxonomyResult.data as { taxonomy?: { name: string; id?: string } }).taxonomy;

Copilot uses AI. Check for mistakes.
Comment on lines +1297 to +1304
// Paginated term query via repository (avoids N+1 of handleTermList)
const { TaxonomyRepository } = await import("../database/repositories/taxonomy.js");
const repo = new TaxonomyRepository(ec.db);
const limit = Math.min(args.limit ?? 50, 100);
let query = ec.db
.selectFrom("_emdash_taxonomy_terms" as never)
.selectAll()
.where("taxonomy_id" as never, "=", taxonomy.id as never)
.orderBy("label" as never, "asc")
.limit(limit + 1);
const terms = await repo.findByName(args.taxonomy);

// Manual cursor pagination over the sorted results
let startIdx = 0;
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pagination implementation loads all terms via repo.findByName() and then slices in memory. That defeats DB-level pagination and can become expensive for large taxonomies. Consider moving pagination into the repository/SQL query (e.g., ORDER BY label, id with a cursor encoding both fields, or switch ordering to id if you want to keep an id cursor) so the DB only returns limit + 1 rows.

Copilot uses AI. Check for mistakes.
Comment on lines +458 to +460
let media;
try {
await mediaRepo.create({
media = await mediaRepo.create({
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let media; introduces an untyped local that becomes any, losing type safety around media.id. Prefer declaring it with a concrete type (e.g., let media: MediaItem) or restructure to keep const media = await mediaRepo.create(...) in scope for the return (e.g., return after the try/catch).

Copilot uses AI. Check for mistakes.
The global " → "" replacement was running before the already-quoted
check, so input like "hello world" became ""hello world"", breaking
FTS5 phrase syntax. Move the quoted-phrase check first and only escape
interior quotes.
@ascorbic ascorbic merged commit 6492ea2 into main Apr 13, 2026
29 checks passed
@ascorbic ascorbic deleted the fix/correctness-bugs branch April 13, 2026 18:49
@emdashbot emdashbot bot mentioned this pull request Apr 13, 2026
fmhall pushed a commit to fmhall/emdash that referenced this pull request Apr 13, 2026
…S escaping (emdash-cms#497)

* fix: correct migration rollback, media ID, MCP taxonomy tools, and FTS query escaping

- Migration 011 down(): drop own indexes (idx_sections_*) instead of
  indexes from migration 015 (H4)
- Plugin media upload: return the DB-generated media ID instead of the
  orphaned ULID used only for the storage key prefix (H5)
- MCP taxonomy tools: delegate to handler layer (handleTaxonomyList,
  handleTermList, handleTermCreate) instead of raw SQL with as-never
  casts. Gets slug validation, duplicate checking, and type safety (H6, M14)
- FTS escapeQuery: check for operators before escaping quotes, not after.
  Previously, queries with both quotes and operators produced malformed
  FTS5 syntax (M19)

* fix: restore quote escaping on FTS operator path, restore MCP term pagination

Addresses adversarial review:
- escapeQuery: restore quote escaping before returning on operator path.
  The previous fix removed escaping, creating a regression where queries
  with both quotes and operators were sent raw to FTS5.
- MCP taxonomy_list_terms: restore limit/cursor pagination and { items }
  response shape. The handler layer (handleTermList) has no pagination
  and runs N+1 queries, so keep paginated inline query via repository.

* chore: add changeset for correctness bug fixes

* fix: escape only interior quotes in FTS5 quoted phrases

The global " → "" replacement was running before the already-quoted
check, so input like "hello world" became ""hello world"", breaking
FTS5 phrase syntax. Move the quoted-phrase check first and only escape
interior quotes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants