Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow enums to be remapped like scalars #184

Merged
merged 14 commits into from
Apr 10, 2024
6 changes: 6 additions & 0 deletions .changeset/fair-otters-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"gql.tada": minor
"@gql.tada/internal": patch
---

Allow GraphQL enum types to be remapped with the `scalars` configuration option.
6 changes: 2 additions & 4 deletions packages/internal/src/introspection/preprocess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const printFields = (fields: readonly IntrospectionField[]) => {
export const printIntrospectionType = (type: IntrospectionType) => {
if (type.kind === 'ENUM') {
const values = printNamedTypes(type.enumValues);
return `{ kind: 'ENUM'; name: ${printName(type.name)}; type: ${values}; }`;
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}; }`;
Expand All @@ -73,10 +73,8 @@ export const printIntrospectionType = (type: IntrospectionType) => {
const name = printName(type.name);
const possibleTypes = printNamedTypes(type.possibleTypes);
return `{ kind: 'UNION'; name: ${name}; fields: {}; possibleTypes: ${possibleTypes}; }`;
} else if (type.kind === 'SCALAR') {
return 'unknown';
} else {
return 'never';
return 'unknown';
}
};

Expand Down
9 changes: 9 additions & 0 deletions src/__tests__/api.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,17 @@ describe('graphql()', () => {
});

describe('graphql() with custom scalars', () => {
enum TestEnum {
value = 'value',
test = 'test',
}

const graphql = initGraphQLTada<{
introspection: simpleIntrospection;
scalars: {
ID: [string];
String: { value: string };
test: TestEnum;
};
}>();

Expand All @@ -162,6 +168,7 @@ describe('graphql() with custom scalars', () => {
fragment Fields on Todo @_unmask {
id
text
test
}
`);

