From e6a701e9124f6baeff64afb2df7cad7e8fa35501 Mon Sep 17 00:00:00 2001 From: Perreal Guillaume <guillaume.perreal@irstea.fr> Date: Thu, 16 May 2019 12:48:26 +0200 Subject: [PATCH] Ajout d'une partie du typescript et configuration des outils. --- .gitignore | 1 + composer.json | 3 + package-lock.json | Bin 76840 -> 127521 bytes package.json | 60 +++- rollup.config.js | 16 + src/ts/cache.service.spec.ts | 374 ---------------------- src/ts/cache.service.ts | 178 ---------- src/ts/index.ts | 3 + src/ts/metadata/index.ts | 3 + src/ts/metadata/iri.metadata.ts | 40 +++ src/ts/metadata/registry.ts | 68 ++++ src/ts/metadata/resource.metadata.ts | 56 ++++ src/ts/service/abstract-repository.ts | 42 +++ src/ts/service/abstract-resource-cache.ts | 63 ++++ src/ts/service/api.ts | 128 ++++++++ src/ts/service/index.ts | 3 + src/ts/types/collection.ts | 39 +++ src/ts/types/date-time.ts | 4 + src/ts/types/index.ts | 4 + src/ts/types/resource.ts | 53 +++ src/ts/types/uuid.ts | 16 + tsconfig.declaration.json | 9 + tsconfig.json | 32 +- 23 files changed, 617 insertions(+), 578 deletions(-) create mode 100644 rollup.config.js delete mode 100644 src/ts/cache.service.spec.ts delete mode 100644 src/ts/cache.service.ts create mode 100644 src/ts/index.ts create mode 100644 src/ts/metadata/index.ts create mode 100644 src/ts/metadata/iri.metadata.ts create mode 100644 src/ts/metadata/registry.ts create mode 100644 src/ts/metadata/resource.metadata.ts create mode 100644 src/ts/service/abstract-repository.ts create mode 100644 src/ts/service/abstract-resource-cache.ts create mode 100644 src/ts/service/api.ts create mode 100644 src/ts/service/index.ts create mode 100644 src/ts/types/collection.ts create mode 100644 src/ts/types/date-time.ts create mode 100644 src/ts/types/index.ts create mode 100644 src/ts/types/resource.ts create mode 100644 src/ts/types/uuid.ts create mode 100644 tsconfig.declaration.json diff --git a/.gitignore b/.gitignore index afdcfdb..3113a65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /vendor /composer.lock /dist +/out /node_modules .php_cs.*cache .idea diff --git a/composer.json b/composer.json index 794c815..51d0020 100644 --- a/composer.json +++ b/composer.json @@ -44,6 +44,9 @@ "sebastian/phpcpd": "^4.0" }, "scripts": { + "post-install-cmd": "@nodejs-build", + "post-update-cmd": "@nodejs-build", + "nodejs-build": "npm install && npm run-script build || exit 0", "fix-cs": "@php vendor/bin/php-cs-fixer fix --verbose", "phploc": "@php vendor/bin/phploc --no-interaction src/php tests", "test": [ diff --git a/package-lock.json b/package-lock.json index 7b82f5b2cce22f6c310df1d259770bfc2287ecce..db7ae56286e6dcef07e1a416641a736007c43014 100644 GIT binary patch delta 25585 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�R+OPC!S=>}t?q+{io(xsP;aEOcQf`YO| zSDrfl2xsa`WjLB-D}^z>61t^M4I4<LDLG>TN_MbgTId@96uiFU9Zliq;=33wOa6ih z4_q!n4gw%GU9H(}+T!cjvVgU!056^+a=)h->~y(g#kQa`W?Iir#KDlL@pMlC``uvT z6&D4(x*D}Ooq?<Fiss9O2GQXqC|3XP9I(I=?S%kp(IG=zU8qAT0t(dw$-)ku*!DlS zH>hisCrXJIj|_F7%3LuWV;t`g$pid;tqm*#30O~|Gxj9l9*KP<Nkw2|-EVA2U|%uD z8=>xh8iJhx{1(V{HTqikAUMna^!;#bhea>EB{uROK28EPjEG(miRc4wxxTsk)c<uy zR6AnL%eMn+G?9zT@hSroPJQYJRUZ{Q`G(Q5V0ST4(se3F^oq(tY;~JNeNoQ$2HK>L z58Yb7rUYKuZH+Qkp0if-w$lYW*ze??o5rwD;H?!_fSnuyD%Sd5tg#yEGJ2W6{Ag0L zhA%d)B=)*K+J5WyrKG}K(}!#I03aE6v(cB{cQyLl_nkhTjLQykD<zysF2Rgh4^{4a zpBE)XUJRFA5hq=}vIHPD0e9E{1`7obSgbO%N)$9*jB<)L1?vj))lN~KPbHUa;(o8S zq&&NltCn1bgIaSa7t(T%L~6OXk)V$s1_wV60JNWMo{OIR>4#(8HTqwgk4LXH?<ChJ zxyQlZ6VWr=jri}*m!C)-j~d+N=!;v|qxWeSqKDpKqTd34^CvDu?|lQ517|$4ERrp& zRz-UG(IOnrnqbin4RS;gK_6&UEqLP2{ifGk)CbFQ-xb25#=s_?>nTk{??I)GpB?6N zb=pA2-8Q{IjJCWq0S1#lZ`uIei=AS@x*M@5_l`S2_%o3rJ`z=)I~&#i&hyb{tVfQ5 z*$VXP#LyGVbt%AUhCoOJcrMY3I;)u;ZibCXemt`#ID)YfpQ9!LE@tw0tLTrMia~Yx zY*}<i{pE6zX||ynNwW4L?J=!}Vb)k`9{1oWC$!df$y$=vfBZ_)Bh`;xKiDH8m1|OX zX5O7vwT7DurC^B;x)p=3wAzgkCsuu~I-1kXZWBdwmSl?_P;$)Xu-+o3t4^)a2i6$b zLs1yAB&p>CXagye#Ci<B`mr<7|FgXwxqthy$D;Z>u0P;xo4N(Ca9{=j0|Vex11fss zf4q=f?{<nakUPMGCHRGvh-d1<Y(pauQJy#9UUnD)C^9qd&llNzrx0Y@r3@rf17O&p zBXQhZXw?!_DR$VgBd+{(nbVf+Xi}?{TFKfu?!*fxwt24Zyz6%A@<||0GHy}o4Iz5# zKfQFQYZhq)okJZv4S>(ln9=gIqZfxYd(~pf>bRe#bqZR+LAz$utwnjzSju9(J?c)f z5;M&Vh|-W!@<M}YHpMYFLn~Bu9-8e$juY!{*!h8LsYen))`A`CmAC!u*+ae3!lztL z?BoPEpDkhpR5q}X89`L9FZ08tt+iQYmRnT8u#L5niK`T%iFSFKZ}6+^uq*WzO|*)@ zY^RXMCUs>tv)MVR#L`?}<A-mYkEpLb9B*nz32+$21M`8eT>yIC?y&SF0-dybXh`ww zSX-!esqEC|g`jK()C9Ks*<xQNBqzvDIg`QBYB3#}9pHTj*1VhJhjyE8EeC{eVzpwm zIftjYDMS?GnX)IqF;nujcqq>J@fFx>lD|NkxEQbColl-i9T&1ZqDg=efRW91JDAJ( zG$@qIWr_7Au|46*%qYLG`C%BC#HfwT!akCzVDol>O-jpB;2DcFyMjfeoFQ^}eVJ2= zbsas>4GD~T<=zi||MAr6eeh1(^;29wfat{n9=MI9k3GGG<{N;#(Qm`JT&@t9(rnj+ zIz84Mp&w&VyDpkPZEnS`g4pp4z(L!J;L6S8K%-Ts5%eoJ)_&Jp$$hNpc=|L3E)U&r zV_@~figw$Cy6|*Wtj`*0F>jgqMu;~S`JrJOiNgE$-u;J1qJRG-H2R76WWbjC($}Bp z<ZW^`lm=$q>cWgD7_R3J2&O)<IM=ACbXFu;nzWW}iKZ$j-)2{HvWARD$OvHot-u(` zB4Gt$SupT%Wm0bpj4D5g6|;Mb_b!f{+%-0V^1c;4`zpG1zyOU~v2Tv99Ua$|jMe8$ z4CywP3%BMtDxNpGQ@0JA(7^9^?@!-%E)}2p8&A<0@PeVfei0~Ldx<WK{SnilJj3Dh zaOfz$QmK`@-b~9gqv2@PXh`#Zu~A*}J!S=0MICSqh-QY)b8K6g%~7wEu9XXZ4=)We z!O+8GBwHH}Ts$EyZ9G+pi|yVw|I6m_=%IgcbH_QFI=1t!M^dMvM^8QzaW{@_?c%y? z(e_~8*nW;~Wp8iwJ52yHwGgN>oT0;MpF@@<MVX83kd6l`y77Z2qIdtJQ}=`q{P@vh zmaT*l_knBCyUreq;J=Sv2Ca_E64uP#b@cKl?rhb~2_=oxda2RHOUl5gqd^^_26SnV zy!YOp{1AWSC@{5R{N)_@DY#sV7?%OG<vM2!9WS39VPj^9WXGi5MS=4yJ4NTwr+)N_ z2a}&{$UR#T_j}}HdnFsk`=R^ji>IFv8g4<t+Y+jzLn_}Ll7-NansON{Y3YKHm1p`w zc80yawnW8QzfH}=a#hKVr`0yW`dAJcx(4p4a*fIQnN{8oI;g}>tFb0|er>ND<nR31 zFW)>3B-n-Nc90YRU=9?l8@u~B?y~E!-t()AdZ?>dXOeYLFk7se)mzZzc*dRG{qY9o z1O0a6aM6KR?m~etuaVo*1USz-T|7fmg>IcvX^24^%q&OKqG|-{u$UL~G*Qv8X&qq{ z*qCXRphspn;8XL9qYObU0<4ecjJYV(SVS~J5|yMFTRI?=uLG47Ami~$NSdVsA7DZe zinuS52Skl49uhVAWel)QW?c+3^F?(6fwAw{12s!J;u0PglWpqk`n5i=W0IfTID7Nn z_x#8^QpXe2FuDcEKA+UcEAjW&FyiJb$xZZ?FFXYdaaCd+syUjQ;h1X#>@q{myYo@D zmVxYex*yT~=zA--cjxvR^<TeceVxp0sEt+|LXVcnh16dWlr^5(BeGT#%InST!yhz{ zN57(y5&qFD0D0TxT(%TO7@0#F8wx8{ue<!%FN82z;=-(;RGB6$t9_Z%Dh=EqfrZ2b z`e<kuf=*T*E}Bk19{>W6QK*w`c{RohQs{Rfj@>o(qM!OGz)54gF-q-R23Npy0=6G{ z*rX;vm0wQaVtLUQ+h$wk8x^@o^|<cL6T7M?gcFaa?ZK+cR9jwwvzG>G)W|B}e3a3u zZPr=518WUIg)2?LCzzOHusS|TJSQ;{M#PxQx#S-7RPq;Wp?vA~7P3ubDNCO8x=>!m zXpU?l!>p5?XR1cTz5L`Ou^%bMPk>P*t3d!fnF$K%$3BXkPu>Q**!P`z2K!F6l8?Rl z3wB<=ow^u3{JHaqqrd_jvjIEm5wupLw3#~;$!-~0_9xSc8up8;qNWXVIl*%?oSh*W zc|Ol@=|NHGP=q_+vp`zG&bx|1FO1M=3RK5O=NtU(^pE|kBd7EjO%4F*OaXYs)#Djp z&H?NH@1u|IKLE0t`*+)*3gDKkosR0b1CQ+zP!a8B*&sd1=ceOMNkiRYY>O#%n44FL zjIEPoYf&f;r$ktsFD&4A?^a8&Tb>TJO1m#InF`k`5;YG{g*yqeGWstro;nWT^1Dbv ziXni8e*l@k{auY;MU}&Ggs4|;teqZ-ofdnt51Giotq~i=Ym+QCm7l*41Ka!zJN&Z^ z78QN`;??LMyAJ_d>U{L`KYjB(rVbQ~%})^gE_zVn<QXuZ6UPt$;}k&l+^xHBJtl^u zX)8cgOWp&T4V>?Xnr_<Q^J|1DsS>+G){RQs8Ni_dT(Mx+h*7MhLTsUD4vI2$tnI`& zjc#4P^6Y)ymV*`V+p<3Q;+5!2;q{Ae0tG~0{^hIhNoG-k)r<hsJt4&c>;G^!tTiuy zo{wjnjsn>E!EdfB03<m+RDoA_(*)4UgSD37+7JUjj)ycM6-0mW{yYD_0ut79{Ji%V zFlvAnDiRly60ls8zp*+C%46unQqA;aL`}VbHVJNu@C%$=<yt+q=;Y<uN?rLX<xn-Z z+OQP6YGk-RN3?;mx}dH8jO+|xW=XR-MDq((g)COJakUeyp_5bAF8LYY<-d3B|H}Z! z-xy%why@N2aVvW92hT=-+<rd#;h#Dc^Ia~j)uCYQq!p7GmJKu$27M#Q`(DY-Q2+?{ zv@z$8$pJ)<v~Cq2n@DBuW&qX0>O-*I8<5jdMk|V{--b%gv@aDQquJH^{A{_;dHHL% zS=XeS<H>#*y$Yy4z{6-_7n1wrBmz4`Od-Idm(Q*bp_Y|1DL=%Bd`W>8c3rYs6Q@e! z6Ks~Qp&2_<=n#D^*W(!r85PrrnVpV%{S1@iTTD7Av8YZ=Co>(^TLLS>*%>#_!7)jj zEdQra?l;@meMbb)<JR>3gb)9f=9LTknqBgXI0or|Xl}31jBOumnBX8jbS}hu=i!|{ zK3@J4kC3D)pjXK@&`%D%7J#b<PP3EXyhu)f7y&-(EjF<XBsh5iS>gT90~7@~<CE$5 zNi6}~&LkyZB-T$}{XzmHM4$WgaRHMvtfOR_xdx8o8X!T+rO^Z_^`X`z=qQ<B)t_sE z+vxWe@Tj*y^&X%%b2#sju2yAbzE_3Od8d|BBz)8t<f1h02&EW#+n^cinQ?i2lHS*2 zvBx@k+Z`NeDsd;>_jGUG+*iJh?7aFw>d7s`2&4uw6dPCpu&QxrhPCk!;t1eti52V3 zC+|DF6HjWQKW;vH-}AkX-dCRJmv@AKZTSO-U1iGcvTIkN^iblgk+*bo$xS!;f~=$q z_KYr|cxWJ6mOxE$Lih!$?Ji4#m2OH5QE5OD(ys-?P!G^jz2TSQSqoIBtwa3C5B9H~ z`!-5I<;rs#|6(#7f!23mR)9%y2pNf$q|*l!j|iv&m*V?%(>4<tEP?3f09TT`a%|^M zZ>F}Rlh2<`pxU<_O*XQ7olESIgRj|n>2^xEcHq5nm+h-Ju_FB3^#%%IL7$uy$~IRR zd3B{+N-xcvTGpbM-?$z<{Mggc^<O*{iGT8R^oc9~;mUga@1ObmV}-nVg}uSL|Cj60 z5@DPxa&Fu7-D*>kr1Bt*BQuvMsgr6}3f)c>xDZOMsZszwYE15xGexqCRMS>hGt$V^ zl`Ga#GbM=33!XwW+j?crwt9O3TwN5fC=admeU|L*ZgF^ccKJn{!2?>*A^m7Gv@yVP zG5UA!e*(CGfR`CC=Hhb&{`%SIdy(hQZ^}T^cc0$6`rKC2E9<b^_jM>Jmd8tBiJ4?A zgHM;;YQmd&_W2vp|Mb4}z1M&I$s;Fs2P^tBz&U>T?fBM#;X6E-z#j&}8bnd8=1RV~ ztO6G$Q|nb$x~kZyEcvWlXM@fhn~kPKbybFYor#tcX=d4|Olzn^%>Y*=Uu-LUC^(Hd zR!2i}A!m6%*C*(hl#v9&0d8jMf$I<M&CHk|o<eurqbVYpX6xy?5p1(8V?SN&<<^TV zhh!^FwWBW$y8+8{U!J$6WosT&-H21bY+r|Qs8Q;-ub$c;pm^tu{un;H^RI8FkdM5P zI&+UgjvR?KAyE2IbnJ~sKk_2520#9e=XdVjOkIiO9|g{nOINqps!an3Tj~|GFh9o9 zlSN}lHgc78%}zS{AHlbxw_dpf&cEC{5(3brfB3@D6Nz&=CjKPb-Nm)D+5=76T!FwP z5Yr;2^On2lRzWamr^Pn93rNzY1;fCsa>k;X0SM4AU6-ur_N2^o6hagG@_1m8-PN$q z73_SP&3AARU)7?z?N%9W=86reFiFTS(X-D#7UKf?m8kW}zdUQ%ap2Mhc=Zyb=(eRE zE{&x-Tom0ZT#C4lT<%-FQKJ{AaI4)lVUMo*yw(eYQ5|ieO^FhgY!Mjy_MlG7tQrnG zEz5`TC9%X2Av3PI+^V&pM7;s=%l@>}LOVT(2&8%LZEw4M?=7Edo;|Vl>m_j;G3p1X zgBN#weL$$)%RQ1HGSzTGQB|3+%|@S6dl@uua80V+UbKjIyU*xc$LlC1LF~_SW~RW_ zb3`M{O*(y}qPHeG2svF0d8E=)YT5#I7Yk-pYwUs?SH5$c*BXXE-n>FA=$l4}fhEP4 z?g$W3s5h_<+`_?{!HdJPq%Q2VXAjy+yVQl?90*))HN6R7otKfh*X_}MS#GiFY+_bx z04ktye_%2mK^O6U7hbxS!KT&tAh~v*M}2Q6!goG)I(5B;fGCrLM%lhc6utTGci+U~ zfAIL9Ls5}45bBofUa=Xv8}JtM?x(&Q{pyQkM1Jl3U!%)ptz;Yp!X-f;-|B)HlL(r` zAMIX7FZP}ht0jK|x3UFlptOh~m!>-yj=B^Twp&WjZJJ$2*9f(cHbXQUREu(h9v1=n zB{GCin)NGPZBZN$osz*wqmphnVTa=<g>_)|JKlO0sQf%}_1*_Qdm(jl&tOmeX#H|5 zLt?(i6Jz%}l795gmZA5SvftFTVU<~igG_T#wgNU-@T?tsN_PJ0RO;cY*~5dj*P>5c zyb|qwHu~|;y>?>5c|@Muii$soMnC=e2Oe1$fe){Z^X>KhHojJ)syv9aCI)>i1Lb<P znZWyAbw>GUp<IvEsC&W7e}3ZlRNcfR?~#S<q$q|}H&<)2&{lrR+toSLRp{mt1Z?Rc zDcFHMAo@WPtr6Y))HC-+UwHqq=={IA9@SnW@6$|E@-u=*uGkz{9@>Os>K!w~4oel> z)FEhWc)jjOE|Mcv%$THNw<;#!vX;7K2F-D#&PoNQw;WhGq2A<zncSu5o*{RekW-_< z+K8!JXQMy)r~mxq9;F<44zO)i{Ms54jB!8Ep#7We7)pKe`fSoecyp#@A-U38Xq?5W z)g@t8Mj;5}xe|S)c9Ym}88;_8KyMDNvG0T@MlqiJnV(gTo&<KrK6w3N@oe<jf3_WC z_iNlakqJ;4b_=4{|Jl=94U_L-0YUR~ct}fGEjLa3nMykgmDc0-(3ie;^jOSsjnC8Q zT|YveTQgqcmnGC!upFKQk#9p&^v3n{7^%9wj$ND%jfq_hI#{|A$0$9tuDiy!6To}T z7JK&QA^PmzOLu|r&1|T4dR>fEC*6A9@fg3SVSO7UKPVLe_zfoa@#O96z^V<AB|@I7 zU*25DZ(Y+{wt=Pvn2k?$2qVika1gGTaS0@2(j;9P_w_JKgjl|7w53TwA886dnbl^N zm#I3_maJ9NLfPpu{78b?RdF0vRJGedhQlVpSw>UG;%)QHhHjG#;F{j@H-$U#pBW2) z#_^vS3*VWsus;1Rd^^G$ul$`qdNf6*zx`RfbH7O`2xN|<tJyvhR;D}(HkpcBWq4<* zK{|nZtPw5>bbgVZ*-W(!)n_sQYxKT?HtKdTS3-^L)F-0?60&v=6MM}?ZBp%*Nm2n` zX%XmD<k?LCS)5q~&|72o-MxSH^Vd^=U-9bC;t}}^SHM*qg40=*Ta8&Lv@LEt*Jnl7 z7-WGz*X@+MS<Nl7-6oywSM(*X=nIpTXOuCcUEeBl-mt=S*g1$_rYO9_&j&p*ka)<d z`?JJ3AN_Ct^T~Vv_b>cm>Y-igCkV4ibaSB40U2%{ERH+r;60%A5{Y<)ayW!Vp;Hbt zhw0TYa;fCW$y8o8N067RlxCLbF=i#*8pB1`t(EvPIH0tuv?xMlxS6wn!$<2CD<RG3 zW6yzAcwQ38g#Z1Uzxb=iV$M>`L_Hl}#4^P9fJ-XY=7VM0!P+Gs;g*2{P5Q$CpF1-= zThaRkSIM@_Au$jY0~XVn{%S@}T14J7Frb5THDnZ!+0KH4tMx&dNq4!DfaXgY2;l|v z_!H|Gf#{c-j{*ArnaKO>)srBnMZyY<olKx}Kq&s@-69~=?j;Bwi;WkhH0;!Poy-r? zQb~}ig)yaiX(;q*+bt>lLZ|aSU2l|a-2rimTCi9IipPif-fWp`6zFP3423En!H?BS zd6D&NwV7@u!~^2Hq+Gc1i&w}u*R?mg??LD+M$Yyl%5MDhmGf~(_x1>ct0uJpi!crf zcyKQbNOF4q$SdTPy^?t4?A=6Pe&wk(qpnO&7_r#(0@!1-StCE0(ZNa|Gz!&1E}T_4 zqpb_2D!&jpm~0h9$Hf$@XrsW(mdiT;*6mB3mPb-~;4;j3$b3u>25dYbF6@14FaHXV zX%kOz_uC&H5^wJ(ajFiGt|ey2zpHk<19kY^fN7?Sn3Y|q4W!$tW|SJ6&+DymFK>1j zpgc_q9j($GOtSQ>!cuT)M8HtcpP&phn&tZ>6O_bxE0k8121--)Y<{C*ow@hl{L(aa zVja8|eK`ffn%C5gtytFGh_+21?%pO&Zp~FD**;|;nHuWzHA6A-JT|2U#j+nO3#azl zszzVGb_D~s6Cb3e*^Jw+LoN2kfl*GLCPTgJu#3Z9fB_6<_4;=SFX!IdQ4pZHE?CQy zyfb?Eg*(^fT*<}Uh1kKm1uoqn=cTZXNS(kR0p9yl4}u@9-wy!qEwDV0e}P@wE8Bh7 zx(2|bUiyc(fwDCUCwONllWH4=U>4UW?RM5=$e>!<O*3#Nac9SE`hl2H8)p?b_rL!7 zvFOhkG`jSatI?ll*C7{dSXFUrlum<PKrPAS+>kiAUYv5I(Uc1k4Q!VXc+cq~S#Jn{ z;Z0I7I}j>iIO}tR;-XQ4yI?oLvs0L?qZX?49F1PZno#sRUqO#0Tr98wfic@BXC-ag zzuiQY`TBy)W~W^}KdT1epi@UEZ#lR78a$3(`lTnM<FEe4L;JDdwztb)dJMdJFGSpq z!H|Om(T%H@<Iw->Q;gCzXgq_LvH-REPBq)tSd|)MWc2zc$hBdz^YXK)tI^Zkr5)xN z0BwwqpNYQu>bH)?(Pz>5AA2P6v;lX-#dSPd{G}u?Es4b4^+#B9SM3sj6OXz%byCL3 zabKYu(xNw6WbJ9IQ5a3B8mxpNhiiqfQ`I59NpVFMrdVd=AWB+abvbWEGsR)s%}a1| zz<Y^5BKo`Gndm3Tt0z3$Gy$I*fIx8u8~#`D3rRsNPI8fiue|HzQzBWY!NYWA;nMvq zlqshvt6!bj9vlQDY>5RD#}ODpimFuZ4hp5*3<P!*%nmI3)6Rk}DCDdK!+4=?!98@D z9{{!#G48Ijo3%FSRzS~#D57|h0W<)l_&I2-+?SE%od{T1hjGdz4NJ1>lCZtcK5}u> z00(ef01qfh#gn9$IrE!TX5F@fk2Z5^zu|B45F9GIBo;;}c~sFTIX<T7Gr+E#JMA&t z1lG<fY`0svwlEOH#)8zyl{QDM%xvg%Y_&~hm?GzwWJ~w!V!PgF;p$=(Ovi=R4D431 zE^_s7=9}H_$n1RfRO)6#s+UfznPxG^VCN@Kr_MeHz>`CU-Jy1efo|^lRjKID-hX%J zXHKOqM3=l)<leb`6xf7&6jB3bYp~QEg!uq9y@fZ-wY$a9yjiQqeaBC1r2`@D1<mJr zekIo!+vK1CiL}%PQS-fklG>dC4&fD|#!BfR-?O@u7|g!S>2l_fc+sEKACIP0{OLob ziO~0tL{b>NT75<+*&Lue_NVoJC)a|>qT>=Z8k;adea_Kj1sCb23$zGqX@jF~>NZ+! zy*m^^EcBS}6P&{5@iK){nRI4|cV>B*VcW<MZ04UKudb)yCcZ71Odxm-_#y(8dH|$| zh$qq7a9tlB7dK_e>9!k=@bV|t-c@6Wpl~N|@|dM-NUgn6a!k+07gMO*ZKLWCHnDcv zUsj4eE*rKyn8|fAyn<OuJzP%Q!b%+&!I;AUB|3`^>G3GrF#OE!aTfjhuf_56AnNQ~ z>}iYp3jEoQqx(0pX8npFQxnXX$-p147K93om0Vy%`vqh%8t^z!2fL#V+ja(NKc{Ic zqK9;QFo?Q!yYecJ@>;v*U_~2{xzbu)p^L2Bwb8*C;Gxm$CvSj^geCC(*ME(Q>c4Rz zYL%YqFNWQ`KJFudt(0>;3QPj1<t}<zVFlMS8IZ`WS(LlpK&c=`pe^mnoYIPJ%VaUc zl~lwRvgi`(jrexE3*)11pRAihs6CIrm-^JrbB94+FILf`Kl+WQqaz<hqrz|Aj5>|= z2?TX2r5-KTsi0H#ReFlSg5*Gjl22eXPU4k}pF=9EU@#_Fg)94+02{6dTJ87QB`qN` zV}*@{I1S7oQ~<3!RO+>$LMA8B`QH+bpjf;`M*rZqo{ldQi=eVJs8GvR!zvWAX^(5v zo26o4&9fE_7ZxkF;0$sOL}DRpmS)3h6@kip6LmdD0tY<J3_2k-tM*HN)8?uo0Z+0u zB!Nt$;Hz&31pV{L8h`A*%ozLl=sV--`D<VODX@~!=%wFoCVzKH;dp0kh=OlfA!QfZ z%W~c#O0!Yj&J{vi%hF9<u_%!MB*-4BOmg#iKv1rXl~A_Mq1BGtEOrX*xt#8|#56o` zy%~}*AT3_ewG;0Iq$HraeCd79--CYd$rRu&BVVVYzx%bb;A(e|gavgDGOu|h$ze-V zQ`Wtjj>${A8ERe|BuIf)kgheRvpR~;#-((|Z{+nVQX6pjb|pJ!SYa*+@;vQh8i0Um zg`VM!HE$OKf9Rmi7QG4r)HfR4je`%ocxfIkNI0fN71tzfDU`iNdOGuaIw#E*qpa9~ zfXegagM0Zzz`<C*y7@gwFS7qKaFft;VN`7pz!#-k6%=RyJkuGt#E4n(Y=Xjp6iqun ze>w$V=;u$y-m||Eko3Lm@Bhq+2R8AD(f56R>nH-WaKK)CY(GdbZs}gcVypy!8gUS> zp6!ah$D$!zhQI+)%}zCHW-pos$ZID}yZx+M4!Xd*)^|<P!n1vYfJ)Mo3<rMEgBI3m zmYJqiJ|k&t!KS^;a3)|{I`IabPN+u7rVIR3E%aziyNe@HVwCL<+SK{Ol%%+HLu3)1 zey+MwTXu#aS&o(%SnM`6f?FB#9BQdSxKg!po#I=q9-*oDh^s76PhsnMlps}&DHy$U zuf<yksSavxt<lc3+JoL8p69tYAtn8_a%$G7O7!Rd@}D1z9UULJnR+w@N79cF*Y*<* z#Y4B3eF!M|fA7<=Q<9R?3<x2djhZ!_oFP6H3_u!{WwE>xqF4Xr6Or}$gZF;!4_~`* zN?F9&J>oPdz{0*3-Sr-dzWf)@J+@yQzu(!v1>~?81$|O5J(M04hCu;@S+uAL)m<@Y z$tkX;3uB4+lhI-}>(k5C0)*m_=AZ}81B<p9wG`q^==4ovq)7-naG0g->I~(KbBOC^ zVwLD}^0^ogQ9<s6U8*Z!5?nnO%dzqJZ0m|2M{1ni)E=r%04m2>Z(@J)6NiiABlErz zfDXs$^3WciD~xE9)w69=l#xPTQrKc|W=j)poENz%G;Z=WuQRJCv?+<22iW|w%Sf#$ zQxhAiRK<sNIyV*zYQD~v$a=R`EVOpd?T7Dg3J4#6bdw0~+2`Zd+~3T7Iqz$JjJ)*6 zWD)B*ts-t1O{?Q5V_DE?rUXKOm<o)I^a9f4Oq0+c*l&_kRYNCkt0-2QJZ?j^x+=lJ zxM0Zju__U|1F7k{(Oo91k+`pOl6CoQ*`Sh2_x1m}i3bZ`J-!vupGMasaNxmi(+(6x zKlJHmKtiNUt<TidKGc`VTtVik5=FX8Vp1_2SRg0k{D|?5EG5sorYKB>uAdt_OQ|rk z5lVxh7GIw7Fh&Mq5oFPr+g02t8o<w!xLVHK`}kixd*l=#DR1bd(XBg=$6CbGYwGeo z=`@z^J|1df4DH79!}QWK$gtNXgK?JY8=4M@5QDEO4u(mMrH*P%kg2c)c9c$4lgDm% zN=(f5s+z6}u;_XNmrNJs%21Md5F^Bq9F{M``%?b=H$Q#kSj=sW`6eI!D82=ZBM@km z&|MSB_U>X@N#n^a)UVTpvQe*&IADy}P=?I+6$+~%tA4z`cV@Q%Vc;M9>%zUC{rB?G zho<9wUTnO~ld$8b*5BBFviG6=A^^q4`Cq+?tCutcB<3ja*)CVB7BW?shsMx+LNJJ0 zH<q?uBvqTwIo%2emjaWeJa?*Dl3XK1p`T^=!qA=1LA0XO4#Pk-#!J8@2aw;b?;64W z;opAb1aLYf0Wy^<aRdy&O2D#-b;8eYrQoAy4uzqBl$2Y*r$;@RFr5!@>EhPiUlzr# zJ~a!&Q;@2l6_hno#7MSG3t59CSj^&Nq$^TzQvutVSw`txSO>m#9ipMU)P+EPr5Yu3 zwC}SSUL>el15*JKF%f3N-mcyPVq^eS9ppT-!%cw3#({M##+ZK}eg6K(exkyBdno`4 z;>?U|;vzzp<?*~@mFoS{lCOh=(7@Z&=xP|O_$3t?=DI{_jLa<}t&B5xEz7AGX^jHE z+7hw^)J6M#wY~6H(82|YQ&7xzov|DThbM_89@yk&0w778b@YQjiCx{4fG*rz0ckLh zOXuZJ-n?E^It)a*nMThX`|{ABMI>FcB^@6{+zU^<!X8bPjz3^KHc09hfLGQp2AiAs zZM)^)cp-KJdU^>R_j)aHfK+F*hET25U{lX9^HuByeC2~DQ)g1q=Pm%J@q?+0K=X`` zAz@KQmyHEOIOFo%s;{_uUxNyTX<@3tl@T`%Oc8|YB1>annBl0DuOS1wV|Gie724n@ zLz~u%cFRdK%MJ{3oyu|rc&L}L1#oSXEqXtzc(N%1w%g8UPNhC~1i^ORc{=rlzX@vI z!_i-T_0-OdbEzZ_{urRVXOo!Go4<Qu=iTQ3k!usMsYZY6cgX19{O((JKvy1n<>`k~ znbZkI9ofS;YsRAyJ4K%gfHDy${76dUWAku9JPl*cI7r9~TlJBT;+1|A<$DdQ3n2Ek z-B>o#g((OWvirHtpg7==jzO)K*+Exrwra2(ENUPVi!{R;Om1MQN_J9~R~|^-A~D(F zY+jEQfb+ZQZZ;-4NO=Pw$hFLW>rCpasRYO1aXh;DyO#jlA<nAjqHwQWcd1Dj$mM*- z1YRK4=S`)>c4y0c6U~?1sg61L(#`b!tW0yzf}dlo>abS56=cT@Zq9%)I`njn@rJCz z+HC=(RXLLX@b8_DzW=jlU+FxYx{^Acfb<C`FiCQ>^TqS2W0#Y{xTkhee1LN&*^NNn zmsJj`wMRvCRzRSBv0tA-s|tsE#S#+wg*nLSS#&_kj}BJEM}q29o75-^aPG;(ij66# zW|ak&8P11dP%Jo8*1+>zJ;@-kGdiDo0_e)b&dVnOQSD3TQ*qv<O-etpzZ~Wlf{bSc zK~oxafshyq*u4|9SGJ^Tz6LKad^X6Sqba79XT`KU_UfKk1YsNF6*{8&3!%;|>VUSe zU<PV#R$PcGB_`i}<@+zBny0USXD-gptB(S>z3^%LE#J9Lvd8_$9>;nC5Qnu;L5CI% z3aapIA#er@yVcEWgX{n{++aB_XL^NkgU<}i;&c)6fHOHENOF=HHSj`+L5wA9e$l2z z!yMz{epW*%IyrPsCyLBk?Trxbk&ADliA7N5)+@jKSnAma0Cbxqz~6cLsnjJv6a=a6 z1s{a1^_9NL<e2=B$z{e=9VAAh1g|-$ju=kK2xz-N<<X)7%xzqNCB9*ofu`VV1u5SK zNz`GlNO&44=7nkxq|%m5q%(+jo!$Rm001=swhPEz3UWDuME0Al2e1_DkG%3bPo>Jo zkAg@RFcGhVV+5FTXTcqqf#6>-_fU}a2keY95YsxP@pPD7NjN$#Lox(s5Sj`b-K-@{ zFgGwjct=g{5`C`K9IV<Eh!un-6K1<XX0|9X!%o%?YLGgxoi^NOks7h&;`RBS!+G>~ zx5dNspiTOo_~8%4d&VKm@;@G^bjaJ8yd8Kv54`=Yz_s#i$Ev^1Dw1qZ2chKMBPMxI Qa)y5Vkymb}QzujZF9i|~djJ3c delta 2877 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 diff --git a/package.json b/package.json index 7466024..cbe80b8 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,16 @@ "name": "irstea-ng-model", "version": "1.0.0", "description": "Runtime libray for the composer package irstea/ng-model-generator-bundle.", + "types": "dist/index.d.ts", "main": "dist/index.js", + "module": "dist/index.esm.js", "directories": {}, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "prebuild": "rm -rf dist/ && tsc -p ./tsconfig.declaration.json", + "build": "tsc -p . && rollup -c", + "postbuild": "rm -rf out", + "clean": "rm -rf dist/* out/*" }, "repository": { "type": "git", @@ -18,26 +24,54 @@ "author": "Irstea - pôle IS", "license": "LGPL-3.0-or-later", "peerDependencies": { - "@angular/common": "^7.2.9", - "@angular/core": "^7.2.9" + "@angular/common": "^7.2.15" }, "dependencies": { "lodash": "^4.17.11", - "rxjs": "^6.4.0", - "rxjs-etc": "^9.4.0" + "rxjs": "^6.5.2", + "rxjs-etc": "^9.5.0", + "tslib": "^1.9.3" }, "devDependencies": { - "@angular/common": "^7.2.9", - "@angular/core": "^7.2.9", - "irstea-typescript-config": "^1.0.3", - "prettier": "^1.16.4", + "@angular/common": "^7.2.15", + "@angular/compiler": "^7.2.15", + "@angular/core": "^7.2.15", + "codelyzer": "^5.0.1", + "husky": "^2.3.0", + "irstea-typescript-config": "^1.0.6", + "lint-staged": "^8.1.7", + "prettier": "^1.17.1", "prettier-tslint": "^0.4.2", - "rxjs-marbles": "^5.0.0", - "rxjs-tslint-rules": "^4.18.2", - "tslint": "^5.14.0", + "rollup": "^1.12.1", + "rollup-plugin-node-resolve": "^5.0.0", + "rxjs-marbles": "^5.0.2", + "rxjs-tslint-rules": "^4.23.1", + "tslint": "^5.16.0", "tslint-config-prettier": "^1.18.0", + "tslint-defocus": "^2.0.6", "tslint-plugin-prettier": "^2.0.1", - "typescript": "^3.3.3333", + "typescript": "^3.4.5", "zone.js": "~0.8.26" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "src/**/*.{json,md}": [ + "prettier --write", + "git add" + ], + "src/**/*.ts": [ + "prettier-tslint fix", + "git add" + ] + }, + "prettier": { + "printWidth": 80, + "semi": true, + "singleQuote": true, + "trailingComma": "es5" } } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..5911d85 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,16 @@ +import resolve from 'rollup-plugin-node-resolve'; + +import pkg from './package.json'; + +export default [ + // CommonJS (for Node) and ES module (for bundlers) build. + { + input: 'out/index.js', + external: Object.keys(pkg.dependencies).concat(Object.keys(pkg.peerDependencies)), + output: [ + { file: pkg.main, format: 'cjs' }, + { file: pkg.module, format: 'es' }, + ], + plugins: [resolve()], + }, +]; diff --git a/src/ts/cache.service.spec.ts b/src/ts/cache.service.spec.ts deleted file mode 100644 index 4b23a8b..0000000 --- a/src/ts/cache.service.spec.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { HttpHeaderResponse } from '@angular/common/http'; -import { inject, TestBed } from '@angular/core/testing'; -import * as _ from 'lodash'; -import { forkJoin, Observable } from 'rxjs'; -import { catchError, map, mergeMap } from 'rxjs/operators'; - -import { MarbleTestScheduler } from '../../../testing/marbles'; -import { Collection, IRI, IRI_PROPERTY, Resource } from '../../shared/models'; -import { safeForkJoin } from '../../shared/rxjs'; - -import { IRIMismatchError, MissingIRIError, ResourceCache, ValueHolder } from './cache.service'; - -describe('cache.service', () => { - interface MyResource extends Resource { - readonly '@id': IRI<MyResource>; - readonly '@type': 'MyResource'; - value: string; - value2?: string; - } - - function iri(x: string): IRI<MyResource> { - return x as any; - } - - const MY_IRI = iri('/bla/a'); - const OTHER_IRI = iri('/bla/B'); - const VALUES: { [name: string]: MyResource } = { - a: { '@id': MY_IRI, '@type': 'MyResource', value: 'foo' }, - b: { '@id': MY_IRI, '@type': 'MyResource', value: 'bar' }, - c: { '@id': MY_IRI, '@type': 'MyResource', value: 'bar', value2: 'quz' }, - d: { '@id': OTHER_IRI, '@type': 'MyResource', value: 'zig' }, - }; - let scheduler: MarbleTestScheduler<any>; - beforeEach(() => { - scheduler = MarbleTestScheduler.create(VALUES, 'error'); - }); - - describe('ValueHolder', () => { - let holder: ValueHolder<any>; - - beforeEach(() => { - holder = new ValueHolder<MyResource>(iri('/bla/a')); - }); - - it('should be created', () => { - expect(holder).toBeTruthy(); - }); - - describe('.set()', () => { - function testSet({ value, error, SET_M }: any) { - scheduler.withError(error).run(({ expectObservable }) => { - expectObservable(holder.set(value)).toBe(SET_M); - }); - } - - it('should provide the value', () => - testSet({ - value: VALUES.a, - SET_M: '(a|)', - })); - - it(`should refuse value without ${IRI_PROPERTY}`, () => - testSet({ - value: {}, - error: new MissingIRIError(), - SET_M: '#', - })); - - it('should refuse value with different @id', () => - testSet({ - value: { '@id': iri('bar') }, - error: new IRIMismatchError(MY_IRI, iri('bar')), - SET_M: '#', - })); - - it('should always points to the same instance', () => { - forkJoin(holder.set(VALUES.a), holder.set(VALUES.b)).subscribe(([a, b]: MyResource[]) => { - expect(a).toBe(b); - }); - }); - }); - - describe('.update()', () => { - it('should provide the value from the server', () => - scheduler.run(({ cold, expectObservable }) => { - const REQ_M = '---a|'; - const UPD_M = '--(a|) '; - - const request$ = cold(REQ_M); - expectObservable(holder.update(request$)).toBe(UPD_M); - })); - - it('should cancel pending requests', () => { - const LOCAL_VALUES = { - a: VALUES.a, - b: VALUES.b, - j: [VALUES.b, VALUES.b], - }; - const REQ1_M = '---a|'; - const REQ2_M = 'b| '; - - const UPDA_M = '(j|) '; - const REQ1_S = '(^!) '; - const REQ2_S = '(^!) '; - - scheduler.withValues(LOCAL_VALUES).run(({ cold, expectObservable, expectSubscriptions }) => { - const request1$ = cold(REQ1_M); - const request2$ = cold(REQ2_M); - - expectObservable( - safeForkJoin([ - // - holder.update(request1$), - holder.update(request2$), - ]) - ).toBe(UPDA_M); - expectSubscriptions(request1$.subscriptions).toBe(REQ1_S); - expectSubscriptions(request2$.subscriptions).toBe(REQ2_S); - }); - }); - - it('should propagate errors', () => - scheduler.run(({ cold, expectObservable }) => { - const REQ_M = '#'; - const UPD_M = '#'; - - const request$ = cold(REQ_M); - expectObservable(holder.update(request$)).toBe(UPD_M); - })); - - it('should restart on errors', () => - scheduler.run(({ cold, expectObservable }) => { - const REQ1_M = '#'; - const UPD1_M = '#'; - - const obs$ = holder.update(cold(REQ1_M)); - expectObservable(obs$).toBe(UPD1_M); - - const REQ2_M = '-a|'; - const UPD2_M = '-a|'; - - const obs2$ = holder.update(cold(REQ2_M)); - expectObservable(obs2$).toBe(UPD2_M); - })); - }); - - describe('.listen()', () => { - function testListen({ REQUEST_M, LISTEN_M, initial }: any) { - scheduler.run(({ cold, expectObservable }) => { - if (initial) { - holder.set(initial); - } - expectObservable(holder.listen(() => cold(REQUEST_M))).toBe(LISTEN_M); - }); - } - - it('should provide the value', () => - testListen({ - initial: VALUES.a, - REQUEST_M: /**/ ' ', - LISTEN_M: /***/ '(a|)', - })); - - it('should cache the value', () => - testListen({ - initial: VALUES.a, - REQUEST_M: /**/ 'b| ', - LISTEN_M: /***/ '(a|)', - })); - - it('should propagate errors', () => - testListen({ - REQUEST_M: /**/ '#', - LISTEN_M: /***/ '#', - })); - }); - - it('.invalidate() should cause the value to be requested again', () => - scheduler.run(({ cold, expectObservable }) => { - const REQUEST_M = /**/ '(a|)'; - const LISTEN_M = /***/ '(a|)'; - const requestFactory = jasmine.createSpy('requestFactory'); - requestFactory.and.returnValue(cold(REQUEST_M)); - - return holder - .set(VALUES.a) - .toPromise() - .then(() => holder.invalidate()) - .then(() => expectObservable(holder.listen(requestFactory)).toBe(LISTEN_M)) - .then(() => expect(requestFactory).toHaveBeenCalled()); - })); - }); - - describe('ResourceCache', () => { - beforeEach(() => - TestBed.configureTestingModule({ - providers: [ResourceCache], - }) - ); - - it('should be created', inject([ResourceCache], (service: ResourceCache) => { - expect(service).toBeDefined(); - })); - - describe('.get()', () => { - it('should provide the value', inject([ResourceCache], (service: ResourceCache) => - scheduler.run(({ cold, expectObservable }) => { - const getQuery$ = cold('a|'); - - expectObservable(service.get(MY_IRI, () => getQuery$)).toBe('(a|)'); - }) - )); - - it('should cache the value', inject([ResourceCache], (service: ResourceCache) => - scheduler.run(({ cold, expectObservable }) => { - const getQuery$ = cold('a|'); - const getQuery2$ = cold('b|'); - // tslint:disable-next-line:rxjs-finnish - const queries$ = cold('ab|', { a: getQuery$, b: getQuery2$ }); - - expectObservable(queries$.pipe(mergeMap(query$ => service.get(MY_IRI, () => query$)))).toBe('aa|'); - }) - )); - - it('should propagate errors', inject([ResourceCache], (service: ResourceCache) => - scheduler.run(({ cold, expectObservable }) => { - const getQuery$ = cold('#'); - - expectObservable(service.get(MY_IRI, () => getQuery$)).toBe('#'); - }) - )); - }); - - describe('.put()', () => { - it('should provide the value', inject([ResourceCache], (service: ResourceCache) => { - const putRequest$ = scheduler.createColdObservable('a|'); - - scheduler.expectObservable(service.put(MY_IRI, putRequest$)).toBe('a|'); - })); - - it('should not cache the value', inject([ResourceCache], (service: ResourceCache) => - scheduler.run(({ cold, expectObservable }) => { - const putRequest$ = cold('a|'); - const putRequest2$ = cold('b|'); - // tslint:disable-next-line:rxjs-finnish - const requests$ = cold('ab|', { a: putRequest$, b: putRequest2$ }); - - expectObservable( - requests$.pipe( - mergeMap((request$: Observable<MyResource>) => service.put(MY_IRI, request$)), - map(x => _.clone(x)) - ) - ).toBe('ab|'); - }) - )); - - it('should propagate errors', inject([ResourceCache], (service: ResourceCache) => - scheduler.run(({ cold, expectObservable }) => { - const putRequest$ = cold('#'); - - expectObservable(service.put(MY_IRI, putRequest$)).toBe('#'); - }) - )); - }); - - describe('.post()', () => { - it('should provide the value', inject([ResourceCache], (service: ResourceCache) => - scheduler.run(({ cold, expectObservable }) => { - const postRequest$ = cold('a|'); - - expectObservable(service.post(postRequest$)).toBe('a|'); - }) - )); - - it('should propagate errors', inject([ResourceCache], (service: ResourceCache) => - scheduler.run(({ cold, expectObservable }) => { - const postRequest$ = cold('#'); - - expectObservable(service.post(postRequest$)).toBe('#'); - }) - )); - }); - - describe('.delete()', () => { - it('should clear the cache on successful fetch', inject([ResourceCache], (service: ResourceCache) => { - const response = new HttpHeaderResponse({ status: 200 }); - const values = { r: response, a: VALUES.a, b: VALUES.b }; - const sched = scheduler.withValues(values); - - sched.run(async ({ cold, expectObservable }) => { - await service.get(MY_IRI, () => cold('a|')).toPromise(); - await service.delete(MY_IRI, cold('r|')).toPromise(); - - return expectObservable(service.get(MY_IRI, () => cold('b|'))).toBe('(b|)'); - }); - })); - - it('should propagate errors', inject([ResourceCache], (service: ResourceCache) => - scheduler.run(({ cold, expectObservable }) => { - const deleteRequest$ = cold('#'); - - expectObservable(service.delete(MY_IRI, deleteRequest$)).toBe('#'); - }) - )); - - it('should not clear the cache on error', inject([ResourceCache], (service: ResourceCache) => { - const error = new HttpHeaderResponse({ status: 500 }); - const values = { a: VALUES.a, b: VALUES.b, e: error }; - scheduler - .withValues(values) - .withError(error) - .run(async ({ cold, expectObservable }) => { - await service.get(MY_IRI, () => cold('a|')).toPromise(); - await service - .delete(MY_IRI, cold('#')) - .pipe(catchError(e => e)) - .toPromise(); - - return expectObservable(service.get(MY_IRI, () => cold('b|'))).toBe('(a|)'); - }); - })); - }); - - describe('.getAll()', () => { - it('should provide the returned value', inject([ResourceCache], (service: ResourceCache) => { - const values = { - a: { 'hydra:member': [VALUES.a, VALUES.d], 'hydra:totalItems': 2 }, - }; - scheduler.withValues(values).run(({ cold, expectObservable }) => { - const getAllRequest$ = cold('a|'); - expectObservable(service.getAll(getAllRequest$)).toBe('a|'); - }); - })); - - it('should nicely handle empty collections', inject([ResourceCache], (service: ResourceCache) => { - const values = { - a: { 'hydra:member': [] as any, 'hydra:totalItems': 0 }, - }; - scheduler.withValues(values).run(({ cold, expectObservable }) => { - const getAllRequest$ = cold('a|'); - expectObservable(service.getAll(getAllRequest$)).toBe('a|'); - }); - })); - - it('should propagate errors', inject([ResourceCache], (service: ResourceCache) => - scheduler.run(({ cold, expectObservable }) => { - const getAllRequest$ = cold<Collection<MyResource>>('#'); - expectObservable(service.getAll(getAllRequest$)).toBe('#'); - }) - )); - - it('should populate the cache', inject([ResourceCache], (service: ResourceCache) => { - const values = { - a: VALUES.a, - b: VALUES.d, - h: { - 'hydra:member': [VALUES.a, VALUES.d], - 'hydra:totalItems': 2, - }, - }; - scheduler.withValues(values).run(({ cold, expectObservable }) => { - const getAllRequest$ = cold('h|'); - const getRequest$ = cold('b|'); - const requests$ = cold<() => Observable<any>>('ab|', { - a: () => service.getAll(getAllRequest$), - b: () => service.get(MY_IRI, () => getRequest$), - }); - - expectObservable(requests$.pipe(mergeMap((sendRequest: () => Observable<any>) => sendRequest()))).toBe('ha|'); - }); - })); - }); - }); -}); diff --git a/src/ts/cache.service.ts b/src/ts/cache.service.ts deleted file mode 100644 index 1ed095f..0000000 --- a/src/ts/cache.service.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { HttpResponseBase } from '@angular/common/http'; -import * as _ from 'lodash'; -import { Observable, of, race, Subject, throwError } from 'rxjs'; -import { map, switchMap, take, tap } from 'rxjs/operators'; - -import { - AbstractResourceCache, - Collection, - COLLECTION_MEMBERS, - getCollectionMembers, - IRI, - IRI_PROPERTY, - Resource, -} from '../../shared/models'; -import { safeForkJoin } from '../../shared/rxjs'; - -export class APICacheError extends Error {} - -export class MissingIRIError extends APICacheError { - public constructor() { - super(`resource must have an ${IRI_PROPERTY} property`); - } -} - -export class IRIMismatchError extends APICacheError { - public constructor(expected: any, actual: any) { - super(`${IRI_PROPERTY}s mismatch: ${actual} !== ${expected}`); - } -} - -/** - * ValueHolder gère les requêtes d'une seule ressource. - * - * @internal - */ -export class ValueHolder<R extends Resource> { - private readonly value$ = new Subject<R>(); - - private readonly value = {} as R; - private version = 0; - - constructor(private readonly iri: IRI<R>) {} - - public set(value: R): Observable<R> { - if (!(IRI_PROPERTY in value)) { - return throwError(new MissingIRIError()); - } - if (value[IRI_PROPERTY] !== this.iri) { - return throwError(new IRIMismatchError(this.iri, value[IRI_PROPERTY])); - } - - _.assign(this.value, value); - _(this.value) - .keys() - .difference(_.keys(value)) - .forEach(key => delete this.value[key]); - this.version++; - this.value$.next(this.value); - - return of(this.value); - } - - public listen(queryFactory: () => Observable<R>): Observable<R> { - if (this.version > 0) { - return of(this.value); - } - return this.update(queryFactory()); - } - - public update(request$: Observable<R>): Observable<R> { - return race(this.value$.pipe(take(1)), request$.pipe(switchMap((item: R) => this.set(item)))); - } - - public invalidate(): void { - this.version = 0; - } - - public delete(): void { - this.value$.complete(); - } -} - -/** - * Implémentation d'un cache de resource. - * - * Cette implémentation met en place une queue de requête ainsi qu'un observable pour chaque ressource. - * - * La queue de requête permet de mettre à jour une ressource en cache. switchMap est utilisé pour prendre en compte - * les valeurs des dernières requêtes. - * - * L'Observable s'assure de retourner toujours la même référence d'objet tout au long de la vie - * de la ressource dans le cache, il permet aussi de faire suivre tout mise à jour à d'eventuels subscribers. - */ -export class ResourceCache extends AbstractResourceCache { - private readonly holders = new Map<IRI<Resource>, ValueHolder<Resource>>(); - - /** - * Retourne la ressource identifiée par l'IRI donné. - * - * Effectue une requête si on le connait pas. - */ - public get<R extends Resource>(iri: IRI<R>, requestFactory: () => Observable<R>): Observable<R> { - return this.getHolder(iri).listen(requestFactory); - } - - /** - * Envoie une requête de mise à jour puis met à jour le cache avec la réponse du serveur. - */ - public put<R extends Resource>(iri: IRI<R>, query$: Observable<R>): Observable<R> { - return this.getHolder(iri).update(query$); - } - - /** - * Envoie une requête de création puis met à jour le chache avec la réponse. - */ - public post<R extends Resource>(query$: Observable<R>): Observable<R> { - return query$.pipe(switchMap(item => this.received(item))); - } - - /** - * Supprime une ressource sur le serveur puis en cache. - */ - public delete<R extends Resource>(iri: IRI<R>, query$: Observable<HttpResponseBase>): Observable<HttpResponseBase> { - return query$.pipe( - tap(() => { - if (!this.holders.has(iri)) { - return; - } - this.holders.get(iri).delete(); - this.holders.delete(iri); - }) - ); - } - - /** - * Fait une requête pour plusieurs ressources puis les mets en cache. - */ - public getAll<R extends Resource>(query$: Observable<Collection<R>>): Observable<Collection<R>> { - return query$.pipe( - switchMap((coll: Collection<R>) => { - const members = getCollectionMembers(coll); - const memberObservables$ = members.map(item => this.received(item)); - return safeForkJoin(memberObservables$).pipe( - map(items => Object.assign({} as Collection<R>, coll, { [COLLECTION_MEMBERS]: items })) - ); - }) - ); - } - - /** - * Invalide la valeur d'une IRI pour forcer une mise-à -jour. - */ - public invalidate<R extends Resource>(iri: IRI<R>): void { - if (!this.holders.has(iri)) { - return; - } - this.holders.get(iri).invalidate(); - } - - /** - * Retourne le ValueHolder d'une IRI, ou le crée si nécessaire. - */ - private getHolder<R extends Resource>(iri: IRI<R>): ValueHolder<R> { - let holder = this.holders.get(iri) as ValueHolder<R>; - if (!holder) { - holder = new ValueHolder<R>(iri); - this.holders.set(iri, holder); - } - return holder; - } - - /** - * Retourne le ValueHolder d'une IRI, ou le crée si nécessaire. - */ - private received<R extends Resource>(item: R): Observable<R> { - return this.getHolder(item[IRI_PROPERTY]).set(item); - } -} diff --git a/src/ts/index.ts b/src/ts/index.ts new file mode 100644 index 0000000..2616010 --- /dev/null +++ b/src/ts/index.ts @@ -0,0 +1,3 @@ +export * from './metadata'; +export * from './service'; +export * from './types'; diff --git a/src/ts/metadata/index.ts b/src/ts/metadata/index.ts new file mode 100644 index 0000000..93f3e06 --- /dev/null +++ b/src/ts/metadata/index.ts @@ -0,0 +1,3 @@ +export * from './iri.metadata'; +export * from './resource.metadata'; +export * from './registry'; diff --git a/src/ts/metadata/iri.metadata.ts b/src/ts/metadata/iri.metadata.ts new file mode 100644 index 0000000..1f34dd3 --- /dev/null +++ b/src/ts/metadata/iri.metadata.ts @@ -0,0 +1,40 @@ +import { IRI } from '../types'; + +/** + * Type des paramètres + */ +export type IRIParameters = string[] | string; + +/** + * Informations sur l'IRI d'une resource. + * + * P: types des paramètres. + */ +export class IRIMetadata<P extends IRIParameters> { + public constructor( + private readonly testPattern: RegExp, + private readonly capturePattern: RegExp, + private readonly template: (parameters: P) => string + ) {} + + public validate(path: string): boolean { + return this.testPattern.test(path); + } + + public generate(parameters: P): IRI<any> { + return this.template(parameters) as any; + } + + public parse(path: string): P { + const matches = this.capturePattern.exec(path); + if (!matches) { + throw new Error( + `Invalid path: ${path} does not match ${this.capturePattern}` + ); + } + if (matches.length == 2) { + return matches[1] as any; + } + return matches.slice(1) as any; + } +} diff --git a/src/ts/metadata/registry.ts b/src/ts/metadata/registry.ts new file mode 100644 index 0000000..9c01077 --- /dev/null +++ b/src/ts/metadata/registry.ts @@ -0,0 +1,68 @@ +import { AbstractRepository } from '../service'; +import { Resource } from '../types'; + +import { IRIParameters } from './iri.metadata'; +import { ResourceMetadata } from './resource.metadata'; + +/** + * Sur-type encadrant des API. + */ +export interface APIMeta { + [type: string]: { + resource: Resource; + metadata: ResourceMetadata<any, any, IRIParameters>; + repository: AbstractRepository<any, any, IRIParameters>; + iriParameters: IRIParameters; + }; +} + +/** + * Classe abstraite d'un registre des metadonnées des ressources. + */ +export interface APIMetadataRegistry<API extends APIMeta> { + /** + * Vérifie si on a des métadonnées pour le type de ressourcce indiqué. + */ + has<T extends keyof API>(type: T): type is T; + + /** + * (Construit et) retourne l'instance de métadonnées pour le type de resource 'type'. + */ + get<T extends keyof API>(type: T): API[T]['metadata']; +} + +/** + * Registre de métadonnées qui construit les instances à la demande (ce qui permet de gérer les + * dépendances entre métadonnées). + */ +export class LazyMetadataRegistry<API extends APIMeta> + implements APIMetadataRegistry<API> { + private readonly instances = {} as { [T in keyof API]: API[T]['metadata'] }; + + protected constructor( + private readonly builders: { + readonly [T in keyof API]: ( + r?: APIMetadataRegistry<API> + ) => API[T]['metadata'] + } + ) {} + + public has<T extends keyof API>(type: T): type is T { + return typeof type === 'string' && type in this.builders; + } + + public get<T extends keyof API>(type: T): API[T]['metadata'] { + if (!this.has(type)) { + throw new Error(`Invalid resource type: ${type}`); + } + return this.getOrCreate(type); + } + + protected getOrCreate<T extends keyof API>(type: T): API[T]['metadata'] { + let metadata = this.instances[type]; + if (!metadata) { + metadata = this.instances[type] = this.builders[type](this); + } + return metadata; + } +} diff --git a/src/ts/metadata/resource.metadata.ts b/src/ts/metadata/resource.metadata.ts new file mode 100644 index 0000000..ed75e53 --- /dev/null +++ b/src/ts/metadata/resource.metadata.ts @@ -0,0 +1,56 @@ +import { getResourceType, IRI, Resource } from '../types'; + +import { IRIMetadata, IRIParameters } from './iri.metadata'; + +/** + * Metadonnées d'une ressource. + * + * R : type de resource, e.g. Person + * T : valeur de la propriété '@type', e.g. 'Person'. + */ +export class ResourceMetadata< + R extends Resource, + T extends string, + P extends IRIParameters +> { + public constructor( + public readonly type: T, + public readonly iri: IRIMetadata<P>, + private readonly requiredProperties: Array<keyof R>, + private readonly types: Array<ResourceMetadata<any, any, P>> = [] + ) { + this.types.unshift(this); + } + + /** + * Vérifie si l'argument représente une ressource R ou dérivée. + */ + public isResource(that: unknown): that is R { + const type = getResourceType(that); + if (!type) { + return false; + } + return this.types.some(t => t.type === type); + } + + /** + * Vérifie si une propriété est obligatoire dans la resource R. + */ + public isRequired(property: keyof R): boolean { + return this.requiredProperties.includes(property); + } + + /** + * Génère une IRI à partir de ses paramètres. + */ + public generateIRI(parameters: P): IRI<R> { + return this.iri.generate(parameters); + } + + /** + * Extrait les paramètres d'une IRI. + */ + public getIRIParameters(iri: IRI<R>): P { + return this.iri.parse(iri as any); + } +} diff --git a/src/ts/service/abstract-repository.ts b/src/ts/service/abstract-repository.ts new file mode 100644 index 0000000..0620ded --- /dev/null +++ b/src/ts/service/abstract-repository.ts @@ -0,0 +1,42 @@ +import { HttpClient } from '@angular/common/http'; + +import { APIMeta, IRIParameters, ResourceMetadata } from '../metadata'; +import { IRI, Resource } from '../types'; + +import { AbstractResourceCache } from './abstract-resource-cache'; + +/** + * Classe de base d'un repository. + */ +export abstract class AbstractRepository< + R extends Resource, + T extends string, + P extends IRIParameters +> { + public constructor( + public readonly metadata: ResourceMetadata<R, T, P>, + protected readonly client: HttpClient, + protected readonly cache: AbstractResourceCache + ) {} + + /** + * Génère une IRI à partir de ses paramètres. + */ + public generateIRI(parameters: P): IRI<R> { + return this.metadata.generateIRI(parameters); + } + + /** + * Extrait les paramètres d'une IRI. + */ + public getIRIParameters(iri: IRI<R>): P { + return this.metadata.getIRIParameters(iri); + } +} + +/** + * Classe abstraite de la façade de l'API. + */ +export interface APIRepositoryRegistry<API extends APIMeta> { + get<T extends keyof API>(type: T): API[T]['repository']; +} diff --git a/src/ts/service/abstract-resource-cache.ts b/src/ts/service/abstract-resource-cache.ts new file mode 100644 index 0000000..558a87e --- /dev/null +++ b/src/ts/service/abstract-resource-cache.ts @@ -0,0 +1,63 @@ +import { HttpResponseBase } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { Collection, IRI, Resource } from '../types'; + +/** + * AbstractResourceCache est une classe abstraite qui définit l'interface d'un cache de ressources. + * + * Elle doit être implémentée puis être fournie en provider d'un module core. Par exemple: + * + * final class ResourceCache<T extends Resource> extends AbstractResourceCache<T> { + * // Implémentation + * } + * + * providers: [ + * [ provider: AbstractResourceCache, useClass: ResourceCache ], + * ] + */ +export abstract class AbstractResourceCache { + /** + * Récupère une ressource par son IRI. N'exécute la requête requestFactory que si on ne dispose + * pas d'une version en cache. + */ + public abstract get<R extends Resource>( + iri: IRI<R>, + requestFactory: () => Observable<R> + ): Observable<R>; + + /** + * Met à jour une ressource existante, rafraîchit le cache local avec la réponse. + */ + public abstract put<R extends Resource>( + iri: IRI<R>, + request: Observable<R> + ): Observable<R>; + + /** + * Crée une nouvelle ressource et met la ressource créée dans le cache. + */ + public abstract post<R extends Resource>( + request: Observable<R> + ): Observable<R>; + + /** + * Supprime une ressource en distant et dans le cache. + */ + public abstract delete<R extends Resource>( + iri: IRI<R>, + request: Observable<HttpResponseBase> + ): Observable<HttpResponseBase>; + + /** + * Effectue une recherche et met en cache toutes les ressources récupérées. + */ + public abstract getAll<R extends Resource>( + request: Observable<Collection<R>> + ): Observable<Collection<R>>; + + /** + * Invalide une ressource en cache. + */ + public abstract invalidate<R extends Resource>(iri: IRI<R>): void; +} diff --git a/src/ts/service/api.ts b/src/ts/service/api.ts new file mode 100644 index 0000000..19e9d12 --- /dev/null +++ b/src/ts/service/api.ts @@ -0,0 +1,128 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { forkJoin, Observable } from 'rxjs'; + +import { APIMeta, APIMetadataRegistry } from '../metadata'; +import { IRI, Resource } from '../types'; + +import { APIRepositoryRegistry } from './abstract-repository'; +import { AbstractResourceCache } from './abstract-resource-cache'; + +/** + * Options supplémentaires pour les requêtes. + */ +export interface RequestOptions { + body?: any; + headers?: + | HttpHeaders + | { + [header: string]: string | string[]; + }; + params?: { [param: string]: string | string[] }; +} + +/** + * Service permettant d'accéder à l'API. + */ +export interface APIService<API extends APIMeta> { + /** + * Métadonnées de l'API. + */ + readonly metadata: APIMetadataRegistry<API>; + + /** + * Repositories de l'API + */ + readonly repositories: APIRepositoryRegistry<API>; + + /** + * Récupère une ressource par son IRI ou par son type et les paramètres de son IRI. + */ + get<R extends Resource>(iri: IRI<R>, options?: RequestOptions): Observable<R>; + get<T extends keyof API>( + type: T, + parameters: API[T]['iriParameters'], + options?: RequestOptions + ): Observable<API[T]['resource']>; + + /** + * Récupère des ressources par leurs IRIs. + */ + getMany<R extends Resource>( + iris: Array<IRI<R>>, + options?: RequestOptions + ): Observable<R[]>; + + /** + * Génère l'IRI d'une resource à partir de son type et des paramètres d'IRI. + */ + generateIRI<T extends keyof API, P extends string[]>( + type: T, + parameters: P + ): IRI<any>; + + /** + * Invalide le cache pour une IRI. + */ + invalidate<R extends Resource>(iri: IRI<R>): void; +} + +/** + * Implémentation de base d'une api + */ +export abstract class AbstractAPIService< + API extends APIMeta, + MR extends APIMetadataRegistry<API>, + RR extends APIRepositoryRegistry<API> +> implements APIService<API> { + public constructor( + public readonly metadata: MR, + public readonly repositories: RR, + private readonly cache: AbstractResourceCache, + private readonly client: HttpClient + ) {} + + public get<R extends Resource>( + iri: IRI<R>, + options?: RequestOptions + ): Observable<R>; + public get<T extends keyof API>( + type: T, + parameters: API[T]['iriParameters'], + options?: RequestOptions + ): Observable<API[T]['resource']>; + + public get<T extends keyof API, R extends Resource = API[T]['resource']>( + typeOrIRI: T | IRI<R>, + parametersOrOptions?: API[T]['iriParameters'] | RequestOptions, + options?: RequestOptions + ): Observable<R> { + let iri: IRI<R>; + if (this.metadata.has(typeOrIRI as string)) { + iri = this.metadata + .get(typeOrIRI as string) + .generateIRI(parametersOrOptions as API[T]['iriParameters']); + } else { + iri = typeOrIRI as IRI<R>; + options = parametersOrOptions as RequestOptions; + } + return this.cache.get(iri, () => this.client.get<R>(iri as any, options)); + } + + public getMany<R extends Resource>( + iris: Array<IRI<R>>, + options?: RequestOptions + ): Observable<R[]> { + return forkJoin(iris.map(iri => this.get(iri, options))); + } + + public generateIRI<T extends keyof API>( + type: T, + parameters: API[T]['iriParameters'] + ): IRI<API[T]['resource']> { + return this.metadata.get(type).generateIRI(parameters); + } + + public invalidate<R extends Resource>(iri: IRI<R>): void { + this.cache.invalidate(iri); + } +} diff --git a/src/ts/service/index.ts b/src/ts/service/index.ts new file mode 100644 index 0000000..269a008 --- /dev/null +++ b/src/ts/service/index.ts @@ -0,0 +1,3 @@ +export * from './abstract-repository'; +export * from './abstract-resource-cache'; +export * from './api'; diff --git a/src/ts/types/collection.ts b/src/ts/types/collection.ts new file mode 100644 index 0000000..3c28689 --- /dev/null +++ b/src/ts/types/collection.ts @@ -0,0 +1,39 @@ +import { Resource } from './resource'; + +/** + * Nom de la propriété contenant les resource d'une collection. + */ +export const COLLECTION_MEMBERS = 'hydra:member'; + +/** + * Nom de la propriété contenant le nombre total d'objet d'une collection. + */ +export const COLLECTION_TOTAL_COUNT = 'hydra:totalItems'; + +/** + * Collection représente une collection de respoucres JSON-LD pour un type T donné. + */ +export interface Collection<R extends Resource> { + [COLLECTION_MEMBERS]: R[]; + [COLLECTION_TOTAL_COUNT]: number; + [property: string]: any; +} + +/** + * Retourne les membres d'une collection. + */ +export function getCollectionMembers<R extends Resource>( + collection: Collection<R> +): R[] { + return collection[COLLECTION_MEMBERS]; +} + +/** + * Retourne le nombre total d'items + * @param collection + */ +export function getCollectionTotalCount<R extends Resource>( + collection: Collection<R> +): number { + return collection[COLLECTION_TOTAL_COUNT]; +} diff --git a/src/ts/types/date-time.ts b/src/ts/types/date-time.ts new file mode 100644 index 0000000..f404731 --- /dev/null +++ b/src/ts/types/date-time.ts @@ -0,0 +1,4 @@ +/** + * Full DateTime in ISO-8601 format. + */ +export type DateTime = string; diff --git a/src/ts/types/index.ts b/src/ts/types/index.ts new file mode 100644 index 0000000..c0568a2 --- /dev/null +++ b/src/ts/types/index.ts @@ -0,0 +1,4 @@ +export * from './collection'; +export * from './date-time'; +export * from './resource'; +export * from './uuid'; diff --git a/src/ts/types/resource.ts b/src/ts/types/resource.ts new file mode 100644 index 0000000..1848919 --- /dev/null +++ b/src/ts/types/resource.ts @@ -0,0 +1,53 @@ +/** + * IRI typé. + * + * Internationalized Resource Identifier - RFC 3987 + */ +const IRI = Symbol('IRI'); + +/* Les IRI sont en fait des chaînes mais pour forcer un typage fort on les définit + * comme un type "opaque". De cette façon, il est impossible de mélanger + * IRI et chaînes, et le type générique R permet d'interdire les assignations entre + * IRI de resources différentes. + */ +export interface IRI<R extends Resource> extends String { + readonly [IRI]?: R; +} + +/** + * Nom de la propriété d'une resource contenant son IRI. + */ +export const IRI_PROPERTY = '@id'; + +/** + * Nom de la propriété d'une resource contenant son type. + */ +export const TYPE_PROPERTY = '@type'; + +/** + * Resource + */ +export interface Resource { + readonly [IRI_PROPERTY]: IRI<any>; + readonly [TYPE_PROPERTY]: string; + [property: string]: any; +} + +/** + * Vérifie que l'argument est une ressource. + */ +export function isResource(what: unknown): what is Resource { + return ( + typeof what === 'object' && + what !== null && + TYPE_PROPERTY in what && + typeof (what as any)[TYPE_PROPERTY] === 'string' + ); +} + +/** + * Retourne le type d'une ressource, ou null + */ +export function getResourceType(that: unknown): string | null { + return isResource(that) ? that[TYPE_PROPERTY] : null; +} diff --git a/src/ts/types/uuid.ts b/src/ts/types/uuid.ts new file mode 100644 index 0000000..8dda7a7 --- /dev/null +++ b/src/ts/types/uuid.ts @@ -0,0 +1,16 @@ +/** + * Universally Unique IDentifier - RFC 4122 + */ +export type UUID = string; + +/** + * Teste si une donnée (chaîne) est formatée selon un UUID. + */ +export function isUUID(data: unknown): data is UUID { + return ( + typeof data === 'string' && + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test( + data + ) + ); +} diff --git a/tsconfig.declaration.json b/tsconfig.declaration.json new file mode 100644 index 0000000..ef88678 --- /dev/null +++ b/tsconfig.declaration.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/ts/index.ts"], + "compilerOptions": { + "outFile": "dist/index.d.ts", + "declaration": true, + "emitDeclarationOnly": true + } +} diff --git a/tsconfig.json b/tsconfig.json index 48c1e49..4764da3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,23 +1,29 @@ { "extends": "./node_modules/irstea-typescript-config/tsconfig.json", "compileOnSave": false, + "include": ["src/ts/**/*"], + "exclude": ["src/ts/**/*.spec.ts"], "compilerOptions": { - "outDir": "./dist/out-tsc", - "sourceMap": false, - "inlineSourceMap": false, - "declaration": false, + "baseUrl": "./", + "paths": {}, + "strict": true, + "lib": ["dom", "es2017"], + "typeRoots": ["node_modules/@types"], + "outDir": "out/", + "target": "es5", + "module": "esnext", "moduleResolution": "node", + "declaration": false, + "importHelpers": true, + "noEmitHelpers": true, + "noEmitOnError": true, + "pretty": true, + "removeComments": true, + "sourceMap": true, + "stripInternal": true, + "inlineSourceMap": false, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "importHelpers": true, - "target": "es5", - "typeRoots": [ - "node_modules/@types" - ], - "lib": [ - "es2017", - "dom" - ], "locale": "fr" } } -- GitLab