Skip to content

FormSpec Resource — Library Reference ​

Version: 1.0 Status: Draft License: Creative Commons CC0 (dokumen) — kode-nya sendiri FSL (open source) Governed by: docs/architecture/01-architecture-overview.md §2, §4.3, docs/spec/backend/01-core-basic.md, docs/spec/backend/02-core-extended.md

formspec-resource bukan binary — ia adalah Go library (import "github.com/primadi/formspec/resource") yang di-compile menjadi satu proses dengan aplikasi Go. Untuk app non-Go, engine yang sama di-embed ke dalam proses formspec-sidecar (lihat 04-formspec-sidecar.md). Dokumen ini menjelaskan fitur, desain internal, dan permukaan API (ctx.* serta REST API yang dihasilkan) dari library ini.


1. Peran ​

FormSpec Resource adalah mesin bisnis yang dikompilasi ke dalam app: entity engine, penegakan permission, generator REST API, dan (target) admin panel. Ia:

  • Tidak membaca YAML dari filesystem di production — memuat manifest yang sudah diverifikasi dari artifact yang di-pull lewat plane protocol (lihat 01-formspec-ctl.md §5)
  • Menyediakan ctx.* primitives untuk script Starlark (ctx.db, ctx.lock, ctx.pubsub, dst)
  • Meng-generate route REST CRUD + custom action dari spec Document/Entity
  • Menegakkan permission model deny-by-default (lihat docs/spec/backend/01-core-basic.md §1.1, §5, §8)

Dua binary embed engine yang sama: app Go native (import langsung) dan formspec-sidecar (untuk app non-Go — lihat 04-formspec-sidecar.md).


2. Fitur ​

FiturPackageStatus
Entity engine — CRUD, optimistic concurrency (Version), soft delete, search+pagination, Submit/Cancel/Amend lifecycleinternal/db (crud.go)✅ Implemented
Schema migration — generate DDL dari EntitySpec (dialect-aware SQLite/Postgres), checksum-tracked migration runnerinternal/db (ddl.go, migrate.go)✅ Implemented
Manifest loading & validasi — multi-doc YAML, kind validation, reserved-field rulesinternal/manifest, pkg/spec✅ Implemented (untuk kind Document/Entity)
REST API generator — CRUD routes + custom action routes, deny-by-default via Exposeinternal/api✅ Implemented
Permission enforcement — explicit required-permission per action, auto-prefix, module footprintinternal/permission✅ Implemented
Action dispatch — routing berdasarkan impl.type (script/native/sidecar)internal/action✅ Implemented (sidecar butuh endpoint; serve kini punya --sidecar-endpoint)
State machine — validasi transisi stateinternal/entity (state_machine.go) + internal/db⚠️ Dua implementasi terpisah, tidak konsisten — lihat §7
ctx.* primitives untuk Starlark — db/cache/lock/queue/pubsub/storage/kvstore/config/loginternal/starlark✅ Implemented (Fase 2.9 — lihat §7)
Auth — JWT (HS256/RS256/ES256) + dev token, wildcard permission matchinginternal/auth✅ Implemented
Tenant isolation — {workspace} URL scoping, cross-tenant → 404internal/api (middleware.go)✅ Implemented
Idempotency storeinternal/db (idempotency.go)✅ Implemented
Outbox (substrat event delivery)internal/db (outbox.go, outbox_worker.go)✅ Implemented (tersambung ke mutasi + action dispatch — §7)
UI manifest-driven (/{ws}/_ui/...) — Table/Form/Page turunan dari Entity manifests, plus frontend kinds authoredinternal/ui, renderers/react-shadcn✅ Implemented (/{ws}/_admin tinggal rute framework — lihat §7)

3. Desain Internal ​

3.1 Package Map ​

