From 8c0022eb6d2c43c8febf1d4c277822e6f2649a8a Mon Sep 17 00:00:00 2001 From: Luckas Date: Mon, 6 Jan 2025 06:16:42 +0300 Subject: [PATCH] fix: `selectAll` infinite recursion (#948) - Closes [MET-786](https://linear.app/metatypedev/issue/MET-786/typescript-client-selectall-infinite-recursion). #### Migration notes --- - [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 ## Summary by CodeRabbit - **New Features** - Added nested composite structure support across multiple client implementations - Enhanced selection handling for composite queries - Expanded type definitions for more complex data representations - **Bug Fixes** - Improved selection processing logic in client implementations - Updated version compatibility for SDK imports - **Chores** - Updated package dependencies to newer SDK versions - Reformatted and improved code readability across multiple files --- examples/templates/deno/api/example.ts | 2 +- src/metagen-client-rs/src/selection.rs | 3 +- src/metagen/src/client_py/static/client.py | 21 +- src/metagen/src/client_ts/static/mod.ts | 6 + tests/injection/random_injection_test.ts | 253 ++++++------ tests/metagen/metagen_test.ts | 19 + tests/metagen/typegraphs/identities/rs/fdk.rs | 2 +- tests/metagen/typegraphs/sample.ts | 88 ++-- tests/metagen/typegraphs/sample/py/client.py | 391 +++++++++++++----- tests/metagen/typegraphs/sample/py/main.py | 29 +- .../typegraphs/sample/py_upload/client.py | 21 +- tests/metagen/typegraphs/sample/rs/client.rs | 110 +++++ tests/metagen/typegraphs/sample/rs/main.rs | 35 ++ tests/metagen/typegraphs/sample/ts/client.ts | 85 ++++ tests/metagen/typegraphs/sample/ts/main.ts | 125 ++++-- .../typegraphs/sample/ts_upload/client.ts | 6 + 16 files changed, 880 insertions(+), 316 deletions(-) diff --git a/examples/templates/deno/api/example.ts b/examples/templates/deno/api/example.ts index 84ed8f73f..2e0c5e11e 100644 --- a/examples/templates/deno/api/example.ts +++ b/examples/templates/deno/api/example.ts @@ -1,4 +1,4 @@ -import { Policy, t, typegraph } from "jsr:@typegraph/sdk@0.5.0-rc.7"; +import { Policy, t, typegraph } from "jsr:@typegraph/sdk@0.5.0-rc.9"; import { PythonRuntime } from "jsr:@typegraph/sdk@0.5.0-rc.9/runtimes/python"; import { DenoRuntime } from "jsr:@typegraph/sdk@0.5.0-rc.9/runtimes/deno"; diff --git a/src/metagen-client-rs/src/selection.rs b/src/metagen-client-rs/src/selection.rs index 103d372c0..bd6e70225 100644 --- a/src/metagen-client-rs/src/selection.rs +++ b/src/metagen-client-rs/src/selection.rs @@ -393,8 +393,7 @@ where SelT: Selection + Into, { fn all() -> Self { - let sel = SelT::all(); - Self::Get(sel.into(), PhantomData) + Self::Skip } } impl Selection for CompositeSelectArgs diff --git a/src/metagen/src/client_py/static/client.py b/src/metagen/src/client_py/static/client.py index d3131ef62..ad78b7282 100644 --- a/src/metagen/src/client_py/static/client.py +++ b/src/metagen/src/client_py/static/client.py @@ -17,12 +17,12 @@ def selection_to_nodes( parent_path: str, ) -> typing.List["SelectNode[typing.Any]"]: out = [] - flags = selection.get("_") - if flags is not None and not isinstance(flags, SelectionFlags): + sub_flags = selection.get("_") + if sub_flags is not None and not isinstance(sub_flags, SelectionFlags): raise Exception( - f"selection field '_' should be of type SelectionFlags but found {type(flags)}" + f"selection field '_' should be of type SelectionFlags but found {type(sub_flags)}" ) - select_all = True if flags is not None and flags.select_all else False + select_all = True if sub_flags is not None and sub_flags.select_all else False found_nodes = set(selection.keys()) for node_name, meta_fn in metas.items(): found_nodes.discard(node_name) @@ -104,7 +104,7 @@ def selection_to_nodes( # flags are recursive for any subnode that's not specified if sub_selections is None: - sub_selections = {"_": flags} + sub_selections = {"_": sub_flags} # selection types are always TypedDicts as well if not isinstance(sub_selections, dict): @@ -119,6 +119,17 @@ def selection_to_nodes( raise Exception( "unreachable: union/either NodeMetas can't have subnodes" ) + + # skip non explicit composite selection when using select_all + sub_flags = sub_selections.get("_") + + if ( + isinstance(sub_flags, SelectionFlags) + and sub_flags.select_all + and instance_selection is None + ): + continue + sub_nodes = selection_to_nodes( typing.cast("SelectionErased", sub_selections), meta.sub_nodes, diff --git a/src/metagen/src/client_ts/static/mod.ts b/src/metagen/src/client_ts/static/mod.ts index c1911f318..dbd1d2580 100644 --- a/src/metagen/src/client_ts/static/mod.ts +++ b/src/metagen/src/client_ts/static/mod.ts @@ -107,6 +107,12 @@ function _selectionToNodeSet( "unreachable: union/either NodeMetas can't have subnodes", ); } + + // skip non explicit composite selection when using selectAll + if (subSelections?._ === "selectAll" && !instanceSelection) { + continue; + } + node.subNodes = _selectionToNodeSet( // assume it's a Selection. If it's an argument // object, mismatch between the node desc should hopefully diff --git a/tests/injection/random_injection_test.ts b/tests/injection/random_injection_test.ts index 93061d16c..54eb95627 100644 --- a/tests/injection/random_injection_test.ts +++ b/tests/injection/random_injection_test.ts @@ -17,62 +17,65 @@ const cases = [ ]; for (const testCase of cases) { - Meta.test({ - name: testCase.testName, - only: false, - }, async (t) => { - const engine = await t.engine(testCase.typegraph); + Meta.test( + { + name: testCase.testName, + only: false, + }, + async (t) => { + const engine = await t.engine(testCase.typegraph); - await t.should("generate random values", async () => { - await gql` - query { - randomUser { - id - ean - name - age - married - birthday - phone - gender - firstname - lastname - friends - occupation - street - city - postcode - country - uri - hostname + await t.should("generate random values", async () => { + await gql` + query { + randomUser { + id + ean + name + age + married + birthday + phone + gender + firstname + lastname + friends + occupation + street + city + postcode + country + uri + hostname + } } - } - ` - .expectData({ - randomUser: { - id: "1069ace0-cdb1-5c1f-8193-81f53d29da35", - ean: "0497901391205", - name: "Landon Glover", - age: 38, - married: true, - birthday: "2124-06-22T22:00:07.302Z", - phone: "(587) 901-3720", - gender: "Male", - firstname: "Landon", - lastname: "Mengoni", - friends: ["Hettie", "Mary", "Lydia", "Ethel", "Jennie"], - occupation: "Health Care Manager", - street: "837 Wubju Drive", - city: "Urbahfec", - postcode: "IM9 9AD", - country: "Indonesia", - uri: "http://wubju.bs/ma", - hostname: "wubju.bs", - }, - }) - .on(engine); - }); - }); + ` + .expectData({ + randomUser: { + id: "1069ace0-cdb1-5c1f-8193-81f53d29da35", + ean: "0497901391205", + name: "Landon Glover", + age: 38, + married: true, + birthday: "2125-06-22T22:00:07.302Z", + phone: "(587) 901-3720", + gender: "Male", + firstname: "Landon", + lastname: "Mengoni", + friends: ["Hettie", "Mary", "Lydia", "Ethel", "Jennie"], + occupation: "Health Care Manager", + street: "837 Wubju Drive", + city: "Urbahfec", + postcode: "IM9 9AD", + country: "Indonesia", + uri: "http://wubju.bs/ma", + hostname: "wubju.bs", + }, + }) + .on(engine); + }); + }, + ); } Meta.test("random injection on unions", async (t) => { @@ -80,88 +83,92 @@ Meta.test("random injection on unions", async (t) => { await t.should("work on random lists", async () => { await gql` - query { - randomList { - names - } - } - `.expectData({ - randomList: { - names: [ - "Hettie Huff", - "Ada Mills", - "Ethel Marshall", - "Emily Gonzales", - "Lottie Barber", - ], - }, - }).on(engine); + query { + randomList { + names + } + } + ` + .expectData({ + randomList: { + names: [ + "Hettie Huff", + "Ada Mills", + "Ethel Marshall", + "Emily Gonzales", + "Lottie Barber", + ], + }, + }) + .on(engine); }); await t.should( "generate random values for enums, either and union variants", async () => { await gql` - query { - testEnumStr { - educationLevel - }, - testEnumInt { - bits - }, - testEnumFloat { - cents - }, - testEither { - toy { - ... on Toygun { - color - } - ... on Rubix { - name, - size - } + query { + testEnumStr { + educationLevel } - }, - testUnion { - field { - ... on Rgb { - R - G - B + testEnumInt { + bits + } + testEnumFloat { + cents + } + testEither { + toy { + ... on Toygun { + color + } + ... on Rubix { + name + size + } } - ... on Vec { - x - y - z + } + testUnion { + field { + ... on Rgb { + R + G + B + } + ... on Vec { + x + y + z + } } } } - } - `.expectData({ - testEnumStr: { - educationLevel: "secondary", - }, - testEnumInt: { - bits: 0, - }, - testEnumFloat: { - cents: 0.5, - }, - testEither: { - toy: { - name: "1*ajw]krgDnCzXD*N!Fx", - size: 3336617896968192, + ` + .expectData({ + testEnumStr: { + educationLevel: "secondary", }, - }, - testUnion: { - field: { - B: 779226068287.488, - G: 396901315143.2704, - R: 895648526657.1263, + testEnumInt: { + bits: 0, }, - }, - }).on(engine); + testEnumFloat: { + cents: 0.5, + }, + testEither: { + toy: { + name: "1*ajw]krgDnCzXD*N!Fx", + size: 3336617896968192, + }, + }, + testUnion: { + field: { + B: 779226068287.488, + G: 396901315143.2704, + R: 895648526657.1263, + }, + }, + }) + .on(engine); }, ); }); diff --git a/tests/metagen/metagen_test.ts b/tests/metagen/metagen_test.ts index 3104218d1..88e48683e 100644 --- a/tests/metagen/metagen_test.ts +++ b/tests/metagen/metagen_test.ts @@ -592,6 +592,24 @@ Meta.test( compositeNoArgs: postSchema, compositeArgs: postSchema, }); + const expectedSchemaC = zod.object({ + scalarOnly: zod.object({ scalar: zod.number() }), + withStruct: zod.object({ + scalar: zod.number(), + composite: zod.object({ value: zod.number() }), + }), + withStructNested: zod.object({ + scalar: zod.number(), + composite: zod.object({ + value: zod.number(), + nested: zod.object({ inner: zod.number() }), + }), + }), + withList: zod.object({ + scalar: zod.number(), + list: zod.array(zod.object({ value: zod.number() })), + }), + }); const expectedSchema = zod.tuple([ expectedSchemaQ, expectedSchemaQ, @@ -604,6 +622,7 @@ Meta.test( compositeUnion2: zod.object({}), mixedUnion: zod.string(), }), + expectedSchemaC, ]); const cases = [ { diff --git a/tests/metagen/typegraphs/identities/rs/fdk.rs b/tests/metagen/typegraphs/identities/rs/fdk.rs index fc7a76483..e5cc2e6a7 100644 --- a/tests/metagen/typegraphs/identities/rs/fdk.rs +++ b/tests/metagen/typegraphs/identities/rs/fdk.rs @@ -109,7 +109,7 @@ impl Router { } pub fn init(&self, args: InitArgs) -> Result { - static MT_VERSION: &str = "0.5.0-rc.8"; + static MT_VERSION: &str = "0.5.0-rc.9"; if args.metatype_version != MT_VERSION { return Err(InitError::VersionMismatch(MT_VERSION.into())); } diff --git a/tests/metagen/typegraphs/sample.ts b/tests/metagen/typegraphs/sample.ts index 9657c0927..11fc4405f 100644 --- a/tests/metagen/typegraphs/sample.ts +++ b/tests/metagen/typegraphs/sample.ts @@ -22,55 +22,60 @@ export const tg = await typegraph({ const random = new RandomRuntime({ seed: 0 }); const deno = new DenoRuntime(); - const post = t.struct({ - id: t.uuid({ asId: true, config: { auto: true } }), - slug: t.string(), - title: t.string(), - }, { name: "post" }); + const post = t.struct( + { + id: t.uuid({ asId: true, config: { auto: true } }), + slug: t.string(), + title: t.string(), + }, + { name: "post" }, + ); - const user = t.struct({ - id: t.uuid({ asId: true, config: { auto: true } }), - email: t.email(), - posts: t.list(g.ref("post")), - }, { name: "user" }); + const user = t.struct( + { + id: t.uuid({ asId: true, config: { auto: true } }), + email: t.email(), + posts: t.list(g.ref("post")), + }, + { name: "user" }, + ); const compositeUnion = t.union([post, user]); const scalarUnion = t.union([t.string(), t.integer()]); const mixedUnion = t.union([post, user, t.string(), t.integer()]); + const nestedComposite = t.struct({ + scalar: t.integer(), + composite: t.struct({ + value: t.integer(), + nested: t.struct({ + inner: t.integer(), + }), + }), + list: t.list(t.struct({ value: t.integer() })), + }); + g.expose( { getUser: random.gen(user), getPosts: random.gen(post), scalarNoArgs: random.gen(t.string()), - scalarArgs: deno.func( - post, - t.string(), - { - code: () => "hello", - effect: fx.update(), - }, - ), + scalarArgs: deno.func(post, t.string(), { + code: () => "hello", + effect: fx.update(), + }), compositeNoArgs: deno.func(t.struct({}), post, { code: genPost, effect: fx.update(), }), - compositeArgs: deno.func( - t.struct({ id: t.string() }), - post, - { - code: genPost, - effect: fx.update(), - }, - ), - scalarUnion: deno.func( - t.struct({ id: t.string() }), - scalarUnion, - { - code: () => "hello", - }, - ), + compositeArgs: deno.func(t.struct({ id: t.string() }), post, { + code: genPost, + effect: fx.update(), + }), + scalarUnion: deno.func(t.struct({ id: t.string() }), scalarUnion, { + code: () => "hello", + }), compositeUnion: deno.func( t.struct({ id: t.string() }), compositeUnion, @@ -78,13 +83,16 @@ export const tg = await typegraph({ code: genPost, }, ), - mixedUnion: deno.func( - t.struct({ id: t.string() }), - mixedUnion, - { - code: () => "hello", - }, - ), + mixedUnion: deno.func(t.struct({ id: t.string() }), mixedUnion, { + code: () => "hello", + }), + nestedComposite: deno.func(t.struct({}), nestedComposite, { + code: () => ({ + scalar: 0, + composite: { value: 1, nested: { inner: 2 } }, + list: [{ value: 3 }], + }), + }), }, Policy.public(), ); diff --git a/tests/metagen/typegraphs/sample/py/client.py b/tests/metagen/typegraphs/sample/py/client.py index c1d09cedf..d19492338 100644 --- a/tests/metagen/typegraphs/sample/py/client.py +++ b/tests/metagen/typegraphs/sample/py/client.py @@ -20,12 +20,12 @@ def selection_to_nodes( parent_path: str, ) -> typing.List["SelectNode[typing.Any]"]: out = [] - flags = selection.get("_") - if flags is not None and not isinstance(flags, SelectionFlags): + sub_flags = selection.get("_") + if sub_flags is not None and not isinstance(sub_flags, SelectionFlags): raise Exception( - f"selection field '_' should be of type SelectionFlags but found {type(flags)}" + f"selection field '_' should be of type SelectionFlags but found {type(sub_flags)}" ) - select_all = True if flags is not None and flags.select_all else False + select_all = True if sub_flags is not None and sub_flags.select_all else False found_nodes = set(selection.keys()) for node_name, meta_fn in metas.items(): found_nodes.discard(node_name) @@ -107,7 +107,7 @@ def selection_to_nodes( # flags are recursive for any subnode that's not specified if sub_selections is None: - sub_selections = {"_": flags} + sub_selections = {"_": sub_flags} # selection types are always TypedDicts as well if not isinstance(sub_selections, dict): @@ -122,6 +122,17 @@ def selection_to_nodes( raise Exception( "unreachable: union/either NodeMetas can't have subnodes" ) + + # skip non explicit composite selection when using select_all + sub_flags = sub_selections.get("_") + + if ( + isinstance(sub_flags, SelectionFlags) + and sub_flags.select_all + and instance_selection is None + ): + continue + sub_nodes = selection_to_nodes( typing.cast("SelectionErased", sub_selections), meta.sub_nodes, @@ -809,7 +820,7 @@ class NodeDescs: @staticmethod def scalar(): return NodeMeta() - + @staticmethod def Post(): return NodeMeta( @@ -937,29 +948,85 @@ def RootMixedUnionFn(): }, ) + @staticmethod + def RootNestedCompositeFnOutputCompositeStructNestedStruct(): + return NodeMeta( + sub_nodes={ + "inner": NodeDescs.scalar, + }, + ) + + @staticmethod + def RootNestedCompositeFnOutputCompositeStruct(): + return NodeMeta( + sub_nodes={ + "value": NodeDescs.scalar, + "nested": NodeDescs.RootNestedCompositeFnOutputCompositeStructNestedStruct, + }, + ) + + @staticmethod + def RootNestedCompositeFnOutputListStruct(): + return NodeMeta( + sub_nodes={ + "value": NodeDescs.scalar, + }, + ) + + @staticmethod + def RootNestedCompositeFnOutput(): + return NodeMeta( + sub_nodes={ + "scalar": NodeDescs.scalar, + "composite": NodeDescs.RootNestedCompositeFnOutputCompositeStruct, + "list": NodeDescs.RootNestedCompositeFnOutputListStruct, + }, + ) + + @staticmethod + def RootNestedCompositeFn(): + return_node = NodeDescs.RootNestedCompositeFnOutput() + return NodeMeta( + sub_nodes=return_node.sub_nodes, + variants=return_node.variants, + ) + + UserIdStringUuid = str PostSlugString = str -Post = typing.TypedDict("Post", { - "id": UserIdStringUuid, - "slug": PostSlugString, - "title": PostSlugString, -}, total=False) - -RootCompositeArgsFnInput = typing.TypedDict("RootCompositeArgsFnInput", { - "id": PostSlugString, -}, total=False) +Post = typing.TypedDict( + "Post", + { + "id": UserIdStringUuid, + "slug": PostSlugString, + "title": PostSlugString, + }, + total=False, +) + +RootCompositeArgsFnInput = typing.TypedDict( + "RootCompositeArgsFnInput", + { + "id": PostSlugString, + }, + total=False, +) UserEmailStringEmail = str UserPostsPostList = typing.List[Post] -User = typing.TypedDict("User", { - "id": UserIdStringUuid, - "email": UserEmailStringEmail, - "posts": UserPostsPostList, -}, total=False) +User = typing.TypedDict( + "User", + { + "id": UserIdStringUuid, + "email": UserEmailStringEmail, + "posts": UserPostsPostList, + }, + total=False, +) RootScalarUnionFnOutputT1Integer = int @@ -983,111 +1050,249 @@ def RootMixedUnionFn(): ] +RootNestedCompositeFnOutputCompositeStructNestedStruct = typing.TypedDict( + "RootNestedCompositeFnOutputCompositeStructNestedStruct", + { + "inner": RootScalarUnionFnOutputT1Integer, + }, + total=False, +) + +RootNestedCompositeFnOutputCompositeStruct = typing.TypedDict( + "RootNestedCompositeFnOutputCompositeStruct", + { + "value": RootScalarUnionFnOutputT1Integer, + "nested": RootNestedCompositeFnOutputCompositeStructNestedStruct, + }, + total=False, +) + +RootNestedCompositeFnOutputListStruct = typing.TypedDict( + "RootNestedCompositeFnOutputListStruct", + { + "value": RootScalarUnionFnOutputT1Integer, + }, + total=False, +) + +RootNestedCompositeFnOutputListRootNestedCompositeFnOutputListStructList = typing.List[ + RootNestedCompositeFnOutputListStruct +] -PostSelections = typing.TypedDict("PostSelections", { - "_": SelectionFlags, - "id": ScalarSelectNoArgs, - "slug": ScalarSelectNoArgs, - "title": ScalarSelectNoArgs, -}, total=False) - -UserSelections = typing.TypedDict("UserSelections", { - "_": SelectionFlags, - "id": ScalarSelectNoArgs, - "email": ScalarSelectNoArgs, - "posts": CompositeSelectNoArgs["PostSelections"], -}, total=False) - -RootCompositeUnionFnOutputSelections = typing.TypedDict("RootCompositeUnionFnOutputSelections", { - "_": SelectionFlags, - "post": CompositeSelectNoArgs["PostSelections"], - "user": CompositeSelectNoArgs["UserSelections"], -}, total=False) - -RootMixedUnionFnOutputSelections = typing.TypedDict("RootMixedUnionFnOutputSelections", { - "_": SelectionFlags, - "post": CompositeSelectNoArgs["PostSelections"], - "user": CompositeSelectNoArgs["UserSelections"], -}, total=False) +RootNestedCompositeFnOutput = typing.TypedDict( + "RootNestedCompositeFnOutput", + { + "scalar": RootScalarUnionFnOutputT1Integer, + "composite": RootNestedCompositeFnOutputCompositeStruct, + "list": RootNestedCompositeFnOutputListRootNestedCompositeFnOutputListStructList, + }, + total=False, +) + + +PostSelections = typing.TypedDict( + "PostSelections", + { + "_": SelectionFlags, + "id": ScalarSelectNoArgs, + "slug": ScalarSelectNoArgs, + "title": ScalarSelectNoArgs, + }, + total=False, +) + +UserSelections = typing.TypedDict( + "UserSelections", + { + "_": SelectionFlags, + "id": ScalarSelectNoArgs, + "email": ScalarSelectNoArgs, + "posts": CompositeSelectNoArgs["PostSelections"], + }, + total=False, +) + +RootCompositeUnionFnOutputSelections = typing.TypedDict( + "RootCompositeUnionFnOutputSelections", + { + "_": SelectionFlags, + "post": CompositeSelectNoArgs["PostSelections"], + "user": CompositeSelectNoArgs["UserSelections"], + }, + total=False, +) + +RootMixedUnionFnOutputSelections = typing.TypedDict( + "RootMixedUnionFnOutputSelections", + { + "_": SelectionFlags, + "post": CompositeSelectNoArgs["PostSelections"], + "user": CompositeSelectNoArgs["UserSelections"], + }, + total=False, +) + +RootNestedCompositeFnOutputCompositeStructNestedStructSelections = typing.TypedDict( + "RootNestedCompositeFnOutputCompositeStructNestedStructSelections", + { + "_": SelectionFlags, + "inner": ScalarSelectNoArgs, + }, + total=False, +) + +RootNestedCompositeFnOutputCompositeStructSelections = typing.TypedDict( + "RootNestedCompositeFnOutputCompositeStructSelections", + { + "_": SelectionFlags, + "value": ScalarSelectNoArgs, + "nested": CompositeSelectNoArgs[ + "RootNestedCompositeFnOutputCompositeStructNestedStructSelections" + ], + }, + total=False, +) + +RootNestedCompositeFnOutputListStructSelections = typing.TypedDict( + "RootNestedCompositeFnOutputListStructSelections", + { + "_": SelectionFlags, + "value": ScalarSelectNoArgs, + }, + total=False, +) + +RootNestedCompositeFnOutputSelections = typing.TypedDict( + "RootNestedCompositeFnOutputSelections", + { + "_": SelectionFlags, + "scalar": ScalarSelectNoArgs, + "composite": CompositeSelectNoArgs[ + "RootNestedCompositeFnOutputCompositeStructSelections" + ], + "list": CompositeSelectNoArgs[ + "RootNestedCompositeFnOutputListStructSelections" + ], + }, + total=False, +) class QueryGraph(QueryGraphBase): def __init__(self): - super().__init__({ - "UserIdStringUuid": "String!", - "PostSlugString": "String!", - "post": "post!", - "user": "user!", - }) - + super().__init__( + { + "UserIdStringUuid": "String!", + "PostSlugString": "String!", + "post": "post!", + "user": "user!", + } + ) + def get_user(self, select: UserSelections) -> QueryNode[User]: node = selection_to_nodes( - {"getUser": select}, - {"getUser": NodeDescs.RootGetUserFn}, - "$q" + {"getUser": select}, {"getUser": NodeDescs.RootGetUserFn}, "$q" )[0] - return QueryNode(node.node_name, node.instance_name, node.args, node.sub_nodes, node.files) + return QueryNode( + node.node_name, node.instance_name, node.args, node.sub_nodes, node.files + ) def get_posts(self, select: PostSelections) -> QueryNode[Post]: node = selection_to_nodes( - {"getPosts": select}, - {"getPosts": NodeDescs.RootGetPostsFn}, - "$q" + {"getPosts": select}, {"getPosts": NodeDescs.RootGetPostsFn}, "$q" )[0] - return QueryNode(node.node_name, node.instance_name, node.args, node.sub_nodes, node.files) + return QueryNode( + node.node_name, node.instance_name, node.args, node.sub_nodes, node.files + ) def scalar_no_args(self) -> QueryNode[PostSlugString]: node = selection_to_nodes( - {"scalarNoArgs": True}, - {"scalarNoArgs": NodeDescs.RootScalarNoArgsFn}, - "$q" + {"scalarNoArgs": True}, {"scalarNoArgs": NodeDescs.RootScalarNoArgsFn}, "$q" )[0] - return QueryNode(node.node_name, node.instance_name, node.args, node.sub_nodes, node.files) + return QueryNode( + node.node_name, node.instance_name, node.args, node.sub_nodes, node.files + ) - def scalar_args(self, args: typing.Union[Post, PlaceholderArgs]) -> MutationNode[PostSlugString]: + def scalar_args( + self, args: typing.Union[Post, PlaceholderArgs] + ) -> MutationNode[PostSlugString]: node = selection_to_nodes( - {"scalarArgs": args}, - {"scalarArgs": NodeDescs.RootScalarArgsFn}, - "$q" + {"scalarArgs": args}, {"scalarArgs": NodeDescs.RootScalarArgsFn}, "$q" )[0] - return MutationNode(node.node_name, node.instance_name, node.args, node.sub_nodes, node.files) + return MutationNode( + node.node_name, node.instance_name, node.args, node.sub_nodes, node.files + ) def composite_no_args(self, select: PostSelections) -> MutationNode[Post]: node = selection_to_nodes( - {"compositeNoArgs": select}, - {"compositeNoArgs": NodeDescs.RootCompositeNoArgsFn}, - "$q" + {"compositeNoArgs": select}, + {"compositeNoArgs": NodeDescs.RootCompositeNoArgsFn}, + "$q", + )[0] + return MutationNode( + node.node_name, node.instance_name, node.args, node.sub_nodes, node.files + ) + + def composite_args( + self, + args: typing.Union[RootCompositeArgsFnInput, PlaceholderArgs], + select: PostSelections, + ) -> MutationNode[Post]: + node = selection_to_nodes( + {"compositeArgs": (args, select)}, + {"compositeArgs": NodeDescs.RootCompositeArgsFn}, + "$q", )[0] - return MutationNode(node.node_name, node.instance_name, node.args, node.sub_nodes, node.files) + return MutationNode( + node.node_name, node.instance_name, node.args, node.sub_nodes, node.files + ) - def composite_args(self, args: typing.Union[RootCompositeArgsFnInput, PlaceholderArgs], select: PostSelections) -> MutationNode[Post]: + def scalar_union( + self, args: typing.Union[RootCompositeArgsFnInput, PlaceholderArgs] + ) -> QueryNode[RootScalarUnionFnOutput]: node = selection_to_nodes( - {"compositeArgs": (args, select)}, - {"compositeArgs": NodeDescs.RootCompositeArgsFn}, - "$q" + {"scalarUnion": args}, {"scalarUnion": NodeDescs.RootScalarUnionFn}, "$q" )[0] - return MutationNode(node.node_name, node.instance_name, node.args, node.sub_nodes, node.files) + return QueryNode( + node.node_name, node.instance_name, node.args, node.sub_nodes, node.files + ) - def scalar_union(self, args: typing.Union[RootCompositeArgsFnInput, PlaceholderArgs]) -> QueryNode[RootScalarUnionFnOutput]: + def composite_union( + self, + args: typing.Union[RootCompositeArgsFnInput, PlaceholderArgs], + select: RootCompositeUnionFnOutputSelections, + ) -> QueryNode[RootCompositeUnionFnOutput]: node = selection_to_nodes( - {"scalarUnion": args}, - {"scalarUnion": NodeDescs.RootScalarUnionFn}, - "$q" + {"compositeUnion": (args, select)}, + {"compositeUnion": NodeDescs.RootCompositeUnionFn}, + "$q", )[0] - return QueryNode(node.node_name, node.instance_name, node.args, node.sub_nodes, node.files) + return QueryNode( + node.node_name, node.instance_name, node.args, node.sub_nodes, node.files + ) - def composite_union(self, args: typing.Union[RootCompositeArgsFnInput, PlaceholderArgs], select: RootCompositeUnionFnOutputSelections) -> QueryNode[RootCompositeUnionFnOutput]: + def mixed_union( + self, + args: typing.Union[RootCompositeArgsFnInput, PlaceholderArgs], + select: RootMixedUnionFnOutputSelections, + ) -> QueryNode[RootMixedUnionFnOutput]: node = selection_to_nodes( - {"compositeUnion": (args, select)}, - {"compositeUnion": NodeDescs.RootCompositeUnionFn}, - "$q" + {"mixedUnion": (args, select)}, + {"mixedUnion": NodeDescs.RootMixedUnionFn}, + "$q", )[0] - return QueryNode(node.node_name, node.instance_name, node.args, node.sub_nodes, node.files) + return QueryNode( + node.node_name, node.instance_name, node.args, node.sub_nodes, node.files + ) - def mixed_union(self, args: typing.Union[RootCompositeArgsFnInput, PlaceholderArgs], select: RootMixedUnionFnOutputSelections) -> QueryNode[RootMixedUnionFnOutput]: + def nested_composite( + self, select: RootNestedCompositeFnOutputSelections + ) -> QueryNode[RootNestedCompositeFnOutput]: node = selection_to_nodes( - {"mixedUnion": (args, select)}, - {"mixedUnion": NodeDescs.RootMixedUnionFn}, - "$q" + {"nestedComposite": select}, + {"nestedComposite": NodeDescs.RootNestedCompositeFn}, + "$q", )[0] - return QueryNode(node.node_name, node.instance_name, node.args, node.sub_nodes, node.files) + return QueryNode( + node.node_name, node.instance_name, node.args, node.sub_nodes, node.files + ) diff --git a/tests/metagen/typegraphs/sample/py/main.py b/tests/metagen/typegraphs/sample/py/main.py index 2f9ed1d14..2df0d0f26 100644 --- a/tests/metagen/typegraphs/sample/py/main.py +++ b/tests/metagen/typegraphs/sample/py/main.py @@ -141,4 +141,31 @@ } ) -print(json.dumps([res1, res1a, res2, res3, res4, res5])) +res6 = gql_client.query( + { + "scalarOnly": qg.nested_composite({"_": SelectionFlags(select_all=True)}), + "withStruct": qg.nested_composite( + { + "_": SelectionFlags(select_all=True), + "composite": {"_": SelectionFlags(select_all=True)}, + } + ), + "withStructNested": qg.nested_composite( + { + "_": SelectionFlags(select_all=True), + "composite": { + "_": SelectionFlags(select_all=True), + "nested": {"_": SelectionFlags(select_all=True)}, + }, + } + ), + "withList": qg.nested_composite( + { + "_": SelectionFlags(select_all=True), + "list": {"_": SelectionFlags(select_all=True)}, + } + ), + }, +) + +print(json.dumps([res1, res1a, res2, res3, res4, res5, res6])) diff --git a/tests/metagen/typegraphs/sample/py_upload/client.py b/tests/metagen/typegraphs/sample/py_upload/client.py index ff3873dfe..393857a97 100644 --- a/tests/metagen/typegraphs/sample/py_upload/client.py +++ b/tests/metagen/typegraphs/sample/py_upload/client.py @@ -20,12 +20,12 @@ def selection_to_nodes( parent_path: str, ) -> typing.List["SelectNode[typing.Any]"]: out = [] - flags = selection.get("_") - if flags is not None and not isinstance(flags, SelectionFlags): + sub_flags = selection.get("_") + if sub_flags is not None and not isinstance(sub_flags, SelectionFlags): raise Exception( - f"selection field '_' should be of type SelectionFlags but found {type(flags)}" + f"selection field '_' should be of type SelectionFlags but found {type(sub_flags)}" ) - select_all = True if flags is not None and flags.select_all else False + select_all = True if sub_flags is not None and sub_flags.select_all else False found_nodes = set(selection.keys()) for node_name, meta_fn in metas.items(): found_nodes.discard(node_name) @@ -107,7 +107,7 @@ def selection_to_nodes( # flags are recursive for any subnode that's not specified if sub_selections is None: - sub_selections = {"_": flags} + sub_selections = {"_": sub_flags} # selection types are always TypedDicts as well if not isinstance(sub_selections, dict): @@ -122,6 +122,17 @@ def selection_to_nodes( raise Exception( "unreachable: union/either NodeMetas can't have subnodes" ) + + # skip non explicit composite selection when using select_all + sub_flags = sub_selections.get("_") + + if ( + isinstance(sub_flags, SelectionFlags) + and sub_flags.select_all + and instance_selection is None + ): + continue + sub_nodes = selection_to_nodes( typing.cast("SelectionErased", sub_selections), meta.sub_nodes, diff --git a/tests/metagen/typegraphs/sample/rs/client.rs b/tests/metagen/typegraphs/sample/rs/client.rs index f9829ec77..735fbba47 100644 --- a/tests/metagen/typegraphs/sample/rs/client.rs +++ b/tests/metagen/typegraphs/sample/rs/client.rs @@ -164,6 +164,62 @@ mod node_metas { ..RootMixedUnionFnOutput() } } + pub fn RootNestedCompositeFnOutputCompositeStructNestedStruct() -> NodeMeta { + NodeMeta { + arg_types: None, + variants: None, + sub_nodes: Some( + [ + ("inner".into(), scalar as NodeMetaFn), + ].into() + ), + input_files: None, + } + } + pub fn RootNestedCompositeFnOutputCompositeStruct() -> NodeMeta { + NodeMeta { + arg_types: None, + variants: None, + sub_nodes: Some( + [ + ("value".into(), scalar as NodeMetaFn), + ("nested".into(), RootNestedCompositeFnOutputCompositeStructNestedStruct as NodeMetaFn), + ].into() + ), + input_files: None, + } + } + pub fn RootNestedCompositeFnOutputListStruct() -> NodeMeta { + NodeMeta { + arg_types: None, + variants: None, + sub_nodes: Some( + [ + ("value".into(), scalar as NodeMetaFn), + ].into() + ), + input_files: None, + } + } + pub fn RootNestedCompositeFnOutput() -> NodeMeta { + NodeMeta { + arg_types: None, + variants: None, + sub_nodes: Some( + [ + ("scalar".into(), scalar as NodeMetaFn), + ("composite".into(), RootNestedCompositeFnOutputCompositeStruct as NodeMetaFn), + ("list".into(), RootNestedCompositeFnOutputListStruct as NodeMetaFn), + ].into() + ), + input_files: None, + } + } + pub fn RootNestedCompositeFn() -> NodeMeta { + NodeMeta { + ..RootNestedCompositeFnOutput() + } + } } use types::*; @@ -209,6 +265,26 @@ pub mod types { PostSlugString(PostSlugString), RootScalarUnionFnOutputT1Integer(RootScalarUnionFnOutputT1Integer), } + #[derive(Debug, serde::Serialize, serde::Deserialize)] + pub struct RootNestedCompositeFnOutputCompositeStructNestedStructPartial { + pub inner: Option, + } + #[derive(Debug, serde::Serialize, serde::Deserialize)] + pub struct RootNestedCompositeFnOutputCompositeStructPartial { + pub value: Option, + pub nested: Option, + } + #[derive(Debug, serde::Serialize, serde::Deserialize)] + pub struct RootNestedCompositeFnOutputListStructPartial { + pub value: Option, + } + pub type RootNestedCompositeFnOutputListRootNestedCompositeFnOutputListStructList = Vec; + #[derive(Debug, serde::Serialize, serde::Deserialize)] + pub struct RootNestedCompositeFnOutputPartial { + pub scalar: Option, + pub composite: Option, + pub list: Option, + } } #[derive(Default, Debug)] pub struct PostSelections { @@ -236,6 +312,29 @@ pub struct RootMixedUnionFnOutputSelections { pub user: CompositeSelect, NoAlias>, } impl_union_selection_traits!(RootMixedUnionFnOutputSelections, ("post", post), ("user", user)); +#[derive(Default, Debug)] +pub struct RootNestedCompositeFnOutputCompositeStructNestedStructSelections { + pub inner: ScalarSelect, +} +impl_selection_traits!(RootNestedCompositeFnOutputCompositeStructNestedStructSelections, inner); +#[derive(Default, Debug)] +pub struct RootNestedCompositeFnOutputCompositeStructSelections { + pub value: ScalarSelect, + pub nested: CompositeSelect, ATy>, +} +impl_selection_traits!(RootNestedCompositeFnOutputCompositeStructSelections, value, nested); +#[derive(Default, Debug)] +pub struct RootNestedCompositeFnOutputListStructSelections { + pub value: ScalarSelect, +} +impl_selection_traits!(RootNestedCompositeFnOutputListStructSelections, value); +#[derive(Default, Debug)] +pub struct RootNestedCompositeFnOutputSelections { + pub scalar: ScalarSelect, + pub composite: CompositeSelect, ATy>, + pub list: CompositeSelect, ATy>, +} +impl_selection_traits!(RootNestedCompositeFnOutputSelections, scalar, composite, list); impl QueryGraph { @@ -383,4 +482,15 @@ impl QueryGraph { _marker: PhantomData, } } + pub fn nested_composite( + &self, + ) -> UnselectedNode, QueryMarker, RootNestedCompositeFnOutputPartial> + { + UnselectedNode { + root_name: "nestedComposite".into(), + root_meta: node_metas::RootNestedCompositeFn, + args: NodeArgsErased::None, + _marker: PhantomData, + } + } } diff --git a/tests/metagen/typegraphs/sample/rs/main.rs b/tests/metagen/typegraphs/sample/rs/main.rs index 2de88e6ab..7735f2f31 100644 --- a/tests/metagen/typegraphs/sample/rs/main.rs +++ b/tests/metagen/typegraphs/sample/rs/main.rs @@ -136,6 +136,35 @@ fn main() -> Result<(), BoxErr> { }), )) .await?; + + let res6 = gql + .query(( + api1.nested_composite().select(all()), + api1.nested_composite() + .select(RootNestedCompositeFnOutputSelections { + composite: select(all()), + ..all() + }), + api1.nested_composite() + .select(RootNestedCompositeFnOutputSelections { + composite: select(RootNestedCompositeFnOutputCompositeStructSelections { + nested: select( + RootNestedCompositeFnOutputCompositeStructNestedStructSelections { + ..all() + }, + ), + ..all() + }), + ..all() + }), + api1.nested_composite() + .select(RootNestedCompositeFnOutputSelections { + list: select(all()), + ..all() + }), + )) + .await?; + println!( "{}", serde_json::to_string_pretty(&serde_json::json!([ @@ -169,6 +198,12 @@ fn main() -> Result<(), BoxErr> { "compositeUnion1": res5.1, "compositeUnion2": res5.2, "mixedUnion": res5.3 + }, + { + "scalarOnly": res6.0, + "withStruct": res6.1, + "withStructNested": res6.2, + "withList": res6.3 } ]))? ); diff --git a/tests/metagen/typegraphs/sample/ts/client.ts b/tests/metagen/typegraphs/sample/ts/client.ts index b96737447..fb796ffe7 100644 --- a/tests/metagen/typegraphs/sample/ts/client.ts +++ b/tests/metagen/typegraphs/sample/ts/client.ts @@ -110,6 +110,12 @@ function _selectionToNodeSet( "unreachable: union/either NodeMetas can't have subnodes", ); } + + // skip non explicit composite selection when using selectAll + if (subSelections?._ === "selectAll" && !instanceSelection) { + continue; + } + node.subNodes = _selectionToNodeSet( // assume it's a Selection. If it's an argument // object, mismatch between the node desc should hopefully @@ -968,6 +974,42 @@ const nodeMetas = { }, }; }, + RootNestedCompositeFnOutputCompositeStructNestedStruct(): NodeMeta { + return { + subNodes: [ + ["inner", nodeMetas.scalar], + ], + }; + }, + RootNestedCompositeFnOutputCompositeStruct(): NodeMeta { + return { + subNodes: [ + ["value", nodeMetas.scalar], + ["nested", nodeMetas.RootNestedCompositeFnOutputCompositeStructNestedStruct], + ], + }; + }, + RootNestedCompositeFnOutputListStruct(): NodeMeta { + return { + subNodes: [ + ["value", nodeMetas.scalar], + ], + }; + }, + RootNestedCompositeFnOutput(): NodeMeta { + return { + subNodes: [ + ["scalar", nodeMetas.scalar], + ["composite", nodeMetas.RootNestedCompositeFnOutputCompositeStruct], + ["list", nodeMetas.RootNestedCompositeFnOutputListStruct], + ], + }; + }, + RootNestedCompositeFn(): NodeMeta { + return { + ...nodeMetas.RootNestedCompositeFnOutput(), + }; + }, }; export type UserIdStringUuid = string; export type PostSlugString = string; @@ -998,6 +1040,22 @@ export type RootMixedUnionFnOutput = | (User) | (PostSlugString) | (RootScalarUnionFnOutputT1Integer); +export type RootNestedCompositeFnOutputCompositeStructNestedStruct = { + inner: RootScalarUnionFnOutputT1Integer; +}; +export type RootNestedCompositeFnOutputCompositeStruct = { + value: RootScalarUnionFnOutputT1Integer; + nested: RootNestedCompositeFnOutputCompositeStructNestedStruct; +}; +export type RootNestedCompositeFnOutputListStruct = { + value: RootScalarUnionFnOutputT1Integer; +}; +export type RootNestedCompositeFnOutputListRootNestedCompositeFnOutputListStructList = Array; +export type RootNestedCompositeFnOutput = { + scalar: RootScalarUnionFnOutputT1Integer; + composite: RootNestedCompositeFnOutputCompositeStruct; + list: RootNestedCompositeFnOutputListRootNestedCompositeFnOutputListStructList; +}; export type PostSelections = { _?: SelectionFlags; @@ -1021,6 +1079,25 @@ export type RootMixedUnionFnOutputSelections = { "post"?: CompositeSelectNoArgs; "user"?: CompositeSelectNoArgs; }; +export type RootNestedCompositeFnOutputCompositeStructNestedStructSelections = { + _?: SelectionFlags; + inner?: ScalarSelectNoArgs; +}; +export type RootNestedCompositeFnOutputCompositeStructSelections = { + _?: SelectionFlags; + value?: ScalarSelectNoArgs; + nested?: CompositeSelectNoArgs; +}; +export type RootNestedCompositeFnOutputListStructSelections = { + _?: SelectionFlags; + value?: ScalarSelectNoArgs; +}; +export type RootNestedCompositeFnOutputSelections = { + _?: SelectionFlags; + scalar?: ScalarSelectNoArgs; + composite?: CompositeSelectNoArgs; + list?: CompositeSelectNoArgs; +}; export class QueryGraph extends _QueryGraphBase { constructor() { @@ -1104,4 +1181,12 @@ export class QueryGraph extends _QueryGraphBase { )[0]; return new QueryNode(inner) as QueryNode; } + nestedComposite(select: RootNestedCompositeFnOutputSelections) { + const inner = _selectionToNodeSet( + { "nestedComposite": select }, + [["nestedComposite", nodeMetas.RootNestedCompositeFn]], + "$q", + )[0]; + return new QueryNode(inner) as QueryNode; + } } diff --git a/tests/metagen/typegraphs/sample/ts/main.ts b/tests/metagen/typegraphs/sample/ts/main.ts index 6416ac3cc..896b0221d 100644 --- a/tests/metagen/typegraphs/sample/ts/main.ts +++ b/tests/metagen/typegraphs/sample/ts/main.ts @@ -22,27 +22,32 @@ const preparedQ = gqlClient.prepareQuery(() => ({ scalarNoArgs: api1.scalarNoArgs(), })); -const preparedM = gqlClient.prepareMutation(( - args: PreparedArgs<{ - id: string; - slug: string; - title: string; - }>, -) => ({ - scalarArgs: api1.scalarArgs({ - id: args.get("id"), - slug: args.get("slug"), - title: args.get("title"), - }), - compositeNoArgs: api1.compositeNoArgs({ - _: "selectAll", - }), - compositeArgs: api1.compositeArgs({ - id: args.get("id"), - }, { - _: "selectAll", +const preparedM = gqlClient.prepareMutation( + ( + args: PreparedArgs<{ + id: string; + slug: string; + title: string; + }>, + ) => ({ + scalarArgs: api1.scalarArgs({ + id: args.get("id"), + slug: args.get("slug"), + title: args.get("title"), + }), + compositeNoArgs: api1.compositeNoArgs({ + _: "selectAll", + }), + compositeArgs: api1.compositeArgs( + { + id: args.get("id"), + }, + { + _: "selectAll", + }, + ), }), -})); +); const res1 = await preparedQ.perform({}); const res1a = await preparedQ.perform({}); @@ -75,41 +80,71 @@ const res4 = await gqlClient.mutation({ compositeNoArgs: api1.compositeNoArgs({ _: "selectAll", }), - compositeArgs: api1.compositeArgs({ - id: "94be5420-8c4a-4e67-b4f4-e1b2b54832a2", - }, { - _: "selectAll", - }), + compositeArgs: api1.compositeArgs( + { + id: "94be5420-8c4a-4e67-b4f4-e1b2b54832a2", + }, + { + _: "selectAll", + }, + ), }); const res5 = await gqlClient.query({ scalarUnion: api1.scalarUnion({ id: "94be5420-8c4a-4e67-b4f4-e1b2b54832a2", }), - compositeUnion1: api1.compositeUnion({ - id: "94be5420-8c4a-4e67-b4f4-e1b2b54832a2", - }, { - post: { - "_": "selectAll", + compositeUnion1: api1.compositeUnion( + { + id: "94be5420-8c4a-4e67-b4f4-e1b2b54832a2", }, - }), - compositeUnion2: api1.compositeUnion({ - id: "94be5420-8c4a-4e67-b4f4-e1b2b54832a2", - }, { - user: { - "_": "selectAll", + { + post: { + _: "selectAll", + }, }, - }), - mixedUnion: api1.mixedUnion({ - id: "94be5420-8c4a-4e67-b4f4-e1b2b54832a2", - }, { - post: { - "_": "selectAll", + ), + compositeUnion2: api1.compositeUnion( + { + id: "94be5420-8c4a-4e67-b4f4-e1b2b54832a2", + }, + { + user: { + _: "selectAll", + posts: { _: "selectAll" }, + }, + }, + ), + mixedUnion: api1.mixedUnion( + { + id: "94be5420-8c4a-4e67-b4f4-e1b2b54832a2", }, - user: { - "_": "selectAll", + { + post: { + _: "selectAll", + }, + user: { + _: "selectAll", + posts: { _: "selectAll" }, + }, }, + ), +}); + +const res6 = await gqlClient.query({ + scalarOnly: api1.nestedComposite({ _: "selectAll" }), + withStruct: api1.nestedComposite({ + _: "selectAll", + composite: { _: "selectAll" }, + }), + withStructNested: api1.nestedComposite({ + _: "selectAll", + composite: { _: "selectAll", nested: { _: "selectAll" } }, + }), + withList: api1.nestedComposite({ + _: "selectAll", + list: { _: "selectAll" }, }), }); -console.log(JSON.stringify([res1, res1a, res2, res3, res4, res5])); +console.log(JSON.stringify([res1, res1a, res2, res3, res4, res5, res6])); diff --git a/tests/metagen/typegraphs/sample/ts_upload/client.ts b/tests/metagen/typegraphs/sample/ts_upload/client.ts index 206f12044..bafdb967b 100644 --- a/tests/metagen/typegraphs/sample/ts_upload/client.ts +++ b/tests/metagen/typegraphs/sample/ts_upload/client.ts @@ -110,6 +110,12 @@ function _selectionToNodeSet( "unreachable: union/either NodeMetas can't have subnodes", ); } + + // skip non explicit composite selection when using selectAll + if (subSelections?._ === "selectAll" && !instanceSelection) { + continue; + } + node.subNodes = _selectionToNodeSet( // assume it's a Selection. If it's an argument // object, mismatch between the node desc should hopefully