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: Add graphql.scalar() utility to check/retrieve scalar/enum types #45

Merged
merged 6 commits into from
Jan 28, 2024
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
5 changes: 5 additions & 0 deletions .changeset/mighty-jokes-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'gql.tada': minor
---

Add `graphql.scalar()` utility to retrieve or type check the type of scalars and enums.
38 changes: 37 additions & 1 deletion src/__tests__/api.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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<typeof graphql.scalar<'test'>>;
type expected = 'value' | 'more';
expectTypeOf<actual>().toEqualTypeOf<expected>();
});

it('should return the type of a given enum', () => {
type actual = ReturnType<typeof graphql.scalar<'String'>>;
expectTypeOf<actual>().toEqualTypeOf<string>();
});

it('should narrow the type of a passed value', () => {
const actual = graphql.scalar('test', 'more');
expectTypeOf<typeof actual>().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<typeof actual>().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<mirrorFragmentTypeRec<value, data>>().toEqualTypeOf<data>();
Expand Down
4 changes: 4 additions & 0 deletions src/__tests__/fixtures/simpleSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
37 changes: 28 additions & 9 deletions src/__tests__/introspection.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<simpleIntrospection>;
expectTypeOf<expected>().toMatchTypeOf<simpleSchema>();
describe('mapIntrospection', () => {
it('prepares sample schema', () => {
type expected = mapIntrospection<simpleIntrospection>;
expectTypeOf<expected>().toMatchTypeOf<simpleSchema>();
});

it('applies scalar types as appropriate', () => {
type expected = mapIntrospection<simpleIntrospection, { ID: 'ID' }>;

type idScalar = expected['types']['ID']['type'];
expectTypeOf<idScalar>().toEqualTypeOf<'ID'>();
});
});

test('applies scalar types as appropriate', () => {
type expected = mapIntrospection<simpleIntrospection, { ID: 'ID' }>;
describe('getScalarType', () => {
it('gets the type of a scalar', () => {
expectTypeOf<getScalarType<simpleSchema, 'String'>>().toEqualTypeOf<string>();
});

it('gets the type of an enum', () => {
expectTypeOf<getScalarType<simpleSchema, 'test'>>().toEqualTypeOf<'value' | 'more'>();
});
});

type idScalar = expected['types']['ID']['type'];
expectTypeOf<idScalar>().toEqualTypeOf<'ID'>();
describe('getScalarTypeNames', () => {
it('gets the names of all scalars and enums', () => {
type actual = getScalarTypeNames<simpleSchema>;
expectTypeOf<actual>().toEqualTypeOf<'test' | 'ID' | 'String' | 'Boolean' | 'Int'>();
});
});
51 changes: 49 additions & 2 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
ScalarsLike,
IntrospectionLikeType,
mapIntrospection,
getScalarTypeNames,
getScalarType,
} from './introspection';

import type {
Expand Down Expand Up @@ -120,6 +122,45 @@ interface GraphQLTadaAPI<Schema extends IntrospectionLikeType> {
input: In,
fragments?: Fragments
): getDocumentNode<parseDocument<In>, Schema, getFragmentsOfDocumentsRec<Fragments>>;

/** 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<graphql.scalar<'myEnum'>>;
*
* const myEnumValue = graphql.scalar('myEnum', 'value');
* ```
*/
scalar<
const Name extends getScalarTypeNames<Schema>,
const Value extends getScalarType<Schema, Name> | null | undefined,
>(
name: Name,
value: Value
): Value;

scalar<const Name extends getScalarTypeNames<Schema>>(
name: Name,
value: getScalarType<Schema, Name>
): getScalarType<Schema, Name>;
}

type schemaOfConfig<Setup extends AbstractSetupSchema> = mapIntrospection<
Expand Down Expand Up @@ -156,7 +197,7 @@ type schemaOfConfig<Setup extends AbstractSetupSchema> = mapIntrospection<
function initGraphQLTada<const Setup extends AbstractSetupSchema>() {
type Schema = schemaOfConfig<Setup>;

return function graphql(input: string, fragments?: readonly TadaDocumentNode[]): any {
function graphql(input: string, fragments?: readonly TadaDocumentNode[]): any {
const definitions = _parse(input).definitions as writable<DefinitionNode>[];
const seen = new Set<unknown>();
for (const document of fragments || []) {
Expand All @@ -175,7 +216,13 @@ function initGraphQLTada<const Setup extends AbstractSetupSchema>() {
}

return { kind: Kind.DOCUMENT, definitions };
} as GraphQLTadaAPI<Schema>;
}

graphql.scalar = function scalar(_schema: Schema, value: any) {
return value;
};

return graphql as GraphQLTadaAPI<Schema>;
}

/** Alias to a GraphQL parse function returning an exact document type.
Expand Down
19 changes: 18 additions & 1 deletion src/introspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -222,6 +223,22 @@ type mapIntrospection<
types: mapIntrospectionTypes<Query, Scalars>;
};

type getScalarTypeNames<Schema extends IntrospectionLikeType> =
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;
};
Expand All @@ -233,4 +250,4 @@ export type IntrospectionLikeType = {
types: { [name: string]: any };
};

export type { mapIntrospectionTypes, mapIntrospection };
export type { mapIntrospectionTypes, mapIntrospection, getScalarType, getScalarTypeNames };
36 changes: 36 additions & 0 deletions website/src/content/docs/reference/gql-tada-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof graphql.scalar<'Media'>>;
```

### `readFragment()`

| | Description |
Expand Down
Loading