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

Commit

Permalink
Prototyping typed GQL queries without AST nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
paulomarg committed Oct 25, 2023
1 parent a0ea8dd commit 79c0430
Show file tree
Hide file tree
Showing 19 changed files with 774 additions and 72 deletions.
1 change: 1 addition & 0 deletions packages/shopify-api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.cache
3 changes: 3 additions & 0 deletions packages/shopify-api/codegen/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './shopify-api';
export * from './preset';
export * from './types';
53 changes: 53 additions & 0 deletions packages/shopify-api/codegen/preset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type {Types} from '@graphql-codegen/plugin-helpers';
import {preset as hydrogenPreset} from '@shopify/hydrogen-codegen';

import {ApiType, type ShopifyApiPresetConfig} from './types';

interface ApiPresetConfig {
importTypesFrom: string;
namespacedImportName: string;
interfaceExtension: string;
}

type ApiPresetConfigs = {
[key in ApiType]: ApiPresetConfig;
};

const apiPresetConfigs: ApiPresetConfigs = {
Admin: {
importTypesFrom: './admin.types',
namespacedImportName: 'AdminTypes',
interfaceExtension: `declare module '@shopify/shopify-api' {\n interface AdminQueries extends %%QUERY%% {}\n interface AdminMutations extends %%MUTATION%% {}\n}`,
},
Storefront: {
importTypesFrom: './storefront.types',
namespacedImportName: 'StorefrontTypes',
interfaceExtension: `declare module '@shopify/shopify-api' {\n interface StorefrontQueries extends %%QUERY%% {}\n interface StorefrontMutations extends %%MUTATION%% {}\n}`,
},
};

export const preset: Types.OutputPreset<ShopifyApiPresetConfig> = {
buildGeneratesSection: (options) => {
const apiType = options.presetConfig.apiType;

const {interfaceExtension, ...customPresetConfigs} =
apiPresetConfigs[apiType];

return hydrogenPreset.buildGeneratesSection({
...options,
presetConfig: {
...customPresetConfigs,
interfaceExtension: ({
queryType,
mutationType,
}: {
queryType: string;
mutationType: string;
}) =>
interfaceExtension
.replace('%%QUERY%%', queryType)
.replace('%%MUTATION%%', mutationType),
},
});
},
};
79 changes: 79 additions & 0 deletions packages/shopify-api/codegen/shopify-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import fs from 'fs';
import path from 'path';

import {preset} from './preset';
import {ApiType} from './types';

const CACHE_DIR = path.join(__dirname, '..', '.cache');

interface ShopifyApiTypesOptions {
apiType: ApiType;
apiVersion?: string;
outputDir?: string;
documents?: string[];
}

interface ApiConfig {
schema: string;
schemaFile: string;
typesFile: string;
queryTypesFile: string;
}

type ApiConfigs = {
[key in ApiType]: ApiConfig;
};

const apiConfigs: ApiConfigs = {
Admin: {
schema: 'https://shopify.dev/admin-graphql-direct-proxy%%API_VERSION%%',
schemaFile: `${CACHE_DIR}/admin%%API_VERSION%%.schema.json`,
typesFile: 'admin.types.ts',
queryTypesFile: 'admin.generated.d.ts',
},
Storefront: {
schema:
'https://shopify.dev/storefront-graphql-direct-proxy%%API_VERSION%%',
schemaFile: `${CACHE_DIR}/storefront%%API_VERSION%%.schema.json`,
typesFile: 'storefront.types.ts',
queryTypesFile: 'storefront.generated.d.ts',
},
};

export const shopifyApiTypes = ({
apiType,
apiVersion,
outputDir = './',
documents = ['./**/*.{ts,tsx}'],
}: ShopifyApiTypesOptions) => {
const config = apiConfigs[apiType];

const schema = config.schema.replace(
'%%API_VERSION%%',
apiVersion ? `/${apiVersion}` : '',
);
const schemaFile = config.schemaFile.replace(
'%%API_VERSION%%',
apiVersion ? `-${apiVersion}` : '',
);

console.log(`Loading ${apiType} schema from ${schemaFile}`);

const schemaFileExists = fs.existsSync(schemaFile);

return {
...(schemaFileExists
? {}
: {[schemaFile]: {schema, plugins: ['introspection']}}),
[`${outputDir}/${config.typesFile}`]: {
schema: schemaFileExists ? schemaFile : schema,
plugins: ['typescript'],
},
[`${outputDir}/${config.queryTypesFile}`]: {
schema: schemaFileExists ? schemaFile : schema,
preset,
documents,
presetConfig: {apiType},
},
};
};
8 changes: 8 additions & 0 deletions packages/shopify-api/codegen/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export enum ApiType {
Admin = 'Admin',
Storefront = 'Storefront',
}

