From 3643286278bbb26aa63b5ed983ad65096531429e Mon Sep 17 00:00:00 2001 From: Natoandro Date: Mon, 9 Dec 2024 22:45:17 +0300 Subject: [PATCH] feat: injection on output types (outjection) (#935) - [x] Solve [MET-140](https://linear.app/metatypedev/issue/MET-140/gate-computestage-processing-from-context-should-evaluate-in-all-cases) - Enable injection on output - [x] Solve [MET-47](https://linear.app/metatypedev/issue/MET-47/gate-from-parent-for-eitherunion) - test from parent injection for either/union types #### Migration notes - _N/A_ --- - [x] The change comes with new or modified tests - [ ] Hard-to-understand functions have explanatory comments - [ ] End-user documentation is updated to reflect the change --- .ghjk/lock.json | 24 +++--- deno.lock | 25 ++++++ ghjk.ts | 2 +- import_map.json | 3 +- src/common/src/typegraph/mod.rs | 3 + src/common/src/typegraph/types.rs | 23 ++++++ src/common/src/typegraph/validator/mod.rs | 20 +++++ src/metagen/src/client_rs/mod.rs | 2 +- src/metagen/src/fdk_rust/stubs.rs | 1 + src/metagen/src/tests/fixtures.rs | 1 + src/typegate/src/engine/injection/dynamic.ts | 7 ++ src/typegate/src/engine/planner/args.ts | 11 +-- .../src/engine/planner/injection_utils.ts | 10 +++ src/typegate/src/engine/planner/mod.ts | 64 +++++++++++++-- src/typegate/src/engine/typecheck/result.ts | 5 ++ src/typegate/src/runtimes/deno/deno.ts | 81 +++++++++++++++++-- src/typegate/src/runtimes/graphql.ts | 2 +- src/typegate/src/runtimes/random.ts | 2 +- src/typegate/src/runtimes/typegraph.ts | 5 +- src/typegate/src/typegraph/mod.ts | 18 +++-- src/typegate/src/typegraph/types.ts | 4 +- ...core__tests__successful_serialization.snap | 1 - src/typegraph/core/src/typedef/func.rs | 11 ++- src/typegraph/core/src/typegraph.rs | 3 +- .../core/src/types/type_ref/injection.rs | 8 ++ .../__snapshots__/typegraph_test.ts.snap | 12 --- tests/injection/injection.py | 59 +++++++++++++- tests/injection/injection.ts | 20 ++++- tests/injection/injection_test.ts | 30 +++++++ tests/injection/nested_context.py | 24 ++++-- tests/injection/outjection.py | 33 ++++++++ tests/injection/outjection_test.ts | 51 ++++++++++++ tests/injection/random_injection.py | 69 ++++++++++++++-- tests/injection/random_injection.ts | 25 +++++- .../__snapshots__/planner_test.ts.snap | 1 - .../typename/prisma/migration_lock.toml | 2 +- .../__snapshots__/query_parsers_test.ts.snap | 4 - .../__snapshots__/graphql_test.ts.snap | 5 -- .../grpc/__snapshots__/grpc_test.ts.snap | 2 - .../runtimes/kv/__snapshots__/kv_test.ts.snap | 10 --- .../runtimes/s3/__snapshots__/s3_test.ts.snap | 5 -- .../__snapshots__/temporal_test.ts.snap | 7 -- tests/utils/bindings_test.ts | 10 ++- tools/test.ts | 25 +++++- 44 files changed, 611 insertions(+), 119 deletions(-) create mode 100644 src/typegate/src/engine/injection/dynamic.ts create mode 100644 tests/injection/outjection.py create mode 100644 tests/injection/outjection_test.ts diff --git a/.ghjk/lock.json b/.ghjk/lock.json index dc28719b40..98ab0e57f8 100644 --- a/.ghjk/lock.json +++ b/.ghjk/lock.json @@ -724,6 +724,12 @@ "crateName": "cargo-udeps", "locked": true, "specifiedVersion": true + }, + "bciqb53vnbgb6fcovjas7p4vhxkqbqicchrmz4g6cz2c2ffj2lqaog3i": { + "version": "v9.15.0", + "buildDepConfigs": {}, + "portRef": "pnpm_ghrel@0.1.0", + "specifiedVersion": true } } }, @@ -772,7 +778,7 @@ "bciqkgncxbauys2qfguplxcz2auxrcyamj4b6htqk2fqvohfm3afd7sa", "bciqihcmo6l5uwzih3e3ujc55curep4arfomo6rzkdsfim74unxexiqy", "bciqezep4ufkgwesldlm5etyfkgdsiickfudx7cosydcz6xtgeorn2hy", - "bciqaixkkacuuligsvtjcfdfgjgl65owtyspiiljb3vmutlgymecsiwq", + "bciqlt3rqqcn2tgexcgf7ndjceavwycoddgtn63fkh6z6i6pgmz7jr6y", "bciqlt27ioikxnpkqq37hma7ibn5e5wpzfarbvoh77zwdkarwghtvzxa", "bciqojan3zglnfctnmqyxvnxaha46yrnlhj77j3kw4mxadvauqepqdba", "bciqcnbruy2q6trpvia52n2yis4t27taoz4mxkeguqz5aif7ex6rp26y", @@ -808,7 +814,7 @@ "bciqkgncxbauys2qfguplxcz2auxrcyamj4b6htqk2fqvohfm3afd7sa", "bciqihcmo6l5uwzih3e3ujc55curep4arfomo6rzkdsfim74unxexiqy", "bciqezep4ufkgwesldlm5etyfkgdsiickfudx7cosydcz6xtgeorn2hy", - "bciqaixkkacuuligsvtjcfdfgjgl65owtyspiiljb3vmutlgymecsiwq", + "bciqlt3rqqcn2tgexcgf7ndjceavwycoddgtn63fkh6z6i6pgmz7jr6y", "bciqlt27ioikxnpkqq37hma7ibn5e5wpzfarbvoh77zwdkarwghtvzxa", "bciqojan3zglnfctnmqyxvnxaha46yrnlhj77j3kw4mxadvauqepqdba", "bciqcnbruy2q6trpvia52n2yis4t27taoz4mxkeguqz5aif7ex6rp26y", @@ -820,7 +826,7 @@ "ghjkEnvProvInstSet____ecma": { "installs": [ "bciqezep4ufkgwesldlm5etyfkgdsiickfudx7cosydcz6xtgeorn2hy", - "bciqaixkkacuuligsvtjcfdfgjgl65owtyspiiljb3vmutlgymecsiwq", + "bciqlt3rqqcn2tgexcgf7ndjceavwycoddgtn63fkh6z6i6pgmz7jr6y", "bciqlt27ioikxnpkqq37hma7ibn5e5wpzfarbvoh77zwdkarwghtvzxa", "bciqminqcmgw3fbbhibwc7tf6mrupttheic7kpiykadbowqmnzhmzo5a" ], @@ -865,7 +871,7 @@ "ghjkEnvProvInstSet_______task_env_dev-website": { "installs": [ "bciqezep4ufkgwesldlm5etyfkgdsiickfudx7cosydcz6xtgeorn2hy", - "bciqaixkkacuuligsvtjcfdfgjgl65owtyspiiljb3vmutlgymecsiwq", + "bciqlt3rqqcn2tgexcgf7ndjceavwycoddgtn63fkh6z6i6pgmz7jr6y", "bciqlt27ioikxnpkqq37hma7ibn5e5wpzfarbvoh77zwdkarwghtvzxa", "bciqminqcmgw3fbbhibwc7tf6mrupttheic7kpiykadbowqmnzhmzo5a", "bciqfpjzi6gguk7dafyicfjpzpwtybgyc2dsnxg2zxkcmyinzy7abpla", @@ -880,7 +886,7 @@ "bciqlubbahrp4pxohyffmn5yj52atjgmn5nxepmkdev6wtmvpbx7kr7y", "bciqminqcmgw3fbbhibwc7tf6mrupttheic7kpiykadbowqmnzhmzo5a", "bciqezep4ufkgwesldlm5etyfkgdsiickfudx7cosydcz6xtgeorn2hy", - "bciqaixkkacuuligsvtjcfdfgjgl65owtyspiiljb3vmutlgymecsiwq", + "bciqlt3rqqcn2tgexcgf7ndjceavwycoddgtn63fkh6z6i6pgmz7jr6y", "bciqlt27ioikxnpkqq37hma7ibn5e5wpzfarbvoh77zwdkarwghtvzxa", "bciqfpjzi6gguk7dafyicfjpzpwtybgyc2dsnxg2zxkcmyinzy7abpla", "bciqkgncxbauys2qfguplxcz2auxrcyamj4b6htqk2fqvohfm3afd7sa", @@ -894,7 +900,7 @@ "bciqlubbahrp4pxohyffmn5yj52atjgmn5nxepmkdev6wtmvpbx7kr7y", "bciqminqcmgw3fbbhibwc7tf6mrupttheic7kpiykadbowqmnzhmzo5a", "bciqezep4ufkgwesldlm5etyfkgdsiickfudx7cosydcz6xtgeorn2hy", - "bciqaixkkacuuligsvtjcfdfgjgl65owtyspiiljb3vmutlgymecsiwq", + "bciqlt3rqqcn2tgexcgf7ndjceavwycoddgtn63fkh6z6i6pgmz7jr6y", "bciqlt27ioikxnpkqq37hma7ibn5e5wpzfarbvoh77zwdkarwghtvzxa", "bciqfpjzi6gguk7dafyicfjpzpwtybgyc2dsnxg2zxkcmyinzy7abpla", "bciqkgncxbauys2qfguplxcz2auxrcyamj4b6htqk2fqvohfm3afd7sa", @@ -939,7 +945,7 @@ "bciqpu7gxs3zm7i4gwp3m3cfdxwz27ixvsykdnbxrl5m5mt3xbb3b4la", "bciqjme7csfq43oenkrsakdhaha34hgy6vdwkfffki2ank3kf6mjcguq", "bciqezep4ufkgwesldlm5etyfkgdsiickfudx7cosydcz6xtgeorn2hy", - "bciqaixkkacuuligsvtjcfdfgjgl65owtyspiiljb3vmutlgymecsiwq", + "bciqlt3rqqcn2tgexcgf7ndjceavwycoddgtn63fkh6z6i6pgmz7jr6y", "bciqlt27ioikxnpkqq37hma7ibn5e5wpzfarbvoh77zwdkarwghtvzxa" ], "allowedBuildDeps": "bciqek3tmrhm4iohl6tvdzlhxwhv7b52makvvgehltxv52d3l7rbki3y" @@ -2843,8 +2849,8 @@ "moduleSpecifier": "https://raw.githubusercontent.com/metatypedev/ghjk/v0.2.1/ports/node.ts" } }, - "bciqaixkkacuuligsvtjcfdfgjgl65owtyspiiljb3vmutlgymecsiwq": { - "version": "v9.4.0", + "bciqlt3rqqcn2tgexcgf7ndjceavwycoddgtn63fkh6z6i6pgmz7jr6y": { + "version": "v9.15.0", "port": { "ty": "denoWorker@v1", "name": "pnpm_ghrel", diff --git a/deno.lock b/deno.lock index 9e243f308b..cfd6968f5d 100644 --- a/deno.lock +++ b/deno.lock @@ -42,6 +42,7 @@ "jsr:@std/yaml@^1.0.4": "jsr:@std/yaml@1.0.5", "npm:@noble/hashes@1.4.0": "npm:@noble/hashes@1.4.0", "npm:@sentry/node@7.70.0": "npm:@sentry/node@7.70.0", + "npm:@sinonjs/fake-timers@13.0.5": "npm:@sinonjs/fake-timers@13.0.5", "npm:@types/node": "npm:@types/node@18.16.19", "npm:chance@1.1.11": "npm:chance@1.1.11", "npm:graphql@16.8.1": "npm:graphql@16.8.1", @@ -229,6 +230,18 @@ "tslib": "tslib@2.7.0" } }, + "@sinonjs/commons@3.0.1": { + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dependencies": { + "type-detect": "type-detect@4.0.8" + } + }, + "@sinonjs/fake-timers@13.0.5": { + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dependencies": { + "@sinonjs/commons": "@sinonjs/commons@3.0.1" + } + }, "@types/node@18.16.19": { "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", "dependencies": {} @@ -284,6 +297,10 @@ "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "dependencies": {} }, + "type-detect@4.0.8": { + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dependencies": {} + }, "validator@13.12.0": { "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", "dependencies": {} @@ -764,6 +781,14 @@ "https://deno.land/x/cliffy@v1.0.0-rc.4/table/table.ts": "298671e72e61f1ab18b42ae36643181993f79e29b39dc411fdc6ffd53aa04684", "https://deno.land/x/code_block_writer@12.0.0/mod.ts": "2c3448060e47c9d08604c8f40dee34343f553f33edcdfebbf648442be33205e5", "https://deno.land/x/code_block_writer@12.0.0/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff", + "https://deno.land/x/color_util@1.0.1/colors/cmykcolor.ts": "f717cee02bdec255c7c2879b55033da7547d46c1fbb8ada7980d49bd2c1554ee", + "https://deno.land/x/color_util@1.0.1/colors/hexcolor.ts": "3b75112b8d693f157d3e6367f9ec44779f3ccef7c7b5f8e0859897f51eb05c4f", + "https://deno.land/x/color_util@1.0.1/colors/hsbcolor.ts": "bdfe171f7e6de43625ea4a33bce2b25dec410fa67f34af71821a6c73f4e23351", + "https://deno.land/x/color_util@1.0.1/colors/hslcolor.ts": "2c61e987dc49822444adbd62249e33cc6fd6a0a33a20fcb287240cf96e6ef41f", + "https://deno.land/x/color_util@1.0.1/colors/hwbcolor.ts": "d1e9558d89c6fbd1b6a7f9bb86fc9c93e114a8e0eb614aee83cbc8201a67f15a", + "https://deno.land/x/color_util@1.0.1/colors/mod.ts": "31099d332b123efc9d3ae0016173daba7cb8242440bf198851a34b339c69a555", + "https://deno.land/x/color_util@1.0.1/colors/rgbcolor.ts": "384ceac5fd708cb6515df2a371de6bf1eea58376003c7015d82819c7809a1b26", + "https://deno.land/x/color_util@1.0.1/mod.ts": "d79c71f1c6583c56cdf56963d020928fdef05d8060d59601fe4f46f7ee1488fd", "https://deno.land/x/compress@v0.4.5/deps.ts": "096395daebc7ed8a18f0484e4ffcc3a7f70e50946735f7df9611a7fcfd8272cc", "https://deno.land/x/convert_bytes@v2.1.1/mod.ts": "036bd2d9519c8ad44bd5a15d4e42123dc16843f793b3c81ca1fca905b21dd7df", "https://deno.land/x/convert_bytes@v2.1.1/src/unit.ts": "ebfa749b09d2f6cf16a3a6cae5ab6042ae66667b9780071cbb933fcbdcff9731", diff --git a/ghjk.ts b/ghjk.ts index ea1c538a51..bb7a4cf3d1 100644 --- a/ghjk.ts +++ b/ghjk.ts @@ -50,7 +50,7 @@ if (Deno.build.os == "linux" && !Deno.env.has("NO_MOLD")) { env("_ecma").install( installs.node, - ports.pnpm({ version: "v9.4.0" }), + ports.pnpm({ version: "v9.15.0" }), ports.npmi({ packageName: "node-gyp", version: "10.0.1" })[0], ); diff --git a/import_map.json b/import_map.json index 5fdfc8c448..1916d2fb38 100644 --- a/import_map.json +++ b/import_map.json @@ -40,6 +40,7 @@ "sentry": "npm:@sentry/node@7.70.0", "swc": "https://deno.land/x/swc@0.2.1/mod.ts", "swc/types": "https://esm.sh/@swc/core@1.3.87/types.d.ts?pin=v135", - "validator": "npm:validator@13.12.0" + "validator": "npm:validator@13.12.0", + "@sinonjs/fake-timers": "npm:@sinonjs/fake-timers@13.0.5" } } diff --git a/src/common/src/typegraph/mod.rs b/src/common/src/typegraph/mod.rs index f63079e273..f33c8857e1 100644 --- a/src/common/src/typegraph/mod.rs +++ b/src/common/src/typegraph/mod.rs @@ -87,6 +87,9 @@ pub struct Queries { pub struct TypeMeta { pub prefix: Option, pub secrets: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + pub outjection_secrets: Vec, pub queries: Queries, pub cors: Cors, pub auths: Vec, diff --git a/src/common/src/typegraph/types.rs b/src/common/src/typegraph/types.rs index 8827536d60..22d2b3da75 100644 --- a/src/common/src/typegraph/types.rs +++ b/src/common/src/typegraph/types.rs @@ -181,6 +181,24 @@ pub enum InjectionNode { }, } +impl InjectionNode { + pub fn collect_secrets_into(&self, collector: &mut Vec) -> Result<()> { + match self { + InjectionNode::Leaf { injection } => { + if let Injection::Secret(d) = injection { + collector.extend(d.values::()?); + } + } + InjectionNode::Parent { children } => { + for child in children.values() { + child.collect_secrets_into(collector)?; + } + } + } + Ok(()) + } +} + #[skip_serializing_none] #[derive(Serialize, Deserialize, Clone, Debug)] pub struct FunctionTypeData { @@ -188,7 +206,12 @@ pub struct FunctionTypeData { #[serde(rename = "parameterTransform")] pub parameter_transform: Option, pub output: Id, + #[serde(skip_serializing_if = "IndexMap::is_empty")] + #[serde(default)] pub injections: IndexMap, + #[serde(skip_serializing_if = "IndexMap::is_empty")] + #[serde(default)] + pub outjections: IndexMap, #[serde(rename = "runtimeConfig")] pub runtime_config: serde_json::Value, pub materializer: u32, diff --git a/src/common/src/typegraph/validator/mod.rs b/src/common/src/typegraph/validator/mod.rs index 4a6d5dfa83..217ad6a925 100644 --- a/src/common/src/typegraph/validator/mod.rs +++ b/src/common/src/typegraph/validator/mod.rs @@ -170,6 +170,26 @@ impl<'a> TypeVisitor<'a> for Validator { ); path.pop(); } + // TODO validate outjection + // if !data.outjections.is_empty() { + // let outj_cx = InjectionValidationContext { + // fn_path: current_node.path.to_vec(), + // fn_idx: current_node.type_idx, + // input_idx: data.output, + // parent_object, + // validator: context, + // }; + // for (k, outj) in data.outjections.iter() { + // path.push(k.clone()); + // self.validate_injection( + // &mut path, + // *parent_object.properties.get(k).unwrap(), + // outj, + // &outj_cx, + // ); + // path.pop(); + // } + // } } else if let TypeNode::Either { data, .. } = type_node { let variants = data.one_of.clone(); for i in 0..variants.len() { diff --git a/src/metagen/src/client_rs/mod.rs b/src/metagen/src/client_rs/mod.rs index 0f15f9d533..5fefdf98c2 100644 --- a/src/metagen/src/client_rs/mod.rs +++ b/src/metagen/src/client_rs/mod.rs @@ -17,7 +17,6 @@ use crate::shared::client::*; use crate::shared::types::NameMemo; use crate::shared::types::TypeRenderer; use crate::utils::GenDestBuf; -use normpath::PathExt; use utils::normalize_type_title; #[derive(Serialize, Deserialize, Debug, garde::Validate)] @@ -436,6 +435,7 @@ pub fn gen_cargo_toml(crate_name: Option<&str>) -> String { #[cfg(debug_assertions)] let dependency = if is_test { + use normpath::PathExt; let client_path = Path::new(env!("CARGO_MANIFEST_DIR")) .join("../metagen-client-rs") .normalize() diff --git a/src/metagen/src/fdk_rust/stubs.rs b/src/metagen/src/fdk_rust/stubs.rs index 54cfa8ecd0..ad636ef45b 100644 --- a/src/metagen/src/fdk_rust/stubs.rs +++ b/src/metagen/src/fdk_rust/stubs.rs @@ -138,6 +138,7 @@ mod test { input: 1, output: 1, injections: Default::default(), + outjections: Default::default(), runtime_config: Default::default(), rate_calls: false, rate_weight: None, diff --git a/src/metagen/src/tests/fixtures.rs b/src/metagen/src/tests/fixtures.rs index dd86021e33..7dcb060b22 100644 --- a/src/metagen/src/tests/fixtures.rs +++ b/src/metagen/src/tests/fixtures.rs @@ -80,6 +80,7 @@ pub fn test_typegraph_2() -> Typegraph { input: 1, output: 1, injections: Default::default(), + outjections: Default::default(), runtime_config: Default::default(), rate_calls: false, rate_weight: None, diff --git a/src/typegate/src/engine/injection/dynamic.ts b/src/typegate/src/engine/injection/dynamic.ts new file mode 100644 index 0000000000..43018d2b89 --- /dev/null +++ b/src/typegate/src/engine/injection/dynamic.ts @@ -0,0 +1,7 @@ +// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +// SPDX-License-Identifier: MPL-2.0 + +export default { + "now": () => new Date().toISOString(), + // "uuid": () => +} as const; diff --git a/src/typegate/src/engine/planner/args.ts b/src/typegate/src/engine/planner/args.ts index f84452b2e5..baa9cc1e78 100644 --- a/src/typegate/src/engine/planner/args.ts +++ b/src/typegate/src/engine/planner/args.ts @@ -43,6 +43,7 @@ import { import { QueryFunction as JsonPathQuery } from "../../libs/jsonpath.ts"; import { getInjection } from "../../typegraph/utils.ts"; import { GeneratorNode } from "../../runtimes/random.ts"; +import DynamicInjection from "../injection/dynamic.ts"; class MandatoryArgumentError extends Error { constructor(argDetails: string) { @@ -104,7 +105,7 @@ export function collectArgs( stageId, effect, parentProps, - injectionTree, + injectionTree ?? {}, ); const argTypeNode = typegraph.type(typeIdx, Type.OBJECT); for (const argName of Object.keys(astNodes)) { @@ -169,11 +170,6 @@ export function collectArgs( }; } -const GENERATORS = { - "now": () => new Date().toISOString(), - // "uuid": () => -} as const; - interface Dependencies { context: Set; parent: Set; @@ -814,7 +810,8 @@ class ArgumentCollector { if (generatorName == null) { return null; } - const generator = GENERATORS[generatorName as keyof typeof GENERATORS]; + const generator = + DynamicInjection[generatorName as keyof typeof DynamicInjection]; if (generator == null) { throw new Error( `Unknown generator '${generatorName}' for dynamic injection`, diff --git a/src/typegate/src/engine/planner/injection_utils.ts b/src/typegate/src/engine/planner/injection_utils.ts index d253c5dbee..0dfa78c2de 100644 --- a/src/typegate/src/engine/planner/injection_utils.ts +++ b/src/typegate/src/engine/planner/injection_utils.ts @@ -16,3 +16,13 @@ export function selectInjection( } return null; } + +export function getInjectionValues( + data: InjectionData, +): T[] { + if ("value" in data) { + return [data.value as T]; + } + + return Object.values(data).filter((v) => typeof v === "string") as T[]; +} diff --git a/src/typegate/src/engine/planner/mod.ts b/src/typegate/src/engine/planner/mod.ts index ed4709ef92..aadabf533c 100644 --- a/src/typegate/src/engine/planner/mod.ts +++ b/src/typegate/src/engine/planner/mod.ts @@ -26,9 +26,19 @@ import { getLogger } from "../../log.ts"; import { generateVariantMatcher } from "../typecheck/matching_variant.ts"; import { mapValues } from "@std/collections/map-values"; import { DependencyResolver } from "./dependency_resolver.ts"; +import { Runtime } from "../../runtimes/Runtime.ts"; +import { getInjection } from "../../typegraph/utils.ts"; +import { Injection } from "../../typegraph/types.ts"; +import { getInjectionValues } from "./injection_utils.ts"; const logger = getLogger(import.meta); +interface Scope { + runtime: Runtime; + fnIdx: number; + path: string[]; +} + interface Node { name: string; path: string[]; @@ -37,6 +47,7 @@ interface Node { typeIdx: number; parent?: Node; parentStage?: ComputeStage; + scope?: Scope; } export interface Plan { @@ -281,6 +292,19 @@ export class Planner { ) { throw this.unexpectedFieldError(node, name); } + const fieldType = fieldIdx == null ? null : this.tg.type(fieldIdx); + const scope: Scope | undefined = + (fieldType && fieldType.type === Type.FUNCTION) + ? { + runtime: this.tg + .runtimeReferences[ + this.tg.materializer(fieldType.materializer).runtime + ], + fnIdx: fieldIdx, + path: [], + } + : node.scope && { ...node.scope, path: [...node.scope.path, name] }; + return { parent: node, name, @@ -289,9 +313,17 @@ export class Planner { args: args ?? [], typeIdx: props[name], parentStage, + scope, }; } + #getOutjection(scope: Scope): Injection | null { + const outjectionTree = + this.tg.type(scope.fnIdx, Type.FUNCTION).outjections ?? + {}; + return getInjection(outjectionTree, scope.path); + } + /** * Create compute stages for `node` and its child nodes. * @param field {FieldNode} The selection field for node @@ -374,6 +406,25 @@ export class Planner { return stages; } + #createOutjectionStage(node: Node, outjection: Injection): ComputeStage { + return this.createComputeStage(node, { + // TODO parent if from parent + dependencies: [], + args: null, + effect: null, + runtime: this.tg.runtimeReferences[this.tg.denoRuntimeIdx], + batcher: this.tg.nextBatcher(this.tg.type(node.typeIdx)), + rateCalls: true, + rateWeight: 0, + materializer: { + runtime: this.tg.denoRuntimeIdx, + name: "outjection", + data: outjection, + effect: { effect: null, idempotent: true }, + }, + }); + } + /** * Create `ComputeStage`s for `node` and its child nodes, * where `node` corresponds to a selection field for a value (non-function type). @@ -381,6 +432,12 @@ export class Planner { * @param policies */ private traverseValueField(node: Node): ComputeStage[] { + const outjection = node.scope && this.#getOutjection(node.scope!); + if (outjection) { + return [ + this.#createOutjectionStage(node, outjection), + ]; + } const stages: ComputeStage[] = []; const schema = this.tg.type(node.typeIdx); @@ -392,11 +449,8 @@ export class Planner { ); } - const runtime = (schema.type === Type.FUNCTION) - ? this.tg - .runtimeReferences[(this.tg.materializer(schema.materializer)).runtime] - : node.parentStage?.props.runtime ?? - this.tg.runtimeReferences[this.tg.denoRuntimeIdx]; + const runtime = node.scope?.runtime ?? + this.tg.runtimeReferences[this.tg.denoRuntimeIdx]; const stage = this.createComputeStage(node, { dependencies: node.parentStage ? [node.parentStage.id()] : [], diff --git a/src/typegate/src/engine/typecheck/result.ts b/src/typegate/src/engine/typecheck/result.ts index 760bb8a89f..3cae058621 100644 --- a/src/typegate/src/engine/typecheck/result.ts +++ b/src/typegate/src/engine/typecheck/result.ts @@ -26,6 +26,9 @@ import { type Validator, type ValidatorFn, } from "./common.ts"; +import { getLogger } from "../../log.ts"; + +const logger = getLogger(import.meta); export function generateValidator( tg: TypeGraph, @@ -42,6 +45,8 @@ export function generateValidator( const messages = errors .map(([path, msg]) => ` - at ${path}: ${msg}\n`) .join(""); + logger.error("Validation failed: value={}", value); + logger.error("Validation errors:\n{}", messages); throw new Error(`Validation errors:\n${messages}`); } }; diff --git a/src/typegate/src/runtimes/deno/deno.ts b/src/typegate/src/runtimes/deno/deno.ts index 38a9a71956..d05392b5e4 100644 --- a/src/typegate/src/runtimes/deno/deno.ts +++ b/src/typegate/src/runtimes/deno/deno.ts @@ -2,11 +2,20 @@ // SPDX-License-Identifier: MPL-2.0 import type { ComputeStage } from "../../engine/query_engine.ts"; -import type { TypeGraphDS, TypeMaterializer } from "../../typegraph/mod.ts"; +import { + TypeGraph, + type TypeGraphDS, + type TypeMaterializer, +} from "../../typegraph/mod.ts"; import type { Typegate } from "../../typegate/mod.ts"; import { Runtime } from "../Runtime.ts"; import type { Resolver, RuntimeInitParams } from "../../types.ts"; -import type { DenoRuntimeData } from "../../typegraph/types.ts"; +import type { + DenoRuntimeData, + Injection, + InjectionData, + TypeNode, +} from "../../typegraph/types.ts"; import * as ast from "graphql/ast"; import { InternalAuth } from "../../services/auth/protocols/internal.ts"; import { DenoMessenger } from "./deno_messenger.ts"; @@ -14,6 +23,11 @@ import type { Task } from "./shared_types.ts"; import { path } from "compress/deps.ts"; import { globalConfig as config } from "../../config.ts"; import { createArtifactMeta } from "../utils/deno.ts"; +import { getInjectionValues } from "../../engine/planner/injection_utils.ts"; +import DynamicInjection from "../../engine/injection/dynamic.ts"; +import { getLogger } from "../../log.ts"; + +const logger = getLogger(import.meta); const predefinedFuncs: Record>> = { identity: ({ _, ...args }) => args, @@ -55,10 +69,19 @@ export class DenoRuntime extends Runtime { const secrets: Record = {}; for (const m of materializers) { + let secrets = (m.data.secrets as string[]) ?? []; + if (m.name === "outjection") { + secrets = m.data.source === "secret" + ? [...getInjectionValues(m.data)] + : []; + } for (const secretName of (m.data.secrets as []) ?? []) { secrets[secretName] = secretManager.secretOrFail(secretName); } } + for (const secretName of tg.meta.outjectionSecrets ?? []) { + secrets[secretName] = secretManager.secretOrFail(secretName); + } // maps from the module code to the op number/id const registry = new Map(); @@ -83,7 +106,7 @@ export class DenoRuntime extends Runtime { const matData = mat.data; const entryPoint = artifacts[matData.entryPoint as string]; const depMetas = (matData.deps as string[]).map((dep) => - createArtifactMeta(typegraphName, artifacts[dep]), + createArtifactMeta(typegraphName, artifacts[dep]) ); const moduleMeta = createArtifactMeta(typegraphName, entryPoint); @@ -176,10 +199,16 @@ export class DenoRuntime extends Runtime { return [stage.withResolver(() => typename)]; } - if (stage.props.materializer != null) { - return [ - stage.withResolver(this.delegate(stage.props.materializer, verbose)), - ]; + const mat = stage.props.materializer; + if (mat != null) { + if (mat.name === "outjection") { + return [ + stage.withResolver( + this.outject(stage.props.outType, mat.data as Injection), + ), + ]; + } + return [stage.withResolver(this.delegate(mat, verbose))]; } if (this.tg.meta.namespaces!.includes(stage.props.typeIdx)) { @@ -299,4 +328,42 @@ export class DenoRuntime extends Runtime { throw new Error(`unsupported materializer ${mat.name}`); } + + outject(typeNode: TypeNode, outjection: Injection): Resolver { + switch (outjection.source) { + case "static": { + const value = JSON.parse(getInjectionData(outjection.data) as string); + return () => value; + } + case "context": { + const key = getInjectionData(outjection.data) as string; + // TODO error if null + return ({ _: { context } }) => context[key] ?? null; + } + case "secret": { + const key = getInjectionData(outjection.data) as string; + return () => this.secrets[key] ?? null; + } + case "random": { + return () => TypeGraph.getRandomStatic(this.tg, typeNode, null); + } + case "dynamic": { + const gen = getInjectionData( + outjection.data, + ) as keyof typeof DynamicInjection; + return DynamicInjection[gen]; + } + default: { + logger.error(`unsupported outjection source '${outjection.source}'`); + throw new Error(`unsupported outjection source '${outjection.source}'`); + } + } + } +} + +function getInjectionData(d: InjectionData) { + if ("value" in d) { + return d.value; + } + return d["none"] ?? null; } diff --git a/src/typegate/src/runtimes/graphql.ts b/src/typegate/src/runtimes/graphql.ts index 727a50b6b2..01888779b9 100644 --- a/src/typegate/src/runtimes/graphql.ts +++ b/src/typegate/src/runtimes/graphql.ts @@ -88,7 +88,7 @@ export class GraphQLRuntime extends Runtime { verbose && logger.debug( - "remote graphql:", + "remote graphql: {}", typeof query === "string" ? query : " with inlined vars", ); diff --git a/src/typegate/src/runtimes/random.ts b/src/typegate/src/runtimes/random.ts index 0972a7db37..a8c7f9d480 100644 --- a/src/typegate/src/runtimes/random.ts +++ b/src/typegate/src/runtimes/random.ts @@ -69,7 +69,7 @@ export type GeneratorNode = export default function randomizeRecursively( typ: TypeNode, - chance: typeof Chance, + chance: Chance, tgTypes: TypeNode[], generatorNode: GeneratorNode | null, ): any { diff --git a/src/typegate/src/runtimes/typegraph.ts b/src/typegate/src/runtimes/typegraph.ts index 36ef2fdc59..aa4d8cf83f 100644 --- a/src/typegate/src/runtimes/typegraph.ts +++ b/src/typegate/src/runtimes/typegraph.ts @@ -523,7 +523,10 @@ export class TypeGraphRuntime extends Runtime { entries = entries.sort((a, b) => b[1] - a[1]); return entries .map((entry) => - this.formatInputFields(entry, type.injections[entry[0]] ?? null) + this.formatInputFields( + entry, + (type.injections ?? {})[entry[0]] ?? null, + ) ) .filter((f) => f !== null); }, diff --git a/src/typegate/src/typegraph/mod.ts b/src/typegate/src/typegraph/mod.ts index 83b6a4ffcc..1a0156dcc6 100644 --- a/src/typegate/src/typegraph/mod.ts +++ b/src/typegate/src/typegraph/mod.ts @@ -335,19 +335,25 @@ export class TypeGraph implements AsyncDisposable { getRandom( schema: TypeNode, generator: GeneratorNode | null, - ): number | string | null { - const tgTypes: TypeNode[] = this.tg.types; + ) { + return TypeGraph.getRandomStatic(this.tg, schema, generator); + } + static getRandomStatic( + tg: TypeGraphDS, + schema: TypeNode, + generator: GeneratorNode | null, + ) { let seed = 12; // default seed if ( - this.tg.meta.randomSeed !== undefined && - this.tg.meta.randomSeed !== null + tg.meta.randomSeed !== undefined && + tg.meta.randomSeed !== null ) { - seed = this.tg.meta.randomSeed; + seed = tg.meta.randomSeed; } const chance: typeof Chance = new Chance(seed); try { - const result = randomizeRecursively(schema, chance, tgTypes, generator); + const result = randomizeRecursively(schema, chance, tg.types, generator); return result; } catch (_) { diff --git a/src/typegate/src/typegraph/types.ts b/src/typegate/src/typegraph/types.ts index edaaeb5c2c..84cd5570a7 100644 --- a/src/typegate/src/typegraph/types.ts +++ b/src/typegate/src/typegraph/types.ts @@ -97,7 +97,8 @@ export type FunctionNode = { description?: string | null; enum?: string[] | null; input: number; - injections: Record; + injections?: Record; + outjections?: Record; parameterTransform?: FunctionParameterTransform | null; output: number; runtimeConfig: unknown; @@ -433,6 +434,7 @@ export interface Policy { export interface TypeMeta { prefix?: string | null; secrets: string[]; + outjectionSecrets?: string[]; queries: Queries; cors: Cors; auths: Auth[]; diff --git a/src/typegraph/core/src/snapshots/typegraph_core__tests__successful_serialization.snap b/src/typegraph/core/src/snapshots/typegraph_core__tests__successful_serialization.snap index 6b2de1f5eb..048b0a6aee 100644 --- a/src/typegraph/core/src/snapshots/typegraph_core__tests__successful_serialization.snap +++ b/src/typegraph/core/src/snapshots/typegraph_core__tests__successful_serialization.snap @@ -22,7 +22,6 @@ expression: typegraph.0 "policies": [], "input": 2, "output": 4, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, diff --git a/src/typegraph/core/src/typedef/func.rs b/src/typegraph/core/src/typedef/func.rs index d04a8aa2c2..377d27e8c2 100644 --- a/src/typegraph/core/src/typedef/func.rs +++ b/src/typegraph/core/src/typedef/func.rs @@ -37,7 +37,15 @@ impl TypeConversion for Func { _ => return Err(errors::invalid_input_type(&inp_id.repr()?)), }; - let output = ctx.register_type(TypeId(self.data.out))?.into(); + let out_id = TypeId(self.data.out); + let output = ctx.register_type(out_id)?.into(); + let outjection_tree = match out_id.as_xdef()?.type_def { + TypeDef::Struct(s) => collect_injections(s, Default::default())?, + _ => Default::default(), + }; + + let outjection_secrets = outjection_tree.get_secrets()?; + ctx.meta.outjection_secrets.extend(outjection_secrets); let parameter_transform = self .data @@ -80,6 +88,7 @@ impl TypeConversion for Func { parameter_transform, output, injections: injection_tree.0, + outjections: outjection_tree.0, runtime_config: self.collect_runtime_config(ctx)?, materializer: mat_id, rate_calls: self.data.rate_calls, diff --git a/src/typegraph/core/src/typegraph.rs b/src/typegraph/core/src/typegraph.rs index 4a5afd9a40..71b11b31fd 100644 --- a/src/typegraph/core/src/typegraph.rs +++ b/src/typegraph/core/src/typegraph.rs @@ -51,7 +51,7 @@ struct RuntimeContexts { pub struct TypegraphContext { name: String, path: Option>, - meta: TypeMeta, + pub(crate) meta: TypeMeta, types: Vec>, runtimes: Vec, materializers: Vec>, @@ -113,6 +113,7 @@ pub fn init(params: TypegraphInitParams) -> Result<()> { prefix: params.prefix, rate: params.rate.map(|v| v.into()), secrets: vec![], + outjection_secrets: vec![], random_seed: Default::default(), artifacts: Default::default(), }, diff --git a/src/typegraph/core/src/types/type_ref/injection.rs b/src/typegraph/core/src/types/type_ref/injection.rs index 7296cc758b..948e976e98 100644 --- a/src/typegraph/core/src/types/type_ref/injection.rs +++ b/src/typegraph/core/src/types/type_ref/injection.rs @@ -120,6 +120,14 @@ impl InjectionTree { } } } + + pub fn get_secrets(&self) -> Result> { + let mut collector = Vec::new(); + for node in self.0.values() { + node.collect_secrets_into(&mut collector)?; + } + Ok(collector) + } } impl Hash for InjectionTree { diff --git a/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap b/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap index 555023cea5..b81a657e97 100644 --- a/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap +++ b/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap @@ -24,7 +24,6 @@ snapshot[`typegraphs creation 1`] = ` ], "input": 2, "output": 18, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -290,7 +289,6 @@ snapshot[`typegraphs creation 2`] = ` ], "input": 2, "output": 3, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -320,7 +318,6 @@ snapshot[`typegraphs creation 2`] = ` ], "input": 2, "output": 3, - "injections": {}, "runtimeConfig": null, "materializer": 2, "rate_weight": null, @@ -438,7 +435,6 @@ snapshot[`typegraphs creation 3`] = ` ], "input": 2, "output": 5, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -481,7 +477,6 @@ snapshot[`typegraphs creation 3`] = ` ], "input": 7, "output": 8, - "injections": {}, "runtimeConfig": null, "materializer": 2, "rate_weight": null, @@ -517,7 +512,6 @@ snapshot[`typegraphs creation 3`] = ` ], "input": 2, "output": 2, - "injections": {}, "runtimeConfig": null, "materializer": 4, "rate_weight": null, @@ -687,7 +681,6 @@ snapshot[`typegraphs creation 4`] = ` ], "input": 2, "output": 18, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -953,7 +946,6 @@ snapshot[`typegraphs creation 5`] = ` ], "input": 2, "output": 3, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -983,7 +975,6 @@ snapshot[`typegraphs creation 5`] = ` ], "input": 2, "output": 3, - "injections": {}, "runtimeConfig": null, "materializer": 2, "rate_weight": null, @@ -1101,7 +1092,6 @@ snapshot[`typegraphs creation 6`] = ` ], "input": 2, "output": 5, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -1144,7 +1134,6 @@ snapshot[`typegraphs creation 6`] = ` ], "input": 7, "output": 8, - "injections": {}, "runtimeConfig": null, "materializer": 2, "rate_weight": null, @@ -1180,7 +1169,6 @@ snapshot[`typegraphs creation 6`] = ` ], "input": 2, "output": 2, - "injections": {}, "runtimeConfig": null, "materializer": 4, "rate_weight": null, diff --git a/tests/injection/injection.py b/tests/injection/injection.py index a01c3b7b58..76b733e673 100644 --- a/tests/injection/injection.py +++ b/tests/injection/injection.py @@ -31,6 +31,24 @@ def injection(g: Graph): } ) + req_out = t.struct( + { + "a": t.integer(), + "raw_int": t.integer(), + "raw_str": t.string(), + "secret": t.integer(), + "context": t.string(), + "optional_context": t.string().optional(), + "raw_obj": t.struct({"in": t.integer()}), + "alt_raw": t.string(), + "alt_secret": t.string(), + "alt_context": t.string(), + "alt_context_opt": t.string().optional(), + "alt_context_opt_missing": t.string().optional(), + "date": t.datetime(), + } + ) + operation = t.enum(["insert", "modify", "remove", "read"]) req2 = t.struct( @@ -48,6 +66,7 @@ def injection(g: Graph): res2 = t.struct({"operation": operation}) copy = t.struct({"a2": t.integer().from_parent("a")}) + copy_out = t.struct({"a2": t.integer()}) user = t.struct( { @@ -75,11 +94,15 @@ def injection(g: Graph): find_messages = messages_db.find_many(message) + union = t.union([t.string(), t.integer()]) + either = t.either([t.string(), t.integer()]) + identity1 = deno.func(req, req_out, code="(x) => x") + g.expose( Policy.public(), - test=deno.identity(req).extend( + test=identity1.extend( { - "parent": deno.identity(copy), + "parent": deno.func(copy, copy_out, code="(x) => x"), "graphql": gql.query( t.struct({"id": t.integer().from_parent("a")}), user, @@ -87,7 +110,7 @@ def injection(g: Graph): ), } ), - effect_none=deno.identity(req2), + effect_none=deno.func(req2, res2, code="(x) => x"), effect_create=deno.func(req2, res2, code="(x) => x", effect=effects.create()), effect_delete=deno.func(req2, res2, code="(x) => x", effect=effects.delete()), effect_update=deno.func(req2, res2, code="(x) => x", effect=effects.update()), @@ -110,4 +133,34 @@ def injection(g: Graph): ), path=("user",), ), + union=deno.identity( + t.struct( + { + "integer": t.integer(), + "string": t.string(), + } + ) + ).extend( + { + "injected": deno.func( + t.struct( + { + "union1": union.from_parent("integer"), + "union2": union.from_parent("string"), + "either1": either.from_parent("integer"), + "either2": either.from_parent("string"), + } + ), + t.struct( + { + "union1": union, + "union2": union, + "either1": either, + "either2": either, + } + ), + code="x => x", + ) + } + ), ) diff --git a/tests/injection/injection.ts b/tests/injection/injection.ts index 4326192690..92a2297f13 100644 --- a/tests/injection/injection.ts +++ b/tests/injection/injection.ts @@ -26,11 +26,29 @@ const tpe = t.struct({ date: t.datetime().inject("now"), }); +const out = t.struct({ + a: t.integer(), + raw_int: t.integer(), + raw_str: t.string(), + secret: t.integer(), + context: t.string(), + optional_context: t.string().optional(), + raw_obj: t.struct({ in: t.integer() }), + alt_raw: t.string(), + alt_secret: t.string(), + alt_context: t.string(), + alt_context_opt: t.string().optional(), + alt_context_opt_missing: t.string().optional(), + date: t.datetime(), +}); + export const tg = await typegraph("injection", (g: any) => { const deno = new DenoRuntime(); const pub = Policy.public(); g.expose({ - test: deno.identity(t.struct({ input: tpe })).withPolicy(pub), + test: deno.func(t.struct({ input: tpe }), t.struct({ input: out }), { + code: "x => x", + }).withPolicy(pub), }); }); diff --git a/tests/injection/injection_test.ts b/tests/injection/injection_test.ts index 9612b7222d..2113073f5e 100644 --- a/tests/injection/injection_test.ts +++ b/tests/injection/injection_test.ts @@ -131,6 +131,36 @@ Meta.test("Injected values", async (t) => { }) .on(e); }); + + await t.should("inject into union", async () => { + await gql` + query { + union(integer: 12, string: "hello") { + integer + string + injected { + union1 + union2 + either1 + either2 + } + } + } + ` + .expectData({ + union: { + integer: 12, + string: "hello", + injected: { + union1: 12, + union2: "hello", + either1: 12, + either2: "hello", + }, + }, + }) + .on(e); + }); }); mf.install(); diff --git a/tests/injection/nested_context.py b/tests/injection/nested_context.py index 5663dc34dd..6e9febab8d 100644 --- a/tests/injection/nested_context.py +++ b/tests/injection/nested_context.py @@ -12,17 +12,25 @@ def nested_context(g: Graph): g.expose( has_profile, - injectedId=deno.identity( + injectedId=deno.func( # TODO validate the path against the profiler result?? - t.struct({"id": t.integer().from_context("profile.id")}) + t.struct({"id": t.integer().from_context("profile.id")}), + t.struct({"id": t.integer()}), + code="x => x", ), - secondProfileData=deno.identity( - t.struct({"second": t.integer().from_context("profile.data[1]")}) + secondProfileData=deno.func( + t.struct({"second": t.integer().from_context("profile.data[1]")}), + t.struct({"second": t.integer()}), + code="x => x", ), - customKey=deno.identity( - t.struct({"custom": t.integer().from_context('profile["custom key"]')}) + customKey=deno.func( + t.struct({"custom": t.integer().from_context('profile["custom key"]')}), + t.struct({"custom": t.integer()}), + code="x => x", ), - optional=deno.identity( - t.struct({"optional": t.email().optional().from_context("profile.email")}) + optional=deno.func( + t.struct({"optional": t.email().optional().from_context("profile.email")}), + t.struct({"optional": t.email().optional()}), + code="x => x", ), ) diff --git a/tests/injection/outjection.py b/tests/injection/outjection.py new file mode 100644 index 0000000000..e3899e080e --- /dev/null +++ b/tests/injection/outjection.py @@ -0,0 +1,33 @@ +# Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +# SPDX-License-Identifier: MPL-2.0 + +from typegraph import typegraph, Policy, t, Graph +from typegraph.runtimes import RandomRuntime, DenoRuntime + + +@typegraph() +def outjection(g: Graph): + deno = DenoRuntime() + random = RandomRuntime(seed=1) + + g.expose( + Policy.public(), + randomUser=deno.identity(t.struct()).extend( + { + "id": t.uuid().from_random(), + "age": t.integer().set(19), + "email": t.email().from_context("user_email"), + "password": t.string().from_secret( + "user_password" + ), ## should we allow this? + "createdAt": t.datetime().inject("now"), + "firstPost": random.gen( + t.struct( + { + "title": t.string(), + } + ) + ), # .extend({"publisherEmail": t.email().from_parent("email")}), + } + ), + ) diff --git a/tests/injection/outjection_test.ts b/tests/injection/outjection_test.ts new file mode 100644 index 0000000000..5ac097d148 --- /dev/null +++ b/tests/injection/outjection_test.ts @@ -0,0 +1,51 @@ +// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +// SPDX-License-Identifier: MPL-2.0 + +import { gql, Meta } from "test-utils/mod.ts"; +import FakeTimers from "@sinonjs/fake-timers"; + +Meta.test("outjection", async (t) => { + const e = await t.engine("injection/outjection.py", { + secrets: { + "user_password": "some-unguessable-super-secret", + }, + }); + + await t.should("return context-value", async () => { + const clock = FakeTimers.install(); + try { + await gql` + query { + randomUser { + id + age + email + password + createdAt + firstPost { + title + # publisherEmail + } + } + } + ` + .withContext({ user_email: "john@doe.com" }) + .expectData({ + randomUser: { + id: "c268eb36-af9a-59ea-a06c-fe5400289ad3", + age: 19, + email: "john@doe.com", + password: "some-unguessable-super-secret", + createdAt: new Date().toISOString(), + firstPost: { + title: "]1*ajw]krgD", + // publisherEmail: "john@doe.com", + }, + }, + }) + .on(e); + } finally { + clock.uninstall(); + } + }); +}); diff --git a/tests/injection/random_injection.py b/tests/injection/random_injection.py index 10de670d56..68f58f0d81 100644 --- a/tests/injection/random_injection.py +++ b/tests/injection/random_injection.py @@ -37,6 +37,29 @@ def random_injection(g: Graph): } ) + user_out = t.struct( + { + "id": t.uuid(), + "ean": t.ean(), + "name": t.string(), + "age": t.integer(), + "married": t.boolean(), + "birthday": t.datetime(), + "friends": t.list(t.string()), + "phone": t.string(), + "gender": t.string(), + "firstname": t.string(), + "lastname": t.string(), + "occupation": t.string(), + "street": t.string(), + "city": t.string(), + "postcode": t.string(), + "country": t.string(), + "uri": t.uri(), + "hostname": t.string(), + } + ) + # test int, str, float enum test_enum_str = t.struct( { @@ -45,24 +68,44 @@ def random_injection(g: Graph): ).from_random(), } ) + test_enum_str_out = t.struct( + { + "educationLevel": t.string(), + } + ) test_enum_int = t.struct( { "bits": t.integer(enum=[0, 1]).from_random(), } ) + test_enum_int_out = t.struct( + { + "bits": t.integer(), + } + ) test_enum_float = t.struct( { "cents": t.float(enum=[0.25, 0.5, 1.0]).from_random(), } ) + test_enum_float_out = t.struct( + { + "cents": t.float(), + } + ) # test either rubix_cube = t.struct({"name": t.string(), "size": t.integer()}, name="Rubix") toygun = t.struct({"color": t.string()}, name="Toygun") - toy = t.either([rubix_cube, toygun], name="Toy").from_random() + toy = t.either([rubix_cube, toygun], name="Toy") toy_struct = t.struct( + { + "toy": toy.from_random(), + } + ) + toy_struct_out = t.struct( { "toy": toy, } @@ -76,21 +119,31 @@ def random_injection(g: Graph): "field": t.union([rgb, vec], name="UnionStruct").from_random(), } ) + union_struct_out = t.struct( + { + "field": t.union([rgb, vec]), + } + ) random_list = t.struct( { "names": t.list(t.string(config={"gen": "name"})).from_random(), } ) + random_list_out = t.struct( + { + "names": t.list(t.string()), + }, + ) # Configure random injection seed value or the default will be used g.configure_random_injection(seed=1) g.expose( pub, - randomUser=deno.identity(user), - randomList=deno.identity(random_list), - testEnumStr=deno.identity(test_enum_str), - testEnumInt=deno.identity(test_enum_int), - testEnumFloat=deno.identity(test_enum_float), - testEither=deno.identity(toy_struct), - testUnion=deno.identity(union_struct), + randomUser=deno.func(user, user_out, code="(x) => x"), + randomList=deno.func(random_list, random_list_out, code="(x) => x"), + testEnumStr=deno.func(test_enum_str, test_enum_str_out, code="(x) => x"), + testEnumInt=deno.func(test_enum_int, test_enum_int_out, code="(x) => x"), + testEnumFloat=deno.func(test_enum_float, test_enum_float_out, code="(x) => x"), + testEither=deno.func(toy_struct, toy_struct_out, code="(x) => x"), + testUnion=deno.func(union_struct, union_struct_out, code="(x) => x"), ) diff --git a/tests/injection/random_injection.ts b/tests/injection/random_injection.ts index e2a71ff71a..dd224e5a76 100644 --- a/tests/injection/random_injection.ts +++ b/tests/injection/random_injection.ts @@ -28,6 +28,28 @@ const user = t.struct({ hostname: t.string({ format: "hostname" }).fromRandom(), }); +const userOut = t.struct({ + id: t.uuid(), + ean: t.ean(), + name: t.string(), + age: t.integer(), + married: t.boolean(), + email: t.string({ format: "email" }), + birthday: t.datetime(), + friends: t.list(t.string()), + phone: t.string(), + gender: t.string(), + firstname: t.string(), + lastname: t.string(), + occupation: t.string(), + street: t.string(), + city: t.string(), + postcode: t.string(), + country: t.string(), + uri: t.string({ format: "uri" }), + hostname: t.string({ format: "hostname" }), +}); + export const tg = await typegraph("random_injection", (g: any) => { const pub = Policy.public(); const deno = new DenoRuntime(); @@ -36,6 +58,7 @@ export const tg = await typegraph("random_injection", (g: any) => { g.configureRandomInjection({ seed: 1 }); g.expose({ - randomUser: deno.identity(user).withPolicy(pub), + // randomUser: deno.identity(user).withPolicy(pub), + randomUser: deno.func(user, userOut, { code: "x => x" }).withPolicy(pub), }); }); diff --git a/tests/planner/__snapshots__/planner_test.ts.snap b/tests/planner/__snapshots__/planner_test.ts.snap index 9ccdd48046..d62e8a890e 100644 --- a/tests/planner/__snapshots__/planner_test.ts.snap +++ b/tests/planner/__snapshots__/planner_test.ts.snap @@ -97,7 +97,6 @@ snapshot[`planner 2`] = ` { one: { funcType: { - injections: {}, input: 2, materializer: 0, output: 3, diff --git a/tests/prisma-migrations/typename/prisma/migration_lock.toml b/tests/prisma-migrations/typename/prisma/migration_lock.toml index 99e4f20090..fbffa92c2b 100644 --- a/tests/prisma-migrations/typename/prisma/migration_lock.toml +++ b/tests/prisma-migrations/typename/prisma/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (i.e. Git) -provider = "postgresql" +provider = "postgresql" \ No newline at end of file diff --git a/tests/query_parsers/__snapshots__/query_parsers_test.ts.snap b/tests/query_parsers/__snapshots__/query_parsers_test.ts.snap index 5e58126d63..74d5ae5d69 100644 --- a/tests/query_parsers/__snapshots__/query_parsers_test.ts.snap +++ b/tests/query_parsers/__snapshots__/query_parsers_test.ts.snap @@ -30,7 +30,6 @@ snapshot[`GraphQL parser 1`] = ` type: "object", }, { - injections: {}, input: 3, materializer: 1, output: 5, @@ -79,7 +78,6 @@ snapshot[`GraphQL parser 1`] = ` type: "string", }, { - injections: {}, input: 5, materializer: 2, output: 5, @@ -104,7 +102,6 @@ snapshot[`GraphQL parser 1`] = ` type: "object", }, { - injections: {}, input: 3, materializer: 3, output: 10, @@ -137,7 +134,6 @@ snapshot[`GraphQL parser 1`] = ` type: "string", }, { - injections: {}, input: 10, materializer: 4, output: 10, diff --git a/tests/runtimes/graphql/__snapshots__/graphql_test.ts.snap b/tests/runtimes/graphql/__snapshots__/graphql_test.ts.snap index db69a81df8..971c1c6ec9 100644 --- a/tests/runtimes/graphql/__snapshots__/graphql_test.ts.snap +++ b/tests/runtimes/graphql/__snapshots__/graphql_test.ts.snap @@ -31,7 +31,6 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` ], "input": 2, "output": 4, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -75,7 +74,6 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` ], "input": 6, "output": 7, - "injections": {}, "runtimeConfig": null, "materializer": 2, "rate_weight": null, @@ -113,7 +111,6 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` ], "input": 10, "output": 4, - "injections": {}, "runtimeConfig": null, "materializer": 3, "rate_weight": null, @@ -149,7 +146,6 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` ], "input": 17, "output": 20, - "injections": {}, "runtimeConfig": null, "materializer": 4, "rate_weight": null, @@ -255,7 +251,6 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` ], "input": 22, "output": 76, - "injections": {}, "runtimeConfig": null, "materializer": 6, "rate_weight": null, diff --git a/tests/runtimes/grpc/__snapshots__/grpc_test.ts.snap b/tests/runtimes/grpc/__snapshots__/grpc_test.ts.snap index 1d6c7690fb..927d158355 100644 --- a/tests/runtimes/grpc/__snapshots__/grpc_test.ts.snap +++ b/tests/runtimes/grpc/__snapshots__/grpc_test.ts.snap @@ -24,7 +24,6 @@ snapshot[`Typegraph using grpc 1`] = ` ], "input": 2, "output": 5, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -165,7 +164,6 @@ snapshot[`Typegraph using grpc 2`] = ` ], "input": 2, "output": 5, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, diff --git a/tests/runtimes/kv/__snapshots__/kv_test.ts.snap b/tests/runtimes/kv/__snapshots__/kv_test.ts.snap index 64b059222e..6c13a71348 100644 --- a/tests/runtimes/kv/__snapshots__/kv_test.ts.snap +++ b/tests/runtimes/kv/__snapshots__/kv_test.ts.snap @@ -32,7 +32,6 @@ snapshot[`Typegraph using kv 1`] = ` ], "input": 2, "output": 4, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -68,7 +67,6 @@ snapshot[`Typegraph using kv 1`] = ` ], "input": 6, "output": 3, - "injections": {}, "runtimeConfig": null, "materializer": 2, "rate_weight": null, @@ -93,7 +91,6 @@ snapshot[`Typegraph using kv 1`] = ` ], "input": 2, "output": 8, - "injections": {}, "runtimeConfig": null, "materializer": 3, "rate_weight": null, @@ -112,7 +109,6 @@ snapshot[`Typegraph using kv 1`] = ` ], "input": 10, "output": 12, - "injections": {}, "runtimeConfig": null, "materializer": 4, "rate_weight": null, @@ -149,7 +145,6 @@ snapshot[`Typegraph using kv 1`] = ` ], "input": 14, "output": 16, - "injections": {}, "runtimeConfig": null, "materializer": 5, "rate_weight": null, @@ -315,7 +310,6 @@ snapshot[`Typegraph using kv 2`] = ` ], "input": 2, "output": 4, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -351,7 +345,6 @@ snapshot[`Typegraph using kv 2`] = ` ], "input": 6, "output": 3, - "injections": {}, "runtimeConfig": null, "materializer": 2, "rate_weight": null, @@ -376,7 +369,6 @@ snapshot[`Typegraph using kv 2`] = ` ], "input": 2, "output": 8, - "injections": {}, "runtimeConfig": null, "materializer": 3, "rate_weight": null, @@ -395,7 +387,6 @@ snapshot[`Typegraph using kv 2`] = ` ], "input": 10, "output": 12, - "injections": {}, "runtimeConfig": null, "materializer": 4, "rate_weight": null, @@ -432,7 +423,6 @@ snapshot[`Typegraph using kv 2`] = ` ], "input": 14, "output": 16, - "injections": {}, "runtimeConfig": null, "materializer": 5, "rate_weight": null, diff --git a/tests/runtimes/s3/__snapshots__/s3_test.ts.snap b/tests/runtimes/s3/__snapshots__/s3_test.ts.snap index 6db6d67019..050dbedbfd 100644 --- a/tests/runtimes/s3/__snapshots__/s3_test.ts.snap +++ b/tests/runtimes/s3/__snapshots__/s3_test.ts.snap @@ -31,7 +31,6 @@ snapshot[`s3 typegraphs 1`] = ` ], "input": 2, "output": 5, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -106,7 +105,6 @@ snapshot[`s3 typegraphs 1`] = ` ], "input": 11, "output": 12, - "injections": {}, "runtimeConfig": null, "materializer": 2, "rate_weight": null, @@ -136,7 +134,6 @@ snapshot[`s3 typegraphs 1`] = ` ], "input": 14, "output": 12, - "injections": {}, "runtimeConfig": null, "materializer": 3, "rate_weight": null, @@ -161,7 +158,6 @@ snapshot[`s3 typegraphs 1`] = ` ], "input": 16, "output": 19, - "injections": {}, "runtimeConfig": null, "materializer": 4, "rate_weight": null, @@ -206,7 +202,6 @@ snapshot[`s3 typegraphs 1`] = ` ], "input": 21, "output": 19, - "injections": {}, "runtimeConfig": null, "materializer": 5, "rate_weight": null, diff --git a/tests/runtimes/temporal/__snapshots__/temporal_test.ts.snap b/tests/runtimes/temporal/__snapshots__/temporal_test.ts.snap index ecc325b509..ea0b481d09 100644 --- a/tests/runtimes/temporal/__snapshots__/temporal_test.ts.snap +++ b/tests/runtimes/temporal/__snapshots__/temporal_test.ts.snap @@ -30,7 +30,6 @@ snapshot[`Typegraph using temporal 1`] = ` ], "input": 2, "output": 3, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -77,7 +76,6 @@ snapshot[`Typegraph using temporal 1`] = ` ], "input": 7, "output": 3, - "injections": {}, "runtimeConfig": null, "materializer": 2, "rate_weight": null, @@ -155,7 +153,6 @@ snapshot[`Typegraph using temporal 1`] = ` ], "input": 14, "output": 15, - "injections": {}, "runtimeConfig": null, "materializer": 4, "rate_weight": null, @@ -344,7 +341,6 @@ snapshot[`Typegraph using temporal 2`] = ` ], "input": 2, "output": 3, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -389,7 +385,6 @@ snapshot[`Typegraph using temporal 2`] = ` ], "input": 7, "output": 9, - "injections": {}, "runtimeConfig": null, "materializer": 2, "rate_weight": null, @@ -428,7 +423,6 @@ snapshot[`Typegraph using temporal 2`] = ` ], "input": 11, "output": 14, - "injections": {}, "runtimeConfig": null, "materializer": 3, "rate_weight": null, @@ -476,7 +470,6 @@ snapshot[`Typegraph using temporal 2`] = ` ], "input": 16, "output": 17, - "injections": {}, "runtimeConfig": null, "materializer": 4, "rate_weight": null, diff --git a/tests/utils/bindings_test.ts b/tests/utils/bindings_test.ts index 616c906034..42689515a3 100644 --- a/tests/utils/bindings_test.ts +++ b/tests/utils/bindings_test.ts @@ -57,7 +57,6 @@ Deno.test("typegraphValidate", () => { ], "input": 2, "output": 4, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -116,10 +115,13 @@ Deno.test("typegraphValidate", () => { "artifacts": {}, }, }; - const str = JSON.stringify(json); - assertEquals(JSON.stringify(JSON.parse(Meta.typegraphValidate(str))), str); + const str = JSON.stringify(json, null, 2); + assertEquals( + JSON.stringify(JSON.parse(Meta.typegraphValidate(str)), null, 2), + str, + ); const out = typegraph_validate({ json: str }); assert("Valid" in out); - assertEquals(JSON.stringify(JSON.parse(out.Valid.json)), str); + assertEquals(JSON.stringify(JSON.parse(out.Valid.json), null, 2), str); }); diff --git a/tools/test.ts b/tools/test.ts index 44a9c7de21..c27af1ea6f 100755 --- a/tools/test.ts +++ b/tools/test.ts @@ -6,10 +6,11 @@ /** * A utility script to run tests. * - * Usage: deno run -A test.ts ... [-- ...] + * Usage: ghjk x test-e2e ... [-- ...] * * : * test directory name or a file name, relative to the `tests` directory. + * You can omit the extension or the `_test.ts` suffix. * These paths specifies which tests to run. * An empty list will run all the tests. * @@ -17,7 +18,7 @@ * arguments to path to the `deno test` command. * * Example: - * $ deno run -A test.ts policies prisma/one_to_many_test.ts -- -q + * $ ghjk x test-e2e policies prisma/one_to_many_test.ts -- -q * > run all the tests under tests/policies and the test file tests/prisma/one_to_many_test.ts, * with the -q flag. */ @@ -53,7 +54,25 @@ async function listTestFiles(filesArg: string[]): Promise { path = wd.resolve("tests", inPath); stat = await path.stat(); if (!stat) { - throw new Error(`unable to resolve test files under "${inPath}"`); + const pathStr = path.toString(); + if (!pathStr.endsWith("_test.ts")) { + let suffixedPath: typeof path; + if (pathStr.endsWith("_test")) { + suffixedPath = $.path(pathStr + ".ts"); + } else { + suffixedPath = $.path(pathStr + "_test.ts"); + } + const stat2 = await suffixedPath.stat(); + if (!stat2) { + throw new Error( + `unable to resolve test files under "${inPath}", nor "${suffixedPath.toString()}"`, + ); + } + path = suffixedPath; + stat = stat2; + } else { + throw new Error(`unable to resolve test files under "${inPath}"`); + } } } if (stat.isDirectory) {