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 @@ -47,9 +47,9 @@
"example:codegen:orm": "tsx src/cli/index.ts generate-orm --config examples/multi-target.config.ts",
"example:codegen:sdk:schema": "node dist/cli/index.js generate --schema examples/example.schema.graphql --output examples/output/generated-sdk-schema",
"example:codegen:orm:schema": "node dist/cli/index.js generate-orm --schema examples/example.schema.graphql --output examples/output/generated-orm-schema",
"example:sdk": "tsx examples/react-query-sdk.ts",
"example:orm": "tsx examples/orm-sdk.ts",
"example:sdk:typecheck": "tsc --noEmit --jsx react --esModuleInterop --skipLibCheck --moduleResolution node examples/react-hooks-test.tsx"
"example:sdk": "tsx examples/react-hooks-sdk-test.tsx",
"example:orm": "tsx examples/orm-sdk-test.ts",
"example:sdk:typecheck": "tsc --noEmit --jsx react --esModuleInterop --skipLibCheck --moduleResolution node examples/react-hooks-sdk-test.tsx"
},
"dependencies": {
"@babel/generator": "^7.28.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`client-generator generateCreateClientFile generates createClient factory with models 1`] = `
"/**
* ORM Client - createClient factory
* @generated by @constructive-io/graphql-codegen
* DO NOT EDIT - changes will be overwritten
*/
import { OrmClient } from "./client";
import type { OrmClientConfig } from "./client";
import { UserModel } from "./models/user";
import { PostModel } from "./models/post";
export type { OrmClientConfig, QueryResult, GraphQLError } from "./client";
export { GraphQLRequestError } from "./client";
export { QueryBuilder } from "./query-builder";
export * from "./select-types";
/**
* Create an ORM client instance
*
* @example
* \`\`\`typescript
* const db = createClient({
* endpoint: 'https://api.example.com/graphql',
* headers: { Authorization: 'Bearer token' },
* });
*
* // Query users
* const users = await db.user.findMany({
* select: { id: true, name: true },
* first: 10,
* }).execute();
*
* // Create a user
* const newUser = await db.user.create({
* data: { name: 'John', email: 'john@example.com' },
* select: { id: true },
* }).execute();
* \`\`\`
*/
export function createClient(config: OrmClientConfig) {
const client = new OrmClient(config);
return {
user: new UserModel(client),
post: new PostModel(client)
};
}"
`;

exports[`client-generator generateCreateClientFile includes custom query/mutation operations when available 1`] = `
"/**
* ORM Client - createClient factory
* @generated by @constructive-io/graphql-codegen
* DO NOT EDIT - changes will be overwritten
*/
import { OrmClient } from "./client";
import type { OrmClientConfig } from "./client";
import { UserModel } from "./models/user";
import { createQueryOperations } from "./query";
import { createMutationOperations } from "./mutation";
export type { OrmClientConfig, QueryResult, GraphQLError } from "./client";
export { GraphQLRequestError } from "./client";
export { QueryBuilder } from "./query-builder";
export * from "./select-types";
/**
* Create an ORM client instance
*
* @example
* \`\`\`typescript
* const db = createClient({
* endpoint: 'https://api.example.com/graphql',
* headers: { Authorization: 'Bearer token' },
* });
*
* // Query users
* const users = await db.user.findMany({
* select: { id: true, name: true },
* first: 10,
* }).execute();
*
* // Create a user
* const newUser = await db.user.create({
* data: { name: 'John', email: 'john@example.com' },
* select: { id: true },
* }).execute();
* \`\`\`
*/
export function createClient(config: OrmClientConfig) {
const client = new OrmClient(config);
return {
user: new UserModel(client),
query: createQueryOperations(client),
mutation: createMutationOperations(client)
};
}"
`;

exports[`client-generator generateOrmClientFile generates OrmClient class with execute method 1`] = `
"/**
* ORM Client - Runtime GraphQL executor
* @generated by @constructive-io/graphql-codegen
* DO NOT EDIT - changes will be overwritten
*/

export interface OrmClientConfig {
endpoint: string;
headers?: Record<string, string>;
}

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

/**
* Error thrown when GraphQL request fails
*/
export class GraphQLRequestError extends Error {
constructor(
public readonly errors: GraphQLError[],
public readonly data: unknown = null
) {
const messages = errors.map(e => e.message).join('; ');
super(\`GraphQL Error: \${messages}\`);
this.name = 'GraphQLRequestError';
}
}

/**
* Discriminated union for query results
* Use .ok to check success, or use .unwrap() to get data or throw
*/
export type QueryResult<T> =
| { ok: true; data: T; errors: undefined }
| { ok: false; data: null; errors: GraphQLError[] };

/**
* Legacy QueryResult type for backwards compatibility
* @deprecated Use QueryResult discriminated union instead
*/
export interface LegacyQueryResult<T> {
data: T | null;
errors?: GraphQLError[];
}

export class OrmClient {
private endpoint: string;
private headers: Record<string, string>;

constructor(config: OrmClientConfig) {
this.endpoint = config.endpoint;
this.headers = config.headers ?? {};
}

async execute<T>(
document: string,
variables?: Record<string, unknown>
): Promise<QueryResult<T>> {
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...this.headers,
},
body: JSON.stringify({
query: document,
variables: variables ?? {},
}),
});

if (!response.ok) {
return {
ok: false,
data: null,
errors: [{ message: \`HTTP \${response.status}: \${response.statusText}\` }],
};
}

const json = (await response.json()) as {
data?: T;
errors?: GraphQLError[];
};

// Return discriminated union based on presence of errors
if (json.errors && json.errors.length > 0) {
return {
ok: false,
data: null,
errors: json.errors,
};
}

return {
ok: true,
data: json.data as T,
errors: undefined,
};
}

setHeaders(headers: Record<string, string>): void {
this.headers = { ...this.headers, ...headers };
}

getEndpoint(): string {
return this.endpoint;
}
}
"
`;

exports[`client-generator generateSelectTypesFile generates select type utilities 1`] = `
"/**
* Type utilities for select inference
* @generated by @constructive-io/graphql-codegen
* DO NOT EDIT - changes will be overwritten
*/

export interface ConnectionResult<T> {
nodes: T[];
totalCount: number;
pageInfo: PageInfo;
}

export interface PageInfo {
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor?: string | null;
endCursor?: string | null;
}

export interface FindManyArgs<TSelect, TWhere, TOrderBy> {
select?: TSelect;
where?: TWhere;
orderBy?: TOrderBy[];
first?: number;
last?: number;
after?: string;
before?: string;
offset?: number;
}

export interface FindFirstArgs<TSelect, TWhere> {
select?: TSelect;
where?: TWhere;
}

export interface CreateArgs<TSelect, TData> {
data: TData;
select?: TSelect;
}

export interface UpdateArgs<TSelect, TWhere, TData> {
where: TWhere;
data: TData;
select?: TSelect;
}

export interface DeleteArgs<TWhere> {
where: TWhere;
}

/**
* Recursively validates select objects, rejecting unknown keys.
*
* This type ensures that users can only select fields that actually exist
* in the GraphQL schema. It returns \`never\` if any excess keys are found
* at any nesting level, causing a TypeScript compile error.
*
* Why this is needed:
* TypeScript's excess property checking has a quirk where it only catches
* invalid fields when they are the ONLY fields. When mixed with valid fields
* (e.g., \`{ id: true, invalidField: true }\`), the structural typing allows
* the excess property through. This type explicitly checks for and rejects
* such cases.
*
* @example
* // This will cause a type error because 'invalid' doesn't exist:
* type Result = DeepExact<{ id: true, invalid: true }, { id?: boolean }>;
* // Result = never (causes assignment error)
*
* @example
* // This works because all fields are valid:
* type Result = DeepExact<{ id: true }, { id?: boolean; name?: boolean }>;
* // Result = { id: true }
*/
export type DeepExact<T, Shape> = T extends Shape
? Exclude<keyof T, keyof Shape> extends never
? {
[K in keyof T]: K extends keyof Shape
? T[K] extends { select: infer NS }
? Shape[K] extends { select?: infer ShapeNS }
? { select: DeepExact<NS, NonNullable<ShapeNS>> }
: T[K]
: T[K]
: never;
}
: never
: never;

/**
* Infer result type from select configuration
*/
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 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)
: never
: K extends keyof TEntity
? TEntity[K]
: never;
};
"
`;
Loading