From 740bbc7affb5ef1fd6586c0affb87bba451e52b7 Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Mon, 13 Jul 2026 21:49:18 -0700 Subject: [PATCH] Move Camino package codegen into package runtime --- .yarn/install-state.gz | Bin 8348 -> 13737 bytes flake.lock | 10 +- flake.nix | 2 +- package.json | 6 +- src/codegen-ts-runtime.ts | 414 +++++++++++++++++++ src/schema-compiler.ts | 809 ++++++++++++++++++++++++++++++++++++++ src/schema-ir.ts | 148 +++++++ yarn-project.nix | 13 + yarn.lock | 110 ++++++ 9 files changed, 1505 insertions(+), 7 deletions(-) create mode 100644 src/codegen-ts-runtime.ts create mode 100644 src/schema-compiler.ts create mode 100644 src/schema-ir.ts diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index 93cecc4f2cd532b3538459aec7869b59b2b5b10f..5b4694f3804f22899d66d098522e168166960522 100644 GIT binary patch literal 13737 zcmb2|=3oE==C^aR`-Asj|JT3Ydi~%2`Zp=>PxJTp-Iw(DtN-=#<+CRjx1U~Cll%Vs z{{7`Ka=m+-_r_bieEi)1*X8!dU!G5!fBW~Vd%OR9xp?y9y~e+*)vNVCNBbYU{;lqD zoPTPVc+ytAHcRK1%6<2?%v^h2Y);KG`d%|dI`>@KOrEE~tJH41jQ$ptZvFa%>8BaRDW!Zk4id*GuMmd?!S+?UfMQ`l?w6nU_n=xcX?d=G4I3b3)Ucyo-Om`tjXvw`Fei z{M+>h#QC4hU!Uvee_Z%a{rhfl{du*uA1<|52HD1}_VT^#?s0e9%{0%A?wm}=j_;WJ zO1AvH+LHyL7s_Vb3Mw=EotSgs>Wf9~p{~~Pp&q7FTYjHiD0^^m)26kNF~Y^AZ;u63 zt#X|5klE*zssFFmTl0(KlX9<?n+?O9X*u2aor;oW}6e&_u>J7>nx%*ExlJ7094n#s

w{CM%D9IubZS1`EjHx+AHSi5er=Yc%KHi^p|y991rV@j&xyISIPap|;~7R$34 z{@yCEl}eu+y3xQz3qKUsF? zfWv!3)yDfZm-jw%NHb+vCgrAicScP6^_#B_T+Q=R$zupy9Nc+e!Mp9U^=DQ++?UF9 z^TsmOrJfghe@Ga&zA@at_P`wZeG_hWN|}d*nTbzQ3SsD;bY~$)&&)3(%OBq?;ZuG& zOKZ*o&9udu^Ef58m;GR9`gr@Yt5DFcw?8CPx7*I}x|S!L-K4j*_@(<;<^#zN1T5lEll($Vx^d`rWL)ylIK{JI|mBll#D3u+0jlBq@gy&Boh#=4G^K0L8M`+q@G(;G`kx!wmk zPD!N<+aI(nyz25UJjL?%nnO<&TPuPut@7>Po^jy7r1l3#H+XKE)Ma$VsDoE zbTo?ygq9nZzc*!I(b{#X`FO8_|ApQZ4*N6gYo7bK@rO<1PtY#@Hp|Drq9W&l#D|P& zeia(rhjt(8kL-NbCHBFmk#URgLB8c7+vWb36sQY{H1Kl<3F~Velyq2LJv;IJF@aVA zmDeZMF+EKXnvkm9sdw}e>n68O3%ip#(+=pIJ2N>=i22k}=Oc5(vL)uMn_@CW*J`nS ziIc>hia*u!>uZX3Sz6iMozl+#{ofPAtXJpf)xSTu^u5fT|IUeFGuKFzb7$~GVMbn|J@#(}IDKVFT%ai!I?T<5?*PX0>n)_k9Si308 z92EW((UCk7r9?7CGE$WSUU3cd=4!;*1jqIy4;EtuAj?n|3j% zAj)Z0Vsey1(H5>eHjTv1$3*8a)H1&7xn^@`?dRPuJRELUrKfM#NWRY=!TzW9o{REg zq4syOaTTvk0*faV@UccEcrYHBXL{1Bf?M5J}sxzigSg?#^S&Fen^QZfnqS)%tMGR>`UG?V1Jl2h*7?98WLc;r;CH zbj`BWZxgdB%eP4{+`cv{Zhdu8K&3n>c#ExH>c{#eqF!P9m6$^EGR#6z>0Y$w{E z*#D#`Ox8o;?_{|Nj(ep0mAoYvcPB8I6db(WXK#M!n(^l!Y{v41MKVnR%`ZPz@~(Kv zve|t(i_8MH*(x5k_Z*KY7bHoP&R{f`5n7`V_C15q)knb0FZl(7Vq#nIYvnI2|6LDF zH2H1v`P#?spWeqrE<5TT_}<%XOHm`1HkwG}t9ndUWSMx`+9_D#%O~G`$@!2+&xq6yy)E3LeBM77%aj0uBCn48kW=qJYBM$%Gg6Hb`# zSUX!K>qn@WptYgUmcpi$n(UR0|uuh!Sq4!6H_ zYVqR7lg;I)fAg!%-t+Ux%a6}GuJ6AwU-EO!pX}Mi@1Lqa{jqlA_n-0Bm7mUOK9*(w z{eHiWVS_%CXaE!I3#}VfsuP`-vFY(XE%Fm+>Tx|K{xUq^VvF<9<40>w2t0IfSt`f$ zZ3X*t8~&q9rZBu_v_H9JiHXUHGm(?Ghi>|y`(*m*z}&N&y{sPZl33wvnwWgo;aj?c zwsmx;BYUL7>GF$*kIX-K8FvGx$|Mkat2ifoP^zigFMmA;DoMz>{t9s<*u0<_| z@~<-0PIH`Omdl%{_v~l!Q&5ktqUq$hWxpG-8o z$*A>2mV?LAL3F{X>9hDfrfp17-?uJkSKxNF=_^bk=9zOYF^HY`QX`C~#ivy1Zn~rN zY@O4I%y*_t2-u{+zO!xC%LfL94vQij!}e*)oRqlI>9eN&Y?v5&hC4*KQ-^+5dlvD1}o7i7ucq3 z;d`RAqKTu>^rWLyC+~$FP2ysnfqD*duENSk682_zsW8s0=ZV~-a^S*)D3Q$RUym)= zz2?HxoS`cg)55}?pdLAWclQ;lNx$@ zHc2q9OgBj|o>ar9zr*f9wuaXtk>JDbrXi8n{>3!RU7Tr@dCOTkU@EIGW24gk*dmie-K7acT7(olg`dPj{Gh=>LSBds5wlCqCB|GW2fAVDDzEjXAt_gM_Be!mG-b zs~w`F9i>h*nOs^OZDAp$Vb0z@;dqvUdVuDpgR@k0I^P&g31SZ1X?if$Okqvwp5qP< z%7?pFT;l6^X<_S?!KU&~WLAn07l)tgjYB~O>kYKke6wFn5anz>)8@(ajYrpbJ&)+0N?(v{sxvbJ->l8=mwbx%*8A)qwf zK1c?)c=2b_Swgm2b2zrElO}yJaH}8LnHWD#MI19U$wNu zIx{0;0coebDI^l4_FwSiMqKa^!u{bNzA7N7p)O7N|m@_mbv;A zo8k$1A+Dm+D}!3L``+6iCS<5MEnOrqHPx~-Y}s6y>l1<(E<2{~H!8}+i&uQ|>Y*wx87+Esb;`gOa9>pYzUrp2=OuM7F~ZNl2X z6lc~YT2YGisf#@&OmB7Un9(V|NT*C%@Q};qAU`Ijg%XX=ZeBL?65%LTicY(`+Gr6qi6=t$*m{-yc2GSp%iVEu$UAq=vusk1XB zPN)@2~}JG!0lf(Ml=ew!!x2 zC7oAO)z;6vyfwGnaQXe37_+x;oB5CZ;#K>3{H;0v-@1F-^J8>g{+q$kd-mc-f2L=D zjy3bw{aY>;9>DyDYw4lYJ`I1hpD%8+oqcU-8bcV*yW^VH?y{S(dFi~v05pnFMm&W_GahTyWenS-+uS=L)^2! zK2uL#Gd2t7Igz#TJi}g@C{KxP;#Vscb4BY-3oqTyz|&NIB7cI*qV^*q z>$BZ7R!v*dbv#`sH?+@T_O#lnNlV|Toy@YTDu4D?RrHcbf^qo*e2ET49om%>&%54Aq+VZ&e6WO=l|Fl{C{qWI85)T!Qau)Eh z+B-`t>74Wm;Bi~}#=&=y?~ zK8>|_E2H*7mlWTVeJRg_A`ISbQD2k0o7?}#9GCA#Z%8z+9!Hsq4O6YOraa>c$+VY;!o;GIHTbCD&=czPj99ofE=8 zA;Ew{No8eA%#7N;+uV`I7!K}Bn-`b|UdA`<$IK0@5!;YPBWBUD^}U;VX3W%PQ`}2km(H;?{ZJopovY!z%y# zyVA_Ao8A5#@pi^JW8WkBe9B9h!{vfzKiqOFm&dBylQ9t|Y4Y z&@;`cQv`L>+Ghnl>#S8-XtF=RPSGjl@X7c}9%k=JYlH8Jtzz8WUpG5(XVw~bHM5ml z7#2A3nR&h3`Xq)wbmj>O(IPhqUY?cQ6Q}RmBxda?tDbTFcaZL44*QCX1i7WmjUn+% z6`$IAeVJruEv7J4!g2CiUCXuC_nFT9E+0F+{Q9<=S9`lR@AL_Bd8oZ|_swfdJ!(A~ zZEqEwD&r5)jas;ZPhz4`=>3$vGPAv+G}vMz)yfP*!?>rfz1s3(_A1FW*A_6HGI_|M zbudB8@~HJfS>0O#v!*3qJbyNItK0$Cm)<*#SGo1s-Y{%$Vt;%nK>Lnen#89GkBmB| zC{;DBd3XJt&@FY5)Kq_`)14h^*{5u`C|q*7H^4;y^i&PzRb z(6Q9`^Cxiyja3p0itm*@N&UXrczaN`Vs+S)IO(meukP6FUVo*?i)lyR-O5zqdnQ|X z7qs^^gh{nGoS0a8;H|>e&Cc8YFHoqOvPDGJ`snjnGVMWP7MxAeMRo@gLOxA*OS;ve zwAph8cb4huzK@kxJEo>yciycRx_wEPjiBl6VC&Sy=Qyu;Jk63`#guTE=}@c-Bh!fx zv|bIyIH zUh6)KOot=0w0EBrc%V~V5I61l57$rLYttUQ(kPcmT(RkWa@(8jr-j^E+nF1MHiWJ( zsxUA{@je+}dW&l$#TH zxPFgJ^p`lXPezsPDXOmLHl>_ibD*@v^jgS_ds>1KG7oRNraa4d-4wa=knPhaee*WE zTv6k!In$ImMM$-gM^hn9!GY()`Oh!JQ}0fH?hq6geQfJG*8Dk;ONpcWXAIwF@HNa?(h)Li&5TFReu_JyUPpLXr7je$IjPX^Hl@K)x9`Y}I|d1H z?wddQYaZu!JNb{DMey^dwQp^IH_e;Pb|EzGwA#$%X{RI(wwx>2^g=pj+7eOb!XyQk zPzBM7cNDZ&v)oCVCxT+(M`#hYyA z_diuCRhG-0cYckP{|>$!Z{~G@x_)feZpx)@FFe;*{r%N~4?(l(5VGrX-)* zG%=RpU#{l?-AJQZ4a)>0DxE zm}j_nHe;FinrX~tPT^gBMK$qahn)moDY4savElMzeS4rGp?U5Ghe)gFUu!~7ZEL>v z#6r3B&bB2tT~AK1j$UoJ^U?Nj^;L6DyR4O9&37;pIr2+qt%2=^!&<9_Yc`b}ocw6F zjCaDMS${m4n2!fo9{c<_&*L$N_M5{=mo=6iyZk9*o9L9unx1~aCvL`Oiv~0-p73IB z<;5fO=Gg2>OS*3M;A*Gs?I+s1erK$jBg5gjB6I3hs{%Rcw9ri{x(^@k{FV|BuXppx z+=4PqJ&#SoqN_d5PF%obIos{u{0YJsX{MO& z=M_P7H##p9DZDP>P{>hd8Rui?^uBsyW61IZ@)Z)%1Lxa!~LNx7u)+ong)(^Pk1 zZgpHs%B$c$)s}?| zoBM3CpWee|D`LC!c=HwO(2(C|wY!2_3$t&(OY)1KZuxiR`t05E=MRf|JM#P#+&brq zo93!Cg^IQ8k5eXAE|8nV{V1VG>)N*#F303EJX&t^+0X2;QsR`q!uYLmsd`JDME54) ztsK=8i`SoU4_E#DJhe0PM0EVhg{sy~Mn%(ZunA3_(jN9u{Kg)yWjaCCD(#(f4CD<& zIm|XVb3bO`WUb>?Z z{W0rdHm`@lrpSd$SvzM1&szQBSVQVsS$D0XFRQd{t1dF8ofb*G88&_LzlGAWg|GdO z9eQ*tY!7GS?M;&{=7;ZNR$8a06eyxDv=@2=`<5jB)<2|mRt_3zoA6@?-% zxr75B)}&@nSQQ;~-N3=uA?@((?53v&OI#Wz$V5-7YBzLPXdNfEb<@{*v#-8ip8lMD za=t_P>%gd_kJ4f~hs>*{e{-<2R{g5Md^i3{%Z@84p4>(kPjC8gD>yvp+U!{+`!zhb zv7W41e6lg6BR=n-4(rU7UdsIIPb$ucIrVvo;iR>;7ibZH;c(qO?Em;N2RYSC(Fx^VZCjduc>&bTTGFJ|xXXNpa` z)3H1}e(ReDlX7R5yqsi~x+&RQ$ZNXHDn%K~V~qxV8>ZzgO}thllvCaL`Q5y-=-U&- z_kEka`+4?rAD2_yGiOfRde&%OjQ8|u`p)ORXlW{```%ETyG$$R{0XL(X|nBIm)5>M97gs1IU9d$5M2`%w8c9nHPb8o*On;lNAs6V?V4`oA9-NnV<*2wS2T62l_!WFV869! z>Q=^utX5a-6u&On;MuV{z-@Z3YT9%;kn7fMH`iHRyY=_%y&W;NVS6M2YPYOgEpw z6vEH>GH5C1x0tMhU26jjSB5jWFWtVUBsJ{P6z0iST6;d2hizywS;l;cQ{GUZ_sJc- zITd$1z1hnYBHtenxF$AXw_(fG**0%nia%aGAN$Vb_wB3am%lwC5N3aUYn}X~Sc|Q@ zg!jG+w-?_g-TUl%-n%W*-&NMuUdd2@=NYwkQ@y&LX4Ky&r~K}SZLKQ2?RTel^Do1# zb9d~?`D(n)x^Uv%SFDZJiLF<=XOusBw&CjO&*hJ}wqHHp{q{(2)9U#LZ+1$vhTA7? z6Argt7ZdOL<{EESCnx+S$F6Ex1H+Aed>>Q*w(Y(xwra#pZaol_2bq0+Lr2j zzyCU1^ygaeyeHz*xIQdD{$6JOpIaaAWrNo3{G9r|;?MNDpZc}W!>1*E+-z;&|8uUL zTz~SiRkCv{&)wR-L-ya?>gw0#fp&k^R$CWjy!>$g{_V!kI_>`--s6)gwfNKj{oA$u zJkLM8zjwRQ`BD3R-Ng~VPxT+ZzjOQcI{&MO_m{tapj{nbWpiwDetg>|=ABo*U*fj6 zH*<~q^Z2%X<;ULh*A_os|Mu^TN#@OJgW=7&i=i>s;c1s zU;TFut?y;`OexmBzb^OS>7Qkpb-QCuRchXzSNpeSn{D>R0?n)c&Y0J~yshuG_0fF! ziI?SS(mw9lx_ZgGJGLCrb=$Z7)%$&SY1mJBi+kG7_20~SzBcx7tvf$M{?Gfj!uB*( zpKd(iy`%iw*W(Xstr^72`Sz~M3^hnt&19zOad&HsS5hdy@{AW&CVpS}+MPPRn;k+g zxnzcCtUe@pqWS1V!NN=>#n}&cq*n)YEORL zJa{jh@9btbhwRgB{)ZZK6VLi;J?on4C9(M85|_>2K0MYmQf zgKwWSOL7;V!>sFZJI{D~W*y`Bb?48PFxJ^8qrAWF){>MwbXe8y%F{c_I|>cg>(>`J zNpUu28C<$`)nQTBm0OxC_P?oSYkkjXR3x;HO=($6O3LS{2Oe4nwMQp;O}!(z^~KJE z|Cdf&$f0bwR_9Hc|CEQ3MGQyM=a~G8=q>p;B(&+)ZXG zvDSYUo?gi3^S&$RikW$9S#&$iMcu z!R0L{&ws9sv^c}{)xASaG@#7PJ9A^=wA8=ymud`me*bqU(s;qd;HK!)mfZ5u5;w(P z8b+}v-TH3j9Me}8dVqV&in|`oFT&EbZr(QF(Dl*Z&Bi8Zo-Lb@;P-RQ{kfrD*&8-1 zdM*s?<)5+1_SMw6UddnHcE(m)T=#p!UoWud{@-_J6K|)~i|(~g&N|Gp@;yU3r}j&p zXR1X@f9M^SP_=tp^jMg2PFB0|s~el0_nbC6P_XIe1*Kc=vt0eI6s!;WyS^%GVfERL z*0()U4r?-!41+l8E7SPnej9wrd+)w~qs{x~)p9kPzTZ0fRDb`ce|z`Zm;P(M9`WSo ze4C1wmdAE{tK7bOy4bc8k8ZL3nW9tYC1zi=X-ApV|0$t0GHaM#{I@7NfAH^fqf@t@ zyS_gcnjAedCHHXbNtM&Bd#4+nym@{1<`-MP-#nXnyLxK9-Gur*+bnHm{UzSsd!4R7 zx3=`drAO1U{G7)cV<7v_eSvULEY8a@1{lV z-g%+;ZtdGh`G}$??t8B6FDvfaeR*9i_n~WMcZx$@x)X|j--`3wvOaE4QMF6t?)J|Q z-{1TFBDVf*op1T(#h=0?*5n?sU*&sEHbT_m&u{OeS5B2*lmEvY`TKW?=<~;sM++w; zh$y!lHWIj)Xz+L?v!TtZ#(EFO9UE7Lr~FkpZTeDi;jEB@E419*=akrZT)O0OvgTvT z=F^^8v!Al8|90uRYoD5Xba$a^xaU-s)JGR*S8wYoUMc(I)@il*>mI#1pmi>MU*X3V z{=6GjKN_!Bo-60uCVYCjYp(tGORlWze^T&tTS$4?&%)fpv1L2=|Jjjq>o&VQ&o}8G5BhKC>uFEOk4d=H&l!{3*H^!3 zuFWHHxtb4;?Mn*^-mQp_uX}oa?^)xzyT?_0ciSm)&&n=ecsuSw%*=cDTyELzPg!=H zfBwGGl09!fWzYWbUzzs{=Yk)rr>n-tMeM10eg4$;Aen#5=UUW#;(sr@=TqS1&aJSZULs>1Lkb+RTR@r-!VIkN5`slL57ddfeu}jIgHU> z&PP^NIP^B3tmVJty?Dt23GQa^TRnn3U+xt#?>Sj^AmHzbd*5=|r8n%N_3Pnk~<6nX*buqB1P{>Z$DXWxFS9tiQQoT3p_xx?AFr zD-4a9vZGvB<)6(ysp{3f^5IhDo{&jQOYJ-+vnlESPzk-^q!sLyaz|?||E!YhK|Wdu zoPn$!OH+GGR{2~K-`}*s_0+bfw_2*Yj~>;ZuP?_^t&Y(J$I(%1l2yTJB%lb z)m&3fe|3-A_x|*yYcB87rA{1qQoU}gW6+(crboB77w7K!_2}j0PzhjdFx%Rka>JvvQpjwiwg0YtZfc)K9}^herfH_EIiTb&wMk$>y7E|n5P0& zTX$5kKm1U2W4g)GjZ1Dl+PlW&)hmuuOS|W)eRcYB z?lF~5*threJEj>dzgpGUm8?8xD5URG3}y&g(ObCS%cV7YH8z~<)k#eaIwy9@KRLp8MH#Xd0STJ5O)pJ*=o zW9nz$_Di#~=1-a0zIDCn4Y54!;x=9P{r_K_$~t_DZTGi|8F#JZws}`4Jnz``?X2MK z+RtHmAGh?b5Wg+5ziO*f{abDGZL)gr+MkETo!_@_Z`_J&r&Wcuy!PLITE{5Y*&h~n zOQU>6V#tf=%%i&Ps-gyG-mq9^23V&}d;C*{`{gp-wV!5(NR;2`%sF)=F~I$d@Z@_5 zdrB>PXCCODV8ycgQ(pUjmu^dn@)99{=$L+EB2diPn4Mg z>velsCKrai<4>|WWuV8ObDra={*H}rswbw*{c@I7(dNko(Vdq=`6?&OT6iGrWyz1< zMqQfE4d#?+Pd8J^U2$p&4>w=g&ZAtv@>fkPR-L(qUwqoKtIkrP-_w4utVo=c_sX_9 zQ%(Nly3jpLt9Z*wU*6Q3&K0s!?5p3p(h0MLQzjk^X;{1Ju)W1=PWG24n9Aec-Io@% zQm74kxK8oFwXJuw+b+*mG%r6TR+uW(G<~<~j7}}T_L4%^szsj+6LK^pO=b%oPubNr z{qU}+yrWl()SEnme3msi$e*bA;XgeuN33jxV~PIt1q!tX-^)nNv_BEGHb&;e`lfqD ztA!H}NUX96D2!Nq+SJqF)~=WbO-~r(p64`FbYA*r>3D=YE!3+_xgl@LWGnW5slX6c7TNgf~HDwc=M%;Nw5^U}tETZgRf*-78D ze0pEZKJ(->#j~H=OB~r}KM|WGVzuhvR7s(e{?iW4-g57&=i|Av*k`{mwYg#UUt8eL*{B~IkcdFXGcOT+)43+*a=G1Cf^1b6Mo16aH4{;t-wO;F- zuB^%oy|eU=|0~AXe}6x1qx$N?wzBX z+Fj((oc5xfdFQSeS<|N%{+%&Yc(R1gcb4vnyfUY~^8aR6v)g|P3~t-HKJwNgp6v9t z^xa9*cN~(eu2?$lvfv%F;)vUI*K>Mo@1Jz2&zZ}US1y$Ow0rHgm9I|ye4n0lHUGaQTXb*zR+|>y7rNftF~98J_2avFifycJa<$D)AMWh7O&g( zEwwg!-L`Fy_DjBqv&}zMt9pKY?d^wGrv`kzbLh&=>(op_xzOR zdRr^2y+u#zbA|7Prf&XL+Ec#m_>o&}yQ?;+*!aJQv#ft8d?E4C{aT(?X>X-1w#(GN zU3z-9;rFG>XV2WWw?6mv-OwGezmHwc-4R>K{n(Y;li&Z;)Tl{Y&zASzUa7l1c=wC4 z?0Cc7z6Rmz4eQP1SC5?*83XmON4R-R$xo*WbN&h~Ij6f8GCYwkP@iANwnAqGbJN_ILY| z?}nCPo}09nXzY}8SrxU^tbB#pjBjrYx368$DXu73efiSc>B|qxhs=@Cjrw#kM`4z` z__59>-5D(tmsoz?z_w6(`q9}dm*rjKlYeqWKj+>_!KMBuVk@$vKN@}bw{|Y`z5aiT zrp7u-lO-5e$TkGJI8w6G)th`QY{|Je-FqBFkjZ^Pwp z{lCQ-^Y{LDyG^fW@%=wvrrh^nw~6)@%f}4?fllF~&%0ME?tHp!Lx$~?$sbEEFXg$u z^+j`girAev&(7$uOZkUh-Kt(`rgbQ6od@Gf&U;F;ubqBt+;-tZrH}I6hs^n6KPOn< zb;!zn9Lpb`{e4r-W8u4}UfI9*>OL&3@$r`I8ov79|MVnYl-s@WI&ziGt$lxWJ-1G` z`(wZLm6QBhqA$2Vw$k_}w&wz05f4i;X!}9St02FS!fhPN9bSE0f}Hz!QR7C(_#cWTp`&IGeb8yO~E@sLlgUY_+f;z#2yv9-CIUmF(XKB;?r`Jd-% z6CJBR?%&H796tB(eQiX^tyjUtKYoAD&+se$!Tw#p`%?4!H?^G=HP5Zx`H%bWx4mV( z++Ut^bD8Td;d0FqjwfvM7x^$LM0dIvG6l{meZxDWjOn)U-K4J}Y-~E}`*ta&70k(b zk|$(&rag-NsQCM5YkeZ}PI{lv;}pyMsC;*N)Z??8?)^UW(%QXWzW<-@!{y8EKVCim zruL=C8H@Ud)6G+j_if|WuPymtq_|sbJWF_9;T?$g8zTd P|F>VDsd}hggOLFMfGcDo literal 8348 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 { + console.error( + "Usage: camino-codegen-ts-runtime [implementation-import]", + ); + process.exit(1); +}; + +const required = (value: string | undefined) => value ?? usage(); + +const stringLiteral = (value: string) => JSON.stringify(value); + +const upperFirst = (value: string) => + value.length === 0 ? value : `${value[0]?.toUpperCase()}${value.slice(1)}`; + +const lowerFirst = (value: string) => + value.length === 0 ? value : `${value[0]?.toLowerCase()}${value.slice(1)}`; + +const camel = (value: string) => + value + .split(/[^a-zA-Z0-9]+/) + .filter(Boolean) + .map((part, index) => + index === 0 ? lowerFirst(part) : upperFirst(lowerFirst(part)), + ) + .join(""); + +const isIdentifier = (value: string) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value); + +const classId = (schema: ClassSchema) => + `${schema.id.namespace}:${schema.id.name}:${schema.id.version}:${ + schema.id.hash ?? "" + }`; + +const symbolId = (value: { namespace: string; name: string; version: string; hash?: string }) => + `${value.namespace}:${value.name}:${value.version}:${value.hash ?? ""}`; + +const tsTypeForTypeRef = (type: TypeRef): string => { + if (type.enumValues && type.enumValues.length > 0) { + return type.enumValues.map(stringLiteral).join(" | "); + } + switch (type.protoType.replace(/^\./, "")) { + case "string": + case "google.protobuf.StringValue": + return "string"; + case "bool": + case "google.protobuf.BoolValue": + return "boolean"; + case "double": + case "float": + case "google.protobuf.DoubleValue": + case "google.protobuf.FloatValue": + case "int32": + case "sint32": + case "sfixed32": + case "uint32": + case "fixed32": + case "int64": + case "sint64": + case "sfixed64": + case "uint64": + case "fixed64": + case "google.protobuf.Int32Value": + case "google.protobuf.UInt32Value": + case "google.protobuf.Int64Value": + case "google.protobuf.UInt64Value": + return "number"; + case "bytes": + case "google.protobuf.BytesValue": + return "Uint8Array"; + default: + return "unknown"; + } +}; + +const tsTypeForField = (field: FieldSchema): string => tsTypeForTypeRef(field.type); + +const tsValueTypeForField = (field: FieldSchema) => + field.repeated ? `${tsTypeForField(field)}[]` : tsTypeForField(field); + +const tsFieldApiType = (field: FieldSchema) => + field.conflict === "crdt" + ? `CrdtField<${tsValueTypeForField(field)}>` + : `Field<${tsValueTypeForField(field)}>`; + +const functionExportName = (fn: FunctionRef, fallback: string) => { + if (isIdentifier(fn.symbol)) { + return fn.symbol; + } + const fallbackName = camel(fallback); + if (isIdentifier(fallbackName)) { + return fallbackName; + } + throw new Error(`Cannot generate TS export name for function ${fn.symbol}`); +}; + +const generatedWarning = `// @generated by camino-codegen-ts-runtime. Do not edit.\n`; + +type FunctionSpec = { + exportName: string; + symbol: string; + operation?: string; + packageNamespace: string; + packageName: string; + field?: { + name: string; + type: string; + storage: string; + conflict: string; + }; +}; + +const fieldTypeId = (field: FieldSchema) => + field.type.symbol ? symbolId(field.type.symbol) : field.type.protoType; + +const crdtStorageTypeId = (field: FieldSchema) => + field.type.protoType || + (field.type.symbol + ? `${field.type.symbol.namespace}.${field.type.symbol.name}` + : fieldTypeId(field)); + +const fieldStorageId = (field: FieldSchema) => + field.ops && field.storage.kind === "stored" ? "derived" : field.storage.kind; + +const interfaceTypeName = (implementation: InterfaceImplementation) => + `${implementation.interface.name}Ref`; + +const renderInterfaceRefTypes = (schema: ClassSchema) => + schema.implements + .map((implementation) => { + const typeParams = implementation.typeBindings.map((binding) => binding.name); + const generic = + typeParams.length > 0 ? `<${typeParams.join(", ")}>` : ""; + const bindings = + typeParams.length > 0 + ? typeParams + .map((param) => ` readonly ${param}: ${param};`) + .join("\n") + : " readonly value: unknown;"; + return `export type ${interfaceTypeName(implementation)}${generic} = ObjectRef & { + readonly $caminoInterfaces: { + readonly ${stringLiteral(symbolId(implementation.interface))}: { +${bindings} + }; + }; +};`; + }) + .join("\n\n"); + +const interfaceMixins = (schema: ClassSchema) => + schema.implements.map((implementation) => { + const typeArgs = implementation.typeBindings.map((binding) => + tsTypeForTypeRef(binding.type), + ); + return `${interfaceTypeName(implementation)}${ + typeArgs.length > 0 ? `<${typeArgs.join(", ")}>` : "" + }`; + }); + +const operationFunctionSpecs = (schema: ClassSchema): FunctionSpec[] => + schema.fields.flatMap((field) => { + const serviceName = field.ops?.service.replace(/^\./, ""); + if (!serviceName) { + return []; + } + const service = schema.operationServices.find( + (candidate) => candidate.fullName === serviceName, + ); + if (!service) { + throw new Error( + `${schema.id.name}.${field.name} points to missing operation service ${field.ops?.service}`, + ); + } + return service.operations.map((operation) => ({ + exportName: functionExportName( + operation.function, + `${field.name}_${operation.name}`, + ), + symbol: operation.function.symbol, + ...(operation.function.operation + ? { operation: operation.function.operation } + : {}), + packageNamespace: operation.function.packageNamespace, + packageName: operation.function.packageName, + field: { + name: field.name, + type: fieldTypeId(field), + storage: fieldStorageId(field), + conflict: field.conflict, + }, + })); + }); + +const renderRuntime = (schema: ClassSchema) => { + const className = schema.id.name; + const objectTypeName = `${className}Object`; + const refTypeName = `${className}Ref`; + const contextTypeName = `${className}Context`; + const implementationTypeName = `${className}Implementation`; + const serveName = `serve${className}Runtime`; + const createObjectName = `create${className}Object`; + const interfaceRefTypes = renderInterfaceRefTypes(schema); + const refType = + [ `ObjectRef<${stringLiteral(classId(schema))}>`, ...interfaceMixins(schema)] + .join(" & "); + const edgeFields = new Set(schema.edges.map((edge) => edge.sourceField)); + const writableFields = schema.fields.filter( + (field) => + field.storage.kind === "stored" && + !field.ops && + !edgeFields.has(field.name), + ); + const functionSpecs = [ + ...(schema.constructorSpec + ? [ + { + exportName: functionExportName( + schema.constructorSpec.function, + "constructor", + ), + symbol: schema.constructorSpec.function.symbol, + ...(schema.constructorSpec.function.operation + ? { operation: schema.constructorSpec.function.operation } + : {}), + packageNamespace: schema.constructorSpec.function.packageNamespace, + packageName: schema.constructorSpec.function.packageName, + }, + ] + : []), + ...schema.methods.map((method) => ({ + exportName: functionExportName(method.function, method.name), + symbol: method.function.symbol, + ...(method.function.operation ? { operation: method.function.operation } : {}), + packageNamespace: method.function.packageNamespace, + packageName: method.function.packageName, + })), + ...schema.fields.flatMap((field) => + field.storage.resolver + ? [ + { + exportName: functionExportName(field.storage.resolver, field.name), + symbol: field.storage.resolver.symbol, + ...(field.storage.resolver.operation + ? { operation: field.storage.resolver.operation } + : {}), + packageNamespace: field.storage.resolver.packageNamespace, + packageName: field.storage.resolver.packageName, + }, + ] + : [], + ), + ...operationFunctionSpecs(schema), + ]; + const firstFunction = functionSpecs[0]; + if (!firstFunction) { + throw new Error(`${schema.id.name} has no runtime functions to generate`); + } + const mixedPackage = functionSpecs.find( + (spec) => + spec.packageNamespace !== firstFunction.packageNamespace || + spec.packageName !== firstFunction.packageName, + ); + if (mixedPackage) { + throw new Error( + `${schema.id.name} points to multiple packages; v0 TS runtime generation expects one package`, + ); + } + + return `${generatedWarning}import { + type CaminoClient, + createCrdtField, + createField, + type CrdtField, + type Field, + type FieldOperation, + type InvokeRequest, + objectRef, + type ObjectRef, + type RuntimeHandler, + serveQuixosPackageRuntime, +} from "@quixos/camino-package-runtime"; + +${interfaceRefTypes ? `${interfaceRefTypes}\n\n` : ""}export type ${refTypeName} = ${refType}; + +export type ${objectTypeName} = { + id: ${refTypeName}; +${writableFields + .map( + (field) => + ` ${camel(field.name)}: ${tsFieldApiType(field)};`, + ) + .join("\n")} +}; + +export type ${contextTypeName} = { + object: ${objectTypeName}; + camino: CaminoClient; + request: InvokeRequest; +}; + +export type ${implementationTypeName} = { +${functionSpecs + .map((spec) => + spec.operation + ? ` ${spec.exportName}: FieldOperation<${contextTypeName}>;` + : ` ${spec.exportName}: RuntimeHandler<${contextTypeName}>;`, + ) + .filter((line, index, lines) => lines.indexOf(line) === index) + .join("\n")} +}; + +const packageNamespace = ${stringLiteral(firstFunction.packageNamespace)}; +const packageName = ${stringLiteral(firstFunction.packageName)}; +const runtimeProtocolVersion = ${stringLiteral("camino-orch-v0")}; + +const functions = [ +${functionSpecs + .map( + (spec) => + ` { exportName: ${stringLiteral(spec.exportName)}, symbol: ${stringLiteral(spec.symbol)}${ + spec.operation ? `, operation: ${stringLiteral(spec.operation)}` : "" + }${ + spec.field + ? `, field: { name: ${stringLiteral(spec.field.name)}, type: ${stringLiteral(spec.field.type)}, storage: ${stringLiteral(spec.field.storage)}, conflict: ${stringLiteral(spec.field.conflict)} }` + : "" + } },`, + ) + .join("\n")} +] as const; + +export const ${createObjectName} = ( + camino: CaminoClient, + objectId: string, +): ${objectTypeName} => { + const id = objectRef<${stringLiteral(classId(schema))}>(objectId) as ${refTypeName}; + return { + id, +${writableFields + .map( + (fieldSchema) => + fieldSchema.conflict === "crdt" + ? ` ${camel(fieldSchema.name)}: createCrdtField(camino, id, ${stringLiteral(fieldSchema.name)}, ${stringLiteral(crdtStorageTypeId(fieldSchema))}),` + : ` ${camel(fieldSchema.name)}: createField(camino, id, ${stringLiteral(fieldSchema.name)}),`, + ) + .join("\n")} + }; +}; + +export const create${className}Context = ( + camino: CaminoClient, + request: InvokeRequest, +): ${contextTypeName} => ({ + object: ${createObjectName}(camino, request.objectId), + camino, + request, +}); + +export const ${serveName} = (implementation: ${implementationTypeName}) => { + serveQuixosPackageRuntime({ + packageNamespace, + packageName, + runtimeProtocolVersion, + functions, + implementation, + createContext: create${className}Context, + }); +}; +`; +}; + +const renderServer = (schema: ClassSchema, implementationImport: string) => { + const className = schema.id.name; + return `${generatedWarning}import * as implementation from ${stringLiteral(implementationImport)}; +import { serve${className}Runtime } from "./${schema.id.namespace}.${className}.runtime.js"; + +serve${className}Runtime(implementation); +`; +}; + +const main = async () => { + const sourceFile = required(process.argv[2]); + const outDir = required(process.argv[3]); + const implementationImport = process.argv[4] ?? "../task.impl.js"; + const compiled = await compileCaminoSchema(sourceFile); + if (compiled.classes.length !== 1) { + throw new Error( + `Expected exactly one Camino class in ${sourceFile}, found ${compiled.classes.length}`, + ); + } + const schema = compiled.classes[0]; + if (!schema) { + throw new Error(`No Camino class found in ${sourceFile}`); + } + fs.mkdirSync(outDir, { recursive: true }); + const baseName = `${schema.id.namespace}.${schema.id.name}`; + fs.writeFileSync(path.join(outDir, `${baseName}.runtime.ts`), renderRuntime(schema)); + fs.writeFileSync( + path.join(outDir, `${baseName}.server.ts`), + renderServer(schema, implementationImport), + ); +}; + +await main(); diff --git a/src/schema-compiler.ts b/src/schema-compiler.ts new file mode 100644 index 0000000..3b9354a --- /dev/null +++ b/src/schema-compiler.ts @@ -0,0 +1,809 @@ +import fs from "node:fs"; +import path from "node:path"; +import protobuf from "protobufjs"; +import type { + Cardinality, + ClassSchema, + ConflictStrategy, + FieldSchema, + FieldStorage, + FieldStorageKind, + FunctionRef, + InterfaceFieldContract, + InterfaceImplementation, + InterfaceSchema, + MethodSchema, + OperationSchema, + OperationServiceSchema, + MigrationSpec, + SchemaCompileResult, + SymbolRef, + TypeRef, +} from "./schema-ir.js"; + +type OptionBag = Record; + +const CLASS_OPTION = "(camino.class)"; +const INTERFACE_OPTION = "(camino.interface)"; +const IMPLEMENTS_OPTION = "(camino.implements)"; +const CONSTRUCTOR_OPTION = "(camino.constructor)"; +const METHOD_OPTION = "(camino.method)"; +const MIGRATION_OPTION = "(camino.migration)"; +const CONFLICT_OPTION = "(camino.conflict)"; +const EDGE_OPTION = "(camino.edge)"; +const FIELD_STORAGE_OPTION = "(camino.field_storage)"; +const FIELD_OPS_OPTION = "(camino.field_ops)"; +const INTERFACE_FIELD_OPTION = "(camino.interface_field)"; +const DISPLAY_LABEL_OPTION = "(camino.display_label)"; +const IMPL_OPTION = "(camino.impl)"; +const SCHEMA_NAMESPACE_OPTION = "(camino.schema_namespace)"; +const SCHEMA_VERSION_OPTION = "(camino.schema_version)"; + +const enumName = (value: unknown) => + String(value) + .replace(/([a-z0-9])([A-Z])/g, "$1_$2") + .replace(/-/g, "_") + .toUpperCase(); + +const lowerEnumName = (value: unknown) => enumName(value).toLowerCase(); + +const getOption = ( + options: OptionBag | undefined, + name: string, +): unknown => { + if (!options) { + return undefined; + } + if (Object.hasOwn(options, name)) { + return options[name]; + } + const bareName = name.replace(/^\((.*)\)$/, "$1"); + if (Object.hasOwn(options, bareName)) { + return options[bareName]; + } + return undefined; +}; + +const getParsedOptions = ( + reflection: protobuf.ReflectionObject, +): OptionBag[] => { + const parsedOptions = (reflection as unknown as { parsedOptions?: unknown }) + .parsedOptions; + return Array.isArray(parsedOptions) + ? parsedOptions.filter( + (option): option is OptionBag => + Boolean(option) && typeof option === "object" && !Array.isArray(option), + ) + : []; +}; + +const getReflectionOptions = ( + reflection: protobuf.ReflectionObject, + name: string, +): unknown[] => { + const parsedValues = getParsedOptions(reflection).flatMap((option) => { + const value = getOption(option, name); + return value === undefined ? [] : [value]; + }); + if (parsedValues.length > 0) { + return parsedValues; + } + const value = getOption(reflection.options, name); + return value === undefined ? [] : [value]; +}; + +const getReflectionOption = ( + reflection: protobuf.ReflectionObject, + name: string, +): unknown => getReflectionOptions(reflection, name)[0]; + +const asObject = (value: unknown): OptionBag | undefined => + value && typeof value === "object" && !Array.isArray(value) + ? (value as OptionBag) + : undefined; + +const asArray = (value: unknown): unknown[] => + Array.isArray(value) ? value : value === undefined ? [] : [value]; + +const readString = ( + object: OptionBag | undefined, + key: string, +): string | undefined => { + const value = object?.[key]; + return typeof value === "string" ? value : undefined; +}; + +const readNumber = ( + object: OptionBag | undefined, + key: string, +): number | undefined => { + const value = object?.[key]; + return typeof value === "number" && Number.isFinite(value) + ? value + : undefined; +}; + +const readBoolean = ( + object: OptionBag | undefined, + key: string, +): boolean | undefined => { + const value = object?.[key]; + return typeof value === "boolean" ? value : undefined; +}; + +const symbolRefFromOption = ( + value: unknown, + fallback: SymbolRef, +): SymbolRef => { + const object = asObject(value); + if (!object) { + return fallback; + } + return { + namespace: readString(object, "namespace") ?? fallback.namespace, + name: readString(object, "name") ?? fallback.name, + version: readString(object, "version") ?? fallback.version, + ...(readString(object, "hash") + ? { hash: readString(object, "hash") } + : {}), + }; +}; + +const typeRefFromOption = ( + value: unknown, + fallbackVersion: string, +): TypeRef | undefined => { + const object = asObject(value); + if (!object) { + return undefined; + } + const symbol = asObject(object.symbol) + ? symbolRefFromOption(object.symbol, { + namespace: "", + name: "", + version: fallbackVersion, + }) + : undefined; + const protoType = readString(object, "proto_type")?.replace(/^\./, ""); + const symbolProtoType = + symbol?.namespace && symbol.name ? `${symbol.namespace}.${symbol.name}` : ""; + const resolvedProtoType = protoType ?? symbolProtoType; + if (!resolvedProtoType) { + return undefined; + } + return { + protoType: resolvedProtoType, + ...(symbol?.namespace && symbol.name ? { symbol } : {}), + }; +}; + +const interfaceFieldContractFromOption = ( + value: unknown, + schemaVersion: string, +): InterfaceFieldContract | undefined => { + const object = asObject(value); + if (!object) { + return undefined; + } + const type = typeRefFromOption(object.type, schemaVersion); + return { + required: readBoolean(object, "required") ?? false, + readable: readBoolean(object, "readable") ?? false, + writable: readBoolean(object, "writable") ?? false, + watchable: readBoolean(object, "watchable") ?? false, + ...(readString(object, "type_param") + ? { typeParam: readString(object, "type_param") } + : {}), + ...(type ? { type } : {}), + }; +}; + +const functionRefFromOption = (value: unknown): FunctionRef => { + const object = asObject(value); + if (!object) { + throw new Error("Expected function ref option object"); + } + const packageNamespace = readString(object, "package_namespace"); + const packageName = readString(object, "package_name"); + const symbol = readString(object, "symbol"); + if (!packageNamespace || !packageName || !symbol) { + throw new Error("Function ref requires package_namespace, package_name, and symbol"); + } + return { + packageNamespace, + packageName, + symbol, + ...(readString(object, "operation") + ? { operation: readString(object, "operation") } + : {}), + ...(readString(object, "version_ref") + ? { versionRef: readString(object, "version_ref") } + : {}), + }; +}; + +const conflictFromOption = (value: unknown): ConflictStrategy => { + const normalized = enumName(value); + if (!value || normalized === "CONFLICT_STRATEGY_UNSPECIFIED") { + return "preserve_conflicts"; + } + if (normalized === "REPLACE") { + return "replace"; + } + if (normalized === "PRESERVE_CONFLICTS") { + return "preserve_conflicts"; + } + if (normalized === "CRDT") { + return "crdt"; + } + throw new Error(`Unknown Camino conflict strategy: ${String(value)}`); +}; + +const cardinalityFromOption = (value: unknown): Cardinality => { + const normalized = lowerEnumName(value); + if (!value || normalized === "cardinality_unspecified") { + return "many"; + } + if ( + normalized === "optional_one" || + normalized === "exactly_one" || + normalized === "many" || + normalized === "many_unique" || + normalized === "many_ordered" + ) { + return normalized; + } + throw new Error(`Unknown Camino cardinality: ${String(value)}`); +}; + +const fieldStorageKindFromOption = (value: unknown): FieldStorageKind => { + if (value === 5) { + return "static_final"; + } + const normalized = lowerEnumName(value); + if (!value || normalized === "field_storage_kind_unspecified") { + return "stored"; + } + if ( + normalized === "stored" || + normalized === "derived" || + normalized === "lazy" || + normalized === "external" || + normalized === "static_final" + ) { + return normalized; + } + throw new Error(`Unknown Camino field storage kind: ${String(value)}`); +}; + +const fieldStorageFromOption = (value: unknown): FieldStorage => { + const object = asObject(value); + if (!object) { + return { + kind: "stored", + cache: false, + }; + } + const kind = fieldStorageKindFromOption(object.kind); + const resolver = object.resolver === undefined + ? undefined + : functionRefFromOption(object.resolver); + if ((kind === "derived" || kind === "lazy" || kind === "external") && !resolver) { + throw new Error(`${kind} field storage requires resolver`); + } + return { + kind, + ...(resolver ? { resolver } : {}), + cache: object.cache === true, + }; +}; + +const fieldOpsFromOption = (value: unknown) => { + const object = asObject(value); + if (!object) { + return undefined; + } + const implementation = enumName(object.implementation); + if (implementation !== "SERVICE") { + throw new Error( + `Unsupported Camino field implementation: ${String(object.implementation)}`, + ); + } + const service = readString(object, "service"); + if (!service) { + throw new Error("SERVICE field_ops requires service"); + } + return { + implementation: "service" as const, + service, + }; +}; + +const extractFileDefaults = (sourceFile: string) => { + const text = fs.readFileSync(sourceFile, "utf8"); + const packageMatch = /^\s*package\s+([a-zA-Z0-9_.]+)\s*;/m.exec(text); + const namespaceMatch = + /^\s*option\s+\(camino\.schema_namespace\)\s*=\s*"([^"]+)"\s*;/m.exec( + text, + ); + const versionMatch = + /^\s*option\s+\(camino\.schema_version\)\s*=\s*"([^"]+)"\s*;/m.exec(text); + + return { + packageName: packageMatch?.[1] ?? "", + schemaNamespace: namespaceMatch?.[1] ?? packageMatch?.[1] ?? "", + schemaVersion: versionMatch?.[1] ?? "1", + }; +}; + +const protoSymbolFromReflection = ( + reflection: protobuf.ReflectionObject | null, + fallbackVersion: string, +): SymbolRef | undefined => { + if (!reflection?.fullName) { + return undefined; + } + const parts = reflection.fullName.replace(/^\./, "").split("."); + const name = parts.pop(); + if (!name) { + return undefined; + } + return { + namespace: parts.join("."), + name, + version: fallbackVersion, + }; +}; + +const typeRefForField = ( + field: protobuf.Field, + schemaVersion: string, +): TypeRef => { + const symbol = protoSymbolFromReflection(field.resolvedType, schemaVersion); + const enumValues = + field.resolvedType instanceof protobuf.Enum + ? Object.entries(field.resolvedType.values) + .filter(([_name, value]) => value !== 0) + .map(([name]) => name) + : undefined; + return { + protoType: field.type, + ...(symbol ? { symbol } : {}), + ...(enumValues ? { enumValues } : {}), + }; +}; + +const typeRefForType = ( + type: protobuf.Type | undefined, + fallbackType: string, + schemaVersion: string, +): TypeRef => { + const symbol = protoSymbolFromReflection(type ?? null, schemaVersion); + return { + protoType: type?.fullName?.replace(/^\./, "") ?? fallbackType, + ...(symbol ? { symbol } : {}), + }; +}; + +const fieldSchemaFromField = ( + field: protobuf.Field, + schemaVersion: string, +): FieldSchema => { + const storage = fieldStorageFromOption( + getReflectionOption(field, FIELD_STORAGE_OPTION), + ); + const ops = fieldOpsFromOption(getReflectionOption(field, FIELD_OPS_OPTION)); + if (ops && storage.kind !== "stored" && storage.kind !== "static_final") { + throw new Error( + `Field ${field.fullName} cannot declare both field_ops and package-resolved storage`, + ); + } + if (storage.kind === "static_final" && !ops) { + throw new Error(`Field ${field.fullName} with static_final storage requires field_ops`); + } + const interfaceContract = interfaceFieldContractFromOption( + getReflectionOption(field, INTERFACE_FIELD_OPTION), + schemaVersion, + ); + return { + name: field.name, + tag: field.id, + type: typeRefForField(field, schemaVersion), + conflict: conflictFromOption(getReflectionOption(field, CONFLICT_OPTION)), + repeated: field.repeated, + optional: field.optional, + storage, + ...(ops ? { ops } : {}), + ...(interfaceContract ? { interfaceContract } : {}), + isDisplayLabel: getReflectionOption(field, DISPLAY_LABEL_OPTION) === true, + }; +}; + +const isStringType = (type: TypeRef) => + type.protoType === "string" || type.protoType === "google.protobuf.StringValue"; + +const validateDisplayLabelFields = ( + type: protobuf.Type, + fields: FieldSchema[], + operationServices: OperationServiceSchema[], +) => { + const displayLabelFields = fields.filter((field) => field.isDisplayLabel); + if (displayLabelFields.length > 1) { + throw new Error( + `Class ${type.fullName} declares multiple Camino display label fields`, + ); + } + const displayLabelField = displayLabelFields[0]; + if (!displayLabelField) { + return; + } + if (!displayLabelField.ops) { + if (!isStringType(displayLabelField.type)) { + throw new Error( + `Display label field ${type.fullName}.${displayLabelField.name} must have string type`, + ); + } + return; + } + const serviceName = displayLabelField.ops.service.replace(/^\./, ""); + const getOperation = operationServices + .find((service) => service.fullName === serviceName) + ?.operations.find((operation) => operation.name === "Get"); + if (!getOperation || !isStringType(getOperation.outputType)) { + throw new Error( + `Display label field ${type.fullName}.${displayLabelField.name} must resolve to string`, + ); + } +}; + +const typeBindingFromOption = ( + value: unknown, + schemaVersion: string, +) => { + const object = asObject(value); + if (!object) { + return undefined; + } + const name = readString(object, "name"); + const type = typeRefFromOption(object.type, schemaVersion); + if (!name || !type) { + throw new Error("Interface type_binding requires name and type"); + } + return { name, type }; +}; + +const interfaceImplementationFromOption = ( + value: unknown, + defaults: ReturnType, +): InterfaceImplementation => { + const object = asObject(value); + if (!object) { + throw new Error("Expected implements option object"); + } + const interfaceId = symbolRefFromOption(object.interface, { + namespace: defaults.schemaNamespace, + name: "", + version: defaults.schemaVersion, + }); + if (!interfaceId.name) { + throw new Error("implements option requires interface name"); + } + return { + interface: interfaceId, + typeBindings: asArray(object.type_binding) + .map((binding) => typeBindingFromOption(binding, defaults.schemaVersion)) + .filter((binding): binding is NonNullable => + Boolean(binding), + ), + }; +}; + +const serviceName = (service: protobuf.Service) => + service.fullName.replace(/^\./, ""); + +const operationFromMethod = ( + method: protobuf.Method, + schemaVersion: string, +): OperationSchema => { + const fn = functionRefFromOption(getReflectionOption(method, IMPL_OPTION)); + const methodWithTypes = method as protobuf.Method & { + resolvedRequestType?: protobuf.Type; + resolvedResponseType?: protobuf.Type; + }; + return { + name: method.name, + inputType: typeRefForType( + methodWithTypes.resolvedRequestType, + method.requestType, + schemaVersion, + ), + outputType: typeRefForType( + methodWithTypes.resolvedResponseType, + method.responseType, + schemaVersion, + ), + function: fn, + }; +}; + +const operationServiceFromService = ( + service: protobuf.Service, + schemaVersion: string, +): OperationServiceSchema => ({ + name: service.name, + fullName: serviceName(service), + operations: service.methodsArray.map((method) => + operationFromMethod(method, schemaVersion), + ), +}); + +const classSchemaFromType = ( + type: protobuf.Type, + sourceFile: string, + defaults: ReturnType, + operationServices: OperationServiceSchema[], +): ClassSchema | undefined => { + const classOption = asObject(getReflectionOption(type, CLASS_OPTION)); + if (!classOption) { + return undefined; + } + + const fallbackClassRef = { + namespace: defaults.schemaNamespace, + name: type.name, + version: defaults.schemaVersion, + }; + const classId = symbolRefFromOption(classOption.id, fallbackClassRef); + const classVersion = readNumber(classOption, "version") ?? Number(classId.version); + if (!Number.isInteger(classVersion) || classVersion <= 0) { + throw new Error(`Class ${type.fullName} has invalid Camino class version`); + } + + const fields = type.fieldsArray.map((field) => + fieldSchemaFromField(field, defaults.schemaVersion), + ); + validateDisplayLabelFields(type, fields, operationServices); + const edges = type.fieldsArray.flatMap((field) => { + const edgeOption = asObject(getReflectionOption(field, EDGE_OPTION)); + if (!edgeOption) { + return []; + } + const targetClass = symbolRefFromOption(edgeOption.target, { + namespace: defaults.schemaNamespace, + name: "", + version: defaults.schemaVersion, + }); + if (!targetClass.name) { + throw new Error(`Edge field ${type.fullName}.${field.name} requires target`); + } + const cardinality = cardinalityFromOption(edgeOption.cardinality); + const sourceType = typeRefForField(field, defaults.schemaVersion); + if ( + sourceType.symbol?.namespace !== "camino" || + sourceType.symbol.name !== "Ref" + ) { + throw new Error(`Edge field ${type.fullName}.${field.name} must use camino.Ref`); + } + const isMany = + cardinality === "many" || + cardinality === "many_unique" || + cardinality === "many_ordered"; + if (isMany && !field.repeated) { + throw new Error(`Edge field ${type.fullName}.${field.name} must be repeated for ${cardinality}`); + } + if (!isMany && field.repeated) { + throw new Error(`Edge field ${type.fullName}.${field.name} must not be repeated for ${cardinality}`); + } + const edgeId = symbolRefFromOption(edgeOption.id, { + namespace: defaults.schemaNamespace, + name: `${type.name}.${field.name}`, + version: defaults.schemaVersion, + }); + return [ + { + id: edgeId, + sourceField: field.name, + fromClass: classId, + toClass: targetClass, + cardinality, + ...(readString(edgeOption, "inverse") + ? { inverse: readString(edgeOption, "inverse") } + : {}), + fields: [], + }, + ]; + }); + + const methods: MethodSchema[] = getReflectionOptions(type, METHOD_OPTION) + .map(asObject) + .filter((option): option is OptionBag => Boolean(option)) + .map((option) => { + const name = readString(option, "name"); + if (!name) { + throw new Error(`Method option on ${type.fullName} requires name`); + } + return { + name, + function: functionRefFromOption(option.function), + }; + }); + + const migrations: MigrationSpec[] = getReflectionOptions(type, MIGRATION_OPTION) + .map(asObject) + .filter((option): option is OptionBag => Boolean(option)) + .map((option) => { + const fromVersion = readNumber(option, "from_version"); + const toVersion = readNumber(option, "to_version"); + if (!fromVersion || !toVersion) { + throw new Error(`Migration option on ${type.fullName} requires versions`); + } + return { + fromVersion, + toVersion, + function: functionRefFromOption(option.function), + }; + }); + + const constructorOption = asObject( + getReflectionOption(type, CONSTRUCTOR_OPTION), + ); + const constructorSpec = constructorOption + ? { function: functionRefFromOption(constructorOption.function) } + : undefined; + + return { + id: classId, + version: classVersion, + fields, + edges, + methods, + migrations, + operationServices, + implements: getReflectionOptions(type, IMPLEMENTS_OPTION).map((option) => + interfaceImplementationFromOption(option, defaults), + ), + ...(constructorSpec ? { constructorSpec } : {}), + sourceFile, + protoMessage: type.fullName, + }; +}; + +const interfaceSchemaFromType = ( + type: protobuf.Type, + sourceFile: string, + defaults: ReturnType, +): InterfaceSchema | undefined => { + const interfaceOption = asObject(getReflectionOption(type, INTERFACE_OPTION)); + if (!interfaceOption) { + return undefined; + } + const classOption = asObject(getReflectionOption(type, CLASS_OPTION)); + if (classOption) { + throw new Error(`${type.fullName} cannot be both a Camino class and interface`); + } + const fallbackInterfaceRef = { + namespace: defaults.schemaNamespace, + name: type.name, + version: defaults.schemaVersion, + }; + const interfaceId = symbolRefFromOption( + interfaceOption.id, + fallbackInterfaceRef, + ); + const interfaceVersion = + readNumber(interfaceOption, "version") ?? Number(interfaceId.version); + if (!Number.isInteger(interfaceVersion) || interfaceVersion <= 0) { + throw new Error(`Interface ${type.fullName} has invalid Camino interface version`); + } + return { + id: interfaceId, + version: interfaceVersion, + fields: type.fieldsArray.map((field) => + fieldSchemaFromField(field, defaults.schemaVersion), + ), + sourceFile, + protoMessage: type.fullName, + }; +}; + +const walkTypes = ( + namespace: protobuf.NamespaceBase, + visit: (type: protobuf.Type) => void, +) => { + for (const nested of namespace.nestedArray) { + if (nested instanceof protobuf.Type) { + visit(nested); + walkTypes(nested, visit); + } else if (nested instanceof protobuf.Namespace) { + walkTypes(nested, visit); + } + } +}; + +const walkServices = ( + namespace: protobuf.NamespaceBase, + visit: (service: protobuf.Service) => void, +) => { + for (const nested of namespace.nestedArray) { + if (nested instanceof protobuf.Service) { + visit(nested); + } else if (nested instanceof protobuf.Namespace) { + walkServices(nested, visit); + } + } +}; + +export const compileCaminoSchema = async ( + sourceFile: string, +): Promise => { + const absoluteSourceFile = path.resolve(sourceFile); + const defaults = extractFileDefaults(absoluteSourceFile); + const root = new protobuf.Root(); + const envIncludeDirs = [ + process.env.QUIXOS_PROTO_PATH, + process.env.CAMINO_PROTO_PATH, + ] + .filter((value): value is string => Boolean(value)) + .join(path.delimiter) + .split(path.delimiter) + .map((entry) => entry.trim()) + .filter(Boolean) + .map((entry) => path.resolve(entry)); + const includeDirs = [ + path.dirname(absoluteSourceFile), + path.resolve("../../quixos-protocol/proto"), + path.resolve("quixos-protocol/proto"), + path.resolve("proto"), + ...envIncludeDirs, + ]; + root.resolvePath = (origin, target) => { + if (path.isAbsolute(target) && fs.existsSync(target)) { + return target; + } + const originDir = origin ? path.dirname(origin) : undefined; + const candidates = [ + ...(originDir ? [path.resolve(originDir, target)] : []), + ...includeDirs.map((dir) => path.resolve(dir, target)), + ]; + const found = candidates.find((candidate) => fs.existsSync(candidate)); + return found ?? target; + }; + + await root.load(absoluteSourceFile, { keepCase: true }); + root.resolveAll(); + + const operationServices: OperationServiceSchema[] = []; + walkServices(root, (service) => { + operationServices.push( + operationServiceFromService(service, defaults.schemaVersion), + ); + }); + + const classes: ClassSchema[] = []; + const interfaces: InterfaceSchema[] = []; + walkTypes(root, (type) => { + const interfaceSchema = interfaceSchemaFromType( + type, + absoluteSourceFile, + defaults, + ); + if (interfaceSchema) { + interfaces.push(interfaceSchema); + } + const classSchema = classSchemaFromType( + type, + absoluteSourceFile, + defaults, + operationServices, + ); + if (classSchema) { + classes.push(classSchema); + } + }); + + return { + sourceFile: absoluteSourceFile, + classes, + interfaces, + }; +}; diff --git a/src/schema-ir.ts b/src/schema-ir.ts new file mode 100644 index 0000000..92fc5b0 --- /dev/null +++ b/src/schema-ir.ts @@ -0,0 +1,148 @@ +export type SymbolRef = { + namespace: string; + name: string; + version: string; + hash?: string; +}; + +export type FunctionRef = { + packageNamespace: string; + packageName: string; + symbol: string; + operation?: string; + versionRef?: string; +}; + +export type ConflictStrategy = + | "replace" + | "preserve_conflicts" + | "crdt"; + +export type Cardinality = + | "optional_one" + | "exactly_one" + | "many" + | "many_unique" + | "many_ordered"; + +export type FieldStorageKind = + | "stored" + | "derived" + | "lazy" + | "external" + | "static_final"; + +export type FieldStorage = { + kind: FieldStorageKind; + resolver?: FunctionRef; + cache: boolean; +}; + +export type FieldOps = { + implementation: "service"; + service: string; +}; + +export type TypeRef = { + protoType: string; + symbol?: SymbolRef; + enumValues?: string[]; +}; + +export type TypeBinding = { + name: string; + type: TypeRef; +}; + +export type InterfaceFieldContract = { + required: boolean; + readable: boolean; + writable: boolean; + watchable: boolean; + typeParam?: string; + type?: TypeRef; +}; + +export type InterfaceImplementation = { + interface: SymbolRef; + typeBindings: TypeBinding[]; +}; + +export type FieldSchema = { + name: string; + tag: number; + type: TypeRef; + conflict: ConflictStrategy; + repeated: boolean; + optional: boolean; + storage: FieldStorage; + ops?: FieldOps; + interfaceContract?: InterfaceFieldContract; + isDisplayLabel: boolean; +}; + +export type EdgeSchema = { + id: SymbolRef; + sourceField: string; + fromClass: SymbolRef; + toClass: SymbolRef; + cardinality: Cardinality; + inverse?: string; + fields: FieldSchema[]; +}; + +export type MethodSchema = { + name: string; + function: FunctionRef; +}; + +export type MigrationSpec = { + fromVersion: number; + toVersion: number; + function: FunctionRef; +}; + +export type ConstructorSpec = { + function: FunctionRef; +}; + +export type OperationSchema = { + name: string; + inputType: TypeRef; + outputType: TypeRef; + function: FunctionRef; +}; + +export type OperationServiceSchema = { + name: string; + fullName: string; + operations: OperationSchema[]; +}; + +export type ClassSchema = { + id: SymbolRef; + version: number; + fields: FieldSchema[]; + edges: EdgeSchema[]; + methods: MethodSchema[]; + migrations: MigrationSpec[]; + operationServices: OperationServiceSchema[]; + implements: InterfaceImplementation[]; + constructorSpec?: ConstructorSpec; + sourceFile: string; + protoMessage: string; +}; + +export type InterfaceSchema = { + id: SymbolRef; + version: number; + fields: FieldSchema[]; + sourceFile: string; + protoMessage: string; +}; + +export type SchemaCompileResult = { + sourceFile: string; + classes: ClassSchema[]; + interfaces: InterfaceSchema[]; +}; diff --git a/yarn-project.nix b/yarn-project.nix index 8e90bd7..5e99fde 100644 --- a/yarn-project.nix +++ b/yarn-project.nix @@ -162,16 +162,29 @@ cacheEntries = { "@bufbuild/protoplugin@npm:2.12.1" = { filename = "@bufbuild-protoplugin-npm-2.12.1-e1aebb80bf-c7b401ab31.zip"; hash = "sha512-x7QBqzHZSsaCXLm75c/3XQXSjuINRnskt6XUKZuaygZmhuo5t6OoRTuLWgGD+qd4HoP8LliAZhgHudMVX3Hr6Q=="; }; "@connectrpc/connect-node@npm:2.1.2" = { filename = "@connectrpc-connect-node-npm-2.1.2-021434d5d5-ba8b0c9384.zip"; hash = "sha512-uosMk4TDgps3eqeoJtwM1Qpgk1J9LscBRgSRKDYzbqlgSitazqQrEEd/i+RWnknd7WbKD8d/X1sCcDXgKeE+qQ=="; }; "@connectrpc/connect@npm:2.1.2" = { filename = "@connectrpc-connect-npm-2.1.2-ab34825de3-4e15000890.zip"; hash = "sha512-ThUACJAjt1Lv1E+pQKWuHh+gDhvrxLoM7ScruTWSDYw7hbd0UOZErUQruMqaJ1sMFxNviWuo7/A2F4Ex2wHWFQ=="; }; +"@protobufjs/aspromise@npm:1.1.2" = { filename = "@protobufjs-aspromise-npm-1.1.2-71d00b938f-a83343a468.zip"; hash = "sha512-qDNDpGj/W17Gv/Nv14imTIOeSKB/+fT4E1ZPWMr0TQEc1lBO0hR780g1vXp90hBwUq91WWHGsJj9iQK09lANDw=="; }; +"@protobufjs/base64@npm:1.1.2" = { filename = "@protobufjs-base64-npm-1.1.2-cd8ca6814a-eec925e681.zip"; hash = "sha512-7skl5oEIGvGQuO4jH5utMQHhiau8GC/yedprUx59vSpW8fMG83qAsb6eAKotJxaQ0I3MXzJvccnu2FRmdcjK9g=="; }; +"@protobufjs/codegen@npm:2.0.5" = { filename = "@protobufjs-codegen-npm-2.0.5-bb74ff329d-1b8a2ae56e.zip"; hash = "sha512-G4oq5W7mClbp0gXNS2ByoVA8UGm467kFcQ+XT/AJig0HAGQcE34KjZje3xRCMVahBqlDNpXL9SV0gQ9VAA/cqw=="; }; +"@protobufjs/eventemitter@npm:1.1.1" = { filename = "@protobufjs-eventemitter-npm-1.1.1-dbe0dfc812-8e06193d46.zip"; hash = "sha512-jgYZPUYpxefAnU+MLduo/E36c58BSfM6HZAVaNNbt7i1J3pOhFK6873QswL9WZzyVdGTJnqpOgpHR+I80HPErA=="; }; +"@protobufjs/fetch@npm:1.1.1" = { filename = "@protobufjs-fetch-npm-1.1.1-b98d5396a8-a497ff5433.zip"; hash = "sha512-pJf/VDOFToV38EJ5g+o5uRE7SagSD5RRUpHXYzJwYdLDAT5g4k6kNtCR2vrgGg9usYZ+OxYWBF2Wox2LPGRu1A=="; }; +"@protobufjs/float@npm:1.0.2" = { filename = "@protobufjs-float-npm-1.0.2-5678f64d08-18f2bdede7.zip"; hash = "sha512-GPK97edv/PAXBwivFcnJ22JZt3HmuExRsG3zSpwznbvuwmfRTOC93SCswUKx2YDZg9MUNDmN9/mOsMlKDreQaQ=="; }; +"@protobufjs/path@npm:1.1.2" = { filename = "@protobufjs-path-npm-1.1.2-641d08de76-cece0a938e.zip"; hash = "sha512-zs4Kk45/Xf0voD+MFPLxz4sNbhOscyb/TJbqMR7/1ft64LunVPv1BTEq8uOFACUMkOaFBrl8AjYKQ3k9iKDYtA=="; }; +"@protobufjs/pool@npm:1.1.0" = { filename = "@protobufjs-pool-npm-1.1.0-47a76f96a1-eda2718b7f.zip"; hash = "sha512-7aJxi38iKsbmrTb3WKku+Q0mUmAmoZ9PF/Zo9F4DBqW9c03vP0j1H4E0rgl4tiYqXFF8CLEVpVF1bRo6rfzwOA=="; }; +"@protobufjs/utf8@npm:1.1.2" = { filename = "@protobufjs-utf8-npm-1.1.2-9c7ca5968f-975c6e2c03.zip"; hash = "sha512-l1xuLAMZvVDBdTHRhrU3qIy00yBePKkpfNhCtjHTQdoO4/8WvoZWzSZgdW3HU7KcimoUwK2O1gLB+R3fIuez8w=="; }; "@quixos/camino-datatypes@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-datatypes.git#commit=e061e290709c672d2dc675e5c871733de195211a" = { filename = "@quixos-camino-datatypes-https-faf980267e-4ed01c75c1.zip"; hash = "sha512-TtAcdcG7mReSI/kmbu+U91w39uYetoOiQbq6ex48zaTphDXn1vO1aaw0k8kEDhQgE1LMZrFgHyPzjlUIkyKdrA=="; }; "@types/node@npm:24.13.3" = { filename = "@types-node-npm-24.13.3-b512a0bbeb-a5bc08f49b.zip"; hash = "sha512-pbwI9JuVgdzcqQ4CzXcZejx5mECAdmSULlzVFhpVPU0q6AZPfjKUQQ10jX0Ji1wYSL5/pQwG8pxBk6sWvk5Z6w=="; }; +"@types/node@npm:26.1.1" = { filename = "@types-node-npm-26.1.1-7a7a9f32f4-25ac509319.zip"; hash = "sha512-JaxQkxlXNt0HTVAnrbuoVhe8lR1aQVBuvTuQcwSKy3IzYT84ItX5uUR/nAhByR9jh8Rig5FnZ+PvefI5F0UsRg=="; }; "@typescript/vfs@npm:1.6.4" = { filename = "@typescript-vfs-npm-1.6.4-78935c9d3e-acb9de42f2.zip"; hash = "sha512-rLneQvI/2j9149eQC6EG7zI6L0588OSpTbtFfibTU8pjuzUZPtGjL8hzPzrlkJlhKykLBr1iIWlIYb65v7YvYg=="; }; "debug@npm:4.4.3" = { filename = "debug-npm-4.4.3-0105c6123a-d79136ec6c.zip"; hash = "sha512-15E27GyD7L79D2pVk9pqnJHsTX3cS1TIg9bnHsmsy19noaXpbQCjKBlrW1yG02XpjYo6cIVqrxa057GYXmf1pg=="; }; +"long@npm:5.3.2" = { filename = "long-npm-5.3.2-f80d0f7d39-7130fe1cbc.zip"; hash = "sha512-cTD+HLzi3KBnNLNbcNOAyj9wJxx/iFLJIqfGLIbE418MOSkFZeynEzxiWQjUDhJqxXwCsbGkY2uUV9d+HmC5gQ=="; }; "ms@npm:2.1.3" = { filename = "ms-npm-2.1.3-81ff3cfac1-d924b57e73.zip"; hash = "sha512-2SS1fnMSs7Y60h/Fs9wK9eeNYaH8fPtUV+2vJjJr9ivlMHzIf/toYu8cKzOwIzzbXU8BxMlYzA1mCUi2Wih6SA=="; }; +"protobufjs@npm:7.6.5" = { filename = "protobufjs-npm-7.6.5-ff4a872120-863eaca9c6.zip"; hash = "sha512-hj6sqcb0W/z7h4fFRfU+nGaWUH4yjjmBtGQ8EtNd9/KCYCHBDj/TC+80LBOo6dMGVHusnAhJ7jv1D3cKEvAdxQ=="; }; "typescript@npm:5.4.5" = { filename = "typescript-npm-5.4.5-8568a42232-2954022ada.zip"; hash = "sha512-KVQCKto0D9PWqeK45TT2XVfJLV85iaJjdUp4q6VJ9+ZSmswZIZE1YKS4FsRtzn30pNKfnxGj3A1CE7t20EMlHg=="; }; "typescript@npm:5.9.3" = { filename = "typescript-npm-5.9.3-48715be868-6bd7552ce3.zip"; hash = "sha512-a9dVLOOfl+cR21qgSPb5mVtT8cUvfYZnwavcFwDGinajCPV5zTCc5rU2Rt606aG+fIE6k7qvCijM1TajAnDhxQ=="; }; "typescript@patch:typescript@npm%3A5.4.5#optional!builtin::version=5.4.5&hash=5adc0c" = { filename = "typescript-patch-6e159bfddb-db2ad2a16c.zip"; hash = "sha512-2yrSoWyoKfUEJ+6x2hVeekXlmO7HsIbYtOi6ROWiNfdY5gbWgcZpkiMNP8O4mVhl5f0LIqLJVIbQsyAPgwcuyQ=="; }; "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" = { filename = "typescript-patch-6fda4d02cf-ad09fdf7a7.zip"; hash = "sha512-rQn996dWgU3OZbxgwWV7QNREUTRoWO6iMOEPLpWiidkYO24y5cEelazAzMIUtPNiidytS/GIawrbhNcR0zakMA=="; }; "undici-types@npm:7.18.2" = { filename = "undici-types-npm-7.18.2-3e6d69d829-85a7918911.zip"; hash = "sha512-haeRiRE6I4lZ16ZHNo5PfFVZw6QE69uPxEiBRc6UJvzYIlKoRKMCeY38Djfm+xeP9IHtA7xMr2NMV1fZ70NSHQ=="; }; +"undici-types@npm:8.3.0" = { filename = "undici-types-npm-8.3.0-d34470de3e-c8aa7e2fbe.zip"; hash = "sha512-yKp+L76/zlGWVNr63A7OWb6IjSzK8YD7RJXah157U20kVjRcOEBpx+bz6cmrdDXwdJV9owbxQjQ+7ob/gEiFWg=="; }; }; in overriddenProject diff --git a/yarn.lock b/yarn.lock index be65bd7..b47d038 100644 --- a/yarn.lock +++ b/yarn.lock @@ -66,6 +66,71 @@ __metadata: languageName: node linkType: hard +"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/aspromise@npm:1.1.2" + checksum: 10c0/a83343a468ff5b5ec6bff36fd788a64c839e48a07ff9f4f813564f58caf44d011cd6504ed2147bf34835bd7a7dd2107052af755961c6b098fd8902b4f6500d0f + languageName: node + linkType: hard + +"@protobufjs/base64@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/base64@npm:1.1.2" + checksum: 10c0/eec925e681081af190b8ee231f9bad3101e189abbc182ff279da6b531e7dbd2a56f1f306f37a80b1be9e00aa2d271690d08dcc5f326f71c9eed8546675c8caf6 + languageName: node + linkType: hard + +"@protobufjs/codegen@npm:^2.0.5": + version: 2.0.5 + resolution: "@protobufjs/codegen@npm:2.0.5" + checksum: 10c0/1b8a2ae56ee60a56e9d205cd4b6072a1503c5069b8ebb905710f974ff0098a0d0700641c137e0a8d98dedf14423156a106a9433695cbf52574810f55000fdcab + languageName: node + linkType: hard + +"@protobufjs/eventemitter@npm:^1.1.1": + version: 1.1.1 + resolution: "@protobufjs/eventemitter@npm:1.1.1" + checksum: 10c0/8e06193d4629c5e7c09d4f8c2ddba8fc4dfa739f0149f33a1d901568d35bb7b8b5277a4e8452baf3bdd0b302fd599cf255d193267aa93a0a4747e23cd073c4ac + languageName: node + linkType: hard + +"@protobufjs/fetch@npm:^1.1.1": + version: 1.1.1 + resolution: "@protobufjs/fetch@npm:1.1.1" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.1" + checksum: 10c0/a497ff5433854e8577f0427983ea39b9113b49a8120f94515291d763327061d2c3013e60e24ea436d091dafae01a0f6eb1867e3b1616045d96a31d8b3c646ed4 + languageName: node + linkType: hard + +"@protobufjs/float@npm:^1.0.2": + version: 1.0.2 + resolution: "@protobufjs/float@npm:1.0.2" + checksum: 10c0/18f2bdede76ffcf0170708af15c9c9db6259b771e6b84c51b06df34a9c339dbbeec267d14ce0bddd20acc142b1d980d983d31434398df7f98eb0c94a0eb79069 + languageName: node + linkType: hard + +"@protobufjs/path@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/path@npm:1.1.2" + checksum: 10c0/cece0a938e7f5dfd2fa03f8c14f2f1cf8b0d6e13ac7326ff4c96ea311effd5fb7ae0bba754fbf505312af2e38500250c90e68506b97c02360a43793d88a0d8b4 + languageName: node + linkType: hard + +"@protobufjs/pool@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/pool@npm:1.1.0" + checksum: 10c0/eda2718b7f222ac6e6ad36f758a92ef90d26526026a19f4f17f668f45e0306a5bd734def3f48f51f8134ae0978b6262a5c517c08b115a551756d1a3aadfcf038 + languageName: node + linkType: hard + +"@protobufjs/utf8@npm:^1.1.1": + version: 1.1.2 + resolution: "@protobufjs/utf8@npm:1.1.2" + checksum: 10c0/975c6e2c0319bd50c17531d186b537a88cb4d3205e3ca9297cd842b631d341da0ee3ff16be8656cd2660756dc753b29c8a6a14c0ad8ed602c1f91ddf22e7b3f3 + languageName: node + linkType: hard + "@quixos/camino-datatypes@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-datatypes.git#commit=e061e290709c672d2dc675e5c871733de195211a": version: 0.1.0 resolution: "@quixos/camino-datatypes@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-datatypes.git#commit=e061e290709c672d2dc675e5c871733de195211a" @@ -86,10 +151,22 @@ __metadata: "@connectrpc/connect-node": "npm:^2.1.2" "@quixos/camino-datatypes": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-datatypes.git#commit=e061e290709c672d2dc675e5c871733de195211a" "@types/node": "npm:^24" + protobufjs: "npm:^7.5.4" typescript: "npm:^5.9.3" + bin: + camino-codegen-ts-runtime: dist/src/codegen-ts-runtime.js languageName: unknown linkType: soft +"@types/node@npm:>=13.7.0": + version: 26.1.1 + resolution: "@types/node@npm:26.1.1" + dependencies: + undici-types: "npm:~8.3.0" + checksum: 10c0/25ac5093195736dd074d5027adbba85617bc951d5a41506ebd3b9073048acb7233613f3822d5f9b9447f9c0841c91f6387c46283916767e3ef79f23917452c46 + languageName: node + linkType: hard + "@types/node@npm:^24": version: 24.13.3 resolution: "@types/node@npm:24.13.3" @@ -122,6 +199,13 @@ __metadata: languageName: node linkType: hard +"long@npm:^5.3.2": + version: 5.3.2 + resolution: "long@npm:5.3.2" + checksum: 10c0/7130fe1cbce2dca06734b35b70d380ca3f70271c7f8852c922a7c62c86c4e35f0c39290565eca7133c625908d40e126ac57c02b1b1a4636b9457d77e1e60b981 + languageName: node + linkType: hard + "ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" @@ -129,6 +213,25 @@ __metadata: languageName: node linkType: hard +"protobufjs@npm:^7.5.4": + version: 7.6.5 + resolution: "protobufjs@npm:7.6.5" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.2" + "@protobufjs/base64": "npm:^1.1.2" + "@protobufjs/codegen": "npm:^2.0.5" + "@protobufjs/eventemitter": "npm:^1.1.1" + "@protobufjs/fetch": "npm:^1.1.1" + "@protobufjs/float": "npm:^1.0.2" + "@protobufjs/path": "npm:^1.1.2" + "@protobufjs/pool": "npm:^1.1.0" + "@protobufjs/utf8": "npm:^1.1.1" + "@types/node": "npm:>=13.7.0" + long: "npm:^5.3.2" + checksum: 10c0/863eaca9c6f45bfcfb8787c545f53e9c6696507e328e3981b4643c12d35df7f2826021c10e3fd30bef342c13a8e9d306547bac9c0849ee3bf50f770a12f01dc5 + languageName: node + linkType: hard + "typescript@npm:5.4.5": version: 5.4.5 resolution: "typescript@npm:5.4.5" @@ -175,3 +278,10 @@ __metadata: checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d languageName: node linkType: hard + +"undici-types@npm:~8.3.0": + version: 8.3.0 + resolution: "undici-types@npm:8.3.0" + checksum: 10c0/c8aa7e2fbebfce519654dafadc0ece59be888d2ccaf180fb4495da875e7b536d2456345c384069c7e6f3e9c9ab7435f074957da306f142343eee86ff8048855a + languageName: node + linkType: hard