PackageTanggung jawab
internal/entityRegistry (LoadEntities/RegisterArtifactManifest), SyncSchema, GetEntityStore; StateMachineEngine (transisi state, guard via Starlark)
internal/apiGenerator route (GenerateRoutes, GenerateCustomActionRoutes), router chi (RouterBuilder), middleware chain
internal/actionDispatcher — routing eksekusi by ImplType; executor native/script/sidecar
internal/permissionRegistry permission & "uses" declaration, module footprint, deteksi cross-module write
internal/dbEntityStore (CRUD lengkap), DDL generator, migration runner, child-table store, natural-key counter, audit log, idempotency store, outbox
internal/manifestLoader multi-doc YAML
internal/starlarkCtxAPI — permukaan ctx.* untuk script; evaluator kondisi/guard
internal/datastoreRegistry/resolver/factory koneksi per driver (sqlite/postgres/valkey/redis/s3/...) — dipakai ctx.* resolusi datastore
internal/authTokenValidator (JWT/dev), Identity, permission matching
internal/validationCross-field rules (after/before/exists:), validasi action params

3.2 Middleware Chain (REST API) ​

Recovery → Logging → CORS → RequestID → Tenant ({workspace} dari URL)
  → Auth (JWT/dev token) → per-route RequirePermission → Handler

Cross-tenant access → 404 (bukan 403) — mencegah workspace enumeration (internal/api/middleware.go).

3.3 Alur Registrasi Route ​

Document/Entity spec (Expose: [{type: rest, actions: [list, find, create, ...]}])
  → GenerateRoutes(spec)         — CRUD standar sesuai Expose
  → GenerateCustomActionRoutes   — POST /{module}/{plural}/{id}/{action}
  → chi Router mount di /{workspace}/api/v1/...

3.4 Alur Eksekusi Action ​

POST /{workspace}/api/v1/{module}/{plural}/{id}/{action}
  → Middleware chain (auth, permission)
  → Dispatcher.Dispatch(impl.type)
      type: native  → NativeExecutor (lookup handler by ref — lihat §5.3)
      type: script  → ScriptExecutor (resolve .star file, jalankan via internal/starlark)
      type: sidecar → SidecarExecutor (panggil app process via socket — lihat 04-formspec-sidecar.md)
  → ExecuteResult { NewState, Events, ... }

4. ctx.* API (untuk Script Starlark) ​

Permukaan primitive yang dipanggil dari .star script (internal/starlark/primitive.go):

PrimitiveMethodFungsi
ctx.db.query(), .get(), .set(), .delete()Akses datastore relasional/dokumen
ctx.cache.get(), .set(), .delete()Key-value cache (Valkey/Redis)
ctx.lock.acquire(), .release()Distributed lock (mutual exclusion — lihat docs/architecture/05-failover.md §3.4)
ctx.queue—Job queue
ctx.pubsub—Publish/subscribe realtime
ctx.storage—Object storage (S3-compatible)
ctx.kvstore—KV store sederhana
ctx.tenant / ctx.user / ctx.auth.has()Identitas & permission check
ctx.now(), ctx.next_key(), ctx.log.{info,warn,error}, ctx.config.get()Utility

Semua primitive mendukung .named("name") untuk binding ke datastore spesifik (multi-datastore per kind: Datastore — lihat docs/spec/platform/06-datastore.md).

Status implementasi: ✅ ter-wire (Fase 2.9, 2026-08-27). Resolver di-inject dari resource/formspec.go (newDispatcher) melalui action.ScriptExecutor → starlark.ScriptExecutor → CtxAPI; sembilan primitive closed-set resolve ke backend nyata dan uses.primitives ditegakkan di ProdMode/StrictMode. Riwayat "selalu not yet implemented" sudah tidak berlaku — lihat §7 butir 3.


5. Kontrak Embedding ​

5.1 Untuk App Go Native ​

Model yang didokumentasikan (docs/architecture/01-architecture-overview.md §2) sekarang punya facade publik nyata di folder resource/ (resource/formspec.go, resource/syncagent.go — package formspec, bukan lagi di bawah internal/, supaya bisa di-import dari module lain). Import path berakhiran /resource (nama folder), tapi identifier package-nya tetap formspec — jadi pemanggilannya tetap formspec.New(...), bukan resource.New(...):