Expand All @@ -179,13 +186,15 @@ describe('graphql() with custom scalars', () => {
expectTypeOf<FragmentOf<typeof fragment>>().toEqualTypeOf<{
id: [string];
text: { value: string };
test: TestEnum | null;
}>();

expectTypeOf<ResultOf<typeof query>>().toEqualTypeOf<{
todos:
| ({
id: [string];
text: { value: string };
test: TestEnum | null;
} | null)[]
| null;
}>();
Expand Down
3 changes: 1 addition & 2 deletions src/__tests__/fixtures/simpleSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,8 @@ export type simpleSchema = {
};

test: {
kind: 'ENUM';
name: 'test';
type: 'value' | 'more';
enumValues: 'value' | 'more';
};

SmallTodo: {
Expand Down
19 changes: 19 additions & 0 deletions src/__tests__/introspection.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,23 @@ describe('mapIntrospection', () => {
type idScalar = expected['types']['ID']['type'];
expectTypeOf<idScalar>().toEqualTypeOf<'ID'>();
});

it('still uses default scalars when applying custom scalars', () => {
type expected = addIntrospectionScalars<mapIntrospection<simpleIntrospection>, { ID: 'ID' }>;
type intScalar = expected['types']['Int']['type'];
expectTypeOf<intScalar>().toEqualTypeOf<number>();
});

it('allows enums to be remapped', () => {
enum TestEnum {
test = 'test',
value = 'value',
}
type expected = addIntrospectionScalars<
mapIntrospection<simpleIntrospection>,
{ test: TestEnum }
>;
type testEnum = expected['types']['test']['type'];
expectTypeOf<testEnum>().toEqualTypeOf<TestEnum>();
});
});
50 changes: 14 additions & 36 deletions src/introspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@ interface IntrospectionSchema {
readonly types: readonly any[];
}

interface IntrospectionScalarType {
readonly kind: 'SCALAR';
readonly name: string;
readonly specifiedByURL?: string | null;
}

export interface IntrospectionObjectType {
readonly kind: 'OBJECT';
readonly name: string;
Expand All @@ -49,33 +43,27 @@ export interface IntrospectionObjectType {
interface IntrospectionInterfaceType {
readonly kind: 'INTERFACE';
readonly name: string;
// Usually this would be `IntrospectionField`.
// However, to save TypeScript some work, instead, we constraint it to `any` here.
readonly fields: readonly any[];
readonly possibleTypes: readonly IntrospectionNamedTypeRef[];
// The `interfaces` field isn't used. It's omitted here
readonly fields: readonly any[] /*readonly IntrospectionField[]*/;
readonly possibleTypes: readonly any[] /*readonly IntrospectionNamedTypeRef[]*/;
// NOTE: The `interfaces` field isn't used. It's omitted here
}

interface IntrospectionUnionType {
readonly kind: 'UNION';
readonly name: string;
readonly possibleTypes: readonly IntrospectionNamedTypeRef[];
}

interface IntrospectionEnumValue {
readonly name: string;
readonly possibleTypes: readonly any[] /*readonly IntrospectionNamedTypeRef[]*/;
}

interface IntrospectionEnumType {
readonly kind: 'ENUM';
readonly name: string;
readonly enumValues: readonly IntrospectionEnumValue[];
readonly enumValues: readonly any[] /*readonly IntrospectionEnumValue[]*/;
}

interface IntrospectionInputObjectType {
readonly kind: 'INPUT_OBJECT';
readonly name: string;
readonly inputFields: readonly IntrospectionInputValue[];
readonly inputFields: readonly any[] /*readonly IntrospectionInputValue[]*/;
}

export interface IntrospectionListTypeRef {
Expand Down Expand Up @@ -103,24 +91,17 @@ export interface IntrospectionField {
// The `args` field isn't used. It's omitted here
}

interface IntrospectionInputValue {
readonly name: string;
readonly type: IntrospectionTypeRef;
readonly defaultValue?: string | null;
}

interface DefaultScalars {
ID: string;
Boolean: boolean;
String: string;
Float: number;
Int: number;
readonly ID: string;
readonly Boolean: boolean;
readonly String: string;
readonly Float: number;
readonly Int: number;
}

type mapEnum<T extends IntrospectionEnumType> = {
kind: 'ENUM';
name: T['name'];
type: T['enumValues'][number]['name'];
enumValues: T['enumValues'][number]['name'];
};

type mapField<T> = T extends IntrospectionField
Expand Down Expand Up @@ -179,9 +160,7 @@ type mapType<Type> = Type extends IntrospectionEnumType
? mapUnion<Type>
: Type extends IntrospectionInputObjectType
? mapInputObject<Type>
: Type extends IntrospectionScalarType
? unknown
: never;
: unknown;

/** @internal */
type mapIntrospectionTypes<Query extends IntrospectionQuery> = obj<{
Expand All @@ -195,7 +174,6 @@ type mapIntrospectionTypes<Query extends IntrospectionQuery> = obj<{
/** @internal */
type mapIntrospectionScalarTypes<Scalars extends ScalarsLike = DefaultScalars> = obj<{
[P in keyof Scalars | keyof DefaultScalars]: {
kind: 'SCALAR';
name: P;
type: P extends keyof Scalars
? Scalars[P]
Expand Down Expand Up @@ -234,7 +212,7 @@ type addIntrospectionScalars<
export type IntrospectionLikeInput = SchemaLike | IntrospectionQuery;

export type ScalarsLike = {
[name: string]: any;
readonly [name: string]: any;
};

export type SchemaLike = {
Expand Down
10 changes: 7 additions & 3 deletions src/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@ type unwrapTypeRec<
Fragments
>
: unknown
: IsOptional extends false
? Introspection['types'][Type['name']]['type']
: null | Introspection['types'][Type['name']]['type']
: Introspection['types'][Type['name']] extends { type: any }
? IsOptional extends false
? Introspection['types'][Type['name']]['type']
: Introspection['types'][Type['name']]['type'] | null
: IsOptional extends false
? Introspection['types'][Type['name']]['enumValues']
: Introspection['types'][Type['name']]['enumValues'] | null
: unknown;

type getTypeDirective<Node> = Node extends { directives: any[] }
Expand Down
37 changes: 15 additions & 22 deletions src/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type getInputObjectTypeRec<
: {}) &
InputObject
>
: InputObject;
: obj<InputObject>;

type unwrapTypeRec<TypeRef, Introspection extends SchemaLike, IsOptional> = TypeRef extends {
kind: 'NON_NULL';
Expand Down Expand Up @@ -83,43 +83,36 @@ type _getVariablesRec<
: {}) &
VariablesObject
>
: VariablesObject;
: obj<VariablesObject>;

type getVariablesType<
Document extends DocumentNodeLike,
Introspection extends SchemaLike,
> = Document['definitions'][0] extends {
kind: Kind.OPERATION_DEFINITION;
variableDefinitions: any;
}
? obj<_getVariablesRec<Document['definitions'][0]['variableDefinitions'], Introspection>>
: {};
> = _getVariablesRec<Document['definitions'][0]['variableDefinitions'], Introspection>;

type _getScalarType<
TypeName,
Introspection extends SchemaLike,
> = TypeName extends keyof Introspection['types']
? Introspection['types'][TypeName] extends { kind: 'SCALAR' | 'ENUM'; type: any }
? Introspection['types'][TypeName]['type']
: Introspection['types'][TypeName] extends { kind: 'INPUT_OBJECT'; inputFields: any }
? obj<getInputObjectTypeRec<Introspection['types'][TypeName]['inputFields'], Introspection>>
: never
? Introspection['types'][TypeName] extends { kind: 'INPUT_OBJECT'; inputFields: any }
? getInputObjectTypeRec<Introspection['types'][TypeName]['inputFields'], Introspection>
: Introspection['types'][TypeName] extends { type: any }
? Introspection['types'][TypeName]['type']
: Introspection['types'][TypeName]['enumValues']
: unknown;

type getScalarType<
TypeName,
Introspection extends SchemaLike,
OrType = never,
> = TypeName extends keyof Introspection['types']
? Introspection['types'][TypeName] extends { kind: 'SCALAR' | 'ENUM'; type: any }
? Introspection['types'][TypeName]['type'] | OrType
: Introspection['types'][TypeName] extends { kind: 'INPUT_OBJECT'; inputFields: any }
?
| obj<
getInputObjectTypeRec<Introspection['types'][TypeName]['inputFields'], Introspection>
>
| OrType
: never
? Introspection['types'][TypeName] extends { kind: 'INPUT_OBJECT'; inputFields: any }
? getInputObjectTypeRec<Introspection['types'][TypeName]['inputFields'], Introspection> | OrType
: Introspection['types'][TypeName] extends { type: any }
? Introspection['types'][TypeName]['type'] | OrType
: Introspection['types'][TypeName] extends { enumValues: any }
? Introspection['types'][TypeName]['enumValues'] | OrType
: never
: never;

export type { getVariablesType, getScalarType };
6 changes: 4 additions & 2 deletions website/reference/gql-tada-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -501,9 +501,11 @@ declare module 'gql.tada' {
This is used either via [`setupSchema`](#setupschema) or [`initGraphQLTada()`](#initgraphqltada) to set
up your schema and scalars. Your configuration objects must match the shape of this type.

The `scalars` option is optional and can be used to set up more scalars, apart
from the default ones (like: Int, Float, String, Boolean).
The `scalars` option is optional and can be used to set up custom scalar and enum types.
It must be an object map of scalar names to their desired TypeScript types.
When a scalar or enum is missing in your custom `scalars` object, a fallback will be
used for the built-in scalars (`Int`, `Float`, `String`, `Boolean`, and `ID`) and for
enums, the `enumValues` defined by the schema will be used.

The `disableMasking` flag may be set to `true` instead of using `@_unmask` on individual fragments
and allows fragment masking to be disabled globally.
Loading