first commit
This commit is contained in:
+7216
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
Code,Name,Status
|
||||
001,JAWA,true
|
||||
002,SUNDA,true
|
||||
003,BATAK,true
|
||||
004,MADURA,true
|
||||
005,BUGIS,true
|
||||
006,MELAYU,true
|
||||
007,MINANG,true
|
||||
008,BETAWI,true
|
||||
009,BANJAR,true
|
||||
010,BALINESE,true
|
||||
011,ACEH,true
|
||||
012,DAYAK,true
|
||||
013,SASAK,true
|
||||
014,CHINESE,true
|
||||
015,ARAB,true
|
||||
016,INDIA,true
|
||||
017,AMBON,true
|
||||
018,PAPUA,true
|
||||
019,MINAHASA,true
|
||||
020,TORAJA,true
|
||||
|
@@ -0,0 +1,21 @@
|
||||
Code,Name,Status
|
||||
IGD,INSTALASI GAWAT DARURAT,true
|
||||
RI,RAWAT INAP,true
|
||||
RJ,RAWAT JALAN,true
|
||||
OK,INSTALASI KAMAR OPERASI,true
|
||||
VK,INSTALASI KEBIDANAN,true
|
||||
LAB,INSTALASI LABORATORIUM,true
|
||||
RAD,INSTALASI RADIOLOGI,true
|
||||
FAR,INSTALASI FARMASI,true
|
||||
GIZI,INSTALASI GIZI,true
|
||||
REHAB,INSTALASI REHABILITASI,true
|
||||
INTENSIVE,INSTALASI INTENSIVE,true
|
||||
NICU,INSTALASI NICU,true
|
||||
PICU,INSTALASI PICU,true
|
||||
ICU,INSTALASI ICU,true
|
||||
OKULARIS,INSTALASI OKULARIS,true
|
||||
KARDIO,INSTALASI KARDIOVASKULER,true
|
||||
NEURO,INSTALASI NEUROLOGI,true
|
||||
ONKO,INSTALASI ONKOLOGI,true
|
||||
GERIATRI,INSTALASI GERIATRI,true
|
||||
PSIKIATRI,INSTALASI PSIKIATRI,true
|
||||
|
@@ -0,0 +1,21 @@
|
||||
Code,Name,Status
|
||||
ID,INDONESIA,true
|
||||
EN,ENGLISH,true
|
||||
JV,JAWA,true
|
||||
SU,SUNDA,true
|
||||
MA,MADURA,true
|
||||
MI,MINANG,true
|
||||
BT,BATAK,true
|
||||
BG,BUGIS,true
|
||||
ML,MELAYU,true
|
||||
BN,BANJAR,true
|
||||
BL,BALINESE,true
|
||||
AC,ACEH,true
|
||||
DY,DAYAK,true
|
||||
SS,SASAK,true
|
||||
ZH,CHINESE,true
|
||||
AR,ARABIC,true
|
||||
HI,HINDI,true
|
||||
AM,AMBONESE,true
|
||||
PP,PAPUAN,true
|
||||
MH,MINAHASA,true
|
||||
|
+35
@@ -0,0 +1,35 @@
|
||||
Code,Name
|
||||
11,ACEH
|
||||
12,SUMATERA UTARA
|
||||
13,SUMATERA BARAT
|
||||
14,RIAU
|
||||
15,JAMBI
|
||||
16,SUMATERA SELATAN
|
||||
17,BENGKULU
|
||||
18,LAMPUNG
|
||||
19,KEPULAUAN BANGKA BELITUNG
|
||||
21,KEPULAUAN RIAU
|
||||
31,DKI JAKARTA
|
||||
32,JAWA BARAT
|
||||
33,JAWA TENGAH
|
||||
34,DI YOGYAKARTA
|
||||
35,JAWA TIMUR
|
||||
36,BANTEN
|
||||
51,BALI
|
||||
52,NUSA TENGGARA BARAT
|
||||
53,NUSA TENGGARA TIMUR
|
||||
61,KALIMANTAN BARAT
|
||||
62,KALIMANTAN TENGAH
|
||||
63,KALIMANTAN SELATAN
|
||||
64,KALIMANTAN TIMUR
|
||||
65,KALIMANTAN UTARA
|
||||
71,SULAWESI UTARA
|
||||
72,SULAWESI TENGAH
|
||||
73,SULAWESI SELATAN
|
||||
74,SULAWESI TENGGARA
|
||||
75,GORONTALO
|
||||
76,SULAWESI BARAT
|
||||
81,MALUKU
|
||||
82,MALUKU UTARA
|
||||
91,PAPUA BARAT
|
||||
94,PAPUA
|
||||
|
+515
@@ -0,0 +1,515 @@
|
||||
Code,Province_Code,Name
|
||||
1101,11,KABUPATEN SIMEULUE
|
||||
1102,11,KABUPATEN ACEH SINGKIL
|
||||
1103,11,KABUPATEN ACEH SELATAN
|
||||
1104,11,KABUPATEN ACEH TENGGARA
|
||||
1105,11,KABUPATEN ACEH TIMUR
|
||||
1106,11,KABUPATEN ACEH TENGAH
|
||||
1107,11,KABUPATEN ACEH BARAT
|
||||
1108,11,KABUPATEN ACEH BESAR
|
||||
1109,11,KABUPATEN PIDIE
|
||||
1110,11,KABUPATEN BIREUEN
|
||||
1111,11,KABUPATEN ACEH UTARA
|
||||
1112,11,KABUPATEN ACEH BARAT DAYA
|
||||
1113,11,KABUPATEN GAYO LUES
|
||||
1114,11,KABUPATEN ACEH TAMIANG
|
||||
1115,11,KABUPATEN NAGAN RAYA
|
||||
1116,11,KABUPATEN ACEH JAYA
|
||||
1117,11,KABUPATEN BENER MERIAH
|
||||
1118,11,KABUPATEN PIDIE JAYA
|
||||
1171,11,KOTA BANDA ACEH
|
||||
1172,11,KOTA SABANG
|
||||
1173,11,KOTA LANGSA
|
||||
1174,11,KOTA LHOKSEUMAWE
|
||||
1175,11,KOTA SUBULUSSALAM
|
||||
1201,12,KABUPATEN NIAS
|
||||
1202,12,KABUPATEN MANDAILING NATAL
|
||||
1203,12,KABUPATEN TAPANULI SELATAN
|
||||
1204,12,KABUPATEN TAPANULI TENGAH
|
||||
1205,12,KABUPATEN TAPANULI UTARA
|
||||
1206,12,KABUPATEN TOBA SAMOSIR
|
||||
1207,12,KABUPATEN LABUHAN BATU
|
||||
1208,12,KABUPATEN ASAHAN
|
||||
1209,12,KABUPATEN SIMALUNGUN
|
||||
1210,12,KABUPATEN DAIRI
|
||||
1211,12,KABUPATEN KARO
|
||||
1212,12,KABUPATEN DELI SERDANG
|
||||
1213,12,KABUPATEN LANGKAT
|
||||
1214,12,KABUPATEN NIAS SELATAN
|
||||
1215,12,KABUPATEN HUMBANG HASUNDUTAN
|
||||
1216,12,KABUPATEN PAKPAK BHARAT
|
||||
1217,12,KABUPATEN SAMOSIR
|
||||
1218,12,KABUPATEN SERDANG BEDAGAI
|
||||
1219,12,KABUPATEN BATU BARA
|
||||
1220,12,KABUPATEN PADANG LAWAS UTARA
|
||||
1221,12,KABUPATEN PADANG LAWAS
|
||||
1222,12,KABUPATEN LABUHAN BATU SELATAN
|
||||
1223,12,KABUPATEN LABUHAN BATU UTARA
|
||||
1224,12,KABUPATEN NIAS UTARA
|
||||
1225,12,KABUPATEN NIAS BARAT
|
||||
1271,12,KOTA SIBOLGA
|
||||
1272,12,KOTA TANJUNG BALAI
|
||||
1273,12,KOTA PEMATANG SIANTAR
|
||||
1274,12,KOTA TEBING TINGGI
|
||||
1275,12,KOTA MEDAN
|
||||
1276,12,KOTA BINJAI
|
||||
1277,12,KOTA PADANGSIDIMPUAN
|
||||
1278,12,KOTA GUNUNGSITOLI
|
||||
1301,13,KABUPATEN KEPULAUAN MENTAWAI
|
||||
1302,13,KABUPATEN PESISIR SELATAN
|
||||
1303,13,KABUPATEN SOLOK
|
||||
1304,13,KABUPATEN SIJUNJUNG
|
||||
1305,13,KABUPATEN TANAH DATAR
|
||||
1306,13,KABUPATEN PADANG PARIAMAN
|
||||
1307,13,KABUPATEN AGAM
|
||||
1308,13,KABUPATEN LIMA PULUH KOTA
|
||||
1309,13,KABUPATEN PASAMAN
|
||||
1310,13,KABUPATEN SOLOK SELATAN
|
||||
1311,13,KABUPATEN DHARMASRAYA
|
||||
1312,13,KABUPATEN PASAMAN BARAT
|
||||
1371,13,KOTA PADANG
|
||||
1372,13,KOTA SOLOK
|
||||
1373,13,KOTA SAWAH LUNTO
|
||||
1374,13,KOTA PADANG PANJANG
|
||||
1375,13,KOTA BUKITTINGGI
|
||||
1376,13,KOTA PAYAKUMBUH
|
||||
1377,13,KOTA PARIAMAN
|
||||
1401,14,KABUPATEN KUANTAN SINGINGI
|
||||
1402,14,KABUPATEN INDRAGIRI HULU
|
||||
1403,14,KABUPATEN INDRAGIRI HILIR
|
||||
1404,14,KABUPATEN PELALAWAN
|
||||
1405,14,KABUPATEN S I A K
|
||||
1406,14,KABUPATEN KAMPAR
|
||||
1407,14,KABUPATEN ROKAN HULU
|
||||
1408,14,KABUPATEN BENGKALIS
|
||||
1409,14,KABUPATEN ROKAN HILIR
|
||||
1410,14,KABUPATEN KEPULAUAN MERANTI
|
||||
1471,14,KOTA PEKANBARU
|
||||
1473,14,KOTA D U M A I
|
||||
1501,15,KABUPATEN KERINCI
|
||||
1502,15,KABUPATEN MERANGIN
|
||||
1503,15,KABUPATEN SAROLANGUN
|
||||
1504,15,KABUPATEN BATANG HARI
|
||||
1505,15,KABUPATEN MUARO JAMBI
|
||||
1506,15,KABUPATEN TANJUNG JABUNG TIMUR
|
||||
1507,15,KABUPATEN TANJUNG JABUNG BARAT
|
||||
1508,15,KABUPATEN TEBO
|
||||
1509,15,KABUPATEN BUNGO
|
||||
1571,15,KOTA JAMBI
|
||||
1572,15,KOTA SUNGAI PENUH
|
||||
1601,16,KABUPATEN OGAN KOMERING ULU
|
||||
1602,16,KABUPATEN OGAN KOMERING ILIR
|
||||
1603,16,KABUPATEN MUARA ENIM
|
||||
1604,16,KABUPATEN LAHAT
|
||||
1605,16,KABUPATEN MUSI RAWAS
|
||||
1606,16,KABUPATEN MUSI BANYUASIN
|
||||
1607,16,KABUPATEN BANYU ASIN
|
||||
1608,16,KABUPATEN OGAN KOMERING ULU SELATAN
|
||||
1609,16,KABUPATEN OGAN KOMERING ULU TIMUR
|
||||
1610,16,KABUPATEN OGAN ILIR
|
||||
1611,16,KABUPATEN EMPAT LAWANG
|
||||
1612,16,KABUPATEN PENUKAL ABAB LEMATANG ILIR
|
||||
1613,16,KABUPATEN MUSI RAWAS UTARA
|
||||
1671,16,KOTA PALEMBANG
|
||||
1672,16,KOTA PRABUMULIH
|
||||
1673,16,KOTA PAGAR ALAM
|
||||
1674,16,KOTA LUBUKLINGGAU
|
||||
1701,17,KABUPATEN BENGKULU SELATAN
|
||||
1702,17,KABUPATEN REJANG LEBONG
|
||||
1703,17,KABUPATEN BENGKULU UTARA
|
||||
1704,17,KABUPATEN KAUR
|
||||
1705,17,KABUPATEN SELUMA
|
||||
1706,17,KABUPATEN MUKOMUKO
|
||||
1707,17,KABUPATEN LEBONG
|
||||
1708,17,KABUPATEN KEPAHIANG
|
||||
1709,17,KABUPATEN BENGKULU TENGAH
|
||||
1771,17,KOTA BENGKULU
|
||||
1801,18,KABUPATEN LAMPUNG BARAT
|
||||
1802,18,KABUPATEN TANGGAMUS
|
||||
1803,18,KABUPATEN LAMPUNG SELATAN
|
||||
1804,18,KABUPATEN LAMPUNG TIMUR
|
||||
1805,18,KABUPATEN LAMPUNG TENGAH
|
||||
1806,18,KABUPATEN LAMPUNG UTARA
|
||||
1807,18,KABUPATEN WAY KANAN
|
||||
1808,18,KABUPATEN TULANGBAWANG
|
||||
1809,18,KABUPATEN PESAWARAN
|
||||
1810,18,KABUPATEN PRINGSEWU
|
||||
1811,18,KABUPATEN MESUJI
|
||||
1812,18,KABUPATEN TULANG BAWANG BARAT
|
||||
1813,18,KABUPATEN PESISIR BARAT
|
||||
1871,18,KOTA BANDAR LAMPUNG
|
||||
1872,18,KOTA METRO
|
||||
1901,19,KABUPATEN BANGKA
|
||||
1902,19,KABUPATEN BELITUNG
|
||||
1903,19,KABUPATEN BANGKA BARAT
|
||||
1904,19,KABUPATEN BANGKA TENGAH
|
||||
1905,19,KABUPATEN BANGKA SELATAN
|
||||
1906,19,KABUPATEN BELITUNG TIMUR
|
||||
1971,19,KOTA PANGKAL PINANG
|
||||
2101,21,KABUPATEN KARIMUN
|
||||
2102,21,KABUPATEN BINTAN
|
||||
2103,21,KABUPATEN NATUNA
|
||||
2104,21,KABUPATEN LINGGA
|
||||
2105,21,KABUPATEN KEPULAUAN ANAMBAS
|
||||
2171,21,KOTA BATAM
|
||||
2172,21,KOTA TANJUNG PINANG
|
||||
3101,31,KABUPATEN KEPULAUAN SERIBU
|
||||
3171,31,KOTA JAKARTA SELATAN
|
||||
3172,31,KOTA JAKARTA TIMUR
|
||||
3173,31,KOTA JAKARTA PUSAT
|
||||
3174,31,KOTA JAKARTA BARAT
|
||||
3175,31,KOTA JAKARTA UTARA
|
||||
3201,32,KABUPATEN BOGOR
|
||||
3202,32,KABUPATEN SUKABUMI
|
||||
3203,32,KABUPATEN CIANJUR
|
||||
3204,32,KABUPATEN BANDUNG
|
||||
3205,32,KABUPATEN GARUT
|
||||
3206,32,KABUPATEN TASIKMALAYA
|
||||
3207,32,KABUPATEN CIAMIS
|
||||
3208,32,KABUPATEN KUNINGAN
|
||||
3209,32,KABUPATEN CIREBON
|
||||
3210,32,KABUPATEN MAJALENGKA
|
||||
3211,32,KABUPATEN SUMEDANG
|
||||
3212,32,KABUPATEN INDRAMAYU
|
||||
3213,32,KABUPATEN SUBANG
|
||||
3214,32,KABUPATEN PURWAKARTA
|
||||
3215,32,KABUPATEN KARAWANG
|
||||
3216,32,KABUPATEN BEKASI
|
||||
3217,32,KABUPATEN BANDUNG BARAT
|
||||
3218,32,KABUPATEN PANGANDARAN
|
||||
3271,32,KOTA BOGOR
|
||||
3272,32,KOTA SUKABUMI
|
||||
3273,32,KOTA BANDUNG
|
||||
3274,32,KOTA CIREBON
|
||||
3275,32,KOTA BEKASI
|
||||
3276,32,KOTA DEPOK
|
||||
3277,32,KOTA CIMAHI
|
||||
3278,32,KOTA TASIKMALAYA
|
||||
3279,32,KOTA BANJAR
|
||||
3301,33,KABUPATEN CILACAP
|
||||
3302,33,KABUPATEN BANYUMAS
|
||||
3303,33,KABUPATEN PURBALINGGA
|
||||
3304,33,KABUPATEN BANJARNEGARA
|
||||
3305,33,KABUPATEN KEBUMEN
|
||||
3306,33,KABUPATEN PURWOREJO
|
||||
3307,33,KABUPATEN WONOSOBO
|
||||
3308,33,KABUPATEN MAGELANG
|
||||
3309,33,KABUPATEN BOYOLALI
|
||||
3310,33,KABUPATEN KLATEN
|
||||
3311,33,KABUPATEN SUKOHARJO
|
||||
3312,33,KABUPATEN WONOGIRI
|
||||
3313,33,KABUPATEN KARANGANYAR
|
||||
3314,33,KABUPATEN SRAGEN
|
||||
3315,33,KABUPATEN GROBOGAN
|
||||
3316,33,KABUPATEN BLORA
|
||||
3317,33,KABUPATEN REMBANG
|
||||
3318,33,KABUPATEN PATI
|
||||
3319,33,KABUPATEN KUDUS
|
||||
3320,33,KABUPATEN JEPARA
|
||||
3321,33,KABUPATEN DEMAK
|
||||
3322,33,KABUPATEN SEMARANG
|
||||
3323,33,KABUPATEN TEMANGGUNG
|
||||
3324,33,KABUPATEN KENDAL
|
||||
3325,33,KABUPATEN BATANG
|
||||
3326,33,KABUPATEN PEKALONGAN
|
||||
3327,33,KABUPATEN PEMALANG
|
||||
3328,33,KABUPATEN TEGAL
|
||||
3329,33,KABUPATEN BREBES
|
||||
3371,33,KOTA MAGELANG
|
||||
3372,33,KOTA SURAKARTA
|
||||
3373,33,KOTA SALATIGA
|
||||
3374,33,KOTA SEMARANG
|
||||
3375,33,KOTA PEKALONGAN
|
||||
3376,33,KOTA TEGAL
|
||||
3401,34,KABUPATEN KULON PROGO
|
||||
3402,34,KABUPATEN BANTUL
|
||||
3403,34,KABUPATEN GUNUNG KIDUL
|
||||
3404,34,KABUPATEN SLEMAN
|
||||
3471,34,KOTA YOGYAKARTA
|
||||
3501,35,KABUPATEN PACITAN
|
||||
3502,35,KABUPATEN PONOROGO
|
||||
3503,35,KABUPATEN TRENGGALEK
|
||||
3504,35,KABUPATEN TULUNGAGUNG
|
||||
3505,35,KABUPATEN BLITAR
|
||||
3506,35,KABUPATEN KEDIRI
|
||||
3507,35,KABUPATEN MALANG
|
||||
3508,35,KABUPATEN LUMAJANG
|
||||
3509,35,KABUPATEN JEMBER
|
||||
3510,35,KABUPATEN BANYUWANGI
|
||||
3511,35,KABUPATEN BONDOWOSO
|
||||
3512,35,KABUPATEN SITUBONDO
|
||||
3513,35,KABUPATEN PROBOLINGGO
|
||||
3514,35,KABUPATEN PASURUAN
|
||||
3515,35,KABUPATEN SIDOARJO
|
||||
3516,35,KABUPATEN MOJOKERTO
|
||||
3517,35,KABUPATEN JOMBANG
|
||||
3518,35,KABUPATEN NGANJUK
|
||||
3519,35,KABUPATEN MADIUN
|
||||
3520,35,KABUPATEN MAGETAN
|
||||
3521,35,KABUPATEN NGAWI
|
||||
3522,35,KABUPATEN BOJONEGORO
|
||||
3523,35,KABUPATEN TUBAN
|
||||
3524,35,KABUPATEN LAMONGAN
|
||||
3525,35,KABUPATEN GRESIK
|
||||
3526,35,KABUPATEN BANGKALAN
|
||||
3527,35,KABUPATEN SAMPANG
|
||||
3528,35,KABUPATEN PAMEKASAN
|
||||
3529,35,KABUPATEN SUMENEP
|
||||
3571,35,KOTA KEDIRI
|
||||
3572,35,KOTA BLITAR
|
||||
3573,35,KOTA MALANG
|
||||
3574,35,KOTA PROBOLINGGO
|
||||
3575,35,KOTA PASURUAN
|
||||
3576,35,KOTA MOJOKERTO
|
||||
3577,35,KOTA MADIUN
|
||||
3578,35,KOTA SURABAYA
|
||||
3579,35,KOTA BATU
|
||||
3601,36,KABUPATEN PANDEGLANG
|
||||
3602,36,KABUPATEN LEBAK
|
||||
3603,36,KABUPATEN TANGERANG
|
||||
3604,36,KABUPATEN SERANG
|
||||
3671,36,KOTA TANGERANG
|
||||
3672,36,KOTA CILEGON
|
||||
3673,36,KOTA SERANG
|
||||
3674,36,KOTA TANGERANG SELATAN
|
||||
5101,51,KABUPATEN JEMBRANA
|
||||
5102,51,KABUPATEN TABANAN
|
||||
5103,51,KABUPATEN BADUNG
|
||||
5104,51,KABUPATEN GIANYAR
|
||||
5105,51,KABUPATEN KLUNGKUNG
|
||||
5106,51,KABUPATEN BANGLI
|
||||
5107,51,KABUPATEN KARANG ASEM
|
||||
5108,51,KABUPATEN BULELENG
|
||||
5171,51,KOTA DENPASAR
|
||||
5201,52,KABUPATEN LOMBOK BARAT
|
||||
5202,52,KABUPATEN LOMBOK TENGAH
|
||||
5203,52,KABUPATEN LOMBOK TIMUR
|
||||
5204,52,KABUPATEN SUMBAWA
|
||||
5205,52,KABUPATEN DOMPU
|
||||
5206,52,KABUPATEN BIMA
|
||||
5207,52,KABUPATEN SUMBAWA BARAT
|
||||
5208,52,KABUPATEN LOMBOK UTARA
|
||||
5271,52,KOTA MATARAM
|
||||
5272,52,KOTA BIMA
|
||||
5301,53,KABUPATEN SUMBA BARAT
|
||||
5302,53,KABUPATEN SUMBA TIMUR
|
||||
5303,53,KABUPATEN KUPANG
|
||||
5304,53,KABUPATEN TIMOR TENGAH SELATAN
|
||||
5305,53,KABUPATEN TIMOR TENGAH UTARA
|
||||
5306,53,KABUPATEN BELU
|
||||
5307,53,KABUPATEN ALOR
|
||||
5308,53,KABUPATEN LEMBATA
|
||||
5309,53,KABUPATEN FLORES TIMUR
|
||||
5310,53,KABUPATEN SIKKA
|
||||
5311,53,KABUPATEN ENDE
|
||||
5312,53,KABUPATEN NGADA
|
||||
5313,53,KABUPATEN MANGGARAI
|
||||
5314,53,KABUPATEN ROTE NDAO
|
||||
5315,53,KABUPATEN MANGGARAI BARAT
|
||||
5316,53,KABUPATEN SUMBA TENGAH
|
||||
5317,53,KABUPATEN SUMBA BARAT DAYA
|
||||
5318,53,KABUPATEN NAGEKEO
|
||||
5319,53,KABUPATEN MANGGARAI TIMUR
|
||||
5320,53,KABUPATEN SABU RAIJUA
|
||||
5321,53,KABUPATEN MALAKA
|
||||
5371,53,KOTA KUPANG
|
||||
6101,61,KABUPATEN SAMBAS
|
||||
6102,61,KABUPATEN BENGKAYANG
|
||||
6103,61,KABUPATEN LANDAK
|
||||
6104,61,KABUPATEN MEMPAWAH
|
||||
6105,61,KABUPATEN SANGGAU
|
||||
6106,61,KABUPATEN KETAPANG
|
||||
6107,61,KABUPATEN SINTANG
|
||||
6108,61,KABUPATEN KAPUAS HULU
|
||||
6109,61,KABUPATEN SEKADAU
|
||||
6110,61,KABUPATEN MELAWI
|
||||
6111,61,KABUPATEN KAYONG UTARA
|
||||
6112,61,KABUPATEN KUBU RAYA
|
||||
6171,61,KOTA PONTIANAK
|
||||
6172,61,KOTA SINGKAWANG
|
||||
6201,62,KABUPATEN KOTAWARINGIN BARAT
|
||||
6202,62,KABUPATEN KOTAWARINGIN TIMUR
|
||||
6203,62,KABUPATEN KAPUAS
|
||||
6204,62,KABUPATEN BARITO SELATAN
|
||||
6205,62,KABUPATEN BARITO UTARA
|
||||
6206,62,KABUPATEN SUKAMARA
|
||||
6207,62,KABUPATEN LAMANDAU
|
||||
6208,62,KABUPATEN SERUYAN
|
||||
6209,62,KABUPATEN KATINGAN
|
||||
6210,62,KABUPATEN PULANG PISAU
|
||||
6211,62,KABUPATEN GUNUNG MAS
|
||||
6212,62,KABUPATEN BARITO TIMUR
|
||||
6213,62,KABUPATEN MURUNG RAYA
|
||||
6271,62,KOTA PALANGKA RAYA
|
||||
6301,63,KABUPATEN TANAH LAUT
|
||||
6302,63,KABUPATEN KOTA BARU
|
||||
6303,63,KABUPATEN BANJAR
|
||||
6304,63,KABUPATEN BARITO KUALA
|
||||
6305,63,KABUPATEN TAPIN
|
||||
6306,63,KABUPATEN HULU SUNGAI SELATAN
|
||||
6307,63,KABUPATEN HULU SUNGAI TENGAH
|
||||
6308,63,KABUPATEN HULU SUNGAI UTARA
|
||||
6309,63,KABUPATEN TABALONG
|
||||
6310,63,KABUPATEN TANAH BUMBU
|
||||
6311,63,KABUPATEN BALANGAN
|
||||
6371,63,KOTA BANJARMASIN
|
||||
6372,63,KOTA BANJAR BARU
|
||||
6401,64,KABUPATEN PASER
|
||||
6402,64,KABUPATEN KUTAI BARAT
|
||||
6403,64,KABUPATEN KUTAI KARTANEGARA
|
||||
6404,64,KABUPATEN KUTAI TIMUR
|
||||
6405,64,KABUPATEN BERAU
|
||||
6409,64,KABUPATEN PENAJAM PASER UTARA
|
||||
6411,64,KABUPATEN MAHAKAM HULU
|
||||
6471,64,KOTA BALIKPAPAN
|
||||
6472,64,KOTA SAMARINDA
|
||||
6474,64,KOTA BONTANG
|
||||
6501,65,KABUPATEN MALINAU
|
||||
6502,65,KABUPATEN BULUNGAN
|
||||
6503,65,KABUPATEN TANA TIDUNG
|
||||
6504,65,KABUPATEN NUNUKAN
|
||||
6571,65,KOTA TARAKAN
|
||||
7101,71,KABUPATEN BOLAANG MONGONDOW
|
||||
7102,71,KABUPATEN MINAHASA
|
||||
7103,71,KABUPATEN KEPULAUAN SANGIHE
|
||||
7104,71,KABUPATEN KEPULAUAN TALAUD
|
||||
7105,71,KABUPATEN MINAHASA SELATAN
|
||||
7106,71,KABUPATEN MINAHASA UTARA
|
||||
7107,71,KABUPATEN BOLAANG MONGONDOW UTARA
|
||||
7108,71,KABUPATEN SIAU TAGULANDANG BIARO
|
||||
7109,71,KABUPATEN MINAHASA TENGGARA
|
||||
7110,71,KABUPATEN BOLAANG MONGONDOW SELATAN
|
||||
7111,71,KABUPATEN BOLAANG MONGONDOW TIMUR
|
||||
7171,71,KOTA MANADO
|
||||
7172,71,KOTA BITUNG
|
||||
7173,71,KOTA TOMOHON
|
||||
7174,71,KOTA KOTAMOBAGU
|
||||
7201,72,KABUPATEN BANGGAI KEPULAUAN
|
||||
7202,72,KABUPATEN BANGGAI
|
||||
7203,72,KABUPATEN MOROWALI
|
||||
7204,72,KABUPATEN POSO
|
||||
7205,72,KABUPATEN DONGGALA
|
||||
7206,72,KABUPATEN TOLI-TOLI
|
||||
7207,72,KABUPATEN BUOL
|
||||
7208,72,KABUPATEN PARIGI MOUTONG
|
||||
7209,72,KABUPATEN TOJO UNA-UNA
|
||||
7210,72,KABUPATEN SIGI
|
||||
7211,72,KABUPATEN BANGGAI LAUT
|
||||
7212,72,KABUPATEN MOROWALI UTARA
|
||||
7271,72,KOTA PALU
|
||||
7301,73,KABUPATEN KEPULAUAN SELAYAR
|
||||
7302,73,KABUPATEN BULUKUMBA
|
||||
7303,73,KABUPATEN BANTAENG
|
||||
7304,73,KABUPATEN JENEPONTO
|
||||
7305,73,KABUPATEN TAKALAR
|
||||
7306,73,KABUPATEN GOWA
|
||||
7307,73,KABUPATEN SINJAI
|
||||
7308,73,KABUPATEN MAROS
|
||||
7309,73,KABUPATEN PANGKAJENE DAN KEPULAUAN
|
||||
7310,73,KABUPATEN BARRU
|
||||
7311,73,KABUPATEN BONE
|
||||
7312,73,KABUPATEN SOPPENG
|
||||
7313,73,KABUPATEN WAJO
|
||||
7314,73,KABUPATEN SIDENRENG RAPPANG
|
||||
7315,73,KABUPATEN PINRANG
|
||||
7316,73,KABUPATEN ENREKANG
|
||||
7317,73,KABUPATEN LUWU
|
||||
7318,73,KABUPATEN TANA TORAJA
|
||||
7322,73,KABUPATEN LUWU UTARA
|
||||
7325,73,KABUPATEN LUWU TIMUR
|
||||
7326,73,KABUPATEN TORAJA UTARA
|
||||
7371,73,KOTA MAKASSAR
|
||||
7372,73,KOTA PAREPARE
|
||||
7373,73,KOTA PALOPO
|
||||
7401,74,KABUPATEN BUTON
|
||||
7402,74,KABUPATEN MUNA
|
||||
7403,74,KABUPATEN KONAWE
|
||||
7404,74,KABUPATEN KOLAKA
|
||||
7405,74,KABUPATEN KONAWE SELATAN
|
||||
7406,74,KABUPATEN BOMBANA
|
||||
7407,74,KABUPATEN WAKATOBI
|
||||
7408,74,KABUPATEN KOLAKA UTARA
|
||||
7409,74,KABUPATEN BUTON UTARA
|
||||
7410,74,KABUPATEN KONAWE UTARA
|
||||
7411,74,KABUPATEN KOLAKA TIMUR
|
||||
7412,74,KABUPATEN KONAWE KEPULAUAN
|
||||
7413,74,KABUPATEN MUNA BARAT
|
||||
7414,74,KABUPATEN BUTON TENGAH
|
||||
7415,74,KABUPATEN BUTON SELATAN
|
||||
7471,74,KOTA KENDARI
|
||||
7472,74,KOTA BAUBAU
|
||||
7501,75,KABUPATEN BOALEMO
|
||||
7502,75,KABUPATEN GORONTALO
|
||||
7503,75,KABUPATEN POHUWATO
|
||||
7504,75,KABUPATEN BONE BOLANGO
|
||||
7505,75,KABUPATEN GORONTALO UTARA
|
||||
7571,75,KOTA GORONTALO
|
||||
7601,76,KABUPATEN MAJENE
|
||||
7602,76,KABUPATEN POLEWALI MANDAR
|
||||
7603,76,KABUPATEN MAMASA
|
||||
7604,76,KABUPATEN MAMUJU
|
||||
7605,76,KABUPATEN MAMUJU UTARA
|
||||
7606,76,KABUPATEN MAMUJU TENGAH
|
||||
8101,81,KABUPATEN MALUKU TENGGARA BARAT
|
||||
8102,81,KABUPATEN MALUKU TENGGARA
|
||||
8103,81,KABUPATEN MALUKU TENGAH
|
||||
8104,81,KABUPATEN BURU
|
||||
8105,81,KABUPATEN KEPULAUAN ARU
|
||||
8106,81,KABUPATEN SERAM BAGIAN BARAT
|
||||
8107,81,KABUPATEN SERAM BAGIAN TIMUR
|
||||
8108,81,KABUPATEN MALUKU BARAT DAYA
|
||||
8109,81,KABUPATEN BURU SELATAN
|
||||
8171,81,KOTA AMBON
|
||||
8172,81,KOTA TUAL
|
||||
8201,82,KABUPATEN HALMAHERA BARAT
|
||||
8202,82,KABUPATEN HALMAHERA TENGAH
|
||||
8203,82,KABUPATEN KEPULAUAN SULA
|
||||
8204,82,KABUPATEN HALMAHERA SELATAN
|
||||
8205,82,KABUPATEN HALMAHERA UTARA
|
||||
8206,82,KABUPATEN HALMAHERA TIMUR
|
||||
8207,82,KABUPATEN PULAU MOROTAI
|
||||
8208,82,KABUPATEN PULAU TALIABU
|
||||
8271,82,KOTA TERNATE
|
||||
8272,82,KOTA TIDORE KEPULAUAN
|
||||
9101,91,KABUPATEN FAKFAK
|
||||
9102,91,KABUPATEN KAIMANA
|
||||
9103,91,KABUPATEN TELUK WONDAMA
|
||||
9104,91,KABUPATEN TELUK BINTUNI
|
||||
9105,91,KABUPATEN MANOKWARI
|
||||
9106,91,KABUPATEN SORONG SELATAN
|
||||
9107,91,KABUPATEN SORONG
|
||||
9108,91,KABUPATEN RAJA AMPAT
|
||||
9109,91,KABUPATEN TAMBRAUW
|
||||
9110,91,KABUPATEN MAYBRAT
|
||||
9111,91,KABUPATEN MANOKWARI SELATAN
|
||||
9112,91,KABUPATEN PEGUNUNGAN ARFAK
|
||||
9171,91,KOTA SORONG
|
||||
9401,94,KABUPATEN MERAUKE
|
||||
9402,94,KABUPATEN JAYAWIJAYA
|
||||
9403,94,KABUPATEN JAYAPURA
|
||||
9404,94,KABUPATEN NABIRE
|
||||
9408,94,KABUPATEN KEPULAUAN YAPEN
|
||||
9409,94,KABUPATEN BIAK NUMFOR
|
||||
9410,94,KABUPATEN PANIAI
|
||||
9411,94,KABUPATEN PUNCAK JAYA
|
||||
9412,94,KABUPATEN MIMIKA
|
||||
9413,94,KABUPATEN BOVEN DIGOEL
|
||||
9414,94,KABUPATEN MAPPI
|
||||
9415,94,KABUPATEN ASMAT
|
||||
9416,94,KABUPATEN YAHUKIMO
|
||||
9417,94,KABUPATEN PEGUNUNGAN BINTANG
|
||||
9418,94,KABUPATEN TOLIKARA
|
||||
9419,94,KABUPATEN SARMI
|
||||
9420,94,KABUPATEN KEEROM
|
||||
9426,94,KABUPATEN WAROPEN
|
||||
9427,94,KABUPATEN SUPIORI
|
||||
9428,94,KABUPATEN MAMBERAMO RAYA
|
||||
9429,94,KABUPATEN NDUGA
|
||||
9430,94,KABUPATEN LANNY JAYA
|
||||
9431,94,KABUPATEN MAMBERAMO TENGAH
|
||||
9432,94,KABUPATEN YALIMO
|
||||
9433,94,KABUPATEN PUNCAK
|
||||
9434,94,KABUPATEN DOGIYAI
|
||||
9435,94,KABUPATEN INTAN JAYA
|
||||
9436,94,KABUPATEN DEIYAI
|
||||
9471,94,KOTA JAYAPURA
|
||||
|
@@ -0,0 +1,21 @@
|
||||
Code,Name,Status
|
||||
SP001,DOKTER UMUM,true
|
||||
SP002,DOKTER ANAK,true
|
||||
SP003,DOKTER KANDUNGAN,true
|
||||
SP004,DOKTER BEDAH,true
|
||||
SP005,DOKTER JANTUNG,true
|
||||
SP006,DOKTER SARAF,true
|
||||
SP007,DOKTER THT,true
|
||||
SP008,DOKTER MATA,true
|
||||
SP009,DOKTER KULIT DAN KELAMIN,true
|
||||
SP010,DOKTER GIGI,true
|
||||
SP011,DOKTER PSIKIATRI,true
|
||||
SP012,DOKTER ORTHOPEDI,true
|
||||
SP013,DOKTER UROLOGI,true
|
||||
SP014,DOKTER PARU,true
|
||||
SP015,DOKTER GIZI,true
|
||||
SP016,DOKTER REHABILITASI MEDIS,true
|
||||
SP017,DOKTER GERIATRI,true
|
||||
SP018,DOKTER ONKOLOGI,true
|
||||
SP019,DOKTER KARDIOVASKULER,true
|
||||
SP020,DOKTER NEFROLOGI,true
|
||||
|
@@ -0,0 +1,21 @@
|
||||
Code,Specialist_Code,Name,Status
|
||||
SS001,SP001,MEDICINE,true
|
||||
SS002,SP002,PEDIATRICS,true
|
||||
SS003,SP003,OBSTETRICS,true
|
||||
SS004,SP004,SURGERY,true
|
||||
SS005,SP005,CARDIOLOGY,true
|
||||
SS006,SP006,NEUROLOGY,true
|
||||
SS007,SP007,OTORHINOLARYNGOLOGY,true
|
||||
SS008,SP008,OPHTHALMOLOGY,true
|
||||
SS009,SP009,DERMATOLOGY,true
|
||||
SS010,SP010,DENTISTRY,true
|
||||
SS011,SP011,PSYCHIATRY,true
|
||||
SS012,SP012,ORTHOPEDICS,true
|
||||
SS013,SP013,UROLOGY,true
|
||||
SS014,SP014,PULMONOLOGY,true
|
||||
SS015,SP015,NUTRITION,true
|
||||
SS016,SP016,PHYSICAL MEDICINE,true
|
||||
SS017,SP017,GERIATRICS,true
|
||||
SS018,SP018,ONCOLOGY,true
|
||||
SS019,SP019,CARDIOVASCULAR,true
|
||||
SS020,SP020,NEPHROLOGY,true
|
||||
|
@@ -0,0 +1,21 @@
|
||||
Code,Name,Status
|
||||
001,POLI UMUM,true
|
||||
002,POLI ANAK,true
|
||||
003,POLI KANDUNGAN,true
|
||||
004,POLI BEDAH,true
|
||||
005,POLI JANTUNG,true
|
||||
006,POLI SARAF,true
|
||||
007,POLI THT,true
|
||||
008,POLI MATA,true
|
||||
009,POLI KULIT,true
|
||||
010,POLI GIGI,true
|
||||
011,POLI PSIKIATRI,true
|
||||
012,POLI ORTHOPEDI,true
|
||||
013,POLI UROLOGI,true
|
||||
014,POLI PARU,true
|
||||
015,POLI GIZI,true
|
||||
016,POLI REHABILITASI,true
|
||||
017,POLI GERIATRI,true
|
||||
018,POLI ONKOLOGI,true
|
||||
019,POLI KARDIOVASKULER,true
|
||||
020,POLI NEFROLOGI,true
|
||||
|
+80535
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,227 @@
|
||||
// internal/infrastructure/database/database.go
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"service-general/internal/infrastructure/config"
|
||||
"service-general/pkg/logger" // Import logger Anda
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/lib/pq" // PASTIKAN IMPORT INI ADA
|
||||
|
||||
// Driver GORM di-import secara eksplisit untuk Migration
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/driver/sqlserver"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger" // Import logger dari GORM
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// ... (tipe DatabaseType dan interface Service tetap sama)
|
||||
|
||||
type service struct {
|
||||
// GORM DB adalah sumber utama
|
||||
gormDatabases map[string]*gorm.DB
|
||||
// Map untuk menyimpan koneksi mentah yang diekstrak dari GORM
|
||||
sqlDatabases map[string]*sql.DB
|
||||
sqlxDatabases map[string]*sqlx.DB
|
||||
mongoClients map[string]*mongo.Client
|
||||
readReplicas map[string][]*sql.DB
|
||||
configs map[string]config.DatabaseConfig
|
||||
readConfigs map[string][]config.DatabaseConfig
|
||||
mu sync.RWMutex
|
||||
readBalancer map[string]int
|
||||
// PERBAIKAN 1: Kembali ke tipe yang benar untuk listener PostgreSQL
|
||||
listeners map[string]*pq.Listener
|
||||
listenersMu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
dbManager *service
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// ... (fungsi New dan loadFromConfig tetap sama)
|
||||
|
||||
// --- PERBAIKAN 2: Buat Adapter untuk Logger GORM ---
|
||||
|
||||
// gormLoggerAdapter adalah adapter untuk menghubungkan logger kita dengan GORM
|
||||
type gormLoggerAdapter struct {
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewGormLoggerAdapter membuat instance baru dari adapter
|
||||
func NewGormLoggerAdapter(l logger.Logger) logger.Interface {
|
||||
// Tentukan level log GORM berdasarkan level logger kita
|
||||
// Ini adalah implementasi sederhana, Anda bisa membuatnya lebih dinamis
|
||||
logLevel := logger.Silent
|
||||
if l != nil {
|
||||
// Anda bisa menambahkan logika untuk menentukan level dari logger Anda
|
||||
// Untuk sekarang, kita hardcode ke Info
|
||||
logLevel = logger.Info
|
||||
}
|
||||
|
||||
return &gormLoggerAdapter{
|
||||
logger: l,
|
||||
// Konfigurasi logger GORM
|
||||
Config: logger.Config{
|
||||
SlowThreshold: 200 * time.Millisecond,
|
||||
LogLevel: logLevel,
|
||||
IgnoreRecordNotFoundError: false,
|
||||
Colorful: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// LogMode menetapkan level log
|
||||
func (l *gormLoggerAdapter) LogMode(level logger.LogLevel) logger.Interface {
|
||||
newLogger := *l
|
||||
newLogger.Config.LogLevel = level
|
||||
return &newLogger
|
||||
}
|
||||
|
||||
// Info mencatat log info
|
||||
func (l *gormLoggerAdapter) Info(ctx context.Context, msg string, data ...interface{}) {
|
||||
l.logger.WithContext(ctx).Info(fmt.Sprintf(msg, data...))
|
||||
}
|
||||
|
||||
// Warn mencatat log warning
|
||||
func (l *gormLoggerAdapter) Warn(ctx context.Context, msg string, data ...interface{}) {
|
||||
l.logger.WithContext(ctx).Warn(fmt.Sprintf(msg, data...))
|
||||
}
|
||||
|
||||
// Error mencatat log error
|
||||
func (l *gormLoggerAdapter) Error(ctx context.Context, msg string, data ...interface{}) {
|
||||
l.logger.WithContext(ctx).Error(fmt.Sprintf(msg, data...))
|
||||
}
|
||||
|
||||
// Trace mencatat log trace (SQL query)
|
||||
func (l *gormLoggerAdapter) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
|
||||
elapsed := time.Since(begin)
|
||||
sql, rows := fc()
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"duration": elapsed,
|
||||
"rows": rows,
|
||||
"sql": sql,
|
||||
"error": err,
|
||||
"function": "database.trace",
|
||||
}
|
||||
|
||||
switch {
|
||||
case err != nil && l.Config.LogLevel >= logger.Error:
|
||||
l.logger.WithContext(ctx).WithFields(logger.Any("error", err)).Error("Database error", logger.Any("details", fields))
|
||||
case elapsed > l.Config.SlowThreshold && l.Config.LogLevel >= logger.Warn:
|
||||
l.logger.WithContext(ctx).Warn("Slow database query", logger.Any("details", fields))
|
||||
case l.Config.LogLevel == logger.Info:
|
||||
l.logger.WithContext(ctx).Debug("Database query", logger.Any("details", fields))
|
||||
}
|
||||
}
|
||||
|
||||
// --- Metode Koneksi GORM (diperbaiki untuk menggunakan adapter) ---
|
||||
|
||||
func (s *service) openPostgresGORM(config config.DatabaseConfig) (*gorm.DB, error) {
|
||||
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=Asia/Jakarta",
|
||||
config.Host, config.Username, config.Password, config.Database, config.Port, config.SSLMode)
|
||||
|
||||
if config.Schema != "" {
|
||||
dsn += fmt.Sprintf(" search_path=%s", config.Schema)
|
||||
}
|
||||
|
||||
// PERBAIKAN: Gunakan adapter logger kita
|
||||
gormConfig := &gorm.Config{
|
||||
Logger: NewGormLoggerAdapter(logger.Default()),
|
||||
}
|
||||
|
||||
if config.RequireSSL {
|
||||
// ... (konfigurasi SSL tetap sama)
|
||||
}
|
||||
|
||||
return gorm.Open(postgres.Open(dsn), gormConfig)
|
||||
}
|
||||
|
||||
func (s *service) openMySQLGORM(config config.DatabaseConfig) (*gorm.DB, error) {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
config.Username, config.Password, config.Host, config.Port, config.Database)
|
||||
|
||||
if config.RequireSSL {
|
||||
dsn += "&tls=true"
|
||||
}
|
||||
|
||||
// PERBAIKAN: Gunakan adapter logger kita
|
||||
gormConfig := &gorm.Config{
|
||||
Logger: NewGormLoggerAdapter(logger.Default()),
|
||||
}
|
||||
|
||||
return gorm.Open(mysql.Open(dsn), gormConfig)
|
||||
}
|
||||
|
||||
func (s *service) openSQLServerGORM(config config.DatabaseConfig) (*gorm.DB, error) {
|
||||
dsn := fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s",
|
||||
config.Username, config.Password, config.Host, config.Port, config.Database)
|
||||
|
||||
if config.RequireSSL {
|
||||
// ... (konfigurasi SSL tetap sama)
|
||||
}
|
||||
|
||||
// PERBAIKAN: Gunakan adapter logger kita
|
||||
gormConfig := &gorm.Config{
|
||||
Logger: NewGormLoggerAdapter(logger.Default()),
|
||||
}
|
||||
|
||||
return gorm.Open(sqlserver.Open(dsn), gormConfig)
|
||||
}
|
||||
|
||||
func (s *service) openSQLiteGORM(config config.DatabaseConfig) (*gorm.DB, error) {
|
||||
// PERBAIKAN: Gunakan adapter logger kita
|
||||
gormConfig := &gorm.Config{
|
||||
Logger: NewGormLoggerAdapter(logger.Default()),
|
||||
}
|
||||
|
||||
return gorm.Open(sqlite.Open(config.Path), gormConfig)
|
||||
}
|
||||
|
||||
// --- Metode Lainnya ---
|
||||
|
||||
// ... (semua metode lain seperti GetGormDB, GetDB, Migrate, Health, Close, dll. tidak perlu diubah)
|
||||
// ... (kecuali mungkin bagian Close yang membersihkan listeners)
|
||||
|
||||
// Di fungsi Close, pastikan tipe listener sudah benar
|
||||
func (s *service) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
var errs []error
|
||||
|
||||
for name, listener := range s.listeners {
|
||||
if err := listener.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to close listener for %s: %w", name, err))
|
||||
}
|
||||
}
|
||||
// ... (sisa kode Close)
|
||||
}
|
||||
|
||||
// Di fungsi ListenForChanges, pastikan Anda menggunakan pq.NewListener
|
||||
func (s *service) ListenForChanges(ctx context.Context, dbName string, channels []string, callback func(string, string)) error {
|
||||
// ... (kode Anda sudah benar menggunakan pq.NewListener)
|
||||
// ...
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
h1:w6tFa/NMDQdQDXl91m3KqSaPSa9ktGQH7la07Gu6eoc=
|
||||
20250904105930.sql h1:MEM6blCgke9DzWQSTnLzasbPIrcHssNNrJqZpSkEo6k=
|
||||
20250904141448.sql h1:J8cmYNk4ZrG9fhfbi2Z1IWz7YkfvhFqTzrLFo58BPY0=
|
||||
20250908062237.sql h1:Pu23yEW/aKkwozHoOuROvHS/GK4ngARJGdO7FB7HZuI=
|
||||
20250908062323.sql h1:oXl6Z143tOpIl4EfP4B8JNU8LrMvVmHEtCgAfiB4gs8=
|
||||
20250908073811.sql h1:m2aNXfnGxnLq1+rVWrh4f60q7fhyhV3gEwNu/OIqQlE=
|
||||
20250908073839.sql h1:cPk54xjLdMs26uY8ZHjNWLuyfAMzV7Zb0/9oJQrsw04=
|
||||
20250910055902.sql h1:5xwjAV6QbtZT9empTJKfhyAjdknbHzb15B0Ku5dzqtQ=
|
||||
20250915123412.sql h1:D83xaU2YlDEd21HLup/YQpQ2easMToYCyy/oK6AFgQs=
|
||||
20250916043819.sql h1:ekoTJsBqQZ8G8n0qJ03d13+eoNoc7sAUEQGA5D/CCxk=
|
||||
20250917040616.sql h1:zoCnmcXuM7AVv85SmN7RmFglCgJnoDmpRWExH0LAc9Q=
|
||||
20250917040751.sql h1:J1xyRrh32y1+lezwAyNwPcUQ6ABBSgbvzNLva4SVdQU=
|
||||
20250917045138.sql h1:jKe1Z0uOLG4SGBYM+S/3P+/zMPztmgoderD5swnMuCg=
|
||||
20250917093645.sql h1:cNI3Pbz1R3LxvIXLuexafJFCXUXrmuFCgXXJ2sG+FW0=
|
||||
20250918073552.sql h1:RJ1SvMzP6aeWnoPVD3eVAmIQOkcp6Php8z3QRri6v4g=
|
||||
20250918073742.sql h1:+cEsnJTJFybe2fR69ZoOiX2R6c6iITl4m6WTZ1hjyzY=
|
||||
20250918074745.sql h1:2hNVQCXF/dVYXAh+T/7oBFgERGWxzVb2FXJjwkFWGCI=
|
||||
20250923025134.sql h1:Ykz/qpHiGDXPsCsWTjydQFVSibZP2D+h2fIeb2h2JGA=
|
||||
20250924051317.sql h1:yQuW6SwJxIOM5fcxeAaie5lSm1oLysU/C2hH2xNCVoQ=
|
||||
20250929034321.sql h1:101FJ8VH12mrZWlt/X1gvKUGOhoiF8tFbjiapAjnHzg=
|
||||
20250929034428.sql h1:i+pROD9p+g5dOmmZma6WF/0Hw5g3Ha28NN85iTo1K34=
|
||||
20250930025550.sql h1:+F+CsCUXD/ql0tHGEow70GhPBX1ZybVn+bh/T4YMh7Y=
|
||||
20250930140351.sql h1:9AAEG1AnOAH+o0+oHL5G7I8vqlWOhwRlCGyyCpT/y1Q=
|
||||
20251002085604.sql h1:3xZ68eYp4urXRnvotNH1XvG2mYOSDV/j3zHEZ/txg5E=
|
||||
20251003032030.sql h1:HB+mQ2lXMNomHDpaRhB/9IwYI9/YiDO5eOJ+nAQH/jw=
|
||||
20251005060450.sql h1:LbtCE2b+8osM3CvnmQJH1uCPtn+d7WchsslBOz8bL3Q=
|
||||
20251006041122.sql h1:MlS7f21z06sutnf9dIekt5fuHJr4lgcQ4uCuCXAGsfc=
|
||||
20251006045658.sql h1:3FmGCPCzjgMPdWDRodZTsx3KVaodd9zB9ilib69aewk=
|
||||
20251006045928.sql h1:Z5g31PmnzNwk/OKdODcxZGm8fjJQdMFK32Xfnt3bRHg=
|
||||
20251007022859.sql h1:FO03zEfaNEk/aXwY81d5Lp3MoBB9kPQuXlXJ4BPiSR8=
|
||||
20251008031337.sql h1:l+sxUAGvcTfj3I6kAFHo+T6AYodC9k9GkR+jaKO2xXc=
|
||||
20251008031554.sql h1:AqrVfIhSzY3PCy8ZlP5W91wn2iznfIuj5qQfubp6/94=
|
||||
20251008052346.sql h1:nxnXmooIJ6r1mmzwnw+6efxLfc/k9h2aE6RMptPRons=
|
||||
20251008073620.sql h1:6YsJp1W4SmQJ1lxpqF27BBlDC1zqhw7Yhc7pLzQTY6M=
|
||||
20251009042854.sql h1:nkBV+R6j0fg7/JY6wH3eb5Vv0asJLnXmb6lINfT/GLQ=
|
||||
20251009052657.sql h1:EPvdsib5rzCGPryd10HShGKvFPwM/R5S2lIVwtYxpms=
|
||||
20251010031743.sql h1:T8IZmx8/btRFKLzTe78MzcBsPJNodnLvB0tby9QkirQ=
|
||||
20251010070721.sql h1:5NQUk/yOV6sABLCB7swx++YIOyJe6MnU+yt1nRzde5w=
|
||||
20251010072711.sql h1:ZJNqR2piyu8xJhBvVABSlnGEoKSKae3wuEs+wshPe4k=
|
||||
20251013044536.sql h1:0Xjw8fNILiT8nnfrJDZgQnPf3dntmIoilbapnih8AE4=
|
||||
20251013051438.sql h1:lfSuw5mgJnePBJamvhZ81osFIouXeiIEiSZ/evdwo48=
|
||||
20251013081808.sql h1:ijgjNX08G6GBjA/ks8EKtb7P7Y7Cg7zbhqEOruGnv6M=
|
||||
20251014060047.sql h1:0jqj49WTtneEIMQDBoo4c095ZGi8sCrA8NnHBrPU6D8=
|
||||
20251014063537.sql h1:VZLXol0PTsTW21Epg6vBPsztWkDtcxup9F/z88EGgIg=
|
||||
20251014063720.sql h1:2HVUyCV0ud3BJJDH2GEKZN/+IWLFPCsN1KqhP6csO14=
|
||||
20251015045455.sql h1:MeLWmMhAOAz8b15Dd7IAQnt6JxjSml02XCXK22C0Lpg=
|
||||
20251016010845.sql h1:4BncQdDOasRZJkzVJrSJJA7091A9VPNVx/faUCUPhBM=
|
||||
20251016011023.sql h1:9JB9eFZKURK5RoCVDKR6glSvdJ8NTXrN7K/4q51zkz4=
|
||||
20251016062912.sql h1:ACNn0fe+EMqUt3hoY+Dr3uqAV/QICBa1+mIW7fUc9Fk=
|
||||
20251017060617.sql h1:4T3t9ifWrEQTPMSM0XJ98pF7Qdt+UfgtMui17bhrnWI=
|
||||
20251017082207.sql h1:8vLG1l/saRRMHXkyA4nelJyjaSddhZd6r7R+Uo4JS/c=
|
||||
20251018032635.sql h1:2xey5gnO3y2XSOrU8MLlIfoylPKbRGDRtHDD07B3MbQ=
|
||||
20251018040322.sql h1:k/pdNiSoT8zFPqNQ/avOD0vYkNh3BTD64IlHrfVXr7I=
|
||||
20251019093915.sql h1:hFcQE0y+p5dZiVwePGsRGto9m/q6kJNiUZbVDd5Rnjk=
|
||||
20251020062553.sql h1:Iw7hulcm5iRQlfW+ygA4iTPxLqkxx6h9vXMXEwUAHKs=
|
||||
20251021041042.sql h1:wMgSivBV2A0NDcsLmKGIp0kMcVh2IODSG9b4dgzCaOM=
|
||||
20251021075552.sql h1:8gfSMAglflNO6L0sSzxFNEubYN8/O4thT7OQT+WH+3M=
|
||||
20251023044432.sql h1:MkvajJs3bfk9+wHvQ43/ccAluJEBARm1gWr1u92ccLA=
|
||||
20251024034832.sql h1:x3s3VEVYLOSKLAFxJGb2+c1FyTMMvPE+9k4Ew7rKQaI=
|
||||
20251024074315.sql h1:EjAjelgi5qAfcRq/8vPTlGGYHvAKxNTllm8f0SzZDns=
|
||||
20251025013451.sql h1:6hnuIiwYiG+6nLhOY/+Yyn+I6ZCFNRZxrJNqBV6HLqE=
|
||||
20251025013609.sql h1:evPJaTD8WxYRMOJZHkSr7ONLx9PYxT+ankzQt9c/sJ0=
|
||||
20251027075128.sql h1:/iFQBM1sytjqpyQSOx61q33gnorMgxTiFVSuL6bQqsM=
|
||||
20251027091406.sql h1:eCZGtUkxAzEAqpC9UsGpP8Df9mS0DEOqSl885LgqpvM=
|
||||
20251102002037.sql h1:lFJbuoZ2LMQnUNGdcwHVY3Xlfslgzu9t2WByT8yfOZI=
|
||||
20251102091932.sql h1:rmdhb5m+P+fU8jROBZNyeYgZKuQvucsuljXv4ZVzvks=
|
||||
20251103081637.sql h1:tf3BcwTeIw+oxMEisKDDfyKnBfalTLs8b0PJA8JWYxY=
|
||||
20251104042334.sql h1:7PDMWOhmJywolAPKFZ14XaDBeMvcxShaXFN2IemNtzk=
|
||||
20251104043530.sql h1:qvYVp3ysPf27f1BcoRNCFGovxuVE12lg9d6Xzda6zWU=
|
||||
20251104080952.sql h1:avghpv1n3yaCDR/TA0X+hgxDGoLBQGu/GJUwj4VT/Ic=
|
||||
20251104084135.sql h1:rg+eRE5/5sYWR7z+Xyn0zKw8rr8P/oWxF0xhcNVnNec=
|
||||
20251105044629.sql h1:4NU27HeKUNFsV82LacnwmnCSAH0pSbZR9J9/ZESRs6M=
|
||||
20251105121808.sql h1:fii6LjqWYjrm/pEIqttfvJI6QEUL49gque8wYHh1+yI=
|
||||
20251106035305.sql h1:oQ7BwnxPuwY2q98adIVc+lNwL/Sz1OceLJeClDo9/TI=
|
||||
20251106040137.sql h1:ppcqkVoT0o9jZcjI/TN7LuaPxXhJQhnIXEJtloP/46o=
|
||||
20251106041333.sql h1:2JkxyelQ/EeB+boL5bfpnzefw32ttEGKvKchtQjWmAU=
|
||||
20251106042006.sql h1:ruppYa1kAJQUU3ufQBbKGMcXrGbGJJiRPclT+dNc/YQ=
|
||||
20251106050412.sql h1:MiEMJ1HCFYnalKuq3Z38xJeogfBAMqsTv2sG4EF8dDw=
|
||||
20251106063418.sql h1:y3veDJPjKekOWLCZek/LgQwXPRhZtOppTfUXiqoL95s=
|
||||
20251106071906.sql h1:/TUZA3XpMY23qEJXdkTwlzrNMvSSl6JJniPcgAttBaw=
|
||||
20251106073157.sql h1:78txeibJ602DMD7huD618ZSMt6phSRzDNPTlo0PGyrc=
|
||||
20251106074218.sql h1:8Xz7WywrtUnSxOHhlal53gG9rE7r86LFUt5zBFe/mIs=
|
||||
20251106081846.sql h1:jp91Bf5bxGXMiUB1VIuN6y768vb2iWwow44WfCE5J5k=
|
||||
20251106082844.sql h1:RHYzRO4G1fSWwf+xc/3QezZ/Iil67cZPIgNpNz3TNhQ=
|
||||
20251106090021.sql h1:dFDk6mq+zjbYWmfWIrHf9DiKvvoXHjrr0++zssMTWP8=
|
||||
20251106144745.sql h1:aHcr23iBFqCHer5D/SsPMXBCLjGqUYvWYfRU8jSJgIw=
|
||||
20251107012049.sql h1:hu/7NHhnAkT4xK0RNtqmMDdH1Bo5EZbl7itDRjiCT+g=
|
||||
20251107064812.sql h1:sfCXDQYnMf0ddrQ9oYljWJLLSt9NJjJV6o8VS3p7aZE=
|
||||
20251107064937.sql h1:DlYGJ9LZFwZyR7jBP5zaGB128aIc4HAixBKPYCz9EkY=
|
||||
20251107071420.sql h1:ynCdZAd2utLl+FhtWZwtahNXgIVOvuk3s/rOq7lfXA4=
|
||||
20251107074318.sql h1:WE9cPhibWtZ0dbu1VEGirTeY6ijFYGMNhHdBtM32kOc=
|
||||
20251107075050.sql h1:8tvneruqdynDOaJK1+0z4CH7YXZStZpGdqwIeOMLik4=
|
||||
20251107080604.sql h1:8c4jd4Tql7tcdhbI9NS0tgvN+ADu9FnCf8wMUbmW7A0=
|
||||
20251107081830.sql h1:SAAe3lmsm9vGXuSEsDdl7ad0EAxP5CMmFRDEgp9M7yY=
|
||||
20251107091033.sql h1:JLdX/u7GUdBfjrPrMSNAqc8HtSoj0YA9iW9Vc6FJZdw=
|
||||
20251107091209.sql h1:CzhYtwAwT+GHrbqcagnJE+v3mbl/rObf1IJaLCKlzrs=
|
||||
20251107091541.sql h1:+3ZyWJTftDY2JeWThXuIxGWpUBnyMPyOyY4jBjdWYJI=
|
||||
20251110012217.sql h1:f4Z8TuGc+XMSJ+Ekn4/PeHRE2FlHWkc5gKPJB0hAX7c=
|
||||
20251110012306.sql h1:ENPyI6Kjdk6qKJQb0yJ6MCTDPAmO1WD/uhKuCSl+jYo=
|
||||
20251110052049.sql h1:OrQ0acnyoQLKnTitZfnBcVr5jDslF59OFLaqT7SpdVs=
|
||||
20251110062042.sql h1:9KwldQt0NpVPR86L0T4hlkfHAGau+7CiZYgu5rF+yhg=
|
||||
20251110063202.sql h1:A117DuZmZ6U0jWHA3DISnr+yvBjKIr1ObrUr047YezQ=
|
||||
20251110063633.sql h1:qTiC0F19JnhUIXF4LGJQ21jEV6kKGyhTr1x2kimFqPQ=
|
||||
20251110085551.sql h1:HZcJM0RSC6HBaUSjKBE8MgDG8Vn9f3LmwA/OnT9Cp7I=
|
||||
20251110091516.sql h1:W3AQhQLgirEWuCObbLl+Prdrbq6k6EEY1xcoWsmbog4=
|
||||
20251110091948.sql h1:3tsITMrZr/T+L4wqUMz8sHS229jCJl4T0Nu3dMccxH8=
|
||||
20251110092729.sql h1:uU+k88RH/e0Ns4/SmJl03RVYPscBAPuiLfxR6CJqaf0=
|
||||
20251110093522.sql h1:O7upSj8VNjzvroL4IU59bfxKATOkAVGBArcUbVNq9aM=
|
||||
20251110100258.sql h1://JSArUMNI3/gAtYDx2VN94C198SFW0ANjgs+p6eCRM=
|
||||
20251110100545.sql h1:ENPOqeJYRpMI4e8VCKwaQgaql8se6pIidAhG2cjskBg=
|
||||
20251110155448.sql h1:VW9KE0jxWy4flZ28sdXmhY6JWd6eKAX/cVL8KWRVii4=
|
||||
20251111072601.sql h1:99WWzGjcDDFNC2cHRfPu4MBLWNrgPMY5HAYUbmIcVRA=
|
||||
20251111073546.sql h1:iIGwFNfUgStdczw/PXTH3SD84xAyxrbZICofc3M8TMk=
|
||||
20251111074148.sql h1:mzkezSKwCV2bTZ/0BHiTCX5qAy4uHpwar1xzwAH15Ko=
|
||||
20251111074652.sql h1:lYh4/3BHV24pgPC0pP4RUKb+XtL/6AsZGPItRpnzBiQ=
|
||||
20251111082257.sql h1:+cIZ1lf8HYGSL6t6U88plLKk8nOO1YVdV7h3YOM84ds=
|
||||
20251111111017.sql h1:xzlhR2xLfOj4ddYlesS+bpEeDrxiAf5ILNOspsbZjBU=
|
||||
20251113101344.sql h1:vSzThY3p6XYTj0dJ2uFVkHmlytyrFAnGE58CJLx364I=
|
||||
20251113120533.sql h1:lfjgdqRGo/EohrVw8sWlFbDjh3ASTsfQ6pr3qjafqvc=
|
||||
20251114062746.sql h1:6DLYjfJ60PkAABZTXvOwSE+xrU25oyoX7gpYlBnA9cw=
|
||||
20251117005942.sql h1:0XoJKca8IOaB9QKFVF/qPY7jKcIgh6m/LODQuE06SAs=
|
||||
20251117075427.sql h1:LhY2urosfoSu1/vkHmgsNP4JA4DuWBs9gL59yMqcF8M=
|
||||
20251118074929.sql h1:3RWBD6BziQVw6WSfthgoVuhTELHER9NrIuZm4hY/F1A=
|
||||
20251119063438.sql h1:EVTG3ZrHjCmM95YPASZTRPiwHrG6e33A2vO4hLSAaBQ=
|
||||
20251119065730.sql h1:s98wnZOCW6NbnwDS3H53XIQ60u6B9bDwBLNvy5+Rn64=
|
||||
20251119072302.sql h1:ZL+VHekLYqgNtOFKlj02WHrAk11dtTxQkmyTKE8IOzY=
|
||||
20251119072450.sql h1:SiJy2vpSvoPFw4J1SW7HjGSJzrqR62NsjRYAeabV+kc=
|
||||
20251120005512.sql h1:hJUaWXYJ3HiSquBTw8OKhymjA4O43ehAMGfiOY8W87Q=
|
||||
20251120074415.sql h1:dNmcqZHqfZwi/wtYvO/pSFgImN2scXL0p/7g0+HLp0s=
|
||||
20251121033803.sql h1:onlddYGo+tQdGaEdJPqdsx3sncGnwEvqMSCksF6vjjI=
|
||||
20251124071457.sql h1:ikQLIXFikUbkUd55uJBmEvqLGEC9t0db+Om4TQsWP7M=
|
||||
20251125125303.sql h1:OLKbNPO36AHtIDursW9AeBvf60sahQKC1iOjHmpx0MA=
|
||||
20251126064057.sql h1:6al3PTWbw/WGiBcrRrVWppOMLtG+eRaH/qSLbnmh1kQ=
|
||||
20251201081333.sql h1:dD+jcisoUYnxRYWdtVcnxbDeYjUW12/R/qarYQr+utg=
|
||||
20251201104439.sql h1:h4tz5uetbCKeOI3agSVmAeh6jUfECYy0LhQ7HooYhzg=
|
||||
20251201113804.sql h1:DdUzqfLdy4AUpVSRFDrIJXntGmtKgZrtvv8MAT4mMxs=
|
||||
20251201113858.sql h1:XSpicm4aWjrfeY5EYIiw8Ios9v7BGfzZJxpW/57Qwqs=
|
||||
20251201114751.sql h1:qx+ShdHLacVFcRwwGrW1zMKFHOjMGdnusk7CEfyV8ig=
|
||||
20251201114913.sql h1:6l/m2/6fmTIjFmKio0QmB8oTwBDCHTIkbuYTODQ7Gwk=
|
||||
20251202030445.sql h1:5+6ss5KdN6pll8Qtf0RGGyy/BURNO3dBgIBJOzDb624=
|
||||
20251202044430.sql h1:nJtlQDGj7ZJtJ07NYcF41JCn9ck50GyDPoitTOe8P/s=
|
||||
20251202063820.sql h1:dXGXAp2Aq1PUuwAT5FtBquZUeEv5Pdb81/bFt+KR274=
|
||||
20251202064000.sql h1:+W5j/AipU7qnFLCTaS7jVd/SIbIPo4k5rRScAvu0L44=
|
||||
20251202130629.sql h1:rB/+hjwgQnHoA/uOv4pJbB0iXAeaPMinlzRI/755raY=
|
||||
20251202160848.sql h1:KbTqHicChkfszdW9pQzoPlnvtECKDPmwpV3Rc7+5a8o=
|
||||
20251202180207.sql h1:4DMAex+Go7MuMt15Di/J1C/ag15KwgWW1u8KDFmbJ9M=
|
||||
20251202231005.sql h1:4GcnNRFZnjY1nASBj1IUkzLYCYIfnSE9s6kcWXZSiO8=
|
||||
20251203205052.sql h1:0TZ4D3YKO2lmOAa60nhkJAQSx8QHgd+Zj4R39bPdU8Y=
|
||||
20251205073858.sql h1:F4o2SDJ/deNjvKaimBGccZ9t4Fir01YocT6emhEoqiU=
|
||||
20251205211957.sql h1:rJLemdhIo7NQRgsl5DANVuz8pAee7HciyGVET7V/DMk=
|
||||
20251205214433.sql h1:1axU9bZaXW+sASNXQn08dKBX+fDEj1qHGiigEoJQXjY=
|
||||
20251205221124.sql h1:TEy3NdLtM8+BkHwVT11kKvX3V28PP1bDMocZ0404Tm4=
|
||||
20251206021053.sql h1:cEmwbMKPz8DY2OUsr3ym3blyp823418qMnx/AJFFYqY=
|
||||
20251207020537.sql h1:hCENQuZ6jXSp3LrmZ7FgLxvI1gJNgBa+F39/RosKfgk=
|
||||
20251207212015.sql h1:uhNmVl9qXZnqwy3GXfxh5JJjjXK4mztmL19ioezZwVQ=
|
||||
20251207221222.sql h1:QMxCwIwb4M9jA9z8BJ9aNsOcMVwuZ1ZSsx0FjOlQjek=
|
||||
20251209022744.sql h1:BBb5VV+hUGtKW+nZhu/C6OhBgiQhs84vMYfLM51V+VA=
|
||||
20251209025908.sql h1:42z/3wPoOt/nkS92xZ9PHeu7m0IWgVICETkf6Ugtx7w=
|
||||
20251209030538.sql h1:M2zy92ZTBTHLzbKs7cv1S5epLbJPQPVxME5E8nrPL2w=
|
||||
20251209051742.sql h1:AKCu0xgzUlH5MjjBQLiHq6Gw54HA0j++Eknj1i6FHAw=
|
||||
20251209064304.sql h1:MSMSfF2HVU7Qg/nyHmh89i7+oAj3V2wbPv/5AQcYNYM=
|
||||
20251209070128.sql h1:CU3U33jEhzNFa2yc7aBGHv/XcyFEHRkjHUJFLTKZRjc=
|
||||
20251209084929.sql h1:5H1z+0hOmDHQastabHLaxN5d6YWiD2p4+nUf1Fmthrc=
|
||||
20251210145148.sql h1:tAs3iNEHnASYEQ4/h8XDXmKPvVASgfP6oTjnFdmBqFA=
|
||||
20251211101547.sql h1:hGxvjf2XLoHJIQQlCoSOgqOrIkGjk1A7CLdDV1R4pgc=
|
||||
20251211113942.sql h1:uxNunwWePW7UpR+8KtdvqQ4mUofJascWOOUEyRCKIEI=
|
||||
20251216074834.sql h1:jja2bz2dtaC4327V2iqs9LP7MsW6ycz68BM0KPld9xs=
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,277 @@
|
||||
# Flexible Database Seeder
|
||||
|
||||
A flexible and extensible database seeder for Go applications with GORM support.
|
||||
|
||||
## Features
|
||||
|
||||
- 🎯 **Flexible Configuration**: Support for any table structure
|
||||
- 📊 **CSV Import**: Import data from CSV files with customizable mapping
|
||||
- 🔄 **Batch Processing**: Efficient bulk inserts with configurable batch sizes
|
||||
- 🧪 **Dry Run Mode**: Preview seeding without actual database changes
|
||||
- ✅ **Validation**: Validate CSV files and configurations before seeding
|
||||
- 📝 **Column Mapping**: Map CSV columns to struct fields flexibly
|
||||
- 🗂️ **Multiple Tables**: Support for all master data tables
|
||||
- 🔍 **Progress Tracking**: Detailed logging and error reporting
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Build the Seeder CLI
|
||||
|
||||
```bash
|
||||
make seeder-build
|
||||
```
|
||||
|
||||
### List Available Tables
|
||||
|
||||
```bash
|
||||
make seeder-list
|
||||
# or
|
||||
./bin/seeder list
|
||||
```
|
||||
|
||||
### Seed a Specific Table
|
||||
|
||||
```bash
|
||||
make seeder-seed TABLE=province
|
||||
# or
|
||||
./bin/seeder seed province
|
||||
```
|
||||
|
||||
### Seed All Tables
|
||||
|
||||
```bash
|
||||
make seeder-seed-all
|
||||
# or
|
||||
./bin/seeder seed all
|
||||
```
|
||||
|
||||
### Dry Run (Preview)
|
||||
|
||||
```bash
|
||||
make seeder-dry-run TABLE=ethnic
|
||||
# or
|
||||
./bin/seeder dry-run ethnic
|
||||
```
|
||||
|
||||
### Validate Configuration
|
||||
|
||||
```bash
|
||||
make seeder-validate
|
||||
# or
|
||||
./bin/seeder validate all
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Command Line Options
|
||||
|
||||
```bash
|
||||
# Seed with custom batch size and delete existing data
|
||||
./bin/seeder seed province -batch-size=200 -delete-before
|
||||
|
||||
# Seed with custom CSV file path
|
||||
./bin/seeder seed province -csv-path=/path/to/custom/provinces.csv
|
||||
|
||||
# Seed with custom table name
|
||||
./bin/seeder seed province -table-name=provinces_backup
|
||||
|
||||
# Dry run with specific options
|
||||
./bin/seeder dry-run ethnic -batch-size=10
|
||||
```
|
||||
|
||||
### Makefile Targets
|
||||
|
||||
```bash
|
||||
# Advanced seeding with options
|
||||
make seeder-advanced TABLE=province BATCH_SIZE=200 DELETE_BEFORE=1
|
||||
|
||||
# Build seeder
|
||||
make seeder-build
|
||||
|
||||
# Clean seeder binary
|
||||
make clean-seeder
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Table Configuration
|
||||
|
||||
Each table has a configuration that includes:
|
||||
|
||||
- **TableName**: Database table name
|
||||
- **CSVFile**: Path to CSV file
|
||||
- **ColumnMap**: Mapping between CSV headers and struct fields
|
||||
- **DeleteBefore**: Whether to delete existing data before seeding
|
||||
- **BatchSize**: Number of records to insert per batch
|
||||
|
||||
### Default Registry
|
||||
|
||||
The seeder comes with pre-configured tables:
|
||||
|
||||
| Table | CSV File | Description |
|
||||
|-------|----------|-------------|
|
||||
| province | provinces.csv | Province data |
|
||||
| regency | regencies.csv | Regency/City data |
|
||||
| district | districts.csv | District data |
|
||||
| village | villages.csv | Village data |
|
||||
| ethnic | ethnics.csv | Ethnic groups |
|
||||
| language | languages.csv | Languages |
|
||||
| installation | installations.csv | Hospital installations |
|
||||
| unit | units.csv | Medical units |
|
||||
| specialist | specialists.csv | Medical specialists |
|
||||
| subspecialist | subspecialists.csv | Medical subspecialists |
|
||||
|
||||
### CSV File Format
|
||||
|
||||
CSV files should follow these conventions:
|
||||
|
||||
1. **Header Row**: First row must contain column names
|
||||
2. **Column Names**: Use snake_case or PascalCase
|
||||
3. **Data Types**: Automatic type conversion is supported
|
||||
4. **Empty Values**: Empty cells are handled gracefully
|
||||
|
||||
Example CSV format:
|
||||
```csv
|
||||
Code,Name,Status
|
||||
001,General Medicine,true
|
||||
002,Cardiology,true
|
||||
003,Neurology,true
|
||||
```
|
||||
|
||||
## Extending the Seeder
|
||||
|
||||
### Adding New Tables
|
||||
|
||||
1. **Create Entity Struct** (if not exists):
|
||||
```go
|
||||
type MyEntity struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code string `gorm:"uniqueIndex;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
Status bool `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
```
|
||||
|
||||
2. **Register in DefaultRegistry**:
|
||||
```go
|
||||
registry.Register("mytable", TableConfig{
|
||||
TableName: "MyTable",
|
||||
CSVFile: filepath.Join(basePath, "mytable.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Name": "Name",
|
||||
"Status": "Status",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
```
|
||||
|
||||
3. **Add to GetEntityByTableName**:
|
||||
```go
|
||||
case "mytable":
|
||||
return &MyEntity{}
|
||||
```
|
||||
|
||||
### Custom Column Mapping
|
||||
|
||||
The seeder supports flexible column mapping:
|
||||
|
||||
```go
|
||||
ColumnMap: map[string]string{
|
||||
"CSV_Column_Name": "StructFieldName",
|
||||
"province_code": "ProvinceCode",
|
||||
"full_name": "Name",
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Type Conversion
|
||||
|
||||
For complex types, you can extend the `setFieldValue` function in `MasterSeeder`.
|
||||
|
||||
## Error Handling
|
||||
|
||||
The seeder provides detailed error messages for:
|
||||
|
||||
- **File Not Found**: CSV file doesn't exist
|
||||
- **Invalid CSV Format**: Malformed CSV files
|
||||
- **Database Errors**: Connection issues, constraint violations
|
||||
- **Type Conversion**: Invalid data types
|
||||
- **Validation**: Missing required fields
|
||||
|
||||
## Logging
|
||||
|
||||
The seeder provides comprehensive logging:
|
||||
|
||||
```
|
||||
2024-01-01 12:00:00 [INFO] Starting seed for table: province
|
||||
2024-01-01 12:00:00 [INFO] CSV file: internal/infrastructure/database/csv/provinces.csv
|
||||
2024-01-01 12:00:00 [INFO] Batch size: 50
|
||||
2024-01-01 12:00:00 [INFO] Delete before: true
|
||||
2024-01-01 12:00:00 [INFO] Seeded province: 11 - ACEH
|
||||
2024-01-01 12:00:00 [INFO] Seeding completed. 38 records processed from Province
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Batch Size**: Use larger batch sizes (100-500) for better performance
|
||||
2. **Indexing**: Consider dropping indexes during seeding for large datasets
|
||||
3. **Transactions**: Each batch is processed in a transaction
|
||||
4. **Dry Run**: Always use dry-run first to validate data
|
||||
5. **Delete Strategy**: Use `delete-before` for clean slate seeding
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Database connection (optional, defaults to localhost)
|
||||
export DATABASE_URL="host=localhost user=postgres password=postgres dbname=health port=5432 sslmode=disable"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **CSV File Not Found**
|
||||
```
|
||||
Error: CSV file not found: internal/infrastructure/database/csv/provinces.csv
|
||||
```
|
||||
Solution: Ensure CSV files are in the correct directory
|
||||
|
||||
2. **Database Connection Failed**
|
||||
```
|
||||
Error: Failed to connect to database: ...
|
||||
```
|
||||
Solution: Check DATABASE_URL environment variable
|
||||
|
||||
3. **Invalid Column Mapping**
|
||||
```
|
||||
Error: Failed to set field X: field not found
|
||||
```
|
||||
Solution: Check ColumnMap configuration
|
||||
|
||||
4. **Type Conversion Error**
|
||||
```
|
||||
Error: Failed to parse int from 'ABC': ...
|
||||
```
|
||||
Solution: Check CSV data types match expected format
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Use dry-run mode to preview data without inserting:
|
||||
```bash
|
||||
./bin/seeder dry-run province
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Add tests for new functionality
|
||||
4. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
This seeder is part of the service-general project and follows the same license terms.
|
||||
@@ -0,0 +1,399 @@
|
||||
package seeders
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SeederCLI adalah command-line interface untuk seeder
|
||||
type SeederCLI struct {
|
||||
db *gorm.DB
|
||||
registry *SeederRegistry
|
||||
}
|
||||
|
||||
// NewSeederCLI membuat CLI baru
|
||||
func NewSeederCLI(db *gorm.DB) *SeederCLI {
|
||||
return &SeederCLI{
|
||||
db: db,
|
||||
registry: DefaultRegistry(),
|
||||
}
|
||||
}
|
||||
|
||||
// Run menjalankan CLI seeder
|
||||
func (c *SeederCLI) Run(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return c.showHelp()
|
||||
}
|
||||
|
||||
command := args[0]
|
||||
switch command {
|
||||
case "list":
|
||||
return c.listTables()
|
||||
case "seed":
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("usage: seeder seed <table-name|all> [options]")
|
||||
}
|
||||
return c.seedTable(args[1], args[2:])
|
||||
case "dry-run":
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("usage: seeder dry-run <table-name|all> [options]")
|
||||
}
|
||||
return c.dryRunTable(args[1], args[2:])
|
||||
case "validate":
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("usage: seeder validate <table-name|all>")
|
||||
}
|
||||
return c.validateTable(args[1])
|
||||
default:
|
||||
return fmt.Errorf("unknown command: %s", command)
|
||||
}
|
||||
}
|
||||
|
||||
// showHelp menampilkan bantuan
|
||||
func (c *SeederCLI) showHelp() error {
|
||||
help := `
|
||||
Seeder CLI - Flexible Database Seeder
|
||||
|
||||
Usage:
|
||||
seeder <command> [arguments]
|
||||
|
||||
Commands:
|
||||
list Show all available tables
|
||||
seed <table|all> Seed data for specific table or all tables
|
||||
dry-run <table|all> Preview seeding without actual insertion
|
||||
validate <table|all> Validate CSV files and configuration
|
||||
|
||||
Examples:
|
||||
seeder list
|
||||
seeder seed province
|
||||
seeder seed all
|
||||
seeder dry-run ethnic
|
||||
seeder validate regency
|
||||
|
||||
Options:
|
||||
-batch-size=N Set batch size for insertion (default: from config)
|
||||
-delete-before Delete existing data before seeding
|
||||
-no-delete Don't delete existing data (default)
|
||||
-csv-path=PATH Override CSV file path
|
||||
-table-name=NAME Override table name
|
||||
|
||||
Notes:
|
||||
- CSV files should be in internal/infrastructure/database/csv/
|
||||
- First row of CSV should be header with column names
|
||||
- Column names in CSV will be mapped to struct fields
|
||||
`
|
||||
fmt.Println(help)
|
||||
return nil
|
||||
}
|
||||
|
||||
// listTables menampilkan daftar tabel yang tersedia
|
||||
func (c *SeederCLI) listTables() error {
|
||||
tables := c.registry.List()
|
||||
|
||||
fmt.Println("Available tables for seeding:")
|
||||
fmt.Println(strings.Repeat("-", 50))
|
||||
|
||||
for _, table := range tables {
|
||||
config, _ := c.registry.Get(table)
|
||||
fmt.Printf("%-20s %s\n", table, config.CSVFile)
|
||||
}
|
||||
|
||||
fmt.Printf("\nTotal: %d tables\n", len(tables))
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedTable melakukan seeding untuk tabel tertentu
|
||||
func (c *SeederCLI) seedTable(tableName string, options []string) error {
|
||||
if tableName == "all" {
|
||||
return c.seedAllTables(options)
|
||||
}
|
||||
|
||||
config, exists := c.registry.Get(tableName)
|
||||
if !exists {
|
||||
return fmt.Errorf("table '%s' not found in registry", tableName)
|
||||
}
|
||||
|
||||
// Parse options
|
||||
opts := c.parseOptions(options)
|
||||
|
||||
// Override config dengan options
|
||||
if opts.CSVPath != "" {
|
||||
config.CSVFile = opts.CSVPath
|
||||
}
|
||||
if opts.TableName != "" {
|
||||
config.TableName = opts.TableName
|
||||
}
|
||||
if opts.BatchSize > 0 {
|
||||
config.BatchSize = opts.BatchSize
|
||||
}
|
||||
if opts.DeleteBefore {
|
||||
config.DeleteBefore = true
|
||||
}
|
||||
|
||||
// Validasi config
|
||||
if err := config.Validate(); err != nil {
|
||||
return fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Get entity
|
||||
entity := GetEntityByTableName(config.TableName)
|
||||
if entity == nil {
|
||||
return fmt.Errorf("entity not found for table '%s'", config.TableName)
|
||||
}
|
||||
|
||||
// Create seeder
|
||||
seederConfig := SeederConfig{
|
||||
CSVPath: config.CSVFile,
|
||||
TableName: config.TableName,
|
||||
ColumnMap: config.ColumnMap,
|
||||
SkipHeader: true,
|
||||
BatchSize: config.BatchSize,
|
||||
DeleteBefore: config.DeleteBefore,
|
||||
DryRun: false,
|
||||
}
|
||||
|
||||
seeder := NewMasterSeeder(c.db, seederConfig)
|
||||
|
||||
log.Printf("Starting seed for table: %s", config.TableName)
|
||||
log.Printf("CSV file: %s", config.CSVFile)
|
||||
log.Printf("Batch size: %d", config.BatchSize)
|
||||
log.Printf("Delete before: %v", config.DeleteBefore)
|
||||
|
||||
if err := seeder.SeedFromCSV(entity); err != nil {
|
||||
return fmt.Errorf("seeding failed: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully seeded table: %s", config.TableName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// dryRunTable melakukan dry-run untuk tabel tertentu
|
||||
func (c *SeederCLI) dryRunTable(tableName string, options []string) error {
|
||||
if tableName == "all" {
|
||||
return c.dryRunAllTables(options)
|
||||
}
|
||||
|
||||
config, exists := c.registry.Get(tableName)
|
||||
if !exists {
|
||||
return fmt.Errorf("table '%s' not found in registry", tableName)
|
||||
}
|
||||
|
||||
// Parse options
|
||||
opts := c.parseOptions(options)
|
||||
|
||||
// Override config dengan options
|
||||
if opts.CSVPath != "" {
|
||||
config.CSVFile = opts.CSVPath
|
||||
}
|
||||
if opts.TableName != "" {
|
||||
config.TableName = opts.TableName
|
||||
}
|
||||
|
||||
// Validasi config
|
||||
if err := config.Validate(); err != nil {
|
||||
return fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Get entity
|
||||
entity := GetEntityByTableName(config.TableName)
|
||||
if entity == nil {
|
||||
return fmt.Errorf("entity not found for table '%s'", config.TableName)
|
||||
}
|
||||
|
||||
// Create seeder dengan dry-run mode
|
||||
seederConfig := SeederConfig{
|
||||
CSVPath: config.CSVFile,
|
||||
TableName: config.TableName,
|
||||
ColumnMap: config.ColumnMap,
|
||||
SkipHeader: true,
|
||||
BatchSize: 10, // Smaller batch for dry-run
|
||||
DeleteBefore: false,
|
||||
DryRun: true,
|
||||
}
|
||||
|
||||
seeder := NewMasterSeeder(c.db, seederConfig)
|
||||
|
||||
log.Printf("Starting dry-run for table: %s", config.TableName)
|
||||
log.Printf("CSV file: %s", config.CSVFile)
|
||||
|
||||
if err := seeder.SeedFromCSV(entity); err != nil {
|
||||
return fmt.Errorf("dry-run failed: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Dry-run completed for table: %s", config.TableName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateTable memvalidasi konfigurasi dan CSV file
|
||||
func (c *SeederCLI) validateTable(tableName string) error {
|
||||
if tableName == "all" {
|
||||
return c.validateAllTables()
|
||||
}
|
||||
|
||||
config, exists := c.registry.Get(tableName)
|
||||
if !exists {
|
||||
return fmt.Errorf("table '%s' not found in registry", tableName)
|
||||
}
|
||||
|
||||
// Validasi config
|
||||
if err := config.Validate(); err != nil {
|
||||
return fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Get entity
|
||||
entity := GetEntityByTableName(config.TableName)
|
||||
if entity == nil {
|
||||
return fmt.Errorf("entity not found for table '%s'", config.TableName)
|
||||
}
|
||||
|
||||
// Baca CSV file dan validasi
|
||||
file, err := os.Open(config.CSVFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open CSV file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := csv.NewReader(file)
|
||||
headers, err := reader.Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read CSV header: %w", err)
|
||||
}
|
||||
|
||||
// Hitung jumlah baris
|
||||
rowCount := 0
|
||||
for {
|
||||
_, err := reader.Read()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
rowCount++
|
||||
}
|
||||
|
||||
fmt.Printf("Validation passed for table: %s\n", tableName)
|
||||
fmt.Printf("CSV file: %s\n", config.CSVFile)
|
||||
fmt.Printf("Headers: %v\n", headers)
|
||||
fmt.Printf("Expected rows: %d\n", rowCount)
|
||||
fmt.Printf("Column mapping: %v\n", config.ColumnMap)
|
||||
fmt.Printf("Entity type: %T\n", entity)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedAllTables melakukan seeding untuk semua tabel
|
||||
func (c *SeederCLI) seedAllTables(options []string) error {
|
||||
tables := c.registry.List()
|
||||
|
||||
log.Printf("Starting seed for all tables (%d tables)", len(tables))
|
||||
|
||||
for _, table := range tables {
|
||||
if err := c.seedTable(table, options); err != nil {
|
||||
log.Printf("Error seeding table %s: %v", table, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Completed seeding all tables")
|
||||
return nil
|
||||
}
|
||||
|
||||
// dryRunAllTables melakukan dry-run untuk semua tabel
|
||||
func (c *SeederCLI) dryRunAllTables(options []string) error {
|
||||
tables := c.registry.List()
|
||||
|
||||
log.Printf("Starting dry-run for all tables (%d tables)", len(tables))
|
||||
|
||||
for _, table := range tables {
|
||||
if err := c.dryRunTable(table, options); err != nil {
|
||||
log.Printf("Error in dry-run for table %s: %v", table, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Completed dry-run for all tables")
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAllTables memvalidasi semua tabel
|
||||
func (c *SeederCLI) validateAllTables() error {
|
||||
tables := c.registry.List()
|
||||
|
||||
fmt.Printf("Validating all tables (%d tables):\n", len(tables))
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
for _, table := range tables {
|
||||
fmt.Printf("\n[%s]\n", table)
|
||||
if err := c.validateTable(table); err != nil {
|
||||
fmt.Printf("❌ Validation failed: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("✅ Validation passed\n")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SeederOptions berisi parsed options
|
||||
type SeederOptions struct {
|
||||
CSVPath string
|
||||
TableName string
|
||||
BatchSize int
|
||||
DeleteBefore bool
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
// parseOptions mem-parsing command line options
|
||||
func (c *SeederCLI) parseOptions(options []string) SeederOptions {
|
||||
var opts SeederOptions
|
||||
|
||||
for _, option := range options {
|
||||
switch {
|
||||
case strings.HasPrefix(option, "-batch-size="):
|
||||
fmt.Sscanf(option, "-batch-size=%d", &opts.BatchSize)
|
||||
case option == "-delete-before":
|
||||
opts.DeleteBefore = true
|
||||
case strings.HasPrefix(option, "-csv-path="):
|
||||
opts.CSVPath = strings.TrimPrefix(option, "-csv-path=")
|
||||
case strings.HasPrefix(option, "-table-name="):
|
||||
opts.TableName = strings.TrimPrefix(option, "-table-name=")
|
||||
}
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// MainCLI adalah entry point utama untuk CLI
|
||||
func MainCLI() {
|
||||
// Setup database connection
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
cli := NewSeederCLI(db)
|
||||
|
||||
if err := cli.Run(os.Args[1:]); err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// setupDatabase setup koneksi database
|
||||
func setupDatabase() (*gorm.DB, error) {
|
||||
// Bisa diambil dari config atau environment variable
|
||||
dsn := os.Getenv("DATABASE_URL")
|
||||
if dsn == "" {
|
||||
dsn = "host=localhost user=postgres password=postgres dbname=health port=5432 sslmode=disable"
|
||||
}
|
||||
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
package seeders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TableConfig berisi konfigurasi untuk setiap tabel
|
||||
type TableConfig struct {
|
||||
TableName string
|
||||
CSVFile string
|
||||
Entity interface{}
|
||||
ColumnMap map[string]string
|
||||
DeleteBefore bool
|
||||
BatchSize int
|
||||
}
|
||||
|
||||
// SeederRegistry berisi registry untuk semua tabel yang bisa di-seed
|
||||
type SeederRegistry struct {
|
||||
tables map[string]TableConfig
|
||||
}
|
||||
|
||||
// NewSeederRegistry membuat registry baru
|
||||
func NewSeederRegistry() *SeederRegistry {
|
||||
return &SeederRegistry{
|
||||
tables: make(map[string]TableConfig),
|
||||
}
|
||||
}
|
||||
|
||||
// Register mendaftarkan tabel ke registry
|
||||
func (r *SeederRegistry) Register(name string, config TableConfig) {
|
||||
r.tables[name] = config
|
||||
}
|
||||
|
||||
// Get mendapatkan konfigurasi tabel berdasarkan nama
|
||||
func (r *SeederRegistry) Get(name string) (TableConfig, bool) {
|
||||
config, exists := r.tables[name]
|
||||
return config, exists
|
||||
}
|
||||
|
||||
// List mengembalikan daftar nama tabel yang terdaftar
|
||||
func (r *SeederRegistry) List() []string {
|
||||
names := make([]string, 0, len(r.tables))
|
||||
for name := range r.tables {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// DefaultRegistry berisi konfigurasi default untuk semua tabel
|
||||
func DefaultRegistry() *SeederRegistry {
|
||||
registry := NewSeederRegistry()
|
||||
|
||||
basePath := "internal/infrastructure/database/csv"
|
||||
|
||||
// Register Province
|
||||
registry.Register("province", TableConfig{
|
||||
TableName: "Province",
|
||||
CSVFile: filepath.Join(basePath, "provinces.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Name": "Name",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
|
||||
// Register Regency
|
||||
registry.Register("regency", TableConfig{
|
||||
TableName: "Regency",
|
||||
CSVFile: filepath.Join(basePath, "regencies.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Province_Code": "ProvinceCode",
|
||||
"Name": "Name",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 100,
|
||||
})
|
||||
|
||||
// Register District
|
||||
registry.Register("district", TableConfig{
|
||||
TableName: "District",
|
||||
CSVFile: filepath.Join(basePath, "districts.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Regency_Code": "RegencyCode",
|
||||
"Name": "Name",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 200,
|
||||
})
|
||||
|
||||
// Register Village
|
||||
registry.Register("village", TableConfig{
|
||||
TableName: "Village",
|
||||
CSVFile: filepath.Join(basePath, "villages.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"District_Code": "DistrictCode",
|
||||
"Name": "Name",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 500,
|
||||
})
|
||||
|
||||
// Register Ethnic
|
||||
registry.Register("ethnic", TableConfig{
|
||||
TableName: "Ethnic",
|
||||
CSVFile: filepath.Join(basePath, "ethnics.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Name": "Name",
|
||||
"Status": "Status",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
|
||||
// Register Language
|
||||
registry.Register("language", TableConfig{
|
||||
TableName: "Language",
|
||||
CSVFile: filepath.Join(basePath, "languages.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Name": "Name",
|
||||
"Status": "Status",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
|
||||
// Register Installation
|
||||
registry.Register("installation", TableConfig{
|
||||
TableName: "Installation",
|
||||
CSVFile: filepath.Join(basePath, "installations.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Name": "Name",
|
||||
"Status": "Status",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
|
||||
// Register Unit
|
||||
registry.Register("unit", TableConfig{
|
||||
TableName: "Unit",
|
||||
CSVFile: filepath.Join(basePath, "units.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Name": "Name",
|
||||
"Status": "Status",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
|
||||
// Register Specialist
|
||||
registry.Register("specialist", TableConfig{
|
||||
TableName: "Specialist",
|
||||
CSVFile: filepath.Join(basePath, "specialists.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Name": "Name",
|
||||
"Status": "Status",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
|
||||
// Register SubSpecialist
|
||||
registry.Register("subspecialist", TableConfig{
|
||||
TableName: "SubSpecialist",
|
||||
CSVFile: filepath.Join(basePath, "subspecialists.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Specialist_Code": "SpecialistCode",
|
||||
"Name": "Name",
|
||||
"Status": "Status",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
|
||||
// Register RolPages (Menu Structure)
|
||||
registry.Register("rol_pages", TableConfig{
|
||||
TableName: "rol_pages",
|
||||
Entity: &RolPages{},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 20,
|
||||
})
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
// ValidateConfig memvalidasi konfigurasi
|
||||
func (c *TableConfig) Validate() error {
|
||||
if c.TableName == "" {
|
||||
return fmt.Errorf("table name is required")
|
||||
}
|
||||
if c.CSVFile == "" {
|
||||
return fmt.Errorf("CSV file path is required")
|
||||
}
|
||||
|
||||
// Cek apakah file CSV ada
|
||||
if _, err := os.Stat(c.CSVFile); os.IsNotExist(err) {
|
||||
return fmt.Errorf("CSV file not found: %s", c.CSVFile)
|
||||
}
|
||||
|
||||
if c.BatchSize <= 0 {
|
||||
c.BatchSize = 100
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEntityByTableName mendapatkan entity berdasarkan nama tabel
|
||||
func GetEntityByTableName(tableName string) interface{} {
|
||||
switch strings.ToLower(tableName) {
|
||||
case "province":
|
||||
return &Province{}
|
||||
case "regency":
|
||||
return &Regency{}
|
||||
case "district":
|
||||
return &District{}
|
||||
case "village":
|
||||
return &Village{}
|
||||
// case "ethnic":
|
||||
// return &Ethnic{}
|
||||
case "language":
|
||||
return &Language{}
|
||||
case "installation":
|
||||
return &Installation{}
|
||||
case "unit":
|
||||
return &Unit{}
|
||||
case "specialist":
|
||||
return &Specialist{}
|
||||
case "subspecialist":
|
||||
return &SubSpecialist{}
|
||||
case "rol_pages":
|
||||
return &RolPages{}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Entity definitions
|
||||
|
||||
type Province struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code string `gorm:"uniqueIndex;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Regency struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code string `gorm:"uniqueIndex;not null"`
|
||||
ProvinceCode string `gorm:"index;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type District struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code string `gorm:"uniqueIndex;not null"`
|
||||
RegencyCode string `gorm:"index;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Village struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code string `gorm:"uniqueIndex;not null"`
|
||||
DistrictCode string `gorm:"index;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Language struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code interface{} `gorm:"uniqueIndex;not null"`
|
||||
Name interface{} `gorm:"not null"`
|
||||
Status interface{} `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Installation struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code interface{} `gorm:"uniqueIndex;not null"`
|
||||
Name interface{} `gorm:"not null"`
|
||||
Status interface{} `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Unit struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code interface{} `gorm:"uniqueIndex;not null"`
|
||||
Name interface{} `gorm:"not null"`
|
||||
Status interface{} `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Specialist struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code interface{} `gorm:"uniqueIndex;not null"`
|
||||
Name interface{} `gorm:"not null"`
|
||||
Status interface{} `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type SubSpecialist struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code interface{} `gorm:"uniqueIndex;not null"`
|
||||
SpecialistCode interface{} `gorm:"index;not null"`
|
||||
Name interface{} `gorm:"not null"`
|
||||
Status interface{} `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
// RolPages struct untuk menu hierarchy
|
||||
type RolPages struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Name string `gorm:"not null;size:20"`
|
||||
Icon string `gorm:"size:100"`
|
||||
URL string `gorm:"not null"`
|
||||
Level int16 `gorm:"not null;default:0"`
|
||||
Sort int16 `gorm:"not null;default:0"`
|
||||
Parent *int64 `gorm:"index"`
|
||||
Active bool `gorm:"not null;default:true"`
|
||||
CreatedAt time.Time `gorm:"not null;default:CURRENT_TIMESTAMP"`
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time `gorm:"index"`
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
package seeders
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SeederConfig berisi konfigurasi untuk seeding
|
||||
type SeederConfig struct {
|
||||
CSVPath string
|
||||
TableName string
|
||||
ColumnMap map[string]string // mapping CSV header ke field struct
|
||||
SkipHeader bool
|
||||
BatchSize int
|
||||
DryRun bool
|
||||
DeleteBefore bool
|
||||
}
|
||||
|
||||
// MasterSeeder adalah struct utama untuk seeding fleksibel
|
||||
type MasterSeeder struct {
|
||||
db *gorm.DB
|
||||
config SeederConfig
|
||||
}
|
||||
|
||||
// NewMasterSeeder membuat instance seeder baru
|
||||
func NewMasterSeeder(db *gorm.DB, config SeederConfig) *MasterSeeder {
|
||||
if config.BatchSize == 0 {
|
||||
config.BatchSize = 100
|
||||
}
|
||||
return &MasterSeeder{
|
||||
db: db,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// SeedFromCSV melakukan seeding dari file CSV
|
||||
func (s *MasterSeeder) SeedFromCSV(model interface{}) error {
|
||||
if s.config.CSVPath == "" {
|
||||
return fmt.Errorf("CSV path is required")
|
||||
}
|
||||
|
||||
file, err := os.Open(s.config.CSVPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open CSV file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := csv.NewReader(file)
|
||||
reader.Comma = ','
|
||||
|
||||
// Baca header
|
||||
headers, err := reader.Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read CSV header: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("CSV Headers: %v", headers)
|
||||
|
||||
// Hapus data lama jika diminta
|
||||
if s.config.DeleteBefore {
|
||||
if err := s.db.Where("1=1").Delete(model).Error; err != nil {
|
||||
return fmt.Errorf("failed to delete existing data: %w", err)
|
||||
}
|
||||
log.Printf("Deleted existing data from %s", s.config.TableName)
|
||||
}
|
||||
|
||||
count := 0
|
||||
batch := []interface{}{}
|
||||
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err != nil {
|
||||
if err.Error() == "EOF" {
|
||||
break
|
||||
}
|
||||
log.Printf("Error reading row %d: %v", count+1, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(record) < len(headers) {
|
||||
log.Printf("Skipping incomplete row %d", count+1)
|
||||
continue
|
||||
}
|
||||
|
||||
// Buat instance model baru
|
||||
instance := reflect.New(reflect.TypeOf(model).Elem()).Interface()
|
||||
|
||||
// Mapping data dari CSV ke struct
|
||||
if err := s.mapCSVToStruct(headers, record, instance); err != nil {
|
||||
log.Printf("Error mapping row %d: %v", count+1, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Set audit fields
|
||||
s.setAuditFields(instance)
|
||||
|
||||
if s.config.DryRun {
|
||||
log.Printf("Dry run - Would insert: %+v", instance)
|
||||
} else {
|
||||
batch = append(batch, instance)
|
||||
|
||||
// Insert batch jika sudah penuh
|
||||
if len(batch) >= s.config.BatchSize {
|
||||
if err := s.insertBatch(batch); err != nil {
|
||||
return fmt.Errorf("failed to insert batch: %w", err)
|
||||
}
|
||||
count += len(batch)
|
||||
batch = []interface{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert sisa batch
|
||||
if len(batch) > 0 && !s.config.DryRun {
|
||||
if err := s.insertBatch(batch); err != nil {
|
||||
return fmt.Errorf("failed to insert final batch: %w", err)
|
||||
}
|
||||
count += len(batch)
|
||||
}
|
||||
|
||||
log.Printf("Seeding completed. %d records processed from %s", count, s.config.TableName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// mapCSVToStruct mapping data CSV ke struct berdasarkan column map atau reflection
|
||||
func (s *MasterSeeder) mapCSVToStruct(headers []string, record []string, instance interface{}) error {
|
||||
v := reflect.ValueOf(instance).Elem()
|
||||
t := v.Type()
|
||||
|
||||
for i, header := range headers {
|
||||
if i >= len(record) {
|
||||
continue
|
||||
}
|
||||
|
||||
value := strings.TrimSpace(record[i])
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Cari field berdasarkan column map atau nama header
|
||||
fieldName := s.getFieldName(header)
|
||||
|
||||
field, found := t.FieldByName(fieldName)
|
||||
if !found {
|
||||
// Coba dengan nama field yang berbeda case
|
||||
for j := 0; j < t.NumField(); j++ {
|
||||
f := t.Field(j)
|
||||
if strings.EqualFold(f.Name, fieldName) {
|
||||
field = f
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
if err := s.setFieldValue(v.FieldByName(field.Name), value); err != nil {
|
||||
return fmt.Errorf("failed to set field %s: %w", field.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getFieldName mendapatkan nama field dari header CSV
|
||||
func (s *MasterSeeder) getFieldName(header string) string {
|
||||
// Cek di column map dulu
|
||||
if fieldName, exists := s.config.ColumnMap[header]; exists {
|
||||
return fieldName
|
||||
}
|
||||
|
||||
// Konversi header ke PascalCase
|
||||
words := strings.Split(header, "_")
|
||||
for i, word := range words {
|
||||
words[i] = strings.Title(strings.ToLower(word))
|
||||
}
|
||||
return strings.Join(words, "")
|
||||
}
|
||||
|
||||
// setFieldValue mengisi value ke field struct dengan type conversion
|
||||
func (s *MasterSeeder) setFieldValue(field reflect.Value, value string) error {
|
||||
if !field.CanSet() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
field.SetString(value)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
// Handle bigserial (int64)
|
||||
if value == "" {
|
||||
field.SetInt(0)
|
||||
} else {
|
||||
intVal, err := s.parseInt(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.SetInt(intVal)
|
||||
}
|
||||
case reflect.Bool:
|
||||
boolVal := strings.ToLower(value) == "true" || value == "1"
|
||||
field.SetBool(boolVal)
|
||||
case reflect.Interface:
|
||||
// Untuk field interface{} (seperti Code, Name, Status pada Ethnic)
|
||||
field.Set(reflect.ValueOf(value))
|
||||
default:
|
||||
// Handle pointer types
|
||||
if field.Kind() == reflect.Ptr {
|
||||
if value == "" {
|
||||
field.Set(reflect.Zero(field.Type()))
|
||||
} else {
|
||||
// Create new instance of the pointed type
|
||||
newValue := reflect.New(field.Type().Elem())
|
||||
if err := s.setFieldValue(newValue.Elem(), value); err != nil {
|
||||
return err
|
||||
}
|
||||
field.Set(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseInt parsing string ke int64
|
||||
func (s *MasterSeeder) parseInt(value string) (int64, error) {
|
||||
var intVal int64
|
||||
_, err := fmt.Sscanf(value, "%d", &intVal)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse int from '%s': %w", value, err)
|
||||
}
|
||||
return intVal, nil
|
||||
}
|
||||
|
||||
// setAuditFields mengisi field audit (CreatedAt, UpdatedAt, DeletedAt)
|
||||
func (s *MasterSeeder) setAuditFields(instance interface{}) {
|
||||
v := reflect.ValueOf(instance).Elem()
|
||||
now := time.Now()
|
||||
|
||||
// Set CreatedAt jika field ada
|
||||
if field := v.FieldByName("CreatedAt"); field.IsValid() && field.CanSet() {
|
||||
if field.Kind() == reflect.Struct {
|
||||
// Handle time.Time
|
||||
field.Set(reflect.ValueOf(now))
|
||||
} else if field.Kind() == reflect.Interface {
|
||||
field.Set(reflect.ValueOf(now))
|
||||
}
|
||||
}
|
||||
|
||||
// Set UpdatedAt jika field ada (pointer)
|
||||
if field := v.FieldByName("UpdatedAt"); field.IsValid() && field.CanSet() {
|
||||
if field.Kind() == reflect.Ptr {
|
||||
field.Set(reflect.ValueOf(&now))
|
||||
}
|
||||
}
|
||||
|
||||
// Set DeletedAt ke nil jika field ada
|
||||
if field := v.FieldByName("DeletedAt"); field.IsValid() && field.CanSet() {
|
||||
if field.Kind() == reflect.Ptr {
|
||||
field.Set(reflect.Zero(field.Type()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// insertBatch insert batch data ke database
|
||||
func (s *MasterSeeder) insertBatch(batch []interface{}) error {
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
for _, item := range batch {
|
||||
// Gunakan FirstOrCreate untuk menghindari duplicate
|
||||
v := reflect.ValueOf(item).Elem()
|
||||
|
||||
// Buat kondisi where berdasarkan field utama (Code atau ID)
|
||||
var condition interface{}
|
||||
if codeField := v.FieldByName("Code"); codeField.IsValid() {
|
||||
condition = map[string]interface{}{"Code": codeField.Interface()}
|
||||
} else if idField := v.FieldByName("Id"); idField.IsValid() {
|
||||
condition = map[string]interface{}{"Id": idField.Interface()}
|
||||
}
|
||||
|
||||
if condition != nil {
|
||||
result := tx.Where(condition).FirstOrCreate(item)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
} else {
|
||||
// Jika tidak ada kondisi, insert langsung
|
||||
if err := tx.Create(item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions untuk seeding spesifik
|
||||
|
||||
// SeedProvinces seed data provinsi dari CSV
|
||||
func SeedProvinces(db *gorm.DB, csvPath string) error {
|
||||
config := SeederConfig{
|
||||
CSVPath: csvPath,
|
||||
TableName: "Province",
|
||||
ColumnMap: map[string]string{"Code": "Code", "Name": "Name"},
|
||||
SkipHeader: true,
|
||||
BatchSize: 50,
|
||||
DeleteBefore: true,
|
||||
}
|
||||
|
||||
seeder := NewMasterSeeder(db, config)
|
||||
|
||||
// Definisikan struct untuk Province
|
||||
type Province struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code string `gorm:"uniqueIndex;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
return seeder.SeedFromCSV(&Province{})
|
||||
}
|
||||
|
||||
// SeedRegencies seed data kabupaten dari CSV
|
||||
func SeedRegencies(db *gorm.DB, csvPath string) error {
|
||||
config := SeederConfig{
|
||||
CSVPath: csvPath,
|
||||
TableName: "Regency",
|
||||
ColumnMap: map[string]string{"Code": "Code", "Province_Code": "ProvinceCode", "Name": "Name"},
|
||||
SkipHeader: true,
|
||||
BatchSize: 100,
|
||||
DeleteBefore: true,
|
||||
}
|
||||
|
||||
seeder := NewMasterSeeder(db, config)
|
||||
|
||||
type Regency struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code string `gorm:"uniqueIndex;not null"`
|
||||
ProvinceCode string `gorm:"index;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
return seeder.SeedFromCSV(&Regency{})
|
||||
}
|
||||
|
||||
// SeedEthnics seed data suku bangsa dari CSV
|
||||
// func SeedEthnics(db *gorm.DB, csvPath string) error {
|
||||
// config := SeederConfig{
|
||||
// CSVPath: csvPath,
|
||||
// TableName: "Ethnic",
|
||||
// ColumnMap: map[string]string{"Code": "Code", "Name": "Name", "Status": "Status"},
|
||||
// SkipHeader: true,
|
||||
// BatchSize: 50,
|
||||
// DeleteBefore: true,
|
||||
// }
|
||||
|
||||
// seeder := NewMasterSeeder(db, config)
|
||||
|
||||
// // Gunakan struct Ethnic yang sudah ada
|
||||
// return seeder.SeedFromCSV(&Ethnic{})
|
||||
// }
|
||||
@@ -0,0 +1,50 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE SEQUENCE IF NOT EXISTS rol_pages_id_seq;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rol_pages (
|
||||
id int4 NOT NULL DEFAULT nextval('rol_pages_id_seq'),
|
||||
"name" varchar(20) NOT NULL,
|
||||
icon varchar(100) NULL,
|
||||
url text NOT NULL,
|
||||
"level" int2 NOT NULL DEFAULT 0,
|
||||
sort int2 NOT NULL DEFAULT 0,
|
||||
parent int4 NULL,
|
||||
active bool NOT NULL DEFAULT true,
|
||||
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamp NULL,
|
||||
deleted_at timestamp NULL,
|
||||
CONSTRAINT rol_pages_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT rol_pages_parent_fkey FOREIGN KEY (parent) REFERENCES rol_pages(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Index untuk performa query
|
||||
CREATE INDEX idx_rol_pages_parent ON rol_pages(parent) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_rol_pages_level ON rol_pages(level) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_rol_pages_active ON rol_pages(active) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_rol_pages_sort ON rol_pages(sort) WHERE deleted_at IS NULL;
|
||||
|
||||
-- Trigger untuk update updated_at
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$ language 'plpgsql';
|
||||
|
||||
CREATE TRIGGER update_rol_pages_updated_at BEFORE UPDATE ON rol_pages
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TRIGGER IF EXISTS update_rol_pages_updated_at ON rol_pages;
|
||||
DROP FUNCTION IF EXISTS update_updated_at_column();
|
||||
DROP INDEX IF EXISTS idx_rol_pages_parent;
|
||||
DROP INDEX IF EXISTS idx_rol_pages_level;
|
||||
DROP INDEX IF EXISTS idx_rol_pages_active;
|
||||
DROP INDEX IF EXISTS idx_rol_pages_sort;
|
||||
DROP TABLE IF EXISTS rol_pages;
|
||||
DROP SEQUENCE IF EXISTS rol_pages_id_seq;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,17 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE rol_component (
|
||||
id int4 NOT NULL,
|
||||
"name" varchar(100) NULL,
|
||||
description text NULL,
|
||||
directory text NOT NULL,
|
||||
active bool NOT NULL,
|
||||
fk_rol_pages_id int4 NOT NULL,
|
||||
sort int2 NULL
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
SELECT 'down SQL query';
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,20 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE rol_permission (
|
||||
id int4 NOT NULL,
|
||||
"create" bool NULL,
|
||||
"read" bool NULL,
|
||||
"update" bool NULL,
|
||||
"disable" bool NULL,
|
||||
"delete" bool NULL,
|
||||
active bool NULL,
|
||||
fk_rol_pages_id int4 NULL,
|
||||
role_keycloak _text NULL,
|
||||
group_keycloak _text NULL
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
SELECT 'down SQL query';
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,16 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE role_access.rol_master (
|
||||
id int4 NOT NULL,
|
||||
"name" varchar(100) NULL,
|
||||
active bool NULL,
|
||||
created_at timestamp NULL,
|
||||
updated_at timestamp NULL,
|
||||
CONSTRAINT rol_master_role_pk PRIMARY KEY (id)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
SELECT 'down SQL query';
|
||||
-- +goose StatementEnd
|
||||
Reference in New Issue
Block a user