From ae2d399d6e624a69e05f373e446892f005e6e871 Mon Sep 17 00:00:00 2001 From: BOHEUS <56270748+BOHEUS@users.noreply.github.com> Date: Mon, 10 Nov 2025 08:37:09 +0000 Subject: [PATCH] Hacktober apps fix (#15733) - update dependencies in mailchimp, stripe and last email interaction apps - fix logic in mailchimp integration, now it's triggered by update of People records and allows for update Mailchimp records - fix logic in stripe integration, now properly reads data from webhook - update READMEs to make it more understandable to non-technical users --- .../.yarn/install-state.gz | Bin 9352 -> 10275 bytes .../last-email-interaction/README.md | 2 +- .../application.config.ts | 2 - .../last-email-interaction/package.json | 2 +- .../serverlessFunctions/test/src/index.ts | 124 +++--- .../last-email-interaction/yarn.lock | 37 +- .../.yarn/install-state.gz | Bin 9277 -> 10454 bytes .../mailchimp-synchronizer/README.md | 16 +- .../application.config.ts | 28 +- .../mailchimp-synchronizer/package.json | 4 +- .../mailchimp/src/index.ts | 350 ++++++++++++----- .../mailchimp-synchronizer/yarn.lock | 35 +- .../.yarn/install-state.gz | Bin 9274 -> 10517 bytes .../stripe-synchronizer/README.md | 23 +- .../stripe-synchronizer/application.config.ts | 4 +- .../stripe-synchronizer/package.json | 4 +- .../serverlessFunctions/stripe/src/index.ts | 354 +++++++++++++----- .../serverlessFunctions/stripe/src/types.ts | 37 ++ .../stripe-synchronizer/yarn.lock | 35 +- packages/twenty-apps/hello-world/package.json | 2 +- packages/twenty-apps/hello-world/yarn.lock | 10 +- 21 files changed, 751 insertions(+), 318 deletions(-) create mode 100644 packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/serverlessFunctions/stripe/src/types.ts diff --git a/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/.yarn/install-state.gz b/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/.yarn/install-state.gz index 4e0bb6876de2b1e8f820428cf7f0bb92fed9cfde..fa2d957d93d4644920150e50e99bb4f7dfb5a448 100644 GIT binary patch literal 10275 zcmV+;DBRZ{iwFP!000001MR)rj-^?S9yT$qn(3bI>1mAwOYnsOhc09oonG*WR(I9TsFAJA znt!Lhykoj+N{aob7?<4>3-@Cj1bI+gMex9%H|LPa_4|gwqZV&d;>wo?i*SGc} zub*E3&AZ6IaTi}a&-=fA`n~I)T>o*tx_@?ed;OIDjl1iQ?;q}7cd@vrv>63YbYq}yX)`0u-j)l?;q~2|Ecxt_NPyO)-v%?tk{|HlE#n`mcWZ@(+G-^^4!U{(Dyqa`oKPX`u#92GFTVW2EQaByKxl zPnMipWmBUaIJaSs?7ka?FC2sGaoI-$+`X?h!2n~n*032&aSp}qZTGO~-f5&pEPvPM zcWJ7uj68fYGsBp3>=jO$Yq3PS&pEocuGmC6m<~YiJ2Vz>axTqoYAbVD+cL46x{-nz z8mlhA*|+9cM9Sg|8t*mxXwcsE`NLa}Zi9?4d%CEHwwTDFf!qOIcx{y?Xas#>O3t>^ zJ!!E|;cN`Cg=UH~;%=0ox|@v&(RgaK7UF@EO&r!17ala$u$b>`f41)39gw+A@4bnQ zPOQnRZ#C~Q!hHfF=;*C&?uk9YGFr~Y0I5SZ@4#+MywHNlwvq#{DTdHVyrt7}=)tyO zZs*)!yXCt+e@|us4%)M0S7+t1HKwS}Jt?@4zSN1)+uoF(xk0vUlOEtA>|`@#xCPC` z3~FWw8OL7$^->#g1wb(m|~Vr~xa`usZ#d@eQRh`si*Wvj=Wq2W6GVF1Im)(; zbS|My!?W$x`kp?AYM2{Pb_nnlLWW?2*SkKy81u?mXJVv)r>6J>B2J|k0s>v?-j$@0 z%-QN?=Lv2wb?WKum}~BhW=lL#L|AMl(KMQqb++z^W9JgyG}h+n(ShqI={wth`ylMQ zd3n0Et<1Fc8aa)?b^yx^Pq3hLYN*)L0^A#NO9|Lk?y#|a>00}B)J*NeXHSQ*r>i40 zn%$`bqxTVGO;q6b@caWqp_8M7*~m6@Y>9zAhouI`9-X^XQlzqB0_zw)ZI3ay6FMyk z-DspfV&#a~Lu71Jz(H%YG1yF7r@q64=c)_4_iPL2yT_jl>L%E22f;F5k-2e5!;N#; zZlm7Y=E4Hfw_B?>yl1_oJt_Ax*3oSNPcgB!oZB8gb{fR#*4;ho6>qj;;0Q*rohi|D z**o9Aub+Cf-l_7KNZ-pt=jMnNVnZm+vrJ|Gn3xg{$>)6=B?2boZZNiY>pVcZwemS`kDw~EWI1f<@%P?by-07zHyAvr|=|Qb2yF4W@)^0{iU#T_?*yiHVWthIBk1f0!h*l z#?q$h4g3^LEoi}57|Yrz^5Wu=Js2R`tY_2g5~IhC?rx|{$B^ZHPH)4O^{tUZ0XE?~ zzrPcqheAq-kEPj`owDfMvHG@bSow=0Yp_lwG}x(2dtwkTfDAEmb7|XYw=ojusDylZ zCI$7*)I8mLUCdQLuN^lqv@BIG8Oq-h$S?5Tq z`vt+X?`DELN?;r zae4!#AxXxd?_B>bCTUSY2b;A<^~y@631zQ_YXbr9Y0`HF4&E{rF4dJF&7z0Txp8<` zaklU}R;_EFT{l(z!^9LcwJlu$j9Y1W41_In;5+9JUN5a|9Bb~bu4!#A&N=t6O&k&7 zprgt5HoJ^X9l%7GqbE-Hw#gaxn98ttv7!@n$FXDYEjhW(rgJQA$*Xl5RX}GqN2mSH z`Db$)8-_p%FX|J<^dXgJvF9GsS3@76~wGJm>|kF zdkk+orpFvW_|Ex{(HtAaSf>-KoKjsy*eIb>bOoeupx)sVi@136i>C<0aUH@~xgnj?;_T`Jm@!lW3 zw1>~`fA-~HzjJ%IdsY9&?>>M1|3CZk5AETz)4%leqwC){_q@MEvqI zGJ0B}#03kk0ZF!4WXwkU0$!90IO`Vx*IxkQegUBQ1>oKn09{`I`g{QZ@&#bP7Xag4 z01|rvVCn_np%(yYUH~e20l?w~V1pL`?_B^wcL5;V1>j^C0CimeT6F<{(*=cenBE;o04-J^y9J{gdCg{&76JxAi>Z>mUB;?&a&>{Mi@Jp2w*& zx0H$zI7T*5T(fy{1QP46Xtl^IVQ!0(mXBPLlGaC74K>!9XlpRWZi{UQ%+*idRYn_S zmR%>MBbe`zy3!X;@z|Pc2dMt!>iW-}$Yk;Je2rS%y}JMD1waOO*WZ5@dHe9!?EbTg zztahVDkK3yOK317vI1+@Mk}U6=B@+k-s`5H+z183op%{2G!rRypV;OS;EG%% zdd@L$R%ySSCVHg^JhbYN4s!kRz2Ch|xc=w_*8J1Y?f&6OzOZM{pFF#L$XDj&+3O$0 zFRl3I!H>4%^~bO5_S3xn!Jpi{`uzT-c|QH*7a!OAi`3ho92~iM1Gc)yEMt$|WQ|U? z*EB;hik38!V$g=!36=~NQk`LzIoH|)rTea=c`j22g61*I=0vg9QVG^J)oF@=b9O#= zurR^q3iuYe+~$@t0WGw(*M!k{JMb7w-A=_*Yt2(YqCDP&xrsx9^if%|m=4Tbu&5H? z!K3rmz9G^;6PQjlZgbt_JKs3?`s0T$U*`QAH-B_{7x{=&kG9<}oqGH7#nT(?pU5Y8 zaw-CJg2EZ}bg+ok_HcvljSR9Eq&23+3|1n@)qucf-;2p4#sDWmBw2$OckL|=r|!{{ zeV~Ywv1~Q#X5Jv6a+*2SIM%Q;r*gv)%_w+RK2B^X%j@dyO)xh%+7n&bqb(WGIEfw~*+LD5zfIxPzb)NNFSgXf-5F&=3{E(K?sIe19bJG*J_v$ytR`#aC>?jP~z z(PsOmKiek_-}sXjK#N{jsH!YkYq)}Rl)4EDQ==ee%|>fkPz28ykkYCUoDgFQu~6r+ za2kgpuAKpE?_KE(bVWu|FNcWpjD+Xx`J06ls<>ml_kh~HkFaeW`G%LV>wepShfKcjci(Z-9oi>?$hMC zg#l#Nd0*2P>;-*M$Y@H1H+NdJHdNML=$`hvZ#nCMsdv-5YU&Wca?kE#a5w_Y1;Il` zpB~Ma=@cpfF=a?^6XWf7^oL*E#xsBR)Ylo{CRaAvty;p^|BWJzG1JW zBqTUvYz5DbO!KE-V4KY-lTEL#7($$ zw*eDLP{n)KoGmm&w0eBgO5yynvAi`PQmkUXoa5YZ=fnLV|cPF6Fu zf)JN#{oW*HN5kJ925pE%y8Am{KH0DCUOdU$KYqloN89h4er=z$H&Hl2`w4D?o{8x#C!TV;9JXeG z@$OB`t&K3FARU8}3rfvBw3>K(F=VP!XU8sHsA-eNvik_E4kWj=m(LIi#Iil8VkkPu znP0bsCeA4XEQVEf(|Dxl+;np9A{nw}WaSVp7T&8GiJZPVdeh`qFj3|b$2%Q4gm)Us6p zJ&HtN5TXLjclg?~v7l>2B?BR;0w+{NuxUj^*C}Z+0n~6h7W7zuwrPq^W85sGkl$!s zcyg~EEe?*^&OBO;xOCEh0wE{xvP`H_BsXdwgLdfZwDZ(|#^_rFZHr8>DMUKvCIU%u z1rA%jXpd~%{DkJONhz>kkW{tb!*{fx%{qwuJO%zezk9Ks-DW(we|VMl;t|IlZNT$a z6+Y3EW7(uy0XwJL*t})gkY#`}aV2ir>b-H2Ze-^Mopv=+zEP7 zyZ9mzyhKAA?4~?e$yfE=v8Tk2IR*oCJacR>DBNJ9dp22utBz$Wd~PsG>SArh##k-6 zH4kBEK>USl+#q8ngz?>MTN zT(AcsK|_?f03>NqPIc!#X-sUvtLQxqdjL$DgoT&%)=ob0ZSFE`2#K14)0uB2WQg@j zJ9P9i7Dl~XC%k@&v`%apyhrZ^>g*gioUwR6nk#$?j9~;ZjA+gs38F>7<}dI7~NT2|Y$dO{SdB}gc?Y95UcnmDF<*2_I~ zO`HugWrev!_3hcQfm;sez)tE(g|-WHT1-=_ReK(Qzn}L{^3|)mSC5ve2b=EvPNg?- zmnGVu2(3Qrq)|=Am{VFKtE6)I;+{gbbC=aNO=hni-7FYQAa6$ny5NXopxTS{_dywQ0ecE)J1wOanxvuj(!d$SlWl^Rz=zBdO2Ve-AH2w_+>pRvee+H3{ujJ-xR z^R;5=I?sVWv-^KUo^-SU-_C(whhXivp@}UXb1vDqU>0zr8#=c+7y+`n=C%uC#i>Zz zW^rqm#V+I5y%6Lw!+3g*-D&UejZ7c|q1cNCeb@WMW{JI^fy(n{o`+x_R85I7o7LY0 z&SXSsy?6Dk)?ol*b(s4W7LkqI3)=67qbY;I!r9Smb{uUdufV+#HL8unYS{sibCV5t zU2rxn!o`~EeD2sEMLxGL|FqiXX#0J`uYb4v`X>LKtD;a&p3+Ycby2yAmMaw@vH$^q zEOSw>8e$SHTx<8}u@^R%Q0Y*;du@B}1x0*mdnXZ&(RTeuYz(PdtH_q4I#fA&rr9|d zyo`rb&og9YB@o`)U=YNj+Us6vMZ|Bj7g(s;bMlh#)mM}BL6uo;ZDPE=yg6JI1tqbq ztvttC#Tf0gRWoG;(ClH2ba5dB=YBoG5Ks0Ox8K$^KiYn8zptYlUz&?zoV0Qe9`H#n@gttrs027lA-lsgP4nrN8OQzK2T8 zqM%eoy%s?`2P0asm{W=zQ(Y``nuRyviG^-@s?mIqTNWD=sQ#|>6MY=jM(erKpm`fb zhXNIo3eKAfnc5J$Zz_70>gv9tj65tlX)!&Ujx)dhrPqG_G;i}&kzW2Z40#+_vBFIwtqynYM>{5|qjp(4;gH}`A zZB~;yV3BO4J;8kjsqXE>8~5_u22Dlhu-+l(ju-&4Lqbp(@cb24lM!r&-HqyqpMbU^ z_)jHe!)6n=7@`j+r>)hjE2YhZF17^abY0Wjtc0i)zNXVGPH<5U*r?GsaT>5gX)s2y z9(7nsALs4Yf}JAwv)g>~^3~nTeD(0IGNPjR_)9^DLQUZAoipgxxfm#2_7Bk)P#Pk(q~4|gvfdufk0;M;AT*N*M2 zX<}7NZ6vRO7;T#QAX(j@%~GS$l1;@C$1L&*?VvH+W-u9pXE&!31-1kjT>QvhqPkM8 zRjDGAV?>d3%RK-c<{H6Ts-yF|wrb86r5;ezVVHH)Ia&;D1J=a7>hCO){~VK@q2VIm zssZDDgsn_%d6m?^Sbt{d-fEWA8P>| zY`(YM`Z^X5&|N)W)|d=hC0fz{NG*qL9+J?y4R&TE?$JE7BNechyZR<45nzs@RE2h&)LECWEhORE_D%U|Fd|A5~7Q-hq8)QMq}02D24~Ja$a+JD-Xd7CgaTgCN2O=-)Q)b& z^qmVFOFHRU#T}IJh(?swH5{O@cENgMm|)t_zH;C>&B^LMD%>=O18!Gss3i^^C5hU@ znv3*>(Ry==J%<9C51{a3iAr)`F^cx0ExNAlotv7uZpYFsSAG&Zl3ELowNynIk~rpI zK1UY|ihB9#?&0ncmmY1pZ@XT3LWYiMG75>wf-|vLZw)3Q#!y!Vs6sn8M;FfY>Kx0; zwHv0FoDWy&>OvZcl5A_8OG$ydhzPwW?U3FZE96`hMO@(6)#x~n!h3h|y+wMdY|{I5 zr;&Z3RHr=x1{cN~bG_D%tyW==bf7FMk$K%{5mMsVw)l0&Z#tDqWcsFE@c#y*p*S|7 zF58K0E4%FT>-D2g?_NF@e2=!;+o0l`#=7m@WY&=#MJ-=A3asViYBX^5gGM#AyUlb{ zn@u%m5vhQ)D^LmLQ9!g8HLj$9zBXc1{x@T^=Cc46DS_iD1Hjpbb(5|H9OsF!b4oR* zR~`N^_vxn5Dgtj^GFx_D}5vzO*Fak9v?xrdwtiOf){ zkKPK%OTiPNCR*a2WEN-M{6T?=_g}tPch4V7l#VvuxA9-<`1Co75Z|jI(YO^6Sp*Q4 zPb!Vx@}9%@oU_@QGzc9!bOUc|?Q9(@r|&b$rU}bB@a!cNM#JUGrd_5X?}9caV@FjV%G z&afwLUCuBP`iyC|M}385*1XC|Z;MO7bDjaeu!qln)d6^%gm|n0zs<2ni_Xp+yR#@w zHR)g$yFjeEJ2}%9mJ$z|gQI2ov=Y?MMvG5ZV{a(bi)`Q%x?xu=*hx`(!IX^JXvC0F zl}F*%AV1ZXdD#F=A%`_zGr^;k)Bv|8`$4sDymwQRzb`=GInS za;@#9R+Aq;;P892@!rmm==EE=i6xA*5^h7%YC5k%)T~XLu}%YJqBkaPzK~@X9g|v4 z<_-zNgkfj$9vC}XiPxG>oU|celvC`bV~s|s;WAzLBz{pZf_RNysqrS^EGAmS!cb+dCnn`$W=_RuRAY2v zt#D(>LiV5_J3N0&e;8lfM!x#iy!UA9eOumJGh9+<>6XIUSnj=eeJ%pC4Vbxn&n8Zt z(n{gF2G>Y{iCt*7W~&Bz7*!rFh7CB}>Ytb1dc`@y?Ht=r+}2{3SvC=Kow3o+OPLQ3 zRyx{%-v;(;$viWuC311*q-m={fkjr&;@C%|c+H36H1DOUjbc%Txk@j6H4(WI#cY;o zvv@nBKmp`AAaaiyC{h8$d7h4APMEaj4tk5&|KWoDM;r3n-rTJW#i6N^^oA#S~N zTGUK?j!~rp^yNj61dbn8aB1Xd=F6+98(XfY$p)O+V-{JD1iEbZVuJbM-0X{(V177}`67m>AI|%|h=KZt zGh{Dfn*HIV(u)|Ve>fTQA_nRo&R)ETnf`}U{Vrmr|Ka?&i+F^?31t`YMjTGSx`>S@ zhqIL~B6mET(sL2j)Zz4(;}>l_{rUBeUNc<&(EM}zG%teQ@?q|W_`#d4otLs4;`^_1 z=c~N+Z!Uu zxBs<2_@nFJ`Q^&%f@br?tA~YajfLe6afPFP#78SAXzk=j|_kbO9Wr4`*A!SsXapa9@uD-!FRO zXVA;#8c(Kd?QEXgoqYA^D=7V?=)9ahFHEP1eMUxUC#NlG@r z7KLgOXsJCXIYyV=+h}Z~w>`D$StQ@5Qv@r;DY*`g>pPG5(`pTwVY0Up^h)fANt&d-~Bgv&Vx)79ZBmt@AYUXobo-M06TW^EHU* z_0JbqvK*~%`e1hVot?L%?e^Ar>t~Q;e;<(K1*~Yh`S-3aV6E8AzkhWBJFss4gR2YJ z)^zh9UR}VBp_~8c>gn&icL3*Lh2Mv=9`XG0Jz5y~c7LXSE=JZgj@L6D;(eB3dg74d*7k` zXqEg2wwCuSt{-i^N2+=FM88qZ*U#@B*N@&PcJrTo2fg>BHP9c_mc?`C$=`F(JKhDWNdgt2Be|~iV8^Uh>i>nJ5YH;#|UvJ%g|66wt z9>{z1Ute9oQlgvx=IR1==-m9bR~NAD<>tS;x_|{GH~;ghl8z7zYSM?8Hnck@|Nf3)2mP3qw+slPyc{OAEnAJ+Qs^QO$v)2AN8P~Z$h zxxhnr^i0DK<0d{kY)2dIkwy%Boy7g|>!05pwm7+ z&M&SLkKk8JThD3WM-L4AKrgPN$I!l|_B@s#bS^JGt-yO!@OHG}-jW~{Yk>G`5~MdD zya%o4!FvgB{-^Jt-*oh}+4F;cFhAkE*IKIr{}c|*(bju3+~QfdeIKdY;XvR2{2iSA zqh|nppex$W-hrd7_-FPG91WR$FbLj<(_K5d1pIYH`W;lHAHDB;;^-NYAIN^}^8sW> zPu_fcI6*DQ{>E^E(+3wC>g$7db(~{M2XizZk!VHkXSey} z<*U1w`Rd`zw_?fB%T6A6di3I!Ka&_&FIEY-YrATVLV3MO{e)vq-|Mt1 zuNEoj>R8h38qL>*Pdn|^YuZ>zze|u6&SIh^H4jzh+L1uYwSIUiMosFGa!0taWGyCx z+Bk-G^S@kOKsJ5zzh1q6fad7M4(GI^gFSsdur$87jePa^>vy#2{>)y#qq{3N|J$z> zI{)MqO&6fVI8QbYew;Ixe)Q?x%g0&=hc8Pz^Cq53;P*b&^ytM)=OysLZTz#t@q?mQ z+<*CE-93K`M?2bbAL>xLqt`*6ow%bld*`6bKe}yjv;iNbW4ALp_6342NAI5e5bnWq z^5oI$I^T|QcoyR>vO#+EYRmJ6{K0y~^A6Pud-&{EQ!?JZS&v=}`hni8M=#6!z$ern zZN9G`yT5<+$Io6pd|}U@9=$^2FaQ5?R{qUDyt;s~>No$#)j!sh@|*wjY&7}J|MiV| zmNVA^Uwd_+4KCXpQV3(|HX@XVFx#ZPCOdJMl@l`R%>d#ilu~l$|iHIIoqnu zZ>$_($z5`}T+xOtSYPDQ2N}ZlI5RGpk-dOm zuLxQnD4CG>xq!&H%#^pgtI<_MYOt`ue_+_A?~;7K`zP6e{Yz}b>E7Mx(>0=`p5=Q= zT3~>lI(2aBJrR)^5l=i3@&Eny-SuC5ar^4W@#g+-esTYB_wsl3L4SDtZ~Wx?mA;JY z=huJd&f{<2`4=zZ{`XJsyZ*uTAH|#d+q+lS&+*^CyZ+Yw!`++s;q{+fnQsXlw%Xo0 z2kZnKL%WK|SjMbv?OeXv92SyhmOWs$-nS25pk|$E!v=0HOL{b0t~mj0jnQDYhMu^Q zhS_vp>Hs z87pNbc1(wr3gkhS9o;sJ!S%Roqc=#R-B;5FzIyM_wXJFF0%SeO)yW7)i2JsWeSVu= zgoS~JEoSC6)*89pP-1VM9%gF|vQ7ehPHHNnfl<2nZX6t2f>d-zZ0$K0kcufvQw3#_ z-C$8ljEyKfZMV+37meQf$3DMXCnp`K2ti>}%%%GRc5z^8V3T*s>=P6lZHO7GXP5V3+V>R!*#C!K?4vTeK+c|CWz_Ew=`e^yHl95>h=Dv)mPaT9T z!JAUls0}w;4WP-O-jgYSqG@=K)w#8hF_Y7Ql#w^6GinN24w zvT64C*ypEUo{b%I5s(-Qj}%{1WaUC<8nnfb!Fnc!MP!>fQzv<|IoVKG%wg)d0)k_a z$;zgEA#KhL*c+|UQ>oW^5o>EN-OSzE$3B0$Hd|X%*gbb{-MY!wa&gNL59Ba$=8zQY z(N^~aZ4Oi1l@n$MLSJnoQyX)FEZBq&JH@uAfrn?y>62E_4%)cqR+wvyk9~f~iImjB zWOoMB*h2%~pX+RBvl}a+h3YRVQrFY@W5=QMX{BpVy*)QQrZgvXk} zGe27X7~wk<_ogK*4JU#lstsr$BFHvaCp4eDsW-`H#+)l$&=HAM@N^!>8YSC~vi8Kp zVbODU%4KVaxSBQ$YTj)7M1`h<_s2f}ROTJ4Y+}T07827|L*x+jG23i3%kR#>5zNu+ zu=8rBZOM|;=~#PZg6f_YFz4i{D-f6BS}d!R0gQ}2IdSadWloLDl;or3pEeLEyOpMU zPhrNLd&E*~n%;OWw`l4Zh8+Yb)SFoc?lHZkmO6B#ZP`2D-kKpaY*APnxl9aN$3=$J z7>tIGy+9g&1J6Ier4brfQytNVjGR7zVrYo7=OCt8L6OXCtFeq>OLL6DgkZR*OM;@< z@EyZF$DAWU8VuZ{kHM^d3H0^c5De9C`VB}VoLAo2SsZ;wl?m28^X!DcTP4YV8UAGSy&d@X?rgz!;(f;@C zQ;$xB6~}m3ZgW}5?%UB?Bzn4=wm~@BF0)@Udusw&?l$(+y?j%jLuXj}#%{57H#12YI@P?3 zm^fZEWzZ{dDuf<`LxFn)g-DCPC8&c{-UX$d)nYHd@R2(5;l>F{HIb_~`Syppi?9 z>C?uR=vz-wbfj-ejSdyR2(UQI5(I4-vV{U1dAAlm6-{mSG|WCmz#0{hZ;L=jBF1iE zM&+OhsJrexn0*A>N5}t@l%zEr%Oj+93>s}toYZ^cP*NxH9JcC8!HG#qY10vQrN^z$ ziMEx&MU=1sQnoR2WfICB3Fu2EueIwts@PUS-qEZL_m4imzCu@G0LvPI3KL3R9d4b_!T#J<+)3Si)`dW&y)S1|vN)XDsfk%Nm<|@a}}866hc`FwBpRKfB?`HfD>- z8)>z%Y|M(YSTa;S+4iLAk$!@HT1X#R@xU4@A)2Vh@`WJ_+a8Ihc$qTUts8lh&4{P7 z-vuRj1q`TIJm>zn{N*|QpU)-fMmM{QgH@{{Efm!`++u8^879 z?f?Ji%RkVE&rg5p`6t)Eqh@h`f4g7AyZ7Gzwd+5*D&dS)cX20>Y)PE#OLe%$Sj4&( zLW8jswr@dNTPrrf*}1o&tAz)N?}~Niz_GftWgW&2%uPe*1{Gc~k|x?*b6J3jp0N05`h;=<5Q|s|x_2E&xNi02t^3kemwuV=e$sxd2Gy0#J<$ z046Q~Tetvt-~uxI3rNy0AkV&lRQUq3-3v%qFCgdq!C$`qm+bD9-#*;lz0wyyOZ0y5 zE7#xm+k4$F!oT|B_wHW5edmw9xP9TLgivGF0fjN5H-Wvn1-k>XWL*^-t)kVwF>u?6 zJ!6K_h)Q6_-V1aO2G71($82lU?CCT|A1uqV)I`wKa)j)#4QraG#7=J_Ke)R7izg0> z|8BfZmhax&|L_7b)w}EO+fj8<>mS6kGl1L&YdtHDSclo>0$l2{t9M80X8F-}Nm4Q5ZwG3u*yvFmT$+uiGE z*Pom)f`8@r_5R_n#7n(>@mFqNJ;WQe((Lv3{AW^p_uxm%@%meD^wno^{hNP$_vXj< zuhruDr$6~tr5vphw#o-3qEkZ_&0*TeI%*4I>Gf2;oCUGP?hTtQ%FU?L$7qHKfO8Mt zOhP>rmgJyd8!)G#@=Q@BRqM@L`JS~z+4s;h1#^etUC}u!ZbDX=+vD$ty^&^saB7 zm9hXw+ej02O6poLTl2wNxpf;dNuZ)yrnc5@Gtt{((kT<>#F5mnE#&Ou{J`>RleG^{ zh0RN*MerQS+$u*P?NcdrWq}Wycr1bzmQbE8%(CU)f)gh!A1O^am}xnW-ZMm^+Z>n@ zvb(ixHoI?coI7W4XZrkp-o1En_s7rPgx|gidw5K>M@#Sn)$*s~q*?*7J$O=+%4)X6 zDUI%_bCy#JGl9QN`sucmOH5 z4nxLG2AaBpnbkcT!DzOE#f^Bk7zvGeH#=ADck=Gd%V(|+`iM@C7Tr&E`Wq*m(x>fH zI;qqW*OJBp(bJSu9X&^Ef?=&etjT~4mh2gopsb+YmaS|V%Y}zG@15IPu&j}yI`_!A ztb|54QN`W%zbvANyDMwXYM<PBc89?Ld?bqVg3BSh>8+_%9(sLZb->SK=4?plJE840 zqX4rN+M)ovGrfLW@4tLyKfZlIpI71|zadTR42vvo_i zovZT%6ZaOQ`wW@w6qQft6`^*MHA8*1CJM`-^3M8Q;+nFQA$g8U`@PfT#Z&-24EEK} zG+HQZ7>c*bKv(0f5v}5zNDvwW(`6H8oQeamS7fr?g_ihvR8gM7-q<3=~ar`J-B!C$*rU9F05<% zkkz$yCf#nsm+euZaVq7Wt3GaV4@JW^cEOeI$85Q|#}={j>Xcc=m92|M2Ga z)n^a-*(16=T88iI7JhyW+aSljtXZ>)G5e0Wvhg0H^O};a4KjjJm4vlb0I+p{Sq@+Z zZj2>nObf???z{z7b|7zU;X+89RS-Mr#8S=X&@pj}N;5pu?esaOQiUBdsDuSxhwB8^ ztT{U&YdGg1igEHiFpR;ZFuLuwW*bAX`B22Q(z#)4bLbjVT4BreHMR3zI~e+aD%KII zR#gJdbo*;2syKbox`EVpxgws*-5n}ag9HCmRBaTpOX=2X@yt6H1W=bS1L;OsUE zYc^CgAB7lLRc{zzRt=ExWNx#wDZ7hBY)-YUpv%Q#A=8BL!CB z5>@tek^-FE`vUV^b3)~oG(gxFw$+-?)#`3q+&F2YW~(eA=7et6`c9xKuA7rn)iDMQ zsJxP@x;)eLRO2|)=Wo2ceHqUlzI+{zr8P&(?)|i;&S-zDR3XULr*0l=?U}IKs;oh> z5%)D1T8qN#H0M|ymHS}a;$GJ-=o3lGrg{i zihhd`vC0umol87c=!Qb*K1iZ+Qxu?d9C$N6FX_0wVX{fI)37TkLZ?VmEdQ>gEh&e{MnWv~{mx;B`p z?9nTb*ct}1SC2R$`p8}S-n($v5M70U0)Z?;=t)Bpa03$N+5mzuShfzNz4vapSBD|o z5pnH)eo)m59EEVyHDOHEzIK+UoK|5?6{iZhN8df#bb($F|L}3yKP zR5fhjD5LSUX78w#{cYP!F;Tk37@O7}qe}IIxX)!Alns5xj+sbPhIO*7;9Ymy?i;JJ zi&j;W45FT}t1xe34U^8#=qBoIY7>#FXfAOZHj3vXIo#${g6t$nTh_F}do7gWjrgd$ z$wnok<|{f)oMYpRzK5h$p&f#6S>FAsq&5sX(yK;%boyj#kNXW@X5(E2>J;d5k7?4;bu3P8->H z=+HJfg_4_LXjR6lh&ONuF_Csvr@=8TI#LVa^Ek;nWC4S?TDcqPD>NCE*RbLq4hcqY zIe{3wx0EW(Dg{a3!>Fqdo0Y>>*_669X_L`1v1mI`WQ?)VeCg_izmD9aO0H7D&bdqd z?a%f8&q!#F7T|};>f2a}dY6E*foEV~CLI5@@ZC zE$@mq&(CXq@#<87d$w<1`C}Nw!Q%T6E_|!jDjf9?pM6a7%HMhBnml1hMxz;{Viw^{ zNr>oC^qoCfv#v(2tRJ~6YB@fF#)?fCZ5Hux8DOits{7u=gv`BfuK#*&(drs||GXN< zEP!SUB)FO_Q}Ir+d&-`x2k}O0RcZ&MaiYM8;S%fCeM~L3Tccc5q_cpHPOwrTenHn7s#ls=Wy zDw`Q50uyFH-zmJt-o@3V)jRIJz2PF)jSgaJSM>4|*|gg97!AL{j8UekM9 zz1c)7l587=Y;CnAi?kj!4auUe#>JYOS5Ac0nP3A}Mc>6xiYvWkDM8y38{2GAI8Cg1 z!d+E%P!5^Cpi2KmFWT7pITGB9KOD1YDbaDO;VQ5nPW*3`6F_DIO`~{_4ePVQPQ+~x z9i3u_wr+hC5vfU~HquqzfQwgb!bK@QuK-4*bsIxe}bZSG70wR1z97{)~UF&tIv(nT46LZ_8J{bmt?Pe zIcEsdj#I^V&mw(FpHR1t5NK#oW|u=(6!;*~fh|=s)&?e3R326wf!%Pa6K|K{P~02Oe*NOhXP?K5*YRlDbF?78E{tkkgmRrMxoxiPZO4{9CvbTNl!l?Rf*Gg) z)Tv3DGT zkb-4=PGjkwVrqWkvrpKJMQL5p5K{+^sBWZQR^(B1fX!xAalx>`>twyYa4{PuOeQOS zqiADU-jv)JK7bR8ha9sF15)fgXAxoBtmqJz$8K7*DsJ7gOAsH!|+ zw6!HsdWXGd5E)c?evhIdH$$DUCl2gPz9&JFf{b~d`^5dTyZ!HlJv`HgyO)n0+M@;d zVUYY*v7<>AGlRt!H>vz=t2&Ct(Bv_rb<)Ac49Mi&TnMn>_sFVlp8B+66q{;v3@kFr zMzD-7y$dhej;X1J>Za)=XIagRram z$|N|uV3R7ZZp5)@lecONaE}v9dvUY96pKDmp$-ccFTb#jP1M*{6x)`EpI2ai=by;q z9xcNU3p;N;ZWcWV>YK+FwTiU4gQlI@x>l?lsMLWQb$6aoTy85fl}Qo2gut{%>uWcI z>Ta_r6ueb$2oA4yM7P$eA~vDYO2)Qy2<;fK&!NS-xT?Al!y2TCbdc)b8`ZUvL%YpM z8@y9D^jt$4sEvkh3%ku=+CGY$Cf!MP8gUm@lQh^gwH%qJpYUb#y+!8a7RbH^^EtFw zIMnMmcMo@usPt&jece6G#aNZGbdG{yqOeS`r4{o5fDbX@)~X^A$xf3atoR*s$IcF6 z)o}&8TAzG&oPsm86BjHFS#%+y!1PJfSz4@(Ui2dNta9WzRFO&3GUudfgb1T$h9hXh zEIy2T8(aXdul=^YXZOMePzKD6lgH`mbD!0}*f+ne%P+$s@-HivFb&A2I0Qx_$kq&C z-9ag3aRDbDhF~U$fKwyq$^BCR~Ly@ zbjQX)(s0L0m~9H>hEune?F8@&6qb+PZ51>)s|W4mDqzu2(n*4i{YfJfM+@)kSZZZH zw1!o8b5$cc_u?SW*`RG(SZ!3>D2C-)tLt7k5Uf!nZPxeB=mZt~(ejdMw5`;5QT1|y z!Zu+QnoEJRaLsCp8@-xfhQc{ayqbX0mbwz5t01nSZ5yu6T;xM97%owup*P`G{Uu!f zcgO_X#RZGROd#&yQYa8{%LOUhxQ%GEigTZ%zQR`RR(Xf@%?&=FNBs6nefa$6@c-jf z;bR5(b&4(UdAg#MD_20NTGpQaR_nRQG&wfU1#)ssL%HZr5eGPle}deUnX4 zeepF}fF_?9^IYpw6SH^S z)y=I~9)2qH#%@}Fep06@UL&Njr=7U1#V*t6bE?Vm1UnCso?d2iv;e;jZxS$%2$$r5tKhJzZWbN>`If)m(>&CWyYeM(8Tu&Zu+)c#Y~zy!nH zNYFvss=n0b+>7-bMK6mWnCQg_pw@~i(v=wqDqAyP+GAGFWwlplF{o>*)l+ZEIa-20 z)$E^M{n724hcEQS^P>mo{MGl`gP#A5pEhv*-MGJd@kQnT7tz3Z8IN#yyWT|%Tspj4 z<|5j~5AOxLh{;!nw_{wyHRdJ*I34zF!ph7^t_mS4oY z(8C$K7csu}aQ@UqOiewU+j|jngbyc4Ud9mLqiMhwG0^#N?%hRedk%EOEO$Iq00{x`0__qLw@2Wmgo&*CDA z`A>5i^KZUe&2%ZFn7{KTGTy{1dw(4B^{@YQ$JDo4F^8_d`$AuR_Jw{Hf9EtX=8IQ9 zepTbTzFoWK|M1J#asA2P`*(lu53hgYXS2ue+x2%nUdJnsS9bf>WR4!FeDfb(J^$6~ z@4t?C^SfVtwC6P({VOm0?Re07@n1fF=TCp}Zkg9lzIOp!iYGC&`poQ)mf2U${@rAu z8e08xtN*okFT8K|pS-&B_^QqSt$+LP{_dY)^GEaBZvM}!3z(05^B-MZz)0Vl|M=?p zuYKd^|L`q|PibuO`9*Lv1?wvp!FS$W1fRCAc)zzlf8;e#_Vvvg-O;42Co_Ki%-)Wc z+Xwb`8o&M(dwcuy$Ge@Qr)=N+Cs!9R8T#f=uP$JW@y&mFbpZo+Z~n8Z3m6l7^PgWm z|BY|@HawcV`NZa-pWTT^OYadM+&-1}d~p5z@pt0U({ykCi(f?gc`#-7scd&R4?K@% zI-eW;X}iOFunS#c;CVDR`e_ZwJ~z6fiRF*owC(Jsy+ETonu`9^S{BdD?r8D72Qt4) zviZ~Ae{Oic{?k`}5vEtMHTsBgR zm%w8))vOgUD&PY~Z0|s0O^c*-*OE~qSTLw+Zlj`}VkXqAfb$sy2S3g`1G@g!+x-mh zkXn!Z#bfh_l~$30s=aCkx8+NyvS6utTQE}F;j3!;gGFc$Y&z6u&(?BED_WyeNA*Gj zr#_e%4HyZBfo1v4keDVJ8~e*O=LyImvC(T_d*Tc!>3-Dg9HO)~aaMIJ5uJT`30#$9R@Le?8XYUpaQ z42QDCjy+k+sC*%D$ANk$$Pu|ZC4{qUkI*VG)6T_o>pU$f^wWtO8D^;3zFMNDzc=SS! zoB#3`kQyG{we|G2iJcvQqXqauybfp83HT8t(dR@5i5&+6+{S)jz+A{EaWe} z^9Q}ts94_Q4=5f3gBJVN{R$lF&@J2zj)n};tyaAikNseR1w`kCSV zVtS=M&hQSN*7rnLN1j{X(K8SKyqGgbM=rk%q`vXG-O=F8lfsIgNX4w0^}gwL?qer^ zpIOP#^}zE0<+DS=N{+a67adlDVo{aJ@n^&a7PcR|8oh5 z3$L$fFSSpvW(HEBR(6VwmgPR}l&nIJY=WBOQd3B2@@apsSWSJXIWaW@$&IH@skt;R z!d$b#QMeY-E&;EO=~?P5|M5%0^go}#%fXK2Dfqb$wR!lqqcd&7 zsUG=_PpCh7f6NmfpLewQzUrO+^y-gp-#mPwFP`7(^#`E z-rdFeunwY93mdYmBngzT9B*=QgGr%qAk{WO6w#P+P)dz6)7^Vk|KX}oE?kF$!LA9D z%0BDj)a&26|MBhX*Wo|<;`W8#|Fs|d%Jui(?xQ-r@b@0{A?o>m^Zy44F;vU`<^TW( Cy!r?L diff --git a/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/README.md b/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/README.md index d954bf3281..7153e2b486 100644 --- a/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/README.md +++ b/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/README.md @@ -17,5 +17,5 @@ twenty app sync ## Flow - Checks if fields are created, if not, creates them on fly -- Extracts the timedate of message and calculates the last interaction status +- Extracts the datetime of message and calculates the last interaction status - Fetches all users and companies connected to them and updates their Last interaction and Interaction status fields \ No newline at end of file diff --git a/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/application.config.ts b/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/application.config.ts index 4169ea83d8..72427e0e4e 100644 --- a/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/application.config.ts +++ b/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/application.config.ts @@ -9,13 +9,11 @@ const config: ApplicationConfig = { TWENTY_API_KEY: { universalIdentifier: 'aae3f523-4c1f-4805-b3ee-afeb676c381e', isSecret: true, - value: '', description: 'Required to send requests to Twenty', }, TWENTY_API_URL: { universalIdentifier: '6d19bb04-45bb-46aa-a4e5-4a2682c7b19d', isSecret: false, - value: '', description: 'Optional, defaults to cloud API URL', }, }, diff --git a/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/package.json b/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/package.json index 792ccd5e6d..d61e24601f 100644 --- a/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/package.json +++ b/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/package.json @@ -10,7 +10,7 @@ "packageManager": "yarn@4.9.2", "dependencies": { "axios": "^1.12.2", - "twenty-sdk": "^0.0.3" + "twenty-sdk": "^0.0.4" }, "devDependencies": { "@types/node": "^24.7.2" diff --git a/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/serverlessFunctions/test/src/index.ts b/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/serverlessFunctions/test/src/index.ts index c26e60d595..93178f7b7e 100644 --- a/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/serverlessFunctions/test/src/index.ts +++ b/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/serverlessFunctions/test/src/index.ts @@ -1,5 +1,4 @@ import axios from 'axios'; -import { setTimeout } from 'timers/promises'; import { type ServerlessFunctionConfig } from 'twenty-sdk/application'; const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? ''; @@ -7,7 +6,6 @@ const TWENTY_URL = process.env.TWENTY_API_URL !== '' && process.env.TWENTY_API_URL !== undefined ? `${process.env.TWENTY_API_URL}/rest` : 'https://api.twenty.com/rest'; -const DELAY = 500; const create_last_interaction = (id: string) => { return { @@ -100,6 +98,53 @@ const interactionData = (date: string, status: string) => { }; }; +const updateInteractionStatus = async (objectName: string, id: string, messageDate: string, status: string) => { + const options = { + method: 'PATCH', + url: `${TWENTY_URL}/${objectName}/${id}`, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${TWENTY_API_KEY}`, + }, + data: { + lastInteraction: messageDate, + interactionStatus: status + } + }; + try { + const response = await axios.request(options); + if (response.status === 200) { + console.log('Successfully updated company last interaction field'); + } + } catch (error) { + if (axios.isAxiosError(error)) { + throw error; + } + throw error; + } +} + +const fetchRelatedCompanyId = async (id: string) => { + const options = { + method: 'GET', + url: `${TWENTY_URL}/people/${id}`, + headers: { + Authorization: `Bearer ${TWENTY_API_KEY}`, + }, + }; + try { + const req = await axios.request(options); + if (req.status === 200 && req.data.person.companyId !== null && req.data.person.companyId !== undefined) { + return req.data.person.companyId; + } + } catch (error) { + if (axios.isAxiosError(error)) { + throw error; + } + throw error; + } +} + export const main = async (params: { properties: Record; recordId: string; @@ -147,7 +192,6 @@ export const main = async (params: { if (response2.status === 201) { console.log('Successfully created company last interaction field'); } - await setTimeout(DELAY); } if (company_interaction_status === undefined) { const response2 = await axios.request( @@ -156,7 +200,6 @@ export const main = async (params: { if (response2.status === 201) { console.log('Successfully created company interaction status field'); } - await setTimeout(DELAY); } if (person_last_interaction === undefined) { const response2 = await axios.request( @@ -165,7 +208,6 @@ export const main = async (params: { if (response2.status === 201) { console.log('Successfully created person last interaction field'); } - await setTimeout(DELAY); } if (person_interaction_status === undefined) { const response2 = await axios.request( @@ -174,11 +216,10 @@ export const main = async (params: { if (response2.status === 201) { console.log('Successfully created person interaction status field'); } - await setTimeout(DELAY); } // Extract the timestamp of message - const messageDate = properties.receivedAt; + const messageDate = properties.after.receivedAt; const interactionStatus = calculateStatus(messageDate); // Get the details of person and related company @@ -190,8 +231,7 @@ export const main = async (params: { }, }; const messageDetails = await axios.request(messageOptions); - await setTimeout(DELAY); - const peopleIds = []; + const peopleIds: string[] = []; for (const participant of messageDetails.data.messages .messageParticipants) { peopleIds.push(participant.personId); @@ -199,78 +239,22 @@ export const main = async (params: { const companiesIds = []; for (const id of peopleIds) { - const options = { - method: 'GET', - url: `${TWENTY_URL}/people/${id}`, - headers: { - Authorization: `Bearer ${TWENTY_API_KEY}`, - }, - }; - try { - const req = await axios.request(options); - companiesIds.push(req.data.person.companyId); - await setTimeout(DELAY); - } catch (error) { - if (axios.isAxiosError(error)) { - throw error; - } - throw error; - } + companiesIds.push(await fetchRelatedCompanyId(id)); } // Update the field value depending on the timestamp for (const id of peopleIds) { - const peopleOptions = { - method: 'PATCH', - url: `${TWENTY_URL}/people/${id}`, - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${TWENTY_API_KEY}`, - }, - data: interactionData(messageDate, interactionStatus), - }; - try { - const response = await axios.request(options); - if (response.status === 200) { - console.log('Successfully updated company last interaction field'); - await setTimeout(DELAY); - } - } catch (error) { - if (axios.isAxiosError(error)) { - throw error; - } - throw error; - } + await updateInteractionStatus("people", id, messageDate, interactionStatus); } for (const id of companiesIds) { - const companiesOptions = { - method: 'PATCH', - url: `${TWENTY_URL}/companies/${id}`, - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${TWENTY_API_KEY}`, - }, - data: interactionData(messageDate, interactionStatus), - }; - try { - const req = await axios.request(companiesOptions); - if (req.status === 200) { - console.log(`Successfully updated company with ID ${id}`); - await setTimeout(DELAY); - } - } catch (error) { - if (axios.isAxiosError(error)) { - throw error; - } - throw error; - } + await updateInteractionStatus("companies", id, messageDate, interactionStatus); } } catch (error) { if (axios.isAxiosError(error)) { - console.error(error); + console.error(error.message); return {}; } - console.log(error); + console.error(error); return {}; } }; diff --git a/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/yarn.lock b/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/yarn.lock index 816b723340..b9e0067362 100644 --- a/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/yarn.lock +++ b/packages/twenty-apps/hacktoberfest-2025/last-email-interaction/yarn.lock @@ -5,13 +5,14 @@ __metadata: version: 8 cacheKey: 10c0 -"Last email interaction@workspace:.": - version: 0.0.0-use.local - resolution: "Last email interaction@workspace:." +"@types/node@npm:^24.7.2": + version: 24.10.0 + resolution: "@types/node@npm:24.10.0" dependencies: - axios: "npm:^1.12.2" - languageName: unknown - linkType: soft + undici-types: "npm:~7.16.0" + checksum: 10c0/f82ed7194e16f5590ef7afdc20c6d09068c76d50278b485ede8f0c5749683536e3064ffa8def8db76915196afb3724b854aa5723c64d6571b890b14492943b46 + languageName: node + linkType: hard "async-function@npm:^1.0.0": version: 1.0.0 @@ -217,6 +218,16 @@ __metadata: languageName: node linkType: hard +"last-email-interaction@workspace:.": + version: 0.0.0-use.local + resolution: "last-email-interaction@workspace:." + dependencies: + "@types/node": "npm:^24.7.2" + axios: "npm:^1.12.2" + twenty-sdk: "npm:^0.0.4" + languageName: unknown + linkType: soft + "math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" @@ -246,3 +257,17 @@ __metadata: checksum: 10c0/fe7dd8b1bdbbbea18d1459107729c3e4a2243ca870d26d34c2c1bcd3e4425b7bcc5112362df2d93cc7fb9746f6142b5e272fd1cc5c86ddf8580175186f6ad42b languageName: node linkType: hard + +"twenty-sdk@npm:^0.0.4": + version: 0.0.4 + resolution: "twenty-sdk@npm:0.0.4" + checksum: 10c0/550f1d85bf0701396c9dd2d4c6bc55ba1b067fce13636f8540eec60ab6a4257c6d7cd86cb3f62e0974bf99467bc31270d92b17b9681a1b7a6281b7ef97224080 + languageName: node + linkType: hard + +"undici-types@npm:~7.16.0": + version: 7.16.0 + resolution: "undici-types@npm:7.16.0" + checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a + languageName: node + linkType: hard diff --git a/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/.yarn/install-state.gz b/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/.yarn/install-state.gz index 880526dee4a7a2fc3e9c5375746b33c019ffe04b..54e1243c1b5d327252989b41e3fe451d76ebbc52 100644 GIT binary patch literal 10454 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 diff --git a/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/README.md b/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/README.md index 343a141606..023e0c4d11 100644 --- a/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/README.md +++ b/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/README.md @@ -5,6 +5,7 @@ Synchronizing contacts between Twenty and Mailchimp ## Requirements - twenty-cli `npm install -g twenty-cli` - an `apiKey`. Go to `https://twenty.com/settings/api-webhooks` to generate one +- Mailchimp API key - Mailchimp > avatar in top right corner > Profile > Extras > API keys ## Setup 1. Add app to your workspace @@ -13,13 +14,20 @@ twenty auth login cd mailchimp-synchronizer twenty app sync ``` - 2. Go to Settings > Integrations > Mailchimp synchronizer > Settings and add required variables ## Flow - Check if required variables are set, if not, exit -- Validate data based on set constraints -- If all constraints are checked, send a request to Mailchimp with new contact +- Validate data based on set constraints, if data doesn't match constraints, exit +- Check if person already exists in Mailchimp: + - if yes, check if UPDATE_PERSON is set to true + - if UPDATE_PERSON is true, check if Twenty record is the same as Mailchimp record + - if they're the same, exit + - if not, update + - if UPDATE_PERSON is false, exit + - if person doesn't exist in Mailchimp, send a request to Mailchimp with new contact ## Note -- SMS support is experimental and may cause errors \ No newline at end of file +- SMS support is experimental and may cause errors +- constraints are directly responsible for sent data so if e.g. you want to have a company name in +Mailchimp, you have to set IS_COMPANY_CONSTRAINT to true \ No newline at end of file diff --git a/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/application.config.ts b/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/application.config.ts index b0dfedae96..3de3b32dab 100644 --- a/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/application.config.ts +++ b/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/application.config.ts @@ -3,63 +3,67 @@ import { type ApplicationConfig } from 'twenty-sdk/application'; const config: ApplicationConfig = { universalIdentifier: '1eadac4e-db9f-4cce-b20b-de75f41e34dc', displayName: 'Mailchimp synchronizer', - description: '', + description: 'Synchronizes Twenty contacts in Mailchimp', + icon: "IconMailFast", applicationVariables: { TWENTY_API_KEY: { universalIdentifier: '0af17af3-66b8-40cf-b6e2-6a29a1da5464', isSecret: true, - value: '', description: 'Required to send requests to Twenty', }, TWENTY_API_URL: { universalIdentifier: '12949c1c-aed7-4a9f-bd06-9fd15f0bfa63', - value: '', + isSecret: false, description: 'Optional, defaults to cloud API URL', }, MAILCHIMP_API_KEY: { universalIdentifier: 'f10d4e8a-8055-4eb2-b9ad-efd69d43b1f0', isSecret: true, - value: '', description: 'Required to send requests to Mailchimp', }, - MAILCHIMP_URL: { + MAILCHIMP_SERVER_PREFIX: { universalIdentifier: '6c8b6ac9-dd45-4f0b-a397-c4a38edccfd9', - value: '', + isSecret: false, + description: 'Required to send requests to Mailchimp (it\'s found in url, e.g. https://us9.admin.mailchimp.com > us9 is prefix)', + }, + MAILCHIMP_AUDIENCE_ID: { + universalIdentifier: '5492f06f-bb29-4c93-9436-b4736a396376', + isSecret: false, description: 'Required to send requests to Mailchimp', }, IS_EMAIL_CONSTRAINT: { universalIdentifier: '62626c57-470f-4866-be1e-5b4d7ec09f9f', + isSecret: false, value: 'false', description: 'Set to true if you want to add additional constraint (default is false)', }, IS_PHONE_CONSTRAINT: { universalIdentifier: 'fac8ec5b-dade-46bf-b938-3dfdef0aa298', + isSecret: false, value: 'false', description: 'Set to true if you want to add additional constraint (default is false)', }, IS_COMPANY_CONSTRAINT: { universalIdentifier: '9ffd8e76-4ab2-42f9-8549-3622a5ae2343', + isSecret: false, value: 'false', description: 'Set to true if you want to add additional constraint (default is false)', }, IS_ADDRESS_CONSTRAINT: { universalIdentifier: '4b899eb6-517e-4afd-bbf8-88097900ea42', + isSecret: false, value: 'false', description: 'Set to true if you want to add additional constraint (default is false)', }, UPDATE_PERSON: { universalIdentifier: '9d753e1e-4408-40ca-b0f0-5c7e8625c2aa', + isSecret: true, value: 'false', - description: 'Set to true if you want to update record if it exists', - }, - MAILCHIMP_AUDIENCE_ID: { - universalIdentifier: '5492f06f-bb29-4c93-9436-b4736a396376', - value: '', - description: 'Required to send requests to Mailchimp', + description: 'Set to true if you want to update record in Mailchimp if it exists', }, }, }; diff --git a/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/package.json b/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/package.json index 0081e496bd..654f8a3e3c 100644 --- a/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/package.json +++ b/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/package.json @@ -9,8 +9,8 @@ }, "packageManager": "yarn@4.9.2", "dependencies": { - "axios": "^1.13.1", - "twenty-sdk": "^0.0.3" + "axios": "^1.13.2", + "twenty-sdk": "^0.0.4" }, "devDependencies": { "@types/node": "^24.7.2" diff --git a/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/serverlessFunctions/mailchimp/src/index.ts b/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/serverlessFunctions/mailchimp/src/index.ts index db191ba9f2..4ce00dac41 100644 --- a/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/serverlessFunctions/mailchimp/src/index.ts +++ b/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/serverlessFunctions/mailchimp/src/index.ts @@ -7,16 +7,19 @@ const TWENTY_API_URL: string = : 'https://api.twenty.com/rest'; const TWENTY_API_KEY: string = process.env.TWENTY_API_KEY ?? ''; const MAILCHIMP_API_URL: string = - process.env.MAILCHIMP_URL !== '' && process.env.MAILCHIMP_URL !== undefined - ? `https://${process.env.MAILCHIMP_URL}.api.mailchimp.com/3.0/` + process.env.MAILCHIMP_SERVER_PREFIX !== '' && + process.env.MAILCHIMP_SERVER_PREFIX !== undefined + ? `https://${process.env.MAILCHIMP_SERVER_PREFIX}.api.mailchimp.com/3.0/` : ''; const MAILCHIMP_API_KEY: string = process.env.MAILCHIMP_API_KEY ?? ''; const MAILCHIMP_AUDIENCE_ID: string = process.env.MAILCHIMP_AUDIENCE_ID ?? ''; -const IS_EMAIL_CONSTRAINT: boolean = process.env.IS_EMAIL_CONSTRAINT == 'true'; -const IS_COMPANY_CONSTRAINT: boolean = process.env.COMPANY_CONSTRAINT == 'true'; -const IS_PHONE_CONSTRAINT: boolean = process.env.IS_PHONE_CONSTRAINT == 'true'; +const IS_EMAIL_CONSTRAINT: boolean = process.env.IS_EMAIL_CONSTRAINT === 'true'; +const IS_COMPANY_CONSTRAINT: boolean = + process.env.COMPANY_CONSTRAINT === 'true'; +const IS_PHONE_CONSTRAINT: boolean = process.env.IS_PHONE_CONSTRAINT === 'true'; const IS_ADDRESS_CONSTRAINT: boolean = - process.env.IS_ADDRESS_CONSTRAINT == 'true'; + process.env.IS_ADDRESS_CONSTRAINT === 'true'; +const UPDATE_PERSON: boolean = process.env.UPDATE_PERSON === 'true'; type mailchimpAddress = { street1: string; @@ -27,6 +30,29 @@ type mailchimpAddress = { country: string; }; +type mailchimpRecord = { + id?: string; + email_channel?: { + email: string; + marketing_consent?: { + status: string; + }; + }; + sms_channel?: { + sms_phone: string; + marketing_consent?: { + status: string; + }; + }; + mergeFields: { + FNAME: string; + LNAME: string; + ADDRESS: string | mailchimpAddress; + COMPANY: string; + PHONE: string; + }; +}; + type twentyAddress = { addressStreet1: string; addressStreet2: string; @@ -36,22 +62,27 @@ type twentyAddress = { addressCountry: string; }; +type twentyCompany = { + name: string; + address: twentyAddress; +}; + type twentyPerson = { name: { firstName: string; lastName: string; }; - email: { + emails: { primaryEmail: string; }; phones: { primaryPhoneNumber: string; primaryPhoneCallingCode: string; }; - companyId: string; + companyId: string | null; }; -const fetchCompanyData = async (companyId: string): Promise => { +const fetchCompanyData = async (companyId: string): Promise => { const options = { method: 'GET', headers: { @@ -60,11 +91,13 @@ const fetchCompanyData = async (companyId: string): Promise => { url: `${TWENTY_API_URL}/company/${companyId}`, }; try { - const temp = await axios.request(options); - return { - name: temp.data.name as string, - address: temp.data.address as twentyAddress, - }; + const response = await axios.request(options); + return response.status === 200 + ? ({ + name: response.data.name as string, + address: response.data.address as twentyAddress, + } as twentyCompany) + : ({} as twentyCompany); } catch (error) { if (axios.isAxiosError(error)) { throw error; @@ -94,7 +127,7 @@ const checkAddress = (address: twentyAddress): mailchimpAddress => { const checkAudiencePermissions = async ( audienceId: string, -): Promise => { +): Promise => { const options = { method: 'GET', headers: { @@ -109,7 +142,6 @@ const checkAudiencePermissions = async ( if (axios.isAxiosError(error)) { throw error; } - throw error; } }; @@ -120,23 +152,23 @@ const prepareData = ( phoneNumber: string, phoneCallingCode: string, companyName: string, - address: mailchimpAddress | null, -): object => { - const data = {} as any; + address: mailchimpAddress | string, +): mailchimpRecord => { + let data = { + mergeFields: {}, + } as mailchimpRecord; + data.mergeFields.FNAME = firstName; + data.mergeFields.LNAME = lastName; if (IS_EMAIL_CONSTRAINT) { - data['email_channel'] = { + data.email_channel = { email: email, marketing_consent: { status: 'unknown', }, }; } - data['mergeFields'] = { - FNAME: firstName, - LNAME: lastName, - }; if (IS_ADDRESS_CONSTRAINT) { - data['mergeFields']['ADDRESS'] = address; + data.mergeFields.ADDRESS = address; } if (IS_PHONE_CONSTRAINT) { const mergedPhoneNumber: string = phoneCallingCode.startsWith('+') @@ -156,110 +188,247 @@ const prepareData = ( return data; }; +const addTwentyPersonToMailchimp = async ( + convertedRecord: mailchimpRecord, +): Promise => { + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${MAILCHIMP_API_KEY}`, + }, + url: `${MAILCHIMP_API_URL}/audiences/${MAILCHIMP_AUDIENCE_ID}/contacts`, + data: convertedRecord, + }; + try { + const response = await axios.request(options); + return response.status === 200; + } catch (error) { + if (axios.isAxiosError(error)) { + throw error; + } + } +}; + +const checkIfTwentyPersonExistsInMailchimp = async ( + email?: string, + phoneNumber?: string, + cursor?: string, +) => { + const options = { + method: 'GET', + headers: { + Authorization: `Bearer ${MAILCHIMP_API_KEY}`, + }, + url: cursor + ? `${MAILCHIMP_API_URL}/audiences/${MAILCHIMP_AUDIENCE_ID}/contacts?count%3D1000%26cursor%3D${cursor}` + : `${MAILCHIMP_API_URL}/audiences/${MAILCHIMP_AUDIENCE_ID}/contacts?count%3D1000`, + }; + try { + const response = await axios.request(options); + const doesPersonExist: mailchimpRecord | undefined = + (response.data.contacts.find( + (contact: any) => contact.email_channel.email === email, + ) as mailchimpRecord) || + (response.data.contacts.find( + (contact: any) => contact.sms_channel.sms_phone === phoneNumber, + ) as mailchimpRecord); + if (doesPersonExist !== undefined) { + return doesPersonExist; + } + if (response.data.next_cursor === undefined) { + return undefined; + } else { + await checkIfTwentyPersonExistsInMailchimp( + email, + phoneNumber, + response.data.next_cursor, + ); + } + return undefined; + } catch (error) { + if (axios.isAxiosError(error)) { + throw error; + } + } +}; + +const compareTwoRecords = ( + object1: mailchimpRecord, + object2: mailchimpRecord, +) => { + return ( + object1.email_channel?.email === object2.email_channel?.email && + object1.sms_channel?.sms_phone === object2.sms_channel?.sms_phone && + object1.mergeFields.FNAME === object2.mergeFields.FNAME && + object1.mergeFields.LNAME === object2.mergeFields.LNAME && + object1.mergeFields.ADDRESS === object2.mergeFields.ADDRESS && + object1.mergeFields.COMPANY === object2.mergeFields.COMPANY + ); +}; + +const updateTwentyPersonInMailchimp = async ( + mailchimpRecordId: string, + twentyConvertedRecord: mailchimpRecord, +): Promise => { + const options = { + method: 'PATCH', + headers: { + Authorization: `Bearer ${MAILCHIMP_API_KEY}`, + 'Content-Type': 'application/json', + }, + url: `${MAILCHIMP_API_URL}/audiences/${MAILCHIMP_AUDIENCE_ID}/contacts/${mailchimpRecordId}`, + data: twentyConvertedRecord, + }; + try { + const response = await axios.request(options); + return response.status === 200; + } catch (error) { + if (axios.isAxiosError(error)) { + throw error; + } + } +}; + export const main = async (params: { properties: Record; recordId: string; userId: string; -}): Promise => { +}): Promise => { if (!IS_EMAIL_CONSTRAINT && !IS_PHONE_CONSTRAINT) { - console.log( + console.warn( 'Function exited as there are no constraints to email nor phone number', ); return {}; } - if ( MAILCHIMP_API_URL === '' || MAILCHIMP_API_KEY === '' || MAILCHIMP_AUDIENCE_ID === '' ) { - console.log('Missing Mailchimp required parameters'); + console.warn('Missing Mailchimp required parameters'); return {}; } - if (IS_COMPANY_CONSTRAINT && TWENTY_API_KEY === '') { - console.log('Missing Twenty related parameters'); + console.warn('Missing Twenty related parameters'); return {}; } try { - const { properties, recordId } = params; - - console.log(properties); - console.log(recordId); - - const twentyRecord: twentyPerson = properties as twentyPerson; - const email: boolean = - IS_EMAIL_CONSTRAINT && twentyRecord.email.primaryEmail !== ''; - const phoneNumber: boolean = - IS_PHONE_CONSTRAINT && - twentyRecord.phones.primaryPhoneNumber !== '' && - twentyRecord.phones.primaryPhoneCallingCode !== ''; - const company: any = IS_COMPANY_CONSTRAINT - ? await fetchCompanyData(properties.after.companyId) - : null; - const companyName: string = company['name'] !== '' ? company['name'] : null; - const address: mailchimpAddress | null = IS_ADDRESS_CONSTRAINT - ? checkAddress(company['address']) - : null; - + const { properties } = params; + const twentyRecord: twentyPerson = properties.after as twentyPerson; if ( twentyRecord.name.firstName === '' || twentyRecord.name.lastName === '' ) { - console.error('First or last name is empty'); - return {}; + throw new Error('First or last name is empty'); } - const audiencePermissions: string[] = await checkAudiencePermissions( - MAILCHIMP_AUDIENCE_ID, - ); + const audiencePermissions: string[] | undefined = + await checkAudiencePermissions(MAILCHIMP_AUDIENCE_ID); if ( IS_EMAIL_CONSTRAINT && - audiencePermissions.includes('Email') && - !email + audiencePermissions?.includes('Email') && + twentyRecord.emails.primaryEmail === '' ) { - console.error('Email is empty'); - return {}; + throw new Error('Email is empty'); } if ( IS_PHONE_CONSTRAINT && - audiencePermissions.includes('SMS') && - !phoneNumber + audiencePermissions?.includes('SMS') && + (twentyRecord.phones.primaryPhoneNumber === '' || + twentyRecord.phones.primaryPhoneCallingCode === '') ) { - console.error('Phone number is empty'); - return {}; + throw new Error('Phone number is empty'); } - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${MAILCHIMP_API_KEY}`, - }, - url: `${MAILCHIMP_API_URL}/audiences/${MAILCHIMP_AUDIENCE_ID}/contacts`, - data: prepareData( - twentyRecord.name.firstName, - twentyRecord.name.lastName, - twentyRecord.email.primaryEmail, - twentyRecord.phones.primaryPhoneNumber, - twentyRecord.phones.primaryPhoneCallingCode, - companyName, - address, - ), - }; - const temp = await axios.request(options); - if (temp.status === 200) { - console.log('Person has been successfully added'); - return {}; - } else { - throw temp; + let companyName: string = ''; + let address: mailchimpAddress | string = ''; + if (IS_COMPANY_CONSTRAINT) { + if (twentyRecord.companyId === null) { + throw new Error('Missing relation to company record'); + } + const company: twentyCompany = await fetchCompanyData( + twentyRecord.companyId, + ); + companyName = company.name ?? ''; // either "" or name + address = IS_ADDRESS_CONSTRAINT ? checkAddress(company.address) : ''; } + + const twentyPersonToMailchimpRecord: mailchimpRecord = prepareData( + twentyRecord.name.firstName, + twentyRecord.name.lastName, + twentyRecord.emails.primaryEmail, + twentyRecord.phones.primaryPhoneNumber, + twentyRecord.phones.primaryPhoneCallingCode, + companyName, + address, + ); + console.log(twentyPersonToMailchimpRecord); + + const isTwentyPersonInMailchimp: mailchimpRecord | undefined = + await checkIfTwentyPersonExistsInMailchimp( + twentyRecord.emails.primaryEmail, + twentyPersonToMailchimpRecord.sms_channel?.sms_phone, + ); + if (isTwentyPersonInMailchimp !== undefined) { + console.log( + `Person ${twentyRecord.name.firstName} ${twentyRecord.name.lastName} found in Mailchimp`, + ); + if (UPDATE_PERSON) { + if ( + !compareTwoRecords( + isTwentyPersonInMailchimp, + twentyPersonToMailchimpRecord, + ) && + isTwentyPersonInMailchimp.id + ) { + const isTwentyPersonUpdatedInMailchimp: boolean | undefined = + await updateTwentyPersonInMailchimp( + isTwentyPersonInMailchimp.id, + twentyPersonToMailchimpRecord, + ); + if (!isTwentyPersonUpdatedInMailchimp) { + throw new Error( + `Person ${twentyRecord.name.firstName} ${twentyRecord.name.lastName} update in Mailchimp failed`, + ); + } else { + console.log( + `Person ${twentyRecord.name.firstName} ${twentyRecord.name.lastName} update in Mailchimp succeeded`, + ); + } + } else { + console.log( + `Person ${twentyRecord.name.firstName} ${twentyRecord.name.lastName} wasn't updated in Mailchimp because they're the same`, + ); + } + } else { + console.log( + `Person ${twentyRecord.name.firstName} ${twentyRecord.name.lastName} wasn't updated in Mailchimp because UPDATE_PERSON is set to false`, + ); + } + } else { + console.log( + `${twentyRecord.name.firstName} ${twentyRecord.name.lastName} doesn't exist in Mailchimp, adding`, + ); + const isTwentyPersonAddedToMailchimp: boolean | undefined = + await addTwentyPersonToMailchimp(twentyPersonToMailchimpRecord); + if (!isTwentyPersonAddedToMailchimp) { + throw new Error( + `Adding ${twentyRecord.name.firstName} ${twentyRecord.name.lastName} person to Mailchimp failed`, + ); + } else { + console.log( + `Person ${twentyRecord.name.firstName} ${twentyRecord.name.lastName} has been successfully added`, + ); + } + } + return {}; } catch (error) { if (axios.isAxiosError(error)) { - console.error(error.message); + console.error(error.response); return {}; } console.error(error); @@ -276,5 +445,10 @@ export const config: ServerlessFunctionConfig = { type: 'databaseEvent', eventName: 'person.created', }, + { + universalIdentifier: '657ece26-4478-4408-a257-4e9e16cce279', + type: 'databaseEvent', + eventName: 'person.updated', + }, ], }; diff --git a/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/yarn.lock b/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/yarn.lock index 8b626e0b40..f394702ffd 100644 --- a/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/yarn.lock +++ b/packages/twenty-apps/hacktoberfest-2025/mailchimp-synchronizer/yarn.lock @@ -5,6 +5,15 @@ __metadata: version: 8 cacheKey: 10c0 +"@types/node@npm:^24.7.2": + version: 24.10.0 + resolution: "@types/node@npm:24.10.0" + dependencies: + undici-types: "npm:~7.16.0" + checksum: 10c0/f82ed7194e16f5590ef7afdc20c6d09068c76d50278b485ede8f0c5749683536e3064ffa8def8db76915196afb3724b854aa5723c64d6571b890b14492943b46 + languageName: node + linkType: hard + "async-function@npm:^1.0.0": version: 1.0.0 resolution: "async-function@npm:1.0.0" @@ -26,14 +35,14 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.13.1": - version: 1.13.1 - resolution: "axios@npm:1.13.1" +"axios@npm:^1.13.2": + version: 1.13.2 + resolution: "axios@npm:1.13.2" dependencies: follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.4" proxy-from-env: "npm:^1.1.0" - checksum: 10c0/de9c3c6de43d3ee1146d3afe78645f19450cac6a5d7235bef8b8e8eeb705c2e47e2d231dea99cecaec4dae1897c521118ca9413b9d474063c719c4d94c5b9adc + checksum: 10c0/e8a42e37e5568ae9c7a28c348db0e8cf3e43d06fcbef73f0048669edfe4f71219664da7b6cc991b0c0f01c28a48f037c515263cb79be1f1ae8ff034cd813867b languageName: node linkType: hard @@ -213,7 +222,9 @@ __metadata: version: 0.0.0-use.local resolution: "mailchimp-synchronizer@workspace:." dependencies: - axios: "npm:^1.13.1" + "@types/node": "npm:^24.7.2" + axios: "npm:^1.13.2" + twenty-sdk: "npm:^0.0.4" languageName: unknown linkType: soft @@ -246,3 +257,17 @@ __metadata: checksum: 10c0/fe7dd8b1bdbbbea18d1459107729c3e4a2243ca870d26d34c2c1bcd3e4425b7bcc5112362df2d93cc7fb9746f6142b5e272fd1cc5c86ddf8580175186f6ad42b languageName: node linkType: hard + +"twenty-sdk@npm:^0.0.4": + version: 0.0.4 + resolution: "twenty-sdk@npm:0.0.4" + checksum: 10c0/550f1d85bf0701396c9dd2d4c6bc55ba1b067fce13636f8540eec60ab6a4257c6d7cd86cb3f62e0974bf99467bc31270d92b17b9681a1b7a6281b7ef97224080 + languageName: node + linkType: hard + +"undici-types@npm:~7.16.0": + version: 7.16.0 + resolution: "undici-types@npm:7.16.0" + checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a + languageName: node + linkType: hard diff --git a/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/.yarn/install-state.gz b/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/.yarn/install-state.gz index c5a6ac9676b5a374526d070d5f052ab7210c2220..d1c64dfe959235855106b7466f03a2a61498e196 100644 GIT binary patch literal 10517 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 diff --git a/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/README.md b/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/README.md index 6559219f0f..cb55bba6f7 100644 --- a/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/README.md +++ b/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/README.md @@ -5,7 +5,7 @@ Synchronizes customers from Stripe to Twenty ## Requirements - twenty-cli `npm install -g twenty-cli` - an `apiKey`. Go to `https://twenty.com/settings/api-webhooks` to generate one -- +- Stripe secret API key - available in Stripe workbench ## Setup 1. Synchronize app @@ -14,15 +14,24 @@ twenty auth login cd stripe-synchronizer twenty app sync ``` -2. Go to Settings > Integrations > Stripe synchronizer > Settings and add values +2. Go to Stripe > Workbench > Webhooks and add webhook: +- events: customer.subscription.created and customer.subscription.updated +- webhook endpoint +- destination: `{TWENTY_URL}/s/webhook/stripe`, e.g. https://workspace.twenty.com/s/webhook/stripe +3. Go to Twenty > Settings > Integrations > Stripe synchronizer > Settings and add values ## Flow -1. Retrieve webhook -2. Check if it's either subscription created or updated -3. Read customer ID, sub status and quantity -4. Read customer data +1. Retrieve Stripe webhook +2. Check if it's either subscription created or updated, if not, exit +3. Read customer ID, sub status and quantity from webhook +4. Read customer data from Stripe API, if business name is empty, exit 5. Check if customer company exists in Twenty, if not, create it 6. Check if related person exists in Twenty, if not, create it and link to company +## Notes +- app synchronizes only new customers, those created before start of app won't be synchronized unless they're updated +- customers will be added to Twenty People object only if their name and email are filled with data, otherwise app will throw an error + ## Todo -- add validation of signature key from Stripe to ensure that incoming request is valid \ No newline at end of file +- add validation of signature key from Stripe to ensure that incoming request is valid + (possible once request headers are exposed to serverless functions) \ No newline at end of file diff --git a/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/application.config.ts b/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/application.config.ts index e4e3e5f4f7..fb496a0003 100644 --- a/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/application.config.ts +++ b/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/application.config.ts @@ -4,23 +4,21 @@ const config: ApplicationConfig = { universalIdentifier: '0ed2bcb8-64ab-4ca1-b875-eeabf41b5f95', displayName: 'Stripe synchronizer', description: 'Plugin synchronizing data from Stripe to Twenty', + icon: "IconMoneyBag", applicationVariables: { TWENTY_API_KEY: { universalIdentifier: 'b0d9569b-da3e-4dad-b7b1-36c96f0598b9', isSecret: true, - value: '', description: 'Required to send requests to Twenty', }, TWENTY_API_URL: { universalIdentifier: 'fa50e016-e045-497a-9cdf-0949e7ef9f7a', isSecret: false, - value: '', description: 'Optional, defaults to cloud API URL', }, STRIPE_API_KEY: { universalIdentifier: '807d67d6-f720-49c4-a93e-ef16cf4fe919', isSecret: true, - value: '', description: 'Required to send request to Stripe', }, }, diff --git a/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/package.json b/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/package.json index b67ad4d2db..daaf361ee7 100644 --- a/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/package.json +++ b/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/package.json @@ -9,8 +9,8 @@ }, "packageManager": "yarn@4.9.2", "dependencies": { - "axios": "^1.13.1", - "twenty-sdk": "^0.0.3" + "axios": "^1.13.2", + "twenty-sdk": "^0.0.4" }, "devDependencies": { "@types/node": "^24.7.2" diff --git a/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/serverlessFunctions/stripe/src/index.ts b/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/serverlessFunctions/stripe/src/index.ts index 8fd54b9548..a007e545b7 100644 --- a/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/serverlessFunctions/stripe/src/index.ts +++ b/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/serverlessFunctions/stripe/src/index.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import { type ServerlessFunctionConfig } from 'twenty-sdk/application'; +import { type stripeCustomer, type stripeEvent, type stripeStatus, type twentyObject } from './types'; const TWENTY_API_KEY: string = process.env.TWENTY_API_KEY ?? ''; const TWENTY_API_URL: string = @@ -9,43 +10,9 @@ const TWENTY_API_URL: string = const STRIPE_API_KEY: string = process.env.STRIPE_API_KEY ?? ''; const STRIPE_API_URL: string = 'https://api.stripe.com/v1/customers'; -enum stripeStatus { - Incomplete = 'INCOMPLETE', - IncompleteExpired = 'INCOMPLETE_EXPIRED', - Trialing = 'TRIALING', - Active = 'ACTIVE', - PastDue = 'PAST_DUE', - Canceled = 'CANCELED', - Unpaid = 'UNPAID', - Paused = 'PAUSED', -} - -type stripeData = { - quantity: number; -}; - -type stripeItems = { - data: stripeData[]; -}; - -type stripeResponse = { - customer: string; - items: stripeItems; - status: stripeStatus; - type: string; -}; - -type stripeCustomer = { - businessName: string; -}; - -type twentyObject = { - id: string; - nameSingular: string; - fields: Record[]; -}; - -const getCompaniesObject = async (): Promise => { +const getTwentyObjectData = async ( + objectSingularName: string, +): Promise => { const options = { method: 'GET', headers: { @@ -57,7 +24,7 @@ const getCompaniesObject = async (): Promise => { const response = await axios.request(options); if (response.status === 200) { const companyObject = response.data.data.objects.find( - (object: twentyObject) => object.nameSingular === 'company', + (object: twentyObject) => object.nameSingular === objectSingularName, ); return (companyObject as twentyObject) ?? ({} as twentyObject); } @@ -76,59 +43,61 @@ const createFields = async (objectId: string, fieldName: string) => { objectMetadataId: objectId, name: 'seats', label: 'Seats', + icon: 'IconMan', } : { type: 'SELECT', objectMetadataId: objectId, name: 'subStatus', label: 'Sub Status', + icon: 'IconStatusChange', options: [ { color: 'iris', label: 'Incomplete', - value: stripeStatus.Incomplete, + value: 'INCOMPLETE', position: 1, }, { color: 'sky', label: 'Incomplete (expired)', - value: stripeStatus.IncompleteExpired, + value: 'INCOMPLETE_EXPIRED', position: 2, }, { color: 'amber', label: 'Trialing', - value: stripeStatus.Trialing, + value: 'TRIALING', position: 3, }, { color: 'green', label: 'Active', - value: stripeStatus.Active, + value: 'ACTIVE', position: 4, }, { color: 'orange', label: 'Past due', - value: stripeStatus.PastDue, + value: 'PAST_DUE', position: 5, }, { color: 'brown', label: 'Canceled', - value: stripeStatus.Canceled, + value: 'CANCELED', position: 6, }, { color: 'red', label: 'Unpaid', - value: stripeStatus.Unpaid, + value: 'UNPAID', position: 7, }, { color: 'gray', label: 'Paused', - value: stripeStatus.Paused, + value: 'PAUSED', position: 8, }, ], @@ -167,7 +136,11 @@ const getStripeCustomerData = async ( try { const response = await axios(options); return response.status === 200 - ? (response.data as stripeCustomer) + ? ({ + name: response.data.name, + businessName: response.data.business_name, + email: response.data.email, + } as stripeCustomer) : ({} as stripeCustomer); } catch (error) { if (axios.isAxiosError(error)) { @@ -176,10 +149,9 @@ const getStripeCustomerData = async ( } }; -const checkIfCompanyExistsInTwenty = async (name: string | undefined) => { - if (!name) { - return {}; - } +const checkIfCompanyExistsInTwenty = async ( + name: string | undefined, +): Promise => { const options = { method: 'GET', headers: { @@ -189,9 +161,10 @@ const checkIfCompanyExistsInTwenty = async (name: string | undefined) => { }; try { const response = await axios(options); - return response.status === 200 && response.data.data.companies.length > 0 - ? response.data.data.companies[0] - : {}; + return response.status === 200 && + response.data.data.companies[0].id !== undefined + ? (response.data.data.companies[0].id as string) + : ''; } catch (error) { if (axios.isAxiosError(error)) { throw error; @@ -200,8 +173,8 @@ const checkIfCompanyExistsInTwenty = async (name: string | undefined) => { }; const updateTwentyCompany = async ( - companyId: string, - seats: number, + companyId: string | undefined, + seats: number | null, subStatus: stripeStatus, ): Promise => { const options = { @@ -226,11 +199,11 @@ const updateTwentyCompany = async ( } }; -const createTwentyCustomer = async ( +const createTwentyCompany = async ( customerName: string | undefined, - seats: number, + seats: number | null, subStatus: string, -) => { +): Promise => { const options = { method: 'POST', headers: { @@ -246,7 +219,7 @@ const createTwentyCustomer = async ( }; try { const response = await axios(options); - return response.status === 201 ? response.data : {}; + return response.status === 201 ? (response.data.data.id as string) : ''; } catch (error) { if (axios.isAxiosError(error)) { throw error; @@ -254,85 +227,258 @@ const createTwentyCustomer = async ( } }; -export const main = async (params: { - properties: unknown; -}): Promise => { +const checkIfStripePersonExistsInTwenty = async (email: string | null) => { + const options = { + method: 'GET', + headers: { + Authorization: `Bearer ${TWENTY_API_KEY}`, + 'Content-Type': 'application/json', + }, + url: `${TWENTY_API_URL}/people?filter=emails.primaryEmail%5Beq%5D%3A%22${email}%22`, // mail is unique by default so there can be only 1 person with given mail + }; + try { + const response = await axios.request(options); + return response.status === 200 && + response.data.data.people[0].id !== undefined + ? (response.data.data.people[0].id as string) + : ''; + } catch (error) { + if (axios.isAxiosError(error)) { + throw error; + } + } +}; + +const addTwentyPerson = async ( + firstName: string, + lastName: string, + email: string, + companyId: string, + seats: number, + subStatus: stripeStatus, +): Promise => { + const options = { + method: 'POST', + headers: { + Authorization: `Bearer ${TWENTY_API_KEY}`, + 'Content-Type': 'application/json', + }, + url: `${TWENTY_API_URL}/people`, + data: { + firstName: firstName, + lastName: lastName, + emails: { primaryEmail: email }, + companyId: companyId, + seats: seats, + subStatus: subStatus, + }, + }; + try { + const response = await axios.request(options); + return response.status === 201; + } catch (error) { + if (axios.isAxiosError(error)) { + throw error; + } + } +}; + +const updateTwentyPerson = async ( + id: string, + seats: number, + subStatus: stripeStatus, +): Promise => { + const options = { + method: 'PATCH', + headers: { + Authorization: `Bearer ${TWENTY_API_KEY}`, + 'Content-Type': 'application/json', + }, + url: `${TWENTY_API_URL}/people/${id}`, + data: { + seats: seats, + subStatus: subStatus, + }, + }; + try { + const response = await axios.request(options); + return response.status === 200; + } catch (error) { + if (axios.isAxiosError(error)) { + throw error; + } + } +}; + +export const main = async ( + params: Record, +): Promise => { if (TWENTY_API_KEY === '' || STRIPE_API_KEY === '') { - console.warn('Missing variables'); - return {}; + throw new Error('Missing variables'); } try { - // TODO: add validation of signature key from Stripe - const { properties } = params; - const stripe = properties as stripeResponse; - const allowed_types = [ + // TODO: add validation of signature key from Stripe (not possible at the moment as headers aren't accessible in serverless functions) + const stripe = params as stripeEvent; + const allowed_types: string[] = [ 'customer.subscription.created', 'customer.subscription.updated', ]; if (!allowed_types.includes(stripe.type)) { - console.error('Wrong webhook'); + throw new Error('Wrong type of webhook'); + } + + const stripeCustomer: stripeCustomer | undefined = + await getStripeCustomerData(stripe.data.object.customer); + if ( + stripeCustomer?.businessName === undefined || + stripeCustomer?.businessName === '' + ) { + console.warn('Set customer business name in Stripe'); return {}; } - const companyObject = await getCompaniesObject(); + const companyObject = await getTwentyObjectData('company'); if ( companyObject?.fields.find((field) => field.name === 'seats') === undefined ) { - const t: boolean | undefined = companyObject?.id + const seatsFieldCreated: boolean | undefined = companyObject?.id ? await createFields(companyObject?.id, 'seats') : false; - if (t === false) { - console.error('Seats field creation failed'); - return {}; + if (!seatsFieldCreated) { + throw new Error('Seats field creation in Company object failed'); + } else { + console.info('Seats field creation in Company object succeeded'); } } if ( companyObject?.fields.find((field) => field.name === 'subStatus') === undefined ) { - const t: boolean | undefined = companyObject?.id + const subStatusFieldCreated: boolean | undefined = companyObject?.id ? await createFields(companyObject?.id, 'subStatus') : false; - if (t === false) { - console.error('Sub status field creation failed'); - return {}; + if (!subStatusFieldCreated) { + throw new Error('Sub status field creation in Company object failed'); + } else { + console.info('Sub status field creation in Company object succeeded'); } } - const stripeCustomer = await getStripeCustomerData(stripe.customer); - if (stripeCustomer?.businessName) { - console.warn('Set customer business name in Stripe'); - return {}; + const personObject = await getTwentyObjectData('person'); + if ( + personObject?.fields.find((field) => field.name === 'seats') === undefined + ) { + const seatsFieldCreated: boolean | undefined = personObject?.id + ? await createFields(personObject?.id, 'seats') + : false; + if (!seatsFieldCreated) { + throw new Error('Seats field creation in People object failed'); + } else { + console.info('Seats field creation in People object succeeded'); + } } - const twentyCustomer = await checkIfCompanyExistsInTwenty( - stripeCustomer?.businessName, - ); - if (Object.keys(twentyCustomer).length === 0) { - const a = await createTwentyCustomer( - stripeCustomer?.businessName, - stripe.items.data[0].quantity, - stripe.status.toUpperCase(), - ); - if (Object.keys(a).length === 0) { - console.error('Creation of Stripe customer in Twenty failed'); - return {}; + if ( + personObject?.fields.find((field) => field.name === 'subStatus') === + undefined + ) { + const subStatusFieldCreated: boolean | undefined = personObject?.id + ? await createFields(personObject?.id, 'subStatus') + : false; + if (!subStatusFieldCreated) { + throw new Error('Sub status field creation in People object failed'); + } else { + console.info('Sub status field creation in People object succeeded'); + } + } + + const twentyCompanyId: string | undefined = + await checkIfCompanyExistsInTwenty(stripeCustomer?.businessName); + const seats: number = + stripe.data.object.quantity ?? + stripe.data.object.items.data.reduce( + (acc, item) => acc + item.quantity, + 0, + ); // we don't know if subscription has only 1 item (product) or more + let updatedTwentyCompanyId: string | undefined; + if (twentyCompanyId === '') { + const twentyCompanyCreated: string | undefined = + await createTwentyCompany( + stripeCustomer?.businessName, + seats, + stripe.data.object.status.toUpperCase(), + ); + if (twentyCompanyCreated === '') { + throw new Error('Creation of Stripe customer in Twenty failed'); + } else { + console.log('Creation of Stripe customer in Twenty succeeded'); + updatedTwentyCompanyId = twentyCompanyCreated; } } else { - const a = await updateTwentyCompany( - twentyCustomer.id, - stripe.items.data[0].quantity, - stripe.status.toUpperCase() as stripeStatus, - ); - if (!a) { - console.error('Update of Stripe customer in Twenty failed'); - return {}; + const twentyCompanyUpdated: boolean | undefined = + await updateTwentyCompany( + twentyCompanyId, + seats, + stripe.data.object.status.toUpperCase() as stripeStatus, + ); + if (!twentyCompanyUpdated) { + throw new Error('Update of Stripe customer in Twenty failed'); + } else { + console.log('Update of Stripe customer in Twenty succeeded'); + updatedTwentyCompanyId = twentyCompanyId; } } + + if (updatedTwentyCompanyId === undefined || updatedTwentyCompanyId === '') { + throw new Error('TwentyCompanyId not found'); + } else { + const stripeCustomerInTwenty: string | undefined = + await checkIfStripePersonExistsInTwenty(stripeCustomer.email); + if (stripeCustomerInTwenty === '') { + if (!stripeCustomer.name) { + throw new Error('Missing Stripe customer first or last name'); + } + if (!stripeCustomer.email) { + throw new Error('Missing Stripe customer email'); + } + const firstName: string = stripeCustomer.name?.split(' ')[0]; + const lastName: string = stripeCustomer.name?.split(' ')[1]; + const addedStripePersonToTwenty: boolean | undefined = + await addTwentyPerson( + firstName, + lastName, + stripeCustomer.email, + updatedTwentyCompanyId, + seats, + stripe.data.object.status.toUpperCase() as stripeStatus, + ); + if (!addedStripePersonToTwenty) { + throw new Error('Adding Stripe person to Twenty failed'); + } else { + console.log('Stripe person was added to Twenty'); + } + } else if (stripeCustomerInTwenty !== undefined) { + const updatedStripePersonInTwenty: boolean | undefined = + await updateTwentyPerson( + stripeCustomerInTwenty, + seats, + stripe.data.object.status.toUpperCase() as stripeStatus, + ); + if (!updatedStripePersonInTwenty) { + throw new Error('Update of Stripe person in Twenty failed'); + } else { + console.log('Update of Stripe person in Twenty succeeded'); + } + } else { + throw new Error('Twenty not found'); + } + } + return {}; } catch (error) { if (axios.isAxiosError(error)) { console.error(error.message); + return {}; } console.error(error); return {}; @@ -346,7 +492,7 @@ export const config: ServerlessFunctionConfig = { { universalIdentifier: '55f58e19-d832-43c4-9f8b-3f29fc05c162', type: 'route', - path: '/stripe', + path: '/webhook/stripe', httpMethod: 'POST', isAuthRequired: false, }, diff --git a/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/serverlessFunctions/stripe/src/types.ts b/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/serverlessFunctions/stripe/src/types.ts new file mode 100644 index 0000000000..ab7f29601b --- /dev/null +++ b/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/serverlessFunctions/stripe/src/types.ts @@ -0,0 +1,37 @@ +export type stripeStatus = 'INCOMPLETE' | 'INCOMPLETE_EXPIRED' | 'TRIALING' | 'ACTIVE' | 'PAST_DUE' | 'CANCELED' | 'UNPAID' | 'PAUSED'; + +type stripeItem = { + quantity: number; +}; + +type stripeItemsData = { + data: stripeItem[]; +}; + +type stripeEventObject = { + customer: string; + items: stripeItemsData; + status: stripeStatus; + quantity: number | null; +}; + +type stripeEventData = { + object: stripeEventObject; +}; + +export type stripeEvent = { + data: stripeEventData; + type: string; +}; + +export type stripeCustomer = { + businessName?: string; + name: string | null; + email: string | null; +}; + +export type twentyObject = { + id: string; + nameSingular: string; + fields: Record[]; +}; \ No newline at end of file diff --git a/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/yarn.lock b/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/yarn.lock index d915cde279..ab7ce2976d 100644 --- a/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/yarn.lock +++ b/packages/twenty-apps/hacktoberfest-2025/stripe-synchronizer/yarn.lock @@ -5,6 +5,15 @@ __metadata: version: 8 cacheKey: 10c0 +"@types/node@npm:^24.7.2": + version: 24.10.0 + resolution: "@types/node@npm:24.10.0" + dependencies: + undici-types: "npm:~7.16.0" + checksum: 10c0/f82ed7194e16f5590ef7afdc20c6d09068c76d50278b485ede8f0c5749683536e3064ffa8def8db76915196afb3724b854aa5723c64d6571b890b14492943b46 + languageName: node + linkType: hard + "async-function@npm:^1.0.0": version: 1.0.0 resolution: "async-function@npm:1.0.0" @@ -26,14 +35,14 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.13.1": - version: 1.13.1 - resolution: "axios@npm:1.13.1" +"axios@npm:^1.13.2": + version: 1.13.2 + resolution: "axios@npm:1.13.2" dependencies: follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.4" proxy-from-env: "npm:^1.1.0" - checksum: 10c0/de9c3c6de43d3ee1146d3afe78645f19450cac6a5d7235bef8b8e8eeb705c2e47e2d231dea99cecaec4dae1897c521118ca9413b9d474063c719c4d94c5b9adc + checksum: 10c0/e8a42e37e5568ae9c7a28c348db0e8cf3e43d06fcbef73f0048669edfe4f71219664da7b6cc991b0c0f01c28a48f037c515263cb79be1f1ae8ff034cd813867b languageName: node linkType: hard @@ -243,6 +252,22 @@ __metadata: version: 0.0.0-use.local resolution: "stripe-synchronizer@workspace:." dependencies: - axios: "npm:^1.13.1" + "@types/node": "npm:^24.7.2" + axios: "npm:^1.13.2" + twenty-sdk: "npm:^0.0.4" languageName: unknown linkType: soft + +"twenty-sdk@npm:^0.0.4": + version: 0.0.4 + resolution: "twenty-sdk@npm:0.0.4" + checksum: 10c0/550f1d85bf0701396c9dd2d4c6bc55ba1b067fce13636f8540eec60ab6a4257c6d7cd86cb3f62e0974bf99467bc31270d92b17b9681a1b7a6281b7ef97224080 + languageName: node + linkType: hard + +"undici-types@npm:~7.16.0": + version: 7.16.0 + resolution: "undici-types@npm:7.16.0" + checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a + languageName: node + linkType: hard diff --git a/packages/twenty-apps/hello-world/package.json b/packages/twenty-apps/hello-world/package.json index b9d6159f46..e82d368c11 100644 --- a/packages/twenty-apps/hello-world/package.json +++ b/packages/twenty-apps/hello-world/package.json @@ -10,7 +10,7 @@ "packageManager": "yarn@4.9.2", "dependencies": { "axios": "^1.12.2", - "twenty-sdk": "^0.0.3" + "twenty-sdk": "^0.0.4" }, "devDependencies": { "@types/node": "^24.7.2" diff --git a/packages/twenty-apps/hello-world/yarn.lock b/packages/twenty-apps/hello-world/yarn.lock index 755c8ef66c..429a933e68 100644 --- a/packages/twenty-apps/hello-world/yarn.lock +++ b/packages/twenty-apps/hello-world/yarn.lock @@ -224,7 +224,7 @@ __metadata: dependencies: "@types/node": "npm:^24.7.2" axios: "npm:^1.12.2" - twenty-sdk: "npm:0.0.3-alpha" + twenty-sdk: "npm:^0.0.4" languageName: unknown linkType: soft @@ -258,10 +258,10 @@ __metadata: languageName: node linkType: hard -"twenty-sdk@npm:0.0.3-alpha": - version: 0.0.3-alpha - resolution: "twenty-sdk@npm:0.0.3-alpha" - checksum: 10c0/e8028f47767e3fa6318100f26542e0477b68e36d363df5f3a6c20d8442951fecba60089d07e97224b0b3533a729bbd3526cca08728079577cfa204dbffaecb03 +"twenty-sdk@npm:^0.0.4": + version: 0.0.4 + resolution: "twenty-sdk@npm:0.0.4" + checksum: 10c0/550f1d85bf0701396c9dd2d4c6bc55ba1b067fce13636f8540eec60ab6a4257c6d7cd86cb3f62e0974bf99467bc31270d92b17b9681a1b7a6281b7ef97224080 languageName: node linkType: hard