Skip to content

Commit

Permalink
Add GraphQL client type utilities to hydrogen-codegen (#1584)
Browse files Browse the repository at this point in the history
* Extract type codegen utilities from Storefront Client

* Move codegen type utilities to hydrogen-codegen package

* Changesets

* Rename type

* Make rest parameter optional only if other properties are not required

* Fix ClientReturn

* Fix issues with optional variables

* Use GenericVariables

* Fix variables

* Add tests for client types

* Run type tests properly in Vitest v1

* Simplify types and add comments

* Simplify types in storefront client
  • Loading branch information
frandiox committed Dec 20, 2023
1 parent 306d302 commit 9ad7c5e
Show file tree
Hide file tree
Showing 13 changed files with 570 additions and 87 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-pens-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/hydrogen-codegen': minor
---

Export type utilities to create GraphQL clients that can consume the types generated by this package.
74 changes: 58 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"fast-glob": "^3.2.12",
"flame-chart-js": "2.3.1",
"get-port": "^7.0.0",
"type-fest": "^3.6.0",
"type-fest": "^4.5.0",
"vitest": "^1.0.4"
},
"dependencies": {
Expand Down
7 changes: 4 additions & 3 deletions packages/hydrogen-codegen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"build": "tsup --clean",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit",
"test": "cross-env SHOPIFY_UNIT_TEST=1 vitest run",
"test:watch": "cross-env SHOPIFY_UNIT_TEST=1 vitest"
"test": "cross-env SHOPIFY_UNIT_TEST=1 vitest run --typecheck",
"test:watch": "cross-env SHOPIFY_UNIT_TEST=1 vitest --typecheck"
},
"exports": {
".": {
Expand Down Expand Up @@ -51,7 +51,8 @@
"dependencies": {
"@graphql-codegen/add": "^5.0.0",
"@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-operations": "^4.0.1"
"@graphql-codegen/typescript-operations": "^4.0.1",
"type-fest": "^4.5.0"
},
"peerDependencies": {
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
Expand Down
128 changes: 128 additions & 0 deletions packages/hydrogen-codegen/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* This file has utilities to create GraphQL clients
* that consume the types generated by the Hydrogen preset.
*/

import type {ExecutionArgs} from 'graphql';
import type {SetOptional, IsNever} from 'type-fest';

/**
* A generic type for `variables` in GraphQL clients
*/
export type GenericVariables = ExecutionArgs['variableValues'];

/**
* Use this type to make parameters optional in GraphQL clients
* when no variables need to be passed.
*/
export type EmptyVariables = {[key: string]: never};

/**
* GraphQL client's generic operation interface.
*/
interface CodegenOperations {
// Real example:
// '#graphql\n query TestQuery { test }': {return: R; variables: V};
// --
// However, since the interface passed as CodegenOperations might sitll be empty
// (if the user hasn't generated the code yet), we use `any` here.
[key: string]: any;
}

/**
* Used as the return type for GraphQL clients. It picks
* the return type from the generated operation types.
* @example
* graphqlQuery: (...) => Promise<ClientReturn<...>>
* graphqlQuery: (...) => Promise<{data: ClientReturn<...>}>
*/
export type ClientReturn<
GeneratedOperations extends CodegenOperations,
RawGqlString extends string,
OverrideReturnType extends any = never,
> = IsNever<OverrideReturnType> extends true
? // Nothing passed to override the return type
RawGqlString extends keyof GeneratedOperations
? // Known query, use generated return type
GeneratedOperations[RawGqlString]['return']
: // Unknown query, return 'any' to avoid red squiggly underlines in editor
any
: // Override return type is passed, use it
OverrideReturnType;

/**
* Checks if the generated variables for an operation
* are optional or required.
*/
export type IsOptionalVariables<
VariablesParam,
OptionalVariableNames extends string = never,
// The following are just extracted repeated types, not parameters:
VariablesWithoutOptionals = Omit<VariablesParam, OptionalVariableNames>,
> = VariablesWithoutOptionals extends EmptyVariables
? // No expected required variables, object is optional
true
: GenericVariables extends VariablesParam
? // We don't have information about the variables, so we assume object is optional
true
: Partial<VariablesWithoutOptionals> extends VariablesWithoutOptionals
? // All known variables are optional, object is optional
true
: // Some known variables are required, object is required
false;

/**
* Used as the type for the GraphQL client's variables. It checks
* the generated operation types to see if variables are optional.
* @example
* graphqlQuery: (query: string, param: ClientVariables<...>) => Promise<...>
* Where `param` is required.
*/
export type ClientVariables<
GeneratedOperations extends CodegenOperations,
RawGqlString extends string,
OptionalVariableNames extends string = never,
VariablesKey extends string = 'variables',
// The following are just extracted repeated types, not parameters:
GeneratedVariables = RawGqlString extends keyof GeneratedOperations
? SetOptional<
GeneratedOperations[RawGqlString]['variables'],
Extract<
keyof GeneratedOperations[RawGqlString]['variables'],
OptionalVariableNames
>
>
: GenericVariables,
VariablesWrapper = Record<VariablesKey, GeneratedVariables>,
> = IsOptionalVariables<GeneratedVariables, OptionalVariableNames> extends true
? // Variables are all optional: object wrapper is optional
Partial<VariablesWrapper>
: // Some variables are required: object wrapper is required
VariablesWrapper;

/**
* Similar to ClientVariables, but makes the whole wrapper optional:
* @example
* graphqlQuery: (query: string, ...params: ClientVariablesInRestParams<...>) => Promise<...>
* Where the first item in `params` might be optional depending on the query.
*/
export type ClientVariablesInRestParams<
GeneratedOperations extends CodegenOperations,
RawGqlString extends string,
OtherParams extends Record<string, any> = {},
OptionalVariableNames extends string = never,
// The following are just extracted repeated types, not parameters:
ProcessedVariables = OtherParams &
ClientVariables<GeneratedOperations, RawGqlString, OptionalVariableNames>,
> = Partial<OtherParams> extends OtherParams
? // No required keys in OtherParams: keep checking
IsOptionalVariables<
GeneratedOperations[RawGqlString]['variables'],
OptionalVariableNames
> extends true
? // No required keys in OtherParams and variables are also optional: rest param is optional
[ProcessedVariables?]
: // No required keys in OtherParams but variables are required: rest param is required
[ProcessedVariables]
: // There are required keys in OtherParams: rest param is required
[ProcessedVariables];
1 change: 1 addition & 0 deletions packages/hydrogen-codegen/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export {plugin} from './plugin.js';
export {schema, getSchema} from './schema.js';
export {processSources} from './sources.js';
export {pluckConfig} from './pluck.js';
export type * from './client.js';
Loading

0 comments on commit 9ad7c5e

Please sign in to comment.