diff --git a/.changeset/mighty-jokes-repair.md b/.changeset/mighty-jokes-repair.md new file mode 100644 index 00000000..a85e64d8 --- /dev/null +++ b/.changeset/mighty-jokes-repair.md @@ -0,0 +1,5 @@ +--- +'gql.tada': minor +--- + +Add `graphql.scalar()` utility to retrieve or type check the type of scalars and enums. diff --git a/src/__tests__/api.test-d.ts b/src/__tests__/api.test-d.ts index c31d014e..19cc0859 100644 --- a/src/__tests__/api.test-d.ts +++ b/src/__tests__/api.test-d.ts @@ -19,7 +19,7 @@ type schema = simpleSchema; type value = { __value: true }; type data = { __data: true }; -describe('Public API', () => { +describe('graphql()', () => { const graphql = initGraphQLTada<{ introspection: simpleIntrospection }>(); it('should create a fragment mask on masked fragments', () => { @@ -101,6 +101,42 @@ describe('Public API', () => { }); }); +describe('graphql.scalar()', () => { + const graphql = initGraphQLTada<{ introspection: simpleIntrospection }>(); + + it('should return the type of a given enum', () => { + type actual = ReturnType>; + type expected = 'value' | 'more'; + expectTypeOf().toEqualTypeOf(); + }); + + it('should return the type of a given enum', () => { + type actual = ReturnType>; + expectTypeOf().toEqualTypeOf(); + }); + + it('should narrow the type of a passed value', () => { + const actual = graphql.scalar('test', 'more'); + expectTypeOf().toEqualTypeOf<'more'>(); + }); + + it('should accept the type or null of a passed value', () => { + const input: null | 'more' = {} as any; + const actual = graphql.scalar('test', input); + expectTypeOf().toEqualTypeOf<'more' | null>(); + }); + + it('should reject invalid values of a passed value', () => { + // @ts-expect-error + const actual = graphql.scalar('test', 'invalid'); + }); + + it('should reject invalid names of types', () => { + // @ts-expect-error + const actual = graphql.scalar('what', null); + }); +}); + describe('mirrorFragmentTypeRec', () => { it('mirrors null and undefined', () => { expectTypeOf>().toEqualTypeOf(); diff --git a/src/__tests__/fixtures/simpleSchema.ts b/src/__tests__/fixtures/simpleSchema.ts index a16c3d12..660c2634 100644 --- a/src/__tests__/fixtures/simpleSchema.ts +++ b/src/__tests__/fixtures/simpleSchema.ts @@ -326,21 +326,25 @@ export type simpleSchema = { ID: { kind: 'SCALAR'; + name: 'ID'; type: string | number; }; String: { kind: 'SCALAR'; + name: 'String'; type: string; }; Boolean: { kind: 'SCALAR'; + name: 'Boolean'; type: boolean; }; Int: { kind: 'SCALAR'; + name: 'Int'; type: number; }; diff --git a/src/__tests__/introspection.test-d.ts b/src/__tests__/introspection.test-d.ts index 883ac348..9ebe22df 100644 --- a/src/__tests__/introspection.test-d.ts +++ b/src/__tests__/introspection.test-d.ts @@ -1,16 +1,35 @@ -import { test, expectTypeOf } from 'vitest'; +import { describe, it, expectTypeOf } from 'vitest'; import type { simpleIntrospection } from './fixtures/simpleIntrospection'; import type { simpleSchema } from './fixtures/simpleSchema'; -import type { mapIntrospection } from '../introspection'; +import type { mapIntrospection, getScalarType, getScalarTypeNames } from '../introspection'; -test('prepares sample schema', () => { - type expected = mapIntrospection; - expectTypeOf().toMatchTypeOf(); +describe('mapIntrospection', () => { + it('prepares sample schema', () => { + type expected = mapIntrospection; + expectTypeOf().toMatchTypeOf(); + }); + + it('applies scalar types as appropriate', () => { + type expected = mapIntrospection; + + type idScalar = expected['types']['ID']['type']; + expectTypeOf().toEqualTypeOf<'ID'>(); + }); }); -test('applies scalar types as appropriate', () => { - type expected = mapIntrospection; +describe('getScalarType', () => { + it('gets the type of a scalar', () => { + expectTypeOf>().toEqualTypeOf(); + }); + + it('gets the type of an enum', () => { + expectTypeOf>().toEqualTypeOf<'value' | 'more'>(); + }); +}); - type idScalar = expected['types']['ID']['type']; - expectTypeOf().toEqualTypeOf<'ID'>(); +describe('getScalarTypeNames', () => { + it('gets the names of all scalars and enums', () => { + type actual = getScalarTypeNames; + expectTypeOf().toEqualTypeOf<'test' | 'ID' | 'String' | 'Boolean' | 'Int'>(); + }); }); diff --git a/src/api.ts b/src/api.ts index a3b685c4..8c03a61e 100644 --- a/src/api.ts +++ b/src/api.ts @@ -6,6 +6,8 @@ import type { ScalarsLike, IntrospectionLikeType, mapIntrospection, + getScalarTypeNames, + getScalarType, } from './introspection'; import type { @@ -120,6 +122,45 @@ interface GraphQLTadaAPI { input: In, fragments?: Fragments ): getDocumentNode, Schema, getFragmentsOfDocumentsRec>; + + /** Function to validate the type of a given scalar or enum value. + * + * @param name - The name of a scalar or enum type. + * @param value - An optional scalar value of the given type. + * @returns A {@link DocumentNode} with result and variables types. + * + * @remarks + * This function validates that a value matches an enum or scalar type + * as a type check. + * + * You can use it to retrieve the type of a given scalar or enum type + * for use in a utility function or separate component that only + * accepts a primitive scalar or enum value. + * + * Note that this function does not perform runtime checks of your + * scalar value! + * + * @example + * ``` + * import { graphql } from 'gql.tada'; + * + * type myEnum = ReturnType>; + * + * const myEnumValue = graphql.scalar('myEnum', 'value'); + * ``` + */ + scalar< + const Name extends getScalarTypeNames, + const Value extends getScalarType | null | undefined, + >( + name: Name, + value: Value + ): Value; + + scalar>( + name: Name, + value: getScalarType + ): getScalarType; } type schemaOfConfig = mapIntrospection< @@ -156,7 +197,7 @@ type schemaOfConfig = mapIntrospection< function initGraphQLTada() { type Schema = schemaOfConfig; - return function graphql(input: string, fragments?: readonly TadaDocumentNode[]): any { + function graphql(input: string, fragments?: readonly TadaDocumentNode[]): any { const definitions = _parse(input).definitions as writable[]; const seen = new Set(); for (const document of fragments || []) { @@ -175,7 +216,13 @@ function initGraphQLTada() { } return { kind: Kind.DOCUMENT, definitions }; - } as GraphQLTadaAPI; + } + + graphql.scalar = function scalar(_schema: Schema, value: any) { + return value; + }; + + return graphql as GraphQLTadaAPI; } /** Alias to a GraphQL parse function returning an exact document type. diff --git a/src/introspection.ts b/src/introspection.ts index 264b6ffe..5730436e 100644 --- a/src/introspection.ts +++ b/src/introspection.ts @@ -122,6 +122,7 @@ type mapScalar< Scalars extends ScalarsLike = DefaultScalars, > = { kind: 'SCALAR'; + name: Type['name']; type: Type['name'] extends keyof Scalars ? Scalars[Type['name']] : Type['name'] extends keyof DefaultScalars @@ -222,6 +223,22 @@ type mapIntrospection< types: mapIntrospectionTypes; }; +type getScalarTypeNames = + Schema['types'][keyof Schema['types']] extends infer Type + ? Type extends { kind: 'SCALAR' | 'ENUM'; name: any } + ? Type['name'] + : never + : never; + +type getScalarType< + Schema extends IntrospectionLikeType, + Name extends keyof Schema['types'], +> = Schema['types'][Name] extends { kind: 'SCALAR'; type: infer Type } + ? Type + : Schema['types'][Name] extends { kind: 'ENUM'; type: infer Type } + ? Type + : never; + export type ScalarsLike = { [name: string]: any; }; @@ -233,4 +250,4 @@ export type IntrospectionLikeType = { types: { [name: string]: any }; }; -export type { mapIntrospectionTypes, mapIntrospection }; +export type { mapIntrospectionTypes, mapIntrospection, getScalarType, getScalarTypeNames }; diff --git a/website/src/content/docs/reference/gql-tada-api.mdx b/website/src/content/docs/reference/gql-tada-api.mdx index beb603a0..6d9c9d25 100644 --- a/website/src/content/docs/reference/gql-tada-api.mdx +++ b/website/src/content/docs/reference/gql-tada-api.mdx @@ -45,6 +45,42 @@ const bookQuery = graphql(` `, [bookFragment]); ``` +### `graphql.scalar()` + +| | Description | +| ----------- | ----------- | +| `name` argument | A name of a GraphQL scalar or enum. | +| `value` argument | The value to be type-checked against the type. | +| returns | The `value` will be returned directly. | + +Type checks a given input value to be of a scalar or enum type and +returns the value directly. + +You can use this utility to add a type check for a scalar or enum value, +or to retrieve the type of a scalar or enum. +This is useful if you’re writing a function or component that only accepts +a scalar or enum, but not a full fragment. + +:::note +It’s not recommended to use this utiliy to replace fragments, i.e. to +create your own object types. Try to use fragments where appropriate +instead. +::: + +#### Example + +```ts {"Call graphql.scalar to type check a value against a scalar type:":4-5} {"Use ReturnType to get the type of a scalar directly:":8-9} +import { graphql } from 'gql.tada'; + +function validateMediaEnum(value: 'Book' | 'Song' | 'Video') { + + const media = graphql.scalar('Media', value); +} + + +type Media = ReturnType>; +``` + ### `readFragment()` | | Description |