01. domain: bentuk data HazardEvent
Bukan bagianmu: milik Grace (P1). File asli: internal/domain/hazard.go, di branch
feat/p1 / feat/p3. Kamu perlu memahaminya karena store dan api memakai bentuk ini.
Kenapa ada
BMKG dan PVMBG punya format data masing-masing (SeismicEvent, VolcanicReport). Spek meminta
satu canonical format (bentuk resmi tunggal) bernama HazardEvent, yang dipakai BNPB untuk
menyatukan keduanya. Semua bagian aggregator menyentuh bentuk ini:
| Bagian | Peran terhadap HazardEvent |
|---|---|
mapping (Grace) | menghasilkan HazardEvent dari data sumber |
| klien sumber (Grace) | memanggil mapping, lalu mengembalikan HazardEvent |
ingest (kamu) | membawa HazardEvent dari klien ke handler |
store (kamu) | menyimpan dan membaca HazardEvent |
api (kamu) | menyajikan HazardEvent |
publisher (Mike) | mengirim HazardEvent ke Kafka |
Kenapa dibutuhkan
Di Go, dua struct yang dideklarasikan di dua package berbeda dianggap dua tipe berbeda,
walau isinya mirip. Jadi semua bagian harus meng-import satu definisi yang sama. Itulah
fungsi package domain. Package ini hanya berisi tipe dan dua fungsi kecil, tanpa logika bisnis.
Bagian 1: tiga “enum”
Go tidak punya enum. Caranya: buat tipe baru dari string, lalu daftar nilai sahnya sebagai
const. (Pelajaran 8.)
Dikutip dari internal/domain/hazard.go, baris 11 sampai 16
type Source string
const (
SourceBMKG Source = "BMKG"
SourcePVMBG Source = "PVMBG"
)Dikutip dari internal/domain/hazard.go, baris 25 sampai 32
type Severity string
const (
SeverityNormal Severity = "NORMAL"
SeverityWaspada Severity = "WASPADA"
SeveritySiaga Severity = "SIAGA"
SeverityAwas Severity = "AWAS"
)Manfaat: fungsi yang meminta Severity tidak bisa diberi sembarang string tanpa konversi
eksplisit, dan tidak ada yang mengetik "awas" huruf kecil secara tidak sengaja.
Nilai-nilainya persis seperti di spek: BMKG/PVMBG, SEISMIC/VOLCANIC,
NORMAL/WASPADA/SIAGA/AWAS.
Bagian 2: struct HazardEvent
Dikutip dari internal/domain/hazard.go, baris 34 sampai 46
type HazardEvent struct {
HazardID string `json:"hazard_id" bson:"hazard_id"`
Source Source `json:"source" bson:"source"`
SourceRefID string `json:"source_ref_id" bson:"source_ref_id"`
HazardType HazardType `json:"hazard_type" bson:"hazard_type"`
Severity Severity `json:"severity" bson:"severity"`
AreaName string `json:"area_name" bson:"area_name"`
Latitude float64 `json:"latitude" bson:"latitude"`
Longitude float64 `json:"longitude" bson:"longitude"`
OccurredAt time.Time `json:"occurred_at" bson:"occurred_at"`
IngestedAt time.Time `json:"ingested_at" bson:"ingested_at"`
Attributes map[string]any `json:"attributes" bson:"attributes"`
}Sebelas field, urutannya sama dengan tabel di spek. Yang perlu kamu pahami untuk tugasmu:
- Tag
`json:"hazard_id" bson:"hazard_id"`memberi nama field di JSON (dipakaiapi) dan di MongoDB (dipakaistore). Tanpa tag, namanya akanHazardID, bukanhazard_idseperti yang diminta spek. (Pelajaran 3.) Attributes map[string]anyadalah kantong untuk data khusus sumber yang tidak punya tempat di format resmi:magnitude,depth_km,eruption_count_24h, dan field baru seperticonfidence_level. Bentuknya bebas, jadi field baru dari sumber tidak merusak apa pun. Inilah yang membuat Problem 1 (aggregator tidak crash) dan Problem 4 (penyimpanan tanpa migration) bisa terjadi. (Pelajaran 4.)SourceRefID+Sourceadalah id asli dari instansi.storememakainya sebagai kunci unik untuk upsert.IngestedAtadalah kapan BNPB menerima data.store.Listmemakainya untuk filtersince. Diisi oleh mapper (mapper.now()), selisihnya dengan saat response tiba hanya milidetik.
Bagian 3: NewHazardID, ID yang deterministik
Dikutip dari internal/domain/hazard.go, baris 48 sampai 51
func NewHazardID(source Source, sourceRefID string) string {
sum := sha256.Sum256([]byte(string(source) + ":" + sourceRefID))
return "HZ-" + hex.EncodeToString(sum[:16])
}hazard_id dibuat dari hash (SHA-256) gabungan source dan id aslinya, diberi awalan HZ-.
Deterministik berarti masukan yang sama selalu menghasilkan ID yang sama.
Kenapa ini penting untuk P4: record yang sama selalu ber-ID sama. Jadi polling ulang, restart
aggregator, dan koreksi severity menghasilkan HazardEvent dengan hazard_id identik, dan
upsert di store menimpa dokumen yang sama alih-alih membuat dokumen baru. Kalau ID-nya
UUID acak, setiap polling ulang akan menciptakan “peristiwa baru” untuk kejadian yang sama.
Bagian 4: Validate
Dikutip dari internal/domain/hazard.go, baris 53 sampai 83
func (event HazardEvent) Validate() error {
var errs []error
if event.HazardID == "" {
errs = append(errs, errors.New("hazard_id is required"))
}
if event.Source != SourceBMKG && event.Source != SourcePVMBG {
errs = append(errs, fmt.Errorf("unknown source %q", event.Source))
}
if event.SourceRefID == "" {
errs = append(errs, errors.New("source_ref_id is required"))
}
if event.HazardType != HazardTypeSeismic && event.HazardType != HazardTypeVolcanic {
errs = append(errs, fmt.Errorf("unknown hazard_type %q", event.HazardType))
}
if !validSeverity(event.Severity) {
errs = append(errs, fmt.Errorf("unknown severity %q", event.Severity))
}
if event.AreaName == "" {
errs = append(errs, errors.New("area_name is required"))
}
if event.OccurredAt.IsZero() {
errs = append(errs, errors.New("occurred_at is required"))
}
if event.IngestedAt.IsZero() {
errs = append(errs, errors.New("ingested_at is required"))
}
if event.Attributes == nil {
errs = append(errs, errors.New("attributes is required"))
}
return errors.Join(errs...)
}Memeriksa bahwa event lengkap dan memakai nilai enum yang sah. Ia mengumpulkan semua
masalah (errors.Join) alih-alih berhenti di yang pertama. Mapper memanggilnya sebelum
mengembalikan event, jadi event yang sampai ke store sudah valid. Karena MongoDB tidak punya
constraint di sisi database, Validate inilah yang menggantikan perannya. (Pelajaran 2.)
Cara menjelaskan domain dalam 30 detik
HazardEventadalah bentuk resmi tunggal untuk data dari BMKG dan PVMBG. Field khusus sumber, termasuk yang baru muncul seperticonfidence_level, masuk keAttributesyang bentuknya bebas.hazard_iddibuat deterministik dari source dan id aslinya, sehingga data yang sama selalu mendapat ID yang sama. Package ini milik Grace, dan kami memakainya apa adanya.
Latihan
- Kenapa
domaintidak berisi logika apa pun selainNewHazardIDdanValidate? - Kalau
hazard_idmemakai UUID acak, apa yang berubah pada perilakustore.Save? - Kenapa
Attributesbertipemap[string]anydan bukan struct dengan field tetap?