Skip to content

feat(marketplace): plugin marketplace data model + Go store — closes #249 - #396

Merged
tayebmokni merged 1 commit into
mainfrom
feat/249-marketplace-model
May 18, 2026
Merged

feat(marketplace): plugin marketplace data model + Go store — closes #249#396
tayebmokni merged 1 commit into
mainfrom
feat/249-marketplace-model

Conversation

@tayebmokni

Copy link
Copy Markdown
Contributor

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, author
    FK 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_at trigger.
  • 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. tested BOOLEAN. 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/marketplace

  • doc.go + types.go — sentinels (ErrNotFound, ErrAlreadyExists,
    ErrInvalidInput), typed enums (ListingStatus, InstallEventType
    with .Valid()), and the five row structs.
  • store.go — umbrella Store, PgxQuerier interface (so callers can
    pass a pgx.Tx), SQLSTATE helpers translating 23505→ErrAlreadyExists
    and 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 on tested),
    ListByVersion.
  • ratings.go — Submit (UPSERT, created_at preserved on re-rate),
    Aggregate (zero-valued Aggregate for empty versions, not
    ErrNotFound).
  • events.go — RecordInstallEvent (append-only), CountByListing with
    a window=0 lifetime path.

Tests

testcontainers Postgres applying the full migration tree:

  • 16 unit tests — validation guards (empty slug, out-of-range stars,
    bad enum), constructor panics, SQLSTATE detection.
  • 15 integration tests — Listings create + slug uniqueness + category
    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-rating
    case, Events monotonic id + window count, full migration
    up→down→up round-trip.

Test plan

  • cd packages/go && go vet ./plugins/marketplace/... — clean
  • cd packages/go && go test -race -count=1 -short ./plugins/marketplace/... — 16/16 unit pass, 15 integration skip cleanly
  • cd 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

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
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
tayebmokni force-pushed the feat/249-marketplace-model branch from 9ea67a5 to 43da7be Compare May 18, 2026 22:46
@tayebmokni
tayebmokni merged commit ae3a99f into main May 18, 2026
9 checks passed
@tayebmokni
tayebmokni deleted the feat/249-marketplace-model branch May 18, 2026 22:52
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 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants