From 8e1650573d09a8e223e7012e183ee695973786dc Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Mon, 9 Sep 2024 13:21:24 +0700 Subject: [PATCH 01/64] move db schemas --- {src/db => drizzle}/migrations/0000_futuristic_colossus.sql | 0 {src/db => drizzle}/migrations/0001_dapper_the_professor.sql | 0 {src/db => drizzle}/migrations/0002_smart_vermin.sql | 0 {src/db => drizzle}/migrations/0003_confused_lethal_legion.sql | 0 {src/db => drizzle}/migrations/meta/0000_snapshot.json | 0 {src/db => drizzle}/migrations/meta/0001_snapshot.json | 0 {src/db => drizzle}/migrations/meta/0002_snapshot.json | 0 {src/db => drizzle}/migrations/meta/0003_snapshot.json | 0 {src/db => drizzle}/migrations/meta/_journal.json | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename {src/db => drizzle}/migrations/0000_futuristic_colossus.sql (100%) rename {src/db => drizzle}/migrations/0001_dapper_the_professor.sql (100%) rename {src/db => drizzle}/migrations/0002_smart_vermin.sql (100%) rename {src/db => drizzle}/migrations/0003_confused_lethal_legion.sql (100%) rename {src/db => drizzle}/migrations/meta/0000_snapshot.json (100%) rename {src/db => drizzle}/migrations/meta/0001_snapshot.json (100%) rename {src/db => drizzle}/migrations/meta/0002_snapshot.json (100%) rename {src/db => drizzle}/migrations/meta/0003_snapshot.json (100%) rename {src/db => drizzle}/migrations/meta/_journal.json (100%) diff --git a/src/db/migrations/0000_futuristic_colossus.sql b/drizzle/migrations/0000_futuristic_colossus.sql similarity index 100% rename from src/db/migrations/0000_futuristic_colossus.sql rename to drizzle/migrations/0000_futuristic_colossus.sql diff --git a/src/db/migrations/0001_dapper_the_professor.sql b/drizzle/migrations/0001_dapper_the_professor.sql similarity index 100% rename from src/db/migrations/0001_dapper_the_professor.sql rename to drizzle/migrations/0001_dapper_the_professor.sql diff --git a/src/db/migrations/0002_smart_vermin.sql b/drizzle/migrations/0002_smart_vermin.sql similarity index 100% rename from src/db/migrations/0002_smart_vermin.sql rename to drizzle/migrations/0002_smart_vermin.sql diff --git a/src/db/migrations/0003_confused_lethal_legion.sql b/drizzle/migrations/0003_confused_lethal_legion.sql similarity index 100% rename from src/db/migrations/0003_confused_lethal_legion.sql rename to drizzle/migrations/0003_confused_lethal_legion.sql diff --git a/src/db/migrations/meta/0000_snapshot.json b/drizzle/migrations/meta/0000_snapshot.json similarity index 100% rename from src/db/migrations/meta/0000_snapshot.json rename to drizzle/migrations/meta/0000_snapshot.json diff --git a/src/db/migrations/meta/0001_snapshot.json b/drizzle/migrations/meta/0001_snapshot.json similarity index 100% rename from src/db/migrations/meta/0001_snapshot.json rename to drizzle/migrations/meta/0001_snapshot.json diff --git a/src/db/migrations/meta/0002_snapshot.json b/drizzle/migrations/meta/0002_snapshot.json similarity index 100% rename from src/db/migrations/meta/0002_snapshot.json rename to drizzle/migrations/meta/0002_snapshot.json diff --git a/src/db/migrations/meta/0003_snapshot.json b/drizzle/migrations/meta/0003_snapshot.json similarity index 100% rename from src/db/migrations/meta/0003_snapshot.json rename to drizzle/migrations/meta/0003_snapshot.json diff --git a/src/db/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json similarity index 100% rename from src/db/migrations/meta/_journal.json rename to drizzle/migrations/meta/_journal.json From fef6e60264ba05de0ac1a0b4cc4ef91f60940dc5 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Mon, 9 Sep 2024 13:23:59 +0700 Subject: [PATCH 02/64] refactor: inject deps serverless neon --- .gitignore | 1 + bun.lockb | Bin 87344 -> 112820 bytes drizzle.config.ts | 7 ++----- package.json | 11 ++++++++--- src/app.ts | 14 ++++++++++++++ src/commons/libs/db.ts | 8 -------- src/db/index.ts | 20 ++++++-------------- src/db/migrate.ts | 22 +++++++++++++--------- src/index.ts | 2 +- src/services/route/get-all.ts | 8 ++++++-- src/services/schedule/get-all.ts | 6 +++++- src/services/schedule/sync.ts | 14 +++++++++----- src/services/station/get-all.ts | 6 ++++-- src/services/station/get-by-id.ts | 6 ++++-- src/services/station/sync.ts | 8 +++++--- src/services/sync/get-all.ts | 6 ++++-- src/services/sync/get-by-id.ts | 4 +++- src/services/utils/index.ts | 2 ++ src/services/utils/sync.ts | 8 +++++--- src/types.ts | 14 ++++++++++++++ wrangler.example.toml | 17 +++++++++++++++++ 21 files changed, 123 insertions(+), 61 deletions(-) create mode 100644 src/app.ts delete mode 100644 src/commons/libs/db.ts create mode 100644 src/services/utils/index.ts create mode 100644 src/types.ts create mode 100644 wrangler.example.toml diff --git a/.gitignore b/.gitignore index 5a4337e..16bab0b 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ yarn-error.log* package-lock.json **/*.bun .env +wrangler.toml diff --git a/bun.lockb b/bun.lockb index 4466669420e64beb2c469a0b2f3af6bba09fbb25..b5fbe8231185f46358cdc8901499b2dfff3be3ee 100755 GIT binary patch delta 39027 zcmeFacUTlnvp2lEfU*P$qDxLjBuEsH97G8speR8Jl93z~3@j*MycHZ3Q4s?Q7!U&( zz=%16ih`nmigGI`iUKP5R?Y6{?j!g4p7WgZUGG2dT>h--s_yFQ?&>hJ%U1aTp%Yz0 za&#u-EFY@>X!n-tfK|d8ihfsLjhIkI$n}hr&axW&w{Y|0%+aUlG`zIwxn9aaeHxq; z$)XLT(b5;k29t@T)VPq~2pTOeHZdtIAtZ4ta797>6ABCm`UR*6&>ajKO$_KONJ{~I z1XKd(Dghd)uQ9iFoK%e9~lPShVyG2vN%+plHY`pd)~80Xh=sN}y;^B2XEi;Xtt= zabaXTY35J}%g2Sq1tum!gTu73?d(Ji;9`qXl0sszPZS}I8Hd4t*i)E53I!r!l1ylN zpopVbJ1#ahilzip~rU<^AfDkLE^F=Sy#Oj2T?B8@f{ zGK3_~PmYKRrunE~7tzwE0tKVf4S-@-s&V<^-1IkPj{Gf9%>NK5<~s)zed7Q(O#np? zT*l=`DntLH;@R8`_CQgAE;lXD%{UY&=KG|?ak}R~v4ZPBv4Rua^d6vC-VP|VC4COG z2J<}xiu@{|I8@hxV$W8LV{^uSB3FSKw+B>!qJrp%Xc#dXO$ZXuYw2GVIQicK#V&3F zibGY$rPqOCm!IQORZUJ>0w{KU1@Li*)wF0ds4{&s8*b1^hHKMkAWQ!NY3!omK(PWQ zP*fNg6q^u(mOvU6^ngO7L4k2GaoS0VNr_Pr^Fh2QHeo?XLNIN#E{z7AmM#nw8`3_J zMuX8xzYP>CJ_J-1C_6bOI3g&5M)T0;v@A6?IVc)6 zAw7`Bu8mGkObQGN523t!Q6dzED>yJIL_0JlF(@3oJ~%KTAtW$3I3gkLtr5rYJwTy> z?DU94?XalW`LunLI2CQ;2+}7SbGmFbq|uQ134uW&p!^5$u}5r8I6brpDE35POlovs zQcyT7qOnm6Lue7EoEG~7#r*C-u}8xL6Jr-aSMHsRz5w%|@^;EQDX*oxlyXAK*(fKZ zW&dO79o)byjK8#TSCSrQW%6B_}G$1xj@ zS0{(Yl3gDcm=um)8w*Xvp$rZQO-4anLP%0lL`cFkxW^t%4UCS0`f0QRI}Rf*A~-1_ zB!osl8oMkiA~7ie{Gi#M)5VjgaQNw5ssmIE@<};xd|?PsR6Gj!NE1SXgHV4xlt+C@ zQ#teuP;~OhsnGu@2m+IEY$||a!PuCPB%FCL{UUabW5`WVi0OY-xCRVI zUkFJF34)cG7VpgI(SOn#;tcIaMc8H93qn%8p#Y}cfTAKhptxX|0!2f0fQkZL6dpm& zl88j83M$fa<;~GhoVXwrs09G^tY*7$x`O7;sn`W5ZXWGi zeg~v++PQde3Q*2U`NscrX=YR2N_ju!^^`MGUQ9V1<;~Qrpq!PO6_l4#vxV~ZWM~ks z7a>5gfriin(8C((t=^ojhSiT;2{nU1o_*N0Y?Jn+m+z+9vo9J(J$_*Cz^pv}WT+d1 z?I=Op=r;Cfe3kfijXBp_?Q$pj{57`1!ju_ne)VtnZB=6n%1ev?o)D>jzWBtn+hZdW zY;UF068)CEcI_;!+u1^MysyZdyQDsC#+{SRm-DrDUS9R^u21)Ux%Ky5jcmsR7cV(F zcO!erqt3}MLjAqm?>&##TGOfiT>4?FePq%$OGT-!Q#s37S)Hq%SDR1&aOP?AQbJh# z0^8#5bERGD8{^#SjK-R3zIQqj|GKl+zT?Lwwrfl5iz>e@mCk#GjW74)$Q(J9U(6C; zzx$YH|FV0Yt&)43w;!lk)_YfELcVKdedws{#~l(G4EIwx-{cK?E|Tnj>c+!k9EPRV1~m;>lC1>a;$okOc#Vck%-qgGAL=dp272#%Z1z z+ObNlTIS7+Hmh9?JsX10?0VHTO1&k7t{Z%?Br9oz*3lFX=hedtcP%qp%k+G=v0}X5zIzI9lY4$#Fz&k*;vT%B zJmlU4(;27tMlMWF9q?6}X0?{N+Nt^e_WYaj@zJM5B}ZAjdfX2K2QrcRyVb>Y%r!@!};~=hU(73~SkM-}F3gP71j`)Gg29 zZS*B!DND^$^i8){2_6dfmztcir!+0HQ+Q0BuI)DelS3pJlU#0B?%sG}@x|Av|YC|Sv4vVN=}oGKlyIE zsyTYWkZnh4P{ zLX&V)WYX!xHVI3H6`g1VBF!M=B`p~N48lj!l34_(6uwQnZ8hg&gU08f|VF%*Xojl(S&E<*@;8B2OP;VolDzeMEASTSUW z5{)t-8A`~HAdMMeC3JTv)K4f$nlTu{MB@lcrin0(2D?Q%gHV$+W@bYQ4kT#aaAW!f zB7dY6{Rh!7(u!fsB;;9^^hUy)WyMs4O$ggeBU)sPnR6ip`vR!gEn!TD2T)lnCIhw= z*oHZFGp30W+hi@7TY+iGVS@I0LQc+#=>hwkE%4}AKYbIC54`KZgX0t^MD>$lCvzfm zU@8+*`j8SN)MSk5l|;V06|)z3_M99ejG5-Jg<}dzLn<9ooLP1#Z?5|~>_IL@tkfAgcRoF$o4@B zU7XV+kABHu%!TCO3CNsKk}(tWggcz8Fd{S}KwS$BV62fRxDZ<^3Fz;l4iplhj$O|IQd4ZQBq&H<4B9(ZaP`E>lbgbI*m4iSOl6T>z))uqrJW z%kg&soMPf_kV2ov&JqTfq zcnm3fqCnP6NMQmjHRvSb-gt*OzzRC$&CERDnv+_ zvWHyQ0s(+1*gOusOsWzj*ka^XG#J7Lfs*_7OffAQZ4N4iOBfamPFIux7gt&2f`@ZD zh^eE^Svb)wSg9c8geu8Z;3lN7YO|>t#t(ju_oY^e|0!5-_ILCbgfs5vn`<3`k zNWnjH7=HK5Q(*B7IXa#cQxbZLEC}lG5~IeFnE(>f!_kMN&Or)S473L?V=AANC@g>O zJdz2NL?|32Cge)L3+~__4x75M_(QmbaU=&w8ggqAlD14?Sh8>+1aKi{y7Q&V`BGhw z;tU`-BU2N$G4u$|3K1%z{URLt{Ig3;7_hJ zQw#>@Ulk0>6c#TVB&)D^&!AjuE7%?Wuy*Ai+#G$T*dI)o+2SLt|5d^LDNHe7;&8&) za0drghd-HEzU_~!$kl{3lwSdI(|}8QC8z~&=WHC~Yk}(tm7(wOUgv=?4g4+hK1@9D zX69tVT0&NQEO-zsc%)kj{aQR`0T-7G&aooC=P!MQLQQZP{IBIJX_2}q$W z@E`$m{1c?mPI41ts>8Tr4MWKK(zq$IKA~Dj%_f@Q3_5NaWiih2cu0YnU_YiVKnk_P ziY0H%9Do$AJe(E85S$g~Cv4smO@twV218Xc#*7L_qEW+=`H{;cbqVRh7H3Y@7zKB; zAv33rOOV1|;jC^yAcX_P>9I_heXgWTZM=yvB+wY>TIdEzSh&#`PU9RPg~o7Hra=m= z5hAo{g~LJDh!a|%&lVW{|wUNokX27Gl=%As_)AH_}cQX$}t2cTSoD<6z9Kn7Q>DCT2> zPmtGO6o+6gKo}5A=HjQQAcV{Ruc!j#Ne5v541fS2laprCvfu_Uq(cGgFag(K6czkM zO8;w$736U9Aw|b7;--un63ffg%s15F##7Z zMgA248*i=x@cK_E`p9*%G>4+T8vrrDBLJrB0hsRz052ZO@qc7s#s-waH5f$&Pf00< z3c-CdH~sHW)ZfC*j}&|C6^=hDZUZ2F3&0C07HG!=T!T@}|BjS$D9)1404&$dc=1wHjM|aX$pnXvgfxqrrYKSF zdWbCu0tK$%-=QLqe+)POU=$4;2l636wSl6cxL+~G*Svd1A1hv40^uIW?{Qq!ff%%l2dDjm z2rxHXe-z-b|06yQ;duZy_!0oq>>BtFFQmxeJ**%f)5yQVrT@uU1&Weq9Q2Xvn1Bl@ z=w<(R4lQKC|J^eS^nWv{mY-saTey6r*k!K(*nYsZGTtTpZ4U9^4G36af$$s2wgjF8uO<#b8iYPa02vERZH z=iG>$moah9V{$qSNDKfKAo9IjK&_=Wi|F^V7UK~K5suy{>IBhYZ$8mbqK_2q^kEV6 zeE396qSOaVx%#pQabG@>DB{3Sl}QMbAN0FrTlK8qrRQ)(5eO z=|Oy=QABacgO^W=&VZ6fmM0!L-IF=d~0prClG9ct4P;?GN z%Odzn84))~QA{L^S0tavm`IJpQpyWpy!b_?gvJ6C-38HK3;0Tz5e=j$Eeggfice%g ztd7D`I?*s*{30vDG#W+EK~xaUSIUNHCq?UHV7y}ZM0P}e43@Hth4JDSO(7g(QPc^d z!?Ap&rV@RmXlERZR~(il6dvuWPJy(;A&Zv+d8&0p%T!PJ~SKVE7n z&pLqb7^zF}n3~V=65+~&-!53Shc0QVO4F4t8S*?S^PN%t!m^+zdRhx!$NFDCFaIj} z#^k>yAE-86F#K_0yMrhzx3{iw+9%c-@RPgTSr{exmJ77_?mC|^svj@gY?Wz zHO$r~?;WxBMTXSN4(<329gD|w`nw(M**wSd?u^fc4W+T!umc%cw38Vxvl4`y0R?cZ;y6@+a|HtYkPe!XIOlmeqoE% zx0`j}-IDYiz6OtaaLRe&>Sysgn*GM_?cZrF{9syLQuL36qa9CG96LSVF^G=q_Lb{S zL|=QyV;y`Z@aqy38f8~dozF}ohw&V9UqPi*%6g5n_A_6Z99`R&C;A!IXilO&4HHA zwohNWpS5}UmeuAg1Z=)rT;^83U193|^W`1H z+!ayLUFR$O_P`Rdki{0f_aJ-m$2QZ-oIRJ@T7&%#WL_B|JJ2YqRDb4mija%UmB1Y( z`;P4u>Nu;eT^r;4y8Ncl1*^kKE!#v)KGe})-{LWkdgMeK1w+G}8>{y8H7^Vjj`B6R zUZ5hn`Kh;Jz*{@tT`G4YiW18A+1%>1d?#;g-`ah@SbnrZ*8b#&KJMQuOCCLl%76No z2P|U?S;UA0zUl5u1SX*4r!8W!MKq_MdeUt+_gm?iaqg+5?$v%in~e*Vsx|J)%~;l< z_qb%Ys8+1&Nwv;_;Bo%WsS;UIbr-^-P3Ki!o~?f(@vqNZ%kkCFykFf4mcNk~SjH}# zc(Rdh+PFMq!@i@@msDy$RP5Kic|bAv%q#5=)k?C;r-bBtmAvu`dSCjwY{>uieEOuv zA$NA|FxbL+VaFhJQdln9$xCdntiGbKZvLn3YfW4FrqAEBM&r3Sky4a6b9naaWlz+M z&BtjG3!3O#t0x@jmn}$}_#$BArr0CLP4bH$yHxXZtUpnj>%uvv(1g>$)&PROk;@RK zAtR9RrWk@GV?J?|V$g)GAx99wB!1~o1x7Ft_?rP~S_pCaH$xH_p@h^Xst5xaVMN3) z27Uvc7EWBG80jPNTB7!6%nn43ZH;)KUzZ zFuoa%B{cGW6+uQEkwGzN_*Gb1JkdZg1Zl`fAoR9Uh6&??Q6jOLVt^bNNkj|9fG0v= zBon6j+#>1ZL*harH{XSKtz86)sY|fc`r!EP8-HIqHNd#l@9K7E){jK#xw}7~Fm>&% zj7#qJGfY^JuAz19&3E-RPvW<%ydrHrv1T?cvu}HKkoDhsn!8{+X0X`%pIBIk8601% zS-9#|M#7SolkeNU8a%w8zj?HnkhS53;vvObPZx<>n=QKQ=Iqzi-XdpxZuNngqIx51 z!Y|8s4*SXDHK~+oKm{e8mQ9j-NDMKL9QQ3&c6@c$?Oiv;j|9eS+R^H|DB@&p|CDEp zmq$0s%eM-DSsLN^?2+u9AwI`vDEaMK;_<+cXw-)32b(g^cBr-tTo8>U9BeGd#_|m)Z(CeL8Y>T8Zwc}Ui*ga+<$X9uX#&& z&2tD$V$>KcZ#sQ&@&WBTX?jQ7vYsXF(OWq3yNO@ofqS8@LQ6mAxG2i^*SQ6c9m+`i zP(SI>WL>$XN5&qN>m54$lqW0+%USI1XGMFAQa#3}Y!NYaT0ifkMB)9dO#fvf!gb~= zgcF|>T0h<)aR0{QO1+yG2MVW;J+pMEr%=70;X8|zonJ~c%z5YBGG6ms8|NApJQgrt zB!5K9Tk%H3?7a?mbmmA-3ScV5dbKsImbjO3X0ecvynw24;r0&6Y`>*_rru>T^Ny7{ z=iSWNTmYZ1>#Sh0k0c5o8B>!QcQ;hIb)`4^`V4QaSGJ4ts?UZSHXN$A5AU1S-ML5k ztU!S9&1G^gJlQ#M{$Ux-UWMy5E+mQZQ&@VS4@IQr#q z(X-%HLWxdSl7vbXK7>`hd&Xm425;{btV%g@DbD2W6Sdl}ciRSfHC9dVmL2I6Kg#=} zvPQ|OMfxW;*YuxzEuU0S;rjTqJHbd_|6}`zkefk61-kvh-#cxBC%{Y=+dpLF+h@jw zU)QA@C46_PJ3xPZo;^F5>3uk^prAT)RcXb0!vjibV)+8{juuy)j8#gZh%;8 zNXg5!4rcgQ!qg#Y1+RH>LOqV7#&1kfcyXjWtI>V#jFap~OJ|y=?>X&N-E#DERiW&n z-A1N;@|(04OsM-5mObVtU1UeYyPm6wW?}*%e&H+Nq?^TJuRfYI`Fz%z&lZ~+n|tF| zFZ2B2wQbklc7~R*$$90Ydas@>DIe9V-*IAp*A*FddR-m8-96D~EUnM+!*TYQ_7m@U z%**06kLADSqr(f+T$3+P!z~+*x{I{W+chmOfpPYr=*spR6^Hc*tFt-&-{nugTekG{ z#|b5NW#QjebgpSKx40qja7BcC8=SVY;F!3B@0hrfw~9(-o945d`dZc=ob_mB^&R^F zpZyc0O-Cr-oUgw~R;XA^_e;}(>jyhdeVMdw`^vNFx|d2Sg9;a0OZTKW?J#!`1^3yR z&0;5YX`8;B$a>@wpT&L@`a}5J2>OzUx0lv!zIx&0%6SI@54i0tw8@>5Q$IbY!GH1h z={XC0oU&dFe4cKk*?joX#}hmqu$tGp5n>CUx>P=C{mXTK#*Jo&lH{%TR*vae8_+hMdq!CJZ$JUFamu|NA1tf^k1 zP%-Du_L2oQy4en^0@u9x@anMrnx>!ORnxw0aI0gu$v(|=cPcggFxjIC#6^@^6X;To-3M-t0Kp56`EL;Eq!J2BXFMqFz*>_-^qjX60hD%7xhgDi#E)k zf7oaER)@YN;f$UYZwUIxulkdP=?@oYRJT2;x7vK*OmXA671{>E+08!}wr^Z>n#Vlq zQ+>2jaCv{jls(cj7ritW7C&|>!(sOp>vIQA%}r`qA>sR@rYlkO==87KB$vxaf4%#5 z{}dgwCq-kI-c!B2{GclPd-$S<^=F|9*UB&JZW-BozI~`V)2Og7JLjxz-x}{m4NV^NHt_b|mMLEp zH-Ef-P2-3}U!cR>#=A9TIqHf^75)C1^+z8bo*BN;`C|K-b@F#CMlAC?|9SuPh^l15 zPu3)x=#2Ydy6pN|cnDg}VjGNTn(`_)GpK5|yWgV|4g<^02Ev@@++Q|&Q@8Q0hRIS( z(r3B1&AZ(d>+o?$sD}S)(fsU^0?Bj5btY8|i?VT?&loumP`SM3RoryAz9BJ2>p}OH zM}KDvzC8En@8H*d-&<=fhBS?u7BGF={@AN+3K{YX{MUSbQF+-wbxq8`Ayc%6a89?ev~Ta_HoWktQLSGz^JpFG zMrwz-Yfw;|U_n%N&XN5~ZCFih@?L7@0dZk|_a>}%a7=u(B7VNI=)y|HbTKwOL4kR5 z56z)zUz_#)`;oHV+x;U%lV8jBw4YRRB7n%RfGq{_1%}cMir12nmz|xGN);igC`nxBN`B$&HBp9mCZ;M`j z?dG(Al%`R$BFD%tc(!?+mjTmp%+XC^zB|MF9#yF=>9HSEa~qzZ*0R{Q)&#E8xUBHJ ztSeu2qs_%{g5`BOWx9=%o|lKNRov&ZwQHE8O>f1Du4f&Qx87G(J49@`wcFfHYMgz| z^Tn(RM={QEjyyQ$@tT+0e6~a2LfPH5-p8N+C1BRSfqColgd6+AYc@12yjapF{MzJH zzWrot_Tohi*WxlfvdX1e;%rP8${auUX+*MP;FKV;-85l532)_9yx~yKgqn^8BJUHH zcUrwL)jpF|wtzYDK*hH1JkzAV=LW5yB^b!cmMRL)n{0Zh$$r9~A0JAT_SqC^h1OlX zFgBjE5^@g1`S@`PbqS8Dzm`E99V2vt=@&5l$-Y~P4-}4x-Z{~F_^q4I(0kV&eSbgW zdrEfg*Q;+$uIs1hob_GhSF3)wSV=2zJ2AFXa4OeeJP>cAlmZnL){R)U@u0Jg?4gyB z&UX#oo#~k|X|yrxQyL#U-<{XPnqx zSRsucDAVDbjf%JPDz^LJp8E7oRFB}v9;cn!;l|I0U8(9c9J+cVz3tG^nX>){Rnc*` z-+JZ_%{6ApUOg`VHLR}UuIc;xs*e*li&zvXpq75rO9<>9cu z?l5))G;FOoypFA>J@4N3h^N}Y{WtdUn75bLyrctZqO0<}$|ibdhX#&I8ZXty>P=S8 zc0YP-=@TI*`;L3k8$?^(Jmd#Hea!NWs1eb-5#iVtk_T{XwRaJwV@ zgUl!modXs z=|H-Xbe%x6e1+ND8yg>-ysyxH|5R3wMY5kyne($B&HInQ$qvlxn*GAN>sEBN&Wo3e zO%4vz&zU!4HOpN0;+IRo4aXL5Q;A7rh3t5G?a_yWyH|^BeKGx-_^}O^kM6uYyX*be z+XW_8JmwYinzu~qfx>kw`Edie<5TN~WJ^7KyEt5B!#VjW%idplQgZr?dhtd(EwL5y z!)uf4<8Q0ox*M@*z38s*PDJ3^s;ukE4{P99n9E{!%=*&Qw|7rnsr&?6-#UA1(L<75 zmeLc)yq2QjK$CI!-x0t&3HCM;Egh z7nx>f`60S#vW2Id%Wa-^A0Tcdxv+T{2YG91Z;N_WW$bf*PkD0b7wzns6O|4)Ijl|h zbhfw}S(4fQzBAS}`HB7Uiez0k<>haeZ?|=rWV!I?R?Cs|S6!MqR9A~f?;&2jSK2)m zs9k%pKVW#}>};mA`f2(Kq6&9*j6dY0FRgjF zyy?xDmA+m2!W-wqdqv)((~ZcGGVCtTw%F zxFA`_Dcc~j?^vGbT5o~E=W}$x(|yojl&O8UW=>RZ#6<7CJc^GJYRTO9 zL7a``7?H})I8M~^Gfog13;F!2g4ek5m;FXKbq`&eCh;<3qv0ZrHAi;eTsW`jxY^EG zamxEM1eBibe>S{*%M8s&bIO0-y=EWP@Tk35&f{J1swCFg$zM3TImeVrUcI~a?KIbl z-1j_dQ`OZup0n5wl%MAG?H6twbIs&7d&S(l>2GW$ic@PhUOp58pgt5{1v(ZXF&vODe827{hB zL#ZbHdFM4~irGFJUcJs%nlIw@v_G@_-RSade|OGYdA0iBtoAR9_0H@PSsJNuSB>`L zIFH`byn63Bcg)jb>%`ke(;kl8{KU7SYRQmsH!n4LN*C^2tfTZrxt)>@m)Ej#W9k<-#R`W6En^&JF^nPTGIecX5))Ka-r_CpepsBCdcS+Fb9VeTeOl9z!o0Q@z zUd0{pcP^*>?f=Z7ZNBc+X)*be^?qEEGOF1%ta^6&t%XG*Uz-Bk-`=d<^!$pDT7<~9 zf!#Atu+EvRo+kA<;)Y&V)m**g`B8A5_$XReu;%xZ+E?5YzD%{be7(dn^T{&* zvjpw+Ly2jxw?DtNDLV8;G^@XL>$BS9iBp!pP%p2wS^V9<^_qH2xC-YGO}>vk%WJUK zo{);WqMi>#ly6>HvSNtgx1{V{R|GsuPo`Y1r|TP}-t=I9-edN&-;$Ldyy(f4uVdbd zDY>M{Prp)E!aC^hxUZSCnEXMNUtfSi{em*uZ`_1-SNrHxdRcFzzvvp6dckwk#Qjee zetOn#*m=1$R!4iF=toninv-MHPxE=*E=o?1AC`$qJ{YxeM~UF-l3#kE{pWctZd4tY z;c(IX=BXF;=kEP%9{yqWkU8P{1yQd;4!Mt?pjdrt+@q4<%D`fwvYXL5bsu{DLKiL1 zZM)-dV!8VA_d4NZQn4WUMg9xCdW#el63aW&d^e?p1v$EUtbN(zv_j~Wp%t6;W$g5^ zKa>)yYjbI1BR}Q*IKJ0I#i)FI1)Gjtn-}Tq1c&=h_4BACr#p8#Mtab?B ztvoz;@X%dbXL!^<&AFC-=F_TR!q)O`8+#u>Qt2)feSAFPGh6B{#-NyY9>E zji_U;Y&*if%vFr1^Gm#nKf4I-8h2$t%E@3~=EJ!eO7}dLIlSJIKJwab#~e@id4Q7a z7cPm`QnGd?9i3Iajn^O4kDn>+{O-!irG{RTqIa{odecE?4X@rC>(V}%kH6@w?b)aq zca1$g&vcCTsu@>JTa^NxGUQjj&j@jIIN`D2A`J83jyO7qd}-&dT_ zcq|`DDy9jmg3`;piW3%}jydLacc(d@w$~ul>^1}@L@*8ytsvV6QQSIVYwMOdb zKeeY#xoyVRwJ^7F)F)cYKs+v&^oRdTx+A2F{{)}rEsrkN{}voAK?` zV4P)OZpyL5kcO&px<$dUiw%?`Zr9dssbWb?H|?OY{kEI*OBb(8vNX*4zILVE+}JDK zP6xPMjGvz1*Y>DOu+{fm;gzD=RG)Rze5HDtZbc=$SrjqVbz`1ci>%_Y6=?^?4O_FX z&n#i5skq9whP~ImE^u_1uYWjB`t_8ILz})$=juf#zQIvSu0J%v)AkP%*Etv(eh*aD zEdKGdGPK3aIaBfoTgs^NON3G7NSV=!J!;+`(x+u!a5$^h`sv(ar^EBlv^d8#oRZmU z_PmBv%vptQQq}?$JhZ%E(d6Cv2fyoSl*nkTP&>D?A!bR&t0;r#s&gV1b^T~z&%HgN zb7Z)P@2)TQs~U6NWzSA#FCS(e-!Ai@$Y)&cM^Z88W3*emiWdtDXq4VKZluj@@H}pB z^G&JmLW6JHz;G|e39Vn}=KH2rb+w9bdoZtKyVoq4B^I^mwI8qjNIB-3_I-ti(>M`= z>IrQBZC=Hq^vIWykKxxj0#;p5+k7c{M0Z(1(B5JRHM_cqD1&GZlkhi>d-mo9)l44W zaM|bcf4Pl-qS7rX&Za-*7qr0f9(sG8FFAq(uQY~9k~|cI&qg* zu_|FQdb?=WNX;Hr^hC`W+G_p8H@&F~-$vNVu1cjX5REf@eRKGwl9{($&Q+IWJRbg0 z$Wo{;bmg5ng++#EC$W9)xQf$3={;V>n}ib*<`>U;mmBcZz2czTRF8W${Ure!Qu6j$ zJFl&v?S1{p_w=-PBK;jv^E!R*_n77+3|+GK>!{v+J0HY8W%Y1Ay61epd7oEr_2`)& zM)llbZ`TO)vts{faxOZM9I(`6jgP~Tw&)~1m%ulp6f_*3?Akv?QS63?@SDAx7VGNl z5q^JowZ^-TH->*L;~I?9u9jEvyBo~{4FONmj_2m-7e^mH>bvMuM4bym(wIlR( z&ZMXa$Q)T@bo}$Aljn;RSHg40+9QZeUa%{pGi@eRbYLqVHf zS!ERL`0LyK$ql;|GHx6%2|O3w<~`cZvYx$ZAbiQi+3~L`?r+&RT6>W!DY0?H_tV*)~s;^>p3d*Qam$ zQj_*MKIY5j7fQ$8jtbOXYz%3S@xY*<1XFalCi7zdz@Xf#Pb8EhV6U! zc_okHN4$zXu8eM~UU9oPsoA9@tinXBJ>F*5m*&kPwtnZWs@457rk|bK)|Bw|Wof@} z&e(%Or>CenK0R)I>W;e3HJWLbI)3?;I$70|dO7P4P4JR*lh;+z26N#D*TX)4jijG> zw0OY@t@^(Vx6JvFc&2oZ64UtcnWv(eH%2Ad&8aJivU|E}O{7VG(Xjl~@q3mPi}NUc z%xm!7mTRkmpSXxe1r&Y^esTC!LVozNlWW(ldYGsi^SbTn3ikX(o|Cn7#5v% zdwjuDHDt&9XQxHpnCx30nlWSnk7E2HG<6AXwP)x@F{_p>*{$_Mbm%nGXDTm-tFwKh zSKSkJee*dnWpb{{S&NR7e?JrbWN|&vV#@ol+VrrIZ?7D^v-Os`^GbEDULpL>V*_zI zf$!m^k&sH{V>~4y_!-ZLi~NixLN1A~$a5l|pYejoSjd;7nP}i=v=DlW@CzL4_pyl0 zi}=3V@semEMV9+n#FP|1(Q6_%1xs~;=nKE-4PldtqMgMoVqYp>sdl0(mCu^L3CG2J zjCaIte#U#EkDt*&xG&)=(n*x^Gd>XXrF=O)65jlbF5)OZ;}gML##iJs5y;QzCQkD+ zz7SGrd_}$z5&VoE;vzqzmyl!g73m}5`5E7c8~lv#gjzaZksm}VKckgut~iWtTv6mAQM&TRJ+}UpN-K~04}v3i3-w*T!ZW``dG)fS#MEt_ zcCJhin7NJVBuI#D*W>LaF*@P2ozHS{I%dwxb%xd9>w7^M)_S-De@6ho=zlC9L}*?fll0{IGm?NMLMCro3E4-omfn9 zIOii}I#EY)I3K8}(1{6qxTQGzuqvHcPH{NjFH!@Kr8t}iJ$3Ndy;NP~H;hJs$5I^5 z2Op!sV<`@_9Y%T#cx(~36dbC61D^+Oq&S?j>^Sh)B0k?6&+8+!r6H;Kt$1!tY>0*(O0(gsnov@vVP{rE{^ET_GXMJdOWQ%FhzMxebv<4Z{BqeyK^a% z9SXXr-B5#9@Ar)JexXGvl3LZdH)zBd6pZ9ms?&Z$BZJZ---@3)`uy(V=D zE?9kF`J*U{aO2SrLTu`4hdV4D2z)p2y78faUCsV^of^F_t}vq81~ybKdy`q%6JOb? zr&q-6_beRk?zpd3Q7#&PO3?#;9ZQhD^|)!tW1-^#B{1JrWpy`7iXod&tPmFRSAt&_ zI38cNf`P1I)Gu2HMMdaDCW3-WWQ$WjP<=oXf2b+GYzfJwetp^r{-%%#+?gbo`mO0; z_zOeS466mIpneg0t|6!Z_8bnI`dwxTQ~Wuv__8p}%x75@nNfuLS>rexBBm^o2q+U$ z<^FK-TU*k3(_!$*A2bk!zvkDqgCC&*b()Y-j4{&wcuZM6*+OwM__ILx1L5>3KqVmu z*%ZR$n2dx4l*h93BVu%d$i}DsVT={yxXYPTt>Yh0&SebxUqFcoUDSF4XB_QegzvYR zZ+t7r9{9<*aCAQbFa!SmFqG$oT<<4B%sB!yq44zy!?Y-b({TWAG2F@yoNQ7^f8c zBY&2{9B<&l>41$81K=M;LvPYFfzt8UkK^zTE>&GR@$}6YLh!8#F}htw9siOx6@V`W zX9MN{ya0HvR|LQcH0hl(I|qv5_XzOA`b_}*RD25nKYqqfis4gT@&j1>taJweKa$)D zCR>;~)s;5Rwe1JVKbhnVvKegJO(_!C#Xje}BUY zs5QU_U<&|`!{6wEUr=GTE7+L89k?|9j1B&%4E}5l`FFu1fMQp&0I~o%fII-EA^tKu z{u(R06q!U0UiJz0`N@_zLdc?Nk#ZE{XV#v z25+Fj3&1~#cL#U?Tmd!!TYw?J2w)7*2J}Fg zpMWobZa^#GCDc!A1~dX50nP!k0c!!%0XhIpfCgX!U_8JNl;Pjg{LU^QS3AOQG*fcbzR0Dc}G0>JGn3=j^807L>70HOfVfEYk50AB>)>(c~4A|MHX z?`bE~!EbSLTLSQtL^o^^T;`L=dF_t=gT5pJz!eKSuK_R@-~+&pL%+mP#QB2r24@R) zq8dODAOK(hpg2vHV7^yj<9rzoU;=~zLjXd6p#UksFn~Bf41mj&C_oY*0gwjZoRb0I z{6mU*aQ*2`bsN=$ps`LSR0A0XD z0JLMC3WaPJP7 z0Wbi#0kA?Zz#M=lU>0B|zymNF;0u@nz$zXBaFXE)fvd+BKnb82fbOyqkPXNJWCB*; zmt>a1O$GqxK^njhFb{wREd#m;;15^`NCqSU5&;Q-ct9K=77zo721EfC03rbqfN($< zAQTV+2nGZJ<^uw;)&RIk0W1bA0i<&8mjcC#(*dgis{q(D`vK@PxqzL39RO_UCIA80 z2*?3!0IUV91FQ%91;8>^fX#sIfNfAexvOjing=KV>;n`53ITfny8ycZ*mZjW*fV%l z!2MA`IiL)12yhTk3cwyb05}Xd0zla@z;VC{0Omuv)M@zd6yPM_44{fw_Hh*ZCfwZs zTnAhOTm@VKTn5wtE&(nAE&$F0aM3yocmj9~s0Ta(2t!8=1z?-M0=@uV0Gr^ZyONJnsRWfDXV1 zKo{UM0QF(MZU7yO#8LeTWB|RQA8x(_`T)Iv9>6!i55N$}CgXHl=>G~J!Rib$GwZC}4-!)yT^&6=+BCwd%YcnB zeK4dCH(o&($PILijdXN>MgO598!%9(iH;FxGRi`2RBQ!OVoFN5k%OqH2n+@oMqW5C z(ZUU5cQ0MN)BOxt#=t-sDx%^d+3kAS``n@-i@}J3STKpOWUM4Ec7@Tm5*nWj)rV#pqmJB9a`ar{ApF0Lxbi8Cewfhy zEJr^_O#N&?e?-jqBB$^QN=rfMV_}si=SOQEAWIv9X|ylIkMTOu13AYYKabj4nEJH(_7~kun zNX6M|kTcK|VQ!d8_Ayz-6iFhd*M;FJNj&eBRS1?OH~e(P`X5h?=Tu-m97U*UAtBW# zM_*28_Q@$w;lHlOs12)qP+38?|5q*ocIAA|!swDNxb7B<)rm zx?QtJhO`1E8Z4W{#BXv6RJ5X8K}<*r-YALBrM=e3caWh^Ao;M}h6Dz|Jfh{idGcH?=kg{j zqh~_6{4gQvei#xvf5_9v5Eq7B?_3@r!BJFqV|DkRWBC1+1aKvvB3(DCu6%jnwH4B|AX{*E9~uV}7Otfr zkpOLHZ&x@7=R5dv%XoljEr?(o(-+Rt5*l+iYdsdiWl5K|9VF5q@hunj3W>;!l0i>1 z!~r-A5*d(8nKEtNQW?Q;P8sx&i^TNra&Gk?9|3a7>Hc|L9y`r`oA{0^+0z@ND|lT# z`nLp+gt!I@g*h#FA*$JeIt@tUgJ~PT%Q6&b#O-ggZnBUK7j6yv8f&R5C1e}*$!udl zHTF0an1(?&fmK;&60H54NOy@;Xd3V&Sc|YBM_75)l5|-EDx}>8qYooLC4O4R_%+~E z2skRd4a^xM9sNlex{RHz(s=8`jo z3OGjv(;;V?u8s#3qGHlf@pi~*LQYmH8XXmBhn((uu!Yg6_;XZj9kN4kiz26vfqrrf z8Lp02HlI+Dnj{y@peMqH33DKwdNo9aDIpCp&@sqU6{O3sp?$_WdZsW-63GZ)RMZyI zCVhP!lV8!%IALqt2nM|XG~$hbif6uKxR~c8B#I{t)pS2MXaT2>nk*h~tLtsRfCy&LosjG32P=K4cj^+=i(rb5!^rkl@M*WvDoG z7$As@5Co!0I#3my^uQFHbW{u>QXQNk;H0F;31&w{8N#KDoLDp}>>U-42qgwOxR_9Z z@u-MJpbd0k{{(`zu~1Jnn;Q{{D}susxPny7BthH@4A2Cyjqd;?LthDM@Bl$+P9~EqgTMzUCCn%O~FpY}4Bl&9r?NQ>aBF~?nFF2@w)>IIeX;U3NecB17 zJTXzatRfYtii*YaJI8aSJZs1?i3)8+Md|sSg9?5{h1dBlkyp-Jk7cQFSyaHD-#Ms& zT2$zr-;&wNdC$-?Dg+l5#OHU8Wy*PyVxXN0+C{|~`kjLc>qP|)`Yk!9oacvSsc>~v zh@sy(s5o;}aG>9kZ_0UxpsXSl@{Sv)Xw~l=YASinsGSOqcbyy2=y#5(DxAX?6{_gB z#78AhN1QIprlK}d(X?Qoxl0rkzmbZoMY;evd8mkvRHQAAbKnG~Vn0$bxj+I-0ZyKO z(8dN)#W>F7p#pY6A>4dmf2IPk{po7d8+9s(7nI?eO+|mCqH>YiKn4#Qn&eB2bSiEa zN`%S%gxVbBsDO%Oq$>z?1zEs)Sm@xOEXOvWQSaudkX@u4H&7ZC%8`n(1rnHiIQ^)g zk5n`+kifQL0yCX@r$5LF$&EBgg#d#xx;kuQtcVImNks_*5mbX+OogkY;)sC+HU}(2 z1-7Ijj)BC4vv5(NFR7ShWJToqq^3o_K24{hnK9_GcvirAHiHN^W-7oLXXP>l2^CS3 z3UvnRa9CjFqGE1R!O%$Kpd#JD`xJI_T_U$n7X5YuA(l}f~i5Gn%+2lq` zeMUotQ-dt zHgpuZod5GiNQD-qV!m-JGSsKhVKWqgwudK{{ro0zFapkm+@q8zNbtaV-$-Vb_p|Py zcz%PHg0oX`LAkNq*zg39hR+Ew4d!!0ie8X;GUQCn8x>MQZjn^bQ7Rf7NQ^jTsPLpz z+&GYcr(@4iflR3gb0Faw$BlKGjxp%i7=(^n5fq}rqEg}NNC|nm_~*@&3ZY7cwj;Zn zoNiPMRVuh0DIrf|R5aCKZEl=9?idYfH`S>>jEVIFvTQ2Ss-TzttUE&vT!Et4wJ;5+ z*sBvJPW#d8EAt5?upnS(@HUy#VrnrQ)Ot98z*|r7Y#c)>svgIB12*WRg1AzF_dtZc z2agw2m{%$^A6Eh~gPu4@69zqLkdi^q5u}9Y{J6) zAEM&*QZW{PyCWIV7q>Sm3?nHrLUmL`*?TWDbX`WyeZru_VFza(EyIq8>vv(Su_Mm) z>oW*D;uDYpJ7VmBi{hY@k9=En)1G)XAp1`*-t={8QS|aSb^AuJlDiB{oH?LYSm6(QMm;-x)uH*4_i5_Skwe`adlU9zk*=LM@*Gm~z;O>d)6O|hkpbgDMK7**btrzfp~djGW!}ztZjgf!;+z-B z;IsvrF3X5_&O61RhcdKW^Hc@sE{vb9c`*WX*}P5SbZHSd=fNYBF)hR`&r*;s8%c%o zr6PTTddPtmtac+mTT35Se0!gJUekP#=<68aBPS2FutW#%HNW_st0L&TgU#&6v$fie+s*r z*f^3ZtjF$>WW#FX#6NL7{>QN$r#$`{{}Q6H^RuuCOF|+c!~uGyYsS6q>27!R_$NZj z?g5Dl5?m<6fuF-7(C&dCAtQu95!wS6vYeI^f*Z%I#Ek=d^}2hed+eA~SG{`m>b-hD z_3G7F{cO;kSCTi%OSak_&SSrMa~^xOlnh`c1p$TA?h)_a!CFbB5BpT{L&G z>`C84)yS$?N*0w2cV*~GrS#9cXs0`V_b7^D+z`fd^k_GY>bt{q`f&I>{kwa>#F41} zsLWK0L0#?#zEC@^6G{DSk}f@VoeH`|{CG=xQ8@@5)v%IY1^?t-h6(m!c^{0-I(~`@ zUaHb92EK~wsFRib00g8f8!`$HidNv1(Wyou{_VP1AgG%)%Ykd*KeDT;<;tRwtlx-M zjvTwfTagv=$t3mmS$j$-dplWFgtii9n%H`|ke;cIk0dqJk7(7h!nhgoLQqgr`Cf@Wjm&}`) zG?bHF5eyJWob{7F8q;5bX7qz08l*-2vl$vUsyd^e4N=caNV{9wJEOaXsV~7fYwDk% zX+HXo6EtBajl2ZS>Ww)X?Q1g>iH;-8R3wVIXdZc~pFPBlOiNdbc~ZZv1dB zZ7%4IJ{o$=Z-1DVf<^rJXLi5~8tYtH6`oVyirj-TU&O@x%0g*%d9k#%Ji9VySC(t= zIJWiG1#AF9w6S|`HyqDlt5?#kDW~p9Tlr1clW15$#kIEO<3+J&smA_3a_=o}n%qW2 zz6hl;;-oS=YwS6U)Nziaw%}HFc4T{3SW3xAY+0U-r_FLwf)Rn+tUI16LitkEUB6;o zca;cy_n_vu?(t|1-)_PgTUe=Mvw-^D2`cm|xeK3A*v0e;CQ!B1o$XgvS0$KAPPTIEG9BE4O4k6$9o%5Kz9vL;Tt*X@xT$PF&NgCxEr2(Ln&ba?zRjY zR^5aX7B{y{Gjd$Ti(JVr!r;y&7zjnIvipqY765Lp52=>*Cp|Q}lwBHF*{%_NX?D%X z9$PVtSX5z3Rec{~UuMg0!7#Z43*i9;LA}#QBUg`6DD!kw&!QAZ5MxN|c|&3g&KNK# zv@JUWH;l72&Y$EZnBD0(Vora5n#Nxr;H zqVByz;?*`lZ{DGq5uA=J=-Z)V*X5pc-6EKY!*qalhDit>JjeO}@Nbuvpfvl^ykda&Q86Er@a-yz`S0WF)o(%w`c zwL8ct@Qld1L$GDIp6{i?IirIXCN@@g_0U*POGg-LwYBv*y4;(p5hE>?iUobHhsJvl z4!l6{(ULadlf&IKKE?%Q%;7JL)!d(rC4rlyBySG*!ec)~K(&FGK(SwWJ38vgHgd0U z&+&v6HkL~#j2}PfI$m=>ox9qZ`Pw2LKYlJ&wSDA*%O$pi&mT#gZP7-_2-OB=!wsmA{?AUQq{*My9Qe;2Jgw~h@K5jpp5}Dh9i6th=Az`k`P(SUVfytwA zP&f+Yqh=v}pa4qR#CFZiE-e|Ja7prTCv&Ah=?vizmn5aM3e1@iAeZ(cJWPOFD?C6y+VXrP zWUli_a?fO6L*usXgaOE;uMurBfm8JxR#e^Mt8FYHJnJz+Mi#cXG*B7=FV>bmO|&Fb&mqQOk}$8XUNE+tv9HsKYoKQwnk_5?HknB>Q3pu+{i8Y%bV#a7`heXF>Bkm(}8r| zgj4Qf$5|3CtZW zG3cjvXyi4#ZkP!*AJ(?Sm6p#SK5-$&e7S1!IzO8LjlZzk>qz&y%qt6E=7S3aE@Fr8 zP0_DO|M@x<4nLfs56;e*=WNK3)(TL6a1=s**$$mYk8lV?(rOedp^p-p{_YAc)El=E zsh__|ujn7G(5r|4`67LrOb{76{MQ}&)2F**T&x;5kW!*>XD*rMDt<~oEW@6FKB(V+ tKog(Z!@c$)N=N^FK(lK{(w?MV2x95oiYtAHk(|tNmC4F`WIdwC~E)5 z)XC@WZnMaJ_onfI=TnP%oy{>k9>s5{eMu9tGK%Nudd$u1<sWR+;W#xI zQwXbqS~5$%y8Tj9j7btwAXus>B-ft+~{Zv_!FgTp@Zz{%MJ6ae6(+r6qb~j7-xS z^m|Yn^=L$fHigC_7d#m*0i~YJk|`W?DH%arJ}ROS0-u(en#64cLk%vI8<;CoU23{E zL8s>&F%qP&0*b1#%E|OKqOHc6a9MY&u)!)9!v*j(Xa%4YPb6X?s!bZH*Tr$%M2r%} zH5wFs%o+kp_FX~kL0iiD_|()PIxWZfgD3wm)I;%l$n`zOXpvu9x;7(2r%kUR=?z&U z$`_a)dm=9M`+_Q(wvi0IEponJ#6JHeHvY*QFrv#6cNXU`IpS5j~_;tqWVaQR&Z`_*KT+1F$-E&J1*)K>JXgvZXe{%!K`DTOAZf@ff~6jB z1W)p<;K|=EL>fU0P#T&4a6}CMJS1|D{{M`~050nvhr|*+rURmHQ)$tx1f^Y~(|Zgb z6qi9OTRL!>Ni+_WdVCL*7FC0UR|hV=9=pIJ;NH{LEer)aDs)y=4Y@V2U#~7_Zw_rZ z{Y8zN)h@AZRlUu-_kZ^FC&6e>-Ow&sYBo23-6q79kwR7S@m5h+U$@;7ktNdt6MzHx0+ z{Au;(TdMN6_YSsNSpC`Z=jMCH|1sd<>-K$Grk^jj@%RAWiGv0geYsliz??mMn{fTN zJ$gR<)as|B8f5hOW6Ie-0*{PoG_1wIp0i#!M0aedbj&j6iN@}h!=*%pU+SHx_kL2JDU`E{1~zCrtS3+yVhj%**jBz(XM>8n_G9+dDbL#Lfw|{doL?ZH$)at-RR>ip$0m%g$%<{l`E+J) z8?M?`j>Xu9sUP7+#UIw?*d3cd-ia04hO47ZI4(e}!Ob>MHOGX-Xu?bjA#B6?`i1f} zSg|Htm1N3d{K8bZAa-G@VpEo57sgw#V!Lp@6SKDuSFJT;G4{}yu^fA`Y5Q=~-sRC6 zTV@@qo`Vv#VMe%vp|O7@x?B z9mCbRIF_lS9E-CLR6hpSRjk6zI*<=#i=Dzv=inS|MKP;>slXhZ!_>90IdC_Cn&a#O z`FOV2Ib3}UvSyI+)Hd(N>|Mgu-LZLas30Dy`Z&0@q6Kl5*nqy^OxP;a(VW>=4_8lu ztc7HO-W19lyun#ulS-pWy@9=JIG@d;UBgv*m06B!n7VuwB>*yylsVCvpTVNt!qr|? zIj*x9j+=d;sR3LB^-6t^N+p4P1=k*2IYw6PaOZ=*+lQ*>p+r-xBDw0QC3C15X8I?D z4cRiNYvRakBUZ8sy&D1!hYYPAaGSwV&n4Hd)mTieFx3((mIHFrie0M}rnblVRO%iF zE=F`mXHEBkYsPM9Le-y9@`s#bZmxm653_d~lTo79WHrL8rm21nc)eBR7 zuw@SQ!&F@~EGIHdy#&WQnO0$L&Vg#)j-$^SimGOy+6NrXDVBj-pn5Dg8f~#5^-ge& z!I>~Omq7JO9dTx&K5!HYnnU+qgQHNScIwrTLNOHwNe8Y}w@TKD8^-hw zIH^@f+*&q64N`1!gKAb*S3BDy9#gPE66)dIN^T0Ku*iAp6 z6eONxKDBYv2aAf#ty7RW80xARsA_p_meW1V@;rnzkQ^@T1FkabVpWG0yLv1>-)TW% z5?uq;KY}j zF{egrl65SkG;SQbgXOeP=ETnli!br`zh{a@Z;+}>L_L_7U zSnPq6<`6;-3%0&g`+G?1Q$N);8c40K{wpG&mO5VItkfs$l*0VT;-McNHbc zu6CdzOF>DBZzoF9etu3R41$^lL0tzUNSt_HtKx!~Lys_3Q4ovi5vF#=Ow({lgQo=- zDGnY!A5@2enM2Pobq)B6%a+Aq2`II*3?N?wZ!Y9b8H zOHE8dNowK@N*IK`jZN^;CyDx?)In+=CCT9xO6^2hDAuZEHxVVt?z|$ibZE+P%`py~ zeAOx00t2C;vBaXsX}AR(?Kvzqmq5#x;4tu6v}t&ts^X<`Z^o|m4O3+{V-B&n4+N1N^9T~K#qR_bP=V7zYv3q zDDh;ZVKnKmWLNfNCwW<(rrTs{o2@H9!|p;=d;b7g6fTH;;PA*8^&x>9wf{ey^k|=k_6|Vw9zZ?44^Yp41OD|< zFZq={;o%~l`2W#KUkS$4Ftz&s_^78-{=Yx!S@h68JjeD7?TP;@r)${Q^au)?=e|_= zf*Q-Q!u0k~tVo`Ml zswOicqRZ@M+_sinCilOLkB!w74ryN5y>h?zSA%jgog2BEbw6LuY~#lv3qs81pKNY& zYKeN(q3uKOSzXAQd{DD+?)nR{vj-j8d#eFAbvUMdhK6b98a3X5ZJ$eVdCtUm?>AC) zV$u6abrY%s#;Pt%eSlQ6XJNb#7HFQ8hx)kxKoy%tr= z=V(~>AB@? znQXhUU>H-MFmf5r;!e<%Ezi-gqbH1Jb0o7Y6xYBKtbxJ^i!xheG{=4}#80`{7wBrA zG}4XbSo%rQZC#30V5~E6ti~zQ#V*sZNvDkbvN?7^)Ky-tVU123=_YV&{Au#bU#>Bn zfBo{tcoSXgYEf?{7p}`5^XNdz;F#G#y=z_RwB5?Q>pN$UK27&snLN|r-m>T%+y2X< zHp^_zKA4!?>%^i#v3oz9DASKgB^w>jtkuS7Chtw_Q`jVH)85Y?hCV#?b?~^+susEh z$#>ds-QRnAmrA$Rynm4GFlTG~mj}j%L^Li=FF2N`Ik#rYoChDwA9d`3GiZgz;68g< zosO!-bzhtrqmJ}=+~vW#;jiC}ee+;m=Iw$-`J=W(RTwyXidiI{?-R;Y6-|dgh`)+d69ydOiXNG>A zV6*A3+=lnNM9>3`a?_(cGEj&nv1#T9s<`+)Ry|%@@%U!XW({q2jCnTW%JAM!zx*20 zqkaEIZ({dcZtv`na>&Vg?d^I^i(^jbFP^>Ht9SWMHWRHIZmGnvGYIeI#O0lzTlvK9 zn|LE)@$P30X6EE}d_LRlL8^80geR@ftv&F*j&NsJgl;Ui!Z9PydFIvyy51%Z;i}qh zZt6*sg2T&%H-qAmFO#9sx3fZinagp%K0P{SRLg2s>ldH0Ue|MC{jJWoUp8#2onL)c zC9~a|9d>P48ReYd@MFc^p&ORGc-r#v_#J7@ANQG^jDviY#&EDkxAkY&RlJk`;L7do zb2i;5^bx}6`i%4M;dgGk5In&q;>h0BPbU_vimvioR`}Yg8lTaQ`{HJLI~0dorCuN7 z;EQmS(VL~j25K_?t-e-tr^26?KDgZBUd{XAr$a4@KABy7TX_1&vU^MJKFPcLU2*!y z=MF7{7xp}w-!(D6Ps6s^Kc4ekRSOjCA)3d z3O2o;-Na)N_cp%|ZzGb_|qM?4siirCTBZ$G0Qsm=0cyJUwkU6JG5t{mGmk{ zT6^^Tr(7n*`&_c8)pB~d>gz?blyTFe3xXaL6lcA%_0}DuRG4tZ@$JH~t4%_r$$`I$|Drfsn>A3f4&;5u;8NGQ* zY@jBoXF7EL^ghww#mCI!P zkUEv^X6T0hl(`{kXV)9U9VRZhzvjo#UtSlVh)Av!(|F3-!xm%H=Qvos{8`^VKg=nz zUs}+S_*m<@A66CdU0s%q!u{MDjbUCRbGtR+i~NqRTvoUHpYA6Xzn@z5{JN_}Q8Qn; zMvdOHR>vVo$TJVt_+xFp>`!LaK(!mD!a{EA=q&E5H1ok#6A?K-&J@lk_HUQr8InR#S~B@It` z_ssG_Uhk``Umxn~;P>Ta>Zzq?>OB4!_@P+UZ(eMN3NQK~9wj_Mi4D}m`^l93M<@OH zwSB+bKZZ10xxdSM7iN8~>BzkiS8@#nd5%%9t`wZFbl$LQMdi_7=I`~amEyEBYH7{k zH;^_x6#img3)#+AMf zZ}YoVUGV*d3N=q;aEmOKPGOrr)fzw5qO<*_pZ?r^IX*_eW7qc^sSI zw_edDZQqpmG8N}gh4N)`wsN)BA=dM5EfpRPvWks(H)~MSTf@%TzL|QreX|w@H(^AP zzQfhrUWX>8JRAD8+5NTiy1mK#^~tT@&U%`A?!BJle+?BWLrgE=mCK~F%{uq_#lsh{ zu#~etksS}exp3;{JHKatWuKn=@Xy)HT+g>?|MmRd+`_<;{f?AlZT-uU);& zx4IU)nbkC5N0#JVxR=~;)-tPx?a!xgZJS}&YYSB*KT*>+1IlIcvD^EvxotaZ&dpku zJKXxtz~3+N>mO8eN~V^HS&a2}?%J+H|nf z!?jaZRJFm~$a;-o{gQRJ;?A^xo)G>lw26Jer^TJ`4I7p7bmor}s*ihB^m^_H`@(w- zZAP6=*s^w8HP^zRO0Hg(>()(rzH^G}lFz1B=*^N6-U=l)P!oQC+NGcJI}WSim6bAb z)Xc%p_Z0tep>aWS;Sc49eYg4SU)@!9D`IEtbGRIT=S!=APn|5YdfE&Zy236aNlQJH|xKUiUIyn@Vr`j2WDDVbQhRw*7jP32#-|@D5G0R=;h#wzuu~>0Jl*410BG z*$UHCvB!3_vTZ74r&qKXJ5wY&-%X2hdse`zy$Rrf^n)gLA#?@u zO-G#U*}(S4%qffADl9K36CS-{R4x;CW9sJvjW^BovOTUb-}hu(!OvSmj=robY_B-A z$J-XqZUyu<>L)D5JL?cf(icZrEiL9y zvuCmeXC48I&1`6?z!icmtguu7p4-aY7L>SDh2RG^vP7UF!8UfjL_j~m=C-qX-^ngn zqF@KhRs`Ztmbsm**jTWO`7A66o$A=lrYQmwv5r0Ljv`Qrf;<*1lmtd^D1T)06oHBO z%5pDztOz)B6IlGjnl6%EOvLZ0_OZo^03LXkx1YUM1aN@h0E=8KyWk}y1o>>0BFGZo zTOMQ|7PqIDxXKhALRq;?hFys2@I>#Ed}_%bosMe$JoF{xp4N7<@4M}l`h2Rlr~iq! zOJ2V_?bkV^=T6;U_p7TvZad{MGvk%xhPlm-dsw7izl4gEdTEs^mq|Ux(CE%5C!CoR z(;#bO)u#qteX;NSO}d^c{idTC2S#k%W3V{7ZLdRUkEx6L=JpKTwbyRr-~qK;9CE*H z9r)TmeHy)*RVw~jsTI`ZSf9)jHTDD-MT8x`Qt7~qVcj}CeY*A4?FqxIvLmD3y}TIS z?%BTk&)-zkrc^odZgu(C##Ob?{yeME%+9!5d@6hFA7w}4Fvon0+Le8HJi@W*#*(8P zyK5{t#<7qyMqbA`Hvde!vTqPhu#qd;m#w&vV^55oPI9c-S-Bc%f1Ki2jv|pZ2!28Q zyCRWxND;@{o-6T+z=)jT*y>V=D-iEdTEFKxwy{JaZj%cfQ(aJO z#1(uIzh71)(nh(&u^oy;+9|(qtinYjuge@8ph(QcdaiKnCq*J{ma81Ac1iY+7dU+uXvtUB+`z#!LgHyMA|Yp@mzmd_LBC@Esl*+ zB+{n2jpzHz?aIDryFo|Df3&gZu^pYqd|bP8TioJL ziLHICiyXID?+aL~X_K+_Rm|}vJDZH2Ja7Cl9e&6*_IE#gMjg}`es}xf<=kGuTYOr~ zJNfWUyJ|iG)!+ZP@Nu^}AyKh)0;^SVnD?pI?8vzmKd5=@plf^k6pfkGI^Ct!lu7C{ z=Z}oGOF%Trj-W3=l*{CVo5Q#z-FD|Yvh6c0D^z;5HgbK{>KWB~>Bj7gXj8SRx!smU z6V@M@=CEkM+W3H<)KiaCy6(YW>|593(od5rE@*Y(DL$hPYB~(G@~$?+(Rwj=ern65 z=We$@V}Haysokn49cMq;&ir5hxvJ3CJZ_7x%Zn~g2GzZ6_{-O9j_HUw)8h}n+;c6e zhwFR9qlEW>W7otFfs{jdgL&2H&CPFY-b{LP<5HkuZ)ei)4V&{ZJ3Wf8nBpgwChXi} zbK9o)QW0x3i@tTK0pRP~EWAK{@i@dZ)egVU+_~z0d4A{I>)tAIsqnC-^!FsMYO>1D z7rN0mlH@T!m*N>eSTCM|%AX^p;p=G&=JwkV3*`@vD%M3?N`(!1F$V7!=Dg$1W%nym zBXhqY%iwp8c{Q?OMRWS{3D>#bIKiYo-`Mcwt8|gD5WsmXeo0l9r(PW-0j`sPVxyq~ zu2TRN=>uUiK-X!2WOORd1n4RPNJd|X(vJV_#iX-xj_&wwDRCx4D^ga{Wt^^ z06znVfib{XfS&2-u+0Lpf$_iuU^p-W=n3=!XxGI6y#adAqVL1o0UdyjKqsIx&;{rU z1Op-T!+}r~8Usy$Fd!U=06YLszzgsOe1zPFyki*MrO?S|2~-2D0BgVoz^bA@QREx| zCxCuN;SbQ9*Z~|bgn02W9`q&RpTK+IBk%@z3(#ECT+-Yf2aW#?TdSm1d8!Ol)nSWQIQc(WjYL$GU*q|aw;ze z$afh)+llH_8dU0`K*(nmuo5t=!+&dmHNf|P6+k^AY7Nj@ppiHT8@(fh)je;1}Q$a8c;u&)0Oni{d@tKJXjxP^OPSU%kSAxL#BhS_Sa+`O3nK0N&m~ z15rh+i<;-RB~R~>IKZ5b_4f4j_Vn==@&kAWzJ^d7!29`mz|0b6${%{F$SlaSku*G1 z!5sHhW2PPZFc!vmH!Qk2nk-@wlKS1ab%G*0CNfUM??#UaF z++ZV|eLej>y|IRc?PNYvI0fRUBv~kK-2J;DS4S7Y&d1Z6>`aBP!F-~ll5Jt%g@*$+ z=yy#K4gAO;QOFKKMV*BuA!yf9I15g-)Kqv9!Vi?f@N@e6>ff3RD?@oZIeJIDU=aVL zc>DKm-SYhHe^v3rd^`~eMpk%E9a54lT)DF5aL>(^?GXo}AcLBMTVuo#EQEqMDoGkX zKKqjLZc{;_*l|BkAGzb!u7TvEq;?3af7j~ShL0|yl~|ipjcRU1A-^$i=cJ@q=+&_P z^L@#G=vBNtJV?SI{N9-NRpnO{+?(+2os?`38~6Ox>4&fepG{~a@egaUqA(j4s#%qU zUz?y#CCx+RIVW|W(U-@m_;GTjxs`;fVZ5DcPbHy#80u6KGZfqV`BYHPX)O-ESm%W* z!hBe$?p6_Q!^2TZ?Z9O@UHLw;x(-iqr2mFP7%@BE{@Dju^7Itdb?Xy@?ujh%o%}ZNN6wWt=onCkf(kKgd zRbe+Qs-bD^Gm~bQ+RcTA;(qG{R}3?=%aeAbx2lbd17ODfu;;{Bdae$*PS8 zp)zb28XzU(hDk5qs3NnCN1;I@r1@em?C*pXsHD1hmQi<9@Qm6W#6ZL?B_3(^@+wkg zI|zHCcwZ+aS4a1Y&uWcItr`vcU};02aS$5!z%D3u5L~*0K5!6Lf;uVLJYx8z*IWI$ zA`Kn^QViQ2g(R}v?`HnOzZ`YZ*kB5jcq+RH(|cgJ`@0DFpiWA9kb-KVZ~L{Me+nM* zVTRT#^(@nOj70?afN!*c)12BUc9m9A=W$8Pk&vb1%NVfijCwYZV^%bPiuR^J8h9N^%9|@A9ER4?O*Xx!Ug1>0f#M z>9t*i8v~&6#UjQBsFI38`D=e@gXjG@5d|hjd3m8^d=NE_JXQXj&^L(J7;ukKl4nK91_2vd?5J4DyrSfX zfJR;l33bE=k}M@t1Tm6JMul4^r==i`E7j+nKw?}du$5%;{qa^cy4@QSptddAaN%tX!5QrI4M!~?94mV~Tqu}HJ&mkzI@V6oO z|9wOMT${KYDFkHkp@$w$Nj4((RlH~WZ>sxu!TB}%n<;{0g4lkU_4x0HyOEzMp1mvK zU{P|Ph=VFF$G@KlB_olNH%0XE_4GyiO41@FwF*?21=?Vx%j6$d$$z-I%PekR?w=2U=6oFDY4KRD6oK7_d^6OiM~W7&ytNmXeD} z$rvMR{P7+@&ekL)mARCt;-!1PZ=T2EEzClCyrARmZ%$9r^WK6(DDNaJ2E_LY%QpEQ2sXj_?-T6l&mQIxKEVO!*{Bx;hqL;9=HEw8>#7uS{ zKl*e%8K5n-*y<&mgGGRn%E`M~PQN^}DymWoCE1gOrp4psTZbJl)jabOT-v~wUJcRR zu5NGR4gO}y4@)hS1X9;03~c4I@LEBs#@$;;g0GX3a%xj6UElQZI!j9}I(Z8#sdgnv zRsCvp{rDO8R+U;PX{?g^ul{;@-rlCAnz`P>3-VnfNA}yojNeyZT4Q?h9lJeaq^O4It!Qj3?~LK-Xrl>A%fmSGKknWEri9AiPlEup-*W{N0KWEwCc8VDar0 zAzFNMMTi=#h%@B-%@rY9d~-#J8mtJX-ha0uL<_8l&an9QiV!WnxgtajR>X4h{pN}g zExx%TL=9HNGm7k+D?+ruig4|N`?_zg2+`u3D?-#L8Nig}QJ6&>&2(U`Y%1O0D(R%4 zp__LcGfI{)CBYOlbjVc?!u!r<(p|ceV+!{d^d1TK1WC<={RlGbpDmO}-Fxt*&lV*w z6@0Pa0zCb>AWOV9!4+gFyzj$DR2U@li=u?_eeneOYhN5(wPN|9!q*|ZdESLs{*84j z`XI?j92F5aT<@Vz7~&P5q|>Hkc%&!jJrd$F;v(XcQZo|K@-M4PS`(^l#u zkhV`hG)cA>D#h}4!l4Y_zGXye8Y<8xX_K`X=_9@3(sU7NNpZ-GsE?0J(t1&)1LE}B z2>JlmdksJiNNu_YYSLwB{bXTUqHssgJJdmn!FX+wm-5hSY^xoSmMZxPahbfc&~XHx zmA7CezmXT_GCnqM#&>)bJ}-JTzonuRI_WR=G9g_zYE+WeBLis&z4RHG3A$9lXDhF1 z8XuReO^Qcc+LU1)lm$^Qr>*ly*Cryfp*DT6J~hRIZi16_DZ1pijQBzL2GUEqdG{Qw z7veVaPOf5FL@y6n>=B4Jbl%i(QhKIGd|dn>3e`!Ya9UWFlsy83JCpgERplFcsHBq6 zZv*e{S<)!vC1uo4?*LK1m=Y5S64T;R^t#j(fuG7d1yP#JQW+fo!OBB?91z{(FhlX$ z1P`%alDCnFoQ(t?^wH}p`eGDH#)bx>U^wqsw?qQZlCp>RFhovRWghXVNvY{V!F=Ay zR(^nht@11Z(;&fT0Pkj_JVQXK6g_;zq0-2YB;+RLk%SlCYYGKfeDx~ygaHi^5(_KW z@otSu+J&a1ERKV?KD7D(53IxVxQx_v%H)Z~j$2T%NA*j2_?9xL+s8AI`ji4wj|^S> z5Yb*!!os|yjL7-nc>65hPC+G?s2X~Zp^7L9{mA42sluJXyc29G<)vs!h0-7U(VM&}Z~tC`jXLxRe<^*-aTt3R<`m%eysJp2uLI6cGYFkSSdsu8kXlg_wr( zGc7$e8M#Vnj||P!W|rR)qdE<)(t;eM!j=T-Ek58w zD^i33eqa!} zg}22UdDj_yO-;!`3$v24N1)JuAYW6soyD6AsXy?hd1+huIlNk*nu$*UECwNa1n#$E zrt*yiLo#1J&utrj*sP{>fTJR*AWoEDFpZWzGfg_N#hGcKaAc~KX-P<*eetPjBR%3$ zMrI7sr6h`jThmBHk(M>VF2I5i2iZ>kkcWv}@(>?nq(Kx0=kaw+Qc@GN!t^{oS_seN U%>?gh__f8 { + const db = createDB(env.DATABASE_URL) + Container.set("env", env) + Container.set("db", db) + return await app.fetch(request) + }, +} diff --git a/src/commons/libs/db.ts b/src/commons/libs/db.ts deleted file mode 100644 index 35b5891..0000000 --- a/src/commons/libs/db.ts +++ /dev/null @@ -1,8 +0,0 @@ -import postgres from "postgres" - -if (!process.env.DATABASE_URL) - throw new Error("Cannot migrate. DATABASE_URL is not set") - -export const db = postgres(process.env.DATABASE_URL) - -export default db diff --git a/src/db/index.ts b/src/db/index.ts index b70bf70..8f51fc0 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,16 +1,8 @@ -import { drizzle } from "drizzle-orm/postgres-js" -import dbConnection from "../commons/libs/db" +import { neon } from "@neondatabase/serverless" +import { drizzle } from "drizzle-orm/neon-http" +export * as dbSchema from "./schema" -import { station, schedule, sync } from "./schema" - -const dbSchema = { - station, - schedule, - sync, +export const createDB = (url: string) => { + const sql = neon(url) + return drizzle(sql) } - -const db = drizzle(dbConnection, { - schema: dbSchema, -}) - -export { dbSchema, db } diff --git a/src/db/migrate.ts b/src/db/migrate.ts index 6f70c64..9e3a5e5 100644 --- a/src/db/migrate.ts +++ b/src/db/migrate.ts @@ -1,16 +1,20 @@ +import { config } from "dotenv" import { migrate } from "drizzle-orm/postgres-js/migrator" +import postgres from "postgres" + +import { drizzle } from "drizzle-orm/postgres-js" import { logger } from "../commons/utils/log" -import { db } from "./index" -// https://orm.drizzle.team/docs/migrations +config({ path: ".env" }) -try { - // This will run migrations on the database, skipping the ones already applied - await migrate(db, { migrationsFolder: "./src/db/migrations" }) +const url = `${process.env.DATABASE_URL}` +const db = drizzle(postgres(url, { ssl: "require", max: 1 })) - logger.info("Migration success") - process.exit(0) -} catch (error) { - logger.error(`Migration error: ${error}`) +const main = async () => { + logger.info("Migrating database") + await migrate(db, { migrationsFolder: "drizzle/migrations" }) + console.log("Migration complete") process.exit(0) } + +main() diff --git a/src/index.ts b/src/index.ts index 9568f9e..ff72fdb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import { logger } from "./commons/utils/log" import swagger from "./commons/libs/swagger" import { rateLimit } from "elysia-rate-limit" -const app = new Elysia() +export const app = new Elysia({ aot: false }) .use(controllers) .get("/", (ctx) => { ctx.set.redirect = "/docs" diff --git a/src/services/route/get-all.ts b/src/services/route/get-all.ts index 559d7b4..e9f7f4b 100644 --- a/src/services/route/get-all.ts +++ b/src/services/route/get-all.ts @@ -1,14 +1,16 @@ import { and, asc, eq, gte, sql } from "drizzle-orm" import { InternalServerError } from "elysia" import Cache from "../../commons/utils/cache" +import { getSecondsRemainingFromNow } from "../../commons/utils/date" import { handleError } from "../../commons/utils/error" import { logger } from "../../commons/utils/log" -import { db, dbSchema } from "../../db" +import { dbSchema } from "../../db" import { Schedule, Station } from "../../db/schema" -import { getSecondsRemainingFromNow } from "../../commons/utils/date" +import { getDB } from "../../types" export const getAll = async (trainId: string) => { try { + const db = getDB() const cache = new Cache<(Schedule & { stationName: Station["name"] })[]>( `route-${trainId}`, { @@ -56,6 +58,8 @@ export const getAll = async (trainId: string) => { export const getAllFrom = async (trainId: string, fromStationId?: string) => { try { + const db = getDB() + const cache = new Cache<(Schedule & { stationName: Station["name"] })[]>( `route-${trainId}-${fromStationId}`, { diff --git a/src/services/schedule/get-all.ts b/src/services/schedule/get-all.ts index f453f5b..9d2d5f3 100644 --- a/src/services/schedule/get-all.ts +++ b/src/services/schedule/get-all.ts @@ -3,11 +3,13 @@ import { InternalServerError } from "elysia" import Cache from "../../commons/utils/cache" import { handleError } from "../../commons/utils/error" import { logger } from "../../commons/utils/log" -import { db, dbSchema } from "../../db" +import { dbSchema } from "../../db" import { Schedule } from "../../db/schema" +import { getDB } from "../../types" export const getAll = async (stationId: string) => { try { + const db = getDB() const cache = new Cache(`schedule-${stationId}`, { ttl: 60 * @@ -39,6 +41,8 @@ export const getAll = async (stationId: string) => { export const getAllFromNow = async (stationId: string) => { try { + const db = getDB() + const now = new Date() const currentSecond = now.getSeconds() diff --git a/src/services/schedule/sync.ts b/src/services/schedule/sync.ts index 9c98f88..77c0892 100644 --- a/src/services/schedule/sync.ts +++ b/src/services/schedule/sync.ts @@ -1,15 +1,18 @@ import { eq, sql } from "drizzle-orm" -import { db, dbSchema } from "../../db" +import { InternalServerError } from "elysia" +import { z } from "zod" import { parseTime } from "../../commons/utils/date" +import { handleError } from "../../commons/utils/error" import { logger } from "../../commons/utils/log" -import { z } from "zod" +import { dbSchema } from "../../db" import { NewStation } from "../../db/schema" -import { sleep } from "bun" -import { InternalServerError } from "elysia" -import { handleError } from "../../commons/utils/error" +import { getDB } from "../../types" +import { sleep } from "../utils" export const syncItem = async (id: string) => { try { + const db = getDB() + const req = await fetch( `https://api-partner.krl.co.id/krlweb/v1/schedule?stationid=${id}&timefrom=00:00&timeto=24:00`, ).then((res) => res.json()) @@ -93,6 +96,7 @@ export const syncItem = async (id: string) => { } export const sync = async () => { + const db = getDB() const stationsQuery = await db.query.station.findMany() const initialStations = await stationsQuery.map(({ id }) => id) diff --git a/src/services/station/get-all.ts b/src/services/station/get-all.ts index d5c96c8..2046831 100644 --- a/src/services/station/get-all.ts +++ b/src/services/station/get-all.ts @@ -1,13 +1,15 @@ import { asc, eq } from "drizzle-orm" import { InternalServerError } from "elysia" -import { db, dbSchema } from "../../db" +import Cache from "../../commons/utils/cache" import { handleError } from "../../commons/utils/error" import { logger } from "../../commons/utils/log" +import { dbSchema } from "../../db" import { Station } from "../../db/schema" -import Cache from "../../commons/utils/cache" +import { getDB } from "../../types" export const getAll = async () => { try { + const db = getDB() const cache = new Cache("station-all", { ttl: 60 * diff --git a/src/services/station/get-by-id.ts b/src/services/station/get-by-id.ts index 0929468..c690609 100644 --- a/src/services/station/get-by-id.ts +++ b/src/services/station/get-by-id.ts @@ -1,13 +1,15 @@ import { eq } from "drizzle-orm" import { InternalServerError } from "elysia" -import { db, dbSchema } from "../../db" +import Cache from "../../commons/utils/cache" import { handleError } from "../../commons/utils/error" import { logger } from "../../commons/utils/log" +import { dbSchema } from "../../db" import { Station } from "../../db/schema" -import Cache from "../../commons/utils/cache" +import { getDB } from "../../types" export const getItemById = async (stationId: string) => { try { + const db = getDB() const cache = new Cache(`station-${stationId}`, { ttl: 60 * diff --git a/src/services/station/sync.ts b/src/services/station/sync.ts index 6ec2921..bc3e240 100644 --- a/src/services/station/sync.ts +++ b/src/services/station/sync.ts @@ -1,12 +1,14 @@ -import { z } from "zod" -import { db, dbSchema } from "../../db" -import { logger } from "../../commons/utils/log" import { sql } from "drizzle-orm" import { InternalServerError } from "elysia" +import { z } from "zod" import { handleError } from "../../commons/utils/error" +import { logger } from "../../commons/utils/log" +import { dbSchema } from "../../db" +import { getDB } from "../../types" export const sync = async () => { try { + const db = getDB() logger.info("[SYNC][STATION] Syncing station data started") const req = await fetch( diff --git a/src/services/sync/get-all.ts b/src/services/sync/get-all.ts index 218c991..88e03ec 100644 --- a/src/services/sync/get-all.ts +++ b/src/services/sync/get-all.ts @@ -1,11 +1,13 @@ +import { asc, desc } from "drizzle-orm" import { InternalServerError } from "elysia" import { handleError } from "../../commons/utils/error" import { logger } from "../../commons/utils/log" -import { db, dbSchema } from "../../db" -import { asc, desc } from "drizzle-orm" +import { dbSchema } from "../../db" +import { getDB } from "../../types" export const getAll = async () => { try { + const db = getDB() const items = await db.query.sync.findMany({ limit: 20, orderBy: [desc(dbSchema.sync.n), asc(dbSchema.sync.createdAt)], diff --git a/src/services/sync/get-by-id.ts b/src/services/sync/get-by-id.ts index 8e6f5b4..e2a7a51 100644 --- a/src/services/sync/get-by-id.ts +++ b/src/services/sync/get-by-id.ts @@ -1,11 +1,13 @@ import { eq } from "drizzle-orm" import { InternalServerError } from "elysia" -import { db, dbSchema } from "../../db" import { handleError } from "../../commons/utils/error" import { logger } from "../../commons/utils/log" +import { dbSchema } from "../../db" +import { getDB } from "../../types" export const getItemById = async (syncId: string) => { try { + const db = getDB() const item = await db.query.sync.findFirst({ where: eq(dbSchema.sync.id, syncId), }) diff --git a/src/services/utils/index.ts b/src/services/utils/index.ts new file mode 100644 index 0000000..1f5ea0a --- /dev/null +++ b/src/services/utils/index.ts @@ -0,0 +1,2 @@ +export const sleep = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)) diff --git a/src/services/utils/sync.ts b/src/services/utils/sync.ts index d5cb793..dfea6d6 100644 --- a/src/services/utils/sync.ts +++ b/src/services/utils/sync.ts @@ -1,8 +1,9 @@ import { sql } from "drizzle-orm" -import { db, dbSchema } from "../../db" -import { NewSync, sync } from "../../db/schema" -import { handleError } from "../../commons/utils/error" import { SyncItem, SyncType } from "../../commons/types" +import { handleError } from "../../commons/utils/error" +import { dbSchema } from "../../db" +import { NewSync } from "../../db/schema" +import { getDB } from "../../types" /** A function wrapper utils to handle syncing status */ export const syncWrapper = @@ -18,6 +19,7 @@ export const syncWrapper = }, ) => async () => { + const db = getDB() const start = await db .insert(dbSchema.sync) .values({ diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..78f639c --- /dev/null +++ b/src/types.ts @@ -0,0 +1,14 @@ +import Container from "typedi" +import type { NeonHttpDatabase } from "drizzle-orm/neon-http" + +export interface Env { + DB: NeonHttpDatabase + DATABASE_URL: string + REDIS_URL: string + SYNC_TOKEN: string +} +export const getEnv = () => Container.get("env") +export const getDB = () => + Container.get>( + "DrizzleDatabase", + ) diff --git a/wrangler.example.toml b/wrangler.example.toml new file mode 100644 index 0000000..b29cec6 --- /dev/null +++ b/wrangler.example.toml @@ -0,0 +1,17 @@ +name = "comuline-api" +compatibility_date = "2024-08-21" +main = "src/app.ts" +minify = true +compatibility_flags = [ "nodejs_compat_v2" ] + +[limits] +cpu_ms = 30000 + + +[vars] +DATABASE_URL = "" +REDIS_URL = "" +SYNC_TOKEN = "" + + + From bfd65bda3d52ca2b8bebd80039efe6cb68d8a956 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:07:47 +0700 Subject: [PATCH 03/64] wip kv --- .gitignore | 1 + src/app.ts | 1 + src/commons/libs/cache.ts | 7 ------- src/commons/utils/cache.ts | 18 +++++++++--------- src/types.ts | 4 ++++ wrangler.example.toml | 3 ++- 6 files changed, 17 insertions(+), 17 deletions(-) delete mode 100644 src/commons/libs/cache.ts diff --git a/.gitignore b/.gitignore index 16bab0b..fb5a1bf 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ package-lock.json **/*.bun .env wrangler.toml +.wrangler diff --git a/src/app.ts b/src/app.ts index bd3cf9b..553de8c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -9,6 +9,7 @@ export default { const db = createDB(env.DATABASE_URL) Container.set("env", env) Container.set("db", db) + Container.set("kv", env.KV) return await app.fetch(request) }, } diff --git a/src/commons/libs/cache.ts b/src/commons/libs/cache.ts deleted file mode 100644 index 4aba9af..0000000 --- a/src/commons/libs/cache.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Redis from "ioredis" - -if (!process.env.REDIS_URL) throw new Error("REDIS_URL is not set") - -const cache = new Redis(process.env.REDIS_URL) - -export default cache diff --git a/src/commons/utils/cache.ts b/src/commons/utils/cache.ts index c887043..43f938d 100644 --- a/src/commons/utils/cache.ts +++ b/src/commons/utils/cache.ts @@ -1,7 +1,9 @@ -import cache from "../libs/cache" +import { KVNamespace } from "@cloudflare/workers-types" +import { getKV } from "../../types" class Cache { protected ttl: number | null + protected cache: KVNamespace public key: string public cached: T | null @@ -9,22 +11,20 @@ class Cache { this.ttl = options ? options.ttl ?? null : null this.key = key this.cached = null + this.cache = getKV() } async set(value: T) { const self = this - await cache.set( - self.key, - typeof value === "string" ? value : JSON.stringify(value), - ) - if (self.ttl) return await cache.expire(self.key, self.ttl) - - return + const val = typeof value === "string" ? value : JSON.stringify(value) + return await self.cache.put(self.key, val, { + expirationTtl: self.ttl ?? undefined, + }) } async get() { const self = this - const data = await cache.get(self.key) + const data = await self.cache.get(self.key) if (data) { self.cached = JSON.parse(data) as T } diff --git a/src/types.ts b/src/types.ts index 78f639c..79b7d92 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,14 +1,18 @@ import Container from "typedi" import type { NeonHttpDatabase } from "drizzle-orm/neon-http" +import { type KVNamespace } from "@cloudflare/workers-types" export interface Env { DB: NeonHttpDatabase DATABASE_URL: string REDIS_URL: string SYNC_TOKEN: string + KV: KVNamespace } export const getEnv = () => Container.get("env") export const getDB = () => Container.get>( "DrizzleDatabase", ) + +export const getKV = () => Container.get("KV") diff --git a/wrangler.example.toml b/wrangler.example.toml index b29cec6..92a262c 100644 --- a/wrangler.example.toml +++ b/wrangler.example.toml @@ -10,7 +10,8 @@ cpu_ms = 30000 [vars] DATABASE_URL = "" -REDIS_URL = "" +PSTASH_REDIS_REST_TOKEN = "" +UPSTASH_REDIS_REST_URL = "" SYNC_TOKEN = "" From 72820c202d57a8640e5ffed9a010d781a9660352 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:02:18 +0700 Subject: [PATCH 04/64] init hono, openapi, scalar --- bun.lockb | Bin 112820 -> 118070 bytes package.json | 8 +++-- src/app.ts | 97 +++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 89 insertions(+), 16 deletions(-) diff --git a/bun.lockb b/bun.lockb index b5fbe8231185f46358cdc8901499b2dfff3be3ee..8d67ffff863c7a838c92365e9998ed6967210e29 100755 GIT binary patch delta 21848 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 aU!0T#o9Qx!b+Q)+Cos=l;(X@R^#1|oGnPRB delta 18728 zcmeHv33yFc*ZU4q}LQJw`P%qT#Y1>7(&U_Y7K3bE*!M9 zQCezf&0-_u6Z( zz0S@_w(ZMmY`s~1eptx-XKO^BDO$W|O}_6prdzPB6If8N&epYE5=&1}9? zNc^;0;0TiMhAf!e+Oh;}ElF}cL`?(GUqNevzAj5rZP2g5dxD+;tqVH4nk3Z$y-;nz zg_fQN+$6~iZZb;ifzBjP5#(F9x+HmkD?uA?(7c>XXMUt4ZJ;C(w$8+YC3gh=ccDQ z^Q3v0HrGE3iYnJcou+}-mn7LWfShtrVIo-kbG1jp>db;FOw%(9@|-D>bQ>1o?r(r% z90j-T}|;pFuyYE>Fz|v;{1~?camlx%?MUs8z`G%P+`D z&wCD1JYT~u|89@$Q4>a^FI5|sD}X$&PFgk$I8laBE*;}bk_jBF=K9TF zYj72mP5cEYuhmJN9tLHTm*})f8;!3E%BJ6ja$e)$wvvP~U5h~3M;f$~BviTXfoBUf z0OkJ8ZDD`zFeNoRH;Xkv1$S^89WqT#$;rxTm!FrPm+2gZ>PgwTPp9XmNzKD02}X0d zgYrzSJtj$5E!Qzn9(X-y6VM4+Y0gxqB=wKfCRUI=AvFX2oB+=g^NqrQtiZ@9&4ej= z?ebGbWv1T-&!!zWAum5AH6vZ~Zfab;Y2nolnrfTW-Ul4gWLz((pso&(MM4PKKKq{%-iW;n#*g8-8o}tLBzZR{Eo{ zaf}UOIK1J{+77GSP{sx_HbPcPR<;xIVRIb4TEkknJYF+Yjx#MkH$7b<@I3KMXI_3T z7VJ`jW++RdR_@Yi7-((Cd3M*ai+E#oi!YSJka_iO_8JMkokY_a4r<*6-}R0x>)voIV;zn4HXP@_87K?? z{~{$6GaSqC;6DnK6EGp(z3HGl!S=9?S#pzHR|ai)yk<+8CfDA%WxmHqdCQ%;E;sot ziORcL#Yrk|XfxNq%N{$@RgK~TqlJxPylkRA6(Ai+#a=e^8XP~mqF&GppJ@52OqNDg z5lID&Y+^ALH?qm!RHJefHKQ1Bn|X+vBw=*)R@x|9yh6n&JBBjE3iKenqlHW{J~q*r z3Xm?MVjr97OOqsp(=?x05kN7%Hqna;d~Ie|bxG=`wepG<`{^!fd??0Gtw)N%{cNV= zH837c^NKag?%F6O*3;Yx9Gt=>(+sa@^9$e*JXkBAXt9s(`rG7t?qmtDifAebu*nz9 zv@5`B4uG@qTqTP0ixxvECeS7}QbC~2B*Xo>t2*|iyMZ?I67W{F$2i|;`D`tk(%5S5 zkIkW}j6uahy4%=hK8!LpugqPVE%4IE!3mX{2@d<*f2l*3V5zNC#7o@7GQYGNJQy0Hjvm3SxFR$8KbfUY>Y^Jd&YfIBm zyBIly64o7kTm%=Va@=DOUidt7tz|A1w6KXfRE#v4?zXU*UxNQuwml7Qu-XhN9FCT@&!M#gjr>aKTQEy7(lzitmb2QUn3l|%8jEPI~zH>+TAJSv|$5UsB+<*MGKk|ZgT^xc4fYe z+(>QO-qGf#aYAFSQ_UxT+md#5u$m)VX_m)s!(!&QB1=cB{9!Aa($Q+J*;;FWHAB=H z+?vW!xEF;_YJE13HhG5VVZ$7U9QQ6!X+X4j0XV!7v7o$E=fEY<{lHk0e<-#rFPc0k zly-HpniUjuR-0jb(<$a?Z$PX$w2fAdP3jdbk8VS|VyyD^HdGE`ZA%u5Ro>W^rdX`z zuy#0Vp$ABYVS9pO7sn*wF-O5AsJ*jNb;7hh-KjJzT7DvocEwsvFQCvu_kCl{`#1+- zI3cW64vu364AUsu6p6E4Bz0*NYtBZl3(65lEz#y~@XiqxToJe-72G{=DHYtX2uT`N z!R-Q0S^B#6V~?gNV3?haapqf`MiK24U z)oM?cu2xe9_GTwFP`rtpwozdj`82BItY%-F40&FND#6j_#v(cQ#QB7rj9~=i7H9E?-EVQe;)f5`bTXb*tSW^LVz17^Iid+NerIn4W$ZbH* zPJ8X0-H=IA8^pmWwA|c^+^LFO04A>0rdQ-PR^)D1LgXzsX!NI#-)k%az%XiSAx7FM&UXq}yt1bsw^BQn0g5A_S+WZ+f zOpUh(2PV2J_ z+(6W6hnrS-QEFNvIGV?UgQ|Rr@ag0Zh6lhFYZ7gK0FEcCxqbU2&4txBojDU6D_xzB zBJ#E*Du2Ri{z)%X!>&0T8;HlzM$H7rI;eG~{opL>i}D_FyjZH$Of&5`c&X(lkz-Y0 zTUezYVg{?C&8|B*-c8y#)4{PSCe=nIzpMLAufra~?(Epx7&Qkuc!I9mW^k;jwsJSX z@yOWGu=0RDnts|YoK?xecE#YZ2)skld+omHlXW-sL9UZ}tb74E>_=BEbrz-Ic&@6Z z^3Q##e2`Uc*^exPt@4}wXbMQ}WZE^@Y95+gah}0AtH8muSQhx8{4klO46&LW{WUeU zfUu}Pl@GC+K0#q3-5(Nbs{aJeRvT?Pa+=&n$Z7r6b{J&?kkk5GiJaEr8gh2EzxGcVa?_C0 zef0qaOI=dEsq;8@dU9O7LT-+Y%H_QL>hqtVwa^pd zw5lE)R(-0Vg@07RKP*cK4eG;`6~&&Hc;46qDi0S|pZ^Z!e&Fir(-+^Y^h_n?fw8XY z!;~9fS=HyyD9a7j<*K4=;h}&#kfzI}>Eo*f+(Eit@n_T@GA@ASX8_fJnL7VGC_hYV z0536vrz*vBxl1YZL@(ziOj3d#>tmcPafo+>EOcu(O@ z9=YDCi~j;CZt*LN<6+ADZ_MCf%KUAe-qGn@otA_0!<03;2e5_@0e&8(+z)rgR0w^G zBf}ryC2zI%f0L^7|IZBQh6(uV6Zk8%CgfYFA)YgO!$&EbvK7j!gSG=@lZJz`XapxZ z!5s_6#kt^Nw-1sVi-R(l92Hyx_CXUgTnbvjZnXUd*A zQRg3_n(I7L0I@0s|A)$T*ZnIB@XRLbGn)d+lbNc|h_&N)Mpefhqpr$h4-Uxc)9ew- zF4h|0zC-l#swgY;$hqh5W6$5m9_<*!j`R1i=kH^Wb{t}FxU7c}ru?S+``GgzI41pl z?15{scl~|r`M-ATv1#$(|M}QMODE=2m#ubsYEpXF*|}rB`l`#`tzQJTia&N)L7f(Q(DH?L;h<~au7c~m$Swxa(nSthvIx2?vx_0*T;`yji=hj+VdVRogYJNv z_L^Ocpd+t2@Qe50*Bv5-3XqPXGNh>#vfLrkXa>@BI*)WTMXYd$F*FBh23b0yiZqLEBF(0xB8SMKBBbN#F49~|Ug;2dv=(VTiMJeL0u4es zkv1WnMCMfvF_}`37Em$LDdf4@f#0$^kv>DENT-qS+YT|E#v?7HBS>8o{EkD+paP`N zQW?_cC}fR;+*iSlYwY5AIuC9qxGrn$ViwI=i?vw|JA#`-G4En+-i96DwTl<&8o0yY zdatvKmucxbtj#+fbRXO+l(Zgev&MrqthWpNS{K}B;D&9m3!=3fur_Nw$YY}&e`YXf zBi80!584Y(A@h3JjiR8UF@KgUGO+?2f-DS=Wcl1b`P4c+b(ufDY%|HJgCJUyVy(P z_c%le9YOj51@Cp>4=m(huo8(t{N7fkPakIY>XE%SaDX%sz)W zLJN={rE5q(rnpjvI7Um6enK~q9;c-J4)G}!Aw5BNk(N>N0SEr*WG&KDBtCSA(=-U_ z8QO&OESV2F#5qbq`WY1?Jx`v89O42wk$z64NdG~;A34MqG#=?iI)d~|3O?)*m#6^g zWhy(2Rr%0^IvlZ!uV}^*c-%n``Uc!L6mb+D2X4_(yZDYSgUdMNL5UyR#Wh;+G1lfI ztPQyDDef572Hfgnc5$6X4q&r~GfPE-Y(^CU!&2bNU{*+w^Dm#T~eu`s<20su0@DO%qKGq? zCb&gs?4kx;2A5HWX`ZzUGc7oaX`aM1!PTO;bC@Q$)#vQOgKmPGd&ssXvt> z4Itky93qg$BW+AakOookMTclY1xSOb3~5se`O+bp(F~-`={(XF6miKRTGAY(t>`k+ z))aHuAwp;Y(onjFv<<~wafr6G^vYXe$(Al(eO{dczV9iI2;5R~t3=SHTZbv9p&R{l z{ji7er<(8L$KBdgdTT1z)HMDM^YJk1^W!Kv?J+d0Dbz;$esVVrZ4cosrMDbfQRiFk za@`mzxiwam82Y`tHq=pJ%8A3D(xKZiToas=o1UNVOwYaAP5XN+{xQ2JfbadT@Ob>* zlahaI!;&7x-~Kg9Kzod|TWq;>%SRo~BNkum@o~zP2wKljnReb;Q!&AMH)d{G^{;6$ zHM@rLAqu|-Y@?iy7IB3S9%>Jl-2%9lKVR`<@T}CU0N4Et@aGji*t-CJ%&6cx<8uv{ z)zr)Q`@-W~iKmuc#{KJR6iT)ALjJsdjHU2+=w`{8_jP*#RN z%ymSbJ8lh%e{4aCKb7$l0?L!%&%S(q1K0@^1G|9Tz#d>Pz+d791A~y~kA~fW z9zahZ3E*Y61HFLW0DlSPWoG-f1zG?t0sdy}1NZ`d0DpYGkBGAU=^?$;7yhU;8)U;9q7d$ zwtFDc7H9`F1DXRZfR;cjU>LOJ?~we(Ga2X)@Mlu~wAu~eZ@C?SXn?=}-bR~;0DqqU z1-Jry1zZ9y0B3*_;054CjIaJCE)28{z+d4)0scD2A32`{o&XX6{+JvIbOqSS`6Dxb z<7dzR8sr<`EN~9^0N4kV0xtn~fvdo`zyUzr*=ScNBdG?s0Vbe2Py=uWc#GErY5}}s zd57|jtP9iwJOSQ~4SL_GVfixf; z7!8b3%3?%=ZkfoA1G0c@AO{!^J}?2W0KCKT7k%mn@qTO>Je@o8=586ne#kz_ zbKyDhDx3#+7Y+t`1MJ_ucI>r^c+olmd3FzW6LuGN8}^d!KqA1wlgpHduEL>o?JCTL z+=U)g`LB)nPm0ye%$t)pG;h?W00+SP85>*O;Qc{)Zw~>uYaW`tm%W&`AaBpbz#8Bk zfKA7yWaoJvmA-Mc7{E$R1DynnP>SLZ&@z#I8W;;Wfec^_Fd9e) z(tuQ86p#Xp1SSJhfT=)%o__{(1mFUAS!V+rz}^O6Wi@E70DT?cIV=Q-HDu?T54-}r z2(W{4WPBOmNNWQY0j~kefTh3^KmnEmtASO(8vsYvH-RF6jq(;?EW|eCHv#Vh8-ew} zI_`ZfGHjf8fepZW09S4Xwg6j!bpY2F13Q5oz%F37;t?;J6&^+Q2yhtq2si{B1U>`~ z0Q-SbU?1=Sz&m~~@ELFpI16wj;C;sXjpzIu@GI~oa1r|nc+ya`g5tER) z3Va7#0=@;l0xknrfNy}Wb^a&N?}2MNe+iW3{|T_n55NuJI`AWK6Zje6{#fo8KtM@e z)`uVucm8e7|AnANOut>W+}tE3twe^chN^IQNHXh z!bOx)uZM^CdKqf|`i1aBe$U`J|^96JXr^+kNueQET$H&Qb-USl~i6rC*Zp z5eF1!5+*iNS)K&l>MA>u#28(-NdG_XKSLS!IOHp)RK#4KvKz$TxaN26+^PeP^>utz zeO0X%D?hT_dS$#__y!o)1IvGSkoDWT(j$29{i2Ff#-drB5Imn z>7kleU718>L~jf@K-th6`sr5-T>)qBeID)D#&W!ZScZwpWeA98m4C6EaW(O9R&brN zQ`@(x^R1L?sKoaX_L@#OgBg0i(?D6$M?}gy8z{&6z>^N5+!y8b;%Bcvc3@>gRhw|O z=fwt#`$E9Ds@UAK+P2W8SuwIeZCC`|14eL=C5tveZj5hF87TMq;9$3WSHhfB^D&$HOSRRM?>Cs ze=6NMGqWMPdV8MShDJ(Gvhe9x3JEVrxJC?prN&z)TBus8J)PBS>h&I>-0Z(1wo-}j z8!07d>(7T|AG9@%dHm@wYqhz|ZKKq-Y0AxHn7{`DY}=5816@52u2I#DR0Ucnw*J`8 z?|CcXLtw!O<>mgc;4j|Fn*JhE6eypA^{>L!unSrHI!fIqU_s+j=Qn+BtUbBnGU6qC zQC)fC+GlRJk6lvU@TscrsC(c!W!MuUD!{ncIlk@O8%;0PKMjHQVIA1Hv{g-z^Zb=p z2Z+(~a(|_KfUpD@KjU6HD9z}-G-kdm*7|FKq{Lt8HxRZoE;N4q!!EylP41VW&+xF0 ztgmt9F+{0zb>DA3eyCwZJK!bdudLv<#wE#G6UP*u`SL*y5xp(7oB*p&FZ0yU+W>-oC2Wj>${bs?M2HrhxSJp%YDHpkI z4@mF}&E3=5W~W$qtWv_bQP;88vF$~#Zkm(xXf+B$Ba?O)vGRlm*z?`im5-*0Vi zqNbC!9U8P%`f%G|NWe#3zFlg{9cvWSWT#j4m5h7B1vj^S_PK2j-b1_{VxY#ow#w^h zD>rDTY#%B{7aA9PyBuk8F8+-LscvGqzNL+;zsIk{j(9Hj;8&G3#^vChWq!RP!2vXk|ZI1Q<7wyZpRw)6sezG9VGHjoPh~Qg0-d%($Bz`dH67Rzi^esjW zUIAfv9Xex_^&>Htw=Bvf?xV`c5sk1WRk?H3n&2QMECp?=++(Ulm9b2%8QWNS25ke3 z8_l!b)@O|Hv`kj_rz+7SNO_-oH}2#guF+5Fes$dbN{K3CsoECt@Bz0qE>?g3-01ER zi!YQ`wyiRjscjLldX0i+RmLM#0#U~WiH^qQ*cJZayG~t89bDOamGNI~>(^L0&TXrV zwyH#xu~e-=91cho%j6mH%Fa~y-`semdK%V{;uQ?nK|l zt?dy#-#j+c`#=y{A_Q_2oYPM^0f~;rMfE-pCRrBg8JQ=ZcFOwlPudy+c@Jgw(6Qk~tRR|3R2WA2>k035lu? zPM8{D?aprcAud4N)D5HLDF4b7?ygQ^4$P&tsO`T(U$RJoO3B ziMf(6Hh-Bl;AT_(@Gh56-|qKuy+aY;`KQpr-^|sXpLA~Azt^wP`wQ5q+!}|U;!2+v z-pU7AqOWrMZP8Q-&lbIv{qsZi!}<$nP% C9_$wY diff --git a/package.json b/package.json index 96f9069..b279749 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ }, "version": "1.0.50", "scripts": { + "app": "wrangler dev src/app.ts", "test": "echo \"Error: no test specified\" && exit 1", "dev": "bun run --watch src/index.ts", "start": "bun run src/index.ts", @@ -22,18 +23,21 @@ }, "dependencies": { "@elysiajs/swagger": "^0.8.5", + "@hono/zod-openapi": "^0.16.0", "@neondatabase/serverless": "^0.9.5", + "@scalar/hono-api-reference": "^0.5.145", "dotenv": "^16.4.5", "drizzle-orm": "^0.33.0", "elysia": "latest", "elysia-rate-limit": "^2.1.0", + "hono": "^4.5.11", "ioredis": "^5.3.2", "pg": "^8.11.3", "pino": "^8.19.0", "pino-pretty": "^10.3.1", - "postgres": "^3.4.3", + "postgres": "^3.4.4", "typedi": "^0.10.0", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240903.0", diff --git a/src/app.ts b/src/app.ts index 553de8c..225be5b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,15 +1,84 @@ -import { Context } from "elysia" -import { app } from "./index" -import { Env } from "./types" -import Container from "typedi" -import { createDB } from "./db" - -export default { - async fetch(request: Request, env: Env, ctx: Context): Promise { - const db = createDB(env.DATABASE_URL) - Container.set("env", env) - Container.set("db", db) - Container.set("kv", env.KV) - return await app.fetch(request) +import { OpenAPIHono, z } from "@hono/zod-openapi" +import { Hono } from "hono" +import { createRoute } from "@hono/zod-openapi" + +import { apiReference } from "@scalar/hono-api-reference" + +const ParamsSchema = z.object({ + id: z + .string() + .min(3) + .openapi({ + param: { + name: "id", + in: "path", + }, + example: "1212121", + }), +}) + +const UserSchema = z + .object({ + id: z.string().openapi({ + example: "123", + }), + name: z.string().openapi({ + example: "John Doe", + }), + age: z.number().openapi({ + example: 42, + }), + }) + .openapi("User") + +const route = createRoute({ + method: "get", + path: "/users/{id}", + request: { + params: ParamsSchema, + }, + responses: { + 200: { + content: { + "application/json": { + schema: UserSchema, + }, + }, + description: "Retrieve the user", + }, + }, +}) + +const app = new OpenAPIHono() + +app.use( + "/reference", + apiReference({ + cdn: "https://cdn.jsdelivr.net/npm/@scalar/api-reference", + spec: { + url: "/doc", + }, + }), +) + +app.get("/", (c) => c.json({ status: "ok" })) + +app.openapi(route, (c) => { + const { id } = c.req.valid("param") + return c.json({ + id, + age: 20, + name: "Ultra-man", + }) +}) + +// The OpenAPI documentation will be available at /doc +app.doc("/doc", { + openapi: "3.0.0", + info: { + version: "1.0.0", + title: "My API", }, -} +}) + +export default app From 734c04b871a889bee3b083653078cd521c05591b Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:15:28 +0700 Subject: [PATCH 05/64] wip: station route --- bun.lockb | Bin 118070 -> 118496 bytes package.json | 1 + src/app.ts | 69 ++----------------- src/db/schema/index.ts | 4 ++ src/modules/v1/index.ts | 8 +++ src/modules/v1/station/station.controller.ts | 45 ++++++++++++ src/modules/v1/station/station.route.ts | 47 +++++++++++++ src/modules/v1/station/station.schema.ts | 38 ++++++++++ 8 files changed, 150 insertions(+), 62 deletions(-) create mode 100644 src/modules/v1/index.ts create mode 100644 src/modules/v1/station/station.controller.ts create mode 100644 src/modules/v1/station/station.route.ts create mode 100644 src/modules/v1/station/station.schema.ts diff --git a/bun.lockb b/bun.lockb index 8d67ffff863c7a838c92365e9998ed6967210e29..4729462e361ac3a581501d6780a6a1317795ef57 100755 GIT binary patch delta 18580 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 KxYt-&&;JBC{qeQ{ delta 18465 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 HqMrW+8%LgI diff --git a/package.json b/package.json index b279749..6f45cd0 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@scalar/hono-api-reference": "^0.5.145", "dotenv": "^16.4.5", "drizzle-orm": "^0.33.0", + "drizzle-zod": "^0.5.1", "elysia": "latest", "elysia-rate-limit": "^2.1.0", "hono": "^4.5.11", diff --git a/src/app.ts b/src/app.ts index 225be5b..2d49f45 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,79 +1,24 @@ -import { OpenAPIHono, z } from "@hono/zod-openapi" -import { Hono } from "hono" -import { createRoute } from "@hono/zod-openapi" - +import { OpenAPIHono } from "@hono/zod-openapi" import { apiReference } from "@scalar/hono-api-reference" - -const ParamsSchema = z.object({ - id: z - .string() - .min(3) - .openapi({ - param: { - name: "id", - in: "path", - }, - example: "1212121", - }), -}) - -const UserSchema = z - .object({ - id: z.string().openapi({ - example: "123", - }), - name: z.string().openapi({ - example: "John Doe", - }), - age: z.number().openapi({ - example: 42, - }), - }) - .openapi("User") - -const route = createRoute({ - method: "get", - path: "/users/{id}", - request: { - params: ParamsSchema, - }, - responses: { - 200: { - content: { - "application/json": { - schema: UserSchema, - }, - }, - description: "Retrieve the user", - }, - }, -}) +import v1 from "./modules/v1" const app = new OpenAPIHono() +app.route("/v1", v1) + app.use( - "/reference", + "/docs", apiReference({ cdn: "https://cdn.jsdelivr.net/npm/@scalar/api-reference", spec: { - url: "/doc", + url: "/openapi", }, }), ) app.get("/", (c) => c.json({ status: "ok" })) -app.openapi(route, (c) => { - const { id } = c.req.valid("param") - return c.json({ - id, - age: 20, - name: "Ultra-man", - }) -}) - -// The OpenAPI documentation will be available at /doc -app.doc("/doc", { +app.doc("/openapi", { openapi: "3.0.0", info: { version: "1.0.0", diff --git a/src/db/schema/index.ts b/src/db/schema/index.ts index 732f3c3..98c7aea 100644 --- a/src/db/schema/index.ts +++ b/src/db/schema/index.ts @@ -12,6 +12,7 @@ import { time, uuid, } from "drizzle-orm/pg-core" +import { createSelectSchema } from "drizzle-zod" export const schedule = pgTable( "schedule", @@ -47,6 +48,9 @@ export const station = pgTable("station", { }) export type Station = typeof station.$inferSelect + +export const stationSchema = createSelectSchema(station) + export type NewStation = typeof station.$inferInsert export const syncFromEnum = pgEnum("sync_from", ["cron", "manual"]) diff --git a/src/modules/v1/index.ts b/src/modules/v1/index.ts new file mode 100644 index 0000000..e702f40 --- /dev/null +++ b/src/modules/v1/index.ts @@ -0,0 +1,8 @@ +import { OpenAPIHono } from "@hono/zod-openapi" +import stationController from "./station/station.controller" + +const v1 = new OpenAPIHono() + +v1.route("/station", stationController) + +export default v1 diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts new file mode 100644 index 0000000..5ca04aa --- /dev/null +++ b/src/modules/v1/station/station.controller.ts @@ -0,0 +1,45 @@ +import { OpenAPIHono } from "@hono/zod-openapi" +import * as route from "./station.route" + +const controller = new OpenAPIHono() + +controller.openapi(route.getAll, (c) => { + return c.json({ + status: 200, + data: [ + { + id: "AC", + name: "ANCOL", + daop: 1, + fgEnable: 1, + haveSchedule: true, + updatedAt: "2024-03-10T09:55:07.213Z", + }, + { + id: "AK", + name: "ANGKE", + daop: 1, + fgEnable: 1, + haveSchedule: true, + updatedAt: "2024-03-10T09:55:07.213Z", + }, + ], + }) +}) + +controller.openapi(route.getById, (c) => { + const { id } = c.req.valid("param") + return c.json({ + status: 200, + data: { + id, + name: "ANCOL", + daop: 1, + fgEnable: 1, + haveSchedule: true, + updatedAt: "2024-03-10T09:55:07.213Z", + }, + }) +}) + +export default controller diff --git a/src/modules/v1/station/station.route.ts b/src/modules/v1/station/station.route.ts new file mode 100644 index 0000000..31ec63a --- /dev/null +++ b/src/modules/v1/station/station.route.ts @@ -0,0 +1,47 @@ +import { createRoute, z } from "@hono/zod-openapi" +import { getByIdRequestSchema, stationResponseSchema } from "./station.schema" + +export const getAll = createRoute({ + method: "get", + path: "/", + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + status: z.number().openapi({ + example: 200, + }), + data: z.array(stationResponseSchema), + }), + }, + }, + description: "Retrieve all the station", + }, + }, + tags: ["Station"], +}) + +export const getById = createRoute({ + method: "get", + path: "/{id}", + request: { + params: getByIdRequestSchema, + }, + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + status: z.number().openapi({ + example: 200, + }), + data: stationResponseSchema, + }), + }, + }, + description: "Retrieve one of the station", + }, + }, + tags: ["Station"], +}) diff --git a/src/modules/v1/station/station.schema.ts b/src/modules/v1/station/station.schema.ts new file mode 100644 index 0000000..0ec1a98 --- /dev/null +++ b/src/modules/v1/station/station.schema.ts @@ -0,0 +1,38 @@ +import { z } from "zod" +import { stationSchema } from "../../../db/schema" + +export const stationResponseSchema = z + .object({ + id: stationSchema.shape.id.openapi({ + example: "MRI", + }), + name: stationSchema.shape.name.openapi({ + example: "MANGGARAI", + }), + daop: stationSchema.shape.daop.openapi({ + example: 1, + }), + fgEnable: stationSchema.shape.fgEnable.openapi({ + example: 1, + }), + haveSchedule: stationSchema.shape.haveSchedule.openapi({ + example: true, + }), + updatedAt: stationSchema.shape.updatedAt.openapi({ + example: "2024-03-10T09:55:07.213Z", + }), + }) + .openapi("Station") satisfies typeof stationSchema + +export const getByIdRequestSchema = z.object({ + id: z + .string() + .min(2) + .openapi({ + param: { + name: "id", + in: "path", + }, + example: "MRI", + }), +}) From d89c39825279cbb34519044be1f33cc3c892a94b Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:41:51 +0700 Subject: [PATCH 06/64] wip --- drizzle.config.ts | 9 +- .../migrations/0000_futuristic_colossus.sql | 23 -- .../migrations/0000_illegal_human_torch.sql | 11 + .../migrations/0001_dapper_the_professor.sql | 31 -- .../migrations/0001_first_newton_destine.sql | 1 + drizzle/migrations/0002_smart_vermin.sql | 1 - .../0003_confused_lethal_legion.sql | 1 - drizzle/migrations/meta/0000_snapshot.json | 164 +++-------- drizzle/migrations/meta/0001_snapshot.json | 278 ++++-------------- drizzle/migrations/meta/0002_snapshot.json | 270 ----------------- drizzle/migrations/meta/0003_snapshot.json | 275 ----------------- drizzle/migrations/meta/_journal.json | 32 +- package.json | 3 + src/app.ts | 44 ++- src/db/schema-new/index.ts | 46 +++ src/db/schema/index.ts | 1 - src/modules/v1/station/station.controller.ts | 158 +++++++--- src/modules/v1/station/station.route.ts | 71 ++++- src/modules/v1/station/station.schema.ts | 22 +- 19 files changed, 427 insertions(+), 1014 deletions(-) delete mode 100644 drizzle/migrations/0000_futuristic_colossus.sql create mode 100644 drizzle/migrations/0000_illegal_human_torch.sql delete mode 100644 drizzle/migrations/0001_dapper_the_professor.sql create mode 100644 drizzle/migrations/0001_first_newton_destine.sql delete mode 100644 drizzle/migrations/0002_smart_vermin.sql delete mode 100644 drizzle/migrations/0003_confused_lethal_legion.sql delete mode 100644 drizzle/migrations/meta/0002_snapshot.json delete mode 100644 drizzle/migrations/meta/0003_snapshot.json create mode 100644 src/db/schema-new/index.ts diff --git a/drizzle.config.ts b/drizzle.config.ts index d3077d5..10b4b73 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,7 +1,14 @@ import type { Config } from "drizzle-kit" -export default { +/* export default { schema: "./src/db/schema", out: "./drizzle/migrations", dialect: "postgresql", +} satisfies Config */ + +export default { + out: "./drizzle/migrations", + dialect: "sqlite", + driver: "d1-http", + schema: "./src/db/schema-new", } satisfies Config diff --git a/drizzle/migrations/0000_futuristic_colossus.sql b/drizzle/migrations/0000_futuristic_colossus.sql deleted file mode 100644 index 078434d..0000000 --- a/drizzle/migrations/0000_futuristic_colossus.sql +++ /dev/null @@ -1,23 +0,0 @@ -CREATE TABLE IF NOT EXISTS "schedule" ( - "id" text PRIMARY KEY NOT NULL, - "station_id" text DEFAULT NULL, - "train_id" text DEFAULT NULL, - "line" text DEFAULT NULL, - "route" text DEFAULT NULL, - "color" text DEFAULT NULL, - "destination" text DEFAULT NULL, - "time_estimated" time DEFAULT NULL, - "destination_time" time DEFAULT NULL, - "updated_at" text DEFAULT (CURRENT_TIMESTAMP), - CONSTRAINT "schedule_id_unique" UNIQUE("id") -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "station" ( - "id" text PRIMARY KEY NOT NULL, - "name" text DEFAULT NULL, - "daop" integer DEFAULT NULL, - "fg_enable" integer DEFAULT NULL, - "have_schedule" boolean DEFAULT true, - "updated_at" text DEFAULT (CURRENT_TIMESTAMP), - CONSTRAINT "station_id_unique" UNIQUE("id") -); diff --git a/drizzle/migrations/0000_illegal_human_torch.sql b/drizzle/migrations/0000_illegal_human_torch.sql new file mode 100644 index 0000000..0c08581 --- /dev/null +++ b/drizzle/migrations/0000_illegal_human_torch.sql @@ -0,0 +1,11 @@ +CREATE TABLE `station` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `type` text NOT NULL, + `metadata` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `station_id_unique` ON `station` (`id`);--> statement-breakpoint +CREATE INDEX `station_idx` ON `station` (`id`); \ No newline at end of file diff --git a/drizzle/migrations/0001_dapper_the_professor.sql b/drizzle/migrations/0001_dapper_the_professor.sql deleted file mode 100644 index aba4df6..0000000 --- a/drizzle/migrations/0001_dapper_the_professor.sql +++ /dev/null @@ -1,31 +0,0 @@ -DO $$ BEGIN - CREATE TYPE "sync_from" AS ENUM('cron', 'manual'); -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - CREATE TYPE "sync_item" AS ENUM('station', 'schedule'); -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - CREATE TYPE "sync_status" AS ENUM('PENDING', 'SUCCESS', 'FAILED'); -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "sync" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "n" bigserial NOT NULL, - "type" "sync_from" DEFAULT 'manual', - "status" "sync_status" DEFAULT 'PENDING', - "item" "sync_item", - "duration" bigint DEFAULT 0, - "message" text DEFAULT NULL, - "started_at" text DEFAULT (CURRENT_TIMESTAMP), - "ended_at" text DEFAULT NULL, - "created_at" text DEFAULT (CURRENT_TIMESTAMP), - CONSTRAINT "sync_id_unique" UNIQUE("id") -); diff --git a/drizzle/migrations/0001_first_newton_destine.sql b/drizzle/migrations/0001_first_newton_destine.sql new file mode 100644 index 0000000..6252b58 --- /dev/null +++ b/drizzle/migrations/0001_first_newton_destine.sql @@ -0,0 +1 @@ +CREATE INDEX `station_type_idx` ON `station` (`type`); \ No newline at end of file diff --git a/drizzle/migrations/0002_smart_vermin.sql b/drizzle/migrations/0002_smart_vermin.sql deleted file mode 100644 index db5b6c2..0000000 --- a/drizzle/migrations/0002_smart_vermin.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE INDEX IF NOT EXISTS "station_idx" ON "schedule" ("station_id"); \ No newline at end of file diff --git a/drizzle/migrations/0003_confused_lethal_legion.sql b/drizzle/migrations/0003_confused_lethal_legion.sql deleted file mode 100644 index 74f14f3..0000000 --- a/drizzle/migrations/0003_confused_lethal_legion.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE INDEX IF NOT EXISTS "time_estimated_idx" ON "schedule" ("time_estimated"); \ No newline at end of file diff --git a/drizzle/migrations/meta/0000_snapshot.json b/drizzle/migrations/meta/0000_snapshot.json index 52e356c..14436a9 100644 --- a/drizzle/migrations/meta/0000_snapshot.json +++ b/drizzle/migrations/meta/0000_snapshot.json @@ -1,157 +1,85 @@ { - "id": "3971c9c2-bffe-4208-bf4a-20601164fd08", + "version": "6", + "dialect": "sqlite", + "id": "80e17491-98fa-42b1-b306-3962e64e21a5", "prevId": "00000000-0000-0000-0000-000000000000", - "version": "5", - "dialect": "pg", "tables": { - "schedule": { - "name": "schedule", - "schema": "", + "station": { + "name": "station", "columns": { "id": { "name": "id", "type": "text", "primaryKey": true, - "notNull": true - }, - "station_id": { - "name": "station_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" + "notNull": true, + "autoincrement": false }, - "train_id": { - "name": "train_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "line": { - "name": "line", + "name": { + "name": "name", "type": "text", "primaryKey": false, - "notNull": false, - "default": "NULL" + "notNull": true, + "autoincrement": false }, - "route": { - "name": "route", + "type": { + "name": "type", "type": "text", "primaryKey": false, - "notNull": false, - "default": "NULL" + "notNull": true, + "autoincrement": false }, - "color": { - "name": "color", + "metadata": { + "name": "metadata", "type": "text", "primaryKey": false, "notNull": false, - "default": "NULL" + "autoincrement": false }, - "destination": { - "name": "destination", + "created_at": { + "name": "created_at", "type": "text", "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "time_estimated": { - "name": "time_estimated", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "destination_time": { - "name": "destination_time", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" }, "updated_at": { "name": "updated_at", "type": "text", "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" } }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "schedule_id_unique": { - "name": "schedule_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - }, - "station": { - "name": "station", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "daop": { - "name": "daop", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "fg_enable": { - "name": "fg_enable", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "have_schedule": { - "name": "have_schedule", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": "true" + "indexes": { + "station_id_unique": { + "name": "station_id_unique", + "columns": [ + "id" + ], + "isUnique": true }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" + "station_idx": { + "name": "station_idx", + "columns": [ + "id" + ], + "isUnique": false } }, - "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, - "uniqueConstraints": { - "station_id_unique": { - "name": "station_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } + "uniqueConstraints": {} } }, "enums": {}, - "schemas": {}, "_meta": { - "columns": {}, "schemas": {}, - "tables": {} + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} } -} +} \ No newline at end of file diff --git a/drizzle/migrations/meta/0001_snapshot.json b/drizzle/migrations/meta/0001_snapshot.json index 2f410ac..a794328 100644 --- a/drizzle/migrations/meta/0001_snapshot.json +++ b/drizzle/migrations/meta/0001_snapshot.json @@ -1,264 +1,92 @@ { - "id": "4e205dc5-b3ad-4093-989f-1aa9745b846e", - "prevId": "3971c9c2-bffe-4208-bf4a-20601164fd08", - "version": "5", - "dialect": "pg", + "version": "6", + "dialect": "sqlite", + "id": "aea2927c-ff35-44be-b0cb-086a5dd6748d", + "prevId": "80e17491-98fa-42b1-b306-3962e64e21a5", "tables": { - "schedule": { - "name": "schedule", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "station_id": { - "name": "station_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "train_id": { - "name": "train_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "line": { - "name": "line", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "route": { - "name": "route", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "color": { - "name": "color", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "destination": { - "name": "destination", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "time_estimated": { - "name": "time_estimated", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "destination_time": { - "name": "destination_time", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "schedule_id_unique": { - "name": "schedule_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - }, "station": { "name": "station", - "schema": "", "columns": { "id": { "name": "id", "type": "text", "primaryKey": true, - "notNull": true + "notNull": true, + "autoincrement": false }, "name": { "name": "name", "type": "text", "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "daop": { - "name": "daop", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "fg_enable": { - "name": "fg_enable", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "have_schedule": { - "name": "have_schedule", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": "true" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "station_id_unique": { - "name": "station_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - }, - "sync": { - "name": "sync", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, "notNull": true, - "default": "gen_random_uuid()" - }, - "n": { - "name": "n", - "type": "bigserial", - "primaryKey": false, - "notNull": true + "autoincrement": false }, "type": { "name": "type", - "type": "sync_from", - "primaryKey": false, - "notNull": false, - "default": "'manual'" - }, - "status": { - "name": "status", - "type": "sync_status", - "primaryKey": false, - "notNull": false, - "default": "'PENDING'" - }, - "item": { - "name": "item", - "type": "sync_item", - "primaryKey": false, - "notNull": false - }, - "duration": { - "name": "duration", - "type": "bigint", - "primaryKey": false, - "notNull": false, - "default": 0 - }, - "message": { - "name": "message", "type": "text", "primaryKey": false, - "notNull": false, - "default": "NULL" + "notNull": true, + "autoincrement": false }, - "started_at": { - "name": "started_at", + "metadata": { + "name": "metadata", "type": "text", "primaryKey": false, "notNull": false, - "default": "(CURRENT_TIMESTAMP)" + "autoincrement": false }, - "ended_at": { - "name": "ended_at", + "created_at": { + "name": "created_at", "type": "text", "primaryKey": false, - "notNull": false, - "default": "NULL" + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" }, - "created_at": { - "name": "created_at", + "updated_at": { + "name": "updated_at", "type": "text", "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "station_id_unique": { + "name": "station_id_unique", + "columns": [ + "id" + ], + "isUnique": true + }, + "station_idx": { + "name": "station_idx", + "columns": [ + "id" + ], + "isUnique": false + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + "type" + ], + "isUnique": false } }, - "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, - "uniqueConstraints": { - "sync_id_unique": { - "name": "sync_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } + "uniqueConstraints": {} } }, - "enums": { - "sync_from": { - "name": "sync_from", - "values": { - "cron": "cron", - "manual": "manual" - } - }, - "sync_item": { - "name": "sync_item", - "values": { - "station": "station", - "schedule": "schedule" - } - }, - "sync_status": { - "name": "sync_status", - "values": { - "PENDING": "PENDING", - "SUCCESS": "SUCCESS", - "FAILED": "FAILED" - } - } - }, - "schemas": {}, + "enums": {}, "_meta": { - "columns": {}, "schemas": {}, - "tables": {} + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} } -} +} \ No newline at end of file diff --git a/drizzle/migrations/meta/0002_snapshot.json b/drizzle/migrations/meta/0002_snapshot.json deleted file mode 100644 index 8151826..0000000 --- a/drizzle/migrations/meta/0002_snapshot.json +++ /dev/null @@ -1,270 +0,0 @@ -{ - "id": "64239a74-574c-46d7-b0d1-e0285ce4bd81", - "prevId": "4e205dc5-b3ad-4093-989f-1aa9745b846e", - "version": "5", - "dialect": "pg", - "tables": { - "schedule": { - "name": "schedule", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "station_id": { - "name": "station_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "train_id": { - "name": "train_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "line": { - "name": "line", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "route": { - "name": "route", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "color": { - "name": "color", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "destination": { - "name": "destination", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "time_estimated": { - "name": "time_estimated", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "destination_time": { - "name": "destination_time", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": { - "station_idx": { - "name": "station_idx", - "columns": ["station_id"], - "isUnique": false - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "schedule_id_unique": { - "name": "schedule_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - }, - "station": { - "name": "station", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "daop": { - "name": "daop", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "fg_enable": { - "name": "fg_enable", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "have_schedule": { - "name": "have_schedule", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": "true" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "station_id_unique": { - "name": "station_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - }, - "sync": { - "name": "sync", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "n": { - "name": "n", - "type": "bigserial", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "sync_from", - "primaryKey": false, - "notNull": false, - "default": "'manual'" - }, - "status": { - "name": "status", - "type": "sync_status", - "primaryKey": false, - "notNull": false, - "default": "'PENDING'" - }, - "item": { - "name": "item", - "type": "sync_item", - "primaryKey": false, - "notNull": false - }, - "duration": { - "name": "duration", - "type": "bigint", - "primaryKey": false, - "notNull": false, - "default": 0 - }, - "message": { - "name": "message", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "started_at": { - "name": "started_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - }, - "ended_at": { - "name": "ended_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "sync_id_unique": { - "name": "sync_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - } - }, - "enums": { - "sync_from": { - "name": "sync_from", - "values": { - "cron": "cron", - "manual": "manual" - } - }, - "sync_item": { - "name": "sync_item", - "values": { - "station": "station", - "schedule": "schedule" - } - }, - "sync_status": { - "name": "sync_status", - "values": { - "PENDING": "PENDING", - "SUCCESS": "SUCCESS", - "FAILED": "FAILED" - } - } - }, - "schemas": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/drizzle/migrations/meta/0003_snapshot.json b/drizzle/migrations/meta/0003_snapshot.json deleted file mode 100644 index e3d862a..0000000 --- a/drizzle/migrations/meta/0003_snapshot.json +++ /dev/null @@ -1,275 +0,0 @@ -{ - "id": "a483bfd1-dad0-4549-98bb-58a0196f6440", - "prevId": "64239a74-574c-46d7-b0d1-e0285ce4bd81", - "version": "5", - "dialect": "pg", - "tables": { - "schedule": { - "name": "schedule", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "station_id": { - "name": "station_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "train_id": { - "name": "train_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "line": { - "name": "line", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "route": { - "name": "route", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "color": { - "name": "color", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "destination": { - "name": "destination", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "time_estimated": { - "name": "time_estimated", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "destination_time": { - "name": "destination_time", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": { - "station_idx": { - "name": "station_idx", - "columns": ["station_id"], - "isUnique": false - }, - "time_estimated_idx": { - "name": "time_estimated_idx", - "columns": ["time_estimated"], - "isUnique": false - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "schedule_id_unique": { - "name": "schedule_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - }, - "station": { - "name": "station", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "daop": { - "name": "daop", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "fg_enable": { - "name": "fg_enable", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "have_schedule": { - "name": "have_schedule", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": "true" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "station_id_unique": { - "name": "station_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - }, - "sync": { - "name": "sync", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "n": { - "name": "n", - "type": "bigserial", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "sync_from", - "primaryKey": false, - "notNull": false, - "default": "'manual'" - }, - "status": { - "name": "status", - "type": "sync_status", - "primaryKey": false, - "notNull": false, - "default": "'PENDING'" - }, - "item": { - "name": "item", - "type": "sync_item", - "primaryKey": false, - "notNull": false - }, - "duration": { - "name": "duration", - "type": "bigint", - "primaryKey": false, - "notNull": false, - "default": 0 - }, - "message": { - "name": "message", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "started_at": { - "name": "started_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - }, - "ended_at": { - "name": "ended_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "sync_id_unique": { - "name": "sync_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - } - }, - "enums": { - "sync_from": { - "name": "sync_from", - "values": { - "cron": "cron", - "manual": "manual" - } - }, - "sync_item": { - "name": "sync_item", - "values": { - "station": "station", - "schedule": "schedule" - } - }, - "sync_status": { - "name": "sync_status", - "values": { - "PENDING": "PENDING", - "SUCCESS": "SUCCESS", - "FAILED": "FAILED" - } - } - }, - "schemas": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index e8a85ba..3f2eef4 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -1,34 +1,20 @@ { - "version": "5", - "dialect": "pg", + "version": "7", + "dialect": "sqlite", "entries": [ { "idx": 0, - "version": "5", - "when": 1709969312190, - "tag": "0000_futuristic_colossus", + "version": "6", + "when": 1726033595340, + "tag": "0000_illegal_human_torch", "breakpoints": true }, { "idx": 1, - "version": "5", - "when": 1709976152377, - "tag": "0001_dapper_the_professor", - "breakpoints": true - }, - { - "idx": 2, - "version": "5", - "when": 1710153775566, - "tag": "0002_smart_vermin", - "breakpoints": true - }, - { - "idx": 3, - "version": "5", - "when": 1710159697753, - "tag": "0003_confused_lethal_legion", + "version": "6", + "when": 1726035106303, + "tag": "0001_first_newton_destine", "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/package.json b/package.json index 6f45cd0..f00e84b 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,9 @@ "db:migrate": "bun run src/db/migrate.ts", "db:pull": "drizzle-kit introspect:pg", "db:check": "drizzle-kit check:pg", + "migration:drop": "drizzle-kit drop", + "migration:generate": "drizzle-kit generate", + "migration:apply": "bun run src/db/migrate.ts", "format": "prettier -w .", "format:check": "prettier -c .", "prepare": "husky" diff --git a/src/app.ts b/src/app.ts index 2d49f45..13f706d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,8 +1,28 @@ +import { D1Database, KVNamespace } from "@cloudflare/workers-types" import { OpenAPIHono } from "@hono/zod-openapi" import { apiReference } from "@scalar/hono-api-reference" +import { DrizzleD1Database } from "drizzle-orm/d1" +import { NewStation, station } from "./db/schema-new" import v1 from "./modules/v1" +import { dbMiddleware } from "./modules/v1/station/station.route" -const app = new OpenAPIHono() +export type Bindings = { + DB: D1Database + KV: KVNamespace +} + +export type Variables = { + db: DrizzleD1Database +} + +export type Environments = { + Bindings: Bindings + Variables: Variables +} + +const app = new OpenAPIHono() + +app.use(dbMiddleware) app.route("/v1", v1) @@ -18,6 +38,28 @@ app.use( app.get("/", (c) => c.json({ status: "ok" })) +app.post("/echo", async (c) => { + const db = c.get("db") + + const insert = { + id: "AC", + name: "ANCOL", + type: "KRL", + metadata: { + fgEnable: 1, + haveSchedule: true, + daop: 1, + }, + } satisfies NewStation + + const data = await db.insert(station).values(insert).returning() + + return c.json({ + status: 200, + data, + }) +}) + app.doc("/openapi", { openapi: "3.0.0", info: { diff --git a/src/db/schema-new/index.ts b/src/db/schema-new/index.ts new file mode 100644 index 0000000..28533ad --- /dev/null +++ b/src/db/schema-new/index.ts @@ -0,0 +1,46 @@ +import { sql } from "drizzle-orm" +import { index, sqliteTable, text } from "drizzle-orm/sqlite-core" +import { createSelectSchema } from "drizzle-zod" +import { z } from "zod" + +const stationMetadata = z.object({ + /** KRL Metadata */ + daop: z.number().nullable(), + fgEnable: z.number().nullable(), + haveSchedule: z.boolean().nullable(), +}) + +export type StationMetadata = z.infer + +export const station = sqliteTable( + "station", + { + id: text("id").primaryKey().unique(), + name: text("name").notNull(), + type: text("type", { + /* Station type */ + enum: ["KRL", "MRT", "LRT"], + }).notNull(), + metadata: text("metadata", { + mode: "json", + }).$type(), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => { + return { + stationIdx: index("station_idx").on(table.id), + typeIdx: index("station_type_idx").on(table.type), + } + }, +) + +export const stationSchema = createSelectSchema(station) + +export type NewStation = typeof station.$inferInsert + +export type Station = z.infer diff --git a/src/db/schema/index.ts b/src/db/schema/index.ts index 98c7aea..9b58af1 100644 --- a/src/db/schema/index.ts +++ b/src/db/schema/index.ts @@ -3,7 +3,6 @@ import { bigint, bigserial, boolean, - date, index, integer, pgEnum, diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index 5ca04aa..767d7a1 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -1,45 +1,137 @@ -import { OpenAPIHono } from "@hono/zod-openapi" +import { OpenAPIHono, z } from "@hono/zod-openapi" +import { eq, getTableColumns, SQL, sql } from "drizzle-orm" +import { SQLiteTable } from "drizzle-orm/sqlite-core" +import { Environments } from "../../../app" +import { NewStation, station } from "../../../db/schema-new" import * as route from "./station.route" +import { stationResponseSchema } from "./station.schema" -const controller = new OpenAPIHono() +const controller = new OpenAPIHono() -controller.openapi(route.getAll, (c) => { - return c.json({ - status: 200, - data: [ - { - id: "AC", - name: "ANCOL", - daop: 1, - fgEnable: 1, - haveSchedule: true, - updatedAt: "2024-03-10T09:55:07.213Z", - }, - { - id: "AK", - name: "ANGKE", - daop: 1, - fgEnable: 1, - haveSchedule: true, - updatedAt: "2024-03-10T09:55:07.213Z", +controller.use(route.dbMiddleware) + +controller.openapi(route.getAll, async (c) => { + const db = c.get("db") + const stations = await db.select().from(station) + + return c.json( + { + metadata: { + status: 200, + message: "Success", }, - ], - }) + data: stations, + }, + 200, + ) }) -controller.openapi(route.getById, (c) => { +controller.openapi(route.getById, async (c) => { const { id } = c.req.valid("param") - return c.json({ - status: 200, - data: { - id, - name: "ANCOL", - daop: 1, - fgEnable: 1, - haveSchedule: true, - updatedAt: "2024-03-10T09:55:07.213Z", + const db = c.get("db") + const data = await db.select().from(station).where(eq(station.id, id)) + + if (data.length === 0) { + return c.json( + { + metadata: { + status: 404, + message: "Station data is not found", + }, + }, + 404, + ) + } + + return c.json( + { + metadata: { + status: 200, + message: "Success", + }, + data: stationResponseSchema.parse(data[0]), }, + 200, + ) +}) + +controller.openapi(route.sync, async (c) => { + const db = c.get("db") + + const req = await fetch( + "https://api-partner.krl.co.id/krlweb/v1/krl-station", + ).then((res) => res.json()) + + const schema = z.object({ + status: z.number(), + message: z.string(), + data: z.array( + z.object({ + sta_id: z.string(), + sta_name: z.string(), + group_wil: z.number(), + fg_enable: z.number(), + }), + ), }) + + const parsed = schema.parse(req) + + const filterdStation = parsed.data.filter((d) => !d.sta_id.includes("WIL")) + + const insertStations = filterdStation.map((s) => { + return { + id: s.sta_id, + name: s.sta_name, + type: "KRL", + metadata: { + fgEnable: s.fg_enable, + haveSchedule: true, + daop: s.group_wil === 0 ? 1 : s.group_wil, + }, + } + }) satisfies NewStation[] + + let chunkSize = 32 + + for (let i = 0; i < insertStations.length; i += chunkSize) { + await db + .insert(station) + .values(insertStations.slice(i, i + chunkSize)) + .onConflictDoUpdate({ + target: station.id, + set: conflictUpdateAllExcept(station, ["id", "name"]), + }) + .returning() + } + + return c.json( + { + metadata: { + status: 200, + message: "Success", + }, + }, + 200, + ) }) export default controller + +export function conflictUpdateAllExcept< + T extends SQLiteTable, + E extends (keyof T["$inferInsert"])[], +>(table: T, except: E) { + const columns = getTableColumns(table) + const updateColumns = Object.entries(columns).filter( + ([col]) => !except.includes(col as keyof typeof table.$inferInsert), + ) + + return updateColumns.reduce( + (acc, [colName, table]) => ({ + ...acc, + [colName]: sql.raw(`excluded.${table.name}`), + }), + {}, + ) as Omit, E[number]> +} diff --git a/src/modules/v1/station/station.route.ts b/src/modules/v1/station/station.route.ts index 31ec63a..6075f39 100644 --- a/src/modules/v1/station/station.route.ts +++ b/src/modules/v1/station/station.route.ts @@ -1,6 +1,17 @@ import { createRoute, z } from "@hono/zod-openapi" +import { drizzle } from "drizzle-orm/d1" +import { createMiddleware } from "hono/factory" +import { Environments } from "../../../app" +import * as schema from "../../../db/schema-new" import { getByIdRequestSchema, stationResponseSchema } from "./station.schema" +export const connectDB = (env: D1Database) => drizzle(env, { schema }) + +export const dbMiddleware = createMiddleware(async (c, next) => { + c.set("db", connectDB(c.env.DB)) + await next() +}) + export const getAll = createRoute({ method: "get", path: "/", @@ -9,8 +20,13 @@ export const getAll = createRoute({ content: { "application/json": { schema: z.object({ - status: z.number().openapi({ - example: 200, + metadata: z.object({ + status: z.number().openapi({ + example: 200, + }), + message: z.string().openapi({ + example: "Success", + }), }), data: z.array(stationResponseSchema), }), @@ -33,8 +49,13 @@ export const getById = createRoute({ content: { "application/json": { schema: z.object({ - status: z.number().openapi({ - example: 200, + metadata: z.object({ + status: z.number().openapi({ + example: 200, + }), + message: z.string().openapi({ + example: "Success", + }), }), data: stationResponseSchema, }), @@ -42,6 +63,48 @@ export const getById = createRoute({ }, description: "Retrieve one of the station", }, + 404: { + content: { + "application/json": { + schema: z.object({ + metadata: z.object({ + status: z.number().openapi({ + example: 404, + }), + message: z.string().openapi({ + example: "Station data is not found", + }), + }), + }), + }, + }, + description: "Station data is not found", + }, + }, + tags: ["Station"], +}) + +export const sync = createRoute({ + method: "post", + path: "/", + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + metadata: z.object({ + status: z.number().openapi({ + example: 200, + }), + message: z.string().openapi({ + example: "Success", + }), + }), + }), + }, + }, + description: "Sync station data", + }, }, tags: ["Station"], }) diff --git a/src/modules/v1/station/station.schema.ts b/src/modules/v1/station/station.schema.ts index 0ec1a98..96f6ace 100644 --- a/src/modules/v1/station/station.schema.ts +++ b/src/modules/v1/station/station.schema.ts @@ -1,5 +1,5 @@ import { z } from "zod" -import { stationSchema } from "../../../db/schema" +import { StationMetadata, stationSchema } from "../../../db/schema-new" export const stationResponseSchema = z .object({ @@ -9,16 +9,24 @@ export const stationResponseSchema = z name: stationSchema.shape.name.openapi({ example: "MANGGARAI", }), - daop: stationSchema.shape.daop.openapi({ - example: 1, + type: stationSchema.shape.type.openapi({ + type: "string", + example: "KRL", }), - fgEnable: stationSchema.shape.fgEnable.openapi({ - example: 1, + metadata: stationSchema.shape.metadata.openapi({ + type: "object", + example: { + fgEnable: 1, + haveSchedule: true, + daop: 1, + } satisfies StationMetadata, }), - haveSchedule: stationSchema.shape.haveSchedule.openapi({ - example: true, + createdAt: stationSchema.shape.createdAt.openapi({ + format: "date-time", + example: "2024-03-10T09:55:07.213Z", }), updatedAt: stationSchema.shape.updatedAt.openapi({ + format: "date-time", example: "2024-03-10T09:55:07.213Z", }), }) From 9b30c1f15c33428368cb60f90c639fc8951f4d6a Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Fri, 13 Sep 2024 10:13:42 +0700 Subject: [PATCH 07/64] use neon instead d1 --- .dev.example.vars | 2 + .gitignore | 1 + docker-compose.yml | 20 ++- drizzle.config.ts | 9 +- .../migrations/0000_illegal_human_torch.sql | 11 -- drizzle/migrations/0000_right_lilith.sql | 20 +++ .../migrations/0001_first_newton_destine.sql | 1 - drizzle/migrations/meta/0000_snapshot.json | 122 ++++++++++++------ drizzle/migrations/meta/0001_snapshot.json | 92 ------------- drizzle/migrations/meta/_journal.json | 15 +-- package.json | 1 + src/app.ts | 10 +- src/db/migrate.ts | 10 +- src/db/schema-new/index.ts | 39 +++--- src/modules/v1/station/station.controller.ts | 32 ++--- src/modules/v1/station/station.route.ts | 20 ++- src/modules/v1/station/station.schema.ts | 3 + wrangler.example.toml | 8 -- 18 files changed, 193 insertions(+), 223 deletions(-) create mode 100644 .dev.example.vars delete mode 100644 drizzle/migrations/0000_illegal_human_torch.sql create mode 100644 drizzle/migrations/0000_right_lilith.sql delete mode 100644 drizzle/migrations/0001_first_newton_destine.sql delete mode 100644 drizzle/migrations/meta/0001_snapshot.json diff --git a/.dev.example.vars b/.dev.example.vars new file mode 100644 index 0000000..e454f87 --- /dev/null +++ b/.dev.example.vars @@ -0,0 +1,2 @@ +DATABASE_URL="postgresql://comuline:password@localhost:5432/comuline" +COMULINE_ENV="development" \ No newline at end of file diff --git a/.gitignore b/.gitignore index fb5a1bf..2503d14 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ package-lock.json .env wrangler.toml .wrangler +.dev.vars diff --git a/docker-compose.yml b/docker-compose.yml index c54e5e2..42299ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,19 +3,17 @@ version: "3.9" services: postgres: image: "postgres:15.2-alpine" - restart: always - container_name: "comuline-db" ports: - "5432:5432" - volumes: - - db:/var/lib/postgresql/data env_file: - ./.env.db - redis: - image: redis - container_name: "comuline-cache" + pg_proxy: + image: ghcr.io/neondatabase/wsproxy:latest + environment: + APPEND_PORT: "postgres:5432" + ALLOW_ADDR_REGEX: ".*" + LOG_TRAFFIC: "true" ports: - - "6379:6379" - -volumes: - db: + - "5433:80" + depends_on: + - postgres diff --git a/drizzle.config.ts b/drizzle.config.ts index 10b4b73..97794f9 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,14 +1,7 @@ import type { Config } from "drizzle-kit" -/* export default { - schema: "./src/db/schema", - out: "./drizzle/migrations", - dialect: "postgresql", -} satisfies Config */ - export default { out: "./drizzle/migrations", - dialect: "sqlite", - driver: "d1-http", + dialect: "postgresql", schema: "./src/db/schema-new", } satisfies Config diff --git a/drizzle/migrations/0000_illegal_human_torch.sql b/drizzle/migrations/0000_illegal_human_torch.sql deleted file mode 100644 index 0c08581..0000000 --- a/drizzle/migrations/0000_illegal_human_torch.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE `station` ( - `id` text PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `type` text NOT NULL, - `metadata` text, - `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, - `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL -); ---> statement-breakpoint -CREATE UNIQUE INDEX `station_id_unique` ON `station` (`id`);--> statement-breakpoint -CREATE INDEX `station_idx` ON `station` (`id`); \ No newline at end of file diff --git a/drizzle/migrations/0000_right_lilith.sql b/drizzle/migrations/0000_right_lilith.sql new file mode 100644 index 0000000..0a27377 --- /dev/null +++ b/drizzle/migrations/0000_right_lilith.sql @@ -0,0 +1,20 @@ +DO $$ BEGIN + CREATE TYPE "public"."station_type" AS ENUM('KRL', 'MRT', 'LRT'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "station" ( + "uid" text PRIMARY KEY NOT NULL, + "id" text NOT NULL, + "name" text NOT NULL, + "type" "station_type" NOT NULL, + "metadata" jsonb, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "station_uid_unique" UNIQUE("uid") +); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "station_uidx" ON "station" USING btree ("uid");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "station_idx" ON "station" USING btree ("id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "station_type_idx" ON "station" USING btree ("type"); \ No newline at end of file diff --git a/drizzle/migrations/0001_first_newton_destine.sql b/drizzle/migrations/0001_first_newton_destine.sql deleted file mode 100644 index 6252b58..0000000 --- a/drizzle/migrations/0001_first_newton_destine.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE INDEX `station_type_idx` ON `station` (`type`); \ No newline at end of file diff --git a/drizzle/migrations/meta/0000_snapshot.json b/drizzle/migrations/meta/0000_snapshot.json index 14436a9..d447655 100644 --- a/drizzle/migrations/meta/0000_snapshot.json +++ b/drizzle/migrations/meta/0000_snapshot.json @@ -1,85 +1,135 @@ { - "version": "6", - "dialect": "sqlite", - "id": "80e17491-98fa-42b1-b306-3962e64e21a5", + "id": "c80c0a7c-b2d3-4986-98d7-4207b4451438", "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", "tables": { - "station": { + "public.station": { "name": "station", + "schema": "", "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, "id": { "name": "id", "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false + "primaryKey": false, + "notNull": true }, "name": { "name": "name", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "type": { "name": "type", - "type": "text", + "type": "station_type", + "typeSchema": "public", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "metadata": { "name": "metadata", - "type": "text", + "type": "jsonb", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "created_at": { "name": "created_at", - "type": "text", + "type": "timestamp with time zone", "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "CURRENT_TIMESTAMP" + "notNull": false, + "default": "now()" }, "updated_at": { "name": "updated_at", - "type": "text", + "type": "timestamp with time zone", "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "CURRENT_TIMESTAMP" + "notNull": false, + "default": "now()" } }, "indexes": { - "station_id_unique": { - "name": "station_id_unique", + "station_uidx": { + "name": "station_uidx", "columns": [ - "id" + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "isUnique": true + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} }, "station_idx": { "name": "station_idx", "columns": [ - "id" + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "isUnique": false + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} } }, "foreignKeys": {}, "compositePrimaryKeys": {}, - "uniqueConstraints": {} + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + } + } } }, - "enums": {}, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT" + ] + } + }, + "schemas": {}, + "sequences": {}, "_meta": { + "columns": {}, "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} + "tables": {} } } \ No newline at end of file diff --git a/drizzle/migrations/meta/0001_snapshot.json b/drizzle/migrations/meta/0001_snapshot.json deleted file mode 100644 index a794328..0000000 --- a/drizzle/migrations/meta/0001_snapshot.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "aea2927c-ff35-44be-b0cb-086a5dd6748d", - "prevId": "80e17491-98fa-42b1-b306-3962e64e21a5", - "tables": { - "station": { - "name": "station", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "metadata": { - "name": "metadata", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "CURRENT_TIMESTAMP" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "CURRENT_TIMESTAMP" - } - }, - "indexes": { - "station_id_unique": { - "name": "station_id_unique", - "columns": [ - "id" - ], - "isUnique": true - }, - "station_idx": { - "name": "station_idx", - "columns": [ - "id" - ], - "isUnique": false - }, - "station_type_idx": { - "name": "station_type_idx", - "columns": [ - "type" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - } - }, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 3f2eef4..7abc21e 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -1,19 +1,12 @@ { "version": "7", - "dialect": "sqlite", + "dialect": "postgresql", "entries": [ { "idx": 0, - "version": "6", - "when": 1726033595340, - "tag": "0000_illegal_human_torch", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1726035106303, - "tag": "0001_first_newton_destine", + "version": "7", + "when": 1726196281461, + "tag": "0000_right_lilith", "breakpoints": true } ] diff --git a/package.json b/package.json index f00e84b..3001e1f 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "scripts": { "app": "wrangler dev src/app.ts", "test": "echo \"Error: no test specified\" && exit 1", + "deploy": "wrangler deploy", "dev": "bun run --watch src/index.ts", "start": "bun run src/index.ts", "db:push": "drizzle-kit push:pg", diff --git a/src/app.ts b/src/app.ts index 13f706d..0efde08 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,18 +1,19 @@ -import { D1Database, KVNamespace } from "@cloudflare/workers-types" +import { KVNamespace } from "@cloudflare/workers-types" import { OpenAPIHono } from "@hono/zod-openapi" import { apiReference } from "@scalar/hono-api-reference" -import { DrizzleD1Database } from "drizzle-orm/d1" +import { NeonDatabase } from "drizzle-orm/neon-serverless" import { NewStation, station } from "./db/schema-new" import v1 from "./modules/v1" import { dbMiddleware } from "./modules/v1/station/station.route" export type Bindings = { - DB: D1Database KV: KVNamespace + DATABASE_URL: string + COMULINE_ENV: string } export type Variables = { - db: DrizzleD1Database + db: NeonDatabase } export type Environments = { @@ -42,6 +43,7 @@ app.post("/echo", async (c) => { const db = c.get("db") const insert = { + uid: "st_krl_ac", id: "AC", name: "ANCOL", type: "KRL", diff --git a/src/db/migrate.ts b/src/db/migrate.ts index 9e3a5e5..6543c23 100644 --- a/src/db/migrate.ts +++ b/src/db/migrate.ts @@ -1,17 +1,15 @@ import { config } from "dotenv" +import { drizzle } from "drizzle-orm/postgres-js" import { migrate } from "drizzle-orm/postgres-js/migrator" import postgres from "postgres" -import { drizzle } from "drizzle-orm/postgres-js" -import { logger } from "../commons/utils/log" - -config({ path: ".env" }) +config({ path: ".dev.vars" }) const url = `${process.env.DATABASE_URL}` -const db = drizzle(postgres(url, { ssl: "require", max: 1 })) +const db = drizzle(postgres(url)) const main = async () => { - logger.info("Migrating database") + console.info("Migrating database") await migrate(db, { migrationsFolder: "drizzle/migrations" }) console.log("Migration complete") process.exit(0) diff --git a/src/db/schema-new/index.ts b/src/db/schema-new/index.ts index 28533ad..bfb7d60 100644 --- a/src/db/schema-new/index.ts +++ b/src/db/schema-new/index.ts @@ -1,5 +1,11 @@ -import { sql } from "drizzle-orm" -import { index, sqliteTable, text } from "drizzle-orm/sqlite-core" +import { + index, + jsonb, + pgEnum, + pgTable, + text, + timestamp, +} from "drizzle-orm/pg-core" import { createSelectSchema } from "drizzle-zod" import { z } from "zod" @@ -12,27 +18,26 @@ const stationMetadata = z.object({ export type StationMetadata = z.infer -export const station = sqliteTable( +export const stationTypeEnum = pgEnum("station_type", ["KRL", "MRT", "LRT"]) + +export const station = pgTable( "station", { - id: text("id").primaryKey().unique(), + uid: text("uid").primaryKey().unique().notNull(), + id: text("id").notNull(), name: text("name").notNull(), - type: text("type", { - /* Station type */ - enum: ["KRL", "MRT", "LRT"], - }).notNull(), - metadata: text("metadata", { - mode: "json", - }).$type(), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - updatedAt: text("updated_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), + type: stationTypeEnum("type").notNull(), + metadata: jsonb("metadata").$type(), + createdAt: timestamp("created_at", { + withTimezone: true, + }).defaultNow(), + updatedAt: timestamp("updated_at", { + withTimezone: true, + }).defaultNow(), }, (table) => { return { + stationUidx: index("station_uidx").on(table.uid), stationIdx: index("station_idx").on(table.id), typeIdx: index("station_type_idx").on(table.type), } diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index 767d7a1..37fb50c 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -1,8 +1,8 @@ import { OpenAPIHono, z } from "@hono/zod-openapi" import { eq, getTableColumns, SQL, sql } from "drizzle-orm" -import { SQLiteTable } from "drizzle-orm/sqlite-core" +import { PgTable } from "drizzle-orm/pg-core" import { Environments } from "../../../app" -import { NewStation, station } from "../../../db/schema-new" +import { NewStation, station, stationSchema } from "../../../db/schema-new" import * as route from "./station.route" import { stationResponseSchema } from "./station.schema" @@ -81,6 +81,7 @@ controller.openapi(route.sync, async (c) => { const insertStations = filterdStation.map((s) => { return { + uid: `st_krl_${s.sta_id.toLocaleLowerCase()}`, id: s.sta_id, name: s.sta_name, type: "KRL", @@ -92,18 +93,19 @@ controller.openapi(route.sync, async (c) => { } }) satisfies NewStation[] - let chunkSize = 32 - - for (let i = 0; i < insertStations.length; i += chunkSize) { - await db - .insert(station) - .values(insertStations.slice(i, i + chunkSize)) - .onConflictDoUpdate({ - target: station.id, - set: conflictUpdateAllExcept(station, ["id", "name"]), - }) - .returning() - } + await db + .insert(station) + .values(insertStations) + .onConflictDoUpdate({ + target: station.uid, + set: { + updatedAt: new Date(), + uid: sql`excluded.uid`, + id: sql`excluded.id`, + name: sql`excluded.name`, + }, + }) + .returning() return c.json( { @@ -119,7 +121,7 @@ controller.openapi(route.sync, async (c) => { export default controller export function conflictUpdateAllExcept< - T extends SQLiteTable, + T extends PgTable, E extends (keyof T["$inferInsert"])[], >(table: T, except: E) { const columns = getTableColumns(table) diff --git a/src/modules/v1/station/station.route.ts b/src/modules/v1/station/station.route.ts index 6075f39..270db33 100644 --- a/src/modules/v1/station/station.route.ts +++ b/src/modules/v1/station/station.route.ts @@ -1,14 +1,28 @@ import { createRoute, z } from "@hono/zod-openapi" -import { drizzle } from "drizzle-orm/d1" +import { neonConfig, Pool } from "@neondatabase/serverless" + +import { drizzle } from "drizzle-orm/neon-serverless" import { createMiddleware } from "hono/factory" import { Environments } from "../../../app" import * as schema from "../../../db/schema-new" import { getByIdRequestSchema, stationResponseSchema } from "./station.schema" -export const connectDB = (env: D1Database) => drizzle(env, { schema }) +export const connectDB = (url: string, env: string) => { + if (env === "development") { + // Set the WebSocket proxy to work with the local instance + neonConfig.wsProxy = (host) => `${host}:5433/v1` + // Disable all authentication and encryption + neonConfig.useSecureWebSocket = false + neonConfig.pipelineTLS = false + neonConfig.pipelineConnect = false + } + + const pool = new Pool({ connectionString: url, ssl: true }) + return drizzle(pool, { schema }) +} export const dbMiddleware = createMiddleware(async (c, next) => { - c.set("db", connectDB(c.env.DB)) + c.set("db", connectDB(c.env.DATABASE_URL, c.env.COMULINE_ENV)) await next() }) diff --git a/src/modules/v1/station/station.schema.ts b/src/modules/v1/station/station.schema.ts index 96f6ace..abc71fe 100644 --- a/src/modules/v1/station/station.schema.ts +++ b/src/modules/v1/station/station.schema.ts @@ -3,6 +3,9 @@ import { StationMetadata, stationSchema } from "../../../db/schema-new" export const stationResponseSchema = z .object({ + uid: stationSchema.shape.uid.openapi({ + example: "st_krl_mri", + }), id: stationSchema.shape.id.openapi({ example: "MRI", }), diff --git a/wrangler.example.toml b/wrangler.example.toml index 92a262c..878868d 100644 --- a/wrangler.example.toml +++ b/wrangler.example.toml @@ -2,17 +2,9 @@ name = "comuline-api" compatibility_date = "2024-08-21" main = "src/app.ts" minify = true -compatibility_flags = [ "nodejs_compat_v2" ] [limits] cpu_ms = 30000 -[vars] -DATABASE_URL = "" -PSTASH_REDIS_REST_TOKEN = "" -UPSTASH_REDIS_REST_URL = "" -SYNC_TOKEN = "" - - From 0db2d90a2c6d95a16b4c95d9ceb5220a94118b95 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Sun, 10 Nov 2024 18:45:03 +0700 Subject: [PATCH 08/64] script: change port --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3001e1f..7717d5d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ }, "version": "1.0.50", "scripts": { - "app": "wrangler dev src/app.ts", + "app": "wrangler dev src/app.ts --port 3001", "test": "echo \"Error: no test specified\" && exit 1", "deploy": "wrangler deploy", "dev": "bun run --watch src/index.ts", From 83a32b83180719045c67e170c3d2db6b96079c30 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Sun, 10 Nov 2024 18:47:19 +0700 Subject: [PATCH 09/64] fix: consistent station schema --- src/db/schema-new/index.ts | 23 ++++++++++++++--------- src/modules/v1/station/station.schema.ts | 12 +++++++----- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/db/schema-new/index.ts b/src/db/schema-new/index.ts index bfb7d60..aef2f9f 100644 --- a/src/db/schema-new/index.ts +++ b/src/db/schema-new/index.ts @@ -9,11 +9,16 @@ import { import { createSelectSchema } from "drizzle-zod" import { z } from "zod" +/** Station Metadata */ const stationMetadata = z.object({ - /** KRL Metadata */ - daop: z.number().nullable(), - fgEnable: z.number().nullable(), - haveSchedule: z.boolean().nullable(), + /** Comuline metadata */ + has_schedule: z.boolean().nullable(), + /** Original metadata */ + original: z.object({ + /** KRL */ + daop: z.number().nullable(), + fg_enable: z.number().nullable(), + }), }) export type StationMetadata = z.infer @@ -28,18 +33,18 @@ export const station = pgTable( name: text("name").notNull(), type: stationTypeEnum("type").notNull(), metadata: jsonb("metadata").$type(), - createdAt: timestamp("created_at", { + created_at: timestamp("created_at", { withTimezone: true, }).defaultNow(), - updatedAt: timestamp("updated_at", { + updated_at: timestamp("updated_at", { withTimezone: true, }).defaultNow(), }, (table) => { return { - stationUidx: index("station_uidx").on(table.uid), - stationIdx: index("station_idx").on(table.id), - typeIdx: index("station_type_idx").on(table.type), + station_uidx: index("station_uidx").on(table.uid), + station_idx: index("station_idx").on(table.id), + type_idx: index("station_type_idx").on(table.type), } }, ) diff --git a/src/modules/v1/station/station.schema.ts b/src/modules/v1/station/station.schema.ts index abc71fe..1b16253 100644 --- a/src/modules/v1/station/station.schema.ts +++ b/src/modules/v1/station/station.schema.ts @@ -19,16 +19,18 @@ export const stationResponseSchema = z metadata: stationSchema.shape.metadata.openapi({ type: "object", example: { - fgEnable: 1, - haveSchedule: true, - daop: 1, + has_schedule: true, + original: { + daop: 1, + fg_enable: 1, + }, } satisfies StationMetadata, }), - createdAt: stationSchema.shape.createdAt.openapi({ + created_at: stationSchema.shape.created_at.openapi({ format: "date-time", example: "2024-03-10T09:55:07.213Z", }), - updatedAt: stationSchema.shape.updatedAt.openapi({ + updated_at: stationSchema.shape.updated_at.openapi({ format: "date-time", example: "2024-03-10T09:55:07.213Z", }), From 018a748b56ecf3324cecd67edfd26886367f0360 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Sun, 10 Nov 2024 18:48:05 +0700 Subject: [PATCH 10/64] fix: proper sql station --- .../{0000_right_lilith.sql => 0000_faithful_tyger_tiger.sql} | 0 drizzle/migrations/meta/0000_snapshot.json | 2 +- drizzle/migrations/meta/_journal.json | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename drizzle/migrations/{0000_right_lilith.sql => 0000_faithful_tyger_tiger.sql} (100%) diff --git a/drizzle/migrations/0000_right_lilith.sql b/drizzle/migrations/0000_faithful_tyger_tiger.sql similarity index 100% rename from drizzle/migrations/0000_right_lilith.sql rename to drizzle/migrations/0000_faithful_tyger_tiger.sql diff --git a/drizzle/migrations/meta/0000_snapshot.json b/drizzle/migrations/meta/0000_snapshot.json index d447655..6258988 100644 --- a/drizzle/migrations/meta/0000_snapshot.json +++ b/drizzle/migrations/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "c80c0a7c-b2d3-4986-98d7-4207b4451438", + "id": "d29cef4a-e37d-4b2b-8b3a-1313657171a9", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 7abc21e..0033fbe 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1726196281461, - "tag": "0000_right_lilith", + "when": 1726200221153, + "tag": "0000_faithful_tyger_tiger", "breakpoints": true } ] From ce5a9b5074e2eb422bec1be21065eec98c9ebee4 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Sun, 10 Nov 2024 18:48:39 +0700 Subject: [PATCH 11/64] refactor: station controller --- src/app.ts | 112 ++++---- src/modules/api.ts | 4 + src/modules/utils/response.ts | 61 ++++ src/modules/v1/database.ts | 28 ++ src/modules/v1/index.ts | 6 +- src/modules/v1/station/station.controller.ts | 278 ++++++++++--------- src/modules/v1/station/station.route.ts | 124 --------- src/modules/v1/station/station.schema.ts | 13 - 8 files changed, 310 insertions(+), 316 deletions(-) create mode 100644 src/modules/api.ts create mode 100644 src/modules/utils/response.ts create mode 100644 src/modules/v1/database.ts delete mode 100644 src/modules/v1/station/station.route.ts diff --git a/src/app.ts b/src/app.ts index 0efde08..badf2de 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,19 +1,18 @@ -import { KVNamespace } from "@cloudflare/workers-types" -import { OpenAPIHono } from "@hono/zod-openapi" import { apiReference } from "@scalar/hono-api-reference" -import { NeonDatabase } from "drizzle-orm/neon-serverless" -import { NewStation, station } from "./db/schema-new" +import { createAPI } from "./modules/api" import v1 from "./modules/v1" -import { dbMiddleware } from "./modules/v1/station/station.route" +import { Database } from "./modules/v1/database" +import { HTTPException } from "hono/http-exception" +import { constructResponse } from "./modules/utils/response" export type Bindings = { - KV: KVNamespace DATABASE_URL: string COMULINE_ENV: string } export type Variables = { - db: NeonDatabase + db: Database["db"] + constructResponse: typeof constructResponse } export type Environments = { @@ -21,53 +20,62 @@ export type Environments = { Variables: Variables } -const app = new OpenAPIHono() +const api = createAPI() -app.use(dbMiddleware) - -app.route("/v1", v1) - -app.use( - "/docs", - apiReference({ - cdn: "https://cdn.jsdelivr.net/npm/@scalar/api-reference", - spec: { - url: "/openapi", - }, - }), -) - -app.get("/", (c) => c.json({ status: "ok" })) - -app.post("/echo", async (c) => { - const db = c.get("db") - - const insert = { - uid: "st_krl_ac", - id: "AC", - name: "ANCOL", - type: "KRL", - metadata: { - fgEnable: 1, - haveSchedule: true, - daop: 1, +const app = api + .doc("/openapi", (c) => ({ + openapi: "3.0.0", + info: { + version: "1.0.0", + title: "Comuline API", }, - } satisfies NewStation - - const data = await db.insert(station).values(insert).returning() - - return c.json({ - status: 200, - data, + servers: [ + { + url: new URL(c.req.url).origin, + description: c.env.COMULINE_ENV, + }, + ], + })) + .use(async (c, next) => { + const { db } = new Database({ + COMULINE_ENV: c.env.COMULINE_ENV, + DATABASE_URL: c.env.DATABASE_URL, + }) + c.set("db", db) + c.set("constructResponse", constructResponse) + await next() + }) + .route("/v1", v1) + .use( + "/docs", + apiReference({ + cdn: "https://cdn.jsdelivr.net/npm/@scalar/api-reference", + spec: { + url: "/openapi", + }, + }), + ) + .get("/status", (c) => c.json({ status: "ok" })) + .notFound((c) => c.redirect("/docs")) + .onError((err, c) => { + if (err instanceof HTTPException) { + return c.json( + { + success: false, + message: err.message, + cause: err.cause, + }, + err.status, + ) + } + return c.json( + { + success: false, + message: err.message, + cause: err.cause, + }, + 500, + ) }) -}) - -app.doc("/openapi", { - openapi: "3.0.0", - info: { - version: "1.0.0", - title: "My API", - }, -}) export default app diff --git a/src/modules/api.ts b/src/modules/api.ts new file mode 100644 index 0000000..267c019 --- /dev/null +++ b/src/modules/api.ts @@ -0,0 +1,4 @@ +import { OpenAPIHono } from "@hono/zod-openapi" +import { Environments } from "../app" + +export const createAPI = () => new OpenAPIHono() diff --git a/src/modules/utils/response.ts b/src/modules/utils/response.ts new file mode 100644 index 0000000..e623ad7 --- /dev/null +++ b/src/modules/utils/response.ts @@ -0,0 +1,61 @@ +import { HTTPException } from "hono/http-exception" +import { z } from "zod" + +export const constructResponse = ( + schema: T, + data: z.infer, +): z.infer => { + const result = schema.safeParse(data) + + if (!result.success) { + console.log(result.error.issues) + throw new HTTPException(417, { + message: "Failed to construct a response", + cause: result.error.issues, + }) + } + + return result.data +} + +export const buildDataResponseSchema = ( + status: number, + schema: z.ZodTypeAny, +) => ({ + [status]: { + content: { + "application/json": { + schema: z.object({ + metadata: z.object({ + success: z.boolean().default(true), + }), + data: schema, + }), + }, + }, + description: "Success", + }, +}) + +export const buildMetadataResponseSchema = ( + status: number, + description?: string, + success?: boolean, +) => ({ + [status]: { + content: { + "application/json": { + schema: z.object({ + metadata: z.object({ + success: z.boolean().default(success ?? false), + message: z + .string() + .min(1) + .default(description || "Error"), + }), + }), + }, + }, + description: description || "Error", + }, +}) diff --git a/src/modules/v1/database.ts b/src/modules/v1/database.ts new file mode 100644 index 0000000..e3ff46e --- /dev/null +++ b/src/modules/v1/database.ts @@ -0,0 +1,28 @@ +import { neonConfig, Pool } from "@neondatabase/serverless" +import { drizzle, NeonDatabase } from "drizzle-orm/neon-serverless" +import * as schema from "../../db/schema-new" + +export class Database< + T extends { + DATABASE_URL: string + COMULINE_ENV: string + }, +> { + db: NeonDatabase + + constructor(protected env: T) { + this.db = connectDB(env.DATABASE_URL, env.COMULINE_ENV) + } +} + +export const connectDB = (url: string, env: string) => { + if (env === "development") { + neonConfig.wsProxy = (host) => `${host}:5433/v1` + neonConfig.useSecureWebSocket = false + neonConfig.pipelineTLS = false + neonConfig.pipelineConnect = false + } + + const pool = new Pool({ connectionString: url, ssl: true }) + return drizzle(pool, { schema }) +} diff --git a/src/modules/v1/index.ts b/src/modules/v1/index.ts index e702f40..d249239 100644 --- a/src/modules/v1/index.ts +++ b/src/modules/v1/index.ts @@ -1,8 +1,8 @@ -import { OpenAPIHono } from "@hono/zod-openapi" +import { createAPI } from "../api" import stationController from "./station/station.controller" -const v1 = new OpenAPIHono() +const api = createAPI() -v1.route("/station", stationController) +const v1 = api.route("/station", stationController) export default v1 diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index 37fb50c..daf0005 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -1,139 +1,169 @@ -import { OpenAPIHono, z } from "@hono/zod-openapi" -import { eq, getTableColumns, SQL, sql } from "drizzle-orm" -import { PgTable } from "drizzle-orm/pg-core" -import { Environments } from "../../../app" -import { NewStation, station, stationSchema } from "../../../db/schema-new" -import * as route from "./station.route" +import { createRoute, z } from "@hono/zod-openapi" +import { eq, sql } from "drizzle-orm" +import { HTTPException } from "hono/http-exception" +import { NewStation, station, StationType } from "../../../db/schema-new" +import { createAPI } from "../../api" +import { + buildDataResponseSchema, + buildMetadataResponseSchema, +} from "../../utils/response" import { stationResponseSchema } from "./station.schema" -const controller = new OpenAPIHono() +const api = createAPI() -controller.use(route.dbMiddleware) +const createStationKey = (type: StationType, id: string) => + `st_${type}_${id}`.toLocaleLowerCase() -controller.openapi(route.getAll, async (c) => { - const db = c.get("db") - const stations = await db.select().from(station) - - return c.json( - { - metadata: { - status: 200, - message: "Success", +const stationController = api + .openapi( + createRoute({ + method: "get", + path: "/", + responses: { + ...buildDataResponseSchema(200, z.array(stationResponseSchema)), }, - data: stations, + tags: ["Station"], + description: "Get all KRL station data", + }), + async (c) => { + const { db } = c.var + const stations = await db.select().from(station) + + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse( + z.array(stationResponseSchema), + stations, + ), + }, + 200, + ) }, - 200, ) -}) - -controller.openapi(route.getById, async (c) => { - const { id } = c.req.valid("param") - const db = c.get("db") - const data = await db.select().from(station).where(eq(station.id, id)) - - if (data.length === 0) { - return c.json( - { - metadata: { - status: 404, - message: "Station data is not found", - }, + .openapi( + createRoute({ + method: "get", + path: "/{id}", + request: { + params: z.object({ + id: z + .string() + .min(2) + .openapi({ + param: { + name: "id", + in: "path", + }, + example: "MRI", + }), + }), }, - 404, - ) - } - - return c.json( - { - metadata: { - status: 200, - message: "Success", + responses: { + ...buildDataResponseSchema(200, stationResponseSchema), + ...buildMetadataResponseSchema(404, "Not found"), }, - data: stationResponseSchema.parse(data[0]), + tags: ["Station"], + }), + async (c) => { + const { id } = c.req.valid("param") + const db = c.get("db") + const data = await db.select().from(station).where(eq(station.id, id)) + + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse(stationResponseSchema, data[0]), + }, + 200, + ) }, - 200, ) -}) - -controller.openapi(route.sync, async (c) => { - const db = c.get("db") - - const req = await fetch( - "https://api-partner.krl.co.id/krlweb/v1/krl-station", - ).then((res) => res.json()) - - const schema = z.object({ - status: z.number(), - message: z.string(), - data: z.array( - z.object({ - sta_id: z.string(), - sta_name: z.string(), - group_wil: z.number(), - fg_enable: z.number(), - }), - ), - }) - - const parsed = schema.parse(req) - - const filterdStation = parsed.data.filter((d) => !d.sta_id.includes("WIL")) - - const insertStations = filterdStation.map((s) => { - return { - uid: `st_krl_${s.sta_id.toLocaleLowerCase()}`, - id: s.sta_id, - name: s.sta_name, - type: "KRL", - metadata: { - fgEnable: s.fg_enable, - haveSchedule: true, - daop: s.group_wil === 0 ? 1 : s.group_wil, - }, - } - }) satisfies NewStation[] - - await db - .insert(station) - .values(insertStations) - .onConflictDoUpdate({ - target: station.uid, - set: { - updatedAt: new Date(), - uid: sql`excluded.uid`, - id: sql`excluded.id`, - name: sql`excluded.name`, - }, - }) - .returning() - - return c.json( - { - metadata: { - status: 200, - message: "Success", + .openapi( + createRoute({ + method: "post", + path: "/", + responses: { + ...buildMetadataResponseSchema(201, "Success", true), }, + tags: ["Station"], + }), + async (c) => { + const { db } = c.var + + const req = await fetch( + "https://api-partner.krl.co.id/krlweb/v1/krl-station", + ).then((res) => res.json()) + + const schema = z.object({ + status: z.number(), + message: z.string(), + data: z.array( + z.object({ + sta_id: z.string(), + sta_name: z.string(), + group_wil: z.number(), + fg_enable: z.number(), + }), + ), + }) + + const parsed = schema.safeParse(req) + + if (!parsed.success) { + throw new HTTPException(417, { + message: parsed.error.message, + cause: parsed.error.cause, + }) + } + + const filterdStation = parsed.data.data.filter( + (d) => !d.sta_id.includes("WIL"), + ) + + const insertStations = filterdStation.map((s) => { + return { + uid: createStationKey("KRL", s.sta_id), + id: s.sta_id, + name: s.sta_name, + type: "KRL", + metadata: { + has_schedule: true, + original: { + fg_enable: s.fg_enable, + daop: s.group_wil === 0 ? 1 : s.group_wil, + }, + }, + } + }) satisfies NewStation[] + + await db + .insert(station) + .values(insertStations) + .onConflictDoUpdate({ + target: station.uid, + set: { + updated_at: new Date(), + uid: sql`excluded.uid`, + id: sql`excluded.id`, + name: sql`excluded.name`, + }, + }) + + return c.json( + { + metadata: { + success: true, + message: "Success", + }, + }, + 200, + ) }, - 200, - ) -}) - -export default controller - -export function conflictUpdateAllExcept< - T extends PgTable, - E extends (keyof T["$inferInsert"])[], ->(table: T, except: E) { - const columns = getTableColumns(table) - const updateColumns = Object.entries(columns).filter( - ([col]) => !except.includes(col as keyof typeof table.$inferInsert), ) - return updateColumns.reduce( - (acc, [colName, table]) => ({ - ...acc, - [colName]: sql.raw(`excluded.${table.name}`), - }), - {}, - ) as Omit, E[number]> -} +export default stationController diff --git a/src/modules/v1/station/station.route.ts b/src/modules/v1/station/station.route.ts deleted file mode 100644 index 270db33..0000000 --- a/src/modules/v1/station/station.route.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { createRoute, z } from "@hono/zod-openapi" -import { neonConfig, Pool } from "@neondatabase/serverless" - -import { drizzle } from "drizzle-orm/neon-serverless" -import { createMiddleware } from "hono/factory" -import { Environments } from "../../../app" -import * as schema from "../../../db/schema-new" -import { getByIdRequestSchema, stationResponseSchema } from "./station.schema" - -export const connectDB = (url: string, env: string) => { - if (env === "development") { - // Set the WebSocket proxy to work with the local instance - neonConfig.wsProxy = (host) => `${host}:5433/v1` - // Disable all authentication and encryption - neonConfig.useSecureWebSocket = false - neonConfig.pipelineTLS = false - neonConfig.pipelineConnect = false - } - - const pool = new Pool({ connectionString: url, ssl: true }) - return drizzle(pool, { schema }) -} - -export const dbMiddleware = createMiddleware(async (c, next) => { - c.set("db", connectDB(c.env.DATABASE_URL, c.env.COMULINE_ENV)) - await next() -}) - -export const getAll = createRoute({ - method: "get", - path: "/", - responses: { - 200: { - content: { - "application/json": { - schema: z.object({ - metadata: z.object({ - status: z.number().openapi({ - example: 200, - }), - message: z.string().openapi({ - example: "Success", - }), - }), - data: z.array(stationResponseSchema), - }), - }, - }, - description: "Retrieve all the station", - }, - }, - tags: ["Station"], -}) - -export const getById = createRoute({ - method: "get", - path: "/{id}", - request: { - params: getByIdRequestSchema, - }, - responses: { - 200: { - content: { - "application/json": { - schema: z.object({ - metadata: z.object({ - status: z.number().openapi({ - example: 200, - }), - message: z.string().openapi({ - example: "Success", - }), - }), - data: stationResponseSchema, - }), - }, - }, - description: "Retrieve one of the station", - }, - 404: { - content: { - "application/json": { - schema: z.object({ - metadata: z.object({ - status: z.number().openapi({ - example: 404, - }), - message: z.string().openapi({ - example: "Station data is not found", - }), - }), - }), - }, - }, - description: "Station data is not found", - }, - }, - tags: ["Station"], -}) - -export const sync = createRoute({ - method: "post", - path: "/", - responses: { - 200: { - content: { - "application/json": { - schema: z.object({ - metadata: z.object({ - status: z.number().openapi({ - example: 200, - }), - message: z.string().openapi({ - example: "Success", - }), - }), - }), - }, - }, - description: "Sync station data", - }, - }, - tags: ["Station"], -}) diff --git a/src/modules/v1/station/station.schema.ts b/src/modules/v1/station/station.schema.ts index 1b16253..27ab1ba 100644 --- a/src/modules/v1/station/station.schema.ts +++ b/src/modules/v1/station/station.schema.ts @@ -36,16 +36,3 @@ export const stationResponseSchema = z }), }) .openapi("Station") satisfies typeof stationSchema - -export const getByIdRequestSchema = z.object({ - id: z - .string() - .min(2) - .openapi({ - param: { - name: "id", - in: "path", - }, - example: "MRI", - }), -}) From 4070dc89dad6005a8e4fad30df700da955784ee9 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:15:55 +0700 Subject: [PATCH 12/64] refactor: station sync --- src/modules/v1/station/station.controller.ts | 32 +++++++++++++------- src/modules/v1/sync.ts | 31 +++++++++++++++++++ 2 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 src/modules/v1/sync.ts diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index daf0005..20c98d6 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -1,13 +1,14 @@ import { createRoute, z } from "@hono/zod-openapi" import { eq, sql } from "drizzle-orm" -import { HTTPException } from "hono/http-exception" import { NewStation, station, StationType } from "../../../db/schema-new" import { createAPI } from "../../api" import { buildDataResponseSchema, buildMetadataResponseSchema, } from "../../utils/response" +import { Sync } from "../sync" import { stationResponseSchema } from "./station.schema" +import { HTTPException } from "hono/http-exception" const api = createAPI() @@ -57,6 +58,7 @@ const stationController = api name: "id", in: "path", }, + default: "MRI", example: "MRI", }), }), @@ -95,9 +97,7 @@ const stationController = api async (c) => { const { db } = c.var - const req = await fetch( - "https://api-partner.krl.co.id/krlweb/v1/krl-station", - ).then((res) => res.json()) + // TODO: Refactor to CLI const schema = z.object({ status: z.number(), @@ -112,18 +112,28 @@ const stationController = api ), }) - const parsed = schema.safeParse(req) + const sync = new Sync( + "https://api-partner.krl.co.id/krlweb/v1/krl-station", + { + headers: { + Authorization: + "Bearer VXcYZMFtwUAoikVByzKuaZZeTo1AtCiSjejSHNdpLxyKk_SFUzog5MOkUN1ktAhFnBFoz6SlWAJBJIS-lHYsdFLSug2YNiaNllkOUsDbYkiDtmPc9XWc", + Host: "api-partner.krl.co.id", + Origin: "https://commuterline.id", + Referer: "https://commuterline.id/", + }, + }, + ) + + const res = await sync.request(schema) - if (!parsed.success) { + if (res instanceof Response) { throw new HTTPException(417, { - message: parsed.error.message, - cause: parsed.error.cause, + message: "Failed to sync", }) } - const filterdStation = parsed.data.data.filter( - (d) => !d.sta_id.includes("WIL"), - ) + const filterdStation = res.data.filter((d) => !d.sta_id.includes("WIL")) const insertStations = filterdStation.map((s) => { return { diff --git a/src/modules/v1/sync.ts b/src/modules/v1/sync.ts new file mode 100644 index 0000000..cd26d35 --- /dev/null +++ b/src/modules/v1/sync.ts @@ -0,0 +1,31 @@ +import { z } from "zod" + +export class Sync { + constructor( + protected url: string | URL | Request, + protected init?: FetchRequestInit, + ) { + this.url = url + this.init = init + } + + async request( + expectedSchema: T, + ): Promise | Response> { + const req = await fetch(this.url, this.init) + + if (!req.ok) return req + + const data = await req.json() + + const parsedData = expectedSchema.safeParse(data) + + if (!parsedData.success) { + throw new Error(parsedData.error.message, { + cause: parsedData.error.cause, + }) + } + + return parsedData.data + } +} From 0862809706c30396f1dedd591913e300777568cc Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:16:16 +0700 Subject: [PATCH 13/64] feat: sync schedule --- drizzle/migrations/0001_faithful_storm.sql | 20 ++ drizzle/migrations/meta/0001_snapshot.json | 270 +++++++++++++++++++++ drizzle/migrations/meta/_journal.json | 7 + src/db/schema-new/index.ts | 52 ++++ src/sync.ts | 187 ++++++++++++++ 5 files changed, 536 insertions(+) create mode 100644 drizzle/migrations/0001_faithful_storm.sql create mode 100644 drizzle/migrations/meta/0001_snapshot.json create mode 100644 src/sync.ts diff --git a/drizzle/migrations/0001_faithful_storm.sql b/drizzle/migrations/0001_faithful_storm.sql new file mode 100644 index 0000000..62c6bca --- /dev/null +++ b/drizzle/migrations/0001_faithful_storm.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS "schedule" ( + "id" text PRIMARY KEY NOT NULL, + "station_id" text NOT NULL, + "station_origin_id" text, + "station_origin_name" text NOT NULL, + "station_destination_id" text, + "station_destination_name" text NOT NULL, + "train_id" text NOT NULL, + "line" text NOT NULL, + "route" text NOT NULL, + "time_departure" time NOT NULL, + "time_at_destination" time NOT NULL, + "metadata" jsonb, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "schedule_id_unique" UNIQUE("id") +); +--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "schedule_idx" ON "schedule" USING btree ("id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "schedule_station_idx" ON "schedule" USING btree ("station_id"); \ No newline at end of file diff --git a/drizzle/migrations/meta/0001_snapshot.json b/drizzle/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..1545bf4 --- /dev/null +++ b/drizzle/migrations/meta/0001_snapshot.json @@ -0,0 +1,270 @@ +{ + "id": "13768c82-84d4-46b6-aaf0-1e084219f026", + "prevId": "d29cef4a-e37d-4b2b-8b3a-1313657171a9", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_origin_name": { + "name": "station_origin_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_destination_name": { + "name": "station_destination_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time_departure": { + "name": "time_departure", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "time_at_destination": { + "name": "time_at_destination", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 0033fbe..07e5de7 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1726200221153, "tag": "0000_faithful_tyger_tiger", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1731324915407, + "tag": "0001_faithful_storm", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema-new/index.ts b/src/db/schema-new/index.ts index aef2f9f..1f851da 100644 --- a/src/db/schema-new/index.ts +++ b/src/db/schema-new/index.ts @@ -4,7 +4,9 @@ import { pgEnum, pgTable, text, + time, timestamp, + uniqueIndex, } from "drizzle-orm/pg-core" import { createSelectSchema } from "drizzle-zod" import { z } from "zod" @@ -14,6 +16,7 @@ const stationMetadata = z.object({ /** Comuline metadata */ has_schedule: z.boolean().nullable(), /** Original metadata */ + /** TODO: Change to origin */ original: z.object({ /** KRL */ daop: z.number().nullable(), @@ -49,8 +52,57 @@ export const station = pgTable( }, ) +const stationScheduleMetadata = z.object({ + /** Origin metadata */ + origin: z.object({ + color: z.string().nullable(), + }), +}) + +export type StationScheduleMetadata = z.infer + +export const schedule = pgTable( + "schedule", + { + id: text("id").primaryKey().unique().notNull(), + station_id: text("station_id").notNull(), + station_origin_id: text("station_origin_id"), + station_origin_name: text("station_origin_name").notNull(), + station_destination_id: text("station_destination_id"), + station_destination_name: text("station_destination_name").notNull(), + train_id: text("train_id").notNull(), + line: text("line").notNull(), + route: text("route").notNull(), + time_departure: time("time_departure").notNull(), + time_at_destination: time("time_at_destination").notNull(), + metadata: jsonb("metadata").$type(), + created_at: timestamp("created_at", { + withTimezone: true, + }).defaultNow(), + updated_at: timestamp("updated_at", { + withTimezone: true, + }).defaultNow(), + }, + (table) => { + return { + schedule_idx: uniqueIndex("schedule_idx").on(table.id), + schedule_station_idx: index("schedule_station_idx").on(table.station_id), + } + }, +) + export const stationSchema = createSelectSchema(station) export type NewStation = typeof station.$inferInsert export type Station = z.infer + +export type StationType = Station["type"] + +export const scheduleSchema = createSelectSchema(schedule, { + metadata: stationScheduleMetadata.nullable(), +}) + +export type ScheduleType = z.infer + +type Json = ScheduleType["metadata"] diff --git a/src/sync.ts b/src/sync.ts new file mode 100644 index 0000000..bef890a --- /dev/null +++ b/src/sync.ts @@ -0,0 +1,187 @@ +import { sleep } from "bun" +import { eq, sql } from "drizzle-orm" +import { z } from "zod" +import { NewStation, schedule, station } from "./db/schema-new" +import { Database } from "./modules/v1/database" + +export function parseTime(timeString: string): Date { + const [hours, minutes, seconds] = timeString.split(":").map(Number) + const date = new Date() + date.setHours(hours ?? date.getHours()) + date.setMinutes(minutes ?? date.getMinutes()) + date.setSeconds(seconds ?? date.getSeconds()) + + return date +} + +const sync = async () => { + if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL env is missing") + + const { db } = new Database({ + COMULINE_ENV: "development", + DATABASE_URL: process.env.DATABASE_URL, + }) + + const stations = await db + .select({ id: station.id, metadata: station.metadata, name: station.name }) + .from(station) + + const batchSizes = 5 + const totalBatches = Math.ceil(stations.length / batchSizes) + + const schema = z.object({ + status: z.number(), + data: z.array( + z.object({ + train_id: z.string(), + ka_name: z.string(), + route_name: z.string(), + dest: z.string(), + time_est: z.string(), + color: z.string(), + dest_time: z.string(), + }), + ), + }) + + for (let i = 0; i < totalBatches; i++) { + const start = i * batchSizes + const end = start + batchSizes + const batch = stations.slice(start, end) + + await Promise.allSettled( + batch.map(async ({ id, metadata }) => { + await sleep(5000) + + const url = `https://api-partner.krl.co.id/krlweb/v1/schedule?stationid=${id}&timefrom=00:00&timeto=23:00` + + const headers = { + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0", + Accept: "application/json, text/javascript, */*; q=0.01", + "Accept-Language": "en-US,en;q=0.5", + Authorization: + "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIzIiwianRpIjoiMDYzNWIyOGMzYzg3YTY3ZTRjYWE4YTI0MjYxZGYwYzIxNjYzODA4NWM2NWU4ZjhiYzQ4OGNlM2JiZThmYWNmODU4YzY0YmI0MjgyM2EwOTUiLCJpYXQiOjE3MjI2MTc1MTQsIm5iZiI6MTcyMjYxNzUxNCwiZXhwIjoxNzU0MTUzNTE0LCJzdWIiOiI1Iiwic2NvcGVzIjpbXX0.Jz_sedcMtaZJ4dj0eWVc4_pr_wUQ3s1-UgpopFGhEmJt_iGzj6BdnOEEhcDDdIz-gydQL5ek0S_36v5h6P_X3OQyII3JmHp1SEDJMwrcy4FCY63-jGnhPBb4sprqUFruDRFSEIs1cNQ-3rv3qRDzJtGYc_bAkl2MfgZj85bvt2DDwBWPraZuCCkwz2fJvox-6qz6P7iK9YdQq8AjJfuNdl7t_1hMHixmtDG0KooVnfBV7PoChxvcWvs8FOmtYRdqD7RSEIoOXym2kcwqK-rmbWf9VuPQCN5gjLPimL4t2TbifBg5RWNIAAuHLcYzea48i3okbhkqGGlYTk3iVMU6Hf_Jruns1WJr3A961bd4rny62lNXyGPgNLRJJKedCs5lmtUTr4gZRec4Pz_MqDzlEYC3QzRAOZv0Ergp8-W1Vrv5gYyYNr-YQNdZ01mc7JH72N2dpU9G00K5kYxlcXDNVh8520-R-MrxYbmiFGVlNF2BzEH8qq6Ko9m0jT0NiKEOjetwegrbNdNq_oN4KmHvw2sHkGWY06rUeciYJMhBF1JZuRjj3JTwBUBVXcYZMFtwUAoikVByzKuaZZeTo1AtCiSjejSHNdpLxyKk_SFUzog5MOkUN1ktAhFnBFoz6SlWAJBJIS-lHYsdFLSug2YNiaNllkOUsDbYkiDtmPc9XWc", + Priority: "u=0", + } + + console.info(`[SYNC][SCHEDULE][${id}] Send preflight`) + const optionsResponse = await fetch(url, { + method: "OPTIONS", + headers: { + ...headers, + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "authorization,content-type", + }, + credentials: "include", + mode: "cors", + }) + + if (!optionsResponse.ok) { + throw new Error( + `OPTIONS request failed with status: ${optionsResponse.status}`, + ) + } + const req = await fetch(url, { + method: "GET", + headers, + credentials: "include", + mode: "cors", + }) + + console.info(`[SYNC][SCHEDULE][${id}] Fetched data from API`) + + if (req.status === 200) { + try { + const data = await req.json() + + const parsed = schema.safeParse(data) + + if (!parsed.success) { + console.error(`[SYNC][SCHEDULE][${id}] Error parse`) + } else { + const values = parsed.data.data.map((d) => { + const [origin, destination] = d.route_name.split("-") + + return { + id: `sc_krl_${id}_${d.train_id}`.toLowerCase(), + station_id: id, + station_origin_id: stations.find( + ({ name }) => name === origin, + )?.id!, + station_origin_name: origin, + station_destination_id: stations.find( + ({ name }) => name === destination, + )?.id!, + station_destination_name: d.dest, + train_id: d.train_id, + line: d.ka_name, + route: d.route_name, + time_departure: parseTime(d.dest_time).toLocaleTimeString(), + time_at_destination: parseTime( + d.dest_time, + ).toLocaleTimeString(), + metadata: { + origin: { + color: d.color, + }, + }, + } + }) + + const insert = await db + .insert(schedule) + .values(values) + .onConflictDoUpdate({ + target: schedule.id, + set: { + time_departure: sql`excluded.time_departure`, + time_at_destination: sql`excluded.time_at_destination`, + metadata: sql`excluded.metadata`, + updated_at: new Date(), + }, + }) + .returning() + + console.info( + `[SYNC][SCHEDULE][${id}] Inserted ${insert.length} rows`, + ) + } + } catch (err) { + console.error( + `[SYNC][SCHEDULE][${id}] Error inserting schedule data. Trace: ${JSON.stringify( + err, + )}. Status: ${req.status}.`, + ) + } + } else if (req.status === 404) { + console.info(`[SYNC][SCHEDULE][${id}] No schedule data found`) + const payload: Partial = { + metadata: metadata + ? { + ...metadata, + has_schedule: false, + } + : null, + updated_at: new Date(), + } + await db.update(station).set(payload).where(eq(station.id, id)) + console.info( + `[SYNC][SCHEDULE][${id}] Updated station schedule availability status`, + ) + } else { + const err = await req.json() + const txt = await req.text() + console.error( + `[SYNC][SCHEDULE][${id}] Error fetch schedule data. Trace: ${JSON.stringify( + err, + )}. Status: ${req.status}. Req: ${txt}`, + ) + throw new Error(JSON.stringify(err)) + } + }), + ) + } +} + +sync() From 2becfe7e6bb891ebd84c24ea10bfc63d53f811a1 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:16:21 +0700 Subject: [PATCH 14/64] feat: schedule api --- src/modules/v1/index.ts | 6 +- .../v1/schedule/schedule.controller.ts | 59 +++++++++++++++ src/modules/v1/schedule/schedule.schema.ts | 74 +++++++++++++++++++ 3 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 src/modules/v1/schedule/schedule.controller.ts create mode 100644 src/modules/v1/schedule/schedule.schema.ts diff --git a/src/modules/v1/index.ts b/src/modules/v1/index.ts index d249239..c64ae6d 100644 --- a/src/modules/v1/index.ts +++ b/src/modules/v1/index.ts @@ -1,8 +1,10 @@ import { createAPI } from "../api" +import scheduleController from "./schedule/schedule.controller" import stationController from "./station/station.controller" const api = createAPI() -const v1 = api.route("/station", stationController) - +const v1 = api + .route("/station", stationController) + .route("/schedule", scheduleController) export default v1 diff --git a/src/modules/v1/schedule/schedule.controller.ts b/src/modules/v1/schedule/schedule.controller.ts new file mode 100644 index 0000000..c96ba01 --- /dev/null +++ b/src/modules/v1/schedule/schedule.controller.ts @@ -0,0 +1,59 @@ +import { createRoute, z } from "@hono/zod-openapi" +import { asc, eq } from "drizzle-orm" +import { schedule } from "../../../db/schema-new" +import { createAPI } from "../../api" +import { + buildDataResponseSchema, + buildMetadataResponseSchema, +} from "../../utils/response" +import { scheduleResponseSchema } from "./schedule.schema" + +const api = createAPI() + +const scheduleController = api.openapi( + createRoute({ + method: "get", + path: "/{station_id}", + request: { + params: z.object({ + station_id: z + .string() + .min(2) + .openapi({ + param: { + name: "station_id", + in: "path", + }, + default: "AC", + example: "AC", + }), + }), + }, + responses: { + ...buildDataResponseSchema(200, z.array(scheduleResponseSchema)), + ...buildMetadataResponseSchema(404, "Not found"), + }, + tags: ["Schedule"], + }), + async (c) => { + const param = c.req.valid("param") + const db = c.get("db") + const data = await db + .select() + .from(schedule) + .where(eq(schedule.station_id, param.station_id.toLocaleUpperCase())) + .orderBy(asc(schedule.time_departure)) + + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse(z.array(scheduleResponseSchema), data), + }, + 200, + ) + }, +) + +export default scheduleController diff --git a/src/modules/v1/schedule/schedule.schema.ts b/src/modules/v1/schedule/schedule.schema.ts new file mode 100644 index 0000000..a0a24ce --- /dev/null +++ b/src/modules/v1/schedule/schedule.schema.ts @@ -0,0 +1,74 @@ +import { z } from "zod" +import { + scheduleSchema, + StationScheduleMetadata, + stationSchema, +} from "../../../db/schema-new" + +export const scheduleResponseSchema = z + .object({ + id: scheduleSchema.shape.id.openapi({ + example: "sc_krl_ac_2400", + description: "Schedule unique ID", + }), + station_id: scheduleSchema.shape.station_id.openapi({ + example: "AC", + description: "Station ID where the train stops", + }), + station_origin_id: scheduleSchema.shape.station_origin_id.openapi({ + example: "JAKK", + description: "Station ID where the train originates", + }), + station_origin_name: scheduleSchema.shape.station_origin_name.openapi({ + example: "JAKARTAKOTA", + description: "Station name where the train originates", + }), + station_destination_id: scheduleSchema.shape.station_destination_id.openapi( + { + example: "TPK", + description: "Station ID where the train terminates", + }, + ), + station_destination_name: + scheduleSchema.shape.station_destination_name.openapi({ + example: "TANJUNGPRIUK", + description: "Station name where the train terminates", + }), + train_id: scheduleSchema.shape.train_id.openapi({ + example: "2400", + description: "Train ID", + }), + line: scheduleSchema.shape.line.openapi({ + example: "COMMUTER LINE TANJUNGPRIUK", + description: "Train line", + }), + route: scheduleSchema.shape.route.openapi({ + example: "JAKARTAKOTA-TANJUNGPRIUK", + description: "Train route", + }), + time_departure: scheduleSchema.shape.time_departure.openapi({ + example: "06:07:00", + description: "Train departure time", + }), + time_at_destination: scheduleSchema.shape.time_at_destination.openapi({ + example: "06:16:00", + description: "Train arrival time at destination", + }), + metadata: scheduleSchema.shape.metadata.openapi({ + type: "object", + example: { + origin: { + color: "#DD0067", + }, + } satisfies StationScheduleMetadata, + }), + created_at: stationSchema.shape.created_at.openapi({ + format: "date-time", + example: "2024-03-10T09:55:07.213Z", + }), + updated_at: stationSchema.shape.updated_at.openapi({ + format: "date-time", + example: "2024-03-10T09:55:07.213Z", + }), + }) + .openapi("Schedule") satisfies typeof scheduleSchema From 823b8f1bbdde4338bac84fbbe59a22cdb5374d9d Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Mon, 11 Nov 2024 22:02:41 +0700 Subject: [PATCH 15/64] fix: unify handle err metadata --- src/app.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/app.ts b/src/app.ts index badf2de..53ad6c2 100644 --- a/src/app.ts +++ b/src/app.ts @@ -61,18 +61,22 @@ const app = api if (err instanceof HTTPException) { return c.json( { - success: false, - message: err.message, - cause: err.cause, + metadata: { + success: false, + message: err.message, + cause: err.cause, + }, }, err.status, ) } return c.json( { - success: false, - message: err.message, - cause: err.cause, + metadata: { + success: false, + message: err.message, + cause: err.cause, + }, }, 500, ) From 8a5fe43eb0201423fb9a4471da78ad01187305c7 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Mon, 11 Nov 2024 22:03:04 +0700 Subject: [PATCH 16/64] refactor: rename to table --- src/db/schema-new/index.ts | 14 ++++++-------- src/modules/v1/schedule/schedule.controller.ts | 8 ++++---- src/modules/v1/station/station.controller.ts | 15 +++++++++------ 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/db/schema-new/index.ts b/src/db/schema-new/index.ts index 1f851da..730b643 100644 --- a/src/db/schema-new/index.ts +++ b/src/db/schema-new/index.ts @@ -28,7 +28,7 @@ export type StationMetadata = z.infer export const stationTypeEnum = pgEnum("station_type", ["KRL", "MRT", "LRT"]) -export const station = pgTable( +export const stationTable = pgTable( "station", { uid: text("uid").primaryKey().unique().notNull(), @@ -52,7 +52,7 @@ export const station = pgTable( }, ) -const stationScheduleMetadata = z.object({ +export const stationScheduleMetadata = z.object({ /** Origin metadata */ origin: z.object({ color: z.string().nullable(), @@ -61,7 +61,7 @@ const stationScheduleMetadata = z.object({ export type StationScheduleMetadata = z.infer -export const schedule = pgTable( +export const scheduleTable = pgTable( "schedule", { id: text("id").primaryKey().unique().notNull(), @@ -91,18 +91,16 @@ export const schedule = pgTable( }, ) -export const stationSchema = createSelectSchema(station) +export const stationSchema = createSelectSchema(stationTable) -export type NewStation = typeof station.$inferInsert +export type NewStation = typeof stationTable.$inferInsert export type Station = z.infer export type StationType = Station["type"] -export const scheduleSchema = createSelectSchema(schedule, { +export const scheduleSchema = createSelectSchema(scheduleTable, { metadata: stationScheduleMetadata.nullable(), }) export type ScheduleType = z.infer - -type Json = ScheduleType["metadata"] diff --git a/src/modules/v1/schedule/schedule.controller.ts b/src/modules/v1/schedule/schedule.controller.ts index c96ba01..4d61cbd 100644 --- a/src/modules/v1/schedule/schedule.controller.ts +++ b/src/modules/v1/schedule/schedule.controller.ts @@ -1,6 +1,6 @@ import { createRoute, z } from "@hono/zod-openapi" import { asc, eq } from "drizzle-orm" -import { schedule } from "../../../db/schema-new" +import { scheduleTable } from "../../../db/schema-new" import { createAPI } from "../../api" import { buildDataResponseSchema, @@ -40,9 +40,9 @@ const scheduleController = api.openapi( const db = c.get("db") const data = await db .select() - .from(schedule) - .where(eq(schedule.station_id, param.station_id.toLocaleUpperCase())) - .orderBy(asc(schedule.time_departure)) + .from(scheduleTable) + .where(eq(scheduleTable.station_id, param.station_id.toLocaleUpperCase())) + .orderBy(asc(scheduleTable.time_departure)) return c.json( { diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index 20c98d6..9c576e0 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -1,6 +1,7 @@ import { createRoute, z } from "@hono/zod-openapi" import { eq, sql } from "drizzle-orm" -import { NewStation, station, StationType } from "../../../db/schema-new" +import { HTTPException } from "hono/http-exception" +import { NewStation, stationTable, StationType } from "../../../db/schema-new" import { createAPI } from "../../api" import { buildDataResponseSchema, @@ -8,7 +9,6 @@ import { } from "../../utils/response" import { Sync } from "../sync" import { stationResponseSchema } from "./station.schema" -import { HTTPException } from "hono/http-exception" const api = createAPI() @@ -28,7 +28,7 @@ const stationController = api }), async (c) => { const { db } = c.var - const stations = await db.select().from(station) + const stations = await db.select().from(stationTable) return c.json( { @@ -72,7 +72,10 @@ const stationController = api async (c) => { const { id } = c.req.valid("param") const db = c.get("db") - const data = await db.select().from(station).where(eq(station.id, id)) + const data = await db + .select() + .from(stationTable) + .where(eq(stationTable.id, id)) return c.json( { @@ -152,10 +155,10 @@ const stationController = api }) satisfies NewStation[] await db - .insert(station) + .insert(stationTable) .values(insertStations) .onConflictDoUpdate({ - target: station.uid, + target: stationTable.uid, set: { updated_at: new Date(), uid: sql`excluded.uid`, From 6d43f9d1e8ef9c23c59d3c888689c43f6aa5db21 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Mon, 11 Nov 2024 22:42:53 +0700 Subject: [PATCH 17/64] refactor: better station and schedule schema --- .../migrations/0000_faithful_tyger_tiger.sql | 20 -- .../migrations/0000_overjoyed_rockslide.sql | 57 ++++ drizzle/migrations/0001_faithful_storm.sql | 20 -- drizzle/migrations/meta/0000_snapshot.json | 174 ++++++++++- drizzle/migrations/meta/0001_snapshot.json | 270 ------------------ drizzle/migrations/meta/_journal.json | 11 +- src/db/schema-new/index.ts | 41 ++- src/modules/v1/schedule/schedule.schema.ts | 11 +- src/modules/v1/station/station.controller.ts | 2 +- src/modules/v1/station/station.schema.ts | 6 +- src/sync.ts | 30 +- 11 files changed, 287 insertions(+), 355 deletions(-) delete mode 100644 drizzle/migrations/0000_faithful_tyger_tiger.sql create mode 100644 drizzle/migrations/0000_overjoyed_rockslide.sql delete mode 100644 drizzle/migrations/0001_faithful_storm.sql delete mode 100644 drizzle/migrations/meta/0001_snapshot.json diff --git a/drizzle/migrations/0000_faithful_tyger_tiger.sql b/drizzle/migrations/0000_faithful_tyger_tiger.sql deleted file mode 100644 index 0a27377..0000000 --- a/drizzle/migrations/0000_faithful_tyger_tiger.sql +++ /dev/null @@ -1,20 +0,0 @@ -DO $$ BEGIN - CREATE TYPE "public"."station_type" AS ENUM('KRL', 'MRT', 'LRT'); -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "station" ( - "uid" text PRIMARY KEY NOT NULL, - "id" text NOT NULL, - "name" text NOT NULL, - "type" "station_type" NOT NULL, - "metadata" jsonb, - "created_at" timestamp with time zone DEFAULT now(), - "updated_at" timestamp with time zone DEFAULT now(), - CONSTRAINT "station_uid_unique" UNIQUE("uid") -); ---> statement-breakpoint -CREATE INDEX IF NOT EXISTS "station_uidx" ON "station" USING btree ("uid");--> statement-breakpoint -CREATE INDEX IF NOT EXISTS "station_idx" ON "station" USING btree ("id");--> statement-breakpoint -CREATE INDEX IF NOT EXISTS "station_type_idx" ON "station" USING btree ("type"); \ No newline at end of file diff --git a/drizzle/migrations/0000_overjoyed_rockslide.sql b/drizzle/migrations/0000_overjoyed_rockslide.sql new file mode 100644 index 0000000..07f8163 --- /dev/null +++ b/drizzle/migrations/0000_overjoyed_rockslide.sql @@ -0,0 +1,57 @@ +DO $$ BEGIN + CREATE TYPE "public"."station_type" AS ENUM('KRL', 'MRT', 'LRT'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "schedule" ( + "id" text PRIMARY KEY NOT NULL, + "station_id" text NOT NULL, + "station_origin_id" text, + "station_destination_id" text, + "train_id" text NOT NULL, + "line" text NOT NULL, + "route" text NOT NULL, + "time_departure" time NOT NULL, + "time_at_destination" time NOT NULL, + "metadata" jsonb, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "schedule_id_unique" UNIQUE("id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "station" ( + "uid" text PRIMARY KEY NOT NULL, + "id" text NOT NULL, + "name" text NOT NULL, + "type" "station_type" NOT NULL, + "metadata" jsonb, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "station_uid_unique" UNIQUE("uid"), + CONSTRAINT "station_id_unique" UNIQUE("id") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "schedule" ADD CONSTRAINT "schedule_station_id_station_id_fk" FOREIGN KEY ("station_id") REFERENCES "public"."station"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "schedule" ADD CONSTRAINT "schedule_station_origin_id_station_id_fk" FOREIGN KEY ("station_origin_id") REFERENCES "public"."station"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "schedule" ADD CONSTRAINT "schedule_station_destination_id_station_id_fk" FOREIGN KEY ("station_destination_id") REFERENCES "public"."station"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "schedule_idx" ON "schedule" USING btree ("id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "schedule_station_idx" ON "schedule" USING btree ("station_id");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "station_uidx" ON "station" USING btree ("uid");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "station_idx" ON "station" USING btree ("id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "station_type_idx" ON "station" USING btree ("type"); \ No newline at end of file diff --git a/drizzle/migrations/0001_faithful_storm.sql b/drizzle/migrations/0001_faithful_storm.sql deleted file mode 100644 index 62c6bca..0000000 --- a/drizzle/migrations/0001_faithful_storm.sql +++ /dev/null @@ -1,20 +0,0 @@ -CREATE TABLE IF NOT EXISTS "schedule" ( - "id" text PRIMARY KEY NOT NULL, - "station_id" text NOT NULL, - "station_origin_id" text, - "station_origin_name" text NOT NULL, - "station_destination_id" text, - "station_destination_name" text NOT NULL, - "train_id" text NOT NULL, - "line" text NOT NULL, - "route" text NOT NULL, - "time_departure" time NOT NULL, - "time_at_destination" time NOT NULL, - "metadata" jsonb, - "created_at" timestamp with time zone DEFAULT now(), - "updated_at" timestamp with time zone DEFAULT now(), - CONSTRAINT "schedule_id_unique" UNIQUE("id") -); ---> statement-breakpoint -CREATE UNIQUE INDEX IF NOT EXISTS "schedule_idx" ON "schedule" USING btree ("id");--> statement-breakpoint -CREATE INDEX IF NOT EXISTS "schedule_station_idx" ON "schedule" USING btree ("station_id"); \ No newline at end of file diff --git a/drizzle/migrations/meta/0000_snapshot.json b/drizzle/migrations/meta/0000_snapshot.json index 6258988..0344b39 100644 --- a/drizzle/migrations/meta/0000_snapshot.json +++ b/drizzle/migrations/meta/0000_snapshot.json @@ -1,9 +1,172 @@ { - "id": "d29cef4a-e37d-4b2b-8b3a-1313657171a9", + "id": "3a3dc1d9-e80d-4925-a6fc-2d71565b88de", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", "tables": { + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time_departure": { + "name": "time_departure", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "time_at_destination": { + "name": "time_at_destination", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, "public.station": { "name": "station", "schema": "", @@ -65,7 +228,7 @@ "nulls": "last" } ], - "isUnique": false, + "isUnique": true, "concurrently": false, "method": "btree", "with": {} @@ -110,6 +273,13 @@ "columns": [ "uid" ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] } } } diff --git a/drizzle/migrations/meta/0001_snapshot.json b/drizzle/migrations/meta/0001_snapshot.json deleted file mode 100644 index 1545bf4..0000000 --- a/drizzle/migrations/meta/0001_snapshot.json +++ /dev/null @@ -1,270 +0,0 @@ -{ - "id": "13768c82-84d4-46b6-aaf0-1e084219f026", - "prevId": "d29cef4a-e37d-4b2b-8b3a-1313657171a9", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.schedule": { - "name": "schedule", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "station_id": { - "name": "station_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "station_origin_id": { - "name": "station_origin_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "station_origin_name": { - "name": "station_origin_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "station_destination_id": { - "name": "station_destination_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "station_destination_name": { - "name": "station_destination_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "train_id": { - "name": "train_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "line": { - "name": "line", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "route": { - "name": "route", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "time_departure": { - "name": "time_departure", - "type": "time", - "primaryKey": false, - "notNull": true - }, - "time_at_destination": { - "name": "time_at_destination", - "type": "time", - "primaryKey": false, - "notNull": true - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "schedule_idx": { - "name": "schedule_idx", - "columns": [ - { - "expression": "id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "schedule_station_idx": { - "name": "schedule_station_idx", - "columns": [ - { - "expression": "station_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "schedule_id_unique": { - "name": "schedule_id_unique", - "nullsNotDistinct": false, - "columns": [ - "id" - ] - } - } - }, - "public.station": { - "name": "station", - "schema": "", - "columns": { - "uid": { - "name": "uid", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "station_type", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "station_uidx": { - "name": "station_uidx", - "columns": [ - { - "expression": "uid", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "station_idx": { - "name": "station_idx", - "columns": [ - { - "expression": "id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "station_type_idx": { - "name": "station_type_idx", - "columns": [ - { - "expression": "type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "station_uid_unique": { - "name": "station_uid_unique", - "nullsNotDistinct": false, - "columns": [ - "uid" - ] - } - } - } - }, - "enums": { - "public.station_type": { - "name": "station_type", - "schema": "public", - "values": [ - "KRL", - "MRT", - "LRT" - ] - } - }, - "schemas": {}, - "sequences": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 07e5de7..222e8cb 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -5,15 +5,8 @@ { "idx": 0, "version": "7", - "when": 1726200221153, - "tag": "0000_faithful_tyger_tiger", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1731324915407, - "tag": "0001_faithful_storm", + "when": 1731339553074, + "tag": "0000_overjoyed_rockslide", "breakpoints": true } ] diff --git a/src/db/schema-new/index.ts b/src/db/schema-new/index.ts index 730b643..1eb358a 100644 --- a/src/db/schema-new/index.ts +++ b/src/db/schema-new/index.ts @@ -1,3 +1,4 @@ +import { relations } from "drizzle-orm" import { index, jsonb, @@ -15,9 +16,8 @@ import { z } from "zod" const stationMetadata = z.object({ /** Comuline metadata */ has_schedule: z.boolean().nullable(), - /** Original metadata */ - /** TODO: Change to origin */ - original: z.object({ + /** Origin metadata */ + origin: z.object({ /** KRL */ daop: z.number().nullable(), fg_enable: z.number().nullable(), @@ -32,7 +32,7 @@ export const stationTable = pgTable( "station", { uid: text("uid").primaryKey().unique().notNull(), - id: text("id").notNull(), + id: text("id").unique().notNull(), name: text("name").notNull(), type: stationTypeEnum("type").notNull(), metadata: jsonb("metadata").$type(), @@ -45,7 +45,7 @@ export const stationTable = pgTable( }, (table) => { return { - station_uidx: index("station_uidx").on(table.uid), + station_uidx: uniqueIndex("station_uidx").on(table.uid), station_idx: index("station_idx").on(table.id), type_idx: index("station_type_idx").on(table.type), } @@ -65,11 +65,15 @@ export const scheduleTable = pgTable( "schedule", { id: text("id").primaryKey().unique().notNull(), - station_id: text("station_id").notNull(), - station_origin_id: text("station_origin_id"), - station_origin_name: text("station_origin_name").notNull(), - station_destination_id: text("station_destination_id"), - station_destination_name: text("station_destination_name").notNull(), + station_id: text("station_id") + .notNull() + .references(() => stationTable.id), + station_origin_id: text("station_origin_id").references( + () => stationTable.id, + ), + station_destination_id: text("station_destination_id").references( + () => stationTable.id, + ), train_id: text("train_id").notNull(), line: text("line").notNull(), route: text("route").notNull(), @@ -91,6 +95,21 @@ export const scheduleTable = pgTable( }, ) +export const scheduleTableRelations = relations(scheduleTable, ({ one }) => ({ + station: one(stationTable, { + fields: [scheduleTable.station_id], + references: [stationTable.id], + }), + station_origin: one(stationTable, { + fields: [scheduleTable.station_origin_id], + references: [stationTable.id], + }), + station_destination: one(stationTable, { + fields: [scheduleTable.station_destination_id], + references: [stationTable.id], + }), +})) + export const stationSchema = createSelectSchema(stationTable) export type NewStation = typeof stationTable.$inferInsert @@ -104,3 +123,5 @@ export const scheduleSchema = createSelectSchema(scheduleTable, { }) export type ScheduleType = z.infer + +export type NewSchedule = typeof scheduleTable.$inferInsert diff --git a/src/modules/v1/schedule/schedule.schema.ts b/src/modules/v1/schedule/schedule.schema.ts index a0a24ce..ec2d360 100644 --- a/src/modules/v1/schedule/schedule.schema.ts +++ b/src/modules/v1/schedule/schedule.schema.ts @@ -1,4 +1,4 @@ -import { z } from "zod" +import { z } from "@hono/zod-openapi" import { scheduleSchema, StationScheduleMetadata, @@ -19,21 +19,12 @@ export const scheduleResponseSchema = z example: "JAKK", description: "Station ID where the train originates", }), - station_origin_name: scheduleSchema.shape.station_origin_name.openapi({ - example: "JAKARTAKOTA", - description: "Station name where the train originates", - }), station_destination_id: scheduleSchema.shape.station_destination_id.openapi( { example: "TPK", description: "Station ID where the train terminates", }, ), - station_destination_name: - scheduleSchema.shape.station_destination_name.openapi({ - example: "TANJUNGPRIUK", - description: "Station name where the train terminates", - }), train_id: scheduleSchema.shape.train_id.openapi({ example: "2400", description: "Train ID", diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index 9c576e0..8ccc6cf 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -146,7 +146,7 @@ const stationController = api type: "KRL", metadata: { has_schedule: true, - original: { + origin: { fg_enable: s.fg_enable, daop: s.group_wil === 0 ? 1 : s.group_wil, }, diff --git a/src/modules/v1/station/station.schema.ts b/src/modules/v1/station/station.schema.ts index 27ab1ba..fb71f7a 100644 --- a/src/modules/v1/station/station.schema.ts +++ b/src/modules/v1/station/station.schema.ts @@ -1,5 +1,5 @@ -import { z } from "zod" -import { StationMetadata, stationSchema } from "../../../db/schema-new" +import { z } from "@hono/zod-openapi" +import { type StationMetadata, stationSchema } from "../../../db/schema-new" export const stationResponseSchema = z .object({ @@ -20,7 +20,7 @@ export const stationResponseSchema = z type: "object", example: { has_schedule: true, - original: { + origin: { daop: 1, fg_enable: 1, }, diff --git a/src/sync.ts b/src/sync.ts index bef890a..a4d3d1d 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1,7 +1,12 @@ import { sleep } from "bun" import { eq, sql } from "drizzle-orm" import { z } from "zod" -import { NewStation, schedule, station } from "./db/schema-new" +import { + NewSchedule, + NewStation, + scheduleTable, + stationTable, +} from "./db/schema-new" import { Database } from "./modules/v1/database" export function parseTime(timeString: string): Date { @@ -23,8 +28,12 @@ const sync = async () => { }) const stations = await db - .select({ id: station.id, metadata: station.metadata, name: station.name }) - .from(station) + .select({ + id: stationTable.id, + metadata: stationTable.metadata, + name: stationTable.name, + }) + .from(stationTable) const batchSizes = 5 const totalBatches = Math.ceil(stations.length / batchSizes) @@ -109,15 +118,13 @@ const sync = async () => { station_origin_id: stations.find( ({ name }) => name === origin, )?.id!, - station_origin_name: origin, station_destination_id: stations.find( ({ name }) => name === destination, )?.id!, - station_destination_name: d.dest, train_id: d.train_id, line: d.ka_name, route: d.route_name, - time_departure: parseTime(d.dest_time).toLocaleTimeString(), + time_departure: parseTime(d.time_est).toLocaleTimeString(), time_at_destination: parseTime( d.dest_time, ).toLocaleTimeString(), @@ -126,14 +133,14 @@ const sync = async () => { color: d.color, }, }, - } + } satisfies NewSchedule }) const insert = await db - .insert(schedule) + .insert(scheduleTable) .values(values) .onConflictDoUpdate({ - target: schedule.id, + target: scheduleTable.id, set: { time_departure: sql`excluded.time_departure`, time_at_destination: sql`excluded.time_at_destination`, @@ -165,7 +172,10 @@ const sync = async () => { : null, updated_at: new Date(), } - await db.update(station).set(payload).where(eq(station.id, id)) + await db + .update(stationTable) + .set(payload) + .where(eq(scheduleTable.id, id)) console.info( `[SYNC][SCHEDULE][${id}] Updated station schedule availability status`, ) From f7865ee2b0af4930855e97f9185457169bd94ad6 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Mon, 11 Nov 2024 22:42:59 +0700 Subject: [PATCH 18/64] feat: route --- src/modules/v1/index.ts | 3 ++ src/modules/v1/route.ts | 72 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/modules/v1/route.ts diff --git a/src/modules/v1/index.ts b/src/modules/v1/index.ts index c64ae6d..462a44a 100644 --- a/src/modules/v1/index.ts +++ b/src/modules/v1/index.ts @@ -1,4 +1,5 @@ import { createAPI } from "../api" +import routeController from "./route" import scheduleController from "./schedule/schedule.controller" import stationController from "./station/station.controller" @@ -7,4 +8,6 @@ const api = createAPI() const v1 = api .route("/station", stationController) .route("/schedule", scheduleController) + .route("/route", routeController) + export default v1 diff --git a/src/modules/v1/route.ts b/src/modules/v1/route.ts new file mode 100644 index 0000000..4eecb65 --- /dev/null +++ b/src/modules/v1/route.ts @@ -0,0 +1,72 @@ +import { createRoute, z } from "@hono/zod-openapi" +import { eq } from "drizzle-orm" +import { scheduleTable } from "../../db/schema-new" +import { createAPI } from "../api" +import { + buildDataResponseSchema, + buildMetadataResponseSchema, +} from "../utils/response" + +const api = createAPI() + +const routeController = api.openapi( + createRoute({ + method: "get", + path: "/{train_id}", + request: { + params: z.object({ + train_id: z + .string() + .min(2) + .openapi({ + param: { + name: "train_id", + in: "path", + }, + default: "2400", + example: "2400", + }), + }), + }, + responses: { + ...buildDataResponseSchema(200, z.any()), + ...buildMetadataResponseSchema(404, "Not found"), + }, + tags: ["Route"], + }), + async (c) => { + const param = c.req.valid("param") + const { db } = c.var + + const data = await db.query.scheduleTable.findMany({ + with: { + station: { + columns: { + name: true, + }, + }, + }, + orderBy: (scheduleTable, { asc }) => [asc(scheduleTable.time_departure)], + where: eq(scheduleTable.train_id, param.train_id), + }) + + const schedules = data.map(({ id, station_id, station, ...rest }) => ({ + id, + station_id, + station_name: station.name, + ...rest, + })) + + return c.json( + { + metadata: { + success: true, + }, + data: schedules, + }, + 200, + ) + }, +) + +export default routeController From eff14df3f676d32ef960d495375f067c00a8a1e4 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 12:40:46 +0700 Subject: [PATCH 19/64] refactor: response schema --- src/modules/utils/response.ts | 109 +++++++++++------- src/modules/v1/route.ts | 20 ++-- .../v1/schedule/schedule.controller.ts | 20 ++-- src/modules/v1/station/station.controller.ts | 44 ++++--- 4 files changed, 121 insertions(+), 72 deletions(-) diff --git a/src/modules/utils/response.ts b/src/modules/utils/response.ts index e623ad7..dbe9b4e 100644 --- a/src/modules/utils/response.ts +++ b/src/modules/utils/response.ts @@ -1,4 +1,5 @@ import { HTTPException } from "hono/http-exception" +import { StatusCode } from "hono/utils/http-status" import { z } from "zod" export const constructResponse = ( @@ -18,44 +19,70 @@ export const constructResponse = ( return result.data } -export const buildDataResponseSchema = ( - status: number, - schema: z.ZodTypeAny, -) => ({ - [status]: { - content: { - "application/json": { - schema: z.object({ - metadata: z.object({ - success: z.boolean().default(true), - }), - data: schema, - }), - }, - }, - description: "Success", - }, -}) - -export const buildMetadataResponseSchema = ( - status: number, - description?: string, - success?: boolean, -) => ({ - [status]: { - content: { - "application/json": { - schema: z.object({ - metadata: z.object({ - success: z.boolean().default(success ?? false), - message: z - .string() - .min(1) - .default(description || "Error"), - }), - }), - }, - }, - description: description || "Error", - }, -}) +interface BaseResponseSchema { + status: number +} + +interface DataResponseSchema extends BaseResponseSchema { + type: "data" + schema: z.ZodTypeAny +} + +interface MetadataResponseSchema extends BaseResponseSchema { + type: "metadata" + description?: string +} + +export const buildResponseSchemas = ( + responses: Array, +) => { + const result: Record = {} + + for (const { status, ...rest } of responses) { + if (rest.type === "data") { + const { schema } = rest + result[status] = { + content: { + "application/json": { + schema: z.object({ + metadata: z.object({ + success: z.boolean().default(true), + }), + data: schema, + }), + }, + }, + description: "Success", + } + } else { + const { description } = rest + + const defaultDescription = getDefaultDescription(status as StatusCode) + + result[status] = { + content: { + "application/json": { + schema: z.object({ + metadata: z.object({ + success: z.boolean().default(false), + message: z.string().min(1).default(defaultDescription), + }), + }), + }, + }, + description: defaultDescription, + } + } + } + + return result +} + +const getDefaultDescription = (status: StatusCode) => { + switch (status) { + case 404: + return "Not found" + default: + return "Internal server error" + } +} diff --git a/src/modules/v1/route.ts b/src/modules/v1/route.ts index 4eecb65..550a8d2 100644 --- a/src/modules/v1/route.ts +++ b/src/modules/v1/route.ts @@ -2,10 +2,7 @@ import { createRoute, z } from "@hono/zod-openapi" import { eq } from "drizzle-orm" import { scheduleTable } from "../../db/schema-new" import { createAPI } from "../api" -import { - buildDataResponseSchema, - buildMetadataResponseSchema, -} from "../utils/response" +import { buildResponseSchemas } from "../utils/response" const api = createAPI() @@ -28,10 +25,17 @@ const routeController = api.openapi( }), }), }, - responses: { - ...buildDataResponseSchema(200, z.any()), - ...buildMetadataResponseSchema(404, "Not found"), - }, + responses: buildResponseSchemas([ + { + status: 200, + type: "data", + schema: z.any(), + }, + { + status: 404, + type: "metadata", + }, + ]), tags: ["Route"], }), async (c) => { diff --git a/src/modules/v1/schedule/schedule.controller.ts b/src/modules/v1/schedule/schedule.controller.ts index 4d61cbd..db82686 100644 --- a/src/modules/v1/schedule/schedule.controller.ts +++ b/src/modules/v1/schedule/schedule.controller.ts @@ -2,10 +2,7 @@ import { createRoute, z } from "@hono/zod-openapi" import { asc, eq } from "drizzle-orm" import { scheduleTable } from "../../../db/schema-new" import { createAPI } from "../../api" -import { - buildDataResponseSchema, - buildMetadataResponseSchema, -} from "../../utils/response" +import { buildResponseSchemas } from "../../utils/response" import { scheduleResponseSchema } from "./schedule.schema" const api = createAPI() @@ -29,10 +26,17 @@ const scheduleController = api.openapi( }), }), }, - responses: { - ...buildDataResponseSchema(200, z.array(scheduleResponseSchema)), - ...buildMetadataResponseSchema(404, "Not found"), - }, + responses: buildResponseSchemas([ + { + status: 200, + type: "data", + schema: z.array(scheduleResponseSchema), + }, + { + status: 404, + type: "metadata", + }, + ]), tags: ["Schedule"], }), async (c) => { diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index 8ccc6cf..c8efd03 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -3,10 +3,7 @@ import { eq, sql } from "drizzle-orm" import { HTTPException } from "hono/http-exception" import { NewStation, stationTable, StationType } from "../../../db/schema-new" import { createAPI } from "../../api" -import { - buildDataResponseSchema, - buildMetadataResponseSchema, -} from "../../utils/response" +import { buildResponseSchemas } from "../../utils/response" import { Sync } from "../sync" import { stationResponseSchema } from "./station.schema" @@ -20,11 +17,15 @@ const stationController = api createRoute({ method: "get", path: "/", - responses: { - ...buildDataResponseSchema(200, z.array(stationResponseSchema)), - }, + responses: buildResponseSchemas([ + { + status: 200, + type: "data", + schema: z.array(stationResponseSchema), + }, + ]), tags: ["Station"], - description: "Get all KRL station data", + description: "Get all station data", }), async (c) => { const { db } = c.var @@ -63,10 +64,18 @@ const stationController = api }), }), }, - responses: { - ...buildDataResponseSchema(200, stationResponseSchema), - ...buildMetadataResponseSchema(404, "Not found"), - }, + responses: buildResponseSchemas([ + { + status: 200, + type: "data", + schema: stationResponseSchema, + }, + { + status: 404, + type: "metadata", + }, + ]), + tags: ["Station"], }), async (c) => { @@ -92,9 +101,14 @@ const stationController = api createRoute({ method: "post", path: "/", - responses: { - ...buildMetadataResponseSchema(201, "Success", true), - }, + responses: buildResponseSchemas([ + { + status: 201, + type: "metadata", + description: "Success", + }, + ]), + tags: ["Station"], }), async (c) => { From 06b5f3d6a98a478de28926a6c5d0ddd2e24e7204 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:32:43 +0700 Subject: [PATCH 20/64] fix: response generation --- src/modules/utils/response.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/modules/utils/response.ts b/src/modules/utils/response.ts index dbe9b4e..fe920bc 100644 --- a/src/modules/utils/response.ts +++ b/src/modules/utils/response.ts @@ -1,5 +1,6 @@ +import { type RouteConfig } from "@hono/zod-openapi" import { HTTPException } from "hono/http-exception" -import { StatusCode } from "hono/utils/http-status" +import { type StatusCode } from "hono/utils/http-status" import { z } from "zod" export const constructResponse = ( @@ -35,8 +36,8 @@ interface MetadataResponseSchema extends BaseResponseSchema { export const buildResponseSchemas = ( responses: Array, -) => { - const result: Record = {} +): RouteConfig["responses"] => { + let result: RouteConfig["responses"] = {} for (const { status, ...rest } of responses) { if (rest.type === "data") { @@ -53,11 +54,12 @@ export const buildResponseSchemas = ( }, }, description: "Success", - } + } satisfies RouteConfig["responses"][string] } else { const { description } = rest - const defaultDescription = getDefaultDescription(status as StatusCode) + const defaultDescription = + description ?? getDefaultDescription(status as StatusCode) result[status] = { content: { @@ -71,7 +73,7 @@ export const buildResponseSchemas = ( }, }, description: defaultDescription, - } + } satisfies RouteConfig["responses"][string] } } From 1895baf107e68d3be43f6a5044e660ad0b7016bc Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:33:19 +0700 Subject: [PATCH 21/64] fix: route response schema --- src/modules/v1/index.ts | 2 +- src/modules/v1/route.ts | 76 --------------- src/modules/v1/route/route.controller.ts | 116 +++++++++++++++++++++++ src/modules/v1/route/route.schema.ts | 37 ++++++++ 4 files changed, 154 insertions(+), 77 deletions(-) delete mode 100644 src/modules/v1/route.ts create mode 100644 src/modules/v1/route/route.controller.ts create mode 100644 src/modules/v1/route/route.schema.ts diff --git a/src/modules/v1/index.ts b/src/modules/v1/index.ts index 462a44a..a5460b0 100644 --- a/src/modules/v1/index.ts +++ b/src/modules/v1/index.ts @@ -1,5 +1,5 @@ import { createAPI } from "../api" -import routeController from "./route" +import routeController from "./route/route.controller" import scheduleController from "./schedule/schedule.controller" import stationController from "./station/station.controller" diff --git a/src/modules/v1/route.ts b/src/modules/v1/route.ts deleted file mode 100644 index 550a8d2..0000000 --- a/src/modules/v1/route.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { createRoute, z } from "@hono/zod-openapi" -import { eq } from "drizzle-orm" -import { scheduleTable } from "../../db/schema-new" -import { createAPI } from "../api" -import { buildResponseSchemas } from "../utils/response" - -const api = createAPI() - -const routeController = api.openapi( - createRoute({ - method: "get", - path: "/{train_id}", - request: { - params: z.object({ - train_id: z - .string() - .min(2) - .openapi({ - param: { - name: "train_id", - in: "path", - }, - default: "2400", - example: "2400", - }), - }), - }, - responses: buildResponseSchemas([ - { - status: 200, - type: "data", - schema: z.any(), - }, - { - status: 404, - type: "metadata", - }, - ]), - tags: ["Route"], - }), - async (c) => { - const param = c.req.valid("param") - const { db } = c.var - - const data = await db.query.scheduleTable.findMany({ - with: { - station: { - columns: { - name: true, - }, - }, - }, - orderBy: (scheduleTable, { asc }) => [asc(scheduleTable.time_departure)], - where: eq(scheduleTable.train_id, param.train_id), - }) - - const schedules = data.map(({ id, station_id, station, ...rest }) => ({ - id, - station_id, - station_name: station.name, - ...rest, - })) - - return c.json( - { - metadata: { - success: true, - }, - data: schedules, - }, - 200, - ) - }, -) - -export default routeController diff --git a/src/modules/v1/route/route.controller.ts b/src/modules/v1/route/route.controller.ts new file mode 100644 index 0000000..82d5977 --- /dev/null +++ b/src/modules/v1/route/route.controller.ts @@ -0,0 +1,116 @@ +import { createRoute, z } from "@hono/zod-openapi" +import { eq } from "drizzle-orm" +import { scheduleTable } from "../../../db/schema-new" +import { createAPI } from "../../api" +import { buildResponseSchemas } from "../../utils/response" +import { RouteResponse, routeResponseSchema } from "./route.schema" + +const api = createAPI() + +const routeController = api.openapi( + createRoute({ + method: "get", + path: "/{train_id}", + request: { + params: z.object({ + train_id: z + .string() + .min(2) + .openapi({ + param: { + name: "train_id", + in: "path", + }, + default: "2400", + example: "2400", + }), + }), + }, + responses: buildResponseSchemas([ + { + status: 200, + type: "data", + schema: routeResponseSchema, + }, + { + status: 404, + type: "metadata", + }, + ]), + tags: ["Route"], + }), + async (c) => { + const param = c.req.valid("param") + const { db } = c.var + + const data = await db.query.scheduleTable.findMany({ + with: { + station: { + columns: { + name: true, + }, + }, + station_destination: { + columns: { + name: true, + }, + }, + }, + orderBy: (scheduleTable, { asc }) => [asc(scheduleTable.time_departure)], + where: eq(scheduleTable.train_id, param.train_id), + }) + + if (data.length === 0) + return c.json( + { + metadata: { + success: true, + }, + data: [], + }, + 200, + ) + + const response = { + routes: data.map( + ({ + id, + station_id, + station, + time_departure, + created_at, + updated_at, + }) => ({ + id, + station_id, + station_name: station.name, + time_departure, + created_at, + updated_at, + }), + ), + details: { + train_id: param.train_id, + line: data[0].line, + route: data[0].route, + station_origin_id: data[0].station_origin_id, + station_origin_name: data[0].station.name, + station_destination_id: data[0].station_destination_id, + station_destination_name: data[0].station_destination?.name ?? "", + time_at_destination: data[0].time_at_destination, + }, + } satisfies RouteResponse + + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse(routeResponseSchema, response), + }, + 200, + ) + }, +) + +export default routeController diff --git a/src/modules/v1/route/route.schema.ts b/src/modules/v1/route/route.schema.ts new file mode 100644 index 0000000..1350407 --- /dev/null +++ b/src/modules/v1/route/route.schema.ts @@ -0,0 +1,37 @@ +import { z } from "@hono/zod-openapi" +import { scheduleResponseSchema } from "../schedule/schedule.schema" +import { stationResponseSchema } from "../station/station.schema" + +export const routeResponseSchema = z + .object({ + routes: z.array( + z.object({ + id: scheduleResponseSchema.shape.id, + station_id: scheduleResponseSchema.shape.station_id, + station_name: stationResponseSchema.shape.name.openapi({ + example: "ANCOL", + }), + time_departure: scheduleResponseSchema.shape.time_departure, + created_at: scheduleResponseSchema.shape.created_at, + updated_at: scheduleResponseSchema.shape.updated_at, + }), + ), + details: z.object({ + train_id: scheduleResponseSchema.shape.train_id, + line: scheduleResponseSchema.shape.line, + route: scheduleResponseSchema.shape.route, + station_origin_id: scheduleResponseSchema.shape.station_origin_id, + station_origin_name: stationResponseSchema.shape.name.openapi({ + example: "JAKARTAKOTA", + }), + station_destination_id: + scheduleResponseSchema.shape.station_destination_id, + station_destination_name: z.string().optional().openapi({ + example: "TANJUNGPRIUK", + }), + time_at_destination: scheduleResponseSchema.shape.time_at_destination, + }), + }) + .openapi("Route") + +export type RouteResponse = z.infer From 51599cabc2770ec2406cc7a033a936f10fb4fe30 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:37:53 +0700 Subject: [PATCH 22/64] fix: add desc --- src/modules/v1/route/route.controller.ts | 1 + src/modules/v1/schedule/schedule.controller.ts | 1 + src/modules/v1/station/station.controller.ts | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/v1/route/route.controller.ts b/src/modules/v1/route/route.controller.ts index 82d5977..0c6173a 100644 --- a/src/modules/v1/route/route.controller.ts +++ b/src/modules/v1/route/route.controller.ts @@ -38,6 +38,7 @@ const routeController = api.openapi( }, ]), tags: ["Route"], + description: "Get route by train id", }), async (c) => { const param = c.req.valid("param") diff --git a/src/modules/v1/schedule/schedule.controller.ts b/src/modules/v1/schedule/schedule.controller.ts index db82686..86325cf 100644 --- a/src/modules/v1/schedule/schedule.controller.ts +++ b/src/modules/v1/schedule/schedule.controller.ts @@ -38,6 +38,7 @@ const scheduleController = api.openapi( }, ]), tags: ["Schedule"], + description: "Get all active schedule by station id", }), async (c) => { const param = c.req.valid("param") diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index c8efd03..2f16e9a 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -75,8 +75,8 @@ const stationController = api type: "metadata", }, ]), - tags: ["Station"], + description: "Get station by id", }), async (c) => { const { id } = c.req.valid("param") From c931ccb923ff0f2cd47deaf844dcfa15dd4713fc Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:20:21 +0700 Subject: [PATCH 23/64] feat: init redis upstash --- docker-compose.yml | 15 +++++++++++++++ package.json | 3 +++ src/app.ts | 2 ++ src/modules/v1/cache.ts | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 src/modules/v1/cache.ts diff --git a/docker-compose.yml b/docker-compose.yml index 42299ad..3971fd5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ version: "3.9" services: + # Serverless PostgresSQL postgres: image: "postgres:15.2-alpine" ports: @@ -17,3 +18,17 @@ services: - "5433:80" depends_on: - postgres + + # Serverless Redis + redis: + image: redis + ports: + - "6379:6379" + serverless-redis-http: + ports: + - "8079:80" + image: hiett/serverless-redis-http:latest + env_file: + - ./.env.db + depends_on: + - redis diff --git a/package.json b/package.json index 7717d5d..b064faa 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "version": "1.0.50", "scripts": { "app": "wrangler dev src/app.ts --port 3001", + "docker:up": "docker-compose -p comuline-api up -d", + "docker:down": "docker-compose down", "test": "echo \"Error: no test specified\" && exit 1", "deploy": "wrangler deploy", "dev": "bun run --watch src/index.ts", @@ -30,6 +32,7 @@ "@hono/zod-openapi": "^0.16.0", "@neondatabase/serverless": "^0.9.5", "@scalar/hono-api-reference": "^0.5.145", + "@upstash/redis": "^1.34.3", "dotenv": "^16.4.5", "drizzle-orm": "^0.33.0", "drizzle-zod": "^0.5.1", diff --git a/src/app.ts b/src/app.ts index 53ad6c2..1db6f89 100644 --- a/src/app.ts +++ b/src/app.ts @@ -8,6 +8,8 @@ import { constructResponse } from "./modules/utils/response" export type Bindings = { DATABASE_URL: string COMULINE_ENV: string + UPSTASH_REDIS_REST_TOKEN: string + UPSTASH_REDIS_REST_URL: string } export type Variables = { diff --git a/src/modules/v1/cache.ts b/src/modules/v1/cache.ts new file mode 100644 index 0000000..83cab20 --- /dev/null +++ b/src/modules/v1/cache.ts @@ -0,0 +1,34 @@ +import { Redis } from "@upstash/redis/cloudflare" + +export class Cache { + protected kv: Redis + public key: string + + constructor( + key: string, + protected env: { + UPSTASH_REDIS_REST_TOKEN: string + UPSTASH_REDIS_REST_URL: string + }, + ) { + this.key = key + this.kv = Redis.fromEnv(env) + } + + async get(): Promise { + const data = await this.kv.get(this.key) + return data ?? null + } + + async set(value: T, ttl?: number): Promise { + await this.kv.set( + this.key, + JSON.stringify(value), + ttl + ? { + ex: ttl, + } + : undefined, + ) + } +} From 96cc5c4c3cc8d6d159338c71f6d842ea6696e92d Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:29:47 +0700 Subject: [PATCH 24/64] fix: add more stations --- ...sql => 0000_talented_daimon_hellstrom.sql} | 0 drizzle/migrations/0001_tiny_shadowcat.sql | 1 + drizzle/migrations/meta/0000_snapshot.json | 2 +- drizzle/migrations/meta/0001_snapshot.json | 306 ++++++++++++++++++ drizzle/migrations/meta/_journal.json | 11 +- src/db/schema-new/index.ts | 7 +- src/modules/v1/station/station.controller.ts | 49 ++- 7 files changed, 371 insertions(+), 5 deletions(-) rename drizzle/migrations/{0000_overjoyed_rockslide.sql => 0000_talented_daimon_hellstrom.sql} (100%) create mode 100644 drizzle/migrations/0001_tiny_shadowcat.sql create mode 100644 drizzle/migrations/meta/0001_snapshot.json diff --git a/drizzle/migrations/0000_overjoyed_rockslide.sql b/drizzle/migrations/0000_talented_daimon_hellstrom.sql similarity index 100% rename from drizzle/migrations/0000_overjoyed_rockslide.sql rename to drizzle/migrations/0000_talented_daimon_hellstrom.sql diff --git a/drizzle/migrations/0001_tiny_shadowcat.sql b/drizzle/migrations/0001_tiny_shadowcat.sql new file mode 100644 index 0000000..7ffbb3a --- /dev/null +++ b/drizzle/migrations/0001_tiny_shadowcat.sql @@ -0,0 +1 @@ +ALTER TYPE "station_type" ADD VALUE 'LOCAL'; \ No newline at end of file diff --git a/drizzle/migrations/meta/0000_snapshot.json b/drizzle/migrations/meta/0000_snapshot.json index 0344b39..3f6a54f 100644 --- a/drizzle/migrations/meta/0000_snapshot.json +++ b/drizzle/migrations/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "3a3dc1d9-e80d-4925-a6fc-2d71565b88de", + "id": "ebac93b2-10b0-46c7-8348-9827ee12aef5", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", diff --git a/drizzle/migrations/meta/0001_snapshot.json b/drizzle/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..3a6e3f7 --- /dev/null +++ b/drizzle/migrations/meta/0001_snapshot.json @@ -0,0 +1,306 @@ +{ + "id": "dd1edb26-6f6a-4a0b-856b-8b73207f9055", + "prevId": "ebac93b2-10b0-46c7-8348-9827ee12aef5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time_departure": { + "name": "time_departure", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "time_at_destination": { + "name": "time_at_destination", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT", + "LOCAL" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 222e8cb..25e4751 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -5,8 +5,15 @@ { "idx": 0, "version": "7", - "when": 1731339553074, - "tag": "0000_overjoyed_rockslide", + "when": 1731395911889, + "tag": "0000_talented_daimon_hellstrom", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1731396377710, + "tag": "0001_tiny_shadowcat", "breakpoints": true } ] diff --git a/src/db/schema-new/index.ts b/src/db/schema-new/index.ts index 1eb358a..bc59e70 100644 --- a/src/db/schema-new/index.ts +++ b/src/db/schema-new/index.ts @@ -26,7 +26,12 @@ const stationMetadata = z.object({ export type StationMetadata = z.infer -export const stationTypeEnum = pgEnum("station_type", ["KRL", "MRT", "LRT"]) +export const stationTypeEnum = pgEnum("station_type", [ + "KRL", + "MRT", + "LRT", + "LOCAL", +]) export const stationTable = pgTable( "station", diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index 2f16e9a..98daaf7 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -152,7 +152,7 @@ const stationController = api const filterdStation = res.data.filter((d) => !d.sta_id.includes("WIL")) - const insertStations = filterdStation.map((s) => { + const stations = filterdStation.map((s) => { return { uid: createStationKey("KRL", s.sta_id), id: s.sta_id, @@ -168,6 +168,53 @@ const stationController = api } }) satisfies NewStation[] + const newStations = [ + /** Bandara Soekarno Hatta */ + { + uid: createStationKey("KRL", "BST"), + id: "BST", + name: "BANDARA SOEKARNO HATTA", + type: "KRL", + metadata: { + has_schedule: true, + origin: { + fg_enable: 1, + daop: 1, + }, + }, + }, + /** Cikampek */ + { + uid: createStationKey("KRL", "CKP"), + id: "CKP", + name: "CIKAMPEK", + type: "LOCAL", + metadata: { + has_schedule: true, + origin: { + fg_enable: 1, + daop: 1, + }, + }, + }, + /** Purwakarta */ + { + uid: createStationKey("KRL", "PWK"), + id: "PWK", + name: "PURWAKARTA", + type: "LOCAL", + metadata: { + has_schedule: true, + origin: { + fg_enable: 1, + daop: 2, + }, + }, + }, + ] satisfies NewStation[] + + const insertStations = [...newStations, ...stations] + await db .insert(stationTable) .values(insertStations) From 0847f5d0db3b3a1dc1291a6dd0a1436e85df18a2 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:29:56 +0700 Subject: [PATCH 25/64] fix: add redis env --- .dev.example.vars | 6 +++++- .env.db | 9 ++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.dev.example.vars b/.dev.example.vars index e454f87..8d85a07 100644 --- a/.dev.example.vars +++ b/.dev.example.vars @@ -1,2 +1,6 @@ DATABASE_URL="postgresql://comuline:password@localhost:5432/comuline" -COMULINE_ENV="development" \ No newline at end of file +COMULINE_ENV="development" + +# Take token from .env.db +UPSTASH_REDIS_REST_TOKEN="" +UPSTASH_REDIS_REST_URL="http://localhost:8079" \ No newline at end of file diff --git a/.env.db b/.env.db index 3977482..46a866b 100644 --- a/.env.db +++ b/.env.db @@ -1,3 +1,10 @@ +# PostgreSQL POSTGRES_USER="comuline" POSTGRES_PASSWORD="password" -POSTGRES_DB="comuline" \ No newline at end of file +POSTGRES_DB="comuline" + +# Redis +SRH_MODE=env +# openssl rand -base64 32 +SRH_TOKEN="1Pf91ZNy5LDTKG621uhX/E73P8RmhVZu43kIV/WCmHg=" +SRH_CONNECTION_STRING=redis://redis:6379 \ No newline at end of file From 37f7212deecb3662782bc2ebac90de8f2e4bef77 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:30:05 +0700 Subject: [PATCH 26/64] fix: schedule sync station name --- src/sync.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/sync.ts b/src/sync.ts index a4d3d1d..50d361d 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -110,7 +110,29 @@ const sync = async () => { console.error(`[SYNC][SCHEDULE][${id}] Error parse`) } else { const values = parsed.data.data.map((d) => { - const [origin, destination] = d.route_name.split("-") + let [origin, destination] = d.route_name.split("-") + + const fixName = (name: string) => { + switch (name) { + case "TANJUNGPRIUK": + return "TANJUNG PRIOK" + case "JAKARTAKOTA": + return "JAKARTA KOTA" + case "KAMPUNGBANDAN": + return "KAMPUNG BANDAN" + case "TANAHABANG": + return "TANAH ABANG" + case "PARUNGPANJANG": + return "PARUNG PANJANG" + case "BANDARASOEKARNOHATTA": + return "BANDARA SOEKARNO HATTA" + default: + return name + } + } + + origin = fixName(origin) + destination = fixName(destination) return { id: `sc_krl_${id}_${d.train_id}`.toLowerCase(), From 8f316fd59c865982c03de89234011b55ae5f533c Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:31:22 +0700 Subject: [PATCH 27/64] fix: remove unused --- src/modules/v1/route/route.controller.ts | 4 ---- src/modules/v1/schedule/schedule.controller.ts | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/modules/v1/route/route.controller.ts b/src/modules/v1/route/route.controller.ts index 0c6173a..1504912 100644 --- a/src/modules/v1/route/route.controller.ts +++ b/src/modules/v1/route/route.controller.ts @@ -32,10 +32,6 @@ const routeController = api.openapi( type: "data", schema: routeResponseSchema, }, - { - status: 404, - type: "metadata", - }, ]), tags: ["Route"], description: "Get route by train id", diff --git a/src/modules/v1/schedule/schedule.controller.ts b/src/modules/v1/schedule/schedule.controller.ts index 86325cf..76757da 100644 --- a/src/modules/v1/schedule/schedule.controller.ts +++ b/src/modules/v1/schedule/schedule.controller.ts @@ -32,10 +32,6 @@ const scheduleController = api.openapi( type: "data", schema: z.array(scheduleResponseSchema), }, - { - status: 404, - type: "metadata", - }, ]), tags: ["Schedule"], description: "Get all active schedule by station id", From cc9d54b3f84eb005dc93fb6575dce318b1526d46 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:14:28 +0700 Subject: [PATCH 28/64] fix: string date mode --- src/db/schema-new/index.ts | 4 ++++ src/sync.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/db/schema-new/index.ts b/src/db/schema-new/index.ts index bc59e70..bedaf11 100644 --- a/src/db/schema-new/index.ts +++ b/src/db/schema-new/index.ts @@ -43,9 +43,11 @@ export const stationTable = pgTable( metadata: jsonb("metadata").$type(), created_at: timestamp("created_at", { withTimezone: true, + mode: "string", }).defaultNow(), updated_at: timestamp("updated_at", { withTimezone: true, + mode: "string", }).defaultNow(), }, (table) => { @@ -86,10 +88,12 @@ export const scheduleTable = pgTable( time_at_destination: time("time_at_destination").notNull(), metadata: jsonb("metadata").$type(), created_at: timestamp("created_at", { + mode: "string", withTimezone: true, }).defaultNow(), updated_at: timestamp("updated_at", { withTimezone: true, + mode: "string", }).defaultNow(), }, (table) => { diff --git a/src/sync.ts b/src/sync.ts index 50d361d..c0438b6 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -167,7 +167,7 @@ const sync = async () => { time_departure: sql`excluded.time_departure`, time_at_destination: sql`excluded.time_at_destination`, metadata: sql`excluded.metadata`, - updated_at: new Date(), + updated_at: new Date().toLocaleString(), }, }) .returning() @@ -192,7 +192,7 @@ const sync = async () => { has_schedule: false, } : null, - updated_at: new Date(), + updated_at: new Date().toLocaleString(), } await db .update(stationTable) From 5c4400bf3234c9f915dde056fa0d5b26e70b8d31 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:14:34 +0700 Subject: [PATCH 29/64] fix: desc --- src/modules/v1/route/route.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/v1/route/route.controller.ts b/src/modules/v1/route/route.controller.ts index 1504912..f9a1175 100644 --- a/src/modules/v1/route/route.controller.ts +++ b/src/modules/v1/route/route.controller.ts @@ -34,7 +34,7 @@ const routeController = api.openapi( }, ]), tags: ["Route"], - description: "Get route by train id", + description: "Get sequence of station by train id", }), async (c) => { const param = c.req.valid("param") From f59e3c7044bc1909e394e9805269fa4c57b35f50 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:16:09 +0700 Subject: [PATCH 30/64] fix: db on delete --- drizzle/migrations/0002_serious_the_hand.sql | 23 ++ drizzle/migrations/meta/0002_snapshot.json | 306 +++++++++++++++++++ drizzle/migrations/meta/_journal.json | 7 + src/db/schema-new/index.ts | 10 +- 4 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 drizzle/migrations/0002_serious_the_hand.sql create mode 100644 drizzle/migrations/meta/0002_snapshot.json diff --git a/drizzle/migrations/0002_serious_the_hand.sql b/drizzle/migrations/0002_serious_the_hand.sql new file mode 100644 index 0000000..a4dfe01 --- /dev/null +++ b/drizzle/migrations/0002_serious_the_hand.sql @@ -0,0 +1,23 @@ +ALTER TABLE "schedule" DROP CONSTRAINT "schedule_station_id_station_id_fk"; +--> statement-breakpoint +ALTER TABLE "schedule" DROP CONSTRAINT "schedule_station_origin_id_station_id_fk"; +--> statement-breakpoint +ALTER TABLE "schedule" DROP CONSTRAINT "schedule_station_destination_id_station_id_fk"; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "schedule" ADD CONSTRAINT "schedule_station_id_station_id_fk" FOREIGN KEY ("station_id") REFERENCES "public"."station"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "schedule" ADD CONSTRAINT "schedule_station_origin_id_station_id_fk" FOREIGN KEY ("station_origin_id") REFERENCES "public"."station"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "schedule" ADD CONSTRAINT "schedule_station_destination_id_station_id_fk" FOREIGN KEY ("station_destination_id") REFERENCES "public"."station"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/drizzle/migrations/meta/0002_snapshot.json b/drizzle/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..a919664 --- /dev/null +++ b/drizzle/migrations/meta/0002_snapshot.json @@ -0,0 +1,306 @@ +{ + "id": "5db983ea-4af8-40b5-9bca-1f274a3c4e3a", + "prevId": "dd1edb26-6f6a-4a0b-856b-8b73207f9055", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time_departure": { + "name": "time_departure", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "time_at_destination": { + "name": "time_at_destination", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT", + "LOCAL" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 25e4751..40bc0af 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1731396377710, "tag": "0001_tiny_shadowcat", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1731399355344, + "tag": "0002_serious_the_hand", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema-new/index.ts b/src/db/schema-new/index.ts index bedaf11..aa96861 100644 --- a/src/db/schema-new/index.ts +++ b/src/db/schema-new/index.ts @@ -74,12 +74,20 @@ export const scheduleTable = pgTable( id: text("id").primaryKey().unique().notNull(), station_id: text("station_id") .notNull() - .references(() => stationTable.id), + .references(() => stationTable.id, { + onDelete: "cascade", + }), station_origin_id: text("station_origin_id").references( () => stationTable.id, + { + onDelete: "set null", + }, ), station_destination_id: text("station_destination_id").references( () => stationTable.id, + { + onDelete: "set null", + }, ), train_id: text("train_id").notNull(), line: text("line").notNull(), From d2e5ef744e8faa59efe39b6f9bf1a3f8bfc00caa Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:24:19 +0700 Subject: [PATCH 31/64] fix: date string mode --- src/modules/v1/station/station.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index 98daaf7..0b86eef 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -221,7 +221,7 @@ const stationController = api .onConflictDoUpdate({ target: stationTable.uid, set: { - updated_at: new Date(), + updated_at: new Date().toLocaleString(), uid: sql`excluded.uid`, id: sql`excluded.id`, name: sql`excluded.name`, From d9309d661e1b26c80421444d36af0c276046dc6e Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:36:39 +0700 Subject: [PATCH 32/64] feat: cache func --- src/db/schema-new/index.ts | 2 +- src/modules/v1/cache.ts | 2 +- src/modules/v1/route/route.controller.ts | 23 +++++++- src/modules/v1/route/route.schema.ts | 2 +- .../v1/schedule/schedule.controller.ts | 30 +++++++++- src/modules/v1/station/station.controller.ts | 55 +++++++++++++++++-- src/modules/v1/uitls.ts | 8 +++ 7 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 src/modules/v1/uitls.ts diff --git a/src/db/schema-new/index.ts b/src/db/schema-new/index.ts index aa96861..0ea98cc 100644 --- a/src/db/schema-new/index.ts +++ b/src/db/schema-new/index.ts @@ -139,6 +139,6 @@ export const scheduleSchema = createSelectSchema(scheduleTable, { metadata: stationScheduleMetadata.nullable(), }) -export type ScheduleType = z.infer +export type Schedule = z.infer export type NewSchedule = typeof scheduleTable.$inferInsert diff --git a/src/modules/v1/cache.ts b/src/modules/v1/cache.ts index 83cab20..8dc673f 100644 --- a/src/modules/v1/cache.ts +++ b/src/modules/v1/cache.ts @@ -5,11 +5,11 @@ export class Cache { public key: string constructor( - key: string, protected env: { UPSTASH_REDIS_REST_TOKEN: string UPSTASH_REDIS_REST_URL: string }, + key: string, ) { this.key = key this.kv = Redis.fromEnv(env) diff --git a/src/modules/v1/route/route.controller.ts b/src/modules/v1/route/route.controller.ts index f9a1175..9f7beef 100644 --- a/src/modules/v1/route/route.controller.ts +++ b/src/modules/v1/route/route.controller.ts @@ -3,7 +3,9 @@ import { eq } from "drizzle-orm" import { scheduleTable } from "../../../db/schema-new" import { createAPI } from "../../api" import { buildResponseSchemas } from "../../utils/response" -import { RouteResponse, routeResponseSchema } from "./route.schema" +import { Cache } from "../cache" +import { Route, routeResponseSchema } from "./route.schema" +import { getSecsToMidnight } from "../uitls" const api = createAPI() @@ -40,6 +42,21 @@ const routeController = api.openapi( const param = c.req.valid("param") const { db } = c.var + const cache = new Cache(c.env, `route:${param.train_id}`) + + const cached = await cache.get() + + if (cached) + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse(routeResponseSchema, cached), + }, + 200, + ) + const data = await db.query.scheduleTable.findMany({ with: { station: { @@ -96,7 +113,9 @@ const routeController = api.openapi( station_destination_name: data[0].station_destination?.name ?? "", time_at_destination: data[0].time_at_destination, }, - } satisfies RouteResponse + } satisfies Route + + await cache.set(response, getSecsToMidnight()) return c.json( { diff --git a/src/modules/v1/route/route.schema.ts b/src/modules/v1/route/route.schema.ts index 1350407..5b717d8 100644 --- a/src/modules/v1/route/route.schema.ts +++ b/src/modules/v1/route/route.schema.ts @@ -34,4 +34,4 @@ export const routeResponseSchema = z }) .openapi("Route") -export type RouteResponse = z.infer +export type Route = z.infer diff --git a/src/modules/v1/schedule/schedule.controller.ts b/src/modules/v1/schedule/schedule.controller.ts index 76757da..27f4355 100644 --- a/src/modules/v1/schedule/schedule.controller.ts +++ b/src/modules/v1/schedule/schedule.controller.ts @@ -1,9 +1,11 @@ import { createRoute, z } from "@hono/zod-openapi" import { asc, eq } from "drizzle-orm" -import { scheduleTable } from "../../../db/schema-new" +import { scheduleTable, Schedule } from "../../../db/schema-new" import { createAPI } from "../../api" import { buildResponseSchemas } from "../../utils/response" import { scheduleResponseSchema } from "./schedule.schema" +import { Cache } from "../cache" +import { getSecsToMidnight } from "../uitls" const api = createAPI() @@ -38,13 +40,37 @@ const scheduleController = api.openapi( }), async (c) => { const param = c.req.valid("param") - const db = c.get("db") + const { db } = c.var + + const cache = new Cache>( + c.env, + `schedules:${param.station_id}`, + ) + + const cached = await cache.get() + + if (cached) + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse( + z.array(scheduleResponseSchema), + cached, + ), + }, + 200, + ) + const data = await db .select() .from(scheduleTable) .where(eq(scheduleTable.station_id, param.station_id.toLocaleUpperCase())) .orderBy(asc(scheduleTable.time_departure)) + await cache.set(data, getSecsToMidnight()) + return c.json( { metadata: { diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index 0b86eef..b4e6d86 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -1,10 +1,17 @@ import { createRoute, z } from "@hono/zod-openapi" import { eq, sql } from "drizzle-orm" import { HTTPException } from "hono/http-exception" -import { NewStation, stationTable, StationType } from "../../../db/schema-new" +import { + NewStation, + Station, + stationTable, + StationType, +} from "../../../db/schema-new" import { createAPI } from "../../api" import { buildResponseSchemas } from "../../utils/response" +import { Cache } from "../cache" import { Sync } from "../sync" +import { getSecsToMidnight } from "../uitls" import { stationResponseSchema } from "./station.schema" const api = createAPI() @@ -29,8 +36,29 @@ const stationController = api }), async (c) => { const { db } = c.var + + const cache = new Cache>(c.env, "stations") + + const cached = await cache.get() + + if (cached) + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse( + z.array(stationResponseSchema), + cached, + ), + }, + 200, + ) + const stations = await db.select().from(stationTable) + await cache.set(stations, getSecsToMidnight()) + return c.json( { metadata: { @@ -79,12 +107,31 @@ const stationController = api description: "Get station by id", }), async (c) => { - const { id } = c.req.valid("param") - const db = c.get("db") + const param = c.req.valid("param") + + const { db } = c.var + + const cache = new Cache(c.env, `station:${param.id}`) + + const cached = await cache.get() + + if (cached) + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse(stationResponseSchema, cached), + }, + 200, + ) + const data = await db .select() .from(stationTable) - .where(eq(stationTable.id, id)) + .where(eq(stationTable.id, param.id)) + + await cache.set(data[0], getSecsToMidnight()) return c.json( { diff --git a/src/modules/v1/uitls.ts b/src/modules/v1/uitls.ts new file mode 100644 index 0000000..b086d2d --- /dev/null +++ b/src/modules/v1/uitls.ts @@ -0,0 +1,8 @@ +export function getSecsToMidnight(): number { + const now = new Date() + const tomorrow = new Date(now) + tomorrow.setHours(0, 0, 0, 0) + tomorrow.setDate(tomorrow.getDate() + 1) + + return Math.floor((tomorrow.getTime() - now.getTime()) / 1000) +} From 080217dce8284f36189282d0df54a0e01dc55701 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:38:27 +0700 Subject: [PATCH 33/64] refactor: get rid of elysia --- src/commons/libs/swagger.ts | 34 ---- src/commons/types.ts | 58 ------- src/commons/utils/cache.ts | 35 ---- src/commons/utils/date.ts | 15 -- src/commons/utils/error.ts | 6 - src/commons/utils/log.ts | 21 --- src/controllers/index.ts | 22 --- src/controllers/route.ts | 259 ------------------------------ src/controllers/schedule.ts | 136 ---------------- src/controllers/station.ts | 157 ------------------ src/controllers/sync.ts | 118 -------------- src/index.ts | 32 ---- src/services/index.ts | 4 - src/services/route/get-all.ts | 117 -------------- src/services/route/index.ts | 29 ---- src/services/schedule/get-all.ts | 78 --------- src/services/schedule/index.ts | 33 ---- src/services/schedule/sync.ts | 145 ----------------- src/services/station/get-all.ts | 46 ------ src/services/station/get-by-id.ts | 39 ----- src/services/station/index.ts | 40 ----- src/services/station/sync.ts | 66 -------- src/services/sync/get-all.ts | 25 --- src/services/sync/get-by-id.ts | 24 --- src/services/sync/index.ts | 24 --- src/services/utils/index.ts | 2 - src/services/utils/sync.ts | 98 ----------- src/types.ts | 18 --- 28 files changed, 1681 deletions(-) delete mode 100644 src/commons/libs/swagger.ts delete mode 100644 src/commons/types.ts delete mode 100644 src/commons/utils/cache.ts delete mode 100644 src/commons/utils/date.ts delete mode 100644 src/commons/utils/error.ts delete mode 100644 src/commons/utils/log.ts delete mode 100644 src/controllers/index.ts delete mode 100644 src/controllers/route.ts delete mode 100644 src/controllers/schedule.ts delete mode 100644 src/controllers/station.ts delete mode 100644 src/controllers/sync.ts delete mode 100644 src/index.ts delete mode 100644 src/services/index.ts delete mode 100644 src/services/route/get-all.ts delete mode 100644 src/services/route/index.ts delete mode 100644 src/services/schedule/get-all.ts delete mode 100644 src/services/schedule/index.ts delete mode 100644 src/services/schedule/sync.ts delete mode 100644 src/services/station/get-all.ts delete mode 100644 src/services/station/get-by-id.ts delete mode 100644 src/services/station/index.ts delete mode 100644 src/services/station/sync.ts delete mode 100644 src/services/sync/get-all.ts delete mode 100644 src/services/sync/get-by-id.ts delete mode 100644 src/services/sync/index.ts delete mode 100644 src/services/utils/index.ts delete mode 100644 src/services/utils/sync.ts delete mode 100644 src/types.ts diff --git a/src/commons/libs/swagger.ts b/src/commons/libs/swagger.ts deleted file mode 100644 index a409160..0000000 --- a/src/commons/libs/swagger.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { swagger as primitiveSwagger } from "@elysiajs/swagger" - -const swagger = () => - primitiveSwagger({ - path: "/docs", - exclude: ["/docs", "/docs/json", "/", "/health"], - documentation: { - info: { - title: "Comuline API", - description: "API documentation for Comuline API", - version: "1.0.0", - }, - tags: [ - { - name: "Station", - description: "Station related endpoints", - }, - { - name: "Schedule", - description: "Schedule related endpoints", - }, - { - name: "Route", - description: "Route related endpoints", - }, - { - name: "Utility", - description: "Utility related endpoints", - }, - ], - }, - }) - -export default swagger diff --git a/src/commons/types.ts b/src/commons/types.ts deleted file mode 100644 index b41da1b..0000000 --- a/src/commons/types.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { t } from "elysia" - -export type APIResponse = { - data: T - status: number - message: "OK" | "ERROR" | "NOT_FOUND" | string -} - -export const syncResponse = (item: SyncItem) => ({ - 200: t.Object( - { - status: t.Number(), - data: t.Object({ - id: t.String(), - status: t.String(), - type: t.Union([t.Literal("manual"), t.Literal("cron")]), - item: t.Union([t.Literal("station"), t.Literal("schedule")]), - }), - }, - { - default: { - status: 200, - data: { - id: "08dd3ed8-8dd7-4c0d-8463-7422ce3e07b9", - type: "manual", - item, - status: "PENDING", - }, - }, - }, - ), -}) - -export const scheduleResponseObject = { - id: t.Nullable(t.String()), - name: t.Nullable(t.String()), - daop: t.Nullable(t.Number()), - fgEnable: t.Nullable(t.Number()), - haveSchedule: t.Nullable(t.Boolean()), - updatedAt: t.Nullable(t.String()), -} - -export const syncResponseObject = { - id: t.Nullable(t.String()), - n: t.Number(), - type: t.Nullable(t.String()), - status: t.Nullable(t.String()), - item: t.Nullable(t.String()), - duration: t.Nullable(t.Number()), - message: t.Nullable(t.String()), - startedAt: t.Nullable(t.String()), - endedAt: t.Nullable(t.String()), - createdAt: t.Nullable(t.String()), -} - -export type SyncType = "manual" | "cron" - -export type SyncItem = "station" | "schedule" diff --git a/src/commons/utils/cache.ts b/src/commons/utils/cache.ts deleted file mode 100644 index 43f938d..0000000 --- a/src/commons/utils/cache.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { KVNamespace } from "@cloudflare/workers-types" -import { getKV } from "../../types" - -class Cache { - protected ttl: number | null - protected cache: KVNamespace - public key: string - public cached: T | null - - constructor(key: string, options?: { ttl?: number }) { - this.ttl = options ? options.ttl ?? null : null - this.key = key - this.cached = null - this.cache = getKV() - } - - async set(value: T) { - const self = this - const val = typeof value === "string" ? value : JSON.stringify(value) - return await self.cache.put(self.key, val, { - expirationTtl: self.ttl ?? undefined, - }) - } - - async get() { - const self = this - const data = await self.cache.get(self.key) - if (data) { - self.cached = JSON.parse(data) as T - } - return self.cached - } -} - -export default Cache diff --git a/src/commons/utils/date.ts b/src/commons/utils/date.ts deleted file mode 100644 index 3f35938..0000000 --- a/src/commons/utils/date.ts +++ /dev/null @@ -1,15 +0,0 @@ -export function parseTime(timeString: string): Date { - const [hours, minutes, seconds] = timeString.split(":").map(Number) - const date = new Date() - date.setHours(hours ?? date.getHours()) - date.setMinutes(minutes ?? date.getMinutes()) - date.setSeconds(seconds ?? date.getSeconds()) - - return date -} - -export function getSecondsRemainingFromNow(): number { - return ( - 60 * new Date(Date.now()).getMinutes() * new Date(Date.now()).getHours() - ) -} diff --git a/src/commons/utils/error.ts b/src/commons/utils/error.ts deleted file mode 100644 index e45998f..0000000 --- a/src/commons/utils/error.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function handleError(e: any): string { - if (e instanceof Error) { - return e.message - } - return JSON.stringify(e) -} diff --git a/src/commons/utils/log.ts b/src/commons/utils/log.ts deleted file mode 100644 index ee92e8a..0000000 --- a/src/commons/utils/log.ts +++ /dev/null @@ -1,21 +0,0 @@ -import pino from "pino" - -const transport = pino.transport({ - targets: [ - // Uncomment the following lines to log to a file in your local machine - /* { - level: "trace", - target: "pino/file", - options: { - destination: "./logs/file.log", - }, - }, */ - { - level: "trace", - target: "pino-pretty", - options: {}, - }, - ], -}) - -export const logger = pino({}, transport) diff --git a/src/controllers/index.ts b/src/controllers/index.ts deleted file mode 100644 index ac545e8..0000000 --- a/src/controllers/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Elysia, { NotFoundError } from "elysia" -import { APIResponse } from "../commons/types" -import scheduleController from "./schedule" -import stationController from "./station" -import syncController from "./sync" -import routeController from "./route" - -const controllers = new Elysia({ prefix: "/v1" }) - .onError((ctx) => { - return { - status: (ctx.error as NotFoundError).status ?? 500, - message: ctx.error.message.includes("{") - ? JSON.parse(ctx.error.message) - : ctx.error.message, - } satisfies Partial - }) - .use(stationController) - .use(scheduleController) - .use(routeController) - .use(syncController) - -export default controllers diff --git a/src/controllers/route.ts b/src/controllers/route.ts deleted file mode 100644 index 588a58e..0000000 --- a/src/controllers/route.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { Elysia, InternalServerError, t } from "elysia" -import * as service from "../services" -import { SyncType, syncResponse } from "../commons/types" - -const routeController = (app: Elysia) => - app.group("/route", (app) => { - app.get( - "/:trainId", - async (ctx) => { - if (ctx.query.from_station_id) - return await service.route.getAllFrom( - ctx.params.trainId, - ctx.query.from_station_id.toLocaleUpperCase(), - ) - return await service.route.getAll(ctx.params.trainId) - }, - { - params: t.Object({ - trainId: t.String(), - }), - query: t.Object({ - from_station_id: t.Optional(t.String()), - }), - response: { - 404: t.Object( - { - status: t.Number(), - message: t.String(), - }, - { - default: { - status: 404, - message: "Route data is not found", - }, - }, - ), - 200: t.Object( - { - status: t.Number(), - data: t.Array( - t.Object({ - id: t.Nullable(t.String()), - stationId: t.Nullable(t.String()), - stationName: t.Nullable(t.String()), - trainId: t.Nullable(t.String()), - line: t.Nullable(t.String()), - route: t.Nullable(t.String()), - color: t.Nullable(t.String()), - destination: t.Nullable(t.String()), - timeEstimated: t.Nullable(t.String()), - destinationTime: t.Nullable(t.String()), - updatedAt: t.Nullable(t.String()), - }), - ), - }, - { - default: { - status: 200, - data: [ - { - id: "BKS-5171", - stationId: "BKS", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "21:47:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:08.132Z", - stationName: "BEKASI", - }, - { - id: "KRI-5171", - stationId: "KRI", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "21:50:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:14.794Z", - stationName: "KRANJI", - }, - { - id: "CUK-5171", - stationId: "CUK", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "21:56:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:10.959Z", - stationName: "CAKUNG", - }, - { - id: "KLDB-5171", - stationId: "KLDB", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "21:58:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:14.141Z", - stationName: "KLENDERBARU", - }, - { - id: "BUA-5171", - stationId: "BUA", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "21:59:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:08.931Z", - stationName: "BUARAN", - }, - { - id: "KLD-5171", - stationId: "KLD", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:01:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:14.080Z", - stationName: "KLENDER", - }, - { - id: "JNG-5171", - stationId: "JNG", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:10:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:13.396Z", - stationName: "JATINEGARA", - }, - { - id: "MTR-5171", - stationId: "MTR", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:12:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:10.858Z", - stationName: "MATRAMAN", - }, - { - id: "MRI-5171", - stationId: "MRI", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:18:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:15.636Z", - stationName: "MANGGARAI", - }, - { - id: "SUD-5171", - stationId: "SUD", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:22:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:18.281Z", - stationName: "SUDIRMAN", - }, - { - id: "SUDB-5171", - stationId: "SUDB", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:23:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:11.019Z", - stationName: "SUDIRMAN BARU", - }, - { - id: "KAT-5171", - stationId: "KAT", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:24:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:13.317Z", - stationName: "KARET", - }, - { - id: "THB-5171", - stationId: "THB", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:30:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:18.884Z", - stationName: "TANAHABANG", - }, - { - id: "DU-5171", - stationId: "DU", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:38:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:12.541Z", - stationName: "DURI", - }, - ], - }, - }, - ), - }, - - detail: { - description: - "Get a list of schedule data for a train route from a train ID sorted by timeEstimated", - tags: ["Route"], - }, - }, - ) - - return app - }) - -export default routeController diff --git a/src/controllers/schedule.ts b/src/controllers/schedule.ts deleted file mode 100644 index f84af34..0000000 --- a/src/controllers/schedule.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { Elysia, InternalServerError, t } from "elysia" -import * as service from "../services" -import { SyncType, syncResponse } from "../commons/types" - -const scheduleController = (app: Elysia) => - app.group("/schedule", (app) => { - app.post( - "/", - async (ctx) => { - const type: SyncType = ctx.query.from_cron ? "cron" : "manual" - - if (process.env.NODE_ENV === "development") { - return await service.schedule.sync(type) - } - const token = ctx.headers.authorization - - if (!token) { - throw new InternalServerError("Please provide a token") - } - - if (token.split(" ")[1] !== process.env.SYNC_TOKEN) { - throw new InternalServerError("Invalid token") - } - - return await service.schedule.sync(type) - }, - { - headers: - process.env.NODE_ENV === "development" - ? undefined - : t.Object({ - authorization: t.String(), - }), - query: t.Object({ - from_cron: t.Optional(t.BooleanString()), - }), - detail: { - description: "Sync schedule data", - tags: ["Schedule"], - }, - response: syncResponse("schedule"), - }, - ) - - app.get( - "/:stationId", - async (ctx) => { - return await service.schedule.getAll( - ctx.params.stationId.toLocaleUpperCase(), - ctx.query.is_from_now ?? false, - ) - }, - { - params: t.Object({ - stationId: t.String(), - }), - query: t.Object({ - is_from_now: t.Optional(t.BooleanString()), - }), - response: { - 404: t.Object( - { - status: t.Number(), - message: t.String(), - }, - { - default: { - status: 404, - message: "Schedule data is not found", - }, - }, - ), - 200: t.Object( - { - status: t.Number(), - data: t.Array( - t.Object({ - id: t.Nullable(t.String()), - stationId: t.Nullable(t.String()), - trainId: t.Nullable(t.String()), - line: t.Nullable(t.String()), - route: t.Nullable(t.String()), - color: t.Nullable(t.String()), - destination: t.Nullable(t.String()), - timeEstimated: t.Nullable(t.String()), - destinationTime: t.Nullable(t.String()), - updatedAt: t.Nullable(t.String()), - }), - ), - }, - { - default: { - status: 200, - data: [ - { - id: "AC-2400", - stationId: "AC", - trainId: "2400", - line: "COMMUTER LINE TANJUNGPRIUK", - route: "JAKARTAKOTA-TANJUNGPRIUK", - color: "#DD0067", - destination: "TANJUNGPRIUK", - timeEstimated: "06:07:00", - destinationTime: "06:16:00", - updatedAt: "2024-03-09T13:06:10.662Z", - }, - { - id: "AC-2401", - stationId: "AC", - trainId: "2401", - line: "COMMUTER LINE TANJUNGPRIUK", - route: "TANJUNGPRIUK-JAKARTAKOTA", - color: "#DD0067", - destination: "JAKARTAKOTA", - timeEstimated: "06:34:00", - destinationTime: "06:42:00", - updatedAt: "2024-03-09T13:06:10.662Z", - }, - ], - }, - }, - ), - }, - - detail: { - description: - "Get a list of schedule data for a station from a station ID", - tags: ["Schedule"], - }, - }, - ) - - return app - }) - -export default scheduleController diff --git a/src/controllers/station.ts b/src/controllers/station.ts deleted file mode 100644 index 85f2176..0000000 --- a/src/controllers/station.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { Elysia, InternalServerError, t } from "elysia" -import * as service from "../services" -import { - SyncType, - scheduleResponseObject, - syncResponse, -} from "../commons/types" - -const stationController = (app: Elysia) => - app.group("/station", (app) => { - app.post( - "/", - async (ctx) => { - const type: SyncType = ctx.query.from_cron ? "cron" : "manual" - - if (process.env.NODE_ENV === "development") - return await service.station.sync(type) - - const token = ctx.headers.authorization - - if (!token) throw new InternalServerError("Please provide a token") - - if (token.split(" ")[1] !== process.env.SYNC_TOKEN) - throw new InternalServerError("Invalid token") - - return await service.station.sync(type) - }, - { - headers: - process.env.NODE_ENV === "development" - ? undefined - : t.Object({ - authorization: t.Nullable(t.String()), - }), - query: t.Object({ - from_cron: t.Optional(t.BooleanString()), - }), - detail: { - description: "Sync station data", - tags: ["Station"], - }, - response: syncResponse("station"), - }, - ) - - app.get( - "/", - async (ctx) => { - return await service.station.getAll() - }, - { - response: { - 404: t.Object( - { - status: t.Number(), - message: t.String(), - }, - { - default: { - status: 404, - message: "Station data is not found", - }, - }, - ), - 200: t.Object( - { - status: t.Number(), - data: t.Array(t.Object(scheduleResponseObject)), - }, - { - default: { - status: 200, - data: [ - { - id: "AC", - name: "ANCOL", - daop: 1, - fgEnable: 1, - haveSchedule: true, - updatedAt: "2024-03-10T09:55:07.213Z", - }, - { - id: "AK", - name: "ANGKE", - daop: 1, - fgEnable: 1, - haveSchedule: true, - updatedAt: "2024-03-10T09:55:07.213Z", - }, - ], - }, - }, - ), - }, - detail: { - description: "Get a list of station data", - tags: ["Station"], - }, - }, - ) - - app.get( - "/:id", - async (ctx) => { - if (!ctx.params.id) - throw new InternalServerError("Station ID is required") - - return await service.station.getById(ctx.params.id.toLocaleUpperCase()) - }, - { - params: t.Object({ - id: t.String(), - }), - response: { - 404: t.Object( - { - status: t.Number(), - message: t.String(), - }, - { - default: { - status: 404, - message: "Station data is not found", - }, - }, - ), - 200: t.Object( - { - status: t.Number(), - data: t.Object(scheduleResponseObject), - }, - { - default: { - status: 200, - data: { - id: "AC", - name: "ANCOL", - daop: 1, - fgEnable: 1, - haveSchedule: true, - updatedAt: "2024-03-10T09:55:07.213Z", - }, - }, - }, - ), - }, - detail: { - description: "Get a station data from a station ID", - tags: ["Station"], - }, - }, - ) - - return app - }) - -export default stationController diff --git a/src/controllers/sync.ts b/src/controllers/sync.ts deleted file mode 100644 index 31d4123..0000000 --- a/src/controllers/sync.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Elysia, t } from "elysia" -import { syncResponseObject } from "../commons/types" -import * as service from "../services" - -const syncController = (app: Elysia) => - app.group("/sync", (app) => { - app.get( - "/", - async () => { - return await service.sync.getAll() - }, - { - response: { - 200: t.Object( - { - status: t.Number(), - data: t.Array(t.Object(syncResponseObject)), - }, - { - default: { - status: 200, - data: [ - { - id: "2bb72322-7152-4b79-bea5-e3f639a71501", - n: 12, - type: "manual", - status: "FAILED", - item: "station", - duration: 12, - message: "Not implemented", - startedAt: "2024-03-10 12:19:24.500629+00", - endedAt: "2024-03-10T12:19:24.515Z", - createdAt: "2024-03-10 12:19:24.500629+00", - }, - { - id: "5f3523fe-b56b-4306-8498-a160588c2839", - n: 11, - type: "manual", - status: "SUCCESS", - item: "station", - duration: 11, - message: null, - startedAt: "2024-03-10 12:19:24.500629+00", - endedAt: "2024-03-10T12:19:24.515Z", - createdAt: "2024-03-10 12:19:24.500629+00", - }, - ], - }, - }, - ), - }, - - detail: { - description: "Get the most updated 20 sync data", - tags: ["Utility"], - }, - }, - ) - - app.get( - "/:id", - async (ctx) => { - return await service.sync.getItemById(ctx.params.id) - }, - { - params: t.Object({ - id: t.String(), - }), - - response: { - 404: t.Object( - { - status: t.Number(), - message: t.String(), - }, - { - default: { - status: 404, - message: "Sync data is not found", - }, - }, - ), - 200: t.Object( - { - status: t.Number(), - data: t.Object(syncResponseObject), - }, - { - default: { - status: 200, - data: { - id: "5f3523fe-b56b-4306-8498-a160588c2839", - n: 11, - type: "manual", - status: "SUCCESS", - item: "station", - duration: 11, - message: null, - startedAt: "2024-03-10 12:19:24.500629+00", - endedAt: "2024-03-10T12:19:24.515Z", - createdAt: "2024-03-10 12:19:24.500629+00", - }, - }, - }, - ), - }, - - detail: { - description: "Get a sync data item", - tags: ["Utility"], - }, - }, - ) - - return app - }) - -export default syncController diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index ff72fdb..0000000 --- a/src/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Elysia } from "elysia" -import controllers from "./controllers" -import { logger } from "./commons/utils/log" -import swagger from "./commons/libs/swagger" -import { rateLimit } from "elysia-rate-limit" - -export const app = new Elysia({ aot: false }) - .use(controllers) - .get("/", (ctx) => { - ctx.set.redirect = "/docs" - }) - .get("/health", () => { - return { - status: 200, - data: { - message: "OK", - }, - } - }) - .use(swagger()) - .use(rateLimit({ max: 5 })) - -try { - app.listen(process.env.NODE_ENV === "development" ? 3001 : 3000) -} catch (e) { - logger.error("[MAIN] Error starting server", e) - process.exit(1) -} - -logger.info( - `[MAIN] Service is running at ${app.server?.hostname}:${app.server?.port}`, -) diff --git a/src/services/index.ts b/src/services/index.ts deleted file mode 100644 index 9104257..0000000 --- a/src/services/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./station" -export * from "./schedule" -export * from "./sync" -export * from "./route" diff --git a/src/services/route/get-all.ts b/src/services/route/get-all.ts deleted file mode 100644 index e9f7f4b..0000000 --- a/src/services/route/get-all.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { and, asc, eq, gte, sql } from "drizzle-orm" -import { InternalServerError } from "elysia" -import Cache from "../../commons/utils/cache" -import { getSecondsRemainingFromNow } from "../../commons/utils/date" -import { handleError } from "../../commons/utils/error" -import { logger } from "../../commons/utils/log" -import { dbSchema } from "../../db" -import { Schedule, Station } from "../../db/schema" -import { getDB } from "../../types" - -export const getAll = async (trainId: string) => { - try { - const db = getDB() - const cache = new Cache<(Schedule & { stationName: Station["name"] })[]>( - `route-${trainId}`, - { - ttl: getSecondsRemainingFromNow(), - }, - ) - - const cached = await cache.get() - - if (cached) return cached - - const result = await db - .select() - .from(dbSchema.schedule) - .leftJoin( - dbSchema.station, - eq(dbSchema.schedule.stationId, dbSchema.station.id), - ) - .where(eq(dbSchema.schedule.trainId, trainId)) - .orderBy(asc(dbSchema.schedule.timeEstimated)) - - const schedules = result.map((res) => ({ - ...res.schedule, - stationName: res.station?.name || null, - })) - // Add the last station schedule - schedules.push({ - ...schedules[0], - stationName: schedules[0].destination, - timeEstimated: schedules[0].destinationTime, - }) - - if (schedules.length === 0) { - logger.error(`[QUERY][ROUTE][${trainId}] Route data is not found`) - return null - } - - await cache.set(schedules) - - return schedules - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} - -export const getAllFrom = async (trainId: string, fromStationId?: string) => { - try { - const db = getDB() - - const cache = new Cache<(Schedule & { stationName: Station["name"] })[]>( - `route-${trainId}-${fromStationId}`, - { - ttl: getSecondsRemainingFromNow(), - }, - ) - - const cached = await cache.get() - - if (cached) return cached - - const result = await db - .select() - .from(dbSchema.schedule) - .leftJoin( - dbSchema.station, - eq(dbSchema.schedule.stationId, dbSchema.station.id), - ) - .where( - and( - eq(dbSchema.schedule.trainId, trainId), - gte( - dbSchema.schedule.timeEstimated, - sql`( - SELECT time_estimated FROM schedule - WHERE station_id = ${fromStationId} AND train_id = ${trainId} - )`, - ), - ), - ) - .orderBy(asc(dbSchema.schedule.timeEstimated)) - - const schedules = result.map((res) => ({ - ...res.schedule, - stationName: res.station?.name || null, - })) - // Add the last station schedule - schedules.push({ - ...schedules[0], - stationName: schedules[0].destination, - timeEstimated: schedules[0].destinationTime, - }) - - if (schedules.length === 0) { - logger.error(`[QUERY][ROUTE][${trainId}] Route data is not found`) - return null - } - - await cache.set(schedules) - - return schedules - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/route/index.ts b/src/services/route/index.ts deleted file mode 100644 index 377a6c1..0000000 --- a/src/services/route/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NotFoundError } from "elysia" -import { getAll, getAllFrom } from "./get-all" - -export const route = { - getAll: async (trainId: string) => { - const routes = await getAll(trainId) - - if (!routes) { - throw new NotFoundError("Route data is not found") - } - - return { - status: 200, - data: routes, - } - }, - getAllFrom: async (trainId: string, fromStationId?: string) => { - const routes = await getAllFrom(trainId, fromStationId) - - if (!routes) { - throw new NotFoundError("Route data is not found") - } - - return { - status: 200, - data: routes, - } - }, -} diff --git a/src/services/schedule/get-all.ts b/src/services/schedule/get-all.ts deleted file mode 100644 index 9d2d5f3..0000000 --- a/src/services/schedule/get-all.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { asc, eq, sql } from "drizzle-orm" -import { InternalServerError } from "elysia" -import Cache from "../../commons/utils/cache" -import { handleError } from "../../commons/utils/error" -import { logger } from "../../commons/utils/log" -import { dbSchema } from "../../db" -import { Schedule } from "../../db/schema" -import { getDB } from "../../types" - -export const getAll = async (stationId: string) => { - try { - const db = getDB() - const cache = new Cache(`schedule-${stationId}`, { - ttl: - 60 * - new Date(Date.now()).getMinutes() * - new Date(Date.now()).getHours(), - }) - - const cached = await cache.get() - - if (cached) return cached - - const schedules = await db.query.schedule.findMany({ - where: eq(dbSchema.schedule.stationId, stationId), - orderBy: [asc(dbSchema.schedule.timeEstimated)], - }) - - if (schedules.length === 0) { - logger.error(`[QUERY][SCHEDULE][${stationId}] Schedule data is not found`) - return null - } - - await cache.set(schedules) - - return schedules - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} - -export const getAllFromNow = async (stationId: string) => { - try { - const db = getDB() - - const now = new Date() - - const currentSecond = now.getSeconds() - - const minutes = now.getMinutes() - - const cache = new Cache(`schedule-${stationId}-${minutes}`, { - ttl: 60 - currentSecond, - }) - - const cached = await cache.get() - - if (cached) return cached - - const schedules = await db.query.schedule.findMany({ - where: sql`station_id = ${stationId} AND time_estimated > (CURRENT_TIME AT TIME ZONE 'Asia/Jakarta')::time`, - orderBy: [asc(dbSchema.schedule.timeEstimated)], - }) - - if (schedules.length === 0) { - logger.warn( - `[QUERY][SCHEDULE][${stationId}] Schedule data from now is not found`, - ) - return null - } - - await cache.set(schedules) - - return schedules - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/schedule/index.ts b/src/services/schedule/index.ts deleted file mode 100644 index 5b6d595..0000000 --- a/src/services/schedule/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { NotFoundError } from "elysia" -import { SyncType } from "../../commons/types" -import { syncWrapper } from "../utils/sync" -import { getAll, getAllFromNow } from "./get-all" -import { sync as syncSchedule } from "./sync" - -export const schedule = { - sync: async (type: SyncType) => { - const data = await syncWrapper(syncSchedule, { - item: "schedule", - type, - })() - - return { - status: 200, - data, - } - }, - getAll: async (stationId: string, fromNow: boolean) => { - const schedules = fromNow - ? await getAllFromNow(stationId) - : await getAll(stationId) - - if (!schedules) { - throw new NotFoundError("Schedule data is not found") - } - - return { - status: 200, - data: schedules, - } - }, -} diff --git a/src/services/schedule/sync.ts b/src/services/schedule/sync.ts deleted file mode 100644 index 77c0892..0000000 --- a/src/services/schedule/sync.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { eq, sql } from "drizzle-orm" -import { InternalServerError } from "elysia" -import { z } from "zod" -import { parseTime } from "../../commons/utils/date" -import { handleError } from "../../commons/utils/error" -import { logger } from "../../commons/utils/log" -import { dbSchema } from "../../db" -import { NewStation } from "../../db/schema" -import { getDB } from "../../types" -import { sleep } from "../utils" - -export const syncItem = async (id: string) => { - try { - const db = getDB() - - const req = await fetch( - `https://api-partner.krl.co.id/krlweb/v1/schedule?stationid=${id}&timefrom=00:00&timeto=24:00`, - ).then((res) => res.json()) - - logger.info(`[SYNC][SCHEDULE][${id}] Fetched data from API`) - - const schema = z.object({ - status: z.number(), - data: z.array( - z.object({ - train_id: z.string(), - ka_name: z.string(), - route_name: z.string(), - dest: z.string(), - time_est: z.string(), - color: z.string(), - dest_time: z.string(), - }), - ), - }) - - if ((req as unknown as { status: number }).status === 404) { - logger.warn(`[SYNC][SCHEDULE][${id}] No schedule data found`) - - const payload: Partial = { - haveSchedule: false, - updatedAt: new Date().toISOString(), - } - - await db - .update(dbSchema.station) - .set(payload) - .where(eq(dbSchema.station.id, id)) - - logger.warn( - `[SYNC][SCHEDULE][${id}] Updated station schedule availability status`, - ) - } else if ((req as unknown as { status: number }).status === 200) { - const parsedData = schema.parse(req) - - const insert = await db - .insert(dbSchema.schedule) - .values( - parsedData.data.map((d) => { - return { - id: `${id}-${d.train_id}`, - stationId: id, - trainId: d.train_id, - line: d.ka_name, - route: d.route_name, - destination: d.dest, - timeEstimated: parseTime(d.time_est).toLocaleTimeString(), - destinationTime: parseTime(d.dest_time).toLocaleTimeString(), - color: d.color, - } - }), - ) - .onConflictDoUpdate({ - target: dbSchema.schedule.id, - set: { - timeEstimated: sql`excluded.time_estimated`, - destinationTime: sql`excluded.destination_time`, - color: sql`excluded.color`, - updatedAt: new Date().toISOString(), - }, - }) - .returning() - - logger.info(`[SYNC][SCHEDULE][${id}] Inserted ${insert.length} rows`) - } else { - logger.error( - `[SYNC][SCHEDULE][${id}] Error fetch schedule data. Trace: ${JSON.stringify( - req, - )}`, - ) - throw new Error("Failed to fetch schedule data for: " + id) - } - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} - -export const sync = async () => { - const db = getDB() - const stationsQuery = await db.query.station.findMany() - - const initialStations = await stationsQuery.map(({ id }) => id) - - if (initialStations.length === 0) { - const err = "No station data is existing. Please sync station data first." - logger.error("[SYNC][SCHEDULE] " + err) - throw new Error(err) - } - - const blacklistQuery = await db - .select({ - id: dbSchema.station.id, - }) - .from(dbSchema.station) - .where(eq(dbSchema.station.haveSchedule, false)) - - const blacklist = await blacklistQuery.map(({ id }) => id) - - const stations = - blacklist.length > 0 - ? initialStations.filter((s) => !blacklist.includes(s)) - : initialStations - - try { - logger.info("[SYNC][SCHEDULE] Syncing schedule data started") - const batchSizes = 5 - const totalBatches = Math.ceil(stations.length / batchSizes) - - for (let i = 0; i < totalBatches; i++) { - const start = i * batchSizes - const end = start + batchSizes - const batch = stations.slice(start, end) - - await Promise.allSettled( - batch.map(async (id) => { - await sleep(300) - await syncItem(id) - }), - ) - } - logger.info("[SYNC][SCHEDULE] Syncing schedule data finished") - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/station/get-all.ts b/src/services/station/get-all.ts deleted file mode 100644 index 2046831..0000000 --- a/src/services/station/get-all.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { asc, eq } from "drizzle-orm" -import { InternalServerError } from "elysia" -import Cache from "../../commons/utils/cache" -import { handleError } from "../../commons/utils/error" -import { logger } from "../../commons/utils/log" -import { dbSchema } from "../../db" -import { Station } from "../../db/schema" -import { getDB } from "../../types" - -export const getAll = async () => { - try { - const db = getDB() - const cache = new Cache("station-all", { - ttl: - 60 * - new Date(Date.now()).getMinutes() * - new Date(Date.now()).getHours(), - }) - - const cached = await cache.get() - - if (cached) return cached - - const stations = await db.query.station.findMany({ - orderBy: [ - asc(dbSchema.station.id), - asc(dbSchema.station.daop), - asc(dbSchema.station.name), - ], - where: eq(dbSchema.station.haveSchedule, true), - }) - - if (stations.length === 0) { - logger.error(`[QUERY][STATION][ALL] Stations data is not found`) - throw new Error( - "No station data is existing. Please sync station data first.", - ) - } - - await cache.set(stations) - - return stations - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/station/get-by-id.ts b/src/services/station/get-by-id.ts deleted file mode 100644 index c690609..0000000 --- a/src/services/station/get-by-id.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { eq } from "drizzle-orm" -import { InternalServerError } from "elysia" -import Cache from "../../commons/utils/cache" -import { handleError } from "../../commons/utils/error" -import { logger } from "../../commons/utils/log" -import { dbSchema } from "../../db" -import { Station } from "../../db/schema" -import { getDB } from "../../types" - -export const getItemById = async (stationId: string) => { - try { - const db = getDB() - const cache = new Cache(`station-${stationId}`, { - ttl: - 60 * - new Date(Date.now()).getMinutes() * - new Date(Date.now()).getHours(), - }) - - const cached = await cache.get() - - if (cached) return cached - - const station = await db.query.station.findFirst({ - where: eq(dbSchema.station.id, stationId), - }) - - if (!station) { - logger.error(`[QUERY][STATION][${stationId}] Station data is not found`) - return null - } - - await cache.set(station) - - return station - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/station/index.ts b/src/services/station/index.ts deleted file mode 100644 index 11da2d2..0000000 --- a/src/services/station/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NotFoundError } from "elysia" -import { syncWrapper } from "../utils/sync" -import { getAll } from "./get-all" -import { getItemById } from "./get-by-id" -import { sync as syncStation } from "./sync" -import { SyncType } from "../../commons/types" - -export const station = { - sync: async (type: SyncType) => { - const data = await syncWrapper(syncStation, { - item: "station", - type, - })() - - return { - status: 200, - data, - } - }, - getAll: async () => { - const stations = await getAll() - - return { - status: 200, - data: stations, - } - }, - getById: async (id: string) => { - const station = await getItemById(id) - - if (!station) { - throw new NotFoundError("Station data is not found") - } - - return { - status: 200, - data: station, - } - }, -} diff --git a/src/services/station/sync.ts b/src/services/station/sync.ts deleted file mode 100644 index bc3e240..0000000 --- a/src/services/station/sync.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { sql } from "drizzle-orm" -import { InternalServerError } from "elysia" -import { z } from "zod" -import { handleError } from "../../commons/utils/error" -import { logger } from "../../commons/utils/log" -import { dbSchema } from "../../db" -import { getDB } from "../../types" - -export const sync = async () => { - try { - const db = getDB() - logger.info("[SYNC][STATION] Syncing station data started") - - const req = await fetch( - "https://api-partner.krl.co.id/krlweb/v1/krl-station", - ).then((res) => res.json()) - - logger.info("[SYNC][STATION] Fetched data from API") - - const schema = z.object({ - status: z.number(), - message: z.string(), - data: z.array( - z.object({ - sta_id: z.string(), - sta_name: z.string(), - group_wil: z.number(), - fg_enable: z.number(), - }), - ), - }) - - const parsed = schema.parse(req) - - const filterdStation = parsed.data.filter((d) => !d.sta_id.includes("WIL")) - - const insert = await db - .insert(dbSchema.station) - .values( - filterdStation.map((s) => { - return { - id: s.sta_id, - name: s.sta_name, - fgEnable: s.fg_enable, - daop: s.group_wil === 0 ? 1 : s.group_wil, - } - }), - ) - .onConflictDoUpdate({ - target: dbSchema.station.id, - set: { - updatedAt: new Date().toISOString(), - id: sql`excluded.id`, - name: sql`excluded.name`, - daop: sql`excluded.daop`, - }, - }) - .returning() - - logger.info(`[SYNC][STATION] Inserted ${insert.length} rows`) - logger.info("[SYNC][STATION] Syncing station data finished") - } catch (e) { - logger.error("[SYNC][STATION] Error", e) - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/sync/get-all.ts b/src/services/sync/get-all.ts deleted file mode 100644 index 88e03ec..0000000 --- a/src/services/sync/get-all.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { asc, desc } from "drizzle-orm" -import { InternalServerError } from "elysia" -import { handleError } from "../../commons/utils/error" -import { logger } from "../../commons/utils/log" -import { dbSchema } from "../../db" -import { getDB } from "../../types" - -export const getAll = async () => { - try { - const db = getDB() - const items = await db.query.sync.findMany({ - limit: 20, - orderBy: [desc(dbSchema.sync.n), asc(dbSchema.sync.createdAt)], - }) - - if (items.length === 0) { - logger.error(`[QUERY][SYNC][ALL] Sync data is not found`) - return [] - } - - return items - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/sync/get-by-id.ts b/src/services/sync/get-by-id.ts deleted file mode 100644 index e2a7a51..0000000 --- a/src/services/sync/get-by-id.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { eq } from "drizzle-orm" -import { InternalServerError } from "elysia" -import { handleError } from "../../commons/utils/error" -import { logger } from "../../commons/utils/log" -import { dbSchema } from "../../db" -import { getDB } from "../../types" - -export const getItemById = async (syncId: string) => { - try { - const db = getDB() - const item = await db.query.sync.findFirst({ - where: eq(dbSchema.sync.id, syncId), - }) - - if (!item) { - logger.error(`[QUERY][SYNC][${syncId}] Sync data is not found`) - return null - } - - return item - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/sync/index.ts b/src/services/sync/index.ts deleted file mode 100644 index eaf722e..0000000 --- a/src/services/sync/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { getItemById } from "./get-by-id" -import { getAll } from "./get-all" -import { NotFoundError } from "elysia" - -export const sync = { - getAll: async () => { - const items = await getAll() - - return { - status: 200, - data: items, - } - }, - getItemById: async (id: string) => { - const item = await getItemById(id) - - if (!item) throw new NotFoundError("Sync data is not found") - - return { - status: 200, - data: item, - } - }, -} diff --git a/src/services/utils/index.ts b/src/services/utils/index.ts deleted file mode 100644 index 1f5ea0a..0000000 --- a/src/services/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const sleep = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)) diff --git a/src/services/utils/sync.ts b/src/services/utils/sync.ts deleted file mode 100644 index dfea6d6..0000000 --- a/src/services/utils/sync.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { sql } from "drizzle-orm" -import { SyncItem, SyncType } from "../../commons/types" -import { handleError } from "../../commons/utils/error" -import { dbSchema } from "../../db" -import { NewSync } from "../../db/schema" -import { getDB } from "../../types" - -/** A function wrapper utils to handle syncing status */ -export const syncWrapper = - ( - fn: () => Promise, - { - item, - type, - }: { - // TODO: Change to infer type from dbSchema.sync - type: SyncType - item: SyncItem - }, - ) => - async () => { - const db = getDB() - const start = await db - .insert(dbSchema.sync) - .values({ - item, - type, - }) - .returning({ id: dbSchema.sync.id, n: dbSchema.sync.n }) - - const initalPayload = start[0] - const startTime = performance.now() - - fn() - .then(async () => { - const payload: Partial = { - ...initalPayload, - status: "SUCCESS", - } - await db - .insert(dbSchema.sync) - .values(payload) - .onConflictDoUpdate({ - target: dbSchema.sync.id, - set: { - status: sql`excluded.status`, - }, - }) - }) - .catch(async (e) => { - const error = handleError(e) - - const payload: Partial = { - ...initalPayload, - status: "FAILED", - message: error, - } - - await db - .insert(dbSchema.sync) - .values(payload) - .onConflictDoUpdate({ - target: dbSchema.sync.id, - set: { - status: sql`excluded.status`, - message: sql`excluded.message`, - }, - }) - }) - .finally(async () => { - const endTime = performance.now() - const duration = Math.ceil(endTime - startTime) - - const payload: Partial = { - ...initalPayload, - endedAt: new Date().toISOString(), - duration, - } - - await db - .insert(dbSchema.sync) - .values(payload) - .onConflictDoUpdate({ - target: dbSchema.sync.id, - set: { - endedAt: sql`excluded.ended_at`, - duration: sql`excluded.duration`, - }, - }) - }) - - return { - id: initalPayload.id, - type, - item, - status: "PENDING", - } - } diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 79b7d92..0000000 --- a/src/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Container from "typedi" -import type { NeonHttpDatabase } from "drizzle-orm/neon-http" -import { type KVNamespace } from "@cloudflare/workers-types" - -export interface Env { - DB: NeonHttpDatabase - DATABASE_URL: string - REDIS_URL: string - SYNC_TOKEN: string - KV: KVNamespace -} -export const getEnv = () => Container.get("env") -export const getDB = () => - Container.get>( - "DrizzleDatabase", - ) - -export const getKV = () => Container.get("KV") From e9a36eaca8d043ea82f202d5c1932aa15db61699 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:39:29 +0700 Subject: [PATCH 34/64] fix: main file, run cmds --- package.json | 10 +--------- src/{app.ts => index.ts} | 0 src/modules/api.ts | 2 +- 3 files changed, 2 insertions(+), 10 deletions(-) rename src/{app.ts => index.ts} (100%) diff --git a/package.json b/package.json index b064faa..baf6aa4 100644 --- a/package.json +++ b/package.json @@ -7,19 +7,11 @@ }, "version": "1.0.50", "scripts": { - "app": "wrangler dev src/app.ts --port 3001", + "dev": "wrangler dev src/index.ts --port 3001", "docker:up": "docker-compose -p comuline-api up -d", "docker:down": "docker-compose down", "test": "echo \"Error: no test specified\" && exit 1", "deploy": "wrangler deploy", - "dev": "bun run --watch src/index.ts", - "start": "bun run src/index.ts", - "db:push": "drizzle-kit push:pg", - "db:studio": "drizzle-kit studio", - "db:generate": "drizzle-kit generate:pg", - "db:migrate": "bun run src/db/migrate.ts", - "db:pull": "drizzle-kit introspect:pg", - "db:check": "drizzle-kit check:pg", "migration:drop": "drizzle-kit drop", "migration:generate": "drizzle-kit generate", "migration:apply": "bun run src/db/migrate.ts", diff --git a/src/app.ts b/src/index.ts similarity index 100% rename from src/app.ts rename to src/index.ts diff --git a/src/modules/api.ts b/src/modules/api.ts index 267c019..9eda74e 100644 --- a/src/modules/api.ts +++ b/src/modules/api.ts @@ -1,4 +1,4 @@ import { OpenAPIHono } from "@hono/zod-openapi" -import { Environments } from "../app" +import { Environments } from ".." export const createAPI = () => new OpenAPIHono() From 9cae214f53920f5796a628d58536af910b2cefa0 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:41:30 +0700 Subject: [PATCH 35/64] chore: update, remove unused --- bun.lockb | Bin 118496 -> 93582 bytes package.json | 10 +--------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/bun.lockb b/bun.lockb index 4729462e361ac3a581501d6780a6a1317795ef57..a69dcfe370439fc9cba785f0c549e1d2e36addb0 100755 GIT binary patch delta 16873 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 delta 31386 zcmeIbXIK==);2snf-ne3QgRj)l$=2ZOdtYcKt?eRFeFJ56ikCT$5LA{qHc2zm~+lK zVFb51hiyjRySk@!j+=eH^L*FyzQ0ai+`Vems#=v+RaZ|pt0&*oyfkkJ5irzWp3uP8f@ z)+je8SwZQ5e0g48iAdC@u1KVZbnnpuQ_zCEtc*f$k?1~nT9NFGoQ&*@g2K$)lH}YR zktkh}mFJeN5S@pdHsBbjk`}lJ0osx+pp?(c$jPMz)j=dJpc;|1g)c!HfSv|z2)YN9 zwsa+^3Frb)+CzD1G`C2Uk4!XwURs_U4HxC5xzSE7=#5BP!Qw)NGKY3{Cq&SMt#P75 z;IE>{kdaduC`yAOUa?zVZf=&SGk98HgsOlb70t-a$B-3>4x{_1d^;#arE65Q4Acnx z1W;P>6qMhu2%VauNGhV0&PFZiF$f9pSDJ|vt!a8L-y?em@6IOz2r#4XoN{8+#;yGhe<~Ks=}w#vk((lM+WTBIwvO1*I9RLFtef zfzmG4QqdY)eoWt~XcrHjZw*R^=qci<4JCMrL>Q&glc3ZX+j@ya7~N7YP})On5wD~L zNDhI zyOaT?1y}iqM5uFVQBF!ma)wAW#-DFkNp4YcI`n7;AUSPW`vATIGeK$B$_v~IsnInH zB-M4K%9vojK6OE9eKQK&(z0@sM3N9b+ovEto`9zv@B%zlo0KodiYpTJY0jrx21@gP z0;TPf=agj23zO5axaVf|SBTDp^7TIiO8a9gDAFrS)8z%Z1JE;CVf@glj8`PsnqY&1 z%?TzYn2umlf^7>nE!ePNyMm4Kwkeokyr@*LJzBhSgr zz*KG+LGm@FN~SH}QF$3Dh4~7F=wl?`@GM+4^1Vf()=_*%EdZq*@~4VU2c@Q6)Q&g( zOiJWVh6u zgGfjVGHB0N#9dYJ?+Ra{;u54&6f2VDDELYoKbQYW?+M6tMLn>t$_vxo`YKBHa1po! zf>K4xLFpu(4@z4;9hA0gKzasex)}v13I$E=#G7<-XFk24??EK7(6Uj!i02!e(uHrw z22ca2i3O!ouww#m@Ebs>dcm-SaTLZ|FdSiA1p^Z7Kp1Dipi~B_EEOzEuvEc91;Y_6 zPB0L`@&pSO3`?-!e_9l8prIyczzI;=KwtEYM5KZ1LoX7nZKL$fx^}5`grvg3`_1a> zEgWKMPHL>(Y?z`QxAww-7uU@F++;Gn?lXh;-DuurX5Z_B&TLAn*p`&tbk(UBnq6Bs z@AKbv@nO=6f-Ph0Z+i4<+{^ya)|M6qMmKjKzpJ&ZWgGG9g-4=VPi!*qz?6|)ioSQU zZT(5sX`bl&t1l-V#16qzH)$w$8C+^&o83re6tU@m=J2wu^5Yv?&Y4{-*Eu}z#@WV$ z`+sfBER7hrRr+@068p~Ee9dni_IDTmHDURhgLR%SZD8@WV$SUu6&*)E^n6#Z|42KN z&(pU&sV@6&jKGGr#4jQc*## zLB7|g-1%@R!s$W#mn$zWs1ik;x03W2bam+Av6s&#D&LECt$7@tn{uyfGuIcpvfmX{{dGb#Vp`u1&DlR6oNsH_@OX~B{)I~w zTP^KxevY;2I!{o#ko%OA*e!CeU+Zs7Y_eZEr+;)ux?T1=@?UsIL@(7V{mBq=U zOSc@pvh~`Ow!1n_INzhdx3%UCb004q$&|Ou`oaId-$)%-#dUbxG zX**wh&(~pn0$8_bOD=()@`WOV?P3_&a+`%)cJXGmaG651FjA!HxAHz4=#dQg=R4J zbg&N?UJgRuNTET>?E@r>z%`>4BCr*rE@q?> zB;AZ88cD_L%t#r?D@1~XnG*|Rsd$7rNd!4#PRfj>n)NL(ptL{5oh^ukiBvLzMxb;U zy-}bx7;5v}G!)mdBo+;%;y_DM2{IR4L#d>$6>m2%YuG>sD^k`_DxPFTDnU+K5erkP z_?s0;1o5>dWgx4pNhQczYhqz06?e2DiDpvC6x_0Tq96>1VSwH~aN)EEaA*^ICw{@I zkNtgH#A+ZWK%8q!Dv|$$9kDQ%YBKgBQ8Kx08X{h3Ps$Kc?uZ{L$b9n<&4&m@av`@y zB2g3<>W>gF-Ssn0!%0XNjSw%LiV&aU212o1UT0?^E*GIVE^aSEytH0pK}#ou_`Guw z;`QD88RybO$k87mzJ7ZUistlby9jwZBE)N6@H6faLYzKvGgne(E0rvAbJp=d_R?4_C!SU$M5Slr)@ zSU5-}M-dUh4GLX^b=`Tx;1{0`;OOGRTgGH}V&N#&JdMcaq^DhoMB9Tm9B%0o_x2!_ zj<6UHV$nz{e&#_EK?*!c8OVK4QVEjmMJ$}8lC566HQ`c+Y54+bI>rE`2rIKBUr0X+7O;7Tdio2i{E~G6M|%imIRL6D#8mx4)|n^a7H4E3 zp`_A7Djph2EIg%>=a8jB#_v-)K|R%i%TvPu@nk8fM3PI0qy=#uq-lbEP)oWrixa{~ znU_>^G9obRyh1c@BZRqukSkUTJ~ZHG=UR)Q&&D)Z)AgN?5BI&k>y9SB(wj~z9 zQpp!Y(t*+D#ymEPA7u%*b6Nq8w+A{GU#d8)6|e_a>?^{NPm7zUb!*NBhna_c!g+*fZRs{8Ky2QgR7$0i`1X7xuXYAFS_og{ zd2qA>oJ`X>4&6d8sG5Bc!kDybu7N;%ZqMS6hSM2^JqX%S2#(f(Z`CSrbmsC!RD+|n zKoQtzJ9Jc+C4!?eZri0<297IGav7nPd>&|T*om(sKU$r@(UQ?;*sAXUM~#G=Vv=V+ zIoi9{orP5&oMs-lmR#L8A*5Pau);kBhvlKP9@i45cwQe@Td^#jB({-?$H$X05RERR z5+tt+v51gL_QSAYMymO*jedivU`@d;Pn?`UDkG$tyAUa*wygOXp+M5JU2_c?oi;rq zLp1v%6v>75AQVNcqnm4V6@<|UMRCFz2=U2o{*>Bv6Qmgk#S-hN=1ez|7!{@gHjc~I zK9LG&Idc)>6;va{D{b0cC@2phK5hp>P=#9b5HiFf6wN7_jS!Nx<%CET)12u^5@W(} zwuqHVcH){s`vU7Y*7$n8NLj2@9M_9ff{f@zEZPSv%|zU9^ElT|^%3vPNqNd3j6DW- zs8dbEGy|yIjg!_+#gO6B%^jc?LV(NYsS;2sCOs2Ol~f&81QE>!OXiOM4O$PHur70@ z!lJpO7OMOw0sS3KgjIq&D5Vv}S|FnRfLX%vuzK$JpHQj?md_oXMSS^c5!?@W_-&6r zD5V)N%-r$sC{5Q(mG0kB+S7diZ6IA$z9=0GJt(D$GAM)N-%+ZlFJK9b257!9KpkMb zik|>V4@$Lw>6F1CP^A!{lqw)pisRo=TJTJO7Bm~6hdQP5GA{OCsRrT}093w+r&7^k z1Zcra0D4eL6)&R<4oXdcos_{rDV6U5Oi66wKF)-%sA8{!Qf)T?dQeLFO3L6+r!@X1 zKRVPanjnUn9T2P%2~_@RjPkS zskV=*bd=Ig`vTBfd{fcyp!A@WrvE`19RG%Ta|#%m8V9A+kR_lrN=Fs1P8pIN-Hx>VHDA#TONkXXmj}-n7)A8lk zS^+vOyi_gtPpA&khp5umMrlumLS7fN6)5e|2-^QNPb595C>69*@&As}B{vS~sN#;G zG)pHH?W~ILLL+fdN`nb1o>CfA%2W}QQbpZVw7V)^ozk^GMHR13X_P`0Pbm$is`&p8 zs^k*V7H6n3W~wq$N`rk>{C`5}kmVvhZAd>={(Mz_N@;wdiuUK?G5$0_t#u+7@&A}o z`}^N0n6_-Ps%29_X3d-=kWF zer?@L(o=$zXzr~bAn$;t9J&KpEeOpm*!C4VAA?SVi(z#=| z=*-k8n(}RRhK0+Y?G8GGm7AY({*qQ8JCWOSn1}2dw|V+r|2t?MGN+e}wC$~@>>DP@ z8f_C$ljI({_M-0)y(to`{>mmJC?T(k+v^LBR-uuJt*gee? z^USyHQW}PhNpJLYsC95iu;}Oxy>DagH)_!93yGDRDMz=Ky@^ejIIZXFU3D5~P5q%c z>&Pk>qy0?hE_bJ%6MYGlzMCFyQI>sedDAQDeI75i%pKpT^AcBfQTr*+)-C_0Q><3I zdP3DeHP+3)xTO8bem9o6Y+OHVNxv2jeP3*x8PzQ)`hx4_-YfsulKv`gcFCeQ=0v~M zfJ1QsRU@8kf0UFxbNkofy)tu>J`e9nYEZj=7ZxZ#_U)SJT^Jg*;q=P}Km0C5-C(Q8 zSLb;JsfYVLDp6ieCKJCWUWkve+#@w!{2}cA&^~dyq9z(n_Ow5*`C6@Zdg`^Cqbt4f z*&s{&uGQ8;uSwb~Z(Q?zM7Dfyda?76a~tsM6+3)+*^3R;<`c3-Nt>88GNrAx?)67L zPuz+wJCB+cX?cOHO)^t92pqojR`x;XZdMW9`)xViZ}#Cfox}@ot@=Lo$h4p#k6wG7 z4oyopbqlD_e|ECk=Tp75{vjJHPv?5rm%px)^lTMde2_peHh|@cEXHJ<+~hLYnmTz+P2=ylYQ3iak+A~*?^pn-k&yf zd)!6xG@bNJHdBgLB_?%z^!AQXm(649dCYxOJ%0H6;=Q}-y#4TM?e?k8b3+C%x->ng z&U*JH6WU#N_S{1J&lJCXea>R{1lRQYi3_@_)y`18c4JO-czbo8V%&v0?>e;Co;&4P zd)@q?A5RJ&*Q*FUS-tPgvVpg4GNs1m2jriw9@=xT-=T5!G^X^;LM$t9*yS>Cg|CLxzXXbPcXG@CH#Gl$U^a+=jZy7Y=xQf%X7c+`C9Ssn`Cq! zbH<7tke!rwZr*}B;?vT;-S4-&rhT?!qqCd;yz{*C`sdoQFW>Ju_C@=o(YZh07ui0Q zhcho9sMXF?y>_!s^s-cb==-*9x9Hw~>G$byp=sSU+Pk{PyZC-Aim6mieH&fBZ{HqP z<||g5n7H+?F8;w)8GgmBW^e2`Jw5GU)3d#%66;hmWyj;U8a-ILU~`J;l2g6UJk2Wz zZc^N!$&CijQk(akJ6o@N&4~+BWg;!lRbvWg-?-QOIoTyei zbM@L?sE$frA#0ZQ*NzLX2VZ+pxBUBvJ%>A&mRuTlqj*)p)uV^UGoLhluwwk0z7`%zt=ybJp11eCTc*}`)lHbnY^HW&ry*^Kei;6 z?RJ>Fc`9*BH&eO}{ObFx-L0Z&efmZ&D!V`Jj|Iz?eycEQV5aMKalS>TGX<-{eR|F; z-aDnY$t~|(_STBE5iY~J9+h<{EWP5|Kr~pbcDCxZyI$jRaazRb!W{-Dss@-iyjUFj z#nFD?qry&x+xvI))7{{-d!NL{JIXv(d9YoMq_<|F%epj+*mw2j%*&m1xkXGrJj%s; zNiP;e$J(o_U3}VkaZF0)kM5#?6P1mh?Rj(g!195fI|tp4SbKh)LAOcOb@cniTKLTG ze{K5Fthb{lcQ~0ks>_H^AJR*2J(!spl};vQm?;Om@v3y1e@#2-k1b!`bxMsH(e6-0 zUQl?qx%);>KD%9a5PNsrvO}j%c%&X-mL&H3W7kc8hnRVtY}^yGqbE<8^L(gUn;q3F z_h{R*HOXOtjZUrDj5o~7&Lp8;X)D;g(Jyb_AE-uDvic$(Uadz(@81Ma&)TL)mHDb*{F`sf)2fyQe8gVvuEhlR#(;ZI&rVesFZ7LE#2Cx zYo8OAV}>U-s2TPAre4X;f-7TAF5LBHc=1WKHJ96 zA*EkO@zd|=r#en;d1im#(DYk6(X0D>lYH_kjkB!x;*47Dgr|_Sej0hj=cEUud(D1S z6t_I}&B5FH^xe>^@1Z593r#I%uwK z->A;9XUD23Zld0umAdu!xl~?i&I~YGzCbT)=d0T>d$Zp3^S)6w=51D;&f=c>Z3Z8j z# zIQF5MV%MKq(Gy3KD@%WT(puEVv)f+$LyFXuDjX(hlTOpPgOe*S@9*GM z|IEJGPII^Hsynfh#ef*?P0BIFXSeJ(RI9P@Oq{Fnrh5-3ZtXB)Ut`a;Gtwp(k7V`+ zYVNhJbK|LUZ_N42y{q4UdEn&Htnm_~cKQ=`T$!`?jOUUyE`|Q<#=Uo!huu6`qo!B* zjhWMX%{PUG{K4nZ06d(&Afl%@!T%W$^I$x9|qhD`HHCJfCa#N5c3; zrtM`O{Xe|_YOcS<|8Dr9h1Kyy{ie`My(bc8_3iWr_K>|=IaL%)Exj5NU9x{ofFdkQ zI&*yD5xvYEUuS9?cHT9A&BfbZR@OBIR@*=5e?K{TPxRGi>CK-sxUpTW#@?h~ij46g zEBnZl^wGa?XiRSQ+%Yi4p=m$uv~!W(X;;`Dix0ZpnOD=!`pPBQ!>b?Ca`Gk)m;dpo z-cj>QvDaJHM5MkqJdm!_cBAcxmtPLm1gn+dCnyEg$hVn%V0l^Sm?nGHHFB9>vT;-Y zk;{I>e{Y@=s$68eeQV?DPu?e6ZZQ@eJ8^o1^yc(qAr4~?+KpOgWLTZ;sA+p*_aZgL z{_2XY#OrF_Y_pkmBqUt)M&5jNy0p|NG3`>_7aw&WOLcb8{P@uDEYS#t#4ZE56wab%*<9AAfMo_ds&X+wnu9 zUyVHE;&0Wz=b9G>!^@`sad*#y=%9~n28#BcSQ)N$eWLjf27e0^|KYDRrDILV~{__4<0S`Mh}A9>s1aaO{ehq(_Q z8SOe1|J)|BXRE(Tb6vX3HS=k#T(G)g-8n~@N6TX?V+@9_ z6U}J%{qZJ)VKv>lo0JWIefCkF*-X!rFN4p&KG5~ewYJQpusXT+d6t7V8+EWQ2=BhA zVaCA6Y84Jq*L!v4+gnv$N=c2$o;J!yI$d3AX7B%~zjE-O`xj52U0!XxET-jIc}Z1R z!7;Y!{WG&VzbXq9Wq*F2GpDX1Yj>VS+DA3T&D9m>n{D^)uSi|i>e|sQ)!rK>~qEGgH`ieD>E103ZFC~AfwaCg578O2IlrEx?JLVZ1B1_tu@be+N51yeQOu0 zu6R}RxMl+ete^U5Rz+@#F^_uwsiS$GQCMk?5?`$*Vf$HcWU_K%tm*Z zeUtBqzcF8;EGifF{;;4@Hd{{I3d~}x<27!HEWgid^X9#1vFpmc^^4* zsn6%s5vP7#ePdw5Kjr-k+2Q@7s{>jYhraxtm|-lRa_aLUvtC54)nTM+h>Scg(8Ik} zbZiUts`0)`*;bwvXV+V(Rkam)Lx0~Y z(o;UzR8_TM&5eWBW(#$F zf8GX(sdDEqe=X*4nEAW6-bZ3e>PX{H#a!Bcef`Vqxpv)m9l7t2NM00~DFa5kMGem! z-XN#C@zNVJJ@W3p&n*9In!!u|HMdUee>3ud%ZQEhcUM%H=G}{4GtA$p@#vb>KNkBo z?AG|X>x6etju)uaE>gXAq>rIh@Wr#sr!txMcE?UCz5aDM>*pRh%CSq**{G!pn^*Moc#@skWsRoai$k_;yWcYn|JY;OzPeBMk@@}2lp_|ebIBOh^Ng)sSi`PE z9LL|WJI@STG`rRlb?`D^OWeUr09Q=Mq+S@bhvS+gdjrq4_{dbvn z5A(k9<7KPJZCZaUD?k6S+`V{`Z>V+raR(ARz?{rl&@MJwU2W$*Jx=&9+iBgh_4$QM zQ-doe?Pe{{?0>V};`tB5uJ@Grsb_0EW&{^yUal!|@2T%&X7k9!dh7Ddi(f1A+MQeN zF@V$zFpK$f=j$a~+mDkojm#p;mZT~R&uQIIyw%d!WO==@opxipIoID5zdq2u!tPk3 z>)$?g*UMk*oKe^LNH-k~JH_$3`wptLIhI8AmXYnndSq?yXeN$)2A5u&W;zl^ zE+b|G^@u_q&2%Ok!5s(JASs&ZLgYy@QanhH904aIhRHJ0WUwA7N{+_MhX=r22j`R$ z&2%RxQezOaGt6mN--Y)gGv1B;lJclavt;)M zA8x;!@ndtvVV4N4j@P>v-Bs95JEA(qrQBeoTt_a8R{csJ4>$J zbH_USo*(X&bZ}|lI(h5d!hJ{P_#K-ldgiM1C~On9Oj+FTFY}WrR%Sax8uyzR`gx1& z`@^f1Z*8h(9xWTGNn(eaks8#sFNw;O5$zG^+stStn|#icF*&4TUm252R^psTm@FC7 zk95O1pKQdrfJm}sOd*luTts%@+@Bcc$d~~n1LtCL0Ot~7o-1Ppl72W3A}4SjOziVy z%n(w7^H6dDXC-mzCu2&0~3$ zgh+~I%nTyOc_!I`^DJUmB4b#Rf%9x~0OvWxe4vaeBmHomOHSZCkJt~AG4n~uAlSwX zSjwPiW+8DIjFlYRq`}clIk^FD_DnqzIwYD|LdId0Z#xTh9~#a4L7ohiG0R8`yiT*6 z%)xmDd4uyx5>+ZAU1y{JOQV_9?1e8Jq8y#Hkvs=#*IbW7U_`};0}?XacEmP z+BPnlIYOR*`vfj_d^B^6%oz{sU#v%Jz?~pb6VSdTXy1fr<`nrnLB^aW9Vg0|Gh`*s zX9+V&#+)PFa6V5q;(UQfCd-(MM2_<%vIFPK#Bhp?xk55zKb&un z6F65A`)M-fCMm)B7P)})ZQ?Q=tyzWEOpj*nk{i=y%st{oWK0zqhjTTl!udW4njvHU zB-3zyK%U_IkhGX7V;+$?I6o$DaDGCfX33bRq#WmG-3ydW!aeo2_wGUgTO zhVyH(5$88VGDpU|C32kKksWg|G8-{6Wzo!EB%=%?vk4;u?jtdui;>xkk(nFKd?qL6 z%9t<2ex8i^N=k74MlRr7LtN&|nD1m5&OgYF`7*JHA>IoT#Q1;J;DrfdF+-+<)M3a| z5DkX3T$CWz#Q(B_)Mdz95G{tZD^Cz>Gh{J{gdtx*bQscUae}xWLso(4GDN&2L9EA+ zL=b(3Yyzpz5S^t7VgrUGffzDmCx{V4jQ&Uv8#5#m#DpOSK^ic`Vp)Q?Aw%*(Oc`<# z#Ec;h%M--r3>gSw!H|m}mJD%Sks!8W$Z!yAhE#&sphYVa#I_6>4`RoVY7l$0Z&iZW z0qp~EMEgJ*p?#|p#7<}*h%?#;(irVqlOS$__JO#deITxA-`WImQ?w7H8QQllLF|V1 zfw-f6ARcJn`UJ5j+6Ur=_JMe#eH#+QK4>3^FWLv5X?B z$gqmeREv(_vSW@9@?gXfH+{FZ<}}>f=x!cX(U*%MgZF5Q3qr}zicFz^?|V)%nwPOr z(}$`wn$12hUr|_?p~#QH2FU=eqR&^`s#-V$aYjf<|5;E&PjD+S5Mq6lGD1uiRdnF$ zv#=B1G)3l>oi;Bx$n2ovf@Nv;-ha8t_A49|3ruzU4%z-G5jm4G(CL^OWzcv2mb>KufGo$ zhx<=+1ZY?b2v;TK_s;yAwbe8y4og)!`eL|@XT&tPal^l1m(Z%zT|`;=KgHjo2&0p5TQ;0yQx{s65IEuW4MEsDPQ zy&PBxtO8a8YXI8o>wxw6-&@fJ1U3SjfX%=bU@K4oYy-9fOMs<7888>1Lqvc+fE-8y zl7SRJ0dxl9$^1{2%9aSV0$KxYfCwNGpax5~(6syL?*{Z9pWcqsJ7;>AOK(W&y`vNe z1Hu6_fIeoi0O$h^D}X*aumS7=2fz_<0>%S-P{}%9yD6#h&>dV)#L;&~deAV?3+N3{ zLy`kYKr#>tP&;Y?v;;zc<^X*Y#~tthJOLxX7%&0eK=)hV9q=Cb3t$jmhrti28VG0t zbpb8lE`(J;HE;!fJi-4KHEPw@KfLNeC5C?Ps=uHIu zQH}oo)tLT}+XMj@z!hi;(AzBf(rqe`2BZV@EnykZ6`-#&M+31yD?ksRZ<5yobbwFL z@fr94yat{Dmw}1EG+;I`2j~cRqkQglQa4a{pcz2l?4@rF(>I9|0s2mLTOa|TGbsYl z2k5%+1?j&6&w&@f72qmx4VVtpM;iL}{WstSK-U>l#L=~aE)6;sIMKO}OI^;_mdbmO zj{))#MSnM?)A=L##lR$BGB5?03iO9;08k8+00V(R0DV3*1fb7M=zHO%z%XDqFaj6} zi~>djV*vV_{WxGeBeD}sK%fIamxp+OzVKZdh7*p%ZRpZqucS6dr!j2+Z3s09Y8sh9 zIzVR|9qt}LcYqE*H3K@w=viJ)@NBF#~A+CIAZI%4iJg3^)Of zKqH_oZIUpX>FVGQ_yN9v4?sCpjUy1G%zrZwpV(2^5@Zh#C( z0J;G2Kv#gSJ9Iqy0KI`;KoTGa6hI0<<+MbBrmFZf+Q6{tOYgz>wxvZ3}7Qbdxjo|5#A5% z1NH#BfStg0fcD@HU^lQApt1wNLEsQT(^2_x;23aJ8>bV%$(aWjV)kAR231K>}9E^u^lq#d;nene*tfS*T5U#J@8J&{{a00d{Xg3`p@8Lny)|&@D2D5P{2(rCT3gzz6WA4dK zIG2<7xqJEg2_7T`Crglc`Mdif({i?>m@!vUORtBlJq<4GuJO6wSu92bcL?4x1xHU} zahAMLQBcTRyCL!juArdQ+uh6E6Bikk$4$Xm6eK9i9li?%C!d0UDoFg?eca=`se@6z zK*47gM1jcS?jsUzw0L)1k+XP75K5v}POB|=B!k4u6Z+VBV#Z1-IDr-XmqF_34!NgD za1-n1fB#uzYvln*eB8Z#+yg~hFhUY&6kLEo;_dF~9)RXTq63NGBn%Q?cYjn*bWD{; za3jWP^K@6DC(c7;j7);3F;1PAyBGRM<&ip za`$%k|K&DT@OVbcQSyWF9#t?!6$IyKTqdp{!8dJ$%B>n#5GN7b;0hkxAo1lZCpgL# ze7w>bWu6MbsQwHAgXR1rh)+^DkpHe zxICPM^SzsoE11e{93=k8gMkpd^YV`6l&=?ZnNSH>m&$>!;Cl`d*aGU{DY*I-ywpJg zqoa8Q=f8sAIx#cU58Vxk;O$rNRHw>=VHNxV!AD)_PE@^CXra2qUmy!%y<;7C~TfhT5A1a}B|l>d^68>4?7SHb_V;5rWlsYd*t zhe&W4EO@&^9v|2o>Tw@7XNU#0RNWr`$sGG53++_F6S3e#59;W+LL&Go7ToM{1#uF= zo3Y@CPgQN$LbU|tQ_~WB_Ce&ykDlNPS@7Zqi8n?Z_5Pa;(-sBLDhTeB1y6un9hXAEhrpu7V?I!G|H=R%)$+qhrp^A-o)7f~h)4a3(D{LWD%s zL4wm{!BwJ4;*0IE9zVYXr;19h8@RT@SvBXYTJW<7iORZvzv>vEMS&xa~) zsJGx?nmR|Mz7c3%I$z~cbHp+i# z)4w$7_pN~ea~{_^Y9E5DY{APTO2Pt7>nAwV7W_bR5=?XK%uM)2Q1BE930)qQ=y1U! zx8O_?B5Dng2tK<7caxk1J%O7jsH0U898!uIWWaL&%kQ_~vXbVZ{oo_&i#+raztE|) zS<_L9Bx>HacHAQUk5ynVh#5;0diy{(B2%2p2aj9Oai;+@q2a7L%vJHFhV1n^jJNn! zLsnOVF|&HqkYAN78aNpabL@PY7U300r`@}TY&cT*(~Gq}R5^4oeP6Rz+{=VeFqCWEOIlVPPdrn06UnznhF@U6hd^1g_v!f={a_PR( zQxs*&-l8?@judErX`41}Ck{5zNas_~X`gG#8fZfMNTg_h6b7-q**BdQg#6MzM;O&1M|{+jT?3YO%TY~qOHVU)gB=S0`wAvBda)VnUYGH< z5}ct=KHuK2rBljseigw$V-;Zs*M*K+)(qADqmv%Hfu{Iu z(z4_2*@e}Ay#Irl1((8$-eefrv9FQB-x?{Xnbzx{ro3|ZdsC#qRf@KzDJ0YgSL9yZ zb)lc0ok}}~D%g&d>L71CQdl9yr?Wb)bJ|y2rE`;8Bt?0UI73oxusC||szXDNmtM4S zJ)dRAPSjyqH`)ly~mzo4?w7 zPml&(0fOAUyhTq`d5>##U?U!9AN(b$v1fbKL;D3+$!1@^cy&7IulSXsu>(65Df|WJ z%APIDdT*;+AJ6Or?SlJdJ=4}t7Od-k?3YC0z}}&G1&7TUJDRWXt(*PuR|>(U^UV>d z5ni(@5B!oWbYPq5qGo~<=+zM!J@dmF&-<0)xC7e%k-HUbcT7>XY8y5ANwmt6*g$LvA7eGd&63h z#m|$?st<*NcYgcbCXeDChAl#hKz^3A^<)nF-#k=D?5|a#L+1kSdr_p|`oHAO z*6TOJb`RxeNf0DnUhGTc6}RjD_x>djz6c;{l`+S{Z&;IG65*Qx8{u~Jaa5f}DhcjuO2_!K zx`yaU%>Xvh5DRJh0Cut=Izad;K)`_s6|-7Z2}=!@3Dr^~ZWhSiqA3+<8NK+X7p(j7 zrT*#+6MjOYCvt;W10(2u5ybW~LbE>wvBgFxqi!&}2J!yFCkDFodH&q&k9oPjlui*SXso^(C zeOr?czw!$1@=3^Z$IMA(V}D7otURYR6a3pRu=Lz{=}B_puM}9$?HWRRtxGajeyz(X zCwb?<<{+;a%j(#MOquvWB-_XoR`@cK%{Rq}eT-z6QOdMsD-mD26>xWz!iNu@7d9Cf zG{GqrR-n36ZDH6RW@y1b?0sQ{!BOl~GnCRgiv4De+Kh=}?;wYGdekgSWE4K`VC_)l zxL@M1gELO<4&l!zwiz;537>a(o|4$*cJuwMk%Ie)%u`gY+_hX3p7bpAMvpmHpK!> zHjiazQ)2Yj(bfmxVt%xRX?z}26eZ37SusNpdfbcbmKV~g>u4&h&1th9sx5TmY=pYE+pE#xc z#JOGjlc$k_Zl|z9oQPx3TVjgB;|bH(1;% z&99GtKHq7v->wEXJ1C92UwdND>e`^R8f>O;Q&ZYWdcLWZwc&DZ_TeJuDZ;uCjuig2 z&QC5yt(z!L66e59rg>{!Q@9khu9Tc)f&+Vt)~wdmj!Uu0fix%d z_p9!;ZlbunSQmQHytS@aT#8yZQJloXkzIqlR<*8aT#5uo_8!$Pd^F_vhf>?YOS_O? zHLGSq9Uas|XdpM)XTNXYg zQ%HaQcjI1MDq;IMFqZ842u6?1bYL8e`Cs@w+&uVz8)gA}pcmu7u5)Iz*%=Our&299 z(y4K7+%o9pUYIs8IX4R*A1f?Wq@d7LR7YNzDlaH>OU}(J;Q|HnRD~NptCNwFmXTVL zo8zV^&M0)tQpo$drQ+K=oK|@f63CMa-Eht+$ji;gS9#6F3Q1|a*}d$v#IVpUXq-folQS7#|}$jteX5i9<8V?^>;br59nLsK(iW>V z7zuxiaie#<-ds(R&_n3qoWlIvEPP{(wkapAcZwo8H-+_V!&s~On~zcC(?ZpfDDvI7 zU+JkTIy!9AHjIN6H%|QX0h*qU8qc};xwBPajDwl#IRK=p5VaZX2-ySSj2&yxgVEG2 zfRKNZ;O4{ji((vYe`-`I;~qEAl9S{G3V&aAe-p;WMD@!*GOI$UAXlb2p9-T3+0QVf ze(amBjJ;I;TTdX>?@<^)tj4rNu3~P5^87SKp<6+EMmAd&!&o`f&l0M~3QbUtbMs?c zG+`Va=wlzqDN)m#lj4?}sxnk=n$@Sz5s;1&R2HoaXYA|qj~x)r2iT1vjFT;XT>X`q z7Ol?1jLIDtN56lVB(x@Ug8zN`pio#K3N_|`Tt-Sh03-6|1dabT6Ora`aceY2QV5UHP=L34AbY@xv8~TPbc2u&uyi^ZtDeyzRuw`Q(kbCm%c6ipwZyXL+oI&3 zi;0kkE-WxE3`Zl2pGyvezt$hL(Vs1kXYAQdJ(v(~6@#4XDcl|6*(WsyPA|Tih*gJ^ z6cnbo6)5sETfPE9d`XR8 z=7br{uXcs_#F|77#oQXknT9=$`x%9L{R{7g2ZGeH9c+)f9p}2ni}hG(QeokvlkjO_SR}6 zg0yNJW*R?(tF<@~KmawfFGo}oGC1KY!#v1F5|0ZJHn=t;Yr4u7N>%1RQMCyN@PT=T%oUYt=H&cQ_LL3(tCC>MK<*DnO_T;R=E+G###R zK}N6C@|=`{gog;}{N@`7~t6h*4MC=1_RmjHG9mEl)3x`JOet<_Z{u{x_V za!d7^{7k}EjLqxHI5quy<&o^~kr*_-P^aG-|0}i79m4AEXht~qmXH?r6C$4n0LXhtFC{-u={bNOiJhe4~4Qs+U(M?lpc1Q$cz_v_a92~-b?+|F9 z?LgO{Zg>dj#cphbb)dN;qrI)hf%(L+Ufzrqo9M(;Y-{Jt{K>E>SRJ;RyD)w0uphfJ zmVV*C-#P`YG>Aei5rONQI%H=3h==RpD3--_a-1`}cjK8OYJ>q_TTWK>U!em>c V7EF1AZ8sAbKh15EdN7RP{{f_ixZMB% diff --git a/package.json b/package.json index baf6aa4..c63e7a4 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/comuline", "email": "support@comuline.com" }, - "version": "1.0.50", + "version": "2.0", "scripts": { "dev": "wrangler dev src/index.ts --port 3001", "docker:up": "docker-compose -p comuline-api up -d", @@ -20,7 +20,6 @@ "prepare": "husky" }, "dependencies": { - "@elysiajs/swagger": "^0.8.5", "@hono/zod-openapi": "^0.16.0", "@neondatabase/serverless": "^0.9.5", "@scalar/hono-api-reference": "^0.5.145", @@ -28,15 +27,8 @@ "dotenv": "^16.4.5", "drizzle-orm": "^0.33.0", "drizzle-zod": "^0.5.1", - "elysia": "latest", - "elysia-rate-limit": "^2.1.0", "hono": "^4.5.11", - "ioredis": "^5.3.2", - "pg": "^8.11.3", - "pino": "^8.19.0", - "pino-pretty": "^10.3.1", "postgres": "^3.4.4", - "typedi": "^0.10.0", "zod": "^3.23.8" }, "devDependencies": { From 78b1c243426a004684bba1de54b38116a5e9fa25 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:41:52 +0700 Subject: [PATCH 36/64] fix: rename schema --- drizzle.config.ts | 2 +- src/db/schema-new/index.ts | 144 --------------- src/db/schema/index.ts | 171 ++++++++++++------ src/modules/v1/database.ts | 2 +- src/modules/v1/route/route.controller.ts | 2 +- .../v1/schedule/schedule.controller.ts | 2 +- src/modules/v1/schedule/schedule.schema.ts | 2 +- src/modules/v1/station/station.controller.ts | 2 +- src/modules/v1/station/station.schema.ts | 2 +- src/sync.ts | 2 +- 10 files changed, 126 insertions(+), 205 deletions(-) delete mode 100644 src/db/schema-new/index.ts diff --git a/drizzle.config.ts b/drizzle.config.ts index 97794f9..6651542 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -3,5 +3,5 @@ import type { Config } from "drizzle-kit" export default { out: "./drizzle/migrations", dialect: "postgresql", - schema: "./src/db/schema-new", + schema: "./src/db/schema", } satisfies Config diff --git a/src/db/schema-new/index.ts b/src/db/schema-new/index.ts deleted file mode 100644 index 0ea98cc..0000000 --- a/src/db/schema-new/index.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { relations } from "drizzle-orm" -import { - index, - jsonb, - pgEnum, - pgTable, - text, - time, - timestamp, - uniqueIndex, -} from "drizzle-orm/pg-core" -import { createSelectSchema } from "drizzle-zod" -import { z } from "zod" - -/** Station Metadata */ -const stationMetadata = z.object({ - /** Comuline metadata */ - has_schedule: z.boolean().nullable(), - /** Origin metadata */ - origin: z.object({ - /** KRL */ - daop: z.number().nullable(), - fg_enable: z.number().nullable(), - }), -}) - -export type StationMetadata = z.infer - -export const stationTypeEnum = pgEnum("station_type", [ - "KRL", - "MRT", - "LRT", - "LOCAL", -]) - -export const stationTable = pgTable( - "station", - { - uid: text("uid").primaryKey().unique().notNull(), - id: text("id").unique().notNull(), - name: text("name").notNull(), - type: stationTypeEnum("type").notNull(), - metadata: jsonb("metadata").$type(), - created_at: timestamp("created_at", { - withTimezone: true, - mode: "string", - }).defaultNow(), - updated_at: timestamp("updated_at", { - withTimezone: true, - mode: "string", - }).defaultNow(), - }, - (table) => { - return { - station_uidx: uniqueIndex("station_uidx").on(table.uid), - station_idx: index("station_idx").on(table.id), - type_idx: index("station_type_idx").on(table.type), - } - }, -) - -export const stationScheduleMetadata = z.object({ - /** Origin metadata */ - origin: z.object({ - color: z.string().nullable(), - }), -}) - -export type StationScheduleMetadata = z.infer - -export const scheduleTable = pgTable( - "schedule", - { - id: text("id").primaryKey().unique().notNull(), - station_id: text("station_id") - .notNull() - .references(() => stationTable.id, { - onDelete: "cascade", - }), - station_origin_id: text("station_origin_id").references( - () => stationTable.id, - { - onDelete: "set null", - }, - ), - station_destination_id: text("station_destination_id").references( - () => stationTable.id, - { - onDelete: "set null", - }, - ), - train_id: text("train_id").notNull(), - line: text("line").notNull(), - route: text("route").notNull(), - time_departure: time("time_departure").notNull(), - time_at_destination: time("time_at_destination").notNull(), - metadata: jsonb("metadata").$type(), - created_at: timestamp("created_at", { - mode: "string", - withTimezone: true, - }).defaultNow(), - updated_at: timestamp("updated_at", { - withTimezone: true, - mode: "string", - }).defaultNow(), - }, - (table) => { - return { - schedule_idx: uniqueIndex("schedule_idx").on(table.id), - schedule_station_idx: index("schedule_station_idx").on(table.station_id), - } - }, -) - -export const scheduleTableRelations = relations(scheduleTable, ({ one }) => ({ - station: one(stationTable, { - fields: [scheduleTable.station_id], - references: [stationTable.id], - }), - station_origin: one(stationTable, { - fields: [scheduleTable.station_origin_id], - references: [stationTable.id], - }), - station_destination: one(stationTable, { - fields: [scheduleTable.station_destination_id], - references: [stationTable.id], - }), -})) - -export const stationSchema = createSelectSchema(stationTable) - -export type NewStation = typeof stationTable.$inferInsert - -export type Station = z.infer - -export type StationType = Station["type"] - -export const scheduleSchema = createSelectSchema(scheduleTable, { - metadata: stationScheduleMetadata.nullable(), -}) - -export type Schedule = z.infer - -export type NewSchedule = typeof scheduleTable.$inferInsert diff --git a/src/db/schema/index.ts b/src/db/schema/index.ts index 9b58af1..0ea98cc 100644 --- a/src/db/schema/index.ts +++ b/src/db/schema/index.ts @@ -1,79 +1,144 @@ -import { sql } from "drizzle-orm" +import { relations } from "drizzle-orm" import { - bigint, - bigserial, - boolean, index, - integer, + jsonb, pgEnum, pgTable, text, time, - uuid, + timestamp, + uniqueIndex, } from "drizzle-orm/pg-core" import { createSelectSchema } from "drizzle-zod" +import { z } from "zod" -export const schedule = pgTable( - "schedule", +/** Station Metadata */ +const stationMetadata = z.object({ + /** Comuline metadata */ + has_schedule: z.boolean().nullable(), + /** Origin metadata */ + origin: z.object({ + /** KRL */ + daop: z.number().nullable(), + fg_enable: z.number().nullable(), + }), +}) + +export type StationMetadata = z.infer + +export const stationTypeEnum = pgEnum("station_type", [ + "KRL", + "MRT", + "LRT", + "LOCAL", +]) + +export const stationTable = pgTable( + "station", { - id: text("id").primaryKey().unique(), - stationId: text("station_id").default(sql`NULL`), - trainId: text("train_id").default(sql`NULL`), - line: text("line").default(sql`NULL`), - route: text("route").default(sql`NULL`), - color: text("color").default(sql`NULL`), - destination: text("destination").default(sql`NULL`), - timeEstimated: time("time_estimated").default(sql`NULL`), - destinationTime: time("destination_time").default(sql`NULL`), - updatedAt: text("updated_at").default(sql`(CURRENT_TIMESTAMP)`), + uid: text("uid").primaryKey().unique().notNull(), + id: text("id").unique().notNull(), + name: text("name").notNull(), + type: stationTypeEnum("type").notNull(), + metadata: jsonb("metadata").$type(), + created_at: timestamp("created_at", { + withTimezone: true, + mode: "string", + }).defaultNow(), + updated_at: timestamp("updated_at", { + withTimezone: true, + mode: "string", + }).defaultNow(), }, (table) => { return { - stationIdx: index("station_idx").on(table.stationId), - timeEstimatedIdx: index("time_estimated_idx").on(table.timeEstimated), + station_uidx: uniqueIndex("station_uidx").on(table.uid), + station_idx: index("station_idx").on(table.id), + type_idx: index("station_type_idx").on(table.type), } }, ) -export type Schedule = typeof schedule.$inferSelect - -export const station = pgTable("station", { - id: text("id").primaryKey().unique(), - name: text("name").default(sql`NULL`), - daop: integer("daop").default(sql`NULL`), - fgEnable: integer("fg_enable").default(sql`NULL`), - haveSchedule: boolean("have_schedule").default(sql`true`), - updatedAt: text("updated_at").default(sql`(CURRENT_TIMESTAMP)`), +export const stationScheduleMetadata = z.object({ + /** Origin metadata */ + origin: z.object({ + color: z.string().nullable(), + }), }) -export type Station = typeof station.$inferSelect +export type StationScheduleMetadata = z.infer -export const stationSchema = createSelectSchema(station) +export const scheduleTable = pgTable( + "schedule", + { + id: text("id").primaryKey().unique().notNull(), + station_id: text("station_id") + .notNull() + .references(() => stationTable.id, { + onDelete: "cascade", + }), + station_origin_id: text("station_origin_id").references( + () => stationTable.id, + { + onDelete: "set null", + }, + ), + station_destination_id: text("station_destination_id").references( + () => stationTable.id, + { + onDelete: "set null", + }, + ), + train_id: text("train_id").notNull(), + line: text("line").notNull(), + route: text("route").notNull(), + time_departure: time("time_departure").notNull(), + time_at_destination: time("time_at_destination").notNull(), + metadata: jsonb("metadata").$type(), + created_at: timestamp("created_at", { + mode: "string", + withTimezone: true, + }).defaultNow(), + updated_at: timestamp("updated_at", { + withTimezone: true, + mode: "string", + }).defaultNow(), + }, + (table) => { + return { + schedule_idx: uniqueIndex("schedule_idx").on(table.id), + schedule_station_idx: index("schedule_station_idx").on(table.station_id), + } + }, +) -export type NewStation = typeof station.$inferInsert +export const scheduleTableRelations = relations(scheduleTable, ({ one }) => ({ + station: one(stationTable, { + fields: [scheduleTable.station_id], + references: [stationTable.id], + }), + station_origin: one(stationTable, { + fields: [scheduleTable.station_origin_id], + references: [stationTable.id], + }), + station_destination: one(stationTable, { + fields: [scheduleTable.station_destination_id], + references: [stationTable.id], + }), +})) -export const syncFromEnum = pgEnum("sync_from", ["cron", "manual"]) -export const syncStatusEnum = pgEnum("sync_status", [ - "PENDING", - "SUCCESS", - "FAILED", -]) +export const stationSchema = createSelectSchema(stationTable) + +export type NewStation = typeof stationTable.$inferInsert -export const syncItemEnum = pgEnum("sync_item", ["station", "schedule"]) +export type Station = z.infer -export const sync = pgTable("sync", { - id: uuid("id").defaultRandom().primaryKey().unique(), - n: bigserial("n", { mode: "number" }), - type: syncFromEnum("type").default("manual"), - status: syncStatusEnum("status").default("PENDING"), - item: syncItemEnum("item"), - duration: bigint("duration", { - mode: "number", - }).default(0), - message: text("message").default(sql`NULL`), - startedAt: text("started_at").default(sql`(CURRENT_TIMESTAMP)`), - endedAt: text("ended_at").default(sql`NULL`), - createdAt: text("created_at").default(sql`(CURRENT_TIMESTAMP)`), +export type StationType = Station["type"] + +export const scheduleSchema = createSelectSchema(scheduleTable, { + metadata: stationScheduleMetadata.nullable(), }) -export type NewSync = typeof sync.$inferInsert +export type Schedule = z.infer + +export type NewSchedule = typeof scheduleTable.$inferInsert diff --git a/src/modules/v1/database.ts b/src/modules/v1/database.ts index e3ff46e..c11182f 100644 --- a/src/modules/v1/database.ts +++ b/src/modules/v1/database.ts @@ -1,6 +1,6 @@ import { neonConfig, Pool } from "@neondatabase/serverless" import { drizzle, NeonDatabase } from "drizzle-orm/neon-serverless" -import * as schema from "../../db/schema-new" +import * as schema from "../../db/schema" export class Database< T extends { diff --git a/src/modules/v1/route/route.controller.ts b/src/modules/v1/route/route.controller.ts index 9f7beef..1e568ee 100644 --- a/src/modules/v1/route/route.controller.ts +++ b/src/modules/v1/route/route.controller.ts @@ -1,6 +1,6 @@ import { createRoute, z } from "@hono/zod-openapi" import { eq } from "drizzle-orm" -import { scheduleTable } from "../../../db/schema-new" +import { scheduleTable } from "../../../db/schema" import { createAPI } from "../../api" import { buildResponseSchemas } from "../../utils/response" import { Cache } from "../cache" diff --git a/src/modules/v1/schedule/schedule.controller.ts b/src/modules/v1/schedule/schedule.controller.ts index 27f4355..23027ce 100644 --- a/src/modules/v1/schedule/schedule.controller.ts +++ b/src/modules/v1/schedule/schedule.controller.ts @@ -1,6 +1,6 @@ import { createRoute, z } from "@hono/zod-openapi" import { asc, eq } from "drizzle-orm" -import { scheduleTable, Schedule } from "../../../db/schema-new" +import { scheduleTable, Schedule } from "../../../db/schema" import { createAPI } from "../../api" import { buildResponseSchemas } from "../../utils/response" import { scheduleResponseSchema } from "./schedule.schema" diff --git a/src/modules/v1/schedule/schedule.schema.ts b/src/modules/v1/schedule/schedule.schema.ts index ec2d360..83bb4ac 100644 --- a/src/modules/v1/schedule/schedule.schema.ts +++ b/src/modules/v1/schedule/schedule.schema.ts @@ -3,7 +3,7 @@ import { scheduleSchema, StationScheduleMetadata, stationSchema, -} from "../../../db/schema-new" +} from "../../../db/schema" export const scheduleResponseSchema = z .object({ diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index b4e6d86..97c1107 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -6,7 +6,7 @@ import { Station, stationTable, StationType, -} from "../../../db/schema-new" +} from "../../../db/schema" import { createAPI } from "../../api" import { buildResponseSchemas } from "../../utils/response" import { Cache } from "../cache" diff --git a/src/modules/v1/station/station.schema.ts b/src/modules/v1/station/station.schema.ts index fb71f7a..fa7b0c6 100644 --- a/src/modules/v1/station/station.schema.ts +++ b/src/modules/v1/station/station.schema.ts @@ -1,5 +1,5 @@ import { z } from "@hono/zod-openapi" -import { type StationMetadata, stationSchema } from "../../../db/schema-new" +import { type StationMetadata, stationSchema } from "../../../db/schema" export const stationResponseSchema = z .object({ diff --git a/src/sync.ts b/src/sync.ts index c0438b6..9a84c33 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -6,7 +6,7 @@ import { NewStation, scheduleTable, stationTable, -} from "./db/schema-new" +} from "./db/schema" import { Database } from "./modules/v1/database" export function parseTime(timeString: string): Date { From d5c8e350538121532bc417a20d229edb5ba36237 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:42:47 +0700 Subject: [PATCH 37/64] fix: move utils --- src/index.ts | 2 +- src/modules/v1/route/route.controller.ts | 4 ++-- src/modules/v1/schedule/schedule.controller.ts | 4 ++-- src/modules/v1/station/station.controller.ts | 4 ++-- src/{modules => }/utils/response.ts | 0 src/{modules/v1/uitls.ts => utils/time.ts} | 0 6 files changed, 7 insertions(+), 7 deletions(-) rename src/{modules => }/utils/response.ts (100%) rename src/{modules/v1/uitls.ts => utils/time.ts} (100%) diff --git a/src/index.ts b/src/index.ts index 1db6f89..c4063ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import { createAPI } from "./modules/api" import v1 from "./modules/v1" import { Database } from "./modules/v1/database" import { HTTPException } from "hono/http-exception" -import { constructResponse } from "./modules/utils/response" +import { constructResponse } from "./utils/response" export type Bindings = { DATABASE_URL: string diff --git a/src/modules/v1/route/route.controller.ts b/src/modules/v1/route/route.controller.ts index 1e568ee..edbbdbe 100644 --- a/src/modules/v1/route/route.controller.ts +++ b/src/modules/v1/route/route.controller.ts @@ -2,10 +2,10 @@ import { createRoute, z } from "@hono/zod-openapi" import { eq } from "drizzle-orm" import { scheduleTable } from "../../../db/schema" import { createAPI } from "../../api" -import { buildResponseSchemas } from "../../utils/response" +import { buildResponseSchemas } from "../../../utils/response" import { Cache } from "../cache" import { Route, routeResponseSchema } from "./route.schema" -import { getSecsToMidnight } from "../uitls" +import { getSecsToMidnight } from "../../../utils/time" const api = createAPI() diff --git a/src/modules/v1/schedule/schedule.controller.ts b/src/modules/v1/schedule/schedule.controller.ts index 23027ce..1216552 100644 --- a/src/modules/v1/schedule/schedule.controller.ts +++ b/src/modules/v1/schedule/schedule.controller.ts @@ -2,10 +2,10 @@ import { createRoute, z } from "@hono/zod-openapi" import { asc, eq } from "drizzle-orm" import { scheduleTable, Schedule } from "../../../db/schema" import { createAPI } from "../../api" -import { buildResponseSchemas } from "../../utils/response" +import { buildResponseSchemas } from "../../../utils/response" import { scheduleResponseSchema } from "./schedule.schema" import { Cache } from "../cache" -import { getSecsToMidnight } from "../uitls" +import { getSecsToMidnight } from "../../../utils/time" const api = createAPI() diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index 97c1107..65f86b2 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -8,10 +8,10 @@ import { StationType, } from "../../../db/schema" import { createAPI } from "../../api" -import { buildResponseSchemas } from "../../utils/response" +import { buildResponseSchemas } from "../../../utils/response" import { Cache } from "../cache" import { Sync } from "../sync" -import { getSecsToMidnight } from "../uitls" +import { getSecsToMidnight } from "../../../utils/time" import { stationResponseSchema } from "./station.schema" const api = createAPI() diff --git a/src/modules/utils/response.ts b/src/utils/response.ts similarity index 100% rename from src/modules/utils/response.ts rename to src/utils/response.ts diff --git a/src/modules/v1/uitls.ts b/src/utils/time.ts similarity index 100% rename from src/modules/v1/uitls.ts rename to src/utils/time.ts From b4dbf260682fd590b9d2963275717e03bda840ae Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:45:27 +0700 Subject: [PATCH 38/64] refactor: table schema --- src/db/index.ts | 8 -- src/db/schema/index.ts | 146 +------------------------------- src/db/schema/schedule.table.ts | 89 +++++++++++++++++++ src/db/schema/station.table.ts | 66 +++++++++++++++ 4 files changed, 157 insertions(+), 152 deletions(-) delete mode 100644 src/db/index.ts create mode 100644 src/db/schema/schedule.table.ts create mode 100644 src/db/schema/station.table.ts diff --git a/src/db/index.ts b/src/db/index.ts deleted file mode 100644 index 8f51fc0..0000000 --- a/src/db/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { neon } from "@neondatabase/serverless" -import { drizzle } from "drizzle-orm/neon-http" -export * as dbSchema from "./schema" - -export const createDB = (url: string) => { - const sql = neon(url) - return drizzle(sql) -} diff --git a/src/db/schema/index.ts b/src/db/schema/index.ts index 0ea98cc..6d21403 100644 --- a/src/db/schema/index.ts +++ b/src/db/schema/index.ts @@ -1,144 +1,2 @@ -import { relations } from "drizzle-orm" -import { - index, - jsonb, - pgEnum, - pgTable, - text, - time, - timestamp, - uniqueIndex, -} from "drizzle-orm/pg-core" -import { createSelectSchema } from "drizzle-zod" -import { z } from "zod" - -/** Station Metadata */ -const stationMetadata = z.object({ - /** Comuline metadata */ - has_schedule: z.boolean().nullable(), - /** Origin metadata */ - origin: z.object({ - /** KRL */ - daop: z.number().nullable(), - fg_enable: z.number().nullable(), - }), -}) - -export type StationMetadata = z.infer - -export const stationTypeEnum = pgEnum("station_type", [ - "KRL", - "MRT", - "LRT", - "LOCAL", -]) - -export const stationTable = pgTable( - "station", - { - uid: text("uid").primaryKey().unique().notNull(), - id: text("id").unique().notNull(), - name: text("name").notNull(), - type: stationTypeEnum("type").notNull(), - metadata: jsonb("metadata").$type(), - created_at: timestamp("created_at", { - withTimezone: true, - mode: "string", - }).defaultNow(), - updated_at: timestamp("updated_at", { - withTimezone: true, - mode: "string", - }).defaultNow(), - }, - (table) => { - return { - station_uidx: uniqueIndex("station_uidx").on(table.uid), - station_idx: index("station_idx").on(table.id), - type_idx: index("station_type_idx").on(table.type), - } - }, -) - -export const stationScheduleMetadata = z.object({ - /** Origin metadata */ - origin: z.object({ - color: z.string().nullable(), - }), -}) - -export type StationScheduleMetadata = z.infer - -export const scheduleTable = pgTable( - "schedule", - { - id: text("id").primaryKey().unique().notNull(), - station_id: text("station_id") - .notNull() - .references(() => stationTable.id, { - onDelete: "cascade", - }), - station_origin_id: text("station_origin_id").references( - () => stationTable.id, - { - onDelete: "set null", - }, - ), - station_destination_id: text("station_destination_id").references( - () => stationTable.id, - { - onDelete: "set null", - }, - ), - train_id: text("train_id").notNull(), - line: text("line").notNull(), - route: text("route").notNull(), - time_departure: time("time_departure").notNull(), - time_at_destination: time("time_at_destination").notNull(), - metadata: jsonb("metadata").$type(), - created_at: timestamp("created_at", { - mode: "string", - withTimezone: true, - }).defaultNow(), - updated_at: timestamp("updated_at", { - withTimezone: true, - mode: "string", - }).defaultNow(), - }, - (table) => { - return { - schedule_idx: uniqueIndex("schedule_idx").on(table.id), - schedule_station_idx: index("schedule_station_idx").on(table.station_id), - } - }, -) - -export const scheduleTableRelations = relations(scheduleTable, ({ one }) => ({ - station: one(stationTable, { - fields: [scheduleTable.station_id], - references: [stationTable.id], - }), - station_origin: one(stationTable, { - fields: [scheduleTable.station_origin_id], - references: [stationTable.id], - }), - station_destination: one(stationTable, { - fields: [scheduleTable.station_destination_id], - references: [stationTable.id], - }), -})) - -export const stationSchema = createSelectSchema(stationTable) - -export type NewStation = typeof stationTable.$inferInsert - -export type Station = z.infer - -export type StationType = Station["type"] - -export const scheduleSchema = createSelectSchema(scheduleTable, { - metadata: stationScheduleMetadata.nullable(), -}) - -export type Schedule = z.infer - -export type NewSchedule = typeof scheduleTable.$inferInsert +export * from "./station.table" +export * from "./schedule.table" diff --git a/src/db/schema/schedule.table.ts b/src/db/schema/schedule.table.ts new file mode 100644 index 0000000..da5442a --- /dev/null +++ b/src/db/schema/schedule.table.ts @@ -0,0 +1,89 @@ +import { + index, + jsonb, + pgTable, + text, + time, + timestamp, + uniqueIndex, +} from "drizzle-orm/pg-core" +import { createSelectSchema } from "drizzle-zod" +import { z } from "zod" +import { stationTable } from "./station.table" +import { relations } from "drizzle-orm" + +export const stationScheduleMetadata = z.object({ + /** Origin metadata */ + origin: z.object({ + color: z.string().nullable(), + }), +}) + +export type StationScheduleMetadata = z.infer + +export const scheduleTable = pgTable( + "schedule", + { + id: text("id").primaryKey().unique().notNull(), + station_id: text("station_id") + .notNull() + .references(() => stationTable.id, { + onDelete: "cascade", + }), + station_origin_id: text("station_origin_id").references( + () => stationTable.id, + { + onDelete: "set null", + }, + ), + station_destination_id: text("station_destination_id").references( + () => stationTable.id, + { + onDelete: "set null", + }, + ), + train_id: text("train_id").notNull(), + line: text("line").notNull(), + route: text("route").notNull(), + time_departure: time("time_departure").notNull(), + time_at_destination: time("time_at_destination").notNull(), + metadata: jsonb("metadata").$type(), + created_at: timestamp("created_at", { + mode: "string", + withTimezone: true, + }).defaultNow(), + updated_at: timestamp("updated_at", { + withTimezone: true, + mode: "string", + }).defaultNow(), + }, + (table) => { + return { + schedule_idx: uniqueIndex("schedule_idx").on(table.id), + schedule_station_idx: index("schedule_station_idx").on(table.station_id), + } + }, +) + +export const scheduleTableRelations = relations(scheduleTable, ({ one }) => ({ + station: one(stationTable, { + fields: [scheduleTable.station_id], + references: [stationTable.id], + }), + station_origin: one(stationTable, { + fields: [scheduleTable.station_origin_id], + references: [stationTable.id], + }), + station_destination: one(stationTable, { + fields: [scheduleTable.station_destination_id], + references: [stationTable.id], + }), +})) + +export const scheduleSchema = createSelectSchema(scheduleTable, { + metadata: stationScheduleMetadata.nullable(), +}) + +export type Schedule = z.infer + +export type NewSchedule = typeof scheduleTable.$inferInsert diff --git a/src/db/schema/station.table.ts b/src/db/schema/station.table.ts new file mode 100644 index 0000000..a346bcd --- /dev/null +++ b/src/db/schema/station.table.ts @@ -0,0 +1,66 @@ +import { + index, + jsonb, + pgEnum, + pgTable, + text, + timestamp, + uniqueIndex, +} from "drizzle-orm/pg-core" +import { createSelectSchema } from "drizzle-zod" +import { z } from "zod" + +/** Station Metadata */ +const stationMetadata = z.object({ + /** Comuline metadata */ + has_schedule: z.boolean().nullable(), + /** Origin metadata */ + origin: z.object({ + /** KRL */ + daop: z.number().nullable(), + fg_enable: z.number().nullable(), + }), +}) + +export type StationMetadata = z.infer + +export const stationTypeEnum = pgEnum("station_type", [ + "KRL", + "MRT", + "LRT", + "LOCAL", +]) + +export const stationTable = pgTable( + "station", + { + uid: text("uid").primaryKey().unique().notNull(), + id: text("id").unique().notNull(), + name: text("name").notNull(), + type: stationTypeEnum("type").notNull(), + metadata: jsonb("metadata").$type(), + created_at: timestamp("created_at", { + withTimezone: true, + mode: "string", + }).defaultNow(), + updated_at: timestamp("updated_at", { + withTimezone: true, + mode: "string", + }).defaultNow(), + }, + (table) => { + return { + station_uidx: uniqueIndex("station_uidx").on(table.uid), + station_idx: index("station_idx").on(table.id), + type_idx: index("station_type_idx").on(table.type), + } + }, +) + +export const stationSchema = createSelectSchema(stationTable) + +export type NewStation = typeof stationTable.$inferInsert + +export type Station = z.infer + +export type StationType = Station["type"] From 75c2e9ffa186d025b9024657d1e5effe581870a7 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:46:20 +0700 Subject: [PATCH 39/64] remove unused --- Dockerfile | 18 ------------------ wrangler.example.toml | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) delete mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 0de5967..0000000 --- a/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM oven/bun - -WORKDIR /app - -COPY package.json . -COPY bun.lockb . - -# TODO: RUN MIGRATION ON PRODUCTION, HOW TF DO I DO THAT? -RUN bun install - -COPY src src -COPY tsconfig.json . - -ENV NODE_ENV production - -CMD ["bun", "src/index.ts"] - -EXPOSE 3000 \ No newline at end of file diff --git a/wrangler.example.toml b/wrangler.example.toml index 878868d..f227d3b 100644 --- a/wrangler.example.toml +++ b/wrangler.example.toml @@ -1,6 +1,6 @@ name = "comuline-api" compatibility_date = "2024-08-21" -main = "src/app.ts" +main = "src/index.ts" minify = true [limits] From bcdc988094a48f014e9f83a7d1c67a2834fde490 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:22:15 +0700 Subject: [PATCH 40/64] feat: run sync cli --- package.json | 4 +- src/modules/v1/station/station.controller.ts | 160 +------------------ src/sync/headers.ts | 9 ++ src/{sync.ts => sync/schedule.ts} | 33 +--- src/sync/station.ts | 143 +++++++++++++++++ src/utils/time.ts | 10 ++ 6 files changed, 177 insertions(+), 182 deletions(-) create mode 100644 src/sync/headers.ts rename src/{sync.ts => sync/schedule.ts} (77%) create mode 100644 src/sync/station.ts diff --git a/package.json b/package.json index c63e7a4..3af29a6 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "migration:apply": "bun run src/db/migrate.ts", "format": "prettier -w .", "format:check": "prettier -c .", - "prepare": "husky" + "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" }, "dependencies": { "@hono/zod-openapi": "^0.16.0", diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index 65f86b2..73564b1 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -1,24 +1,14 @@ import { createRoute, z } from "@hono/zod-openapi" -import { eq, sql } from "drizzle-orm" -import { HTTPException } from "hono/http-exception" -import { - NewStation, - Station, - stationTable, - StationType, -} from "../../../db/schema" -import { createAPI } from "../../api" +import { eq } from "drizzle-orm" +import { Station, stationTable } from "../../../db/schema" import { buildResponseSchemas } from "../../../utils/response" -import { Cache } from "../cache" -import { Sync } from "../sync" import { getSecsToMidnight } from "../../../utils/time" +import { createAPI } from "../../api" +import { Cache } from "../cache" import { stationResponseSchema } from "./station.schema" const api = createAPI() -const createStationKey = (type: StationType, id: string) => - `st_${type}_${id}`.toLocaleLowerCase() - const stationController = api .openapi( createRoute({ @@ -144,147 +134,5 @@ const stationController = api ) }, ) - .openapi( - createRoute({ - method: "post", - path: "/", - responses: buildResponseSchemas([ - { - status: 201, - type: "metadata", - description: "Success", - }, - ]), - - tags: ["Station"], - }), - async (c) => { - const { db } = c.var - - // TODO: Refactor to CLI - - const schema = z.object({ - status: z.number(), - message: z.string(), - data: z.array( - z.object({ - sta_id: z.string(), - sta_name: z.string(), - group_wil: z.number(), - fg_enable: z.number(), - }), - ), - }) - - const sync = new Sync( - "https://api-partner.krl.co.id/krlweb/v1/krl-station", - { - headers: { - Authorization: - "Bearer VXcYZMFtwUAoikVByzKuaZZeTo1AtCiSjejSHNdpLxyKk_SFUzog5MOkUN1ktAhFnBFoz6SlWAJBJIS-lHYsdFLSug2YNiaNllkOUsDbYkiDtmPc9XWc", - Host: "api-partner.krl.co.id", - Origin: "https://commuterline.id", - Referer: "https://commuterline.id/", - }, - }, - ) - - const res = await sync.request(schema) - - if (res instanceof Response) { - throw new HTTPException(417, { - message: "Failed to sync", - }) - } - - const filterdStation = res.data.filter((d) => !d.sta_id.includes("WIL")) - - const stations = filterdStation.map((s) => { - return { - uid: createStationKey("KRL", s.sta_id), - id: s.sta_id, - name: s.sta_name, - type: "KRL", - metadata: { - has_schedule: true, - origin: { - fg_enable: s.fg_enable, - daop: s.group_wil === 0 ? 1 : s.group_wil, - }, - }, - } - }) satisfies NewStation[] - - const newStations = [ - /** Bandara Soekarno Hatta */ - { - uid: createStationKey("KRL", "BST"), - id: "BST", - name: "BANDARA SOEKARNO HATTA", - type: "KRL", - metadata: { - has_schedule: true, - origin: { - fg_enable: 1, - daop: 1, - }, - }, - }, - /** Cikampek */ - { - uid: createStationKey("KRL", "CKP"), - id: "CKP", - name: "CIKAMPEK", - type: "LOCAL", - metadata: { - has_schedule: true, - origin: { - fg_enable: 1, - daop: 1, - }, - }, - }, - /** Purwakarta */ - { - uid: createStationKey("KRL", "PWK"), - id: "PWK", - name: "PURWAKARTA", - type: "LOCAL", - metadata: { - has_schedule: true, - origin: { - fg_enable: 1, - daop: 2, - }, - }, - }, - ] satisfies NewStation[] - - const insertStations = [...newStations, ...stations] - - await db - .insert(stationTable) - .values(insertStations) - .onConflictDoUpdate({ - target: stationTable.uid, - set: { - updated_at: new Date().toLocaleString(), - uid: sql`excluded.uid`, - id: sql`excluded.id`, - name: sql`excluded.name`, - }, - }) - - return c.json( - { - metadata: { - success: true, - message: "Success", - }, - }, - 200, - ) - }, - ) export default stationController diff --git a/src/sync/headers.ts b/src/sync/headers.ts new file mode 100644 index 0000000..a5d570a --- /dev/null +++ b/src/sync/headers.ts @@ -0,0 +1,9 @@ +export const KAI_HEADERS = { + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0", + Accept: "application/json, text/javascript, */*; q=0.01", + "Accept-Language": "en-US,en;q=0.5", + Authorization: + "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIzIiwianRpIjoiMDYzNWIyOGMzYzg3YTY3ZTRjYWE4YTI0MjYxZGYwYzIxNjYzODA4NWM2NWU4ZjhiYzQ4OGNlM2JiZThmYWNmODU4YzY0YmI0MjgyM2EwOTUiLCJpYXQiOjE3MjI2MTc1MTQsIm5iZiI6MTcyMjYxNzUxNCwiZXhwIjoxNzU0MTUzNTE0LCJzdWIiOiI1Iiwic2NvcGVzIjpbXX0.Jz_sedcMtaZJ4dj0eWVc4_pr_wUQ3s1-UgpopFGhEmJt_iGzj6BdnOEEhcDDdIz-gydQL5ek0S_36v5h6P_X3OQyII3JmHp1SEDJMwrcy4FCY63-jGnhPBb4sprqUFruDRFSEIs1cNQ-3rv3qRDzJtGYc_bAkl2MfgZj85bvt2DDwBWPraZuCCkwz2fJvox-6qz6P7iK9YdQq8AjJfuNdl7t_1hMHixmtDG0KooVnfBV7PoChxvcWvs8FOmtYRdqD7RSEIoOXym2kcwqK-rmbWf9VuPQCN5gjLPimL4t2TbifBg5RWNIAAuHLcYzea48i3okbhkqGGlYTk3iVMU6Hf_Jruns1WJr3A961bd4rny62lNXyGPgNLRJJKedCs5lmtUTr4gZRec4Pz_MqDzlEYC3QzRAOZv0Ergp8-W1Vrv5gYyYNr-YQNdZ01mc7JH72N2dpU9G00K5kYxlcXDNVh8520-R-MrxYbmiFGVlNF2BzEH8qq6Ko9m0jT0NiKEOjetwegrbNdNq_oN4KmHvw2sHkGWY06rUeciYJMhBF1JZuRjj3JTwBUBVXcYZMFtwUAoikVByzKuaZZeTo1AtCiSjejSHNdpLxyKk_SFUzog5MOkUN1ktAhFnBFoz6SlWAJBJIS-lHYsdFLSug2YNiaNllkOUsDbYkiDtmPc9XWc", + Priority: "u=0", +} diff --git a/src/sync.ts b/src/sync/schedule.ts similarity index 77% rename from src/sync.ts rename to src/sync/schedule.ts index 9a84c33..eacea32 100644 --- a/src/sync.ts +++ b/src/sync/schedule.ts @@ -6,24 +6,17 @@ import { NewStation, scheduleTable, stationTable, -} from "./db/schema" -import { Database } from "./modules/v1/database" - -export function parseTime(timeString: string): Date { - const [hours, minutes, seconds] = timeString.split(":").map(Number) - const date = new Date() - date.setHours(hours ?? date.getHours()) - date.setMinutes(minutes ?? date.getMinutes()) - date.setSeconds(seconds ?? date.getSeconds()) - - return date -} +} from "../db/schema" +import { Database } from "../modules/v1/database" +import { parseTime } from "../utils/time" +import { KAI_HEADERS } from "./headers" 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") const { db } = new Database({ - COMULINE_ENV: "development", + COMULINE_ENV: process.env.COMULINE_ENV, DATABASE_URL: process.env.DATABASE_URL, }) @@ -64,21 +57,11 @@ const sync = async () => { const url = `https://api-partner.krl.co.id/krlweb/v1/schedule?stationid=${id}&timefrom=00:00&timeto=23:00` - const headers = { - "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0", - Accept: "application/json, text/javascript, */*; q=0.01", - "Accept-Language": "en-US,en;q=0.5", - Authorization: - "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIzIiwianRpIjoiMDYzNWIyOGMzYzg3YTY3ZTRjYWE4YTI0MjYxZGYwYzIxNjYzODA4NWM2NWU4ZjhiYzQ4OGNlM2JiZThmYWNmODU4YzY0YmI0MjgyM2EwOTUiLCJpYXQiOjE3MjI2MTc1MTQsIm5iZiI6MTcyMjYxNzUxNCwiZXhwIjoxNzU0MTUzNTE0LCJzdWIiOiI1Iiwic2NvcGVzIjpbXX0.Jz_sedcMtaZJ4dj0eWVc4_pr_wUQ3s1-UgpopFGhEmJt_iGzj6BdnOEEhcDDdIz-gydQL5ek0S_36v5h6P_X3OQyII3JmHp1SEDJMwrcy4FCY63-jGnhPBb4sprqUFruDRFSEIs1cNQ-3rv3qRDzJtGYc_bAkl2MfgZj85bvt2DDwBWPraZuCCkwz2fJvox-6qz6P7iK9YdQq8AjJfuNdl7t_1hMHixmtDG0KooVnfBV7PoChxvcWvs8FOmtYRdqD7RSEIoOXym2kcwqK-rmbWf9VuPQCN5gjLPimL4t2TbifBg5RWNIAAuHLcYzea48i3okbhkqGGlYTk3iVMU6Hf_Jruns1WJr3A961bd4rny62lNXyGPgNLRJJKedCs5lmtUTr4gZRec4Pz_MqDzlEYC3QzRAOZv0Ergp8-W1Vrv5gYyYNr-YQNdZ01mc7JH72N2dpU9G00K5kYxlcXDNVh8520-R-MrxYbmiFGVlNF2BzEH8qq6Ko9m0jT0NiKEOjetwegrbNdNq_oN4KmHvw2sHkGWY06rUeciYJMhBF1JZuRjj3JTwBUBVXcYZMFtwUAoikVByzKuaZZeTo1AtCiSjejSHNdpLxyKk_SFUzog5MOkUN1ktAhFnBFoz6SlWAJBJIS-lHYsdFLSug2YNiaNllkOUsDbYkiDtmPc9XWc", - Priority: "u=0", - } - console.info(`[SYNC][SCHEDULE][${id}] Send preflight`) const optionsResponse = await fetch(url, { method: "OPTIONS", headers: { - ...headers, + ...KAI_HEADERS, "Access-Control-Request-Method": "GET", "Access-Control-Request-Headers": "authorization,content-type", }, @@ -93,7 +76,7 @@ const sync = async () => { } const req = await fetch(url, { method: "GET", - headers, + headers: KAI_HEADERS, credentials: "include", mode: "cors", }) diff --git a/src/sync/station.ts b/src/sync/station.ts new file mode 100644 index 0000000..60d3648 --- /dev/null +++ b/src/sync/station.ts @@ -0,0 +1,143 @@ +import { sql } from "drizzle-orm" +import { z } from "zod" +import { NewStation, stationTable, StationType } from "../db/schema" +import { Database } from "../modules/v1/database" +import { KAI_HEADERS } from "./headers" + +const createStationKey = (type: StationType, id: string) => + `st_${type}_${id}`.toLocaleLowerCase() + +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") + + const { db } = new Database({ + COMULINE_ENV: process.env.COMULINE_ENV, + DATABASE_URL: process.env.DATABASE_URL, + }) + + const schema = z.object({ + status: z.number(), + message: z.string(), + data: z.array( + z.object({ + sta_id: z.string(), + sta_name: z.string(), + group_wil: z.number(), + fg_enable: z.number(), + }), + ), + }) + + const url = "https://api-partner.krl.co.id/krlweb/v1/krl-station" + + const req = await fetch(url, { + method: "GET", + headers: KAI_HEADERS, + }) + + if (!req.ok) + throw new Error( + `[SYNC][STATION] Request failed with status: ${req.status}`, + { + cause: await req.text(), + }, + ) + + const data = await req.json() + + const parsedData = schema.safeParse(data) + + if (!parsedData.success) { + throw new Error(parsedData.error.message, { + cause: parsedData.error.cause, + }) + } + + const filteredStation = parsedData.data.data.filter( + (d) => !d.sta_id.includes("WIL"), + ) + + const stations = filteredStation.map((s) => { + return { + uid: createStationKey("KRL", s.sta_id), + id: s.sta_id, + name: s.sta_name, + type: "KRL", + metadata: { + has_schedule: true, + origin: { + fg_enable: s.fg_enable, + daop: s.group_wil === 0 ? 1 : s.group_wil, + }, + }, + } + }) satisfies NewStation[] + + const newStations = [ + /** Bandara Soekarno Hatta */ + { + uid: createStationKey("KRL", "BST"), + id: "BST", + name: "BANDARA SOEKARNO HATTA", + type: "KRL", + metadata: { + has_schedule: true, + origin: { + fg_enable: 1, + daop: 1, + }, + }, + }, + /** Cikampek */ + { + uid: createStationKey("KRL", "CKP"), + id: "CKP", + name: "CIKAMPEK", + type: "LOCAL", + metadata: { + has_schedule: true, + origin: { + fg_enable: 1, + daop: 1, + }, + }, + }, + /** Purwakarta */ + { + uid: createStationKey("KRL", "PWK"), + id: "PWK", + name: "PURWAKARTA", + type: "LOCAL", + metadata: { + has_schedule: true, + origin: { + fg_enable: 1, + daop: 2, + }, + }, + }, + ] satisfies NewStation[] + + const insertStations = [...newStations, ...stations] + + await db + .insert(stationTable) + .values(insertStations) + .onConflictDoUpdate({ + target: stationTable.uid, + set: { + updated_at: new Date().toLocaleString(), + uid: sql`excluded.uid`, + id: sql`excluded.id`, + name: sql`excluded.name`, + }, + }) + .returning() + + console.info(`[SYNC][STATION] Inserted ${insertStations.length} rows`) + + process.exit(0) +} + +sync() diff --git a/src/utils/time.ts b/src/utils/time.ts index b086d2d..7b9bc46 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -6,3 +6,13 @@ export function getSecsToMidnight(): number { return Math.floor((tomorrow.getTime() - now.getTime()) / 1000) } + +export function parseTime(timeString: string): Date { + const [hours, minutes, seconds] = timeString.split(":").map(Number) + const date = new Date() + date.setHours(hours ?? date.getHours()) + date.setMinutes(minutes ?? date.getMinutes()) + date.setSeconds(seconds ?? date.getSeconds()) + + return date +} From af84349bf86017906792023a57068e381d5255fa Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:22:21 +0700 Subject: [PATCH 41/64] fix: deploy cli --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3af29a6..10ce1cf 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "docker:up": "docker-compose -p comuline-api up -d", "docker:down": "docker-compose down", "test": "echo \"Error: no test specified\" && exit 1", - "deploy": "wrangler deploy", + "deploy": "wrangler deploy --minify src/index.ts", "migration:drop": "drizzle-kit drop", "migration:generate": "drizzle-kit generate", "migration:apply": "bun run src/db/migrate.ts", From 3eabccf52ccaa9367ec56f19cc4123dbc4b83ad3 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:44:59 +0700 Subject: [PATCH 42/64] perf: prepared statement --- src/modules/v1/route/route.controller.ts | 36 +++++++++++-------- .../v1/schedule/schedule.controller.ts | 11 ++++-- src/modules/v1/station/station.controller.ts | 13 ++++--- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/modules/v1/route/route.controller.ts b/src/modules/v1/route/route.controller.ts index edbbdbe..7bb29c4 100644 --- a/src/modules/v1/route/route.controller.ts +++ b/src/modules/v1/route/route.controller.ts @@ -1,6 +1,6 @@ import { createRoute, z } from "@hono/zod-openapi" -import { eq } from "drizzle-orm" -import { scheduleTable } from "../../../db/schema" +import { asc, eq, sql } from "drizzle-orm" +import { scheduleTable, stationTable } from "../../../db/schema" import { createAPI } from "../../api" import { buildResponseSchemas } from "../../../utils/response" import { Cache } from "../cache" @@ -57,21 +57,29 @@ const routeController = api.openapi( 200, ) - const data = await db.query.scheduleTable.findMany({ - with: { - station: { - columns: { - name: true, + const query = db.query.scheduleTable + .findMany({ + with: { + station: { + columns: { + name: true, + }, }, - }, - station_destination: { - columns: { - name: true, + station_destination: { + columns: { + name: true, + }, }, }, - }, - orderBy: (scheduleTable, { asc }) => [asc(scheduleTable.time_departure)], - where: eq(scheduleTable.train_id, param.train_id), + orderBy: (scheduleTable, { asc }) => [ + asc(scheduleTable.time_departure), + ], + where: eq(scheduleTable.train_id, sql.placeholder("train_id")), + }) + .prepare("query_route_by_train_id") + + const data = await query.execute({ + train_id: param.train_id, }) if (data.length === 0) diff --git a/src/modules/v1/schedule/schedule.controller.ts b/src/modules/v1/schedule/schedule.controller.ts index 1216552..67619e1 100644 --- a/src/modules/v1/schedule/schedule.controller.ts +++ b/src/modules/v1/schedule/schedule.controller.ts @@ -1,5 +1,5 @@ import { createRoute, z } from "@hono/zod-openapi" -import { asc, eq } from "drizzle-orm" +import { asc, eq, sql } from "drizzle-orm" import { scheduleTable, Schedule } from "../../../db/schema" import { createAPI } from "../../api" import { buildResponseSchemas } from "../../../utils/response" @@ -63,11 +63,16 @@ const scheduleController = api.openapi( 200, ) - const data = await db + const query = db .select() .from(scheduleTable) - .where(eq(scheduleTable.station_id, param.station_id.toLocaleUpperCase())) + .where(eq(scheduleTable.station_id, sql.placeholder("station_id"))) .orderBy(asc(scheduleTable.time_departure)) + .prepare("query_schedule_by_station_id") + + const data = await query.execute({ + station_id: param.station_id.toLocaleUpperCase(), + }) await cache.set(data, getSecsToMidnight()) diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index 73564b1..591418a 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -1,5 +1,5 @@ import { createRoute, z } from "@hono/zod-openapi" -import { eq } from "drizzle-orm" +import { eq, sql } from "drizzle-orm" import { Station, stationTable } from "../../../db/schema" import { buildResponseSchemas } from "../../../utils/response" import { getSecsToMidnight } from "../../../utils/time" @@ -45,7 +45,9 @@ const stationController = api 200, ) - const stations = await db.select().from(stationTable) + const query = db.select().from(stationTable).prepare("query_all_stations") + + const stations = await query.execute() await cache.set(stations, getSecsToMidnight()) @@ -116,10 +118,13 @@ const stationController = api 200, ) - const data = await db + const query = db .select() .from(stationTable) - .where(eq(stationTable.id, param.id)) + .where(eq(stationTable.id, sql.placeholder("id"))) + .prepare("query_station_by_id") + + const data = await query.execute({ id: param.id }) await cache.set(data[0], getSecsToMidnight()) From 1a13e5e08108acf75f021943f8c032fbf92e906c Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:45:06 +0700 Subject: [PATCH 43/64] fix: remove unused --- src/modules/v1/sync.ts | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 src/modules/v1/sync.ts diff --git a/src/modules/v1/sync.ts b/src/modules/v1/sync.ts deleted file mode 100644 index cd26d35..0000000 --- a/src/modules/v1/sync.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { z } from "zod" - -export class Sync { - constructor( - protected url: string | URL | Request, - protected init?: FetchRequestInit, - ) { - this.url = url - this.init = init - } - - async request( - expectedSchema: T, - ): Promise | Response> { - const req = await fetch(this.url, this.init) - - if (!req.ok) return req - - const data = await req.json() - - const parsedData = expectedSchema.safeParse(data) - - if (!parsedData.success) { - throw new Error(parsedData.error.message, { - cause: parsedData.error.cause, - }) - } - - return parsedData.data - } -} From 9a6b6f8b06995432932f44d4a4823a491ebb0d14 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:49:11 +0700 Subject: [PATCH 44/64] perf: train idx --- .env.example | 6 - drizzle/migrations/0003_first_dorian_gray.sql | 1 + drizzle/migrations/meta/0003_snapshot.json | 321 ++++++++++++++++++ drizzle/migrations/meta/_journal.json | 7 + package.json | 6 +- src/db/schema/schedule.table.ts | 1 + 6 files changed, 333 insertions(+), 9 deletions(-) delete mode 100644 .env.example create mode 100644 drizzle/migrations/0003_first_dorian_gray.sql create mode 100644 drizzle/migrations/meta/0003_snapshot.json diff --git a/.env.example b/.env.example deleted file mode 100644 index b517e92..0000000 --- a/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -REDIS_URL="redis://localhost:6379" -DATABASE_URL="postgresql://comuline:password@localhost:5432/comuline" -# SYNC_TOKEN is a secret key used in production level to authenticate requests to the POST /v1/station and POST /v1/schedule endpoint. -# You can generate a new secret on the command line with: -# openssl rand -base64 32 -SYNC_TOKEN="" diff --git a/drizzle/migrations/0003_first_dorian_gray.sql b/drizzle/migrations/0003_first_dorian_gray.sql new file mode 100644 index 0000000..dea4939 --- /dev/null +++ b/drizzle/migrations/0003_first_dorian_gray.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS "schedule_train_idx" ON "schedule" USING btree ("train_id"); \ No newline at end of file diff --git a/drizzle/migrations/meta/0003_snapshot.json b/drizzle/migrations/meta/0003_snapshot.json new file mode 100644 index 0000000..a020147 --- /dev/null +++ b/drizzle/migrations/meta/0003_snapshot.json @@ -0,0 +1,321 @@ +{ + "id": "d2d9cd87-d69d-46ff-9890-21be36c0cb3b", + "prevId": "5db983ea-4af8-40b5-9bca-1f274a3c4e3a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time_departure": { + "name": "time_departure", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "time_at_destination": { + "name": "time_at_destination", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_train_idx": { + "name": "schedule_train_idx", + "columns": [ + { + "expression": "train_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT", + "LOCAL" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 40bc0af..470d374 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1731399355344, "tag": "0002_serious_the_hand", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1731404897712, + "tag": "0003_first_dorian_gray", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 10ce1cf..b5f02e8 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "docker:down": "docker-compose down", "test": "echo \"Error: no test specified\" && exit 1", "deploy": "wrangler deploy --minify src/index.ts", - "migration:drop": "drizzle-kit drop", - "migration:generate": "drizzle-kit generate", - "migration:apply": "bun run src/db/migrate.ts", + "migrate:drop": "drizzle-kit drop", + "migrate:generate": "drizzle-kit generate", + "migrate:apply": "bun run src/db/migrate.ts", "format": "prettier -w .", "format:check": "prettier -c .", "prepare": "husky", diff --git a/src/db/schema/schedule.table.ts b/src/db/schema/schedule.table.ts index da5442a..b32878a 100644 --- a/src/db/schema/schedule.table.ts +++ b/src/db/schema/schedule.table.ts @@ -61,6 +61,7 @@ export const scheduleTable = pgTable( return { schedule_idx: uniqueIndex("schedule_idx").on(table.id), schedule_station_idx: index("schedule_station_idx").on(table.station_id), + schedule_train_idx: index("schedule_train_idx").on(table.train_id), } }, ) From 04ebd6676e265f3c767c29ef9c9c75cdb93697b7 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:50:37 +0700 Subject: [PATCH 45/64] remove unused --- src/modules/v1/schedule/schedule.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/v1/schedule/schedule.controller.ts b/src/modules/v1/schedule/schedule.controller.ts index 67619e1..79c769c 100644 --- a/src/modules/v1/schedule/schedule.controller.ts +++ b/src/modules/v1/schedule/schedule.controller.ts @@ -36,7 +36,7 @@ const scheduleController = api.openapi( }, ]), tags: ["Schedule"], - description: "Get all active schedule by station id", + description: "Get all schedule by station id", }), async (c) => { const param = c.req.valid("param") From d7e7d9a6f51d03393f2da9304785c594e5e667b7 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:15:20 +0700 Subject: [PATCH 46/64] fix: strict trail --- src/index.ts | 6 +++++- src/modules/api.ts | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index c4063ce..ee4d053 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import v1 from "./modules/v1" import { Database } from "./modules/v1/database" import { HTTPException } from "hono/http-exception" import { constructResponse } from "./utils/response" +import { trimTrailingSlash } from "hono/trailing-slash" export type Bindings = { DATABASE_URL: string @@ -38,6 +39,7 @@ const app = api }, ], })) + .use(trimTrailingSlash()) .use(async (c, next) => { const { db } = new Database({ COMULINE_ENV: c.env.COMULINE_ENV, @@ -58,7 +60,9 @@ const app = api }), ) .get("/status", (c) => c.json({ status: "ok" })) - .notFound((c) => c.redirect("/docs")) + .notFound(() => { + throw new HTTPException(404, { message: "Not found" }) + }) .onError((err, c) => { if (err instanceof HTTPException) { return c.json( diff --git a/src/modules/api.ts b/src/modules/api.ts index 9eda74e..8569309 100644 --- a/src/modules/api.ts +++ b/src/modules/api.ts @@ -1,4 +1,5 @@ import { OpenAPIHono } from "@hono/zod-openapi" import { Environments } from ".." -export const createAPI = () => new OpenAPIHono() +export const createAPI = () => + new OpenAPIHono({ strict: true }) From 1cfe62e604f5a3ae624025a8770f758375881a64 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:15:24 +0700 Subject: [PATCH 47/64] fix: verbose --- src/modules/v1/route/route.controller.ts | 2 +- src/modules/v1/schedule/schedule.controller.ts | 2 +- src/modules/v1/station/station.controller.ts | 17 ++++++++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/modules/v1/route/route.controller.ts b/src/modules/v1/route/route.controller.ts index 7bb29c4..2ffdc8f 100644 --- a/src/modules/v1/route/route.controller.ts +++ b/src/modules/v1/route/route.controller.ts @@ -36,7 +36,7 @@ const routeController = api.openapi( }, ]), tags: ["Route"], - description: "Get sequence of station by train id", + description: "Get sequence of station stop by train ID", }), async (c) => { const param = c.req.valid("param") diff --git a/src/modules/v1/schedule/schedule.controller.ts b/src/modules/v1/schedule/schedule.controller.ts index 79c769c..318c92f 100644 --- a/src/modules/v1/schedule/schedule.controller.ts +++ b/src/modules/v1/schedule/schedule.controller.ts @@ -36,7 +36,7 @@ const scheduleController = api.openapi( }, ]), tags: ["Schedule"], - description: "Get all schedule by station id", + description: "Get all schedule by station ID", }), async (c) => { const param = c.req.valid("param") diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index 591418a..e737c88 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -73,7 +73,7 @@ const stationController = api params: z.object({ id: z .string() - .min(2) + .min(1) .openapi({ param: { name: "id", @@ -96,7 +96,7 @@ const stationController = api }, ]), tags: ["Station"], - description: "Get station by id", + description: "Get station by ID", }), async (c) => { const param = c.req.valid("param") @@ -124,7 +124,18 @@ const stationController = api .where(eq(stationTable.id, sql.placeholder("id"))) .prepare("query_station_by_id") - const data = await query.execute({ id: param.id }) + const data = await query.execute({ id: param.id.toLocaleUpperCase() }) + + if (data.length === 0) + return c.json( + { + metadata: { + success: false, + message: "Station not found", + }, + }, + 404, + ) await cache.set(data[0], getSecsToMidnight()) From 2e1542bc067be6bc91fcd349ff1a51a541786ad1 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:33:20 +0700 Subject: [PATCH 48/64] refactor: move types --- src/index.ts | 17 ----------------- src/modules/api.ts | 2 +- src/type.ts | 19 +++++++++++++++++++ 3 files changed, 20 insertions(+), 18 deletions(-) create mode 100644 src/type.ts diff --git a/src/index.ts b/src/index.ts index ee4d053..6f07af9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,23 +6,6 @@ import { HTTPException } from "hono/http-exception" import { constructResponse } from "./utils/response" import { trimTrailingSlash } from "hono/trailing-slash" -export type Bindings = { - DATABASE_URL: string - COMULINE_ENV: string - UPSTASH_REDIS_REST_TOKEN: string - UPSTASH_REDIS_REST_URL: string -} - -export type Variables = { - db: Database["db"] - constructResponse: typeof constructResponse -} - -export type Environments = { - Bindings: Bindings - Variables: Variables -} - const api = createAPI() const app = api diff --git a/src/modules/api.ts b/src/modules/api.ts index 8569309..f78c1cd 100644 --- a/src/modules/api.ts +++ b/src/modules/api.ts @@ -1,5 +1,5 @@ import { OpenAPIHono } from "@hono/zod-openapi" -import { Environments } from ".." +import { type Environments } from "@/type" export const createAPI = () => new OpenAPIHono({ strict: true }) diff --git a/src/type.ts b/src/type.ts new file mode 100644 index 0000000..94c8a50 --- /dev/null +++ b/src/type.ts @@ -0,0 +1,19 @@ +import { Database } from "./modules/v1/database" +import { constructResponse } from "./utils/response" + +export type Bindings = { + DATABASE_URL: string + COMULINE_ENV: string + UPSTASH_REDIS_REST_TOKEN: string + UPSTASH_REDIS_REST_URL: string +} + +export type Variables = { + db: Database["db"] + constructResponse: typeof constructResponse +} + +export type Environments = { + Bindings: Bindings + Variables: Variables +} From d11af433b389018c421ec912888d84ce6e07fc13 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:33:29 +0700 Subject: [PATCH 49/64] feat: aliases path --- src/modules/v1/database.ts | 2 +- src/modules/v1/route/route.controller.ts | 10 +++++----- src/modules/v1/schedule/schedule.controller.ts | 8 ++++---- src/modules/v1/schedule/schedule.schema.ts | 2 +- src/modules/v1/station/station.controller.ts | 8 ++++---- src/modules/v1/station/station.schema.ts | 2 +- tsconfig.json | 6 +++++- 7 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/modules/v1/database.ts b/src/modules/v1/database.ts index c11182f..c0dd86a 100644 --- a/src/modules/v1/database.ts +++ b/src/modules/v1/database.ts @@ -1,6 +1,6 @@ import { neonConfig, Pool } from "@neondatabase/serverless" import { drizzle, NeonDatabase } from "drizzle-orm/neon-serverless" -import * as schema from "../../db/schema" +import * as schema from "@/db/schema" export class Database< T extends { diff --git a/src/modules/v1/route/route.controller.ts b/src/modules/v1/route/route.controller.ts index 2ffdc8f..c63f667 100644 --- a/src/modules/v1/route/route.controller.ts +++ b/src/modules/v1/route/route.controller.ts @@ -1,11 +1,11 @@ import { createRoute, z } from "@hono/zod-openapi" -import { asc, eq, sql } from "drizzle-orm" -import { scheduleTable, stationTable } from "../../../db/schema" -import { createAPI } from "../../api" -import { buildResponseSchemas } from "../../../utils/response" +import { eq, sql } from "drizzle-orm" +import { scheduleTable } from "@/db/schema" +import { buildResponseSchemas } from "@/utils/response" +import { getSecsToMidnight } from "@/utils/time" +import { createAPI } from "@/modules/api" import { Cache } from "../cache" import { Route, routeResponseSchema } from "./route.schema" -import { getSecsToMidnight } from "../../../utils/time" const api = createAPI() diff --git a/src/modules/v1/schedule/schedule.controller.ts b/src/modules/v1/schedule/schedule.controller.ts index 318c92f..b7cb4c8 100644 --- a/src/modules/v1/schedule/schedule.controller.ts +++ b/src/modules/v1/schedule/schedule.controller.ts @@ -1,11 +1,11 @@ import { createRoute, z } from "@hono/zod-openapi" import { asc, eq, sql } from "drizzle-orm" -import { scheduleTable, Schedule } from "../../../db/schema" -import { createAPI } from "../../api" -import { buildResponseSchemas } from "../../../utils/response" +import { scheduleTable, Schedule } from "@/db/schema" +import { createAPI } from "@/modules/api" +import { buildResponseSchemas } from "@/utils/response" import { scheduleResponseSchema } from "./schedule.schema" import { Cache } from "../cache" -import { getSecsToMidnight } from "../../../utils/time" +import { getSecsToMidnight } from "@/utils/time" const api = createAPI() diff --git a/src/modules/v1/schedule/schedule.schema.ts b/src/modules/v1/schedule/schedule.schema.ts index 83bb4ac..61cfa73 100644 --- a/src/modules/v1/schedule/schedule.schema.ts +++ b/src/modules/v1/schedule/schedule.schema.ts @@ -3,7 +3,7 @@ import { scheduleSchema, StationScheduleMetadata, stationSchema, -} from "../../../db/schema" +} from "@/db/schema" export const scheduleResponseSchema = z .object({ diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts index e737c88..3e4e2e5 100644 --- a/src/modules/v1/station/station.controller.ts +++ b/src/modules/v1/station/station.controller.ts @@ -1,9 +1,9 @@ import { createRoute, z } from "@hono/zod-openapi" import { eq, sql } from "drizzle-orm" -import { Station, stationTable } from "../../../db/schema" -import { buildResponseSchemas } from "../../../utils/response" -import { getSecsToMidnight } from "../../../utils/time" -import { createAPI } from "../../api" +import { Station, stationTable } from "@/db/schema" +import { buildResponseSchemas } from "@/utils/response" +import { getSecsToMidnight } from "@/utils/time" +import { createAPI } from "@/modules/api" import { Cache } from "../cache" import { stationResponseSchema } from "./station.schema" diff --git a/src/modules/v1/station/station.schema.ts b/src/modules/v1/station/station.schema.ts index fa7b0c6..f0ceb2a 100644 --- a/src/modules/v1/station/station.schema.ts +++ b/src/modules/v1/station/station.schema.ts @@ -1,5 +1,5 @@ import { z } from "@hono/zod-openapi" -import { type StationMetadata, stationSchema } from "../../../db/schema" +import { type StationMetadata, stationSchema } from "@/db/schema" export const stationResponseSchema = z .object({ diff --git a/tsconfig.json b/tsconfig.json index 2ca47bb..9c7a8eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -100,6 +100,10 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */, + "baseUrl": "./src", + "paths": { + "@/*": ["*"] + } } } From 7b577d0a9f6ef369b1febebcf8ce6f9471b91fc3 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Wed, 13 Nov 2024 14:16:40 +0700 Subject: [PATCH 50/64] chore: update wrangler --- bun.lockb | Bin 93582 -> 93681 bytes package.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/bun.lockb b/bun.lockb index a69dcfe370439fc9cba785f0c549e1d2e36addb0..e875b001ec32dcf97b1b6d594b81f88f5508f4c6 100755 GIT binary patch delta 12794 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 R<4g!2z}%|88o;W({tqZQK=J?p delta 12758 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 n3a75jN3 Date: Wed, 13 Nov 2024 14:16:45 +0700 Subject: [PATCH 51/64] feat: redirect / --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 6f07af9..826cac9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,6 +42,7 @@ const app = api }, }), ) + .get("/", (c) => c.redirect("/docs")) .get("/status", (c) => c.json({ status: "ok" })) .notFound(() => { throw new HTTPException(404, { message: "Not found" }) From b0e07136d8ec8b101d4d03ca775a5676e94608d6 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:37:59 +0700 Subject: [PATCH 52/64] refactor: use timestamp instead for arrival and departutre --- drizzle/migrations/0004_great_vance_astro.sql | 4 + drizzle/migrations/meta/0004_snapshot.json | 323 ++++++++++++++++++ drizzle/migrations/meta/_journal.json | 7 + src/db/schema/schedule.table.ts | 10 +- src/modules/v1/route/route.controller.ts | 17 +- src/modules/v1/route/route.schema.ts | 4 +- .../v1/schedule/schedule.controller.ts | 9 +- src/modules/v1/schedule/schedule.schema.ts | 11 +- src/sync/schedule.ts | 10 +- src/utils/time.ts | 19 +- 10 files changed, 382 insertions(+), 32 deletions(-) create mode 100644 drizzle/migrations/0004_great_vance_astro.sql create mode 100644 drizzle/migrations/meta/0004_snapshot.json diff --git a/drizzle/migrations/0004_great_vance_astro.sql b/drizzle/migrations/0004_great_vance_astro.sql new file mode 100644 index 0000000..de9cecc --- /dev/null +++ b/drizzle/migrations/0004_great_vance_astro.sql @@ -0,0 +1,4 @@ +ALTER TABLE "schedule" ADD COLUMN "departs_at" timestamp with time zone DEFAULT now();--> statement-breakpoint +ALTER TABLE "schedule" ADD COLUMN "arrives_at" timestamp with time zone DEFAULT now();--> statement-breakpoint +ALTER TABLE "schedule" DROP COLUMN IF EXISTS "time_departure";--> statement-breakpoint +ALTER TABLE "schedule" DROP COLUMN IF EXISTS "time_at_destination"; \ No newline at end of file diff --git a/drizzle/migrations/meta/0004_snapshot.json b/drizzle/migrations/meta/0004_snapshot.json new file mode 100644 index 0000000..4c99c04 --- /dev/null +++ b/drizzle/migrations/meta/0004_snapshot.json @@ -0,0 +1,323 @@ +{ + "id": "f1783790-c69c-479f-a903-cc6365ccfed6", + "prevId": "d2d9cd87-d69d-46ff-9890-21be36c0cb3b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "departs_at": { + "name": "departs_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "arrives_at": { + "name": "arrives_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_train_idx": { + "name": "schedule_train_idx", + "columns": [ + { + "expression": "train_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT", + "LOCAL" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 470d374..7bbee0f 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1731404897712, "tag": "0003_first_dorian_gray", "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1731486602497, + "tag": "0004_great_vance_astro", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema/schedule.table.ts b/src/db/schema/schedule.table.ts index b32878a..0a6da77 100644 --- a/src/db/schema/schedule.table.ts +++ b/src/db/schema/schedule.table.ts @@ -45,8 +45,14 @@ export const scheduleTable = pgTable( train_id: text("train_id").notNull(), line: text("line").notNull(), route: text("route").notNull(), - time_departure: time("time_departure").notNull(), - time_at_destination: time("time_at_destination").notNull(), + departs_at: timestamp("departs_at", { + mode: "string", + withTimezone: true, + }).defaultNow(), + arrives_at: timestamp("arrives_at", { + mode: "string", + withTimezone: true, + }).defaultNow(), metadata: jsonb("metadata").$type(), created_at: timestamp("created_at", { mode: "string", diff --git a/src/modules/v1/route/route.controller.ts b/src/modules/v1/route/route.controller.ts index c63f667..7ab9264 100644 --- a/src/modules/v1/route/route.controller.ts +++ b/src/modules/v1/route/route.controller.ts @@ -71,9 +71,7 @@ const routeController = api.openapi( }, }, }, - orderBy: (scheduleTable, { asc }) => [ - asc(scheduleTable.time_departure), - ], + orderBy: (scheduleTable, { asc }) => [asc(scheduleTable.departs_at)], where: eq(scheduleTable.train_id, sql.placeholder("train_id")), }) .prepare("query_route_by_train_id") @@ -95,18 +93,11 @@ const routeController = api.openapi( const response = { routes: data.map( - ({ - id, - station_id, - station, - time_departure, - created_at, - updated_at, - }) => ({ + ({ id, station_id, station, departs_at, created_at, updated_at }) => ({ id, station_id, station_name: station.name, - time_departure, + departs_at, created_at, updated_at, }), @@ -119,7 +110,7 @@ const routeController = api.openapi( station_origin_name: data[0].station.name, station_destination_id: data[0].station_destination_id, station_destination_name: data[0].station_destination?.name ?? "", - time_at_destination: data[0].time_at_destination, + arrives_at: data[0].arrives_at, }, } satisfies Route diff --git a/src/modules/v1/route/route.schema.ts b/src/modules/v1/route/route.schema.ts index 5b717d8..ba075dc 100644 --- a/src/modules/v1/route/route.schema.ts +++ b/src/modules/v1/route/route.schema.ts @@ -11,7 +11,7 @@ export const routeResponseSchema = z station_name: stationResponseSchema.shape.name.openapi({ example: "ANCOL", }), - time_departure: scheduleResponseSchema.shape.time_departure, + departs_at: scheduleResponseSchema.shape.departs_at, created_at: scheduleResponseSchema.shape.created_at, updated_at: scheduleResponseSchema.shape.updated_at, }), @@ -29,7 +29,7 @@ export const routeResponseSchema = z station_destination_name: z.string().optional().openapi({ example: "TANJUNGPRIUK", }), - time_at_destination: scheduleResponseSchema.shape.time_at_destination, + arrives_at: scheduleResponseSchema.shape.arrives_at, }), }) .openapi("Route") diff --git a/src/modules/v1/schedule/schedule.controller.ts b/src/modules/v1/schedule/schedule.controller.ts index b7cb4c8..713887e 100644 --- a/src/modules/v1/schedule/schedule.controller.ts +++ b/src/modules/v1/schedule/schedule.controller.ts @@ -67,7 +67,7 @@ const scheduleController = api.openapi( .select() .from(scheduleTable) .where(eq(scheduleTable.station_id, sql.placeholder("station_id"))) - .orderBy(asc(scheduleTable.time_departure)) + .orderBy(asc(scheduleTable.departs_at)) .prepare("query_schedule_by_station_id") const data = await query.execute({ @@ -81,7 +81,12 @@ const scheduleController = api.openapi( metadata: { success: true, }, - data: c.var.constructResponse(z.array(scheduleResponseSchema), data), + data: c.var.constructResponse( + z.array(scheduleResponseSchema), + data.map((x) => { + return x + }), + ), }, 200, ) diff --git a/src/modules/v1/schedule/schedule.schema.ts b/src/modules/v1/schedule/schedule.schema.ts index 61cfa73..4c0b57c 100644 --- a/src/modules/v1/schedule/schedule.schema.ts +++ b/src/modules/v1/schedule/schedule.schema.ts @@ -1,5 +1,6 @@ import { z } from "@hono/zod-openapi" import { + Schedule, scheduleSchema, StationScheduleMetadata, stationSchema, @@ -37,12 +38,14 @@ export const scheduleResponseSchema = z example: "JAKARTAKOTA-TANJUNGPRIUK", description: "Train route", }), - time_departure: scheduleSchema.shape.time_departure.openapi({ - example: "06:07:00", + departs_at: scheduleSchema.shape.departs_at.openapi({ + format: "date-time", + example: "2024-03-10T09:55:07.213Z", description: "Train departure time", }), - time_at_destination: scheduleSchema.shape.time_at_destination.openapi({ - example: "06:16:00", + arrives_at: scheduleSchema.shape.arrives_at.openapi({ + format: "date-time", + example: "2024-03-10T09:55:09.213Z", description: "Train arrival time at destination", }), metadata: scheduleSchema.shape.metadata.openapi({ diff --git a/src/sync/schedule.ts b/src/sync/schedule.ts index eacea32..eb1db7e 100644 --- a/src/sync/schedule.ts +++ b/src/sync/schedule.ts @@ -129,10 +129,8 @@ const sync = async () => { train_id: d.train_id, line: d.ka_name, route: d.route_name, - time_departure: parseTime(d.time_est).toLocaleTimeString(), - time_at_destination: parseTime( - d.dest_time, - ).toLocaleTimeString(), + departs_at: parseTime(d.time_est).toISOString(), + arrives_at: parseTime(d.dest_time).toISOString(), metadata: { origin: { color: d.color, @@ -147,8 +145,8 @@ const sync = async () => { .onConflictDoUpdate({ target: scheduleTable.id, set: { - time_departure: sql`excluded.time_departure`, - time_at_destination: sql`excluded.time_at_destination`, + departs_at: sql`excluded.departs_at`, + arrives_at: sql`excluded.arrives_at`, metadata: sql`excluded.metadata`, updated_at: new Date().toLocaleString(), }, diff --git a/src/utils/time.ts b/src/utils/time.ts index 7b9bc46..9958a33 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -9,10 +9,23 @@ export function getSecsToMidnight(): number { export function parseTime(timeString: string): Date { const [hours, minutes, seconds] = timeString.split(":").map(Number) + + // Create date object const date = new Date() - date.setHours(hours ?? date.getHours()) - date.setMinutes(minutes ?? date.getMinutes()) - date.setSeconds(seconds ?? date.getSeconds()) + + // Get the timezone offset in minutes (GMT+7 = -420 minutes) + const targetOffset = -420 // GMT+7 in minutes + const currentOffset = date.getTimezoneOffset() + + // Calculate the difference in offset + const offsetDiff = targetOffset - currentOffset + + // Set time components and adjust for timezone + date.setHours( + hours ?? date.getHours(), + (minutes ?? date.getMinutes()) + offsetDiff, + seconds ?? date.getSeconds(), + ) return date } From 1e5e1905dff1b20f3d1c5201a1e2d43db2a3f7d5 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:46:18 +0700 Subject: [PATCH 53/64] feat: date not null --- .../migrations/0005_rare_senator_kelly.sql | 4 + drizzle/migrations/meta/0005_snapshot.json | 323 ++++++++++++++++++ drizzle/migrations/meta/_journal.json | 7 + src/db/schema/schedule.table.ts | 16 +- 4 files changed, 346 insertions(+), 4 deletions(-) create mode 100644 drizzle/migrations/0005_rare_senator_kelly.sql create mode 100644 drizzle/migrations/meta/0005_snapshot.json diff --git a/drizzle/migrations/0005_rare_senator_kelly.sql b/drizzle/migrations/0005_rare_senator_kelly.sql new file mode 100644 index 0000000..3aefcdb --- /dev/null +++ b/drizzle/migrations/0005_rare_senator_kelly.sql @@ -0,0 +1,4 @@ +ALTER TABLE "schedule" ALTER COLUMN "departs_at" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "schedule" ALTER COLUMN "arrives_at" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "schedule" ALTER COLUMN "created_at" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "schedule" ALTER COLUMN "updated_at" SET NOT NULL; \ No newline at end of file diff --git a/drizzle/migrations/meta/0005_snapshot.json b/drizzle/migrations/meta/0005_snapshot.json new file mode 100644 index 0000000..e190837 --- /dev/null +++ b/drizzle/migrations/meta/0005_snapshot.json @@ -0,0 +1,323 @@ +{ + "id": "65f42182-0d09-4f8a-b7f5-f593648f5c31", + "prevId": "f1783790-c69c-479f-a903-cc6365ccfed6", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "departs_at": { + "name": "departs_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "arrives_at": { + "name": "arrives_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_train_idx": { + "name": "schedule_train_idx", + "columns": [ + { + "expression": "train_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT", + "LOCAL" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 7bbee0f..7e7895b 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1731486602497, "tag": "0004_great_vance_astro", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1731487109892, + "tag": "0005_rare_senator_kelly", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema/schedule.table.ts b/src/db/schema/schedule.table.ts index 0a6da77..7a6111a 100644 --- a/src/db/schema/schedule.table.ts +++ b/src/db/schema/schedule.table.ts @@ -48,20 +48,28 @@ export const scheduleTable = pgTable( departs_at: timestamp("departs_at", { mode: "string", withTimezone: true, - }).defaultNow(), + }) + .notNull() + .defaultNow(), arrives_at: timestamp("arrives_at", { mode: "string", withTimezone: true, - }).defaultNow(), + }) + .notNull() + .defaultNow(), metadata: jsonb("metadata").$type(), created_at: timestamp("created_at", { mode: "string", withTimezone: true, - }).defaultNow(), + }) + .notNull() + .defaultNow(), updated_at: timestamp("updated_at", { withTimezone: true, mode: "string", - }).defaultNow(), + }) + .notNull() + .defaultNow(), }, (table) => { return { From 83a80d2f5e620f66168f366345799f9a25f82a83 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:56:36 +0700 Subject: [PATCH 54/64] fix: not null date --- src/db/schema/station.table.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/db/schema/station.table.ts b/src/db/schema/station.table.ts index a346bcd..41e02c5 100644 --- a/src/db/schema/station.table.ts +++ b/src/db/schema/station.table.ts @@ -42,11 +42,15 @@ export const stationTable = pgTable( created_at: timestamp("created_at", { withTimezone: true, mode: "string", - }).defaultNow(), + }) + .notNull() + .defaultNow(), updated_at: timestamp("updated_at", { withTimezone: true, mode: "string", - }).defaultNow(), + }) + .notNull() + .defaultNow(), }, (table) => { return { From 1dc9ad2705b2c1bf27542ac9825c4521004a64e7 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:56:40 +0700 Subject: [PATCH 55/64] fix --- src/modules/v1/schedule/schedule.schema.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/modules/v1/schedule/schedule.schema.ts b/src/modules/v1/schedule/schedule.schema.ts index 4c0b57c..1d12976 100644 --- a/src/modules/v1/schedule/schedule.schema.ts +++ b/src/modules/v1/schedule/schedule.schema.ts @@ -1,10 +1,5 @@ +import { scheduleSchema, StationScheduleMetadata } from "@/db/schema" import { z } from "@hono/zod-openapi" -import { - Schedule, - scheduleSchema, - StationScheduleMetadata, - stationSchema, -} from "@/db/schema" export const scheduleResponseSchema = z .object({ @@ -56,11 +51,11 @@ export const scheduleResponseSchema = z }, } satisfies StationScheduleMetadata, }), - created_at: stationSchema.shape.created_at.openapi({ + created_at: scheduleSchema.shape.created_at.openapi({ format: "date-time", example: "2024-03-10T09:55:07.213Z", }), - updated_at: stationSchema.shape.updated_at.openapi({ + updated_at: scheduleSchema.shape.updated_at.openapi({ format: "date-time", example: "2024-03-10T09:55:07.213Z", }), From 4961097b6c77025c603073926faa8a82a31823bf Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:15:05 +0700 Subject: [PATCH 56/64] fix: derp missing --- .../migrations/0006_hesitant_hedge_knight.sql | 2 + drizzle/migrations/meta/0006_snapshot.json | 323 ++++++++++++++++++ drizzle/migrations/meta/_journal.json | 7 + 3 files changed, 332 insertions(+) create mode 100644 drizzle/migrations/0006_hesitant_hedge_knight.sql create mode 100644 drizzle/migrations/meta/0006_snapshot.json diff --git a/drizzle/migrations/0006_hesitant_hedge_knight.sql b/drizzle/migrations/0006_hesitant_hedge_knight.sql new file mode 100644 index 0000000..aae3167 --- /dev/null +++ b/drizzle/migrations/0006_hesitant_hedge_knight.sql @@ -0,0 +1,2 @@ +ALTER TABLE "station" ALTER COLUMN "created_at" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "station" ALTER COLUMN "updated_at" SET NOT NULL; \ No newline at end of file diff --git a/drizzle/migrations/meta/0006_snapshot.json b/drizzle/migrations/meta/0006_snapshot.json new file mode 100644 index 0000000..70d122e --- /dev/null +++ b/drizzle/migrations/meta/0006_snapshot.json @@ -0,0 +1,323 @@ +{ + "id": "bab82009-af8d-4269-8815-e14cb1f6302d", + "prevId": "65f42182-0d09-4f8a-b7f5-f593648f5c31", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "departs_at": { + "name": "departs_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "arrives_at": { + "name": "arrives_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_train_idx": { + "name": "schedule_train_idx", + "columns": [ + { + "expression": "train_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT", + "LOCAL" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 7e7895b..1d94adc 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1731487109892, "tag": "0005_rare_senator_kelly", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1731489275577, + "tag": "0006_hesitant_hedge_knight", + "breakpoints": true } ] } \ No newline at end of file From 3812318822c6f2c658faec7a09f4a89b0bf8afae Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:15:59 +0700 Subject: [PATCH 57/64] fix: change station metadata --- src/db/schema/station.table.ts | 2 +- src/modules/v1/station/station.schema.ts | 2 +- src/sync/schedule.ts | 2 +- src/sync/station.ts | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/db/schema/station.table.ts b/src/db/schema/station.table.ts index 41e02c5..d211b1b 100644 --- a/src/db/schema/station.table.ts +++ b/src/db/schema/station.table.ts @@ -13,7 +13,7 @@ import { z } from "zod" /** Station Metadata */ const stationMetadata = z.object({ /** Comuline metadata */ - has_schedule: z.boolean().nullable(), + active: z.boolean().optional(), /** Origin metadata */ origin: z.object({ /** KRL */ diff --git a/src/modules/v1/station/station.schema.ts b/src/modules/v1/station/station.schema.ts index f0ceb2a..7c82cad 100644 --- a/src/modules/v1/station/station.schema.ts +++ b/src/modules/v1/station/station.schema.ts @@ -19,7 +19,7 @@ export const stationResponseSchema = z metadata: stationSchema.shape.metadata.openapi({ type: "object", example: { - has_schedule: true, + active: true, origin: { daop: 1, fg_enable: 1, diff --git a/src/sync/schedule.ts b/src/sync/schedule.ts index eb1db7e..b1a59ff 100644 --- a/src/sync/schedule.ts +++ b/src/sync/schedule.ts @@ -170,7 +170,7 @@ const sync = async () => { metadata: metadata ? { ...metadata, - has_schedule: false, + active: false, } : null, updated_at: new Date().toLocaleString(), diff --git a/src/sync/station.ts b/src/sync/station.ts index 60d3648..1c03dde 100644 --- a/src/sync/station.ts +++ b/src/sync/station.ts @@ -65,7 +65,7 @@ const sync = async () => { name: s.sta_name, type: "KRL", metadata: { - has_schedule: true, + active: true, origin: { fg_enable: s.fg_enable, daop: s.group_wil === 0 ? 1 : s.group_wil, @@ -82,7 +82,7 @@ const sync = async () => { name: "BANDARA SOEKARNO HATTA", type: "KRL", metadata: { - has_schedule: true, + active: true, origin: { fg_enable: 1, daop: 1, @@ -96,7 +96,7 @@ const sync = async () => { name: "CIKAMPEK", type: "LOCAL", metadata: { - has_schedule: true, + active: true, origin: { fg_enable: 1, daop: 1, @@ -110,7 +110,7 @@ const sync = async () => { name: "PURWAKARTA", type: "LOCAL", metadata: { - has_schedule: true, + active: true, origin: { fg_enable: 1, daop: 2, From 7f2c38768610f123233f7417c82f96b86dc0231b Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:46:33 +0700 Subject: [PATCH 58/64] feat: add tsup and build cmd --- .gitignore | 3 +++ bun.lockb | Bin 93681 -> 135153 bytes package.json | 3 +++ tsup.config.ts | 11 +++++++++++ 4 files changed, 17 insertions(+) create mode 100644 tsup.config.ts diff --git a/.gitignore b/.gitignore index 2503d14..e9a89b5 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ package-lock.json wrangler.toml .wrangler .dev.vars + +# build +.dist \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index e875b001ec32dcf97b1b6d594b81f88f5508f4c6..7982f2fb657d3af2e28259dbb938bd64535ad9d7 100755 GIT binary patch delta 38332 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 QFwx^IYh1CwRqp5c-^Yvx`2YX_ diff --git a/package.json b/package.json index a84f3d5..d95922b 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "version": "2.0", "scripts": { "dev": "wrangler dev src/index.ts --port 3001", + "build": "tsup --clean", "docker:up": "docker-compose -p comuline-api up -d", "docker:down": "docker-compose down", "test": "echo \"Error: no test specified\" && exit 1", @@ -40,6 +41,8 @@ "husky": "^9.0.11", "lint-staged": "^15.2.2", "prettier": "^3.2.5", + "tsup": "^8.3.5", + "typescript": "^5.6.3", "wrangler": "^3.86.1" }, "module": "src/index.js", diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..3c31d34 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig, type Options } from "tsup" + +export default defineConfig((options: Options) => ({ + entry: ["src/index.ts"], + format: ["esm"], + minify: true, + outDir: ".dist", + clean: true, + metafile: true, + ...options, +})) From fb0b4b4af1b0eab15e16c123026d2f90f3f5441b Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Thu, 14 Nov 2024 12:28:27 +0700 Subject: [PATCH 59/64] feat: add cors --- src/index.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 826cac9..2071d8c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import { Database } from "./modules/v1/database" import { HTTPException } from "hono/http-exception" import { constructResponse } from "./utils/response" import { trimTrailingSlash } from "hono/trailing-slash" - +import { cors } from "hono/cors" const api = createAPI() const app = api @@ -23,6 +23,14 @@ const app = api ], })) .use(trimTrailingSlash()) + .use("*", async (c, next) => + cors({ + origin: (o) => o, + allowMethods: ["GET", "OPTIONS", "POST", "PUT", "DELETE"], + allowHeaders: ["Origin", "Content-Type"], + credentials: true, + })(c, next), + ) .use(async (c, next) => { const { db } = new Database({ COMULINE_ENV: c.env.COMULINE_ENV, From 463057d1edeb9a39ed376f05102b6fe71f8f5b76 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Sun, 24 Nov 2024 17:26:27 +0700 Subject: [PATCH 60/64] fix: define typesafe metadata --- src/modules/v1/schedule/schedule.schema.ts | 11 +++++++++++ src/modules/v1/station/station.schema.ts | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/modules/v1/schedule/schedule.schema.ts b/src/modules/v1/schedule/schedule.schema.ts index 1d12976..dac67b6 100644 --- a/src/modules/v1/schedule/schedule.schema.ts +++ b/src/modules/v1/schedule/schedule.schema.ts @@ -45,6 +45,17 @@ export const scheduleResponseSchema = z }), metadata: scheduleSchema.shape.metadata.openapi({ type: "object", + properties: { + origin: { + type: "object", + properties: { + color: { + type: "string", + nullable: true, + }, + }, + }, + }, example: { origin: { color: "#DD0067", diff --git a/src/modules/v1/station/station.schema.ts b/src/modules/v1/station/station.schema.ts index 7c82cad..9e5e7bc 100644 --- a/src/modules/v1/station/station.schema.ts +++ b/src/modules/v1/station/station.schema.ts @@ -18,6 +18,21 @@ export const stationResponseSchema = z }), metadata: stationSchema.shape.metadata.openapi({ type: "object", + properties: { + origin: { + type: "object", + properties: { + daop: { + type: "number", + nullable: true, + }, + fg_enable: { + type: "number", + nullable: true, + }, + }, + }, + }, example: { active: true, origin: { From 24a0f28d089815834694795766aca7e618d8e5ff Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Sun, 24 Nov 2024 17:52:54 +0700 Subject: [PATCH 61/64] fix: `station_origin_id` & `station_destination_id` not null --- .../migrations/0007_naive_pepper_potts.sql | 2 + drizzle/migrations/meta/0007_snapshot.json | 323 ++++++++++++++++++ drizzle/migrations/meta/_journal.json | 7 + src/db/schema/schedule.table.ts | 18 +- 4 files changed, 340 insertions(+), 10 deletions(-) create mode 100644 drizzle/migrations/0007_naive_pepper_potts.sql create mode 100644 drizzle/migrations/meta/0007_snapshot.json diff --git a/drizzle/migrations/0007_naive_pepper_potts.sql b/drizzle/migrations/0007_naive_pepper_potts.sql new file mode 100644 index 0000000..818eb18 --- /dev/null +++ b/drizzle/migrations/0007_naive_pepper_potts.sql @@ -0,0 +1,2 @@ +ALTER TABLE "schedule" ALTER COLUMN "station_origin_id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "schedule" ALTER COLUMN "station_destination_id" SET NOT NULL; \ No newline at end of file diff --git a/drizzle/migrations/meta/0007_snapshot.json b/drizzle/migrations/meta/0007_snapshot.json new file mode 100644 index 0000000..beb332d --- /dev/null +++ b/drizzle/migrations/meta/0007_snapshot.json @@ -0,0 +1,323 @@ +{ + "id": "511ffc79-573f-46fb-adaf-1de89c7f7d69", + "prevId": "bab82009-af8d-4269-8815-e14cb1f6302d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "departs_at": { + "name": "departs_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "arrives_at": { + "name": "arrives_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_train_idx": { + "name": "schedule_train_idx", + "columns": [ + { + "expression": "train_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT", + "LOCAL" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 1d94adc..7b19e5b 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -50,6 +50,13 @@ "when": 1731489275577, "tag": "0006_hesitant_hedge_knight", "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1732445107060, + "tag": "0007_naive_pepper_potts", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema/schedule.table.ts b/src/db/schema/schedule.table.ts index 7a6111a..04cbd9e 100644 --- a/src/db/schema/schedule.table.ts +++ b/src/db/schema/schedule.table.ts @@ -30,18 +30,16 @@ export const scheduleTable = pgTable( .references(() => stationTable.id, { onDelete: "cascade", }), - station_origin_id: text("station_origin_id").references( - () => stationTable.id, - { + station_origin_id: text("station_origin_id") + .references(() => stationTable.id, { onDelete: "set null", - }, - ), - station_destination_id: text("station_destination_id").references( - () => stationTable.id, - { + }) + .notNull(), + station_destination_id: text("station_destination_id") + .references(() => stationTable.id, { onDelete: "set null", - }, - ), + }) + .notNull(), train_id: text("train_id").notNull(), line: text("line").notNull(), route: text("route").notNull(), From b756bfe60e901e76af11646a0bb3dd99d62c4385 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:31:35 +0700 Subject: [PATCH 62/64] fix: date --- src/sync/schedule.ts | 4 ++-- src/sync/station.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sync/schedule.ts b/src/sync/schedule.ts index b1a59ff..3e4584e 100644 --- a/src/sync/schedule.ts +++ b/src/sync/schedule.ts @@ -148,7 +148,7 @@ const sync = async () => { departs_at: sql`excluded.departs_at`, arrives_at: sql`excluded.arrives_at`, metadata: sql`excluded.metadata`, - updated_at: new Date().toLocaleString(), + updated_at: new Date().toISOString(), }, }) .returning() @@ -173,7 +173,7 @@ const sync = async () => { active: false, } : null, - updated_at: new Date().toLocaleString(), + updated_at: new Date().toISOString(), } await db .update(stationTable) diff --git a/src/sync/station.ts b/src/sync/station.ts index 1c03dde..3a1cc25 100644 --- a/src/sync/station.ts +++ b/src/sync/station.ts @@ -127,7 +127,7 @@ const sync = async () => { .onConflictDoUpdate({ target: stationTable.uid, set: { - updated_at: new Date().toLocaleString(), + updated_at: new Date().toISOString(), uid: sql`excluded.uid`, id: sql`excluded.id`, name: sql`excluded.name`, From 250edd01bfd0d983e1fbee8affc27bd2f9244e06 Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Sat, 30 Nov 2024 14:03:01 +0700 Subject: [PATCH 63/64] feat: add github actions --- .github/workflows/api.build.yml | 28 ++++++++++++++++++++++++++++ .github/workflows/api.deploy.yml | 28 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 .github/workflows/api.build.yml create mode 100644 .github/workflows/api.deploy.yml diff --git a/.github/workflows/api.build.yml b/.github/workflows/api.build.yml new file mode 100644 index 0000000..6d43e30 --- /dev/null +++ b/.github/workflows/api.build.yml @@ -0,0 +1,28 @@ +name: Build API on Pull Request + +on: + pull_request: + branches: ["main"] + types: [opened, reopened, synchronize] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref || github.run_id }} + cancel-in-progress: true + +jobs: + build-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: "latest" + + - name: Install package deps + run: bun i + + - name: Build package + run: bun run build diff --git a/.github/workflows/api.deploy.yml b/.github/workflows/api.deploy.yml new file mode 100644 index 0000000..a36a489 --- /dev/null +++ b/.github/workflows/api.deploy.yml @@ -0,0 +1,28 @@ +name: Deploy API to Cloudflare Workers + +on: + push: + branches: ["main"] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Installing Bun + uses: oven-sh/setup-bun@v2 + + - name: Install package deps + run: bun i + + - name: Build package + run: bun run build + + - name: Deploy to Cloudflare Workers + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: deploy --minify src/index.ts From 6bd135dccb1153dc184febbf3a9d6b416b29b57e Mon Sep 17 00:00:00 2001 From: Abiel Zulio M <7030944+abielzulio@users.noreply.github.com> Date: Sat, 30 Nov 2024 14:14:31 +0700 Subject: [PATCH 64/64] add docs --- README.md | 105 ++++++++++++++++++------------------------------------ 1 file changed, 34 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 8dff5db..679579e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # @comuline/api -An API to get the schedule of KRL commuter line in Jakarta and Yogyakarta using [Elsyia](https://elysiajs.com/) and [Bun](https://bun.sh/), deployed to [Render](https://render.com/). This API is primarily used on the [web app](https://comuline.com/) ([source code](https://github.com/comuline/web)). +An API to get the schedule of KRL commuter line in Jakarta and Yogyakarta using [Hono](https://hono.dev/) and [Bun](https://bun.sh/), deployed to [Cloudflare Workers](https://workers.cloudflare.com/). This API is primarily used on the [web app](https://comuline.com/) ([source code](https://github.com/comuline/web)). ### How does it work? @@ -8,11 +8,11 @@ This API uses a daily cron job (at 00:00) to fetch the schedule of KRL commuter ### Technology stacks -1. [Elsyia](https://elysiajs.com/) API framework +1. [Hono](https://hono.dev/) API framework 2. [Bun](https://bun.sh/) runtime -3. PostgresSQL ([Neon](https://neon.tech/)) -4. Redis ([Upstash](https://upstash.com/)) -5. [Render](https://render.com/) deployment platform +3. (Serverless) PostgresSQL ([Neon](https://neon.tech/)) +4. (Serverless) Redis ([Upstash](https://upstash.com/)) +5. [Cloudflare Workers](https://workers.cloudflare.com/) deployment platform 6. [Drizzle](https://orm.drizzle.team/) ORM ## Getting Started @@ -31,42 +31,44 @@ git clone https://github.com/comuline/api.git bun install ``` -3. Run database locally +3. Copy the `.dev.example.vars` to `.dev.vars` -```bash -docker-compose up -d +``` +cp .dev.example.vars .dev.vars ``` -4. Copy the `.env.example` to `.env` +4. Generate `UPSTASH_REDIS_REST_TOKEN` using `openssl rand -hex 32` and copy it to your `.dev.vars` file -``` -cp .env.example .env +5. Run database locally + +```bash +docker-compose up -d ``` -5. Run the database migration +6. Run the database migration ```bash -bun db:generate && bun db:migrate +bun run migrate:apply ``` -6. Sync the data and populate it into your local database (once only as you needed) +7. Sync the data and populate it into your local database (once only as you needed) ```bash # Please do this in order # 1. Sync station data and wait until it's done -curl --request POST --url http://localhost:3001/v1/station/ +bun run sync:station # 2. Sync schedule data -curl --request POST --url http://localhost:3001/v1/schedule/ +bun run sync:schedule ``` ### Deployment -1. Create a new PostgreSQL database in [Neon](https://neon.tech/) and copy the connection string value as `DATABASE_URL` in your `.env` file +1. Create a new PostgreSQL database in [Neon](https://neon.tech/) and copy the connection string value as `DATABASE_URL` in your `.production.vars` file 2. Run the database migration ```bash -bun db:generate && bun db:migrate +bun run migrate:apply ``` 3. Sync the data and populate it into your remote database (once only as you needed) @@ -74,70 +76,31 @@ bun db:generate && bun db:migrate ```bash # Please do this in order # 1. Sync station data and wait until it's done -curl --request POST --url http://localhost:3001/v1/station/ +bun run sync:station # 2. Sync schedule data -curl --request POST --url http://localhost:3001/v1/schedule/ - +bun run sync:schedule ``` -4. Generate `SYNC_TOKEN` (This is used in production level only to prevent unauthorized access to your `POST /v1/station` and `POST /v1/schedule` endpoint) +4. Add `COMULINE_ENV` to your `.production.vars` file -```bash -openssl rand -base64 32 -# Copy the output value as a `SYNC_TOKEN` +``` +COMULINE_ENV=production ``` -2. Create a new Redis database in [Upstash](https://upstash.com/) and copy the connection string value as `REDIS_URL` +5. Create a new Redis database in [Upstash](https://upstash.com/) and copy the value of `UPSTASH_REDIS_REST_TOKEN` and `UPSTASH_REDIS_REST_URL` to your `.production.vars` file -3. Create a `Web Service` in [Render](https://render.com/), copy the `DATABASE_URL`, `REDIS_URL`, and `SYNC_TOKEN` as environment variables, and deploy the application. +6. Save your `.production.vars` file to your environment variables in your Cloudflare Workers using `wrangler` + +```bash +bunx wrangler secret put --env production $(cat .production.vars) +``` -4. Set the cron job to fetch the schedule data using [Cron-Job](https://cron-job.org/en/). Don't forget to set the `SYNC_TOKEN` as a header in your request. Add the `?from_cron=true` query parameter to flag the request as a cron job request. +6. Deploy the API to Cloudflare Workers ```bash -# Example -curl --request POST --url https://your-service-name.onrender.com/v1/station?from_cron=true -H "Authorization: Bearer ${SYNC_TOKEN}" -curl --request POST --url https://your-service-name.onrender.com/v1/schedule?from_cron=true -H "Authorization: Bearer ${SYNC_TOKEN}" +bun run deploy ``` ### Database schema -> **Station** - -| Column Name | Data Type | Description | -| ------------ | --------- | ------------------------------- | -| id | TEXT | Primary key (Station ID) | -| name | TEXT | Station name | -| daop | INTEGER | Station regional operation code | -| fgEnable | BOOLEAN | - | -| haveSchedule | BOOLEAN | Schedule availability status | -| updatedAt | TEXT | Last updated date | - -> **Schedule** - -| Column Name | Data Type | Description | -| --------------- | --------- | ----------------------------------- | -| id | TEXT | Primary key (Station ID + Train ID) | -| stationId | TEXT | Station ID | -| trainId | TEXT | Train ID | -| line | TEXT | Train commuter line | -| route | TEXT | Train route | -| color | TEXT | Commuter line color | -| destination | TEXT | Train destination | -| timeEstimated | TIME | Estimated time | -| destinationTime | TIME | Destination time | -| updatedAt | TEXT | Last updated date | - -> **Sync** - -| Column Name | Data Type | Description | -| ----------- | --------- | -------------------------------------- | -| id | TEXT | Primary key (Sync ID) | -| n | BIGINT | n of sync | -| type | ENUM | Sync type (manual, cron) | -| status | ENUM | Sync status (PENDING, SUCCESS, FAILED) | -| item | ENUM | Sync item (station, schedule) | -| duration | BIGINT | Sync duration | -| message | TEXT | Sync message (if status failed) | -| startedAt | TEXT | Sync started date | -| endedAt | TEXT | Sync ended date | -| createdAt | TEXT | Sync created date | +> TBD