Skip to content
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
6 changes: 3 additions & 3 deletions graphql/codegen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
"scripts": {
"clean": "makage clean",
"prepack": "npm run build",
"copy:ts": "makage copy src/core/codegen/orm/query-builder.ts dist/core/codegen/orm --flat",
"build": "makage build && npm run copy:ts",
"build:dev": "makage build --dev && npm run copy:ts",
"copy:templates": "mkdir -p dist/core/codegen/templates && cp src/core/codegen/templates/*.ts dist/core/codegen/templates/",
"build": "makage build && npm run copy:templates",
"build:dev": "makage build --dev && npm run copy:templates",
"dev": "ts-node ./src/index.ts",
"lint": "eslint . --fix",
"fmt": "oxfmt --write .",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,17 @@ exports[`client-generator generateOrmClientFile generates OrmClient class with e
* @generated by @constructive-io/graphql-codegen
* DO NOT EDIT - changes will be overwritten
*/
import type {
GraphQLAdapter,
GraphQLError,
QueryResult,
} from '@constructive-io/graphql-types';

import type { GraphQLAdapter, GraphQLError, QueryResult } from '@constructive-io/graphql-types';

export type { GraphQLAdapter, GraphQLError, QueryResult } from '@constructive-io/graphql-types';
export type {
GraphQLAdapter,
GraphQLError,
QueryResult,
} from '@constructive-io/graphql-types';

/**
* Default adapter that uses fetch for HTTP requests.
Expand Down Expand Up @@ -199,7 +206,7 @@ export class GraphQLRequestError extends Error {
public readonly errors: GraphQLError[],
public readonly data: unknown = null
) {
const messages = errors.map(e => e.message).join('; ');
const messages = errors.map((e) => e.message).join('; ');
super(\`GraphQL Error: \${messages}\`);
this.name = 'GraphQLRequestError';
}
Expand All @@ -214,7 +221,9 @@ export class OrmClient {
} else if (config.endpoint) {
this.adapter = new FetchAdapter(config.endpoint, config.headers);
} else {
throw new Error('OrmClientConfig requires either an endpoint or a custom adapter');
throw new Error(
'OrmClientConfig requires either an endpoint or a custom adapter'
);
}
}

Expand Down Expand Up @@ -252,7 +261,6 @@ exports[`client-generator generateSelectTypesFile generates select type utilitie
* @generated by @constructive-io/graphql-codegen
* DO NOT EDIT - changes will be overwritten
*/

export interface ConnectionResult<T> {
nodes: T[];
totalCount: number;
Expand Down Expand Up @@ -341,15 +349,19 @@ export type DeepExact<T, Shape> = T extends Shape
export type InferSelectResult<TEntity, TSelect> = TSelect extends undefined
? TEntity
: {
[K in keyof TSelect as TSelect[K] extends false | undefined ? never : K]: TSelect[K] extends true
[K in keyof TSelect as TSelect[K] extends false | undefined
? never
: K]: TSelect[K] extends true
? K extends keyof TEntity
? TEntity[K]
: never
: TSelect[K] extends { select: infer NestedSelect }
? K extends keyof TEntity
? NonNullable<TEntity[K]> extends ConnectionResult<infer NodeType>
? ConnectionResult<InferSelectResult<NodeType, NestedSelect>>
: InferSelectResult<NonNullable<TEntity[K]>, NestedSelect> | (null extends TEntity[K] ? null : never)
:
| InferSelectResult<NonNullable<TEntity[K]>, NestedSelect>
| (null extends TEntity[K] ? null : never)
: never
: K extends keyof TEntity
? TEntity[K]
Expand Down
288 changes: 50 additions & 238 deletions graphql/codegen/src/core/codegen/client.ts
Original file line number Diff line number Diff line change
@@ -1,262 +1,74 @@
/**
* Client generator - generates client.ts with configure() and execute()
*/
import { getGeneratedFileHeader } from './utils';

/**
* Generate client.ts content
*/
export function generateClientFile(): string {
return `${getGeneratedFileHeader('GraphQL client configuration and execution')}

// ============================================================================
// Configuration
// ============================================================================

export interface GraphQLClientConfig {
/** GraphQL endpoint URL */
endpoint: string;
/** Default headers to include in all requests */
headers?: Record<string, string>;
}

let globalConfig: GraphQLClientConfig | null = null;

/**
* Configure the GraphQL client
*
* @example
* \`\`\`ts
* import { configure } from './generated';
*
* configure({
* endpoint: 'https://api.example.com/graphql',
* headers: {
* Authorization: 'Bearer <token>',
* },
* });
* \`\`\`
*/
export function configure(config: GraphQLClientConfig): void {
globalConfig = config;
}

/**
* Get the current configuration
* @throws Error if not configured
*/
export function getConfig(): GraphQLClientConfig {
if (!globalConfig) {
throw new Error(
'GraphQL client not configured. Call configure() before making requests.'
);
}
return globalConfig;
}

/**
* Set a single header value
* Useful for updating Authorization after login
*
* @example
* \`\`\`ts
* setHeader('Authorization', 'Bearer <new-token>');
* \`\`\`
*/
export function setHeader(key: string, value: string): void {
const config = getConfig();
globalConfig = {
...config,
headers: { ...config.headers, [key]: value },
};
}

/**
* Merge multiple headers into the current configuration
*
* @example
* \`\`\`ts
* setHeaders({ Authorization: 'Bearer <token>', 'X-Custom': 'value' });
* \`\`\`
* Reads from template files in the templates/ directory for proper type checking.
*/
export function setHeaders(headers: Record<string, string>): void {
const config = getConfig();
globalConfig = {
...config,
headers: { ...config.headers, ...headers },
};
}

// ============================================================================
// Error handling
// ============================================================================

export interface GraphQLError {
message: string;
locations?: Array<{ line: number; column: number }>;
path?: Array<string | number>;
extensions?: Record<string, unknown>;
}

export class GraphQLClientError extends Error {
constructor(
message: string,
public errors: GraphQLError[],
public response?: Response
) {
super(message);
this.name = 'GraphQLClientError';
}
}

// ============================================================================
// Execution
// ============================================================================
import * as fs from 'fs';
import * as path from 'path';
import { getGeneratedFileHeader } from './utils';

export interface ExecuteOptions {
/** Override headers for this request */
headers?: Record<string, string>;
/** AbortSignal for request cancellation */
signal?: AbortSignal;
export interface GenerateClientFileOptions {
/**
* Generate browser-compatible code using native fetch
* When true (default), uses native W3C fetch API
* When false, uses undici fetch with dispatcher support for localhost DNS resolution
* @default true
*/
browserCompatible?: boolean;
}

/**
* Execute a GraphQL operation
*
* @example
* \`\`\`ts
* const result = await execute<CarsQueryResult, CarsQueryVariables>(
* carsQueryDocument,
* { first: 10 }
* );
* \`\`\`
* Find a template file path.
* Templates are at ./templates/ relative to this file in both src/ and dist/.
*/
export async function execute<TData = unknown, TVariables = Record<string, unknown>>(
document: string,
variables?: TVariables,
options?: ExecuteOptions
): Promise<TData> {
const config = getConfig();

const response = await fetch(config.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...config.headers,
...options?.headers,
},
body: JSON.stringify({
query: document,
variables,
}),
signal: options?.signal,
});
function findTemplateFile(templateName: string): string {
const templatePath = path.join(__dirname, 'templates', templateName);

const json = await response.json();

if (json.errors && json.errors.length > 0) {
throw new GraphQLClientError(
json.errors[0].message || 'GraphQL request failed',
json.errors,
response
);
if (fs.existsSync(templatePath)) {
return templatePath;
}

return json.data as TData;
throw new Error(
`Could not find template file: ${templateName}. ` +
`Searched in: ${templatePath}`
);
}

/**
* Execute a GraphQL operation with full response (data + errors)
* Useful when you want to handle partial data with errors
* Read a template file and replace the header with generated file header
*/
export async function executeWithErrors<TData = unknown, TVariables = Record<string, unknown>>(
document: string,
variables?: TVariables,
options?: ExecuteOptions
): Promise<{ data: TData | null; errors: GraphQLError[] | null }> {
const config = getConfig();
function readTemplateFile(templateName: string, description: string): string {
const templatePath = findTemplateFile(templateName);
let content = fs.readFileSync(templatePath, 'utf-8');

const response = await fetch(config.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...config.headers,
...options?.headers,
},
body: JSON.stringify({
query: document,
variables,
}),
signal: options?.signal,
});
// Replace the source file header comment with the generated file header
// Match the header pattern used in template files
const headerPattern =
/\/\*\*[\s\S]*?\* NOTE: This file is read at codegen time and written to output\.[\s\S]*?\*\/\n*/;

const json = await response.json();
content = content.replace(
headerPattern,
getGeneratedFileHeader(description) + '\n'
);

return {
data: json.data ?? null,
errors: json.errors ?? null,
};
return content;
}

// ============================================================================
// QueryClient Factory
// ============================================================================

/**
* Default QueryClient configuration optimized for GraphQL
*
* These defaults provide a good balance between freshness and performance:
* - staleTime: 1 minute - data considered fresh, won't refetch
* - gcTime: 5 minutes - unused data kept in cache
* - refetchOnWindowFocus: false - don't refetch when tab becomes active
* - retry: 1 - retry failed requests once
*/
export const defaultQueryClientOptions = {
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
gcTime: 1000 * 60 * 5, // 5 minutes
refetchOnWindowFocus: false,
retry: 1,
},
},
};

/**
* QueryClient options type for createQueryClient
* Generate client.ts content
* @param options - Generation options
*/
export interface CreateQueryClientOptions {
defaultOptions?: {
queries?: {
staleTime?: number;
gcTime?: number;
refetchOnWindowFocus?: boolean;
retry?: number | boolean;
retryDelay?: number | ((attemptIndex: number) => number);
};
mutations?: {
retry?: number | boolean;
retryDelay?: number | ((attemptIndex: number) => number);
};
};
}

// Note: createQueryClient is available when using with @tanstack/react-query
// Import QueryClient from '@tanstack/react-query' and use these options:
//
// import { QueryClient } from '@tanstack/react-query';
// const queryClient = new QueryClient(defaultQueryClientOptions);
//
// Or merge with your own options:
// const queryClient = new QueryClient({
// ...defaultQueryClientOptions,
// defaultOptions: {
// ...defaultQueryClientOptions.defaultOptions,
// queries: {
// ...defaultQueryClientOptions.defaultOptions.queries,
// staleTime: 30000, // Override specific options
// },
// },
// });
`;
export function generateClientFile(
options: GenerateClientFileOptions = {}
): string {
const { browserCompatible = true } = options;

const templateName = browserCompatible
? 'client.browser.ts'
: 'client.node.ts';

return readTemplateFile(
templateName,
'GraphQL client configuration and execution'
);
}
Loading