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-resourcebukan 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 prosesformspec-sidecar(lihat04-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
| Fitur | Package | Status |
|---|---|---|
Entity engine — CRUD, optimistic concurrency (Version), soft delete, search+pagination, Submit/Cancel/Amend lifecycle | internal/db (crud.go) | ✅ Implemented |
Schema migration — generate DDL dari EntitySpec (dialect-aware SQLite/Postgres), checksum-tracked migration runner | internal/db (ddl.go, migrate.go) | ✅ Implemented |
| Manifest loading & validasi — multi-doc YAML, kind validation, reserved-field rules | internal/manifest, pkg/spec | ✅ Implemented (untuk kind Document/Entity) |
REST API generator — CRUD routes + custom action routes, deny-by-default via Expose | internal/api | ✅ Implemented |
| Permission enforcement — explicit required-permission per action, auto-prefix, module footprint | internal/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 state | internal/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/log | internal/starlark | ✅ Implemented (Fase 2.9 — lihat §7) |
| Auth — JWT (HS256/RS256/ES256) + dev token, wildcard permission matching | internal/auth | ✅ Implemented |
Tenant isolation — {workspace} URL scoping, cross-tenant → 404 | internal/api (middleware.go) | ✅ Implemented |
| Idempotency store | internal/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 authored | internal/ui, renderers/react-shadcn | ✅ Implemented (/{ws}/_admin tinggal rute framework — lihat §7) |
3. Desain Internal
3.1 Package Map
| Package | Tanggung jawab |
|---|---|
internal/entity | Registry (LoadEntities/RegisterArtifactManifest), SyncSchema, GetEntityStore; StateMachineEngine (transisi state, guard via Starlark) |
internal/api | Generator route (GenerateRoutes, GenerateCustomActionRoutes), router chi (RouterBuilder), middleware chain |
internal/action | Dispatcher — routing eksekusi by ImplType; executor native/script/sidecar |
internal/permission | Registry permission & "uses" declaration, module footprint, deteksi cross-module write |
internal/db | EntityStore (CRUD lengkap), DDL generator, migration runner, child-table store, natural-key counter, audit log, idempotency store, outbox |
internal/manifest | Loader multi-doc YAML |
internal/starlark | CtxAPI — permukaan ctx.* untuk script; evaluator kondisi/guard |
internal/datastore | Registry/resolver/factory koneksi per driver (sqlite/postgres/valkey/redis/s3/...) — dipakai ctx.* resolusi datastore |
internal/auth | TokenValidator (JWT/dev), Identity, permission matching |
internal/validation | Cross-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 → HandlerCross-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):
| Primitive | Method | Fungsi |
|---|---|---|
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(...):
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):
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.
SyncAgent(plane-protocol client, disyncagent.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 keapi.RouterBuilder/formspec.App.formspec.App(diresource/formspec.go) yang benar-benar serve API dan sudah dipakaiexamples/reference-app, tapi hanya untuk mode filesystem (SpecPath), belum untuk mode pull-based. Menyatukan dua jalur ini — supaya artifact yang di-pullSyncAgentbenar-benar memicu reload routeformspec.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;loadYAMLIntoRegistryjuga masih hanya mendaftarkanEntity/Document.)- Admin panel terkonsolidasi ke permukaan App. Butir ini menyatakan "
/_admintidak ada sama sekali" — itu tidak lagi benar dan sudah lama tidak benar. Panel entity_admindihapus (planapp-scoped-login.mdD4): permukaan CRUD kini turunan Entity manifests dan disajikan di/{ws}/_ui/...bagi setiapkind: App(Table/Form/detail Page/menu), bukan di jalur_admin./{ws}/_admintinggal rute framework (setup, change-password, oauth callback). Lihatdocs/architecture/02-admin-surfaces.md. - ✅
ctx.*primitives ter-wire (selesai sejak Fase 2.9, 2026-08-27). Butir lama menyatakan "seluruhnya stub" danSetDatastoreResolver"tidak dipanggil di binary manapun" — keduanya tidak lagi benar. Resolver di-wire dariresource/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. Enforcementuses.primitivesaktif di ProdMode/StrictMode. Test pengunci:resource/ctx_primitives_e2e_test.go,ctx_db_module_scoped_e2e_test.go,ctx_uses_enforcement_test.go. - State machine punya dua implementasi yang tidak konsisten.
entity.StateMachineEngine(lengkap, dengan guard evaluation) tidak pernah dipanggil dariinternal/api.HandleCustomAction— enforcement transisi state yang benar-benar jalan justru ada didb.EntityStore.Update(validateStateTransition), sebuah pengecekan terpisah dan lebih sederhana yang ter-trigger saat field state berubah lewatUpdate(). (Masih terbuka sebagai utang arsitektur. Yang berlaku secara operasional:PATCHfield state = pemicu transisi; gate per-transisi lewatTransitionDecl.RequirePermission.) - ✅
emits:events tersambung (selesai sejak Fase 2.4/7.3). Outbox kini di-enqueue dari jalur tulis (enqueueOutboxdicrud.go, di dalam transaksi yang sama dengan mutasi) dan dariresource.call()lewatdisp.SetEventEmitter, lalu dikurasOutboxWorkerke delivery handler yang mendistribusikan ke semua kanal terdeklarasi (websocket, audit_log, queue, pubsub, reliable_event, notification, webhook).unimplementedChannelskosong — tidak ada kanal yang diam-diam dibuang tanpa pesan. SidecarExecutor.Execute. Sudah tidak "selalu return error":impl: {type: sidecar}berfungsi bila endpoint app-process diberikan. Diformspec devendpoint itu disediakan otomatis (--listen/--app-endpoint); diformspec servekini ada--sidecar-endpoint. Tanpa endpoint, action sidecar masih gagal dengan pesan jelas — perilaku yang benar untuk app embedded tanpa app-process.NativeExecutorfungsional 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 viaApp.RegisterNative/RegisterNatives(lihat §5.3). Actionimpl.type: nativetanpa handler terdaftar tetap error "not registered".- ✅ Natural key counter
db.NaturalKeyCounterdipakai (koreksi). Butir lama menyebut dispatcher "men-stubnext_key" — itu tidak lagi benar.EntityStore.generateNaturalKeysmemanggilcounter.GenerateNaturalKeydi dalam transaksi insert (todo 2.1.2), dengan counter komposit(tenant, resource, field, scope, period, seq);natural_key_rulemendukungstrategy: sequence|custom,format,prefix,reset: never|yearly|monthly|daily. internal/ctx,internal/service,internal/tenant,internal/eventskosong — bukan berarti fungsinya hilang; masing-masing tersebar: tenant resolution ada inline diinternal/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
- Satukan
SyncAgentdanApp—SyncAgent.OnDeploy-equivalent (loadYAMLIntoRegistrydisyncagent.go) HARUS pada akhirnya memicu rebuildapi.RouterBuilder/atomic-swap handler diAppyang sedang serve, bukan cuma sync schema. (Masih prioritas #1.) Wire✅ selesai (Fase 2.9). Sisa yang relevan:CtxAPI.SetDatastoreResolvercredential_ref(KMS/Vault) belum ada resolver — kredensial masih lewatconnection+ env. Lihatdocs_internal/plan/serve-parity.md.- Satukan state-machine enforcement — pilih satu (
entity.StateMachineEnginetampaknya lebih lengkap) dan hapus duplikasi didb.crud.go, atau jelaskan pembagian tanggung jawabnya kalau memang disengaja. Implementasikan✅ selesai (endpoint outbound; kini juga diSidecarExecutorserve). Sisa: listenerctx.*+ spawning app child process diformspec serve.- Admin panel generator — usang sebagai rumusan; permukaan UI kini manifest-driven di
/{ws}/_ui/...(lihat butir 2 dandocs/renderers/react-shadcn/).
8. References
| Dokumen | Isi |
|---|---|
docs/spec/backend/01-core-basic.md, docs/spec/backend/02-core-extended.md | Skema normatif Document/Entity/Action/StateMachine |
docs/spec/platform/06-datastore.md | Skema kind: Datastore dan resolusi ctx.* |
docs/architecture/01-architecture-overview.md §2, §4.3 | Model compile-in Go, resource pod |
docs/runtimes/01-formspec-ctl.md | Sisi server dari plane protocol |
docs/runtimes/04-formspec-sidecar.md | Model embedding untuk app non-Go |