Skip to content

Commit c2cd668

Browse files
committed
Support field calls in selection API
1 parent cfe5fb1 commit c2cd668

File tree

6 files changed

+98
-6
lines changed

6 files changed

+98
-6
lines changed

packages/core/spec/Select-type.spec.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { AssertTrue, IsExact } from "conditional-type-checks";
22
import type { DeepFilterNever, Select } from "../src/types.js";
3-
import type { TestSchema } from "./TestSchema.js";
3+
import { $args } from "../src/types.js";
4+
import type { TestSchema, TestSchemaWithFieldCalls, TestSchemaWithNestedFieldCalls } from "./TestSchema.js";
45

56
describe("Select<>", () => {
67
type _SelectingProperties = AssertTrue<IsExact<Select<TestSchema, { num: true }>, { num: number }>>;
@@ -75,5 +76,40 @@ describe("Select<>", () => {
7576
>
7677
>;
7778

79+
type _fieldCallSelection = Select<
80+
TestSchemaWithFieldCalls,
81+
{ fieldCall: { [$args]: { arg: [1, 2, 3]; limit: 10 }; nestedField1: true; nestedField2: { nestedField3: true } } }
82+
>;
83+
type _TestSelectingFieldCall = AssertTrue<
84+
IsExact<_fieldCallSelection, { fieldCall: { nestedField1: number; nestedField2: { nestedField3: string } } }>
85+
>;
86+
87+
type _simpleFieldCallSelection = Select<
88+
{ fieldWithArgs: number },
89+
{
90+
fieldWithArgs: {
91+
[$args]: { arg: [1, 2, 3]; limit: 10 };
92+
};
93+
}
94+
>;
95+
type _TestSelectingSimpleFieldCall = AssertTrue<IsExact<_simpleFieldCallSelection, { fieldWithArgs: number }>>;
96+
97+
type _nestedFieldCallSelection = Select<
98+
TestSchemaWithNestedFieldCalls,
99+
{
100+
outerFieldCall: {
101+
[$args]: { outerArg: "value" };
102+
innerFieldCall: {
103+
[$args]: { innerArg: 123 };
104+
deepField: { [$args]: { deepArg: "value" } };
105+
};
106+
regularField: true;
107+
};
108+
}
109+
>;
110+
type _TestSelectingNestedFieldCall = AssertTrue<
111+
IsExact<_nestedFieldCallSelection, { outerFieldCall: { innerFieldCall: { deepField: string }; regularField: number } }>
112+
>;
113+
78114
test("true", () => undefined);
79115
});

packages/core/spec/TestSchema.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@ export type NestedThing = {
77
nested: NestedThing;
88
};
99

10+
export type TestSchemaWithFieldCalls = {
11+
num: number;
12+
str: string;
13+
fieldCall: {
14+
nestedField1: number;
15+
nestedField2: {
16+
nestedField3: string;
17+
};
18+
};
19+
};
20+
21+
export type TestSchemaWithNestedFieldCalls = {
22+
outerFieldCall: {
23+
innerFieldCall: {
24+
deepField: string;
25+
};
26+
regularField: number;
27+
};
28+
};
29+
1030
export type TestSchema = {
1131
num: number;
1232
str: string;

packages/core/src/AnyClient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { AnyConnection } from "./AnyConnection.js";
22
import type { AnyCoreImplementation } from "./AnyCoreImplementation.js";
33
import type { AnyGadgetTransaction } from "./AnyGadgetTransaction.js";
44
import type { AnyInternalModelManager } from "./AnyInternalModelManager.js";
5+
import type { $args } from "./types.js";
56

67
export const $modelRelationships = Symbol.for("gadget/modelRelationships");
78
export const $coreImplementation = Symbol.for("gadget/coreImplementation");
@@ -22,4 +23,5 @@ export interface AnyClient {
2223
internal: InternalModelManagerNamespace;
2324
[$modelRelationships]?: { [modelName: string]: { [apiIdentifier: string]: { type: string; model: string } } };
2425
[$coreImplementation]?: AnyCoreImplementation;
26+
$args: typeof $args;
2527
}

packages/core/src/types.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,14 @@ type InnerSelect<Schema, Selection extends FieldSelection | null | undefined> =
115115
: Schema extends null
116116
? InnerSelect<Exclude<Schema, null>, Selection> | null
117117
: {
118-
[Key in keyof Selection & keyof Schema]: Selection[Key] extends true
118+
// Exclude the $args symbol key - it's for field arguments, not nested selections
119+
[Key in Exclude<keyof Selection, typeof $args> & keyof Schema]: Selection[Key] extends true
119120
? Schema[Key]
120121
: Selection[Key] extends FieldSelection
121-
? InnerSelect<Schema[Key], Selection[Key]>
122+
? // If Selection[Key] only has $args (no nested field selections), return Schema[Key] directly
123+
Exclude<keyof Selection[Key], typeof $args> extends never
124+
? Schema[Key]
125+
: InnerSelect<Schema[Key], Selection[Key]>
122126
: never;
123127
}
124128
>;
@@ -989,10 +993,32 @@ export type ViewResult<F extends ViewFunction<any, any>> = Awaited<
989993
F extends ViewFunctionWithVariables<any, infer Result> ? Result : F extends ViewFunctionWithoutVariables<infer Result> ? Result : never
990994
>;
991995

996+
/**
997+
* Symbol key for field arguments in selections.
998+
* Using a symbol avoids conflicts with string index signatures.
999+
*/
1000+
export const $args = Symbol.for("gadget/fieldArgs");
1001+
1002+
/**
1003+
* Type for field arguments (e.g., pagination, filtering on a field)
1004+
*/
1005+
export type FieldArgs = Record<string, any>;
1006+
9921007
/**
9931008
* Represents a list of fields selected from a GraphQL API call. Allows nesting, conditional selection.
9941009
* Example: `{ id: true, name: false, richText: { markdown: true, html: false } }`
1010+
*
1011+
* Supports field arguments using the $args symbol:
1012+
* ```
1013+
* {
1014+
* comments: {
1015+
* [$args]: { after: "cursor", first: 10 },
1016+
* body: true
1017+
* }
1018+
* }
1019+
* ```
9951020
**/
9961021
export interface FieldSelection {
1022+
[$args]?: FieldArgs;
9971023
[key: string]: boolean | null | undefined | FieldSelection;
9981024
}

packages/react/src/auto/AutoTableValidators.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type ModelMetadata } from "src/metadata.js";
1+
import { type ModelMetadata } from "../metadata.js";
22
import type { useTable } from "../useTable.js";
33

44
export const InvalidModelErrorMessage = `"model" is not a valid Gadget model`;

packages/test-bundles/tsconfig.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@
33
"compilerOptions": {
44
"target": "es2020",
55
"baseUrl": "./",
6-
"moduleResolution": "node",
6+
"moduleResolution": "bundler",
77
"module": "ES2022",
88
"lib": ["es2020", "DOM"],
99
"jsx": "react-jsx",
1010
"noEmit": true,
1111
"skipLibCheck": true,
12-
"declaration": false
12+
"declaration": false,
13+
"paths": {
14+
"@gadgetinc/react": ["../react/src/index.ts"],
15+
"@gadgetinc/react-shopify-app-bridge": ["../react-shopify-app-bridge/src/index.ts"],
16+
"@gadgetinc/client-hooks": ["../client-hooks/src/index.ts"],
17+
"@gadgetinc/core": ["../core/src/index.ts"],
18+
"@gadgetinc/api-client-core": ["../api-client-core/src/index.ts"],
19+
"tiny-graphql-query-compiler": ["../tiny-graphql-query-compiler/src/index.ts"]
20+
}
1321
},
1422
"include": ["./build.ts", "./bundles"]
1523
}

0 commit comments

Comments
 (0)