Skip to content

Script Runtime — API Penulisan Handler ​

Version: 0.1.0 · Status: Draft

Draft: isi di bawah kontrak yang berlaku. Katalog di sini normatif untuk semua handler impl.script/impl.script_ref (Starlark) dan menetapkan kontrak resolusi untuk impl.type: native. Batas eksekusi sandbox ada di 02-core-extended.md §7.1.

Dokumen ini mengkatalogkan API yang tersedia di dalam script handler — sesuatu yang dipakai di hampir setiap contoh script tapi selama ini tersebar. Semua akses di sini tunduk model permission uses/required_permission (01-core-basic.md §5): apa yang disentuh handler wajib dideklarasikan; akses undeclared diblokir saat resolusi.

1. Entrypoint ​

Setiap script handler mengekspos satu fungsi entrypoint:

python
def execute(resource, params, ctx):
    # ... logika ...
    return ok({ "status": "done" })
ArgumenIsi
resourceRecord yang menjadi sasaran action — objek Entity dengan API di §2. Untuk action type: service yang tidak terikat record, resource bisa None.
paramsPayload masukan action (body request/argumen pemanggil), sudah lolos validasi field-level (L1–L3, 05-field-types.md §3) sebelum entrypoint dipanggil.
ctxContext runtime — primitive ctx.* (§5), identitas (ctx.user, ctx.tenant), utility (ctx.now(), ctx.next_key()), config & secret.

2. Objek resource ​

resource adalah handle ke satu record Entity, bukan dict mentah:

APIArti
resource.idPrimary key (UUID v7, 01-core-basic.md §2).
resource.field(name)Membaca nilai satu field.
resource.set(field, value)Menyetel nilai field di memori — belum dipersist sampai save().
resource.save()Mempersist perubahan dalam transaksi action (01-core-basic.md §3), memicu guard lifecycle dan penulisan outbox event yang berlaku (01-core-basic.md §7).
resource.new()Membuat handle record baru (belum tersimpan) untuk entity yang sama; isi lewat set(...) lalu save().

save() bukan sekadar UPDATE mentah — ia melewati jalur yang sama dengan mutasi lewat API (guard, denormalisasi, event), sehingga script tidak bisa menembus kontrak lifecycle dengan menulis diam-diam.

3. Query dari Script ​

Query builder (02-core-extended.md §16) diakses dari script lewat referensi entity:

python
# Ambil satu record
cust = Customer.query().where(id=params["customer_id"]).first()

# Ambil daftar
overdue = Invoice.query() \
    .where(status="submitted") \
    .where("due_date", "lt", ctx.now()) \
    .all()

<EntityRef>.query() mengembalikan builder dengan .where(...), agregasi, group_by/having, dan include() sesuai kontrak §16; terminatornya .first() (satu record atau None) atau varian pengembali-daftar (.all()). Query lintas category tetap dilarang (01-core-basic.md §3, FORMSPEC.PERSIST.CROSS_CATEGORY), juga dari script. Referensi entity lintas-module memakai notasi qualifier {module}/{entity} (02-core-extended.md §7) dan wajib dideklarasikan di uses.

4. Akses Lintas-Entity ​

Memanggil atau memuat record entity lain dari dalam script:

APIArti
<resource>.load(id)Memuat satu record entity lain berdasarkan id, mengembalikan handle §2.
<resource>.call(action, params)Memanggil action bernama pada entity lain (mis. membuat journal entry dari handler invoice).

Keduanya adalah akses lintas-resource dan wajib dideklarasikan di uses (01-core-basic.md §5); pemanggilan cross-boundary tunduk aturan idempotensi/kompensasi yang sama seperti Integrator (02-core-extended.md §5). Panggilan same-process di-dispatch langsung tanpa melewati jaringan (01-core-basic.md §8).

5. Logging & Primitive ctx.* ​

