feat(marketplace): add categories, FTS5 search, and category filter UI#303
feat(marketplace): add categories, FTS5 search, and category filter UI#303BenjaminPrice wants to merge 8 commits intoemdash-cms:mainfrom
Conversation
…egory integration Adds structured categories with browse-by-category UX across the full stack: Schema: - categories table with 12 seed categories (seo, forms, analytics, etc.) - plugin_categories join table (max 3 per plugin) - FTS5 virtual table with sync triggers for full-text search - FTS5 input sanitization to prevent syntax injection Marketplace Worker: - GET /api/v1/categories endpoint - Category filter on GET /api/v1/plugins?category=slug - FTS5 MATCH search with LIKE fallback for unsanitizable input - Category assignment on plugin create/update (max 3 slugs) - Categories included in plugin detail response Core MarketplaceClient: - getCategories() method on MarketplaceClient interface - Resolves existing contract drift: category param was sent by client but ignored by the Worker Admin UI: - Category filter dropdown in MarketplaceBrowse - Categories fetched via new proxy route - Proxy route: GET /_emdash/api/admin/plugins/marketplace/categories Migration note for production D1: See schema.sql for new tables and FTS5 setup.
Marketplace Worker tests (12 new): - GET /categories returns seeded categories in sort order - Category filtering returns only matching plugins - Empty category returns empty results - Unfiltered search returns all plugins - Plugin detail includes categories - FTS5 searches by name and description - FTS5 returns empty for no matches - FTS5 sanitizes special characters without errors - FTS5 handles empty query after sanitization - Boolean operators are stripped from FTS5 queries - Special characters are stripped from queries Core MarketplaceClient tests (1 new): - getCategories() returns categories from marketplace API
🦋 Changeset detectedLatest commit: 7e77b95 The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
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 |
Scope checkThis PR changes 656 lines across 14 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/blocks
@emdash-cms/cloudflare
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
Pull request overview
Adds structured plugin categories and FTS5-backed search to the marketplace, wiring schema changes through the Marketplace Worker, core proxy/client APIs, and the admin browse UI.
Changes:
- Add
categories+plugin_categoriestables, plusplugins_ftsFTS5 index and triggers in the marketplace schema. - Extend marketplace public/author APIs to list categories, filter plugins by category, include plugin categories in detail, and accept category assignments (max 3).
- Add core + admin client/proxy support for fetching categories, and a category filter dropdown in the admin marketplace browse view.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/marketplace/tests/categories-fts.test.ts | Adds worker-level tests covering categories, category filtering, and FTS5 search/sanitization. |
| packages/marketplace/src/routes/public.ts | Adds GET /categories, adds category param to plugin search, includes categories in plugin detail. |
| packages/marketplace/src/routes/author.ts | Accepts optional categories on create/update and persists them via DB query helper. |
| packages/marketplace/src/db/types.ts | Adds CategoryRow and adds category to search options typing. |
| packages/marketplace/src/db/schema.sql | Adds categories tables, join table, and FTS5 virtual table + triggers; seeds 12 categories. |
| packages/marketplace/src/db/queries.ts | Adds FTS sanitization + FTS-based search condition, category filtering condition, and category CRUD/query helpers. |
| packages/core/tests/unit/plugins/marketplace-client.test.ts | Adds a unit test ensuring the core client calls /api/v1/categories and returns items. |
| packages/core/src/plugins/marketplace.ts | Introduces MarketplaceCategory type and MarketplaceClient.getCategories(). |
| packages/core/src/astro/routes/api/admin/plugins/marketplace/categories.ts | Adds an admin proxy route for categories with permission gating. |
| packages/core/src/api/handlers/marketplace.ts | Adds handleMarketplaceGetCategories() handler that calls the marketplace client. |
| packages/core/src/api/handlers/index.ts | Re-exports the new marketplace categories handler. |
| packages/admin/src/lib/api/marketplace.ts | Adds MarketplaceCategory type, fetchCategories(), and sends category in search params. |
| packages/admin/src/components/MarketplaceBrowse.tsx | Adds React Query call to load categories and a category filter dropdown wired into search. |
| .changeset/clean-walls-rule.md | Adds a changeset documenting the feature addition. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- FTS5 search now gracefully falls back to LIKE if the FTS table is
missing or the query fails (e.g. migration not yet applied). Extracted
query building into executeSearch() to cleanly support both modes.
- Added FTS backfill: INSERT INTO plugins_fts(plugins_fts) VALUES
('rebuild') ensures existing plugins are indexed after migration.
- setPluginCategories() now validates slugs before deleting existing
assignments. Unknown slugs throw an error instead of silently clearing
categories. Delete + insert run in a single db.batch() for atomicity.
Also deduplicates input slugs.
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
ascorbic
left a comment
There was a problem hiding this comment.
Thanks. This is a good improvement. My concern is that this would be the first change to the marketplace schema since release and we have no migration system in place. This is an existing issue, but has become relevant as we need to change the schea.
Could you add a migrations dir and add it to wrangler.jsonc, copy the current sql into an initial 000_initial migration, add another migration with your changes, then change the instructions to use wrangler d1 migrations apply rather than execute
Addresses maintainer feedback: set up a proper migration system before making schema changes, so production D1 databases are updated via wrangler d1 migrations apply instead of manual SQL execution. - migrations/0000_initial.sql: baseline schema as deployed at launch - migrations/0001_categories_fts.sql: categories, FTS5, seed data - wrangler.jsonc: added migrations_dir to D1 database config Tests continue to bootstrap from schema.sql (full schema for fresh in-memory DBs). Migrations are for production D1 incremental updates. Usage: wrangler d1 migrations apply emdash-marketplace --remote
|
Done. Added a
Production deployment: wrangler d1 migrations apply emdash-marketplace --remoteTests still bootstrap from Pushed in 7e77b95. |
What does this PR do?
Adds structured categories and FTS5 full-text search to the marketplace, wired end-to-end from the D1 schema through the API to the admin UI. Also resolves existing contract drift where the core MarketplaceClient sent a
categoryparam the Worker ignored.Type of change
Checklist
pnpm typecheckpassespnpm --silent lint:json | jq '.diagnostics | length'returns 0 (1 pre-existing warning in router.tsx)pnpm testpasses (3 pre-existing failures in auth tests, 0 new failures)pnpm formathas been runAI-generated code disclosure
Changes
Schema (
packages/marketplace/src/db/schema.sql)categoriestable (id, slug, name, description, icon, sort_order)plugin_categoriesjoin table with index on category_idplugins_ftswith INSERT/UPDATE/DELETE sync triggersMarketplace Worker (
packages/marketplace)GET /api/v1/categorieslists all categories sorted by sort_orderGET /api/v1/plugins?category=slugfilters by category via joinsanitizeFtsQuery()strips FTS5 operators (*"():^{}[]|!~) and boolean keywords (AND OR NOT NEAR), wraps remaining words in quotescategories: string[](max 3 slugs)categories: [{slug, name}]Core (
packages/core)MarketplaceClient.getCategories()added to interface + implementationhandleMarketplaceGetCategories()handlerGET /_emdash/api/admin/plugins/marketplace/categoriescategoryparam was already sent by the client but ignored by the WorkerAdmin UI (
packages/admin)fetchCategories()API function +MarketplaceCategorytypeMarketplaceBrowse(React Query, 5min stale time)Tests (13 new)
getCategories()API callMigration note for production D1