Skip to content

Commit

Permalink
feat(tada): add isOneOf to the introspection type and support query (#…
Browse files Browse the repository at this point in the history
…284)

Co-authored-by: Phil Pluckthun <phil@kitten.sh>
  • Loading branch information
JoviDeCroock and kitten authored May 3, 2024
1 parent 3b34406 commit 465bf2a
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-beers-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@gql.tada/internal": minor
---

Add support for querying `isOneOf` in the introspection
5 changes: 5 additions & 0 deletions .changeset/thirty-moose-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gql.tada": minor
---

Add support for `oneOf` input objects as specified in [the RFC](https://github.com/graphql/graphql-spec/pull/825)
7 changes: 4 additions & 3 deletions packages/internal/src/introspection/minify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function mapTypeRef(fromType: IntrospectionTypeRef): IntrospectionTypeRef;
function mapTypeRef(fromType: IntrospectionOutputTypeRef): IntrospectionOutputTypeRef;
function mapTypeRef(fromType: IntrospectionInputTypeRef): IntrospectionInputTypeRef;

function mapTypeRef(fromType: IntrospectionTypeRef): IntrospectionTypeRef {
function mapTypeRef(fromType: IntrospectionTypeRef): IntrospectionTypeRef & { isOneOf?: boolean } {
switch (fromType.kind) {
case 'NON_NULL':
return {
Expand All @@ -33,8 +33,8 @@ function mapTypeRef(fromType: IntrospectionTypeRef): IntrospectionTypeRef {
kind: fromType.kind,
ofType: mapTypeRef(fromType.ofType),
};
case 'ENUM':
case 'INPUT_OBJECT':
case 'ENUM':
case 'SCALAR':
case 'OBJECT':
case 'INTERFACE':
Expand Down Expand Up @@ -99,7 +99,8 @@ function minifyIntrospectionType(type: IntrospectionType): IntrospectionType {
kind: 'INPUT_OBJECT',
name: type.name,
inputFields: type.inputFields.map(mapInputField),
};
isOneOf: (type as any).isOneOf || false,
} as IntrospectionType;
}

case 'OBJECT':
Expand Down
4 changes: 3 additions & 1 deletion packages/internal/src/introspection/preprocess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ export const printIntrospectionType = (type: IntrospectionType) => {
return `{ name: ${printName(type.name)}; enumValues: ${values}; }`;
} else if (type.kind === 'INPUT_OBJECT') {
const fields = printInputFields(type.inputFields);
return `{ kind: 'INPUT_OBJECT'; name: ${printName(type.name)}; inputFields: ${fields}; }`;
return `{ kind: 'INPUT_OBJECT'; name: ${printName(type.name)}; isOneOf: ${
(type as any).isOneOf || false
}; inputFields: ${fields}; }`;
} else if (type.kind === 'OBJECT') {
const fields = printFields(type.fields);
return `{ kind: 'OBJECT'; name: ${printName(type.name)}; fields: ${fields}; }`;
Expand Down
5 changes: 5 additions & 0 deletions packages/internal/src/loaders/__tests__/introspection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('getPeerSupportedFeatures', () => {
"directiveArgumentsIsDeprecated": true,
"directiveIsRepeatable": true,
"fieldArgumentsIsDeprecated": true,
"inputOneOf": false,
"inputValueDeprecation": true,
"specifiedByURL": true,
}
Expand Down Expand Up @@ -68,6 +69,7 @@ describe('toSupportedFeatures', () => {
inputValueDeprecation: false,
directiveArgumentsIsDeprecated: false,
fieldArgumentsIsDeprecated: false,
inputOneOf: false,
});
});

Expand Down Expand Up @@ -144,6 +146,7 @@ describe('makeIntrospectionQuery', () => {
inputValueDeprecation: true,
directiveArgumentsIsDeprecated: true,
fieldArgumentsIsDeprecated: true,
inputOneOf: true,
};

const output = print(makeIntrospectionQuery(support));
Expand Down Expand Up @@ -183,6 +186,7 @@ describe('makeIntrospectionQuery', () => {
kind
name
description
isOneOf
specifiedByURL
fields(includeDeprecated: true) {
name
Expand Down Expand Up @@ -274,6 +278,7 @@ describe('makeIntrospectionQuery', () => {
inputValueDeprecation: false,
directiveArgumentsIsDeprecated: false,
fieldArgumentsIsDeprecated: false,
inputOneOf: false,
};

const output = print(makeIntrospectionQuery(support));
Expand Down
7 changes: 7 additions & 0 deletions packages/internal/src/loaders/introspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface SupportedFeatures {
inputValueDeprecation: boolean;
directiveArgumentsIsDeprecated: boolean;
fieldArgumentsIsDeprecated: boolean;
inputOneOf: boolean;
}

/** Data from a {@link makeIntrospectSupportQuery} result */
Expand Down Expand Up @@ -48,6 +49,7 @@ export const ALL_SUPPORTED_FEATURES: SupportedFeatures = {
inputValueDeprecation: true,
directiveArgumentsIsDeprecated: true,
fieldArgumentsIsDeprecated: true,
inputOneOf: true,
};

export const NO_SUPPORTED_FEATURES: SupportedFeatures = {
Expand All @@ -56,12 +58,14 @@ export const NO_SUPPORTED_FEATURES: SupportedFeatures = {
inputValueDeprecation: false,
directiveArgumentsIsDeprecated: false,
fieldArgumentsIsDeprecated: false,
inputOneOf: false,
};

/** Evaluates data from a {@link makeIntrospectSupportQuery} result to {@link SupportedFeatures} */
export const toSupportedFeatures = (data: IntrospectSupportQueryData): SupportedFeatures => ({
directiveIsRepeatable: _hasField(data.directive, 'isRepeatable'),
specifiedByURL: _hasField(data.type, 'specifiedByURL'),
inputOneOf: _hasField(data.type, 'isOneOf'),
inputValueDeprecation: _hasField(data.inputValue, 'isDeprecated'),
directiveArgumentsIsDeprecated: _supportsDeprecatedArgumentsArg(data.directive),
fieldArgumentsIsDeprecated: _supportsDeprecatedArgumentsArg(data.field),
Expand Down Expand Up @@ -339,6 +343,9 @@ const _makeSchemaFullTypeFragment = (support: SupportedFeatures): FragmentDefini
kind: Kind.FIELD,
name: { kind: Kind.NAME, value: 'description' },
},
...(support.inputOneOf
? ([{ kind: Kind.FIELD, name: { kind: Kind.NAME, value: 'isOneOf' } }] as const)
: []),
...(support.specifiedByURL
? ([
{
Expand Down
2 changes: 2 additions & 0 deletions packages/internal/src/loaders/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface SupportedFeatures {
inputValueDeprecation: boolean;
directiveArgumentsIsDeprecated: boolean;
fieldArgumentsIsDeprecated: boolean;
inputOneOf: boolean;
}

/** Data from a {@link makeIntrospectSupportQuery} result */
Expand Down Expand Up @@ -48,6 +49,7 @@ export const toSupportedFeatures = (data: IntrospectSupportQueryData): Supported
inputValueDeprecation: _hasField(data.inputValue, 'isDeprecated'),
directiveArgumentsIsDeprecated: _supportsDeprecatedArgumentsArg(data.directive),
fieldArgumentsIsDeprecated: _supportsDeprecatedArgumentsArg(data.field),
inputOneOf: _hasField(data.type, 'isOneOf'),
});

let _introspectionQuery: DocumentNode | undefined;
Expand Down
32 changes: 32 additions & 0 deletions src/__tests__/fixtures/simpleIntrospection.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,38 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "OneOfPayload",
"fields": null,
"isOneOf": true,
"inputFields": [
{
"name": "value_1",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "value_2",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DefaultPayload",
Expand Down
31 changes: 31 additions & 0 deletions src/__tests__/fixtures/simpleSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export type simpleSchema =
DefaultPayload: {
kind: 'INPUT_OBJECT';
name: 'DefaultPayload';
isOneOf: false;
inputFields: [
{
name: 'value';
Expand Down Expand Up @@ -216,6 +217,35 @@ export type simpleSchema =
};
};
};
OneOfPayload: {
kind: 'INPUT_OBJECT';
name: 'OneOfPayload';
isOneOf: true;
inputFields: [
{
name: 'value_1';
type: {
kind: 'NON_NULL';
name: never;
ofType: {
kind: 'SCALAR';
name: 'String';
ofType: null;
};
};
defaultValue: null;
},
{
name: 'value_2';
type: {
kind: 'SCALAR';
name: 'String';
ofType: null;
};
defaultValue: null;
},
];
};
Query: {
kind: 'OBJECT';
name: 'Query';
Expand Down Expand Up @@ -400,6 +430,7 @@ export type simpleSchema =
TodoPayload: {
kind: 'INPUT_OBJECT';
name: 'TodoPayload';
isOneOf: false;
inputFields: [
{
name: 'title';
Expand Down
16 changes: 16 additions & 0 deletions src/__tests__/variables.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ describe('getVariablesType', () => {
}>();
});

it('works for one-of-objects', () => {
const query = `
mutation ($id: ID!, $input: OneOfPayload!) {
toggleTodo (id: $id input: $input) { id }
}
`;

type doc = parseDocument<typeof query>;
type variables = getVariablesType<doc, schema>;

expectTypeOf<variables>().toEqualTypeOf<{
id: string;
input: { value_1: string } | { value_2: string };
}>();
});

it('allows optionals for default values', () => {
const query = `
mutation ($id: ID! = "default") {
Expand Down
2 changes: 2 additions & 0 deletions src/introspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ interface IntrospectionEnumType {
interface IntrospectionInputObjectType {
readonly kind: 'INPUT_OBJECT';
readonly name: string;
readonly isOneOf?: boolean;
readonly inputFields: readonly any[] /*readonly IntrospectionInputValue[]*/;
}

Expand Down Expand Up @@ -128,6 +129,7 @@ export type mapObject<T extends IntrospectionObjectType> = {
export type mapInputObject<T extends IntrospectionInputObjectType> = {
kind: 'INPUT_OBJECT';
name: T['name'];
isOneOf: T['isOneOf'] extends boolean ? T['isOneOf'] : false;
inputFields: [...T['inputFields']];
};

Expand Down
39 changes: 35 additions & 4 deletions src/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ type getInputObjectTypeRec<
? getInputObjectTypeRec<
Rest,
Introspection,
(InputField extends { name: any; type: any }
(InputField extends {
name: any;
type: any;
}
? InputField extends { defaultValue?: undefined | null; type: { kind: 'NON_NULL' } }
? { [Name in InputField['name']]: unwrapTypeRec<InputField['type'], Introspection, true> }
? {
[Name in InputField['name']]: unwrapTypeRec<InputField['type'], Introspection, true>;
}
: {
[Name in InputField['name']]?: unwrapTypeRec<
InputField['type'],
Expand All @@ -26,6 +31,26 @@ type getInputObjectTypeRec<
>
: obj<InputObject>;

type getInputObjectTypeOneOfRec<
InputFields,
Introspection extends SchemaLike,
InputObject = never,
> = InputFields extends [infer InputField, ...infer Rest]
? getInputObjectTypeOneOfRec<
Rest,
Introspection,
| (InputField extends {
name: any;
type: any;
}
? {
[Name in InputField['name']]: unwrapTypeRec<InputField['type'], Introspection, false>;
}
: never)
| InputObject
>
: InputObject;

type unwrapTypeRec<TypeRef, Introspection extends SchemaLike, IsOptional> = TypeRef extends {
kind: 'NON_NULL';
ofType: any;
Expand Down Expand Up @@ -94,8 +119,14 @@ type getScalarType<
TypeName,
Introspection extends SchemaLike,
> = TypeName extends keyof Introspection['types']
? Introspection['types'][TypeName] extends { kind: 'INPUT_OBJECT'; inputFields: any }
? getInputObjectTypeRec<Introspection['types'][TypeName]['inputFields'], Introspection>
? Introspection['types'][TypeName] extends {
kind: 'INPUT_OBJECT';
inputFields: any;
isOneOf?: any;
}
? Introspection['types'][TypeName]['isOneOf'] extends true
? getInputObjectTypeOneOfRec<Introspection['types'][TypeName]['inputFields'], Introspection>
: getInputObjectTypeRec<Introspection['types'][TypeName]['inputFields'], Introspection>
: Introspection['types'][TypeName] extends { type: any }
? Introspection['types'][TypeName]['type']
: Introspection['types'][TypeName]['enumValues']
Expand Down

0 comments on commit 465bf2a

Please sign in to comment.