Katalog lengkap ctx.* — closed set 9 primitive infrastruktur (db, cache, lock, queue, pubsub, storage, kvstore, config, log) — hidup di ../platform/06-datastore.md §2 (registrasi service, driver×serves, chain resolusi) dan ../../runtimes/02-formspec-resource.md §4. Yang relevan untuk penulisan handler:

  • Logging — ctx.log.info(...), ctx.log.warn(...), ctx.log.error(...) meng-emit log terstruktur (../platform/09-observability.md §2). Log tidak pernah boleh memuat nilai secret (02-core-extended.md §18) atau PII mentah (../platform/09-observability.md §2.2). Backend log mengikuti service serves: [log] yang dipilih lewat App Registry (default: in-memory per-proses).
  • Config & secret — ctx.config.get("key") untuk key non-secret (01-core-basic.md §10); backend config mengikuti service serves: [config] (default: Config manifest lokal). ctx.secrets untuk key secret: true, tunduk uses: {secrets: [...]} dan selalu di-audit (02-core-extended.md §18).
  • Named primitive — ctx.db.named("analytics") mengakses named logical primitive yang teregistrasi di App Registry; wajib dideklarasikan sebagai key db/analytics di uses.datastores (../platform/06-datastore.md §1.2).
  • Async job — ctx.job.progress(pct, message) melaporkan progres dari handler async yang di-track (02-core-extended.md §13).

6. Kontrak Return ​

Handler mengembalikan salah satu dari dua konstruktor hasil, yang memetakan langsung ke envelope respons HTTP (01-core-basic.md §8.5):

ReturnHasil
ok(data)Sukses — data menjadi data di envelope respons (2xx).
fail(message, code?)Gagal — membatalkan transaksi action dan mengembalikan envelope error {error: {code, message, details}, meta}. Tanpa code, fail memakai kode error generik; conditions/error bisnis SEBAIKNYA membawa code bernamespace App (bukan FORMSPEC.*, 01-core-basic.md §9).

fail() di dalam handler membatalkan seluruh transaksi (§2 save() yang sudah terjadi ikut rollback) — tidak ada hasil parsial, konsisten dengan jaminan atomisitas (01-core-basic.md §3).

7. Resolusi ref Handler Native ​

Untuk impl.type: native, handler ditulis sebagai fungsi Go dan didaftarkan eksplisit ke engine lewat API publik App.RegisterNative / App.RegisterNatives (resource/formspec.go). Action di YAML merujuk handler lewat string ref: "{Type}.{Method}":

yaml
impl:
  type: native
  ref: "OrderResource.UpdateDiscountRule"

7.1 API Registrasi ​

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

type NativeParams struct {
    Module      string         // owning module (mis. "billing")
    Entity      string         // entity atau service (mis. "order")
    ActionName  string         // action yang dipanggil (mis. "checkout")
    ResourceID  string         // ID record entity (kosong untuk service action)
    Resource    map[string]any // data record entity saat ini
    Params      map[string]any // parameter action dari request body
    WorkspaceID string         // workspace saat ini
    UserID      string         // user terautentikasi
}

Registrasi dilakukan saat boot app:

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

Handler yang sudah diregistrasi dipertahankan lintas ReloadSpec() — saat spec hot-reload, handler di-re-register otomatis ke dispatcher baru tanpa perlu memanggil RegisterNative ulang.

7.2 Format ref dan Urutan Resolusi ​

NativeExecutor me-resolve ref dengan mencoba tiga format, berurutan:

#FormatContoh
1Exact ref (TypeName.MethodName)"OrderResource.UpdateDiscountRule"
2module.entity.action"billing.order.update-discount-rule"
3module.TypeName.MethodName"billing.OrderResource.UpdateDiscountRule"

Ref yang tidak cocok dengan handler terdaftar mana pun → error "native handler %q not registered" saat action dieksekusi.

7.3 Auto-scan impl/**/*.go (target desain) ​

Desain normatif jangka panjang: handler ditulis sebagai method Go di impl/ (../platform/08-project-layout.md §2) dan di-scan otomatis saat formspec apply/build. Aturan normatif:

  • Nama harus unik dalam module. Bila lebih dari satu {Type}.{Method} cocok di seluruh impl/ module, itu error saat formspec apply/build — bukan ambiguitas yang dibiarkan sampai runtime.
  • Tidak ada match sama sekali juga error build-time — ref menggantung ditolak sebelum deployment.

Status hari ini: auto-scan belum diimplementasikan — handler tetap didaftarkan eksplisit via RegisterNative/RegisterNatives (§7.1), dan resolusi terjadi di runtime mengikuti urutan §7.2.

7.4 Handler Bawaan Engine ​

RefLokasiFungsi
formspec.core.user.hash-passwordresource/auth_native.goHook before create/update — hash password → password_hash, hapus plaintext
registry.SignatureVerifycmd/formspec-registry/main.goVerify ed25519 server-side (service registry.signature-verify.verify)
registry.vendor.approvecmd/formspec-registry/main.goApprove vendor → status active + grant role vendor + perms registry

Standar terbuka (CC0) dengan implementasi referensi.