go
import "github.com/primadi/formspec/resource"

app, err := formspec.New(formspec.Config{
    SpecPath: "./spec",
    DSN:      "sqlite:data.db",
    Addr:     ":8080",
})
if err != nil {
    log.Fatal(err)
}
log.Fatal(app.ListenAndServe())

formspec.New melakukan persis alur di §3.1–3.4 (load entity, sync schema, wire dispatcher, build route) di balik satu pemanggilan. Contoh penggunaannya ada di examples/reference-app/main.go.

Untuk pengkabelan yang sesuai desain plane protocol (memuat dari artifact Control Plane, bukan disk), syncagent.go menyediakan formspec.NewSyncAgent/(*SyncAgent).Run — mem-port persis apa yang dulu cmd/formspec-resource lakukan (poll snapshot, fetch+verify artifact, evidence). Gap-nya belum berubah (lihat §7 gap #1): SyncAgent men-sync entity registry & schema, tapi tidak menyambungkan hasilnya ke formspec.App/api.RouterBuilder — dua jalur ini masih terpisah secara sengaja (lihat komentar di syncagent.go), karena menyatukannya (hot-reload route saat artifact baru datang) adalah pekerjaan desain tersendiri, bukan sekadar pemindahan kode.

5.2 Untuk App Non-Go (via FormSpec Sidecar) ​

Model single-process embed (lihat 04-formspec-sidecar.md): formspec-sidecar meng-compile-in package formspec yang sama seperti di atas (entity engine, API generator, dsb) sebagai satu proses, ditambah listener socket/HTTP untuk komunikasi dengan proses app (PHP/Python/dst).

5.3 Native Handler API ​

impl.type: native dieksekusi oleh NativeExecutor yang mencari handler Go yang didaftarkan eksplisit lewat API publik App.RegisterNative / App.RegisterNatives (resource/formspec.go):

go
type NativeHandler func(ctx context.Context, params NativeParams) (any, error)

app.RegisterNative("Billing.Order.CalculateTax", calculateTax)
app.RegisterNatives(map[string]formspec.NativeHandler{
    "registry.SignatureVerify": signatureVerify,
    "registry.vendor.approve":  vendorApprove(app),
})

NativeParams membawa konteks eksekusi: Module, Entity, ActionName, ResourceID, Resource (data record), Params (parameter action), WorkspaceID, UserID. Handler yang sudah diregistrasi dipertahankan lintas ReloadSpec() (hot-reload) tanpa perlu registrasi ulang.

Resolusi ref mencoba tiga format berurutan — exact TypeName.MethodName, module.entity.action, lalu module.TypeName.MethodName. Detail lengkap (kontrak handler, format ref, handler bawaan engine) ada di ../spec/backend/06-script-runtime.md §7.


6. Referensi Skema Manifest ​

Skema Document/Entity (yang di-load engine ini) didefinisikan normatif di docs/spec/backend/01-core-basic.md dan docs/spec/backend/03-entity-extension.md — dokumen ini tidak mengulang skemanya, hanya perilaku runtime-nya. Struct Go yang relevan ada di pkg/spec/entity.go: EntitySpec, Field, Action, StateMachine, EventDecl, UsesDecl, ExposeConfig.

Kind lain yang parse valid dan dikonsumsi runtime berbeda: Page/Form/Table/Dashboard/Kanban/Print/Theme dst. di internal/ui (UI registry → bundle /_meta), Api/Webhook/Integrator/Subscription/Config/Service di registry masing-masing (internal/api, internal/webhook, internal/integrator, internal/subscription, internal/config, internal/service), Datastore di DatastoreRegistry (resource/datastoreregistry.go setelah Fase 2.9.5). Yang masih tanpa konsumen runtime di single-server: Environment, Policy (Control Plane), dan KindDefinition/Mockup (lihat §7). (Approval bukan kind — ia state_machine.transitions[].approval pada Entity, dan sudah dikonsumsi runtime approval.)


7. Status Implementasi Hari Ini ​

Ditinjau ulang 2026-10-10. Daftar di bawah ditulis jauh sebelum Fase 2 selesai, dan beberapa butirnya kini basi — yang sudah selesai ditandai ✅ dengan bukti. Yang masih terbuka dipertahankan apa adanya. Butir ✅ tidak boleh dihapus tanpa memeriksa ulang kodenya: dokumen ini pernah menahan pembaca dari fitur yang sudah hidup (mis. ctx.*), dan itu biaya yang sama besarnya dengan mengklaim sesuatu selesai padahal belum.

  1. SyncAgent (plane-protocol client, di syncagent.go) tidak serve REST API sama sekali. Ia menjalankan pull-based convergence (poll snapshot, fetch+verify artifact, evidence) dan meng-update entity registry + schema — tapi tidak pernah menyambungkannya ke api.RouterBuilder/formspec.App. formspec.App (di resource/formspec.go) yang benar-benar serve API dan sudah dipakai examples/reference-app, tapi hanya untuk mode filesystem (SpecPath), belum untuk mode pull-based. Menyatukan dua jalur ini — supaya artifact yang di-pull SyncAgent benar-benar memicu reload route formspec.App, bukan cuma sync schema — adalah gap paling penting yang tersisa untuk membuat model "compile formspec-resource ke app Go" benar-benar berjalan sesuai desain plane protocol di production. (Masih terbuka; loadYAMLIntoRegistry juga masih hanya mendaftarkan Entity/Document.)
  2. Admin panel terkonsolidasi ke permukaan App. Butir ini menyatakan "/_admin tidak ada sama sekali" — itu tidak lagi benar dan sudah lama tidak benar. Panel entity _admin dihapus (plan app-scoped-login.md D4): permukaan CRUD kini turunan Entity manifests dan disajikan di /{ws}/_ui/... bagi setiap kind: App (Table/Form/detail Page/menu), bukan di jalur _admin. /{ws}/_admin tinggal rute framework (setup, change-password, oauth callback). Lihat docs/architecture/02-admin-surfaces.md.
  3. ✅ ctx.* primitives ter-wire (selesai sejak Fase 2.9, 2026-08-27). Butir lama menyatakan "seluruhnya stub" dan SetDatastoreResolver "tidak dipanggil di binary manapun" — keduanya tidak lagi benar. Resolver di-wire dari resource/formspec.go (newDispatcher → action.ScriptExecutor → starlark.ScriptExecutor → CtxAPI); sembilan primitive closed-set (db, cache, lock, queue, pubsub, storage, kvstore, config, log) resolve ke backend nyata, termasuk .named(alias) lewat App Registry. Enforcement uses.primitives aktif di ProdMode/StrictMode. Test pengunci: resource/ctx_primitives_e2e_test.go, ctx_db_module_scoped_e2e_test.go, ctx_uses_enforcement_test.go.
  4. State machine punya dua implementasi yang tidak konsisten. entity.StateMachineEngine (lengkap, dengan guard evaluation) tidak pernah dipanggil dari internal/api.HandleCustomAction — enforcement transisi state yang benar-benar jalan justru ada di db.EntityStore.Update (validateStateTransition), sebuah pengecekan terpisah dan lebih sederhana yang ter-trigger saat field state berubah lewat Update(). (Masih terbuka sebagai utang arsitektur. Yang berlaku secara operasional: PATCH field state = pemicu transisi; gate per-transisi lewat TransitionDecl.RequirePermission.)
  5. ✅ emits: events tersambung (selesai sejak Fase 2.4/7.3). Outbox kini di-enqueue dari jalur tulis (enqueueOutbox di crud.go, di dalam transaksi yang sama dengan mutasi) dan dari resource.call() lewat disp.SetEventEmitter, lalu dikuras OutboxWorker ke delivery handler yang mendistribusikan ke semua kanal terdeklarasi (websocket, audit_log, queue, pubsub, reliable_event, notification, webhook). unimplementedChannels kosong — tidak ada kanal yang diam-diam dibuang tanpa pesan.
  6. SidecarExecutor.Execute. Sudah tidak "selalu return error": impl: {type: sidecar} berfungsi bila endpoint app-process diberikan. Di formspec dev endpoint itu disediakan otomatis (--listen/--app-endpoint); di formspec serve kini ada --sidecar-endpoint. Tanpa endpoint, action sidecar masih gagal dengan pesan jelas — perilaku yang benar untuk app embedded tanpa app-process.
  7. NativeExecutor fungsional dan handler sudah diregistrasi — formspec.core.user.hash-password (resource/auth_native.go) dan handler binary registry (cmd/formspec-registry/main.go: registry.SignatureVerify, registry.vendor.approve) terdaftar via App.RegisterNative/RegisterNatives (lihat §5.3). Action impl.type: native tanpa handler terdaftar tetap error "not registered".
  8. ✅ Natural key counter db.NaturalKeyCounter dipakai (koreksi). Butir lama menyebut dispatcher "men-stub next_key" — itu tidak lagi benar. EntityStore.generateNaturalKeys memanggil counter.GenerateNaturalKey di dalam transaksi insert (todo 2.1.2), dengan counter komposit (tenant, resource, field, scope, period, seq); natural_key_rule mendukung strategy: sequence|custom, format, prefix, reset: never|yearly|monthly|daily.
  9. internal/ctx, internal/service, internal/tenant, internal/events kosong — bukan berarti fungsinya hilang; masing-masing tersebar: tenant resolution ada inline di internal/api/middleware.go, ctx (untuk Go native, bukan Starlark) belum ada bentuknya sama sekali, events paling dekat diwakili outbox yang belum disambungkan. (Masih akurat untuk keempat package; yang berubah hanya bahwa konsumennya kini lengkap.)

7.1 Prioritas Perbaikan ​

  1. Satukan SyncAgent dan App — SyncAgent.OnDeploy-equivalent (loadYAMLIntoRegistry di syncagent.go) HARUS pada akhirnya memicu rebuild api.RouterBuilder/atomic-swap handler di App yang sedang serve, bukan cuma sync schema. (Masih prioritas #1.)
  2. Wire CtxAPI.SetDatastoreResolver ✅ selesai (Fase 2.9). Sisa yang relevan: credential_ref (KMS/Vault) belum ada resolver — kredensial masih lewat connection + env. Lihat docs_internal/plan/serve-parity.md.
  3. Satukan state-machine enforcement — pilih satu (entity.StateMachineEngine tampaknya lebih lengkap) dan hapus duplikasi di db.crud.go, atau jelaskan pembagian tanggung jawabnya kalau memang disengaja.
  4. Implementasikan SidecarExecutor ✅ selesai (endpoint outbound; kini juga di serve). Sisa: listener ctx.* + spawning app child process di formspec serve.
  5. Admin panel generator — usang sebagai rumusan; permukaan UI kini manifest-driven di /{ws}/_ui/... (lihat butir 2 dan docs/renderers/react-shadcn/).

8. References ​

DokumenIsi
docs/spec/backend/01-core-basic.md, docs/spec/backend/02-core-extended.mdSkema normatif Document/Entity/Action/StateMachine
docs/spec/platform/06-datastore.mdSkema kind: Datastore dan resolusi ctx.*
docs/architecture/01-architecture-overview.md §2, §4.3Model compile-in Go, resource pod
docs/runtimes/01-formspec-ctl.mdSisi server dari plane protocol
docs/runtimes/04-formspec-sidecar.mdModel embedding untuk app non-Go

Standar terbuka (CC0) dengan implementasi referensi.