Alex Dunmow b6d40ed8ac feat: bootstrap block/pluginsdk — the proto-first plugin SDK (P1)
Bootstrap the plugin-facing SDK module so the fleet migration (P2) becomes a
mechanical block/core/X -> block/pluginsdk/X import rewrite.

- abi/proto/v1: the single source-of-truth ABI proto tree, copied from the cms
  authoring source (cms/backend/abi/proto/v1) with go_package retargeted to
  git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1. Kills the hand-kept
  cms/core proto duplication (audit gap 4).
- abi/v1: generated Go bindings (buf generate); byte-identical to core's abi/v1
  save the embedded go_package path.
- plugin/ (registration + DI surface), plugin/wasmguest/** (transport shim,
  caps stubs, bnwasm db driver, testdata fixtures + golden .pb), and the
  guest-facing type packages: blocks (+builtin/shared/tags), templates
  (+pongo/bn), auth, settings, content, gating, crypto, rbac, video, ai,
  subscriptions, menus, datasources. Internal imports rewritten core ->
  pluginsdk; zero block/core references remain.
- README/AGENTS(+CLAUDE symlink)/Makefile: proto is the contract, Go is one
  binding; no replace directives; templates/bn is a synced copy authored in cms.

Spec: cms docs/superpowers/specs/2026-07-07-proto-first-plugin-sdk-design.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:51:00 +08:00

121 lines
2.9 KiB
Go

package bnwasm
import (
"errors"
"fmt"
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
// pgxRows adapts a buffered DbRowsResponse to the pgx.Rows interface. The full
// result set is already materialized (the host returns it in one message), so
// iteration is a slice cursor. Only the methods sqlc-generated code exercises
// carry real behavior; the rest satisfy the interface.
type pgxRows struct {
columns []string
rows []*abiv1.DbRow
idx int // index of the row Next() last advanced to; -1 before first Next
err error
closed bool
}
func newPgxRows(resp *abiv1.DbRowsResponse) *pgxRows {
return &pgxRows{columns: resp.GetColumns(), rows: resp.GetRows(), idx: -1}
}
func (r *pgxRows) Close() { r.closed = true }
func (r *pgxRows) Err() error { return r.err }
func (r *pgxRows) CommandTag() pgconn.CommandTag {
return pgconn.NewCommandTag(fmt.Sprintf("SELECT %d", len(r.rows)))
}
func (r *pgxRows) FieldDescriptions() []pgconn.FieldDescription {
fds := make([]pgconn.FieldDescription, len(r.columns))
for i, c := range r.columns {
fds[i] = pgconn.FieldDescription{Name: c}
}
return fds
}
func (r *pgxRows) Next() bool {
if r.err != nil || r.closed {
return false
}
if r.idx+1 >= len(r.rows) {
r.Close() // pgx auto-closes when iteration is exhausted
return false
}
r.idx++
return true
}
func (r *pgxRows) Scan(dest ...any) error {
if r.err != nil {
return r.err
}
if r.idx < 0 || r.idx >= len(r.rows) {
return errors.New("bnwasm: Scan called without a current row (call Next first)")
}
if err := scanRow(r.rows[r.idx], dest); err != nil {
r.err = err
r.Close()
return err
}
return nil
}
func (r *pgxRows) Values() ([]any, error) {
if r.idx < 0 || r.idx >= len(r.rows) {
return nil, errors.New("bnwasm: Values called without a current row")
}
cells := r.rows[r.idx].GetValues()
out := make([]any, len(cells))
for i, c := range cells {
v, _ := naturalValue(c)
out[i] = v
}
return out, nil
}
func (r *pgxRows) RawValues() [][]byte { return nil }
func (r *pgxRows) Conn() *pgx.Conn { return nil }
// pgxRow is the single-row QueryRow result. Like pgx, it defers all errors
// (including the query error) to Scan, and reports pgx.ErrNoRows when empty.
type pgxRow struct {
rows *pgxRows
err error
}
func (r *pgxRow) Scan(dest ...any) error {
if r.err != nil {
return r.err
}
defer r.rows.Close()
if !r.rows.Next() {
if r.rows.Err() != nil {
return r.rows.Err()
}
return pgx.ErrNoRows
}
return r.rows.Scan(dest...)
}
func scanRow(row *abiv1.DbRow, dest []any) error {
cells := row.GetValues()
if len(dest) != len(cells) {
return fmt.Errorf("bnwasm: scan expected %d destinations, got %d columns", len(dest), len(cells))
}
for i, d := range dest {
if err := scanValue(cells[i], d); err != nil {
return fmt.Errorf("bnwasm: scan column %d: %w", i, err)
}
}
return nil
}