feat(marketplace): plugin marketplace data model + Go store — closes #249 - #396
Merged
Conversation
tayebmokni
pushed a commit
that referenced
this pull request
May 18, 2026
…H9 marketplace H9 #396 (marketplace data model) ships 000018-000022 as a five-migration block. Renaming RUM to 000023 so both can merge cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com>
tayebmokni
enabled auto-merge (squash)
May 18, 2026 21:25
…249 Lays the data-model and Go store-layer foundation for the community plugin marketplace. The marketplace UI itself is a separate, later issue; this PR ships the durable schema and the typed read/write surface that the future UI (and any CLI/worker) will share. ## Migrations 000018–000022 - `000018_plugin_listings`: catalogue rows (slug UNIQUE, name, author FK to users, homepage, SPDX license, primary_category, status with CHECK draft|listed|delisted|banned). Partial index on category WHERE status='listed' for the catalogue browse query, partial index on author for the publisher dashboard. Dedicated marketplace_touch_updated_at trigger (the platform-wide touch_updated_at_and_version expects a `version` column the listings row doesn't carry). - `000019_plugin_versions`: per-listing artefact records — version string, wasm_sha256 BYTEA (32 bytes, CHECK octet_length=32), manifest JSONB, optional signature_hex, published_at, deprecated_at. UNIQUE (listing_id, version). Index on (listing_id, published_at DESC) for the "newest first" listing-detail read; index on wasm_sha256 for dedup checks. - `000020_plugin_compat_matrix`: composite PK (plugin_version_id, host_min, host_max) so a version can declare multiple disjoint ranges. `tested` BOOLEAN distinguishes verified-under-CI from declared-only. Range CHECK host_min <= host_max catches the typo class. - `000021_plugin_ratings`: composite PK (plugin_version_id, user_id) so a user gets one row per version (not per listing — a v1 rating stays on v1 when v2 ships). SMALLINT stars CHECK 1..5. Compound index on (plugin_version_id, stars) for index-only AVG(stars) scans. - `000022_plugin_install_events`: append-only telemetry, BIGSERIAL PK, TEXT host_id (privacy-respecting hashed signature), event_type CHECK installed|activated|uninstalled|errored. FKs ON DELETE SET NULL so historical events survive parent deletes. Partial index on errored events for the per-version error-rate query. Every up-migration ships a matching down.sql; round-trip is exercised in TestIntegration_Migrations_RoundTrip. ## Go package — packages/go/plugins/marketplace - `doc.go` + `types.go` — sentinels (`ErrNotFound`, `ErrAlreadyExists`, `ErrInvalidInput`), typed enums (`ListingStatus`, `InstallEventType` with `Valid()`), and the five row structs. - `store.go` — umbrella `Store` bundling the five sub-stores; PgxQuerier interface so callers can pass a pgx.Tx for cross-table transactional writes. SQLSTATE helpers (`isUniqueViolation` → ErrAlreadyExists, `isCheckViolation` → ErrInvalidInput). - `listings.go` — Create/Get/GetBySlug/List/ListByCategory/Update/Delete. Update is COALESCE-pattern (nil pointer = leave alone) so the SQL shape stays constant. - `versions.go` — Publish (sha256 of supplied wasm bytes), Get, ListByListing (DESC), Deprecate. Wasm bytes are NOT stored — only the digest. Callers push bytes to object storage keyed by that digest. - `compat.go` — Upsert (ON CONFLICT DO UPDATE on `tested`), ListByVersion. - `ratings.go` — Submit (UPSERT, created_at preserved on re-rate), Aggregate (returns zero-valued Aggregate for empty versions, not ErrNotFound). - `events.go` — RecordInstallEvent (append-only), CountByListing with a window=0 lifetime path. ## Tests Run via testcontainers Postgres applying the full migration tree: - 16 unit tests covering validation guards (empty slug, out-of-range stars, bad enum values), constructor panics, and SQLSTATE detection. - 15 integration tests covering: Listings create + slug uniqueness + ListByCategory partial-index behaviour + Update with trigger-driven updated_at + Delete, Versions Publish with sha256 capture + duplicate (listing,version) → ErrAlreadyExists + ListByListing ordering + Deprecate idempotency, Compat upsert + inverted-range rejection, Ratings aggregate avg+count + UPSERT preserving created_at + zero- rating empty case, Events monotonic id + window count + lifetime count, and a full migration up→down→up round-trip. ## Test plan - [x] `go vet ./plugins/marketplace/...` — clean - [x] `go test -race -count=1 -short ./plugins/marketplace/...` — 16/16 unit tests pass, 15 integration tests skip cleanly - [x] `go test -race -count=1 ./plugins/marketplace/...` — 31/31 pass under real Postgres (39s wall, one container per integration test) - [x] `go build ./...` — full workspace builds; no regression Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com>
tayebmokni
force-pushed
the
feat/249-marketplace-model
branch
from
May 18, 2026 22:46
9ea67a5 to
43da7be
Compare
tayebmokni
pushed a commit
that referenced
this pull request
May 18, 2026
…H9 marketplace H9 #396 (marketplace data model) ships 000018-000022 as a five-migration block. Renaming RUM to 000023 so both can merge cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com>
4 tasks
tayebmokni
pushed a commit
that referenced
this pull request
May 23, 2026
Adds the public-facing browse/install surface that sits next to the existing plugins manager (PR #385). The catalogue is fed by the marketplace data model from PR #396; the install path reuses the plugin lifecycle.Install runtime so consent semantics stay identical across manual and catalogue flows. Backend (apps/api/internal/admin/marketplace): - GET /api/v1/admin/marketplace/listings (category/q/sort) - GET /api/v1/admin/marketplace/listings/{slug} - GET /api/v1/admin/marketplace/listings/{slug}/versions - GET /api/v1/admin/marketplace/listings/{slug}/ratings - POST /api/v1/admin/marketplace/listings/{slug}/ratings (1..5 stars) - POST /api/v1/admin/marketplace/listings/{slug}/install - Read endpoints require an authenticated principal; install + rating POST gated on plugins.install (CapInstallPlugins). - Install dispatches to lifecycle.Install with the resolved version's wasm bytes via a BundleFetcher abstraction; success and failure both append a plugin_install_events row. Admin UI (apps/admin/src/app/marketplace): - page.tsx + MarketplaceClient — grid w/ search, category chips, and sort-by recent/stars/popular. - [slug]/page.tsx + ListingDetailView — description, version history, compat matrix, rating aggregate + submission form. - [slug]/install/page.tsx + InstallConfirm — reuses the CapabilityReview component from #385 (no duplication) and the consent checkbox before firing the install action. - components/RatingStars (read-only + interactive variants), components/MarketplaceCard. Tests: - Backend: filter/sort, slug detail/404, install dispatch + event capture, capability gate, rating range checks. - Frontend: grid render, URL push for search/sort/category, capability review reuse + consent gating, install success path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com>
tayebmokni
added a commit
that referenced
this pull request
May 23, 2026
## Summary - Adds `apps/api/internal/admin/marketplace` (handler + tests) exposing browse, detail, versions, ratings, and install endpoints over the data model from PR #396. Install dispatches to `lifecycle.Manager.Install` via a bundle-fetcher seam and records `plugin_install_events` rows. - Adds `apps/admin/src/app/marketplace/*` (Next.js App Router): catalogue grid with category chips + search + sort, listing detail with version history / compat matrix / ratings form, and an install confirmation screen that **reuses the `CapabilityReview` component from PR #385** rather than duplicating consent UX. - Sidebar gets a new `Marketplace` entry next to `Plugins`. ## Test plan - [x] `cd apps/api && go vet ./internal/admin/marketplace/...` - [x] `cd apps/api && go test -race -count=1 ./internal/admin/marketplace/...` (all green; covers list filters, slug 404, install dispatch + event capture, capability gate on install + rating, rating range checks) - [x] `pnpm --filter @gonext/admin typecheck` - [x] `pnpm --filter @gonext/admin test` (37 files / 255 tests pass — new tests: `MarketplaceClient.test.tsx`, `RatingStars.test.tsx`, `InstallConfirm.test.tsx` asserts the shared `CapabilityReview` is reused) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lays the data-model + Go store-layer foundation for the community plugin
marketplace (#249). The marketplace UI itself is a separate, later issue;
this PR ships the durable schema and the typed read/write surface the
future UI (and any CLI/worker) will share.
Migrations 000018–000022 (+ down)
000018_plugin_listings: catalogue rows — slug UNIQUE, name, authorFK to users, homepage, SPDX license, primary_category, status with
CHECK draft|listed|delisted|banned. Partial index on category
WHERE status='listed', partial index on author. Dedicated
marketplace_touch_updated_attrigger.000019_plugin_versions: per-listing artefact records — version,wasm_sha256 BYTEA(32 CHECK), manifest JSONB, optional signature_hex,
published_at, deprecated_at. UNIQUE(listing_id, version). Indexes on
(listing_id, published_at DESC) and on wasm_sha256.
000020_plugin_compat_matrix: composite PK (plugin_version_id,host_min, host_max) so a version can declare multiple disjoint
ranges.
testedBOOLEAN. Range CHECK host_min <= host_max.000021_plugin_ratings: composite PK (plugin_version_id, user_id)— one rating per user per version (a v1 rating stays on v1 when
v2 ships). SMALLINT stars CHECK 1..5. Compound index on
(plugin_version_id, stars) for index-only AVG(stars).
000022_plugin_install_events: append-only telemetry, BIGSERIAL PK,TEXT host_id (privacy-respecting hashed signature), event_type CHECK
installed|activated|uninstalled|errored. FKs ON DELETE SET NULL so
events survive parent deletes. Partial index on errored events.
Every up has a matching down; round-trip is exercised under Docker.
Go package —
packages/go/plugins/marketplacedoc.go+types.go— sentinels (ErrNotFound,ErrAlreadyExists,ErrInvalidInput), typed enums (ListingStatus,InstallEventTypewith
.Valid()), and the five row structs.store.go— umbrellaStore,PgxQuerierinterface (so callers canpass a
pgx.Tx), SQLSTATE helpers translating 23505→ErrAlreadyExistsand 23514→ErrInvalidInput.
listings.go— Create/Get/GetBySlug/List/ListByCategory/Update/Delete.Update uses the COALESCE pattern (nil pointer = leave alone) so the
SQL shape stays constant.
versions.go— Publish computes SHA-256 of the supplied wasm bytes;the bytes are not stored, only the digest. Get, ListByListing,
Deprecate.
compat.go— Upsert (ON CONFLICT DO UPDATE ontested),ListByVersion.
ratings.go— Submit (UPSERT,created_atpreserved on re-rate),Aggregate (zero-valued Aggregate for empty versions, not
ErrNotFound).
events.go— RecordInstallEvent (append-only), CountByListing witha window=0 lifetime path.
Tests
testcontainers Postgres applying the full migration tree:
bad enum), constructor panics, SQLSTATE detection.
partial-index + trigger-driven
updated_at+ delete cascade,Versions publish + SHA-256 capture + duplicate→ErrAlreadyExists +
Deprecate idempotency, Compat upsert + inverted-range rejection,
Ratings avg+count + UPSERT preserving
created_at+ zero-ratingcase, Events monotonic id + window count, full migration
up→down→up round-trip.
Test plan
cd packages/go && go vet ./plugins/marketplace/...— cleancd packages/go && go test -race -count=1 -short ./plugins/marketplace/...— 16/16 unit pass, 15 integration skip cleanlycd packages/go && go test -race -count=1 ./plugins/marketplace/...— 31/31 pass (~40s under Docker)cd packages/go && go build ./...— full workspace builds, no regression🤖 Generated with Claude Code