Skip to content
This repository has been archived by the owner on Apr 11, 2024. It is now read-only.

Commit

Permalink
Merge pull request #1054 from Shopify/add_graphql_types
Browse files Browse the repository at this point in the history
Add GQL types for Admin API
  • Loading branch information
paulomarg authored Nov 15, 2023
2 parents 7587f87 + 6e17432 commit b96d16f
Show file tree
Hide file tree
Showing 13 changed files with 178 additions and 75 deletions.
6 changes: 6 additions & 0 deletions .changeset/empty-houses-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@shopify/admin-api-client": minor
"@shopify/graphql-client": minor
---

Added the ability to automatically type GraphQL queries when the files created by @shopify/api-codegen-preset are loaded for the app.
48 changes: 48 additions & 0 deletions packages/admin-api-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,54 @@ const {data, errors, extensions} = await client.request(productQuery, {
});
```

## Typing variables and return objects

This client is compatible with the `@shopify/api-codegen-preset` package.
You can use that package to create types from your operations with the [Codegen CLI](https://www.graphql-cli.com/codegen/).

There are different ways to [configure codegen](https://github.com/Shopify/shopify-api-js/tree/main/packages/api-codegen-preset#configuration) with it, but the simplest way is to:

1. Add the preset package as a dev dependency to your project, for example:
```bash
npm install --save-dev @shopify/api-codegen-preset
```
1. Create a `.graphqlrc.ts` file in your root containing:
```ts
import { ApiType, shopifyApiProject } from "@shopify/api-codegen-preset";
export default {
schema: "https://shopify.dev/admin-graphql-direct-proxy",
documents: ["*.ts", "!node_modules"],
projects: {
default: shopifyApiProject({
apiType: ApiType.Admin,
apiVersion: "2023-10",
outputDir: "./types",
}),
},
};
```
1. Add `"graphql-codegen": "graphql-codegen"` to your `scripts` section in `package.json`.
1. Tag your operations with `#graphql`, for example:
```ts
const {data, errors, extensions} = await client.request(
`#graphql
query Shop {
shop {
name
}
}`
);
console.log(data?.shop.name);
```
1. Run `npm run graphql-codegen` to parse the types from your operations.
> [!NOTE]
> Remember to ensure that your tsconfig includes the files under `./types`!
Once the script runs, it'll create the file `./types/admin.generated.d.ts`.
When TS includes that file, it'll automatically cause the client to detect the types for each query.
## Log Content Types
### `UnsupportedApiVersionLog`
Expand Down
32 changes: 1 addition & 31 deletions packages/admin-api-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,38 +63,8 @@
"@shopify/graphql-client": "^0.7.0"
},
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/plugin-transform-async-to-generator": "^7.20.7",
"@babel/plugin-transform-runtime": "^7.21.0",
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.21.0",
"@changesets/changelog-github": "^0.4.8",
"@changesets/cli": "^2.26.1",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^24.0.1",
"@rollup/plugin-eslint": "^9.0.3",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-terser": "^0.4.0",
"@rollup/plugin-typescript": "^11.0.0",
"@shopify/babel-preset": "^25.0.0",
"@shopify/eslint-plugin": "^42.0.3",
"@shopify/prettier-config": "^1.1.2",
"@shopify/typescript-configs": "^5.1.0",
"@types/jest": "^29.5.0",
"@types/regenerator-runtime": "^0.13.1",
"@typescript-eslint/parser": "^6.7.5",
"babel-jest": "^29.5.0",
"eslint": "^8.51.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.5.0",
"jest-fetch-mock": "^3.0.3",
"prettier": "^2.5.1",
"regenerator-runtime": "^0.13.11",
"rollup": "^3.19.1",
"rollup-plugin-dts": "^5.2.0",
"tslib": "^2.5.0",
"typescript": "^5.2.0"
"regenerator-runtime": "^0.14.0"
},
"bugs": {
"url": "https://github.com/Shopify/shopify-api-js/issues"
Expand Down
20 changes: 8 additions & 12 deletions packages/admin-api-client/src/admin-api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import {
validateDomainAndGetStoreUrl,
generateGetGQLClientParams,
generateGetHeaders,
ApiClientRequestParams,
} from "@shopify/graphql-client";

import {
AdminApiClientOptions,
AdminApiClient,
AdminApiClientConfig,
AdminOperations,
} from "./types";
import {
DEFAULT_CONTENT_TYPE,
Expand Down Expand Up @@ -88,25 +88,21 @@ export function createAdminApiClient({
const getHeaders = generateGetHeaders(config);
const getApiUrl = generateGetApiUrl(config, apiUrlFormatter);

const getGQLClientParams = generateGetGQLClientParams({
const getGQLClientParams = generateGetGQLClientParams<AdminOperations>({
getHeaders,
getApiUrl,
});

const fetch = (...props: ApiClientRequestParams) => {
return graphqlClient.fetch(...getGQLClientParams(...props));
};

const request = <TData>(...props: ApiClientRequestParams) => {
return graphqlClient.request<TData>(...getGQLClientParams(...props));
};

const client: AdminApiClient = {
config,
getHeaders,
getApiUrl,
fetch,
request,
fetch: (...props) => {
return graphqlClient.fetch(...getGQLClientParams(...props));
},
request: (...props) => {
return graphqlClient.request(...getGQLClientParams(...props));
},
};

return Object.freeze(client);
Expand Down
1 change: 1 addition & 0 deletions packages/admin-api-client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { createAdminApiClient } from "./admin-api-client";
export { AdminQueries, AdminMutations } from "./types";
6 changes: 3 additions & 3 deletions packages/admin-api-client/src/tests/admin-api-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ describe("Admin API Client", () => {
)
);

delete global.window;
delete (global as any).window;
});
});
});
Expand Down Expand Up @@ -330,7 +330,7 @@ describe("Admin API Client", () => {

it("returns a headers object that contains both the client default headers and the provided custom headers", () => {
const headers = {
"X-GraphQL-Cost-Include-Fields": true,
"X-GraphQL-Cost-Include-Fields": "1",
};
const updatedHeaders = client.getHeaders(headers);
expect(updatedHeaders).toEqual({
Expand Down Expand Up @@ -361,7 +361,7 @@ describe("Admin API Client", () => {
});

it("throws an error when the api version is not a string", () => {
const version = 123;
const version: any = 123;
expect(() => client.getApiUrl(version)).toThrow(
new Error(
`Admin API Client: the provided apiVersion ("123") is invalid. Current supported API versions: ${mockApiVersions.join(
Expand Down
10 changes: 9 additions & 1 deletion packages/admin-api-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,12 @@ export type AdminApiClientOptions = Omit<
logger?: ApiClientLogger<AdminApiClientLogContentTypes>;
};

export type AdminApiClient = ApiClient<AdminApiClientConfig>;
export interface AdminQueries {
[key: string]: { variables: any; return: any };
}
export interface AdminMutations {
[key: string]: { variables: any; return: any };
}
export type AdminOperations = AdminQueries & AdminMutations;

export type AdminApiClient = ApiClient<AdminApiClientConfig, AdminOperations>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { GraphQLClient } from "../graphql-client/types";

export type InputMaybe<_R = never> = never;

export interface AllOperations {
[key: string]: { variables: any; return: any };
}

type UnpackedInput<InputType> = "input" extends keyof InputType
? InputType["input"]
: InputType;

type UnpackedInputMaybe<InputType> = InputType extends InputMaybe<infer R>
? InputMaybe<UnpackedInput<R>>
: UnpackedInput<InputType>;

export type OperationVariables<
Operation extends keyof Operations,
Operations extends AllOperations
> = Operations[Operation]["variables"] extends { [key: string]: never }
? { [key: string]: never }
: {
variables?: {
[k in keyof Operations[Operation]["variables"]]: UnpackedInputMaybe<
Operations[Operation]["variables"][k]
>;
};
};

export type ResponseWithType<T = any> = Omit<GraphQLClient["fetch"], "json"> & {
json: () => Promise<T>;
};

export type ReturnData<
Operation extends keyof Operations,
Operations extends AllOperations
> = Operation extends keyof Operations ? Operations[Operation]["return"] : any;
60 changes: 45 additions & 15 deletions packages/graphql-client/src/api-client-utilities/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,22 @@ import {
LogContent,
Logger as BaseLogger,
Headers,
OperationVariables,
ClientResponse,
GraphQLClient,
} from "../graphql-client/types";

import {
AllOperations,
OperationVariables,
ResponseWithType,
ReturnData,
} from "./operation-types";

export {
AllOperations,
InputMaybe,
OperationVariables,
} from "./operation-types";

export interface UnsupportedApiVersionLog extends LogContent {
type: "UNSUPPORTED_API_VERSION";
content: {
Expand All @@ -31,28 +42,47 @@ export interface ApiClientConfig {
retries?: number;
}

export interface ApiClientRequestOptions {
variables?: OperationVariables;
export type ApiClientRequestOptions<
Operation extends keyof Operations = string,
Operations extends AllOperations = AllOperations
> = {
apiVersion?: string;
headers?: Headers;
retries?: number;
}
} & (Operation extends keyof Operations
? OperationVariables<Operation, Operations>
: { variables?: { [key: string]: any } });

export type ApiClientRequestParams = [
operation: string,
options?: ApiClientRequestOptions
export type ApiClientRequestParams<
Operation extends keyof Operations,
Operations extends AllOperations
> = [
operation: Operation,
options?: ApiClientRequestOptions<Operation, Operations>
];

export type ApiClientFetch<Operations extends AllOperations = AllOperations> = <
Operation extends keyof Operations = string
>(
...params: ApiClientRequestParams<Operation, Operations>
) => Promise<ResponseWithType<{ data?: ReturnData<Operation, Operations> }>>;

export type ApiClientRequest<Operations extends AllOperations = AllOperations> =
<TData = undefined, Operation extends keyof Operations = string>(
...params: ApiClientRequestParams<Operation, Operations>
) => Promise<
ClientResponse<
TData extends undefined ? ReturnData<Operation, Operations> : TData
>
>;

export interface ApiClient<
TClientConfig extends ApiClientConfig = ApiClientConfig
TClientConfig extends ApiClientConfig = ApiClientConfig,
Operations extends AllOperations = AllOperations
> {
readonly config: Readonly<TClientConfig>;
getHeaders: (headers?: Headers) => Headers;
getApiUrl: (apiVersion?: string) => string;
fetch: (
...props: ApiClientRequestParams
) => ReturnType<GraphQLClient["fetch"]>;
request: <TData = unknown>(
...props: ApiClientRequestParams
) => Promise<ClientResponse<TData>>;
fetch: ApiClientFetch<Operations>;
request: ApiClientRequest<Operations>;
}
19 changes: 13 additions & 6 deletions packages/graphql-client/src/api-client-utilities/utilities.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { RequestParams } from "../graphql-client/types";

import { ApiClient, ApiClientConfig, ApiClientRequestOptions } from "./types";
import {
AllOperations,
ApiClient,
ApiClientConfig,
ApiClientRequestOptions,
} from "./types";

export function generateGetHeaders(
config: ApiClientConfig
Expand All @@ -10,18 +15,20 @@ export function generateGetHeaders(
};
}

export function generateGetGQLClientParams({
export function generateGetGQLClientParams<
Operations extends AllOperations = AllOperations
>({
getHeaders,
getApiUrl,
}: {
getHeaders: ApiClient["getHeaders"];
getApiUrl: ApiClient["getApiUrl"];
}) {
return (
operation: string,
options?: ApiClientRequestOptions
return <Operation extends keyof Operations>(
operation: Operation,
options?: ApiClientRequestOptions<Operation, Operations>
): RequestParams => {
const props: RequestParams = [operation];
const props: RequestParams = [operation as string];

if (options && Object.keys(options).length > 0) {
const {
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql-client/src/graphql-client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type CustomFetchApi = (
}
) => Promise<Response>;

export interface OperationVariables {
interface OperationVariables {
[key: string]: any;
}

Expand Down
3 changes: 1 addition & 2 deletions packages/shopify-api/rest/load-rest-resources.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {ShopifyClients} from 'lib';

import type {ShopifyClients} from '../lib';
import {ConfigInterface} from '../lib/base-types';
import {logger} from '../lib/logger';

Expand Down
9 changes: 5 additions & 4 deletions packages/storefront-api-client/src/storefront-api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
validateApiVersion,
generateGetGQLClientParams,
generateGetHeaders,
ApiClientRequestParams,
ApiClientFetch,
ApiClientRequest,
} from "@shopify/graphql-client";

import {
Expand Down Expand Up @@ -100,12 +101,12 @@ export function createStorefrontApiClient({
getApiUrl,
});

const fetch = (...props: ApiClientRequestParams) => {
const fetch: ApiClientFetch = (...props) => {
return graphqlClient.fetch(...getGQLClientParams(...props));
};

const request = <TData>(...props: ApiClientRequestParams) => {
return graphqlClient.request<TData>(...getGQLClientParams(...props));
const request: ApiClientRequest = (...props) => {
return graphqlClient.request(...getGQLClientParams(...props));
};

const client: StorefrontApiClient = {
Expand Down

0 comments on commit b96d16f

Please sign in to comment.