From cdfd264609eb612f8dff9b2797af9e1554919c01 Mon Sep 17 00:00:00 2001 From: ln2r Date: Tue, 1 Apr 2025 00:11:30 +0700 Subject: [PATCH 1/6] app: add date-fns for time manipulation Signed-off-by: ln2r --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 56d3866..b1d628c 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@neondatabase/serverless": "^0.9.5", "@scalar/hono-api-reference": "^0.5.145", "@upstash/redis": "^1.34.3", + "date-fns": "^4.1.0", "dotenv": "^16.4.5", "drizzle-orm": "^0.33.0", "drizzle-zod": "^0.5.1", @@ -38,7 +39,7 @@ "@cloudflare/workers-types": "^4.20240903.0", "bun-types": "latest", "drizzle-kit": "^0.24.2", - "husky": "^9.0.11", + "husky": "^9.1.7", "lint-staged": "^15.2.2", "prettier": "^3.2.5", "tsup": "^8.3.5", From c8d7fdaf12dd5d73c7365187f220d07a331139bd Mon Sep 17 00:00:00 2001 From: ln2r Date: Tue, 1 Apr 2025 00:12:08 +0700 Subject: [PATCH 2/6] sync: add initial mrt syncing Signed-off-by: ln2r --- src/sync/mrt.ts | 391 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 src/sync/mrt.ts diff --git a/src/sync/mrt.ts b/src/sync/mrt.ts new file mode 100644 index 0000000..65b9d46 --- /dev/null +++ b/src/sync/mrt.ts @@ -0,0 +1,391 @@ +import { z } from "zod" +import { parseTime } from "../utils/time" +import { addMinutes, format } from "date-fns" +import { Database } from "@/modules/v1/database" +import { scheduleTable, stationTable, StationType } from "@/db/schema" +import { sql } from "drizzle-orm" + +interface iApiSchedule { + stasiun_nid: string | null + waktu: string | null +} + +interface iApiStation { + nid: string + title: string + urutan: string + jadwal_lb_biasa: string | null + jadwal_lb_libur: string | null + jadwal_hi_biasa: string | null + jadwal_hi_libur: string | null + estimasi: iApiSchedule[] +} + +const createStationKey = (type: StationType, id: string) => + `st_${type}_${id}`.toLocaleLowerCase() + +const createStationCode = (petaLokalitasUrl: string) => { + const stationCodeStart = petaLokalitasUrl.search(/PETA%20LOKALITAS_/i) + const stationCodeEnd = petaLokalitasUrl.search(/_020920|%20020920/i) + + return petaLokalitasUrl + .substring(stationCodeStart, stationCodeEnd) + .replace(/PETA%20LOKALITAS_/i, "") +} + +const sync = async () => { + if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL env is missing") + if (!process.env.COMULINE_ENV) throw new Error("COMULINE_ENV env is missing") + if (!process.env.MRT_SCHEDULE_ENDPOINT) + throw new Error("MRT_SCHEDULE_ENDPOINT env is missing") + + const { db } = new Database({ + COMULINE_ENV: process.env.COMULINE_ENV, + DATABASE_URL: process.env.DATABASE_URL, + }) + + const stations = await db + .select({ + id: stationTable.id, + metadata: stationTable.metadata, + name: stationTable.name, + }) + .from(stationTable) + + const apiSchema = z.array( + z.object({ + nid: z.string(), + title: z.string(), + urutan: z.string(), + isbig: z.string(), + peta_lokalitas: z.string(), + jadwal_lb_biasa: z.nullable(z.string()), + jadwal_lb_libur: z.nullable(z.string()), + jadwal_hi_biasa: z.nullable(z.string()), + jadwal_hi_libur: z.nullable(z.string()), + estimasi: z.array( + z.object({ + stasiun_nid: z.nullable(z.string()), + waktu: z.nullable(z.string()), + }), + ), + }), + ) + + // hitting the endpoint + const req = await fetch(process.env.MRT_SCHEDULE_ENDPOINT, { + method: "GET", + mode: "cors", + }) + + console.info("[SYNC][SCHEDULE][MRT] Fetched schedule") + + if (req.status !== 200) { + const err = await req.json() + const txt = await req.text() + + console.error( + `[SYNC][SCHEDULE][MRT] Error fetch schedule data. Trace: ${JSON.stringify( + err, + )}. Status: ${req.status}. Req: ${txt}`, + ) + throw new Error(JSON.stringify(err)) + } + + const data = await req.json() + const parsed = apiSchema.parse(data) + const schedule: Schedule[] = [] + const today = new Date() + const todayDay = today.getDay() + const isWeekend = todayDay === 6 || todayDay === 0 + const batchSizes = 100 + const totalBatches = Math.ceil(data.length / batchSizes) + + // looping through stations + parsed.map(async (station) => { + // deciphering station code from lokalitas file + const stationCode = createStationCode(station.peta_lokalitas) + + const lbTimeTable = isWeekend ? "jadwal_lb_libur" : "jadwal_lb_biasa" + const hiTimeTable = isWeekend ? "jadwal_hi_libur" : "jadwal_hi_biasa" + + const newSchedule = [] + + // adding extra time to schedule because it's messing up the timetable + // NOTE: this is only for Istora (35) and Bendungan Hilir (36) + if (station.nid === "36") { + if (!isWeekend) { + station[lbTimeTable] += ", 9:17, 15:40" + } + { + station[hiTimeTable] += ", 7:51" + } + } + + if (station.nid === "35") { + if (!isWeekend) { + station[lbTimeTable] += ", 18:15" + } + } + + // sanitizing schedule + const lbTimes: string[] = + [ + ...new Set( + station[lbTimeTable] + ?.replace(/: |\t|\.\s|\s/gm, ",") + .replace("\r\n", "") + .split(",") + .filter((n) => n), + ), + ].sort() ?? [] + const hiTimes: string[] = + [ + ...new Set( + station[hiTimeTable] + ?.replace(/: |\t|\.\s|\s/gm, ",") + .replace("\r\n", "") + .split(",") + .filter((n) => n), + ), + ].sort() ?? [] + + // formatting + if (lbTimes.length !== 0) { + newSchedule.push( + ...formatData( + stationCode, + lbTimes, + station.estimasi, + station, + today, + "lb", + isWeekend, + ), + ) + } + + if (hiTimes.length !== 0) { + newSchedule.push( + ...formatData( + stationCode, + hiTimes, + station.estimasi, + station, + today, + "hi", + isWeekend, + ), + ) + } + + // upserting station + const stations = await db + .insert(stationTable) + .values({ + uid: createStationKey("MRT", stationCode), + id: stationCode, + name: station.title, + type: "MRT", + metadata: { + active: true, + }, + }) + .onConflictDoUpdate({ + target: stationTable.uid, + set: { + updated_at: new Date().toISOString(), + uid: sql`excluded.uid`, + id: sql`excluded.id`, + name: sql`excluded.name`, + }, + }) + .returning() + console.info(`[SYNC][STATION][MRT] Inserted ${stationCode}`) + + // upserting schedules + const schedules = await db + .insert(scheduleTable) + .values(newSchedule) + .onConflictDoUpdate({ + target: scheduleTable.id, + set: { + departs_at: sql`excluded.departs_at`, + arrives_at: sql`excluded.arrives_at`, + metadata: sql`excluded.metadata`, + updated_at: new Date().toISOString(), + }, + }) + .returning() + console.info( + `[SYNC][SCHEDULE][MRT] Inserted ${schedules.length} rows for ${stationCode}`, + ) + }) +} + +const formatData = ( + stationCode: string, + timetable: string[], + nextSchedule: iApiSchedule[], + currentStation: iApiStation, + date: Date, + direction: "lb" | "hi", + isWeekend: boolean, +): Schedule[] => { + const currentStationUrutan = parseInt(currentStation.urutan) + + // setting up train id, so we can use the train route accordingly + // - if its "hari libur", trains only start from stations with "urutan" 1 and 6 + // - if its not "hari libur" trains start from stations with "urutan" 1, 6, 12 + let initialTrainId = + direction === "hi" + ? isWeekend + ? currentStationUrutan >= 1 && currentStationUrutan < 6 + ? 3 + : currentStationUrutan >= 6 + ? 1 + : 0 + : currentStationUrutan >= 1 && currentStationUrutan < 6 + ? 3 + : currentStationUrutan >= 6 && currentStationUrutan < 12 + ? 2 + : currentStationUrutan >= 12 + ? 1 + : 0 + : 1 + + const data: Schedule[] = [] + + // get the arrival time and station info, by looping on the estimasi object + timetable.map((time, idx) => { + const t = time.trim().replace(";", ":").split(":") + const stationInitial = currentStation.title + .replace("Stasiun", "") + .trim() + .split(" ") + .map((name: string) => name.charAt(0)) + .join("") + + /** + * formatting train id, doing this because of this + * see: https://docs.google.com/spreadsheets/d/199K18JpvbwuPxYt9_bfyq1E2P0DW0wd5dMUDqPDeszI/edit?usp=sharing + * URUTAN STASIUN + * 1 2 3 4 5 6 7 8 9 10 11 12 13 + * MRT-NSL-LB-1 MRT-NSL-LB-1 MRT-NSL-LB-1 MRT-NSL-LB-1 MRT-NSL-LB-1 MRT-NSL-LB-2 MRT-NSL-LB-2 MRT-NSL-LB-2 MRT-NSL-LB-2 MRT-NSL-LB-2 MRT-NSL-LB-2 MRT-NSL-LB-3 MRT-NSL-LB-3 + * MRT-NSL-LB-4 MRT-NSL-LB-4 MRT-NSL-LB-4 MRT-NSL-LB-4 MRT-NSL-LB-4 MRT-NSL-LB-1 MRT-NSL-LB-1 MRT-NSL-LB-1 MRT-NSL-LB-1 MRT-NSL-LB-1 MRT-NSL-LB-1 MRT-NSL-LB-2 MRT-NSL-LB-2 + * MRT-NSL-LB-5 MRT-NSL-LB-5 MRT-NSL-LB-5 MRT-NSL-LB-5 MRT-NSL-LB-5 MRT-NSL-LB-4 MRT-NSL-LB-4 MRT-NSL-LB-4 MRT-NSL-LB-4 MRT-NSL-LB-4 MRT-NSL-LB-4 MRT-NSL-LB-1 MRT-NSL-LB-1 + */ + let trainId: string = + direction === "hi" + ? `MRT-NSL-LB-${initialTrainId++}` + : `MRT-NSL-HI-${initialTrainId++}` + if (direction === "hi" && isWeekend) { + if (currentStationUrutan < 6) { + switch (idx) { + case 0: + trainId = "MRT-NSL-LB-2" + break + } + } + + if (currentStationUrutan >= 6) { + switch (idx) { + case 0: + trainId = "MRT-NSL-LB-2" + break + case 1: + trainId = "MRT-NSL-LB-1" + break + } + } + } + + if (direction === "hi" && !isWeekend) { + if (currentStationUrutan < 6) { + switch (idx) { + case 0: + trainId = "MRT-NSL-LB-1" + break + } + } + + if (currentStationUrutan >= 6 && currentStationUrutan <= 11) { + switch (idx) { + case 0: + trainId = "MRT-NSL-LB-2" + break + case 1: + trainId = "MRT-NSL-LB-1" + break + } + } + + if (currentStationUrutan >= 12) { + switch (idx) { + case 0: + trainId = "MRT-NSL-LB-3" + break + case 1: + trainId = "MRT-NSL-LB-2" + break + case 2: + trainId = "MRT-NSL-LB-1" + break + } + } + } + + // marking "dummy" time + const isActive = + currentStation.nid === "36" && ["9:17", "15:40", "7:51"].includes(time) + ? false + : !(currentStation.nid === "35" && ["18:15"].includes(time)) + + data.push({ + id: `sc_mrt_${stationCode.toLowerCase()}_${trainId}`.toLowerCase(), + station_id: stationCode, + station_origin_id: stationCode, + station_destination_id: direction === "hi" ? "BHI" : "LBB", + train_id: trainId, + line: "MRT NSL", + route: "LEBAK BULUS-BUNDARAN HI", + departs_at: parseTime( + format( + new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + parseInt(t[0]), + parseInt(t[1]), + ), + "HH:mm:ss", + ), + ).toISOString(), + arrives_at: parseTime( + format( + addMinutes( + new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + parseInt(t[0]), + parseInt(t[1]), + ), + parseInt(nextSchedule[direction === "hi" ? 11 : 0].waktu ?? "0"), + ), + "HH:mm:ss", + ), + ).toISOString(), + metadata: { + active_schedule: isActive, + origin: { + color: "#0155b9", + }, + }, + }) + }) + + return data +} + +sync() From 4be274e5fd28467d745bd9ee567eec3fbee39004 Mon Sep 17 00:00:00 2001 From: ln2r Date: Tue, 1 Apr 2025 00:14:10 +0700 Subject: [PATCH 3/6] chore: add mrt schedule endpoint to example vars Signed-off-by: ln2r --- .dev.example.vars | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.dev.example.vars b/.dev.example.vars index 7a23b9d..17fa759 100644 --- a/.dev.example.vars +++ b/.dev.example.vars @@ -6,4 +6,7 @@ UPSTASH_REDIS_REST_TOKEN="" UPSTASH_REDIS_REST_URL="http://localhost:8079" # KRL stuff -KRL_ENDPOINT_BASE_URL="https://api-partner.krl.co.id/krl-webs/v1" \ No newline at end of file +KRL_ENDPOINT_BASE_URL="https://api-partner.krl.co.id/krl-webs/v1" + +# MRT data endpoint +MRT_SCHEDULE_ENDPOINT="https://jakartamrt.co.id/id/val/stasiuns" \ No newline at end of file From b214a4c5c1703c883bb152a1b224f45686d1e1c9 Mon Sep 17 00:00:00 2001 From: ln2r Date: Tue, 1 Apr 2025 00:15:21 +0700 Subject: [PATCH 4/6] chore: add bun lock for date-fns Signed-off-by: ln2r --- bun.lockb | Bin 134956 -> 135291 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bun.lockb b/bun.lockb index a6d9c8a165078c72501e1570a258e21d1681fe45..ee8e1e6ae3bf3d3f9b5f53d7d3052e1fad6d829f 100755 GIT binary patch delta 20949 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 I#q)Ll1Gt)~0{{R3 delta 21180 zcmeHvd0bUh*Z8fRjh;|iJ&{VPnZBX-Zz?E%r z#zr(N%ECF#EH%rhEX~<6MMb4v%PD=o>+EyDo|m5a_k4bTbUu9dT5IpMhqd?GYai~p z_uO9V`NnF`#i2p%_s%=AGq_~*in8r>7EP{teE#%<#n;;(nC|Sj`|WRgyt!p%ZL^85 z)Ld79IWakR`rF}aA=WmTa$>5POjgk6Ap3$IhFlA@lc&kl5HuK+_53MW1bCf9yEYV>L%M>BO+gQV27nfVvKHxdDrjTK!$JK)<8;~*l>4p;>H~Vcrpe?D zdJ41==zg7U0_8ES(dm591}0N*&NRj3r08@2s23C$qqGj_l*wuCQ82JKC7ZpY4x$b8 zl;elCtAp->j0!pVklECqL2H1%i-d=n?oLldWzz~OG~2TmAeXgfgR)i@5}v^Buz-y# z0=0og`k74l%V~myjfq4)*J}&Py*3BsdN!R-o0vI1HFLCSH5%bb%m(F2Oa|pi>;a0u zzBz4>@~|GCFxs8sHkm$$78{v0@$r<$FwB0a%>!Es${k(N>2c6{kdr5eOi3PGc*$KC9C&KzZ0Zb^3-*m+N${P6wKlxl<-TJ_)R8dWf3e37|X;HY)ZA z5Cf^q!#loxn3|#~kEKpT)0`dq%ehl>dxPo5*fsx*o2KKL=VD7PRprC}_$Is0S$TjSF## z9@J?eC~rmyiv5-|6O=b;1}G13NQepn-9hU^F72&mAd0$H^UgM0&~R15=M9%MeBanZ zhASJcYU~MPZy9^T*n7raGhE-;GrEg+&M`KRu^ElMVQfNUQyH67-&{F!$;;DjZ40B= zziLyWXF#@TfO3vW?$J{+Q&Ua9Bm%**&x+U*C86M@K(FZ$g#Op2gatFI3aZkM_hzmcS^{Z z32mXr;n_7*4PZVR}pxnVoQ0{ODDDS_~siPhriw4s%OMFGqZfihx=%Z@0yaLLtm4R}sSg7YONm6^H z43sx#>f}+6yVFLS{!!=pJ5H6I^G|x!zwA`LFsK0xsID8R49WhFS@N$L(gP#n54$+<5NM}$%6NkCXN*9~Ni6{yRaEN8(4sck%f&bu)$uU!zx82$V zet?jj<3VLUcJuKX6x=jUG$40VhX|vBrVfk5#!I1%O`|Qhz$I(k!*xuiB#qlz&e`B- zBecA!;2vhqTwIsNHIK8j^D>!+QaYM=2HYTxI|6Qi#s%YKeMsY;DCZ7jUFN9V!WQh~X5}${`k#yOqOw z6gLpvv{@+hvs(finM^TMYKs=h6x7;bc?)t6Ds2*NzT1e_w~n*M;0ziBbxeAI-MSbO zPhk}*i?xegyhV)aGD=bBZi%w`vT zDJWPQP_V;%#766b*#F*y2i zJgKm?-I@leGb9)dpLmrD+BwXZ{OMx5ILl6)Nhws?F51$p39lb>v%w{4+!=5qG%hZH zR}G~ikKCaS>j}s>I^ibau-LaLEYil1(aJ7d6!d^YtReRU4)cwswElrOYlmi79ZkIu zhUHLDn8W;8Ga45bX9>pf+KD#$M_WgMgQN2-wzZ>vP`JbVWpf%A9w!79glp3m?yx+C z`@IJ#q*IIsn0{0m9&Pb%WpF7P7h;d`D9^fG&h>7MsnROsmUG9z_0jTLw9&YZF&>IR zyYH5Bx68TiK}Lo7<=j{0oL?||8jmTtoO`XDE7iD=h!~Hyng>B`S~<6^oV!=fb#JHZ z2_knVhxv9pTHh(ox(?^1TF|m4*p?v_92IA2j^nX2ZG@LTLIqI{>t7)A-o!Y>DE$o2A?Rl^lT5s!8H7X0V zTSrAG2QpJeR3|Elbyz+|4kj`-+G>MOaL?LyvOWxnt*XM~z^s(SIn38PP_P3|kGoVZ zsz!zVI(b6iDY8;VbGx+=QZGnw*O8q(VPg*ng5ZqScu1^>1|4>>mI}H!tY`H+Y#(U6 zYF8Q}Ufo(31j z>uY%zTqjCz6K%Z&PI*URpk2gJP;ZB2ImYUyklxXjDqV52rg7uIrE1(EaLF1MhKflV zx2T-EQqCoG=McoLtk<~G-Z369TV;&~_pny@K{@B!(_|W^9S{xEAaNYi zT-34_lATKX$9RD0LFoz67QcA*ZRV1}_0hNjaH_PlJTDUKgfk6#v%skupMX<(HJn^` zV?vAv7^SvG%i;>>!QqGm7m2v5Hv&#?1RQr8BNAwRg2N-h7{v;3O8GoEr5pmQ`)W;1 z1E)s&0XU^sqrWi50Z}&lqgC7C~}AqYgW{s6jzP9Omc&+6g>ofa=D? zH!9X0ka#^1YMR?EHz9S_+%#gKu`;}`GaxAk=bgL_lIor%&p~Q^)t>JLiB}Zy0A=g+ zGVC?@#c@dNC2Gfp4aV&XBzO>ZWey}BaCL2`e*g(l6lDm=)~k@XcbxTT#fe~{#=^mI z4kV5q+EiFi=w-MGgJx@&8mE%RL*ktdUu$f)7C`E#>iOC&ryyZ_`bJy5hpI8ccm3_S zi3obcVO@bd>@pR(%qND@`bXldKEsrvYJ4ap9+Vo!3y^qFs_Y^peFI=nEr+Z4ttKcR z5^oh1v9ChnK`BE6A2vLIjTr$+Z7~jotA1B@STDnwFuarJBlmaLACek7H96m6D?3Pa!vQhdMa4PRGIHl)5$|y|+r%Km@OVH~6TAtSh|HJmz@)m(prN_akR$7cU z>f#?ceJq9LdAGo+8og7E8VkWWwf2sJQ~fp{W9Ur;r{&=uBFOEq-iFLGg6)lawtiIL zc37Sn%ROy$M_cxT>#cEhALDwQHw>IAU0t4c9-Pt(bsMFV!KoVC!6j&Qt>cWm0pR*; zdHLW}=@oFQmB{gi-b`?+w>{-~HPeh5!@#K;Yr#3S_AY@_{dPz<^k#umc?ZC0?ctw> zf{6}%T33Q}4h2n$&aQ7V@kcY-RT(v-q#@u3AP$%WfZHZh6#&siyO?r*q!Kk+4Ltl? zyDFosA8Rtvo=G-3IH_qi_tOR7cDn<7-A}n32cmY}Pd$(We>QPBjt`CE6wtnFNUAbbGR{Qsq2RH*@!=Kd4nR5P2 zfb+8f6QyT*Ytu0sf(4kvxw!rn%4RPCxOD=!x1|7Ie}ifR`JDm}WVu%DAEVsi3SFNm zuj^`n2e4MB>p=OspK`sI)I>0&uD?aO!B+qquv4l3`&8NgA8hcS99Z`KmNY=+FZBkP zvc+HPGE<(i!@A6rx88AGzMt|EoB~+?v`)`}^7U6VTg#}5g7aFjqTF$bE;Ho@F9Y0Q zDZm#~manjY>we1huW7lzr>tKFF#Sm@$NV$H75+!dP?QIB3*e4_1GvInfG?(;FF5nB zC})|GaJdDP<*Jz7v%1er% zp=d%CqN>wwoTrrc(xPN(Sk_fsp%pVIT03hJ2^ z;KkLm^<0h8;4FJKAI8rhm)m@fbwTIoT1+{Ao=)fM`AqpR&e!FCgDS82HyXe&{!A=LS`y)P+tOy08}QSm+eqWL@MU&qcK;eUTI2A=m5sb6t&bPf+-hiTPi`?j=H|+5+Rg_G?dDaK0xu(g>Ql6BMqmU zNZV85GM9*;0;C;Cwbn(Q*4Cm`Yn{SLC6F#aO2~KO30Pjfia(sE zZCvNX_cd-px&dj#OHO>&Tkw*LUVo_;)q2?}2Gg*YT{QG%tP~^{SzmF9p_Ght7;Qm1 zoV;FjiHFIJ^by*PbOhO6gJrM5ve%pFNGDSK zMwggG`A9S9CelnweA6W+QvuQ`B;Im~$7vYSsZ@w`8d=|ViRqM#G>f(%eS*9SUHI0J z8|jm@8|hPId&ebaQU=m&+K)7cn!W21vnUJcY$`_jGzD#PiDzgQ(r4-DCRnivR=nrL zC+73sgB9<=3P|%P>U~)8KCF1(DdtlNqzjM|ikxBro)A9ZP-cMocIpQ7D$^RHQw&T z<45;)_}_N;AEZ@e+ksWufmPb!#J5oPL)r%^WT#W)Q`Szb(oU=rq?ah@1FX^qSfvk~ z;uSgy=?J9QT~6^D&D#b0cELVK1r)U#_U(p!yPaYKl|Z@xDd9t>*hqOF!oCk-AEdV^ zeh=*11N-(k@zm)iq#KY%eB>1GQo%>C?<3f^*D2nkVS8cUUf2hzh^!yOzK>zw$4;?@ zwm{kpsqrUHv5nlHz`jplAEX^*+Xwsh!M=S?@d53Jv=36qey7+?S^Hh$Ln=nPhk_2c z#78s>>0UaD^kWMD)FnQlc}Vxs8KnCu>Yz&;pj@P%QVG(7)b%r$_>A(9eokdb4^jN* zF7XBBBQ2(zNWY}SLoV?Z6&!*!hhWVYPH~uqeF1B}fHjc5C2KLPDTX!0PH~jB6uZQC zkJ9D^N@ekJR1*l`?o9CwPFv<1>;NR3Z8#cgt* zz^a_UszCaUY$vfQC$TCgo#uNcI&g9b?SmL{%8743Wu1cCor2pzszO1h;dZCtcBh@f zLPsGTffRejDXP)DGjO{za63p=iaHCoI}5iv>%{Z&5=a*yC4BD`wJ7gvX#KV5*S#b9Z-<=K?=F39Z(lx-$mF5sRadHf_;}@-z6s> zQIL*6ioNXQL+Uc@yA1m-I}?KL`%uNh)Z~>$L$2*<9vohLWKNZnxB7fuWAe%$emno5 zSDS~g`9_a)o&2f$f|R{`XP2$(Y3sZ&H}a`XjZ1gA9qqi@#&(dc>QhLm53MS7@_|>1 zc`C&`U2%#K%DaMjx`KIv^Z>47LHm(*p=Q@zeB2@JM#V_GQ_wFi(Sv3o z?MX+G_M-4#U3}0X?L%jf##7V{toIG9_YLisyWzqk*{(NTB7yRdK1gLq`&0Za7rs20 zk2H~PA{{`9w_SX^Ass~Gj*AbsJ6PpASmis~;r5$$xFH=zTae;#yQ>{;NO8C!9YMBx zF7YU3AWfqENJlPhCR`$UX%^B^ON)`F$RHtHvW*b6iWUfQ$xKbXJ&G24h`MHxP?TR) zB#Bzak5vxhAyiGd+Y?b1O4W_uo}BhC+UY5Rg{4Vzz1*A2t;s19GbiA-@kQxXT||ps zMG4hKYqO{E6ARg;niwj-t|@j|=HQWQU2b?FSFF`1LVaa=0BGH!#OmT#Ax{hJb8gl6 zRX|8lSzYn7P|e=f6#l{ra#BO#*>FUn`W`jkmi7VgpfKkZxuBtlwqy;!!wuDH5n3H2 zv|9c)QL{?=xXH4uP2{OY&#RKR{euwL&{#zFsJA#-x%$`oN7~h6 zagztM9Q+(Y&*Nuf2lc!bdY&g_eBnp?bh;(-@JJ(@zqo9|6lqtCuE-zy^T*qK#p-$d zfHh0ci_`OJB5$~!=g{+NAuku;tBamj8@yA?F`2sRd37Mq;ZKsevu?=b&&KNlbHVd9 zR9Ex@p9-Ek8>Z*g1JBO|_~HTZ;_`>K>Yi}Q8!n{QM6zEsZz%^Re;|H=gwH!52*7F7UK^?cj4>a(nePDbW zl=a=n;6XR>M{-BcWQ$l)YZ>Hiw>&voOtA6)Fx!CHz|%kuFbn7-Q&UB2uboi+0N4fW z20oIjklo7#b_g&S7zDuCwS^uC`XKNS&=cqd@E0E90DMtF`?3Qs{5&jQ~AXMi7p9{_$degZfNOaq<(vVi5l3ScF$3Wx+c0(PJi5CwDw zqJi!}4`3ep;V0kOmyt|{FcRR+!B5}Ve=WdZpdL^Y_!)Y?7o58 zSHLU4o4{t^E#Q4%J+K9M8+a8c1U3L$fgQj*z&7Ar;5Fb4U^}o0cpZ2T*a#Hx-kuAF zAN|b)Qh_Cq6M*?b-gS#1!=FbU?|KeB=>SKY8$dq5k20$P_rRM#?}GdW+yZU`cXZh( z=d#n>7_P4;agb;g!frAO2nK=xuE+J+CD}Dw08N0ZfVcD-FFwg`1l9+r0{8*GfDLF2 z_ybLW0D#NUVmWQ5%gsT10pkG|z)^)w?g#V)x&whgOMnl;SWsT)XrL1|#=~i%r==By zR&rIE2*@@Zt_!l_0EdpQpxpp=KyHWo%)hzI%ry@7Fn8{qAq1dIT9Ttk4t07t=r z0K3jZzyKf-cm#Nu{e3u?VZcz}QGjF07+@qY8b}6GfKfmyz=PpSKAA2?cI8OKE6VQ6 zZpq3Sqr=byv*^BUHw(y9e}q3Zx6V* zwrySqK2G>pu>ki_b{F^!xC02} zu`io}Du4&T#}yw_d}JB1v?_SEgLfs5-wMj%2Yhz<`NZ*!3k~I3x)Mid4$gG}j)M&V zJ{P$$Zj{eOUXhldEr8|#pNo9{@fpc0#pfWOk4=GAKwlsp=p&k%Ol`r00_}hhARKrA z2m?l$WW5YA#M&M_&qiOFks%)Tb%M2`-4vaA^6`SJ^J;lEL$s;_R#wXtBR$2VQK4a> z;o)*(rnp@6_+)X)!{7Lke^}arw_9#qe#tBz?GV~NGz|V+O-`CF+WYx?svlu~F!s9@ zhqlaX2aWL1sL)8nBe`R`u!&Cc+vy^~e;3Bk1a~l-`X8I(AGtjOdiKz8Zt0{|cF7Vs zVu9S9g?}}M$YxK7h}dMfLrv%#|9G6}F`o+iBEZU<% zbZlB6bDu!ZE93?y@5;STh$Qo&8nV?4(OLAC$up2UK+XX1Gk*AgW$xVLp1m_>dx)^m zhzM?FntTfik@~9wa-7pd-^8Df*R4=6UMBG2jdnNQ+E==+qGXf&4OKfDZy%U(dDOL< zPZ!*$V7#B;*8Hw5W`33TctzFgHD$jig^!={MgxDpoAb{_)cK}DqovG#6624P0W&dv zg9^`9 zRI4rfv%Vt!f2^msq&IAIqdNT?qf)~bi)GYI5g~TTbQobi*igp(MfeCmnGX?U8;E~? zBjs7u8YDHCQPS-#cuW|4uRUTwBYAD6=;2q>NBzF`@d>FDro!YmM^S)RN89MSVn|W{ zY|&V>FucfkSHkPhPHEV#aQXoD6?hBkG?$BVu*}BG6`E~taK8VKj-1aj32pYmA!Z5m}C1)R)`3VLe(bHYZmN@lwB7HTek7~ z1ygr-o^Rx=HXgzlU601$b8%@X}YKiP1$i0HVqsro1TtmEwY&GD~(h8f1* z4h?T_I*yWhs9@hc|NR%cpE;zLV7FdrDre4y2W@Gle$4x0Gk4sa<^?^Vp?O|=Q>ZMQ z4XZoJPnpb>g)2qT<+`jy?g)4{;o>CXdxeb zTKM=IKV1*&wmhk*Y8`Wh#`P9*ZLVnOfet+7nH8eGeDi5h)zA3Ry{A`P+s)#IgB2A* zW!L4Relyj+@e_ETTerdo9O#%@p*BP&=88t#>^SMg&4xXLW{qD<2iI#8A?AOdU!lK5 zCO!j;UzOuQ{EgrG@B1Yu;K`QL6R%vE zgc3gd5I4UIm;L5pan7OXx@gZ!O$I;W7srey^b&K7SsXnYR6S;k$1@ z!;Utf@s7-2CK||3=7|6iCC|@;H8o}A^P+9sm*~M4b&NNAT>I#$Nh`lS_!a7gb8rhY z8SnNO_Q4g)%9EvSP@?Tg++9Y>)z72fr#q^sw|f1OON|`u(>BdtPOGWust8HuCvx+7Q>Lk zZfU%yVenU%TRt(d?l7$%?ZnQ5MlEQBS1-F1+^d0ySv-rJ2FNj9AoAb?AFi_<*wX-Y zc$qLK{xTokiT?8He7GqxjF*uN7b}nT`gIL2K_pZ$?V66Qqwzk7%cF-6J`uBvBMjS# zs>W+e-k2D^DeX$iYLrB%68$9ct7IG<)!l|RIXn)w-mj8DbTkIR3Y85aN*zMRJ5Yvw z+;PCgzBeyp5NZWY=b~lp1z4#6doU0LNAMGI!S21GT!^xzD7_afZl5Gtd)f=%WZ+3-( z@kW#P?p^+=y6=I&ijoU)^38=9ukj9)I%kW++uix}PK8EOc?OM(Sa}=7&v=PS)hS<& zPW$|4L}FtQ)8zw;FbLzVD?fG(^t|4^$43qcp1#@X{>AM)WmFfVJ zZ!Q*9g$n$h2>h5a6=Z$<|KI?Ch6>aTEnv^KkWF$iJmXCfwY+)Hm`##uC|V2{P0XDj!^e&eYMz z2QA9A)4&#KykCZ*FSi)CVENpN(f+}Ci>h1Y4m9om2d5_#a3KG*1UC`2rGpUJqT~V) zp6=r$+M9n#lyzSa_#Z@ee*vY4{V!mkW90@Af8&)f9z)v?`mE~uy%l@@-avW!1>qBE zyp!eflr~R9%@0g~h8{p>57PdR$+6zcT{CjZ8G#ZGjCdo{d>OJ7eJqo`mtv6*OhB zM2D}*OCWy6D{0!4oxD2P`&tpY;NXhrJ5mNL!ws?V4x4Wt?Dg8EMQ1uxjKO%z&8MCv zt@=KA;2=tPAuxv7GJTn51&%+PSSYtG6SG8h8JmX=jhDY9^;vuH8Sgy-sDeDtG0H_Ws8Ea9*sN(=}qmn?1FbB17AU zo63@8?d8~{|M$iPeQ8lr3jj7Vw*DhftaR&#cVeHy)?W;b$ja_Il}2TeK$U2bpuW7e z9J^8r2MwxfZEE4bW=0@rw*sTAED)#$#>k`)+VxH4Zf4QT(4oH+c`OM;b4jTJ0CM^{bMU;S~y%&ahUswoF(B$57K*O*N;@ zr%grH2J)>{=-YVt)w+V2bu-3PlVHvg>ld2|IAAmN) zHWh6f$n4b^KxI)@wZBuYS&j3svVR4XLS<1_m0;4ZtVSH|E1Rtm5v}L0R-f9;U889A zn8~T>Q&TfTdrqH{nmHjkZQztl_k^)>`Wn$Xd|GDmgt2L9o3cF4zpN^5Q|n;i z%(rH~@B3-pj~hbfh0OlNTIJbk-=M8KE*!ufv3Nl{CG-c^5j(0`+FzVSdq{$^P^C8UwmxvjF(*- zn%WLQ<~#FQ-h*r3YtZ53G00qhZ&cDi$JEZ>Zq`kDLA`6T=&iLPrdm;p*Tw5Ys=$k6 T?)##tT)0kn7G1w9p7Z)&+WL1P From a65297313a35761c491f16411e810f874f9d1605 Mon Sep 17 00:00:00 2001 From: ln2r Date: Tue, 1 Apr 2025 18:03:28 +0700 Subject: [PATCH 5/6] schedule: add active_schedule to metadata Signed-off-by: ln2r --- src/db/schema/schedule.table.ts | 1 + src/modules/v1/schedule/schedule.schema.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/db/schema/schedule.table.ts b/src/db/schema/schedule.table.ts index 9700599..ee5028a 100644 --- a/src/db/schema/schedule.table.ts +++ b/src/db/schema/schedule.table.ts @@ -14,6 +14,7 @@ import { relations } from "drizzle-orm" export const stationScheduleMetadata = z.object({ /** Origin metadata */ + active_schedule: z.boolean().default(true), origin: z.object({ color: z.string().nullable(), }), diff --git a/src/modules/v1/schedule/schedule.schema.ts b/src/modules/v1/schedule/schedule.schema.ts index dac67b6..7a1710b 100644 --- a/src/modules/v1/schedule/schedule.schema.ts +++ b/src/modules/v1/schedule/schedule.schema.ts @@ -46,6 +46,11 @@ export const scheduleResponseSchema = z metadata: scheduleSchema.shape.metadata.openapi({ type: "object", properties: { + active_schedule: { + type: "boolean", + default: true, + nullable: true, + }, origin: { type: "object", properties: { From 799ea1508d4ed8d7b4632f6082c8aa21b5bec76b Mon Sep 17 00:00:00 2001 From: ln2r Date: Tue, 1 Apr 2025 18:04:00 +0700 Subject: [PATCH 6/6] sync: add mrt sync command Signed-off-by: ln2r --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b1d628c..518563d 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "format:check": "prettier -c .", "prepare": "husky", "sync:schedule": "bun run --env-file .dev.vars src/sync/schedule.ts", - "sync:station": "bun run --env-file .dev.vars src/sync/station.ts" + "sync:station": "bun run --env-file .dev.vars src/sync/station.ts", + "sync:mrt": "bun run --env-file .dev.vars src/sync/mrt.ts" }, "dependencies": { "@hono/zod-openapi": "^0.16.0",