From 7cee3016ff11f3f0546e2a2f92790839ff491364 Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Wed, 25 Sep 2024 14:19:45 +0530 Subject: [PATCH 01/11] Move the `postgres` aliased connectors within the postgres folder and nuke `typescript-deno` (#302) This PR moves the following connector folders within the `postgres` folder under `aliased_connectors`. 1. postgres-alloydb 2. postgres-timescaledb 3. postgres-gcp 4. postgres-cosmos 5. postgres-azure 6. citus 7. yugabyte 8. neon 9. aurora Also, the `typescript-deno` is completely removed as it is not used anywhere anymore. --- .../aliased_connectors}/aurora/README.md | 0 .../aliased_connectors}/aurora/logo.svg | 0 .../aliased_connectors}/aurora/metadata.json | 0 .../aliased_connectors}/citus/README.md | 0 .../aliased_connectors}/citus/logo.svg | 0 .../aliased_connectors}/citus/metadata.json | 0 .../aliased_connectors}/neon/README.md | 0 .../aliased_connectors}/neon/logo.svg | 0 .../aliased_connectors}/neon/metadata.json | 0 .../postgres-alloydb/README.md | 0 .../postgres-alloydb/logo.png | Bin .../postgres-alloydb/metadata.json | 0 .../postgres-azure/README.md | 0 .../postgres-azure/logo.png | Bin .../postgres-azure/metadata.json | 0 .../postgres-cosmos/README.md | 0 .../postgres-cosmos/logo.png | Bin .../postgres-cosmos/metadata.json | 0 .../postgres-gcp/README.md | 0 .../aliased_connectors}/postgres-gcp/logo.png | Bin .../postgres-gcp/metadata.json | 0 .../postgres-timescaledb/README.md | 0 .../postgres-timescaledb/logo.png | Bin .../postgres-timescaledb/metadata.json | 0 .../aliased_connectors}/yugabyte/README.md | 0 .../aliased_connectors}/yugabyte/logo.svg | 0 .../yugabyte/metadata.json | 0 registry/hasura/typescript-deno/README.md | 157 ------------------ registry/hasura/typescript-deno/logo.png | Bin 40438 -> 0 bytes registry/hasura/typescript-deno/metadata.json | 53 ------ 30 files changed, 210 deletions(-) rename registry/hasura/{ => postgres/aliased_connectors}/aurora/README.md (100%) rename registry/hasura/{ => postgres/aliased_connectors}/aurora/logo.svg (100%) rename registry/hasura/{ => postgres/aliased_connectors}/aurora/metadata.json (100%) rename registry/hasura/{ => postgres/aliased_connectors}/citus/README.md (100%) rename registry/hasura/{ => postgres/aliased_connectors}/citus/logo.svg (100%) rename registry/hasura/{ => postgres/aliased_connectors}/citus/metadata.json (100%) rename registry/hasura/{ => postgres/aliased_connectors}/neon/README.md (100%) rename registry/hasura/{ => postgres/aliased_connectors}/neon/logo.svg (100%) rename registry/hasura/{ => postgres/aliased_connectors}/neon/metadata.json (100%) rename registry/hasura/{ => postgres/aliased_connectors}/postgres-alloydb/README.md (100%) rename registry/hasura/{ => postgres/aliased_connectors}/postgres-alloydb/logo.png (100%) rename registry/hasura/{ => postgres/aliased_connectors}/postgres-alloydb/metadata.json (100%) rename registry/hasura/{ => postgres/aliased_connectors}/postgres-azure/README.md (100%) rename registry/hasura/{ => postgres/aliased_connectors}/postgres-azure/logo.png (100%) rename registry/hasura/{ => postgres/aliased_connectors}/postgres-azure/metadata.json (100%) rename registry/hasura/{ => postgres/aliased_connectors}/postgres-cosmos/README.md (100%) rename registry/hasura/{ => postgres/aliased_connectors}/postgres-cosmos/logo.png (100%) rename registry/hasura/{ => postgres/aliased_connectors}/postgres-cosmos/metadata.json (100%) rename registry/hasura/{ => postgres/aliased_connectors}/postgres-gcp/README.md (100%) rename registry/hasura/{ => postgres/aliased_connectors}/postgres-gcp/logo.png (100%) rename registry/hasura/{ => postgres/aliased_connectors}/postgres-gcp/metadata.json (100%) rename registry/hasura/{ => postgres/aliased_connectors}/postgres-timescaledb/README.md (100%) rename registry/hasura/{ => postgres/aliased_connectors}/postgres-timescaledb/logo.png (100%) rename registry/hasura/{ => postgres/aliased_connectors}/postgres-timescaledb/metadata.json (100%) rename registry/hasura/{ => postgres/aliased_connectors}/yugabyte/README.md (100%) rename registry/hasura/{ => postgres/aliased_connectors}/yugabyte/logo.svg (100%) rename registry/hasura/{ => postgres/aliased_connectors}/yugabyte/metadata.json (100%) delete mode 100644 registry/hasura/typescript-deno/README.md delete mode 100644 registry/hasura/typescript-deno/logo.png delete mode 100644 registry/hasura/typescript-deno/metadata.json diff --git a/registry/hasura/aurora/README.md b/registry/hasura/postgres/aliased_connectors/aurora/README.md similarity index 100% rename from registry/hasura/aurora/README.md rename to registry/hasura/postgres/aliased_connectors/aurora/README.md diff --git a/registry/hasura/aurora/logo.svg b/registry/hasura/postgres/aliased_connectors/aurora/logo.svg similarity index 100% rename from registry/hasura/aurora/logo.svg rename to registry/hasura/postgres/aliased_connectors/aurora/logo.svg diff --git a/registry/hasura/aurora/metadata.json b/registry/hasura/postgres/aliased_connectors/aurora/metadata.json similarity index 100% rename from registry/hasura/aurora/metadata.json rename to registry/hasura/postgres/aliased_connectors/aurora/metadata.json diff --git a/registry/hasura/citus/README.md b/registry/hasura/postgres/aliased_connectors/citus/README.md similarity index 100% rename from registry/hasura/citus/README.md rename to registry/hasura/postgres/aliased_connectors/citus/README.md diff --git a/registry/hasura/citus/logo.svg b/registry/hasura/postgres/aliased_connectors/citus/logo.svg similarity index 100% rename from registry/hasura/citus/logo.svg rename to registry/hasura/postgres/aliased_connectors/citus/logo.svg diff --git a/registry/hasura/citus/metadata.json b/registry/hasura/postgres/aliased_connectors/citus/metadata.json similarity index 100% rename from registry/hasura/citus/metadata.json rename to registry/hasura/postgres/aliased_connectors/citus/metadata.json diff --git a/registry/hasura/neon/README.md b/registry/hasura/postgres/aliased_connectors/neon/README.md similarity index 100% rename from registry/hasura/neon/README.md rename to registry/hasura/postgres/aliased_connectors/neon/README.md diff --git a/registry/hasura/neon/logo.svg b/registry/hasura/postgres/aliased_connectors/neon/logo.svg similarity index 100% rename from registry/hasura/neon/logo.svg rename to registry/hasura/postgres/aliased_connectors/neon/logo.svg diff --git a/registry/hasura/neon/metadata.json b/registry/hasura/postgres/aliased_connectors/neon/metadata.json similarity index 100% rename from registry/hasura/neon/metadata.json rename to registry/hasura/postgres/aliased_connectors/neon/metadata.json diff --git a/registry/hasura/postgres-alloydb/README.md b/registry/hasura/postgres/aliased_connectors/postgres-alloydb/README.md similarity index 100% rename from registry/hasura/postgres-alloydb/README.md rename to registry/hasura/postgres/aliased_connectors/postgres-alloydb/README.md diff --git a/registry/hasura/postgres-alloydb/logo.png b/registry/hasura/postgres/aliased_connectors/postgres-alloydb/logo.png similarity index 100% rename from registry/hasura/postgres-alloydb/logo.png rename to registry/hasura/postgres/aliased_connectors/postgres-alloydb/logo.png diff --git a/registry/hasura/postgres-alloydb/metadata.json b/registry/hasura/postgres/aliased_connectors/postgres-alloydb/metadata.json similarity index 100% rename from registry/hasura/postgres-alloydb/metadata.json rename to registry/hasura/postgres/aliased_connectors/postgres-alloydb/metadata.json diff --git a/registry/hasura/postgres-azure/README.md b/registry/hasura/postgres/aliased_connectors/postgres-azure/README.md similarity index 100% rename from registry/hasura/postgres-azure/README.md rename to registry/hasura/postgres/aliased_connectors/postgres-azure/README.md diff --git a/registry/hasura/postgres-azure/logo.png b/registry/hasura/postgres/aliased_connectors/postgres-azure/logo.png similarity index 100% rename from registry/hasura/postgres-azure/logo.png rename to registry/hasura/postgres/aliased_connectors/postgres-azure/logo.png diff --git a/registry/hasura/postgres-azure/metadata.json b/registry/hasura/postgres/aliased_connectors/postgres-azure/metadata.json similarity index 100% rename from registry/hasura/postgres-azure/metadata.json rename to registry/hasura/postgres/aliased_connectors/postgres-azure/metadata.json diff --git a/registry/hasura/postgres-cosmos/README.md b/registry/hasura/postgres/aliased_connectors/postgres-cosmos/README.md similarity index 100% rename from registry/hasura/postgres-cosmos/README.md rename to registry/hasura/postgres/aliased_connectors/postgres-cosmos/README.md diff --git a/registry/hasura/postgres-cosmos/logo.png b/registry/hasura/postgres/aliased_connectors/postgres-cosmos/logo.png similarity index 100% rename from registry/hasura/postgres-cosmos/logo.png rename to registry/hasura/postgres/aliased_connectors/postgres-cosmos/logo.png diff --git a/registry/hasura/postgres-cosmos/metadata.json b/registry/hasura/postgres/aliased_connectors/postgres-cosmos/metadata.json similarity index 100% rename from registry/hasura/postgres-cosmos/metadata.json rename to registry/hasura/postgres/aliased_connectors/postgres-cosmos/metadata.json diff --git a/registry/hasura/postgres-gcp/README.md b/registry/hasura/postgres/aliased_connectors/postgres-gcp/README.md similarity index 100% rename from registry/hasura/postgres-gcp/README.md rename to registry/hasura/postgres/aliased_connectors/postgres-gcp/README.md diff --git a/registry/hasura/postgres-gcp/logo.png b/registry/hasura/postgres/aliased_connectors/postgres-gcp/logo.png similarity index 100% rename from registry/hasura/postgres-gcp/logo.png rename to registry/hasura/postgres/aliased_connectors/postgres-gcp/logo.png diff --git a/registry/hasura/postgres-gcp/metadata.json b/registry/hasura/postgres/aliased_connectors/postgres-gcp/metadata.json similarity index 100% rename from registry/hasura/postgres-gcp/metadata.json rename to registry/hasura/postgres/aliased_connectors/postgres-gcp/metadata.json diff --git a/registry/hasura/postgres-timescaledb/README.md b/registry/hasura/postgres/aliased_connectors/postgres-timescaledb/README.md similarity index 100% rename from registry/hasura/postgres-timescaledb/README.md rename to registry/hasura/postgres/aliased_connectors/postgres-timescaledb/README.md diff --git a/registry/hasura/postgres-timescaledb/logo.png b/registry/hasura/postgres/aliased_connectors/postgres-timescaledb/logo.png similarity index 100% rename from registry/hasura/postgres-timescaledb/logo.png rename to registry/hasura/postgres/aliased_connectors/postgres-timescaledb/logo.png diff --git a/registry/hasura/postgres-timescaledb/metadata.json b/registry/hasura/postgres/aliased_connectors/postgres-timescaledb/metadata.json similarity index 100% rename from registry/hasura/postgres-timescaledb/metadata.json rename to registry/hasura/postgres/aliased_connectors/postgres-timescaledb/metadata.json diff --git a/registry/hasura/yugabyte/README.md b/registry/hasura/postgres/aliased_connectors/yugabyte/README.md similarity index 100% rename from registry/hasura/yugabyte/README.md rename to registry/hasura/postgres/aliased_connectors/yugabyte/README.md diff --git a/registry/hasura/yugabyte/logo.svg b/registry/hasura/postgres/aliased_connectors/yugabyte/logo.svg similarity index 100% rename from registry/hasura/yugabyte/logo.svg rename to registry/hasura/postgres/aliased_connectors/yugabyte/logo.svg diff --git a/registry/hasura/yugabyte/metadata.json b/registry/hasura/postgres/aliased_connectors/yugabyte/metadata.json similarity index 100% rename from registry/hasura/yugabyte/metadata.json rename to registry/hasura/postgres/aliased_connectors/yugabyte/metadata.json diff --git a/registry/hasura/typescript-deno/README.md b/registry/hasura/typescript-deno/README.md deleted file mode 100644 index f4e2311b..00000000 --- a/registry/hasura/typescript-deno/README.md +++ /dev/null @@ -1,157 +0,0 @@ -## Overview - -The Typescript (Deno) Connector allows a running connector to be inferred from a Typescript file (optionally with dependencies) and interpreted by [Deno](https://deno.com). - -[github.com/hasura/ndc-typescript-deno](https://github.com/hasura/ndc-typescript-deno/tree/main#ndc-typescript-deno) - -The connector runs in the following manner: - -* Dependencies are fetched -* Inference is performed -* The functions are served via the [connector protocol](https://github.com/hasura/ndc-spec/tree/main#ndc-specification) - -It assumes that dependencies are specified in accordance with [Deno](https://deno.com) conventions. - -## Typescript Functions Format - -Your functions should be organised into a directory with `index.ts` file acting as the entrypoint. - -``` -// ./functions/index.ts - -import { Hash, encode } from "https://deno.land/x/checksum@1.2.0/mod.ts"; - -/** - * Returns an MD5 hash of the given password - * - * @param pw - Password string - * @returns The MD5 hash of the password string - * @pure This function should only query data without making modifications - */ -export function make_password_hash(pw: string): string { - return new Hash("md5").digest(encode(pw)).hex(); -} -``` - -* JSDoc comments and tags are exposed in the schema -* Async, and normal functions are both supported -* Only exported functions are exposed -* Functions tagged with `@pure` annotations are exposed as functions -* Those without `@pure` annotations are exposed as procedures -* Optional parameters are supported -* Exceptions can be thrown and will be reported to the user - -## Function Development - -For the best user-experience you should develop your functions in the following manner: - -* Have [Deno](https://deno.com) installed -* Have [VSCode](https://code.visualstudio.com) installed -* Have the [Deno VSCode extension](https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno) installed -* Have the Hasura V3 CLI Installed -* Have the Hasura VSCode extension - -An example session: - -``` -> tree -. -├── config.json -├── functions - ├── index.ts - -> cat config.json -{ - "functions": "./functions/index.ts" -} - -> cat functions/index.ts - -export function hello(): string { - return "hello world"; -} - -function foo() { -} - -> deno run -A --watch --check https://deno.land/x/hasura_typescript_connector@0.20/mod.ts serve --configuration ./config.json -Watcher Process started. -Check file:///Users/me/projects/example/functions/index.ts -Inferring schema with map location ./vendor -Vendoring dependencies: /Users/me/bin/binaries/deno vendor --output /Users/me/projects/example/vendor --force /Users/me/projects/example/functions/index.ts -Skipping non-exported function: foo -{"level":30,"time":1697018006809,"pid":89762,"hostname":"spaceship.local","msg":"Server listening at http://0.0.0.0:8100"} -``` - -Alternatively, if you have the `hasura3` CLI installed you can use the `hasura3 watch` command to watch and serve your functions and tunnel them automatically into a hasura project and console. - -If you are happy with your definitions you can deploy your connector via the `hasura3 connector` commands. - - -## Deployment - -You will need: - -* [V3 CLI](https://github.com/hasura/v3-cli) (with a logged in session) -* [Connector Plugin](https://hasura.io/docs/latest/hasura-cli/connector-plugin/) -* A connector configuration file -* Secret service token (optional) - -Your functions directory should be added as a volume to `/functions` - -``` ---volume ./my-functions:/functions -``` - -Create the connector: - -``` -hasura3 connector create my-cool-connector:v1 \ - --github-repo-url https://github.com/hasura/ndc-typescript-deno/tree/v0.20 \ - --config-file config.json \ - --volume ./functions:/functions \ - --env SERVICE_TOKEN_SECRET=MY-SERVICE-TOKEN -``` - -Monitor the deployment status by name: - -``` -hasura connector status my-cool-connector:v1 -``` - -List your connector with its deployed URL: - -``` -hasura connector list -my-cool-connector:v1 https://connector-9XXX7-hyc5v23h6a-ue.a.run.app active -``` - -See [the Typescript Deno SendGrid repository](https://github.com/hasura/ndc-sendgrid-deno) -for an example of what a project structure that uses a connector could look like. - -## Usage - -Include the connector URL in your Hasura V3 project metadata (hml format). -Hasura cloud projects must also set a matching bearer token: - -```yaml -kind: DataConnector -version: v2 -definition: - name: petdatabase - url: - singleUrl: 'https://connector-9XXX7-hyc5v23h6a-ue.a.run.app' - - # And optionally if you have configured a service secret: - headers: - Authorization: - valueFromSecret: BEARER_TOKEN_SECRET -``` - -(NOTE: This will require that the secret includes the `Bearer ` prefix.) - - -## Troubleshooting - -Please [submit a Github issue](https://github.com/hasura/ndc-typescript-deno/issues/new) -if you encounter any problems! diff --git a/registry/hasura/typescript-deno/logo.png b/registry/hasura/typescript-deno/logo.png deleted file mode 100644 index 96f4c8590ce9196e84dfc18f64e58fb4d0ef20a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40438 zcmaI81z23owjkPAu;31j1cC>5had^=?!n#N3Be`MxD(vn-Q6{~YjAIDI{!Im?tOP= zUe`zO>Rq*}YSr4cd=sW1_XP!s5D5SPph$idQvv{BWg&H4gm;i{g?GutkPEDth^zJ+qoa~=2&Q=2C8nOyxqIQlZWSor5 zjLhVMNMvMW{Eo(^yh>sc{|1Nr5+FBscDCnbVsdkHV{~Id-r2&=mhA7ihTrU5oCV0q|4#J3KmS^%iMz#r z&1CEJZ?qr?GW~5~Vqs)v`oDoWTbTYo!2Y)U3+x}f{xu!{-@$lA?X2w_mF*3UOaxi@ z{~7_Kwchy&yWIqb za7L1@Bo#0N{X(+<{R29G3fjiPo@NGG1PKu_;A7v~+L|tB>r=*tPs(`P?-a8qPR(X` zPP_Gt(u4IBx5+mW$)meX`Nwa=j$QvQ+41(9gCQ{tlmhi@b-*e}35|joyya$Wh~!M# z^Z%!GVH^k_W7th|n)4X^{}crzZUYgJ60>AOL?X=pyL`!n!DZ*U;%{WoLe=qZnw+I-vR%>MVMd*KnB10Zu6_H zt!;M}Okz_2+C7K0JpeO=64gE~F_CH_3$(vmo%OHDzXblp5>uza=KsVoUIdj}7?Dg; zPA(eA?>^;El>C8&GsZy{j_1viaw_z%K_zz~7{(3B zXea!8al*fM{m1n@I{Bk_#b^xc z!gswt1#;VWC0_tCxdBpTx4*srHS(onS0L4;_S%4lz{w+7oljykz}NwB0kat>R6%Ue`{>*V0>LE5QO}pCf(SV*zwV7B zn4B)xD{_bdnS*&}XJ^MrZ+UH|&N_UaWJm#WbZTYhb@Hn=D^`D(3mMCvjB4s5HJ85Q zpNioNJ_ZhPx?{#TENqpNH&3#5!=R3$+dC}0{2};_ z!ck3utKCRj&l>H4L7fJ(x#$T;G7sPdj5H9vw-QG<0W$XxvD+#mgLcEu0@<{Ybe`+o zaQce(Fxsmo7P)HWT32(*em`Z?IEUkDXN?!iQfj;(!=YtysD*5DyKDU3UZF4Yl=7u; z`5o|$g&5#J!N>Ik6lD*Kz{y10sVYwhHU#jPNK2LGH^nuVx{aEFI1k@p5{)bIqp!I9 zgCzvdP+(DZ=TEkz{1Get!qLPGaVEQ96k+ zLBQ({KME8mwg{I>OUzrBB+b;GN9OXo#>8=g=YRmcf3j}y2-&C48@?Q?3yZovo7~Ir zpL6{a08buBWu6RNsR?dlFZ!-h32Z$zgzy?w@#DoZGQStse3h@YMUus-+$);@q8mOB zfTrWze@J^Hf4IVje7-bPMmyEAPOgGa_4G*87tyGU;nlj%&et$KivZ4|%OaeptQ&X$ znzpF1cL{$zi?CL!I?UKQx>})k+9nNR_`41TDsI5nK!UOE?``nkV5ISza#?0_zr-*L zK0oZl7;d!dJ366BWeK+T#w9UmlOQBFu|dair&8ZED&>#76aFwB_s`)F{so*mRb<{P zx^!(UBgAFCtU@SH(^8$CUtj6)7bvUN|4YA5@FTGBbca0M@#4j-4u&P!4$xZ%JGqL* zHSXohi_}Lz5e7lom*hSLY8(762@?>p7f#($?6=pZ^-7CX3N#N|(TySsf?e~4XZ7*e z^vmuLQnSL5_i&L-4S(5*LI=11{=nH+kTb&+Uc9-%&oKNk8XEZM-O;nCF+2hY2% zue)5B!htOhh~0n8_6Afm+j+zj%cNuLwUvxP4FRsma%R^Tl)pTnq#jE3DD{sQu`Se? z^tCu9Kn396ZN43I#d2t(CYTOtR{n$uU*4g~f{0sjeioyb#M}rxi3iKeC!E=oeOW?R z|I?WWW0D2qj~5M6l}HOsK{jP=_qu1DwgE); z4A4)-;X9rrW2+V`^^2_{mM-~L|ATgD0F0st2>+PIO>^n+%IrU~EBlXwzt7_p3N5Qi zwWSV?!WLCmkYb4ki?>#4x{pr9^sD^_TFbkSIeJ~ms%c8bEta-28t5}xnw=ctuJm6& zlL#Zdk{z(!jI)f3-10kTFj)))6)0wSTgxneM{O%tPWs(GwJfEEtQ1DeTG?Ys(Di(rI@&2G7oW$Jz zbcoV?^~^*D-(Cb$$4{^G_Q73zLGPfkbRLTL-j}pmG>Cu!D6i1I-X?(YA>REb&3}Yq z-v|_PYTpQL+T?#EwT?aD;@iZC%KQU*vGdoR9s&C`w^8lh**xtN{(10F41REO3$xen z{N=5k{lbw((5&%54fgLQfuKlxQk1w6oHd3%-zd8TlM_)bx?UBwcYwVmH4oNouT1CR z5fGi0ogDTBEVEhv0Jg{Z6T9T4zt|1zL6!N^m}Vwjq4PX*ZvUT2%Dd^6^uHW=cpuOO zz0Vz8yC`;2=>7d2%5JhzG;L8}%U~oysTP$WQ)-|N5;J|#<*E~t`CNK2mDr`*1`1aJ zk1wi$ZhsHsngQZg=;K)=N)#5bUu*MYq{&?bB*fAFxDtYr8$q00eLa4LS-vt4VUGQNax+)v7da#;SD2-&a06jYHc6)G2Pg2?81Tg%8CKul~j3>8_^y!Uqld}v3N(;qX{O4Qi|Vc zh*KrXsP5 z)CbG_z%5g%_n1!>c^XJPguNbFuLjaY0Redc8GyZg9sBAI0Jggijy#^plsz^S48A)h zQd_(^VuRiMC?L=?y04Ib6X1&Z0i2q#Cdb%v_}PfWc~anhm}|qENNr=i-K)+{H5ZyY ztj=!qufh{AfGXy@lhA&-jPb9>`Qr~5gnWc(vFXob4Jw@6fXyn2j2c@-Cu~4G+4j0% z0;%5fCtWIf?ycl41rmn%4L=1D60gd@F*OkG^Dp4h5SufxEFPG{#i4}%*%j__hx*9W z+Mjo|_Z;z6Q}6Y2Zg(*>qDf#Vi$PDVk-TvSY(nGF-`%B5f_Z$jC8heY0qc)`x?(m; z`g#$x>NL5g)b8m5Qvn-=V1w!%a&x*u{b@9r#pwM7=Fu9V8d*0JfO9;Jiz}aq)0*ZD zyePC+Gs=iV4%Hl1#2Yz|_R;a6cJW`tQ=9Fp_(y?(M&+*Y+hR}_!S%`k4hcAq>D5ZL zWX=_qU`^^oxyD&}3+3V7+fGdgS33fJLSf0>K10uuz=IB)wQT^3mlP=>cX5D@TY{cUH?^efCp>prjiEIWK9bfIIeb09}Suiz+C_M4{Z{Pr78?56p&Dw%}JJl{E z|H#W7YJ5LRxG+RL1tAOvGIl-Y4F{~$PszL1>us9H^QC4oP6$RaJX}0R@UYf%*EWzRws-$kmp#EHic!<>$T`+c_i@5kJJ7cniE76#2`PFTMTUFThcG~ z&{$+>_rvh$=q;qYCec(fxYC8vU-RLeuJa``q#Un@$9IG7T=Gokoi#gO{Jq(1a@*c! zd2v3W(UFd_g-8vFQ7)ro@OpdwQ78L=dr)S96!~YqcXVr3>2_C@Lk`;;AQQIuKIGu1 zkQ(Z_R&CG?=p0?!CsXnj)zEd{!zEJ_MB5`YCz-`bn|%FyiCr1Z);8fj%9CRb~_5Dp9E?fDFJISgkdahY>TV*rC<{HT)IU>=rfYK6ilLf-o#< z#I0b55iM-!zHaM-yA?ZCtaNJv)?H}=L2q|_d}Wc??Z@kbQ5%IGx{z;_{bqz60eF+^obtz)2GT^B{%(i> zz=l_k$dQw)oxS;TXl||gyKeSO@D_F`cunbO7oZ%{l~ougUq50oQ)NnNq1hQ;SER4m z8>+@^F;8DeWO&*oLTS#QA21Eh1$S2yZmuCA1YtBF6&>zN_9vO@Gw7(Ox z)F}N0QTW=8xr)^k~futtFJ$<1mR&<;ZY{S0PspVJ~ESumhA#C ztM{o%Cv(rfpa23Tq-w#BCB<<}3`DzWR==+16gq!=5c=U+lKS03<|}gWt07&jk`R>p z6giYJ6d+8(pEu!#3FB91DW_q0p3FbCmqH1T|5QZ7yX>zYq42kUTbb6{Z+(ucs{JAo zYL9{z@?kEd>ZMfwMGK<07YIv=d%V5e!}$jfaf^QddoAj^jhy@5R~5hOq=bRbWO|Tx zfMKm;vsreADifJPyNda&KV6e!7hSvb>!z}41o|wy{rbMM(^4>^miVFC?@jP`y;YL+ zT8k{F?J5mKDM_T1P8bTU)%9p&w z_q#yqb+JZTbLa^kG29>c{_y>zc5CNRPqbOJlAn%wc#qxQ4aC`|FH#PyU?`E^aIN3V zLv?`@=?wR*o!e+j4&`kq#_f;K=QCc49A~v$SA!|*(-JI@kaDMzTdq9WV1Li-;Pwas zF*djlzS>%<_bef$Fj~V|I?0`Y;y6tzZ{3EG-9BRPVv9Auz?f-~?6^@;CIHqoC{*QT zLQ?{0%Gj<;+yv{PU{X%vzNtca0~KVGqmJF+E{s=KRZ_A|xkfo|m~UY+u?L+bI~z(o z!4{u*V!oB#?i*<_ed$=LzZt#0R4*0~Z<3o!UTvEpR@;eYKUw#nz=-JYO= zB7a02DC=~uGafjG>vr8%Ypl#Al+>RA=j(NWffdL>eqH$BKODCXCVa<9{#X#Zki#%J zM$N3jZo}3eGz_A)dN|B_Pt)MAL)F`=;qFrhwppnM($K+wGFMVTCCRzEEm4p6CVHfp&bFbT1=^>_-g)I%(HEbA>czJK?RVh}1l8#-K!+&=+gToz}qSCA1c)0*4$LHh$fe^mB@K|uh2 z*NhhRCq~8{95OTd)qqhMEpS@HApfff2KbGs487jY|7ndL=+h-o4OaptOlc%r2rJWfKb-^4UM10|;i6=vL!Co5t_dHC03f!^HJNlGCqVOp z&F+w1yUj<5@lCF15sKMTZ8WLYb9W>x!)fn{z~bt4h+VUdu^&J^tQE(9?KOA54YgM5 z0f#NRLNakL<3eEcBml~ex}C8_!qk)^py&oEE~fom2acTQaRPiaovSO zxixMK@JLQ%)f5Lkw@cW6_ZzbhDrhCrGhug?;9%6Ypltj6bQA`js( zIt6tzGXH^F*!kD>1QE@7`+ddpKG!`FN-oS&EW`+;p#WB;QkfW&pE7@f2cnWqEmR~j zPUL(L{>->q74Hwr zLR>CJ+6N>0f|X>*>V-f@I_*CICQB%<*uW`5iaGR+28uO>G zw^~fxi4mH}_yn=}FULs}X?yVQbv#z?PC}G%Ub@+LAx;3dxG}RLAwx-wPVUn$mdGR+ z!AnZdz#_f&)K!mzNtwRj$CfI@7GU1`OI0)3=d%tAM84KG|rk21PO$E z1Kl@Ren>+Oc0k-X)LWzY@R?KdpS6wPiujg!xh zGT8A*giZpuzb2yi=4u~uNS8H6miQqaXR#)pVk6?_&8A=sqQO7gPdwVjhd1M_nz>un zD^63%-!}QEcgF*wu(#xtT~P8*mZcYq_*`~x7+r8^iZqWoXvtZrM`XP{%a+>YNgmB( zEpJqqqh0PvhB)|l9j*f3E0)kZH2x;Lo8qz6`f|!ULYv=rr**T3IvFLX=Xg8Z?0Dk; zy@f<&Coj2P+MCaj24g%?qW*fX+RAZv?*q!_IQ@+`dKdNweLKZ#pUtl8-30YF+t0>eQ||asvcHOI z^VP_xN%!+(Y)y&$cgw3w>bF=_VqP&=fz0atWER~9>))8w{Qi;< z!#2;TP|k5b#mw?>?t09k1<$}LY zgTMFl;VAp1HMr+5V3hhorYL3Cb!4)sZ3YWam~35nPK4D6UmhZo4uwTPki_a z^UApj-BI&ul~Og`10L*tv+I2Z!V%Z@&0N@!U(e+l)gu630PhVvU=SsGJX2thn;cFj zOaef;sOMF$(Xgu^lEfbhS4M6M(f);$lu1RT_KFWicbB=doAmI7sX^k|zl& zACNS5{oP8L4)ONWZei^@+pYGTzQb|r_yXoHqVCp2DH$pE#MIM$_WN4ExJD~lU>#3K zhdn3-_ykU~Du#6e1ibv(Arwz9L_zWucpd&mz~dG{!e+sah7vXEm*BGyxUM!B(_G`V zA>!e7L-=U?%!?IpP|_hAnH7OK7XIy2tL2Jd-CFq+7-XD@+!bH*xlMG0g2hrefY9@B zqHRF>9kU_|XjE<}VFjDnQ2bEvtq6-uPW+|5^C=MMvY*DNFFRG0T1|?LcawFgx`>49!118^5C%ZZ6-UJ z&8xxrbeWR;aBD8czR7V<>an>bAB~7}PBQw)X}%hE>pqDdyf3wM#bSNYp*WdXAXmB6A3uRKsALya7 z4a%YLZq+#!xx8>IBH#LCXs zENJ(QkvK|e)EjwBOTtEqI6d zx%@1{UzZ3(mb?xw+{lYnX3%TGg0lp4K<#w2Kw%=9QbX0F`35zEBv*RC)x{TNleyJ$ zy62*0D}#wClrth8i8rX9BIaPdaRsVR%eJiXuI1r0y)o{5eMB!@gjf^{-|Q0EQHyjK zT;|iMn1o)#`bCZ@UmW+$4uSw!J|CsuW0-{458mq)eID<|Cg2s{cyoZJeqWgQt{lMr zTU*2MG^_J&$0NiXbOV#{@=O$TO zAiGxvWk$3mpri?|vt9Gdokyq5aD z_e?>Ez7(sDq}zh8qP))Lb{%{c5CS95-q_pnrcwq$zYA^tQzNZHpZCLwFGrqT$^P@` z#>@RiVXD$wZ1;EgQmRF>D~KjN>~p_H4{^BD$zV|xeHifVL44_z`ANuSi(h2&S>OWj zbJ#Ks)`8>Bnfwp`W#10iwzapcH``6_568O$V+K~Y=N}crL|_)zb&I-54!{22Nt=39 zt_K&%>D4TZ1KoN8?6_vuVf6|(N@s$+*Ik98s2kJhGOEozk7uh)!&wlHF zVW;|TiAa?T5iqB|h$3#}_#-;nw58&+a47Z30H_miXqpc`&x4ngL`<2~l`}RM#d8+k z+m#GXrhUdC&WDvjDv(PfbUK+Wlx17Swf?-8CTIs2O*>j;*xS~K367>3!_)U>VzRPX zygnMq!#%gWg01%I7hhvl-l(D_(5=!v_KAhx9%0>0eI-MZ<}{GuH}LzX)h{ zTvtYoks2`ruH?tfn5>}vV}g%?@K8)?L+;SUE7=1GKT}VCn7kE-BjYA5DU8;pn1)3O zNqe7{+vJfb(;>F~#iB!9X~EuNKSzIqWqz`Vl$05OKV2l(@%-HOeZU(o3KS=md_1RS z6RU5IunYfAJwC(hvKLF+&SjEfoGAMTmiB90BE34bWnyY~pVX|X`3A%_!s8HA_h>@# z(2FM2S0@r(Z{z*zJ)yI5*>7ptjubYt60(l3#F3_HQLYMuvnoWc;2R5(M&$fTjq0A{ z%j}FUs&yr9cwqO8B%E;DaS1(A5QxX=bBBnMvM4+qs)7C6OQ#~)E+d6I`5)+tCJrX_ z_hA*+5Pbt1nlim1?h(=b>Y}$Z0VFRd>HBg5A6c1ajw+#vWwEluO5n)kf*NKn#8~0CF0a z9VelUd5LV6py>EVvyzlOI+$PFfgSSKil9wWLUjPwwE6B3>73hS_-7xKs7wFHm!p-& znCbYQk0|93Ez1%QAR_k?i3l(sPpvGWqGXj0>xPY$aaWhVBTiNffuY8G@+v3MDab|%l zOdH@FrvhAW3WLYg&{7tN+Uqvvl4TS+1nJ4ta9q}V4aiA z(9J&sZe`l-5BP4@D!nIiIA_r2$8`+P#Q5~EK7tL%haTqa7D_F*o6>GisGVe9ez009 zO|9~sI%aW1BI?~r9o5J$GWpHnoi7_A=;;w}dN7Y&pLm2a@pHwaPz2|~+zVD3dPsHS z4a}D~Wbz6xV5z_fk(YA>!<0JzE((^cNK|$?n+i}3BcYuzY&G5=jWPqqCk6L4pctel z+}y@xbZgO1_gj4&PA0S;H*e|mY#>M za6JB;7=Nn!b*~u9BeDBEn5D}BB0EBZ83GuCkHQ=Mc=|<~qb5}02~JFhAeL^tS65*F-AvbFscvF*H4{D$%K70E zdu20y4}8259P5nDyWg%eQ-%Bjz;I6!53JBKhB{$oIECfLz5@-~Ql=3N6-xb3FH!L% znkCx>hN{;XSNvilH+y_0Nx{XGc5Pw_9Hn^{?yJi=S7%fY4vTA#kvM$w1hlSAbX7;A zMfKoQbG=T-oY%Y}=UT%jU(KpJdkyL1LeyXBH@3>^8OaN=?>P)Vv?!=@D2fBJxN)(ie>f+x{NgS zB-<2@ALxyc)t^}Slx&R(?u|CA!JCX$05v--DrHn;QwZo?EwrMUxCfT#ibYp4%dLJq z-!{RQo_y)HNvW5bhvv}wsVTnQY(55o!p>IyfLBQsx6pN)m?W2pf;v{?^I|y zFX|r>kz`$QR}?Rxd~oI;NG=is<1e;Cr8*y9azXfxU8|J?8#?t>qz_ zp;lNkx5yDmVFmySHzuUr4QuH3iiv8~=*`2|@o(y&^R)4HxX2clO=AGNKYbx8tF+<> ztszv4oo$%|A+5L6(+U&q!yk0Zy?sBXEk0`Ti%mqi&W}z#EL$o0j%1oc@Hu*4=w*rj zWfnal|EpSV<-z?(0t;cV`*z@=DKCIB<<{9|E>a%3FmH@4t?m{{<&$>9r+vn1p7fF) z=m}=xTbs@v{@GTQHSW-g<~yy&l?^?V)^Ul80wVZfNI;$_@ywgY-k>uT^$*AK(nRgi zDm`LWy7TK}eZf}g;_a){v@-YC{vv_rthK6+pzA{w#hp=7RrSq*&oUJj!VVC*gyzRPR)qQ82)p0jQ0^yo|aOvsh$Ou;RK(KHb5{A4Wt_2M~=JE4? z(@o_1Hcn=K9U?iZ|9TbY(D4KbXny=tyWe&_0rGn}iAsJvzgT5~X}y_=n0LMmPWFc_ zv@Xly$D+=34}7GknQ1-msN7r~l=x1e*A-^=5!Z2SV`!8YW{&?Ly%yzgrokpnqXc1W zwnUZQ<4^S$4%QWdlAPP3$Cn5wN^uruyuk9EM<@*%7y+; z{$y2wq&=_RZh@!NZCJK)oUtiHqU%wBa43p8#M&C!7TEIO3a0Yd{QhI#86T>m#L`h` zt*|(~@`F1Y(Sw&)p?L#Rb)joXX~{ACUaO||?kz@XWR;G}muCMD2(3xCwq=Sogi;a$ z0v*x98Q3yBjoL62b1@1br9r`&owja=H-eve4sfS+$ft*ik4Hj;%GtUkg9NPd=)#@R zL^A9LQE-4f@&}in_Yfi3nP1TBiw;(cDY(*?RMhB;n5_q>JWQ6PVDOt7*AbZNaW=ic zB@G=~TxE^guPlQ>D+R!=UMBdupS3PgUI#69t;;nYX3_6SnhS+jg06^hm^~f`P~{4H z1l28Rwu$sID4$&EFwfy|Q1|*s$M)I@hj9o4hGGQMnnXw*jbrmpV4*?*bb7NwqVJAd z%ySHKgj8oZbYw+8FS2c}aDjpEUw!M#l$fXZ7z^8aOR1Uctc1m=1(vB#Kte*$b*-;< zY$iF-F*lv;m}IKN{e3kmR>tGF^T%P#2n$XwKIJ+K`Znt>O#tLeOzWQN64KV9$|iq& zaP&TcPgVzZ7ICasw9aZlF>lD+cXOX0Y2ptT_LkA$nxse=8IvwG6tqA2DTM1>LPB7F z3Me9o*GEab7xi?x++hC$#AWo$*wlnaAK54MF_e?`hZON|Sk8xN;Un8-&D!@kd8Nfs zFPv^yTPHMDC?YmH11KE;JfhcN6ry6!w~^jk;+CGuVcR{HJ&N%!Ie{kp-6_<;i25_L z^g|Yf8CuMiJnA&f?#ri0t+de?f+Km0+8!H)6&$hDs%Lh~0cRenh z;NuPKuI7=*ZhCZ9y*dT(9f}1e_jeMued^gvQnKA$2f^a+E~rH+nTt47y5^;kq(a^1 z4{fhtA^J;lqC~>m%>lDE@Pz* zt+=_yK1~eAtCetIBQjr>)@njR{-%pyiBa-t6q$++_Y)QAq=O0zYCIZyEQC*FB7>q1 zrZFlUR@~A79YXpWdT0E(9+i=-k7}bB;R#j{%V$w>8UQlR#F zH%QKsltf(m9ai2QL3%^5Ok8-ebiW!y_OQyl*ad~$4*2%;z)gC8Jfo1h>@~;R2l?US zRpynTY1<^0kKZG^;;*=+rGclkQxZVhHc#!rLnTnvv=Sr1-YZe%_>JaZ`-_Ki`D zKrFNV1ow_J3^w-(YiQ%@AP}A{p>dtiyAOcn=EqD-!2ggXQ5)VNc2e7F{b`Xwi=E2~ z18{y+n8iLl8@@xFm{0bC%*1c@oa3Ga`ubu#cDmB2j`geCahyY)6r-zejTDuG#ADac z2)^3M*Xtbz9`jcSdG;?>3NE3(`Ex((wAo4Y0xjGZ7Q8-CNBlWI9xmZcIu2DN5Prx zL{<7m4xr)}GRHCGbYeehyM9-aaxyx=V*Ba5bo;OrPB#2| zsY#KXT!cOAnYr3tRTu)|VYT8@5!e%Lm*Xx#uK)Pm2>s z#N>IIsyrM#?=Ew~Q;`tOAh8Mi0k-9SHPP{$hk*hdkFXc{%H+6WQ)n`DYnh5!PP+PL z{0t8Dhu-nr-CjU4sAE}XGAHk9+$INr-p_+Qj)GAGOo{vF8}!@Z+3SW?EeXFjNJ)O& z|CN)9BH(I4l*^zRFSwv^UALjXF40m~gmHhZ8Sg6XrJ1=y817P%wI-Y$hrpA;cAY1S z)iu~BA=4bqaFypp4+Ng@70zsRT3UwH`?!U-36QTPzle3&VOl*a(1NEJDdcO?iWywc z$$mWx&*sU|qHskr2ggKGMkGC4mDsdeLyUy}dy+j|Mjia?;yOD@sIXYr;M{uJ_lL(A z(h|bsigqKl2SCnI7q0a(&0uG~hSo|_l&%1a*+Se=o#rQn<%%t2?;jdYgCt%u-EP7^ zAStMWuw-Py7$R_6l$w3Cg2IR=8NT~PV7*4$klfY2*$Id!vK|;n20|YmZ3ywtUF1nB zvs(@}G<9)2Na!Y_I{9<_aMFgC_8;hlNZ)T= zLB_Z6ShY#|zSI7q!zFA=0_r*r%~u_|V(ZG82MDLwV;A2X%!!J}jn^o`r)no(r@7e& zxVB@dAQ4%M=%k)II?MRz*kgU8c3w@CUsx-Pmo8%j@mbDtPx8vwP(q)TcTS4a$R#c> z=m$GCGi$Yrdgtv=al9iw=`;>!gZp+6Z8a-TwF|C5+ME$HOqiU8N zix85V{MP1UX7>1~IUZGjzA0J5A#cB`E$ZX={2I=Ad{{4$$B474-D=Z0rqe7vz_e-$ z?3~c9A#$@AJZ{zt*^;B=wBG>Eklcd}-XP(ALbgn?8c>z(*-=3h`Ma5VLTHF}nAk-?e73GK4!v@UbxfAquGLGPeP6A6Co>BueUD!gSa6 zVAOIwkwsgCC8VJ~H$m)kSW!e#wHJ{A6Ew6|wc{U|UOS)c^1$)+219O;%R&(L$~-70 z{!X?ug@)DFBMc~tjV|o(UZmg3i)0U6U&S=pGn#M1=X9zKlY&-&CQ}ygMklWaEbGj6 z7Kepm&Y|v$Cf(V7d{Y`sO`zcZ)ZcApH=q94pTZDW5+`mcc-@mUDjSdG4-8R7khhDL z!SIgecNR%Zy$M1dQ_;{Jbcq3*J^t?8KST?7=rH(gbT06dTHdEaOs~DzNCiGiOtSep z64%k9a^R|e7lqaMN&>4s#FJcH4-uGs5{xQ|(BU=7lR^azI#J7!{x#vv=TqxlimHc4 z-y|+Z@mxX1uA(}Q5B4ZaE!~_-GMB5Cz`o+_YvC(*(zV#zIwy2tY!!wIlO@bAY>{BM z6wafFIt0lGcdsO6pFezXu+q0#5cFYpEq|{L-+$6T%q$$nXqbyQ-_!thQ#_~yxYVoe z(1$W#cDs$j8!g~I{M>4$`Zmj^|B1stTp4%sPYYJ6RD)oaZ_xK`j9RXz3bcn-DC(w9 z*-A1uB&4Jd&lf~Ullr~Ok2AR_!ba2Y!u8|zj@4l|X!PZ!*d{bH2rbeC+n#({ob5ol z(dD^Mnhqla*To(9QFfpfLKgFbOa<7ji~2ta;NYpEsCeRmCWZn_dc9u2-Qa|DU5c3gib$< zW=xClsQv$m!fI)>oT(Ls}!VRoC4Q_TVr1~cX0YNvsTy>qpf~HvskBB-7h;P;$ z_<^kDWVg9Dr!Z?)`=OWn2t*nR%OpviMd?d~h|;mh9eg^S>$UpHVy#6r$Bi_hOFw-= z@S>|yc4^I>Ug8=KRJ+ytrO0WsSz^?FGE-n*vrCb=fZ1lHfdwP)q8xe&7mdSiU8k3E zqhND*Ctt1fbWT}b4Su9D<@OAI;yOLMd2fCuJVgEj{H&8ne>l#&CTN5mf!QiU7|4Y6 zsfeb7e=dHnoRv3XvYfzUA+4M||4!pd+4%1ETikk&=}p`Ab5ns6dxG(HKuJO1I3z7H z3ZD>U%2U7Z?Ane*gz!pq8zT=3BiF_AgR;;3g%C&PP&sIxiY`UK>udd8RNL}nJ z$A;J&ZgBUQhoUqr0^L)J@bu1px=@#aEyj=w)rWx7sMcDwivsiOmU=}Zc--E+LSo(H zW=EXxn`>j3G4$&%ZQpCsE@(qdyfWQ@Uh1b%}QCo%sE`C@f z;V3U0vZ8%xo7G!%i3hH826XZ=m6?McK2G0YKdma+GY_whwqLIrsil$;Nzuw({!?u` zD#$P>WJ1si!pH&DJG10*%%}127cGzR@0-QCLFd>8 zk4R2xevo_Pb@r;I8F{E}SL> z76Apeq|#??B8%~zCl!44F^%vhw23#pn5J>1ZtQUAs32K{RgO}%sUfPf^4Bf32cHLv zZnZ}lxCe{lF6>QM!_cU#4b#l$xxQiE&EaCg@x4a820HXI)I}ZN%FXdl94X1o-;%Ob zx4%%k7E$Pz0C1627hD`Hso#otTJMP2seg-n&^9V24Jo>(d#F+pt?=wd9I3@s!qE{X zqbj?!n-klw*XAX(A?O6J&?GPD4!*Q!);u9b0AR&Nj>-ZXNXs{Tg-Mjrja-ENXhgFP z>3!Z#xafCd)a&W%^rp9=x>B(9rso9NhapwyzqJHJdKhv_q~rw zCUvn+{@jn8UR99p@&gyMKv8I7g9@kK*6r!WucU*Iy|q$8e5whQf_ivg>b5`1%Mnrf zX9sa^3z_UUJDfT~ebn_1@A2$l46msQ$Y`!{m|K@Gx&YN`qBT=8qEG!{5PX$*ZB<)W zaOqWT4bzqJhM;s~R_+4&X(4Be$u0(EcoF4T&1?KJ$aVB9Sr;EF(TAT-7wk3$+P|F- zkzxT*Yh=~gWio+oF4iY8#aVbYn^}NUh$qH~Z>$Vtj&h@kDH1=jqJVN&9dEv3IkBH~ znuO))xh$Nc$MTT25s-(KE*a;Jlv4fuv36l`Ytw^K2$Ns54kmPczBJjcX*t!Tq@?8E zt45WkV_A)TaBuaxw-uo56tzze@0~})qQWQZAKvli-2)neYR(YYA+I4kAU0h?UeijR zwE^FE!JrX#vx)1k>ia&be)vG#1$p0f$fUicq-iwq~nE#>YXSS6WivhuA<$qA|T7 zvsj^p4@N$x^?nGctT6(2Md$-OB+z7IBO|P-+^9_HSN_1{bNgg;rkAG;QVZB@A;K&X z0MXoOc@{4LOhJI(>vaTg;u|Dgoxi*zvZa3>n2kXw2XW6`%>yG!Yw#f{Or|V};U9=T zbEX|5_1_i-25$Jef{@(Ne5!CZhzZrck&FxrCaAcmwDM ztf>>urJ%_IMFY6yIoMu#yb}b6S5&ka&%cJiuUEnd|z00eV4&z5ox=J}JsG z3h$o&e5s654GnyKI6#WiB<%n!c+;Kx#r?_n=-*leOk!CxyuW6sCevUwAqygakW)I= zO}gKjp!Q00I*-;01x*0W{D{Z6U9+NVnYio54CnmbdD6CNVaKw7ChNC}eE4HpAdus( zE0Js30Jvf8`w<#7t2eP>^SfJe)7T(mma}NlLf=lJk$~R=ez$^zLQ`Iy!BSTuWH!5r z6fB2j5WZ&3oasCAW*He$r*1tJ(DJ22#}2-E*o@CmbE;U(gncV0Ly3-nMnIxT(M z03zP!7$Fg}qqEN*VDi_hRZDKV=|;mFH&EifE(zl-34P!>+r4{FLXJ_m=Kk*RSX%9dAi0s+3*R0)G%c6M&K=JtFzc*p8V=YHM01JLQlD#g_D7p34 zJ2Z^;OR{B)?om~{3y_0nlK%3EMitNZkRe|v!1rrk&Ae!1LDR!aj0Hd_5ogcz<#)eh z4)&P-d7m?PTl5_;1#QcP3m3`HfBp;e>CO}F8jucf=SXT4Cbk2JHj<-sf0dr%|KiPg zGDNiBoeVX?boih@hjq&@2hDW z;}X!lb)da4ZsE2t%>i#7?|0V9*V-^^niY)Z2Lo(q}bn z@*mWZD<(YnZHY$k2Yz0p<{&||Q?-enR=+FT_vGsH78_sObp;8t?mUsMG5^A0ZYA|? zQxfUxX`75spIFxnV*sH3Lf?JsRI4e7+9*(oiIR5>2jwrX>BtFdws5Lbm;>;xnp$vT z$`Z>nShxoN6zZy{K1HrM<)ANEgHza{Q`2P1Vt{}DI*O7CDAHWREaRBg45nn?mm&ZAaRGMQYplGg-b|)~#LVo2^aTHriguPeHrvY-9T2 zmvOQ>7)|$6!8w%K&qsn&&*VuHW&MUt<|LNQqVvant{EHUIqgn#oK9P%`IEWAQRA{@ zOMQm{z*W0;Epw{ftXZ=_e+QE%PcnSumUlB0dTng{s-izpMElcf|oz;Dp;EU9QgLY)F@u6Uq5sp>MjTWalo6M z-OimH^%|h(nP>iF-Zhc>>{k(UjTzF@Pd^j)I>M|9A2r^0oL@W1NGpb|SEyT@N~1o1 z_;usyu1+@ED6aQWSO$EKa2o7ao`@U>=5FzglUaN?aC*(Ds~=Gx^}5SZKR`*m;EG@U zWj&3P>J18*;IQ)s4S(xLC#i`J;6o6>`>F~&oUB?1egtSi)RYSBk`o~27rzZU)o~H) z5CI-gXwXUiW6P+g4vZ8yz~m?0wQw~Mope>RhoChydj3|ynP)5-15T$IKH4^Z#5`aI zgfEa-06t^ZqWt*pgbgxU{kL4zuZeN!>-L@D*N!_4KOi{nee~nSGIjZmhyV~TuLdeq-l4jNW~r%H#eiqk-FC9CStI}cX`S&!kpNzk?Cun*4+p_t>Kb-$)(U0vNl?WY zHKodtA5=@}iaw2fa)`97Us=P(RxrMuO1raG-vK5ek!Npe37JQ=(BH;wl7hilKJ&!-mL zYwF2%+cs^4NFryPd6ub*Q}U8d2%uge6kaeUKVC%}4`tV^Ali~vb8<+HKev; z_3GA_mMvQvCy(CD^XJV|?IQi1Qq}(>wVC$zp`?6|=N*B-)V6I~dGg8M8-Rf^Jo@PG z#%LDxsiT5zAfckS9$CQqJ{ zH1m*m-g(!UlCdUZrBt32F)jkBg!skmxLJ$@+726vyOC1t+SErSJr^xKZ05F#6srR1! z3aofGXMAs~C!(C{`<;04lH;CR2>op02ICyl4=`a-9MEI5x@vNe)v$^&ZNNvsZawYZ zrj8(S5=%H;1E_dcP%%$HX{(+Ga;TotU#)_-;}^j_h90eInaCsfsPI#dJ*Wa<7hMv3?LZS8P(r#nnB8iQ`)P`+1=*{ErYW zHXZ^L#1{`HD1Kl}R>P`sZ3cO!;OQ3ir7_3^*Zkh)WK^a zz#)J#zK5>p95MNVVCFniW4ovOl<#Ri?QmuwUf@=YjP)nMS%%uAT}Y}~lXoN9%i zs^VO;l62M+`Hw#O&|A!Wrw=PuEcdworNcq>lURQykxBTU$myLaQNd`_B6aFy} zD%}7HAj+s8e>5uZ&e*)4UZBOfPx|7;i%q>wn)|auhmJZ|M1#@&T)jF5G?}8|puMzQ zHH%iSUS&>9SzDH3M& z4{`p@^BNCm0&qnX9-@Hh?&)xa#dH;$w3I;Occ0xt?#gfBTarB>LgcOg(D>fE!FLNU zDo>eH?Eh0otjA{7@C8k%lsX)t903#U2Io8X9nxn~(?tgLYbKZUY$%y3Y`Qn02+Dva zIIjI})@B)@e`v^9^5+2BLG#w^GFlG3)whP12;*|Sq~->i4iU~cwD}H6GOQmWFaNFI_ZjaXcsc)Jr&7zz#nmEUq7z;+NN*Mj8Hy#`3$57RkG#s%k`}Msk)qvBe2; zG{OYyDUH%{*Bf(;rU)h>egtR|M3@LISK!HWzz~!@>FHCJZ8u?=-S3^GH&TQO7i&}} z)?g%OtU=W?x0qw`PV^fe>j^Ncu=p5MWBE&mM`&8?S6R#N@q>3&RU1Qb_hR8Ara_%r z|CjVVyQr(}O;GdyK# zVZoYS*Q;002!Xkp$JwwWKus|lu<&uQDaGL-8YSVNiOzuvUT2(frab%1GZDdp@EZNU zjUPWwZc_7zuhh{WJ|qqnHKw(HGeu4%0>uRvoTmq4huCK$dd*bEjhkS6 zgCH0I8kQD92s`n_6V=S<+lXCeT%4@qU&EoLOP8q+q{W4Vz<7A+)KwlnSvP2n#7hSc z=7haVdA4~c^A``XX8-rUuT*x3zYk73yn6s$BAH-1dEK9AI=How-1p+WCh82@8eZ+2 zX!S5|{PqBB#six8t!Kue5Z{jbt;*^ ze%ZCrU@d#xQQ`gZd5hEHwHi4 zMAb<)Rjq{M;*f8ONY7DFyN*CTU$ritQr{x)k69}h9^X(dRsf}56!4^;YTG=1X&bps z9n5Z3jUqRAgRDi}m@x&>!gYI%^VcB2C8%7o(g7vVDsy;{ta_7G586PyetceMQJdh6 zg})f0IaLTirC7sk5d0y5s7;jl4DgUhF+pHN=$ZGAFp~`C09wplkt#D))5H&x#4Lx% zKA(?Ems`$KTk^uPXF}ZwJ-Z5=R0I}=3*kfxZE)Jn;<>;X?0-J_q)>L81rmDL&si)d^xe93mtXwimui>&WTCS0Cd`>TN8Wws zZF%sa|I-a*e153nZ7b3Ij2R7{cnR~TOx=`wfhVV)>C>h4Tia?A3Ks#Puu359z z92Pk9m}nOOfADFwW5-Uj4q~kbGrV|#rdaFt=1Uxw^VSsScYQV@QndT-!h~`4(+;5t zOcDkh8@teo#=ylr8~JKNxZ5nx*O%I8(qK+TEuiEa+ztRa3CSO{~)@3Yb*!#t3z1Du!#?EDE^f zbP3=9XQTLOdGYJzGU(Nrf~i}y$TaQL@$Iz=Mi$Da`~XCW-ns z|29qi(**Yc;N-J$X|i}7j>}`IIkr5!J97@IkQI;}AP+1q(e)WnZLuY@k;0z|8!`YuobVTQBkDKqqnUQr zS!ac`dz4>pg0E<=ELpPDom_C8nb}e!Rn$5J_=bddOsj%UByk37DRvGZyk@H-WOqaj z%oRBK<@A|niRc&zb)|(E4}Ncg06L!cTD5A*@#=5LO=HH4aepni=2&>gQC@k5z2^I8iqC`x(X5csZ;V zVS-T3LuYJ}>rQA=hyzX3tmGJVjEcFF*F@(;brkD&!p|IynZK>bK!D#mDAd7#1j?o< zAmrAZ@MZt&giacg;Tjo*wQ+P>%T$wE+`;z|ch!1N_ZC@ijak7T`oX^}Q|-ZgwA`&Js- z;&->A`t|@gQKgDPYXx9qhx+yaI7zmN4pDJF{V7c*EbuqWarmc?m;qHj{2@&qxJ;iX zy-}2RAN5k_jy)-|NB6Ld(M#|JEtul*1(_mM^t3WHKs$=QAgm$S#Y1eQmMmT(+qP-E zW*=(Zy0vb2({`$>*Pa8W`qs!-nhuZ~sdAcYKok=79%% zg;?flh58~{yLPSbFj}_ERM4b`Zm2^-JN#}?zrLUzziipELS@hj!ufBIDNvyTWiWv? z8tJ#dJ!IAxz69JSwzFr?R>!<+O&XxqObrD+ZrpfvSbUxs* z+;>!>skr+H01`=qgHXV3VjGMJSfrDEcm8Bu`vFhMsGG6#6n#EvgKXJ#K%T!gTO(!o zzb2T^z8X4u(fYMvpb4f?ksSGkK&isdYBKsCtq$gCznWi_)y)sjSrwJ!({vt#Xoot# zF011Y8gsnqm2}AmX>jQB){x+;ZK4iQt5sHpCuu3G%qUA#bL0aBLTDozVG%fddYYWp zEl+y3%TvKTT8YT_X>+HJ3tujN#4vG0>+`zz%N|!6o1WJP4>rzE9RR~!L6s_%Q^aC;| z&jzwdlO}TMWtZtjE+zEaKu0{3uBcqorcLDcPdwqd6Z*P;{{iC|&-Z32N`%kM?-^&F zDZl^yAN1ru)9}bU1U9`(RV!%ME>*Gep*9-Tg+mR8;B_qxHQ6#V(-$-Wd;x^%qR}l7 z?8+;_SLPs^K!K(J;XBDlwRFfECB8hHsb2{(8|Va5+HEroLcKK1C)BRnz2Z#Zmrzeic4rH#kKQ--TG-agoLy>pg#IDAv*?Bws5v_oz`NO}45p0QYGQ-9re(*({DLe>tF~^bZi) zOUO`4O*i1{T#Z07ZP`vazI9=Tu%zKMlsQ32#^j|}i&`e+sdtbFvL;M%41$8UY%;&I ztz{hrMTMcQUFrqV-A(-+HBOE9s#%X;v>2k9d_8rOtkZ}nku+H183!lAD>v^qzFWdN z6Hg@(@-jZmjYesBVgSc`&&>?X-CIGEkH)Pxa-+i+KqLh7AgZZaC_XYoPc< zRXNWK`5ZFu9@PqYS3?kY&CD|(kCV^I6VE600h$Cw4T2X{)!#|7wQJ}j;6jTQEsc4= zfdhq6o65?Nnl*zXlm(3wM}t9*MG@q26zbMqVFO=ZL6a6OTKIw{gto*{U!z8i!#4Zg z_Nm7$40!MJJvFt88MEI#(hwl{V>4J12@Q@buewUAR;enZMvs!IK2dDwcgKz$W$+ts z8DB7*-UppZ0&HLk;z#hHgNBAC`kq7NuTi7Apc(YT$dOKR!6({9iWMniGc$^5K0pxzaam_os zG?AZ1QfcbHx397Mc6rOV=g@oaNPOov{75~QWdbz8F=bDUqVtMYV}|-=@_Wu;BK}hNOSpk@FcZ3bH7vc__No@i z=ADISf;EfZj~#nNn#3O6FCynK%lT?zs*IeWW;s592_OpnQfxMC>M)g=wm4OWPfL}7 zz4BE1C1NuXZvqDG(x^fBR4F`aeXh0r`ZaNqjW8dvs)TN`Swl6~4PrPIREVf7BSs8&nlJdo)_VTD`6?vzEJ#cAg7M^+VhxDe zs^#(Ab1z7(n$_jg&pwySF1^GTDB{!}b#F8of&deE2naGQAj(lFq;nUeY2sGpXU%r$ z(lu}j0L(7F_>#c;<>3a;0ia~0rN%SYrArqB7&<&zwQ9AXb>S5X0UQ7N*T0z%y%%11 zp_9GDer9E5Y6#mlGG)p%U5AJFn5;M60mmG3OjzH?jvZSFTp>e!s&ONY86bV zVLu*xJAI=G6I@wMV~R?&gTTn1-f!a{seXWnc8&d=)KYHE*8Q?+=Yg=GN|Yg538(jd z#M*39yP^SvB5APrl|9;Nj18x)tG4V91WoEx)y=T3g9C@cV#0I$2;posh_aQ(_&hOM zNHEA!7zgTBPfr+30!VAyxLR0!?k_7e95E55m|IK!8~_j?dRD_)`LbN2)cAQT;I>R0B)9f}3s}Bq>Ja z{_s3fZl0ci1`!TMuetWxpxfmpoeeILiP$jr?b}aHo!Y7Ih`EKvkCVW+-h5M@d;a-C z_nZVa>o|p6uyCQ16!u9}hzEZ2YpJMhp#kAgT56in{Bgc}Kd0zi0mhYgN>x7P=Lef* zBJ}m^*C&ik`1L>#b%Ru7V6Jl17VyqHf1%ITM!Eg=Tf@qgv@Zx@J$oK66DQ&rI2@5? z4Z8(x91+i$F*D3u#~BX-8DqB;;s9opty^cw4L4ku5K9&crul_F0tIKVtWyqhtSZ2&C{N8P(A-M9BJ7Mv5 z4K2Jw-u|(QT-v*_TzFhVU-OFM>v8F?nNyzi>cDl_%*}F5zrqe-jfswG0RgJ?&#ob8 zpLhwudE}g}Mr*=*FOl!98&y$(CoS&NbbVHZ$TMw(~dnX!1?EloXRuCyuCF%T~=-^Y?cE!(WYs>Tydg?kIe;!p)`mh5-*m=7~34j#pd+@=Bbl#TA{rBG&rlsQ3jiZ``Uz68f`$t$G z5ORqQ^z_rusHxEd`rb>GXPA{H0laV8w5gh1)`+=nGOk{|`iMQ3{=e{Xb+_bXzV?Ksa{KAcwL|&3sSAFw zu_pvP!TbPW_vz1;$jyJBEk7;r_po1bMp*KCf`Q1l(>KWhAAS?QnGnOs+^2M^>wCIA zcHuS?vBe#Ir6{9ptO9wL7S**s@d-2J@0M9pvJ`X)qB*N}8*L@;eb&>oWjnmd1>bwA z=@|Z9B7qMDnUio4Y3;{TJsLvQE2T-}2o97tDj?s0jO9BEr8{lwds(jN)7XGd zcK_jUHoWjA!ev6djXl*7!^>eZiMW+h%#{_&U~FkA6PP3*y-T$I{~!E0)We)oTi= zmB0GRD{7*0nOt?{RbhXZUUI3t`NkUsLRTzpl(sxkg{X0|MdFQ=A z0hl#vDHO#F$G}fN{iK|7%1H^agorucemhLpS2#~`=(%arCKcK@#Z_-xbrM~zT1Yzq zp*_(*o`sBzW*TL_LAKw#m*MRBzbXoaSs-(f;L74bWQf1IFqRZv|E z6&#PZOi*3X(#wpd2?Is&zI#9mY1gExygF>9EL^w8XfQ+)$Pw`Po4snM-1F8v`Gq=* z^$VjLjgjT`e!2orb5syRNJe1u(n%U(c$U&6-3D zCQ%CUiO`@b;rI|{F=ri9-}LdzsT&l|3axFQtT814hV;-mZKi%pf@tH891}fAn<}6* z?H^_!i`Va!4$b_vZ+f?_CFl2OAfJq1uUZv8Qd&W6{ul5fh!6fHN>3_)?iIR*C+l+) z&l;|#0M{U0!$Ex6rM^r?Ygli;H5w0h8q}yzq|=0W+B#Z!c_p=Jl4s0a_GpAtFEGMh z57V7x`gcrLzR_Ncx37Q`8+WA0zlNq6a~IG4QPoXF1{{~Cez??EiMj~lu|KOvt_+)& zCNr1n>6Yu)oBnN7)0%((s3C|i4fZD#)JHvtmN_d@OnO@~p5#nCP zUo&o=aRBJ<-MhKJV)nsbj)SE8ChFsm<$*& zzn#Fctz|zlEfqxL7jBou`pmg)xP$UW(u!)TAex^xLQq|$FRP`ebq$T4QAdL9kjVqpEo=%-MHLz?^ZcLjx zwE)~IN|C!c&;g})o*mRoL>d++^~5smTpWy?gf=vlleP@C);&KRqMwz4vyJ027{dw3346f^^;!@epeg zu#T}MN&qG}2JYOsvsufmnOOet*+g0Ivyx~(j&?cT1(={>Iu#EM9C)rAdu-Q0Gr(wd zvDTQw4jtM@%Nlln(V|7NVnw)boUjZ@{^B~TN^;Fdxw^XQOXJp4GgR%uwL{2X4o}ehKtt&Mftf-$?;rvg!O^duW&>+t=!}rB8+>&55x~du*JaBy>WAom zQ#a}lkNQiXZ~VCmxOqX}F4Orsy-PiL;JpR1TOBb(nm1Q9rRow;>HdLP22dg@7~lDG zkJXtkcy)FECUVCa!2lQY%R1owrfz1V@?z)SU~sQU#(yx3d&;5|ji`mWX9(1YbU+aGIVNTaAc?UDF3NkQfQjhfu>47Z$G1se-wM z7nq;B`{Ud%X`cu@x6Jn`Qzz^DGIk#O zuOQ#5OKl<=2q&F%vSemu%Ce=)3XS7{3QO<5|DK-Wo)&fwVFQhjc|J~L$;c@tp`p{# z*sk()gcIerZ{ML#C6|TYIP`6*M&v1~+PH7Ur7RIpD@dq>hw$j@ufNuHt$Pj?lHt3T z5Dv4lGL6=TbC3LM*RC`9X@ftsyzq&FW(Rc_=RRnwd(6E3<&jhcGDf~M7$ zOD?&{3>6$0yZxjTG#ufAd1cLB zcilA_dOHpes9NVzeb@2q(C@N9jAPldD{}h+;`tSLD>S+zx!@PA>TNGnUip%Y|L^5M$}hT^2>1as^G~4HQD&L zYLSdqGYf=E%yPVhV6tWR0r}vkb@JGyt-ZJHcOMS$+q!<+ei=4viwf(F!*bx5^2{#v z!_xSI^VIJ&zuz1b%A7z#RW*b8-6gHnzex>wf6Q9-o3c0L9QIIMm@>3(R857=kP~1( z!QW|ijG8ojH%q<9D|it^fCK;(GZmP_DX||ls#Qr|xURkYSHTqkmZj=X2Q3{VU>;hE z{3i2PMS-M->Idbx)-~mV9t{l`!`jRXI`;NykWk!cD_{nLR?mC|PNt~ul?7|}sE~Bf zXkh7Mpg={)#}C>-omCj_tByo5wW7U9G;+=sGX}poq$mzY|2blnf<^jc8&DO|j-I-r zjSNs=?ce&$0q)=&7;uv!Y*8w!U=A3E7RBX#8p-jkY6%Wa(#buX^P)dE&A* z3gFh%_tsi3a{rI2KyJU2ReV z2$1tP=Vi%LpD&Sl8ZI2|DK?Zm>&LaPEemuGasC|XnF^@7#3i55DObLp=+gy!L>matIH1fK-SU)wDj2$+5423sayd)u)F2gML^%MLoBzMP^MJFe zy7vE?LYn~v=^!wJ0@9Q!2-sp2B~K;x=llFE7&Sg$H1EB{mROQJlNe)&1$)J=C`~{l zHWWm9M-Yag_mR#F)BfwbuN&^%Ip_9a+TI^7=gv9%?7h$L>~ro}zq8hFT~MS1ZhByQ z$i6)~L~qQ~IPHD0I8Aa@4@J3#a)mxKl`I+39gnqgo2%ps9{fmWi81a~5WRf+o%f6p z!109^9{4`|@I#YbCv+QsFVZ)0eg>#leZPy7>n54SuRVJ4+m@{UF&x^pYiGO+o_Ttd zXR}i#O)}>N4FcXcQ#e;^*ZPaVW@l&1q{$x}u~ILimHij0m&&kXN@tsD(d`n@7~RbOd=W?i`NL`5k!#J+^eXuRz$myMuP^G75nw;r=so^8#Rsf z$dQkSy%#Z0b8~a$^UuGKOD?(CZ;0jABSiZ5zyD2s^{YEI)Nh%|q~b;V1@Fi?wfCVL zZ@fWnx#i~I%~5@yJ9lo_20qvk?pB<$kP7liR<;%@=#PH)KmdEr5nd_}J@jDAsjCqTo-}E4R79W|L=yh1@uZ2xh&cYiT^bvMkhkL_C+8Xg zQm^>_bUld+)VRb8qH$)efyhD?*Am}=sS9_=m8#uA3fkb}WmM1Ax72I49n^~|xetwy zF%Tj=i{iC##ekMFtXDJDtT`ZCe#keejWO<8tIkMwePbR;Ph+o=3y)7(rvyWB*f1iU zGD6))C$5!IBf1%bHW&l@o&K)XOhcDywx(rDu&ms|xC&&irY+{C(pbj^LC{Il@}8`p zP|qFPPAp5v55lDQ&@YAX8$3^n)Ix&{BJ>BMCfc$=HA|kFx;|`m+C{UbYuko${aI~% z>tbA=qn59hZ@2!mqliW=uFTk2ZeLj89Lf_ne>R4s}AyQq_10R}fo%;pXmoUF}zbq(IYf zr$`44=OR38#4}4}t7g>-CIZ5>RaLi3)D3$xJE)?Ui5ONy?JG@E)#zomC8CO=zQhDItHO4&TGN2g^zb{9&lx^YY zU);CQLV5q|z&$f$7^Wiq^~frf&TXrQo(Y5Ijc7O#*lyi>mIxA~gh3t)X+qst>L)zV z3Tz`W&YL5k)JNe|3Wc27)0%5x(j*ue|L+LGE=3Sl z4ZFVC{<;dbg-Hu{8gHL^A=umP@h%AyI;ckHbeYi#-P9hoX zdViErpR8AHq59^!3)e2&PSlJ`d}?#~Ov4G8Z5OMDku78~A`4Tj1x=erKgu& zjizw_TejhVY*Lc3Z4>`(6=%I{P_Yz5Nt)0O?Ux|%8rrD-9pYe8t_Gxwav7fc#t=1Pv`;2}tC>rb6e7gP zt1!-UdMYWT-nIzRR`tRIGT9lHE?wd~)XwVZz{494&Rzw>vPFyL=FK!lR%ju^!VQ872J6Wc9GP>#pCL`sTq=nJrRlzNqAaU*W;hzNxo$=%;>$ceo>UUV16e zUJK2HVhILbXdsaI*{7e{X38xaWcZK0T;(t;z^zxWUaEgEK*KY>@~rvxJ8#J?xBgN? zGa4HF8Rv=>%RTF|vP+jP#&9Yq@YX9}f3QDA$rUfs1h;#jvC$F(taibvUl58kiZJR@Y*tl6?ib*<36fRyF_ z#5Djhhfwi=0RxOaOT0$+JL;{-Z-F0S7}LP3P-TRV_wPTTnnaJwbI(2FOLqDV8G`cW zn{SdEZ@k{51KzMmhP^<2fhF{qKEW);C%Y31S78bKb_g5*-3O_~+{z@JXs26Nxlgr$p84)rn{E@ERH6 zE00ZDCmmHsqj#2f`a`e%Lf2s^JThUeygzGeInSBUy1{=#BD_6wn?9JN$bYM@N~qUO zaNT&mW6*{x6&(^mSj-<)mjykGNQ4RFWIPBlbDjNgAj^3Egd2af+qgz+YC$^(@1~Pl zWeT1o@fJKw9v+*cS|&U6TtyEUb8M@I=ZyJwyWFW-Ke3dW_E-ryaenV&`Djk6hP3J1 zQV$rJiyzELle2r0ZKu>TEqOQ$6TvKzeF@9ixHCm2eybvfuyh`U<42J8EDg;;9Je|Y z3>?C8mMmKAIb7ZvcgV^P8|uhIJ~90#opiE!GacFx9t$g$uTZ7`i_9k4Y}3Zyn+`YP ze~$b|Xft8|KK}UQ)krZy&G9qu zz>GQON_+@#Pk7{!N7V!5^_aXy@IWJ-b^reTjGjuYy|-=K*68G*l|x}1%wf(cb3K7;$47=E7jL;XF8?I$JH-fLccqwAr%2?)Z5D+u$Fsx}#-Qlb4| z@a;WRDEGd-N@l3nNjzoi?!)qc8u{qjq^mU%%d-V_F|1yjzFF>l=X=?rS}(~eq;^J| z#E&>(zM@4@5o8~2A|!g-%gbfv>R!3$}0HP9LCsPN?pMk(8O4p`^)4o^7xap*)N)3V8PG*FPx_13Tx9gOiDsUNVya7>1Q9jULHx*f9ko_p?<=bwMUkaf`z zRTc}#4FpB+y6aw}No3c@yW~7V7$)OfXs2BN^ip|V#ZV#k;wfaP zA>`-&_gBj=o?oUSt_OUvg8-bOeKLNo1w>U5d`+YfrZ#rYHn~aL{QbMDWpiFZR30Xw zeL|1{<*Q02T{mipW&zq97kR=t8n5T(4`bGNUc2-gZtjCoqGVmTxr;7LJaZ-#FT&mM z$hz~j?+gKl{!BDwkK%)%?uciXsh-RZ^>m7V4s&mOK<6Do%25+>YKYe~fnK<|O9rUU zj7CN1PlIQHI3a7z)Ol%oeRx)>QY#tb81;^sJexPE-b2GqZP_>=NE^Xs^=_b&*7AmO z@sh6_wO`4pbSv(+32ypT*Jm8bH#jh+{w9usEq#i`UkTcIxy@xaXs>ie*50mS! zzwrnuV|PBRYQvWfA1+y0S$0*zOU6u=1V4S3SUUh$-Y#Bn!BF2_znVT>7R;Y--aO+v zzkT~Q&$^kJnUa;ARSt*H%8)7$Ts-7^l{G2`yL0Cby*KkKRJ#ktD8^%~h5d#MDNERW z^UW-CoyChQIv1q2J!G*r%#`#=DNknl9ti5@9G>yfUuYwZn#O_ zdvA;(pW>M#5PS<4E>f+V|5kmHoktt5#BUHmcinZ5pyk8(YI1>KBGmP_zrEXdom7Qm zg@AkV$x)Stz4tFnRX`?&F_<^ADt7pfNx9~2_|N(@*NiPqrB&m4o<8 z1|X?V+o_;o8x;$!js>GIPx1JxX1A#4qs4)MV?1x5BjLfr^@>gVeXA|X-Diw>FL7Ak zo#NO)`vn^2fB@!)v$xB+-A^z=z$cziPenFWMP4x?L(5(@M%QY{$;?%|1#wyoRUWG# zJrXS#=#WTHXsE?l&6d+kv%mDoZe+rjj&Go;`P87rJ4a117j1?tpd+ zA2LcYZ*n!$5Se`zt=p&BfcB}7XpzZcljwZmc@<37sTRuLH1rAM{@`{{*sfmVAsmS7o4M~L@=N&E20gw=ID(~D0kiKl z`_P}q<#=s2S)tI>Nk*6y$48jpW2#B=o`#A2#LVF)(njxVjT)#|gB~mM0zxH6MU?|{ zIDn7E+H;Umt^^}UyT9tWB4ztf&2YqdLZ61Qv7I@4SjVteQxub(j0`;xmhs{n<&)h(z{RZa$>5CnY67>Kl7a2 zqA}wL5hhZ4^yq2E3^@@iY2t@3`kH??DTeVZ2`zY+y#3DGGI;PAs$;Rjc$ce%^TF z&9H4Ux4|eLLqBZ8wP546ZQJCv*It)5-+WWk2JcVU8vxN*baQ_3i<>nh<6$8SQe_p= z3;*tS_iIYznF$|PWgSDT{X#_JS#!r7zgDlIyJFUuiM2n!`h?s*roW4?y*AqXCRf(A zStCdjK;|JGgA4=si7D$1A(E}SC1_hT(F{1GEzMU0WQV@-hcH;TBVTX!0nLTblcctz zoU55U{0N88`c>_3g~Awb@6Xz%#;*5|B3M;~VN*|y-k&OAI7T`eyX@GH!!3S9oASEcs$&_Og<^Z+*SR%m+N-@J7Lt2(eeNVJ>s-!{Jk|g9s~3{|XgE93QTm zDslcj2k~uo_S`JOr1Ja@NnR5dL)8@H+n5wwv;o@7$mpedaLnTVLz|J z`t!~^PbN&9q*)?eU3WZO-?onuEy^Hj^d8+HO0*bd)Fea@qJbs zFNT-TiYQkk^(=M2IgYe}G#iM?g%*S9+5t`vWvPkYW*IL4Y9-0Sa$RX>3q#Gn6fRPWOcG5pvo!$bo_@&z?6%iX8Ardm z`c9LoG8?b$SM4Oxr}`8A7L`0n0MoGhUnG_tF0Y02k06_7oeJwA-PVj-kzyqMH^=}u)iMZYv`iI`;rf;R^BJc%CTK1HNnL~@&Mm}DPxR?M*|?m23CknfQux65)p z9!f!+A5QZsb>thyrl-oKJ**$+3_r3SqR{GLl=N%#EIHZUuTuhHrz<9oY?)MS7wd@S zMWNyGAN78s#oqjEDkRZCXc-(xTRMJxc7_$%n|Mk!sAxjI9$z<=X2``W(RLwp%=hZg zOsJ#wxC9E}8-IVAUCn)cFwO7f5uR|H7XXc$&2ul_VQOb{nl4Ia+`28x8=uE7`22LU zes$7({&}%**Beh-Mh0{;+t!0IPerDOW``8aB-$q#K^18`GOa%ex-0;o(yB;+cSmGU z(q%)#OPt3MsP-zIiHYNAkodrOVQ^(F>TWB1+NV{k3UVFEh>fiJWNUS;;b58(^0Y=d zA(dJeW<-8Uk zUmDSdo$@}(_nj?nEO#nLfa^Hi$s5m!oMLu28Ql&K1xol^zs(#qtYo&!xp6Nh8&nI= z^Sqz>##`sO0tQLnRJWBR=?xui8>TcIrvL*y538!5{j`6a6| zu)34_ahB^&yL8g0pg+qnwxE~M9u5R3C5XoSw}NN^3^LipeerHm)A_(9+9gg*5+waN ze#}c7?`vQ$inaZmu5T23A8ZqCpLo`T3u+R#HHetKuxGBl=v7`RLz6DEqajw>?M~!lB>sl(7P?C|;2n`d$M3L+$b;mn=Gs zqPvH8y3Dxz9~TyAqKn^fjL`9;fDLGfz&NKLFBi%%ldP*{w6Ii>T^wa;_U@{M;;^fP z9RB@iYeH?KE?xeyPOmowyqf2F;BCHna}Y*?X5iqFXv2P+*5N_-r=+c$JNCJmIldX@ z>dv_tyFuR%%$k=i*F)=hE9HfRUdq=I*vn2EuXYWkc8DGw0sWg5u3tM|1Kfporq^GU zDE^}H_D=a}+3ktNf8ne=m*8+xI#D=9Kbqj5%u2>M{&*53H?!2RZ(Qe*KSF~U^w;%2 zjB}SvJCosYBoxY1xs$yOY-L1+68?BKS^Tn4XUjP!_CXo#{`(VCRo_44QlYR5k}0(s zc#yA}Uz`GBJTG^3&TBzjRvm&&cdiWHPteYGxe{+4);vF3$>*<9SeX69RT)yiR0OKAqPRlh_mXX)BQ{ zCZ15%VWS%lm*Uym&-csdr_s~zz53-u^*=MaXS<%kb6IB+&C?K5V@_Npi7s$;_rcHk z)WtE2aE#nu>+a7k%dlJ$$^oKCta{xJ@z|Nu?EOA_74kMG=nV=cfBudu;(o z;jwY^TDSP*A^+@kbQ;x}+n|i?y^@4s9_gHd{Ra;{Oi!Fe*^4$#W&&2jo4+P*#BQfM z5C=JoYG(01Y8KqR3M>OvQNCYS{k&3sF;iQu+fMZ<;951y9jA5%0LD)wZGEzbxGD8JHiWDD>i^2Cwh z`vPvA)cZDZv@L6h^!<&I3*3O?2fNO_t^XkVuK}XGSknmtJl}?XH#fJ)YPL6p%h-*h z9y(HZ=i8|xZn(h{mu7of{O-l=Mt#;T+;f7Fli3&VD4lkXY7{kN!XI{`agRSpNMI>XrM5Jk**npE?(Ng< z+7r()>~!~6cOq$gCR_tzoHuG8Xj7i9O(oJ|_SC0F7DSX68`Ozhm1P$c8w}K)no4`D z7xQx!MHuC&jYl)|0D z(h2P9cZlJA-H&2MNO5qzw~b4y{_+K+TyPH-7WWoH5Og2zQdCKJAC)&nx+`Q@H18GLl6q&ld( zZ#SWMIy(zGbCVaz?Y;|Gr#7B`Us+Ezn9#G!CQcy^sLhE>w{V8B>3$X>i$6PCeLRC9 zv#kc&4?eYyV>myuIpXRiPgr(C<#c5Fyzm(hLSY3R&@r_)+3!LZJ)EbW)Ky{R));S( zu$a8bL+TP48#~EPnIACW^g+(Y&5Sc;CUDfH)R2LYj4jSUDYb<-Ez;WkywG~Kl!76n zbX7PJ4I7=em2w>=y745{s-nc6pU1#r!n4>kT;Vu#jtlH(SI}@}lOl-W9;i~RZEu2A zSr^!1=M3|b>>f^-Y6eG#Gsl_8-WbI_oF3c;B2n0~5F#m({y=NMLNl9Pu4t~%CL_Dckz4dTl@I&8sd zh$9c}T%a;cG77lW-r#(GkC-WU#yp zr`F2rH;deD(evU5q8gYopjYQ>1Cc!_V)FMavYO3DM zmiw@Se4-1dHXOQO?0WVu04BSTX;7Z{ANnhPvi zSv91qN}M;ICz;IVVwQwA=2T9wcF;E<3#b*GipX)0O1qFimqY;qcg4Hjzg}_c9`Csf z5ru3&IgPt*2lh2cA^r72(3VfbwZJ}1;%Pc-$=&G?r7}`YKD1j%#iWSKpVzG1YKPAX zt(T@HRt63SBA!t{zz?SqWPnT8d*eU-)4aQfSAt84)Amy&XEx4suelNFpz0*cU;0lSE2REn=ow$*+g`y?ylR@;p=P zh%Z=axfFg9zu=3Lz#HPdGbW*Ll7GDYjNk7ejg}!+!EP3$CkxVTAEgolF(E2Te^xk{ zM+llEavVL+#avF@Uvu6zR%S($un0#K9#*J%ZjStGl`h{Sm5c%USX7t?vim*2Jv9pEBH6?2E}+o`MN; zawgq*)W$mv-oB7O?5`x_$$DCOQR4$>25=^IY`0u-f4lr(ATFnpNEZ6BY=S0(9 z7RQ#;Kc?!i>K@WWW8C$=JPn1aUFp$qi5ScYp?RWkwOIDqH4gnRFH6l;&F<(~mrOr6 zKb%B-`2HZPHG2Q^$$-{sfJDX6W||~dHzm(V%i5MSzP$u)g7l*n^bYMMGn@fz?UGyo z)If1Smp+k`n}9;t#2Gm0n9}C4N4caU_sGp{GNni;ijg}3%Gu{@w#t6k&JK zgCFQE7~;c?NdgP!;RrOw*>st=;~vda&AfCst?M_>+N8&J+GQt_&oWTH`|AK}Ns-X101Pb6@VP-BCmJ`P;46! zD(-aI(Z7wubKD|Z9yMebjv9T_FIK347Ryx_Z58Ie=hU{sA2skI8pp+Fqb8cL`(B#K zt~<LY;00iYxePtSyF8orBDR#7|SZGzf0N`(sN1qIB& zKvc4X%-ge4;21PLQ0%!HH~M@#a4X}eJciDIcyy1k-PDV;H%>yED1=)F7LF7eJ*jiM zi!)$qwhhgONq_eH-Ff({U+8d@?&Hru{}R47u34it_(LCpIVq&lDhJ3ZJ#^-IZI)-F zIR5gZX4}I zKw1Ddu&;b;8NY-gj2b+x1VzyFpny&&qg$ta%G=X9O`UrUO_AvuE&o_2I({#GD(l(* zKnLLM)1NHKH8a-FHhQ`Iyx8N<{ak<-|MJ3njeQ7{ns=2A9j>3myP3Q4V6 zv$aEk4zg93zxvXc@i;tnu0rKGTsJ^WXnS3!3L3!9Z+5s*HGlr4gJ2?~;OxA-Cz!bQ(3TeW zkae(PIah)(yN>6tS?+eEc@CF$`p#Va6s%ylYWJfJ%u+Fos^{)$Bz;8m-@F2PH|WqS zO~u6H;^9rsc@u8{KNEh-68zzu5j>_J9zD;j+D`I5svFaat0Pr3&uil4P&f2e?sB~1 zmuL_Y5&OYD$yy0k^(Icw?nkx?ghtXsPhK0Ukckctb#yi3CNMkw1PuA4l_)!R>M2!L zG`kK_e(Mpw<#%fkt@REDW9$H>??GAk-Sss}$O}>m;#cvQNmJAzw-OPR4}$l-+`u5*k%0F?xGkei;WBZKXIV&@ovZ z1qu010XK5%5YD$fEz_zm`CIb<>5~dzbu{6{4mWw>jjR4&y*=pz5lJWDH)ydb7v(}l zNi#bLvB6sVCyRTnZ)Uyzs@6pi Date: Wed, 25 Sep 2024 14:07:47 -0400 Subject: [PATCH 02/11] Version 1.0.3 for elasticsearch (#307) --- registry/hasura/elasticsearch/metadata.json | 18 +++++++++++++++++- .../releases/v1.0.3/connector-packaging.json | 11 +++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 registry/hasura/elasticsearch/releases/v1.0.3/connector-packaging.json diff --git a/registry/hasura/elasticsearch/metadata.json b/registry/hasura/elasticsearch/metadata.json index 0c3c24ec..05f0f659 100644 --- a/registry/hasura/elasticsearch/metadata.json +++ b/registry/hasura/elasticsearch/metadata.json @@ -7,7 +7,7 @@ "tags": [ "search" ], - "latest_version": "v1.0.2" + "latest_version": "v1.0.3" }, "author": { "support_email": "support@hasura.io", @@ -82,6 +82,17 @@ "source": { "hash": "19c1dfbce44db9fb9a71e9ca68c93de2d5fade6c" } + }, + { + "version": "1.0.3", + "uri": "https://github.com/hasura/ndc-elasticsearch/releases/download/v1.0.3/connector-definition.tgz", + "checksum": { + "type": "sha256", + "value": "42090a8ad64abe5002b2053041c308ab66dde430186eeb7b6ee76eb9bf8095be" + }, + "source": { + "hash": "4993fed05687b9464c64d44140cd801471ef32b9" + } } ], "source_code": { @@ -117,6 +128,11 @@ "tag": "v1.0.2", "hash": "19c1dfbce44db9fb9a71e9ca68c93de2d5fade6c", "is_verified": true + }, + { + "tag": "v1.0.3", + "hash": "4993fed05687b9464c64d44140cd801471ef32b9", + "is_verified": true } ] } diff --git a/registry/hasura/elasticsearch/releases/v1.0.3/connector-packaging.json b/registry/hasura/elasticsearch/releases/v1.0.3/connector-packaging.json new file mode 100644 index 00000000..9044dc58 --- /dev/null +++ b/registry/hasura/elasticsearch/releases/v1.0.3/connector-packaging.json @@ -0,0 +1,11 @@ +{ + "version": "1.0.3", + "uri": "https://github.com/hasura/ndc-elasticsearch/releases/download/v1.0.3/connector-definition.tgz", + "checksum": { + "type": "sha256", + "value": "42090a8ad64abe5002b2053041c308ab66dde430186eeb7b6ee76eb9bf8095be" + }, + "source": { + "hash": "4993fed05687b9464c64d44140cd801471ef32b9" + } +} From da87c6c1d140e6b7f4125d22745da062b50df55f Mon Sep 17 00:00:00 2001 From: Tristen Harr Date: Thu, 26 Sep 2024 03:38:24 -0500 Subject: [PATCH 03/11] update supporter for community supported connectors (#308) Changed supporters to be Community Supported --- registry/hasura/duckdb/metadata.json | 2 +- registry/hasura/qdrant/metadata.json | 2 +- registry/hasura/turso/metadata.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/registry/hasura/duckdb/metadata.json b/registry/hasura/duckdb/metadata.json index ba77c800..9231638a 100644 --- a/registry/hasura/duckdb/metadata.json +++ b/registry/hasura/duckdb/metadata.json @@ -10,7 +10,7 @@ "latest_version": "v0.1.0" }, "author": { - "support_email": "support@hasura.io", + "support_email": "Community Supported", "homepage": "https://hasura.io", "name": "Hasura" }, diff --git a/registry/hasura/qdrant/metadata.json b/registry/hasura/qdrant/metadata.json index 19361494..5d8cfcac 100644 --- a/registry/hasura/qdrant/metadata.json +++ b/registry/hasura/qdrant/metadata.json @@ -10,7 +10,7 @@ "latest_version": "v0.3.0" }, "author": { - "support_email": "support@hasura.io", + "support_email": "Community Supported", "homepage": "https://hasura.io", "name": "Hasura" }, diff --git a/registry/hasura/turso/metadata.json b/registry/hasura/turso/metadata.json index 9e0072b0..e85076eb 100644 --- a/registry/hasura/turso/metadata.json +++ b/registry/hasura/turso/metadata.json @@ -10,7 +10,7 @@ "latest_version": "v0.1.0" }, "author": { - "support_email": "support@hasura.io", + "support_email": "Community Supported", "homepage": "https://hasura.io", "name": "Hasura" }, From d0b97de2ba8c2193e5dc4ab2dab2d8f812d40675 Mon Sep 17 00:00:00 2001 From: Toan Nguyen Date: Thu, 26 Sep 2024 20:59:52 +0700 Subject: [PATCH 04/11] Add ndc-prometheus v0.0.4 (#303) --- registry/hasura/prometheus/README.md | 20 ++++++++ registry/hasura/prometheus/logo.svg | 50 +++++++++++++++++++ registry/hasura/prometheus/metadata.json | 28 +++++++++++ .../releases/v0.0.4/connector-packaging.json | 11 ++++ 4 files changed, 109 insertions(+) create mode 100644 registry/hasura/prometheus/README.md create mode 100644 registry/hasura/prometheus/logo.svg create mode 100644 registry/hasura/prometheus/metadata.json create mode 100644 registry/hasura/prometheus/releases/v0.0.4/connector-packaging.json diff --git a/registry/hasura/prometheus/README.md b/registry/hasura/prometheus/README.md new file mode 100644 index 00000000..e4d1c7ed --- /dev/null +++ b/registry/hasura/prometheus/README.md @@ -0,0 +1,20 @@ +## Overview + +The Hasura Prometheus Connector allows for connecting Hasura to a Prometheus API server giving you an instant GraphQL API on top of your Prometheus data. + +Data Connectors are the way to connect the Hasura Data Delivery Network (DDN) to external data sources. A data connector is an HTTP service that exposes a set of APIs that Hasura uses to communicate with the data source. Data connectors are built to conform to the [NDC Specification](https://hasura.github.io/ndc-spec/overview.html) using one of Hasura's available SDKs. The data connector is responsible for interpreting work to be done on behalf of the Hasura Engine, using the native query language of the data source. + +The data connector is open source and can be found in the [ndc-prometheus GitHub repository](https://github.com/hasura/ndc-prometheus). + +## Deployment + +The connector is hosted by Hasura and can be used from the [Hasura v3 Console](https://console.hasura.io). + +## Usage + +The Hasura Prometheus connector can be deployed using the [Hasura CLI](https://hasura.io/docs/3.0/cli/overview) by following either the [Quick Start Guide](https://hasura.io/docs/3.0/getting-started/overview/) or [deploying the connector](https://hasura.io/docs/3.0/connectors/deployment). + +## Troubleshooting + +Please [submit a Github issue](https://github.com/hasura/ndc-prometheus/issues/new) +if you encounter any problems! diff --git a/registry/hasura/prometheus/logo.svg b/registry/hasura/prometheus/logo.svg new file mode 100644 index 00000000..5c51f66d --- /dev/null +++ b/registry/hasura/prometheus/logo.svg @@ -0,0 +1,50 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/registry/hasura/prometheus/metadata.json b/registry/hasura/prometheus/metadata.json new file mode 100644 index 00000000..3a70b4b9 --- /dev/null +++ b/registry/hasura/prometheus/metadata.json @@ -0,0 +1,28 @@ +{ + "overview": { + "namespace": "hasura", + "description": "Connect Hasura DDN to a Prometheus API server", + "title": "Prometheus Data Connector", + "logo": "logo.svg", + "tags": ["database"], + "latest_version": "v0.0.4" + }, + "author": { + "support_email": "support@hasura.io", + "homepage": "https://hasura.io", + "name": "Hasura" + }, + "is_verified": true, + "is_hosted_by_hasura": true, + "source_code": { + "is_open_source": true, + "repository": "https://github.com/hasura/ndc-prometheus", + "version": [ + { + "tag": "v0.0.4", + "hash": "69915e6c9736f38a0952c5ce8ad9a2d180783a43", + "is_verified": true + } + ] + } +} diff --git a/registry/hasura/prometheus/releases/v0.0.4/connector-packaging.json b/registry/hasura/prometheus/releases/v0.0.4/connector-packaging.json new file mode 100644 index 00000000..566010dc --- /dev/null +++ b/registry/hasura/prometheus/releases/v0.0.4/connector-packaging.json @@ -0,0 +1,11 @@ +{ + "version": "v0.0.4", + "uri": "https://github.com/hasura/ndc-prometheus/releases/download/v0.0.4/connector-definition.tgz", + "checksum": { + "type": "sha256", + "value": "83acab73b703ff37c6322b990e2782331cffbfe64bf67fb0b2734b3f651689d9" + }, + "source": { + "hash": "69915e6c9736f38a0952c5ce8ad9a2d180783a43" + } +} From 67db552891033de75304f89778fbdd5faf7be502 Mon Sep 17 00:00:00 2001 From: Mohd Bilal Date: Thu, 26 Sep 2024 22:20:48 +0530 Subject: [PATCH 05/11] release version v0.2.0 of the openapi connector (#310) --- registry/hasura/openapi/metadata.json | 2 +- .../openapi/releases/v0.2.0/connector-packaging.json | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 registry/hasura/openapi/releases/v0.2.0/connector-packaging.json diff --git a/registry/hasura/openapi/metadata.json b/registry/hasura/openapi/metadata.json index 45b2a3bd..3a04de96 100644 --- a/registry/hasura/openapi/metadata.json +++ b/registry/hasura/openapi/metadata.json @@ -5,7 +5,7 @@ "title": "OpenAPI Lambda Connector", "logo": "logo.png", "tags": [], - "latest_version": "v0.1.5" + "latest_version": "v0.2.0" }, "author": { "support_email": "support@hasura.io", diff --git a/registry/hasura/openapi/releases/v0.2.0/connector-packaging.json b/registry/hasura/openapi/releases/v0.2.0/connector-packaging.json new file mode 100644 index 00000000..59202d50 --- /dev/null +++ b/registry/hasura/openapi/releases/v0.2.0/connector-packaging.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "uri": "https://github.com/hasura/ndc-open-api-lambda/releases/download/v0.2.0/connector-definition.tgz", + "checksum": { + "type": "sha256", + "value": "6b766e1e2c7c21eddc8d6b7aa024c791a51716f8cdccdf8b8ae2e77f5b3b70e2" + }, + "source": { + "hash": "fa033ba34c5a40e5907a6c6277ecbe941a08edfc" + } + } + \ No newline at end of file From 2b437475484b7cd34c6d315c7657d361aa883878 Mon Sep 17 00:00:00 2001 From: Gavin Ray Date: Thu, 26 Sep 2024 13:39:02 -0400 Subject: [PATCH 06/11] Updates to MySQL, Oracle, Snowflake (#311) --- registry/hasura/mysql/metadata.json | 34 ++++++++++++++++++- .../releases/v1.0.1/connector-packaging.json | 11 ++++++ .../releases/v1.0.2/connector-packaging.json | 11 ++++++ registry/hasura/oracle/metadata.json | 34 ++++++++++++++++++- .../releases/v1.0.1/connector-packaging.json | 11 ++++++ .../releases/v1.0.2/connector-packaging.json | 11 ++++++ registry/hasura/snowflake/metadata.json | 18 +++++++++- .../releases/v1.0.1/connector-packaging.json | 11 ++++++ 8 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 registry/hasura/mysql/releases/v1.0.1/connector-packaging.json create mode 100644 registry/hasura/mysql/releases/v1.0.2/connector-packaging.json create mode 100644 registry/hasura/oracle/releases/v1.0.1/connector-packaging.json create mode 100644 registry/hasura/oracle/releases/v1.0.2/connector-packaging.json create mode 100644 registry/hasura/snowflake/releases/v1.0.1/connector-packaging.json diff --git a/registry/hasura/mysql/metadata.json b/registry/hasura/mysql/metadata.json index 38652674..e255fa48 100644 --- a/registry/hasura/mysql/metadata.json +++ b/registry/hasura/mysql/metadata.json @@ -7,7 +7,7 @@ "tags": [ "database" ], - "latest_version": "v0.1.0" + "latest_version": "v1.0.2" }, "author": { "support_email": "support@hasura.io", @@ -27,6 +27,28 @@ "source": { "hash": "145792746281b606bcef2dfe20d1f0ad69efe01e" } + }, + { + "version": "1.0.1", + "uri": "https://github.com/hasura/ndc-jvm-mono/releases/download/mysql%2Fv1.0.1/package.tar.gz", + "checksum": { + "type": "sha256", + "value": "4503cb8961e3c2c8aede3183187fb69862ac1442a89085d35cf1d4a4989d6db9" + }, + "source": { + "hash": "fbeb926e1d5550bec78a042a36d5ac2a8fba4c9f" + } + }, + { + "version": "1.0.2", + "uri": "https://github.com/hasura/ndc-jvm-mono/releases/download/mysql%2Fv1.0.2/package.tar.gz", + "checksum": { + "type": "sha256", + "value": "86be6695432bef7cfcdccfc376c18f3c89ffefe6d139869ab4739709dd980141" + }, + "source": { + "hash": "145792746281b606bcef2dfe20d1f0ad69efe01e" + } } ], "source_code": { @@ -37,6 +59,16 @@ "tag": "mysql/v0.1.0", "hash": "145792746281b606bcef2dfe20d1f0ad69efe01e", "is_verified": true + }, + { + "tag": "mysql/v1.0.1", + "hash": "fbeb926e1d5550bec78a042a36d5ac2a8fba4c9f", + "is_verified": true + }, + { + "tag": "mysql/v1.0.2", + "hash": "145792746281b606bcef2dfe20d1f0ad69efe01e", + "is_verified": true } ] } diff --git a/registry/hasura/mysql/releases/v1.0.1/connector-packaging.json b/registry/hasura/mysql/releases/v1.0.1/connector-packaging.json new file mode 100644 index 00000000..b4b7a4fc --- /dev/null +++ b/registry/hasura/mysql/releases/v1.0.1/connector-packaging.json @@ -0,0 +1,11 @@ +{ + "version": "1.0.1", + "uri": "https://github.com/hasura/ndc-jvm-mono/releases/download/mysql%2Fv1.0.1/package.tar.gz", + "checksum": { + "type": "sha256", + "value": "4503cb8961e3c2c8aede3183187fb69862ac1442a89085d35cf1d4a4989d6db9" + }, + "source": { + "hash": "fbeb926e1d5550bec78a042a36d5ac2a8fba4c9f" + } +} diff --git a/registry/hasura/mysql/releases/v1.0.2/connector-packaging.json b/registry/hasura/mysql/releases/v1.0.2/connector-packaging.json new file mode 100644 index 00000000..5d6f1b75 --- /dev/null +++ b/registry/hasura/mysql/releases/v1.0.2/connector-packaging.json @@ -0,0 +1,11 @@ +{ + "version": "1.0.2", + "uri": "https://github.com/hasura/ndc-jvm-mono/releases/download/mysql%2Fv1.0.2/package.tar.gz", + "checksum": { + "type": "sha256", + "value": "86be6695432bef7cfcdccfc376c18f3c89ffefe6d139869ab4739709dd980141" + }, + "source": { + "hash": "145792746281b606bcef2dfe20d1f0ad69efe01e" + } +} diff --git a/registry/hasura/oracle/metadata.json b/registry/hasura/oracle/metadata.json index 94604b66..85bea5dc 100644 --- a/registry/hasura/oracle/metadata.json +++ b/registry/hasura/oracle/metadata.json @@ -7,7 +7,7 @@ "tags": [ "database" ], - "latest_version": "v0.1.0" + "latest_version": "v1.0.2" }, "author": { "support_email": "support@hasura.io", @@ -27,6 +27,28 @@ "source": { "hash": "145792746281b606bcef2dfe20d1f0ad69efe01e" } + }, + { + "version": "1.0.1", + "uri": "https://github.com/hasura/ndc-jvm-mono/releases/download/oracle%2Fv1.0.1/package.tar.gz", + "checksum": { + "type": "sha256", + "value": "58ed43d43e0afc74d24c6ec1552ab3fbcc48cf108420f1f6dd6f7bd960b2d126" + }, + "source": { + "hash": "fbeb926e1d5550bec78a042a36d5ac2a8fba4c9f" + } + }, + { + "version": "1.0.2", + "uri": "https://github.com/hasura/ndc-jvm-mono/releases/download/oracle%2Fv1.0.2/package.tar.gz", + "checksum": { + "type": "sha256", + "value": "14891c40124d06c7732f0ca6559c62f3f8bdb1a0d802d4079868aae8a6013935" + }, + "source": { + "hash": "ef3dd9d851e62c18fc1fbee5d049b61065f37f72" + } } ], "source_code": { @@ -37,6 +59,16 @@ "tag": "oracle/v0.1.0", "hash": "145792746281b606bcef2dfe20d1f0ad69efe01e", "is_verified": true + }, + { + "tag": "oracle/v1.0.1", + "hash": "fbeb926e1d5550bec78a042a36d5ac2a8fba4c9f", + "is_verified": true + }, + { + "tag": "oracle/v1.0.2", + "hash": "ef3dd9d851e62c18fc1fbee5d049b61065f37f72", + "is_verified": true } ] } diff --git a/registry/hasura/oracle/releases/v1.0.1/connector-packaging.json b/registry/hasura/oracle/releases/v1.0.1/connector-packaging.json new file mode 100644 index 00000000..56b25785 --- /dev/null +++ b/registry/hasura/oracle/releases/v1.0.1/connector-packaging.json @@ -0,0 +1,11 @@ +{ + "version": "1.0.1", + "uri": "https://github.com/hasura/ndc-jvm-mono/releases/download/oracle%2Fv1.0.1/package.tar.gz", + "checksum": { + "type": "sha256", + "value": "58ed43d43e0afc74d24c6ec1552ab3fbcc48cf108420f1f6dd6f7bd960b2d126" + }, + "source": { + "hash": "fbeb926e1d5550bec78a042a36d5ac2a8fba4c9f" + } +} diff --git a/registry/hasura/oracle/releases/v1.0.2/connector-packaging.json b/registry/hasura/oracle/releases/v1.0.2/connector-packaging.json new file mode 100644 index 00000000..e83d968d --- /dev/null +++ b/registry/hasura/oracle/releases/v1.0.2/connector-packaging.json @@ -0,0 +1,11 @@ +{ + "version": "1.0.2", + "uri": "https://github.com/hasura/ndc-jvm-mono/releases/download/oracle%2Fv1.0.2/package.tar.gz", + "checksum": { + "type": "sha256", + "value": "14891c40124d06c7732f0ca6559c62f3f8bdb1a0d802d4079868aae8a6013935" + }, + "source": { + "hash": "ef3dd9d851e62c18fc1fbee5d049b61065f37f72" + } +} diff --git a/registry/hasura/snowflake/metadata.json b/registry/hasura/snowflake/metadata.json index 93084ae9..cfdbc20d 100644 --- a/registry/hasura/snowflake/metadata.json +++ b/registry/hasura/snowflake/metadata.json @@ -7,7 +7,7 @@ "tags": [ "database" ], - "latest_version": "v0.1.0" + "latest_version": "v1.0.1" }, "author": { "support_email": "support@hasura.io", @@ -27,6 +27,17 @@ "source": { "hash": "145792746281b606bcef2dfe20d1f0ad69efe01e" } + }, + { + "version": "1.0.1", + "uri": "https://github.com/hasura/ndc-jvm-mono/releases/download/snowflake%2Fv1.0.1/package.tar.gz", + "checksum": { + "type": "sha256", + "value": "01791312a91e41e59dd638f7efed7472a37c3d1fe174cc71a844a5cde65e23a5" + }, + "source": { + "hash": "fbeb926e1d5550bec78a042a36d5ac2a8fba4c9f" + } } ], "source_code": { @@ -37,6 +48,11 @@ "tag": "snowflake/v0.1.0", "hash": "145792746281b606bcef2dfe20d1f0ad69efe01e", "is_verified": true + }, + { + "tag": "snowflake/v1.0.1", + "hash": "fbeb926e1d5550bec78a042a36d5ac2a8fba4c9f", + "is_verified": true } ] } diff --git a/registry/hasura/snowflake/releases/v1.0.1/connector-packaging.json b/registry/hasura/snowflake/releases/v1.0.1/connector-packaging.json new file mode 100644 index 00000000..1df80206 --- /dev/null +++ b/registry/hasura/snowflake/releases/v1.0.1/connector-packaging.json @@ -0,0 +1,11 @@ +{ + "version": "1.0.1", + "uri": "https://github.com/hasura/ndc-jvm-mono/releases/download/snowflake%2Fv1.0.1/package.tar.gz", + "checksum": { + "type": "sha256", + "value": "01791312a91e41e59dd638f7efed7472a37c3d1fe174cc71a844a5cde65e23a5" + }, + "source": { + "hash": "fbeb926e1d5550bec78a042a36d5ac2a8fba4c9f" + } +} From faa8be057b8e6a2dff0051e521490594de03de3c Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Fri, 27 Sep 2024 15:06:21 +0530 Subject: [PATCH 07/11] New connector publication automation (#304) This PR adds support to publish a new connector and improves the existing code quality by refactoring it and making the code more testable. The whole connector publication process is also documented [here](https://docs.google.com/document/d/1Biu1_rSNeGOS4CiFaDSc4SnVAd2NVlBJGu107zphXm0/edit#heading=h.nsi7idklva07) --- registry-automation/README.md | 38 ++ registry-automation/cmd/ci.go | 679 +++++++++++-------------- registry-automation/cmd/ci_test.go | 282 +++++++--- registry-automation/cmd/gcp.go | 5 +- registry-automation/cmd/registry_db.go | 283 +++++++++++ registry-automation/cmd/types.go | 185 +++++++ registry-automation/cmd/utils.go | 28 + registry-automation/go.mod | 5 + registry-automation/go.sum | 2 + 9 files changed, 1059 insertions(+), 448 deletions(-) create mode 100644 registry-automation/README.md create mode 100644 registry-automation/cmd/registry_db.go create mode 100644 registry-automation/cmd/types.go diff --git a/registry-automation/README.md b/registry-automation/README.md new file mode 100644 index 00000000..2c0d4852 --- /dev/null +++ b/registry-automation/README.md @@ -0,0 +1,38 @@ +# Introduction + +## Steps to runs + +1. Consider the following `changed_files.json` file: +```json + +{ + "added_files": [ + "registry/hasura/azure-cosmos/releases/v0.1.6/connector-packaging.json" + ], + "modified_files": [ + "registry/hasura/azure-cosmos/metadata.json" + ], + "deleted_files": [] +} +``` + +2. You will require the following environment variables: + +1. GCP_BUCKET_NAME +2. CLOUDINARY_URL +3. GCP_SERVICE_ACCOUNT_KEY +4. CONNECTOR_REGISTRY_GQL_URL +5. CONNECTOR_PUBLICATION_KEY +6. GCP_SERVICE_ACCOUNT_DETAILS + + + +```bash + + +2. Run the following command from the `registry-automation` directory: + + +```bash +go run main.go ci --changed-files-path changed_files.json +``` diff --git a/registry-automation/cmd/ci.go b/registry-automation/cmd/ci.go index cd9c31f9..11847204 100644 --- a/registry-automation/cmd/ci.go +++ b/registry-automation/cmd/ci.go @@ -16,7 +16,6 @@ import ( "github.com/machinebox/graphql" "github.com/spf13/cobra" "google.golang.org/api/option" - "gopkg.in/yaml.v2" ) // ciCmd represents the ci command @@ -26,83 +25,6 @@ var ciCmd = &cobra.Command{ Run: runCI, } -type ChangedFiles struct { - Added []string `json:"added_files"` - Modified []string `json:"modified_files"` - Deleted []string `json:"deleted_files"` -} - -// ConnectorVersion represents a version of a connector, this type is -// used to insert a new version of a connector in the registry. -type ConnectorVersion struct { - // Namespace of the connector, e.g. "hasura" - Namespace string `json:"namespace"` - // Name of the connector, e.g. "mongodb" - Name string `json:"name"` - // Semantic version of the connector version, e.g. "v1.0.0" - Version string `json:"version"` - // Docker image of the connector version (optional) - // This field is only required if the connector version is of type `PrebuiltDockerImage` - Image *string `json:"image,omitempty"` - // URL to the connector's metadata - PackageDefinitionURL string `json:"package_definition_url"` - // Is the connector version multitenant? - IsMultitenant bool `json:"is_multitenant"` - // Type of the connector packaging `PrebuiltDockerImage`/`ManagedDockerBuild` - Type string `json:"type"` -} - -// Create a struct with the following fields: -// type string -// image *string (optional) -type ConnectionVersionMetadata struct { - Type string `yaml:"type"` - Image *string `yaml:"image,omitempty"` -} - -type WhereClause struct { - ConnectorName string - ConnectorNamespace string -} - -func (wc WhereClause) MarshalJSON() ([]byte, error) { - where := map[string]interface{}{ - "_and": []map[string]interface{}{ - {"name": map[string]string{"_eq": wc.ConnectorName}}, - {"namespace": map[string]string{"_eq": wc.ConnectorNamespace}}, - }, - } - return json.Marshal(where) -} - -type ConnectorOverviewUpdate struct { - Set struct { - Docs *string `json:"docs,omitempty"` - Logo *string `json:"logo,omitempty"` - } `json:"_set"` - Where WhereClause `json:"where"` -} - -type ConnectorOverviewUpdates struct { - Updates []ConnectorOverviewUpdate `json:"updates"` -} - -const ( - ManagedDockerBuild = "ManagedDockerBuild" - PrebuiltDockerImage = "PrebuiltDockerImage" -) - -// Make a struct with the fields expected in the command line arguments -type ConnectorRegistryArgs struct { - ChangedFilesPath string - PublicationEnv string - ConnectorRegistryGQLUrl string - ConnectorPublicationKey string - GCPServiceAccountDetails string - GCPBucketName string - CloudinaryUrl string -} - var ciCmdArgs ConnectorRegistryArgs func init() { @@ -125,13 +47,20 @@ func init() { } -func buildContext() { +func buildContext() Context { // Connector registry Hasura GraphQL URL registryGQLURL := os.Getenv("CONNECTOR_REGISTRY_GQL_URL") + var registryGQLClient *graphql.Client + var storageClient *storage.Client + var cloudinaryClient *cloudinary.Cloudinary + var cloudinaryWrapper *CloudinaryWrapper + var storageWrapper *StorageClientWrapper + if registryGQLURL == "" { log.Fatalf("CONNECTOR_REGISTRY_GQL_URL is not set") } else { ciCmdArgs.ConnectorRegistryGQLUrl = registryGQLURL + registryGQLClient = graphql.NewClient(registryGQLURL) } // Connector publication key @@ -147,6 +76,15 @@ func buildContext() { if gcpServiceAccountDetails == "" { log.Fatalf("GCP_SERVICE_ACCOUNT_DETAILS is not set") } else { + var err error + storageClient, err = storage.NewClient(context.Background(), option.WithCredentialsJSON([]byte(gcpServiceAccountDetails))) + if err != nil { + log.Fatalf("Failed to create Google bucket client: %v", err) + } + defer storageClient.Close() + + storageWrapper = &StorageClientWrapper{storageClient} + ciCmdArgs.GCPServiceAccountDetails = gcpServiceAccountDetails } @@ -163,115 +101,211 @@ func buildContext() { if cloudinaryUrl == "" { log.Fatalf("CLOUDINARY_URL is not set") } else { - ciCmdArgs.CloudinaryUrl = cloudinaryUrl + var err error + cloudinaryClient, err = cloudinary.NewFromURL(cloudinaryUrl) + if err != nil { + log.Fatalf("Failed to create cloudinary client: %v", err) + + } + cloudinaryWrapper = &CloudinaryWrapper{cloudinaryClient} + + } + + return Context{ + Env: ciCmdArgs.PublicationEnv, + RegistryGQLClient: registryGQLClient, + StorageClient: storageWrapper, + Cloudinary: cloudinaryWrapper, } } -// processChangedFiles processes the files in the PR and extracts the connector name and version -// This function checks for the following things: -// 1. If a new connector version is added, it adds the connector version to the `newlyAddedConnectorVersions` map. -// 2. If the logo file is modified, it adds the connector name and the path to the modified logo to the `modifiedLogos` map. -// 3. If the README file is modified, it adds the connector name and the path to the modified README to the `modifiedReadmes` map. -func processChangedFiles(changedFiles ChangedFiles) (NewConnectorVersions, ModifiedLogos, ModifiedReadmes) { +type fileProcessor struct { + regex *regexp.Regexp + process func(matches []string, file string) +} - newlyAddedConnectorVersions := make(map[Connector]map[string]string) - modifiedLogos := make(map[Connector]string) - modifiedReadmes := make(map[Connector]string) +// processChangedFiles categorizes changes in connector files within a registry system. +// It handles new and modified files including metadata, logos, READMEs, and connector versions. +// +// The function takes a ChangedFiles struct containing slices of added and modified filenames, +// and returns a ProcessedChangedFiles struct with categorized changes. +// +// Files are processed based on their path and type: +// - metadata.json: New connectors +// - logo.(png|svg): New or modified logos +// - README.md: New or modified READMEs +// - connector-packaging.json: New connector versions +// +// Any files not matching these patterns are logged as skipped. +// +// Example usage: +// +// changedFiles := ChangedFiles{ +// Added: []string{"registry/namespace1/connector1/metadata.json"}, +// Modified: []string{"registry/namespace2/connector2/README.md"}, +// } +// result := processChangedFiles(changedFiles) +func processChangedFiles(changedFiles ChangedFiles) ProcessedChangedFiles { + result := ProcessedChangedFiles{ + NewConnectorVersions: make(map[Connector]map[string]string), + ModifiedLogos: make(map[Connector]string), + ModifiedReadmes: make(map[Connector]string), + NewConnectors: make(map[Connector]MetadataFile), + NewLogos: make(map[Connector]string), + NewReadmes: make(map[Connector]string), + } + + processors := []fileProcessor{ + { + regex: regexp.MustCompile(`^registry/([^/]+)/([^/]+)/metadata.json$`), + process: func(matches []string, file string) { + // IsNew is set to true because we are processing newly added metadata.json + connector := Connector{Name: matches[2], Namespace: matches[1]} + result.NewConnectors[connector] = MetadataFile(file) + fmt.Printf("Processing metadata file for connector: %s\n", connector.Name) + }, + }, + { + regex: regexp.MustCompile(`^registry/([^/]+)/([^/]+)/logo\.(png|svg)$`), + process: func(matches []string, file string) { + connector := Connector{Name: matches[2], Namespace: matches[1]} + result.NewLogos[connector] = file + fmt.Printf("Processing logo file for connector: %s\n", connector.Name) + }, + }, + { + regex: regexp.MustCompile(`^registry/([^/]+)/([^/]+)/README\.md$`), + process: func(matches []string, file string) { + connector := Connector{Name: matches[2], Namespace: matches[1]} + result.NewReadmes[connector] = file + fmt.Printf("Processing README file for connector: %s\n", connector.Name) + }, + }, + { + regex: regexp.MustCompile(`^registry/([^/]+)/([^/]+)/releases/([^/]+)/connector-packaging\.json$`), + process: func(matches []string, file string) { + connector := Connector{Name: matches[2], Namespace: matches[1]} + version := matches[3] + if _, exists := result.NewConnectorVersions[connector]; !exists { + result.NewConnectorVersions[connector] = make(map[string]string) + } + result.NewConnectorVersions[connector][version] = file + }, + }, + } - var connectorVersionPackageRegex = regexp.MustCompile(`^registry/([^/]+)/([^/]+)/releases/([^/]+)/connector-packaging\.json$`) - var logoPngRegex = regexp.MustCompile(`^registry/([^/]+)/([^/]+)/logo\.(png|svg)$`) - var readmeMdRegex = regexp.MustCompile(`^registry/([^/]+)/([^/]+)/README\.md$`) + processFile := func(file string, isModified bool) { + for _, processor := range processors { + if matches := processor.regex.FindStringSubmatch(file); matches != nil { + if isModified { + connector := Connector{Name: matches[2], Namespace: matches[1]} + if processor.regex.String() == processors[1].regex.String() { + result.ModifiedLogos[connector] = file + } else if processor.regex.String() == processors[2].regex.String() { + result.ModifiedReadmes[connector] = file + } + } else { + processor.process(matches, file) + } + return + } + } + fmt.Printf("Skipping %s file: %s\n", map[bool]string{true: "modified", false: "newly added"}[isModified], file) + } for _, file := range changedFiles.Added { + processFile(file, false) + } - // Check if the file is a connector version package - if connectorVersionPackageRegex.MatchString(file) { + for _, file := range changedFiles.Modified { + processFile(file, true) + } - matches := connectorVersionPackageRegex.FindStringSubmatch(file) - if len(matches) == 4 { - connectorNamespace := matches[1] - connectorName := matches[2] - connectorVersion := matches[3] + return result +} - connector := Connector{ - Name: connectorName, - Namespace: connectorNamespace, - } +func processNewConnector(ciCtx Context, connector Connector, metadataFile MetadataFile) (ConnectorOverviewInsert, HubRegistryConnectorInsertInput, error) { + // Process the newly added connector + // Get the string value from metadataFile + var connectorOverviewAndAuthor ConnectorOverviewInsert + var hubRegistryConnectorInsertInput HubRegistryConnectorInsertInput - if _, exists := newlyAddedConnectorVersions[connector]; !exists { - newlyAddedConnectorVersions[connector] = make(map[string]string) - } + connectorMetadata, err := readJSONFile[ConnectorMetadata](string(metadataFile)) + if err != nil { + return connectorOverviewAndAuthor, hubRegistryConnectorInsertInput, fmt.Errorf("Failed to parse the connector metadata file: %v", err) + } - newlyAddedConnectorVersions[connector][connectorVersion] = file - } + docs, err := readFile(fmt.Sprintf("registry/%s/%s/README.md", connector.Namespace, connector.Name)) - } else { - fmt.Println("Skipping newly added file: ", file) - } + if err != nil { + return connectorOverviewAndAuthor, hubRegistryConnectorInsertInput, fmt.Errorf("Failed to read the README file of the connector: %s : %v", connector.Name, err) } - for _, file := range changedFiles.Modified { - if logoPngRegex.MatchString(file) { - // Process the logo file - // print the name of the connector and the version - matches := logoPngRegex.FindStringSubmatch(file) - if len(matches) == 4 { - - connectorNamespace := matches[1] - connectorName := matches[2] - connector := Connector{ - Name: connectorName, - Namespace: connectorNamespace, - } - modifiedLogos[connector] = file - fmt.Printf("Processing logo file for connector: %s\n", connectorName) - } - - } else if readmeMdRegex.MatchString(file) { - // Process the README file - // print the name of the connector and the version - matches := readmeMdRegex.FindStringSubmatch(file) + logoPath := fmt.Sprintf("registry/%s/%s/logo.png", connector.Namespace, connector.Name) - if len(matches) == 3 { + uploadedLogoUrl, err := uploadLogoToCloudinary(ciCtx.Cloudinary, Connector{Name: connector.Name, Namespace: connector.Namespace}, logoPath) + if err != nil { + return connectorOverviewAndAuthor, hubRegistryConnectorInsertInput, err + } - connectorNamespace := matches[1] - connectorName := matches[2] - connector := Connector{ - Name: connectorName, - Namespace: connectorNamespace, - } + // Get connector info from the registry + connectorInfo, err := getConnectorInfoFromRegistry(ciCtx.RegistryGQLClient, connector.Name, connector.Namespace) + if err != nil { + return connectorOverviewAndAuthor, hubRegistryConnectorInsertInput, + fmt.Errorf("Failed to get the connector info from the registry: %v", err) + } - modifiedReadmes[connector] = file + // Check if the connector already exists in the registry + if len(connectorInfo.HubRegistryConnector) > 0 { + if ciCtx.Env == "staging" { + fmt.Printf("Connector already exists in the registry: %s/%s\n", connector.Namespace, connector.Name) + fmt.Println("The connector is going to be overwritten in the registry.") - fmt.Printf("Processing README file for connector: %s\n", connectorName) - } } else { - fmt.Println("Skipping modified file: ", file) + + return connectorOverviewAndAuthor, hubRegistryConnectorInsertInput, fmt.Errorf("Attempting to create a new hub connector, but the connector already exists in the registry: %s/%s", connector.Namespace, connector.Name) } } - return newlyAddedConnectorVersions, modifiedLogos, modifiedReadmes + hubRegistryConnectorInsertInput = HubRegistryConnectorInsertInput{ + Name: connector.Name, + Namespace: connector.Namespace, + Title: connectorMetadata.Overview.Title, + } + + connectorOverviewAndAuthor = ConnectorOverviewInsert{ + Name: connector.Name, + Namespace: connector.Namespace, + Docs: string(docs), + Logo: uploadedLogoUrl, + Title: connectorMetadata.Overview.Title, + Description: connectorMetadata.Overview.Description, + IsVerified: connectorMetadata.IsVerified, + IsHosted: connectorMetadata.IsHostedByHasura, + Author: ConnectorAuthorNestedInsert{ + Data: ConnectorAuthor{ + Name: connectorMetadata.Author.Name, + SupportEmail: connectorMetadata.Author.SupportEmail, + Website: connectorMetadata.Author.Homepage, + }, + }, + } + return connectorOverviewAndAuthor, hubRegistryConnectorInsertInput, nil } // runCI is the main function that runs the CI workflow func runCI(cmd *cobra.Command, args []string) { - buildContext() + ctx := buildContext() changedFilesContent, err := os.Open(ciCmdArgs.ChangedFilesPath) if err != nil { log.Fatalf("Failed to open the file: %v, err: %v", ciCmdArgs.ChangedFilesPath, err) } defer changedFilesContent.Close() - client, err := storage.NewClient(context.Background(), option.WithCredentialsJSON([]byte(ciCmdArgs.GCPServiceAccountDetails))) - if err != nil { - log.Fatalf("Failed to create Google bucket client: %v", err) - } - defer client.Close() - // Read the changed file's contents. This file contains all the changed files in the PR changedFilesByteValue, err := io.ReadAll(changedFilesContent) if err != nil { @@ -288,65 +322,110 @@ func runCI(cmd *cobra.Command, args []string) { // Separate the modified files according to the type of file // Collect the added or modified connectors - newlyAddedConnectorVersions, modifiedLogos, modifiedReadmes := processChangedFiles(changedFiles) + processChangedFiles := processChangedFiles(changedFiles) - // check if the map is empty - if len(newlyAddedConnectorVersions) == 0 && len(modifiedLogos) == 0 && len(modifiedReadmes) == 0 { - fmt.Println("No connectors to be added or modified in the registry") - return - } else { - if len(newlyAddedConnectorVersions) > 0 { - processNewlyAddedConnectorVersions(client, newlyAddedConnectorVersions) - } + newlyAddedConnectorVersions := processChangedFiles.NewConnectorVersions + modifiedLogos := processChangedFiles.ModifiedLogos + modifiedReadmes := processChangedFiles.ModifiedReadmes + + newlyAddedConnectors := processChangedFiles.NewConnectors + + var newConnectorsToBeAdded NewConnectorsInsertInput + var newConnectorVersionsToBeAdded []ConnectorVersion + + newConnectorOverviewsToBeAdded := make([](ConnectorOverviewInsert), 0) + hubRegistryConnectorsToBeAdded := make([](HubRegistryConnectorInsertInput), 0) + connectorOverviewUpdates := make([]ConnectorOverviewUpdate, 0) + + if len(newlyAddedConnectors) > 0 { + fmt.Println("New connectors to be added to the registry: ", newlyAddedConnectors) + + for connector, metadataFile := range newlyAddedConnectors { + connectorOverviewAndAuthor, hubRegistryConnector, err := processNewConnector(ctx, connector, metadataFile) - if len(modifiedReadmes) > 0 { - err := processModifiedReadmes(modifiedReadmes) if err != nil { - log.Fatalf("Failed to process the modified READMEs: %v", err) + log.Fatalf("Failed to process the new connector: %s/%s, Error: %v", connector.Namespace, connector.Name, err) } - fmt.Println("Successfully updated the READMEs in the registry.") + newConnectorOverviewsToBeAdded = append(newConnectorOverviewsToBeAdded, connectorOverviewAndAuthor) + hubRegistryConnectorsToBeAdded = append(hubRegistryConnectorsToBeAdded, hubRegistryConnector) + } - if len(modifiedLogos) > 0 { - err := processModifiedLogos(modifiedLogos) - if err != nil { - log.Fatalf("Failed to process the modified logos: %v", err) - } - fmt.Println("Successfully updated the logos in the registry.") + newConnectorsToBeAdded.HubRegistryConnectors = hubRegistryConnectorsToBeAdded + newConnectorsToBeAdded.ConnectorOverviews = newConnectorOverviewsToBeAdded + + } + + if len(newlyAddedConnectorVersions) > 0 { + newlyAddedConnectors := make(map[Connector]bool) + for connector := range newlyAddedConnectorVersions { + newlyAddedConnectors[connector] = true + } + newConnectorVersionsToBeAdded = processNewlyAddedConnectorVersions(ctx, newlyAddedConnectorVersions, newlyAddedConnectors) + } + + if len(modifiedReadmes) > 0 { + readMeUpdates, err := processModifiedReadmes(modifiedReadmes) + if err != nil { + log.Fatalf("Failed to process the modified READMEs: %v", err) + } + connectorOverviewUpdates = append(connectorOverviewUpdates, readMeUpdates...) + fmt.Println("Successfully updated the READMEs in the registry.") + } + + if len(modifiedLogos) > 0 { + logoUpdates, err := processModifiedLogos(modifiedLogos, ctx.Cloudinary) + if err != nil { + log.Fatalf("Failed to process the modified logos: %v", err) } + connectorOverviewUpdates = append(connectorOverviewUpdates, logoUpdates...) + fmt.Println("Successfully updated the logos in the registry.") + } + + if ctx.Env == "production" { + err = registryDbMutation(ctx.RegistryGQLClient, newConnectorsToBeAdded, connectorOverviewUpdates, newConnectorVersionsToBeAdded) + + } else if ctx.Env == "staging" { + err = registryDbMutationStaging(ctx.RegistryGQLClient, newConnectorsToBeAdded, connectorOverviewUpdates, newConnectorVersionsToBeAdded) + } else { + log.Fatalf("Unexpected: invalid publication environment: %s", ctx.Env) + } + + if err != nil { + log.Fatalf("Failed to update the registry: %v", err) } fmt.Println("Successfully processed the changed files in the PR") } -func processModifiedLogos(modifiedLogos ModifiedLogos) error { - // Iterate over the modified logos and update the logos in the registry - var connectorOverviewUpdates []ConnectorOverviewUpdate - // upload the logo to cloudinary - cloudinary, err := cloudinary.NewFromURL(ciCmdArgs.CloudinaryUrl) +func uploadLogoToCloudinary(cloudinary CloudinaryInterface, connector Connector, logoPath string) (string, error) { + logoContent, err := readFile(logoPath) if err != nil { - return err + fmt.Printf("Failed to read the logo file: %v", err) + return "", err } + imageReader := bytes.NewReader(logoContent) + + uploadResult, err := cloudinary.Upload(context.Background(), imageReader, uploader.UploadParams{ + PublicID: fmt.Sprintf("%s-%s", connector.Namespace, connector.Name), + Format: "png", + }) + if err != nil { + return "", fmt.Errorf("Failed to upload the logo to cloudinary for the connector: %s, Error: %v\n", connector.Name, err) + } + return uploadResult.SecureURL, nil +} + +func processModifiedLogos(modifiedLogos ModifiedLogos, cloudinaryClient CloudinaryInterface) ([]ConnectorOverviewUpdate, error) { + // Iterate over the modified logos and update the logos in the registry + var connectorOverviewUpdates []ConnectorOverviewUpdate + for connector, logoPath := range modifiedLogos { // open the logo file - logoContent, err := readFile(logoPath) + uploadedLogoUrl, err := uploadLogoToCloudinary(cloudinaryClient, connector, logoPath) if err != nil { - fmt.Printf("Failed to read the logo file: %v", err) - return err - } - - imageReader := bytes.NewReader(logoContent) - - uploadResult, err := cloudinary.Upload.Upload(context.Background(), imageReader, uploader.UploadParams{ - PublicID: fmt.Sprintf("%s-%s", connector.Namespace, connector.Name), - Format: "png", - }) - if err != nil { - fmt.Printf("Failed to upload the logo to cloudinary for the connector: %s, Error: %v\n", connector.Name, err) - return err - } else { - fmt.Printf("Successfully uploaded the logo to cloudinary for the connector: %s\n", connector.Name) + return connectorOverviewUpdates, err } var connectorOverviewUpdate ConnectorOverviewUpdate @@ -357,7 +436,7 @@ func processModifiedLogos(modifiedLogos ModifiedLogos) error { *connectorOverviewUpdate.Set.Logo = "" } - *connectorOverviewUpdate.Set.Logo = string(uploadResult.SecureURL) + *connectorOverviewUpdate.Set.Logo = uploadedLogoUrl connectorOverviewUpdate.Where.ConnectorName = connector.Name connectorOverviewUpdate.Where.ConnectorNamespace = connector.Namespace @@ -366,11 +445,11 @@ func processModifiedLogos(modifiedLogos ModifiedLogos) error { } - return updateConnectorOverview(ConnectorOverviewUpdates{Updates: connectorOverviewUpdates}) + return connectorOverviewUpdates, nil } -func processModifiedReadmes(modifiedReadmes ModifiedReadmes) error { +func processModifiedReadmes(modifiedReadmes ModifiedReadmes) ([]ConnectorOverviewUpdate, error) { // Iterate over the modified READMEs and update the READMEs in the registry var connectorOverviewUpdates []ConnectorOverviewUpdate @@ -378,7 +457,7 @@ func processModifiedReadmes(modifiedReadmes ModifiedReadmes) error { // open the README file readmeContent, err := readFile(readmePath) if err != nil { - return err + return connectorOverviewUpdates, err } @@ -394,11 +473,11 @@ func processModifiedReadmes(modifiedReadmes ModifiedReadmes) error { } - return updateConnectorOverview(ConnectorOverviewUpdates{Updates: connectorOverviewUpdates}) + return connectorOverviewUpdates, nil } -func processNewlyAddedConnectorVersions(client *storage.Client, newlyAddedConnectorVersions NewConnectorVersions) { +func processNewlyAddedConnectorVersions(ciCtx Context, newlyAddedConnectorVersions NewConnectorVersions, newConnectorsAdded map[Connector]bool) []ConnectorVersion { // Iterate over the added or modified connectors and upload the connector versions var connectorVersions []ConnectorVersion var uploadConnectorVersionErr error @@ -407,7 +486,8 @@ func processNewlyAddedConnectorVersions(client *storage.Client, newlyAddedConnec for connectorName, versions := range newlyAddedConnectorVersions { for version, connectorVersionPath := range versions { var connectorVersion ConnectorVersion - connectorVersion, uploadConnectorVersionErr = uploadConnectorVersionPackage(client, connectorName, version, connectorVersionPath) + isNewConnector := newConnectorsAdded[connectorName] + connectorVersion, uploadConnectorVersionErr = uploadConnectorVersionPackage(ciCtx, connectorName, version, connectorVersionPath, isNewConnector) if uploadConnectorVersionErr != nil { fmt.Printf("Error while processing version and connector: %s - %s, Error: %v", version, connectorName, uploadConnectorVersionErr) @@ -423,24 +503,18 @@ func processNewlyAddedConnectorVersions(client *storage.Client, newlyAddedConnec if encounteredError { // attempt to cleanup the uploaded connector versions - _ = cleanupUploadedConnectorVersions(client, connectorVersions) // ignore errors while cleaning up + _ = cleanupUploadedConnectorVersions(ciCtx.StorageClient, connectorVersions) // ignore errors while cleaning up // delete the uploaded connector versions from the registry log.Fatalf("Failed to upload the connector version: %v", uploadConnectorVersionErr) - - } else { - fmt.Printf("Connector versions to be added to the registry: %+v\n", connectorVersions) - err := updateRegistryGQL(connectorVersions) - if err != nil { - // attempt to cleanup the uploaded connector versions - _ = cleanupUploadedConnectorVersions(client, connectorVersions) // ignore errors while cleaning up - log.Fatalf("Failed to update the registry: %v", err) - } } + fmt.Println("Successfully added connector versions to the registry.") + return connectorVersions + } -func cleanupUploadedConnectorVersions(client *storage.Client, connectorVersions []ConnectorVersion) error { +func cleanupUploadedConnectorVersions(client StorageClientInterface, connectorVersions []ConnectorVersion) error { // Iterate over the connector versions and delete the uploaded files // from the google bucket fmt.Println("Cleaning up the uploaded connector versions") @@ -455,22 +529,8 @@ func cleanupUploadedConnectorVersions(client *storage.Client, connectorVersions return nil } -// Type that uniquely identifies a connector -type Connector struct { - Name string `json:"name"` - Namespace string `json:"namespace"` -} - -type NewConnectorVersions map[Connector]map[string]string - -// ModifiedLogos represents the modified logos in the PR, the key is the connector name and the value is the path to the modified logo -type ModifiedLogos map[Connector]string - -// ModifiedReadmes represents the modified READMEs in the PR, the key is the connector name and the value is the path to the modified README -type ModifiedReadmes map[Connector]string - // uploadConnectorVersionPackage uploads the connector version package to the registry -func uploadConnectorVersionPackage(client *storage.Client, connector Connector, version string, changedConnectorVersionPath string) (ConnectorVersion, error) { +func uploadConnectorVersionPackage(ciCtx Context, connector Connector, version string, changedConnectorVersionPath string, isNewConnector bool) (ConnectorVersion, error) { var connectorVersion ConnectorVersion @@ -492,7 +552,7 @@ func uploadConnectorVersionPackage(client *storage.Client, connector Connector, return connectorVersion, err } - uploadedTgzUrl, err := uploadConnectorVersionDefinition(client, connector.Namespace, connector.Name, version, connectorMetadataTgzPath) + uploadedTgzUrl, err := uploadConnectorVersionDefinition(ciCtx, connector.Namespace, connector.Name, version, connectorMetadataTgzPath) if err != nil { return connectorVersion, fmt.Errorf("failed to upload the connector version definition - connector: %v version:%v - err: %v", connector.Name, version, err) } else { @@ -501,13 +561,13 @@ func uploadConnectorVersionPackage(client *storage.Client, connector Connector, } // Build payload for registry upsert - return buildRegistryPayload(connector.Namespace, connector.Name, version, connectorVersionMetadata, uploadedTgzUrl) + return buildRegistryPayload(ciCtx, connector.Namespace, connector.Name, version, connectorVersionMetadata, uploadedTgzUrl, isNewConnector) } -func uploadConnectorVersionDefinition(client *storage.Client, connectorNamespace, connectorName string, connectorVersion string, connectorMetadataTgzPath string) (string, error) { +func uploadConnectorVersionDefinition(ciCtx Context, connectorNamespace, connectorName string, connectorVersion string, connectorMetadataTgzPath string) (string, error) { bucketName := ciCmdArgs.GCPBucketName objectName := generateGCPObjectName(connectorNamespace, connectorName, connectorVersion) - uploadedTgzUrl, err := uploadFile(client, bucketName, objectName, connectorMetadataTgzPath) + uploadedTgzUrl, err := uploadFile(ciCtx.StorageClient, bucketName, objectName, connectorMetadataTgzPath) if err != nil { return "", err @@ -552,94 +612,15 @@ func getConnectorVersionMetadata(tgzUrl string, connector Connector, connectorVe return connectorVersionMetadata, tgzPath, nil } -// Write a function that accepts a file path to a YAML file and returns -// the contents of the file as a map[string]interface{}. -// readYAMLFile accepts a file path to a YAML file and returns the contents of the file as a map[string]interface{}. -func readYAMLFile(filePath string) (map[string]interface{}, error) { - // Open the file - file, err := os.Open(filePath) - if err != nil { - return nil, fmt.Errorf("failed to open file: %w", err) - } - defer file.Close() - - // Read the file contents - data, err := io.ReadAll(file) - if err != nil { - return nil, fmt.Errorf("failed to read file: %w", err) - } - - // Unmarshal the YAML contents into a map - var result map[string]interface{} - err = yaml.Unmarshal(data, &result) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal YAML: %w", err) - } - - return result, nil -} - -func getConnectorNamespace(connectorMetadata map[string]interface{}) (string, error) { - connectorOverview, ok := connectorMetadata["overview"].(map[string]interface{}) - if !ok { - return "", fmt.Errorf("could not find connector overview in the connector's metadata") - } - connectorNamespace, ok := connectorOverview["namespace"].(string) - if !ok { - return "", fmt.Errorf("could not find the 'namespace' of the connector in the connector's overview in the connector's metadata.json") - } - return connectorNamespace, nil -} - -// struct to store the response of teh GetConnectorInfo query -type GetConnectorInfoResponse struct { - HubRegistryConnector []struct { - Name string `json:"name"` - MultitenantConnector *struct { - ID string `json:"id"` - } `json:"multitenant_connector"` - } `json:"hub_registry_connector"` -} - -func getConnectorInfoFromRegistry(connectorNamespace string, connectorName string) (GetConnectorInfoResponse, error) { - var respData GetConnectorInfoResponse - client := graphql.NewClient(ciCmdArgs.ConnectorRegistryGQLUrl) - ctx := context.Background() - - req := graphql.NewRequest(` -query GetConnectorInfo ($name: String!, $namespace: String!) { - hub_registry_connector(where: {_and: [{name: {_eq: $name}}, {namespace: {_eq: $namespace}}]}) { - name - multitenant_connector { - id - } - } -}`) - req.Var("name", connectorName) - req.Var("namespace", connectorNamespace) - - req.Header.Set("x-hasura-role", "connector_publishing_automation") - req.Header.Set("x-connector-publication-key", ciCmdArgs.ConnectorPublicationKey) - - // Execute the GraphQL query and check the response. - if err := client.Run(ctx, req, &respData); err != nil { - return respData, err - } else { - if len(respData.HubRegistryConnector) == 0 { - return respData, nil - } - } - - return respData, nil -} - // buildRegistryPayload builds the payload for the registry upsert API func buildRegistryPayload( + ciCtx Context, connectorNamespace string, connectorName string, version string, connectorVersionMetadata map[string]interface{}, uploadedConnectorDefinitionTgzUrl string, + isNewConnector bool, ) (ConnectorVersion, error) { var connectorVersion ConnectorVersion var connectorVersionDockerImage string = "" @@ -659,15 +640,31 @@ func buildRegistryPayload( } - connectorInfo, err := getConnectorInfoFromRegistry(connectorNamespace, connectorName) + connectorInfo, err := getConnectorInfoFromRegistry(ciCtx.RegistryGQLClient, connectorNamespace, connectorName) if err != nil { return connectorVersion, err } + var isMultitenant bool + // Check if the connector exists in the registry first if len(connectorInfo.HubRegistryConnector) == 0 { - return connectorVersion, fmt.Errorf("Inserting a new connector is not supported yet") + + if isNewConnector { + isMultitenant = false + } else { + return connectorVersion, fmt.Errorf("Unexpected: Couldn't get the connector info of the connector: %s", connectorName) + + } + + } else { + if len(connectorInfo.HubRegistryConnector) == 1 { + // check if the connector is multitenant + isMultitenant = connectorInfo.HubRegistryConnector[0].MultitenantConnector != nil + + } + } var connectorVersionType string @@ -693,65 +690,9 @@ func buildRegistryPayload( Version: version, Image: connectorVersionImage, PackageDefinitionURL: uploadedConnectorDefinitionTgzUrl, - IsMultitenant: connectorInfo.HubRegistryConnector[0].MultitenantConnector != nil, + IsMultitenant: isMultitenant, Type: connectorVersionType, } return connectorVersion, nil } - -func updateRegistryGQL(payload []ConnectorVersion) error { - var respData map[string]interface{} - client := graphql.NewClient(ciCmdArgs.ConnectorRegistryGQLUrl) - ctx := context.Background() - - req := graphql.NewRequest(` -mutation InsertConnectorVersion($connectorVersion: [hub_registry_connector_version_insert_input!]!) { - insert_hub_registry_connector_version(objects: $connectorVersion, on_conflict: {constraint: connector_version_namespace_name_version_key, update_columns: [image, package_definition_url, is_multitenant]}) { - affected_rows - returning { - id - } - } -}`) - // add the payload to the request - req.Var("connectorVersion", payload) - - req.Header.Set("x-hasura-role", "connector_publishing_automation") - req.Header.Set("x-connector-publication-key", ciCmdArgs.ConnectorPublicationKey) - - // Execute the GraphQL query and check the response. - if err := client.Run(ctx, req, &respData); err != nil { - return err - } - - return nil -} - -func updateConnectorOverview(updates ConnectorOverviewUpdates) error { - var respData map[string]interface{} - client := graphql.NewClient(ciCmdArgs.ConnectorRegistryGQLUrl) - ctx := context.Background() - - req := graphql.NewRequest(` -mutation UpdateConnector ($updates: [connector_overview_updates!]!) { - update_connector_overview_many(updates: $updates) { - affected_rows - } -}`) - - // add the payload to the request - req.Var("updates", updates.Updates) - - req.Header.Set("x-hasura-role", "connector_publishing_automation") - req.Header.Set("x-connector-publication-key", ciCmdArgs.ConnectorPublicationKey) - - // Execute the GraphQL query and check the response. - if err := client.Run(ctx, req, &respData); err != nil { - return err - } else { - fmt.Printf("Successfully updated the connector overview: %+v\n", respData) - } - - return nil -} diff --git a/registry-automation/cmd/ci_test.go b/registry-automation/cmd/ci_test.go index e68cabaf..2b160f8a 100644 --- a/registry-automation/cmd/ci_test.go +++ b/registry-automation/cmd/ci_test.go @@ -1,100 +1,230 @@ package cmd import ( + "context" + "testing" + + "github.com/machinebox/graphql" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "cloud.google.com/go/storage" + + "github.com/cloudinary/cloudinary-go/v2/api/uploader" ) -func TestProcessAddedOrModifiedConnectorVersions(t *testing.T) { - // Define test cases +// Mock structures +type MockStorageClient struct { + mock.Mock +} + +func (m *MockStorageClient) Bucket(name string) *storage.BucketHandle { + args := m.Called(name) + return args.Get(0).(*storage.BucketHandle) +} + +type MockCloudinaryUploader struct { + mock.Mock +} + +type MockCloudinary struct { + mock.Mock +} + +func (m *MockCloudinary) Upload(ctx context.Context, file interface{}, uploadParams uploader.UploadParams) (*uploader.UploadResult, error) { + args := m.Called(ctx, file, uploadParams) + return args.Get(0).(*uploader.UploadResult), args.Error(1) +} + +type MockGraphQLClient struct { + mock.Mock +} + +func (m *MockGraphQLClient) Run(ctx context.Context, query *graphql.Request, resp interface{}) error { + args := m.Called(ctx, query, resp) + return args.Error(0) +} + +func createTestContext() Context { + return Context{ + Env: "staging", + RegistryGQLClient: &MockGraphQLClient{}, + StorageClient: &MockStorageClient{}, + Cloudinary: &MockCloudinary{}, + } +} + +// Test processChangedFiles +func TestProcessChangedFiles(t *testing.T) { testCases := []struct { - name string - files []string - expectedAddedOrModifiedConnectors map[string]map[string]string + name string + changedFiles ChangedFiles + expected ProcessedChangedFiles }{ { - name: "Test case 1", - files: []string{ - "registry/hasura/releases/v1.0.0/connector-packaging.json", - "registry/hasura/releases/v2.0.0/connector-packaging.json", - "registry/other/releases/v1.0.0/connector-packaging.json", + name: "New connector added", + changedFiles: ChangedFiles{ + Added: []string{"registry/namespace1/connector1/metadata.json"}, }, - expectedAddedOrModifiedConnectors: map[string]map[string]string{ - "hasura": { - "v1.0.0": "registry/hasura/releases/v1.0.0/connector-packaging.json", - "v2.0.0": "registry/hasura/releases/v2.0.0/connector-packaging.json", - }, - "other": { - "v1.0.0": "registry/other/releases/v1.0.0/connector-packaging.json", - }, + expected: ProcessedChangedFiles{ + NewConnectorVersions: map[Connector]map[string]string{}, + ModifiedLogos: map[Connector]string{}, + ModifiedReadmes: map[Connector]string{}, + NewConnectors: map[Connector]MetadataFile{{Name: "connector1", Namespace: "namespace1"}: "registry/namespace1/connector1/metadata.json"}, + NewLogos: map[Connector]string{}, + NewReadmes: map[Connector]string{}, }, }, { - name: "Test case 2", - files: []string{ - "registry/hasura/releases/v1.0.0/connector-packaging.json", - "registry/hasura/releases/v1.0.0/other-file.json", - }, - expectedAddedOrModifiedConnectors: map[string]map[string]string{ - "hasura": { - "v1.0.0": "registry/hasura/releases/v1.0.0/connector-packaging.json", + name: "Modified logo and README", + changedFiles: ChangedFiles{ + Modified: []string{ + "registry/namespace1/connector1/logo.png", + "registry/namespace1/connector1/README.md", }, }, - }, - { - name: "Test case 3", - files: []string{ - "registry/hasura/releases/v1.0.0/other-file.json", - "registry/other/releases/v1.0.0/connector-packaging.json", - }, - expectedAddedOrModifiedConnectors: map[string]map[string]string{ - "other": { - "v1.0.0": "registry/other/releases/v1.0.0/connector-packaging.json", - }, + expected: ProcessedChangedFiles{ + NewConnectorVersions: map[Connector]map[string]string{}, + ModifiedLogos: map[Connector]string{{Name: "connector1", Namespace: "namespace1"}: "registry/namespace1/connector1/logo.png"}, + ModifiedReadmes: map[Connector]string{{Name: "connector1", Namespace: "namespace1"}: "registry/namespace1/connector1/README.md"}, + NewConnectors: map[Connector]MetadataFile{}, + NewLogos: map[Connector]string{}, + NewReadmes: map[Connector]string{}, }, }, } - // Run the test cases for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - // Initialize the map to store the added or modified connectors - addedOrModifiedConnectorVersions := make(map[string]map[string]string) - - var changedFiles ChangedFiles - - changedFiles.Added = tc.files - - // Call the function under test - processChangedFiles(changedFiles) - - // Compare the actual result with the expected result - if len(addedOrModifiedConnectorVersions) != len(tc.expectedAddedOrModifiedConnectors) { - t.Errorf("Unexpected number of connectors. Expected: %d, Got: %d", len(tc.expectedAddedOrModifiedConnectors), len(addedOrModifiedConnectorVersions)) - } - - for connectorName, versions := range addedOrModifiedConnectorVersions { - expectedVersions, ok := tc.expectedAddedOrModifiedConnectors[connectorName] - if !ok { - t.Errorf("Unexpected connector name: %s", connectorName) - continue - } - - if len(versions) != len(expectedVersions) { - t.Errorf("Unexpected number of versions for connector %s. Expected: %d, Got: %d", connectorName, len(expectedVersions), len(versions)) - } - - for version, connectorVersionPath := range versions { - expectedPath, ok := expectedVersions[version] - if !ok { - t.Errorf("Unexpected version for connector %s: %s", connectorName, version) - continue - } - - if connectorVersionPath != expectedPath { - t.Errorf("Unexpected connector version path for connector %s, version %s. Expected: %s, Got: %s", connectorName, version, expectedPath, connectorVersionPath) - } - } - } + result := processChangedFiles(tc.changedFiles) + assert.Equal(t, tc.expected, result) }) } } + +// func TestProcessNewConnector(t *testing.T) { +// ctx := createTestContext() +// connector := Connector{Name: "testconnector", Namespace: "testnamespace"} + +// // Create a temporary directory for our test files +// tempDir, err := os.MkdirTemp("", "connector-test") +// assert.NoError(t, err) +// defer os.RemoveAll(tempDir) // Clean up after the test + +// // Set up the directory structure +// registryDir := filepath.Join(tempDir, "registry", connector.Namespace, connector.Name) +// err = os.MkdirAll(registryDir, 0755) +// assert.NoError(t, err) + +// // Create the metadata file +// metadataFile := filepath.Join(registryDir, "metadata.json") +// tempMetadata := []byte(`{"overview": {"title": "Test Connector", "description": "A test connector"}, "isVerified": true, "isHostedByHasura": false, "author": {"name": "Test Author", "supportEmail": "support@test.com", "homepage": "https://test.com"}}`) +// err = os.WriteFile(metadataFile, tempMetadata, 0666) +// assert.NoError(t, err) + +// // Create the README file +// readmeFile := filepath.Join(registryDir, "README.md") +// err = os.WriteFile(readmeFile, []byte("# Test Connector"), 0644) +// assert.NoError(t, err) + +// // Mock the necessary functions and API calls +// mockCloudinaryUploader := &MockCloudinaryUploader{} +// mockCloudinaryUploader.On("Upload", mock.Anything, mock.Anything, mock.Anything).Return(&uploader.UploadResult{SecureURL: "https://res.cloudinary.com/demo/image/upload/logo.png"}, nil) + +// mockGraphQLClient := ctx.RegistryGQLClient.(*MockGraphQLClient) +// mockGraphQLClient.On("Run", mock.Anything, mock.Anything, mock.Anything).Return(nil) + +// // Run the function +// connectorOverviewInsert, hubRegistryConnectorInsert, err := processNewConnector(ctx, connector, MetadataFile(metadataFile)) + +// // Assert the results +// assert.NoError(t, err) +// assert.Equal(t, "testconnector", connectorOverviewInsert.Name) +// assert.Equal(t, "testnamespace", connectorOverviewInsert.Namespace) +// assert.Equal(t, "Test Connector", connectorOverviewInsert.Title) +// assert.Equal(t, "A test connector", connectorOverviewInsert.Description) +// assert.True(t, connectorOverviewInsert.IsVerified) +// assert.False(t, connectorOverviewInsert.IsHosted) +// assert.Equal(t, "Test Author", connectorOverviewInsert.Author.Data.Name) +// assert.Equal(t, "support@test.com", connectorOverviewInsert.Author.Data.SupportEmail) +// assert.Equal(t, "https://test.com", connectorOverviewInsert.Author.Data.Website) + +// assert.Equal(t, "testconnector", hubRegistryConnectorInsert.Name) +// assert.Equal(t, "testnamespace", hubRegistryConnectorInsert.Namespace) +// assert.Equal(t, "Test Connector", hubRegistryConnectorInsert.Title) + +// mockCloudinaryUploader.AssertExpectations(t) +// mockGraphQLClient.AssertExpectations(t) +// } + +// // Test uploadConnectorVersionPackage +// func TestUploadConnectorVersionPackage(t *testing.T) { +// ctx := createTestContext() +// connector := Connector{Name: "testconnector", Namespace: "testnamespace"} +// version := "v1.0.0" +// changedConnectorVersionPath := "registry/testnamespace/testconnector/releases/v1.0.0/connector-packaging.json" +// isNewConnector := true + +// // Mock necessary functions +// mockStorageClient := ctx.StorageClient.(*MockStorageClient) +// mockStorageClient.On("Bucket", mock.Anything).Return(&storage.BucketHandle{}) + +// mockGraphQLClient := ctx.RegistryGQLClient.(*MockGraphQLClient) +// mockGraphQLClient.On("Run", mock.Anything, mock.Anything, mock.Anything).Return(nil) + +// // Create temporary files +// err := os.MkdirAll("registry/testnamespace/testconnector/releases/v1.0.0", 0755) +// assert.NoError(t, err) +// defer os.RemoveAll("registry/testnamespace/testconnector") + +// packagingContent := []byte(`{"uri": "https://example.com/testconnector-v1.0.0.tgz"}`) +// err = os.WriteFile(changedConnectorVersionPath, packagingContent, 0644) +// assert.NoError(t, err) + +// // Run the function +// connectorVersion, err := uploadConnectorVersionPackage(ctx, connector, version, changedConnectorVersionPath, isNewConnector) + +// // Assert the results +// assert.NoError(t, err) +// assert.Equal(t, "testconnector", connectorVersion.Name) +// assert.Equal(t, "testnamespace", connectorVersion.Namespace) +// assert.Equal(t, "v1.0.0", connectorVersion.Version) + +// mockStorageClient.AssertExpectations(t) +// mockGraphQLClient.AssertExpectations(t) +// } + +// // Test buildRegistryPayload +// func TestBuildRegistryPayload(t *testing.T) { +// ctx := createTestContext() +// connectorNamespace := "testnamespace" +// connectorName := "testconnector" +// version := "v1.0.0" +// connectorVersionMetadata := map[string]interface{}{ +// "packagingDefinition": map[string]interface{}{ +// "type": "ManagedDockerBuild", +// }, +// } +// uploadedConnectorDefinitionTgzUrl := "https://example.com/test.tgz" +// isNewConnector := true + +// // Mock the GraphQL client +// mockGraphQLClient := ctx.RegistryGQLClient.(*MockGraphQLClient) +// mockGraphQLClient.On("Run", mock.Anything, mock.Anything, mock.Anything).Return(nil) + +// // Run the function +// connectorVersion, err := buildRegistryPayload(ctx, connectorNamespace, connectorName, version, connectorVersionMetadata, uploadedConnectorDefinitionTgzUrl, isNewConnector) + +// // Assert the results +// assert.NoError(t, err) +// assert.Equal(t, connectorNamespace, connectorVersion.Namespace) +// assert.Equal(t, connectorName, connectorVersion.Name) +// assert.Equal(t, version, connectorVersion.Version) +// assert.Equal(t, uploadedConnectorDefinitionTgzUrl, connectorVersion.PackageDefinitionURL) +// assert.Equal(t, "ManagedDockerBuild", connectorVersion.Type) +// assert.False(t, connectorVersion.IsMultitenant) +// assert.Nil(t, connectorVersion.Image) + +// mockGraphQLClient.AssertExpectations(t) +// } diff --git a/registry-automation/cmd/gcp.go b/registry-automation/cmd/gcp.go index 40d41d62..4896a92d 100644 --- a/registry-automation/cmd/gcp.go +++ b/registry-automation/cmd/gcp.go @@ -2,7 +2,6 @@ package cmd import ( - "cloud.google.com/go/storage" "context" "fmt" "io" @@ -10,7 +9,7 @@ import ( ) // deleteFile deletes a file from Google Cloud Storage -func deleteFile(client *storage.Client, bucketName, objectName string) error { +func deleteFile(client StorageClientInterface, bucketName, objectName string) error { bucket := client.Bucket(bucketName) object := bucket.Object(objectName) @@ -19,7 +18,7 @@ func deleteFile(client *storage.Client, bucketName, objectName string) error { // uploadFile uploads a file to Google Cloud Storage // document this function with comments -func uploadFile(client *storage.Client, bucketName, objectName, filePath string) (string, error) { +func uploadFile(client StorageClientInterface, bucketName, objectName, filePath string) (string, error) { bucket := client.Bucket(bucketName) object := bucket.Object(objectName) newCtx := context.Background() diff --git a/registry-automation/cmd/registry_db.go b/registry-automation/cmd/registry_db.go new file mode 100644 index 00000000..bad68d36 --- /dev/null +++ b/registry-automation/cmd/registry_db.go @@ -0,0 +1,283 @@ +package cmd + +import ( + "context" + "fmt" + + "github.com/machinebox/graphql" +) + +type HubRegistryConnectorInsertInput struct { + Name string `json:"name"` + Title string `json:"title"` + Namespace string `json:"namespace"` +} + +type NewConnectorsInsertInput struct { + HubRegistryConnectors []HubRegistryConnectorInsertInput `json:"hub_registry_connectors"` + ConnectorOverviews []ConnectorOverviewInsert `json:"connector_overviews"` +} + +// struct to store the response of teh GetConnectorInfo query +type GetConnectorInfoResponse struct { + HubRegistryConnector []struct { + Name string `json:"name"` + MultitenantConnector *struct { + ID string `json:"id"` + } `json:"multitenant_connector"` + } `json:"hub_registry_connector"` +} + +func insertHubRegistryConnector(client graphql.Client, newConnectors NewConnectorsInsertInput) error { + var respData map[string]interface{} + + ctx := context.Background() + + req := graphql.NewRequest(` +mutation InsertHubRegistryConnector ($hub_registry_connectors:[hub_registry_connector_insert_input!]!, $connector_overview_objects: [connector_overview_insert_input!]!){ + + insert_hub_registry_connector(objects: $hub_registry_connectors) { +affected_rows + } + insert_connector_overview(objects: $connector_overview_objects) { + affected_rows + } +} +`) + + // add the payload to the request + req.Var("hub_registry_connectors", newConnectors.HubRegistryConnectors) + req.Var("connectors_overviews", newConnectors.ConnectorOverviews) + + // set the headers + req.Header.Set("x-hasura-role", "connector_publishing_automation") + req.Header.Set("x-connector-publication-key", ciCmdArgs.ConnectorPublicationKey) + + // Execute the GraphQL query and check the response. + if err := client.Run(ctx, req, &respData); err != nil { + return err + } else { + connectorNames := make([]string, 0) + for _, connector := range newConnectors.HubRegistryConnectors { + connectorNames = append(connectorNames, fmt.Sprintf("%s/%s", connector.Namespace, connector.Name)) + } + fmt.Printf("Successfully inserted the following connectors in the registry: %+v\n", connectorNames) + } + + return nil +} + +func getConnectorInfoFromRegistry(client GraphQLClientInterface, connectorNamespace string, connectorName string) (GetConnectorInfoResponse, error) { + var respData GetConnectorInfoResponse + + ctx := context.Background() + + req := graphql.NewRequest(` +query GetConnectorInfo ($name: String!, $namespace: String!) { + hub_registry_connector(where: {_and: [{name: {_eq: $name}}, {namespace: {_eq: $namespace}}]}) { + name + multitenant_connector { + id + } + } +}`) + req.Var("name", connectorName) + req.Var("namespace", connectorNamespace) + + req.Header.Set("x-hasura-role", "connector_publishing_automation") + req.Header.Set("x-connector-publication-key", ciCmdArgs.ConnectorPublicationKey) + + // Execute the GraphQL query and check the response. + if err := client.Run(ctx, req, &respData); err != nil { + return respData, err + } else { + if len(respData.HubRegistryConnector) == 0 { + return respData, nil + } + } + + return respData, nil +} + +func updateRegistryGQL(client graphql.Client, payload []ConnectorVersion) error { + var respData map[string]interface{} + + ctx := context.Background() + + req := graphql.NewRequest(` +mutation InsertConnectorVersion($connectorVersion: [hub_registry_connector_version_insert_input!]!) { + insert_hub_registry_connector_version(objects: $connectorVersion, on_conflict: {constraint: connector_version_namespace_name_version_key, update_columns: [image, package_definition_url, is_multitenant]}) { + affected_rows + returning { + id + } + } +}`) + // add the payload to the request + req.Var("connectorVersion", payload) + + req.Header.Set("x-hasura-role", "connector_publishing_automation") + req.Header.Set("x-connector-publication-key", ciCmdArgs.ConnectorPublicationKey) + + // Execute the GraphQL query and check the response. + if err := client.Run(ctx, req, &respData); err != nil { + return err + } + + return nil +} + +func updateConnectorOverview(updates ConnectorOverviewUpdates) error { + var respData map[string]interface{} + client := graphql.NewClient(ciCmdArgs.ConnectorRegistryGQLUrl) + ctx := context.Background() + + req := graphql.NewRequest(` +mutation UpdateConnector ($updates: [connector_overview_updates!]!) { + update_connector_overview_many(updates: $updates) { + affected_rows + } +}`) + + // add the payload to the request + req.Var("updates", updates.Updates) + + req.Header.Set("x-hasura-role", "connector_publishing_automation") + req.Header.Set("x-connector-publication-key", ciCmdArgs.ConnectorPublicationKey) + + // Execute the GraphQL query and check the response. + if err := client.Run(ctx, req, &respData); err != nil { + return err + } else { + fmt.Printf("Successfully updated the connector overview: %+v\n", respData) + } + + return nil +} + +type ConnectorAuthorNestedInsertOnConflict struct { + Constraint string `json:"constraint"` + UpdateCols []string `json:"update_columns,omitempty"` +} + +type ConnectorAuthorNestedInsert struct { + Data ConnectorAuthor `json:"data"` + OnConflict *ConnectorAuthorNestedInsertOnConflict `json:"on_conflict,omitempty"` +} + +type ConnectorOverviewInsert struct { + Namespace string `json:"namespace"` + Name string `json:"name"` + Title string `json:"title"` + Description string `json:"description"` + Logo string `json:"logo"` + Docs string `json:"docs"` + IsVerified bool `json:"is_verified"` + IsHosted bool `json:"is_hosted_by_hasura"` + Author ConnectorAuthorNestedInsert `json:"author"` +} + +type ConnectorAuthor struct { + Name string `json:"name"` + SupportEmail string `json:"support_email"` + Website string `json:"website"` +} + +// registryDbMutation is a function to insert data into the registry database, all the mutations are done in a single transaction. +func registryDbMutation(client GraphQLClientInterface, newConnectors NewConnectorsInsertInput, connectorOverviewUpdates []ConnectorOverviewUpdate, connectorVersionInserts []ConnectorVersion) error { + var respData map[string]interface{} + ctx := context.Background() + mutationQuery := ` +mutation HubRegistryMutationRequest ( + $hub_registry_connectors:[hub_registry_connector_insert_input!]!, + $connector_overview_inserts: [connector_overview_insert_input!]!, + $connector_overview_updates: [connector_overview_updates!]!, + $connector_version_inserts: [hub_registry_connector_version_insert_input!]! +){ + + insert_hub_registry_connector(objects: $hub_registry_connectors) { +affected_rows + } + insert_connector_overview(objects: $connector_overview_inserts) { + affected_rows + } + insert_hub_registry_connector_version(objects: $connector_version_inserts, on_conflict: {constraint: connector_version_namespace_name_version_key, update_columns: [image, package_definition_url, is_multitenant]}) { + affected_rows + } + + update_connector_overview_many(updates: $connector_overview_updates) { + affected_rows + } +} +` + req := graphql.NewRequest(mutationQuery) + req.Var("hub_registry_connectors", newConnectors.HubRegistryConnectors) + req.Var("connector_overview_inserts", newConnectors.ConnectorOverviews) + req.Var("connector_overview_updates", connectorOverviewUpdates) + req.Var("connector_version_inserts", connectorVersionInserts) + + req.Header.Set("x-hasura-role", "connector_publishing_automation") + req.Header.Set("x-connector-publication-key", ciCmdArgs.ConnectorPublicationKey) + + // Execute the GraphQL query and check the response. + if err := client.Run(ctx, req, &respData); err != nil { + return err + } + + return nil + +} + +// registryDbMutation is a function to insert data into the registry database, all the mutations are done in a single transaction. +func registryDbMutationStaging(client GraphQLClientInterface, newConnectors NewConnectorsInsertInput, connectorOverviewUpdates []ConnectorOverviewUpdate, connectorVersionInserts []ConnectorVersion) error { + var respData map[string]interface{} + ctx := context.Background() + mutationQuery := ` +mutation HubRegistryMutationRequest ( + $hub_registry_connectors:[hub_registry_connector_insert_input!]!, + $connector_overview_inserts: [connector_overview_insert_input!]!, + $connector_overview_updates: [connector_overview_updates!]!, + $connector_version_inserts: [hub_registry_connector_version_insert_input!]! +){ + + insert_hub_registry_connector(objects: $hub_registry_connectors, on_conflict: {constraint: connector_pkey}) { +affected_rows + } + insert_connector_overview(objects: $connector_overview_inserts, on_conflict: {constraint: connector_overview_pkey, update_columns: [docs, logo]}) { + affected_rows + } + insert_hub_registry_connector_version(objects: $connector_version_inserts, on_conflict: {constraint: connector_version_namespace_name_version_key, update_columns: [image, package_definition_url, is_multitenant]}) { + affected_rows + } + + update_connector_overview_many(updates: $connector_overview_updates) { + affected_rows + } +} +` + + // update newConnectors.ConnectorOverviews to have on_conflict + for i := range newConnectors.ConnectorOverviews { + newConnectors.ConnectorOverviews[i].Author.OnConflict = &ConnectorAuthorNestedInsertOnConflict{ + Constraint: "connector_author_connector_title_key", + UpdateCols: []string{}, + } + } + + req := graphql.NewRequest(mutationQuery) + req.Var("hub_registry_connectors", newConnectors.HubRegistryConnectors) + req.Var("connector_overview_inserts", newConnectors.ConnectorOverviews) + req.Var("connector_overview_updates", connectorOverviewUpdates) + req.Var("connector_version_inserts", connectorVersionInserts) + + req.Header.Set("x-hasura-role", "connector_publishing_automation") + req.Header.Set("x-connector-publication-key", ciCmdArgs.ConnectorPublicationKey) + + // Execute the GraphQL query and check the response. + if err := client.Run(ctx, req, &respData); err != nil { + return err + } + + return nil + +} diff --git a/registry-automation/cmd/types.go b/registry-automation/cmd/types.go new file mode 100644 index 00000000..ae51bbf6 --- /dev/null +++ b/registry-automation/cmd/types.go @@ -0,0 +1,185 @@ +package cmd + +import ( + "cloud.google.com/go/storage" + "context" + "encoding/json" + "github.com/cloudinary/cloudinary-go/v2" + "github.com/cloudinary/cloudinary-go/v2/api/uploader" + "github.com/machinebox/graphql" +) + +type ChangedFiles struct { + Added []string `json:"added_files"` + Modified []string `json:"modified_files"` + Deleted []string `json:"deleted_files"` +} + +// ConnectorVersion represents a version of a connector, this type is +// used to insert a new version of a connector in the registry. +type ConnectorVersion struct { + // Namespace of the connector, e.g. "hasura" + Namespace string `json:"namespace"` + // Name of the connector, e.g. "mongodb" + Name string `json:"name"` + // Semantic version of the connector version, e.g. "v1.0.0" + Version string `json:"version"` + // Docker image of the connector version (optional) + // This field is only required if the connector version is of type `PrebuiltDockerImage` + Image *string `json:"image,omitempty"` + // URL to the connector's metadata + PackageDefinitionURL string `json:"package_definition_url"` + // Is the connector version multitenant? + IsMultitenant bool `json:"is_multitenant"` + // Type of the connector packaging `PrebuiltDockerImage`/`ManagedDockerBuild` + Type string `json:"type"` +} + +// Create a struct with the following fields: +// type string +// image *string (optional) +type ConnectionVersionMetadata struct { + Type string `yaml:"type"` + Image *string `yaml:"image,omitempty"` +} + +type WhereClause struct { + ConnectorName string + ConnectorNamespace string +} + +func (wc WhereClause) MarshalJSON() ([]byte, error) { + where := map[string]interface{}{ + "_and": []map[string]interface{}{ + {"name": map[string]string{"_eq": wc.ConnectorName}}, + {"namespace": map[string]string{"_eq": wc.ConnectorNamespace}}, + }, + } + return json.Marshal(where) +} + +type ConnectorOverviewUpdate struct { + Set struct { + Docs *string `json:"docs,omitempty"` + Logo *string `json:"logo,omitempty"` + } `json:"_set"` + Where WhereClause `json:"where"` +} + +type ConnectorOverviewUpdates struct { + Updates []ConnectorOverviewUpdate `json:"updates"` +} + +const ( + ManagedDockerBuild = "ManagedDockerBuild" + PrebuiltDockerImage = "PrebuiltDockerImage" +) + +// Type to represent the metadata.json file +type ConnectorMetadata struct { + Overview struct { + Namespace string `json:"namespace"` + Description string `json:"description"` + Title string `json:"title"` + Logo string `json:"logo"` + Tags []string `json:"tags"` + LatestVersion string `json:"latest_version"` + } `json:"overview"` + Author struct { + SupportEmail string `json:"support_email"` + Homepage string `json:"homepage"` + Name string `json:"name"` + } `json:"author"` + + IsVerified bool `json:"is_verified"` + IsHostedByHasura bool `json:"is_hosted_by_hasura"` + HasuraHubConnector struct { + Namespace string `json:"namespace"` + Name string `json:"name"` + } `json:"hasura_hub_connector"` + SourceCode struct { + IsOpenSource bool `json:"is_open_source"` + Repository string `json:"repository"` + } `json:"source_code"` +} + +// Make a struct with the fields expected in the command line arguments +type ConnectorRegistryArgs struct { + ChangedFilesPath string + PublicationEnv string + ConnectorRegistryGQLUrl string + ConnectorPublicationKey string + GCPServiceAccountDetails string + GCPBucketName string + CloudinaryUrl string +} + +type MetadataFile string + +type NewConnectors map[Connector]MetadataFile + +type ProcessedChangedFiles struct { + NewConnectorVersions NewConnectorVersions + ModifiedLogos ModifiedLogos + ModifiedReadmes ModifiedReadmes + NewConnectors NewConnectors + NewLogos NewLogos + NewReadmes NewReadmes +} + +type GraphQLClientInterface interface { + Run(ctx context.Context, req *graphql.Request, resp interface{}) error +} + +type StorageClientWrapper struct { + *storage.Client +} + +func (s *StorageClientWrapper) Bucket(name string) *storage.BucketHandle { + return s.Client.Bucket(name) +} + +type StorageClientInterface interface { + Bucket(name string) *storage.BucketHandle +} + +type CloudinaryInterface interface { + Upload(ctx context.Context, file interface{}, uploadParams uploader.UploadParams) (*uploader.UploadResult, error) +} + +type CloudinaryWrapper struct { + *cloudinary.Cloudinary +} + +func (c *CloudinaryWrapper) Upload(ctx context.Context, file interface{}, uploadParams uploader.UploadParams) (*uploader.UploadResult, error) { + return c.Cloudinary.Upload.Upload(ctx, file, uploadParams) +} + +// + +type Context struct { + Env string + RegistryGQLClient GraphQLClientInterface + StorageClient StorageClientInterface + Cloudinary CloudinaryInterface +} + +// Type that uniquely identifies a connector +type Connector struct { + Name string `json:"name"` + Namespace string `json:"namespace"` +} + +type NewConnectorVersions map[Connector]map[string]string + +// ModifiedLogos represents the modified logos in the PR, the key is the connector name and the value is the path to the modified logo +type ModifiedLogos map[Connector]string + +// ModifiedReadmes represents the modified READMEs in the PR, the key is the connector name and the value is the path to the modified README +type ModifiedReadmes map[Connector]string + +// ModifiedLogos represents the modified logos in the PR, the key is the connector name and the value is the path to the modified logo +type NewLogos map[Connector]string + +// ModifiedReadmes represents the modified READMEs in the PR, the key is the connector name and the value is the path to the modified README +type NewReadmes map[Connector]string diff --git a/registry-automation/cmd/utils.go b/registry-automation/cmd/utils.go index 45af32a3..178ec188 100644 --- a/registry-automation/cmd/utils.go +++ b/registry-automation/cmd/utils.go @@ -3,6 +3,7 @@ package cmd import ( "encoding/json" "fmt" + "gopkg.in/yaml.v2" "io" "net/http" "os" @@ -120,3 +121,30 @@ func extractTarGz(src, dest string) (string, error) { return fmt.Sprintf("%s/.hasura-connector/connector-metadata.yaml", filepath), nil } + +// Write a function that accepts a file path to a YAML file and returns +// the contents of the file as a map[string]interface{}. +// readYAMLFile accepts a file path to a YAML file and returns the contents of the file as a map[string]interface{}. +func readYAMLFile(filePath string) (map[string]interface{}, error) { + // Open the file + file, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + + // Read the file contents + data, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + // Unmarshal the YAML contents into a map + var result map[string]interface{} + err = yaml.Unmarshal(data, &result) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal YAML: %w", err) + } + + return result, nil +} diff --git a/registry-automation/go.mod b/registry-automation/go.mod index e2eca410..2c03147d 100644 --- a/registry-automation/go.mod +++ b/registry-automation/go.mod @@ -5,12 +5,17 @@ go 1.21.4 require ( github.com/cloudinary/cloudinary-go/v2 v2.8.0 github.com/spf13/cobra v1.8.0 + github.com/stretchr/testify v1.9.0 ) require ( github.com/creasty/defaults v1.7.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gorilla/schema v1.4.1 // indirect github.com/matryer/is v1.4.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( diff --git a/registry-automation/go.sum b/registry-automation/go.sum index c06e0b27..f44c7142 100644 --- a/registry-automation/go.sum +++ b/registry-automation/go.sum @@ -92,6 +92,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= From ca5c65cdda9d997c6776820ce37194823aff6319 Mon Sep 17 00:00:00 2001 From: Toan Nguyen Date: Fri, 27 Sep 2024 16:46:13 +0700 Subject: [PATCH 08/11] Add ndc-go v1.4.0 (#312) --- registry/hasura/go/metadata.json | 7 ++++++- .../go/releases/v1.4.0/connector-packaging.json | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 registry/hasura/go/releases/v1.4.0/connector-packaging.json diff --git a/registry/hasura/go/metadata.json b/registry/hasura/go/metadata.json index 02719841..87f4b21f 100644 --- a/registry/hasura/go/metadata.json +++ b/registry/hasura/go/metadata.json @@ -5,7 +5,7 @@ "title": "Go Connector", "logo": "logo.svg", "tags": [], - "latest_version": "v1.3.1" + "latest_version": "v1.4.0" }, "author": { "support_email": "support@hasura.io", @@ -18,6 +18,11 @@ "is_open_source": true, "repository": "https://github.com/hasura/ndc-sdk-go", "version": [ + { + "tag": "v1.4.0", + "hash": "c31575e0eb0c55e0f326c859f7c1a830dc09c424", + "is_verified": true + }, { "tag": "v1.3.1", "hash": "f6093ca96fe64063df786159a2bb061c01d9197d", diff --git a/registry/hasura/go/releases/v1.4.0/connector-packaging.json b/registry/hasura/go/releases/v1.4.0/connector-packaging.json new file mode 100644 index 00000000..a0c4264b --- /dev/null +++ b/registry/hasura/go/releases/v1.4.0/connector-packaging.json @@ -0,0 +1,11 @@ +{ + "version": "v1.4.0", + "uri": "https://github.com/hasura/ndc-sdk-go/releases/download/v1.4.0/connector-definition.tgz", + "checksum": { + "type": "sha256", + "value": "d599976b66b18c241eb6bef9baf8c77d15d32c41db4392d198887ef848893e6f" + }, + "source": { + "hash": "c31575e0eb0c55e0f326c859f7c1a830dc09c424" + } +} From f6e9e8fe0358c922b9053ec1fc5cda1d7a1d3ebd Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Fri, 27 Sep 2024 15:34:30 +0530 Subject: [PATCH 09/11] Fix bug in the automation publishing if there are no new connectors to add (#313) --- registry-automation/cmd/ci.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/registry-automation/cmd/ci.go b/registry-automation/cmd/ci.go index 11847204..e6c38895 100644 --- a/registry-automation/cmd/ci.go +++ b/registry-automation/cmd/ci.go @@ -331,11 +331,12 @@ func runCI(cmd *cobra.Command, args []string) { newlyAddedConnectors := processChangedFiles.NewConnectors var newConnectorsToBeAdded NewConnectorsInsertInput - var newConnectorVersionsToBeAdded []ConnectorVersion - + newConnectorsToBeAdded.HubRegistryConnectors = make([]HubRegistryConnectorInsertInput, 0) + newConnectorsToBeAdded.ConnectorOverviews = make([]ConnectorOverviewInsert, 0) newConnectorOverviewsToBeAdded := make([](ConnectorOverviewInsert), 0) hubRegistryConnectorsToBeAdded := make([](HubRegistryConnectorInsertInput), 0) connectorOverviewUpdates := make([]ConnectorOverviewUpdate, 0) + newConnectorVersionsToBeAdded := make([]ConnectorVersion, 0) if len(newlyAddedConnectors) > 0 { fmt.Println("New connectors to be added to the registry: ", newlyAddedConnectors) From 4957217e92cc3116c6af47edd993e3193869d20f Mon Sep 17 00:00:00 2001 From: Karthikeyan Chinnakonda Date: Fri, 27 Sep 2024 15:42:12 +0530 Subject: [PATCH 10/11] revert back the changes in #312 (#314) The changes will be added back in a new PR to test out the connector publishing workflow. --- registry/hasura/go/metadata.json | 2 +- .../go/releases/v1.4.0/connector-packaging.json | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 registry/hasura/go/releases/v1.4.0/connector-packaging.json diff --git a/registry/hasura/go/metadata.json b/registry/hasura/go/metadata.json index 87f4b21f..fa065c8b 100644 --- a/registry/hasura/go/metadata.json +++ b/registry/hasura/go/metadata.json @@ -5,7 +5,7 @@ "title": "Go Connector", "logo": "logo.svg", "tags": [], - "latest_version": "v1.4.0" + "latest_version": "v1.3.1" }, "author": { "support_email": "support@hasura.io", diff --git a/registry/hasura/go/releases/v1.4.0/connector-packaging.json b/registry/hasura/go/releases/v1.4.0/connector-packaging.json deleted file mode 100644 index a0c4264b..00000000 --- a/registry/hasura/go/releases/v1.4.0/connector-packaging.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "v1.4.0", - "uri": "https://github.com/hasura/ndc-sdk-go/releases/download/v1.4.0/connector-definition.tgz", - "checksum": { - "type": "sha256", - "value": "d599976b66b18c241eb6bef9baf8c77d15d32c41db4392d198887ef848893e6f" - }, - "source": { - "hash": "c31575e0eb0c55e0f326c859f7c1a830dc09c424" - } -} From 9152eb6a38d564c70c53420bcb8df2bce2973525 Mon Sep 17 00:00:00 2001 From: Toan Nguyen Date: Fri, 27 Sep 2024 17:18:20 +0700 Subject: [PATCH 11/11] add ndc-go v1.4.0 (#315) --- registry/hasura/go/metadata.json | 2 +- .../go/releases/v1.4.0/connector-packaging.json | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 registry/hasura/go/releases/v1.4.0/connector-packaging.json diff --git a/registry/hasura/go/metadata.json b/registry/hasura/go/metadata.json index fa065c8b..87f4b21f 100644 --- a/registry/hasura/go/metadata.json +++ b/registry/hasura/go/metadata.json @@ -5,7 +5,7 @@ "title": "Go Connector", "logo": "logo.svg", "tags": [], - "latest_version": "v1.3.1" + "latest_version": "v1.4.0" }, "author": { "support_email": "support@hasura.io", diff --git a/registry/hasura/go/releases/v1.4.0/connector-packaging.json b/registry/hasura/go/releases/v1.4.0/connector-packaging.json new file mode 100644 index 00000000..a0c4264b --- /dev/null +++ b/registry/hasura/go/releases/v1.4.0/connector-packaging.json @@ -0,0 +1,11 @@ +{ + "version": "v1.4.0", + "uri": "https://github.com/hasura/ndc-sdk-go/releases/download/v1.4.0/connector-definition.tgz", + "checksum": { + "type": "sha256", + "value": "d599976b66b18c241eb6bef9baf8c77d15d32c41db4392d198887ef848893e6f" + }, + "source": { + "hash": "c31575e0eb0c55e0f326c859f7c1a830dc09c424" + } +}