Documentation
¶
Overview ¶
Package gojabase is the REUSABLE read-write-Base goja host: it runs a Hanzo subsystem's self-contained JS bundle (globalThis.handle) inside dop251/goja and gives that bundle PERSISTENCE over per-tenant Base/SQLite, injected as native host globals. It is the storage-bearing sibling of clients/goja (which is the pure JS engine that plans/pricing use with a read-only catalog).
ONE-AND-ONLY-ONE-WAY. Any subsystem that wants "run my TS business logic in goja, persist to Base per tenant" uses THIS package: pass a Bundle + a per- tenant Schema (DDL) + the DataDir, get a Host, and Dispatch(ctx, tenant, req). captable is the pilot; esign (#100) and dataroom (#101) reuse it unchanged — the binding carries ZERO domain logic (no cap-table, no signatures, no rooms), only the engine + the Base bridge.
Host contract (what the binding injects onto the runtime per dispatch) ¶
globalThis.__db.query(sql, args) -> row objects (SELECT)
globalThis.__db.exec(sql, args) -> { changes, lastId } (INSERT/UPDATE/DELETE)
globalThis.__newId() -> collision-resistant id (crypto/rand)
globalThis.__now() -> unix milliseconds
globalThis.handle({ route, params, query, orgId, body }) -> { status, body }
`orgId` is the tenant the caller passed to Dispatch (the validated cloud principal's org); the binding uses it BOTH to select the per-tenant SQLite file AND passes it to handle so a bundle can scope rows by it (defence in depth). `body` is the decoded request body; `params`/`query` are string maps.
Atomicity ¶
Each Dispatch runs handle inside ONE SQLite transaction on the tenant's DB (deferred BEGIN → read-only until the first write). The transaction COMMITS iff handle returns status < 400 and does not throw; otherwise it ROLLS BACK. So a request is all-or-nothing without any JS-visible transaction API — a multi-statement mutation (e.g. a share transfer: delete source + insert target) is atomic for free, and a validation 400 leaves the DB untouched.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func TenantSegment ¶ added in v1.786.216
TenantSegment maps a tenant/org key to an INJECTIVE, traversal-safe single path segment: lowercased unpadded base32 of the RAW org bytes.
This is deliberately NOT a "slug": lowercasing + folding illegal bytes to '_' (the previous slugify) is NON-injective and collapses DISTINCT owners onto ONE physical store — a cross-tenant break. principal.Org returns the org VERBATIM for exactly that reason (folding "Acme"/"acme" or "a b"/"a_b" into one bucket is itself a tenant break); this encoder preserves that distinction all the way to the on-disk name. base32 of the raw bytes is a bijection with its output, so distinct orgs ALWAYS map to distinct segments: "Acme", "acme", "a b" and "a_b" each land on their OWN file (the previous slugify collapsed all four onto two).
The [a-z2-7] output contains no path separator and can never equal "." or ".." (those need chars outside the alphabet), so a segment can never traverse the data tree or an object-store key prefix. Empty tenant → "" (callers reject it).
It is the SINGLE encoding every gojabase tenant-keyed path uses — the per-tenant SQLite filename here AND the dataroom object-store key prefix — so a tenant maps to exactly ONE physical identity everywhere.
Types ¶
type BlobStore ¶ added in v1.786.216
type BlobStore interface {
Put(ctx context.Context, key string, payload []byte) error
Get(ctx context.Context, key string) ([]byte, error)
}
BlobStore is the ONE object-storage seam a bundle uses to persist large binary payloads OUTSIDE its per-tenant SQLite (e.g. sign's PDFs — a 32 MiB base64 blob in a TEXT column would bloat the tenant DB and get copied on every read). The cloud VFS/S3 data plane (deps.VFS) satisfies it, exactly as clients/dataroom already uses it for document bytes. Keys are opaque; gojabase tenant-scopes them.
type Config ¶
type Config struct {
// Name identifies the subsystem ("captable", "esign", "dataroom"). It names
// the goja host AND the per-tenant data subdir ({DataDir}/{Name}/).
Name string
// Bundle is the self-contained goja bundle exposing globalThis.handle.
Bundle []byte
// Schema is the per-tenant SQLite DDL, run (idempotently — use
// CREATE TABLE IF NOT EXISTS) on every tenant DB when it first opens.
Schema string
// DataDir is the deployment data root; per-tenant files land at
// {DataDir}/{Name}/{TenantSegment(tenant)}.db (injective, traversal-safe
// base32 of the raw org bytes — see gojabase/store.go TenantSegment).
DataDir string
// OnOpen is an optional per-tenant seed hook run ONCE after migration (e.g.
// captable seeds the tenant's company row). It runs outside the per-request
// transaction. May be nil.
OnOpen func(ctx context.Context, tenant string, db *sql.DB) error
// HostFns are OPTIONAL extra native host globals injected onto the runtime on
// every Dispatch, ALONGSIDE __db/__newId/__now — for capabilities goja cannot
// provide that a subsystem implements in Go (e.g. esign injects __pdf =
// { stamp, sign } for PDF rendering + x509/PKCS#7 signing). Values are Go
// funcs or map[string]any of Go funcs (goja exposes them as callable JS). They
// are process-global (set once at New), not per-tenant; the binding stays
// domain-free. May be nil. A key MUST NOT collide with __db/__newId/__now/__blob.
HostFns map[string]any
// Blob is the OPTIONAL object-storage seam (see BlobStore). When set, gojabase
// injects globalThis.__blob = { put(key, b64), get(key) -> b64 } on every
// Dispatch, bound to the tenant: keys are prefixed with {Name}/{TenantSegment}
// so a bundle can NEVER address another tenant's blob. Payloads cross as base64
// strings (goja-friendly); gojabase decodes/encodes at the boundary so the
// bundle never handles raw bytes. nil ⇒ no __blob is injected. This is the ONE
// way a bundle keeps big binaries out of its per-tenant SQLite.
Blob BlobStore
}
Config configures a Host.
type Host ¶
type Host struct {
// contains filtered or unexported fields
}
Host is a compiled bundle + its per-tenant Base stores. Safe for concurrent use.
func New ¶
New compiles the bundle (via clients/goja) and prepares the per-tenant store manager. It does NOT open any tenant DB — those open lazily on first Dispatch.
func (*Host) Dispatch ¶
Dispatch resolves the tenant's Base/SQLite store, opens a per-request transaction, injects the RW-Base host globals bound to it, and calls globalThis.handle. It commits on a <400 non-throwing response and rolls back otherwise. tenant MUST be a validated principal's org (the caller resolves it, e.g. via clients/principal.Org) — the binding does not itself authenticate.