Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion packages/core/spec/Select-type.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AssertTrue, IsExact } from "conditional-type-checks";
import type { DeepFilterNever, Select } from "../src/types.js";
import type { TestSchema } from "./TestSchema.js";
import { $args } from "../src/types.js";
import type { TestSchema, TestSchemaWithFieldCalls, TestSchemaWithNestedFieldCalls } from "./TestSchema.js";

describe("Select<>", () => {
type _SelectingProperties = AssertTrue<IsExact<Select<TestSchema, { num: true }>, { num: number }>>;
Expand Down Expand Up @@ -75,5 +76,40 @@ describe("Select<>", () => {
>
>;

type _fieldCallSelection = Select<
TestSchemaWithFieldCalls,
{ fieldCall: { [$args]: { arg: [1, 2, 3]; limit: 10 }; nestedField1: true; nestedField2: { nestedField3: true } } }
>;
type _TestSelectingFieldCall = AssertTrue<
IsExact<_fieldCallSelection, { fieldCall: { nestedField1: number; nestedField2: { nestedField3: string } } }>
>;

type _simpleFieldCallSelection = Select<
{ fieldWithArgs: number },
{
fieldWithArgs: {
[$args]: { arg: [1, 2, 3]; limit: 10 };
};
}
>;
type _TestSelectingSimpleFieldCall = AssertTrue<IsExact<_simpleFieldCallSelection, { fieldWithArgs: number }>>;

type _nestedFieldCallSelection = Select<
TestSchemaWithNestedFieldCalls,
{
outerFieldCall: {
[$args]: { outerArg: "value" };
innerFieldCall: {
[$args]: { innerArg: 123 };
deepField: { [$args]: { deepArg: "value" } };
};
regularField: true;
};
}
>;
type _TestSelectingNestedFieldCall = AssertTrue<
IsExact<_nestedFieldCallSelection, { outerFieldCall: { innerFieldCall: { deepField: string }; regularField: number } }>
>;

test("true", () => undefined);
});
20 changes: 20 additions & 0 deletions packages/core/spec/TestSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ export type NestedThing = {
nested: NestedThing;
};

export type TestSchemaWithFieldCalls = {
num: number;
str: string;
fieldCall: {
nestedField1: number;
nestedField2: {
nestedField3: string;
};
};
};

export type TestSchemaWithNestedFieldCalls = {
outerFieldCall: {
innerFieldCall: {
deepField: string;
};
regularField: number;
};
};

export type TestSchema = {
num: number;
str: string;
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/AnyClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ export interface AnyClient {
internal: InternalModelManagerNamespace;
[$modelRelationships]?: { [modelName: string]: { [apiIdentifier: string]: { type: string; model: string } } };
[$coreImplementation]?: AnyCoreImplementation;
/** Symbol for passing field arguments in selections. Use as `{ field: { [api.$args]: { first: 10 }, nestedField: true } }` */
$args?: symbol;
}
26 changes: 21 additions & 5 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export type FilterNever<T extends Record<string, unknown>> = NonNeverKeys<T> ext
* >; // { apple: "red" }
* ```
*/
type InnerSelect<Schema, Selection extends FieldSelection | null | undefined> = IfAny<
type InnerSelect<Schema, Selection extends FieldSelection | FieldSelectionWithArgs | null | undefined> = IfAny<
Selection,
never,
Selection extends null | undefined
Expand All @@ -115,10 +115,12 @@ type InnerSelect<Schema, Selection extends FieldSelection | null | undefined> =
: Schema extends null
? InnerSelect<Exclude<Schema, null>, Selection> | null
: {
[Key in keyof Selection & keyof Schema]: Selection[Key] extends true
[Key in Exclude<keyof Selection, typeof $args> & keyof Schema]: Selection[Key] extends true
? Schema[Key]
: Selection[Key] extends FieldSelection
? InnerSelect<Schema[Key], Selection[Key]>
: Selection[Key] extends FieldSelection | FieldSelectionWithArgs
? Exclude<keyof Selection[Key], typeof $args> extends never
? Schema[Key]
: InnerSelect<Schema[Key], Selection[Key]>
: never;
}
>;
Expand Down Expand Up @@ -148,7 +150,9 @@ export type DeepFilterNever<T> = T extends Record<string, unknown>
* >; // { apple: "red" }
* ```
*/
export type Select<Schema, Selection extends FieldSelection | null | undefined> = DeepFilterNever<InnerSelect<Schema, Selection>>;
export type Select<Schema, Selection extends FieldSelection | FieldSelectionWithArgs | null | undefined> = DeepFilterNever<
InnerSelect<Schema, Selection>
>;

/** Represents an amount of some currency. Specified as a string so user's aren't tempted to do math on the value. */
export type CurrencyAmount = string;
Expand Down Expand Up @@ -989,10 +993,22 @@ export type ViewResult<F extends ViewFunction<any, any>> = Awaited<
F extends ViewFunctionWithVariables<any, infer Result> ? Result : F extends ViewFunctionWithoutVariables<infer Result> ? Result : never
>;

/** Symbol key for field arguments in selections */
export const $args: unique symbol = Symbol.for("gadget/fieldArgs");

/** Field arguments (e.g., pagination, filtering) */
export type FieldArgs = Record<string, any>;

/**
* Represents a list of fields selected from a GraphQL API call. Allows nesting, conditional selection.
* Example: `{ id: true, name: false, richText: { markdown: true, html: false } }`
**/
export interface FieldSelection {
[key: string]: boolean | null | undefined | FieldSelection;
}

/** A field selection that includes field arguments via the $args symbol key */
export type FieldSelectionWithArgs = {
[$args]: FieldArgs;
[key: string]: boolean | null | undefined | FieldSelection | FieldSelectionWithArgs;
};