export interface ShopifyApiPresetConfig {
apiType: ApiType;
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,13 @@ describe('GraphQL client', () => {
edges {
node {
id
}
}
}
}
}`,
variables: `{
'first': 2,
}`,
variables: {
first: 2,
},
};
const expectedResponse = {
data: {
Expand Down Expand Up @@ -187,7 +187,7 @@ describe('GraphQL client', () => {
domain,
path: `/admin/api/${shopify.config.apiVersion}/graphql.json`,
headers: {
'Content-Length': 219,
'Content-Length': 205,
'Content-Type': 'application/json',
'X-Shopify-Access-Token': accessToken,
},
Expand Down
24 changes: 24 additions & 0 deletions packages/shopify-api/lib/clients/graphql/admin_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type {PostRequestParams, RequestData, RequestReturn} from '../types';

import type {ReturnBody} from './types';

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

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

export type AdminOperations = AdminQueries & AdminMutations;

export type AdminGraphqlReturn<T = any> = Omit<RequestReturn<T>, 'body'> & {
body: ReturnBody<T, AdminOperations>;
};

export type AdminGraphqlParams<T = any> = Omit<
PostRequestParams,
'path' | 'type' | 'data'
> & {
data: RequestData<T, AdminOperations>;
};
16 changes: 9 additions & 7 deletions packages/shopify-api/lib/clients/graphql/graphql_client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {ApiVersion, ShopifyHeader} from '../../types';
import {ConfigInterface} from '../../base-types';
import {httpClientClass, HttpClient} from '../http_client/http_client';
import {DataType, HeaderParams, RequestReturn} from '../http_client/types';
import {DataType, HeaderParams} from '../http_client/types';
import {Session} from '../../session/session';
import {logger} from '../../logger';
import * as ShopifyErrors from '../../error';

import {GraphqlParams, GraphqlClientParams} from './types';
import {GraphqlClientParams} from './types';
import {AdminGraphqlParams, AdminGraphqlReturn} from './admin_types';

export interface GraphqlClientClassParams {
config: ConfigInterface;
Expand Down Expand Up @@ -47,12 +48,13 @@ export class GraphqlClient {
});
}

public async query<T = unknown>(
params: GraphqlParams,
): Promise<RequestReturn<T>> {
public async query<T = any>(
params: AdminGraphqlParams<T>,
): Promise<AdminGraphqlReturn<T>> {
if (
(typeof params.data === 'string' && params.data.length === 0) ||
Object.entries(params.data).length === 0
(typeof params.data === 'object' &&
Object.entries(params.data!).length === 0)
) {
throw new ShopifyErrors.MissingRequiredArgument('Query missing.');
}
Expand Down Expand Up @@ -91,7 +93,7 @@ export class GraphqlClient {
});
}

return result;
return result as AdminGraphqlReturn<T>;
}

protected getApiHeaders(): HeaderParams {
Expand Down
10 changes: 10 additions & 0 deletions packages/shopify-api/lib/clients/graphql/storefront_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {MissingRequiredArgument} from '../../error';

import {GraphqlClient, GraphqlClientClassParams} from './graphql_client';
import {GraphqlClientParams} from './types';
import {
StorefrontGraphqlParams,
StorefrontGraphqlReturn,
} from './storefront_types';

export class StorefrontClient extends GraphqlClient {
baseApiPath = '/api';
Expand All @@ -26,6 +30,12 @@ export class StorefrontClient extends GraphqlClient {
}
}

public async query<T = any>(
params: StorefrontGraphqlParams<T>,
): Promise<StorefrontGraphqlReturn<T>> {
return super.query<T>(params);
}

protected getApiHeaders(): HeaderParams {
const config = this.storefrontClass().config;

Expand Down
27 changes: 27 additions & 0 deletions packages/shopify-api/lib/clients/graphql/storefront_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type {PostRequestParams, RequestData, RequestReturn} from '../types';

import type {ReturnBody} from './types';

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

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

export type StorefrontOperations = StorefrontQueries & StorefrontMutations;

export type StorefrontGraphqlReturn<T = any> = Omit<
RequestReturn<T>,
'body'
> & {
body: ReturnBody<T, StorefrontOperations>;
};

export type StorefrontGraphqlParams<T = any> = Omit<
PostRequestParams,
'path' | 'type' | 'data'
> & {
data: RequestData<T, StorefrontOperations>;
};
36 changes: 33 additions & 3 deletions packages/shopify-api/lib/clients/graphql/types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,44 @@
import {ApiVersion} from '../../types';
import {Session} from '../../session/session';
import {PostRequestParams, RequestReturn} from '../http_client/types';

export type GraphqlParams = Omit<PostRequestParams, 'path' | 'type'>;
import {RequestReturn} from '../http_client/types';

export interface GraphqlClientParams {
session: Session;
apiVersion?: ApiVersion;
}

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

type UnpackedInput<
Operations extends AllOperations,
T extends keyof Operations,
> = {
[k in keyof Operations[T]['variables']]: 'input' extends keyof Operations[T]['variables'][k]
? Operations[T]['variables'][k]['input']
: Operations[T]['variables'][k];
};

export type OperationRequest<
Operations extends AllOperations,
T extends keyof Operations,
> = Operations[T]['variables'] extends {[key: string]: never}
? {query: T; variables?: never}
: {query: T; variables: UnpackedInput<Operations, T>};

export type RequestData<
T,
Operations extends AllOperations,
> = T extends keyof Operations
? OperationRequest<Operations, T>
: {[key: string]: unknown} | string;

export type ReturnBody<
T,
Operations extends AllOperations,
> = T extends keyof Operations ? {data: Operations[T]['return']} : any;

export interface GraphqlProxyParams {
session: Session;
rawBody: string;
Expand Down
Loading

0 comments on commit 79c0430

Please sign in to comment.