From ee8ac2101c616f8bb75a5c08676e1565f9428d83 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Tue, 17 Feb 2026 09:58:46 +0000 Subject: [PATCH] feat: add codegen fixture example with schema and generated output --- __fixtures__/codegen-example/generate.ts | 26 + __fixtures__/codegen-example/output/index.ts | 5 + .../codegen-example/output/orm/client.ts | 137 +++ .../codegen-example/output/orm/index.ts | 48 + .../codegen-example/output/orm/input-types.ts | 775 ++++++++++++++++ .../output/orm/models/category.ts | 236 +++++ .../output/orm/models/comment.ts | 236 +++++ .../output/orm/models/index.ts | 9 + .../codegen-example/output/orm/models/post.ts | 236 +++++ .../codegen-example/output/orm/models/user.ts | 236 +++++ .../output/orm/query-builder.ts | 847 ++++++++++++++++++ .../output/orm/select-types.ts | 140 +++ .../codegen-example/output/orm/types.ts | 8 + __fixtures__/codegen-example/schema.graphql | 510 +++++++++++ 14 files changed, 3449 insertions(+) create mode 100644 __fixtures__/codegen-example/generate.ts create mode 100644 __fixtures__/codegen-example/output/index.ts create mode 100644 __fixtures__/codegen-example/output/orm/client.ts create mode 100644 __fixtures__/codegen-example/output/orm/index.ts create mode 100644 __fixtures__/codegen-example/output/orm/input-types.ts create mode 100644 __fixtures__/codegen-example/output/orm/models/category.ts create mode 100644 __fixtures__/codegen-example/output/orm/models/comment.ts create mode 100644 __fixtures__/codegen-example/output/orm/models/index.ts create mode 100644 __fixtures__/codegen-example/output/orm/models/post.ts create mode 100644 __fixtures__/codegen-example/output/orm/models/user.ts create mode 100644 __fixtures__/codegen-example/output/orm/query-builder.ts create mode 100644 __fixtures__/codegen-example/output/orm/select-types.ts create mode 100644 __fixtures__/codegen-example/output/orm/types.ts create mode 100644 __fixtures__/codegen-example/schema.graphql diff --git a/__fixtures__/codegen-example/generate.ts b/__fixtures__/codegen-example/generate.ts new file mode 100644 index 000000000..a922f837b --- /dev/null +++ b/__fixtures__/codegen-example/generate.ts @@ -0,0 +1,26 @@ +import path from 'node:path'; +import { generate } from '@constructive-io/graphql-codegen'; + +async function main() { + const result = await generate({ + schemaFile: path.resolve(__dirname, 'schema.graphql'), + output: path.resolve(__dirname, 'output'), + orm: true, + }); + + if (!result.success) { + console.error('Generation failed:', result.message); + result.errors?.forEach((e) => console.error(' -', e)); + process.exit(1); + } + + console.log(result.message); + if (result.tables?.length) { + console.log('Tables:', result.tables.join(', ')); + } +} + +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/__fixtures__/codegen-example/output/index.ts b/__fixtures__/codegen-example/output/index.ts new file mode 100644 index 000000000..51e77d3d4 --- /dev/null +++ b/__fixtures__/codegen-example/output/index.ts @@ -0,0 +1,5 @@ +/** + * Generated SDK - auto-generated, do not edit + * @generated by @constructive-io/graphql-codegen + */ +export * from './orm'; diff --git a/__fixtures__/codegen-example/output/orm/client.ts b/__fixtures__/codegen-example/output/orm/client.ts new file mode 100644 index 000000000..c0f12c466 --- /dev/null +++ b/__fixtures__/codegen-example/output/orm/client.ts @@ -0,0 +1,137 @@ +/** + * ORM Client - Runtime GraphQL executor + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +import 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. + * This is used when no custom adapter is provided. + */ +export class FetchAdapter implements GraphQLAdapter { + private headers: Record; + + constructor( + private endpoint: string, + headers?: Record + ) { + this.headers = headers ?? {}; + } + + async execute(document: string, variables?: Record): Promise> { + 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[]; + }; + + 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): void { + this.headers = { ...this.headers, ...headers }; + } + + getEndpoint(): string { + return this.endpoint; + } +} + +/** + * Configuration for creating an ORM client. + * Either provide endpoint (and optional headers) for HTTP requests, + * or provide a custom adapter for alternative execution strategies. + */ +export interface OrmClientConfig { + /** GraphQL endpoint URL (required if adapter not provided) */ + endpoint?: string; + /** Default headers for HTTP requests (only used with endpoint) */ + headers?: Record; + /** Custom adapter for GraphQL execution (overrides endpoint/headers) */ + adapter?: GraphQLAdapter; +} + +/** + * 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'; + } +} + +export class OrmClient { + private adapter: GraphQLAdapter; + + constructor(config: OrmClientConfig) { + if (config.adapter) { + this.adapter = config.adapter; + } 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'); + } + } + + async execute(document: string, variables?: Record): Promise> { + return this.adapter.execute(document, variables); + } + + /** + * Set headers for requests. + * Only works if the adapter supports headers. + */ + setHeaders(headers: Record): void { + if (this.adapter.setHeaders) { + this.adapter.setHeaders(headers); + } + } + + /** + * Get the endpoint URL. + * Returns empty string if the adapter doesn't have an endpoint. + */ + getEndpoint(): string { + return this.adapter.getEndpoint?.() ?? ''; + } +} diff --git a/__fixtures__/codegen-example/output/orm/index.ts b/__fixtures__/codegen-example/output/orm/index.ts new file mode 100644 index 000000000..1c6181f1d --- /dev/null +++ b/__fixtures__/codegen-example/output/orm/index.ts @@ -0,0 +1,48 @@ +/** + * 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'; +import { CommentModel } from './models/comment'; +import { CategoryModel } from './models/category'; +export type { OrmClientConfig, QueryResult, GraphQLError, GraphQLAdapter } from './client'; +export { GraphQLRequestError } from './client'; +export { QueryBuilder } from './query-builder'; +export * from './select-types'; +export * from './models'; +/** + * 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), + comment: new CommentModel(client), + category: new CategoryModel(client), + }; +} diff --git a/__fixtures__/codegen-example/output/orm/input-types.ts b/__fixtures__/codegen-example/output/orm/input-types.ts new file mode 100644 index 000000000..8465a78b6 --- /dev/null +++ b/__fixtures__/codegen-example/output/orm/input-types.ts @@ -0,0 +1,775 @@ +/** + * GraphQL types for ORM client + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +// ============ Scalar Filter Types ============ +export interface StringFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; + distinctFrom?: string; + notDistinctFrom?: string; + in?: string[]; + notIn?: string[]; + lessThan?: string; + lessThanOrEqualTo?: string; + greaterThan?: string; + greaterThanOrEqualTo?: string; + includes?: string; + notIncludes?: string; + includesInsensitive?: string; + notIncludesInsensitive?: string; + startsWith?: string; + notStartsWith?: string; + startsWithInsensitive?: string; + notStartsWithInsensitive?: string; + endsWith?: string; + notEndsWith?: string; + endsWithInsensitive?: string; + notEndsWithInsensitive?: string; + like?: string; + notLike?: string; + likeInsensitive?: string; + notLikeInsensitive?: string; +} +export interface IntFilter { + isNull?: boolean; + equalTo?: number; + notEqualTo?: number; + distinctFrom?: number; + notDistinctFrom?: number; + in?: number[]; + notIn?: number[]; + lessThan?: number; + lessThanOrEqualTo?: number; + greaterThan?: number; + greaterThanOrEqualTo?: number; +} +export interface FloatFilter { + isNull?: boolean; + equalTo?: number; + notEqualTo?: number; + distinctFrom?: number; + notDistinctFrom?: number; + in?: number[]; + notIn?: number[]; + lessThan?: number; + lessThanOrEqualTo?: number; + greaterThan?: number; + greaterThanOrEqualTo?: number; +} +export interface BooleanFilter { + isNull?: boolean; + equalTo?: boolean; + notEqualTo?: boolean; +} +export interface UUIDFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; + distinctFrom?: string; + notDistinctFrom?: string; + in?: string[]; + notIn?: string[]; +} +export interface DatetimeFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; + distinctFrom?: string; + notDistinctFrom?: string; + in?: string[]; + notIn?: string[]; + lessThan?: string; + lessThanOrEqualTo?: string; + greaterThan?: string; + greaterThanOrEqualTo?: string; +} +export interface DateFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; + distinctFrom?: string; + notDistinctFrom?: string; + in?: string[]; + notIn?: string[]; + lessThan?: string; + lessThanOrEqualTo?: string; + greaterThan?: string; + greaterThanOrEqualTo?: string; +} +export interface JSONFilter { + isNull?: boolean; + equalTo?: Record; + notEqualTo?: Record; + distinctFrom?: Record; + notDistinctFrom?: Record; + contains?: Record; + containedBy?: Record; + containsKey?: string; + containsAllKeys?: string[]; + containsAnyKeys?: string[]; +} +export interface BigIntFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; + distinctFrom?: string; + notDistinctFrom?: string; + in?: string[]; + notIn?: string[]; + lessThan?: string; + lessThanOrEqualTo?: string; + greaterThan?: string; + greaterThanOrEqualTo?: string; +} +export interface BigFloatFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; + distinctFrom?: string; + notDistinctFrom?: string; + in?: string[]; + notIn?: string[]; + lessThan?: string; + lessThanOrEqualTo?: string; + greaterThan?: string; + greaterThanOrEqualTo?: string; +} +export interface BitStringFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; +} +export interface InternetAddressFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; + distinctFrom?: string; + notDistinctFrom?: string; + in?: string[]; + notIn?: string[]; + lessThan?: string; + lessThanOrEqualTo?: string; + greaterThan?: string; + greaterThanOrEqualTo?: string; + contains?: string; + containsOrEqualTo?: string; + containedBy?: string; + containedByOrEqualTo?: string; + containsOrContainedBy?: string; +} +export interface FullTextFilter { + matches?: string; +} +export interface StringListFilter { + isNull?: boolean; + equalTo?: string[]; + notEqualTo?: string[]; + distinctFrom?: string[]; + notDistinctFrom?: string[]; + lessThan?: string[]; + lessThanOrEqualTo?: string[]; + greaterThan?: string[]; + greaterThanOrEqualTo?: string[]; + contains?: string[]; + containedBy?: string[]; + overlaps?: string[]; + anyEqualTo?: string; + anyNotEqualTo?: string; + anyLessThan?: string; + anyLessThanOrEqualTo?: string; + anyGreaterThan?: string; + anyGreaterThanOrEqualTo?: string; +} +export interface IntListFilter { + isNull?: boolean; + equalTo?: number[]; + notEqualTo?: number[]; + distinctFrom?: number[]; + notDistinctFrom?: number[]; + lessThan?: number[]; + lessThanOrEqualTo?: number[]; + greaterThan?: number[]; + greaterThanOrEqualTo?: number[]; + contains?: number[]; + containedBy?: number[]; + overlaps?: number[]; + anyEqualTo?: number; + anyNotEqualTo?: number; + anyLessThan?: number; + anyLessThanOrEqualTo?: number; + anyGreaterThan?: number; + anyGreaterThanOrEqualTo?: number; +} +export interface UUIDListFilter { + isNull?: boolean; + equalTo?: string[]; + notEqualTo?: string[]; + distinctFrom?: string[]; + notDistinctFrom?: string[]; + lessThan?: string[]; + lessThanOrEqualTo?: string[]; + greaterThan?: string[]; + greaterThanOrEqualTo?: string[]; + contains?: string[]; + containedBy?: string[]; + overlaps?: string[]; + anyEqualTo?: string; + anyNotEqualTo?: string; + anyLessThan?: string; + anyLessThanOrEqualTo?: string; + anyGreaterThan?: string; + anyGreaterThanOrEqualTo?: string; +} +// ============ Enum Types ============ +export type UserRole = 'ADMIN' | 'EDITOR' | 'VIEWER'; +// ============ Entity Types ============ +export interface User { + id: string; + email?: string | null; + name?: string | null; + bio?: string | null; + isActive?: boolean | null; + role?: UserRole | null; + metadata?: Record | null; + createdAt?: string | null; + updatedAt?: string | null; +} +export interface Post { + id: string; + title?: string | null; + body?: string | null; + slug?: string | null; + isPublished?: boolean | null; + publishedAt?: string | null; + authorId?: string | null; + categoryId?: string | null; + tags?: string | null; + createdAt?: string | null; + updatedAt?: string | null; +} +export interface Comment { + id: string; + body?: string | null; + authorId?: string | null; + postId?: string | null; + createdAt?: string | null; + updatedAt?: string | null; +} +export interface Category { + id: string; + name?: string | null; + slug?: string | null; + description?: string | null; + parentId?: string | null; + createdAt?: string | null; +} +// ============ Relation Helper Types ============ +export interface ConnectionResult { + nodes: T[]; + totalCount: number; + pageInfo: PageInfo; +} +export interface PageInfo { + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor?: string | null; + endCursor?: string | null; +} +// ============ Entity Relation Types ============ +export interface UserRelations { + posts?: ConnectionResult; + comments?: ConnectionResult; +} +export interface PostRelations { + author?: User | null; + category?: Category | null; + comments?: ConnectionResult; +} +export interface CommentRelations { + author?: User | null; + post?: Post | null; +} +export interface CategoryRelations { + parent?: Category | null; + posts?: ConnectionResult; +} +// ============ Entity Types With Relations ============ +export type UserWithRelations = User & UserRelations; +export type PostWithRelations = Post & PostRelations; +export type CommentWithRelations = Comment & CommentRelations; +export type CategoryWithRelations = Category & CategoryRelations; +// ============ Entity Select Types ============ +export type UserSelect = { + id?: boolean; + email?: boolean; + name?: boolean; + bio?: boolean; + isActive?: boolean; + role?: boolean; + metadata?: boolean; + createdAt?: boolean; + updatedAt?: boolean; + posts?: { + select: PostSelect; + first?: number; + filter?: PostFilter; + orderBy?: PostsOrderBy[]; + }; + comments?: { + select: CommentSelect; + first?: number; + filter?: CommentFilter; + orderBy?: CommentsOrderBy[]; + }; +}; +export type PostSelect = { + id?: boolean; + title?: boolean; + body?: boolean; + slug?: boolean; + isPublished?: boolean; + publishedAt?: boolean; + authorId?: boolean; + categoryId?: boolean; + tags?: boolean; + createdAt?: boolean; + updatedAt?: boolean; + author?: { + select: UserSelect; + }; + category?: { + select: CategorySelect; + }; + comments?: { + select: CommentSelect; + first?: number; + filter?: CommentFilter; + orderBy?: CommentsOrderBy[]; + }; +}; +export type CommentSelect = { + id?: boolean; + body?: boolean; + authorId?: boolean; + postId?: boolean; + createdAt?: boolean; + updatedAt?: boolean; + author?: { + select: UserSelect; + }; + post?: { + select: PostSelect; + }; +}; +export type CategorySelect = { + id?: boolean; + name?: boolean; + slug?: boolean; + description?: boolean; + parentId?: boolean; + createdAt?: boolean; + parent?: { + select: CategorySelect; + }; + posts?: { + select: PostSelect; + first?: number; + filter?: PostFilter; + orderBy?: PostsOrderBy[]; + }; +}; +// ============ Table Filter Types ============ +export interface UserFilter { + id?: UUIDFilter; + email?: StringFilter; + name?: StringFilter; + bio?: StringFilter; + isActive?: BooleanFilter; + role?: StringFilter; + metadata?: JSONFilter; + createdAt?: DatetimeFilter; + updatedAt?: DatetimeFilter; + and?: UserFilter[]; + or?: UserFilter[]; + not?: UserFilter; +} +export interface PostFilter { + id?: UUIDFilter; + title?: StringFilter; + body?: StringFilter; + slug?: StringFilter; + isPublished?: BooleanFilter; + publishedAt?: DatetimeFilter; + authorId?: UUIDFilter; + categoryId?: UUIDFilter; + tags?: StringFilter; + createdAt?: DatetimeFilter; + updatedAt?: DatetimeFilter; + and?: PostFilter[]; + or?: PostFilter[]; + not?: PostFilter; +} +export interface CommentFilter { + id?: UUIDFilter; + body?: StringFilter; + authorId?: UUIDFilter; + postId?: UUIDFilter; + createdAt?: DatetimeFilter; + updatedAt?: DatetimeFilter; + and?: CommentFilter[]; + or?: CommentFilter[]; + not?: CommentFilter; +} +export interface CategoryFilter { + id?: UUIDFilter; + name?: StringFilter; + slug?: StringFilter; + description?: StringFilter; + parentId?: UUIDFilter; + createdAt?: DatetimeFilter; + and?: CategoryFilter[]; + or?: CategoryFilter[]; + not?: CategoryFilter; +} +// ============ Table Condition Types ============ +export interface UserCondition { + id?: string | null; + email?: string | null; + name?: string | null; + bio?: string | null; + isActive?: boolean | null; + role?: unknown | null; + metadata?: unknown | null; + createdAt?: string | null; + updatedAt?: string | null; +} +export interface PostCondition { + id?: string | null; + title?: string | null; + body?: string | null; + slug?: string | null; + isPublished?: boolean | null; + publishedAt?: string | null; + authorId?: string | null; + categoryId?: string | null; + tags?: string | null; + createdAt?: string | null; + updatedAt?: string | null; +} +export interface CommentCondition { + id?: string | null; + body?: string | null; + authorId?: string | null; + postId?: string | null; + createdAt?: string | null; + updatedAt?: string | null; +} +export interface CategoryCondition { + id?: string | null; + name?: string | null; + slug?: string | null; + description?: string | null; + parentId?: string | null; + createdAt?: string | null; +} +// ============ OrderBy Types ============ +export type UsersOrderBy = + | 'PRIMARY_KEY_ASC' + | 'PRIMARY_KEY_DESC' + | 'NATURAL' + | 'ID_ASC' + | 'ID_DESC' + | 'EMAIL_ASC' + | 'EMAIL_DESC' + | 'NAME_ASC' + | 'NAME_DESC' + | 'BIO_ASC' + | 'BIO_DESC' + | 'IS_ACTIVE_ASC' + | 'IS_ACTIVE_DESC' + | 'ROLE_ASC' + | 'ROLE_DESC' + | 'METADATA_ASC' + | 'METADATA_DESC' + | 'CREATED_AT_ASC' + | 'CREATED_AT_DESC' + | 'UPDATED_AT_ASC' + | 'UPDATED_AT_DESC'; +export type PostsOrderBy = + | 'PRIMARY_KEY_ASC' + | 'PRIMARY_KEY_DESC' + | 'NATURAL' + | 'ID_ASC' + | 'ID_DESC' + | 'TITLE_ASC' + | 'TITLE_DESC' + | 'BODY_ASC' + | 'BODY_DESC' + | 'SLUG_ASC' + | 'SLUG_DESC' + | 'IS_PUBLISHED_ASC' + | 'IS_PUBLISHED_DESC' + | 'PUBLISHED_AT_ASC' + | 'PUBLISHED_AT_DESC' + | 'AUTHOR_ID_ASC' + | 'AUTHOR_ID_DESC' + | 'CATEGORY_ID_ASC' + | 'CATEGORY_ID_DESC' + | 'TAGS_ASC' + | 'TAGS_DESC' + | 'CREATED_AT_ASC' + | 'CREATED_AT_DESC' + | 'UPDATED_AT_ASC' + | 'UPDATED_AT_DESC'; +export type CommentsOrderBy = + | 'PRIMARY_KEY_ASC' + | 'PRIMARY_KEY_DESC' + | 'NATURAL' + | 'ID_ASC' + | 'ID_DESC' + | 'BODY_ASC' + | 'BODY_DESC' + | 'AUTHOR_ID_ASC' + | 'AUTHOR_ID_DESC' + | 'POST_ID_ASC' + | 'POST_ID_DESC' + | 'CREATED_AT_ASC' + | 'CREATED_AT_DESC' + | 'UPDATED_AT_ASC' + | 'UPDATED_AT_DESC'; +export type CategoriesOrderBy = + | 'PRIMARY_KEY_ASC' + | 'PRIMARY_KEY_DESC' + | 'NATURAL' + | 'ID_ASC' + | 'ID_DESC' + | 'NAME_ASC' + | 'NAME_DESC' + | 'SLUG_ASC' + | 'SLUG_DESC' + | 'DESCRIPTION_ASC' + | 'DESCRIPTION_DESC' + | 'PARENT_ID_ASC' + | 'PARENT_ID_DESC' + | 'CREATED_AT_ASC' + | 'CREATED_AT_DESC'; +// ============ CRUD Input Types ============ +export interface CreateUserInput { + clientMutationId?: string; + user: { + email: string; + name?: string; + bio?: string; + isActive?: boolean; + role?: UserRole; + metadata?: Record; + }; +} +export interface UserPatch { + email?: string | null; + name?: string | null; + bio?: string | null; + isActive?: boolean | null; + role?: UserRole | null; + metadata?: Record | null; +} +export interface UpdateUserInput { + clientMutationId?: string; + id: string; + patch: UserPatch; +} +export interface DeleteUserInput { + clientMutationId?: string; + id: string; +} +export interface CreatePostInput { + clientMutationId?: string; + post: { + title: string; + body?: string; + slug: string; + isPublished?: boolean; + publishedAt?: string; + authorId: string; + categoryId?: string; + tags?: string[]; + }; +} +export interface PostPatch { + title?: string | null; + body?: string | null; + slug?: string | null; + isPublished?: boolean | null; + publishedAt?: string | null; + authorId?: string | null; + categoryId?: string | null; + tags?: string | null; +} +export interface UpdatePostInput { + clientMutationId?: string; + id: string; + patch: PostPatch; +} +export interface DeletePostInput { + clientMutationId?: string; + id: string; +} +export interface CreateCommentInput { + clientMutationId?: string; + comment: { + body: string; + authorId: string; + postId: string; + }; +} +export interface CommentPatch { + body?: string | null; + authorId?: string | null; + postId?: string | null; +} +export interface UpdateCommentInput { + clientMutationId?: string; + id: string; + patch: CommentPatch; +} +export interface DeleteCommentInput { + clientMutationId?: string; + id: string; +} +export interface CreateCategoryInput { + clientMutationId?: string; + category: { + name: string; + slug: string; + description?: string; + parentId?: string; + }; +} +export interface CategoryPatch { + name?: string | null; + slug?: string | null; + description?: string | null; + parentId?: string | null; +} +export interface UpdateCategoryInput { + clientMutationId?: string; + id: string; + patch: CategoryPatch; +} +export interface DeleteCategoryInput { + clientMutationId?: string; + id: string; +} +// ============ Connection Fields Map ============ +export const connectionFieldsMap = { + User: { + posts: 'Post', + comments: 'Comment', + }, + Post: { + comments: 'Comment', + }, + Category: { + posts: 'Post', + }, +} as Record>; +// ============ Payload/Return Types (for custom operations) ============ +export interface CreateUserPayload { + user?: User | null; +} +export type CreateUserPayloadSelect = { + user?: { + select: UserSelect; + }; +}; +export interface UpdateUserPayload { + user?: User | null; +} +export type UpdateUserPayloadSelect = { + user?: { + select: UserSelect; + }; +}; +export interface DeleteUserPayload { + user?: User | null; +} +export type DeleteUserPayloadSelect = { + user?: { + select: UserSelect; + }; +}; +export interface CreatePostPayload { + post?: Post | null; +} +export type CreatePostPayloadSelect = { + post?: { + select: PostSelect; + }; +}; +export interface UpdatePostPayload { + post?: Post | null; +} +export type UpdatePostPayloadSelect = { + post?: { + select: PostSelect; + }; +}; +export interface DeletePostPayload { + post?: Post | null; +} +export type DeletePostPayloadSelect = { + post?: { + select: PostSelect; + }; +}; +export interface CreateCommentPayload { + comment?: Comment | null; +} +export type CreateCommentPayloadSelect = { + comment?: { + select: CommentSelect; + }; +}; +export interface UpdateCommentPayload { + comment?: Comment | null; +} +export type UpdateCommentPayloadSelect = { + comment?: { + select: CommentSelect; + }; +}; +export interface DeleteCommentPayload { + comment?: Comment | null; +} +export type DeleteCommentPayloadSelect = { + comment?: { + select: CommentSelect; + }; +}; +export interface CreateCategoryPayload { + category?: Category | null; +} +export type CreateCategoryPayloadSelect = { + category?: { + select: CategorySelect; + }; +}; +export interface UpdateCategoryPayload { + category?: Category | null; +} +export type UpdateCategoryPayloadSelect = { + category?: { + select: CategorySelect; + }; +}; +export interface DeleteCategoryPayload { + category?: Category | null; +} +export type DeleteCategoryPayloadSelect = { + category?: { + select: CategorySelect; + }; +}; diff --git a/__fixtures__/codegen-example/output/orm/models/category.ts b/__fixtures__/codegen-example/output/orm/models/category.ts new file mode 100644 index 000000000..1568cb0fe --- /dev/null +++ b/__fixtures__/codegen-example/output/orm/models/category.ts @@ -0,0 +1,236 @@ +/** + * Category model for ORM client + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +import { OrmClient } from '../client'; +import { + QueryBuilder, + buildFindManyDocument, + buildFindFirstDocument, + buildFindOneDocument, + buildCreateDocument, + buildUpdateByPkDocument, + buildDeleteByPkDocument, +} from '../query-builder'; +import type { + ConnectionResult, + FindManyArgs, + FindFirstArgs, + CreateArgs, + UpdateArgs, + DeleteArgs, + InferSelectResult, + StrictSelect, +} from '../select-types'; +import type { + Category, + CategoryWithRelations, + CategorySelect, + CategoryFilter, + CategoriesOrderBy, + CreateCategoryInput, + UpdateCategoryInput, + CategoryPatch, +} from '../input-types'; +import { connectionFieldsMap } from '../input-types'; +export class CategoryModel { + constructor(private client: OrmClient) {} + findMany( + args: FindManyArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + categories: ConnectionResult>; + }> { + const { document, variables } = buildFindManyDocument( + 'Category', + 'categories', + args.select, + { + where: args?.where, + orderBy: args?.orderBy as string[] | undefined, + first: args?.first, + last: args?.last, + after: args?.after, + before: args?.before, + offset: args?.offset, + }, + 'CategoryFilter', + 'CategoriesOrderBy', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'Category', + fieldName: 'categories', + document, + variables, + }); + } + findFirst( + args: FindFirstArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + categories: { + nodes: InferSelectResult[]; + }; + }> { + const { document, variables } = buildFindFirstDocument( + 'Category', + 'categories', + args.select, + { + where: args?.where, + }, + 'CategoryFilter', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'Category', + fieldName: 'categories', + document, + variables, + }); + } + findOne( + args: { + id: string; + select: S; + } & StrictSelect + ): QueryBuilder<{ + category: InferSelectResult | null; + }> { + const { document, variables } = buildFindManyDocument( + 'Category', + 'categories', + args.select, + { + where: { + id: { + equalTo: args.id, + }, + }, + first: 1, + }, + 'CategoryFilter', + 'CategoriesOrderBy', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'Category', + fieldName: 'category', + document, + variables, + transform: (data: { + categories?: { + nodes?: InferSelectResult[]; + }; + }) => ({ + category: data.categories?.nodes?.[0] ?? null, + }), + }); + } + create( + args: CreateArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + createCategory: { + category: InferSelectResult; + }; + }> { + const { document, variables } = buildCreateDocument( + 'Category', + 'createCategory', + 'category', + args.select, + args.data, + 'CreateCategoryInput', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'Category', + fieldName: 'createCategory', + document, + variables, + }); + } + update( + args: UpdateArgs< + S, + { + id: string; + }, + CategoryPatch + > & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + updateCategory: { + category: InferSelectResult; + }; + }> { + const { document, variables } = buildUpdateByPkDocument( + 'Category', + 'updateCategory', + 'category', + args.select, + args.where.id, + args.data, + 'UpdateCategoryInput', + 'id', + 'patch', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'Category', + fieldName: 'updateCategory', + document, + variables, + }); + } + delete( + args: DeleteArgs< + { + id: string; + }, + S + > & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + deleteCategory: { + category: InferSelectResult; + }; + }> { + const { document, variables } = buildDeleteByPkDocument( + 'Category', + 'deleteCategory', + 'category', + args.where.id, + 'DeleteCategoryInput', + 'id', + args.select, + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'Category', + fieldName: 'deleteCategory', + document, + variables, + }); + } +} diff --git a/__fixtures__/codegen-example/output/orm/models/comment.ts b/__fixtures__/codegen-example/output/orm/models/comment.ts new file mode 100644 index 000000000..36149fd66 --- /dev/null +++ b/__fixtures__/codegen-example/output/orm/models/comment.ts @@ -0,0 +1,236 @@ +/** + * Comment model for ORM client + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +import { OrmClient } from '../client'; +import { + QueryBuilder, + buildFindManyDocument, + buildFindFirstDocument, + buildFindOneDocument, + buildCreateDocument, + buildUpdateByPkDocument, + buildDeleteByPkDocument, +} from '../query-builder'; +import type { + ConnectionResult, + FindManyArgs, + FindFirstArgs, + CreateArgs, + UpdateArgs, + DeleteArgs, + InferSelectResult, + StrictSelect, +} from '../select-types'; +import type { + Comment, + CommentWithRelations, + CommentSelect, + CommentFilter, + CommentsOrderBy, + CreateCommentInput, + UpdateCommentInput, + CommentPatch, +} from '../input-types'; +import { connectionFieldsMap } from '../input-types'; +export class CommentModel { + constructor(private client: OrmClient) {} + findMany( + args: FindManyArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + comments: ConnectionResult>; + }> { + const { document, variables } = buildFindManyDocument( + 'Comment', + 'comments', + args.select, + { + where: args?.where, + orderBy: args?.orderBy as string[] | undefined, + first: args?.first, + last: args?.last, + after: args?.after, + before: args?.before, + offset: args?.offset, + }, + 'CommentFilter', + 'CommentsOrderBy', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'Comment', + fieldName: 'comments', + document, + variables, + }); + } + findFirst( + args: FindFirstArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + comments: { + nodes: InferSelectResult[]; + }; + }> { + const { document, variables } = buildFindFirstDocument( + 'Comment', + 'comments', + args.select, + { + where: args?.where, + }, + 'CommentFilter', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'Comment', + fieldName: 'comments', + document, + variables, + }); + } + findOne( + args: { + id: string; + select: S; + } & StrictSelect + ): QueryBuilder<{ + comment: InferSelectResult | null; + }> { + const { document, variables } = buildFindManyDocument( + 'Comment', + 'comments', + args.select, + { + where: { + id: { + equalTo: args.id, + }, + }, + first: 1, + }, + 'CommentFilter', + 'CommentsOrderBy', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'Comment', + fieldName: 'comment', + document, + variables, + transform: (data: { + comments?: { + nodes?: InferSelectResult[]; + }; + }) => ({ + comment: data.comments?.nodes?.[0] ?? null, + }), + }); + } + create( + args: CreateArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + createComment: { + comment: InferSelectResult; + }; + }> { + const { document, variables } = buildCreateDocument( + 'Comment', + 'createComment', + 'comment', + args.select, + args.data, + 'CreateCommentInput', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'Comment', + fieldName: 'createComment', + document, + variables, + }); + } + update( + args: UpdateArgs< + S, + { + id: string; + }, + CommentPatch + > & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + updateComment: { + comment: InferSelectResult; + }; + }> { + const { document, variables } = buildUpdateByPkDocument( + 'Comment', + 'updateComment', + 'comment', + args.select, + args.where.id, + args.data, + 'UpdateCommentInput', + 'id', + 'patch', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'Comment', + fieldName: 'updateComment', + document, + variables, + }); + } + delete( + args: DeleteArgs< + { + id: string; + }, + S + > & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + deleteComment: { + comment: InferSelectResult; + }; + }> { + const { document, variables } = buildDeleteByPkDocument( + 'Comment', + 'deleteComment', + 'comment', + args.where.id, + 'DeleteCommentInput', + 'id', + args.select, + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'Comment', + fieldName: 'deleteComment', + document, + variables, + }); + } +} diff --git a/__fixtures__/codegen-example/output/orm/models/index.ts b/__fixtures__/codegen-example/output/orm/models/index.ts new file mode 100644 index 000000000..296a866f0 --- /dev/null +++ b/__fixtures__/codegen-example/output/orm/models/index.ts @@ -0,0 +1,9 @@ +/** + * Models barrel export + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +export { UserModel } from './user'; +export { PostModel } from './post'; +export { CommentModel } from './comment'; +export { CategoryModel } from './category'; diff --git a/__fixtures__/codegen-example/output/orm/models/post.ts b/__fixtures__/codegen-example/output/orm/models/post.ts new file mode 100644 index 000000000..6d8b2ffaa --- /dev/null +++ b/__fixtures__/codegen-example/output/orm/models/post.ts @@ -0,0 +1,236 @@ +/** + * Post model for ORM client + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +import { OrmClient } from '../client'; +import { + QueryBuilder, + buildFindManyDocument, + buildFindFirstDocument, + buildFindOneDocument, + buildCreateDocument, + buildUpdateByPkDocument, + buildDeleteByPkDocument, +} from '../query-builder'; +import type { + ConnectionResult, + FindManyArgs, + FindFirstArgs, + CreateArgs, + UpdateArgs, + DeleteArgs, + InferSelectResult, + StrictSelect, +} from '../select-types'; +import type { + Post, + PostWithRelations, + PostSelect, + PostFilter, + PostsOrderBy, + CreatePostInput, + UpdatePostInput, + PostPatch, +} from '../input-types'; +import { connectionFieldsMap } from '../input-types'; +export class PostModel { + constructor(private client: OrmClient) {} + findMany( + args: FindManyArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + posts: ConnectionResult>; + }> { + const { document, variables } = buildFindManyDocument( + 'Post', + 'posts', + args.select, + { + where: args?.where, + orderBy: args?.orderBy as string[] | undefined, + first: args?.first, + last: args?.last, + after: args?.after, + before: args?.before, + offset: args?.offset, + }, + 'PostFilter', + 'PostsOrderBy', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'Post', + fieldName: 'posts', + document, + variables, + }); + } + findFirst( + args: FindFirstArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + posts: { + nodes: InferSelectResult[]; + }; + }> { + const { document, variables } = buildFindFirstDocument( + 'Post', + 'posts', + args.select, + { + where: args?.where, + }, + 'PostFilter', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'Post', + fieldName: 'posts', + document, + variables, + }); + } + findOne( + args: { + id: string; + select: S; + } & StrictSelect + ): QueryBuilder<{ + post: InferSelectResult | null; + }> { + const { document, variables } = buildFindManyDocument( + 'Post', + 'posts', + args.select, + { + where: { + id: { + equalTo: args.id, + }, + }, + first: 1, + }, + 'PostFilter', + 'PostsOrderBy', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'Post', + fieldName: 'post', + document, + variables, + transform: (data: { + posts?: { + nodes?: InferSelectResult[]; + }; + }) => ({ + post: data.posts?.nodes?.[0] ?? null, + }), + }); + } + create( + args: CreateArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + createPost: { + post: InferSelectResult; + }; + }> { + const { document, variables } = buildCreateDocument( + 'Post', + 'createPost', + 'post', + args.select, + args.data, + 'CreatePostInput', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'Post', + fieldName: 'createPost', + document, + variables, + }); + } + update( + args: UpdateArgs< + S, + { + id: string; + }, + PostPatch + > & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + updatePost: { + post: InferSelectResult; + }; + }> { + const { document, variables } = buildUpdateByPkDocument( + 'Post', + 'updatePost', + 'post', + args.select, + args.where.id, + args.data, + 'UpdatePostInput', + 'id', + 'patch', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'Post', + fieldName: 'updatePost', + document, + variables, + }); + } + delete( + args: DeleteArgs< + { + id: string; + }, + S + > & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + deletePost: { + post: InferSelectResult; + }; + }> { + const { document, variables } = buildDeleteByPkDocument( + 'Post', + 'deletePost', + 'post', + args.where.id, + 'DeletePostInput', + 'id', + args.select, + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'Post', + fieldName: 'deletePost', + document, + variables, + }); + } +} diff --git a/__fixtures__/codegen-example/output/orm/models/user.ts b/__fixtures__/codegen-example/output/orm/models/user.ts new file mode 100644 index 000000000..46d7661e3 --- /dev/null +++ b/__fixtures__/codegen-example/output/orm/models/user.ts @@ -0,0 +1,236 @@ +/** + * User model for ORM client + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +import { OrmClient } from '../client'; +import { + QueryBuilder, + buildFindManyDocument, + buildFindFirstDocument, + buildFindOneDocument, + buildCreateDocument, + buildUpdateByPkDocument, + buildDeleteByPkDocument, +} from '../query-builder'; +import type { + ConnectionResult, + FindManyArgs, + FindFirstArgs, + CreateArgs, + UpdateArgs, + DeleteArgs, + InferSelectResult, + StrictSelect, +} from '../select-types'; +import type { + User, + UserWithRelations, + UserSelect, + UserFilter, + UsersOrderBy, + CreateUserInput, + UpdateUserInput, + UserPatch, +} from '../input-types'; +import { connectionFieldsMap } from '../input-types'; +export class UserModel { + constructor(private client: OrmClient) {} + findMany( + args: FindManyArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + users: ConnectionResult>; + }> { + const { document, variables } = buildFindManyDocument( + 'User', + 'users', + args.select, + { + where: args?.where, + orderBy: args?.orderBy as string[] | undefined, + first: args?.first, + last: args?.last, + after: args?.after, + before: args?.before, + offset: args?.offset, + }, + 'UserFilter', + 'UsersOrderBy', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'User', + fieldName: 'users', + document, + variables, + }); + } + findFirst( + args: FindFirstArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + users: { + nodes: InferSelectResult[]; + }; + }> { + const { document, variables } = buildFindFirstDocument( + 'User', + 'users', + args.select, + { + where: args?.where, + }, + 'UserFilter', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'User', + fieldName: 'users', + document, + variables, + }); + } + findOne( + args: { + id: string; + select: S; + } & StrictSelect + ): QueryBuilder<{ + user: InferSelectResult | null; + }> { + const { document, variables } = buildFindManyDocument( + 'User', + 'users', + args.select, + { + where: { + id: { + equalTo: args.id, + }, + }, + first: 1, + }, + 'UserFilter', + 'UsersOrderBy', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'User', + fieldName: 'user', + document, + variables, + transform: (data: { + users?: { + nodes?: InferSelectResult[]; + }; + }) => ({ + user: data.users?.nodes?.[0] ?? null, + }), + }); + } + create( + args: CreateArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + createUser: { + user: InferSelectResult; + }; + }> { + const { document, variables } = buildCreateDocument( + 'User', + 'createUser', + 'user', + args.select, + args.data, + 'CreateUserInput', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'User', + fieldName: 'createUser', + document, + variables, + }); + } + update( + args: UpdateArgs< + S, + { + id: string; + }, + UserPatch + > & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + updateUser: { + user: InferSelectResult; + }; + }> { + const { document, variables } = buildUpdateByPkDocument( + 'User', + 'updateUser', + 'user', + args.select, + args.where.id, + args.data, + 'UpdateUserInput', + 'id', + 'patch', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'User', + fieldName: 'updateUser', + document, + variables, + }); + } + delete( + args: DeleteArgs< + { + id: string; + }, + S + > & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + deleteUser: { + user: InferSelectResult; + }; + }> { + const { document, variables } = buildDeleteByPkDocument( + 'User', + 'deleteUser', + 'user', + args.where.id, + 'DeleteUserInput', + 'id', + args.select, + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'User', + fieldName: 'deleteUser', + document, + variables, + }); + } +} diff --git a/__fixtures__/codegen-example/output/orm/query-builder.ts b/__fixtures__/codegen-example/output/orm/query-builder.ts new file mode 100644 index 000000000..67c3992b5 --- /dev/null +++ b/__fixtures__/codegen-example/output/orm/query-builder.ts @@ -0,0 +1,847 @@ +/** + * Query Builder - Builds and executes GraphQL operations + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +import { parseType, print } from '@0no-co/graphql.web'; +import * as t from 'gql-ast'; +import type { ArgumentNode, EnumValueNode, FieldNode, VariableDefinitionNode } from 'graphql'; + +import { GraphQLRequestError, OrmClient, QueryResult } from './client'; + +export interface QueryBuilderConfig { + client: OrmClient; + operation: 'query' | 'mutation'; + operationName: string; + fieldName: string; + document: string; + variables?: Record; + transform?: (data: any) => TResult; +} + +export class QueryBuilder { + private config: QueryBuilderConfig; + + constructor(config: QueryBuilderConfig) { + this.config = config; + } + + /** + * Execute the query and return a discriminated union result + * Use result.ok to check success, or .unwrap() to throw on error + */ + async execute(): Promise> { + const rawResult = await this.config.client.execute( + this.config.document, + this.config.variables + ); + if (!rawResult.ok) { + return rawResult; + } + if (!this.config.transform) { + return rawResult as unknown as QueryResult; + } + return { + ok: true, + data: this.config.transform(rawResult.data), + errors: undefined, + }; + } + + /** + * Execute and unwrap the result, throwing GraphQLRequestError on failure + * @throws {GraphQLRequestError} If the query returns errors + */ + async unwrap(): Promise { + const result = await this.execute(); + if (!result.ok) { + throw new GraphQLRequestError(result.errors, result.data); + } + return result.data; + } + + /** + * Execute and unwrap, returning defaultValue on error instead of throwing + */ + async unwrapOr(defaultValue: D): Promise { + const result = await this.execute(); + if (!result.ok) { + return defaultValue; + } + return result.data; + } + + /** + * Execute and unwrap, calling onError callback on failure + */ + async unwrapOrElse( + onError: (errors: import('./client').GraphQLError[]) => D + ): Promise { + const result = await this.execute(); + if (!result.ok) { + return onError(result.errors); + } + return result.data; + } + + toGraphQL(): string { + return this.config.document; + } + + getVariables(): Record | undefined { + return this.config.variables; + } +} + +const OP_QUERY = 'query' as unknown as import('graphql').OperationTypeNode; +const OP_MUTATION = 'mutation' as unknown as import('graphql').OperationTypeNode; +const ENUM_VALUE_KIND = 'EnumValue' as unknown as EnumValueNode['kind']; + +// ============================================================================ +// Selection Builders +// ============================================================================ + +export function buildSelections( + select: Record | undefined, + connectionFieldsMap?: Record>, + entityType?: string +): FieldNode[] { + if (!select) { + return []; + } + + const fields: FieldNode[] = []; + const entityConnections = entityType ? connectionFieldsMap?.[entityType] : undefined; + + for (const [key, value] of Object.entries(select)) { + if (value === false || value === undefined) { + continue; + } + + if (value === true) { + fields.push(t.field({ name: key })); + continue; + } + + if (typeof value === 'object' && value !== null) { + const nested = value as { + select?: Record; + first?: number; + filter?: Record; + orderBy?: string[]; + connection?: boolean; + }; + + if (!nested.select || typeof nested.select !== 'object') { + throw new Error( + `Invalid selection for field "${key}": nested selections must include a "select" object.` + ); + } + + const relatedEntityType = entityConnections?.[key]; + const nestedSelections = buildSelections( + nested.select, + connectionFieldsMap, + relatedEntityType + ); + const isConnection = + nested.connection === true || + nested.first !== undefined || + nested.filter !== undefined || + relatedEntityType !== undefined; + const args = buildArgs([ + buildOptionalArg('first', nested.first), + nested.filter + ? t.argument({ + name: 'filter', + value: buildValueAst(nested.filter), + }) + : null, + buildEnumListArg('orderBy', nested.orderBy), + ]); + + if (isConnection) { + fields.push( + t.field({ + name: key, + args, + selectionSet: t.selectionSet({ + selections: buildConnectionSelections(nestedSelections), + }), + }) + ); + } else { + fields.push( + t.field({ + name: key, + args, + selectionSet: t.selectionSet({ selections: nestedSelections }), + }) + ); + } + } + } + + return fields; +} + +// ============================================================================ +// Document Builders +// ============================================================================ + +export function buildFindManyDocument( + operationName: string, + queryField: string, + select: TSelect, + args: { + where?: TWhere; + orderBy?: string[]; + first?: number; + last?: number; + after?: string; + before?: string; + offset?: number; + }, + filterTypeName: string, + orderByTypeName: string, + connectionFieldsMap?: Record> +): { document: string; variables: Record } { + const selections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + const variableDefinitions: VariableDefinitionNode[] = []; + const queryArgs: ArgumentNode[] = []; + const variables: Record = {}; + + addVariable( + { + varName: 'where', + argName: 'filter', + typeName: filterTypeName, + value: args.where, + }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { + varName: 'orderBy', + typeName: '[' + orderByTypeName + '!]', + value: args.orderBy?.length ? args.orderBy : undefined, + }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { varName: 'first', typeName: 'Int', value: args.first }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { varName: 'last', typeName: 'Int', value: args.last }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { varName: 'after', typeName: 'Cursor', value: args.after }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { varName: 'before', typeName: 'Cursor', value: args.before }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { varName: 'offset', typeName: 'Int', value: args.offset }, + variableDefinitions, + queryArgs, + variables + ); + + const document = t.document({ + definitions: [ + t.operationDefinition({ + operation: OP_QUERY, + name: operationName + 'Query', + variableDefinitions: variableDefinitions.length ? variableDefinitions : undefined, + selectionSet: t.selectionSet({ + selections: [ + t.field({ + name: queryField, + args: queryArgs.length ? queryArgs : undefined, + selectionSet: t.selectionSet({ + selections: buildConnectionSelections(selections), + }), + }), + ], + }), + }), + ], + }); + + return { document: print(document), variables }; +} + +export function buildFindFirstDocument( + operationName: string, + queryField: string, + select: TSelect, + args: { where?: TWhere }, + filterTypeName: string, + connectionFieldsMap?: Record> +): { document: string; variables: Record } { + const selections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + const variableDefinitions: VariableDefinitionNode[] = []; + const queryArgs: ArgumentNode[] = []; + const variables: Record = {}; + + // Always add first: 1 for findFirst + addVariable( + { varName: 'first', typeName: 'Int', value: 1 }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { + varName: 'where', + argName: 'filter', + typeName: filterTypeName, + value: args.where, + }, + variableDefinitions, + queryArgs, + variables + ); + + const document = t.document({ + definitions: [ + t.operationDefinition({ + operation: OP_QUERY, + name: operationName + 'Query', + variableDefinitions, + selectionSet: t.selectionSet({ + selections: [ + t.field({ + name: queryField, + args: queryArgs, + selectionSet: t.selectionSet({ + selections: [ + t.field({ + name: 'nodes', + selectionSet: t.selectionSet({ selections }), + }), + ], + }), + }), + ], + }), + }), + ], + }); + + return { document: print(document), variables }; +} + +export function buildCreateDocument( + operationName: string, + mutationField: string, + entityField: string, + select: TSelect, + data: TData, + inputTypeName: string, + connectionFieldsMap?: Record> +): { document: string; variables: Record } { + const selections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + return { + document: buildInputMutationDocument({ + operationName, + mutationField, + inputTypeName, + resultSelections: [ + t.field({ + name: entityField, + selectionSet: t.selectionSet({ selections }), + }), + ], + }), + variables: { + input: { + [entityField]: data, + }, + }, + }; +} + +export function buildUpdateDocument( + operationName: string, + mutationField: string, + entityField: string, + select: TSelect, + where: TWhere, + data: TData, + inputTypeName: string, + patchFieldName: string, + connectionFieldsMap?: Record> +): { document: string; variables: Record } { + const selections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + return { + document: buildInputMutationDocument({ + operationName, + mutationField, + inputTypeName, + resultSelections: [ + t.field({ + name: entityField, + selectionSet: t.selectionSet({ selections }), + }), + ], + }), + variables: { + input: { + id: where.id, + [patchFieldName]: data, + }, + }, + }; +} + +export function buildUpdateByPkDocument( + operationName: string, + mutationField: string, + entityField: string, + select: TSelect, + id: string | number, + data: TData, + inputTypeName: string, + idFieldName: string, + patchFieldName: string, + connectionFieldsMap?: Record> +): { document: string; variables: Record } { + const selections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + return { + document: buildInputMutationDocument({ + operationName, + mutationField, + inputTypeName, + resultSelections: [ + t.field({ + name: entityField, + selectionSet: t.selectionSet({ selections }), + }), + ], + }), + variables: { + input: { + [idFieldName]: id, + [patchFieldName]: data, + }, + }, + }; +} + +export function buildFindOneDocument( + operationName: string, + queryField: string, + id: string | number, + select: TSelect, + idArgName: string, + idTypeName: string, + connectionFieldsMap?: Record> +): { document: string; variables: Record } { + const selections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + const variableDefinitions: VariableDefinitionNode[] = [ + t.variableDefinition({ + variable: t.variable({ name: idArgName }), + type: parseType(idTypeName), + }), + ]; + + const queryArgs: ArgumentNode[] = [ + t.argument({ + name: idArgName, + value: t.variable({ name: idArgName }), + }), + ]; + + const document = t.document({ + definitions: [ + t.operationDefinition({ + operation: OP_QUERY, + name: operationName + 'Query', + variableDefinitions, + selectionSet: t.selectionSet({ + selections: [ + t.field({ + name: queryField, + args: queryArgs, + selectionSet: t.selectionSet({ selections }), + }), + ], + }), + }), + ], + }); + + return { + document: print(document), + variables: { [idArgName]: id }, + }; +} + +export function buildDeleteDocument( + operationName: string, + mutationField: string, + entityField: string, + where: TWhere, + inputTypeName: string, + select?: TSelect, + connectionFieldsMap?: Record> +): { document: string; variables: Record } { + const entitySelections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + return { + document: buildInputMutationDocument({ + operationName, + mutationField, + inputTypeName, + resultSelections: [ + t.field({ + name: entityField, + selectionSet: t.selectionSet({ + selections: entitySelections, + }), + }), + ], + }), + variables: { + input: { + id: where.id, + }, + }, + }; +} + +export function buildDeleteByPkDocument( + operationName: string, + mutationField: string, + entityField: string, + id: string | number, + inputTypeName: string, + idFieldName: string, + select?: TSelect, + connectionFieldsMap?: Record> +): { document: string; variables: Record } { + const entitySelections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + return { + document: buildInputMutationDocument({ + operationName, + mutationField, + inputTypeName, + resultSelections: [ + t.field({ + name: entityField, + selectionSet: t.selectionSet({ selections: entitySelections }), + }), + ], + }), + variables: { + input: { + [idFieldName]: id, + }, + }, + }; +} + +export function buildCustomDocument( + operationType: 'query' | 'mutation', + operationName: string, + fieldName: string, + select: TSelect, + args: TArgs, + variableDefinitions: Array<{ name: string; type: string }>, + connectionFieldsMap?: Record>, + entityType?: string +): { document: string; variables: Record } { + let actualSelect: TSelect = select; + let isConnection = false; + + if (isCustomSelectionWrapper(select)) { + actualSelect = select.select as TSelect; + isConnection = select.connection === true; + } + + const selections = actualSelect + ? buildSelections(actualSelect as Record, connectionFieldsMap, entityType) + : []; + + const variableDefs = variableDefinitions.map((definition) => + t.variableDefinition({ + variable: t.variable({ name: definition.name }), + type: parseType(definition.type), + }) + ); + const fieldArgs = variableDefinitions.map((definition) => + t.argument({ + name: definition.name, + value: t.variable({ name: definition.name }), + }) + ); + + const fieldSelections = isConnection ? buildConnectionSelections(selections) : selections; + + const document = t.document({ + definitions: [ + t.operationDefinition({ + operation: operationType === 'mutation' ? OP_MUTATION : OP_QUERY, + name: operationName, + variableDefinitions: variableDefs.length ? variableDefs : undefined, + selectionSet: t.selectionSet({ + selections: [ + t.field({ + name: fieldName, + args: fieldArgs.length ? fieldArgs : undefined, + selectionSet: fieldSelections.length + ? t.selectionSet({ selections: fieldSelections }) + : undefined, + }), + ], + }), + }), + ], + }); + + return { + document: print(document), + variables: (args ?? {}) as Record, + }; +} + +function isCustomSelectionWrapper( + value: unknown +): value is { select: Record; connection?: boolean } { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return false; + } + + const record = value as Record; + const keys = Object.keys(record); + + if (!keys.includes('select') || !keys.includes('connection')) { + return false; + } + + if (keys.some((key) => key !== 'select' && key !== 'connection')) { + return false; + } + + return !!record.select && typeof record.select === 'object' && !Array.isArray(record.select); +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +function buildArgs(args: Array): ArgumentNode[] { + return args.filter((arg): arg is ArgumentNode => arg !== null); +} + +function buildOptionalArg(name: string, value: number | string | undefined): ArgumentNode | null { + if (value === undefined) { + return null; + } + const valueNode = + typeof value === 'number' ? t.intValue({ value: value.toString() }) : t.stringValue({ value }); + return t.argument({ name, value: valueNode }); +} + +function buildEnumListArg(name: string, values: string[] | undefined): ArgumentNode | null { + if (!values || values.length === 0) { + return null; + } + return t.argument({ + name, + value: t.listValue({ + values: values.map((value) => buildEnumValue(value)), + }), + }); +} + +function buildEnumValue(value: string): EnumValueNode { + return { + kind: ENUM_VALUE_KIND, + value, + }; +} + +function buildPageInfoSelections(): FieldNode[] { + return [ + t.field({ name: 'hasNextPage' }), + t.field({ name: 'hasPreviousPage' }), + t.field({ name: 'startCursor' }), + t.field({ name: 'endCursor' }), + ]; +} + +function buildConnectionSelections(nodeSelections: FieldNode[]): FieldNode[] { + return [ + t.field({ + name: 'nodes', + selectionSet: t.selectionSet({ selections: nodeSelections }), + }), + t.field({ name: 'totalCount' }), + t.field({ + name: 'pageInfo', + selectionSet: t.selectionSet({ selections: buildPageInfoSelections() }), + }), + ]; +} + +interface VariableSpec { + varName: string; + argName?: string; + typeName: string; + value: unknown; +} + +interface InputMutationConfig { + operationName: string; + mutationField: string; + inputTypeName: string; + resultSelections: FieldNode[]; +} + +function buildInputMutationDocument(config: InputMutationConfig): string { + const document = t.document({ + definitions: [ + t.operationDefinition({ + operation: OP_MUTATION, + name: config.operationName + 'Mutation', + variableDefinitions: [ + t.variableDefinition({ + variable: t.variable({ name: 'input' }), + type: parseType(config.inputTypeName + '!'), + }), + ], + selectionSet: t.selectionSet({ + selections: [ + t.field({ + name: config.mutationField, + args: [ + t.argument({ + name: 'input', + value: t.variable({ name: 'input' }), + }), + ], + selectionSet: t.selectionSet({ + selections: config.resultSelections, + }), + }), + ], + }), + }), + ], + }); + return print(document); +} + +function addVariable( + spec: VariableSpec, + definitions: VariableDefinitionNode[], + args: ArgumentNode[], + variables: Record +): void { + if (spec.value === undefined) return; + + definitions.push( + t.variableDefinition({ + variable: t.variable({ name: spec.varName }), + type: parseType(spec.typeName), + }) + ); + args.push( + t.argument({ + name: spec.argName ?? spec.varName, + value: t.variable({ name: spec.varName }), + }) + ); + variables[spec.varName] = spec.value; +} + +function buildValueAst( + value: unknown +): + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | EnumValueNode { + if (value === null) { + return t.nullValue(); + } + + if (typeof value === 'boolean') { + return t.booleanValue({ value }); + } + + if (typeof value === 'number') { + return Number.isInteger(value) + ? t.intValue({ value: value.toString() }) + : t.floatValue({ value: value.toString() }); + } + + if (typeof value === 'string') { + return t.stringValue({ value }); + } + + if (Array.isArray(value)) { + return t.listValue({ + values: value.map((item) => buildValueAst(item)), + }); + } + + if (typeof value === 'object' && value !== null) { + const obj = value as Record; + return t.objectValue({ + fields: Object.entries(obj).map(([key, val]) => + t.objectField({ + name: key, + value: buildValueAst(val), + }) + ), + }); + } + + throw new Error('Unsupported value type: ' + typeof value); +} diff --git a/__fixtures__/codegen-example/output/orm/select-types.ts b/__fixtures__/codegen-example/output/orm/select-types.ts new file mode 100644 index 000000000..80165efa6 --- /dev/null +++ b/__fixtures__/codegen-example/output/orm/select-types.ts @@ -0,0 +1,140 @@ +/** + * Type utilities for select inference + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +export interface ConnectionResult { + nodes: T[]; + totalCount: number; + pageInfo: PageInfo; +} + +export interface PageInfo { + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor?: string | null; + endCursor?: string | null; +} + +export interface FindManyArgs { + select?: TSelect; + where?: TWhere; + orderBy?: TOrderBy[]; + first?: number; + last?: number; + after?: string; + before?: string; + offset?: number; +} + +export interface FindFirstArgs { + select?: TSelect; + where?: TWhere; +} + +export interface CreateArgs { + data: TData; + select?: TSelect; +} + +export interface UpdateArgs { + where: TWhere; + data: TData; + select?: TSelect; +} + +export type FindOneArgs = { + select?: TSelect; +} & Record; + +export interface DeleteArgs { + where: TWhere; + select?: TSelect; +} + +type DepthLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; +type DecrementDepth = { + 0: 0; + 1: 0; + 2: 1; + 3: 2; + 4: 3; + 5: 4; + 6: 5; + 7: 6; + 8: 7; + 9: 8; + 10: 9; +}; + +/** + * Recursively validates select objects, rejecting unknown keys. + * + * NOTE: Depth is intentionally capped to avoid circular-instantiation issues + * in very large cyclic schemas. + */ +export type DeepExact = Depth extends 0 + ? T extends Shape + ? T + : never + : T extends Shape + ? Exclude extends never + ? { + [K in keyof T]: K extends keyof Shape + ? T[K] extends { select: infer NS } + ? Extract extends { + select?: infer ShapeNS; + } + ? DeepExact< + Omit & { + select: DeepExact, DecrementDepth[Depth]>; + }, + Extract, + DecrementDepth[Depth] + > + : never + : T[K] + : never; + } + : never + : never; + +/** + * Enforces exact select shape while keeping contextual typing on `S extends XxxSelect`. + * Use this as an intersection in overloads: + * `{ select: S } & StrictSelect`. + */ +export type StrictSelect = S extends DeepExact ? {} : never; + +/** + * Hook-optimized strict select variant. + * + * Uses a shallower recursion depth to keep editor autocomplete responsive + * in large schemas while still validating common nested-select mistakes. + */ +export type HookStrictSelect = S extends DeepExact ? {} : never; + +/** + * Infer result type from select configuration + */ +export type InferSelectResult = 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 extends ConnectionResult + ? ConnectionResult> + : + | InferSelectResult, NestedSelect> + | (null extends TEntity[K] ? null : never) + : never + : K extends keyof TEntity + ? TEntity[K] + : never; + }; diff --git a/__fixtures__/codegen-example/output/orm/types.ts b/__fixtures__/codegen-example/output/orm/types.ts new file mode 100644 index 000000000..7c1120bcd --- /dev/null +++ b/__fixtures__/codegen-example/output/orm/types.ts @@ -0,0 +1,8 @@ +/** + * Types re-export + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// Re-export all types from input-types +export * from './input-types'; diff --git a/__fixtures__/codegen-example/schema.graphql b/__fixtures__/codegen-example/schema.graphql new file mode 100644 index 000000000..4d65f6b7e --- /dev/null +++ b/__fixtures__/codegen-example/schema.graphql @@ -0,0 +1,510 @@ +type Query { + users( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + orderBy: [UsersOrderBy!] = [PRIMARY_KEY_ASC] + filter: UserFilter + ): UsersConnection + posts( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + orderBy: [PostsOrderBy!] = [PRIMARY_KEY_ASC] + filter: PostFilter + ): PostsConnection + comments( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + orderBy: [CommentsOrderBy!] = [PRIMARY_KEY_ASC] + filter: CommentFilter + ): CommentsConnection + categories( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + orderBy: [CategoriesOrderBy!] = [PRIMARY_KEY_ASC] + filter: CategoryFilter + ): CategoriesConnection +} + +type Mutation { + createUser(input: CreateUserInput!): CreateUserPayload + updateUser(input: UpdateUserInput!): UpdateUserPayload + deleteUser(input: DeleteUserInput!): DeleteUserPayload + createPost(input: CreatePostInput!): CreatePostPayload + updatePost(input: UpdatePostInput!): UpdatePostPayload + deletePost(input: DeletePostInput!): DeletePostPayload + createComment(input: CreateCommentInput!): CreateCommentPayload + updateComment(input: UpdateCommentInput!): UpdateCommentPayload + deleteComment(input: DeleteCommentInput!): DeleteCommentPayload + createCategory(input: CreateCategoryInput!): CreateCategoryPayload + updateCategory(input: UpdateCategoryInput!): UpdateCategoryPayload + deleteCategory(input: DeleteCategoryInput!): DeleteCategoryPayload +} + +scalar Cursor +scalar UUID +scalar Datetime +scalar JSON + +type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: Cursor + endCursor: Cursor +} + +# ============================================================================ +# User +# ============================================================================ + +type User { + id: UUID! + email: String! + name: String + bio: String + isActive: Boolean! + role: UserRole! + metadata: JSON + createdAt: Datetime! + updatedAt: Datetime! + posts( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + orderBy: [PostsOrderBy!] = [PRIMARY_KEY_ASC] + filter: PostFilter + ): PostsConnection! + comments( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + orderBy: [CommentsOrderBy!] = [PRIMARY_KEY_ASC] + filter: CommentFilter + ): CommentsConnection! +} + +type UsersConnection { + nodes: [User!]! + edges: [UsersEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type UsersEdge { + cursor: Cursor + node: User! +} + +enum UsersOrderBy { + PRIMARY_KEY_ASC + PRIMARY_KEY_DESC + EMAIL_ASC + EMAIL_DESC + NAME_ASC + NAME_DESC + CREATED_AT_ASC + CREATED_AT_DESC +} + +enum UserRole { + ADMIN + EDITOR + VIEWER +} + +input UserFilter { + id: UUIDFilter + email: StringFilter + name: StringFilter + isActive: BooleanFilter + role: UserRoleFilter + createdAt: DatetimeFilter + and: [UserFilter!] + or: [UserFilter!] + not: UserFilter +} + +input UserRoleFilter { + equalTo: UserRole + notEqualTo: UserRole + in: [UserRole!] + notIn: [UserRole!] +} + +input CreateUserInput { + user: UserInput! +} + +input UserInput { + email: String! + name: String + bio: String + isActive: Boolean + role: UserRole + metadata: JSON +} + +input UpdateUserInput { + id: UUID! + patch: UserPatch! +} + +input UserPatch { + email: String + name: String + bio: String + isActive: Boolean + role: UserRole + metadata: JSON +} + +input DeleteUserInput { + id: UUID! +} + +type CreateUserPayload { + user: User +} + +type UpdateUserPayload { + user: User +} + +type DeleteUserPayload { + user: User +} + +# ============================================================================ +# Post +# ============================================================================ + +type Post { + id: UUID! + title: String! + body: String + slug: String! + isPublished: Boolean! + publishedAt: Datetime + authorId: UUID! + categoryId: UUID + tags: [String] + createdAt: Datetime! + updatedAt: Datetime! + author: User! + category: Category + comments( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + orderBy: [CommentsOrderBy!] = [PRIMARY_KEY_ASC] + filter: CommentFilter + ): CommentsConnection! +} + +type PostsConnection { + nodes: [Post!]! + edges: [PostsEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type PostsEdge { + cursor: Cursor + node: Post! +} + +enum PostsOrderBy { + PRIMARY_KEY_ASC + PRIMARY_KEY_DESC + TITLE_ASC + TITLE_DESC + CREATED_AT_ASC + CREATED_AT_DESC + PUBLISHED_AT_ASC + PUBLISHED_AT_DESC +} + +input PostFilter { + id: UUIDFilter + title: StringFilter + slug: StringFilter + isPublished: BooleanFilter + authorId: UUIDFilter + categoryId: UUIDFilter + createdAt: DatetimeFilter + and: [PostFilter!] + or: [PostFilter!] + not: PostFilter +} + +input CreatePostInput { + post: PostInput! +} + +input PostInput { + title: String! + body: String + slug: String! + isPublished: Boolean + publishedAt: Datetime + authorId: UUID! + categoryId: UUID + tags: [String] +} + +input UpdatePostInput { + id: UUID! + patch: PostPatch! +} + +input PostPatch { + title: String + body: String + slug: String + isPublished: Boolean + publishedAt: Datetime + categoryId: UUID + tags: [String] +} + +input DeletePostInput { + id: UUID! +} + +type CreatePostPayload { + post: Post +} + +type UpdatePostPayload { + post: Post +} + +type DeletePostPayload { + post: Post +} + +# ============================================================================ +# Comment +# ============================================================================ + +type Comment { + id: UUID! + body: String! + authorId: UUID! + postId: UUID! + createdAt: Datetime! + updatedAt: Datetime! + author: User! + post: Post! +} + +type CommentsConnection { + nodes: [Comment!]! + edges: [CommentsEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type CommentsEdge { + cursor: Cursor + node: Comment! +} + +enum CommentsOrderBy { + PRIMARY_KEY_ASC + PRIMARY_KEY_DESC + CREATED_AT_ASC + CREATED_AT_DESC +} + +input CommentFilter { + id: UUIDFilter + body: StringFilter + authorId: UUIDFilter + postId: UUIDFilter + createdAt: DatetimeFilter + and: [CommentFilter!] + or: [CommentFilter!] + not: CommentFilter +} + +input CreateCommentInput { + comment: CommentInput! +} + +input CommentInput { + body: String! + authorId: UUID! + postId: UUID! +} + +input UpdateCommentInput { + id: UUID! + patch: CommentPatch! +} + +input CommentPatch { + body: String +} + +input DeleteCommentInput { + id: UUID! +} + +type CreateCommentPayload { + comment: Comment +} + +type UpdateCommentPayload { + comment: Comment +} + +type DeleteCommentPayload { + comment: Comment +} + +# ============================================================================ +# Category +# ============================================================================ + +type Category { + id: UUID! + name: String! + slug: String! + description: String + parentId: UUID + createdAt: Datetime! + parent: Category + posts( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + orderBy: [PostsOrderBy!] = [PRIMARY_KEY_ASC] + filter: PostFilter + ): PostsConnection! +} + +type CategoriesConnection { + nodes: [Category!]! + edges: [CategoriesEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type CategoriesEdge { + cursor: Cursor + node: Category! +} + +enum CategoriesOrderBy { + PRIMARY_KEY_ASC + PRIMARY_KEY_DESC + NAME_ASC + NAME_DESC +} + +input CategoryFilter { + id: UUIDFilter + name: StringFilter + slug: StringFilter + parentId: UUIDFilter + and: [CategoryFilter!] + or: [CategoryFilter!] + not: CategoryFilter +} + +input CreateCategoryInput { + category: CategoryInput! +} + +input CategoryInput { + name: String! + slug: String! + description: String + parentId: UUID +} + +input UpdateCategoryInput { + id: UUID! + patch: CategoryPatch! +} + +input CategoryPatch { + name: String + slug: String + description: String + parentId: UUID +} + +input DeleteCategoryInput { + id: UUID! +} + +type CreateCategoryPayload { + category: Category +} + +type UpdateCategoryPayload { + category: Category +} + +type DeleteCategoryPayload { + category: Category +} + +# ============================================================================ +# Shared Filter Types +# ============================================================================ + +input UUIDFilter { + equalTo: UUID + notEqualTo: UUID + in: [UUID!] + notIn: [UUID!] + isNull: Boolean +} + +input StringFilter { + equalTo: String + notEqualTo: String + in: [String!] + notIn: [String!] + includes: String + like: String + likeInsensitive: String + isNull: Boolean +} + +input BooleanFilter { + equalTo: Boolean + notEqualTo: Boolean +} + +input DatetimeFilter { + equalTo: Datetime + notEqualTo: Datetime + greaterThan: Datetime + greaterThanOrEqualTo: Datetime + lessThan: Datetime + lessThanOrEqualTo: Datetime + isNull: Boolean +}