diff --git a/.changeset/sunny-tools-tell.md b/.changeset/sunny-tools-tell.md new file mode 100644 index 0000000..75070ca --- /dev/null +++ b/.changeset/sunny-tools-tell.md @@ -0,0 +1,5 @@ +--- +'@ssecd/jkn': patch +--- + +Improve path handling diff --git a/src/antrean-fktp.ts b/src/antrean-fktp.ts index 96a818c..7892b6e 100644 --- a/src/antrean-fktp.ts +++ b/src/antrean-fktp.ts @@ -43,7 +43,7 @@ export class AntreanFKTP extends BaseApi<'pcare'> { }[]; }>({ name: this.name + 'Referensi Dokter', - path: `/ref/dokter/kodepoli/${encodeURIComponent(params.kodePoli)}/tanggal/${params.tanggal}`, + path: ['/ref/dokter/kodepoli/:kodePoli/tanggal/:tanggal', params], method: 'GET' }); } diff --git a/src/antrean.ts b/src/antrean.ts index 0eee26e..0407723 100644 --- a/src/antrean.ts +++ b/src/antrean.ts @@ -63,7 +63,7 @@ export class Antrean extends BaseApi<'antrean'> { }[] >({ name: 'Referensi Jadwal Dokter', - path: `/jadwaldokter/kodepoli/${encodeURIComponent(params.poli)}/tanggal/${params.tanggal}`, + path: ['/jadwaldokter/kodepoli/:poli/tanggal/:tanggal', params], method: 'GET' }); } @@ -103,7 +103,7 @@ export class Antrean extends BaseApi<'antrean'> { daftarfp: number; }>({ name: 'Referensi Pasien Fingerprint', - path: `/ref/pasien/fp/identitas/${encodeURIComponent(params.jenis)}/noidentitas/${params.nomor}`, + path: ['/ref/pasien/fp/identitas/:jenis/noidentitas/:nomor', params], method: 'GET' }); } @@ -453,7 +453,7 @@ export class Antrean extends BaseApi<'antrean'> { async perKodeBooking(kodeBooking: string) { return this.send({ name: 'Antrean Per-KodeBooking', - path: `/antrean/pendaftaran/kodebooking/${encodeURIComponent(kodeBooking)}`, + path: ['/antrean/pendaftaran/kodebooking/:kodeBooking', { kodeBooking }], method: 'GET' }); } @@ -487,7 +487,10 @@ export class Antrean extends BaseApi<'antrean'> { }) { return this.send({ name: 'Antrean Belum Dilayani Per-(Poli, Dokter, Hari, dan Jam Praktik)', - path: `/antrean/pendaftaran/kodepoli/${encodeURIComponent(params.poli)}/kodedokter/${encodeURIComponent(params.dokter)}/hari/${params.hari}/jampraktek/${params.jam}`, + path: [ + '/antrean/pendaftaran/kodepoli/:poli/kodedokter/:dokter/hari/:hari/jampraktek/:jam', + params + ], method: 'GET' }); } diff --git a/src/aplicares.ts b/src/aplicares.ts index 1253154..b7e8f1a 100644 --- a/src/aplicares.ts +++ b/src/aplicares.ts @@ -31,11 +31,14 @@ export class Aplicares extends BaseApi<'aplicares'> { * tempat tidur untuk pasien laki-laki, perempuan, dan laki–laki atau * perempuan. */ - async update(data: AplicaresBedData) { - const { ppkCode } = await this.requiredConfig('ppkCode'); + async update(data: AplicaresBedData, kodePpk?: string) { + if (!kodePpk) { + const config = await this.requiredConfig('ppkCode'); + kodePpk = config.ppkCode; + } return this.send({ name: 'Update Ketersediaan Tempat Tidur', - path: `/rest/bed/update/${encodeURIComponent(ppkCode)}`, + path: ['/rest/bed/update/:kodePpk', { kodePpk }], method: 'POST', skipContentTypeHack: true, headers: { 'Content-Type': 'application/json' }, @@ -51,11 +54,14 @@ export class Aplicares extends BaseApi<'aplicares'> { * tempat tidur untuk pasien laki-laki, perempuan, dan laki–laki atau * perempuan. */ - async create(data: AplicaresBedData) { - const { ppkCode } = await this.requiredConfig('ppkCode'); + async create(data: AplicaresBedData, kodePpk?: string) { + if (!kodePpk) { + const config = await this.requiredConfig('ppkCode'); + kodePpk = config.ppkCode; + } return this.send({ name: 'Ruangan Baru', - path: `/rest/bed/create/${encodeURIComponent(ppkCode)}`, + path: ['/rest/bed/create/:kodePpk', { kodePpk }], method: 'POST', skipContentTypeHack: true, headers: { 'Content-Type': 'application/json' }, @@ -76,8 +82,13 @@ export class Aplicares extends BaseApi<'aplicares'> { /** paging limit */ limit: number; + + kodePpk?: string; }) { - const { ppkCode } = await this.requiredConfig('ppkCode'); + if (!params.kodePpk) { + const config = await this.requiredConfig('ppkCode'); + params.kodePpk = config.ppkCode; + } return this.send< { list: (AplicaresBedData & { @@ -89,7 +100,7 @@ export class Aplicares extends BaseApi<'aplicares'> { { totalitems: number } >({ name: 'Ketersediaan Kamar Faskes', - path: `/rest/bed/read/${encodeURIComponent(ppkCode)}/${params.start}/${params.limit}`, + path: ['/rest/bed/read/:kodePpk/:start/:limit', params], method: 'GET', skipDecrypt: true }); @@ -98,17 +109,23 @@ export class Aplicares extends BaseApi<'aplicares'> { /** * Hapus Ruangan */ - async delete(data: { - /** kode kelas ruang rawat sesuai dengan mapping BPJS Kesehatan */ - kodekelas: string; - - /** kode ruangan faskes */ - koderuang: string; - }) { - const { ppkCode } = await this.requiredConfig('ppkCode'); + async delete( + data: { + /** kode kelas ruang rawat sesuai dengan mapping BPJS Kesehatan */ + kodekelas: string; + + /** kode ruangan faskes */ + koderuang: string; + }, + kodePpk?: string + ) { + if (!kodePpk) { + const config = await this.requiredConfig('ppkCode'); + kodePpk = config.ppkCode; + } return this.send({ name: 'Hapus Ruangan', - path: `/rest/bed/delete/${encodeURIComponent(ppkCode)}`, + path: ['/rest/bed/delete/:kodePpk', { kodePpk }], method: 'POST', skipContentTypeHack: true, headers: { 'Content-Type': 'application/json' }, diff --git a/src/apotek/pelayanan-obat.ts b/src/apotek/pelayanan-obat.ts index 68a179a..1e0e5a0 100644 --- a/src/apotek/pelayanan-obat.ts +++ b/src/apotek/pelayanan-obat.ts @@ -54,7 +54,7 @@ export class PelayananObat extends ApotekBaseApi { }; }>({ name: this.name + 'List/Daftar', - path: `/pelayanan/obat/daftar/${encodeURIComponent(params.nomorSep)}`, + path: ['/pelayanan/obat/daftar/:nomorSep', params], method: 'GET' }); } @@ -88,7 +88,7 @@ export class PelayananObat extends ApotekBaseApi { }; }>({ name: this.name + 'Riwayat', - path: `/riwayatobat/${params.awal}/${params.akhir}/${encodeURIComponent(params.nomorKartu)}`, + path: ['/riwayatobat/:awal/:akhir/:nomorKartu', params], method: 'GET' }); } diff --git a/src/apotek/referensi.ts b/src/apotek/referensi.ts index b6b2b94..ab83268 100644 --- a/src/apotek/referensi.ts +++ b/src/apotek/referensi.ts @@ -40,7 +40,7 @@ export class Referensi extends ApotekBaseApi { }[]; }>({ name: this.name + 'Poli', - path: `/referensi/poli/${encodeURIComponent(params.keyword)}`, + path: ['/referensi/poli/:keyword', params], method: 'GET' }); } @@ -66,7 +66,7 @@ export class Referensi extends ApotekBaseApi { }[]; }>({ name: this.name + 'Fasilitas Kesehatan', - path: `/referensi/ppk/${params.jenis}/${encodeURIComponent(params.nama)}`, + path: ['/referensi/ppk/:jenis/:nama', params], method: 'GET' }); } @@ -94,7 +94,7 @@ export class Referensi extends ApotekBaseApi { checkstock: 'True' | 'False'; }>({ name: this.name + 'Setting', - path: `/referensi/settingppk/read/${encodeURIComponent(params.kodeApotek)}`, + path: ['/referensi/settingppk/read/:kodeApotek', params], method: 'GET' }); } @@ -118,7 +118,7 @@ export class Referensi extends ApotekBaseApi { /** * Pencarian data obat */ - async obat(param: { + async obat(params: { /** kode jenis obat */ jenis: string; @@ -128,7 +128,6 @@ export class Referensi extends ApotekBaseApi { /** filter nama obat */ filter?: string; }) { - const filter = encodeURIComponent(param.filter ?? ''); return this.send<{ list: { kode: string; @@ -137,7 +136,7 @@ export class Referensi extends ApotekBaseApi { }[]; }>({ name: this.name + 'Cari Obat', - path: `/referensi/obat/${encodeURIComponent(param.jenis)}/${param.tanggal}/${filter}`, + path: ['/referensi/obat/:jenis/:tanggal/:filter?', params], method: 'GET' }); } diff --git a/src/apotek/sep.ts b/src/apotek/sep.ts index e5a6dff..fbd49a9 100644 --- a/src/apotek/sep.ts +++ b/src/apotek/sep.ts @@ -35,7 +35,7 @@ export class SEP extends ApotekBaseApi { namadokter: string | null; }>({ name: this.name + 'Cari No. Kunjungan/SEP', - path: `/sep/${encodeURIComponent(params.nomorSep)}`, + path: ['/sep/:nomorSep', params], method: 'GET' }); } diff --git a/src/fetcher.ts b/src/fetcher.ts index a662314..b685f83 100644 --- a/src/fetcher.ts +++ b/src/fetcher.ts @@ -126,7 +126,7 @@ export interface Config { export interface SendOption { /** name of request, it helpful for log or collect stats */ name?: string; - path: `/${string}`; + path: `/${string}` | [`/${string}`, Record]; method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; data?: unknown; skipDecrypt?: boolean; @@ -319,14 +319,14 @@ export class Fetcher { option: SendOption ): Promise[T]> { await this.applyConfig(); - if (!option.path.startsWith('/')) throw new Error(`Path must be starts with "/"`); + + const path = normalizePath(option.path); + const baseUrl = this.config.baseUrls[type]; + if (!baseUrl) throw new Error(`Invalid base URL for type "${type}"`); let result = ''; try { - const baseUrl = this.config.baseUrls[type]; - if (!baseUrl) throw new Error(`base url of type "${type}" is invalid`); - - const url = new URL(baseUrl[this.config.mode] + option.path); + const url = new URL(baseUrl[this.config.mode] + path); const init: RequestInit = { method: option.method ?? 'GET' }; const headers = { ...this.getDefaultHeaders(type), ...(option.headers ?? {}) }; @@ -414,3 +414,27 @@ function parseHtml(html?: string) { .replace(/\r?\n+/g, ' - ') // newlines to dash .replace(/\s+/g, ' '); // normalize whitespace } + +/** @internal */ +export function normalizePath(path: SendOption['path']) { + const [pathname, params] = typeof path == 'string' ? [path] : path; + + if (!pathname.startsWith('/')) throw new Error(`Path must start with "/"`); + if (!params) return pathname; + + return pathname.replace(/\/:([A-Za-z0-9_]+)(\?)?/g, (_, key: string, optional?: string) => { + const value = params[key]; + + if (value == null) { + if (optional) return ''; // remove entire `/:param?` + throw new Error(`Missing params: ${key}`); + } + + const component = String(value); + try { + return '/' + encodeURIComponent(decodeURIComponent(component)); + } catch { + return '/' + encodeURIComponent(component); + } + }); +} diff --git a/src/vclaim/monitoring.ts b/src/vclaim/monitoring.ts index 5045e3d..4e8e365 100644 --- a/src/vclaim/monitoring.ts +++ b/src/vclaim/monitoring.ts @@ -109,7 +109,10 @@ export class Monitoring extends VClaimBaseApi { }[]; }>({ name: this.name + 'Data Histori Pelayanan Peserta', - path: `/monitoring/HistoriPelayanan/NoKartu/${encodeURIComponent(params.nomorKartu)}/tglMulai/${params.awal}/tglAkhir/${params.akhir}`, + path: [ + '/monitoring/HistoriPelayanan/NoKartu/:nomorKartu/tglMulai/:awal/tglAkhir/:akhir', + params + ], method: 'GET' }); } diff --git a/src/vclaim/peserta.ts b/src/vclaim/peserta.ts index 02ebe40..685ef21 100644 --- a/src/vclaim/peserta.ts +++ b/src/vclaim/peserta.ts @@ -13,7 +13,7 @@ export class Peserta extends VClaimBaseApi { }) { return this.send<{ peserta: DataPeserta }>({ name: this.name + 'No. Kartu BPJS', - path: `/Peserta/nokartu/${encodeURIComponent(params.nomor)}/tglSEP/${params.tanggal}`, + path: ['/Peserta/nokartu/:nomor/tglSEP/:tanggal', params], method: 'GET' }); } @@ -30,7 +30,7 @@ export class Peserta extends VClaimBaseApi { }) { return this.send<{ peserta: DataPeserta }>({ name: this.name + 'NIK', - path: `/Peserta/nik/${encodeURIComponent(params.nomor)}/tglSEP/${params.tanggal}`, + path: ['/Peserta/nik/:nomor/tglSEP/:tanggal', params], method: 'GET' }); } diff --git a/src/vclaim/prb.ts b/src/vclaim/prb.ts index 0be0c91..63b25c5 100644 --- a/src/vclaim/prb.ts +++ b/src/vclaim/prb.ts @@ -187,7 +187,7 @@ export class PRB extends VClaimBaseApi { }; }>({ name: this.name + 'Cari by Nomor SRB', - path: `/prb/${encodeURIComponent(params.nomorSrb)}/nosep/${encodeURIComponent(params.nomorSep)}`, + path: ['/prb/:nomorSrb/nosep/:nomorSep', params], method: 'GET' }); } @@ -229,7 +229,7 @@ export class PRB extends VClaimBaseApi { }; }>({ name: this.name + 'Cari by Tanggal SRB', - path: `/prb/tglMulai/${params.awal}/tglAkhir/${params.akhir}`, + path: ['/prb/tglMulai/:awal/tglAkhir/:akhir', params], method: 'GET' }); } @@ -251,7 +251,7 @@ export class PRB extends VClaimBaseApi { }[]; }>({ name: this.name + 'Rekap Potensi PRB', - path: `/prbpotensi/tahun/${params.tahun}/bulan/${params.bulan}`, + path: [`/prbpotensi/tahun/:tahun/bulan/:bulan`, params], method: 'GET' }); } diff --git a/src/vclaim/referensi.ts b/src/vclaim/referensi.ts index 827b642..268b64b 100644 --- a/src/vclaim/referensi.ts +++ b/src/vclaim/referensi.ts @@ -10,7 +10,7 @@ export class Referensi extends VClaimBaseApi { }) { return this.send<{ diagnosa: ReferensiResult[] }>({ name: this.name + 'Diagnosa', - path: `/referensi/diagnosa/${encodeURIComponent(params.keyword)}`, + path: ['/referensi/diagnosa/:keyword', params], method: 'GET' }); } @@ -24,7 +24,7 @@ export class Referensi extends VClaimBaseApi { }) { return this.send<{ poli: ReferensiResult[] }>({ name: this.name + 'Poli', - path: `/referensi/poli/${encodeURIComponent(params.keyword)}`, + path: ['/referensi/poli/:keyword', params], method: 'GET' }); } @@ -41,7 +41,7 @@ export class Referensi extends VClaimBaseApi { }) { return this.send<{ faskes: ReferensiResult[] }>({ name: this.name + 'Fasilitas Kesehatan', - path: `/referensi/faskes/${encodeURIComponent(params.keyword)}/${params.jenis}`, + path: ['/referensi/faskes/:keyword/:jenis', params], method: 'GET' }); } @@ -61,7 +61,7 @@ export class Referensi extends VClaimBaseApi { }) { return this.send<{ list: ReferensiResult[] }>({ name: this.name + 'DPJP', - path: `/referensi/dokter/pelayanan/${params.jenis}/tglPelayanan/${params.tanggal}/Spesialis/${encodeURIComponent(params.kode)}`, + path: ['/referensi/dokter/pelayanan/:jenis/tglPelayanan/:tanggal/Spesialis/:kode', params], method: 'GET' }); } @@ -86,7 +86,7 @@ export class Referensi extends VClaimBaseApi { }) { return this.send<{ list: ReferensiResult[] }>({ name: this.name + 'Kabupaten', - path: `/referensi/kabupaten/propinsi/${encodeURIComponent(params.provinsi)}`, + path: ['/referensi/kabupaten/propinsi/:provinsi', params], method: 'GET' }); } @@ -100,7 +100,7 @@ export class Referensi extends VClaimBaseApi { }) { return this.send<{ list: ReferensiResult[] }>({ name: this.name + 'Kecamatan', - path: `/referensi/kecamatan/kabupaten/${encodeURIComponent(params.kabupaten)}`, + path: ['/referensi/kecamatan/kabupaten/:kabupaten', params], method: 'GET' }); } @@ -125,7 +125,7 @@ export class Referensi extends VClaimBaseApi { }) { return this.send<{ list: ReferensiResult[] }>({ name: this.name + 'Obat Generik PRB', - path: `/referensi/obatprb/${encodeURIComponent(params.nama)}`, + path: ['/referensi/obatprb/:nama', params], method: 'GET' }); } @@ -139,7 +139,7 @@ export class Referensi extends VClaimBaseApi { }) { return this.send<{ procedure: ReferensiResult[] }>({ name: this.name + 'Prosedur (Pengajuan Klaim)', - path: `/referensi/procedure/${encodeURIComponent(params.keyword)}`, + path: ['/referensi/procedure/:keyword', params], method: 'GET' }); } @@ -164,7 +164,7 @@ export class Referensi extends VClaimBaseApi { }) { return this.send<{ list: ReferensiResult[] }>({ name: this.name + 'Dokter (Pengajuan Klaim)', - path: `/referensi/dokter/${encodeURIComponent(params.nama)}`, + path: ['/referensi/dokter/:nama', params], method: 'GET' }); } diff --git a/src/vclaim/rencana-kontrol.ts b/src/vclaim/rencana-kontrol.ts index 32ef37f..168b106 100644 --- a/src/vclaim/rencana-kontrol.ts +++ b/src/vclaim/rencana-kontrol.ts @@ -271,7 +271,7 @@ export class RencanaKontrol extends VClaimBaseApi { }; }>({ name: this.name + 'Cari SEP', - path: `/RencanaKontrol/nosep/${encodeURIComponent(params.nomor)}`, + path: ['/RencanaKontrol/nosep/:nomor', params], method: 'GET' }); } @@ -294,7 +294,7 @@ export class RencanaKontrol extends VClaimBaseApi { ({ jnsKontrol: '1'; sep: null } | { jnsKontrol: '2'; sep: SuratKontrolDetail['sep'] }) >({ name: this.name + 'Cari Surat Kontrol', - path: `/RencanaKontrol/noSuratKontrol/${encodeURIComponent(params.nomor)}`, + path: ['/RencanaKontrol/noSuratKontrol/:nomor', params], method: 'GET' }); } @@ -315,10 +315,12 @@ export class RencanaKontrol extends VClaimBaseApi { /** jenis filter (1 = tanggal entri) (2 = tanggal rencana kontrol) */ filter: number; }) { - const bulan = String(params.bulan || 0).padStart(2, '0'); return this.send<{ list: RencanaKontrolListItem[] }>({ name: this.name + 'Data Berdasarkan No. Kartu', - path: `/RencanaKontrol/ListRencanaKontrol/Bulan/${bulan}/Tahun/${params.tahun}/Nokartu/${encodeURIComponent(params.nomorKartu)}/filter/${params.filter}`, + path: [ + '/RencanaKontrol/ListRencanaKontrol/Bulan/:bulan/Tahun/:tahun/Nokartu/:nomorKartu/filter/:filter', + { ...params, bulan: String(params.bulan || 1).padStart(2, '0') } + ], method: 'GET' }); } @@ -338,7 +340,10 @@ export class RencanaKontrol extends VClaimBaseApi { }) { return this.send<{ list: Omit[] }>({ name: this.name + 'Data Berdasarkan Tanggal', - path: `/RencanaKontrol/ListRencanaKontrol/tglAwal/${params.awal}/tglAkhir/${params.akhir}/filter/${params.filter}`, + path: [ + '/RencanaKontrol/ListRencanaKontrol/tglAwal/:awal/tglAkhir/:akhir/filter/:filter', + params + ], method: 'GET' }); } @@ -366,7 +371,10 @@ export class RencanaKontrol extends VClaimBaseApi { }[]; }>({ name: this.name + 'Data Poli/Spesialistik', - path: `/RencanaKontrol/ListSpesialistik/JnsKontrol/${params.jenis}/nomor/${encodeURIComponent(params.nomor)}/TglRencanaKontrol/${params.tanggal}`, + path: [ + '/RencanaKontrol/ListSpesialistik/JnsKontrol/:jenis/nomor/:nomor/TglRencanaKontrol/:tanggal', + params + ], method: 'GET' }); } @@ -393,7 +401,10 @@ export class RencanaKontrol extends VClaimBaseApi { }[]; }>({ name: this.name + 'Data Dokter', - path: `/RencanaKontrol/JadwalPraktekDokter/JnsKontrol/${params.jenis}/KdPoli/${encodeURIComponent(params.kodePoli)}/TglRencanaKontrol/${params.tanggal}`, + path: [ + '/RencanaKontrol/JadwalPraktekDokter/JnsKontrol/:jenis/KdPoli/:kodePoli/TglRencanaKontrol/:tanggal', + params + ], method: 'GET' }); } diff --git a/src/vclaim/rujukan.ts b/src/vclaim/rujukan.ts index d34673d..446f0fb 100644 --- a/src/vclaim/rujukan.ts +++ b/src/vclaim/rujukan.ts @@ -2,6 +2,10 @@ import { VClaimBaseApi } from './base.js'; // TODO: make generic request and response data type as possible export class Rujukan extends VClaimBaseApi { + private validateSumber(params: { sumber: number }) { + if (![1, 2].includes(params.sumber)) throw new Error('Nilai sumber faskes harus 1 atau 2'); + } + /** * Pencarian data rujukan berdasarkan nomor rujukan */ @@ -17,14 +21,14 @@ export class Rujukan extends VClaimBaseApi { * 2 = Faskes tingkat 2 atau Rumah Sakit dan Klinik Utama */ sumber: number; }) { - const nomor = encodeURIComponent(params.nomor); + this.validateSumber(params); const paths: Record = { - 1: `/Rujukan/${nomor}`, - 2: `/Rujukan/RS/${nomor}` + 1: `/Rujukan/:nomor`, + 2: `/Rujukan/RS/:nomor` }; return this.send<{ rujukan: DataRujukan }>({ name: this.name + `Berdasarkan Nomor (${params.sumber})`, - path: paths[params.sumber], + path: [paths[params.sumber], params], method: 'GET' }); } @@ -44,14 +48,14 @@ export class Rujukan extends VClaimBaseApi { * 2 = Faskes tingkat 2 atau Rumah Sakit dan Klinik Utama */ sumber: number; }) { - const nomor = encodeURIComponent(params.nomor); + this.validateSumber(params); const paths: Record = { - 1: `/Rujukan/Peserta/${nomor}`, - 2: `/Rujukan/RS/Peserta/${nomor}` + 1: `/Rujukan/Peserta/:nomor`, + 2: `/Rujukan/RS/Peserta/:nomor` }; return this.send<{ rujukan: DataRujukan }>({ name: this.name + `Berdasarkan No. Kartu (${params.sumber})`, - path: paths[params.sumber], + path: [paths[params.sumber], params], method: 'GET' }); } @@ -71,14 +75,14 @@ export class Rujukan extends VClaimBaseApi { * 2 = Faskes tingkat 2 atau Rumah Sakit dan Klinik Utama */ sumber: number; }) { - const nomor = encodeURIComponent(params.nomor); + this.validateSumber(params); const paths: Record = { - 1: `/Rujukan/List/Peserta/${nomor}`, - 2: `/Rujukan/RS/List/Peserta/${nomor}` + 1: `/Rujukan/List/Peserta/:nomor`, + 2: `/Rujukan/RS/List/Peserta/:nomor` }; return this.send<{ rujukan: DataRujukan[] }>({ name: this.name + `Berdasarkan No. Kartu (${params.sumber}) Multi`, - path: paths[params.sumber], + path: [paths[params.sumber], params], method: 'GET' }); } @@ -431,7 +435,7 @@ export class Rujukan extends VClaimBaseApi { }[]; }>({ name: this.name + 'List Spesialistik Rujukan', - path: `/Rujukan/ListSpesialistik/PPKRujukan/${encodeURIComponent(params.kodePpk)}/TglRujukan/${params.tanggal}`, + path: ['/Rujukan/ListSpesialistik/PPKRujukan/:kodePpk/TglRujukan/:tanggal', params], method: 'GET' }); } @@ -450,7 +454,7 @@ export class Rujukan extends VClaimBaseApi { }[]; }>({ name: this.name + 'List Sarana', - path: `/Rujukan/ListSarana/PPKRujukan/${encodeURIComponent(params.kodePpk)}`, + path: ['/Rujukan/ListSarana/PPKRujukan/:kodePpk', params], method: 'GET' }); } @@ -515,7 +519,7 @@ export class Rujukan extends VClaimBaseApi { namaPoliRujukan: string; }; }>({ - path: `/Rujukan/Keluar/${encodeURIComponent(params.nomor)}`, + path: ['/Rujukan/Keluar/:nomor', params], method: 'GET' }); } @@ -533,7 +537,7 @@ export class Rujukan extends VClaimBaseApi { }) { return this.send<{ jumlahSEP: string }>({ name: this.name + 'List Rujukan Keluar Faskes Berdasarkan No. Rujukan', - path: `/Rujukan/JumlahSEP/${params.jenis}/${encodeURIComponent(params.nomor)}`, + path: ['/Rujukan/JumlahSEP/:jenis/:nomor', params], method: 'GET' }); } diff --git a/src/vclaim/sep.ts b/src/vclaim/sep.ts index 5243549..cb8cd6f 100644 --- a/src/vclaim/sep.ts +++ b/src/vclaim/sep.ts @@ -304,7 +304,7 @@ export class SEP extends VClaimBaseApi { }) { return this.send({ name: this.name + 'Hapus', - path: `/SEP/${encodeURIComponent(params.nomor)}`, + path: ['/SEP/:nomor', params], method: 'GET' }); } @@ -318,7 +318,7 @@ export class SEP extends VClaimBaseApi { }) { return this.send({ name: this.name + 'Cari', - path: `/Rujukan/lastsep/norujukan/${encodeURIComponent(params.nomorRujukan)}`, + path: ['/Rujukan/lastsep/norujukan/:nomorRujukan', params], method: 'GET' }); } @@ -736,7 +736,7 @@ export class SEP extends VClaimBaseApi { }[]; }>({ name: this.name + 'Suplesi Jasa Raharja', - path: `/sep/JasaRaharja/Suplesi/${encodeURIComponent(params.nomorKartu)}/tglPelayanan/${params.tanggalPelayanan}`, + path: ['/sep/JasaRaharja/Suplesi/:nomorKartu/tglPelayanan/:tanggalPelayanan', params], method: 'GET' }); } @@ -761,7 +761,7 @@ export class SEP extends VClaimBaseApi { }[]; }>({ name: this.name + 'Data Induk Kecelakaan', - path: `/sep/KllInduk/List/${encodeURIComponent(params.nomorKartu)}`, + path: ['/sep/KllInduk/List/:nomorKartu', params], method: 'GET' }); } @@ -946,8 +946,6 @@ export class SEP extends VClaimBaseApi { */ filter?: string; }) { - const bulan = String(params.bulan || 0).padStart(2, '0'); - const filter = encodeURIComponent(params.filter || ''); return this.send<{ list: { noSep: string; @@ -966,7 +964,10 @@ export class SEP extends VClaimBaseApi { }[]; }>({ name: this.name + 'List Update Tanggal Pulang', - path: `/Sep/updtglplg/list/bulan/${bulan}/tahun/${params.tahun}/${filter}`, + path: [ + '/Sep/updtglplg/list/bulan/:bulan/tahun/:tahun/:filter?', + { ...params, bulan: String(params.bulan || 1).padStart(2, '0') } + ], method: 'GET' }); } @@ -992,7 +993,7 @@ export class SEP extends VClaimBaseApi { }; }>({ name: this.name + 'INACBG', - path: `/sep/cbg/${encodeURIComponent(params.nomor)}`, + path: ['/sep/cbg/:nomor', params], method: 'GET', skipDecrypt: true }); @@ -1035,7 +1036,7 @@ export class SEP extends VClaimBaseApi { }[]; }>({ name: this.name + 'List Internal', - path: `/SEP/Internal/${encodeURIComponent(params.nomor)}`, + path: ['/SEP/Internal/:nomor', params], method: 'GET' }); } @@ -1087,7 +1088,7 @@ export class SEP extends VClaimBaseApi { status: string; }>({ name: this.name + 'Status Fingerprint', - path: `/SEP/FingerPrint/Peserta/${encodeURIComponent(params.nomorKartu)}/TglPelayanan/${params.tanggal}`, + path: ['/SEP/FingerPrint/Peserta/:nomorKartu/TglPelayanan/:tanggal', params], method: 'GET' }); } @@ -1106,7 +1107,7 @@ export class SEP extends VClaimBaseApi { }[]; }>({ name: this.name + 'List Fingerprint', - path: `/SEP/FingerPrint/List/Peserta/TglPelayanan/${params.tanggal}`, + path: ['/SEP/FingerPrint/List/Peserta/TglPelayanan/:tanggal', params], method: 'GET' }); } @@ -1128,7 +1129,10 @@ export class SEP extends VClaimBaseApi { }[]; }>({ name: this.name + 'List Random Question', - path: `/SEP/FingerPrint/randomquestion/faskesterdaftar/nokapst/${encodeURIComponent(params.nomorKartu)}/tglsep/${params.tanggal}`, + path: [ + '/SEP/FingerPrint/randomquestion/faskesterdaftar/nokapst/:nomorKartu/tglsep/:tanggal', + params + ], method: 'GET' }); } diff --git a/test/fetcher.test.ts b/test/fetcher.test.ts index b95b211..ebe7b22 100644 --- a/test/fetcher.test.ts +++ b/test/fetcher.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { Fetcher } from '../src/fetcher'; +import { Fetcher, normalizePath } from '../src/fetcher'; describe('Fetcher', () => { it.concurrent('validate config', async () => { @@ -23,7 +23,7 @@ describe('Fetcher', () => { ); }); - it.concurrent('optional keys', async () => { + it.concurrent('optional config keys', async () => { const originalEnv = structuredClone(process.env); process.env.JKN_VCLAIM_USER_KEY = 'X'; @@ -48,3 +48,75 @@ describe('Fetcher', () => { expect(config2.apotekUserKey).toEqual(config2.vclaimUserKey); }); }); + +describe('normalizePath', () => { + it('throws when path not starts with /', () => { + expect(() => normalizePath('users/:id' as `/`)).toThrowError('Path must start with "/"'); + expect(() => normalizePath(['users/:id' as `/`, {}])).toThrowError('Path must start with "/"'); + }); + + it('replaces required params', () => { + expect(normalizePath(['/users/:id', { id: '123' }])).toBe('/users/123'); + }); + + it('throws when required param is missing', () => { + expect(() => normalizePath(['/users/:id', {}])).toThrowError('Missing params: id'); + }); + + it('supports optional param using :param? when provided', () => { + expect(normalizePath(['/users/:id/:group?', { id: '123', group: 5 }])).toBe('/users/123/5'); + }); + + it('removes optional segment when missing', () => { + expect(normalizePath(['/users/:id/:group?', { id: '123' }])).toBe('/users/123'); + }); + + it('treats undefined/null as missing (optional removed)', () => { + expect(normalizePath(['/users/:id/:group?', { id: '123', group: undefined }])).toBe( + '/users/123' + ); + expect(normalizePath(['/users/:id/:group?', { id: '123', group: null }])).toBe('/users/123'); + }); + + it('encodes unsafe characters', () => { + expect(normalizePath(['/search/:q', { q: 'hello world' }])).toBe('/search/hello%20world'); + expect(normalizePath(['/tags/:t', { t: 'a/b' }])).toBe('/tags/a%2Fb'); + }); + + it('does not double-encode if value is already encoded', () => { + expect(normalizePath(['/search/:q', { q: 'hello%20world' }])).toBe('/search/hello%20world'); + expect(normalizePath(['/p/:x', { x: 'a%2Fb' }])).toBe('/p/a%2Fb'); + }); + + it("encodes '%' safely if value is not valid percent-encoding", () => { + // decodeURIComponent("100%") throws => fallback branch should encode it + expect(normalizePath(['/discount/:v', { v: '100%' }])).toBe('/discount/100%25'); + }); + + it('supports multiple params in one path', () => { + expect(normalizePath(['/orgs/:org/users/:id', { org: 'acme', id: 7 }])).toBe( + '/orgs/acme/users/7' + ); + }); + + it('replaces same param name multiple times', () => { + expect(normalizePath(['/compare/:id/vs/:id', { id: 'x' }])).toBe('/compare/x/vs/x'); + }); + + it('keeps path unchanged when there are no :params', () => { + expect(normalizePath(['/health', { anything: 'x' }])).toBe('/health'); + }); + + it('does not swallow other segments when optional is in the middle', () => { + expect(normalizePath(['/a/:x?/b', { x: '1' }])).toBe('/a/1/b'); + expect(normalizePath(['/a/:x?/b', { x: undefined }])).toBe('/a/b'); + }); + + it('does not treat 0 as missing', () => { + expect(normalizePath(['/page/:n', { n: 0 }])).toBe('/page/0'); + }); + + it('accept single string path param', () => { + expect(normalizePath('/a')).toBe('/a'); + }); +});