diff --git a/CLAUDE.md b/CLAUDE.md index 907fe7cd1..776c240c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,33 +5,22 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Build & Development Commands ```bash -# Install dependencies -pnpm install - -# Build all packages -pnpm build - -# Build all packages (dev mode - faster, no optimizations) -pnpm build:dev - -# Lint all packages (auto-fix enabled) -pnpm lint - -# Clean build artifacts -pnpm clean - -# Update dependencies interactively -pnpm deps +pnpm install # Install dependencies +pnpm build # Build all packages +pnpm build:dev # Build all packages (dev mode - faster, no optimizations) +pnpm lint # Lint all packages (auto-fix enabled) +pnpm clean # Clean build artifacts +pnpm deps # Update dependencies interactively (pnpm up -r -i -L) ``` ### Per-Package Commands -Navigate to any package directory (e.g., `cd pgpm/cli`) and run: +From any package directory (e.g., `cd pgpm/cli`): ```bash -pnpm build # Build the package +pnpm build # Build the package (uses makage) pnpm lint # Lint with auto-fix -pnpm test # Run tests +pnpm test # Run tests (Jest) pnpm test:watch # Run tests in watch mode pnpm dev # Run in development mode (where available) ``` @@ -41,54 +30,74 @@ pnpm dev # Run in development mode (where available) ```bash cd packages/cli pnpm test -- path/to/test.test.ts -# or with pattern matching: pnpm test -- --testNamePattern="test name pattern" ``` +### Publishing + +Lerna with independent versioning and conventional commits. Publishing only from `main` branch: + +```bash +npx lerna version # Bump versions +npx lerna publish # Publish to npm +``` + ## Project Architecture -This is a **pnpm monorepo** using Lerna for versioning/publishing. The workspace is organized into domain-specific directories: +A **pnpm monorepo** with Lerna for versioning. PostgreSQL-first framework: design your database schema, manage it with pgpm, and get a production-ready GraphQL API automatically via PostGraphile. + +### Data Flow + +``` +PostgreSQL (schema + RLS policies, managed by pgpm migrations) + ↓ +PostGraphile (graphql/server + graphile/* plugins) + ↓ +GraphQL Schema (auto-generated from database) + ↓ +graphql/codegen (--react-query mode OR --orm mode) + ↓ +React Query Hooks or Prisma-like ORM Client +``` -### Core Package Groups +### Workspace Groups | Directory | Purpose | |-----------|---------| -| `pgpm/` | PostgreSQL Package Manager - CLI, core engine, types | -| `graphql/` | GraphQL layer - server, codegen, React hooks, testing | -| `graphile/` | PostGraphile plugins - filters, i18n, meta-schema, PostGIS | -| `postgres/` | PostgreSQL utilities - introspection, testing, seeding, AST | -| `packages/` | Shared utilities - CLI, ORM, query builder | -| `uploads/` | File streaming - S3, ETags, content-type detection | -| `jobs/` | Job scheduling and worker infrastructure | +| `pgpm/` | PostgreSQL Package Manager - CLI (`pgpm`), core engine, types, env, logger | +| `graphql/` | GraphQL layer - server, codegen, query builder, explorer, AST utilities | +| `graphile/` | PostGraphile plugins - filters, i18n, meta-schema, PostGIS, search, uploads, settings | +| `postgres/` | PostgreSQL utilities - introspection, testing (pgsql-test), seeding, AST, query context | +| `packages/` | Shared utilities - CLI (`cnc`), ORM base, query builder, server utils, client | +| `uploads/` | File streaming - S3/MinIO, ETags, content-type detection, UUID hashing | +| `jobs/` | Knative job scheduling - worker, scheduler, service, functions | +| `functions/` | Knative cloud functions (e.g., send-email-link) | -### Key Packages +### Key Packages & CLIs -**pgpm (PostgreSQL Package Manager)** -- `pgpm/cli` - Main CLI tool (`pgpm` command) -- `pgpm/core` - Migration engine, dependency resolution, deployment +**`pgpm` CLI** (`pgpm/cli`) - PostgreSQL Package Manager. Commands: `init`, `add`, `deploy`, `revert`, `verify`, `plan`, `install`, `export`, `docker`, `dump`, `tag`. Manages SQL migrations in Sqitch-compatible format with dependency resolution. -**GraphQL Stack** -- `graphql/server` - Express + PostGraphile API server -- `graphql/codegen` - SDK generator (React Query hooks or Prisma-like ORM) -- `graphql/query` - Fluent GraphQL query builder +**`cnc` CLI** (`packages/cli`, binary: `cnc` or `constructive`) - Full dev toolkit. Commands: `server` (start PostGraphile), `explorer` (GraphiQL UI), `codegen` (generate SDK), `get-graphql-schema`, `jobs`, `context`, `auth`, `execute`. -**Testing Infrastructure** -- `postgres/pgsql-test` - Isolated PostgreSQL test environments with transaction rollback -- `graphile/graphile-test` - GraphQL testing utilities +**`graphql/codegen`** - Generates type-safe clients from GraphQL schema or endpoint: +- `--react-query` mode: TanStack Query v5 hooks with query key factories +- `--orm` mode: Prisma-like fluent API with `InferSelectResult` type inference, discriminated union error handling (`.unwrap()`, `.unwrapOr()`) +- Sources: GraphQL endpoint URL, .graphql schema file, or direct database introspection -### Testing Pattern +**`graphql/server`** - Express + PostGraphile. Supports multi-endpoint routing via subdomain/host detection (schema builder with app-public/auth/admin sub-endpoints). Uses `LISTEN/NOTIFY` for schema cache invalidation. -Tests use `pgsql-test` for database testing with per-test transaction rollback: +**`graphile/graphile-settings`** - Centralizes all PostGraphile plugin config: connection filters, full-text search, PostGIS, i18n, meta-schema, many-to-many, search, upload plugin. Single `getGraphileSettings(opts)` entry point. -```typescript -import { getConnections } from 'pgsql-test'; +**`packages/query-builder`** - Fluent SQL query builder for SELECT/INSERT/UPDATE/DELETE with JOINs, WHERE, GROUP BY. Schema-qualified tables. -let db, teardown; +### Testing Infrastructure -beforeAll(async () => { - ({ db, teardown } = await getConnections()); -}); +**`postgres/pgsql-test`** - Isolated PostgreSQL test environments with per-test transaction rollback: +```typescript +import { getConnections } from 'pgsql-test'; +let db, teardown; +beforeAll(async () => ({ db, teardown } = await getConnections())); beforeEach(() => db.beforeEach()); afterEach(() => db.afterEach()); afterAll(() => teardown()); @@ -100,26 +109,35 @@ test('example', async () => { }); ``` +**`graphile/graphile-test`** - GraphQL testing with PostGraphile snapshot support. + +### Job System + +Background jobs use Knative: jobs are added to `app_jobs.jobs` table → `knative-job-worker` polls and picks up → POSTs to Knative function URL → function executes (e.g., send email) → returns status. + ### Database Configuration -Tests require PostgreSQL. Standard PG environment variables: -- `PGHOST` (default: localhost) -- `PGPORT` (default: 5432) -- `PGUSER` (default: postgres) -- `PGPASSWORD` (default: password) +Tests require PostgreSQL. Standard PG env vars: +- `PGHOST` (default: localhost), `PGPORT` (default: 5432) +- `PGUSER` (default: postgres), `PGPASSWORD` (default: password) For S3/MinIO tests: `MINIO_ENDPOINT`, `AWS_ACCESS_KEY`, `AWS_SECRET_KEY`, `AWS_REGION` -### Build System +## Build System -- Uses `makage` for TypeScript compilation (handles both CJS and ESM output) -- Jest with ts-jest for testing -- ESLint with TypeScript support -- Each package has its own `tsconfig.json` extending root config +- **makage** compiles TypeScript to both CJS and ESM, outputs to `dist/` +- `makage build --dev` for faster dev builds +- Some packages use `copyfiles` for non-TS assets (SQL files, templates) +- Jest with `ts-jest` preset per-package (`jest.config.js` in each package) -### Code Conventions +## Code Conventions -- TypeScript with `strict: true` (but `strictNullChecks: false`) -- Target: ES2022, Module: CommonJS -- Packages publish to npm from `dist/` directory +- TypeScript with `strict: true` but `strictNullChecks: false` +- Target: ES2022, Module: CommonJS, ModuleResolution: node +- 2-space indent, single quotes, semicolons required, no trailing commas +- Imports auto-sorted by `simple-import-sort`, unused imports auto-removed +- `@typescript-eslint/no-explicit-any`: allowed (turned off) +- Unused var pattern: prefix with `_` (e.g., `_unused`) - Workspace dependencies use `workspace:^` protocol +- Packages publish from `dist/` directory +- GraphQL pinned to `15.10.1` via overrides diff --git a/graphql/codegen/examples/example.schema.graphql b/graphql/codegen/examples/example.schema.graphql new file mode 100644 index 000000000..1bba02d5c --- /dev/null +++ b/graphql/codegen/examples/example.schema.graphql @@ -0,0 +1,593 @@ +"""A universally unique identifier as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122).""" +scalar UUID + +""" +A point in time as described by the [ISO +8601](https://en.wikipedia.org/wiki/ISO_8601) standard. May or may not include a timezone. +""" +scalar Datetime + +"""A location as described by the [GeoJSON](https://geojson.org/) format.""" +scalar JSON + +"""A string representing a cursor for pagination.""" +scalar Cursor + +"""The root query type.""" +type Query { + """Reads and enables pagination through a set of `User`.""" + users( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + orderBy: [UsersOrderBy!] = [PRIMARY_KEY_ASC] + filter: UserFilter + condition: UserCondition + ): UsersConnection + + """Reads a single `User` using its globally unique `ID`.""" + user(id: UUID!): User + + """Reads and enables pagination through a set of `Post`.""" + posts( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + orderBy: [PostsOrderBy!] = [PRIMARY_KEY_ASC] + filter: PostFilter + condition: PostCondition + ): PostsConnection + + """Reads a single `Post` using its globally unique `ID`.""" + post(id: UUID!): Post + + """Reads and enables pagination through a set of `Comment`.""" + comments( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + orderBy: [CommentsOrderBy!] = [PRIMARY_KEY_ASC] + filter: CommentFilter + ): CommentsConnection + + """Reads a single `Comment` using its globally unique `ID`.""" + comment(id: UUID!): Comment + + """Reads and enables pagination through a set of `Tag`.""" + tags( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + orderBy: [TagsOrderBy!] = [PRIMARY_KEY_ASC] + ): TagsConnection + + """The currently authenticated user.""" + currentUser: User + + """Search users by name or email.""" + searchUsers(query: String!): [User!] +} + +"""The root mutation type.""" +type Mutation { + """Creates a single `User`.""" + createUser(input: CreateUserInput!): CreateUserPayload + + """Updates a single `User` using its globally unique `ID`.""" + updateUser(input: UpdateUserInput!): UpdateUserPayload + + """Deletes a single `User` using its globally unique `ID`.""" + deleteUser(input: DeleteUserInput!): DeleteUserPayload + + """Creates a single `Post`.""" + createPost(input: CreatePostInput!): CreatePostPayload + + """Updates a single `Post` using its globally unique `ID`.""" + updatePost(input: UpdatePostInput!): UpdatePostPayload + + """Deletes a single `Post` using its globally unique `ID`.""" + deletePost(input: DeletePostInput!): DeletePostPayload + + """Creates a single `Comment`.""" + createComment(input: CreateCommentInput!): CreateCommentPayload + + """Updates a single `Comment` using its globally unique `ID`.""" + updateComment(input: UpdateCommentInput!): UpdateCommentPayload + + """Deletes a single `Comment` using its globally unique `ID`.""" + deleteComment(input: DeleteCommentInput!): DeleteCommentPayload + + """Creates a single `Tag`.""" + createTag(input: CreateTagInput!): CreateTagPayload + + """Authenticate a user and return a JWT token.""" + login(email: String!, password: String!): LoginPayload + + """Register a new user account.""" + register(username: String!, email: String!, password: String!): RegisterPayload +} + +# ============================================================================ +# Entity Types +# ============================================================================ + +type User { + id: UUID! + username: String! + email: String! + displayName: String + bio: String + role: UserRole! + isActive: Boolean! + createdAt: Datetime! + updatedAt: Datetime + + """Reads and enables pagination through a set of `Post`.""" + posts( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + orderBy: [PostsOrderBy!] = [PRIMARY_KEY_ASC] + filter: PostFilter + ): PostsConnection! + + """Reads and enables pagination through a set of `Comment`.""" + comments( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + ): CommentsConnection! +} + +type Post { + id: UUID! + title: String! + content: String + slug: String! + status: PostStatus! + authorId: UUID! + publishedAt: Datetime + createdAt: Datetime! + updatedAt: Datetime + + """Reads the `User` that authored this post.""" + author: User + + """Reads and enables pagination through a set of `Comment`.""" + comments( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + ): CommentsConnection! + + """Reads and enables pagination through a set of `Tag`.""" + tags( + first: Int + last: Int + offset: Int + before: Cursor + after: Cursor + ): TagsConnection! +} + +type Comment { + id: UUID! + body: String! + postId: UUID! + authorId: UUID! + createdAt: Datetime! + updatedAt: Datetime + + """The post this comment belongs to.""" + post: Post + + """The user who authored this comment.""" + author: User +} + +type Tag { + id: UUID! + name: String! + slug: String! + createdAt: Datetime! +} + +# ============================================================================ +# Enums +# ============================================================================ + +enum UserRole { + ADMIN + EDITOR + USER + GUEST +} + +enum PostStatus { + DRAFT + PUBLISHED + ARCHIVED +} + +enum UsersOrderBy { + NATURAL + PRIMARY_KEY_ASC + PRIMARY_KEY_DESC + ID_ASC + ID_DESC + USERNAME_ASC + USERNAME_DESC + EMAIL_ASC + EMAIL_DESC + CREATED_AT_ASC + CREATED_AT_DESC +} + +enum PostsOrderBy { + NATURAL + PRIMARY_KEY_ASC + PRIMARY_KEY_DESC + ID_ASC + ID_DESC + TITLE_ASC + TITLE_DESC + CREATED_AT_ASC + CREATED_AT_DESC + PUBLISHED_AT_ASC + PUBLISHED_AT_DESC +} + +enum CommentsOrderBy { + NATURAL + PRIMARY_KEY_ASC + PRIMARY_KEY_DESC + ID_ASC + ID_DESC + CREATED_AT_ASC + CREATED_AT_DESC +} + +enum TagsOrderBy { + NATURAL + PRIMARY_KEY_ASC + PRIMARY_KEY_DESC + ID_ASC + ID_DESC + NAME_ASC + NAME_DESC +} + +# ============================================================================ +# Connection Types +# ============================================================================ + +type UsersConnection { + nodes: [User!]! + edges: [UsersEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type UsersEdge { + node: User! + cursor: Cursor! +} + +type PostsConnection { + nodes: [Post!]! + edges: [PostsEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type PostsEdge { + node: Post! + cursor: Cursor! +} + +type CommentsConnection { + nodes: [Comment!]! + edges: [CommentsEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type CommentsEdge { + node: Comment! + cursor: Cursor! +} + +type TagsConnection { + nodes: [Tag!]! + edges: [TagsEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type TagsEdge { + node: Tag! + cursor: Cursor! +} + +type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: Cursor + endCursor: Cursor +} + +# ============================================================================ +# Filter Types +# ============================================================================ + +input UserFilter { + id: UUIDFilter + username: StringFilter + email: StringFilter + role: UserRoleFilter + isActive: BooleanFilter + and: [UserFilter!] + or: [UserFilter!] + not: UserFilter +} + +input PostFilter { + id: UUIDFilter + title: StringFilter + status: PostStatusFilter + authorId: UUIDFilter + and: [PostFilter!] + or: [PostFilter!] + not: PostFilter +} + +input CommentFilter { + id: UUIDFilter + postId: UUIDFilter + authorId: UUIDFilter + and: [CommentFilter!] + or: [CommentFilter!] + not: CommentFilter +} + +input UUIDFilter { + equalTo: UUID + notEqualTo: UUID + in: [UUID!] + notIn: [UUID!] + isNull: Boolean +} + +input StringFilter { + equalTo: String + notEqualTo: String + in: [String!] + notIn: [String!] + includes: String + startsWith: String + endsWith: String + isNull: Boolean +} + +input BooleanFilter { + equalTo: Boolean + notEqualTo: Boolean + isNull: Boolean +} + +input UserRoleFilter { + equalTo: UserRole + notEqualTo: UserRole + in: [UserRole!] + notIn: [UserRole!] +} + +input PostStatusFilter { + equalTo: PostStatus + notEqualTo: PostStatus + in: [PostStatus!] + notIn: [PostStatus!] +} + +# ============================================================================ +# Condition Types +# ============================================================================ + +input UserCondition { + id: UUID + username: String + email: String +} + +input PostCondition { + id: UUID + title: String + authorId: UUID +} + +# ============================================================================ +# Mutation Input Types +# ============================================================================ + +input CreateUserInput { + clientMutationId: String + user: UserInput! +} + +input UserInput { + username: String! + email: String! + displayName: String + bio: String + role: UserRole +} + +input UpdateUserInput { + clientMutationId: String + id: UUID! + patch: UserPatch! +} + +input UserPatch { + username: String + email: String + displayName: String + bio: String + role: UserRole + isActive: Boolean +} + +input DeleteUserInput { + clientMutationId: String + id: UUID! +} + +input CreatePostInput { + clientMutationId: String + post: PostInput! +} + +input PostInput { + title: String! + content: String + slug: String! + status: PostStatus + authorId: UUID! +} + +input UpdatePostInput { + clientMutationId: String + id: UUID! + patch: PostPatch! +} + +input PostPatch { + title: String + content: String + slug: String + status: PostStatus + publishedAt: Datetime +} + +input DeletePostInput { + clientMutationId: String + id: UUID! +} + +input CreateCommentInput { + clientMutationId: String + comment: CommentInput! +} + +input CommentInput { + body: String! + postId: UUID! + authorId: UUID! +} + +input UpdateCommentInput { + clientMutationId: String + id: UUID! + patch: CommentPatch! +} + +input CommentPatch { + body: String +} + +input DeleteCommentInput { + clientMutationId: String + id: UUID! +} + +input CreateTagInput { + clientMutationId: String + tag: TagInput! +} + +input TagInput { + name: String! + slug: String! +} + +# ============================================================================ +# Mutation Payload Types +# ============================================================================ + +type CreateUserPayload { + clientMutationId: String + user: User +} + +type UpdateUserPayload { + clientMutationId: String + user: User +} + +type DeleteUserPayload { + clientMutationId: String + user: User +} + +type CreatePostPayload { + clientMutationId: String + post: Post +} + +type UpdatePostPayload { + clientMutationId: String + post: Post +} + +type DeletePostPayload { + clientMutationId: String + post: Post +} + +type CreateCommentPayload { + clientMutationId: String + comment: Comment +} + +type UpdateCommentPayload { + clientMutationId: String + comment: Comment +} + +type DeleteCommentPayload { + clientMutationId: String + comment: Comment +} + +type CreateTagPayload { + clientMutationId: String + tag: Tag +} + +# ============================================================================ +# Custom Operation Payload Types +# ============================================================================ + +type LoginPayload { + token: String! + user: User! +} + +type RegisterPayload { + token: String! + user: User! +} diff --git a/graphql/codegen/examples/multi-target.config.ts b/graphql/codegen/examples/multi-target.config.ts new file mode 100644 index 000000000..75f3628c0 --- /dev/null +++ b/graphql/codegen/examples/multi-target.config.ts @@ -0,0 +1,15 @@ +import type { GraphQLSDKConfigTarget } from '../src/types/config'; + +/** + * Multi-target example config for graphql-codegen + * + * Usage with CLI flags to select mode: + * tsx src/cli/index.ts --config examples/multi-target.config.ts --react-query + * tsx src/cli/index.ts --config examples/multi-target.config.ts --orm + */ +const config: GraphQLSDKConfigTarget = { + endpoint: 'http://api.localhost:3000/graphql', + output: './examples/output/generated-sdk-public', +}; + +export default config; diff --git a/graphql/codegen/jest.config.js b/graphql/codegen/jest.config.js index 86f911c5d..475aa4db7 100644 --- a/graphql/codegen/jest.config.js +++ b/graphql/codegen/jest.config.js @@ -7,9 +7,9 @@ module.exports = { 'ts-jest', { babelConfig: false, - tsconfig: 'tsconfig.json', - }, - ], + tsconfig: 'tsconfig.json' + } + ] }, transformIgnorePatterns: [`/node_modules/*`], testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', diff --git a/graphql/codegen/src/__tests__/codegen/__snapshots__/client-generator.test.ts.snap b/graphql/codegen/src/__tests__/codegen/__snapshots__/client-generator.test.ts.snap index 11e9fad8f..31e4f0863 100644 --- a/graphql/codegen/src/__tests__/codegen/__snapshots__/client-generator.test.ts.snap +++ b/graphql/codegen/src/__tests__/codegen/__snapshots__/client-generator.test.ts.snap @@ -107,13 +107,13 @@ exports[`client-generator generateOrmClientFile generates OrmClient class with e import type { GraphQLAdapter, GraphQLError, - QueryResult, + QueryResult } from '@constructive-io/graphql-types'; export type { GraphQLAdapter, GraphQLError, - QueryResult, + QueryResult } from '@constructive-io/graphql-types'; /** @@ -139,19 +139,19 @@ export class FetchAdapter implements GraphQLAdapter { headers: { 'Content-Type': 'application/json', Accept: 'application/json', - ...this.headers, + ...this.headers }, body: JSON.stringify({ query: document, - variables: variables ?? {}, - }), + variables: variables ?? {} + }) }); if (!response.ok) { return { ok: false, data: null, - errors: [{ message: \`HTTP \${response.status}: \${response.statusText}\` }], + errors: [{ message: \`HTTP \${response.status}: \${response.statusText}\` }] }; } @@ -164,14 +164,14 @@ export class FetchAdapter implements GraphQLAdapter { return { ok: false, data: null, - errors: json.errors, + errors: json.errors }; } return { ok: true, data: json.data as T, - errors: undefined, + errors: undefined }; } @@ -301,8 +301,17 @@ export interface UpdateArgs { select?: TSelect; } -export interface DeleteArgs { +export type FindOneArgs< + TSelect, + TIdName extends string = 'id', + TId = string +> = { + select?: TSelect; +} & Record; + +export interface DeleteArgs { where: TWhere; + select?: TSelect; } /** diff --git a/graphql/codegen/src/__tests__/codegen/__snapshots__/model-generator.test.ts.snap b/graphql/codegen/src/__tests__/codegen/__snapshots__/model-generator.test.ts.snap index dc51d712f..fafb3a3d1 100644 --- a/graphql/codegen/src/__tests__/codegen/__snapshots__/model-generator.test.ts.snap +++ b/graphql/codegen/src/__tests__/codegen/__snapshots__/model-generator.test.ts.snap @@ -7,18 +7,21 @@ exports[`model-generator generates model with all CRUD methods 1`] = ` * DO NOT EDIT - changes will be overwritten */ import { OrmClient } from "../client"; -import { QueryBuilder, buildFindManyDocument, buildFindFirstDocument, buildCreateDocument, buildUpdateDocument, buildDeleteDocument } from "../query-builder"; +import { QueryBuilder, buildFindManyDocument, buildFindFirstDocument, buildFindOneDocument, buildCreateDocument, buildUpdateByPkDocument, buildDeleteByPkDocument } from "../query-builder"; import type { ConnectionResult, FindManyArgs, FindFirstArgs, CreateArgs, UpdateArgs, DeleteArgs, InferSelectResult, DeepExact } from "../select-types"; import type { User, UserWithRelations, UserSelect, UserFilter, UsersOrderBy, CreateUserInput, UpdateUserInput, UserPatch } from "../input-types"; +const defaultSelect = { + id: true +} as const; export class UserModel { constructor(private client: OrmClient) {} - findMany(args?: FindManyArgs, UserFilter, UsersOrderBy>): QueryBuilder<{ + findMany(args?: FindManyArgs, UserFilter, UsersOrderBy>): QueryBuilder<{ users: ConnectionResult>; }> { const { document, variables - } = buildFindManyDocument("User", "users", args?.select, { + } = buildFindManyDocument("User", "users", args?.select ?? defaultSelect, { where: args?.where, orderBy: args?.orderBy as string[] | undefined, first: args?.first, @@ -36,7 +39,7 @@ export class UserModel { variables }); } - findFirst(args?: FindFirstArgs, UserFilter>): QueryBuilder<{ + findFirst(args?: FindFirstArgs, UserFilter>): QueryBuilder<{ users: { nodes: InferSelectResult[]; }; @@ -44,7 +47,7 @@ export class UserModel { const { document, variables - } = buildFindFirstDocument("User", "users", args?.select, { + } = buildFindFirstDocument("User", "users", args?.select ?? defaultSelect, { where: args?.where }, "UserFilter"); return new QueryBuilder({ @@ -56,7 +59,26 @@ export class UserModel { variables }); } - create(args: CreateArgs, CreateUserInput["user"]>): QueryBuilder<{ + findOne(args: { + id: string; + select?: DeepExact; + }): QueryBuilder<{ + user: InferSelectResult | null; + }> { + const { + document, + variables + } = buildFindOneDocument("User", "user", args.id, args.select ?? defaultSelect, "id", "UUID!"); + return new QueryBuilder({ + client: this.client, + operation: "query", + operationName: "User", + fieldName: "user", + document, + variables + }); + } + create(args: CreateArgs, CreateUserInput["user"]>): QueryBuilder<{ createUser: { user: InferSelectResult; }; @@ -64,7 +86,7 @@ export class UserModel { const { document, variables - } = buildCreateDocument("User", "createUser", "user", args.select, args.data, "CreateUserInput"); + } = buildCreateDocument("User", "createUser", "user", args.select ?? defaultSelect, args.data, "CreateUserInput"); return new QueryBuilder({ client: this.client, operation: "mutation", @@ -74,7 +96,7 @@ export class UserModel { variables }); } - update(args: UpdateArgs, { + update(args: UpdateArgs, { id: string; }, UserPatch>): QueryBuilder<{ updateUser: { @@ -84,7 +106,7 @@ export class UserModel { const { document, variables - } = buildUpdateDocument("User", "updateUser", "user", args.select, args.where, args.data, "UpdateUserInput"); + } = buildUpdateByPkDocument("User", "updateUser", "user", args.select ?? defaultSelect, args.where.id, args.data, "UpdateUserInput", "id"); return new QueryBuilder({ client: this.client, operation: "mutation", @@ -94,19 +116,17 @@ export class UserModel { variables }); } - delete(args: DeleteArgs<{ + delete(args: DeleteArgs<{ id: string; - }>): QueryBuilder<{ + }, DeepExact>): QueryBuilder<{ deleteUser: { - user: { - id: string; - }; + user: InferSelectResult; }; }> { const { document, variables - } = buildDeleteDocument("User", "deleteUser", "user", args.where, "DeleteUserInput"); + } = buildDeleteByPkDocument("User", "deleteUser", "user", args.where.id, "DeleteUserInput", "id", args.select ?? defaultSelect); return new QueryBuilder({ client: this.client, operation: "mutation", @@ -126,18 +146,21 @@ exports[`model-generator generates model without update/delete when not availabl * DO NOT EDIT - changes will be overwritten */ import { OrmClient } from "../client"; -import { QueryBuilder, buildFindManyDocument, buildFindFirstDocument, buildCreateDocument, buildUpdateDocument, buildDeleteDocument } from "../query-builder"; +import { QueryBuilder, buildFindManyDocument, buildFindFirstDocument, buildFindOneDocument, buildCreateDocument, buildUpdateByPkDocument, buildDeleteByPkDocument } from "../query-builder"; import type { ConnectionResult, FindManyArgs, FindFirstArgs, CreateArgs, UpdateArgs, DeleteArgs, InferSelectResult, DeepExact } from "../select-types"; import type { AuditLog, AuditLogWithRelations, AuditLogSelect, AuditLogFilter, AuditLogsOrderBy, CreateAuditLogInput, UpdateAuditLogInput, AuditLogPatch } from "../input-types"; +const defaultSelect = { + id: true +} as const; export class AuditLogModel { constructor(private client: OrmClient) {} - findMany(args?: FindManyArgs, AuditLogFilter, AuditLogsOrderBy>): QueryBuilder<{ + findMany(args?: FindManyArgs, AuditLogFilter, AuditLogsOrderBy>): QueryBuilder<{ auditLogs: ConnectionResult>; }> { const { document, variables - } = buildFindManyDocument("AuditLog", "auditLogs", args?.select, { + } = buildFindManyDocument("AuditLog", "auditLogs", args?.select ?? defaultSelect, { where: args?.where, orderBy: args?.orderBy as string[] | undefined, first: args?.first, @@ -155,7 +178,7 @@ export class AuditLogModel { variables }); } - findFirst(args?: FindFirstArgs, AuditLogFilter>): QueryBuilder<{ + findFirst(args?: FindFirstArgs, AuditLogFilter>): QueryBuilder<{ auditLogs: { nodes: InferSelectResult[]; }; @@ -163,7 +186,7 @@ export class AuditLogModel { const { document, variables - } = buildFindFirstDocument("AuditLog", "auditLogs", args?.select, { + } = buildFindFirstDocument("AuditLog", "auditLogs", args?.select ?? defaultSelect, { where: args?.where }, "AuditLogFilter"); return new QueryBuilder({ @@ -175,7 +198,26 @@ export class AuditLogModel { variables }); } - create(args: CreateArgs, CreateAuditLogInput["auditLog"]>): QueryBuilder<{ + findOne(args: { + id: string; + select?: DeepExact; + }): QueryBuilder<{ + auditLog: InferSelectResult | null; + }> { + const { + document, + variables + } = buildFindOneDocument("AuditLog", "auditLog", args.id, args.select ?? defaultSelect, "id", "UUID!"); + return new QueryBuilder({ + client: this.client, + operation: "query", + operationName: "AuditLog", + fieldName: "auditLog", + document, + variables + }); + } + create(args: CreateArgs, CreateAuditLogInput["auditLog"]>): QueryBuilder<{ createAuditLog: { auditLog: InferSelectResult; }; @@ -183,7 +225,7 @@ export class AuditLogModel { const { document, variables - } = buildCreateDocument("AuditLog", "createAuditLog", "auditLog", args.select, args.data, "CreateAuditLogInput"); + } = buildCreateDocument("AuditLog", "createAuditLog", "auditLog", args.select ?? defaultSelect, args.data, "CreateAuditLogInput"); return new QueryBuilder({ client: this.client, operation: "mutation", @@ -203,18 +245,21 @@ exports[`model-generator handles custom query/mutation names 1`] = ` * DO NOT EDIT - changes will be overwritten */ import { OrmClient } from "../client"; -import { QueryBuilder, buildFindManyDocument, buildFindFirstDocument, buildCreateDocument, buildUpdateDocument, buildDeleteDocument } from "../query-builder"; +import { QueryBuilder, buildFindManyDocument, buildFindFirstDocument, buildFindOneDocument, buildCreateDocument, buildUpdateByPkDocument, buildDeleteByPkDocument } from "../query-builder"; import type { ConnectionResult, FindManyArgs, FindFirstArgs, CreateArgs, UpdateArgs, DeleteArgs, InferSelectResult, DeepExact } from "../select-types"; import type { Organization, OrganizationWithRelations, OrganizationSelect, OrganizationFilter, OrganizationsOrderBy, CreateOrganizationInput, UpdateOrganizationInput, OrganizationPatch } from "../input-types"; +const defaultSelect = { + id: true +} as const; export class OrganizationModel { constructor(private client: OrmClient) {} - findMany(args?: FindManyArgs, OrganizationFilter, OrganizationsOrderBy>): QueryBuilder<{ + findMany(args?: FindManyArgs, OrganizationFilter, OrganizationsOrderBy>): QueryBuilder<{ allOrganizations: ConnectionResult>; }> { const { document, variables - } = buildFindManyDocument("Organization", "allOrganizations", args?.select, { + } = buildFindManyDocument("Organization", "allOrganizations", args?.select ?? defaultSelect, { where: args?.where, orderBy: args?.orderBy as string[] | undefined, first: args?.first, @@ -232,7 +277,7 @@ export class OrganizationModel { variables }); } - findFirst(args?: FindFirstArgs, OrganizationFilter>): QueryBuilder<{ + findFirst(args?: FindFirstArgs, OrganizationFilter>): QueryBuilder<{ allOrganizations: { nodes: InferSelectResult[]; }; @@ -240,7 +285,7 @@ export class OrganizationModel { const { document, variables - } = buildFindFirstDocument("Organization", "allOrganizations", args?.select, { + } = buildFindFirstDocument("Organization", "allOrganizations", args?.select ?? defaultSelect, { where: args?.where }, "OrganizationFilter"); return new QueryBuilder({ @@ -252,7 +297,26 @@ export class OrganizationModel { variables }); } - create(args: CreateArgs, CreateOrganizationInput["organization"]>): QueryBuilder<{ + findOne(args: { + id: string; + select?: DeepExact; + }): QueryBuilder<{ + organizationById: InferSelectResult | null; + }> { + const { + document, + variables + } = buildFindOneDocument("Organization", "organizationById", args.id, args.select ?? defaultSelect, "id", "UUID!"); + return new QueryBuilder({ + client: this.client, + operation: "query", + operationName: "Organization", + fieldName: "organizationById", + document, + variables + }); + } + create(args: CreateArgs, CreateOrganizationInput["organization"]>): QueryBuilder<{ registerOrganization: { organization: InferSelectResult; }; @@ -260,7 +324,7 @@ export class OrganizationModel { const { document, variables - } = buildCreateDocument("Organization", "registerOrganization", "organization", args.select, args.data, "CreateOrganizationInput"); + } = buildCreateDocument("Organization", "registerOrganization", "organization", args.select ?? defaultSelect, args.data, "CreateOrganizationInput"); return new QueryBuilder({ client: this.client, operation: "mutation", @@ -270,7 +334,7 @@ export class OrganizationModel { variables }); } - update(args: UpdateArgs, { + update(args: UpdateArgs, { id: string; }, OrganizationPatch>): QueryBuilder<{ modifyOrganization: { @@ -280,7 +344,7 @@ export class OrganizationModel { const { document, variables - } = buildUpdateDocument("Organization", "modifyOrganization", "organization", args.select, args.where, args.data, "UpdateOrganizationInput"); + } = buildUpdateByPkDocument("Organization", "modifyOrganization", "organization", args.select ?? defaultSelect, args.where.id, args.data, "UpdateOrganizationInput", "id"); return new QueryBuilder({ client: this.client, operation: "mutation", @@ -290,19 +354,17 @@ export class OrganizationModel { variables }); } - delete(args: DeleteArgs<{ + delete(args: DeleteArgs<{ id: string; - }>): QueryBuilder<{ + }, DeepExact>): QueryBuilder<{ removeOrganization: { - organization: { - id: string; - }; + organization: InferSelectResult; }; }> { const { document, variables - } = buildDeleteDocument("Organization", "removeOrganization", "organization", args.where, "DeleteOrganizationInput"); + } = buildDeleteByPkDocument("Organization", "removeOrganization", "organization", args.where.id, "DeleteOrganizationInput", "id", args.select ?? defaultSelect); return new QueryBuilder({ client: this.client, operation: "mutation", diff --git a/graphql/codegen/src/__tests__/codegen/__snapshots__/react-query-hooks.test.ts.snap b/graphql/codegen/src/__tests__/codegen/__snapshots__/react-query-hooks.test.ts.snap index efafdb00e..1e1d1710d 100644 --- a/graphql/codegen/src/__tests__/codegen/__snapshots__/react-query-hooks.test.ts.snap +++ b/graphql/codegen/src/__tests__/codegen/__snapshots__/react-query-hooks.test.ts.snap @@ -57,8 +57,6 @@ exports[`Barrel File Generators generateMainBarrel generates main barrel with al * \`\`\` */ export * from "./client"; -export * from "./types"; -export * from "./schema-types"; export * from "./query-keys"; export * from "./mutation-keys"; export * from "./invalidation"; @@ -97,7 +95,6 @@ exports[`Barrel File Generators generateMainBarrel generates main barrel without * \`\`\` */ export * from "./client"; -export * from "./types"; export * from "./queries"; export * from "./mutations";" `; @@ -133,8 +130,6 @@ exports[`Barrel File Generators generateMainBarrel generates main barrel without * \`\`\` */ export * from "./client"; -export * from "./types"; -export * from "./schema-types"; export * from "./queries";" `; @@ -192,33 +187,30 @@ exports[`Custom Mutation Hook Generators generateCustomMutationHook generates cu * DO NOT EDIT - changes will be overwritten */ -import { useMutation } from "@tanstack/react-query"; -import type { UseMutationOptions } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { LoginPayload } from "../schema-types"; -import { customMutationKeys } from "../mutation-keys"; -/** GraphQL mutation document */ -export const loginMutationDocument = \` -mutation LoginMutation($email: String!, $password: String!) { - login(email: $email, password: $password) { - token - } -} -\`; -export interface LoginMutationVariables { - email: string; - password: string; -} -export interface LoginMutationResult { - login: LoginPayload; -} -export function useLoginMutation(options?: Omit, 'mutationFn'>) { +import { useMutation } from '@tanstack/react-query'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { getClient } from '../client'; +import { customMutationKeys } from '../mutation-keys'; +import type { LoginVariables } from '../../orm/mutation'; +import type { LoginPayloadSelect, LoginPayload } from '../../orm/input-types'; +import type { DeepExact, InferSelectResult } from '../../orm/select-types'; + +export type { LoginVariables } from '../../orm/mutation'; +export type { LoginPayloadSelect } from '../../orm/input-types'; + +const defaultSelect = { token: true } as const; + +export function useLoginMutation( + args?: { select?: DeepExact }, + options?: Omit }, Error, LoginVariables>, 'mutationFn'> +) { return useMutation({ mutationKey: customMutationKeys.login(), - mutationFn: (variables: LoginMutationVariables) => execute(loginMutationDocument, variables), - ...options + mutationFn: (variables: LoginVariables) => getClient().mutation.login(variables, { select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), + ...options, }); -}" +} +" `; exports[`Custom Mutation Hook Generators generateCustomMutationHook generates custom mutation hook with input object argument 1`] = ` @@ -228,32 +220,30 @@ exports[`Custom Mutation Hook Generators generateCustomMutationHook generates cu * DO NOT EDIT - changes will be overwritten */ -import { useMutation } from "@tanstack/react-query"; -import type { UseMutationOptions } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { RegisterInput, RegisterPayload } from "../schema-types"; -import { customMutationKeys } from "../mutation-keys"; -/** GraphQL mutation document */ -export const registerMutationDocument = \` -mutation RegisterMutation($input: RegisterInput!) { - register(input: $input) { - token - } -} -\`; -export interface RegisterMutationVariables { - input: RegisterInput; -} -export interface RegisterMutationResult { - register: RegisterPayload; -} -export function useRegisterMutation(options?: Omit, 'mutationFn'>) { +import { useMutation } from '@tanstack/react-query'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { getClient } from '../client'; +import { customMutationKeys } from '../mutation-keys'; +import type { RegisterVariables } from '../../orm/mutation'; +import type { RegisterPayloadSelect, RegisterPayload } from '../../orm/input-types'; +import type { DeepExact, InferSelectResult } from '../../orm/select-types'; + +export type { RegisterVariables } from '../../orm/mutation'; +export type { RegisterPayloadSelect } from '../../orm/input-types'; + +const defaultSelect = { token: true } as const; + +export function useRegisterMutation( + args?: { select?: DeepExact }, + options?: Omit }, Error, RegisterVariables>, 'mutationFn'> +) { return useMutation({ mutationKey: customMutationKeys.register(), - mutationFn: (variables: RegisterMutationVariables) => execute(registerMutationDocument, variables), - ...options + mutationFn: (variables: RegisterVariables) => getClient().mutation.register(variables, { select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), + ...options, }); -}" +} +" `; exports[`Custom Mutation Hook Generators generateCustomMutationHook generates custom mutation hook without arguments 1`] = ` @@ -263,29 +253,28 @@ exports[`Custom Mutation Hook Generators generateCustomMutationHook generates cu * DO NOT EDIT - changes will be overwritten */ -import { useMutation } from "@tanstack/react-query"; -import type { UseMutationOptions } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { LogoutPayload } from "../schema-types"; -import { customMutationKeys } from "../mutation-keys"; -/** GraphQL mutation document */ -export const logoutMutationDocument = \` -mutation LogoutMutation { - logout { - success - } -} -\`; -export interface LogoutMutationResult { - logout: LogoutPayload; -} -export function useLogoutMutation(options?: Omit, 'mutationFn'>) { +import { useMutation } from '@tanstack/react-query'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { getClient } from '../client'; +import { customMutationKeys } from '../mutation-keys'; +import type { LogoutPayloadSelect, LogoutPayload } from '../../orm/input-types'; +import type { DeepExact, InferSelectResult } from '../../orm/select-types'; + +export type { LogoutPayloadSelect } from '../../orm/input-types'; + +const defaultSelect = { success: true } as const; + +export function useLogoutMutation( + args?: { select?: DeepExact }, + options?: Omit }, Error, void>, 'mutationFn'> +) { return useMutation({ mutationKey: customMutationKeys.logout(), - mutationFn: () => execute(logoutMutationDocument), - ...options + mutationFn: () => getClient().mutation.logout({ select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), + ...options, }); -}" +} +" `; exports[`Custom Mutation Hook Generators generateCustomMutationHook generates custom mutation hook without centralized keys 1`] = ` @@ -295,31 +284,28 @@ exports[`Custom Mutation Hook Generators generateCustomMutationHook generates cu * DO NOT EDIT - changes will be overwritten */ -import { useMutation } from "@tanstack/react-query"; -import type { UseMutationOptions } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { LoginPayload } from "../schema-types"; -/** GraphQL mutation document */ -export const loginMutationDocument = \` -mutation LoginMutation($email: String!, $password: String!) { - login(email: $email, password: $password) { - token - } -} -\`; -export interface LoginMutationVariables { - email: string; - password: string; -} -export interface LoginMutationResult { - login: LoginPayload; -} -export function useLoginMutation(options?: Omit, 'mutationFn'>) { +import { useMutation } from '@tanstack/react-query'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { getClient } from '../client'; +import type { LoginVariables } from '../../orm/mutation'; +import type { LoginPayloadSelect, LoginPayload } from '../../orm/input-types'; +import type { DeepExact, InferSelectResult } from '../../orm/select-types'; + +export type { LoginVariables } from '../../orm/mutation'; +export type { LoginPayloadSelect } from '../../orm/input-types'; + +const defaultSelect = { token: true } as const; + +export function useLoginMutation( + args?: { select?: DeepExact }, + options?: Omit }, Error, LoginVariables>, 'mutationFn'> +) { return useMutation({ - mutationFn: (variables: LoginMutationVariables) => execute(loginMutationDocument, variables), - ...options + mutationFn: (variables: LoginVariables) => getClient().mutation.login(variables, { select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), + ...options, }); -}" +} +" `; exports[`Custom Query Hook Generators generateCustomQueryHook generates custom query hook with arguments 1`] = ` @@ -329,72 +315,81 @@ exports[`Custom Query Hook Generators generateCustomQueryHook generates custom q * DO NOT EDIT - changes will be overwritten */ -import { useQuery } from "@tanstack/react-query"; -import type { UseQueryOptions, QueryClient } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { ExecuteOptions } from "../client"; -import type { User } from "../schema-types"; -import { customQueryKeys } from "../query-keys"; -/** GraphQL query document */ -export const searchUsersQueryDocument = \` -query SearchUsersQuery($query: String!, $limit: Int) { - searchUsers(query: $query, limit: $limit) -} -\`; -export interface SearchUsersQueryVariables { - query: string; - limit?: number; -} -export interface SearchUsersQueryResult { - searchUsers: User[]; -} +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryOptions, QueryClient } from '@tanstack/react-query'; +import { getClient } from '../client'; +import { customQueryKeys } from '../query-keys'; +import type { SearchUsersVariables } from '../../orm/query'; +import type { UserSelect, User } from '../../orm/input-types'; +import type { DeepExact, InferSelectResult } from '../../orm/select-types'; + +export type { SearchUsersVariables } from '../../orm/query'; +export type { UserSelect } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** Query key factory - re-exported from query-keys.ts */ export const searchUsersQueryKey = customQueryKeys.searchUsers; + /** * Search users by name or email - * + * * @example * \`\`\`tsx * const { data, isLoading } = useSearchUsersQuery({ query, limit }); - * + * * if (data?.searchUsers) { * console.log(data.searchUsers); * } * \`\`\` */ -export function useSearchUsersQuery(variables: SearchUsersQueryVariables, options?: Omit, 'queryKey' | 'queryFn'>) { +export function useSearchUsersQuery( + variables: SearchUsersVariables, + args?: { select?: DeepExact }, + options?: Omit[] }, Error>, 'queryKey' | 'queryFn'> +) { return useQuery({ queryKey: searchUsersQueryKey(variables), - queryFn: () => execute(searchUsersQueryDocument, variables), + queryFn: () => getClient().query.searchUsers(variables!, { select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), enabled: !!variables && options?.enabled !== false, - ...options + ...options, }); } + /** * Fetch searchUsers without React hooks - * + * * @example * \`\`\`ts * const data = await fetchSearchUsersQuery({ query, limit }); * \`\`\` */ -export async function fetchSearchUsersQuery(variables: SearchUsersQueryVariables, options?: ExecuteOptions): Promise { - return execute(searchUsersQueryDocument, variables, options); +export async function fetchSearchUsersQuery( + variables: SearchUsersVariables, + args?: { select?: DeepExact } +) { + return getClient().query.searchUsers(variables!, { select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(); } + /** * Prefetch searchUsers for SSR or cache warming - * + * * @example * \`\`\`ts * await prefetchSearchUsersQuery(queryClient, { query, limit }); * \`\`\` */ -export async function prefetchSearchUsersQuery(queryClient: QueryClient, variables: SearchUsersQueryVariables, options?: ExecuteOptions): Promise { +export async function prefetchSearchUsersQuery( + queryClient: QueryClient, + variables: SearchUsersVariables, + args?: { select?: DeepExact } +): Promise { await queryClient.prefetchQuery({ queryKey: searchUsersQueryKey(variables), - queryFn: () => execute(searchUsersQueryDocument, variables, options) + queryFn: () => getClient().query.searchUsers(variables!, { select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), }); -}" +} +" `; exports[`Custom Query Hook Generators generateCustomQueryHook generates custom query hook without arguments 1`] = ` @@ -404,67 +399,75 @@ exports[`Custom Query Hook Generators generateCustomQueryHook generates custom q * DO NOT EDIT - changes will be overwritten */ -import { useQuery } from "@tanstack/react-query"; -import type { UseQueryOptions, QueryClient } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { ExecuteOptions } from "../client"; -import type { User } from "../schema-types"; -import { customQueryKeys } from "../query-keys"; -/** GraphQL query document */ -export const currentUserQueryDocument = \` -query CurrentUserQuery { - currentUser -} -\`; -export interface CurrentUserQueryResult { - currentUser: User; -} +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryOptions, QueryClient } from '@tanstack/react-query'; +import { getClient } from '../client'; +import { customQueryKeys } from '../query-keys'; +import type { UserSelect, User } from '../../orm/input-types'; +import type { DeepExact, InferSelectResult } from '../../orm/select-types'; + +export type { UserSelect } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** Query key factory - re-exported from query-keys.ts */ export const currentUserQueryKey = customQueryKeys.currentUser; + /** * Get the current authenticated user - * + * * @example * \`\`\`tsx * const { data, isLoading } = useCurrentUserQuery(); - * + * * if (data?.currentUser) { * console.log(data.currentUser); * } * \`\`\` */ -export function useCurrentUserQuery(options?: Omit, 'queryKey' | 'queryFn'>) { +export function useCurrentUserQuery( + args?: { select?: DeepExact }, + options?: Omit }, Error>, 'queryKey' | 'queryFn'> +) { return useQuery({ queryKey: currentUserQueryKey(), - queryFn: () => execute(currentUserQueryDocument), - ...options + queryFn: () => getClient().query.currentUser({ select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), + ...options, }); } + /** * Fetch currentUser without React hooks - * + * * @example * \`\`\`ts * const data = await fetchCurrentUserQuery(); * \`\`\` */ -export async function fetchCurrentUserQuery(options?: ExecuteOptions): Promise { - return execute(currentUserQueryDocument, undefined, options); +export async function fetchCurrentUserQuery( + args?: { select?: DeepExact } +) { + return getClient().query.currentUser({ select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(); } + /** * Prefetch currentUser for SSR or cache warming - * + * * @example * \`\`\`ts * await prefetchCurrentUserQuery(queryClient); * \`\`\` */ -export async function prefetchCurrentUserQuery(queryClient: QueryClient, options?: ExecuteOptions): Promise { +export async function prefetchCurrentUserQuery( + queryClient: QueryClient, + args?: { select?: DeepExact } +): Promise { await queryClient.prefetchQuery({ queryKey: currentUserQueryKey(), - queryFn: () => execute(currentUserQueryDocument, undefined, options) + queryFn: () => getClient().query.currentUser({ select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), }); -}" +} +" `; exports[`Custom Query Hook Generators generateCustomQueryHook generates custom query hook without centralized keys 1`] = ` @@ -474,66 +477,74 @@ exports[`Custom Query Hook Generators generateCustomQueryHook generates custom q * DO NOT EDIT - changes will be overwritten */ -import { useQuery } from "@tanstack/react-query"; -import type { UseQueryOptions, QueryClient } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { ExecuteOptions } from "../client"; -import type { User } from "../schema-types"; -/** GraphQL query document */ -export const currentUserQueryDocument = \` -query CurrentUserQuery { - currentUser -} -\`; -export interface CurrentUserQueryResult { - currentUser: User; -} +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryOptions, QueryClient } from '@tanstack/react-query'; +import { getClient } from '../client'; +import type { UserSelect, User } from '../../orm/input-types'; +import type { DeepExact, InferSelectResult } from '../../orm/select-types'; + +export type { UserSelect } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** Query key factory for caching */ -export const currentUserQueryKey = () => ["currentUser"] as const; +export const currentUserQueryKey = () => ['currentUser'] as const; + /** * Get the current authenticated user - * + * * @example * \`\`\`tsx * const { data, isLoading } = useCurrentUserQuery(); - * + * * if (data?.currentUser) { * console.log(data.currentUser); * } * \`\`\` */ -export function useCurrentUserQuery(options?: Omit, 'queryKey' | 'queryFn'>) { +export function useCurrentUserQuery( + args?: { select?: DeepExact }, + options?: Omit }, Error>, 'queryKey' | 'queryFn'> +) { return useQuery({ queryKey: currentUserQueryKey(), - queryFn: () => execute(currentUserQueryDocument), - ...options + queryFn: () => getClient().query.currentUser({ select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), + ...options, }); } + /** * Fetch currentUser without React hooks - * + * * @example * \`\`\`ts * const data = await fetchCurrentUserQuery(); * \`\`\` */ -export async function fetchCurrentUserQuery(options?: ExecuteOptions): Promise { - return execute(currentUserQueryDocument, undefined, options); +export async function fetchCurrentUserQuery( + args?: { select?: DeepExact } +) { + return getClient().query.currentUser({ select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(); } + /** * Prefetch currentUser for SSR or cache warming - * + * * @example * \`\`\`ts * await prefetchCurrentUserQuery(queryClient); * \`\`\` */ -export async function prefetchCurrentUserQuery(queryClient: QueryClient, options?: ExecuteOptions): Promise { +export async function prefetchCurrentUserQuery( + queryClient: QueryClient, + args?: { select?: DeepExact } +): Promise { await queryClient.prefetchQuery({ queryKey: currentUserQueryKey(), - queryFn: () => execute(currentUserQueryDocument, undefined, options) + queryFn: () => getClient().query.currentUser({ select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), }); -}" +} +" `; exports[`Mutation Hook Generators generateCreateMutationHook generates create mutation hook for simple table 1`] = ` @@ -543,69 +554,52 @@ exports[`Mutation Hook Generators generateCreateMutationHook generates create mu * DO NOT EDIT - changes will be overwritten */ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import type { UseMutationOptions } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { User } from "../types"; -import { userKeys } from "../query-keys"; -import { userMutationKeys } from "../mutation-keys"; -export type { User } from "../types"; -export const createUserMutationDocument = \` -mutation CreateUserMutation($input: CreateUserInput!) { - createUser(input: $input) { - user { - id - email - name - createdAt - } - } -} -\`; -/** Input type for creating a User */ -interface UserCreateInput { - email?: string | null; - name?: string | null; -} -export interface CreateUserMutationVariables { - input: { - user: UserCreateInput; - }; -} -export interface CreateUserMutationResult { - createUser: { - user: User; - }; -} +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { getClient } from '../client'; +import { userKeys } from '../query-keys'; +import { userMutationKeys } from '../mutation-keys'; +import type { + UserSelect, + UserWithRelations, + CreateUserInput, +} from '../../orm/input-types'; +import type { + DeepExact, + InferSelectResult, +} from '../../orm/select-types'; + +export type { UserSelect, UserWithRelations, CreateUserInput } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** * Mutation hook for creating a User - * + * * @example * \`\`\`tsx - * const { mutate, isPending } = useCreateUserMutation(); - * - * mutate({ - * input: { - * user: { - * // ... fields - * }, - * }, + * const { mutate, isPending } = useCreateUserMutation({ + * select: { id: true, name: true }, * }); + * + * mutate({ name: 'New item' }); * \`\`\` */ -export function useCreateUserMutation(options?: Omit, 'mutationFn'>) { +export function useCreateUserMutation( + args?: { select?: DeepExact }, + options?: Omit } }, Error, CreateUserInput['user']>, 'mutationFn'> +) { const queryClient = useQueryClient(); return useMutation({ mutationKey: userMutationKeys.create(), - mutationFn: (variables: CreateUserMutationVariables) => execute(createUserMutationDocument, variables), + mutationFn: (data: CreateUserInput['user']) => getClient().user.create({ data, select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: userKeys.lists() - }); + queryClient.invalidateQueries({ queryKey: userKeys.lists() }); }, - ...options + ...options, }); -}" +} +" `; exports[`Mutation Hook Generators generateCreateMutationHook generates create mutation hook for table with relationships 1`] = ` @@ -615,74 +609,52 @@ exports[`Mutation Hook Generators generateCreateMutationHook generates create mu * DO NOT EDIT - changes will be overwritten */ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import type { UseMutationOptions } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { Post } from "../types"; -import { postKeys } from "../query-keys"; -import type { PostScope } from "../query-keys"; -import { postMutationKeys } from "../mutation-keys"; -export type { Post } from "../types"; -export const createPostMutationDocument = \` -mutation CreatePostMutation($input: CreatePostInput!) { - createPost(input: $input) { - post { - id - title - content - authorId - published - createdAt - } - } -} -\`; -/** Input type for creating a Post */ -interface PostCreateInput { - title?: string | null; - content?: string | null; - authorId?: string | null; - published?: boolean | null; -} -export interface CreatePostMutationVariables { - input: { - post: PostCreateInput; - }; -} -export interface CreatePostMutationResult { - createPost: { - post: Post; - }; -} +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { getClient } from '../client'; +import { postKeys } from '../query-keys'; +import { postMutationKeys } from '../mutation-keys'; +import type { + PostSelect, + PostWithRelations, + CreatePostInput, +} from '../../orm/input-types'; +import type { + DeepExact, + InferSelectResult, +} from '../../orm/select-types'; + +export type { PostSelect, PostWithRelations, CreatePostInput } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** * Mutation hook for creating a Post - * + * * @example * \`\`\`tsx - * const { mutate, isPending } = useCreatePostMutation(); - * - * mutate({ - * input: { - * post: { - * // ... fields - * }, - * }, + * const { mutate, isPending } = useCreatePostMutation({ + * select: { id: true, name: true }, * }); + * + * mutate({ name: 'New item' }); * \`\`\` */ -export function useCreatePostMutation(options?: Omit, 'mutationFn'>) { +export function useCreatePostMutation( + args?: { select?: DeepExact }, + options?: Omit } }, Error, CreatePostInput['post']>, 'mutationFn'> +) { const queryClient = useQueryClient(); return useMutation({ mutationKey: postMutationKeys.create(), - mutationFn: (variables: CreatePostMutationVariables) => execute(createPostMutationDocument, variables), + mutationFn: (data: CreatePostInput['post']) => getClient().post.create({ data, select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: postKeys.lists() - }); + queryClient.invalidateQueries({ queryKey: postKeys.lists() }); }, - ...options + ...options, }); -}" +} +" `; exports[`Mutation Hook Generators generateCreateMutationHook generates create mutation hook without centralized keys 1`] = ` @@ -692,66 +664,49 @@ exports[`Mutation Hook Generators generateCreateMutationHook generates create mu * DO NOT EDIT - changes will be overwritten */ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import type { UseMutationOptions } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { User } from "../types"; -export type { User } from "../types"; -export const createUserMutationDocument = \` -mutation CreateUserMutation($input: CreateUserInput!) { - createUser(input: $input) { - user { - id - email - name - createdAt - } - } -} -\`; -/** Input type for creating a User */ -interface UserCreateInput { - email?: string | null; - name?: string | null; -} -export interface CreateUserMutationVariables { - input: { - user: UserCreateInput; - }; -} -export interface CreateUserMutationResult { - createUser: { - user: User; - }; -} +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { getClient } from '../client'; +import type { + UserSelect, + UserWithRelations, + CreateUserInput, +} from '../../orm/input-types'; +import type { + DeepExact, + InferSelectResult, +} from '../../orm/select-types'; + +export type { UserSelect, UserWithRelations, CreateUserInput } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** * Mutation hook for creating a User - * + * * @example * \`\`\`tsx - * const { mutate, isPending } = useCreateUserMutation(); - * - * mutate({ - * input: { - * user: { - * // ... fields - * }, - * }, + * const { mutate, isPending } = useCreateUserMutation({ + * select: { id: true, name: true }, * }); + * + * mutate({ name: 'New item' }); * \`\`\` */ -export function useCreateUserMutation(options?: Omit, 'mutationFn'>) { +export function useCreateUserMutation( + args?: { select?: DeepExact }, + options?: Omit } }, Error, CreateUserInput['user']>, 'mutationFn'> +) { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (variables: CreateUserMutationVariables) => execute(createUserMutationDocument, variables), + mutationFn: (data: CreateUserInput['user']) => getClient().user.create({ data, select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ["user", "list"] - }); + queryClient.invalidateQueries({ queryKey: ['user', 'list'] }); }, - ...options + ...options, }); -}" +} +" `; exports[`Mutation Hook Generators generateDeleteMutationHook generates delete mutation hook for simple table 1`] = ` @@ -761,58 +716,52 @@ exports[`Mutation Hook Generators generateDeleteMutationHook generates delete mu * DO NOT EDIT - changes will be overwritten */ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import type { UseMutationOptions } from "@tanstack/react-query"; -import { execute } from "../client"; -import { userKeys } from "../query-keys"; -import { userMutationKeys } from "../mutation-keys"; -export const deleteUserMutationDocument = \` -mutation DeleteUserMutation($input: DeleteUserInput!) { - deleteUser(input: $input) { - clientMutationId - } -} -\`; -export interface DeleteUserMutationVariables { - input: { - id: string; - }; -} -export interface DeleteUserMutationResult { - deleteUser: { - clientMutationId: string | null; - }; -} +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { getClient } from '../client'; +import { userKeys } from '../query-keys'; +import { userMutationKeys } from '../mutation-keys'; +import type { + UserSelect, + UserWithRelations, +} from '../../orm/input-types'; +import type { + DeepExact, + InferSelectResult, +} from '../../orm/select-types'; + +export type { UserSelect, UserWithRelations } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** * Mutation hook for deleting a User - * + * * @example * \`\`\`tsx - * const { mutate, isPending } = useDeleteUserMutation(); - * - * mutate({ - * input: { - * id: 'value-to-delete', - * }, + * const { mutate, isPending } = useDeleteUserMutation({ + * select: { id: true }, * }); + * + * mutate({ id: 'value-to-delete' }); * \`\`\` */ -export function useDeleteUserMutation(options?: Omit, 'mutationFn'>) { +export function useDeleteUserMutation( + args?: { select?: DeepExact }, + options?: Omit } }, Error, { id: string }>, 'mutationFn'> +) { const queryClient = useQueryClient(); return useMutation({ mutationKey: userMutationKeys.all, - mutationFn: (variables: DeleteUserMutationVariables) => execute(deleteUserMutationDocument, variables), + mutationFn: ({ id }: { id: string }) => getClient().user.delete({ where: { id }, select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), onSuccess: (_, variables) => { - queryClient.removeQueries({ - queryKey: userKeys.detail(variables.input.id) - }); - queryClient.invalidateQueries({ - queryKey: userKeys.lists() - }); + queryClient.removeQueries({ queryKey: userKeys.detail(variables.id) }); + queryClient.invalidateQueries({ queryKey: userKeys.lists() }); }, - ...options + ...options, }); -}" +} +" `; exports[`Mutation Hook Generators generateDeleteMutationHook generates delete mutation hook for table with relationships 1`] = ` @@ -822,59 +771,52 @@ exports[`Mutation Hook Generators generateDeleteMutationHook generates delete mu * DO NOT EDIT - changes will be overwritten */ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import type { UseMutationOptions } from "@tanstack/react-query"; -import { execute } from "../client"; -import { postKeys } from "../query-keys"; -import type { PostScope } from "../query-keys"; -import { postMutationKeys } from "../mutation-keys"; -export const deletePostMutationDocument = \` -mutation DeletePostMutation($input: DeletePostInput!) { - deletePost(input: $input) { - clientMutationId - } -} -\`; -export interface DeletePostMutationVariables { - input: { - id: string; - }; -} -export interface DeletePostMutationResult { - deletePost: { - clientMutationId: string | null; - }; -} +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { getClient } from '../client'; +import { postKeys } from '../query-keys'; +import { postMutationKeys } from '../mutation-keys'; +import type { + PostSelect, + PostWithRelations, +} from '../../orm/input-types'; +import type { + DeepExact, + InferSelectResult, +} from '../../orm/select-types'; + +export type { PostSelect, PostWithRelations } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** * Mutation hook for deleting a Post - * + * * @example * \`\`\`tsx - * const { mutate, isPending } = useDeletePostMutation(); - * - * mutate({ - * input: { - * id: 'value-to-delete', - * }, + * const { mutate, isPending } = useDeletePostMutation({ + * select: { id: true }, * }); + * + * mutate({ id: 'value-to-delete' }); * \`\`\` */ -export function useDeletePostMutation(options?: Omit, 'mutationFn'>) { +export function useDeletePostMutation( + args?: { select?: DeepExact }, + options?: Omit } }, Error, { id: string }>, 'mutationFn'> +) { const queryClient = useQueryClient(); return useMutation({ mutationKey: postMutationKeys.all, - mutationFn: (variables: DeletePostMutationVariables) => execute(deletePostMutationDocument, variables), + mutationFn: ({ id }: { id: string }) => getClient().post.delete({ where: { id }, select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), onSuccess: (_, variables) => { - queryClient.removeQueries({ - queryKey: postKeys.detail(variables.input.id) - }); - queryClient.invalidateQueries({ - queryKey: postKeys.lists() - }); + queryClient.removeQueries({ queryKey: postKeys.detail(variables.id) }); + queryClient.invalidateQueries({ queryKey: postKeys.lists() }); }, - ...options + ...options, }); -}" +} +" `; exports[`Mutation Hook Generators generateDeleteMutationHook generates delete mutation hook without centralized keys 1`] = ` @@ -884,55 +826,49 @@ exports[`Mutation Hook Generators generateDeleteMutationHook generates delete mu * DO NOT EDIT - changes will be overwritten */ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import type { UseMutationOptions } from "@tanstack/react-query"; -import { execute } from "../client"; -export const deleteUserMutationDocument = \` -mutation DeleteUserMutation($input: DeleteUserInput!) { - deleteUser(input: $input) { - clientMutationId - } -} -\`; -export interface DeleteUserMutationVariables { - input: { - id: string; - }; -} -export interface DeleteUserMutationResult { - deleteUser: { - clientMutationId: string | null; - }; -} +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { getClient } from '../client'; +import type { + UserSelect, + UserWithRelations, +} from '../../orm/input-types'; +import type { + DeepExact, + InferSelectResult, +} from '../../orm/select-types'; + +export type { UserSelect, UserWithRelations } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** * Mutation hook for deleting a User - * + * * @example * \`\`\`tsx - * const { mutate, isPending } = useDeleteUserMutation(); - * - * mutate({ - * input: { - * id: 'value-to-delete', - * }, + * const { mutate, isPending } = useDeleteUserMutation({ + * select: { id: true }, * }); + * + * mutate({ id: 'value-to-delete' }); * \`\`\` */ -export function useDeleteUserMutation(options?: Omit, 'mutationFn'>) { +export function useDeleteUserMutation( + args?: { select?: DeepExact }, + options?: Omit } }, Error, { id: string }>, 'mutationFn'> +) { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (variables: DeleteUserMutationVariables) => execute(deleteUserMutationDocument, variables), + mutationFn: ({ id }: { id: string }) => getClient().user.delete({ where: { id }, select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), onSuccess: (_, variables) => { - queryClient.removeQueries({ - queryKey: ["user", "detail", variables.input.id] - }); - queryClient.invalidateQueries({ - queryKey: ["user", "list"] - }); + queryClient.removeQueries({ queryKey: ['user', 'detail', variables.id] }); + queryClient.invalidateQueries({ queryKey: ['user', 'list'] }); }, - ...options + ...options, }); -}" +} +" `; exports[`Mutation Hook Generators generateUpdateMutationHook generates update mutation hook for simple table 1`] = ` @@ -942,75 +878,53 @@ exports[`Mutation Hook Generators generateUpdateMutationHook generates update mu * DO NOT EDIT - changes will be overwritten */ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import type { UseMutationOptions } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { User } from "../types"; -import { userKeys } from "../query-keys"; -import { userMutationKeys } from "../mutation-keys"; -export type { User } from "../types"; -export const updateUserMutationDocument = \` -mutation UpdateUserMutation($input: UpdateUserInput!) { - updateUser(input: $input) { - user { - id - email - name - createdAt - } - } -} -\`; -/** Patch type for updating a User - all fields optional */ -interface UserPatch { - email?: string | null; - name?: string | null; - createdAt?: string | null; -} -export interface UpdateUserMutationVariables { - input: { - id: string; - patch: UserPatch; - }; -} -export interface UpdateUserMutationResult { - updateUser: { - user: User; - }; -} +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { getClient } from '../client'; +import { userKeys } from '../query-keys'; +import { userMutationKeys } from '../mutation-keys'; +import type { + UserSelect, + UserWithRelations, + UserPatch, +} from '../../orm/input-types'; +import type { + DeepExact, + InferSelectResult, +} from '../../orm/select-types'; + +export type { UserSelect, UserWithRelations, UserPatch } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** * Mutation hook for updating a User - * + * * @example * \`\`\`tsx - * const { mutate, isPending } = useUpdateUserMutation(); - * - * mutate({ - * input: { - * id: 'value-here', - * patch: { - * // ... fields to update - * }, - * }, + * const { mutate, isPending } = useUpdateUserMutation({ + * select: { id: true, name: true }, * }); + * + * mutate({ id: 'value-here', patch: { name: 'Updated' } }); * \`\`\` */ -export function useUpdateUserMutation(options?: Omit, 'mutationFn'>) { +export function useUpdateUserMutation( + args?: { select?: DeepExact }, + options?: Omit } }, Error, { id: string; patch: UserPatch }>, 'mutationFn'> +) { const queryClient = useQueryClient(); return useMutation({ mutationKey: userMutationKeys.all, - mutationFn: (variables: UpdateUserMutationVariables) => execute(updateUserMutationDocument, variables), + mutationFn: ({ id, patch }: { id: string; patch: UserPatch }) => getClient().user.update({ where: { id }, data: patch, select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), onSuccess: (_, variables) => { - queryClient.invalidateQueries({ - queryKey: userKeys.detail(variables.input.id) - }); - queryClient.invalidateQueries({ - queryKey: userKeys.lists() - }); + queryClient.invalidateQueries({ queryKey: userKeys.detail(variables.id) }); + queryClient.invalidateQueries({ queryKey: userKeys.lists() }); }, - ...options + ...options, }); -}" +} +" `; exports[`Mutation Hook Generators generateUpdateMutationHook generates update mutation hook for table with relationships 1`] = ` @@ -1020,80 +934,53 @@ exports[`Mutation Hook Generators generateUpdateMutationHook generates update mu * DO NOT EDIT - changes will be overwritten */ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import type { UseMutationOptions } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { Post } from "../types"; -import { postKeys } from "../query-keys"; -import type { PostScope } from "../query-keys"; -import { postMutationKeys } from "../mutation-keys"; -export type { Post } from "../types"; -export const updatePostMutationDocument = \` -mutation UpdatePostMutation($input: UpdatePostInput!) { - updatePost(input: $input) { - post { - id - title - content - authorId - published - createdAt - } - } -} -\`; -/** Patch type for updating a Post - all fields optional */ -interface PostPatch { - title?: string | null; - content?: string | null; - authorId?: string | null; - published?: boolean | null; - createdAt?: string | null; -} -export interface UpdatePostMutationVariables { - input: { - id: string; - patch: PostPatch; - }; -} -export interface UpdatePostMutationResult { - updatePost: { - post: Post; - }; -} +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { getClient } from '../client'; +import { postKeys } from '../query-keys'; +import { postMutationKeys } from '../mutation-keys'; +import type { + PostSelect, + PostWithRelations, + PostPatch, +} from '../../orm/input-types'; +import type { + DeepExact, + InferSelectResult, +} from '../../orm/select-types'; + +export type { PostSelect, PostWithRelations, PostPatch } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** * Mutation hook for updating a Post - * + * * @example * \`\`\`tsx - * const { mutate, isPending } = useUpdatePostMutation(); - * - * mutate({ - * input: { - * id: 'value-here', - * patch: { - * // ... fields to update - * }, - * }, + * const { mutate, isPending } = useUpdatePostMutation({ + * select: { id: true, name: true }, * }); + * + * mutate({ id: 'value-here', patch: { name: 'Updated' } }); * \`\`\` */ -export function useUpdatePostMutation(options?: Omit, 'mutationFn'>) { +export function useUpdatePostMutation( + args?: { select?: DeepExact }, + options?: Omit } }, Error, { id: string; patch: PostPatch }>, 'mutationFn'> +) { const queryClient = useQueryClient(); return useMutation({ mutationKey: postMutationKeys.all, - mutationFn: (variables: UpdatePostMutationVariables) => execute(updatePostMutationDocument, variables), + mutationFn: ({ id, patch }: { id: string; patch: PostPatch }) => getClient().post.update({ where: { id }, data: patch, select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), onSuccess: (_, variables) => { - queryClient.invalidateQueries({ - queryKey: postKeys.detail(variables.input.id) - }); - queryClient.invalidateQueries({ - queryKey: postKeys.lists() - }); + queryClient.invalidateQueries({ queryKey: postKeys.detail(variables.id) }); + queryClient.invalidateQueries({ queryKey: postKeys.lists() }); }, - ...options + ...options, }); -}" +} +" `; exports[`Mutation Hook Generators generateUpdateMutationHook generates update mutation hook without centralized keys 1`] = ` @@ -1103,72 +990,50 @@ exports[`Mutation Hook Generators generateUpdateMutationHook generates update mu * DO NOT EDIT - changes will be overwritten */ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import type { UseMutationOptions } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { User } from "../types"; -export type { User } from "../types"; -export const updateUserMutationDocument = \` -mutation UpdateUserMutation($input: UpdateUserInput!) { - updateUser(input: $input) { - user { - id - email - name - createdAt - } - } -} -\`; -/** Patch type for updating a User - all fields optional */ -interface UserPatch { - email?: string | null; - name?: string | null; - createdAt?: string | null; -} -export interface UpdateUserMutationVariables { - input: { - id: string; - patch: UserPatch; - }; -} -export interface UpdateUserMutationResult { - updateUser: { - user: User; - }; -} +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { getClient } from '../client'; +import type { + UserSelect, + UserWithRelations, + UserPatch, +} from '../../orm/input-types'; +import type { + DeepExact, + InferSelectResult, +} from '../../orm/select-types'; + +export type { UserSelect, UserWithRelations, UserPatch } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** * Mutation hook for updating a User - * + * * @example * \`\`\`tsx - * const { mutate, isPending } = useUpdateUserMutation(); - * - * mutate({ - * input: { - * id: 'value-here', - * patch: { - * // ... fields to update - * }, - * }, + * const { mutate, isPending } = useUpdateUserMutation({ + * select: { id: true, name: true }, * }); + * + * mutate({ id: 'value-here', patch: { name: 'Updated' } }); * \`\`\` */ -export function useUpdateUserMutation(options?: Omit, 'mutationFn'>) { +export function useUpdateUserMutation( + args?: { select?: DeepExact }, + options?: Omit } }, Error, { id: string; patch: UserPatch }>, 'mutationFn'> +) { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (variables: UpdateUserMutationVariables) => execute(updateUserMutationDocument, variables), + mutationFn: ({ id, patch }: { id: string; patch: UserPatch }) => getClient().user.update({ where: { id }, data: patch, select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(), onSuccess: (_, variables) => { - queryClient.invalidateQueries({ - queryKey: ["user", "detail", variables.input.id] - }); - queryClient.invalidateQueries({ - queryKey: ["user", "list"] - }); + queryClient.invalidateQueries({ queryKey: ['user', 'detail', variables.id] }); + queryClient.invalidateQueries({ queryKey: ['user', 'list'] }); }, - ...options + ...options, }); -}" +} +" `; exports[`Query Hook Generators generateListQueryHook generates list query hook for simple table 1`] = ` @@ -1178,132 +1043,86 @@ exports[`Query Hook Generators generateListQueryHook generates list query hook f * DO NOT EDIT - changes will be overwritten */ -import { useQuery } from "@tanstack/react-query"; -import type { UseQueryOptions, QueryClient } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { ExecuteOptions } from "../client"; -import type { User, UUIDFilter, StringFilter, DatetimeFilter } from "../types"; -import { userKeys } from "../query-keys"; -export type { User } from "../types"; -export const usersQueryDocument = \` -query UsersQuery($first: Int, $last: Int, $offset: Int, $before: Cursor, $after: Cursor, $filter: UserFilter, $condition: UserCondition, $orderBy: [UsersOrderBy!]) { - users( - first: $first - last: $last - offset: $offset - before: $before - after: $after - filter: $filter - condition: $condition - orderBy: $orderBy - ) { - totalCount - nodes { - id - email - name - createdAt - } - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - } -} -\`; -interface UserFilter { - id?: UUIDFilter; - email?: StringFilter; - name?: StringFilter; - createdAt?: DatetimeFilter; - and?: UserFilter[]; - or?: UserFilter[]; - not?: UserFilter; -} -interface UserCondition { - id?: string; - email?: string; - name?: string; - createdAt?: string; -} -type UsersOrderBy = "ID_ASC" | "ID_DESC" | "EMAIL_ASC" | "EMAIL_DESC" | "NAME_ASC" | "NAME_DESC" | "CREATED_AT_ASC" | "CREATED_AT_DESC" | "NATURAL" | "PRIMARY_KEY_ASC" | "PRIMARY_KEY_DESC"; -export interface UsersQueryVariables { - first?: number; - last?: number; - offset?: number; - before?: string; - after?: string; - filter?: UserFilter; - condition?: UserCondition; - orderBy?: UsersOrderBy[]; -} -export interface UsersQueryResult { - users: { - totalCount: number; - nodes: User[]; - pageInfo: { - hasNextPage: boolean; - hasPreviousPage: boolean; - startCursor: string | null; - endCursor: string | null; - }; - }; -} +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryOptions, QueryClient } from '@tanstack/react-query'; +import { getClient } from '../client'; +import { userKeys } from '../query-keys'; +import type { + UserSelect, + UserWithRelations, + UserFilter, + UsersOrderBy, +} from '../../orm/input-types'; +import type { + FindManyArgs, + DeepExact, + InferSelectResult, + ConnectionResult, +} from '../../orm/select-types'; + +export type { UserSelect, UserWithRelations, UserFilter, UsersOrderBy } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** Query key factory - re-exported from query-keys.ts */ export const usersQueryKey = userKeys.list; + /** * Query hook for fetching User list - * + * * @example * \`\`\`tsx * const { data, isLoading } = useUsersQuery({ + * select: { id: true, name: true }, * first: 10, - * filter: { name: { equalTo: "example" } }, + * where: { name: { equalTo: "example" } }, * orderBy: ['CREATED_AT_DESC'], * }); * \`\`\` */ -export function useUsersQuery(variables?: UsersQueryVariables, options?: Omit, 'queryKey' | 'queryFn'>) { +export function useUsersQuery( + args?: FindManyArgs, UserFilter, UsersOrderBy>, + options?: Omit> }, Error>, 'queryKey' | 'queryFn'> +) { return useQuery({ - queryKey: userKeys.list(variables), - queryFn: () => execute(usersQueryDocument, variables), - ...options + queryKey: userKeys.list(args), + queryFn: () => getClient().user.findMany(args).unwrap(), + ...options, }); } + /** * Fetch User list without React hooks - * + * * @example * \`\`\`ts - * // Direct fetch - * const data = await fetchUsersQuery({ first: 10 }); - * - * // With QueryClient - * const data = await queryClient.fetchQuery({ - * queryKey: usersQueryKey(variables), - * queryFn: () => fetchUsersQuery(variables), - * }); + * const data = await fetchUsersQuery({ first: 10, select: { id: true } }); * \`\`\` */ -export async function fetchUsersQuery(variables?: UsersQueryVariables, options?: ExecuteOptions): Promise { - return execute(usersQueryDocument, variables, options); +export async function fetchUsersQuery( + args?: FindManyArgs, UserFilter, UsersOrderBy>, +) { + return getClient().user.findMany(args).unwrap(); } + /** * Prefetch User list for SSR or cache warming - * + * * @example * \`\`\`ts * await prefetchUsersQuery(queryClient, { first: 10 }); * \`\`\` */ -export async function prefetchUsersQuery(queryClient: QueryClient, variables?: UsersQueryVariables, options?: ExecuteOptions): Promise { +export async function prefetchUsersQuery( + queryClient: QueryClient, + args?: FindManyArgs, UserFilter, UsersOrderBy>, +): Promise { await queryClient.prefetchQuery({ - queryKey: userKeys.list(variables), - queryFn: () => execute(usersQueryDocument, variables, options) + queryKey: userKeys.list(args), + queryFn: () => getClient().user.findMany(args).unwrap(), }); -}" +} +" `; exports[`Query Hook Generators generateListQueryHook generates list query hook for table with relationships 1`] = ` @@ -1313,100 +1132,44 @@ exports[`Query Hook Generators generateListQueryHook generates list query hook f * DO NOT EDIT - changes will be overwritten */ -import { useQuery } from "@tanstack/react-query"; -import type { UseQueryOptions, QueryClient } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { ExecuteOptions } from "../client"; -import type { Post, UUIDFilter, StringFilter, BooleanFilter, DatetimeFilter } from "../types"; -import { postKeys } from "../query-keys"; -import type { PostScope } from "../query-keys"; -export type { Post } from "../types"; -export const postsQueryDocument = \` -query PostsQuery($first: Int, $last: Int, $offset: Int, $before: Cursor, $after: Cursor, $filter: PostFilter, $condition: PostCondition, $orderBy: [PostsOrderBy!]) { - posts( - first: $first - last: $last - offset: $offset - before: $before - after: $after - filter: $filter - condition: $condition - orderBy: $orderBy - ) { - totalCount - nodes { - id - title - content - authorId - published - createdAt - } - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - } -} -\`; -interface PostFilter { - id?: UUIDFilter; - title?: StringFilter; - content?: StringFilter; - authorId?: UUIDFilter; - published?: BooleanFilter; - createdAt?: DatetimeFilter; - and?: PostFilter[]; - or?: PostFilter[]; - not?: PostFilter; -} -interface PostCondition { - id?: string; - title?: string; - content?: string; - authorId?: string; - published?: boolean; - createdAt?: string; -} -type PostsOrderBy = "ID_ASC" | "ID_DESC" | "TITLE_ASC" | "TITLE_DESC" | "CONTENT_ASC" | "CONTENT_DESC" | "AUTHOR_ID_ASC" | "AUTHOR_ID_DESC" | "PUBLISHED_ASC" | "PUBLISHED_DESC" | "CREATED_AT_ASC" | "CREATED_AT_DESC" | "NATURAL" | "PRIMARY_KEY_ASC" | "PRIMARY_KEY_DESC"; -export interface PostsQueryVariables { - first?: number; - last?: number; - offset?: number; - before?: string; - after?: string; - filter?: PostFilter; - condition?: PostCondition; - orderBy?: PostsOrderBy[]; -} -export interface PostsQueryResult { - posts: { - totalCount: number; - nodes: Post[]; - pageInfo: { - hasNextPage: boolean; - hasPreviousPage: boolean; - startCursor: string | null; - endCursor: string | null; - }; - }; -} +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryOptions, QueryClient } from '@tanstack/react-query'; +import { getClient } from '../client'; +import { postKeys } from '../query-keys'; +import type { PostScope } from '../query-keys'; +import type { + PostSelect, + PostWithRelations, + PostFilter, + PostsOrderBy, +} from '../../orm/input-types'; +import type { + FindManyArgs, + DeepExact, + InferSelectResult, + ConnectionResult, +} from '../../orm/select-types'; + +export type { PostSelect, PostWithRelations, PostFilter, PostsOrderBy } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** Query key factory - re-exported from query-keys.ts */ export const postsQueryKey = postKeys.list; + /** * Query hook for fetching Post list - * + * * @example * \`\`\`tsx * const { data, isLoading } = usePostsQuery({ + * select: { id: true, name: true }, * first: 10, - * filter: { name: { equalTo: "example" } }, + * where: { name: { equalTo: "example" } }, * orderBy: ['CREATED_AT_DESC'], * }); * \`\`\` - * + * * @example With scope for hierarchical cache invalidation * \`\`\`tsx * const { data } = usePostsQuery( @@ -1415,49 +1178,51 @@ export const postsQueryKey = postKeys.list; * ); * \`\`\` */ -export function usePostsQuery(variables?: PostsQueryVariables, options?: Omit, 'queryKey' | 'queryFn'> & { scope?: PostScope }) { - const { - scope, - ...queryOptions - } = options ?? {}; +export function usePostsQuery( + args?: FindManyArgs, PostFilter, PostsOrderBy>, + options?: Omit> }, Error>, 'queryKey' | 'queryFn'> & { scope?: PostScope } +) { + const { scope, ...queryOptions } = options ?? {}; return useQuery({ - queryKey: postKeys.list(variables, scope), - queryFn: () => execute(postsQueryDocument, variables), - ...queryOptions + queryKey: postKeys.list(args, scope), + queryFn: () => getClient().post.findMany(args).unwrap(), + ...queryOptions, }); } + /** * Fetch Post list without React hooks - * + * * @example * \`\`\`ts - * // Direct fetch - * const data = await fetchPostsQuery({ first: 10 }); - * - * // With QueryClient - * const data = await queryClient.fetchQuery({ - * queryKey: postsQueryKey(variables), - * queryFn: () => fetchPostsQuery(variables), - * }); + * const data = await fetchPostsQuery({ first: 10, select: { id: true } }); * \`\`\` */ -export async function fetchPostsQuery(variables?: PostsQueryVariables, options?: ExecuteOptions): Promise { - return execute(postsQueryDocument, variables, options); +export async function fetchPostsQuery( + args?: FindManyArgs, PostFilter, PostsOrderBy>, +) { + return getClient().post.findMany(args).unwrap(); } + /** * Prefetch Post list for SSR or cache warming - * + * * @example * \`\`\`ts * await prefetchPostsQuery(queryClient, { first: 10 }); * \`\`\` */ -export async function prefetchPostsQuery(queryClient: QueryClient, variables?: PostsQueryVariables, scope?: PostScope, options?: ExecuteOptions): Promise { +export async function prefetchPostsQuery( + queryClient: QueryClient, + args?: FindManyArgs, PostFilter, PostsOrderBy>, + scope?: PostScope, +): Promise { await queryClient.prefetchQuery({ - queryKey: postKeys.list(variables, scope), - queryFn: () => execute(postsQueryDocument, variables, options) + queryKey: postKeys.list(args, scope), + queryFn: () => getClient().post.findMany(args).unwrap(), }); -}" +} +" `; exports[`Query Hook Generators generateListQueryHook generates list query hook without centralized keys 1`] = ` @@ -1467,130 +1232,84 @@ exports[`Query Hook Generators generateListQueryHook generates list query hook w * DO NOT EDIT - changes will be overwritten */ -import { useQuery } from "@tanstack/react-query"; -import type { UseQueryOptions, QueryClient } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { ExecuteOptions } from "../client"; -import type { User, UUIDFilter, StringFilter, DatetimeFilter } from "../types"; -export type { User } from "../types"; -export const usersQueryDocument = \` -query UsersQuery($first: Int, $last: Int, $offset: Int, $before: Cursor, $after: Cursor, $filter: UserFilter, $condition: UserCondition, $orderBy: [UsersOrderBy!]) { - users( - first: $first - last: $last - offset: $offset - before: $before - after: $after - filter: $filter - condition: $condition - orderBy: $orderBy - ) { - totalCount - nodes { - id - email - name - createdAt - } - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - } -} -\`; -interface UserFilter { - id?: UUIDFilter; - email?: StringFilter; - name?: StringFilter; - createdAt?: DatetimeFilter; - and?: UserFilter[]; - or?: UserFilter[]; - not?: UserFilter; -} -interface UserCondition { - id?: string; - email?: string; - name?: string; - createdAt?: string; -} -type UsersOrderBy = "ID_ASC" | "ID_DESC" | "EMAIL_ASC" | "EMAIL_DESC" | "NAME_ASC" | "NAME_DESC" | "CREATED_AT_ASC" | "CREATED_AT_DESC" | "NATURAL" | "PRIMARY_KEY_ASC" | "PRIMARY_KEY_DESC"; -export interface UsersQueryVariables { - first?: number; - last?: number; - offset?: number; - before?: string; - after?: string; - filter?: UserFilter; - condition?: UserCondition; - orderBy?: UsersOrderBy[]; -} -export interface UsersQueryResult { - users: { - totalCount: number; - nodes: User[]; - pageInfo: { - hasNextPage: boolean; - hasPreviousPage: boolean; - startCursor: string | null; - endCursor: string | null; - }; - }; -} -export const usersQueryKey = (variables?: UsersQueryVariables) => ["user", "list", variables] as const; +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryOptions, QueryClient } from '@tanstack/react-query'; +import { getClient } from '../client'; +import type { + UserSelect, + UserWithRelations, + UserFilter, + UsersOrderBy, +} from '../../orm/input-types'; +import type { + FindManyArgs, + DeepExact, + InferSelectResult, + ConnectionResult, +} from '../../orm/select-types'; + +export type { UserSelect, UserWithRelations, UserFilter, UsersOrderBy } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + +export const usersQueryKey = (variables?: FindManyArgs) => ['user', 'list', variables] as const; + /** * Query hook for fetching User list - * + * * @example * \`\`\`tsx * const { data, isLoading } = useUsersQuery({ + * select: { id: true, name: true }, * first: 10, - * filter: { name: { equalTo: "example" } }, + * where: { name: { equalTo: "example" } }, * orderBy: ['CREATED_AT_DESC'], * }); * \`\`\` */ -export function useUsersQuery(variables?: UsersQueryVariables, options?: Omit, 'queryKey' | 'queryFn'>) { +export function useUsersQuery( + args?: FindManyArgs, UserFilter, UsersOrderBy>, + options?: Omit> }, Error>, 'queryKey' | 'queryFn'> +) { return useQuery({ - queryKey: usersQueryKey(variables), - queryFn: () => execute(usersQueryDocument, variables), - ...options + queryKey: usersQueryKey(args), + queryFn: () => getClient().user.findMany(args).unwrap(), + ...options, }); } + /** * Fetch User list without React hooks - * + * * @example * \`\`\`ts - * // Direct fetch - * const data = await fetchUsersQuery({ first: 10 }); - * - * // With QueryClient - * const data = await queryClient.fetchQuery({ - * queryKey: usersQueryKey(variables), - * queryFn: () => fetchUsersQuery(variables), - * }); + * const data = await fetchUsersQuery({ first: 10, select: { id: true } }); * \`\`\` */ -export async function fetchUsersQuery(variables?: UsersQueryVariables, options?: ExecuteOptions): Promise { - return execute(usersQueryDocument, variables, options); +export async function fetchUsersQuery( + args?: FindManyArgs, UserFilter, UsersOrderBy>, +) { + return getClient().user.findMany(args).unwrap(); } + /** * Prefetch User list for SSR or cache warming - * + * * @example * \`\`\`ts * await prefetchUsersQuery(queryClient, { first: 10 }); * \`\`\` */ -export async function prefetchUsersQuery(queryClient: QueryClient, variables?: UsersQueryVariables, options?: ExecuteOptions): Promise { +export async function prefetchUsersQuery( + queryClient: QueryClient, + args?: FindManyArgs, UserFilter, UsersOrderBy>, +): Promise { await queryClient.prefetchQuery({ - queryKey: usersQueryKey(variables), - queryFn: () => execute(usersQueryDocument, variables, options) + queryKey: usersQueryKey(args), + queryFn: () => getClient().user.findMany(args).unwrap(), }); -}" +} +" `; exports[`Query Hook Generators generateSingleQueryHook generates single query hook for simple table 1`] = ` @@ -1600,71 +1319,80 @@ exports[`Query Hook Generators generateSingleQueryHook generates single query ho * DO NOT EDIT - changes will be overwritten */ -import { useQuery } from "@tanstack/react-query"; -import type { UseQueryOptions, QueryClient } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { ExecuteOptions } from "../client"; -import type { User } from "../types"; -import { userKeys } from "../query-keys"; -export type { User } from "../types"; -export const userQueryDocument = \` -query UserQuery($id: UUID!) { - user(id: $id) { - id - email - name - createdAt - } -} -\`; -export interface UserQueryVariables { - id: string; -} -export interface UserQueryResult { - user: User | null; -} +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryOptions, QueryClient } from '@tanstack/react-query'; +import { getClient } from '../client'; +import { userKeys } from '../query-keys'; +import type { + UserSelect, + UserWithRelations, +} from '../../orm/input-types'; +import type { + DeepExact, + InferSelectResult, +} from '../../orm/select-types'; + +export type { UserSelect, UserWithRelations } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** Query key factory - re-exported from query-keys.ts */ export const userQueryKey = userKeys.detail; + /** * Query hook for fetching a single User - * + * * @example * \`\`\`tsx - * const { data, isLoading } = useUserQuery({ id: 'some-id' }); + * const { data, isLoading } = useUserQuery({ + * id: 'some-id', + * select: { id: true, name: true }, + * }); * \`\`\` */ -export function useUserQuery(variables: UserQueryVariables, options?: Omit, 'queryKey' | 'queryFn'>) { +export function useUserQuery( + args: { id: string; select?: DeepExact }, + options?: Omit | null }, Error>, 'queryKey' | 'queryFn'> +) { return useQuery({ - queryKey: userKeys.detail(variables.id), - queryFn: () => execute(userQueryDocument, variables), - ...options + queryKey: userKeys.detail(args.id), + queryFn: () => getClient().user.findOne(args).unwrap(), + ...options, }); } + /** * Fetch a single User without React hooks - * + * * @example * \`\`\`ts - * const data = await fetchUserQuery({ id: 'some-id' }); + * const data = await fetchUserQuery({ id: 'some-id', select: { id: true } }); * \`\`\` */ -export async function fetchUserQuery(variables: UserQueryVariables, options?: ExecuteOptions): Promise { - return execute(userQueryDocument, variables, options); +export async function fetchUserQuery( + args: { id: string; select?: DeepExact }, +) { + return getClient().user.findOne(args).unwrap(); } + /** * Prefetch a single User for SSR or cache warming - * + * * @example * \`\`\`ts * await prefetchUserQuery(queryClient, { id: 'some-id' }); * \`\`\` */ -export async function prefetchUserQuery(queryClient: QueryClient, variables: UserQueryVariables, options?: ExecuteOptions): Promise { +export async function prefetchUserQuery( + queryClient: QueryClient, + args: { id: string; select?: DeepExact }, +): Promise { await queryClient.prefetchQuery({ - queryKey: userKeys.detail(variables.id), - queryFn: () => execute(userQueryDocument, variables, options) + queryKey: userKeys.detail(args.id), + queryFn: () => getClient().user.findOne(args).unwrap(), }); -}" +} +" `; exports[`Query Hook Generators generateSingleQueryHook generates single query hook for table with relationships 1`] = ` @@ -1674,42 +1402,38 @@ exports[`Query Hook Generators generateSingleQueryHook generates single query ho * DO NOT EDIT - changes will be overwritten */ -import { useQuery } from "@tanstack/react-query"; -import type { UseQueryOptions, QueryClient } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { ExecuteOptions } from "../client"; -import type { Post } from "../types"; -import { postKeys } from "../query-keys"; -import type { PostScope } from "../query-keys"; -export type { Post } from "../types"; -export const postQueryDocument = \` -query PostQuery($id: UUID!) { - post(id: $id) { - id - title - content - authorId - published - createdAt - } -} -\`; -export interface PostQueryVariables { - id: string; -} -export interface PostQueryResult { - post: Post | null; -} +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryOptions, QueryClient } from '@tanstack/react-query'; +import { getClient } from '../client'; +import { postKeys } from '../query-keys'; +import type { PostScope } from '../query-keys'; +import type { + PostSelect, + PostWithRelations, +} from '../../orm/input-types'; +import type { + DeepExact, + InferSelectResult, +} from '../../orm/select-types'; + +export type { PostSelect, PostWithRelations } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + /** Query key factory - re-exported from query-keys.ts */ export const postQueryKey = postKeys.detail; + /** * Query hook for fetching a single Post - * + * * @example * \`\`\`tsx - * const { data, isLoading } = usePostQuery({ id: 'some-id' }); + * const { data, isLoading } = usePostQuery({ + * id: 'some-id', + * select: { id: true, name: true }, + * }); * \`\`\` - * + * * @example With scope for hierarchical cache invalidation * \`\`\`tsx * const { data } = usePostQuery( @@ -1718,42 +1442,51 @@ export const postQueryKey = postKeys.detail; * ); * \`\`\` */ -export function usePostQuery(variables: PostQueryVariables, options?: Omit, 'queryKey' | 'queryFn'> & { scope?: PostScope }) { - const { - scope, - ...queryOptions - } = options ?? {}; +export function usePostQuery( + args: { id: string; select?: DeepExact }, + options?: Omit | null }, Error>, 'queryKey' | 'queryFn'> & { scope?: PostScope } +) { + const { scope, ...queryOptions } = options ?? {}; return useQuery({ - queryKey: postKeys.detail(variables.id, scope), - queryFn: () => execute(postQueryDocument, variables), - ...queryOptions + queryKey: postKeys.detail(args.id, scope), + queryFn: () => getClient().post.findOne(args).unwrap(), + ...queryOptions, }); } + /** * Fetch a single Post without React hooks - * + * * @example * \`\`\`ts - * const data = await fetchPostQuery({ id: 'some-id' }); + * const data = await fetchPostQuery({ id: 'some-id', select: { id: true } }); * \`\`\` */ -export async function fetchPostQuery(variables: PostQueryVariables, options?: ExecuteOptions): Promise { - return execute(postQueryDocument, variables, options); +export async function fetchPostQuery( + args: { id: string; select?: DeepExact }, +) { + return getClient().post.findOne(args).unwrap(); } + /** * Prefetch a single Post for SSR or cache warming - * + * * @example * \`\`\`ts * await prefetchPostQuery(queryClient, { id: 'some-id' }); * \`\`\` */ -export async function prefetchPostQuery(queryClient: QueryClient, variables: PostQueryVariables, scope?: PostScope, options?: ExecuteOptions): Promise { +export async function prefetchPostQuery( + queryClient: QueryClient, + args: { id: string; select?: DeepExact }, + scope?: PostScope, +): Promise { await queryClient.prefetchQuery({ - queryKey: postKeys.detail(variables.id, scope), - queryFn: () => execute(postQueryDocument, variables, options) + queryKey: postKeys.detail(args.id, scope), + queryFn: () => getClient().post.findOne(args).unwrap(), }); -}" +} +" `; exports[`Query Hook Generators generateSingleQueryHook generates single query hook without centralized keys 1`] = ` @@ -1763,69 +1496,78 @@ exports[`Query Hook Generators generateSingleQueryHook generates single query ho * DO NOT EDIT - changes will be overwritten */ -import { useQuery } from "@tanstack/react-query"; -import type { UseQueryOptions, QueryClient } from "@tanstack/react-query"; -import { execute } from "../client"; -import type { ExecuteOptions } from "../client"; -import type { User } from "../types"; -export type { User } from "../types"; -export const userQueryDocument = \` -query UserQuery($id: UUID!) { - user(id: $id) { - id - email - name - createdAt - } -} -\`; -export interface UserQueryVariables { - id: string; -} -export interface UserQueryResult { - user: User | null; -} -export const userQueryKey = (id: string) => ["user", "detail", id] as const; +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryOptions, QueryClient } from '@tanstack/react-query'; +import { getClient } from '../client'; +import type { + UserSelect, + UserWithRelations, +} from '../../orm/input-types'; +import type { + DeepExact, + InferSelectResult, +} from '../../orm/select-types'; + +export type { UserSelect, UserWithRelations } from '../../orm/input-types'; + +const defaultSelect = { id: true } as const; + +export const userQueryKey = (id: string) => ['user', 'detail', id] as const; + /** * Query hook for fetching a single User - * + * * @example * \`\`\`tsx - * const { data, isLoading } = useUserQuery({ id: 'some-id' }); + * const { data, isLoading } = useUserQuery({ + * id: 'some-id', + * select: { id: true, name: true }, + * }); * \`\`\` */ -export function useUserQuery(variables: UserQueryVariables, options?: Omit, 'queryKey' | 'queryFn'>) { +export function useUserQuery( + args: { id: string; select?: DeepExact }, + options?: Omit | null }, Error>, 'queryKey' | 'queryFn'> +) { return useQuery({ - queryKey: userQueryKey(variables.id), - queryFn: () => execute(userQueryDocument, variables), - ...options + queryKey: userQueryKey(args.id), + queryFn: () => getClient().user.findOne(args).unwrap(), + ...options, }); } + /** * Fetch a single User without React hooks - * + * * @example * \`\`\`ts - * const data = await fetchUserQuery({ id: 'some-id' }); + * const data = await fetchUserQuery({ id: 'some-id', select: { id: true } }); * \`\`\` */ -export async function fetchUserQuery(variables: UserQueryVariables, options?: ExecuteOptions): Promise { - return execute(userQueryDocument, variables, options); +export async function fetchUserQuery( + args: { id: string; select?: DeepExact }, +) { + return getClient().user.findOne(args).unwrap(); } + /** * Prefetch a single User for SSR or cache warming - * + * * @example * \`\`\`ts * await prefetchUserQuery(queryClient, { id: 'some-id' }); * \`\`\` */ -export async function prefetchUserQuery(queryClient: QueryClient, variables: UserQueryVariables, options?: ExecuteOptions): Promise { +export async function prefetchUserQuery( + queryClient: QueryClient, + args: { id: string; select?: DeepExact }, +): Promise { await queryClient.prefetchQuery({ - queryKey: userQueryKey(variables.id), - queryFn: () => execute(userQueryDocument, variables, options) + queryKey: userQueryKey(args.id), + queryFn: () => getClient().user.findOne(args).unwrap(), }); -}" +} +" `; exports[`Schema Types Generator generateSchemaTypesFile generates schema types file with empty table types 1`] = ` diff --git a/graphql/codegen/src/__tests__/codegen/client-generator.test.ts b/graphql/codegen/src/__tests__/codegen/client-generator.test.ts index afcf275e4..3bd0fbc02 100644 --- a/graphql/codegen/src/__tests__/codegen/client-generator.test.ts +++ b/graphql/codegen/src/__tests__/codegen/client-generator.test.ts @@ -4,12 +4,12 @@ * Tests the generated ORM client files: client.ts, query-builder.ts, select-types.ts, index.ts */ import { + generateCreateClientFile, generateOrmClientFile, generateQueryBuilderFile, - generateSelectTypesFile, - generateCreateClientFile, + generateSelectTypesFile } from '../../core/codegen/orm/client-generator'; -import type { CleanTable, CleanFieldType, CleanRelations } from '../../types/schema'; +import type { CleanFieldType, CleanRelations,CleanTable } from '../../types/schema'; // ============================================================================ // Test Fixtures @@ -17,14 +17,14 @@ import type { CleanTable, CleanFieldType, CleanRelations } from '../../types/sch const fieldTypes = { uuid: { gqlType: 'UUID', isArray: false } as CleanFieldType, - string: { gqlType: 'String', isArray: false } as CleanFieldType, + string: { gqlType: 'String', isArray: false } as CleanFieldType }; const emptyRelations: CleanRelations = { belongsTo: [], hasOne: [], hasMany: [], - manyToMany: [], + manyToMany: [] }; function createTable(partial: Partial & { name: string }): CleanTable { @@ -34,7 +34,7 @@ function createTable(partial: Partial & { name: string }): CleanTabl relations: partial.relations ?? emptyRelations, query: partial.query, inflection: partial.inflection, - constraints: partial.constraints, + constraints: partial.constraints }; } @@ -91,13 +91,13 @@ describe('client-generator', () => { createTable({ name: 'User', fields: [{ name: 'id', type: fieldTypes.uuid }], - query: { all: 'users', one: 'user', create: 'createUser', update: 'updateUser', delete: 'deleteUser' }, + query: { all: 'users', one: 'user', create: 'createUser', update: 'updateUser', delete: 'deleteUser' } }), createTable({ name: 'Post', fields: [{ name: 'id', type: fieldTypes.uuid }], - query: { all: 'posts', one: 'post', create: 'createPost', update: 'updatePost', delete: 'deletePost' }, - }), + query: { all: 'posts', one: 'post', create: 'createPost', update: 'updatePost', delete: 'deletePost' } + }) ]; const result = generateCreateClientFile(tables, false, false); @@ -114,8 +114,8 @@ describe('client-generator', () => { createTable({ name: 'User', fields: [{ name: 'id', type: fieldTypes.uuid }], - query: { all: 'users', one: 'user', create: 'createUser', update: 'updateUser', delete: 'deleteUser' }, - }), + query: { all: 'users', one: 'user', create: 'createUser', update: 'updateUser', delete: 'deleteUser' } + }) ]; const result = generateCreateClientFile(tables, true, true); @@ -123,8 +123,8 @@ describe('client-generator', () => { expect(result.content).toMatchSnapshot(); expect(result.content).toContain('createQueryOperations'); expect(result.content).toContain('createMutationOperations'); - expect(result.content).toContain("query: createQueryOperations(client)"); - expect(result.content).toContain("mutation: createMutationOperations(client)"); + expect(result.content).toContain('query: createQueryOperations(client)'); + expect(result.content).toContain('mutation: createMutationOperations(client)'); }); }); }); diff --git a/graphql/codegen/src/__tests__/codegen/format-output.test.ts b/graphql/codegen/src/__tests__/codegen/format-output.test.ts index bb8b87099..35169f7fb 100644 --- a/graphql/codegen/src/__tests__/codegen/format-output.test.ts +++ b/graphql/codegen/src/__tests__/codegen/format-output.test.ts @@ -3,8 +3,9 @@ * Verifies that oxfmt formats generated code correctly */ import * as fs from 'node:fs'; -import * as path from 'node:path'; import * as os from 'node:os'; +import * as path from 'node:path'; + import { formatOutput } from '../../core/output'; describe('formatOutput', () => { diff --git a/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts b/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts index d509456fa..241a7ae69 100644 --- a/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts +++ b/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts @@ -6,15 +6,15 @@ * used to validate the AST-based migration produces equivalent results. */ // Jest globals - no import needed -import { generateInputTypesFile, collectInputTypeNames, collectPayloadTypeNames } from '../../core/codegen/orm/input-types-generator'; +import { collectInputTypeNames, collectPayloadTypeNames,generateInputTypesFile } from '../../core/codegen/orm/input-types-generator'; import type { - CleanTable, + CleanArgument, CleanFieldType, CleanRelations, - TypeRegistry, - ResolvedType, - CleanArgument, + CleanTable, CleanTypeRef, + ResolvedType, + TypeRegistry } from '../../types/schema'; // ============================================================================ @@ -32,7 +32,7 @@ const fieldTypes = { json: { gqlType: 'JSON', isArray: false } as CleanFieldType, bigint: { gqlType: 'BigInt', isArray: false } as CleanFieldType, stringArray: { gqlType: 'String', isArray: true } as CleanFieldType, - intArray: { gqlType: 'Int', isArray: true } as CleanFieldType, + intArray: { gqlType: 'Int', isArray: true } as CleanFieldType }; // ============================================================================ @@ -43,7 +43,7 @@ const emptyRelations: CleanRelations = { belongsTo: [], hasOne: [], hasMany: [], - manyToMany: [], + manyToMany: [] }; function createTable(partial: Partial & { name: string }): CleanTable { @@ -53,7 +53,7 @@ function createTable(partial: Partial & { name: string }): CleanTabl relations: partial.relations ?? emptyRelations, query: partial.query, inflection: partial.inflection, - constraints: partial.constraints, + constraints: partial.constraints }; } @@ -89,15 +89,15 @@ const userTable = createTable({ { name: 'age', type: fieldTypes.int }, { name: 'isActive', type: fieldTypes.boolean }, { name: 'createdAt', type: fieldTypes.datetime }, - { name: 'metadata', type: fieldTypes.json }, + { name: 'metadata', type: fieldTypes.json } ], query: { all: 'users', one: 'user', create: 'createUser', update: 'updateUser', - delete: 'deleteUser', - }, + delete: 'deleteUser' + } }); /** @@ -111,7 +111,7 @@ const postTable = createTable({ { name: 'content', type: fieldTypes.string }, { name: 'authorId', type: fieldTypes.uuid }, { name: 'publishedAt', type: fieldTypes.datetime }, - { name: 'tags', type: fieldTypes.stringArray }, + { name: 'tags', type: fieldTypes.stringArray } ], relations: { belongsTo: [ @@ -120,8 +120,8 @@ const postTable = createTable({ isUnique: false, referencesTable: 'User', type: null, - keys: [{ name: 'authorId', type: fieldTypes.uuid }], - }, + keys: [{ name: 'authorId', type: fieldTypes.uuid }] + } ], hasOne: [], hasMany: [ @@ -130,18 +130,18 @@ const postTable = createTable({ isUnique: false, referencedByTable: 'Comment', type: null, - keys: [], - }, + keys: [] + } ], - manyToMany: [], + manyToMany: [] }, query: { all: 'posts', one: 'post', create: 'createPost', update: 'updatePost', - delete: 'deletePost', - }, + delete: 'deletePost' + } }); /** @@ -154,7 +154,7 @@ const commentTable = createTable({ { name: 'body', type: fieldTypes.string }, { name: 'postId', type: fieldTypes.uuid }, { name: 'authorId', type: fieldTypes.uuid }, - { name: 'createdAt', type: fieldTypes.datetime }, + { name: 'createdAt', type: fieldTypes.datetime } ], relations: { belongsTo: [ @@ -163,27 +163,27 @@ const commentTable = createTable({ isUnique: false, referencesTable: 'Post', type: null, - keys: [], + keys: [] }, { fieldName: 'author', isUnique: false, referencesTable: 'User', type: null, - keys: [], - }, + keys: [] + } ], hasOne: [], hasMany: [], - manyToMany: [], + manyToMany: [] }, query: { all: 'comments', one: 'comment', create: 'createComment', update: 'updateComment', - delete: 'deleteComment', - }, + delete: 'deleteComment' + } }); /** @@ -200,18 +200,18 @@ const userTableWithRelations = createTable({ isUnique: false, referencedByTable: 'Post', type: null, - keys: [], + keys: [] }, { fieldName: 'comments', isUnique: false, referencedByTable: 'Comment', type: null, - keys: [], - }, + keys: [] + } ], - manyToMany: [], - }, + manyToMany: [] + } }); /** @@ -222,7 +222,7 @@ const categoryTable = createTable({ fields: [ { name: 'id', type: fieldTypes.uuid }, { name: 'name', type: fieldTypes.string }, - { name: 'slug', type: fieldTypes.string }, + { name: 'slug', type: fieldTypes.string } ], relations: { belongsTo: [], @@ -233,17 +233,17 @@ const categoryTable = createTable({ fieldName: 'posts', rightTable: 'Post', junctionTable: 'PostCategory', - type: null, - }, - ], + type: null + } + ] }, query: { all: 'categories', one: 'category', create: 'createCategory', update: 'updateCategory', - delete: 'deleteCategory', - }, + delete: 'deleteCategory' + } }); /** @@ -255,7 +255,7 @@ const profileTable = createTable({ { name: 'id', type: fieldTypes.uuid }, { name: 'bio', type: fieldTypes.string }, { name: 'userId', type: fieldTypes.uuid }, - { name: 'avatarUrl', type: fieldTypes.string }, + { name: 'avatarUrl', type: fieldTypes.string } ], relations: { belongsTo: [ @@ -264,20 +264,20 @@ const profileTable = createTable({ isUnique: true, referencesTable: 'User', type: null, - keys: [], - }, + keys: [] + } ], hasOne: [], hasMany: [], - manyToMany: [], + manyToMany: [] }, query: { all: 'profiles', one: 'profile', create: 'createProfile', update: 'updateProfile', - delete: 'deleteProfile', - }, + delete: 'deleteProfile' + } }); // User with hasOne to profile @@ -291,8 +291,8 @@ const userTableWithProfile = createTable({ isUnique: true, referencedByTable: 'Profile', type: null, - keys: [], - }, + keys: [] + } ], hasMany: [ { @@ -300,11 +300,11 @@ const userTableWithProfile = createTable({ isUnique: false, referencedByTable: 'Post', type: null, - keys: [], - }, + keys: [] + } ], - manyToMany: [], - }, + manyToMany: [] + } }); // ============================================================================ @@ -318,8 +318,8 @@ const sampleTypeRegistry = createTypeRegistry({ inputFields: [ { name: 'email', type: createNonNull(createTypeRef('SCALAR', 'String')) }, { name: 'password', type: createNonNull(createTypeRef('SCALAR', 'String')) }, - { name: 'rememberMe', type: createTypeRef('SCALAR', 'Boolean') }, - ], + { name: 'rememberMe', type: createTypeRef('SCALAR', 'Boolean') } + ] }, RegisterInput: { kind: 'INPUT_OBJECT', @@ -327,13 +327,13 @@ const sampleTypeRegistry = createTypeRegistry({ inputFields: [ { name: 'email', type: createNonNull(createTypeRef('SCALAR', 'String')) }, { name: 'password', type: createNonNull(createTypeRef('SCALAR', 'String')) }, - { name: 'name', type: createTypeRef('SCALAR', 'String') }, - ], + { name: 'name', type: createTypeRef('SCALAR', 'String') } + ] }, UserRole: { kind: 'ENUM', name: 'UserRole', - enumValues: ['ADMIN', 'USER', 'GUEST'], + enumValues: ['ADMIN', 'USER', 'GUEST'] }, LoginPayload: { kind: 'OBJECT', @@ -341,9 +341,9 @@ const sampleTypeRegistry = createTypeRegistry({ fields: [ { name: 'token', type: createTypeRef('SCALAR', 'String') }, { name: 'user', type: createTypeRef('OBJECT', 'User') }, - { name: 'expiresAt', type: createTypeRef('SCALAR', 'Datetime') }, - ], - }, + { name: 'expiresAt', type: createTypeRef('SCALAR', 'Datetime') } + ] + } }); // ============================================================================ @@ -646,14 +646,14 @@ describe('collectInputTypeNames', () => { const operations = [ { args: [ - { name: 'input', type: createNonNull(createTypeRef('INPUT_OBJECT', 'LoginInput')) }, - ] as CleanArgument[], + { name: 'input', type: createNonNull(createTypeRef('INPUT_OBJECT', 'LoginInput')) } + ] as CleanArgument[] }, { args: [ - { name: 'data', type: createTypeRef('INPUT_OBJECT', 'RegisterInput') }, - ] as CleanArgument[], - }, + { name: 'data', type: createTypeRef('INPUT_OBJECT', 'RegisterInput') } + ] as CleanArgument[] + } ]; const result = collectInputTypeNames(operations); @@ -666,9 +666,9 @@ describe('collectInputTypeNames', () => { const operations = [ { args: [ - { name: 'filter', type: createTypeRef('INPUT_OBJECT', 'UserFilter') }, - ] as CleanArgument[], - }, + { name: 'filter', type: createTypeRef('INPUT_OBJECT', 'UserFilter') } + ] as CleanArgument[] + } ]; const result = collectInputTypeNames(operations); @@ -681,7 +681,7 @@ describe('collectPayloadTypeNames', () => { it('collects Payload type names from operations', () => { const operations = [ { returnType: createTypeRef('OBJECT', 'LoginPayload') }, - { returnType: createTypeRef('OBJECT', 'RegisterPayload') }, + { returnType: createTypeRef('OBJECT', 'RegisterPayload') } ]; const result = collectPayloadTypeNames(operations); @@ -690,14 +690,14 @@ describe('collectPayloadTypeNames', () => { expect(result.has('RegisterPayload')).toBe(true); }); - it('excludes Connection types', () => { + it('includes Connection types', () => { const operations = [ - { returnType: createTypeRef('OBJECT', 'UsersConnection') }, + { returnType: createTypeRef('OBJECT', 'UsersConnection') } ]; const result = collectPayloadTypeNames(operations); - expect(result.has('UsersConnection')).toBe(false); + expect(result.has('UsersConnection')).toBe(true); }); }); @@ -717,7 +717,7 @@ describe('edge cases', () => { it('handles table with only id field', () => { const minimalTable = createTable({ name: 'Minimal', - fields: [{ name: 'id', type: fieldTypes.uuid }], + fields: [{ name: 'id', type: fieldTypes.uuid }] }); const result = generateInputTypesFile(new Map(), new Set(), [minimalTable]); diff --git a/graphql/codegen/src/__tests__/codegen/model-generator.test.ts b/graphql/codegen/src/__tests__/codegen/model-generator.test.ts index 1a27b9062..86de98981 100644 --- a/graphql/codegen/src/__tests__/codegen/model-generator.test.ts +++ b/graphql/codegen/src/__tests__/codegen/model-generator.test.ts @@ -4,7 +4,7 @@ * Tests the generated model classes with findMany, findFirst, create, update, delete methods. */ import { generateModelFile } from '../../core/codegen/orm/model-generator'; -import type { CleanTable, CleanFieldType, CleanRelations } from '../../types/schema'; +import type { CleanFieldType, CleanRelations,CleanTable } from '../../types/schema'; // ============================================================================ // Test Fixtures @@ -15,14 +15,14 @@ const fieldTypes = { string: { gqlType: 'String', isArray: false } as CleanFieldType, int: { gqlType: 'Int', isArray: false } as CleanFieldType, boolean: { gqlType: 'Boolean', isArray: false } as CleanFieldType, - datetime: { gqlType: 'Datetime', isArray: false } as CleanFieldType, + datetime: { gqlType: 'Datetime', isArray: false } as CleanFieldType }; const emptyRelations: CleanRelations = { belongsTo: [], hasOne: [], hasMany: [], - manyToMany: [], + manyToMany: [] }; function createTable(partial: Partial & { name: string }): CleanTable { @@ -32,7 +32,7 @@ function createTable(partial: Partial & { name: string }): CleanTabl relations: partial.relations ?? emptyRelations, query: partial.query, inflection: partial.inflection, - constraints: partial.constraints, + constraints: partial.constraints }; } @@ -49,15 +49,15 @@ describe('model-generator', () => { { name: 'email', type: fieldTypes.string }, { name: 'name', type: fieldTypes.string }, { name: 'isActive', type: fieldTypes.boolean }, - { name: 'createdAt', type: fieldTypes.datetime }, + { name: 'createdAt', type: fieldTypes.datetime } ], query: { all: 'users', one: 'user', create: 'createUser', update: 'updateUser', - delete: 'deleteUser', - }, + delete: 'deleteUser' + } }); const result = generateModelFile(table, false); @@ -73,15 +73,15 @@ describe('model-generator', () => { fields: [ { name: 'id', type: fieldTypes.uuid }, { name: 'action', type: fieldTypes.string }, - { name: 'timestamp', type: fieldTypes.datetime }, + { name: 'timestamp', type: fieldTypes.datetime } ], query: { all: 'auditLogs', one: 'auditLog', create: 'createAuditLog', update: undefined, - delete: undefined, - }, + delete: undefined + } }); const result = generateModelFile(table, false); @@ -99,15 +99,15 @@ describe('model-generator', () => { name: 'Organization', fields: [ { name: 'id', type: fieldTypes.uuid }, - { name: 'name', type: fieldTypes.string }, + { name: 'name', type: fieldTypes.string } ], query: { all: 'allOrganizations', one: 'organizationById', create: 'registerOrganization', update: 'modifyOrganization', - delete: 'removeOrganization', - }, + delete: 'removeOrganization' + } }); const result = generateModelFile(table, false); @@ -125,15 +125,15 @@ describe('model-generator', () => { fields: [ { name: 'id', type: fieldTypes.uuid }, { name: 'name', type: fieldTypes.string }, - { name: 'price', type: fieldTypes.int }, + { name: 'price', type: fieldTypes.int } ], query: { all: 'products', one: 'product', create: 'createProduct', update: 'updateProduct', - delete: 'deleteProduct', - }, + delete: 'deleteProduct' + } }); const result = generateModelFile(table, false); diff --git a/graphql/codegen/src/__tests__/codegen/query-builder.test.ts b/graphql/codegen/src/__tests__/codegen/query-builder.test.ts index b91c420fc..7c0d3b599 100644 --- a/graphql/codegen/src/__tests__/codegen/query-builder.test.ts +++ b/graphql/codegen/src/__tests__/codegen/query-builder.test.ts @@ -5,8 +5,8 @@ * Functions are re-implemented here to avoid ./client import issues. */ import * as t from 'gql-ast'; -import { parseType, print } from 'graphql'; import type { ArgumentNode, FieldNode, VariableDefinitionNode } from 'graphql'; +import { parseType, print } from 'graphql'; // ============================================================================ // Core functions from query-builder.ts (re-implemented for testing) @@ -16,7 +16,7 @@ function buildConnectionSelections(nodeSelections: FieldNode[]): FieldNode[] { return [ t.field({ name: 'nodes', - selectionSet: t.selectionSet({ selections: nodeSelections }), + selectionSet: t.selectionSet({ selections: nodeSelections }) }), t.field({ name: 'totalCount' }), t.field({ @@ -26,10 +26,10 @@ function buildConnectionSelections(nodeSelections: FieldNode[]): FieldNode[] { t.field({ name: 'hasNextPage' }), t.field({ name: 'hasPreviousPage' }), t.field({ name: 'startCursor' }), - t.field({ name: 'endCursor' }), - ], - }), - }), + t.field({ name: 'endCursor' }) + ] + }) + }) ]; } @@ -43,13 +43,13 @@ function addVariable( definitions.push( t.variableDefinition({ variable: t.variable({ name: spec.varName }), - type: parseType(spec.typeName), + type: parseType(spec.typeName) }) ); args.push( t.argument({ name: spec.argName ?? spec.varName, - value: t.variable({ name: spec.varName }), + value: t.variable({ name: spec.varName }) }) ); variables[spec.varName] = spec.value; @@ -66,7 +66,7 @@ function buildSelections(select: Record | undefined): FieldNode fields.push( t.field({ name: key, - selectionSet: t.selectionSet({ selections: buildSelections(nested.select) }), + selectionSet: t.selectionSet({ selections: buildSelections(nested.select) }) }) ); } @@ -104,12 +104,12 @@ function buildFindManyDocument( t.field({ name: queryField, args: queryArgs.length ? queryArgs : undefined, - selectionSet: t.selectionSet({ selections: buildConnectionSelections(selections) }), - }), - ], - }), - }), - ], + selectionSet: t.selectionSet({ selections: buildConnectionSelections(selections) }) + }) + ] + }) + }) + ] }); return { document: print(document), variables }; } @@ -130,8 +130,8 @@ function buildMutationDocument( variableDefinitions: [ t.variableDefinition({ variable: t.variable({ name: 'input' }), - type: parseType(inputTypeName + '!'), - }), + type: parseType(inputTypeName + '!') + }) ], selectionSet: t.selectionSet({ selections: [ @@ -142,15 +142,15 @@ function buildMutationDocument( selections: [ t.field({ name: entityField, - selectionSet: t.selectionSet({ selections }), - }), - ], - }), - }), - ], - }), - }), - ], + selectionSet: t.selectionSet({ selections }) + }) + ] + }) + }) + ] + }) + }) + ] }) ); } @@ -166,7 +166,7 @@ describe('query-builder', () => { id: true, name: true, ignored: false, - profile: { select: { bio: true } }, + profile: { select: { bio: true } } }); expect(result).toHaveLength(3); @@ -199,7 +199,7 @@ describe('query-builder', () => { expect(variables).toEqual({ where: { status: { equalTo: 'active' } }, first: 10, - orderBy: ['NAME_ASC'], + orderBy: ['NAME_ASC'] }); }); }); diff --git a/graphql/codegen/src/__tests__/codegen/query-keys-factory.test.ts b/graphql/codegen/src/__tests__/codegen/query-keys-factory.test.ts index abc8484b9..ad96c2697 100644 --- a/graphql/codegen/src/__tests__/codegen/query-keys-factory.test.ts +++ b/graphql/codegen/src/__tests__/codegen/query-keys-factory.test.ts @@ -6,24 +6,24 @@ * - Mutation keys factory (mutation-keys.ts) * - Cache invalidation helpers (invalidation.ts) */ -import { generateQueryKeysFile } from '../../core/codegen/query-keys'; -import { generateMutationKeysFile } from '../../core/codegen/mutation-keys'; import { generateInvalidationFile } from '../../core/codegen/invalidation'; -import type { CleanTable, CleanFieldType, CleanRelations, CleanOperation, CleanTypeRef } from '../../types/schema'; -import type { QueryKeyConfig, EntityRelationship } from '../../types/config'; +import { generateMutationKeysFile } from '../../core/codegen/mutation-keys'; +import { generateQueryKeysFile } from '../../core/codegen/query-keys'; +import type { EntityRelationship,QueryKeyConfig } from '../../types/config'; +import type { CleanFieldType, CleanOperation, CleanRelations, CleanTable, CleanTypeRef } from '../../types/schema'; const fieldTypes = { uuid: { gqlType: 'UUID', isArray: false } as CleanFieldType, string: { gqlType: 'String', isArray: false } as CleanFieldType, int: { gqlType: 'Int', isArray: false } as CleanFieldType, - datetime: { gqlType: 'Datetime', isArray: false } as CleanFieldType, + datetime: { gqlType: 'Datetime', isArray: false } as CleanFieldType }; const emptyRelations: CleanRelations = { belongsTo: [], hasOne: [], hasMany: [], - manyToMany: [], + manyToMany: [] }; function createTable(partial: Partial & { name: string }): CleanTable { @@ -33,7 +33,7 @@ function createTable(partial: Partial & { name: string }): CleanTabl relations: partial.relations ?? emptyRelations, query: partial.query, inflection: partial.inflection, - constraints: partial.constraints, + constraints: partial.constraints }; } @@ -47,15 +47,15 @@ const simpleUserTable = createTable({ { name: 'id', type: fieldTypes.uuid }, { name: 'email', type: fieldTypes.string }, { name: 'name', type: fieldTypes.string }, - { name: 'createdAt', type: fieldTypes.datetime }, + { name: 'createdAt', type: fieldTypes.datetime } ], query: { all: 'users', one: 'user', create: 'createUser', update: 'updateUser', - delete: 'deleteUser', - }, + delete: 'deleteUser' + } }); const postTable = createTable({ @@ -65,15 +65,15 @@ const postTable = createTable({ { name: 'title', type: fieldTypes.string }, { name: 'content', type: fieldTypes.string }, { name: 'authorId', type: fieldTypes.uuid }, - { name: 'createdAt', type: fieldTypes.datetime }, + { name: 'createdAt', type: fieldTypes.datetime } ], query: { all: 'posts', one: 'post', create: 'createPost', update: 'updatePost', - delete: 'deletePost', - }, + delete: 'deletePost' + } }); const organizationTable = createTable({ @@ -81,15 +81,15 @@ const organizationTable = createTable({ fields: [ { name: 'id', type: fieldTypes.uuid }, { name: 'name', type: fieldTypes.string }, - { name: 'slug', type: fieldTypes.string }, + { name: 'slug', type: fieldTypes.string } ], query: { all: 'organizations', one: 'organization', create: 'createOrganization', update: 'updateOrganization', - delete: 'deleteOrganization', - }, + delete: 'deleteOrganization' + } }); const databaseTable = createTable({ @@ -97,15 +97,15 @@ const databaseTable = createTable({ fields: [ { name: 'id', type: fieldTypes.uuid }, { name: 'name', type: fieldTypes.string }, - { name: 'organizationId', type: fieldTypes.uuid }, + { name: 'organizationId', type: fieldTypes.uuid } ], query: { all: 'databases', one: 'database', create: 'createDatabase', update: 'updateDatabase', - delete: 'deleteDatabase', - }, + delete: 'deleteDatabase' + } }); const tableEntityTable = createTable({ @@ -113,15 +113,15 @@ const tableEntityTable = createTable({ fields: [ { name: 'id', type: fieldTypes.uuid }, { name: 'name', type: fieldTypes.string }, - { name: 'databaseId', type: fieldTypes.uuid }, + { name: 'databaseId', type: fieldTypes.uuid } ], query: { all: 'tables', one: 'table', create: 'createTable', update: 'updateTable', - delete: 'deleteTable', - }, + delete: 'deleteTable' + } }); const fieldTable = createTable({ @@ -130,15 +130,15 @@ const fieldTable = createTable({ { name: 'id', type: fieldTypes.uuid }, { name: 'name', type: fieldTypes.string }, { name: 'tableId', type: fieldTypes.uuid }, - { name: 'type', type: fieldTypes.string }, + { name: 'type', type: fieldTypes.string } ], query: { all: 'fields', one: 'field', create: 'createField', update: 'updateField', - delete: 'deleteField', - }, + delete: 'deleteField' + } }); const simpleConfig: QueryKeyConfig = { @@ -146,17 +146,17 @@ const simpleConfig: QueryKeyConfig = { relationships: {}, generateScopedKeys: true, generateCascadeHelpers: true, - generateMutationKeys: true, + generateMutationKeys: true }; const simpleRelationships: Record = { - post: { parent: 'User', foreignKey: 'authorId' }, + post: { parent: 'User', foreignKey: 'authorId' } }; const hierarchicalRelationships: Record = { database: { parent: 'Organization', foreignKey: 'organizationId' }, table: { parent: 'Database', foreignKey: 'databaseId', ancestors: ['organization'] }, - field: { parent: 'Table', foreignKey: 'tableId', ancestors: ['database', 'organization'] }, + field: { parent: 'Table', foreignKey: 'tableId', ancestors: ['database', 'organization'] } }; const sampleCustomQueries: CleanOperation[] = [ @@ -165,18 +165,18 @@ const sampleCustomQueries: CleanOperation[] = [ kind: 'query', args: [], returnType: createTypeRef('OBJECT', 'User'), - description: 'Get the current authenticated user', + description: 'Get the current authenticated user' }, { name: 'searchUsers', kind: 'query', args: [ { name: 'query', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) }, - { name: 'limit', type: createTypeRef('SCALAR', 'Int') }, + { name: 'limit', type: createTypeRef('SCALAR', 'Int') } ], returnType: createTypeRef('LIST', null, createTypeRef('OBJECT', 'User')), - description: 'Search users by name or email', - }, + description: 'Search users by name or email' + } ]; const sampleCustomMutations: CleanOperation[] = [ @@ -185,18 +185,18 @@ const sampleCustomMutations: CleanOperation[] = [ kind: 'mutation', args: [ { name: 'email', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) }, - { name: 'password', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) }, + { name: 'password', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) } ], returnType: createTypeRef('OBJECT', 'LoginPayload'), - description: 'Authenticate user', + description: 'Authenticate user' }, { name: 'logout', kind: 'mutation', args: [], returnType: createTypeRef('OBJECT', 'LogoutPayload'), - description: 'Log out current user', - }, + description: 'Log out current user' + } ]; describe('generateQueryKeysFile', () => { @@ -204,7 +204,7 @@ describe('generateQueryKeysFile', () => { const result = generateQueryKeysFile({ tables: [simpleUserTable], customQueries: [], - config: simpleConfig, + config: simpleConfig }); expect(result.fileName).toBe('query-keys.ts'); expect(result.content).toMatchSnapshot(); @@ -214,7 +214,7 @@ describe('generateQueryKeysFile', () => { const result = generateQueryKeysFile({ tables: [simpleUserTable, postTable], customQueries: [], - config: simpleConfig, + config: simpleConfig }); expect(result.content).toMatchSnapshot(); }); @@ -225,8 +225,8 @@ describe('generateQueryKeysFile', () => { customQueries: [], config: { ...simpleConfig, - relationships: simpleRelationships, - }, + relationships: simpleRelationships + } }); expect(result.content).toMatchSnapshot(); }); @@ -237,8 +237,8 @@ describe('generateQueryKeysFile', () => { customQueries: [], config: { ...simpleConfig, - relationships: hierarchicalRelationships, - }, + relationships: hierarchicalRelationships + } }); expect(result.content).toMatchSnapshot(); }); @@ -247,7 +247,7 @@ describe('generateQueryKeysFile', () => { const result = generateQueryKeysFile({ tables: [simpleUserTable], customQueries: sampleCustomQueries, - config: simpleConfig, + config: simpleConfig }); expect(result.content).toMatchSnapshot(); }); @@ -259,8 +259,8 @@ describe('generateQueryKeysFile', () => { config: { ...simpleConfig, relationships: simpleRelationships, - generateScopedKeys: false, - }, + generateScopedKeys: false + } }); expect(result.content).toMatchSnapshot(); }); @@ -271,7 +271,7 @@ describe('generateMutationKeysFile', () => { const result = generateMutationKeysFile({ tables: [simpleUserTable], customMutations: [], - config: simpleConfig, + config: simpleConfig }); expect(result.fileName).toBe('mutation-keys.ts'); expect(result.content).toMatchSnapshot(); @@ -281,7 +281,7 @@ describe('generateMutationKeysFile', () => { const result = generateMutationKeysFile({ tables: [simpleUserTable, postTable], customMutations: [], - config: simpleConfig, + config: simpleConfig }); expect(result.content).toMatchSnapshot(); }); @@ -292,8 +292,8 @@ describe('generateMutationKeysFile', () => { customMutations: [], config: { ...simpleConfig, - relationships: simpleRelationships, - }, + relationships: simpleRelationships + } }); expect(result.content).toMatchSnapshot(); }); @@ -302,7 +302,7 @@ describe('generateMutationKeysFile', () => { const result = generateMutationKeysFile({ tables: [simpleUserTable], customMutations: sampleCustomMutations, - config: simpleConfig, + config: simpleConfig }); expect(result.content).toMatchSnapshot(); }); @@ -313,8 +313,8 @@ describe('generateMutationKeysFile', () => { customMutations: [], config: { ...simpleConfig, - relationships: hierarchicalRelationships, - }, + relationships: hierarchicalRelationships + } }); expect(result.content).toMatchSnapshot(); }); @@ -324,7 +324,7 @@ describe('generateInvalidationFile', () => { it('generates invalidation helpers for a single table without relationships', () => { const result = generateInvalidationFile({ tables: [simpleUserTable], - config: simpleConfig, + config: simpleConfig }); expect(result.fileName).toBe('invalidation.ts'); expect(result.content).toMatchSnapshot(); @@ -333,7 +333,7 @@ describe('generateInvalidationFile', () => { it('generates invalidation helpers for multiple tables', () => { const result = generateInvalidationFile({ tables: [simpleUserTable, postTable], - config: simpleConfig, + config: simpleConfig }); expect(result.content).toMatchSnapshot(); }); @@ -343,8 +343,8 @@ describe('generateInvalidationFile', () => { tables: [simpleUserTable, postTable], config: { ...simpleConfig, - relationships: simpleRelationships, - }, + relationships: simpleRelationships + } }); expect(result.content).toMatchSnapshot(); }); @@ -354,8 +354,8 @@ describe('generateInvalidationFile', () => { tables: [organizationTable, databaseTable, tableEntityTable, fieldTable], config: { ...simpleConfig, - relationships: hierarchicalRelationships, - }, + relationships: hierarchicalRelationships + } }); expect(result.content).toMatchSnapshot(); }); @@ -366,8 +366,8 @@ describe('generateInvalidationFile', () => { config: { ...simpleConfig, relationships: simpleRelationships, - generateCascadeHelpers: false, - }, + generateCascadeHelpers: false + } }); expect(result.content).toMatchSnapshot(); }); diff --git a/graphql/codegen/src/__tests__/codegen/react-query-hooks.test.ts b/graphql/codegen/src/__tests__/codegen/react-query-hooks.test.ts index d5e273df6..a8a187295 100644 --- a/graphql/codegen/src/__tests__/codegen/react-query-hooks.test.ts +++ b/graphql/codegen/src/__tests__/codegen/react-query-hooks.test.ts @@ -9,26 +9,26 @@ * - Schema types * - Barrel files */ -import { generateListQueryHook, generateSingleQueryHook } from '../../core/codegen/queries'; -import { generateCreateMutationHook, generateUpdateMutationHook, generateDeleteMutationHook } from '../../core/codegen/mutations'; -import { generateCustomQueryHook } from '../../core/codegen/custom-queries'; -import { generateCustomMutationHook } from '../../core/codegen/custom-mutations'; -import { generateSchemaTypesFile } from '../../core/codegen/schema-types-generator'; import { - generateQueriesBarrel, - generateMutationsBarrel, - generateMainBarrel, - generateCustomQueriesBarrel, generateCustomMutationsBarrel, + generateCustomQueriesBarrel, + generateMainBarrel, + generateMutationsBarrel, + generateQueriesBarrel } from '../../core/codegen/barrel'; +import { generateCustomMutationHook } from '../../core/codegen/custom-mutations'; +import { generateCustomQueryHook } from '../../core/codegen/custom-queries'; +import { generateCreateMutationHook, generateDeleteMutationHook,generateUpdateMutationHook } from '../../core/codegen/mutations'; +import { generateListQueryHook, generateSingleQueryHook } from '../../core/codegen/queries'; +import { generateSchemaTypesFile } from '../../core/codegen/schema-types-generator'; import type { - CleanTable, CleanFieldType, - CleanRelations, CleanOperation, + CleanRelations, + CleanTable, CleanTypeRef, - TypeRegistry, ResolvedType, + TypeRegistry } from '../../types/schema'; const fieldTypes = { @@ -36,14 +36,14 @@ const fieldTypes = { string: { gqlType: 'String', isArray: false } as CleanFieldType, int: { gqlType: 'Int', isArray: false } as CleanFieldType, datetime: { gqlType: 'Datetime', isArray: false } as CleanFieldType, - boolean: { gqlType: 'Boolean', isArray: false } as CleanFieldType, + boolean: { gqlType: 'Boolean', isArray: false } as CleanFieldType }; const emptyRelations: CleanRelations = { belongsTo: [], hasOne: [], hasMany: [], - manyToMany: [], + manyToMany: [] }; function createTable(partial: Partial & { name: string }): CleanTable { @@ -53,7 +53,7 @@ function createTable(partial: Partial & { name: string }): CleanTabl relations: partial.relations ?? emptyRelations, query: partial.query, inflection: partial.inflection, - constraints: partial.constraints, + constraints: partial.constraints }; } @@ -67,15 +67,15 @@ const simpleUserTable = createTable({ { name: 'id', type: fieldTypes.uuid }, { name: 'email', type: fieldTypes.string }, { name: 'name', type: fieldTypes.string }, - { name: 'createdAt', type: fieldTypes.datetime }, + { name: 'createdAt', type: fieldTypes.datetime } ], query: { all: 'users', one: 'user', create: 'createUser', update: 'updateUser', - delete: 'deleteUser', - }, + delete: 'deleteUser' + } }); const postTable = createTable({ @@ -86,15 +86,15 @@ const postTable = createTable({ { name: 'content', type: fieldTypes.string }, { name: 'authorId', type: fieldTypes.uuid }, { name: 'published', type: fieldTypes.boolean }, - { name: 'createdAt', type: fieldTypes.datetime }, + { name: 'createdAt', type: fieldTypes.datetime } ], query: { all: 'posts', one: 'post', create: 'createPost', update: 'updatePost', - delete: 'deletePost', - }, + delete: 'deletePost' + } }); const simpleCustomQueries: CleanOperation[] = [ @@ -103,18 +103,18 @@ const simpleCustomQueries: CleanOperation[] = [ kind: 'query', args: [], returnType: createTypeRef('OBJECT', 'User'), - description: 'Get the current authenticated user', + description: 'Get the current authenticated user' }, { name: 'searchUsers', kind: 'query', args: [ { name: 'query', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) }, - { name: 'limit', type: createTypeRef('SCALAR', 'Int') }, + { name: 'limit', type: createTypeRef('SCALAR', 'Int') } ], returnType: createTypeRef('LIST', null, createTypeRef('OBJECT', 'User')), - description: 'Search users by name or email', - }, + description: 'Search users by name or email' + } ]; const simpleCustomMutations: CleanOperation[] = [ @@ -123,27 +123,27 @@ const simpleCustomMutations: CleanOperation[] = [ kind: 'mutation', args: [ { name: 'email', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) }, - { name: 'password', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) }, + { name: 'password', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) } ], returnType: createTypeRef('OBJECT', 'LoginPayload'), - description: 'Authenticate user', + description: 'Authenticate user' }, { name: 'logout', kind: 'mutation', args: [], returnType: createTypeRef('OBJECT', 'LogoutPayload'), - description: 'Log out current user', + description: 'Log out current user' }, { name: 'register', kind: 'mutation', args: [ - { name: 'input', type: createTypeRef('NON_NULL', null, createTypeRef('INPUT_OBJECT', 'RegisterInput')) }, + { name: 'input', type: createTypeRef('NON_NULL', null, createTypeRef('INPUT_OBJECT', 'RegisterInput')) } ], returnType: createTypeRef('OBJECT', 'RegisterPayload'), - description: 'Register a new user', - }, + description: 'Register a new user' + } ]; function createTypeRegistry(): TypeRegistry { @@ -154,16 +154,16 @@ function createTypeRegistry(): TypeRegistry { name: 'LoginPayload', fields: [ { name: 'token', type: createTypeRef('SCALAR', 'String') }, - { name: 'user', type: createTypeRef('OBJECT', 'User') }, - ], + { name: 'user', type: createTypeRef('OBJECT', 'User') } + ] } as ResolvedType); registry.set('LogoutPayload', { kind: 'OBJECT', name: 'LogoutPayload', fields: [ - { name: 'success', type: createTypeRef('SCALAR', 'Boolean') }, - ], + { name: 'success', type: createTypeRef('SCALAR', 'Boolean') } + ] } as ResolvedType); registry.set('RegisterPayload', { @@ -171,8 +171,8 @@ function createTypeRegistry(): TypeRegistry { name: 'RegisterPayload', fields: [ { name: 'token', type: createTypeRef('SCALAR', 'String') }, - { name: 'user', type: createTypeRef('OBJECT', 'User') }, - ], + { name: 'user', type: createTypeRef('OBJECT', 'User') } + ] } as ResolvedType); registry.set('RegisterInput', { @@ -181,22 +181,22 @@ function createTypeRegistry(): TypeRegistry { inputFields: [ { name: 'email', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) }, { name: 'password', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) }, - { name: 'name', type: createTypeRef('SCALAR', 'String') }, - ], + { name: 'name', type: createTypeRef('SCALAR', 'String') } + ] } as ResolvedType); registry.set('UserRole', { kind: 'ENUM', name: 'UserRole', - enumValues: ['ADMIN', 'USER', 'GUEST'], + enumValues: ['ADMIN', 'USER', 'GUEST'] } as ResolvedType); registry.set('Query', { kind: 'OBJECT', name: 'Query', fields: [ - { name: 'currentUser', type: createTypeRef('OBJECT', 'User') }, - ], + { name: 'currentUser', type: createTypeRef('OBJECT', 'User') } + ] } as ResolvedType); registry.set('Mutation', { @@ -205,8 +205,8 @@ function createTypeRegistry(): TypeRegistry { fields: [ { name: 'login', type: createTypeRef('OBJECT', 'LoginPayload') }, { name: 'logout', type: createTypeRef('OBJECT', 'LogoutPayload') }, - { name: 'register', type: createTypeRef('OBJECT', 'RegisterPayload') }, - ], + { name: 'register', type: createTypeRef('OBJECT', 'RegisterPayload') } + ] } as ResolvedType); return registry; @@ -217,7 +217,7 @@ describe('Query Hook Generators', () => { it('generates list query hook for simple table', () => { const result = generateListQueryHook(simpleUserTable, { reactQueryEnabled: true, - useCentralizedKeys: true, + useCentralizedKeys: true }); expect(result).not.toBeNull(); expect(result.fileName).toBe('useUsersQuery.ts'); @@ -227,7 +227,7 @@ describe('Query Hook Generators', () => { it('generates list query hook without centralized keys', () => { const result = generateListQueryHook(simpleUserTable, { reactQueryEnabled: true, - useCentralizedKeys: false, + useCentralizedKeys: false }); expect(result).not.toBeNull(); expect(result.content).toMatchSnapshot(); @@ -237,7 +237,7 @@ describe('Query Hook Generators', () => { const result = generateListQueryHook(postTable, { reactQueryEnabled: true, useCentralizedKeys: true, - hasRelationships: true, + hasRelationships: true }); expect(result).not.toBeNull(); expect(result.content).toMatchSnapshot(); @@ -248,7 +248,7 @@ describe('Query Hook Generators', () => { it('generates single query hook for simple table', () => { const result = generateSingleQueryHook(simpleUserTable, { reactQueryEnabled: true, - useCentralizedKeys: true, + useCentralizedKeys: true }); expect(result).not.toBeNull(); expect(result.fileName).toBe('useUserQuery.ts'); @@ -258,7 +258,7 @@ describe('Query Hook Generators', () => { it('generates single query hook without centralized keys', () => { const result = generateSingleQueryHook(simpleUserTable, { reactQueryEnabled: true, - useCentralizedKeys: false, + useCentralizedKeys: false }); expect(result).not.toBeNull(); expect(result.content).toMatchSnapshot(); @@ -268,7 +268,7 @@ describe('Query Hook Generators', () => { const result = generateSingleQueryHook(postTable, { reactQueryEnabled: true, useCentralizedKeys: true, - hasRelationships: true, + hasRelationships: true }); expect(result).not.toBeNull(); expect(result.content).toMatchSnapshot(); @@ -281,7 +281,7 @@ describe('Mutation Hook Generators', () => { it('generates create mutation hook for simple table', () => { const result = generateCreateMutationHook(simpleUserTable, { reactQueryEnabled: true, - useCentralizedKeys: true, + useCentralizedKeys: true }); expect(result).not.toBeNull(); expect(result!.fileName).toBe('useCreateUserMutation.ts'); @@ -291,7 +291,7 @@ describe('Mutation Hook Generators', () => { it('generates create mutation hook without centralized keys', () => { const result = generateCreateMutationHook(simpleUserTable, { reactQueryEnabled: true, - useCentralizedKeys: false, + useCentralizedKeys: false }); expect(result).not.toBeNull(); expect(result!.content).toMatchSnapshot(); @@ -300,8 +300,7 @@ describe('Mutation Hook Generators', () => { it('generates create mutation hook for table with relationships', () => { const result = generateCreateMutationHook(postTable, { reactQueryEnabled: true, - useCentralizedKeys: true, - hasRelationships: true, + useCentralizedKeys: true }); expect(result).not.toBeNull(); expect(result!.content).toMatchSnapshot(); @@ -312,7 +311,7 @@ describe('Mutation Hook Generators', () => { it('generates update mutation hook for simple table', () => { const result = generateUpdateMutationHook(simpleUserTable, { reactQueryEnabled: true, - useCentralizedKeys: true, + useCentralizedKeys: true }); expect(result).not.toBeNull(); expect(result!.fileName).toBe('useUpdateUserMutation.ts'); @@ -322,7 +321,7 @@ describe('Mutation Hook Generators', () => { it('generates update mutation hook without centralized keys', () => { const result = generateUpdateMutationHook(simpleUserTable, { reactQueryEnabled: true, - useCentralizedKeys: false, + useCentralizedKeys: false }); expect(result).not.toBeNull(); expect(result!.content).toMatchSnapshot(); @@ -331,8 +330,7 @@ describe('Mutation Hook Generators', () => { it('generates update mutation hook for table with relationships', () => { const result = generateUpdateMutationHook(postTable, { reactQueryEnabled: true, - useCentralizedKeys: true, - hasRelationships: true, + useCentralizedKeys: true }); expect(result).not.toBeNull(); expect(result!.content).toMatchSnapshot(); @@ -343,7 +341,7 @@ describe('Mutation Hook Generators', () => { it('generates delete mutation hook for simple table', () => { const result = generateDeleteMutationHook(simpleUserTable, { reactQueryEnabled: true, - useCentralizedKeys: true, + useCentralizedKeys: true }); expect(result).not.toBeNull(); expect(result!.fileName).toBe('useDeleteUserMutation.ts'); @@ -353,7 +351,7 @@ describe('Mutation Hook Generators', () => { it('generates delete mutation hook without centralized keys', () => { const result = generateDeleteMutationHook(simpleUserTable, { reactQueryEnabled: true, - useCentralizedKeys: false, + useCentralizedKeys: false }); expect(result).not.toBeNull(); expect(result!.content).toMatchSnapshot(); @@ -362,8 +360,7 @@ describe('Mutation Hook Generators', () => { it('generates delete mutation hook for table with relationships', () => { const result = generateDeleteMutationHook(postTable, { reactQueryEnabled: true, - useCentralizedKeys: true, - hasRelationships: true, + useCentralizedKeys: true }); expect(result).not.toBeNull(); expect(result!.content).toMatchSnapshot(); @@ -377,7 +374,7 @@ describe('Custom Query Hook Generators', () => { const result = generateCustomQueryHook({ operation: simpleCustomQueries[0], typeRegistry: createTypeRegistry(), - useCentralizedKeys: true, + useCentralizedKeys: true }); expect(result).not.toBeNull(); expect(result!.fileName).toBe('useCurrentUserQuery.ts'); @@ -388,7 +385,7 @@ describe('Custom Query Hook Generators', () => { const result = generateCustomQueryHook({ operation: simpleCustomQueries[1], typeRegistry: createTypeRegistry(), - useCentralizedKeys: true, + useCentralizedKeys: true }); expect(result).not.toBeNull(); expect(result!.fileName).toBe('useSearchUsersQuery.ts'); @@ -399,7 +396,7 @@ describe('Custom Query Hook Generators', () => { const result = generateCustomQueryHook({ operation: simpleCustomQueries[0], typeRegistry: createTypeRegistry(), - useCentralizedKeys: false, + useCentralizedKeys: false }); expect(result).not.toBeNull(); expect(result!.content).toMatchSnapshot(); @@ -413,7 +410,7 @@ describe('Custom Mutation Hook Generators', () => { const result = generateCustomMutationHook({ operation: simpleCustomMutations[0], typeRegistry: createTypeRegistry(), - useCentralizedKeys: true, + useCentralizedKeys: true }); expect(result).not.toBeNull(); expect(result!.fileName).toBe('useLoginMutation.ts'); @@ -424,7 +421,7 @@ describe('Custom Mutation Hook Generators', () => { const result = generateCustomMutationHook({ operation: simpleCustomMutations[1], typeRegistry: createTypeRegistry(), - useCentralizedKeys: true, + useCentralizedKeys: true }); expect(result).not.toBeNull(); expect(result!.fileName).toBe('useLogoutMutation.ts'); @@ -435,7 +432,7 @@ describe('Custom Mutation Hook Generators', () => { const result = generateCustomMutationHook({ operation: simpleCustomMutations[2], typeRegistry: createTypeRegistry(), - useCentralizedKeys: true, + useCentralizedKeys: true }); expect(result).not.toBeNull(); expect(result!.fileName).toBe('useRegisterMutation.ts'); @@ -446,7 +443,7 @@ describe('Custom Mutation Hook Generators', () => { const result = generateCustomMutationHook({ operation: simpleCustomMutations[0], typeRegistry: createTypeRegistry(), - useCentralizedKeys: false, + useCentralizedKeys: false }); expect(result).not.toBeNull(); expect(result!.content).toMatchSnapshot(); @@ -459,7 +456,7 @@ describe('Schema Types Generator', () => { it('generates schema types file with enums and input objects', () => { const result = generateSchemaTypesFile({ typeRegistry: createTypeRegistry(), - tableTypeNames: new Set(['User', 'Post']), + tableTypeNames: new Set(['User', 'Post']) }); expect(result.fileName).toBe('schema-types.ts'); expect(result.content).toMatchSnapshot(); @@ -468,7 +465,7 @@ describe('Schema Types Generator', () => { it('generates schema types file with empty table types', () => { const result = generateSchemaTypesFile({ typeRegistry: createTypeRegistry(), - tableTypeNames: new Set(), + tableTypeNames: new Set() }); expect(result.content).toMatchSnapshot(); }); @@ -503,27 +500,24 @@ describe('Barrel File Generators', () => { describe('generateMainBarrel', () => { it('generates main barrel with all options enabled', () => { const result = generateMainBarrel([simpleUserTable, postTable], { - hasSchemaTypes: true, hasMutations: true, hasQueryKeys: true, hasMutationKeys: true, - hasInvalidation: true, + hasInvalidation: true }); expect(result).toMatchSnapshot(); }); it('generates main barrel without custom operations', () => { const result = generateMainBarrel([simpleUserTable], { - hasSchemaTypes: false, - hasMutations: true, + hasMutations: true }); expect(result).toMatchSnapshot(); }); it('generates main barrel without mutations', () => { const result = generateMainBarrel([simpleUserTable, postTable], { - hasSchemaTypes: true, - hasMutations: false, + hasMutations: false }); expect(result).toMatchSnapshot(); }); diff --git a/graphql/codegen/src/__tests__/codegen/react-query-optional.test.ts b/graphql/codegen/src/__tests__/codegen/react-query-optional.test.ts index cf21047a6..93143400f 100644 --- a/graphql/codegen/src/__tests__/codegen/react-query-optional.test.ts +++ b/graphql/codegen/src/__tests__/codegen/react-query-optional.test.ts @@ -6,11 +6,11 @@ * - Mutation generators return null (since they require React Query) * - Standalone fetch functions are still generated for queries */ -import { generateListQueryHook, generateSingleQueryHook, generateAllQueryHooks } from '../../core/codegen/queries'; -import { generateCreateMutationHook, generateUpdateMutationHook, generateDeleteMutationHook, generateAllMutationHooks } from '../../core/codegen/mutations'; -import { generateCustomQueryHook, generateAllCustomQueryHooks } from '../../core/codegen/custom-queries'; -import { generateCustomMutationHook, generateAllCustomMutationHooks } from '../../core/codegen/custom-mutations'; -import type { CleanTable, CleanFieldType, CleanRelations, CleanOperation, CleanTypeRef, TypeRegistry } from '../../types/schema'; +import { generateAllCustomMutationHooks,generateCustomMutationHook } from '../../core/codegen/custom-mutations'; +import { generateAllCustomQueryHooks,generateCustomQueryHook } from '../../core/codegen/custom-queries'; +import { generateAllMutationHooks,generateCreateMutationHook, generateDeleteMutationHook, generateUpdateMutationHook } from '../../core/codegen/mutations'; +import { generateAllQueryHooks,generateListQueryHook, generateSingleQueryHook } from '../../core/codegen/queries'; +import type { CleanFieldType, CleanOperation, CleanRelations, CleanTable, CleanTypeRef, TypeRegistry } from '../../types/schema'; // ============================================================================ // Test Fixtures @@ -20,14 +20,14 @@ const fieldTypes = { uuid: { gqlType: 'UUID', isArray: false } as CleanFieldType, string: { gqlType: 'String', isArray: false } as CleanFieldType, int: { gqlType: 'Int', isArray: false } as CleanFieldType, - datetime: { gqlType: 'Datetime', isArray: false } as CleanFieldType, + datetime: { gqlType: 'Datetime', isArray: false } as CleanFieldType }; const emptyRelations: CleanRelations = { belongsTo: [], hasOne: [], hasMany: [], - manyToMany: [], + manyToMany: [] }; function createTable(partial: Partial & { name: string }): CleanTable { @@ -37,7 +37,7 @@ function createTable(partial: Partial & { name: string }): CleanTabl relations: partial.relations ?? emptyRelations, query: partial.query, inflection: partial.inflection, - constraints: partial.constraints, + constraints: partial.constraints }; } @@ -47,15 +47,15 @@ const userTable = createTable({ { name: 'id', type: fieldTypes.uuid }, { name: 'email', type: fieldTypes.string }, { name: 'name', type: fieldTypes.string }, - { name: 'createdAt', type: fieldTypes.datetime }, + { name: 'createdAt', type: fieldTypes.datetime } ], query: { all: 'users', one: 'user', create: 'createUser', update: 'updateUser', - delete: 'deleteUser', - }, + delete: 'deleteUser' + } }); function createTypeRef(kind: CleanTypeRef['kind'], name: string | null, ofType?: CleanTypeRef): CleanTypeRef { @@ -67,7 +67,7 @@ const sampleQueryOperation: CleanOperation = { kind: 'query', args: [], returnType: createTypeRef('OBJECT', 'User'), - description: 'Get the current authenticated user', + description: 'Get the current authenticated user' }; const sampleMutationOperation: CleanOperation = { @@ -75,10 +75,10 @@ const sampleMutationOperation: CleanOperation = { kind: 'mutation', args: [ { name: 'email', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) }, - { name: 'password', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) }, + { name: 'password', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) } ], returnType: createTypeRef('OBJECT', 'LoginPayload'), - description: 'Authenticate user', + description: 'Authenticate user' }; const emptyTypeRegistry: TypeRegistry = new Map(); @@ -111,9 +111,9 @@ describe('Query generators with reactQueryEnabled: false', () => { expect(result.content).toContain('export async function fetchUsersQuery'); }); - it('should still include GraphQL document when disabled', () => { + it('should still include ORM client imports when disabled', () => { const result = generateListQueryHook(userTable, { reactQueryEnabled: false }); - expect(result.content).toContain('usersQueryDocument'); + expect(result.content).toContain('getClient'); }); it('should still include query key factory when disabled', () => { @@ -251,7 +251,7 @@ describe('Custom query generators with reactQueryEnabled: false', () => { const result = generateCustomQueryHook({ operation: sampleQueryOperation, typeRegistry: emptyTypeRegistry, - reactQueryEnabled: false, + reactQueryEnabled: false }); expect(result.content).not.toContain('@tanstack/react-query'); expect(result.content).not.toContain('useQuery'); @@ -261,7 +261,7 @@ describe('Custom query generators with reactQueryEnabled: false', () => { const result = generateCustomQueryHook({ operation: sampleQueryOperation, typeRegistry: emptyTypeRegistry, - reactQueryEnabled: false, + reactQueryEnabled: false }); expect(result.content).not.toContain('export function useCurrentUserQuery'); }); @@ -270,7 +270,7 @@ describe('Custom query generators with reactQueryEnabled: false', () => { const result = generateCustomQueryHook({ operation: sampleQueryOperation, typeRegistry: emptyTypeRegistry, - reactQueryEnabled: false, + reactQueryEnabled: false }); expect(result.content).toContain('export async function fetchCurrentUserQuery'); }); @@ -281,7 +281,7 @@ describe('Custom query generators with reactQueryEnabled: false', () => { const results = generateAllCustomQueryHooks({ operations: [sampleQueryOperation], typeRegistry: emptyTypeRegistry, - reactQueryEnabled: false, + reactQueryEnabled: false }); expect(results.length).toBe(1); expect(results[0].content).not.toContain('@tanstack/react-query'); @@ -299,7 +299,7 @@ describe('Custom mutation generators with reactQueryEnabled: false', () => { const result = generateCustomMutationHook({ operation: sampleMutationOperation, typeRegistry: emptyTypeRegistry, - reactQueryEnabled: false, + reactQueryEnabled: false }); expect(result).toBeNull(); }); @@ -310,7 +310,7 @@ describe('Custom mutation generators with reactQueryEnabled: false', () => { const results = generateAllCustomMutationHooks({ operations: [sampleMutationOperation], typeRegistry: emptyTypeRegistry, - reactQueryEnabled: false, + reactQueryEnabled: false }); expect(results).toEqual([]); }); @@ -326,7 +326,7 @@ describe('Custom mutation generators with reactQueryEnabled: true (default)', () it('should return mutation file by default', () => { const result = generateCustomMutationHook({ operation: sampleMutationOperation, - typeRegistry: emptyTypeRegistry, + typeRegistry: emptyTypeRegistry }); expect(result).not.toBeNull(); expect(result!.content).toContain('useMutation'); diff --git a/graphql/codegen/src/__tests__/codegen/scalars.test.ts b/graphql/codegen/src/__tests__/codegen/scalars.test.ts index 27539d3f4..7c81ae5c9 100644 --- a/graphql/codegen/src/__tests__/codegen/scalars.test.ts +++ b/graphql/codegen/src/__tests__/codegen/scalars.test.ts @@ -2,12 +2,12 @@ * Tests for scalar mappings */ import { - SCALAR_TS_MAP, + BASE_FILTER_TYPE_NAMES, SCALAR_FILTER_MAP, SCALAR_NAMES, - BASE_FILTER_TYPE_NAMES, - scalarToTsType, + SCALAR_TS_MAP, scalarToFilterType, + scalarToTsType } from '../../core/codegen/scalars'; describe('scalars', () => { diff --git a/graphql/codegen/src/__tests__/codegen/schema-types-generator.test.ts b/graphql/codegen/src/__tests__/codegen/schema-types-generator.test.ts index 70fc56de2..526013226 100644 --- a/graphql/codegen/src/__tests__/codegen/schema-types-generator.test.ts +++ b/graphql/codegen/src/__tests__/codegen/schema-types-generator.test.ts @@ -2,7 +2,7 @@ * Snapshot tests for schema-types-generator */ import { generateSchemaTypesFile } from '../../core/codegen/schema-types-generator'; -import type { TypeRegistry, ResolvedType } from '../../types/schema'; +import type { ResolvedType,TypeRegistry } from '../../types/schema'; function createTypeRegistry(types: Array<[string, ResolvedType]>): TypeRegistry { return new Map(types); @@ -12,12 +12,12 @@ describe('schema-types-generator', () => { it('generates enum types as string unions', () => { const registry = createTypeRegistry([ ['Status', { kind: 'ENUM', name: 'Status', enumValues: ['ACTIVE', 'INACTIVE', 'PENDING'] }], - ['Priority', { kind: 'ENUM', name: 'Priority', enumValues: ['LOW', 'MEDIUM', 'HIGH'] }], + ['Priority', { kind: 'ENUM', name: 'Priority', enumValues: ['LOW', 'MEDIUM', 'HIGH'] }] ]); const result = generateSchemaTypesFile({ typeRegistry: registry, - tableTypeNames: new Set(), + tableTypeNames: new Set() }); expect(result.content).toMatchSnapshot(); @@ -34,9 +34,9 @@ describe('schema-types-generator', () => { inputFields: [ { name: 'email', type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String' } } }, { name: 'name', type: { kind: 'SCALAR', name: 'String' } }, - { name: 'age', type: { kind: 'SCALAR', name: 'Int' } }, - ], - }, + { name: 'age', type: { kind: 'SCALAR', name: 'Int' } } + ] + } ], [ 'UpdateUserInput', @@ -45,15 +45,15 @@ describe('schema-types-generator', () => { name: 'UpdateUserInput', inputFields: [ { name: 'id', type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'UUID' } } }, - { name: 'name', type: { kind: 'SCALAR', name: 'String' } }, - ], - }, - ], + { name: 'name', type: { kind: 'SCALAR', name: 'String' } } + ] + } + ] ]); const result = generateSchemaTypesFile({ typeRegistry: registry, - tableTypeNames: new Set(), + tableTypeNames: new Set() }); expect(result.content).toMatchSnapshot(); @@ -61,12 +61,12 @@ describe('schema-types-generator', () => { it('generates union types', () => { const registry = createTypeRegistry([ - ['SearchResult', { kind: 'UNION', name: 'SearchResult', possibleTypes: ['User', 'Post', 'Comment'] }], + ['SearchResult', { kind: 'UNION', name: 'SearchResult', possibleTypes: ['User', 'Post', 'Comment'] }] ]); const result = generateSchemaTypesFile({ typeRegistry: registry, - tableTypeNames: new Set(), + tableTypeNames: new Set() }); expect(result.content).toMatchSnapshot(); @@ -80,9 +80,9 @@ describe('schema-types-generator', () => { kind: 'OBJECT', name: 'Mutation', fields: [ - { name: 'login', type: { kind: 'OBJECT', name: 'LoginPayload' } }, - ], - }, + { name: 'login', type: { kind: 'OBJECT', name: 'LoginPayload' } } + ] + } ], [ 'LoginPayload', @@ -92,15 +92,15 @@ describe('schema-types-generator', () => { fields: [ { name: 'token', type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String' } } }, { name: 'refreshToken', type: { kind: 'SCALAR', name: 'String' } }, - { name: 'user', type: { kind: 'OBJECT', name: 'User' } }, - ], - }, - ], + { name: 'user', type: { kind: 'OBJECT', name: 'User' } } + ] + } + ] ]); const result = generateSchemaTypesFile({ typeRegistry: registry, - tableTypeNames: new Set(['User']), + tableTypeNames: new Set(['User']) }); expect(result.content).toMatchSnapshot(); @@ -111,12 +111,12 @@ describe('schema-types-generator', () => { const registry = createTypeRegistry([ ['User', { kind: 'ENUM', name: 'User', enumValues: ['ADMIN'] }], ['String', { kind: 'ENUM', name: 'String', enumValues: ['A'] }], - ['CustomEnum', { kind: 'ENUM', name: 'CustomEnum', enumValues: ['VALUE_A', 'VALUE_B'] }], + ['CustomEnum', { kind: 'ENUM', name: 'CustomEnum', enumValues: ['VALUE_A', 'VALUE_B'] }] ]); const result = generateSchemaTypesFile({ typeRegistry: registry, - tableTypeNames: new Set(['User']), + tableTypeNames: new Set(['User']) }); expect(result.content).toMatchSnapshot(); diff --git a/graphql/codegen/src/__tests__/codegen/utils.test.ts b/graphql/codegen/src/__tests__/codegen/utils.test.ts index 62dbf33a0..38f9f066d 100644 --- a/graphql/codegen/src/__tests__/codegen/utils.test.ts +++ b/graphql/codegen/src/__tests__/codegen/utils.test.ts @@ -2,25 +2,25 @@ * Tests for codegen utility functions */ import { + getFilterTypeName, + getGeneratedFileHeader, + getOrderByTypeName, + getPrimaryKeyInfo, + getTableNames, + gqlTypeToTs, lcFirst, - ucFirst, toCamelCase, toPascalCase, toScreamingSnake, - getTableNames, - getFilterTypeName, - getOrderByTypeName, - gqlTypeToTs, - getPrimaryKeyInfo, - getGeneratedFileHeader, + ucFirst } from '../../core/codegen/utils'; -import type { CleanTable, CleanRelations } from '../../types/schema'; +import type { CleanRelations,CleanTable } from '../../types/schema'; const emptyRelations: CleanRelations = { belongsTo: [], hasOne: [], hasMany: [], - manyToMany: [], + manyToMany: [] }; // Use any for test fixture overrides to avoid strict type requirements @@ -29,7 +29,7 @@ function createTable(name: string, overrides: Record = {}): Cle name, fields: [], relations: emptyRelations, - ...overrides, + ...overrides } as CleanTable; } @@ -78,7 +78,7 @@ describe('utils', () => { it('uses inflection overrides when provided', () => { const result = getTableNames( createTable('Person', { - inflection: { tableFieldName: 'individual', allRows: 'people' }, + inflection: { tableFieldName: 'individual', allRows: 'people' } }) ); expect(result.singularName).toBe('individual'); @@ -88,7 +88,7 @@ describe('utils', () => { it('uses query.all for plural name', () => { const result = getTableNames( createTable('Child', { - query: { all: 'children', one: 'child', create: 'createChild', update: 'updateChild', delete: 'deleteChild' }, + query: { all: 'children', one: 'child', create: 'createChild', update: 'updateChild', delete: 'deleteChild' } }) ); expect(result.pluralName).toBe('children'); @@ -138,8 +138,8 @@ describe('utils', () => { it('extracts PK from constraints', () => { const table = createTable('User', { constraints: { - primaryKey: [{ name: 'users_pkey', fields: [{ name: 'id', type: { gqlType: 'UUID', isArray: false } }] }], - }, + primaryKey: [{ name: 'users_pkey', fields: [{ name: 'id', type: { gqlType: 'UUID', isArray: false } }] }] + } }); const result = getPrimaryKeyInfo(table); expect(result).toEqual([{ name: 'id', gqlType: 'UUID', tsType: 'string' }]); @@ -147,7 +147,7 @@ describe('utils', () => { it('falls back to id field', () => { const table = createTable('User', { - fields: [{ name: 'id', type: { gqlType: 'UUID', isArray: false } }], + fields: [{ name: 'id', type: { gqlType: 'UUID', isArray: false } }] }); const result = getPrimaryKeyInfo(table); expect(result).toEqual([{ name: 'id', gqlType: 'UUID', tsType: 'string' }]); @@ -161,11 +161,11 @@ describe('utils', () => { name: 'user_roles_pkey', fields: [ { name: 'userId', type: { gqlType: 'UUID', isArray: false } }, - { name: 'roleId', type: { gqlType: 'UUID', isArray: false } }, - ], - }, - ], - }, + { name: 'roleId', type: { gqlType: 'UUID', isArray: false } } + ] + } + ] + } }); const result = getPrimaryKeyInfo(table); expect(result).toHaveLength(2); diff --git a/graphql/codegen/src/__tests__/config/resolve-config.test.ts b/graphql/codegen/src/__tests__/config/resolve-config.test.ts index e7e879345..c3d5cd162 100644 --- a/graphql/codegen/src/__tests__/config/resolve-config.test.ts +++ b/graphql/codegen/src/__tests__/config/resolve-config.test.ts @@ -1,16 +1,16 @@ import type { - GraphQLSDKConfigTarget, + GraphQLSDKConfigTarget } from '../../types/config'; import { - mergeConfig, - getConfigOptions, DEFAULT_CONFIG, + getConfigOptions, + mergeConfig } from '../../types/config'; describe('config resolution', () => { it('resolves config with defaults', () => { const config: GraphQLSDKConfigTarget = { - endpoint: 'https://api.example.com/graphql', + endpoint: 'https://api.example.com/graphql' }; const resolved = getConfigOptions(config); @@ -29,9 +29,9 @@ describe('config resolution', () => { tables: { include: ['User'] }, queryKeys: { relationships: { - database: { parent: 'organization', foreignKey: 'organizationId' }, - }, - }, + database: { parent: 'organization', foreignKey: 'organizationId' } + } + } }; const overrides: GraphQLSDKConfigTarget = { @@ -40,9 +40,9 @@ describe('config resolution', () => { tables: { exclude: ['_internal'] }, queryKeys: { relationships: { - table: { parent: 'database', foreignKey: 'databaseId' }, - }, - }, + table: { parent: 'database', foreignKey: 'databaseId' } + } + } }; const merged = mergeConfig(base, overrides); @@ -50,15 +50,15 @@ describe('config resolution', () => { expect(merged.output).toBe('./generated/custom'); expect(merged.headers).toEqual({ Authorization: 'Bearer base', - 'X-Custom': '1', + 'X-Custom': '1' }); expect(merged.tables).toEqual({ include: ['User'], - exclude: ['_internal'], + exclude: ['_internal'] }); expect(merged.queryKeys?.relationships).toEqual({ database: { parent: 'organization', foreignKey: 'organizationId' }, - table: { parent: 'database', foreignKey: 'databaseId' }, + table: { parent: 'database', foreignKey: 'databaseId' } }); }); }); diff --git a/graphql/codegen/src/__tests__/introspect/infer-tables.test.ts b/graphql/codegen/src/__tests__/introspect/infer-tables.test.ts index af8828802..80091abed 100644 --- a/graphql/codegen/src/__tests__/introspect/infer-tables.test.ts +++ b/graphql/codegen/src/__tests__/introspect/infer-tables.test.ts @@ -6,12 +6,12 @@ */ import { inferTablesFromIntrospection } from '../../core/introspect/infer-tables'; import type { - IntrospectionQueryResponse, - IntrospectionType, - IntrospectionTypeRef, + IntrospectionEnumValue, IntrospectionField, IntrospectionInputValue, - IntrospectionEnumValue, + IntrospectionQueryResponse, + IntrospectionType, + IntrospectionTypeRef } from '../../types/introspection'; // ============================================================================ @@ -99,19 +99,19 @@ function createIntrospection( name: a.name, type: a.type, description: null, - defaultValue: null, + defaultValue: null }) ), deprecationReason: null, description: null, - isDeprecated: false, + isDeprecated: false }); const makeInputField = (f: InputFieldDef): IntrospectionInputValue => ({ name: f.name, type: f.type, description: null, - defaultValue: null, + defaultValue: null }); // Add Query and Mutation types @@ -124,21 +124,21 @@ function createIntrospection( enumValues: null, interfaces: [], possibleTypes: null, - description: null, + description: null }, ...(mutationFields.length > 0 ? [ - { - name: 'Mutation', - kind: 'OBJECT' as const, - fields: mutationFields.map(makeField), - inputFields: null, - enumValues: null, - interfaces: [], - possibleTypes: null, - description: null, - }, - ] + { + name: 'Mutation', + kind: 'OBJECT' as const, + fields: mutationFields.map(makeField), + inputFields: null, + enumValues: null, + interfaces: [], + possibleTypes: null, + description: null + } + ] : []), ...types.map( (t): IntrospectionType => ({ @@ -152,19 +152,19 @@ function createIntrospection( enumValues: t.kind === 'ENUM' ? (t.enumValues ?? []).map( - (v): IntrospectionEnumValue => ({ - name: v, - deprecationReason: null, - description: null, - isDeprecated: false, - }) - ) + (v): IntrospectionEnumValue => ({ + name: v, + deprecationReason: null, + description: null, + isDeprecated: false + }) + ) : null, interfaces: [], possibleTypes: null, - description: null, + description: null }) - ), + ) ]; return { @@ -173,8 +173,8 @@ function createIntrospection( mutationType: mutationFields.length > 0 ? { name: 'Mutation' } : null, subscriptionType: null, types: allTypes, - directives: [], - }, + directives: [] + } }; } @@ -192,8 +192,8 @@ describe('Entity Detection', () => { kind: 'OBJECT', fields: [ { name: 'id', type: nonNull(scalar('UUID')) }, - { name: 'email', type: scalar('String') }, - ], + { name: 'email', type: scalar('String') } + ] }, // UsersConnection type (indicates User is an entity) { @@ -201,15 +201,15 @@ describe('Entity Detection', () => { kind: 'OBJECT', fields: [ { name: 'nodes', type: list(object('User')) }, - { name: 'pageInfo', type: nonNull(object('PageInfo')) }, - ], + { name: 'pageInfo', type: nonNull(object('PageInfo')) } + ] }, // PageInfo (builtin, should be ignored) - { name: 'PageInfo', kind: 'OBJECT', fields: [] }, + { name: 'PageInfo', kind: 'OBJECT', fields: [] } ], [ // Query for users - { name: 'users', type: object('UsersConnection') }, + { name: 'users', type: object('UsersConnection') } ] ); @@ -225,26 +225,26 @@ describe('Entity Detection', () => { { name: 'User', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, { name: 'Post', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, { name: 'PostsConnection', kind: 'OBJECT', fields: [] }, { name: 'Comment', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, - { name: 'CommentsConnection', kind: 'OBJECT', fields: [] }, + { name: 'CommentsConnection', kind: 'OBJECT', fields: [] } ], [ { name: 'users', type: object('UsersConnection') }, { name: 'posts', type: object('PostsConnection') }, - { name: 'comments', type: object('CommentsConnection') }, + { name: 'comments', type: object('CommentsConnection') } ] ); @@ -261,15 +261,15 @@ describe('Entity Detection', () => { { name: 'User', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, // Does not have Connection (should be ignored) { name: 'AuditLog', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], - }, + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] + } ], [{ name: 'users', type: object('UsersConnection') }] ); @@ -297,10 +297,10 @@ describe('Field Extraction', () => { { name: 'email', type: scalar('String') }, { name: 'age', type: scalar('Int') }, { name: 'isActive', type: scalar('Boolean') }, - { name: 'metadata', type: scalar('JSON') }, - ], + { name: 'metadata', type: scalar('JSON') } + ] }, - { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, + { name: 'UsersConnection', kind: 'OBJECT', fields: [] } ], [{ name: 'users', type: object('UsersConnection') }] ); @@ -314,7 +314,7 @@ describe('Field Extraction', () => { 'email', 'age', 'isActive', - 'metadata', + 'metadata' ]); expect(fields.find((f) => f.name === 'id')?.type.gqlType).toBe('UUID'); expect(fields.find((f) => f.name === 'email')?.type.gqlType).toBe('String'); @@ -328,10 +328,10 @@ describe('Field Extraction', () => { kind: 'OBJECT', fields: [ { name: 'id', type: nonNull(scalar('UUID')) }, - { name: 'tags', type: list(scalar('String')) }, - ], + { name: 'tags', type: list(scalar('String')) } + ] }, - { name: 'PostsConnection', kind: 'OBJECT', fields: [] }, + { name: 'PostsConnection', kind: 'OBJECT', fields: [] } ], [{ name: 'posts', type: object('PostsConnection') }] ); @@ -354,27 +354,27 @@ describe('Field Extraction', () => { { name: 'id', type: nonNull(scalar('UUID')) }, { name: 'title', type: scalar('String') }, { name: 'author', type: object('User') }, // belongsTo relation - { name: 'comments', type: object('CommentsConnection') }, // hasMany relation - ], + { name: 'comments', type: object('CommentsConnection') } // hasMany relation + ] }, { name: 'PostsConnection', kind: 'OBJECT', fields: [] }, { name: 'User', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, { name: 'Comment', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, - { name: 'CommentsConnection', kind: 'OBJECT', fields: [] }, + { name: 'CommentsConnection', kind: 'OBJECT', fields: [] } ], [ { name: 'posts', type: object('PostsConnection') }, { name: 'users', type: object('UsersConnection') }, - { name: 'comments', type: object('CommentsConnection') }, + { name: 'comments', type: object('CommentsConnection') } ] ); @@ -402,20 +402,20 @@ describe('Relation Inference', () => { kind: 'OBJECT', fields: [ { name: 'id', type: nonNull(scalar('UUID')) }, - { name: 'author', type: object('User') }, - ], + { name: 'author', type: object('User') } + ] }, { name: 'PostsConnection', kind: 'OBJECT', fields: [] }, { name: 'User', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, - { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, + { name: 'UsersConnection', kind: 'OBJECT', fields: [] } ], [ { name: 'posts', type: object('PostsConnection') }, - { name: 'users', type: object('UsersConnection') }, + { name: 'users', type: object('UsersConnection') } ] ); @@ -435,20 +435,20 @@ describe('Relation Inference', () => { kind: 'OBJECT', fields: [ { name: 'id', type: nonNull(scalar('UUID')) }, - { name: 'posts', type: object('PostsConnection') }, - ], + { name: 'posts', type: object('PostsConnection') } + ] }, { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, { name: 'Post', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, - { name: 'PostsConnection', kind: 'OBJECT', fields: [] }, + { name: 'PostsConnection', kind: 'OBJECT', fields: [] } ], [ { name: 'users', type: object('UsersConnection') }, - { name: 'posts', type: object('PostsConnection') }, + { name: 'posts', type: object('PostsConnection') } ] ); @@ -471,21 +471,21 @@ describe('Relation Inference', () => { // ManyToMany pattern: {entities}By{JunctionTable}{Keys} { name: 'productsByProductCategoryProductIdAndCategoryId', - type: object('ProductsConnection'), - }, - ], + type: object('ProductsConnection') + } + ] }, { name: 'CategoriesConnection', kind: 'OBJECT', fields: [] }, { name: 'Product', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, - { name: 'ProductsConnection', kind: 'OBJECT', fields: [] }, + { name: 'ProductsConnection', kind: 'OBJECT', fields: [] } ], [ { name: 'categories', type: object('CategoriesConnection') }, - { name: 'products', type: object('ProductsConnection') }, + { name: 'products', type: object('ProductsConnection') } ] ); @@ -511,9 +511,9 @@ describe('Query Operation Matching', () => { { name: 'User', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, - { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, + { name: 'UsersConnection', kind: 'OBJECT', fields: [] } ], [{ name: 'allUsers', type: object('UsersConnection') }] ); @@ -529,17 +529,17 @@ describe('Query Operation Matching', () => { { name: 'User', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, - { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, + { name: 'UsersConnection', kind: 'OBJECT', fields: [] } ], [ { name: 'users', type: object('UsersConnection') }, { name: 'user', type: object('User'), - args: [{ name: 'id', type: nonNull(scalar('UUID')) }], - }, + args: [{ name: 'id', type: nonNull(scalar('UUID')) }] + } ] ); @@ -554,12 +554,12 @@ describe('Query Operation Matching', () => { { name: 'User', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, - { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, + { name: 'UsersConnection', kind: 'OBJECT', fields: [] } ], [ - { name: 'users', type: object('UsersConnection') }, + { name: 'users', type: object('UsersConnection') } // No single user query ] ); @@ -579,10 +579,10 @@ describe('Mutation Operation Matching', () => { { name: 'User', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, - { name: 'CreateUserPayload', kind: 'OBJECT', fields: [] }, + { name: 'CreateUserPayload', kind: 'OBJECT', fields: [] } ], [{ name: 'users', type: object('UsersConnection') }], [ @@ -590,9 +590,9 @@ describe('Mutation Operation Matching', () => { name: 'createUser', type: object('CreateUserPayload'), args: [ - { name: 'input', type: nonNull(inputObject('CreateUserInput')) }, - ], - }, + { name: 'input', type: nonNull(inputObject('CreateUserInput')) } + ] + } ] ); @@ -607,11 +607,11 @@ describe('Mutation Operation Matching', () => { { name: 'User', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, { name: 'UpdateUserPayload', kind: 'OBJECT', fields: [] }, - { name: 'DeleteUserPayload', kind: 'OBJECT', fields: [] }, + { name: 'DeleteUserPayload', kind: 'OBJECT', fields: [] } ], [{ name: 'users', type: object('UsersConnection') }], [ @@ -619,16 +619,16 @@ describe('Mutation Operation Matching', () => { name: 'updateUser', type: object('UpdateUserPayload'), args: [ - { name: 'input', type: nonNull(inputObject('UpdateUserInput')) }, - ], + { name: 'input', type: nonNull(inputObject('UpdateUserInput')) } + ] }, { name: 'deleteUser', type: object('DeleteUserPayload'), args: [ - { name: 'input', type: nonNull(inputObject('DeleteUserInput')) }, - ], - }, + { name: 'input', type: nonNull(inputObject('DeleteUserInput')) } + ] + } ] ); @@ -644,15 +644,15 @@ describe('Mutation Operation Matching', () => { { name: 'User', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, - { name: 'UpdateUserPayload', kind: 'OBJECT', fields: [] }, + { name: 'UpdateUserPayload', kind: 'OBJECT', fields: [] } ], [{ name: 'users', type: object('UsersConnection') }], [ { name: 'updateUserById', type: object('UpdateUserPayload') }, - { name: 'updateUser', type: object('UpdateUserPayload') }, + { name: 'updateUser', type: object('UpdateUserPayload') } ] ); @@ -673,9 +673,9 @@ describe('Constraint Inference', () => { { name: 'User', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, - { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, + { name: 'UsersConnection', kind: 'OBJECT', fields: [] } ], [{ name: 'users', type: object('UsersConnection') }] ); @@ -692,14 +692,14 @@ describe('Constraint Inference', () => { { name: 'User', kind: 'OBJECT', - fields: [{ name: 'userId', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'userId', type: nonNull(scalar('UUID')) }] }, { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, { name: 'UpdateUserInput', kind: 'INPUT_OBJECT', - inputFields: [{ name: 'id', type: nonNull(scalar('UUID')) }], - }, + inputFields: [{ name: 'id', type: nonNull(scalar('UUID')) }] + } ], [{ name: 'users', type: object('UsersConnection') }], [{ name: 'updateUser', type: object('UpdateUserPayload') }] @@ -723,12 +723,12 @@ describe('Inflection Building', () => { { name: 'User', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, { name: 'UserFilter', kind: 'INPUT_OBJECT', inputFields: [] }, { name: 'UserPatch', kind: 'INPUT_OBJECT', inputFields: [] }, - { name: 'UpdateUserPayload', kind: 'OBJECT', fields: [] }, + { name: 'UpdateUserPayload', kind: 'OBJECT', fields: [] } ], [{ name: 'users', type: object('UsersConnection') }] ); @@ -750,9 +750,9 @@ describe('Inflection Building', () => { { name: 'User', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, - { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, + { name: 'UsersConnection', kind: 'OBJECT', fields: [] } // No UserFilter, UserPatch, or UpdateUserPayload ], [{ name: 'users', type: object('UsersConnection') }] @@ -776,7 +776,7 @@ describe('Edge Cases', () => { const introspection = createIntrospection( [ // Only built-in types, no entities - { name: 'PageInfo', kind: 'OBJECT', fields: [] }, + { name: 'PageInfo', kind: 'OBJECT', fields: [] } ], [] ); @@ -793,9 +793,9 @@ describe('Edge Cases', () => { { name: 'Orphan', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, - { name: 'OrphansConnection', kind: 'OBJECT', fields: [] }, + { name: 'OrphansConnection', kind: 'OBJECT', fields: [] } ], [] // No query fields ); @@ -814,8 +814,8 @@ describe('Edge Cases', () => { kind: 'OBJECT', fields: [ { name: 'id', type: nonNull(scalar('UUID')) }, - { name: 'posts', type: object('PostsConnection') }, - ], + { name: 'posts', type: object('PostsConnection') } + ] }, { name: 'UsersConnection', kind: 'OBJECT', fields: [] }, { @@ -823,14 +823,14 @@ describe('Edge Cases', () => { kind: 'OBJECT', fields: [ { name: 'id', type: nonNull(scalar('UUID')) }, - { name: 'author', type: object('User') }, - ], + { name: 'author', type: object('User') } + ] }, - { name: 'PostsConnection', kind: 'OBJECT', fields: [] }, + { name: 'PostsConnection', kind: 'OBJECT', fields: [] } ], [ { name: 'users', type: object('UsersConnection') }, - { name: 'posts', type: object('PostsConnection') }, + { name: 'posts', type: object('PostsConnection') } ] ); @@ -852,9 +852,9 @@ describe('Edge Cases', () => { { name: 'Person', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, - { name: 'PeopleConnection', kind: 'OBJECT', fields: [] }, + { name: 'PeopleConnection', kind: 'OBJECT', fields: [] } ], [{ name: 'people', type: object('PeopleConnection') }] ); @@ -875,15 +875,15 @@ describe('Edge Cases', () => { fields: [ { name: 'id', type: nonNull(scalar('UUID')) }, { name: 'street', type: scalar('String') }, - { name: 'city', type: scalar('String') }, - ], + { name: 'city', type: scalar('String') } + ] }, { name: 'AddressesConnection', kind: 'OBJECT', fields: [] }, { name: 'AddressesOrderBy', kind: 'ENUM', - enumValues: ['ID_ASC', 'ID_DESC'], - }, + enumValues: ['ID_ASC', 'ID_DESC'] + } ], [{ name: 'addresses', type: object('AddressesConnection') }] ); @@ -903,10 +903,10 @@ describe('Edge Cases', () => { { name: 'Category', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, { name: 'CategoriesConnection', kind: 'OBJECT', fields: [] }, - { name: 'CategoriesOrderBy', kind: 'ENUM', enumValues: ['ID_ASC'] }, + { name: 'CategoriesOrderBy', kind: 'ENUM', enumValues: ['ID_ASC'] } ], [{ name: 'categories', type: object('CategoriesConnection') }] ); @@ -923,11 +923,11 @@ describe('Edge Cases', () => { { name: 'Status', kind: 'OBJECT', - fields: [{ name: 'id', type: nonNull(scalar('UUID')) }], + fields: [{ name: 'id', type: nonNull(scalar('UUID')) }] }, { name: 'StatusesConnection', kind: 'OBJECT', fields: [] }, // Schema has the actual OrderBy enum - { name: 'StatusesOrderBy', kind: 'ENUM', enumValues: ['ID_ASC'] }, + { name: 'StatusesOrderBy', kind: 'ENUM', enumValues: ['ID_ASC'] } ], [{ name: 'statuses', type: object('StatusesConnection') }] ); diff --git a/graphql/codegen/src/cli/index.ts b/graphql/codegen/src/cli/index.ts index 5ef53ee8c..6698e2a38 100644 --- a/graphql/codegen/src/cli/index.ts +++ b/graphql/codegen/src/cli/index.ts @@ -5,12 +5,12 @@ * This is a thin wrapper around the core generate() function. * All business logic is in the core modules. */ -import { CLI, CLIOptions, Inquirerer, getPackageJson } from 'inquirerer'; +import { CLI, CLIOptions, getPackageJson,Inquirerer } from 'inquirerer'; -import { generate } from '../core/generate'; import { findConfigFile, loadConfigFile } from '../core/config'; +import { generate } from '../core/generate'; import type { GraphQLSDKConfigTarget } from '../types/config'; -import { camelizeArgv, codegenQuestions, printResult, type CodegenAnswers } from './shared'; +import { camelizeArgv, type CodegenAnswers,codegenQuestions, printResult } from './shared'; const usage = ` graphql-codegen - GraphQL SDK generator for Constructive databases @@ -61,6 +61,15 @@ export const commands = async ( const configPath = (argv.config || argv.c || findConfigFile()) as string | undefined; const targetName = (argv.target || argv.t) as string | undefined; + // Collect CLI flags that should override config file settings + const cliOverrides: Partial = {}; + if (argv['react-query'] === true) cliOverrides.reactQuery = true; + if (argv.orm === true) cliOverrides.orm = true; + if (argv.verbose === true || argv.v === true) cliOverrides.verbose = true; + if (argv['dry-run'] === true) cliOverrides.dryRun = true; + if (argv.output || argv.o) cliOverrides.output = (argv.output || argv.o) as string; + if (argv.authorization || argv.a) cliOverrides.authorization = (argv.authorization || argv.a) as string; + // If config file exists, load and run if (configPath) { const loaded = await loadConfigFile(configPath); @@ -87,7 +96,7 @@ export const commands = async ( let hasError = false; for (const name of names) { console.log(`\n[${name}]`); - const result = await generate(targets[name]); + const result = await generate({ ...targets[name], ...cliOverrides }); printResult(result); if (!result.success) hasError = true; } @@ -97,8 +106,8 @@ export const commands = async ( return argv; } - // Single config - const result = await generate(config as GraphQLSDKConfigTarget); + // Single config — merge CLI overrides + const result = await generate({ ...(config as GraphQLSDKConfigTarget), ...cliOverrides }); printResult(result); if (!result.success) process.exit(1); prompter.close(); @@ -114,7 +123,7 @@ export const commands = async ( // Build db config if schemas or apiNames provided const db = (camelized.schemas || camelized.apiNames) ? { schemas: camelized.schemas, - apiNames: camelized.apiNames, + apiNames: camelized.apiNames } : undefined; const result = await generate({ @@ -127,7 +136,7 @@ export const commands = async ( orm: camelized.orm, browserCompatible: camelized.browserCompatible, dryRun: camelized.dryRun, - verbose: camelized.verbose, + verbose: camelized.verbose }); printResult(result); @@ -145,17 +154,17 @@ export const options: Partial = { o: 'output', t: 'target', a: 'authorization', - v: 'verbose', + v: 'verbose' }, boolean: [ - 'help', 'version', 'verbose', 'dry-run', 'react-query', 'orm', 'keep-db', 'browser-compatible', + 'help', 'version', 'verbose', 'dry-run', 'react-query', 'orm', 'keep-db', 'browser-compatible' ], string: [ 'config', 'endpoint', 'schema-file', 'output', 'target', 'authorization', 'pgpm-module-path', 'pgpm-workspace-path', 'pgpm-module-name', - 'schemas', 'api-names', - ], - }, + 'schemas', 'api-names' + ] + } }; if (require.main === module) { diff --git a/graphql/codegen/src/cli/shared.ts b/graphql/codegen/src/cli/shared.ts index f147b9fa8..a1dee66a5 100644 --- a/graphql/codegen/src/cli/shared.ts +++ b/graphql/codegen/src/cli/shared.ts @@ -4,10 +4,11 @@ * These are exported so that packages/cli can use the same questions * and types, ensuring consistency between the two CLIs. */ -import type { Question } from 'inquirerer'; -import type { GenerateResult } from '../core/generate'; import { camelize } from 'inflekt'; import { inflektTree } from 'inflekt/transform-keys'; +import type { Question } from 'inquirerer'; + +import type { GenerateResult } from '../core/generate'; /** * Sanitize function that splits comma-separated strings into arrays @@ -43,13 +44,13 @@ export const codegenQuestions: Question[] = [ name: 'endpoint', message: 'GraphQL endpoint URL', type: 'text', - required: false, + required: false }, { name: 'schema-file', message: 'Path to GraphQL schema file', type: 'text', - required: false, + required: false }, { name: 'output', @@ -57,21 +58,21 @@ export const codegenQuestions: Question[] = [ type: 'text', required: false, default: 'codegen', - useDefault: true, + useDefault: true }, { name: 'schemas', message: 'PostgreSQL schemas (comma-separated)', type: 'text', required: false, - sanitize: splitCommas, + sanitize: splitCommas }, { name: 'api-names', message: 'API names (comma-separated)', type: 'text', required: false, - sanitize: splitCommas, + sanitize: splitCommas }, { name: 'react-query', @@ -79,7 +80,7 @@ export const codegenQuestions: Question[] = [ type: 'confirm', required: false, default: false, - useDefault: true, + useDefault: true }, { name: 'orm', @@ -87,7 +88,7 @@ export const codegenQuestions: Question[] = [ type: 'confirm', required: false, default: false, - useDefault: true, + useDefault: true }, { name: 'browser-compatible', @@ -95,13 +96,13 @@ export const codegenQuestions: Question[] = [ type: 'confirm', required: false, default: true, - useDefault: true, + useDefault: true }, { name: 'authorization', message: 'Authorization header value', type: 'text', - required: false, + required: false }, { name: 'dry-run', @@ -109,7 +110,7 @@ export const codegenQuestions: Question[] = [ type: 'confirm', required: false, default: false, - useDefault: true, + useDefault: true }, { name: 'verbose', @@ -117,8 +118,8 @@ export const codegenQuestions: Question[] = [ type: 'confirm', required: false, default: false, - useDefault: true, - }, + useDefault: true + } ]; /** diff --git a/graphql/codegen/src/client/error.ts b/graphql/codegen/src/client/error.ts index d879712de..368094f84 100644 --- a/graphql/codegen/src/client/error.ts +++ b/graphql/codegen/src/client/error.ts @@ -43,7 +43,7 @@ export const DataErrorType = { EXCLUSION_VIOLATION: 'EXCLUSION_VIOLATION', // Generic errors - UNKNOWN_ERROR: 'UNKNOWN_ERROR', + UNKNOWN_ERROR: 'UNKNOWN_ERROR' } as const; export type DataErrorType = (typeof DataErrorType)[keyof typeof DataErrorType]; @@ -91,36 +91,36 @@ export class DataError extends Error { getUserMessage(): string { switch (this.type) { - case DataErrorType.NETWORK_ERROR: - return 'Network error. Please check your connection and try again.'; - case DataErrorType.TIMEOUT_ERROR: - return 'Request timed out. Please try again.'; - case DataErrorType.UNAUTHORIZED: - return 'You are not authorized. Please log in and try again.'; - case DataErrorType.FORBIDDEN: - return 'You do not have permission to access this resource.'; - case DataErrorType.VALIDATION_FAILED: - return 'Validation failed. Please check your input and try again.'; - case DataErrorType.REQUIRED_FIELD_MISSING: - return this.fieldName - ? `The field "${this.fieldName}" is required.` - : 'A required field is missing.'; - case DataErrorType.UNIQUE_VIOLATION: - return this.fieldName - ? `A record with this ${this.fieldName} already exists.` - : 'A record with this value already exists.'; - case DataErrorType.FOREIGN_KEY_VIOLATION: - return 'This record references a record that does not exist.'; - case DataErrorType.NOT_NULL_VIOLATION: - return this.fieldName - ? `The field "${this.fieldName}" cannot be empty.` - : 'A required field cannot be empty.'; - case DataErrorType.CHECK_VIOLATION: - return this.fieldName - ? `The value for "${this.fieldName}" is not valid.` - : 'The value does not meet the required constraints.'; - default: - return this.message || 'An unexpected error occurred.'; + case DataErrorType.NETWORK_ERROR: + return 'Network error. Please check your connection and try again.'; + case DataErrorType.TIMEOUT_ERROR: + return 'Request timed out. Please try again.'; + case DataErrorType.UNAUTHORIZED: + return 'You are not authorized. Please log in and try again.'; + case DataErrorType.FORBIDDEN: + return 'You do not have permission to access this resource.'; + case DataErrorType.VALIDATION_FAILED: + return 'Validation failed. Please check your input and try again.'; + case DataErrorType.REQUIRED_FIELD_MISSING: + return this.fieldName + ? `The field "${this.fieldName}" is required.` + : 'A required field is missing.'; + case DataErrorType.UNIQUE_VIOLATION: + return this.fieldName + ? `A record with this ${this.fieldName} already exists.` + : 'A record with this value already exists.'; + case DataErrorType.FOREIGN_KEY_VIOLATION: + return 'This record references a record that does not exist.'; + case DataErrorType.NOT_NULL_VIOLATION: + return this.fieldName + ? `The field "${this.fieldName}" cannot be empty.` + : 'A required field cannot be empty.'; + case DataErrorType.CHECK_VIOLATION: + return this.fieldName + ? `The value for "${this.fieldName}" is not valid.` + : 'The value does not meet the required constraints.'; + default: + return this.message || 'An unexpected error occurred.'; } } @@ -148,7 +148,7 @@ export const PG_ERROR_CODES = { DATETIME_FIELD_OVERFLOW: '22008', UNDEFINED_TABLE: '42P01', UNDEFINED_COLUMN: '42703', - INSUFFICIENT_PRIVILEGE: '42501', + INSUFFICIENT_PRIVILEGE: '42501' } as const; // ============================================================================ @@ -187,7 +187,7 @@ export const createError = { new DataError(DataErrorType.NOT_NULL_VIOLATION, message, { fieldName, constraint, code: '23502' }), unknown: (originalError: Error) => - new DataError(DataErrorType.UNKNOWN_ERROR, originalError.message, { originalError }), + new DataError(DataErrorType.UNKNOWN_ERROR, originalError.message, { originalError }) }; // ============================================================================ @@ -293,7 +293,7 @@ export function parseGraphQLError(error: unknown): DataError { code: extCode, fieldName, constraint, - context: gqlError.extensions, + context: gqlError.extensions }); } @@ -303,7 +303,7 @@ export function parseGraphQLError(error: unknown): DataError { code: extCode, fieldName, constraint, - context: gqlError.extensions, + context: gqlError.extensions }); } diff --git a/graphql/codegen/src/client/execute.ts b/graphql/codegen/src/client/execute.ts index 6f59e7cec..1c4441acf 100644 --- a/graphql/codegen/src/client/execute.ts +++ b/graphql/codegen/src/client/execute.ts @@ -5,8 +5,8 @@ import type { DocumentNode } from 'graphql'; import { print } from 'graphql'; +import { createError, type DataError,parseGraphQLError } from './error'; import { TypedDocumentString } from './typed-document'; -import { createError, parseGraphQLError, type DataError } from './error'; // ============================================================================ // Types @@ -69,7 +69,7 @@ export async function execute( endpoint: string, document: TDocument, variables?: VariablesOf, - options: ExecuteOptions = {}, + options: ExecuteOptions = {} ): Promise> { const { headers = {}, timeout = 30000, signal } = options; @@ -88,13 +88,13 @@ export async function execute( headers: { 'Content-Type': 'application/json', Accept: 'application/graphql-response+json, application/json', - ...headers, + ...headers }, body: JSON.stringify({ query: documentToString(document), - ...(variables !== undefined && { variables }), + ...(variables !== undefined && { variables }) }), - signal: combinedSignal, + signal: combinedSignal }); clearTimeout(timeoutId); @@ -182,12 +182,12 @@ export function createGraphQLClient(options: GraphQLClientOptions) { async execute( document: TDocument, variables?: VariablesOf, - options: ExecuteOptions = {}, + options: ExecuteOptions = {} ): Promise> { return execute(endpoint, document, variables, { headers: { ...defaultHeaders, ...options.headers }, timeout: options.timeout ?? defaultTimeout, - signal: options.signal, + signal: options.signal }); }, @@ -196,7 +196,7 @@ export function createGraphQLClient(options: GraphQLClientOptions) { */ getEndpoint(): string { return endpoint; - }, + } }; } diff --git a/graphql/codegen/src/client/index.ts b/graphql/codegen/src/client/index.ts index 53b0bb352..2ec241259 100644 --- a/graphql/codegen/src/client/index.ts +++ b/graphql/codegen/src/client/index.ts @@ -2,24 +2,22 @@ * Client exports */ -export { TypedDocumentString, type DocumentTypeDecoration } from './typed-document'; - export { - DataError, - DataErrorType, createError, - parseGraphQLError, - isDataError, - PG_ERROR_CODES, + DataError, type DataErrorOptions, + DataErrorType, type GraphQLError, + isDataError, + parseGraphQLError, + PG_ERROR_CODES } from './error'; - export { - execute, createGraphQLClient, + execute, type ExecuteOptions, - type GraphQLClientOptions, type GraphQLClient, - type GraphQLResponse, + type GraphQLClientOptions, + type GraphQLResponse } from './execute'; +export { type DocumentTypeDecoration,TypedDocumentString } from './typed-document'; diff --git a/graphql/codegen/src/client/typed-document.ts b/graphql/codegen/src/client/typed-document.ts index ca3f895a4..95350ad96 100644 --- a/graphql/codegen/src/client/typed-document.ts +++ b/graphql/codegen/src/client/typed-document.ts @@ -33,7 +33,7 @@ export class TypedDocumentString this.value = value; this.__meta__ = { hash: this.generateHash(value), - ...meta, + ...meta }; } diff --git a/graphql/codegen/src/core/ast.ts b/graphql/codegen/src/core/ast.ts index d7903bd09..7c3b95db7 100644 --- a/graphql/codegen/src/core/ast.ts +++ b/graphql/codegen/src/core/ast.ts @@ -5,19 +5,18 @@ import type { FieldNode, TypeNode, ValueNode, - VariableDefinitionNode, + VariableDefinitionNode } from 'graphql'; import { camelize, singularize } from 'inflekt'; - import { getCustomAst } from './custom-ast'; import type { ASTFunctionParams, - QueryFieldSelection, MutationASTParams, NestedProperties, ObjectArrayItem, - QueryProperty, + QueryFieldSelection, + QueryProperty } from './types'; const NON_MUTABLE_PROPS = ['createdAt', 'createdBy', 'updatedAt', 'updatedBy']; @@ -32,7 +31,7 @@ const objectToArray = ( isNotNull: obj[k].isNotNull, isArray: obj[k].isArray, isArrayNotNull: obj[k].isArrayNotNull, - properties: obj[k].properties, + properties: obj[k].properties })); interface CreateGqlMutationParams { @@ -52,32 +51,32 @@ const createGqlMutation = ({ selections, variableDefinitions, modelName, - useModel = true, + useModel = true }: CreateGqlMutationParams): DocumentNode => { const opSel: FieldNode[] = !modelName ? [ - t.field({ - name: operationName, - args: selectArgs, - selectionSet: t.selectionSet({ selections }), - }), - ] + t.field({ + name: operationName, + args: selectArgs, + selectionSet: t.selectionSet({ selections }) + }) + ] : [ - t.field({ - name: operationName, - args: selectArgs, - selectionSet: t.selectionSet({ - selections: useModel - ? [ - t.field({ - name: modelName, - selectionSet: t.selectionSet({ selections }), - }), - ] - : selections, - }), - }), - ]; + t.field({ + name: operationName, + args: selectArgs, + selectionSet: t.selectionSet({ + selections: useModel + ? [ + t.field({ + name: modelName, + selectionSet: t.selectionSet({ selections }) + }) + ] + : selections + }) + }) + ]; return t.document({ definitions: [ @@ -85,16 +84,16 @@ const createGqlMutation = ({ operation: 'mutation', name: mutationName, variableDefinitions, - selectionSet: t.selectionSet({ selections: opSel }), - }), - ], + selectionSet: t.selectionSet({ selections: opSel }) + }) + ] }); }; export const getAll = ({ queryName, operationName, - selection, + selection }: ASTFunctionParams): DocumentNode => { const selections = getSelections(selection); @@ -104,15 +103,15 @@ export const getAll = ({ selectionSet: t.selectionSet({ selections: [ t.field({ - name: 'totalCount', + name: 'totalCount' }), t.field({ name: 'nodes', - selectionSet: t.selectionSet({ selections }), - }), - ], - }), - }), + selectionSet: t.selectionSet({ selections }) + }) + ] + }) + }) ]; const ast = t.document({ @@ -120,9 +119,9 @@ export const getAll = ({ t.operationDefinition({ operation: 'query', name: queryName, - selectionSet: t.selectionSet({ selections: opSel }), - }), - ], + selectionSet: t.selectionSet({ selections: opSel }) + }) + ] }); return ast; @@ -131,7 +130,7 @@ export const getAll = ({ export const getCount = ({ queryName, operationName, - query, + query }: Omit): DocumentNode => { const Singular = query.model; const Filter = `${Singular}Filter`; @@ -140,17 +139,17 @@ export const getCount = ({ const variableDefinitions: VariableDefinitionNode[] = [ t.variableDefinition({ variable: t.variable({ name: 'condition' }), - type: t.namedType({ type: Condition }), + type: t.namedType({ type: Condition }) }), t.variableDefinition({ variable: t.variable({ name: 'filter' }), - type: t.namedType({ type: Filter }), - }), + type: t.namedType({ type: Filter }) + }) ]; const args: ArgumentNode[] = [ t.argument({ name: 'condition', value: t.variable({ name: 'condition' }) }), - t.argument({ name: 'filter', value: t.variable({ name: 'filter' }) }), + t.argument({ name: 'filter', value: t.variable({ name: 'filter' }) }) ]; // PostGraphile supports totalCount through connections @@ -161,11 +160,11 @@ export const getCount = ({ selectionSet: t.selectionSet({ selections: [ t.field({ - name: 'totalCount', - }), - ], - }), - }), + name: 'totalCount' + }) + ] + }) + }) ]; const ast = t.document({ @@ -174,9 +173,9 @@ export const getCount = ({ operation: 'query', name: queryName, variableDefinitions, - selectionSet: t.selectionSet({ selections: opSel }), - }), - ], + selectionSet: t.selectionSet({ selections: opSel }) + }) + ] }); return ast; @@ -187,7 +186,7 @@ export const getMany = ({ queryName, operationName, query, - selection, + selection }: ASTFunctionParams): DocumentNode => { const Singular = query.model; const Plural = @@ -200,38 +199,38 @@ export const getMany = ({ const variableDefinitions: VariableDefinitionNode[] = [ t.variableDefinition({ variable: t.variable({ name: 'first' }), - type: t.namedType({ type: 'Int' }), + type: t.namedType({ type: 'Int' }) }), t.variableDefinition({ variable: t.variable({ name: 'last' }), - type: t.namedType({ type: 'Int' }), + type: t.namedType({ type: 'Int' }) }), t.variableDefinition({ variable: t.variable({ name: 'after' }), - type: t.namedType({ type: 'Cursor' }), + type: t.namedType({ type: 'Cursor' }) }), t.variableDefinition({ variable: t.variable({ name: 'before' }), - type: t.namedType({ type: 'Cursor' }), + type: t.namedType({ type: 'Cursor' }) }), t.variableDefinition({ variable: t.variable({ name: 'offset' }), - type: t.namedType({ type: 'Int' }), + type: t.namedType({ type: 'Int' }) }), t.variableDefinition({ variable: t.variable({ name: 'condition' }), - type: t.namedType({ type: Condition }), + type: t.namedType({ type: Condition }) }), t.variableDefinition({ variable: t.variable({ name: 'filter' }), - type: t.namedType({ type: Filter }), + type: t.namedType({ type: Filter }) }), t.variableDefinition({ variable: t.variable({ name: 'orderBy' }), type: t.listType({ - type: t.nonNullType({ type: t.namedType({ type: OrderBy }) }), - }), - }), + type: t.nonNullType({ type: t.namedType({ type: OrderBy }) }) + }) + }) ]; const args: ArgumentNode[] = [ @@ -242,44 +241,44 @@ export const getMany = ({ t.argument({ name: 'before', value: t.variable({ name: 'before' }) }), t.argument({ name: 'condition', - value: t.variable({ name: 'condition' }), + value: t.variable({ name: 'condition' }) }), t.argument({ name: 'filter', value: t.variable({ name: 'filter' }) }), - t.argument({ name: 'orderBy', value: t.variable({ name: 'orderBy' }) }), + t.argument({ name: 'orderBy', value: t.variable({ name: 'orderBy' }) }) ]; const pageInfoFields: FieldNode[] = [ t.field({ name: 'hasNextPage' }), t.field({ name: 'hasPreviousPage' }), t.field({ name: 'endCursor' }), - t.field({ name: 'startCursor' }), + t.field({ name: 'startCursor' }) ]; const dataField: FieldNode = builder?._edges ? t.field({ - name: 'edges', - selectionSet: t.selectionSet({ - selections: [ - t.field({ name: 'cursor' }), - t.field({ - name: 'node', - selectionSet: t.selectionSet({ selections }), - }), - ], - }), + name: 'edges', + selectionSet: t.selectionSet({ + selections: [ + t.field({ name: 'cursor' }), + t.field({ + name: 'node', + selectionSet: t.selectionSet({ selections }) + }) + ] }) + }) : t.field({ - name: 'nodes', - selectionSet: t.selectionSet({ selections }), - }); + name: 'nodes', + selectionSet: t.selectionSet({ selections }) + }); const connectionFields: FieldNode[] = [ t.field({ name: 'totalCount' }), t.field({ name: 'pageInfo', - selectionSet: t.selectionSet({ selections: pageInfoFields }), + selectionSet: t.selectionSet({ selections: pageInfoFields }) }), - dataField, + dataField ]; const ast = t.document({ @@ -293,12 +292,12 @@ export const getMany = ({ t.field({ name: operationName, args, - selectionSet: t.selectionSet({ selections: connectionFields }), - }), - ], - }), - }), - ], + selectionSet: t.selectionSet({ selections: connectionFields }) + }) + ] + }) + }) + ] }); return ast; @@ -308,7 +307,7 @@ export const getOne = ({ queryName, operationName, query, - selection, + selection }: ASTFunctionParams): DocumentNode => { const variableDefinitions: VariableDefinitionNode[] = Object.keys( query.properties @@ -321,7 +320,7 @@ export const getOne = ({ type: fieldType, isNotNull, isArray, - isArrayNotNull, + isArrayNotNull } = field; let type: TypeNode = t.namedType({ type: fieldType }); if (isNotNull) type = t.nonNullType({ type }); @@ -331,7 +330,7 @@ export const getOne = ({ } return t.variableDefinition({ variable: t.variable({ name: fieldName }), - type, + type }); }); @@ -342,7 +341,7 @@ export const getOne = ({ .map((field) => { return t.argument({ name: field.name, - value: t.variable({ name: field.name }), + value: t.variable({ name: field.name }) }); }); @@ -352,8 +351,8 @@ export const getOne = ({ t.field({ name: operationName, args: selectArgs, - selectionSet: t.selectionSet({ selections }), - }), + selectionSet: t.selectionSet({ selections }) + }) ]; const ast = t.document({ @@ -362,9 +361,9 @@ export const getOne = ({ operation: 'query', name: queryName, variableDefinitions, - selectionSet: t.selectionSet({ selections: opSel }), - }), - ], + selectionSet: t.selectionSet({ selections: opSel }) + }) + ] }); return ast; }; @@ -373,7 +372,7 @@ export const createOne = ({ mutationName, operationName, mutation, - selection, + selection }: MutationASTParams): DocumentNode => { if (!mutation.properties?.input?.properties) { throw new Error(`No input field for mutation: ${mutationName}`); @@ -412,14 +411,14 @@ export const createOne = ({ fields: attrs.map((field) => t.objectField({ name: field.name, - value: t.variable({ name: field.name }), + value: t.variable({ name: field.name }) }) - ), - }), - }), - ], - }), - }), + ) + }) + }) + ] + }) + }) ]; const selections = selection @@ -432,7 +431,7 @@ export const createOne = ({ selectArgs, selections, variableDefinitions, - modelName, + modelName }); return ast; @@ -442,7 +441,7 @@ export const patchOne = ({ mutationName, operationName, mutation, - selection, + selection }: MutationASTParams): DocumentNode => { if (!mutation.properties?.input?.properties) { throw new Error(`No input field for mutation: ${mutationName}`); @@ -479,7 +478,7 @@ export const patchOne = ({ ...patchByAttrs.map((field) => t.objectField({ name: field.name, - value: t.variable({ name: field.name }), + value: t.variable({ name: field.name }) }) ), t.objectField({ @@ -490,14 +489,14 @@ export const patchOne = ({ .map((field) => t.objectField({ name: field.name, - value: t.variable({ name: field.name }), + value: t.variable({ name: field.name }) }) - ), - }), - }), - ], - }), - }), + ) + }) + }) + ] + }) + }) ]; const selections = selection @@ -510,7 +509,7 @@ export const patchOne = ({ selectArgs, selections, variableDefinitions, - modelName, + modelName }); return ast; @@ -519,7 +518,7 @@ export const patchOne = ({ export const deleteOne = ({ mutationName, operationName, - mutation, + mutation }: Omit): DocumentNode => { if (!mutation.properties?.input?.properties) { throw new Error(`No input field for mutation: ${mutationName}`); @@ -548,7 +547,7 @@ export const deleteOne = ({ } return t.variableDefinition({ variable: t.variable({ name: fieldName }), - type, + type }); } ); @@ -560,11 +559,11 @@ export const deleteOne = ({ fields: deleteAttrs.map((f) => t.objectField({ name: f.name, - value: t.variable({ name: f.name }), + value: t.variable({ name: f.name }) }) - ), - }), - }), + ) + }) + }) ]; // so we can support column select grants plugin @@ -576,7 +575,7 @@ export const deleteOne = ({ selections, useModel: false, variableDefinitions, - modelName, + modelName }); return ast; @@ -614,7 +613,7 @@ export function getSelections(selection: QueryFieldSelection[] = []): FieldNode[ const [argName, argValue] = variable; const argAst = t.argument({ name: argName, - value: getComplexValueAst(argValue), + value: getComplexValueAst(argValue) }); return argAst ? [...acc, argAst] : acc; }, @@ -627,19 +626,19 @@ export function getSelections(selection: QueryFieldSelection[] = []): FieldNode[ const selectionSet = isBelongTo ? t.selectionSet({ selections: subSelections }) : t.selectionSet({ - selections: [ - t.field({ name: 'totalCount' }), - t.field({ - name: 'nodes', - selectionSet: t.selectionSet({ selections: subSelections }), - }), - ], - }); + selections: [ + t.field({ name: 'totalCount' }), + t.field({ + name: 'nodes', + selectionSet: t.selectionSet({ selections: subSelections }) + }) + ] + }); return t.field({ name, args, - selectionSet, + selectionSet }); } else { return selectionAst(selectionDefn); @@ -670,7 +669,7 @@ function getComplexValueAst(value: unknown): ValueNode { // Handle arrays if (Array.isArray(value)) { return t.listValue({ - values: value.map((item) => getComplexValueAst(item)), + values: value.map((item) => getComplexValueAst(item)) }); } @@ -681,9 +680,9 @@ function getComplexValueAst(value: unknown): ValueNode { fields: Object.entries(obj).map(([key, val]) => t.objectField({ name: key, - value: getComplexValueAst(val), + value: getComplexValueAst(val) }) - ), + ) }); } @@ -699,7 +698,7 @@ function getCreateVariablesAst( type: fieldType, isNotNull, isArray, - isArrayNotNull, + isArrayNotNull } = field; let type: TypeNode = t.namedType({ type: fieldType }); if (isNotNull) type = t.nonNullType({ type }); @@ -709,7 +708,7 @@ function getCreateVariablesAst( } return t.variableDefinition({ variable: t.variable({ name: fieldName }), - type, + type }); }); } @@ -730,14 +729,14 @@ function getUpdateVariablesAst( } return t.variableDefinition({ variable: t.variable({ name: fieldName }), - type, + type }); }); const patcherVariables: VariableDefinitionNode[] = patchers.map((patcher) => { return t.variableDefinition({ variable: t.variable({ name: patcher }), - type: t.nonNullType({ type: t.namedType({ type: 'String' }) }), + type: t.nonNullType({ type: t.namedType({ type: 'String' }) }) }); }); diff --git a/graphql/codegen/src/core/codegen/babel-ast.ts b/graphql/codegen/src/core/codegen/babel-ast.ts index 9a189b2f7..0b981d4de 100644 --- a/graphql/codegen/src/core/codegen/babel-ast.ts +++ b/graphql/codegen/src/core/codegen/babel-ast.ts @@ -9,7 +9,7 @@ import generate from '@babel/generator'; import * as t from '@babel/types'; // Re-export for convenience -export { t, generate }; +export { generate,t }; /** * Generate code from an array of statements @@ -29,7 +29,7 @@ export const commentBlock = (value: string): t.CommentBlock => { value, start: null, end: null, - loc: null, + loc: null }; }; @@ -42,7 +42,7 @@ export const commentLine = (value: string): t.CommentLine => { value, start: null, end: null, - loc: null, + loc: null }; }; diff --git a/graphql/codegen/src/core/codegen/barrel.ts b/graphql/codegen/src/core/codegen/barrel.ts index 25d5a6fa6..dc11a6843 100644 --- a/graphql/codegen/src/core/codegen/barrel.ts +++ b/graphql/codegen/src/core/codegen/barrel.ts @@ -3,18 +3,19 @@ * * Using Babel AST for generating barrel (index.ts) files with re-exports. */ -import type { CleanTable } from '../../types/schema'; import * as t from '@babel/types'; -import { generateCode, addJSDocComment } from './babel-ast'; + +import type { CleanTable } from '../../types/schema'; +import { addJSDocComment,generateCode } from './babel-ast'; +import { getOperationHookName } from './type-resolver'; import { + getCreateMutationHookName, + getDeleteMutationHookName, getListQueryHookName, getSingleQueryHookName, - getCreateMutationHookName, getUpdateMutationHookName, - getDeleteMutationHookName, - hasValidPrimaryKey, + hasValidPrimaryKey } from './utils'; -import { getOperationHookName } from './type-resolver'; /** * Helper to create export * from './module' statement @@ -46,7 +47,7 @@ export function generateQueriesBarrel(tables: CleanTable[]): string { addJSDocComment(statements[0], [ 'Query hooks barrel export', '@generated by @constructive-io/graphql-codegen', - 'DO NOT EDIT - changes will be overwritten', + 'DO NOT EDIT - changes will be overwritten' ]); } @@ -62,16 +63,16 @@ export function generateMutationsBarrel(tables: CleanTable[]): string { // Export all mutation hooks for (const table of tables) { const createHookName = getCreateMutationHookName(table); - const updateHookName = getUpdateMutationHookName(table); - const deleteHookName = getDeleteMutationHookName(table); statements.push(exportAllFrom(`./${createHookName}`)); - // Only add update/delete if they exist - if (table.query?.update !== null) { + // Only add update/delete if they exist AND table has valid PK + if (table.query?.update !== null && hasValidPrimaryKey(table)) { + const updateHookName = getUpdateMutationHookName(table); statements.push(exportAllFrom(`./${updateHookName}`)); } - if (table.query?.delete !== null) { + if (table.query?.delete !== null && hasValidPrimaryKey(table)) { + const deleteHookName = getDeleteMutationHookName(table); statements.push(exportAllFrom(`./${deleteHookName}`)); } } @@ -81,7 +82,7 @@ export function generateMutationsBarrel(tables: CleanTable[]): string { addJSDocComment(statements[0], [ 'Mutation hooks barrel export', '@generated by @constructive-io/graphql-codegen', - 'DO NOT EDIT - changes will be overwritten', + 'DO NOT EDIT - changes will be overwritten' ]); } @@ -95,7 +96,6 @@ export function generateMutationsBarrel(tables: CleanTable[]): string { * @param hasSchemaTypes - Whether schema-types.ts was generated */ export interface MainBarrelOptions { - hasSchemaTypes?: boolean; hasMutations?: boolean; /** Whether query-keys.ts was generated */ hasQueryKeys?: boolean; @@ -112,27 +112,18 @@ export function generateMainBarrel( const opts: MainBarrelOptions = options; const { - hasSchemaTypes = false, hasMutations = true, hasQueryKeys = false, hasMutationKeys = false, - hasInvalidation = false, + hasInvalidation = false } = opts; const tableNames = tables.map((tbl) => tbl.name).join(', '); const statements: t.Statement[] = []; - // Client configuration + // Client configuration (ORM wrapper with configure/getClient) statements.push(exportAllFrom('./client')); - // Entity and filter types - statements.push(exportAllFrom('./types')); - - // Schema types (input, payload, enum types) - if (hasSchemaTypes) { - statements.push(exportAllFrom('./schema-types')); - } - // Centralized query keys (for cache management) if (hasQueryKeys) { statements.push(exportAllFrom('./query-keys')); @@ -185,7 +176,7 @@ export function generateMainBarrel( ' const { mutate } = useCreateCarMutation();', ' // ...', '}', - '```', + '```' ]); } @@ -224,7 +215,7 @@ export function generateRootBarrel(options: RootBarrelOptions = {}): string { if (statements.length > 0) { addJSDocComment(statements[0], [ 'Generated SDK - auto-generated, do not edit', - '@generated by @constructive-io/graphql-codegen', + '@generated by @constructive-io/graphql-codegen' ]); } @@ -277,7 +268,7 @@ export function generateCustomQueriesBarrel( addJSDocComment(statements[0], [ 'Query hooks barrel export', '@generated by @constructive-io/graphql-codegen', - 'DO NOT EDIT - changes will be overwritten', + 'DO NOT EDIT - changes will be overwritten' ]); } @@ -302,15 +293,15 @@ export function generateCustomMutationsBarrel( exportedHooks.add(createHookName); } - // Only add update/delete if they exist - if (table.query?.update !== null) { + // Only add update/delete if they exist AND table has valid PK + if (table.query?.update !== null && hasValidPrimaryKey(table)) { const updateHookName = getUpdateMutationHookName(table); if (!exportedHooks.has(updateHookName)) { statements.push(exportAllFrom(`./${updateHookName}`)); exportedHooks.add(updateHookName); } } - if (table.query?.delete !== null) { + if (table.query?.delete !== null && hasValidPrimaryKey(table)) { const deleteHookName = getDeleteMutationHookName(table); if (!exportedHooks.has(deleteHookName)) { statements.push(exportAllFrom(`./${deleteHookName}`)); @@ -333,7 +324,7 @@ export function generateCustomMutationsBarrel( addJSDocComment(statements[0], [ 'Mutation hooks barrel export', '@generated by @constructive-io/graphql-codegen', - 'DO NOT EDIT - changes will be overwritten', + 'DO NOT EDIT - changes will be overwritten' ]); } diff --git a/graphql/codegen/src/core/codegen/client.ts b/graphql/codegen/src/core/codegen/client.ts index 809c6f513..483837da4 100644 --- a/graphql/codegen/src/core/codegen/client.ts +++ b/graphql/codegen/src/core/codegen/client.ts @@ -1,74 +1,58 @@ /** - * Client generator - generates client.ts with configure() and execute() + * Client generator - generates client.ts as ORM client wrapper * - * Reads from template files in the templates/ directory for proper type checking. + * Generates a configure()/getClient() singleton pattern that wraps the ORM client. + * React Query hooks use getClient() to delegate to ORM model methods. */ -import * as fs from 'fs'; -import * as path from 'path'; import { getGeneratedFileHeader } from './utils'; -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; -} - /** - * Find a template file path. - * Templates are at ./templates/ relative to this file in both src/ and dist/. + * Generate client.ts content - ORM client wrapper with configure/getClient */ -function findTemplateFile(templateName: string): string { - const templatePath = path.join(__dirname, 'templates', templateName); +export function generateClientFile(): string { + const header = getGeneratedFileHeader('ORM client wrapper for React Query hooks'); - if (fs.existsSync(templatePath)) { - return templatePath; - } + const code = ` +import { createClient } from '../orm'; +import type { OrmClientConfig } from '../orm/client'; - throw new Error( - `Could not find template file: ${templateName}. ` + - `Searched in: ${templatePath}` - ); -} +export type { OrmClientConfig } from '../orm/client'; +export type { GraphQLAdapter, GraphQLError, QueryResult } from '../orm/client'; +export { GraphQLRequestError } from '../orm/client'; + +type OrmClientInstance = ReturnType; +let client: OrmClientInstance | null = null; /** - * Read a template file and replace the header with generated file header + * Configure the ORM client for React Query hooks + * + * @example + * \`\`\`ts + * import { configure } from './generated/hooks'; + * + * configure({ + * endpoint: 'https://api.example.com/graphql', + * headers: { Authorization: 'Bearer ' }, + * }); + * \`\`\` */ -function readTemplateFile(templateName: string, description: string): string { - const templatePath = findTemplateFile(templateName); - let content = fs.readFileSync(templatePath, 'utf-8'); - - // 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*/; - - content = content.replace( - headerPattern, - getGeneratedFileHeader(description) + '\n' - ); - - return content; +export function configure(config: OrmClientConfig): void { + client = createClient(config); } /** - * Generate client.ts content - * @param options - Generation options + * Get the configured ORM client instance + * @throws Error if configure() has not been called */ -export function generateClientFile( - options: GenerateClientFileOptions = {} -): string { - const { browserCompatible = true } = options; - - const templateName = browserCompatible - ? 'client.browser.ts' - : 'client.node.ts'; +export function getClient(): OrmClientInstance { + if (!client) { + throw new Error( + 'ORM client not configured. Call configure() before using hooks.' + ); + } + return client; +} +`; - return readTemplateFile( - templateName, - 'GraphQL client configuration and execution' - ); + return header + '\n' + code.trim() + '\n'; } diff --git a/graphql/codegen/src/core/codegen/custom-mutations.ts b/graphql/codegen/src/core/codegen/custom-mutations.ts index 2d9a10aef..ef4cc4b90 100644 --- a/graphql/codegen/src/core/codegen/custom-mutations.ts +++ b/graphql/codegen/src/core/codegen/custom-mutations.ts @@ -4,6 +4,9 @@ * Generates hooks for operations discovered via schema introspection * that are NOT table CRUD operations (e.g., login, register, etc.) * + * Delegates to ORM custom mutation operations: + * getClient().mutation.operationName(args, { select }).unwrap() + * * Output structure: * mutations/ * useLoginMutation.ts @@ -12,24 +15,21 @@ */ import type { CleanOperation, - CleanArgument, - TypeRegistry, + TypeRegistry } from '../../types/schema'; -import * as t from '@babel/types'; -import { generateCode, addJSDocComment, typedParam, createTypedCallExpression } from './babel-ast'; -import { buildCustomMutationString } from './schema-gql-ast'; import { - typeRefToTsType, - isTypeRequired, - getOperationHookName, - getOperationFileName, - getOperationVariablesTypeName, - getOperationResultTypeName, - getDocumentConstName, + buildDefaultSelectLiteral, + getSelectTypeName, + wrapInferSelectResult +} from './select-helpers'; +import { createTypeTracker, - type TypeTracker, + getOperationFileName, + getOperationHookName, + getTypeBaseName, + typeRefToTsType } from './type-resolver'; -import { getGeneratedFileHeader } from './utils'; +import { getGeneratedFileHeader,ucFirst } from './utils'; export interface GeneratedCustomMutationFile { fileName: string; @@ -47,25 +47,6 @@ export interface GenerateCustomMutationHookOptions { useCentralizedKeys?: boolean; } -interface VariablesProp { - name: string; - type: string; - optional: boolean; - docs?: string[]; -} - -function generateVariablesProperties( - args: CleanArgument[], - tracker?: TypeTracker -): VariablesProp[] { - return args.map((arg) => ({ - name: arg.name, - type: typeRefToTsType(arg.type, tracker), - optional: !isTypeRequired(arg.type), - docs: arg.description ? [arg.description] : undefined, - })); -} - export function generateCustomMutationHook( options: GenerateCustomMutationHookOptions ): GeneratedCustomMutationFile | null { @@ -90,210 +71,147 @@ function generateCustomMutationHookInternal( const { operation, typeRegistry, - maxDepth = 2, - skipQueryField = true, tableTypeNames, - useCentralizedKeys = true, + useCentralizedKeys = true } = options; const hookName = getOperationHookName(operation.name, 'mutation'); const fileName = getOperationFileName(operation.name, 'mutation'); - const variablesTypeName = getOperationVariablesTypeName(operation.name, 'mutation'); - const resultTypeName = getOperationResultTypeName(operation.name, 'mutation'); - const documentConstName = getDocumentConstName(operation.name, 'mutation'); + const varTypeName = `${ucFirst(operation.name)}Variables`; const tracker = createTypeTracker({ tableTypeNames }); - const mutationDocument = buildCustomMutationString({ - operation, - typeRegistry, - maxDepth, - skipQueryField, - }); - - const statements: t.Statement[] = []; - - const variablesProps = - operation.args.length > 0 - ? generateVariablesProperties(operation.args, tracker) - : []; + const hasArgs = operation.args.length > 0; + // Resolve types using tracker for import tracking const resultType = typeRefToTsType(operation.returnType, tracker); - - const schemaTypes = tracker.getImportableTypes(); - const tableTypes = tracker.getTableTypes(); - - const reactQueryImport = t.importDeclaration( - [t.importSpecifier(t.identifier('useMutation'), t.identifier('useMutation'))], - t.stringLiteral('@tanstack/react-query') - ); - statements.push(reactQueryImport); - - const reactQueryTypeImport = t.importDeclaration( - [t.importSpecifier(t.identifier('UseMutationOptions'), t.identifier('UseMutationOptions'))], - t.stringLiteral('@tanstack/react-query') - ); - reactQueryTypeImport.importKind = 'type'; - statements.push(reactQueryTypeImport); - - const clientImport = t.importDeclaration( - [t.importSpecifier(t.identifier('execute'), t.identifier('execute'))], - t.stringLiteral('../client') - ); - statements.push(clientImport); - - if (tableTypes.length > 0) { - const typesImport = t.importDeclaration( - tableTypes.map((tt) => t.importSpecifier(t.identifier(tt), t.identifier(tt))), - t.stringLiteral('../types') - ); - typesImport.importKind = 'type'; - statements.push(typesImport); - } - - if (schemaTypes.length > 0) { - const schemaTypesImport = t.importDeclaration( - schemaTypes.map((st) => t.importSpecifier(t.identifier(st), t.identifier(st))), - t.stringLiteral('../schema-types') - ); - schemaTypesImport.importKind = 'type'; - statements.push(schemaTypesImport); - } - - if (useCentralizedKeys) { - const mutationKeyImport = t.importDeclaration( - [t.importSpecifier(t.identifier('customMutationKeys'), t.identifier('customMutationKeys'))], - t.stringLiteral('../mutation-keys') - ); - statements.push(mutationKeyImport); - } - - const mutationDocConst = t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(documentConstName), - t.templateLiteral( - [t.templateElement({ raw: '\n' + mutationDocument, cooked: '\n' + mutationDocument }, true)], - [] - ) - ), - ]); - const mutationDocExport = t.exportNamedDeclaration(mutationDocConst); - addJSDocComment(mutationDocExport, ['GraphQL mutation document']); - statements.push(mutationDocExport); - - if (operation.args.length > 0) { - const variablesInterfaceProps = variablesProps.map((vp) => { - const prop = t.tsPropertySignature( - t.identifier(vp.name), - t.tsTypeAnnotation(t.tsTypeReference(t.identifier(vp.type))) - ); - prop.optional = vp.optional; - return prop; - }); - const variablesInterface = t.tsInterfaceDeclaration( - t.identifier(variablesTypeName), - null, - null, - t.tsInterfaceBody(variablesInterfaceProps) - ); - statements.push(t.exportNamedDeclaration(variablesInterface)); + for (const arg of operation.args) { + typeRefToTsType(arg.type, tracker); } - const resultInterfaceBody = t.tsInterfaceBody([ - t.tsPropertySignature( - t.identifier(operation.name), - t.tsTypeAnnotation(t.tsTypeReference(t.identifier(resultType))) - ), - ]); - const resultInterface = t.tsInterfaceDeclaration( - t.identifier(resultTypeName), - null, - null, - resultInterfaceBody - ); - statements.push(t.exportNamedDeclaration(resultInterface)); + const selectTypeName = getSelectTypeName(operation.returnType); + const payloadTypeName = getTypeBaseName(operation.returnType); + const hasSelect = !!selectTypeName && !!payloadTypeName; + const defaultSelectLiteral = + hasSelect && payloadTypeName + ? buildDefaultSelectLiteral(payloadTypeName, typeRegistry) + : null; - const hasArgs = operation.args.length > 0; + const lines: string[] = []; - const hookBodyStatements: t.Statement[] = []; - const mutationOptions: (t.ObjectProperty | t.SpreadElement)[] = []; + // Imports + lines.push(`import { useMutation } from '@tanstack/react-query';`); + lines.push(`import type { UseMutationOptions } from '@tanstack/react-query';`); + lines.push(`import { getClient } from '../client';`); if (useCentralizedKeys) { - mutationOptions.push( - t.objectProperty( - t.identifier('mutationKey'), - t.callExpression( - t.memberExpression(t.identifier('customMutationKeys'), t.identifier(operation.name)), - [] - ) - ) - ); + lines.push(`import { customMutationKeys } from '../mutation-keys';`); } + // ORM type imports - variable types come from orm/mutation, entity types from orm/input-types if (hasArgs) { - mutationOptions.push( - t.objectProperty( - t.identifier('mutationFn'), - t.arrowFunctionExpression( - [typedParam('variables', t.tsTypeReference(t.identifier(variablesTypeName)))], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(documentConstName), t.identifier('variables')], - [ - t.tsTypeReference(t.identifier(resultTypeName)), - t.tsTypeReference(t.identifier(variablesTypeName)), - ] - ) - ) - ) - ); + lines.push(`import type { ${varTypeName} } from '../../orm/mutation';`); + } + + const inputTypeImports: string[] = []; + if (hasSelect) { + inputTypeImports.push(selectTypeName); + inputTypeImports.push(payloadTypeName); } else { - mutationOptions.push( - t.objectProperty( - t.identifier('mutationFn'), - t.arrowFunctionExpression( - [], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(documentConstName)], - [t.tsTypeReference(t.identifier(resultTypeName))] - ) - ) - ) - ); + // For scalar/Connection returns, import any non-scalar type used in resultType + for (const refType of tracker.referencedTypes) { + if (!inputTypeImports.includes(refType)) { + inputTypeImports.push(refType); + } + } + } + if (inputTypeImports.length > 0) { + lines.push(`import type { ${inputTypeImports.join(', ')} } from '../../orm/input-types';`); } - mutationOptions.push(t.spreadElement(t.identifier('options'))); + if (hasSelect) { + lines.push(`import type { DeepExact, InferSelectResult } from '../../orm/select-types';`); + } - hookBodyStatements.push( - t.returnStatement( - t.callExpression(t.identifier('useMutation'), [t.objectExpression(mutationOptions)]) - ) - ); + lines.push(''); - const optionsType = hasArgs - ? `Omit, 'mutationFn'>` - : `Omit, 'mutationFn'>`; + // Re-export variable types for consumer convenience + if (hasArgs) { + lines.push(`export type { ${varTypeName} } from '../../orm/mutation';`); + } + if (hasSelect) { + lines.push(`export type { ${selectTypeName} } from '../../orm/input-types';`); + } + if (hasArgs || hasSelect) { + lines.push(''); + } - const optionsParam = t.identifier('options'); - optionsParam.optional = true; - optionsParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(optionsType))); + if (hasSelect && defaultSelectLiteral) { + lines.push(`const defaultSelect = ${defaultSelectLiteral} as const;`); + lines.push(''); + } - const hookFunc = t.functionDeclaration( - t.identifier(hookName), - [optionsParam], - t.blockStatement(hookBodyStatements) - ); - const hookExport = t.exportNamedDeclaration(hookFunc); - statements.push(hookExport); + // Hook + if (hasSelect) { + // With select: generic hook + const selectedResult = wrapInferSelectResult(operation.returnType, payloadTypeName); + const resultTypeStr = `{ ${operation.name}: ${selectedResult} }`; + const mutationVarType = hasArgs ? varTypeName : 'void'; + + const optionsType = `Omit, 'mutationFn'>`; + + lines.push(`export function ${hookName}(`); + lines.push(` args?: { select?: DeepExact },`); + lines.push(` options?: ${optionsType}`); + lines.push(`) {`); + lines.push(` return useMutation({`); + + if (useCentralizedKeys) { + lines.push(` mutationKey: customMutationKeys.${operation.name}(),`); + } + + if (hasArgs) { + lines.push(` mutationFn: (variables: ${varTypeName}) => getClient().mutation.${operation.name}(variables, { select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(),`); + } else { + lines.push(` mutationFn: () => getClient().mutation.${operation.name}({ select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(),`); + } + + lines.push(` ...options,`); + lines.push(` });`); + lines.push(`}`); + } else { + // Without select: simple hook (scalar return type) + const resultTypeStr = `{ ${operation.name}: ${resultType} }`; + const mutationVarType = hasArgs ? varTypeName : 'void'; + + const optionsType = `Omit, 'mutationFn'>`; + + lines.push(`export function ${hookName}(`); + lines.push(` options?: ${optionsType}`); + lines.push(`) {`); + lines.push(` return useMutation({`); + + if (useCentralizedKeys) { + lines.push(` mutationKey: customMutationKeys.${operation.name}(),`); + } + + if (hasArgs) { + lines.push(` mutationFn: (variables: ${varTypeName}) => getClient().mutation.${operation.name}(variables).unwrap(),`); + } else { + lines.push(` mutationFn: () => getClient().mutation.${operation.name}().unwrap(),`); + } + + lines.push(` ...options,`); + lines.push(` });`); + lines.push(`}`); + } - const code = generateCode(statements); - const content = getGeneratedFileHeader(`Custom mutation hook for ${operation.name}`) + '\n\n' + code; + const content = getGeneratedFileHeader(`Custom mutation hook for ${operation.name}`) + '\n\n' + lines.join('\n') + '\n'; return { fileName, content, - operationName: operation.name, + operationName: operation.name }; } @@ -317,7 +235,7 @@ export function generateAllCustomMutationHooks( skipQueryField = true, reactQueryEnabled = true, tableTypeNames, - useCentralizedKeys = true, + useCentralizedKeys = true } = options; return operations @@ -330,7 +248,7 @@ export function generateAllCustomMutationHooks( skipQueryField, reactQueryEnabled, tableTypeNames, - useCentralizedKeys, + useCentralizedKeys }) ) .filter((result): result is GeneratedCustomMutationFile => result !== null); diff --git a/graphql/codegen/src/core/codegen/custom-queries.ts b/graphql/codegen/src/core/codegen/custom-queries.ts index daefb0eb0..9c005c5e1 100644 --- a/graphql/codegen/src/core/codegen/custom-queries.ts +++ b/graphql/codegen/src/core/codegen/custom-queries.ts @@ -4,6 +4,9 @@ * Generates hooks for operations discovered via schema introspection * that are NOT table CRUD operations (e.g., currentUser, nodeById, etc.) * + * Delegates to ORM custom query operations: + * getClient().query.operationName(args, { select }).unwrap() + * * Output structure: * queries/ * useCurrentUserQuery.ts @@ -12,25 +15,23 @@ */ import type { CleanOperation, - CleanArgument, - TypeRegistry, + TypeRegistry } from '../../types/schema'; -import * as t from '@babel/types'; -import { generateCode, addJSDocComment, typedParam, createTypedCallExpression } from './babel-ast'; -import { buildCustomQueryString } from './schema-gql-ast'; import { - typeRefToTsType, - isTypeRequired, - getOperationHookName, + buildDefaultSelectLiteral, + getSelectTypeName, + wrapInferSelectResult +} from './select-helpers'; +import { + createTypeTracker, getOperationFileName, - getOperationVariablesTypeName, - getOperationResultTypeName, - getDocumentConstName, + getOperationHookName, getQueryKeyName, - createTypeTracker, - type TypeTracker, + getTypeBaseName, + isTypeRequired, + typeRefToTsType } from './type-resolver'; -import { ucFirst, getGeneratedFileHeader } from './utils'; +import { getGeneratedFileHeader,ucFirst } from './utils'; export interface GeneratedCustomQueryFile { fileName: string; @@ -48,497 +49,330 @@ export interface GenerateCustomQueryHookOptions { useCentralizedKeys?: boolean; } -interface VariablesProp { - name: string; - type: string; - optional: boolean; - docs?: string[]; -} - -function generateVariablesProperties( - args: CleanArgument[], - tracker?: TypeTracker -): VariablesProp[] { - return args.map((arg) => ({ - name: arg.name, - type: typeRefToTsType(arg.type, tracker), - optional: !isTypeRequired(arg.type), - docs: arg.description ? [arg.description] : undefined, - })); -} - export function generateCustomQueryHook( options: GenerateCustomQueryHookOptions ): GeneratedCustomQueryFile { const { operation, typeRegistry, - maxDepth = 2, - skipQueryField = true, reactQueryEnabled = true, tableTypeNames, - useCentralizedKeys = true, + useCentralizedKeys = true } = options; const hookName = getOperationHookName(operation.name, 'query'); const fileName = getOperationFileName(operation.name, 'query'); - const variablesTypeName = getOperationVariablesTypeName(operation.name, 'query'); - const resultTypeName = getOperationResultTypeName(operation.name, 'query'); - const documentConstName = getDocumentConstName(operation.name, 'query'); const queryKeyName = getQueryKeyName(operation.name); + const varTypeName = `${ucFirst(operation.name)}Variables`; const tracker = createTypeTracker({ tableTypeNames }); - const queryDocument = buildCustomQueryString({ - operation, - typeRegistry, - maxDepth, - skipQueryField, - }); - - const statements: t.Statement[] = []; - - const variablesProps = - operation.args.length > 0 - ? generateVariablesProperties(operation.args, tracker) - : []; + const hasArgs = operation.args.length > 0; + const hasRequiredArgs = operation.args.some((arg) => isTypeRequired(arg.type)); + // Resolve types using tracker for import tracking const resultType = typeRefToTsType(operation.returnType, tracker); + for (const arg of operation.args) { + typeRefToTsType(arg.type, tracker); + } + + const selectTypeName = getSelectTypeName(operation.returnType); + const payloadTypeName = getTypeBaseName(operation.returnType); + const hasSelect = !!selectTypeName && !!payloadTypeName; + const defaultSelectLiteral = + hasSelect && payloadTypeName + ? buildDefaultSelectLiteral(payloadTypeName, typeRegistry) + : null; - const schemaTypes = tracker.getImportableTypes(); - const tableTypes = tracker.getTableTypes(); + const lines: string[] = []; + // Imports if (reactQueryEnabled) { - const reactQueryImport = t.importDeclaration( - [t.importSpecifier(t.identifier('useQuery'), t.identifier('useQuery'))], - t.stringLiteral('@tanstack/react-query') - ); - statements.push(reactQueryImport); - const reactQueryTypeImport = t.importDeclaration( - [ - t.importSpecifier(t.identifier('UseQueryOptions'), t.identifier('UseQueryOptions')), - t.importSpecifier(t.identifier('QueryClient'), t.identifier('QueryClient')), - ], - t.stringLiteral('@tanstack/react-query') - ); - reactQueryTypeImport.importKind = 'type'; - statements.push(reactQueryTypeImport); + lines.push(`import { useQuery } from '@tanstack/react-query';`); + lines.push(`import type { UseQueryOptions, QueryClient } from '@tanstack/react-query';`); } - const clientImport = t.importDeclaration( - [t.importSpecifier(t.identifier('execute'), t.identifier('execute'))], - t.stringLiteral('../client') - ); - statements.push(clientImport); - const clientTypeImport = t.importDeclaration( - [t.importSpecifier(t.identifier('ExecuteOptions'), t.identifier('ExecuteOptions'))], - t.stringLiteral('../client') - ); - clientTypeImport.importKind = 'type'; - statements.push(clientTypeImport); - - if (tableTypes.length > 0) { - const typesImport = t.importDeclaration( - tableTypes.map((tt) => t.importSpecifier(t.identifier(tt), t.identifier(tt))), - t.stringLiteral('../types') - ); - typesImport.importKind = 'type'; - statements.push(typesImport); + lines.push(`import { getClient } from '../client';`); + + if (useCentralizedKeys) { + lines.push(`import { customQueryKeys } from '../query-keys';`); } - if (schemaTypes.length > 0) { - const schemaTypesImport = t.importDeclaration( - schemaTypes.map((st) => t.importSpecifier(t.identifier(st), t.identifier(st))), - t.stringLiteral('../schema-types') - ); - schemaTypesImport.importKind = 'type'; - statements.push(schemaTypesImport); + // ORM type imports - variable types come from orm/query, entity types from orm/input-types + if (hasArgs) { + lines.push(`import type { ${varTypeName} } from '../../orm/query';`); } - if (useCentralizedKeys) { - const queryKeyImport = t.importDeclaration( - [t.importSpecifier(t.identifier('customQueryKeys'), t.identifier('customQueryKeys'))], - t.stringLiteral('../query-keys') - ); - statements.push(queryKeyImport); + const inputTypeImports: string[] = []; + if (hasSelect) { + inputTypeImports.push(selectTypeName); + inputTypeImports.push(payloadTypeName); + } else { + // For scalar/Connection returns, import any non-scalar type used in resultType + const baseName = getTypeBaseName(operation.returnType); + if (baseName && !tracker.referencedTypes.has('__skip__')) { + // Import Connection types and other non-scalar types referenced in the result + for (const refType of tracker.referencedTypes) { + if (!inputTypeImports.includes(refType)) { + inputTypeImports.push(refType); + } + } + } + } + if (inputTypeImports.length > 0) { + lines.push(`import type { ${inputTypeImports.join(', ')} } from '../../orm/input-types';`); } - const queryDocConst = t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(documentConstName), - t.templateLiteral( - [t.templateElement({ raw: '\n' + queryDocument, cooked: '\n' + queryDocument }, true)], - [] - ) - ), - ]); - const queryDocExport = t.exportNamedDeclaration(queryDocConst); - addJSDocComment(queryDocExport, ['GraphQL query document']); - statements.push(queryDocExport); - - if (operation.args.length > 0) { - const variablesInterfaceProps = variablesProps.map((vp) => { - const prop = t.tsPropertySignature( - t.identifier(vp.name), - t.tsTypeAnnotation(t.tsTypeReference(t.identifier(vp.type))) - ); - prop.optional = vp.optional; - return prop; - }); - const variablesInterface = t.tsInterfaceDeclaration( - t.identifier(variablesTypeName), - null, - null, - t.tsInterfaceBody(variablesInterfaceProps) - ); - statements.push(t.exportNamedDeclaration(variablesInterface)); + if (hasSelect) { + lines.push(`import type { DeepExact, InferSelectResult } from '../../orm/select-types';`); } - const resultInterfaceBody = t.tsInterfaceBody([ - t.tsPropertySignature( - t.identifier(operation.name), - t.tsTypeAnnotation(t.tsTypeReference(t.identifier(resultType))) - ), - ]); - const resultInterface = t.tsInterfaceDeclaration( - t.identifier(resultTypeName), - null, - null, - resultInterfaceBody - ); - statements.push(t.exportNamedDeclaration(resultInterface)); + lines.push(''); + // Re-export variable types for consumer convenience + if (hasArgs) { + lines.push(`export type { ${varTypeName} } from '../../orm/query';`); + } + if (hasSelect) { + lines.push(`export type { ${selectTypeName} } from '../../orm/input-types';`); + } + if (hasArgs || hasSelect) { + lines.push(''); + } + + if (hasSelect && defaultSelectLiteral) { + lines.push(`const defaultSelect = ${defaultSelectLiteral} as const;`); + lines.push(''); + } + + // Query key if (useCentralizedKeys) { - const queryKeyConst = t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(queryKeyName), - t.memberExpression(t.identifier('customQueryKeys'), t.identifier(operation.name)) - ), - ]); - const queryKeyExport = t.exportNamedDeclaration(queryKeyConst); - addJSDocComment(queryKeyExport, ['Query key factory - re-exported from query-keys.ts']); - statements.push(queryKeyExport); - } else if (operation.args.length > 0) { - const queryKeyArrow = t.arrowFunctionExpression( - [typedParam('variables', t.tsTypeReference(t.identifier(variablesTypeName)), true)], - t.tsAsExpression( - t.arrayExpression([t.stringLiteral(operation.name), t.identifier('variables')]), - t.tsTypeReference(t.identifier('const')) - ) - ); - const queryKeyConst = t.variableDeclaration('const', [ - t.variableDeclarator(t.identifier(queryKeyName), queryKeyArrow), - ]); - const queryKeyExport = t.exportNamedDeclaration(queryKeyConst); - addJSDocComment(queryKeyExport, ['Query key factory for caching']); - statements.push(queryKeyExport); + lines.push(`/** Query key factory - re-exported from query-keys.ts */`); + lines.push(`export const ${queryKeyName} = customQueryKeys.${operation.name};`); + } else if (hasArgs) { + lines.push(`/** Query key factory for caching */`); + lines.push(`export const ${queryKeyName} = (variables?: ${varTypeName}) => ['${operation.name}', variables] as const;`); } else { - const queryKeyArrow = t.arrowFunctionExpression( - [], - t.tsAsExpression( - t.arrayExpression([t.stringLiteral(operation.name)]), - t.tsTypeReference(t.identifier('const')) - ) - ); - const queryKeyConst = t.variableDeclaration('const', [ - t.variableDeclarator(t.identifier(queryKeyName), queryKeyArrow), - ]); - const queryKeyExport = t.exportNamedDeclaration(queryKeyConst); - addJSDocComment(queryKeyExport, ['Query key factory for caching']); - statements.push(queryKeyExport); + lines.push(`/** Query key factory for caching */`); + lines.push(`export const ${queryKeyName} = () => ['${operation.name}'] as const;`); } + lines.push(''); + // Hook if (reactQueryEnabled) { - const hasArgs = operation.args.length > 0; - const hasRequiredArgs = operation.args.some((arg) => isTypeRequired(arg.type)); + const description = operation.description || `Query hook for ${operation.name}`; + const argNames = operation.args.map((a) => a.name).join(', '); + const exampleCall = hasArgs ? `${hookName}({ ${argNames} })` : `${hookName}()`; - const hookBodyStatements: t.Statement[] = []; - const useQueryOptions: (t.ObjectProperty | t.SpreadElement)[] = []; + lines.push(`/**`); + lines.push(` * ${description}`); + lines.push(` *`); + lines.push(` * @example`); + lines.push(` * \`\`\`tsx`); + lines.push(` * const { data, isLoading } = ${exampleCall};`); + lines.push(` *`); + lines.push(` * if (data?.${operation.name}) {`); + lines.push(` * console.log(data.${operation.name});`); + lines.push(` * }`); + lines.push(` * \`\`\``); + lines.push(` */`); + + if (hasSelect) { + // With select: generic hook + const selectedResult = wrapInferSelectResult(operation.returnType, payloadTypeName); + const resultTypeStr = `{ ${operation.name}: ${selectedResult} }`; + const paramsArr: string[] = []; + + if (hasArgs) { + paramsArr.push(` variables${hasRequiredArgs ? '' : '?'}: ${varTypeName},`); + } + paramsArr.push(` args?: { select?: DeepExact },`); + + const optionsType = `Omit, 'queryKey' | 'queryFn'>`; + paramsArr.push(` options?: ${optionsType}`); + + lines.push(`export function ${hookName}(`); + lines.push(paramsArr.join('\n')); + lines.push(`) {`); + lines.push(` return useQuery({`); + + if (hasArgs) { + lines.push(` queryKey: ${queryKeyName}(variables),`); + lines.push(` queryFn: () => getClient().query.${operation.name}(${hasRequiredArgs ? 'variables!' : 'variables'}, { select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(),`); + } else { + lines.push(` queryKey: ${queryKeyName}(),`); + lines.push(` queryFn: () => getClient().query.${operation.name}({ select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(),`); + } - if (hasArgs) { - useQueryOptions.push( - t.objectProperty( - t.identifier('queryKey'), - t.callExpression(t.identifier(queryKeyName), [t.identifier('variables')]) - ) - ); - useQueryOptions.push( - t.objectProperty( - t.identifier('queryFn'), - t.arrowFunctionExpression( - [], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(documentConstName), t.identifier('variables')], - [ - t.tsTypeReference(t.identifier(resultTypeName)), - t.tsTypeReference(t.identifier(variablesTypeName)), - ] - ) - ) - ) - ); if (hasRequiredArgs) { - useQueryOptions.push( - t.objectProperty( - t.identifier('enabled'), - t.logicalExpression( - '&&', - t.unaryExpression('!', t.unaryExpression('!', t.identifier('variables'))), - t.binaryExpression( - '!==', - t.optionalMemberExpression( - t.identifier('options'), - t.identifier('enabled'), - false, - true - ), - t.booleanLiteral(false) - ) - ) - ) - ); + lines.push(` enabled: !!variables && options?.enabled !== false,`); } + + lines.push(` ...options,`); + lines.push(` });`); + lines.push(`}`); } else { - useQueryOptions.push( - t.objectProperty( - t.identifier('queryKey'), - t.callExpression(t.identifier(queryKeyName), []) - ) - ); - useQueryOptions.push( - t.objectProperty( - t.identifier('queryFn'), - t.arrowFunctionExpression( - [], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(documentConstName)], - [t.tsTypeReference(t.identifier(resultTypeName))] - ) - ) - ) - ); - } - useQueryOptions.push(t.spreadElement(t.identifier('options'))); + // Without select: simple hook (scalar return type) + const resultTypeStr = `{ ${operation.name}: ${resultType} }`; + const paramsArr: string[] = []; - hookBodyStatements.push( - t.returnStatement( - t.callExpression(t.identifier('useQuery'), [t.objectExpression(useQueryOptions)]) - ) - ); + if (hasArgs) { + paramsArr.push(` variables${hasRequiredArgs ? '' : '?'}: ${varTypeName},`); + } - const hookParams: t.Identifier[] = []; - if (hasArgs) { - hookParams.push( - typedParam('variables', t.tsTypeReference(t.identifier(variablesTypeName)), !hasRequiredArgs) - ); + const optionsType = `Omit, 'queryKey' | 'queryFn'>`; + paramsArr.push(` options?: ${optionsType}`); + + lines.push(`export function ${hookName}(`); + lines.push(paramsArr.join('\n')); + lines.push(`) {`); + lines.push(` return useQuery({`); + + if (hasArgs) { + lines.push(` queryKey: ${queryKeyName}(variables),`); + lines.push(` queryFn: () => getClient().query.${operation.name}(${hasRequiredArgs ? 'variables!' : 'variables'}).unwrap(),`); + } else { + lines.push(` queryKey: ${queryKeyName}(),`); + lines.push(` queryFn: () => getClient().query.${operation.name}().unwrap(),`); + } + + if (hasRequiredArgs) { + lines.push(` enabled: !!variables && options?.enabled !== false,`); + } + + lines.push(` ...options,`); + lines.push(` });`); + lines.push(`}`); } - const optionsTypeStr = `Omit, 'queryKey' | 'queryFn'>`; - const optionsParam = t.identifier('options'); - optionsParam.optional = true; - optionsParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(optionsTypeStr))); - hookParams.push(optionsParam); - - const hookFunc = t.functionDeclaration( - t.identifier(hookName), - hookParams, - t.blockStatement(hookBodyStatements) - ); - const hookExport = t.exportNamedDeclaration(hookFunc); - const description = operation.description || `Query hook for ${operation.name}`; - const argNames = operation.args.map((a) => a.name).join(', '); - const exampleCall = hasArgs ? `${hookName}({ ${argNames} })` : `${hookName}()`; - addJSDocComment(hookExport, [ - description, - '', - '@example', - '```tsx', - `const { data, isLoading } = ${exampleCall};`, - '', - `if (data?.${operation.name}) {`, - ` console.log(data.${operation.name});`, - '}', - '```', - ]); - statements.push(hookExport); + lines.push(''); } + // Fetch function (non-hook) const fetchFnName = `fetch${ucFirst(operation.name)}Query`; - const hasArgs = operation.args.length > 0; - const hasRequiredArgs = operation.args.some((arg) => isTypeRequired(arg.type)); + const fetchArgNames = operation.args.map((a) => a.name).join(', '); + const fetchExampleCall = hasArgs ? `${fetchFnName}({ ${fetchArgNames} })` : `${fetchFnName}()`; + + lines.push(`/**`); + lines.push(` * Fetch ${operation.name} without React hooks`); + lines.push(` *`); + lines.push(` * @example`); + lines.push(` * \`\`\`ts`); + lines.push(` * const data = await ${fetchExampleCall};`); + lines.push(` * \`\`\``); + lines.push(` */`); + + if (hasSelect) { + const fetchParamsArr: string[] = []; + if (hasArgs) { + fetchParamsArr.push(`variables${hasRequiredArgs ? '' : '?'}: ${varTypeName}`); + } + fetchParamsArr.push(`args?: { select?: DeepExact }`); - const fetchBodyStatements: t.Statement[] = []; - if (hasArgs) { - fetchBodyStatements.push( - t.returnStatement( - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(documentConstName), t.identifier('variables'), t.identifier('options')], - [ - t.tsTypeReference(t.identifier(resultTypeName)), - t.tsTypeReference(t.identifier(variablesTypeName)), - ] - ) - ) - ); + lines.push(`export async function ${fetchFnName}(`); + lines.push(` ${fetchParamsArr.join(',\n ')}`); + lines.push(`) {`); + if (hasArgs) { + lines.push(` return getClient().query.${operation.name}(${hasRequiredArgs ? 'variables!' : 'variables'}, { select: (args?.select ?? defaultSelect) as DeepExact }).unwrap();`); + } else { + lines.push(` return getClient().query.${operation.name}({ select: (args?.select ?? defaultSelect) as DeepExact }).unwrap();`); + } + lines.push(`}`); } else { - fetchBodyStatements.push( - t.returnStatement( - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(documentConstName), t.identifier('undefined'), t.identifier('options')], - [t.tsTypeReference(t.identifier(resultTypeName))] - ) - ) - ); - } + const fetchParamsArr: string[] = []; + if (hasArgs) { + fetchParamsArr.push(`variables${hasRequiredArgs ? '' : '?'}: ${varTypeName}`); + } - const fetchParams: t.Identifier[] = []; - if (hasArgs) { - fetchParams.push( - typedParam('variables', t.tsTypeReference(t.identifier(variablesTypeName)), !hasRequiredArgs) - ); + lines.push(`export async function ${fetchFnName}(`); + lines.push(` ${fetchParamsArr.join(',\n ')}`); + lines.push(`) {`); + if (hasArgs) { + lines.push(` return getClient().query.${operation.name}(${hasRequiredArgs ? 'variables!' : 'variables'}).unwrap();`); + } else { + lines.push(` return getClient().query.${operation.name}().unwrap();`); + } + lines.push(`}`); } - fetchParams.push(typedParam('options', t.tsTypeReference(t.identifier('ExecuteOptions')), true)); - - const fetchFunc = t.functionDeclaration( - t.identifier(fetchFnName), - fetchParams, - t.blockStatement(fetchBodyStatements) - ); - fetchFunc.async = true; - fetchFunc.returnType = t.tsTypeAnnotation( - t.tsTypeReference( - t.identifier('Promise'), - t.tsTypeParameterInstantiation([t.tsTypeReference(t.identifier(resultTypeName))]) - ) - ); - const fetchExport = t.exportNamedDeclaration(fetchFunc); - - const argNames = operation.args.map((a) => a.name).join(', '); - const fetchExampleCall = hasArgs ? `${fetchFnName}({ ${argNames} })` : `${fetchFnName}()`; - addJSDocComment(fetchExport, [ - `Fetch ${operation.name} without React hooks`, - '', - '@example', - '```ts', - `const data = await ${fetchExampleCall};`, - '```', - ]); - statements.push(fetchExport); + // Prefetch function if (reactQueryEnabled) { + lines.push(''); + const prefetchFnName = `prefetch${ucFirst(operation.name)}Query`; + const prefetchArgNames = operation.args.map((a) => a.name).join(', '); + const prefetchExampleCall = hasArgs + ? `${prefetchFnName}(queryClient, { ${prefetchArgNames} })` + : `${prefetchFnName}(queryClient)`; - const prefetchBodyStatements: t.Statement[] = []; - const prefetchQueryOptions: t.ObjectProperty[] = []; + lines.push(`/**`); + lines.push(` * Prefetch ${operation.name} for SSR or cache warming`); + lines.push(` *`); + lines.push(` * @example`); + lines.push(` * \`\`\`ts`); + lines.push(` * await ${prefetchExampleCall};`); + lines.push(` * \`\`\``); + lines.push(` */`); + + if (hasSelect) { + const prefetchParamsArr: string[] = ['queryClient: QueryClient']; + if (hasArgs) { + prefetchParamsArr.push(`variables${hasRequiredArgs ? '' : '?'}: ${varTypeName}`); + } + prefetchParamsArr.push(`args?: { select?: DeepExact }`); + + lines.push(`export async function ${prefetchFnName}(`); + lines.push(` ${prefetchParamsArr.join(',\n ')}`); + lines.push(`): Promise {`); + + if (hasArgs) { + lines.push(` await queryClient.prefetchQuery({`); + lines.push(` queryKey: ${queryKeyName}(variables),`); + lines.push(` queryFn: () => getClient().query.${operation.name}(${hasRequiredArgs ? 'variables!' : 'variables'}, { select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(),`); + lines.push(` });`); + } else { + lines.push(` await queryClient.prefetchQuery({`); + lines.push(` queryKey: ${queryKeyName}(),`); + lines.push(` queryFn: () => getClient().query.${operation.name}({ select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(),`); + lines.push(` });`); + } - if (hasArgs) { - prefetchQueryOptions.push( - t.objectProperty( - t.identifier('queryKey'), - t.callExpression(t.identifier(queryKeyName), [t.identifier('variables')]) - ) - ); - prefetchQueryOptions.push( - t.objectProperty( - t.identifier('queryFn'), - t.arrowFunctionExpression( - [], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(documentConstName), t.identifier('variables'), t.identifier('options')], - [ - t.tsTypeReference(t.identifier(resultTypeName)), - t.tsTypeReference(t.identifier(variablesTypeName)), - ] - ) - ) - ) - ); + lines.push(`}`); } else { - prefetchQueryOptions.push( - t.objectProperty( - t.identifier('queryKey'), - t.callExpression(t.identifier(queryKeyName), []) - ) - ); - prefetchQueryOptions.push( - t.objectProperty( - t.identifier('queryFn'), - t.arrowFunctionExpression( - [], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(documentConstName), t.identifier('undefined'), t.identifier('options')], - [t.tsTypeReference(t.identifier(resultTypeName))] - ) - ) - ) - ); - } + const prefetchParamsArr: string[] = ['queryClient: QueryClient']; + if (hasArgs) { + prefetchParamsArr.push(`variables${hasRequiredArgs ? '' : '?'}: ${varTypeName}`); + } - prefetchBodyStatements.push( - t.expressionStatement( - t.awaitExpression( - t.callExpression( - t.memberExpression(t.identifier('queryClient'), t.identifier('prefetchQuery')), - [t.objectExpression(prefetchQueryOptions)] - ) - ) - ) - ); + lines.push(`export async function ${prefetchFnName}(`); + lines.push(` ${prefetchParamsArr.join(',\n ')}`); + lines.push(`): Promise {`); + + if (hasArgs) { + lines.push(` await queryClient.prefetchQuery({`); + lines.push(` queryKey: ${queryKeyName}(variables),`); + lines.push(` queryFn: () => getClient().query.${operation.name}(${hasRequiredArgs ? 'variables!' : 'variables'}).unwrap(),`); + lines.push(` });`); + } else { + lines.push(` await queryClient.prefetchQuery({`); + lines.push(` queryKey: ${queryKeyName}(),`); + lines.push(` queryFn: () => getClient().query.${operation.name}().unwrap(),`); + lines.push(` });`); + } - const prefetchParams: t.Identifier[] = [ - typedParam('queryClient', t.tsTypeReference(t.identifier('QueryClient'))), - ]; - if (hasArgs) { - prefetchParams.push( - typedParam('variables', t.tsTypeReference(t.identifier(variablesTypeName)), !hasRequiredArgs) - ); + lines.push(`}`); } - prefetchParams.push(typedParam('options', t.tsTypeReference(t.identifier('ExecuteOptions')), true)); - - const prefetchFunc = t.functionDeclaration( - t.identifier(prefetchFnName), - prefetchParams, - t.blockStatement(prefetchBodyStatements) - ); - prefetchFunc.async = true; - prefetchFunc.returnType = t.tsTypeAnnotation( - t.tsTypeReference( - t.identifier('Promise'), - t.tsTypeParameterInstantiation([t.tsVoidKeyword()]) - ) - ); - const prefetchExport = t.exportNamedDeclaration(prefetchFunc); - - const prefetchExampleCall = hasArgs - ? `${prefetchFnName}(queryClient, { ${argNames} })` - : `${prefetchFnName}(queryClient)`; - addJSDocComment(prefetchExport, [ - `Prefetch ${operation.name} for SSR or cache warming`, - '', - '@example', - '```ts', - `await ${prefetchExampleCall};`, - '```', - ]); - statements.push(prefetchExport); } - const code = generateCode(statements); const headerText = reactQueryEnabled ? `Custom query hook for ${operation.name}` : `Custom query functions for ${operation.name}`; - const content = getGeneratedFileHeader(headerText) + '\n\n' + code; + const content = getGeneratedFileHeader(headerText) + '\n\n' + lines.join('\n') + '\n'; return { fileName, content, - operationName: operation.name, + operationName: operation.name }; } @@ -562,7 +396,7 @@ export function generateAllCustomQueryHooks( skipQueryField = true, reactQueryEnabled = true, tableTypeNames, - useCentralizedKeys = true, + useCentralizedKeys = true } = options; return operations @@ -575,7 +409,7 @@ export function generateAllCustomQueryHooks( skipQueryField, reactQueryEnabled, tableTypeNames, - useCentralizedKeys, + useCentralizedKeys }) ); } diff --git a/graphql/codegen/src/core/codegen/gql-ast.ts b/graphql/codegen/src/core/codegen/gql-ast.ts deleted file mode 100644 index 612db24a0..000000000 --- a/graphql/codegen/src/core/codegen/gql-ast.ts +++ /dev/null @@ -1,405 +0,0 @@ -/** - * GraphQL AST builders using gql-ast - * - * Provides utilities for generating GraphQL queries and mutations via AST - * instead of string concatenation. - */ -import * as t from 'gql-ast'; -import { print } from 'graphql'; -import type { - DocumentNode, - FieldNode, - ArgumentNode, - VariableDefinitionNode, -} from 'graphql'; -import type { CleanTable, CleanField } from '../../types/schema'; -import { - getTableNames, - getAllRowsQueryName, - getSingleRowQueryName, - getCreateMutationName, - getUpdateMutationName, - getDeleteMutationName, - getFilterTypeName, - getConditionTypeName, - getOrderByTypeName, - getScalarFields, - getPrimaryKeyInfo, - ucFirst, -} from './utils'; - - - -// ============================================================================ -// Field selection builders -// ============================================================================ - -/** - * Create field selections from CleanField array - */ -function createFieldSelections(fields: CleanField[]): FieldNode[] { - return fields.map((field) => t.field({ name: field.name })); -} - -/** - * Create pageInfo selection - */ -function createPageInfoSelection(): FieldNode { - return t.field({ - name: 'pageInfo', - selectionSet: t.selectionSet({ - selections: [ - t.field({ name: 'hasNextPage' }), - t.field({ name: 'hasPreviousPage' }), - t.field({ name: 'startCursor' }), - t.field({ name: 'endCursor' }), - ], - }), - }); -} - -// ============================================================================ -// List query builder -// ============================================================================ - -export interface ListQueryConfig { - table: CleanTable; -} - -/** - * Build a list query AST for a table - */ -export function buildListQueryAST(config: ListQueryConfig): DocumentNode { - const { table } = config; - const queryName = getAllRowsQueryName(table); - const filterType = getFilterTypeName(table); - const conditionType = getConditionTypeName(table); - const orderByType = getOrderByTypeName(table); - const scalarFields = getScalarFields(table); - - // Variable definitions - all pagination arguments from PostGraphile - const variableDefinitions: VariableDefinitionNode[] = [ - t.variableDefinition({ - variable: t.variable({ name: 'first' }), - type: t.namedType({ type: 'Int' }), - }), - t.variableDefinition({ - variable: t.variable({ name: 'last' }), - type: t.namedType({ type: 'Int' }), - }), - t.variableDefinition({ - variable: t.variable({ name: 'offset' }), - type: t.namedType({ type: 'Int' }), - }), - t.variableDefinition({ - variable: t.variable({ name: 'before' }), - type: t.namedType({ type: 'Cursor' }), - }), - t.variableDefinition({ - variable: t.variable({ name: 'after' }), - type: t.namedType({ type: 'Cursor' }), - }), - t.variableDefinition({ - variable: t.variable({ name: 'filter' }), - type: t.namedType({ type: filterType }), - }), - t.variableDefinition({ - variable: t.variable({ name: 'condition' }), - type: t.namedType({ type: conditionType }), - }), - t.variableDefinition({ - variable: t.variable({ name: 'orderBy' }), - type: t.listType({ - type: t.nonNullType({ type: t.namedType({ type: orderByType }) }), - }), - }), - ]; - - // Query arguments - const args: ArgumentNode[] = [ - t.argument({ name: 'first', value: t.variable({ name: 'first' }) }), - t.argument({ name: 'last', value: t.variable({ name: 'last' }) }), - t.argument({ name: 'offset', value: t.variable({ name: 'offset' }) }), - t.argument({ name: 'before', value: t.variable({ name: 'before' }) }), - t.argument({ name: 'after', value: t.variable({ name: 'after' }) }), - t.argument({ name: 'filter', value: t.variable({ name: 'filter' }) }), - t.argument({ name: 'condition', value: t.variable({ name: 'condition' }) }), - t.argument({ name: 'orderBy', value: t.variable({ name: 'orderBy' }) }), - ]; - - // Field selections - const fieldSelections = createFieldSelections(scalarFields); - - // Connection fields - const connectionFields: FieldNode[] = [ - t.field({ name: 'totalCount' }), - t.field({ - name: 'nodes', - selectionSet: t.selectionSet({ selections: fieldSelections }), - }), - createPageInfoSelection(), - ]; - - return t.document({ - definitions: [ - t.operationDefinition({ - operation: 'query', - name: `${ucFirst(queryName)}Query`, - variableDefinitions, - selectionSet: t.selectionSet({ - selections: [ - t.field({ - name: queryName, - args, - selectionSet: t.selectionSet({ selections: connectionFields }), - }), - ], - }), - }), - ], - }); -} - -// ============================================================================ -// Single item query builder -// ============================================================================ - -export interface SingleQueryConfig { - table: CleanTable; -} - -/** - * Build a single item query AST for a table - */ -export function buildSingleQueryAST(config: SingleQueryConfig): DocumentNode { - const { table } = config; - const queryName = getSingleRowQueryName(table); - const scalarFields = getScalarFields(table); - - // Get primary key info dynamically from table constraints - const pkFields = getPrimaryKeyInfo(table); - // For simplicity, use first PK field (most common case) - const pkField = pkFields[0]; - - // Variable definitions - use dynamic PK field name and type - const variableDefinitions: VariableDefinitionNode[] = [ - t.variableDefinition({ - variable: t.variable({ name: pkField.name }), - type: t.nonNullType({ type: t.namedType({ type: pkField.gqlType }) }), - }), - ]; - - // Query arguments - use dynamic PK field name - const args: ArgumentNode[] = [ - t.argument({ name: pkField.name, value: t.variable({ name: pkField.name }) }), - ]; - - // Field selections - const fieldSelections = createFieldSelections(scalarFields); - - return t.document({ - definitions: [ - t.operationDefinition({ - operation: 'query', - name: `${ucFirst(queryName)}Query`, - variableDefinitions, - selectionSet: t.selectionSet({ - selections: [ - t.field({ - name: queryName, - args, - selectionSet: t.selectionSet({ selections: fieldSelections }), - }), - ], - }), - }), - ], - }); -} - -// ============================================================================ -// Create mutation builder -// ============================================================================ - -export interface CreateMutationConfig { - table: CleanTable; -} - -/** - * Build a create mutation AST for a table - */ -export function buildCreateMutationAST(config: CreateMutationConfig): DocumentNode { - const { table } = config; - const { typeName, singularName } = getTableNames(table); - const mutationName = getCreateMutationName(table); - const inputTypeName = `Create${typeName}Input`; - const scalarFields = getScalarFields(table); - - // Variable definitions - const variableDefinitions: VariableDefinitionNode[] = [ - t.variableDefinition({ - variable: t.variable({ name: 'input' }), - type: t.nonNullType({ type: t.namedType({ type: inputTypeName }) }), - }), - ]; - - // Mutation arguments - const args: ArgumentNode[] = [ - t.argument({ name: 'input', value: t.variable({ name: 'input' }) }), - ]; - - // Field selections - const fieldSelections = createFieldSelections(scalarFields); - - return t.document({ - definitions: [ - t.operationDefinition({ - operation: 'mutation', - name: `${ucFirst(mutationName)}Mutation`, - variableDefinitions, - selectionSet: t.selectionSet({ - selections: [ - t.field({ - name: mutationName, - args, - selectionSet: t.selectionSet({ - selections: [ - t.field({ - name: singularName, - selectionSet: t.selectionSet({ selections: fieldSelections }), - }), - ], - }), - }), - ], - }), - }), - ], - }); -} - -// ============================================================================ -// Update mutation builder -// ============================================================================ - -export interface UpdateMutationConfig { - table: CleanTable; -} - -/** - * Build an update mutation AST for a table - */ -export function buildUpdateMutationAST(config: UpdateMutationConfig): DocumentNode { - const { table } = config; - const { typeName, singularName } = getTableNames(table); - const mutationName = getUpdateMutationName(table); - const inputTypeName = `Update${typeName}Input`; - const scalarFields = getScalarFields(table); - - // Variable definitions - const variableDefinitions: VariableDefinitionNode[] = [ - t.variableDefinition({ - variable: t.variable({ name: 'input' }), - type: t.nonNullType({ type: t.namedType({ type: inputTypeName }) }), - }), - ]; - - // Mutation arguments - const args: ArgumentNode[] = [ - t.argument({ name: 'input', value: t.variable({ name: 'input' }) }), - ]; - - // Field selections - const fieldSelections = createFieldSelections(scalarFields); - - return t.document({ - definitions: [ - t.operationDefinition({ - operation: 'mutation', - name: `${ucFirst(mutationName)}Mutation`, - variableDefinitions, - selectionSet: t.selectionSet({ - selections: [ - t.field({ - name: mutationName, - args, - selectionSet: t.selectionSet({ - selections: [ - t.field({ - name: singularName, - selectionSet: t.selectionSet({ selections: fieldSelections }), - }), - ], - }), - }), - ], - }), - }), - ], - }); -} - -// ============================================================================ -// Delete mutation builder -// ============================================================================ - -export interface DeleteMutationConfig { - table: CleanTable; -} - -/** - * Build a delete mutation AST for a table - */ -export function buildDeleteMutationAST(config: DeleteMutationConfig): DocumentNode { - const { table } = config; - const { typeName } = getTableNames(table); - const mutationName = getDeleteMutationName(table); - const inputTypeName = `Delete${typeName}Input`; - - // Variable definitions - const variableDefinitions: VariableDefinitionNode[] = [ - t.variableDefinition({ - variable: t.variable({ name: 'input' }), - type: t.nonNullType({ type: t.namedType({ type: inputTypeName }) }), - }), - ]; - - // Mutation arguments - const args: ArgumentNode[] = [ - t.argument({ name: 'input', value: t.variable({ name: 'input' }) }), - ]; - - return t.document({ - definitions: [ - t.operationDefinition({ - operation: 'mutation', - name: `${ucFirst(mutationName)}Mutation`, - variableDefinitions, - selectionSet: t.selectionSet({ - selections: [ - t.field({ - name: mutationName, - args, - selectionSet: t.selectionSet({ - selections: [ - t.field({ name: 'clientMutationId' }), - ], - }), - }), - ], - }), - }), - ], - }); -} - -// ============================================================================ -// Print utilities -// ============================================================================ - -/** - * Print AST to GraphQL string - */ -export function printGraphQL(ast: DocumentNode): string { - return print(ast); -} diff --git a/graphql/codegen/src/core/codegen/index.ts b/graphql/codegen/src/core/codegen/index.ts index 407e10527..b77bad93d 100644 --- a/graphql/codegen/src/core/codegen/index.ts +++ b/graphql/codegen/src/core/codegen/index.ts @@ -2,52 +2,48 @@ * Code generation orchestrator * * Coordinates all code generators to produce the complete SDK output. + * Hooks delegate to ORM model methods - types are imported from ORM's input-types.ts. * * Output structure: * generated/ * index.ts - Main barrel export - * client.ts - GraphQL client with configure() and execute() - * types.ts - Entity interfaces and filter types + * client.ts - ORM client wrapper (configure/getClient) * queries/ * index.ts - Query hooks barrel - * useCarsQuery.ts - List query hook (table-based) - * useCarQuery.ts - Single item query hook (table-based) - * useCurrentUserQuery.ts - Custom query hook + * useCarsQuery.ts - List query hook -> ORM findMany + * useCarQuery.ts - Single item query hook -> ORM findOne + * useCurrentUserQuery.ts - Custom query hook -> ORM query.xxx * ... * mutations/ * index.ts - Mutation hooks barrel - * useCreateCarMutation.ts - Table-based CRUD - * useUpdateCarMutation.ts - * useDeleteCarMutation.ts - * useLoginMutation.ts - Custom mutation - * useRegisterMutation.ts + * useCreateCarMutation.ts - -> ORM create + * useUpdateCarMutation.ts - -> ORM update + * useDeleteCarMutation.ts - -> ORM delete + * useLoginMutation.ts - Custom mutation -> ORM mutation.xxx * ... */ +import type { GraphQLSDKConfigTarget, QueryKeyConfig } from '../../types/config'; +import { DEFAULT_QUERY_KEY_CONFIG } from '../../types/config'; import type { - CleanTable, CleanOperation, - TypeRegistry, + CleanTable, + TypeRegistry } from '../../types/schema'; -import type { GraphQLSDKConfigTarget, QueryKeyConfig } from '../../types/config'; -import { DEFAULT_QUERY_KEY_CONFIG } from '../../types/config'; - -import { generateClientFile } from './client'; -import { generateTypesFile } from './types'; -import { generateSchemaTypesFile } from './schema-types-generator'; -import { generateAllQueryHooks } from './queries'; -import { generateAllMutationHooks } from './mutations'; -import { generateAllCustomQueryHooks } from './custom-queries'; -import { generateAllCustomMutationHooks } from './custom-mutations'; -import { generateQueryKeysFile } from './query-keys'; -import { generateMutationKeysFile } from './mutation-keys'; -import { generateInvalidationFile } from './invalidation'; import { - generateQueriesBarrel, - generateMutationsBarrel, - generateMainBarrel, - generateCustomQueriesBarrel, generateCustomMutationsBarrel, + generateCustomQueriesBarrel, + generateMainBarrel, + generateMutationsBarrel, + generateQueriesBarrel } from './barrel'; +import { generateClientFile } from './client'; +import { generateAllCustomMutationHooks } from './custom-mutations'; +import { generateAllCustomQueryHooks } from './custom-queries'; +import { generateInvalidationFile } from './invalidation'; +import { generateMutationKeysFile } from './mutation-keys'; +import { generateAllMutationHooks } from './mutations'; +import { generateAllQueryHooks } from './queries'; +import { generateQueryKeysFile } from './query-keys'; import { getTableNames } from './utils'; // ============================================================================ @@ -114,7 +110,7 @@ export function generateAllFiles( * (they're expected to exist in the shared types directory). */ export function generate(options: GenerateOptions): GenerateResult { - const { tables, customOperations, config, sharedTypesPath } = options; + const { tables, customOperations, config } = options; const files: GeneratedFile[] = []; // Extract codegen options @@ -127,62 +123,18 @@ export function generate(options: GenerateOptions): GenerateResult { const useCentralizedKeys = queryKeyConfig.generateScopedKeys; const hasRelationships = Object.keys(queryKeyConfig.relationships).length > 0; - // 1. Generate client.ts + // 1. Generate client.ts (ORM client wrapper) files.push({ path: 'client.ts', - content: generateClientFile({ - browserCompatible: config.browserCompatible ?? true, - }), + content: generateClientFile() }); // Collect table type names for import path resolution const tableTypeNames = new Set(tables.map((t) => getTableNames(t).typeName)); - // When using shared types, skip generating types.ts and schema-types.ts - // They're already generated in the shared directory - let hasSchemaTypes = false; - let generatedEnumNames: string[] = []; - - if (sharedTypesPath) { - // Using shared types - check if schema-types would be generated - if (customOperations && customOperations.typeRegistry) { - const schemaTypesResult = generateSchemaTypesFile({ - typeRegistry: customOperations.typeRegistry, - tableTypeNames, - }); - if (schemaTypesResult.content.split('\n').length > 10) { - hasSchemaTypes = true; - generatedEnumNames = schemaTypesResult.generatedEnums || []; - } - } - } else { - // 2. Generate schema-types.ts for custom operations (if any) - // NOTE: This must come BEFORE types.ts so that types.ts can import enum types - if (customOperations && customOperations.typeRegistry) { - const schemaTypesResult = generateSchemaTypesFile({ - typeRegistry: customOperations.typeRegistry, - tableTypeNames, - }); - - // Only include if there's meaningful content - if (schemaTypesResult.content.split('\n').length > 10) { - files.push({ - path: 'schema-types.ts', - content: schemaTypesResult.content, - }); - hasSchemaTypes = true; - generatedEnumNames = schemaTypesResult.generatedEnums || []; - } - } - - // 3. Generate types.ts (can now import enums from schema-types) - files.push({ - path: 'types.ts', - content: generateTypesFile(tables, { - enumsFromSchemaTypes: generatedEnumNames, - }), - }); - } + // NOTE: types.ts and schema-types.ts are no longer generated here. + // Hooks now import types directly from the ORM's input-types.ts, + // which serves as the single source of truth for all types. // 3b. Generate centralized query keys (query-keys.ts) let hasQueryKeys = false; @@ -190,11 +142,11 @@ export function generate(options: GenerateOptions): GenerateResult { const queryKeysResult = generateQueryKeysFile({ tables, customQueries: customOperations?.queries ?? [], - config: queryKeyConfig, + config: queryKeyConfig }); files.push({ path: queryKeysResult.fileName, - content: queryKeysResult.content, + content: queryKeysResult.content }); hasQueryKeys = true; } @@ -205,11 +157,11 @@ export function generate(options: GenerateOptions): GenerateResult { const mutationKeysResult = generateMutationKeysFile({ tables, customMutations: customOperations?.mutations ?? [], - config: queryKeyConfig, + config: queryKeyConfig }); files.push({ path: mutationKeysResult.fileName, - content: mutationKeysResult.content, + content: mutationKeysResult.content }); hasMutationKeys = true; } @@ -219,11 +171,11 @@ export function generate(options: GenerateOptions): GenerateResult { if (useCentralizedKeys && queryKeyConfig.generateCascadeHelpers) { const invalidationResult = generateInvalidationFile({ tables, - config: queryKeyConfig, + config: queryKeyConfig }); files.push({ path: invalidationResult.fileName, - content: invalidationResult.content, + content: invalidationResult.content }); hasInvalidation = true; } @@ -232,12 +184,12 @@ export function generate(options: GenerateOptions): GenerateResult { const queryHooks = generateAllQueryHooks(tables, { reactQueryEnabled, useCentralizedKeys, - hasRelationships, + hasRelationships }); for (const hook of queryHooks) { files.push({ path: `queries/${hook.fileName}`, - content: hook.content, + content: hook.content }); } @@ -255,13 +207,13 @@ export function generate(options: GenerateOptions): GenerateResult { skipQueryField, reactQueryEnabled, tableTypeNames, - useCentralizedKeys, + useCentralizedKeys }); for (const hook of customQueryHooks) { files.push({ path: `queries/${hook.fileName}`, - content: hook.content, + content: hook.content }); } } @@ -272,24 +224,21 @@ export function generate(options: GenerateOptions): GenerateResult { content: customQueryHooks.length > 0 ? generateCustomQueriesBarrel( - tables, - customQueryHooks.map((h) => h.operationName) - ) - : generateQueriesBarrel(tables), + tables, + customQueryHooks.map((h) => h.operationName) + ) + : generateQueriesBarrel(tables) }); // 6. Generate table-based mutation hooks (mutations/*.ts) const mutationHooks = generateAllMutationHooks(tables, { reactQueryEnabled, - enumsFromSchemaTypes: generatedEnumNames, - useCentralizedKeys, - hasRelationships, - tableTypeNames, + useCentralizedKeys }); for (const hook of mutationHooks) { files.push({ path: `mutations/${hook.fileName}`, - content: hook.content, + content: hook.content }); } @@ -307,13 +256,13 @@ export function generate(options: GenerateOptions): GenerateResult { skipQueryField, reactQueryEnabled, tableTypeNames, - useCentralizedKeys, + useCentralizedKeys }); for (const hook of customMutationHooks) { files.push({ path: `mutations/${hook.fileName}`, - content: hook.content, + content: hook.content }); } } @@ -328,23 +277,23 @@ export function generate(options: GenerateOptions): GenerateResult { content: customMutationHooks.length > 0 ? generateCustomMutationsBarrel( - tables, - customMutationHooks.map((h) => h.operationName) - ) - : generateMutationsBarrel(tables), + tables, + customMutationHooks.map((h) => h.operationName) + ) + : generateMutationsBarrel(tables) }); } - // 9. Generate main index.ts barrel (with schema-types if present) + // 9. Generate main index.ts barrel + // No longer includes types.ts or schema-types.ts - hooks import from ORM directly files.push({ path: 'index.ts', content: generateMainBarrel(tables, { - hasSchemaTypes, hasMutations, hasQueryKeys, hasMutationKeys, - hasInvalidation, - }), + hasInvalidation + }) }); return { @@ -355,8 +304,8 @@ export function generate(options: GenerateOptions): GenerateResult { mutationHooks: mutationHooks.length, customQueryHooks: customQueryHooks.length, customMutationHooks: customMutationHooks.length, - totalFiles: files.length, - }, + totalFiles: files.length + } }; } @@ -364,34 +313,33 @@ export function generate(options: GenerateOptions): GenerateResult { // Re-exports for convenience // ============================================================================ +export { + generateCustomMutationsBarrel, + generateCustomQueriesBarrel, + generateMainBarrel, + generateMutationsBarrel, + generateQueriesBarrel +} from './barrel'; export { generateClientFile } from './client'; -export { generateTypesFile } from './types'; export { - generateAllQueryHooks, - generateListQueryHook, - generateSingleQueryHook, -} from './queries'; + generateAllCustomMutationHooks, + generateCustomMutationHook +} from './custom-mutations'; +export { + generateAllCustomQueryHooks, + generateCustomQueryHook +} from './custom-queries'; +export { generateInvalidationFile } from './invalidation'; +export { generateMutationKeysFile } from './mutation-keys'; export { generateAllMutationHooks, generateCreateMutationHook, - generateUpdateMutationHook, generateDeleteMutationHook, + generateUpdateMutationHook } from './mutations'; export { - generateAllCustomQueryHooks, - generateCustomQueryHook, -} from './custom-queries'; -export { - generateAllCustomMutationHooks, - generateCustomMutationHook, -} from './custom-mutations'; -export { - generateQueriesBarrel, - generateMutationsBarrel, - generateMainBarrel, - generateCustomQueriesBarrel, - generateCustomMutationsBarrel, -} from './barrel'; + generateAllQueryHooks, + generateListQueryHook, + generateSingleQueryHook +} from './queries'; export { generateQueryKeysFile } from './query-keys'; -export { generateMutationKeysFile } from './mutation-keys'; -export { generateInvalidationFile } from './invalidation'; diff --git a/graphql/codegen/src/core/codegen/invalidation.ts b/graphql/codegen/src/core/codegen/invalidation.ts index 96247303e..ef9a5bf4b 100644 --- a/graphql/codegen/src/core/codegen/invalidation.ts +++ b/graphql/codegen/src/core/codegen/invalidation.ts @@ -4,17 +4,18 @@ * Generates type-safe cache invalidation utilities with cascade support * for parent-child entity relationships. */ -import type { CleanTable } from '../../types/schema'; -import type { QueryKeyConfig, EntityRelationship } from '../../types/config'; -import { getTableNames, getGeneratedFileHeader, ucFirst, lcFirst } from './utils'; import * as t from '@babel/types'; + +import type { EntityRelationship,QueryKeyConfig } from '../../types/config'; +import type { CleanTable } from '../../types/schema'; import { - generateCode, addJSDocComment, - asConst, - typedParam, addLineComment, + asConst, + generateCode, + typedParam } from './babel-ast'; +import { getGeneratedFileHeader, getTableNames, lcFirst,ucFirst } from './utils'; export interface InvalidationGeneratorOptions { tables: CleanTable[]; @@ -247,7 +248,7 @@ function buildEntityInvalidateProperty( const withChildrenProp = t.objectProperty(t.identifier('withChildren'), withChildrenArrowFn); addJSDocComment(withChildrenProp, [ `Invalidate ${singularName} and all child entities`, - `Cascades to: ${descendants.join(', ')}`, + `Cascades to: ${descendants.join(', ')}` ]); innerProperties.push(withChildrenProp); } @@ -397,7 +398,7 @@ export function generateInvalidationFile( 'invalidate.user.lists(queryClient);', '', '// Invalidate specific user', - 'invalidate.user.detail(queryClient, userId);', + 'invalidate.user.detail(queryClient, userId);' ]; if (generateCascadeHelpers && Object.keys(relationships).length > 0) { invalidateDocLines.push(''); @@ -426,7 +427,7 @@ export function generateInvalidationFile( 'Remove queries from cache (for delete operations)', '', 'Use these when an entity is deleted to remove it from cache', - 'instead of just invalidating (which would trigger a refetch).', + 'instead of just invalidating (which would trigger a refetch).' ]); statements.push(removeDecl); @@ -484,6 +485,6 @@ ${description} return { fileName: 'invalidation.ts', - content, + content }; } diff --git a/graphql/codegen/src/core/codegen/mutation-keys.ts b/graphql/codegen/src/core/codegen/mutation-keys.ts index 431d84fe3..5816e3ee1 100644 --- a/graphql/codegen/src/core/codegen/mutation-keys.ts +++ b/graphql/codegen/src/core/codegen/mutation-keys.ts @@ -7,17 +7,18 @@ * - Mutation deduplication * - Tracking mutation state with useIsMutating */ -import type { CleanTable, CleanOperation } from '../../types/schema'; -import type { QueryKeyConfig, EntityRelationship } from '../../types/config'; -import { getTableNames, getGeneratedFileHeader, lcFirst } from './utils'; import * as t from '@babel/types'; + +import type { EntityRelationship,QueryKeyConfig } from '../../types/config'; +import type { CleanOperation,CleanTable } from '../../types/schema'; import { - generateCode, addJSDocComment, asConst, constArray, - typedParam, + generateCode, + typedParam } from './babel-ast'; +import { getGeneratedFileHeader, getTableNames, lcFirst } from './utils'; export interface MutationKeyGeneratorOptions { tables: CleanTable[]; @@ -75,12 +76,12 @@ function generateEntityMutationKeysDeclaration( false, true ) - ]), + ]) ]), constArray([ t.stringLiteral('mutation'), t.stringLiteral(entityKey), - t.stringLiteral('create'), + t.stringLiteral('create') ]) ) ); @@ -92,7 +93,7 @@ function generateEntityMutationKeysDeclaration( constArray([ t.stringLiteral('mutation'), t.stringLiteral(entityKey), - t.stringLiteral('create'), + t.stringLiteral('create') ]) ); @@ -108,7 +109,7 @@ function generateEntityMutationKeysDeclaration( t.stringLiteral('mutation'), t.stringLiteral(entityKey), t.stringLiteral('update'), - t.identifier('id'), + t.identifier('id') ]) ); const updateProp = t.objectProperty(t.identifier('update'), updateArrowFn); @@ -122,7 +123,7 @@ function generateEntityMutationKeysDeclaration( t.stringLiteral('mutation'), t.stringLiteral(entityKey), t.stringLiteral('delete'), - t.identifier('id'), + t.identifier('id') ]) ); const deleteProp = t.objectProperty(t.identifier('delete'), deleteArrowFn); @@ -166,7 +167,7 @@ function generateCustomMutationKeysDeclaration( constArray([ t.stringLiteral('mutation'), t.stringLiteral(op.name), - t.identifier('identifier'), + t.identifier('identifier') ]), constArray([t.stringLiteral('mutation'), t.stringLiteral(op.name)]) ) @@ -243,7 +244,7 @@ function generateUnifiedMutationStoreDeclaration( '', '// Check if a specific user is being updated', 'const isUpdating = useIsMutating({ mutationKey: mutationKeys.user.update(userId) });', - '```', + '```' ]); return decl; @@ -335,6 +336,6 @@ ${description} return { fileName: 'mutation-keys.ts', - content, + content }; } diff --git a/graphql/codegen/src/core/codegen/mutations.ts b/graphql/codegen/src/core/codegen/mutations.ts index 0a3a75227..8e368337e 100644 --- a/graphql/codegen/src/core/codegen/mutations.ts +++ b/graphql/codegen/src/core/codegen/mutations.ts @@ -1,51 +1,31 @@ /** - * Mutation hook generators using Babel AST-based code generation + * Mutation hook generators - delegates to ORM model methods * * Output structure: * mutations/ - * useCreateCarMutation.ts - * useUpdateCarMutation.ts - * useDeleteCarMutation.ts + * useCreateCarMutation.ts -> ORM create + * useUpdateCarMutation.ts -> ORM update + * useDeleteCarMutation.ts -> ORM delete */ import type { CleanTable } from '../../types/schema'; -import * as t from '@babel/types'; -import { generateCode, addJSDocComment, typedParam, createTypedCallExpression } from './babel-ast'; import { - buildCreateMutationAST, - buildUpdateMutationAST, - buildDeleteMutationAST, - printGraphQL, -} from './gql-ast'; -import { - getTableNames, - getCreateMutationHookName, - getUpdateMutationHookName, - getDeleteMutationHookName, getCreateMutationFileName, - getUpdateMutationFileName, - getDeleteMutationFileName, + getCreateMutationHookName, getCreateMutationName, - getUpdateMutationName, + getDefaultSelectFieldName, + getDeleteMutationFileName, + getDeleteMutationHookName, getDeleteMutationName, - getScalarFields, - getPrimaryKeyInfo, - fieldTypeToTs, - ucFirst, - lcFirst, getGeneratedFileHeader, + getPrimaryKeyInfo, + getTableNames, + getUpdateMutationFileName, + getUpdateMutationHookName, + getUpdateMutationName, + hasValidPrimaryKey, + lcFirst } from './utils'; -function isAutoGeneratedField(fieldName: string, pkFieldNames: Set): boolean { - const name = fieldName.toLowerCase(); - if (pkFieldNames.has(fieldName)) return true; - const timestampPatterns = [ - 'createdat', 'created_at', 'createddate', 'created_date', - 'updatedat', 'updated_at', 'updateddate', 'updated_date', - 'deletedat', 'deleted_at', - ]; - return timestampPatterns.includes(name); -} - export interface GeneratedMutationFile { fileName: string; content: string; @@ -53,11 +33,7 @@ export interface GeneratedMutationFile { export interface MutationGeneratorOptions { reactQueryEnabled?: boolean; - enumsFromSchemaTypes?: string[]; useCentralizedKeys?: boolean; - hasRelationships?: boolean; - /** All table type names for determining which types to import from types.ts vs schema-types.ts */ - tableTypeNames?: Set; } export function generateCreateMutationHook( @@ -66,293 +42,95 @@ export function generateCreateMutationHook( ): GeneratedMutationFile | null { const { reactQueryEnabled = true, - enumsFromSchemaTypes = [], - useCentralizedKeys = true, - hasRelationships = false, - tableTypeNames = new Set(), + useCentralizedKeys = true } = options; if (!reactQueryEnabled) { return null; } - const enumSet = new Set(enumsFromSchemaTypes); const { typeName, singularName } = getTableNames(table); const hookName = getCreateMutationHookName(table); + const mutationName = getCreateMutationName(table); const keysName = `${lcFirst(typeName)}Keys`; const mutationKeysName = `${lcFirst(typeName)}MutationKeys`; - const scopeTypeName = `${typeName}Scope`; - const mutationName = getCreateMutationName(table); - const scalarFields = getScalarFields(table); - - const pkFieldNames = new Set(getPrimaryKeyInfo(table).map((pk) => pk.name)); - - const usedEnums = new Set(); - const usedTableTypes = new Set(); - for (const field of scalarFields) { - const cleanType = field.type.gqlType.replace(/!/g, ''); - if (enumSet.has(cleanType)) { - usedEnums.add(cleanType); - } else if (tableTypeNames.has(cleanType) && cleanType !== typeName) { - // Track table types used in scalar fields (excluding the main type which is already imported) - usedTableTypes.add(cleanType); - } - } + const selectTypeName = `${typeName}Select`; + const relationTypeName = `${typeName}WithRelations`; + const createInputTypeName = `Create${typeName}Input`; + + const defaultFieldName = getDefaultSelectFieldName(table); + + const lines: string[] = []; - const mutationAST = buildCreateMutationAST({ table }); - const mutationDocument = printGraphQL(mutationAST); - - const statements: t.Statement[] = []; - - const reactQueryImport = t.importDeclaration( - [ - t.importSpecifier(t.identifier('useMutation'), t.identifier('useMutation')), - t.importSpecifier(t.identifier('useQueryClient'), t.identifier('useQueryClient')), - ], - t.stringLiteral('@tanstack/react-query') - ); - statements.push(reactQueryImport); - - const reactQueryTypeImport = t.importDeclaration( - [t.importSpecifier(t.identifier('UseMutationOptions'), t.identifier('UseMutationOptions'))], - t.stringLiteral('@tanstack/react-query') - ); - reactQueryTypeImport.importKind = 'type'; - statements.push(reactQueryTypeImport); - - const clientImport = t.importDeclaration( - [t.importSpecifier(t.identifier('execute'), t.identifier('execute'))], - t.stringLiteral('../client') - ); - statements.push(clientImport); - - // Import the main type and any other table types used in scalar fields - const allTypesToImport = [typeName, ...Array.from(usedTableTypes)].sort(); - const typesImport = t.importDeclaration( - allTypesToImport.map((t_) => t.importSpecifier(t.identifier(t_), t.identifier(t_))), - t.stringLiteral('../types') - ); - typesImport.importKind = 'type'; - statements.push(typesImport); - - if (usedEnums.size > 0) { - const enumImport = t.importDeclaration( - Array.from(usedEnums).sort().map((e) => t.importSpecifier(t.identifier(e), t.identifier(e))), - t.stringLiteral('../schema-types') - ); - enumImport.importKind = 'type'; - statements.push(enumImport); + // Imports + lines.push(`import { useMutation, useQueryClient } from '@tanstack/react-query';`); + lines.push(`import type { UseMutationOptions } from '@tanstack/react-query';`); + lines.push(`import { getClient } from '../client';`); + + if (useCentralizedKeys) { + lines.push(`import { ${keysName} } from '../query-keys';`); + lines.push(`import { ${mutationKeysName} } from '../mutation-keys';`); } + lines.push(`import type {`); + lines.push(` ${selectTypeName},`); + lines.push(` ${relationTypeName},`); + lines.push(` ${createInputTypeName},`); + lines.push(`} from '../../orm/input-types';`); + lines.push(`import type {`); + lines.push(` DeepExact,`); + lines.push(` InferSelectResult,`); + lines.push(`} from '../../orm/select-types';`); + lines.push(''); + + // Re-export types + lines.push(`export type { ${selectTypeName}, ${relationTypeName}, ${createInputTypeName} } from '../../orm/input-types';`); + lines.push(''); + + lines.push(`const defaultSelect = { ${defaultFieldName}: true } as const;`); + lines.push(''); + + // Hook + lines.push(`/**`); + lines.push(` * Mutation hook for creating a ${typeName}`); + lines.push(` *`); + lines.push(` * @example`); + lines.push(` * \`\`\`tsx`); + lines.push(` * const { mutate, isPending } = ${hookName}({`); + lines.push(` * select: { id: true, name: true },`); + lines.push(` * });`); + lines.push(` *`); + lines.push(` * mutate({ name: 'New item' });`); + lines.push(` * \`\`\``); + lines.push(` */`); + lines.push(`export function ${hookName}(`); + lines.push(` args?: { select?: DeepExact },`); + lines.push(` options?: Omit } }, Error, ${createInputTypeName}['${singularName}']>, 'mutationFn'>`); + lines.push(`) {`); + lines.push(` const queryClient = useQueryClient();`); + lines.push(` return useMutation({`); + if (useCentralizedKeys) { - const queryKeyImport = t.importDeclaration( - [t.importSpecifier(t.identifier(keysName), t.identifier(keysName))], - t.stringLiteral('../query-keys') - ); - statements.push(queryKeyImport); - if (hasRelationships) { - const scopeTypeImport = t.importDeclaration( - [t.importSpecifier(t.identifier(scopeTypeName), t.identifier(scopeTypeName))], - t.stringLiteral('../query-keys') - ); - scopeTypeImport.importKind = 'type'; - statements.push(scopeTypeImport); - } - const mutationKeyImport = t.importDeclaration( - [t.importSpecifier(t.identifier(mutationKeysName), t.identifier(mutationKeysName))], - t.stringLiteral('../mutation-keys') - ); - statements.push(mutationKeyImport); + lines.push(` mutationKey: ${mutationKeysName}.create(),`); } - const reExportDecl = t.exportNamedDeclaration( - null, - [t.exportSpecifier(t.identifier(typeName), t.identifier(typeName))], - t.stringLiteral('../types') - ); - reExportDecl.exportKind = 'type'; - statements.push(reExportDecl); - - const mutationDocConst = t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(`${mutationName}MutationDocument`), - t.templateLiteral( - [t.templateElement({ raw: '\n' + mutationDocument, cooked: '\n' + mutationDocument }, true)], - [] - ) - ), - ]); - statements.push(t.exportNamedDeclaration(mutationDocConst)); - - const inputFields = scalarFields - .filter((f) => !isAutoGeneratedField(f.name, pkFieldNames)) - .map((f) => { - const prop = t.tsPropertySignature( - t.identifier(f.name), - t.tsTypeAnnotation( - t.tsUnionType([ - t.tsTypeReference(t.identifier(fieldTypeToTs(f.type))), - t.tsNullKeyword(), - ]) - ) - ); - prop.optional = true; - return prop; - }); - - const createInputInterface = t.tsInterfaceDeclaration( - t.identifier(`${typeName}CreateInput`), - null, - null, - t.tsInterfaceBody(inputFields) - ); - addJSDocComment(createInputInterface, [`Input type for creating a ${typeName}`]); - statements.push(createInputInterface); - - const variablesInterfaceBody = t.tsInterfaceBody([ - t.tsPropertySignature( - t.identifier('input'), - t.tsTypeAnnotation( - t.tsTypeLiteral([ - t.tsPropertySignature( - t.identifier(lcFirst(typeName)), - t.tsTypeAnnotation(t.tsTypeReference(t.identifier(`${typeName}CreateInput`))) - ), - ]) - ) - ), - ]); - const variablesInterface = t.tsInterfaceDeclaration( - t.identifier(`${ucFirst(mutationName)}MutationVariables`), - null, - null, - variablesInterfaceBody - ); - statements.push(t.exportNamedDeclaration(variablesInterface)); - - const resultInterfaceBody = t.tsInterfaceBody([ - t.tsPropertySignature( - t.identifier(mutationName), - t.tsTypeAnnotation( - t.tsTypeLiteral([ - t.tsPropertySignature( - t.identifier(singularName), - t.tsTypeAnnotation(t.tsTypeReference(t.identifier(typeName))) - ), - ]) - ) - ), - ]); - const resultInterface = t.tsInterfaceDeclaration( - t.identifier(`${ucFirst(mutationName)}MutationResult`), - null, - null, - resultInterfaceBody - ); - statements.push(t.exportNamedDeclaration(resultInterface)); - - const hookBodyStatements: t.Statement[] = []; - hookBodyStatements.push( - t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier('queryClient'), - t.callExpression(t.identifier('useQueryClient'), []) - ), - ]) - ); - - const mutationOptions: (t.ObjectProperty | t.SpreadElement)[] = []; - if (useCentralizedKeys) { - mutationOptions.push( - t.objectProperty( - t.identifier('mutationKey'), - t.callExpression( - t.memberExpression(t.identifier(mutationKeysName), t.identifier('create')), - [] - ) - ) - ); - } - mutationOptions.push( - t.objectProperty( - t.identifier('mutationFn'), - t.arrowFunctionExpression( - [typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)))], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(`${mutationName}MutationDocument`), t.identifier('variables')], - [ - t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationResult`)), - t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)), - ] - ) - ) - ) - ); - - const invalidateQueryKey = useCentralizedKeys - ? t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('lists')), []) - : t.arrayExpression([t.stringLiteral(typeName.toLowerCase()), t.stringLiteral('list')]); - - mutationOptions.push( - t.objectProperty( - t.identifier('onSuccess'), - t.arrowFunctionExpression( - [], - t.blockStatement([ - t.expressionStatement( - t.callExpression( - t.memberExpression(t.identifier('queryClient'), t.identifier('invalidateQueries')), - [t.objectExpression([t.objectProperty(t.identifier('queryKey'), invalidateQueryKey)])] - ) - ), - ]) - ) - ) - ); - mutationOptions.push(t.spreadElement(t.identifier('options'))); - - hookBodyStatements.push( - t.returnStatement( - t.callExpression(t.identifier('useMutation'), [t.objectExpression(mutationOptions)]) - ) - ); - - const optionsTypeStr = `Omit, 'mutationFn'>`; - const optionsParam = t.identifier('options'); - optionsParam.optional = true; - optionsParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(optionsTypeStr))); - - const hookFunc = t.functionDeclaration( - t.identifier(hookName), - [optionsParam], - t.blockStatement(hookBodyStatements) - ); - const hookExport = t.exportNamedDeclaration(hookFunc); - addJSDocComment(hookExport, [ - `Mutation hook for creating a ${typeName}`, - '', - '@example', - '```tsx', - `const { mutate, isPending } = ${hookName}();`, - '', - 'mutate({', - ' input: {', - ` ${lcFirst(typeName)}: {`, - ' // ... fields', - ' },', - ' },', - '});', - '```', - ]); - statements.push(hookExport); - - const code = generateCode(statements); - const content = getGeneratedFileHeader(`Create mutation hook for ${typeName}`) + '\n\n' + code; + lines.push(` mutationFn: (data: ${createInputTypeName}['${singularName}']) => getClient().${singularName}.create({ data, select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(),`); + + const listKey = useCentralizedKeys + ? `${keysName}.lists()` + : `['${typeName.toLowerCase()}', 'list']`; + lines.push(` onSuccess: () => {`); + lines.push(` queryClient.invalidateQueries({ queryKey: ${listKey} });`); + lines.push(` },`); + lines.push(` ...options,`); + lines.push(` });`); + lines.push(`}`); + + const content = getGeneratedFileHeader(`Create mutation hook for ${typeName}`) + '\n\n' + lines.join('\n') + '\n'; return { fileName: getCreateMutationFileName(table), - content, + content }; } @@ -362,10 +140,7 @@ export function generateUpdateMutationHook( ): GeneratedMutationFile | null { const { reactQueryEnabled = true, - enumsFromSchemaTypes = [], - useCentralizedKeys = true, - hasRelationships = false, - tableTypeNames = new Set(), + useCentralizedKeys = true } = options; if (!reactQueryEnabled) { @@ -376,307 +151,98 @@ export function generateUpdateMutationHook( return null; } - const enumSet = new Set(enumsFromSchemaTypes); + if (!hasValidPrimaryKey(table)) { + return null; + } + const { typeName, singularName } = getTableNames(table); const hookName = getUpdateMutationHookName(table); const mutationName = getUpdateMutationName(table); - const scalarFields = getScalarFields(table); const keysName = `${lcFirst(typeName)}Keys`; const mutationKeysName = `${lcFirst(typeName)}MutationKeys`; - const scopeTypeName = `${typeName}Scope`; + const selectTypeName = `${typeName}Select`; + const relationTypeName = `${typeName}WithRelations`; + const patchTypeName = `${typeName}Patch`; const pkFields = getPrimaryKeyInfo(table); const pkField = pkFields[0]; - const pkFieldNames = new Set(pkFields.map((pk) => pk.name)); - - const usedEnums = new Set(); - const usedTableTypes = new Set(); - for (const field of scalarFields) { - const cleanType = field.type.gqlType.replace(/!/g, ''); - if (enumSet.has(cleanType)) { - usedEnums.add(cleanType); - } else if (tableTypeNames.has(cleanType) && cleanType !== typeName) { - usedTableTypes.add(cleanType); - } - } - const mutationAST = buildUpdateMutationAST({ table }); - const mutationDocument = printGraphQL(mutationAST); - - const statements: t.Statement[] = []; - - const reactQueryImport = t.importDeclaration( - [ - t.importSpecifier(t.identifier('useMutation'), t.identifier('useMutation')), - t.importSpecifier(t.identifier('useQueryClient'), t.identifier('useQueryClient')), - ], - t.stringLiteral('@tanstack/react-query') - ); - statements.push(reactQueryImport); - - const reactQueryTypeImport = t.importDeclaration( - [t.importSpecifier(t.identifier('UseMutationOptions'), t.identifier('UseMutationOptions'))], - t.stringLiteral('@tanstack/react-query') - ); - reactQueryTypeImport.importKind = 'type'; - statements.push(reactQueryTypeImport); - - const clientImport = t.importDeclaration( - [t.importSpecifier(t.identifier('execute'), t.identifier('execute'))], - t.stringLiteral('../client') - ); - statements.push(clientImport); - - // Import the main type and any other table types used in scalar fields - const allTypesToImportUpdate = [typeName, ...Array.from(usedTableTypes)].sort(); - const typesImport = t.importDeclaration( - allTypesToImportUpdate.map((t_) => t.importSpecifier(t.identifier(t_), t.identifier(t_))), - t.stringLiteral('../types') - ); - typesImport.importKind = 'type'; - statements.push(typesImport); - - if (usedEnums.size > 0) { - const enumImport = t.importDeclaration( - Array.from(usedEnums).sort().map((e) => t.importSpecifier(t.identifier(e), t.identifier(e))), - t.stringLiteral('../schema-types') - ); - enumImport.importKind = 'type'; - statements.push(enumImport); - } + const lines: string[] = []; + + // Imports + lines.push(`import { useMutation, useQueryClient } from '@tanstack/react-query';`); + lines.push(`import type { UseMutationOptions } from '@tanstack/react-query';`); + lines.push(`import { getClient } from '../client';`); if (useCentralizedKeys) { - const queryKeyImport = t.importDeclaration( - [t.importSpecifier(t.identifier(keysName), t.identifier(keysName))], - t.stringLiteral('../query-keys') - ); - statements.push(queryKeyImport); - if (hasRelationships) { - const scopeTypeImport = t.importDeclaration( - [t.importSpecifier(t.identifier(scopeTypeName), t.identifier(scopeTypeName))], - t.stringLiteral('../query-keys') - ); - scopeTypeImport.importKind = 'type'; - statements.push(scopeTypeImport); - } - const mutationKeyImport = t.importDeclaration( - [t.importSpecifier(t.identifier(mutationKeysName), t.identifier(mutationKeysName))], - t.stringLiteral('../mutation-keys') - ); - statements.push(mutationKeyImport); + lines.push(`import { ${keysName} } from '../query-keys';`); + lines.push(`import { ${mutationKeysName} } from '../mutation-keys';`); } - const reExportDecl = t.exportNamedDeclaration( - null, - [t.exportSpecifier(t.identifier(typeName), t.identifier(typeName))], - t.stringLiteral('../types') - ); - reExportDecl.exportKind = 'type'; - statements.push(reExportDecl); - - const mutationDocConst = t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(`${mutationName}MutationDocument`), - t.templateLiteral( - [t.templateElement({ raw: '\n' + mutationDocument, cooked: '\n' + mutationDocument }, true)], - [] - ) - ), - ]); - statements.push(t.exportNamedDeclaration(mutationDocConst)); - - const patchFields = scalarFields - .filter((f) => !pkFieldNames.has(f.name)) - .map((f) => { - const prop = t.tsPropertySignature( - t.identifier(f.name), - t.tsTypeAnnotation( - t.tsUnionType([ - t.tsTypeReference(t.identifier(fieldTypeToTs(f.type))), - t.tsNullKeyword(), - ]) - ) - ); - prop.optional = true; - return prop; - }); - - const patchInterface = t.tsInterfaceDeclaration( - t.identifier(`${typeName}Patch`), - null, - null, - t.tsInterfaceBody(patchFields) - ); - addJSDocComment(patchInterface, [`Patch type for updating a ${typeName} - all fields optional`]); - statements.push(patchInterface); - - const pkTypeAnnotation = - pkField.tsType === 'string' - ? t.tsStringKeyword() - : pkField.tsType === 'number' - ? t.tsNumberKeyword() - : t.tsTypeReference(t.identifier(pkField.tsType)); - - const variablesInterfaceBody = t.tsInterfaceBody([ - t.tsPropertySignature( - t.identifier('input'), - t.tsTypeAnnotation( - t.tsTypeLiteral([ - t.tsPropertySignature(t.identifier(pkField.name), t.tsTypeAnnotation(pkTypeAnnotation)), - t.tsPropertySignature( - t.identifier('patch'), - t.tsTypeAnnotation(t.tsTypeReference(t.identifier(`${typeName}Patch`))) - ), - ]) - ) - ), - ]); - const variablesInterface = t.tsInterfaceDeclaration( - t.identifier(`${ucFirst(mutationName)}MutationVariables`), - null, - null, - variablesInterfaceBody - ); - statements.push(t.exportNamedDeclaration(variablesInterface)); - - const resultInterfaceBody = t.tsInterfaceBody([ - t.tsPropertySignature( - t.identifier(mutationName), - t.tsTypeAnnotation( - t.tsTypeLiteral([ - t.tsPropertySignature( - t.identifier(singularName), - t.tsTypeAnnotation(t.tsTypeReference(t.identifier(typeName))) - ), - ]) - ) - ), - ]); - const resultInterface = t.tsInterfaceDeclaration( - t.identifier(`${ucFirst(mutationName)}MutationResult`), - null, - null, - resultInterfaceBody - ); - statements.push(t.exportNamedDeclaration(resultInterface)); - - const hookBodyStatements: t.Statement[] = []; - hookBodyStatements.push( - t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier('queryClient'), - t.callExpression(t.identifier('useQueryClient'), []) - ), - ]) - ); - - const mutationOptions: (t.ObjectProperty | t.SpreadElement)[] = []; + lines.push(`import type {`); + lines.push(` ${selectTypeName},`); + lines.push(` ${relationTypeName},`); + lines.push(` ${patchTypeName},`); + lines.push(`} from '../../orm/input-types';`); + lines.push(`import type {`); + lines.push(` DeepExact,`); + lines.push(` InferSelectResult,`); + lines.push(`} from '../../orm/select-types';`); + lines.push(''); + + // Re-export types + lines.push(`export type { ${selectTypeName}, ${relationTypeName}, ${patchTypeName} } from '../../orm/input-types';`); + lines.push(''); + + lines.push(`const defaultSelect = { ${pkField.name}: true } as const;`); + lines.push(''); + + // Hook + lines.push(`/**`); + lines.push(` * Mutation hook for updating a ${typeName}`); + lines.push(` *`); + lines.push(` * @example`); + lines.push(` * \`\`\`tsx`); + lines.push(` * const { mutate, isPending } = ${hookName}({`); + lines.push(` * select: { id: true, name: true },`); + lines.push(` * });`); + lines.push(` *`); + lines.push(` * mutate({ ${pkField.name}: 'value-here', patch: { name: 'Updated' } });`); + lines.push(` * \`\`\``); + lines.push(` */`); + lines.push(`export function ${hookName}(`); + lines.push(` args?: { select?: DeepExact },`); + lines.push(` options?: Omit } }, Error, { ${pkField.name}: ${pkField.tsType}; patch: ${patchTypeName} }>, 'mutationFn'>`); + lines.push(`) {`); + lines.push(` const queryClient = useQueryClient();`); + lines.push(` return useMutation({`); + if (useCentralizedKeys) { - mutationOptions.push( - t.objectProperty( - t.identifier('mutationKey'), - t.memberExpression(t.identifier(mutationKeysName), t.identifier('all')) - ) - ); + lines.push(` mutationKey: ${mutationKeysName}.all,`); } - mutationOptions.push( - t.objectProperty( - t.identifier('mutationFn'), - t.arrowFunctionExpression( - [typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)))], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(`${mutationName}MutationDocument`), t.identifier('variables')], - [ - t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationResult`)), - t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)), - ] - ) - ) - ) - ); - - const detailQueryKey = useCentralizedKeys - ? t.callExpression( - t.memberExpression(t.identifier(keysName), t.identifier('detail')), - [t.memberExpression(t.memberExpression(t.identifier('variables'), t.identifier('input')), t.identifier(pkField.name))] - ) - : t.arrayExpression([ - t.stringLiteral(typeName.toLowerCase()), - t.stringLiteral('detail'), - t.memberExpression(t.memberExpression(t.identifier('variables'), t.identifier('input')), t.identifier(pkField.name)), - ]); - - const listQueryKey = useCentralizedKeys - ? t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('lists')), []) - : t.arrayExpression([t.stringLiteral(typeName.toLowerCase()), t.stringLiteral('list')]); - - mutationOptions.push( - t.objectProperty( - t.identifier('onSuccess'), - t.arrowFunctionExpression( - [t.identifier('_'), t.identifier('variables')], - t.blockStatement([ - t.expressionStatement( - t.callExpression( - t.memberExpression(t.identifier('queryClient'), t.identifier('invalidateQueries')), - [t.objectExpression([t.objectProperty(t.identifier('queryKey'), detailQueryKey)])] - ) - ), - t.expressionStatement( - t.callExpression( - t.memberExpression(t.identifier('queryClient'), t.identifier('invalidateQueries')), - [t.objectExpression([t.objectProperty(t.identifier('queryKey'), listQueryKey)])] - ) - ), - ]) - ) - ) - ); - mutationOptions.push(t.spreadElement(t.identifier('options'))); - - hookBodyStatements.push( - t.returnStatement( - t.callExpression(t.identifier('useMutation'), [t.objectExpression(mutationOptions)]) - ) - ); - - const optionsTypeStr = `Omit, 'mutationFn'>`; - const optionsParam = t.identifier('options'); - optionsParam.optional = true; - optionsParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(optionsTypeStr))); - - const hookFunc = t.functionDeclaration( - t.identifier(hookName), - [optionsParam], - t.blockStatement(hookBodyStatements) - ); - const hookExport = t.exportNamedDeclaration(hookFunc); - addJSDocComment(hookExport, [ - `Mutation hook for updating a ${typeName}`, - '', - '@example', - '```tsx', - `const { mutate, isPending } = ${hookName}();`, - '', - 'mutate({', - ' input: {', - ` ${pkField.name}: ${pkField.tsType === 'string' ? "'value-here'" : '123'},`, - ' patch: {', - ' // ... fields to update', - ' },', - ' },', - '});', - '```', - ]); - statements.push(hookExport); - - const code = generateCode(statements); - const content = getGeneratedFileHeader(`Update mutation hook for ${typeName}`) + '\n\n' + code; + + lines.push(` mutationFn: ({ ${pkField.name}, patch }: { ${pkField.name}: ${pkField.tsType}; patch: ${patchTypeName} }) => getClient().${singularName}.update({ where: { ${pkField.name} }, data: patch, select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(),`); + + const detailKey = useCentralizedKeys + ? `${keysName}.detail(variables.${pkField.name})` + : `['${typeName.toLowerCase()}', 'detail', variables.${pkField.name}]`; + const listKey = useCentralizedKeys + ? `${keysName}.lists()` + : `['${typeName.toLowerCase()}', 'list']`; + + lines.push(` onSuccess: (_, variables) => {`); + lines.push(` queryClient.invalidateQueries({ queryKey: ${detailKey} });`); + lines.push(` queryClient.invalidateQueries({ queryKey: ${listKey} });`); + lines.push(` },`); + lines.push(` ...options,`); + lines.push(` });`); + lines.push(`}`); + + const content = getGeneratedFileHeader(`Update mutation hook for ${typeName}`) + '\n\n' + lines.join('\n') + '\n'; return { fileName: getUpdateMutationFileName(table), - content, + content }; } @@ -686,8 +252,7 @@ export function generateDeleteMutationHook( ): GeneratedMutationFile | null { const { reactQueryEnabled = true, - useCentralizedKeys = true, - hasRelationships = false, + useCentralizedKeys = true } = options; if (!reactQueryEnabled) { @@ -698,233 +263,96 @@ export function generateDeleteMutationHook( return null; } - const { typeName } = getTableNames(table); + if (!hasValidPrimaryKey(table)) { + return null; + } + + const { typeName, singularName } = getTableNames(table); const hookName = getDeleteMutationHookName(table); const mutationName = getDeleteMutationName(table); const keysName = `${lcFirst(typeName)}Keys`; const mutationKeysName = `${lcFirst(typeName)}MutationKeys`; - const scopeTypeName = `${typeName}Scope`; + const selectTypeName = `${typeName}Select`; + const relationTypeName = `${typeName}WithRelations`; const pkFields = getPrimaryKeyInfo(table); const pkField = pkFields[0]; - const mutationAST = buildDeleteMutationAST({ table }); - const mutationDocument = printGraphQL(mutationAST); - - const statements: t.Statement[] = []; - - const reactQueryImport = t.importDeclaration( - [ - t.importSpecifier(t.identifier('useMutation'), t.identifier('useMutation')), - t.importSpecifier(t.identifier('useQueryClient'), t.identifier('useQueryClient')), - ], - t.stringLiteral('@tanstack/react-query') - ); - statements.push(reactQueryImport); - - const reactQueryTypeImport = t.importDeclaration( - [t.importSpecifier(t.identifier('UseMutationOptions'), t.identifier('UseMutationOptions'))], - t.stringLiteral('@tanstack/react-query') - ); - reactQueryTypeImport.importKind = 'type'; - statements.push(reactQueryTypeImport); - - const clientImport = t.importDeclaration( - [t.importSpecifier(t.identifier('execute'), t.identifier('execute'))], - t.stringLiteral('../client') - ); - statements.push(clientImport); + const lines: string[] = []; + + // Imports + lines.push(`import { useMutation, useQueryClient } from '@tanstack/react-query';`); + lines.push(`import type { UseMutationOptions } from '@tanstack/react-query';`); + lines.push(`import { getClient } from '../client';`); if (useCentralizedKeys) { - const queryKeyImport = t.importDeclaration( - [t.importSpecifier(t.identifier(keysName), t.identifier(keysName))], - t.stringLiteral('../query-keys') - ); - statements.push(queryKeyImport); - if (hasRelationships) { - const scopeTypeImport = t.importDeclaration( - [t.importSpecifier(t.identifier(scopeTypeName), t.identifier(scopeTypeName))], - t.stringLiteral('../query-keys') - ); - scopeTypeImport.importKind = 'type'; - statements.push(scopeTypeImport); - } - const mutationKeyImport = t.importDeclaration( - [t.importSpecifier(t.identifier(mutationKeysName), t.identifier(mutationKeysName))], - t.stringLiteral('../mutation-keys') - ); - statements.push(mutationKeyImport); + lines.push(`import { ${keysName} } from '../query-keys';`); + lines.push(`import { ${mutationKeysName} } from '../mutation-keys';`); } - const mutationDocConst = t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(`${mutationName}MutationDocument`), - t.templateLiteral( - [t.templateElement({ raw: '\n' + mutationDocument, cooked: '\n' + mutationDocument }, true)], - [] - ) - ), - ]); - statements.push(t.exportNamedDeclaration(mutationDocConst)); - - const pkTypeAnnotation = - pkField.tsType === 'string' - ? t.tsStringKeyword() - : pkField.tsType === 'number' - ? t.tsNumberKeyword() - : t.tsTypeReference(t.identifier(pkField.tsType)); - - const variablesInterfaceBody = t.tsInterfaceBody([ - t.tsPropertySignature( - t.identifier('input'), - t.tsTypeAnnotation( - t.tsTypeLiteral([ - t.tsPropertySignature(t.identifier(pkField.name), t.tsTypeAnnotation(pkTypeAnnotation)), - ]) - ) - ), - ]); - const variablesInterface = t.tsInterfaceDeclaration( - t.identifier(`${ucFirst(mutationName)}MutationVariables`), - null, - null, - variablesInterfaceBody - ); - statements.push(t.exportNamedDeclaration(variablesInterface)); - - const clientMutationIdProp = t.tsPropertySignature( - t.identifier('clientMutationId'), - t.tsTypeAnnotation(t.tsUnionType([t.tsStringKeyword(), t.tsNullKeyword()])) - ); - - const resultInterfaceBody = t.tsInterfaceBody([ - t.tsPropertySignature( - t.identifier(mutationName), - t.tsTypeAnnotation(t.tsTypeLiteral([clientMutationIdProp])) - ), - ]); - const resultInterface = t.tsInterfaceDeclaration( - t.identifier(`${ucFirst(mutationName)}MutationResult`), - null, - null, - resultInterfaceBody - ); - statements.push(t.exportNamedDeclaration(resultInterface)); - - const hookBodyStatements: t.Statement[] = []; - hookBodyStatements.push( - t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier('queryClient'), - t.callExpression(t.identifier('useQueryClient'), []) - ), - ]) - ); - - const mutationOptions: (t.ObjectProperty | t.SpreadElement)[] = []; + lines.push(`import type {`); + lines.push(` ${selectTypeName},`); + lines.push(` ${relationTypeName},`); + lines.push(`} from '../../orm/input-types';`); + lines.push(`import type {`); + lines.push(` DeepExact,`); + lines.push(` InferSelectResult,`); + lines.push(`} from '../../orm/select-types';`); + lines.push(''); + + // Re-export types + lines.push(`export type { ${selectTypeName}, ${relationTypeName} } from '../../orm/input-types';`); + lines.push(''); + + lines.push(`const defaultSelect = { ${pkField.name}: true } as const;`); + lines.push(''); + + // Hook + lines.push(`/**`); + lines.push(` * Mutation hook for deleting a ${typeName}`); + lines.push(` *`); + lines.push(` * @example`); + lines.push(` * \`\`\`tsx`); + lines.push(` * const { mutate, isPending } = ${hookName}({`); + lines.push(` * select: { id: true },`); + lines.push(` * });`); + lines.push(` *`); + lines.push(` * mutate({ ${pkField.name}: ${pkField.tsType === 'string' ? "'value-to-delete'" : '123'} });`); + lines.push(` * \`\`\``); + lines.push(` */`); + lines.push(`export function ${hookName}(`); + lines.push(` args?: { select?: DeepExact },`); + lines.push(` options?: Omit } }, Error, { ${pkField.name}: ${pkField.tsType} }>, 'mutationFn'>`); + lines.push(`) {`); + lines.push(` const queryClient = useQueryClient();`); + lines.push(` return useMutation({`); + if (useCentralizedKeys) { - mutationOptions.push( - t.objectProperty( - t.identifier('mutationKey'), - t.memberExpression(t.identifier(mutationKeysName), t.identifier('all')) - ) - ); + lines.push(` mutationKey: ${mutationKeysName}.all,`); } - mutationOptions.push( - t.objectProperty( - t.identifier('mutationFn'), - t.arrowFunctionExpression( - [typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)))], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(`${mutationName}MutationDocument`), t.identifier('variables')], - [ - t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationResult`)), - t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)), - ] - ) - ) - ) - ); - - const detailQueryKey = useCentralizedKeys - ? t.callExpression( - t.memberExpression(t.identifier(keysName), t.identifier('detail')), - [t.memberExpression(t.memberExpression(t.identifier('variables'), t.identifier('input')), t.identifier(pkField.name))] - ) - : t.arrayExpression([ - t.stringLiteral(typeName.toLowerCase()), - t.stringLiteral('detail'), - t.memberExpression(t.memberExpression(t.identifier('variables'), t.identifier('input')), t.identifier(pkField.name)), - ]); - - const listQueryKey = useCentralizedKeys - ? t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('lists')), []) - : t.arrayExpression([t.stringLiteral(typeName.toLowerCase()), t.stringLiteral('list')]); - - mutationOptions.push( - t.objectProperty( - t.identifier('onSuccess'), - t.arrowFunctionExpression( - [t.identifier('_'), t.identifier('variables')], - t.blockStatement([ - t.expressionStatement( - t.callExpression( - t.memberExpression(t.identifier('queryClient'), t.identifier('removeQueries')), - [t.objectExpression([t.objectProperty(t.identifier('queryKey'), detailQueryKey)])] - ) - ), - t.expressionStatement( - t.callExpression( - t.memberExpression(t.identifier('queryClient'), t.identifier('invalidateQueries')), - [t.objectExpression([t.objectProperty(t.identifier('queryKey'), listQueryKey)])] - ) - ), - ]) - ) - ) - ); - mutationOptions.push(t.spreadElement(t.identifier('options'))); - - hookBodyStatements.push( - t.returnStatement( - t.callExpression(t.identifier('useMutation'), [t.objectExpression(mutationOptions)]) - ) - ); - - const optionsTypeStr = `Omit, 'mutationFn'>`; - const optionsParam = t.identifier('options'); - optionsParam.optional = true; - optionsParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(optionsTypeStr))); - - const hookFunc = t.functionDeclaration( - t.identifier(hookName), - [optionsParam], - t.blockStatement(hookBodyStatements) - ); - const hookExport = t.exportNamedDeclaration(hookFunc); - addJSDocComment(hookExport, [ - `Mutation hook for deleting a ${typeName}`, - '', - '@example', - '```tsx', - `const { mutate, isPending } = ${hookName}();`, - '', - 'mutate({', - ' input: {', - ` ${pkField.name}: ${pkField.tsType === 'string' ? "'value-to-delete'" : '123'},`, - ' },', - '});', - '```', - ]); - statements.push(hookExport); - - const code = generateCode(statements); - const content = getGeneratedFileHeader(`Delete mutation hook for ${typeName}`) + '\n\n' + code; + + lines.push(` mutationFn: ({ ${pkField.name} }: { ${pkField.name}: ${pkField.tsType} }) => getClient().${singularName}.delete({ where: { ${pkField.name} }, select: (args?.select ?? defaultSelect) as DeepExact }).unwrap(),`); + + const detailKey = useCentralizedKeys + ? `${keysName}.detail(variables.${pkField.name})` + : `['${typeName.toLowerCase()}', 'detail', variables.${pkField.name}]`; + const listKey = useCentralizedKeys + ? `${keysName}.lists()` + : `['${typeName.toLowerCase()}', 'list']`; + + lines.push(` onSuccess: (_, variables) => {`); + lines.push(` queryClient.removeQueries({ queryKey: ${detailKey} });`); + lines.push(` queryClient.invalidateQueries({ queryKey: ${listKey} });`); + lines.push(` },`); + lines.push(` ...options,`); + lines.push(` });`); + lines.push(`}`); + + const content = getGeneratedFileHeader(`Delete mutation hook for ${typeName}`) + '\n\n' + lines.join('\n') + '\n'; return { fileName: getDeleteMutationFileName(table), - content, + content }; } diff --git a/graphql/codegen/src/core/codegen/orm/barrel.ts b/graphql/codegen/src/core/codegen/orm/barrel.ts index fda3b5b15..b1a9e63a0 100644 --- a/graphql/codegen/src/core/codegen/orm/barrel.ts +++ b/graphql/codegen/src/core/codegen/orm/barrel.ts @@ -3,10 +3,11 @@ * * Generates index.ts files that re-export all models and operations. */ -import type { CleanTable } from '../../../types/schema'; import * as t from '@babel/types'; + +import type { CleanTable } from '../../../types/schema'; import { generateCode } from '../babel-ast'; -import { getTableNames, lcFirst, getGeneratedFileHeader } from '../utils'; +import { getGeneratedFileHeader,getTableNames, lcFirst } from '../utils'; export interface GeneratedBarrelFile { fileName: string; @@ -41,7 +42,7 @@ export function generateModelsBarrel(tables: CleanTable[]): GeneratedBarrelFile return { fileName: 'models/index.ts', - content: header + '\n' + code, + content: header + '\n' + code }; } @@ -62,6 +63,6 @@ export * from './input-types'; return { fileName: 'types.ts', - content, + content }; } diff --git a/graphql/codegen/src/core/codegen/orm/client-generator.ts b/graphql/codegen/src/core/codegen/orm/client-generator.ts index 765d95807..293f0be52 100644 --- a/graphql/codegen/src/core/codegen/orm/client-generator.ts +++ b/graphql/codegen/src/core/codegen/orm/client-generator.ts @@ -3,13 +3,14 @@ * * Generates the createClient() factory function and main client file. */ -import type { CleanTable } from '../../../types/schema'; import * as t from '@babel/types'; -import { generateCode, commentBlock } from '../babel-ast'; -import { getTableNames, lcFirst, getGeneratedFileHeader } from '../utils'; import * as fs from 'fs'; import * as path from 'path'; +import type { CleanTable } from '../../../types/schema'; +import { commentBlock,generateCode } from '../babel-ast'; +import { getGeneratedFileHeader,getTableNames, lcFirst } from '../utils'; + export interface GeneratedClientFile { fileName: string; content: string; @@ -61,7 +62,7 @@ function readTemplateFile(templateName: string, description: string): string { export function generateOrmClientFile(): GeneratedClientFile { return { fileName: 'client.ts', - content: readTemplateFile('orm-client.ts', 'ORM Client - Runtime GraphQL executor'), + content: readTemplateFile('orm-client.ts', 'ORM Client - Runtime GraphQL executor') }; } @@ -76,7 +77,7 @@ export function generateQueryBuilderFile(): GeneratedClientFile { content: readTemplateFile( 'query-builder.ts', 'Query Builder - Builds and executes GraphQL operations' - ), + ) }; } @@ -88,7 +89,7 @@ export function generateQueryBuilderFile(): GeneratedClientFile { export function generateSelectTypesFile(): GeneratedClientFile { return { fileName: 'select-types.ts', - content: readTemplateFile('select-types.ts', 'Type utilities for select inference'), + content: readTemplateFile('select-types.ts', 'Type utilities for select inference') }; } @@ -167,7 +168,7 @@ export function generateCreateClientFile( t.exportSpecifier( t.identifier('GraphQLAdapter'), t.identifier('GraphQLAdapter') - ), + ) ], t.stringLiteral('./client') ); @@ -182,7 +183,7 @@ export function generateCreateClientFile( t.exportSpecifier( t.identifier('GraphQLRequestError'), t.identifier('GraphQLRequestError') - ), + ) ], t.stringLiteral('./client') ) @@ -196,7 +197,7 @@ export function generateCreateClientFile( t.exportSpecifier( t.identifier('QueryBuilder'), t.identifier('QueryBuilder') - ), + ) ], t.stringLiteral('./query-builder') ) @@ -217,7 +218,7 @@ export function generateCreateClientFile( t.exportSpecifier( t.identifier('createQueryOperations'), t.identifier('createQueryOperations') - ), + ) ], t.stringLiteral('./query') ) @@ -231,7 +232,7 @@ export function generateCreateClientFile( t.exportSpecifier( t.identifier('createMutationOperations'), t.identifier('createMutationOperations') - ), + ) ], t.stringLiteral('./mutation') ) @@ -257,7 +258,7 @@ export function generateCreateClientFile( t.objectProperty( t.identifier('query'), t.callExpression(t.identifier('createQueryOperations'), [ - t.identifier('client'), + t.identifier('client') ]) ) ); @@ -268,7 +269,7 @@ export function generateCreateClientFile( t.objectProperty( t.identifier('mutation'), t.callExpression(t.identifier('createMutationOperations'), [ - t.identifier('client'), + t.identifier('client') ]) ) ); @@ -279,7 +280,7 @@ export function generateCreateClientFile( t.variableDeclarator( t.identifier('client'), t.newExpression(t.identifier('OrmClient'), [t.identifier('config')]) - ), + ) ]); const returnStmt = t.returnStatement(t.objectExpression(returnProperties)); @@ -329,6 +330,6 @@ export function generateCreateClientFile( return { fileName: 'index.ts', - content: header + '\n' + code, + content: header + '\n' + code }; } diff --git a/graphql/codegen/src/core/codegen/orm/client.ts b/graphql/codegen/src/core/codegen/orm/client.ts index f7e233531..d217115f0 100644 --- a/graphql/codegen/src/core/codegen/orm/client.ts +++ b/graphql/codegen/src/core/codegen/orm/client.ts @@ -54,19 +54,19 @@ export class FetchAdapter implements GraphQLAdapter { headers: { 'Content-Type': 'application/json', Accept: 'application/json', - ...this.headers, + ...this.headers }, body: JSON.stringify({ query: document, - variables: variables ?? {}, - }), + variables: variables ?? {} + }) }); if (!response.ok) { return { ok: false, data: null, - errors: [{ message: `HTTP ${response.status}: ${response.statusText}` }], + errors: [{ message: `HTTP ${response.status}: ${response.statusText}` }] }; } @@ -79,14 +79,14 @@ export class FetchAdapter implements GraphQLAdapter { return { ok: false, data: null, - errors: json.errors, + errors: json.errors }; } return { ok: true, data: json.data as T, - errors: undefined, + errors: undefined }; } diff --git a/graphql/codegen/src/core/codegen/orm/custom-ops-generator.ts b/graphql/codegen/src/core/codegen/orm/custom-ops-generator.ts index 0e7a55cb0..e38554296 100644 --- a/graphql/codegen/src/core/codegen/orm/custom-ops-generator.ts +++ b/graphql/codegen/src/core/codegen/orm/custom-ops-generator.ts @@ -4,16 +4,17 @@ * Generates db.query.* and db.mutation.* namespaces for non-table operations * like login, register, currentUser, etc. */ -import type { CleanOperation, CleanArgument } from '../../../types/schema'; import * as t from '@babel/types'; -import { generateCode } from '../babel-ast'; -import { ucFirst, getGeneratedFileHeader } from '../utils'; + +import type { CleanArgument, CleanOperation, TypeRegistry } from '../../../types/schema'; +import { asConst, generateCode } from '../babel-ast'; +import { NON_SELECT_TYPES, getSelectTypeName } from '../select-helpers'; import { - typeRefToTsType, - isTypeRequired, getTypeBaseName, + isTypeRequired, + typeRefToTsType } from '../type-resolver'; -import { SCALAR_NAMES } from '../scalars'; +import { getGeneratedFileHeader,ucFirst } from '../utils'; export interface GeneratedCustomOpsFile { fileName: string; @@ -45,13 +46,6 @@ function collectInputTypeNamesFromOps(operations: CleanOperation[]): string[] { return Array.from(inputTypes); } -// Types that don't need Select types -const NON_SELECT_TYPES = new Set([ - ...SCALAR_NAMES, - 'Query', - 'Mutation', -]); - /** * Collect all payload/return type names from operations (for Select types) * Filters out scalar types @@ -66,8 +60,6 @@ function collectPayloadTypeNamesFromOps( if ( baseName && !baseName.endsWith('Connection') && - baseName !== 'Query' && - baseName !== 'Mutation' && !NON_SELECT_TYPES.has(baseName) ) { payloadTypes.add(baseName); @@ -78,21 +70,26 @@ function collectPayloadTypeNamesFromOps( } /** - * Get the Select type name for a return type - * Returns null for scalar types, Connection types (no select needed) + * Collect Connection and other non-scalar return type names that need importing + * (for typing QueryBuilder results on scalar/Connection operations) */ -function getSelectTypeName(returnType: CleanArgument['type']): string | null { - const baseName = getTypeBaseName(returnType); - if ( - baseName && - !NON_SELECT_TYPES.has(baseName) && - baseName !== 'Query' && - baseName !== 'Mutation' && - !baseName.endsWith('Connection') - ) { - return `${baseName}Select`; +function collectRawReturnTypeNames( + operations: CleanOperation[] +): string[] { + const types = new Set(); + + for (const op of operations) { + const baseName = getTypeBaseName(op.returnType); + if ( + baseName && + !NON_SELECT_TYPES.has(baseName) && + baseName.endsWith('Connection') + ) { + types.add(baseName); + } } - return null; + + return Array.from(types); } function createImportDeclaration( @@ -153,15 +150,106 @@ function parseTypeAnnotation(typeStr: string): t.TSType { return t.tsTypeReference(t.identifier(typeStr)); } +function buildSelectedResultTsType( + typeRef: CleanArgument['type'], + payloadTypeName: string +): t.TSType { + if (typeRef.kind === 'NON_NULL' && typeRef.ofType) { + return buildSelectedResultTsType(typeRef.ofType as CleanArgument['type'], payloadTypeName); + } + + if (typeRef.kind === 'LIST' && typeRef.ofType) { + return t.tsArrayType( + buildSelectedResultTsType(typeRef.ofType as CleanArgument['type'], payloadTypeName) + ); + } + + return t.tsTypeReference( + t.identifier('InferSelectResult'), + t.tsTypeParameterInstantiation([ + t.tsTypeReference(t.identifier(payloadTypeName)), + t.tsTypeReference(t.identifier('S')) + ]) + ); +} + +function buildDefaultSelectExpression( + typeName: string, + typeRegistry: TypeRegistry, + depth: number = 0 +): t.Expression { + const resolved = typeRegistry.get(typeName); + const fields = resolved?.fields ?? []; + + if (depth > 3 || fields.length === 0) { + // Use first field if available, otherwise fallback to 'id' + const fallbackName = fields.length > 0 ? fields[0].name : 'id'; + return t.objectExpression([t.objectProperty(t.identifier(fallbackName), t.booleanLiteral(true))]); + } + + // Prefer id-like fields + const idLike = fields.find((f) => f.name === 'id' || f.name === 'nodeId'); + if (idLike) { + return t.objectExpression([ + t.objectProperty(t.identifier(idLike.name), t.booleanLiteral(true)) + ]); + } + + // Prefer scalar/enum fields + const scalarField = fields.find((f) => { + const baseName = getTypeBaseName(f.type); + if (!baseName) return false; + if (NON_SELECT_TYPES.has(baseName)) return true; + const baseResolved = typeRegistry.get(baseName); + return baseResolved?.kind === 'ENUM'; + }); + + if (scalarField) { + return t.objectExpression([ + t.objectProperty(t.identifier(scalarField.name), t.booleanLiteral(true)) + ]); + } + + // Fallback: first field (ensure valid selection for object fields) + const first = fields[0]; + + const firstBaseName = getTypeBaseName(first.type); + if (!firstBaseName || NON_SELECT_TYPES.has(firstBaseName)) { + return t.objectExpression([ + t.objectProperty(t.identifier(first.name), t.booleanLiteral(true)) + ]); + } + + const nestedResolved = typeRegistry.get(firstBaseName); + if (nestedResolved?.kind === 'ENUM') { + return t.objectExpression([ + t.objectProperty(t.identifier(first.name), t.booleanLiteral(true)) + ]); + } + + return t.objectExpression([ + t.objectProperty( + t.identifier(first.name), + t.objectExpression([ + t.objectProperty( + t.identifier('select'), + buildDefaultSelectExpression(firstBaseName, typeRegistry, depth + 1) + ) + ]) + ) + ]); +} + function buildOperationMethod( op: CleanOperation, - operationType: 'query' | 'mutation' + operationType: 'query' | 'mutation', + defaultSelectIdent?: t.Identifier ): t.ObjectProperty { const hasArgs = op.args.length > 0; const varTypeName = `${ucFirst(op.name)}Variables`; const varDefs = op.args.map((arg) => ({ name: arg.name, - type: formatGraphQLType(arg.type), + type: formatGraphQLType(arg.type) })); const selectTypeName = getSelectTypeName(op.returnType); @@ -191,14 +279,14 @@ function buildOperationMethod( t.identifier('DeepExact'), t.tsTypeParameterInstantiation([ t.tsTypeReference(t.identifier('S')), - t.tsTypeReference(t.identifier(selectTypeName)), + t.tsTypeReference(t.identifier(selectTypeName)) ]) ) ) ); prop.optional = true; return prop; - })(), + })() ]) ); } else { @@ -216,13 +304,21 @@ function buildOperationMethod( ); prop.optional = true; return prop; - })(), + })() ]) ); } params.push(optionsParam); // Build the QueryBuilder call + const selectExpr = defaultSelectIdent + ? t.logicalExpression( + '??', + t.optionalMemberExpression(t.identifier('options'), t.identifier('select'), false, true), + defaultSelectIdent + ) + : t.optionalMemberExpression(t.identifier('options'), t.identifier('select'), false, true); + const queryBuilderArgs = t.objectExpression([ t.objectProperty(t.identifier('client'), t.identifier('client'), false, true), t.objectProperty(t.identifier('operation'), t.stringLiteral(operationType)), @@ -233,39 +329,45 @@ function buildOperationMethod( t.stringLiteral(operationType), t.stringLiteral(ucFirst(op.name)), t.stringLiteral(op.name), - t.optionalMemberExpression(t.identifier('options'), t.identifier('select'), false, true), + selectExpr, hasArgs ? t.identifier('args') : t.identifier('undefined'), t.arrayExpression( varDefs.map((v) => t.objectExpression([ t.objectProperty(t.identifier('name'), t.stringLiteral(v.name)), - t.objectProperty(t.identifier('type'), t.stringLiteral(v.type)), + t.objectProperty(t.identifier('type'), t.stringLiteral(v.type)) ]) ) - ), + ) ]) - ), + ) ]); const newExpr = t.newExpression(t.identifier('QueryBuilder'), [queryBuilderArgs]); - // Add type parameter if we have a select type + // Add type parameter to QueryBuilder for typed .unwrap() results if (selectTypeName && payloadTypeName) { + // Select-based type: use InferSelectResult (newExpr as any).typeParameters = t.tsTypeParameterInstantiation([ t.tsTypeLiteral([ t.tsPropertySignature( t.identifier(op.name), t.tsTypeAnnotation( - t.tsTypeReference( - t.identifier('InferSelectResult'), - t.tsTypeParameterInstantiation([ - t.tsTypeReference(t.identifier(payloadTypeName)), - t.tsTypeReference(t.identifier('S')), - ]) - ) + buildSelectedResultTsType(op.returnType, payloadTypeName) ) - ), - ]), + ) + ]) + ]); + } else { + // Scalar/Connection type: use raw TS type directly + const rawTsType = typeRefToTsType(op.returnType); + (newExpr as any).typeParameters = t.tsTypeParameterInstantiation([ + t.tsTypeLiteral([ + t.tsPropertySignature( + t.identifier(op.name), + t.tsTypeAnnotation(parseTypeAnnotation(rawTsType)) + ) + ]) ]); } @@ -273,9 +375,12 @@ function buildOperationMethod( // Add type parameters to arrow function if we have a select type if (selectTypeName) { + const defaultType = defaultSelectIdent + ? t.tsTypeQuery(defaultSelectIdent) + : null; const typeParam = t.tsTypeParameter( t.tsTypeReference(t.identifier(selectTypeName)), - null, + defaultType, 'S' ); (typeParam as any).const = true; @@ -289,7 +394,8 @@ function buildOperationMethod( * Generate the query/index.ts file for custom query operations */ export function generateCustomQueryOpsFile( - operations: CleanOperation[] + operations: CleanOperation[], + typeRegistry: TypeRegistry ): GeneratedCustomOpsFile { const statements: t.Statement[] = []; @@ -297,7 +403,8 @@ export function generateCustomQueryOpsFile( const inputTypeNames = collectInputTypeNamesFromOps(operations); const payloadTypeNames = collectPayloadTypeNamesFromOps(operations); const selectTypeNames = payloadTypeNames.map((p) => `${p}Select`); - const allTypeImports = [...new Set([...inputTypeNames, ...payloadTypeNames, ...selectTypeNames])]; + const rawReturnTypeNames = collectRawReturnTypeNames(operations); + const allTypeImports = [...new Set([...inputTypeNames, ...payloadTypeNames, ...selectTypeNames, ...rawReturnTypeNames])]; // Add imports statements.push(createImportDeclaration('../client', ['OrmClient'])); @@ -314,8 +421,29 @@ export function generateCustomQueryOpsFile( if (varInterface) statements.push(varInterface); } + // Default selects (avoid invalid documents when select is omitted) + const defaultSelectIdentsByOpName = new Map(); + for (const op of operations) { + const selectTypeName = getSelectTypeName(op.returnType); + const payloadTypeName = getTypeBaseName(op.returnType); + if (!selectTypeName || !payloadTypeName) continue; + + const ident = t.identifier(`${op.name}DefaultSelect`); + defaultSelectIdentsByOpName.set(op.name, ident); + statements.push( + t.variableDeclaration('const', [ + t.variableDeclarator( + ident, + asConst(buildDefaultSelectExpression(payloadTypeName, typeRegistry)) + ) + ]) + ); + } + // Generate factory function - const operationProperties = operations.map((op) => buildOperationMethod(op, 'query')); + const operationProperties = operations.map((op) => + buildOperationMethod(op, 'query', defaultSelectIdentsByOpName.get(op.name)) + ); const returnObj = t.objectExpression(operationProperties); const returnStmt = t.returnStatement(returnObj); @@ -335,7 +463,7 @@ export function generateCustomQueryOpsFile( return { fileName: 'query/index.ts', - content: header + '\n' + code, + content: header + '\n' + code }; } @@ -343,7 +471,8 @@ export function generateCustomQueryOpsFile( * Generate the mutation/index.ts file for custom mutation operations */ export function generateCustomMutationOpsFile( - operations: CleanOperation[] + operations: CleanOperation[], + typeRegistry: TypeRegistry ): GeneratedCustomOpsFile { const statements: t.Statement[] = []; @@ -351,7 +480,8 @@ export function generateCustomMutationOpsFile( const inputTypeNames = collectInputTypeNamesFromOps(operations); const payloadTypeNames = collectPayloadTypeNamesFromOps(operations); const selectTypeNames = payloadTypeNames.map((p) => `${p}Select`); - const allTypeImports = [...new Set([...inputTypeNames, ...payloadTypeNames, ...selectTypeNames])]; + const rawReturnTypeNames = collectRawReturnTypeNames(operations); + const allTypeImports = [...new Set([...inputTypeNames, ...payloadTypeNames, ...selectTypeNames, ...rawReturnTypeNames])]; // Add imports statements.push(createImportDeclaration('../client', ['OrmClient'])); @@ -368,8 +498,29 @@ export function generateCustomMutationOpsFile( if (varInterface) statements.push(varInterface); } + // Default selects (avoid invalid documents when select is omitted) + const defaultSelectIdentsByOpName = new Map(); + for (const op of operations) { + const selectTypeName = getSelectTypeName(op.returnType); + const payloadTypeName = getTypeBaseName(op.returnType); + if (!selectTypeName || !payloadTypeName) continue; + + const ident = t.identifier(`${op.name}DefaultSelect`); + defaultSelectIdentsByOpName.set(op.name, ident); + statements.push( + t.variableDeclaration('const', [ + t.variableDeclarator( + ident, + asConst(buildDefaultSelectExpression(payloadTypeName, typeRegistry)) + ) + ]) + ); + } + // Generate factory function - const operationProperties = operations.map((op) => buildOperationMethod(op, 'mutation')); + const operationProperties = operations.map((op) => + buildOperationMethod(op, 'mutation', defaultSelectIdentsByOpName.get(op.name)) + ); const returnObj = t.objectExpression(operationProperties); const returnStmt = t.returnStatement(returnObj); @@ -389,7 +540,7 @@ export function generateCustomMutationOpsFile( return { fileName: 'mutation/index.ts', - content: header + '\n' + code, + content: header + '\n' + code }; } diff --git a/graphql/codegen/src/core/codegen/orm/index.ts b/graphql/codegen/src/core/codegen/orm/index.ts index 099826959..afeff9380 100644 --- a/graphql/codegen/src/core/codegen/orm/index.ts +++ b/graphql/codegen/src/core/codegen/orm/index.ts @@ -4,21 +4,21 @@ * Main entry point for ORM code generation. Coordinates all generators * and produces the complete ORM client output. */ -import type { CleanTable, CleanOperation, TypeRegistry } from '../../../types/schema'; import type { GraphQLSDKConfigTarget } from '../../../types/config'; +import type { CleanOperation, CleanTable, TypeRegistry } from '../../../types/schema'; +import { generateModelsBarrel, generateTypesBarrel } from './barrel'; import { + generateCreateClientFile, generateOrmClientFile, generateQueryBuilderFile, - generateSelectTypesFile, - generateCreateClientFile, + generateSelectTypesFile } from './client-generator'; -import { generateAllModelFiles } from './model-generator'; import { - generateCustomQueryOpsFile, generateCustomMutationOpsFile, + generateCustomQueryOpsFile } from './custom-ops-generator'; -import { generateModelsBarrel, generateTypesBarrel } from './barrel'; -import { generateInputTypesFile, collectInputTypeNames, collectPayloadTypeNames } from './input-types-generator'; +import { collectInputTypeNames, collectPayloadTypeNames,generateInputTypesFile } from './input-types-generator'; +import { generateAllModelFiles } from './model-generator'; export interface GeneratedFile { path: string; @@ -80,7 +80,7 @@ export function generateOrm(options: GenerateOrmOptions): GenerateOrmResult { for (const modelFile of modelFiles) { files.push({ path: `models/${modelFile.fileName}`, - content: modelFile.content, + content: modelFile.content }); } @@ -93,7 +93,7 @@ export function generateOrm(options: GenerateOrmOptions): GenerateOrmResult { if (tables.length > 0 || (typeRegistry && (hasCustomQueries || hasCustomMutations))) { const allOps = [ ...(customOperations?.queries ?? []), - ...(customOperations?.mutations ?? []), + ...(customOperations?.mutations ?? []) ]; const usedInputTypes = collectInputTypeNames(allOps); const usedPayloadTypes = collectPayloadTypeNames(allOps); @@ -106,7 +106,7 @@ export function generateOrm(options: GenerateOrmOptions): GenerateOrmResult { const crudPayloadTypes = [ `Create${typeName}Payload`, `Update${typeName}Payload`, - `Delete${typeName}Payload`, + `Delete${typeName}Payload` ]; for (const payloadType of crudPayloadTypes) { if (typeRegistry.has(payloadType)) { @@ -127,12 +127,18 @@ export function generateOrm(options: GenerateOrmOptions): GenerateOrmResult { // 5. Generate custom operations (if any) if (hasCustomQueries && customOperations?.queries) { - const queryOpsFile = generateCustomQueryOpsFile(customOperations.queries); + const queryOpsFile = generateCustomQueryOpsFile( + customOperations.queries, + typeRegistry ?? new Map() + ); files.push({ path: queryOpsFile.fileName, content: queryOpsFile.content }); } if (hasCustomMutations && customOperations?.mutations) { - const mutationOpsFile = generateCustomMutationOpsFile(customOperations.mutations); + const mutationOpsFile = generateCustomMutationOpsFile( + customOperations.mutations, + typeRegistry ?? new Map() + ); files.push({ path: mutationOpsFile.fileName, content: mutationOpsFile.content }); } @@ -150,17 +156,17 @@ export function generateOrm(options: GenerateOrmOptions): GenerateOrmResult { tables: tables.length, customQueries: customOperations?.queries.length ?? 0, customMutations: customOperations?.mutations.length ?? 0, - totalFiles: files.length, - }, + totalFiles: files.length + } }; } // Re-export generators for direct use +export { generateModelsBarrel, generateTypesBarrel } from './barrel'; export { generateOrmClientFile, generateQueryBuilderFile, - generateSelectTypesFile, + generateSelectTypesFile } from './client-generator'; -export { generateModelFile, generateAllModelFiles } from './model-generator'; -export { generateCustomQueryOpsFile, generateCustomMutationOpsFile } from './custom-ops-generator'; -export { generateModelsBarrel, generateTypesBarrel } from './barrel'; +export { generateCustomMutationOpsFile,generateCustomQueryOpsFile } from './custom-ops-generator'; +export { generateAllModelFiles,generateModelFile } from './model-generator'; diff --git a/graphql/codegen/src/core/codegen/orm/input-types-generator.ts b/graphql/codegen/src/core/codegen/orm/input-types-generator.ts index ca7154c57..ee3468f4f 100644 --- a/graphql/codegen/src/core/codegen/orm/input-types-generator.ts +++ b/graphql/codegen/src/core/codegen/orm/input-types-generator.ts @@ -10,24 +10,26 @@ * * Uses Babel AST for robust code generation. */ +import * as t from '@babel/types'; +import { pluralize } from 'inflekt'; + import type { - TypeRegistry, CleanArgument, CleanTable, + TypeRegistry } from '../../../types/schema'; -import * as t from '@babel/types'; -import { generateCode, addLineComment } from '../babel-ast'; +import { addLineComment,generateCode } from '../babel-ast'; +import { scalarToFilterType,scalarToTsType } from '../scalars'; +import { getTypeBaseName } from '../type-resolver'; import { - getTableNames, - getFilterTypeName, getConditionTypeName, - getOrderByTypeName, - isRelationField, + getFilterTypeName, getGeneratedFileHeader, + getOrderByTypeName, + getPrimaryKeyInfo, + getTableNames, + isRelationField } from '../utils'; -import { pluralize } from 'inflekt'; -import { getTypeBaseName } from '../type-resolver'; -import { scalarToTsType, scalarToFilterType } from '../scalars'; export interface GeneratedInputTypesFile { fileName: string; @@ -43,7 +45,7 @@ const EXCLUDED_MUTATION_FIELDS = [ 'id', 'createdAt', 'updatedAt', - 'nodeId', + 'nodeId' ] as const; // ============================================================================ @@ -54,7 +56,7 @@ const EXCLUDED_MUTATION_FIELDS = [ * Overrides for input-type generation */ const INPUT_SCALAR_OVERRIDES: Record = { - JSON: 'Record', + JSON: 'Record' }; /** @@ -63,7 +65,7 @@ const INPUT_SCALAR_OVERRIDES: Record = { function scalarToInputTs(scalar: string): string { return scalarToTsType(scalar, { unknownScalar: 'name', - overrides: INPUT_SCALAR_OVERRIDES, + overrides: INPUT_SCALAR_OVERRIDES }); } @@ -132,18 +134,18 @@ function parseTypeString(typeStr: string): t.TSType { // Handle primitive types switch (typeStr) { - case 'string': - return t.tsStringKeyword(); - case 'number': - return t.tsNumberKeyword(); - case 'boolean': - return t.tsBooleanKeyword(); - case 'null': - return t.tsNullKeyword(); - case 'unknown': - return t.tsUnknownKeyword(); - default: - return t.tsTypeReference(t.identifier(typeStr)); + case 'string': + return t.tsStringKeyword(); + case 'number': + return t.tsNumberKeyword(); + case 'boolean': + return t.tsBooleanKeyword(); + case 'null': + return t.tsNullKeyword(); + case 'unknown': + return t.tsUnknownKeyword(); + default: + return t.tsTypeReference(t.identifier(typeStr)); } } @@ -256,72 +258,72 @@ const SCALAR_FILTER_CONFIGS: ScalarFilterConfig[] = [ { name: 'StringFilter', tsType: 'string', - operators: ['equality', 'distinct', 'inArray', 'comparison', 'string'], + operators: ['equality', 'distinct', 'inArray', 'comparison', 'string'] }, { name: 'IntFilter', tsType: 'number', - operators: ['equality', 'distinct', 'inArray', 'comparison'], + operators: ['equality', 'distinct', 'inArray', 'comparison'] }, { name: 'FloatFilter', tsType: 'number', - operators: ['equality', 'distinct', 'inArray', 'comparison'], + operators: ['equality', 'distinct', 'inArray', 'comparison'] }, { name: 'BooleanFilter', tsType: 'boolean', operators: ['equality'] }, { name: 'UUIDFilter', tsType: 'string', - operators: ['equality', 'distinct', 'inArray'], + operators: ['equality', 'distinct', 'inArray'] }, { name: 'DatetimeFilter', tsType: 'string', - operators: ['equality', 'distinct', 'inArray', 'comparison'], + operators: ['equality', 'distinct', 'inArray', 'comparison'] }, { name: 'DateFilter', tsType: 'string', - operators: ['equality', 'distinct', 'inArray', 'comparison'], + operators: ['equality', 'distinct', 'inArray', 'comparison'] }, { name: 'JSONFilter', tsType: 'Record', - operators: ['equality', 'distinct', 'json'], + operators: ['equality', 'distinct', 'json'] }, { name: 'BigIntFilter', tsType: 'string', - operators: ['equality', 'distinct', 'inArray', 'comparison'], + operators: ['equality', 'distinct', 'inArray', 'comparison'] }, { name: 'BigFloatFilter', tsType: 'string', - operators: ['equality', 'distinct', 'inArray', 'comparison'], + operators: ['equality', 'distinct', 'inArray', 'comparison'] }, { name: 'BitStringFilter', tsType: 'string', operators: ['equality'] }, { name: 'InternetAddressFilter', tsType: 'string', - operators: ['equality', 'distinct', 'inArray', 'comparison', 'inet'], + operators: ['equality', 'distinct', 'inArray', 'comparison', 'inet'] }, { name: 'FullTextFilter', tsType: 'string', operators: ['fulltext'] }, // List filters (for array fields like string[], int[], uuid[]) { name: 'StringListFilter', tsType: 'string[]', - operators: ['equality', 'distinct', 'comparison', 'listArray'], + operators: ['equality', 'distinct', 'comparison', 'listArray'] }, { name: 'IntListFilter', tsType: 'number[]', - operators: ['equality', 'distinct', 'comparison', 'listArray'], + operators: ['equality', 'distinct', 'comparison', 'listArray'] }, { name: 'UUIDListFilter', tsType: 'string[]', - operators: ['equality', 'distinct', 'comparison', 'listArray'], - }, + operators: ['equality', 'distinct', 'comparison', 'listArray'] + } ]; /** @@ -542,7 +544,7 @@ function buildEntityProperties(table: CleanTable): InterfaceProperty[] { properties.push({ name: field.name, type: isNullable ? `${tsType} | null` : tsType, - optional: isNullable, + optional: isNullable }); } @@ -582,13 +584,13 @@ function generateRelationHelperTypes(): t.Statement[] { const connectionResultProps: t.TSPropertySignature[] = [ createPropertySignature('nodes', 'T[]', false), createPropertySignature('totalCount', 'number', false), - createPropertySignature('pageInfo', 'PageInfo', false), + createPropertySignature('pageInfo', 'PageInfo', false) ]; const connectionResultBody = t.tsInterfaceBody(connectionResultProps); const connectionResultDecl = t.tsInterfaceDeclaration( t.identifier('ConnectionResult'), t.tsTypeParameterDeclaration([ - t.tsTypeParameter(null, null, 'T'), + t.tsTypeParameter(null, null, 'T') ]), null, connectionResultBody @@ -601,7 +603,7 @@ function generateRelationHelperTypes(): t.Statement[] { { name: 'hasNextPage', type: 'boolean', optional: false }, { name: 'hasPreviousPage', type: 'boolean', optional: false }, { name: 'startCursor', type: 'string | null', optional: true }, - { name: 'endCursor', type: 'string | null', optional: true }, + { name: 'endCursor', type: 'string | null', optional: true } ]) ); @@ -663,7 +665,7 @@ function buildEntityRelationProperties( properties.push({ name: relation.fieldName, type: `${relatedTypeName} | null`, - optional: true, + optional: true }); } @@ -676,7 +678,7 @@ function buildEntityRelationProperties( properties.push({ name: relation.fieldName, type: `${relatedTypeName} | null`, - optional: true, + optional: true }); } @@ -689,7 +691,7 @@ function buildEntityRelationProperties( properties.push({ name: relation.fieldName, type: `ConnectionResult<${relatedTypeName}>`, - optional: true, + optional: true }); } @@ -702,7 +704,7 @@ function buildEntityRelationProperties( properties.push({ name: relation.fieldName, type: `ConnectionResult<${relatedTypeName}>`, - optional: true, + optional: true }); } @@ -803,8 +805,8 @@ function buildSelectTypeLiteral( ); selectProp.optional = true; return selectProp; - })(), - ]), + })() + ]) ]) ) ); @@ -869,8 +871,8 @@ function buildSelectTypeLiteral( ); p.optional = true; return p; - })(), - ]), + })() + ]) ]) ) ); @@ -932,8 +934,8 @@ function buildSelectTypeLiteral( ); p.optional = true; return p; - })(), - ]), + })() + ]) ]) ) ); @@ -964,8 +966,8 @@ function buildSelectTypeLiteral( ); selectProp.optional = true; return selectProp; - })(), - ]), + })() + ]) ]) ) ); @@ -1077,7 +1079,7 @@ function buildTableConditionProperties(table: CleanTable): InterfaceProperty[] { properties.push({ name: field.name, type: `${tsType} | null`, - optional: true, + optional: true }); } @@ -1214,7 +1216,7 @@ function buildCreateInputInterface(table: CleanTable): t.ExportNamedDeclaration t.tsPropertySignature( t.identifier(singularName), t.tsTypeAnnotation(nestedObjectType) - ), + ) ]; const body = t.tsInterfaceBody(mainProps); @@ -1249,7 +1251,7 @@ function buildPatchProperties(table: CleanTable): InterfaceProperty[] { properties.push({ name: field.name, type: `${tsType} | null`, - optional: true, + optional: true }); } @@ -1264,6 +1266,11 @@ function generateCrudInputTypes(table: CleanTable): t.Statement[] { const { typeName } = getTableNames(table); const patchName = `${typeName}Patch`; + const pkFields = getPrimaryKeyInfo(table); + const pkField = pkFields[0]; + const pkFieldName = pkField?.name ?? 'id'; + const pkFieldTsType = pkField?.tsType ?? 'string'; + // Create input statements.push(buildCreateInputInterface(table)); @@ -1276,8 +1283,8 @@ function generateCrudInputTypes(table: CleanTable): t.Statement[] { statements.push( createExportedInterface(`Update${typeName}Input`, [ { name: 'clientMutationId', type: 'string', optional: true }, - { name: 'id', type: 'string', optional: false }, - { name: 'patch', type: patchName, optional: false }, + { name: pkFieldName, type: pkFieldTsType, optional: false }, + { name: 'patch', type: patchName, optional: false } ]) ); @@ -1285,7 +1292,7 @@ function generateCrudInputTypes(table: CleanTable): t.Statement[] { statements.push( createExportedInterface(`Delete${typeName}Input`, [ { name: 'clientMutationId', type: 'string', optional: true }, - { name: 'id', type: 'string', optional: false }, + { name: pkFieldName, type: pkFieldTsType, optional: false } ]) ); @@ -1456,10 +1463,7 @@ export function collectPayloadTypeNames( for (const op of operations) { const baseName = getTypeBaseName(op.returnType); - if ( - baseName && - (baseName.endsWith('Payload') || !baseName.endsWith('Connection')) - ) { + if (baseName) { payloadTypes.add(baseName); } } @@ -1494,7 +1498,7 @@ function generatePayloadTypes( 'BigFloat', 'Cursor', 'Query', - 'Mutation', + 'Mutation' ]); // Process all types - no artificial limit @@ -1524,7 +1528,7 @@ function generatePayloadTypes( interfaceProps.push({ name: field.name, type: isNullable ? `${tsType} | null` : tsType, - optional: isNullable, + optional: isNullable }); // Follow nested OBJECT types @@ -1563,8 +1567,8 @@ function generatePayloadTypes( ); p.optional = true; return p; - })(), - ]), + })() + ]) ]); } else { propType = t.tsBooleanKeyword(); @@ -1669,6 +1673,6 @@ export function generateInputTypesFile( return { fileName: 'input-types.ts', - content: header + '\n' + code, + content: header + '\n' + code }; } diff --git a/graphql/codegen/src/core/codegen/orm/model-generator.ts b/graphql/codegen/src/core/codegen/orm/model-generator.ts index 0498688b4..5b05c3e41 100644 --- a/graphql/codegen/src/core/codegen/orm/model-generator.ts +++ b/graphql/codegen/src/core/codegen/orm/model-generator.ts @@ -3,15 +3,19 @@ * * Generates per-table model classes with findMany, findFirst, create, update, delete methods. */ -import type { CleanTable } from '../../../types/schema'; import * as t from '@babel/types'; -import { generateCode } from '../babel-ast'; + +import type { CleanTable } from '../../../types/schema'; +import { asConst, generateCode } from '../babel-ast'; import { - getTableNames, - getOrderByTypeName, + getDefaultSelectFieldName, getFilterTypeName, - lcFirst, getGeneratedFileHeader, + getOrderByTypeName, + getPrimaryKeyInfo, + getTableNames, + hasValidPrimaryKey, + lcFirst } from '../utils'; export interface GeneratedModelFile { @@ -45,10 +49,10 @@ function buildMethodBody( t.variableDeclarator( t.objectPattern([ t.objectProperty(t.identifier('document'), t.identifier('document'), false, true), - t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true), + t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true) ]), t.callExpression(t.identifier(builderFn), args) - ), + ) ]); const returnStmt = t.returnStatement( @@ -59,8 +63,8 @@ function buildMethodBody( t.objectProperty(t.identifier('operationName'), t.stringLiteral(typeName)), t.objectProperty(t.identifier('fieldName'), t.stringLiteral(fieldName)), t.objectProperty(t.identifier('document'), t.identifier('document'), false, true), - t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true), - ]), + t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true) + ]) ]) ); @@ -80,16 +84,26 @@ function createClassMethod( return method; } -function createConstTypeParam(constraintTypeName: string): t.TSTypeParameterDeclaration { +function createConstTypeParam( + constraintTypeName: string, + defaultType?: t.TSType +): t.TSTypeParameterDeclaration { const param = t.tsTypeParameter( t.tsTypeReference(t.identifier(constraintTypeName)), - null, + defaultType ?? null, 'S' ); (param as any).const = true; return t.tsTypeParameterDeclaration([param]); } +function tsTypeFromPrimitive(typeName: string): t.TSType { + if (typeName === 'string') return t.tsStringKeyword(); + if (typeName === 'number') return t.tsNumberKeyword(); + if (typeName === 'boolean') return t.tsBooleanKeyword(); + return t.tsTypeReference(t.identifier(typeName)); +} + export function generateModelFile( table: CleanTable, _useSharedTypes: boolean @@ -109,6 +123,12 @@ export function generateModelFile( const deleteInputTypeName = `Delete${typeName}Input`; const patchTypeName = `${typeName}Patch`; + const pkFields = getPrimaryKeyInfo(table); + const pkField = pkFields[0]; + const pkFieldTsType = tsTypeFromPrimitive(pkField.tsType); + const defaultSelectIdent = t.identifier('defaultSelect'); + const defaultSelectFieldName = getDefaultSelectFieldName(table); + const pluralQueryName = table.query?.all ?? pluralName; const createMutationName = table.query?.create ?? `create${typeName}`; const updateMutationName = table.query?.update; @@ -118,18 +138,32 @@ export function generateModelFile( statements.push(createImportDeclaration('../client', ['OrmClient'])); statements.push(createImportDeclaration('../query-builder', [ - 'QueryBuilder', 'buildFindManyDocument', 'buildFindFirstDocument', - 'buildCreateDocument', 'buildUpdateDocument', 'buildDeleteDocument', + 'QueryBuilder', 'buildFindManyDocument', 'buildFindFirstDocument', 'buildFindOneDocument', + 'buildCreateDocument', 'buildUpdateByPkDocument', 'buildDeleteByPkDocument' ])); statements.push(createImportDeclaration('../select-types', [ 'ConnectionResult', 'FindManyArgs', 'FindFirstArgs', 'CreateArgs', - 'UpdateArgs', 'DeleteArgs', 'InferSelectResult', 'DeepExact', + 'UpdateArgs', 'DeleteArgs', 'InferSelectResult', 'DeepExact' ], true)); statements.push(createImportDeclaration('../input-types', [ typeName, relationTypeName, selectTypeName, whereTypeName, orderByTypeName, - createInputTypeName, updateInputTypeName, patchTypeName, + createInputTypeName, updateInputTypeName, patchTypeName ], true)); + // Default select (ensures valid GraphQL selection + sound TS return types) + statements.push( + t.variableDeclaration('const', [ + t.variableDeclarator( + defaultSelectIdent, + asConst( + t.objectExpression([ + t.objectProperty(t.identifier(defaultSelectFieldName), t.booleanLiteral(true)) + ]) + ) + ) + ]) + ); + const classBody: t.ClassBody['body'] = []; // Constructor @@ -147,10 +181,10 @@ export function generateModelFile( t.tsTypeReference(t.identifier('FindManyArgs'), t.tsTypeParameterInstantiation([ t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([ t.tsTypeReference(t.identifier('S')), - t.tsTypeReference(t.identifier(selectTypeName)), + t.tsTypeReference(t.identifier(selectTypeName)) ])), t.tsTypeReference(t.identifier(whereTypeName)), - t.tsTypeReference(t.identifier(orderByTypeName)), + t.tsTypeReference(t.identifier(orderByTypeName)) ])) ); const findManyReturnType = t.tsTypeAnnotation( @@ -160,17 +194,22 @@ export function generateModelFile( t.tsTypeReference(t.identifier('ConnectionResult'), t.tsTypeParameterInstantiation([ t.tsTypeReference(t.identifier('InferSelectResult'), t.tsTypeParameterInstantiation([ t.tsTypeReference(t.identifier(relationTypeName)), - t.tsTypeReference(t.identifier('S')), - ])), + t.tsTypeReference(t.identifier('S')) + ])) ])) - )), - ]), + )) + ]) ])) ); + const findManySelectExpr = t.logicalExpression( + '??', + t.optionalMemberExpression(t.identifier('args'), t.identifier('select'), false, true), + defaultSelectIdent + ); const findManyArgs = [ t.stringLiteral(typeName), t.stringLiteral(pluralQueryName), - t.optionalMemberExpression(t.identifier('args'), t.identifier('select'), false, true), + findManySelectExpr, t.objectExpression([ t.objectProperty(t.identifier('where'), t.optionalMemberExpression(t.identifier('args'), t.identifier('where'), false, true)), t.objectProperty(t.identifier('orderBy'), t.tsAsExpression( @@ -181,12 +220,12 @@ export function generateModelFile( t.objectProperty(t.identifier('last'), t.optionalMemberExpression(t.identifier('args'), t.identifier('last'), false, true)), t.objectProperty(t.identifier('after'), t.optionalMemberExpression(t.identifier('args'), t.identifier('after'), false, true)), t.objectProperty(t.identifier('before'), t.optionalMemberExpression(t.identifier('args'), t.identifier('before'), false, true)), - t.objectProperty(t.identifier('offset'), t.optionalMemberExpression(t.identifier('args'), t.identifier('offset'), false, true)), + t.objectProperty(t.identifier('offset'), t.optionalMemberExpression(t.identifier('args'), t.identifier('offset'), false, true)) ]), t.stringLiteral(whereTypeName), - t.stringLiteral(orderByTypeName), + t.stringLiteral(orderByTypeName) ]; - classBody.push(createClassMethod('findMany', createConstTypeParam(selectTypeName), [findManyParam], findManyReturnType, + classBody.push(createClassMethod('findMany', createConstTypeParam(selectTypeName, t.tsTypeQuery(defaultSelectIdent)), [findManyParam], findManyReturnType, buildMethodBody('buildFindManyDocument', findManyArgs, 'query', typeName, pluralQueryName))); // findFirst method @@ -196,9 +235,9 @@ export function generateModelFile( t.tsTypeReference(t.identifier('FindFirstArgs'), t.tsTypeParameterInstantiation([ t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([ t.tsTypeReference(t.identifier('S')), - t.tsTypeReference(t.identifier(selectTypeName)), + t.tsTypeReference(t.identifier(selectTypeName)) ])), - t.tsTypeReference(t.identifier(whereTypeName)), + t.tsTypeReference(t.identifier(whereTypeName)) ])) ); const findFirstReturnType = t.tsTypeAnnotation( @@ -209,35 +248,103 @@ export function generateModelFile( t.tsPropertySignature(t.identifier('nodes'), t.tsTypeAnnotation( t.tsArrayType(t.tsTypeReference(t.identifier('InferSelectResult'), t.tsTypeParameterInstantiation([ t.tsTypeReference(t.identifier(relationTypeName)), - t.tsTypeReference(t.identifier('S')), + t.tsTypeReference(t.identifier('S')) ]))) - )), + )) ]) - )), - ]), + )) + ]) ])) ); + const findFirstSelectExpr = t.logicalExpression( + '??', + t.optionalMemberExpression(t.identifier('args'), t.identifier('select'), false, true), + defaultSelectIdent + ); const findFirstArgs = [ t.stringLiteral(typeName), t.stringLiteral(pluralQueryName), - t.optionalMemberExpression(t.identifier('args'), t.identifier('select'), false, true), + findFirstSelectExpr, t.objectExpression([ - t.objectProperty(t.identifier('where'), t.optionalMemberExpression(t.identifier('args'), t.identifier('where'), false, true)), + t.objectProperty(t.identifier('where'), t.optionalMemberExpression(t.identifier('args'), t.identifier('where'), false, true)) ]), - t.stringLiteral(whereTypeName), + t.stringLiteral(whereTypeName) ]; - classBody.push(createClassMethod('findFirst', createConstTypeParam(selectTypeName), [findFirstParam], findFirstReturnType, + classBody.push(createClassMethod('findFirst', createConstTypeParam(selectTypeName, t.tsTypeQuery(defaultSelectIdent)), [findFirstParam], findFirstReturnType, buildMethodBody('buildFindFirstDocument', findFirstArgs, 'query', typeName, pluralQueryName))); + // findOne method (only if table has valid PK and singular query) + const singleQueryName = table.query?.one; + if (singleQueryName && hasValidPrimaryKey(table)) { + const pkGqlType = pkField.gqlType.replace(/!/g, '') + '!'; + + const findOneParam = t.identifier('args'); + findOneParam.typeAnnotation = t.tsTypeAnnotation( + t.tsTypeLiteral([ + (() => { + const prop = t.tsPropertySignature( + t.identifier(pkField.name), + t.tsTypeAnnotation(pkFieldTsType) + ); + prop.optional = false; + return prop; + })(), + (() => { + const prop = t.tsPropertySignature( + t.identifier('select'), + t.tsTypeAnnotation( + t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([ + t.tsTypeReference(t.identifier('S')), + t.tsTypeReference(t.identifier(selectTypeName)) + ])) + ) + ); + prop.optional = true; + return prop; + })() + ]) + ); + const findOneReturnType = t.tsTypeAnnotation( + t.tsTypeReference(t.identifier('QueryBuilder'), t.tsTypeParameterInstantiation([ + t.tsTypeLiteral([ + t.tsPropertySignature(t.identifier(singleQueryName), t.tsTypeAnnotation( + t.tsUnionType([ + t.tsTypeReference(t.identifier('InferSelectResult'), t.tsTypeParameterInstantiation([ + t.tsTypeReference(t.identifier(relationTypeName)), + t.tsTypeReference(t.identifier('S')) + ])), + t.tsNullKeyword() + ]) + )) + ]) + ])) + ); + const findOneSelectExpr = t.logicalExpression( + '??', + t.memberExpression(t.identifier('args'), t.identifier('select')), + defaultSelectIdent + ); + const findOneArgs = [ + t.stringLiteral(typeName), + t.stringLiteral(singleQueryName), + t.memberExpression(t.identifier('args'), t.identifier(pkField.name)), + findOneSelectExpr, + t.stringLiteral(pkField.name), + t.stringLiteral(pkGqlType) + ]; + classBody.push(createClassMethod('findOne', createConstTypeParam(selectTypeName, t.tsTypeQuery(defaultSelectIdent)), [findOneParam], findOneReturnType, + buildMethodBody('buildFindOneDocument', findOneArgs, 'query', typeName, singleQueryName))); + } + // create method const createParam = t.identifier('args'); createParam.typeAnnotation = t.tsTypeAnnotation( t.tsTypeReference(t.identifier('CreateArgs'), t.tsTypeParameterInstantiation([ t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([ t.tsTypeReference(t.identifier('S')), - t.tsTypeReference(t.identifier(selectTypeName)), + t.tsTypeReference(t.identifier(selectTypeName)) ])), - t.tsIndexedAccessType(t.tsTypeReference(t.identifier(createInputTypeName)), t.tsLiteralType(t.stringLiteral(singularName))), + t.tsIndexedAccessType(t.tsTypeReference(t.identifier(createInputTypeName)), t.tsLiteralType(t.stringLiteral(singularName))) ])) ); const createReturnType = t.tsTypeAnnotation( @@ -248,23 +355,28 @@ export function generateModelFile( t.tsPropertySignature(t.identifier(entityLower), t.tsTypeAnnotation( t.tsTypeReference(t.identifier('InferSelectResult'), t.tsTypeParameterInstantiation([ t.tsTypeReference(t.identifier(relationTypeName)), - t.tsTypeReference(t.identifier('S')), + t.tsTypeReference(t.identifier('S')) ])) - )), + )) ]) - )), - ]), + )) + ]) ])) ); + const createSelectExpr = t.logicalExpression( + '??', + t.memberExpression(t.identifier('args'), t.identifier('select')), + defaultSelectIdent + ); const createArgs = [ t.stringLiteral(typeName), t.stringLiteral(createMutationName), t.stringLiteral(entityLower), - t.memberExpression(t.identifier('args'), t.identifier('select')), + createSelectExpr, t.memberExpression(t.identifier('args'), t.identifier('data')), - t.stringLiteral(createInputTypeName), + t.stringLiteral(createInputTypeName) ]; - classBody.push(createClassMethod('create', createConstTypeParam(selectTypeName), [createParam], createReturnType, + classBody.push(createClassMethod('create', createConstTypeParam(selectTypeName, t.tsTypeQuery(defaultSelectIdent)), [createParam], createReturnType, buildMethodBody('buildCreateDocument', createArgs, 'mutation', typeName, createMutationName))); // update method (if available) @@ -274,10 +386,16 @@ export function generateModelFile( t.tsTypeReference(t.identifier('UpdateArgs'), t.tsTypeParameterInstantiation([ t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([ t.tsTypeReference(t.identifier('S')), - t.tsTypeReference(t.identifier(selectTypeName)), + t.tsTypeReference(t.identifier(selectTypeName)) ])), - t.tsTypeLiteral([t.tsPropertySignature(t.identifier('id'), t.tsTypeAnnotation(t.tsStringKeyword()))]), - t.tsTypeReference(t.identifier(patchTypeName)), + t.tsTypeLiteral([ + (() => { + const prop = t.tsPropertySignature(t.identifier(pkField.name), t.tsTypeAnnotation(pkFieldTsType)); + prop.optional = false; + return prop; + })() + ]), + t.tsTypeReference(t.identifier(patchTypeName)) ])) ); const updateReturnType = t.tsTypeAnnotation( @@ -288,33 +406,52 @@ export function generateModelFile( t.tsPropertySignature(t.identifier(entityLower), t.tsTypeAnnotation( t.tsTypeReference(t.identifier('InferSelectResult'), t.tsTypeParameterInstantiation([ t.tsTypeReference(t.identifier(relationTypeName)), - t.tsTypeReference(t.identifier('S')), + t.tsTypeReference(t.identifier('S')) ])) - )), + )) ]) - )), - ]), + )) + ]) ])) ); + const updateSelectExpr = t.logicalExpression( + '??', + t.memberExpression(t.identifier('args'), t.identifier('select')), + defaultSelectIdent + ); const updateArgs = [ t.stringLiteral(typeName), t.stringLiteral(updateMutationName), t.stringLiteral(entityLower), - t.memberExpression(t.identifier('args'), t.identifier('select')), - t.memberExpression(t.identifier('args'), t.identifier('where')), + updateSelectExpr, + t.memberExpression( + t.memberExpression(t.identifier('args'), t.identifier('where')), + t.identifier(pkField.name) + ), t.memberExpression(t.identifier('args'), t.identifier('data')), t.stringLiteral(updateInputTypeName), + t.stringLiteral(pkField.name) ]; - classBody.push(createClassMethod('update', createConstTypeParam(selectTypeName), [updateParam], updateReturnType, - buildMethodBody('buildUpdateDocument', updateArgs, 'mutation', typeName, updateMutationName))); + classBody.push(createClassMethod('update', createConstTypeParam(selectTypeName, t.tsTypeQuery(defaultSelectIdent)), [updateParam], updateReturnType, + buildMethodBody('buildUpdateByPkDocument', updateArgs, 'mutation', typeName, updateMutationName))); } - // delete method (if available) + // delete method (if available) - supports optional select for returning entity fields if (deleteMutationName) { const deleteParam = t.identifier('args'); deleteParam.typeAnnotation = t.tsTypeAnnotation( t.tsTypeReference(t.identifier('DeleteArgs'), t.tsTypeParameterInstantiation([ - t.tsTypeLiteral([t.tsPropertySignature(t.identifier('id'), t.tsTypeAnnotation(t.tsStringKeyword()))]), + t.tsTypeLiteral([ + (() => { + const prop = t.tsPropertySignature(t.identifier(pkField.name), t.tsTypeAnnotation(pkFieldTsType)); + prop.optional = false; + return prop; + })() + ]), + t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([ + t.tsTypeReference(t.identifier('S')), + t.tsTypeReference(t.identifier(selectTypeName)) + ])) ])) ); const deleteReturnType = t.tsTypeAnnotation( @@ -323,22 +460,35 @@ export function generateModelFile( t.tsPropertySignature(t.identifier(deleteMutationName), t.tsTypeAnnotation( t.tsTypeLiteral([ t.tsPropertySignature(t.identifier(entityLower), t.tsTypeAnnotation( - t.tsTypeLiteral([t.tsPropertySignature(t.identifier('id'), t.tsTypeAnnotation(t.tsStringKeyword()))]) - )), + t.tsTypeReference(t.identifier('InferSelectResult'), t.tsTypeParameterInstantiation([ + t.tsTypeReference(t.identifier(relationTypeName)), + t.tsTypeReference(t.identifier('S')) + ])) + )) ]) - )), - ]), + )) + ]) ])) ); + const deleteSelectExpr = t.logicalExpression( + '??', + t.memberExpression(t.identifier('args'), t.identifier('select')), + defaultSelectIdent + ); const deleteArgs = [ t.stringLiteral(typeName), t.stringLiteral(deleteMutationName), t.stringLiteral(entityLower), - t.memberExpression(t.identifier('args'), t.identifier('where')), + t.memberExpression( + t.memberExpression(t.identifier('args'), t.identifier('where')), + t.identifier(pkField.name) + ), t.stringLiteral(deleteInputTypeName), + t.stringLiteral(pkField.name), + deleteSelectExpr ]; - classBody.push(createClassMethod('delete', null, [deleteParam], deleteReturnType, - buildMethodBody('buildDeleteDocument', deleteArgs, 'mutation', typeName, deleteMutationName))); + classBody.push(createClassMethod('delete', createConstTypeParam(selectTypeName, t.tsTypeQuery(defaultSelectIdent)), [deleteParam], deleteReturnType, + buildMethodBody('buildDeleteByPkDocument', deleteArgs, 'mutation', typeName, deleteMutationName))); } const classDecl = t.classDeclaration(t.identifier(modelName), null, t.classBody(classBody)); diff --git a/graphql/codegen/src/core/codegen/queries.ts b/graphql/codegen/src/core/codegen/queries.ts index 55852c339..5e6d7285b 100644 --- a/graphql/codegen/src/core/codegen/queries.ts +++ b/graphql/codegen/src/core/codegen/queries.ts @@ -1,39 +1,28 @@ /** - * Query hook generators using Babel AST-based code generation + * Query hook generators - delegates to ORM model methods * * Output structure: * queries/ - * useCarsQuery.ts - List query hook - * useCarQuery.ts - Single item query hook + * useCarsQuery.ts - List query hook -> ORM findMany + * useCarQuery.ts - Single item query hook -> ORM findOne */ import type { CleanTable } from '../../types/schema'; -import * as t from '@babel/types'; -import { generateCode, addJSDocComment, typedParam, createTypedCallExpression } from './babel-ast'; import { - buildListQueryAST, - buildSingleQueryAST, - printGraphQL, -} from './gql-ast'; -import { - getTableNames, - getListQueryHookName, - getSingleQueryHookName, - getListQueryFileName, - getSingleQueryFileName, getAllRowsQueryName, - getSingleRowQueryName, + getDefaultSelectFieldName, getFilterTypeName, - getConditionTypeName, + getGeneratedFileHeader, + getListQueryFileName, + getListQueryHookName, getOrderByTypeName, - getScalarFields, - getScalarFilterType, getPrimaryKeyInfo, + getSingleQueryFileName, + getSingleQueryHookName, + getSingleRowQueryName, + getTableNames, hasValidPrimaryKey, - fieldTypeToTs, - toScreamingSnake, - ucFirst, lcFirst, - getGeneratedFileHeader, + ucFirst } from './utils'; export interface GeneratedQueryFile { @@ -47,55 +36,6 @@ export interface QueryGeneratorOptions { hasRelationships?: boolean; } -function createUnionType(values: string[]): t.TSUnionType { - return t.tsUnionType(values.map((v) => t.tsLiteralType(t.stringLiteral(v)))); -} - -function createFilterInterfaceDeclaration( - name: string, - fieldFilters: Array<{ fieldName: string; filterType: string }>, - isExported: boolean = true -): t.Statement { - const properties: t.TSPropertySignature[] = []; - for (const filter of fieldFilters) { - const prop = t.tsPropertySignature( - t.identifier(filter.fieldName), - t.tsTypeAnnotation(t.tsTypeReference(t.identifier(filter.filterType))) - ); - prop.optional = true; - properties.push(prop); - } - const andProp = t.tsPropertySignature( - t.identifier('and'), - t.tsTypeAnnotation(t.tsArrayType(t.tsTypeReference(t.identifier(name)))) - ); - andProp.optional = true; - properties.push(andProp); - const orProp = t.tsPropertySignature( - t.identifier('or'), - t.tsTypeAnnotation(t.tsArrayType(t.tsTypeReference(t.identifier(name)))) - ); - orProp.optional = true; - properties.push(orProp); - const notProp = t.tsPropertySignature( - t.identifier('not'), - t.tsTypeAnnotation(t.tsTypeReference(t.identifier(name))) - ); - notProp.optional = true; - properties.push(notProp); - const body = t.tsInterfaceBody(properties); - const interfaceDecl = t.tsInterfaceDeclaration( - t.identifier(name), - null, - null, - body - ); - if (isExported) { - return t.exportNamedDeclaration(interfaceDecl); - } - return interfaceDecl; -} - export function generateListQueryHook( table: CleanTable, options: QueryGeneratorOptions = {} @@ -103,742 +43,194 @@ export function generateListQueryHook( const { reactQueryEnabled = true, useCentralizedKeys = true, - hasRelationships = false, + hasRelationships = false } = options; - const { typeName, pluralName } = getTableNames(table); + const { typeName, pluralName, singularName } = getTableNames(table); const hookName = getListQueryHookName(table); const queryName = getAllRowsQueryName(table); const filterTypeName = getFilterTypeName(table); - const conditionTypeName = getConditionTypeName(table); const orderByTypeName = getOrderByTypeName(table); - const scalarFields = getScalarFields(table); const keysName = `${lcFirst(typeName)}Keys`; const scopeTypeName = `${typeName}Scope`; + const selectTypeName = `${typeName}Select`; + const relationTypeName = `${typeName}WithRelations`; - const queryAST = buildListQueryAST({ table }); - const queryDocument = printGraphQL(queryAST); + const defaultFieldName = getDefaultSelectFieldName(table); - const statements: t.Statement[] = []; - - const filterTypesUsed = new Set(); - for (const field of scalarFields) { - const filterType = getScalarFilterType(field.type.gqlType, field.type.isArray); - if (filterType) { - filterTypesUsed.add(filterType); - } - } + const lines: string[] = []; + // Imports if (reactQueryEnabled) { - const reactQueryImport = t.importDeclaration( - [t.importSpecifier(t.identifier('useQuery'), t.identifier('useQuery'))], - t.stringLiteral('@tanstack/react-query') - ); - statements.push(reactQueryImport); - const reactQueryTypeImport = t.importDeclaration( - [ - t.importSpecifier( - t.identifier('UseQueryOptions'), - t.identifier('UseQueryOptions') - ), - t.importSpecifier( - t.identifier('QueryClient'), - t.identifier('QueryClient') - ), - ], - t.stringLiteral('@tanstack/react-query') - ); - reactQueryTypeImport.importKind = 'type'; - statements.push(reactQueryTypeImport); + lines.push(`import { useQuery } from '@tanstack/react-query';`); + lines.push(`import type { UseQueryOptions, QueryClient } from '@tanstack/react-query';`); } - - const clientImport = t.importDeclaration( - [t.importSpecifier(t.identifier('execute'), t.identifier('execute'))], - t.stringLiteral('../client') - ); - statements.push(clientImport); - const clientTypeImport = t.importDeclaration( - [ - t.importSpecifier( - t.identifier('ExecuteOptions'), - t.identifier('ExecuteOptions') - ), - ], - t.stringLiteral('../client') - ); - clientTypeImport.importKind = 'type'; - statements.push(clientTypeImport); - - const typesImport = t.importDeclaration( - [ - t.importSpecifier(t.identifier(typeName), t.identifier(typeName)), - ...Array.from(filterTypesUsed).map((ft) => - t.importSpecifier(t.identifier(ft), t.identifier(ft)) - ), - ], - t.stringLiteral('../types') - ); - typesImport.importKind = 'type'; - statements.push(typesImport); + lines.push(`import { getClient } from '../client';`); if (useCentralizedKeys) { - const queryKeyImport = t.importDeclaration( - [t.importSpecifier(t.identifier(keysName), t.identifier(keysName))], - t.stringLiteral('../query-keys') - ); - statements.push(queryKeyImport); + lines.push(`import { ${keysName} } from '../query-keys';`); if (hasRelationships) { - const scopeTypeImport = t.importDeclaration( - [ - t.importSpecifier( - t.identifier(scopeTypeName), - t.identifier(scopeTypeName) - ), - ], - t.stringLiteral('../query-keys') - ); - scopeTypeImport.importKind = 'type'; - statements.push(scopeTypeImport); - } - } - - const reExportDecl = t.exportNamedDeclaration( - null, - [t.exportSpecifier(t.identifier(typeName), t.identifier(typeName))], - t.stringLiteral('../types') - ); - reExportDecl.exportKind = 'type'; - statements.push(reExportDecl); - - const queryDocConst = t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(`${queryName}QueryDocument`), - t.templateLiteral( - [ - t.templateElement( - { raw: '\n' + queryDocument, cooked: '\n' + queryDocument }, - true - ), - ], - [] - ) - ), - ]); - statements.push(t.exportNamedDeclaration(queryDocConst)); - - const fieldFilters = scalarFields - .map((field) => { - const filterType = getScalarFilterType(field.type.gqlType, field.type.isArray); - return filterType ? { fieldName: field.name, filterType } : null; - }) - .filter((f): f is { fieldName: string; filterType: string } => f !== null); - - statements.push( - createFilterInterfaceDeclaration(filterTypeName, fieldFilters, false) - ); - - // Generate Condition interface (simple equality filter with scalar types) - // Track non-primitive types (enums) that need to be imported - const enumTypesUsed = new Set(); - const conditionProperties: t.TSPropertySignature[] = scalarFields.map( - (field) => { - const tsType = fieldTypeToTs(field.type); - const isPrimitive = - tsType === 'string' || - tsType === 'number' || - tsType === 'boolean' || - tsType === 'unknown' || - tsType.endsWith('[]'); - let typeAnnotation: t.TSType; - if (field.type.isArray) { - const baseType = tsType.replace('[]', ''); - const isBasePrimitive = - baseType === 'string' || - baseType === 'number' || - baseType === 'boolean' || - baseType === 'unknown'; - if (!isBasePrimitive) { - enumTypesUsed.add(baseType); - } - typeAnnotation = t.tsArrayType( - baseType === 'string' - ? t.tsStringKeyword() - : baseType === 'number' - ? t.tsNumberKeyword() - : baseType === 'boolean' - ? t.tsBooleanKeyword() - : t.tsTypeReference(t.identifier(baseType)) - ); - } else { - if (!isPrimitive) { - enumTypesUsed.add(tsType); - } - typeAnnotation = - tsType === 'string' - ? t.tsStringKeyword() - : tsType === 'number' - ? t.tsNumberKeyword() - : tsType === 'boolean' - ? t.tsBooleanKeyword() - : t.tsTypeReference(t.identifier(tsType)); - } - const prop = t.tsPropertySignature( - t.identifier(field.name), - t.tsTypeAnnotation(typeAnnotation) - ); - prop.optional = true; - return prop; + lines.push(`import type { ${scopeTypeName} } from '../query-keys';`); } - ); - - // Add import for enum types if any are used - if (enumTypesUsed.size > 0) { - const schemaTypesImport = t.importDeclaration( - Array.from(enumTypesUsed).map((et) => - t.importSpecifier(t.identifier(et), t.identifier(et)) - ), - t.stringLiteral('../schema-types') - ); - schemaTypesImport.importKind = 'type'; - statements.push(schemaTypesImport); } - const conditionInterface = t.tsInterfaceDeclaration( - t.identifier(conditionTypeName), - null, - null, - t.tsInterfaceBody(conditionProperties) - ); - statements.push(conditionInterface); - - const orderByValues = [ - ...scalarFields.flatMap((f) => [ - `${toScreamingSnake(f.name)}_ASC`, - `${toScreamingSnake(f.name)}_DESC`, - ]), - 'NATURAL', - 'PRIMARY_KEY_ASC', - 'PRIMARY_KEY_DESC', - ]; - const orderByTypeAlias = t.tsTypeAliasDeclaration( - t.identifier(orderByTypeName), - null, - createUnionType(orderByValues) - ); - statements.push(orderByTypeAlias); - - const variablesInterfaceBody = t.tsInterfaceBody([ - (() => { - const p = t.tsPropertySignature( - t.identifier('first'), - t.tsTypeAnnotation(t.tsNumberKeyword()) - ); - p.optional = true; - return p; - })(), - (() => { - const p = t.tsPropertySignature( - t.identifier('last'), - t.tsTypeAnnotation(t.tsNumberKeyword()) - ); - p.optional = true; - return p; - })(), - (() => { - const p = t.tsPropertySignature( - t.identifier('offset'), - t.tsTypeAnnotation(t.tsNumberKeyword()) - ); - p.optional = true; - return p; - })(), - (() => { - const p = t.tsPropertySignature( - t.identifier('before'), - t.tsTypeAnnotation(t.tsStringKeyword()) - ); - p.optional = true; - return p; - })(), - (() => { - const p = t.tsPropertySignature( - t.identifier('after'), - t.tsTypeAnnotation(t.tsStringKeyword()) - ); - p.optional = true; - return p; - })(), - (() => { - const p = t.tsPropertySignature( - t.identifier('filter'), - t.tsTypeAnnotation(t.tsTypeReference(t.identifier(filterTypeName))) - ); - p.optional = true; - return p; - })(), - (() => { - const p = t.tsPropertySignature( - t.identifier('condition'), - t.tsTypeAnnotation(t.tsTypeReference(t.identifier(conditionTypeName))) - ); - p.optional = true; - return p; - })(), - (() => { - const p = t.tsPropertySignature( - t.identifier('orderBy'), - t.tsTypeAnnotation( - t.tsArrayType(t.tsTypeReference(t.identifier(orderByTypeName))) - ) - ); - p.optional = true; - return p; - })(), - ]); - const variablesInterface = t.tsInterfaceDeclaration( - t.identifier(`${ucFirst(pluralName)}QueryVariables`), - null, - null, - variablesInterfaceBody - ); - statements.push(t.exportNamedDeclaration(variablesInterface)); - - const pageInfoType = t.tsTypeLiteral([ - t.tsPropertySignature( - t.identifier('hasNextPage'), - t.tsTypeAnnotation(t.tsBooleanKeyword()) - ), - t.tsPropertySignature( - t.identifier('hasPreviousPage'), - t.tsTypeAnnotation(t.tsBooleanKeyword()) - ), - t.tsPropertySignature( - t.identifier('startCursor'), - t.tsTypeAnnotation( - t.tsUnionType([t.tsStringKeyword(), t.tsNullKeyword()]) - ) - ), - t.tsPropertySignature( - t.identifier('endCursor'), - t.tsTypeAnnotation( - t.tsUnionType([t.tsStringKeyword(), t.tsNullKeyword()]) - ) - ), - ]); - const resultType = t.tsTypeLiteral([ - t.tsPropertySignature( - t.identifier('totalCount'), - t.tsTypeAnnotation(t.tsNumberKeyword()) - ), - t.tsPropertySignature( - t.identifier('nodes'), - t.tsTypeAnnotation( - t.tsArrayType(t.tsTypeReference(t.identifier(typeName))) - ) - ), - t.tsPropertySignature( - t.identifier('pageInfo'), - t.tsTypeAnnotation(pageInfoType) - ), - ]); - const resultInterfaceBody = t.tsInterfaceBody([ - t.tsPropertySignature( - t.identifier(queryName), - t.tsTypeAnnotation(resultType) - ), - ]); - const resultInterface = t.tsInterfaceDeclaration( - t.identifier(`${ucFirst(pluralName)}QueryResult`), - null, - null, - resultInterfaceBody - ); - statements.push(t.exportNamedDeclaration(resultInterface)); - + lines.push(`import type {`); + lines.push(` ${selectTypeName},`); + lines.push(` ${relationTypeName},`); + lines.push(` ${filterTypeName},`); + lines.push(` ${orderByTypeName},`); + lines.push(`} from '../../orm/input-types';`); + lines.push(`import type {`); + lines.push(` FindManyArgs,`); + lines.push(` DeepExact,`); + lines.push(` InferSelectResult,`); + lines.push(` ConnectionResult,`); + lines.push(`} from '../../orm/select-types';`); + lines.push(''); + + // Re-export types for backwards compat + lines.push(`export type { ${selectTypeName}, ${relationTypeName}, ${filterTypeName}, ${orderByTypeName} } from '../../orm/input-types';`); + lines.push(''); + + lines.push(`const defaultSelect = { ${defaultFieldName}: true } as const;`); + lines.push(''); + + // Query key if (useCentralizedKeys) { - const queryKeyConst = t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(`${queryName}QueryKey`), - t.memberExpression(t.identifier(keysName), t.identifier('list')) - ), - ]); - const queryKeyExport = t.exportNamedDeclaration(queryKeyConst); - addJSDocComment(queryKeyExport, [ - 'Query key factory - re-exported from query-keys.ts', - ]); - statements.push(queryKeyExport); + lines.push(`/** Query key factory - re-exported from query-keys.ts */`); + lines.push(`export const ${queryName}QueryKey = ${keysName}.list;`); } else { - const queryKeyArrow = t.arrowFunctionExpression( - [ - typedParam( - 'variables', - t.tsTypeReference( - t.identifier(`${ucFirst(pluralName)}QueryVariables`) - ), - true - ), - ], - t.tsAsExpression( - t.arrayExpression([ - t.stringLiteral(typeName.toLowerCase()), - t.stringLiteral('list'), - t.identifier('variables'), - ]), - t.tsTypeReference(t.identifier('const')) - ) - ); - const queryKeyConst = t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(`${queryName}QueryKey`), - queryKeyArrow - ), - ]); - statements.push(t.exportNamedDeclaration(queryKeyConst)); + lines.push(`export const ${queryName}QueryKey = (variables?: FindManyArgs) => ['${typeName.toLowerCase()}', 'list', variables] as const;`); } + lines.push(''); + // Hook if (reactQueryEnabled) { - const hookBodyStatements: t.Statement[] = []; + const docLines = [ + `/**`, + ` * Query hook for fetching ${typeName} list`, + ` *`, + ` * @example`, + ` * \`\`\`tsx`, + ` * const { data, isLoading } = ${hookName}({`, + ` * select: { id: true, name: true },`, + ` * first: 10,`, + ` * where: { name: { equalTo: "example" } },`, + ` * orderBy: ['CREATED_AT_DESC'],`, + ` * });`, + ` * \`\`\`` + ]; if (hasRelationships && useCentralizedKeys) { - hookBodyStatements.push( - t.variableDeclaration('const', [ - t.variableDeclarator( - t.objectPattern([ - t.objectProperty( - t.identifier('scope'), - t.identifier('scope'), - false, - true - ), - t.restElement(t.identifier('queryOptions')), - ]), - t.logicalExpression( - '??', - t.identifier('options'), - t.objectExpression([]) - ) - ), - ]) - ); - hookBodyStatements.push( - t.returnStatement( - t.callExpression(t.identifier('useQuery'), [ - t.objectExpression([ - t.objectProperty( - t.identifier('queryKey'), - t.callExpression( - t.memberExpression( - t.identifier(keysName), - t.identifier('list') - ), - [t.identifier('variables'), t.identifier('scope')] - ) - ), - t.objectProperty( - t.identifier('queryFn'), - t.arrowFunctionExpression( - [], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], - [ - t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)), - t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)), - ] - ) - ) - ), - t.spreadElement(t.identifier('queryOptions')), - ]), - ]) - ) - ); - } else if (useCentralizedKeys) { - hookBodyStatements.push( - t.returnStatement( - t.callExpression(t.identifier('useQuery'), [ - t.objectExpression([ - t.objectProperty( - t.identifier('queryKey'), - t.callExpression( - t.memberExpression( - t.identifier(keysName), - t.identifier('list') - ), - [t.identifier('variables')] - ) - ), - t.objectProperty( - t.identifier('queryFn'), - t.arrowFunctionExpression( - [], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], - [ - t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)), - t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)), - ] - ) - ) - ), - t.spreadElement(t.identifier('options')), - ]), - ]) - ) - ); - } else { - hookBodyStatements.push( - t.returnStatement( - t.callExpression(t.identifier('useQuery'), [ - t.objectExpression([ - t.objectProperty( - t.identifier('queryKey'), - t.callExpression(t.identifier(`${queryName}QueryKey`), [ - t.identifier('variables'), - ]) - ), - t.objectProperty( - t.identifier('queryFn'), - t.arrowFunctionExpression( - [], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], - [ - t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)), - t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)), - ] - ) - ) - ), - t.spreadElement(t.identifier('options')), - ]), - ]) - ) - ); + docLines.push(` *`); + docLines.push(` * @example With scope for hierarchical cache invalidation`); + docLines.push(` * \`\`\`tsx`); + docLines.push(` * const { data } = ${hookName}(`); + docLines.push(` * { first: 10 },`); + docLines.push(` * { scope: { parentId: 'parent-id' } }`); + docLines.push(` * );`); + docLines.push(` * \`\`\``); } + docLines.push(` */`); + lines.push(...docLines); - const hookParams: t.Identifier[] = [ - typedParam( - 'variables', - t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)), - true - ), - ]; - let optionsTypeStr: string; + let optionsType: string; if (hasRelationships && useCentralizedKeys) { - optionsTypeStr = `Omit, 'queryKey' | 'queryFn'> & { scope?: ${scopeTypeName} }`; + optionsType = `Omit> }, Error>, 'queryKey' | 'queryFn'> & { scope?: ${scopeTypeName} }`; } else { - optionsTypeStr = `Omit, 'queryKey' | 'queryFn'>`; + optionsType = `Omit> }, Error>, 'queryKey' | 'queryFn'>`; } - const optionsParam = t.identifier('options'); - optionsParam.optional = true; - optionsParam.typeAnnotation = t.tsTypeAnnotation( - t.tsTypeReference(t.identifier(optionsTypeStr)) - ); - hookParams.push(optionsParam); - const hookFunc = t.functionDeclaration( - t.identifier(hookName), - hookParams, - t.blockStatement(hookBodyStatements) - ); - const hookExport = t.exportNamedDeclaration(hookFunc); - const docLines = [ - `Query hook for fetching ${typeName} list`, - '', - '@example', - '```tsx', - `const { data, isLoading } = ${hookName}({`, - ' first: 10,', - ' filter: { name: { equalTo: "example" } },', - " orderBy: ['CREATED_AT_DESC'],", - '});', - '```', - ]; + lines.push(`export function ${hookName}(`); + lines.push(` args?: FindManyArgs, ${filterTypeName}, ${orderByTypeName}>,`); + lines.push(` options?: ${optionsType}`); + lines.push(`) {`); + if (hasRelationships && useCentralizedKeys) { - docLines.push(''); - docLines.push('@example With scope for hierarchical cache invalidation'); - docLines.push('```tsx'); - docLines.push(`const { data } = ${hookName}(`); - docLines.push(' { first: 10 },'); - docLines.push(" { scope: { parentId: 'parent-id' } }"); - docLines.push(');'); - docLines.push('```'); + lines.push(` const { scope, ...queryOptions } = options ?? {};`); + lines.push(` return useQuery({`); + lines.push(` queryKey: ${keysName}.list(args, scope),`); + lines.push(` queryFn: () => getClient().${singularName}.findMany(args).unwrap(),`); + lines.push(` ...queryOptions,`); + lines.push(` });`); + } else if (useCentralizedKeys) { + lines.push(` return useQuery({`); + lines.push(` queryKey: ${keysName}.list(args),`); + lines.push(` queryFn: () => getClient().${singularName}.findMany(args).unwrap(),`); + lines.push(` ...options,`); + lines.push(` });`); + } else { + lines.push(` return useQuery({`); + lines.push(` queryKey: ${queryName}QueryKey(args),`); + lines.push(` queryFn: () => getClient().${singularName}.findMany(args).unwrap(),`); + lines.push(` ...options,`); + lines.push(` });`); } - addJSDocComment(hookExport, docLines); - statements.push(hookExport); - } - const fetchFuncBody = t.blockStatement([ - t.returnStatement( - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], - [ - t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)), - t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)), - ] - ) - ), - ]); - const fetchFunc = t.functionDeclaration( - t.identifier(`fetch${ucFirst(pluralName)}Query`), - [ - typedParam( - 'variables', - t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)), - true - ), - typedParam( - 'options', - t.tsTypeReference(t.identifier('ExecuteOptions')), - true - ), - ], - fetchFuncBody - ); - fetchFunc.async = true; - fetchFunc.returnType = t.tsTypeAnnotation( - t.tsTypeReference( - t.identifier('Promise'), - t.tsTypeParameterInstantiation([ - t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)), - ]) - ) - ); - const fetchExport = t.exportNamedDeclaration(fetchFunc); - addJSDocComment(fetchExport, [ - `Fetch ${typeName} list without React hooks`, - '', - '@example', - '```ts', - '// Direct fetch', - `const data = await fetch${ucFirst(pluralName)}Query({ first: 10 });`, - '', - '// With QueryClient', - 'const data = await queryClient.fetchQuery({', - ` queryKey: ${queryName}QueryKey(variables),`, - ` queryFn: () => fetch${ucFirst(pluralName)}Query(variables),`, - '});', - '```', - ]); - statements.push(fetchExport); + lines.push(`}`); + lines.push(''); + } + // Fetch function (non-hook) + lines.push(`/**`); + lines.push(` * Fetch ${typeName} list without React hooks`); + lines.push(` *`); + lines.push(` * @example`); + lines.push(` * \`\`\`ts`); + lines.push(` * const data = await fetch${ucFirst(pluralName)}Query({ first: 10, select: { id: true } });`); + lines.push(` * \`\`\``); + lines.push(` */`); + lines.push(`export async function fetch${ucFirst(pluralName)}Query(`); + lines.push(` args?: FindManyArgs, ${filterTypeName}, ${orderByTypeName}>,`); + lines.push(`) {`); + lines.push(` return getClient().${singularName}.findMany(args).unwrap();`); + lines.push(`}`); + lines.push(''); + + // Prefetch function if (reactQueryEnabled) { - const prefetchParams: t.Identifier[] = [ - typedParam( - 'queryClient', - t.tsTypeReference(t.identifier('QueryClient')) - ), - typedParam( - 'variables', - t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)), - true - ), - ]; + lines.push(`/**`); + lines.push(` * Prefetch ${typeName} list for SSR or cache warming`); + lines.push(` *`); + lines.push(` * @example`); + lines.push(` * \`\`\`ts`); + lines.push(` * await prefetch${ucFirst(pluralName)}Query(queryClient, { first: 10 });`); + lines.push(` * \`\`\``); + lines.push(` */`); + lines.push(`export async function prefetch${ucFirst(pluralName)}Query(`); + lines.push(` queryClient: QueryClient,`); + lines.push(` args?: FindManyArgs, ${filterTypeName}, ${orderByTypeName}>,`); if (hasRelationships && useCentralizedKeys) { - prefetchParams.push( - typedParam( - 'scope', - t.tsTypeReference(t.identifier(scopeTypeName)), - true - ) - ); + lines.push(` scope?: ${scopeTypeName},`); } - prefetchParams.push( - typedParam( - 'options', - t.tsTypeReference(t.identifier('ExecuteOptions')), - true - ) - ); + lines.push(`): Promise {`); - let prefetchQueryKeyExpr: t.Expression; if (hasRelationships && useCentralizedKeys) { - prefetchQueryKeyExpr = t.callExpression( - t.memberExpression(t.identifier(keysName), t.identifier('list')), - [t.identifier('variables'), t.identifier('scope')] - ); + lines.push(` await queryClient.prefetchQuery({`); + lines.push(` queryKey: ${keysName}.list(args, scope),`); + lines.push(` queryFn: () => getClient().${singularName}.findMany(args).unwrap(),`); + lines.push(` });`); } else if (useCentralizedKeys) { - prefetchQueryKeyExpr = t.callExpression( - t.memberExpression(t.identifier(keysName), t.identifier('list')), - [t.identifier('variables')] - ); + lines.push(` await queryClient.prefetchQuery({`); + lines.push(` queryKey: ${keysName}.list(args),`); + lines.push(` queryFn: () => getClient().${singularName}.findMany(args).unwrap(),`); + lines.push(` });`); } else { - prefetchQueryKeyExpr = t.callExpression( - t.identifier(`${queryName}QueryKey`), - [t.identifier('variables')] - ); + lines.push(` await queryClient.prefetchQuery({`); + lines.push(` queryKey: ${queryName}QueryKey(args),`); + lines.push(` queryFn: () => getClient().${singularName}.findMany(args).unwrap(),`); + lines.push(` });`); } - const prefetchFuncBody = t.blockStatement([ - t.expressionStatement( - t.awaitExpression( - t.callExpression( - t.memberExpression( - t.identifier('queryClient'), - t.identifier('prefetchQuery') - ), - [ - t.objectExpression([ - t.objectProperty( - t.identifier('queryKey'), - prefetchQueryKeyExpr - ), - t.objectProperty( - t.identifier('queryFn'), - t.arrowFunctionExpression( - [], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], - [ - t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)), - t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)), - ] - ) - ) - ), - ]), - ] - ) - ) - ), - ]); - - const prefetchFunc = t.functionDeclaration( - t.identifier(`prefetch${ucFirst(pluralName)}Query`), - prefetchParams, - prefetchFuncBody - ); - prefetchFunc.async = true; - prefetchFunc.returnType = t.tsTypeAnnotation( - t.tsTypeReference( - t.identifier('Promise'), - t.tsTypeParameterInstantiation([t.tsVoidKeyword()]) - ) - ); - const prefetchExport = t.exportNamedDeclaration(prefetchFunc); - addJSDocComment(prefetchExport, [ - `Prefetch ${typeName} list for SSR or cache warming`, - '', - '@example', - '```ts', - `await prefetch${ucFirst(pluralName)}Query(queryClient, { first: 10 });`, - '```', - ]); - statements.push(prefetchExport); + lines.push(`}`); } - const code = generateCode(statements); const headerText = reactQueryEnabled ? `List query hook for ${typeName}` : `List query functions for ${typeName}`; - const content = getGeneratedFileHeader(headerText) + '\n\n' + code; + const content = getGeneratedFileHeader(headerText) + '\n\n' + lines.join('\n') + '\n'; return { fileName: getListQueryFileName(table), - content, + content }; } @@ -846,7 +238,6 @@ export function generateSingleQueryHook( table: CleanTable, options: QueryGeneratorOptions = {} ): GeneratedQueryFile | null { - // Skip tables with composite keys - they are handled as custom queries if (!hasValidPrimaryKey(table)) { return null; } @@ -854,536 +245,190 @@ export function generateSingleQueryHook( const { reactQueryEnabled = true, useCentralizedKeys = true, - hasRelationships = false, + hasRelationships = false } = options; const { typeName, singularName } = getTableNames(table); const hookName = getSingleQueryHookName(table); const queryName = getSingleRowQueryName(table); const keysName = `${lcFirst(typeName)}Keys`; const scopeTypeName = `${typeName}Scope`; + const selectTypeName = `${typeName}Select`; + const relationTypeName = `${typeName}WithRelations`; const pkFields = getPrimaryKeyInfo(table); const pkField = pkFields[0]; - const pkName = pkField.name; - const pkTsType = pkField.tsType; + const pkFieldName = pkField?.name ?? 'id'; + const pkFieldTsType = pkField?.tsType ?? 'string'; + const defaultFieldName = getDefaultSelectFieldName(table); - const queryAST = buildSingleQueryAST({ table }); - const queryDocument = printGraphQL(queryAST); - - const statements: t.Statement[] = []; + const lines: string[] = []; + // Imports if (reactQueryEnabled) { - const reactQueryImport = t.importDeclaration( - [t.importSpecifier(t.identifier('useQuery'), t.identifier('useQuery'))], - t.stringLiteral('@tanstack/react-query') - ); - statements.push(reactQueryImport); - const reactQueryTypeImport = t.importDeclaration( - [ - t.importSpecifier( - t.identifier('UseQueryOptions'), - t.identifier('UseQueryOptions') - ), - t.importSpecifier( - t.identifier('QueryClient'), - t.identifier('QueryClient') - ), - ], - t.stringLiteral('@tanstack/react-query') - ); - reactQueryTypeImport.importKind = 'type'; - statements.push(reactQueryTypeImport); + lines.push(`import { useQuery } from '@tanstack/react-query';`); + lines.push(`import type { UseQueryOptions, QueryClient } from '@tanstack/react-query';`); } - - const clientImport = t.importDeclaration( - [t.importSpecifier(t.identifier('execute'), t.identifier('execute'))], - t.stringLiteral('../client') - ); - statements.push(clientImport); - const clientTypeImport = t.importDeclaration( - [ - t.importSpecifier( - t.identifier('ExecuteOptions'), - t.identifier('ExecuteOptions') - ), - ], - t.stringLiteral('../client') - ); - clientTypeImport.importKind = 'type'; - statements.push(clientTypeImport); - - const typesImport = t.importDeclaration( - [t.importSpecifier(t.identifier(typeName), t.identifier(typeName))], - t.stringLiteral('../types') - ); - typesImport.importKind = 'type'; - statements.push(typesImport); + lines.push(`import { getClient } from '../client';`); if (useCentralizedKeys) { - const queryKeyImport = t.importDeclaration( - [t.importSpecifier(t.identifier(keysName), t.identifier(keysName))], - t.stringLiteral('../query-keys') - ); - statements.push(queryKeyImport); + lines.push(`import { ${keysName} } from '../query-keys';`); if (hasRelationships) { - const scopeTypeImport = t.importDeclaration( - [ - t.importSpecifier( - t.identifier(scopeTypeName), - t.identifier(scopeTypeName) - ), - ], - t.stringLiteral('../query-keys') - ); - scopeTypeImport.importKind = 'type'; - statements.push(scopeTypeImport); + lines.push(`import type { ${scopeTypeName} } from '../query-keys';`); } } - const reExportDecl = t.exportNamedDeclaration( - null, - [t.exportSpecifier(t.identifier(typeName), t.identifier(typeName))], - t.stringLiteral('../types') - ); - reExportDecl.exportKind = 'type'; - statements.push(reExportDecl); - - const queryDocConst = t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(`${queryName}QueryDocument`), - t.templateLiteral( - [ - t.templateElement( - { raw: '\n' + queryDocument, cooked: '\n' + queryDocument }, - true - ), - ], - [] - ) - ), - ]); - statements.push(t.exportNamedDeclaration(queryDocConst)); - - const pkTypeAnnotation = - pkTsType === 'string' - ? t.tsStringKeyword() - : pkTsType === 'number' - ? t.tsNumberKeyword() - : t.tsTypeReference(t.identifier(pkTsType)); + lines.push(`import type {`); + lines.push(` ${selectTypeName},`); + lines.push(` ${relationTypeName},`); + lines.push(`} from '../../orm/input-types';`); + lines.push(`import type {`); + lines.push(` DeepExact,`); + lines.push(` InferSelectResult,`); + lines.push(`} from '../../orm/select-types';`); + lines.push(''); - const variablesInterfaceBody = t.tsInterfaceBody([ - t.tsPropertySignature( - t.identifier(pkName), - t.tsTypeAnnotation(pkTypeAnnotation) - ), - ]); - const variablesInterface = t.tsInterfaceDeclaration( - t.identifier(`${ucFirst(singularName)}QueryVariables`), - null, - null, - variablesInterfaceBody - ); - statements.push(t.exportNamedDeclaration(variablesInterface)); + // Re-export types + lines.push(`export type { ${selectTypeName}, ${relationTypeName} } from '../../orm/input-types';`); + lines.push(''); - const resultInterfaceBody = t.tsInterfaceBody([ - t.tsPropertySignature( - t.identifier(queryName), - t.tsTypeAnnotation( - t.tsUnionType([ - t.tsTypeReference(t.identifier(typeName)), - t.tsNullKeyword(), - ]) - ) - ), - ]); - const resultInterface = t.tsInterfaceDeclaration( - t.identifier(`${ucFirst(singularName)}QueryResult`), - null, - null, - resultInterfaceBody - ); - statements.push(t.exportNamedDeclaration(resultInterface)); + lines.push(`const defaultSelect = { ${defaultFieldName}: true } as const;`); + lines.push(''); + // Query key if (useCentralizedKeys) { - const queryKeyConst = t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(`${queryName}QueryKey`), - t.memberExpression(t.identifier(keysName), t.identifier('detail')) - ), - ]); - const queryKeyExport = t.exportNamedDeclaration(queryKeyConst); - addJSDocComment(queryKeyExport, [ - 'Query key factory - re-exported from query-keys.ts', - ]); - statements.push(queryKeyExport); + lines.push(`/** Query key factory - re-exported from query-keys.ts */`); + lines.push(`export const ${queryName}QueryKey = ${keysName}.detail;`); } else { - const queryKeyArrow = t.arrowFunctionExpression( - [typedParam(pkName, pkTypeAnnotation)], - t.tsAsExpression( - t.arrayExpression([ - t.stringLiteral(typeName.toLowerCase()), - t.stringLiteral('detail'), - t.identifier(pkName), - ]), - t.tsTypeReference(t.identifier('const')) - ) - ); - const queryKeyConst = t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(`${queryName}QueryKey`), - queryKeyArrow - ), - ]); - statements.push(t.exportNamedDeclaration(queryKeyConst)); + lines.push(`export const ${queryName}QueryKey = (id: ${pkFieldTsType}) => ['${typeName.toLowerCase()}', 'detail', id] as const;`); } + lines.push(''); + // Hook if (reactQueryEnabled) { - const hookBodyStatements: t.Statement[] = []; + const docLines = [ + `/**`, + ` * Query hook for fetching a single ${typeName}`, + ` *`, + ` * @example`, + ` * \`\`\`tsx`, + ` * const { data, isLoading } = ${hookName}({`, + ` * ${pkFieldName}: 'some-id',`, + ` * select: { id: true, name: true },`, + ` * });`, + ` * \`\`\`` + ]; if (hasRelationships && useCentralizedKeys) { - hookBodyStatements.push( - t.variableDeclaration('const', [ - t.variableDeclarator( - t.objectPattern([ - t.objectProperty( - t.identifier('scope'), - t.identifier('scope'), - false, - true - ), - t.restElement(t.identifier('queryOptions')), - ]), - t.logicalExpression( - '??', - t.identifier('options'), - t.objectExpression([]) - ) - ), - ]) - ); - hookBodyStatements.push( - t.returnStatement( - t.callExpression(t.identifier('useQuery'), [ - t.objectExpression([ - t.objectProperty( - t.identifier('queryKey'), - t.callExpression( - t.memberExpression( - t.identifier(keysName), - t.identifier('detail') - ), - [ - t.memberExpression( - t.identifier('variables'), - t.identifier(pkName) - ), - t.identifier('scope'), - ] - ) - ), - t.objectProperty( - t.identifier('queryFn'), - t.arrowFunctionExpression( - [], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], - [ - t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)), - t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)), - ] - ) - ) - ), - t.spreadElement(t.identifier('queryOptions')), - ]), - ]) - ) - ); - } else if (useCentralizedKeys) { - hookBodyStatements.push( - t.returnStatement( - t.callExpression(t.identifier('useQuery'), [ - t.objectExpression([ - t.objectProperty( - t.identifier('queryKey'), - t.callExpression( - t.memberExpression( - t.identifier(keysName), - t.identifier('detail') - ), - [ - t.memberExpression( - t.identifier('variables'), - t.identifier(pkName) - ), - ] - ) - ), - t.objectProperty( - t.identifier('queryFn'), - t.arrowFunctionExpression( - [], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], - [ - t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)), - t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)), - ] - ) - ) - ), - t.spreadElement(t.identifier('options')), - ]), - ]) - ) - ); - } else { - hookBodyStatements.push( - t.returnStatement( - t.callExpression(t.identifier('useQuery'), [ - t.objectExpression([ - t.objectProperty( - t.identifier('queryKey'), - t.callExpression(t.identifier(`${queryName}QueryKey`), [ - t.memberExpression( - t.identifier('variables'), - t.identifier(pkName) - ), - ]) - ), - t.objectProperty( - t.identifier('queryFn'), - t.arrowFunctionExpression( - [], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], - [ - t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)), - t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)), - ] - ) - ) - ), - t.spreadElement(t.identifier('options')), - ]), - ]) - ) - ); + docLines.push(` *`); + docLines.push(` * @example With scope for hierarchical cache invalidation`); + docLines.push(` * \`\`\`tsx`); + docLines.push(` * const { data } = ${hookName}(`); + docLines.push(` * { ${pkFieldName}: 'some-id' },`); + docLines.push(` * { scope: { parentId: 'parent-id' } }`); + docLines.push(` * );`); + docLines.push(` * \`\`\``); } + docLines.push(` */`); + lines.push(...docLines); - const hookParams: t.Identifier[] = [ - typedParam( - 'variables', - t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)) - ), - ]; - let optionsTypeStr: string; + let optionsType: string; if (hasRelationships && useCentralizedKeys) { - optionsTypeStr = `Omit, 'queryKey' | 'queryFn'> & { scope?: ${scopeTypeName} }`; + optionsType = `Omit | null }, Error>, 'queryKey' | 'queryFn'> & { scope?: ${scopeTypeName} }`; } else { - optionsTypeStr = `Omit, 'queryKey' | 'queryFn'>`; + optionsType = `Omit | null }, Error>, 'queryKey' | 'queryFn'>`; } - const optionsParam = t.identifier('options'); - optionsParam.optional = true; - optionsParam.typeAnnotation = t.tsTypeAnnotation( - t.tsTypeReference(t.identifier(optionsTypeStr)) - ); - hookParams.push(optionsParam); - const hookFunc = t.functionDeclaration( - t.identifier(hookName), - hookParams, - t.blockStatement(hookBodyStatements) - ); - const hookExport = t.exportNamedDeclaration(hookFunc); - const docLines = [ - `Query hook for fetching a single ${typeName}`, - '', - '@example', - '```tsx', - `const { data, isLoading } = ${hookName}({ ${pkName}: 'some-id' });`, - '```', - ]; + lines.push(`export function ${hookName}(`); + lines.push(` args: { ${pkFieldName}: ${pkFieldTsType}; select?: DeepExact },`); + lines.push(` options?: ${optionsType}`); + lines.push(`) {`); + if (hasRelationships && useCentralizedKeys) { - docLines.push(''); - docLines.push('@example With scope for hierarchical cache invalidation'); - docLines.push('```tsx'); - docLines.push(`const { data } = ${hookName}(`); - docLines.push(` { ${pkName}: 'some-id' },`); - docLines.push(" { scope: { parentId: 'parent-id' } }"); - docLines.push(');'); - docLines.push('```'); + lines.push(` const { scope, ...queryOptions } = options ?? {};`); + lines.push(` return useQuery({`); + lines.push(` queryKey: ${keysName}.detail(args.${pkFieldName}, scope),`); + lines.push(` queryFn: () => getClient().${singularName}.findOne(args).unwrap(),`); + lines.push(` ...queryOptions,`); + lines.push(` });`); + } else if (useCentralizedKeys) { + lines.push(` return useQuery({`); + lines.push(` queryKey: ${keysName}.detail(args.${pkFieldName}),`); + lines.push(` queryFn: () => getClient().${singularName}.findOne(args).unwrap(),`); + lines.push(` ...options,`); + lines.push(` });`); + } else { + lines.push(` return useQuery({`); + lines.push(` queryKey: ${queryName}QueryKey(args.${pkFieldName}),`); + lines.push(` queryFn: () => getClient().${singularName}.findOne(args).unwrap(),`); + lines.push(` ...options,`); + lines.push(` });`); } - addJSDocComment(hookExport, docLines); - statements.push(hookExport); - } - const fetchFuncBody = t.blockStatement([ - t.returnStatement( - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], - [ - t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)), - t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)), - ] - ) - ), - ]); - const fetchFunc = t.functionDeclaration( - t.identifier(`fetch${ucFirst(singularName)}Query`), - [ - typedParam( - 'variables', - t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)) - ), - typedParam( - 'options', - t.tsTypeReference(t.identifier('ExecuteOptions')), - true - ), - ], - fetchFuncBody - ); - fetchFunc.async = true; - fetchFunc.returnType = t.tsTypeAnnotation( - t.tsTypeReference( - t.identifier('Promise'), - t.tsTypeParameterInstantiation([ - t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)), - ]) - ) - ); - const fetchExport = t.exportNamedDeclaration(fetchFunc); - addJSDocComment(fetchExport, [ - `Fetch a single ${typeName} without React hooks`, - '', - '@example', - '```ts', - `const data = await fetch${ucFirst(singularName)}Query({ ${pkName}: 'some-id' });`, - '```', - ]); - statements.push(fetchExport); + lines.push(`}`); + lines.push(''); + } + // Fetch function + lines.push(`/**`); + lines.push(` * Fetch a single ${typeName} without React hooks`); + lines.push(` *`); + lines.push(` * @example`); + lines.push(` * \`\`\`ts`); + lines.push(` * const data = await fetch${ucFirst(singularName)}Query({ ${pkFieldName}: 'some-id', select: { id: true } });`); + lines.push(` * \`\`\``); + lines.push(` */`); + lines.push(`export async function fetch${ucFirst(singularName)}Query(`); + lines.push(` args: { ${pkFieldName}: ${pkFieldTsType}; select?: DeepExact },`); + lines.push(`) {`); + lines.push(` return getClient().${singularName}.findOne(args).unwrap();`); + lines.push(`}`); + lines.push(''); + + // Prefetch function if (reactQueryEnabled) { - const prefetchParams: t.Identifier[] = [ - typedParam( - 'queryClient', - t.tsTypeReference(t.identifier('QueryClient')) - ), - typedParam( - 'variables', - t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)) - ), - ]; + lines.push(`/**`); + lines.push(` * Prefetch a single ${typeName} for SSR or cache warming`); + lines.push(` *`); + lines.push(` * @example`); + lines.push(` * \`\`\`ts`); + lines.push(` * await prefetch${ucFirst(singularName)}Query(queryClient, { ${pkFieldName}: 'some-id' });`); + lines.push(` * \`\`\``); + lines.push(` */`); + lines.push(`export async function prefetch${ucFirst(singularName)}Query(`); + lines.push(` queryClient: QueryClient,`); + lines.push(` args: { ${pkFieldName}: ${pkFieldTsType}; select?: DeepExact },`); if (hasRelationships && useCentralizedKeys) { - prefetchParams.push( - typedParam( - 'scope', - t.tsTypeReference(t.identifier(scopeTypeName)), - true - ) - ); + lines.push(` scope?: ${scopeTypeName},`); } - prefetchParams.push( - typedParam( - 'options', - t.tsTypeReference(t.identifier('ExecuteOptions')), - true - ) - ); + lines.push(`): Promise {`); - let prefetchQueryKeyExpr: t.Expression; if (hasRelationships && useCentralizedKeys) { - prefetchQueryKeyExpr = t.callExpression( - t.memberExpression(t.identifier(keysName), t.identifier('detail')), - [ - t.memberExpression(t.identifier('variables'), t.identifier(pkName)), - t.identifier('scope'), - ] - ); + lines.push(` await queryClient.prefetchQuery({`); + lines.push(` queryKey: ${keysName}.detail(args.${pkFieldName}, scope),`); + lines.push(` queryFn: () => getClient().${singularName}.findOne(args).unwrap(),`); + lines.push(` });`); } else if (useCentralizedKeys) { - prefetchQueryKeyExpr = t.callExpression( - t.memberExpression(t.identifier(keysName), t.identifier('detail')), - [t.memberExpression(t.identifier('variables'), t.identifier(pkName))] - ); + lines.push(` await queryClient.prefetchQuery({`); + lines.push(` queryKey: ${keysName}.detail(args.${pkFieldName}),`); + lines.push(` queryFn: () => getClient().${singularName}.findOne(args).unwrap(),`); + lines.push(` });`); } else { - prefetchQueryKeyExpr = t.callExpression( - t.identifier(`${queryName}QueryKey`), - [t.memberExpression(t.identifier('variables'), t.identifier(pkName))] - ); + lines.push(` await queryClient.prefetchQuery({`); + lines.push(` queryKey: ${queryName}QueryKey(args.${pkFieldName}),`); + lines.push(` queryFn: () => getClient().${singularName}.findOne(args).unwrap(),`); + lines.push(` });`); } - const prefetchFuncBody = t.blockStatement([ - t.expressionStatement( - t.awaitExpression( - t.callExpression( - t.memberExpression( - t.identifier('queryClient'), - t.identifier('prefetchQuery') - ), - [ - t.objectExpression([ - t.objectProperty( - t.identifier('queryKey'), - prefetchQueryKeyExpr - ), - t.objectProperty( - t.identifier('queryFn'), - t.arrowFunctionExpression( - [], - createTypedCallExpression( - t.identifier('execute'), - [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], - [ - t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)), - t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)), - ] - ) - ) - ), - ]), - ] - ) - ) - ), - ]); - - const prefetchFunc = t.functionDeclaration( - t.identifier(`prefetch${ucFirst(singularName)}Query`), - prefetchParams, - prefetchFuncBody - ); - prefetchFunc.async = true; - prefetchFunc.returnType = t.tsTypeAnnotation( - t.tsTypeReference( - t.identifier('Promise'), - t.tsTypeParameterInstantiation([t.tsVoidKeyword()]) - ) - ); - const prefetchExport = t.exportNamedDeclaration(prefetchFunc); - addJSDocComment(prefetchExport, [ - `Prefetch a single ${typeName} for SSR or cache warming`, - '', - '@example', - '```ts', - `await prefetch${ucFirst(singularName)}Query(queryClient, { ${pkName}: 'some-id' });`, - '```', - ]); - statements.push(prefetchExport); + lines.push(`}`); } - const code = generateCode(statements); const headerText = reactQueryEnabled ? `Single item query hook for ${typeName}` : `Single item query functions for ${typeName}`; - const content = getGeneratedFileHeader(headerText) + '\n\n' + code; + const content = getGeneratedFileHeader(headerText) + '\n\n' + lines.join('\n') + '\n'; return { fileName: getSingleQueryFileName(table), - content, + content }; } diff --git a/graphql/codegen/src/core/codegen/query-keys.ts b/graphql/codegen/src/core/codegen/query-keys.ts index 791d4eec6..b9c09c468 100644 --- a/graphql/codegen/src/core/codegen/query-keys.ts +++ b/graphql/codegen/src/core/codegen/query-keys.ts @@ -10,17 +10,17 @@ */ import * as t from '@babel/types'; -import type { CleanTable, CleanOperation } from '../../types/schema'; -import type { QueryKeyConfig, EntityRelationship } from '../../types/config'; -import { getTableNames, getGeneratedFileHeader, ucFirst, lcFirst } from './utils'; +import type { EntityRelationship,QueryKeyConfig } from '../../types/config'; +import type { CleanOperation,CleanTable } from '../../types/schema'; import { - generateCode, addJSDocComment, asConst, constArray, - typedParam, + generateCode, keyofTypeof, + typedParam } from './babel-ast'; +import { getGeneratedFileHeader, getTableNames, lcFirst,ucFirst } from './utils'; export interface QueryKeyGeneratorOptions { tables: CleanTable[]; @@ -558,7 +558,7 @@ function generateUnifiedStoreDeclaration( '', '// Invalidate specific user', 'queryClient.invalidateQueries({ queryKey: queryKeys.user.detail(userId) });', - '```', + '```' ]); return decl; @@ -714,6 +714,6 @@ ${description} return { fileName: 'query-keys.ts', - content, + content }; } diff --git a/graphql/codegen/src/core/codegen/scalars.ts b/graphql/codegen/src/core/codegen/scalars.ts index f659cb095..f13047967 100644 --- a/graphql/codegen/src/core/codegen/scalars.ts +++ b/graphql/codegen/src/core/codegen/scalars.ts @@ -40,7 +40,7 @@ export const SCALAR_TS_MAP: Record = { TsQuery: 'string', // File upload - Upload: 'File', + Upload: 'File' }; export const SCALAR_FILTER_MAP: Record = { @@ -59,7 +59,7 @@ export const SCALAR_FILTER_MAP: Record = { BitString: 'BitStringFilter', InternetAddress: 'InternetAddressFilter', FullText: 'FullTextFilter', - Interval: 'StringFilter', + Interval: 'StringFilter' }; export const SCALAR_NAMES = new Set(Object.keys(SCALAR_TS_MAP)); @@ -70,7 +70,7 @@ const LIST_FILTER_SCALARS = new Set(['String', 'Int', 'UUID']); /** All base filter type names - skip these in schema-types.ts to avoid duplicates */ export const BASE_FILTER_TYPE_NAMES = new Set([ ...new Set(Object.values(SCALAR_FILTER_MAP)), - ...Array.from(LIST_FILTER_SCALARS).map((s) => `${s}ListFilter`), + ...Array.from(LIST_FILTER_SCALARS).map((s) => `${s}ListFilter`) ]); export function scalarToTsType( diff --git a/graphql/codegen/src/core/codegen/schema-gql-ast.ts b/graphql/codegen/src/core/codegen/schema-gql-ast.ts deleted file mode 100644 index 1d51f33de..000000000 --- a/graphql/codegen/src/core/codegen/schema-gql-ast.ts +++ /dev/null @@ -1,518 +0,0 @@ -/** - * Dynamic GraphQL AST builders for custom operations - * - * Generates GraphQL query/mutation documents from CleanOperation data - * using gql-ast library for proper AST construction. - */ -import * as t from 'gql-ast'; -import { print } from 'graphql'; -import type { - DocumentNode, - FieldNode, - ArgumentNode, - VariableDefinitionNode, - TypeNode, -} from 'graphql'; -import type { - CleanOperation, - CleanArgument, - CleanTypeRef, - CleanObjectField, - TypeRegistry, -} from '../../types/schema'; -import { getBaseTypeKind, shouldSkipField } from './type-resolver'; - -// ============================================================================ -// Configuration -// ============================================================================ - -export interface FieldSelectionConfig { - /** Max depth for nested object selections */ - maxDepth: number; - /** Skip the 'query' field in payloads */ - skipQueryField: boolean; - /** Type registry for resolving nested types */ - typeRegistry?: TypeRegistry; -} - -// ============================================================================ -// Type Node Builders (GraphQL Type AST) -// ============================================================================ - -/** - * Build a GraphQL type node from CleanTypeRef - * Handles NON_NULL, LIST, and named types - */ -function buildTypeNode(typeRef: CleanTypeRef): TypeNode { - switch (typeRef.kind) { - case 'NON_NULL': - if (typeRef.ofType) { - const innerType = buildTypeNode(typeRef.ofType); - // Can't wrap NON_NULL in NON_NULL - if (innerType.kind === 'NonNullType') { - return innerType; - } - return t.nonNullType({ type: innerType as any }); - } - return t.namedType({ type: 'String' }); - - case 'LIST': - if (typeRef.ofType) { - return t.listType({ type: buildTypeNode(typeRef.ofType) }); - } - return t.listType({ type: t.namedType({ type: 'String' }) }); - - case 'SCALAR': - case 'ENUM': - case 'OBJECT': - case 'INPUT_OBJECT': - return t.namedType({ type: typeRef.name ?? 'String' }); - - default: - return t.namedType({ type: typeRef.name ?? 'String' }); - } -} - -// ============================================================================ -// Variable Definition Builders -// ============================================================================ - -/** - * Build variable definitions from operation arguments - */ -export function buildVariableDefinitions( - args: CleanArgument[] -): VariableDefinitionNode[] { - return args.map((arg) => - t.variableDefinition({ - variable: t.variable({ name: arg.name }), - type: buildTypeNode(arg.type), - }) - ); -} - -/** - * Build argument nodes that reference variables - */ -function buildArgumentNodes(args: CleanArgument[]): ArgumentNode[] { - return args.map((arg) => - t.argument({ - name: arg.name, - value: t.variable({ name: arg.name }), - }) - ); -} - -// ============================================================================ -// Field Selection Builders -// ============================================================================ - -/** - * Check if a type should have selections (is an object type) - */ -function typeNeedsSelections(typeRef: CleanTypeRef): boolean { - const baseKind = getBaseTypeKind(typeRef); - return baseKind === 'OBJECT'; -} - -/** - * Get the resolved fields for a type reference - * Uses type registry for deep resolution - */ -function getResolvedFields( - typeRef: CleanTypeRef, - typeRegistry?: TypeRegistry -): CleanObjectField[] | undefined { - // First check if fields are directly on the typeRef - if (typeRef.fields) { - return typeRef.fields; - } - - // For wrapper types, unwrap and check - if (typeRef.ofType) { - return getResolvedFields(typeRef.ofType, typeRegistry); - } - - // Look up in type registry - if (typeRegistry && typeRef.name) { - const resolved = typeRegistry.get(typeRef.name); - if (resolved?.fields) { - return resolved.fields; - } - } - - return undefined; -} - -/** - * Build field selections for an object type - * Recursively handles nested objects up to maxDepth - */ -export function buildFieldSelections( - typeRef: CleanTypeRef, - config: FieldSelectionConfig, - currentDepth: number = 0 -): FieldNode[] { - const { maxDepth, skipQueryField, typeRegistry } = config; - - // Stop recursion at max depth - if (currentDepth >= maxDepth) { - return []; - } - - const fields = getResolvedFields(typeRef, typeRegistry); - if (!fields || fields.length === 0) { - return []; - } - - const selections: FieldNode[] = []; - - for (const field of fields) { - // Skip internal fields - if (shouldSkipField(field.name, skipQueryField)) { - continue; - } - - const fieldKind = getBaseTypeKind(field.type); - - // For scalar and enum types, just add the field - if (fieldKind === 'SCALAR' || fieldKind === 'ENUM') { - selections.push(t.field({ name: field.name })); - continue; - } - - // For object types, recurse if within depth limit - if (fieldKind === 'OBJECT' && currentDepth < maxDepth - 1) { - const nestedSelections = buildFieldSelections( - field.type, - config, - currentDepth + 1 - ); - - if (nestedSelections.length > 0) { - selections.push( - t.field({ - name: field.name, - selectionSet: t.selectionSet({ selections: nestedSelections }), - }) - ); - } - } - } - - return selections; -} - -/** - * Build selections for a return type, handling connections and payloads - */ -function buildReturnTypeSelections( - returnType: CleanTypeRef, - config: FieldSelectionConfig -): FieldNode[] { - const fields = getResolvedFields(returnType, config.typeRegistry); - - if (!fields || fields.length === 0) { - return []; - } - - // Check if this is a connection type - const hasNodes = fields.some((f) => f.name === 'nodes'); - const hasTotalCount = fields.some((f) => f.name === 'totalCount'); - - if (hasNodes && hasTotalCount) { - return buildConnectionSelections(fields, config); - } - - // Check if this is a mutation payload (has clientMutationId) - const hasClientMutationId = fields.some( - (f) => f.name === 'clientMutationId' - ); - - if (hasClientMutationId) { - return buildPayloadSelections(fields, config); - } - - // Regular object - build normal selections - return buildFieldSelections(returnType, config); -} - -/** - * Build selections for a connection type - */ -function buildConnectionSelections( - fields: CleanObjectField[], - config: FieldSelectionConfig -): FieldNode[] { - const selections: FieldNode[] = []; - - // Add totalCount - const totalCountField = fields.find((f) => f.name === 'totalCount'); - if (totalCountField) { - selections.push(t.field({ name: 'totalCount' })); - } - - // Add nodes with nested selections - const nodesField = fields.find((f) => f.name === 'nodes'); - if (nodesField) { - const nodeSelections = buildFieldSelections(nodesField.type, config); - if (nodeSelections.length > 0) { - selections.push( - t.field({ - name: 'nodes', - selectionSet: t.selectionSet({ selections: nodeSelections }), - }) - ); - } - } - - // Add pageInfo - const pageInfoField = fields.find((f) => f.name === 'pageInfo'); - if (pageInfoField) { - selections.push( - t.field({ - name: 'pageInfo', - selectionSet: t.selectionSet({ - selections: [ - t.field({ name: 'hasNextPage' }), - t.field({ name: 'hasPreviousPage' }), - t.field({ name: 'startCursor' }), - t.field({ name: 'endCursor' }), - ], - }), - }) - ); - } - - return selections; -} - -/** - * Build selections for a mutation payload type - */ -function buildPayloadSelections( - fields: CleanObjectField[], - config: FieldSelectionConfig -): FieldNode[] { - const selections: FieldNode[] = []; - - for (const field of fields) { - // Skip query field - if (shouldSkipField(field.name, config.skipQueryField)) { - continue; - } - - const fieldKind = getBaseTypeKind(field.type); - - // Add scalar fields directly - if (fieldKind === 'SCALAR' || fieldKind === 'ENUM') { - selections.push(t.field({ name: field.name })); - continue; - } - - // For object fields (like the returned entity), add with selections - if (fieldKind === 'OBJECT') { - const nestedSelections = buildFieldSelections(field.type, config); - if (nestedSelections.length > 0) { - selections.push( - t.field({ - name: field.name, - selectionSet: t.selectionSet({ selections: nestedSelections }), - }) - ); - } - } - } - - return selections; -} - -// ============================================================================ -// Custom Query Builder -// ============================================================================ - -export interface CustomQueryConfig { - operation: CleanOperation; - typeRegistry?: TypeRegistry; - maxDepth?: number; - skipQueryField?: boolean; -} - -/** - * Build a custom query AST from a CleanOperation - */ -export function buildCustomQueryAST(config: CustomQueryConfig): DocumentNode { - const { - operation, - typeRegistry, - maxDepth = 2, - skipQueryField = true, - } = config; - - const operationName = `${ucFirst(operation.name)}Query`; - - // Build variable definitions - const variableDefinitions = buildVariableDefinitions(operation.args); - - // Build arguments that reference the variables - const args = buildArgumentNodes(operation.args); - - // Build return type selections - const fieldSelectionConfig: FieldSelectionConfig = { - maxDepth, - skipQueryField, - typeRegistry, - }; - - const returnTypeNeedsSelections = typeNeedsSelections(operation.returnType); - let selections: FieldNode[] = []; - - if (returnTypeNeedsSelections) { - selections = buildReturnTypeSelections( - operation.returnType, - fieldSelectionConfig - ); - } - - // Build the query field - const queryField: FieldNode = - selections.length > 0 - ? t.field({ - name: operation.name, - args: args.length > 0 ? args : undefined, - selectionSet: t.selectionSet({ selections }), - }) - : t.field({ - name: operation.name, - args: args.length > 0 ? args : undefined, - }); - - return t.document({ - definitions: [ - t.operationDefinition({ - operation: 'query', - name: operationName, - variableDefinitions: - variableDefinitions.length > 0 ? variableDefinitions : undefined, - selectionSet: t.selectionSet({ - selections: [queryField], - }), - }), - ], - }); -} - -// ============================================================================ -// Custom Mutation Builder -// ============================================================================ - -export interface CustomMutationConfig { - operation: CleanOperation; - typeRegistry?: TypeRegistry; - maxDepth?: number; - skipQueryField?: boolean; -} - -/** - * Build a custom mutation AST from a CleanOperation - */ -export function buildCustomMutationAST( - config: CustomMutationConfig -): DocumentNode { - const { - operation, - typeRegistry, - maxDepth = 2, - skipQueryField = true, - } = config; - - const operationName = `${ucFirst(operation.name)}Mutation`; - - // Build variable definitions - const variableDefinitions = buildVariableDefinitions(operation.args); - - // Build arguments that reference the variables - const args = buildArgumentNodes(operation.args); - - // Build return type selections - const fieldSelectionConfig: FieldSelectionConfig = { - maxDepth, - skipQueryField, - typeRegistry, - }; - - const returnTypeNeedsSelections = typeNeedsSelections(operation.returnType); - let selections: FieldNode[] = []; - - if (returnTypeNeedsSelections) { - selections = buildReturnTypeSelections( - operation.returnType, - fieldSelectionConfig - ); - } - - // Build the mutation field - const mutationField: FieldNode = - selections.length > 0 - ? t.field({ - name: operation.name, - args: args.length > 0 ? args : undefined, - selectionSet: t.selectionSet({ selections }), - }) - : t.field({ - name: operation.name, - args: args.length > 0 ? args : undefined, - }); - - return t.document({ - definitions: [ - t.operationDefinition({ - operation: 'mutation', - name: operationName, - variableDefinitions: - variableDefinitions.length > 0 ? variableDefinitions : undefined, - selectionSet: t.selectionSet({ - selections: [mutationField], - }), - }), - ], - }); -} - -// ============================================================================ -// Print Utilities -// ============================================================================ - -/** - * Print a document AST to GraphQL string - */ -export function printGraphQL(ast: DocumentNode): string { - return print(ast); -} - -/** - * Build and print a custom query in one call - */ -export function buildCustomQueryString(config: CustomQueryConfig): string { - return printGraphQL(buildCustomQueryAST(config)); -} - -/** - * Build and print a custom mutation in one call - */ -export function buildCustomMutationString( - config: CustomMutationConfig -): string { - return printGraphQL(buildCustomMutationAST(config)); -} - -// ============================================================================ -// Helper Utilities -// ============================================================================ - -/** - * Uppercase first character - */ -function ucFirst(str: string): string { - return str.charAt(0).toUpperCase() + str.slice(1); -} diff --git a/graphql/codegen/src/core/codegen/schema-types-generator.ts b/graphql/codegen/src/core/codegen/schema-types-generator.ts index b4bc6a655..0e8e4b6a1 100644 --- a/graphql/codegen/src/core/codegen/schema-types-generator.ts +++ b/graphql/codegen/src/core/codegen/schema-types-generator.ts @@ -11,19 +11,20 @@ * * Uses Babel AST for robust code generation. */ +import * as t from '@babel/types'; + import type { - TypeRegistry, CleanArgument, ResolvedType, + TypeRegistry } from '../../types/schema'; -import * as t from '@babel/types'; import { generateCode } from './babel-ast'; -import { getTypeBaseName } from './type-resolver'; import { - scalarToTsType, - SCALAR_NAMES, BASE_FILTER_TYPE_NAMES, + SCALAR_NAMES, + scalarToTsType } from './scalars'; +import { getTypeBaseName } from './type-resolver'; import { getGeneratedFileHeader } from './utils'; export interface GeneratedSchemaTypesFile { @@ -49,7 +50,7 @@ const SKIP_TYPES = new Set([ '__InputValue', '__EnumValue', '__Directive', - ...BASE_FILTER_TYPE_NAMES, + ...BASE_FILTER_TYPE_NAMES ]); const SKIP_TYPE_PATTERNS: RegExp[] = []; @@ -390,6 +391,6 @@ export function generateSchemaTypesFile( fileName: 'schema-types.ts', content, generatedEnums: Array.from(enumResult.generatedTypes).sort(), - referencedTableTypes, + referencedTableTypes }; } diff --git a/graphql/codegen/src/core/codegen/select-helpers.ts b/graphql/codegen/src/core/codegen/select-helpers.ts new file mode 100644 index 000000000..d17a770e3 --- /dev/null +++ b/graphql/codegen/src/core/codegen/select-helpers.ts @@ -0,0 +1,90 @@ +/** + * Shared helpers for select type resolution in custom operations + * + * Used by custom-queries.ts, custom-mutations.ts, and orm/custom-ops-generator.ts + */ +import type { CleanArgument, TypeRegistry } from '../../types/schema'; +import { SCALAR_NAMES } from './scalars'; +import { getTypeBaseName } from './type-resolver'; + +/** + * Types that don't need Select types (scalars + root query/mutation types) + */ +export const NON_SELECT_TYPES = new Set([ + ...SCALAR_NAMES, + 'Query', + 'Mutation' +]); + +/** + * Get the Select type name for a return type. + * Returns null for scalar types, Connection types, and root types. + */ +export function getSelectTypeName(returnType: CleanArgument['type']): string | null { + const baseName = getTypeBaseName(returnType); + if ( + baseName && + !NON_SELECT_TYPES.has(baseName) && + !baseName.endsWith('Connection') + ) { + return `${baseName}Select`; + } + return null; +} + +/** + * Wrap a type reference in InferSelectResult, handling NON_NULL and LIST wrappers. + */ +export function wrapInferSelectResult( + typeRef: CleanArgument['type'], + payloadTypeName: string +): string { + if (typeRef.kind === 'NON_NULL' && typeRef.ofType) { + return wrapInferSelectResult(typeRef.ofType as CleanArgument['type'], payloadTypeName); + } + + if (typeRef.kind === 'LIST' && typeRef.ofType) { + return `${wrapInferSelectResult(typeRef.ofType as CleanArgument['type'], payloadTypeName)}[]`; + } + + return `InferSelectResult<${payloadTypeName}, S>`; +} + +/** + * Build a default select literal string for a given type. + * Finds an 'id' or 'nodeId' field, or falls back to first scalar field. + */ +export function buildDefaultSelectLiteral( + typeName: string, + typeRegistry: TypeRegistry, + depth: number = 0 +): string { + const resolved = typeRegistry.get(typeName); + const fields = resolved?.fields ?? []; + + if (depth > 3 || fields.length === 0) { + // Use first field if available, otherwise fallback to 'id' + return fields.length > 0 ? `{ ${fields[0].name}: true }` : `{ id: true }`; + } + + const idLike = fields.find((f) => f.name === 'id' || f.name === 'nodeId'); + if (idLike) return `{ ${idLike.name}: true }`; + + const scalarField = fields.find((f) => { + const baseName = getTypeBaseName(f.type); + if (!baseName) return false; + if (NON_SELECT_TYPES.has(baseName)) return true; + return typeRegistry.get(baseName)?.kind === 'ENUM'; + }); + if (scalarField) return `{ ${scalarField.name}: true }`; + + const first = fields[0]; + + const firstBase = getTypeBaseName(first.type); + if (!firstBase || NON_SELECT_TYPES.has(firstBase) || typeRegistry.get(firstBase)?.kind === 'ENUM') { + return `{ ${first.name}: true }`; + } + + const nested = buildDefaultSelectLiteral(firstBase, typeRegistry, depth + 1); + return `{ ${first.name}: { select: ${nested} } }`; +} diff --git a/graphql/codegen/src/core/codegen/shared/index.ts b/graphql/codegen/src/core/codegen/shared/index.ts index ccf6819fe..ccc31ead8 100644 --- a/graphql/codegen/src/core/codegen/shared/index.ts +++ b/graphql/codegen/src/core/codegen/shared/index.ts @@ -11,12 +11,13 @@ * schema-types.ts - Enums, input types, payload types * filters.ts - Filter types (StringFilter, IntFilter, etc.) */ -import type { CleanTable, CleanOperation, TypeRegistry } from '../../../types/schema'; -import type { GraphQLSDKConfigTarget } from '../../../types/config'; import * as t from '@babel/types'; -import { generateCode, addJSDocComment } from '../babel-ast'; -import { generateTypesFile } from '../types'; + +import type { GraphQLSDKConfigTarget } from '../../../types/config'; +import type { CleanOperation, CleanTable, TypeRegistry } from '../../../types/schema'; +import { addJSDocComment,generateCode } from '../babel-ast'; import { generateSchemaTypesFile } from '../schema-types-generator'; +import { generateTypesFile } from '../types'; import { getTableNames } from '../utils'; /** @@ -64,14 +65,14 @@ export function generateSharedTypes(options: GenerateSharedOptions): GenerateSha if (customOperations && customOperations.typeRegistry) { const schemaTypesResult = generateSchemaTypesFile({ typeRegistry: customOperations.typeRegistry, - tableTypeNames, + tableTypeNames }); // Only include if there's meaningful content if (schemaTypesResult.content.split('\n').length > 10) { files.push({ path: 'schema-types.ts', - content: schemaTypesResult.content, + content: schemaTypesResult.content }); hasSchemaTypes = true; generatedEnumNames = schemaTypesResult.generatedEnums || []; @@ -82,21 +83,21 @@ export function generateSharedTypes(options: GenerateSharedOptions): GenerateSha files.push({ path: 'types.ts', content: generateTypesFile(tables, { - enumsFromSchemaTypes: generatedEnumNames, - }), + enumsFromSchemaTypes: generatedEnumNames + }) }); // 3. Generate barrel export (index.ts) const barrelContent = generateSharedBarrel(hasSchemaTypes); files.push({ path: 'index.ts', - content: barrelContent, + content: barrelContent }); return { files, generatedEnumNames, - hasSchemaTypes, + hasSchemaTypes }; } @@ -118,12 +119,12 @@ function generateSharedBarrel(hasSchemaTypes: boolean): string { if (statements.length > 0) { addJSDocComment(statements[0], [ 'Shared types - auto-generated, do not edit', - '@generated by @constructive-io/graphql-codegen', + '@generated by @constructive-io/graphql-codegen' ]); } return generateCode(statements); } -export { generateTypesFile } from '../types'; export { generateSchemaTypesFile } from '../schema-types-generator'; +export { generateTypesFile } from '../types'; diff --git a/graphql/codegen/src/core/codegen/templates/client.browser.ts b/graphql/codegen/src/core/codegen/templates/client.browser.ts deleted file mode 100644 index 6db2d891c..000000000 --- a/graphql/codegen/src/core/codegen/templates/client.browser.ts +++ /dev/null @@ -1,271 +0,0 @@ -/** - * GraphQL client configuration and execution (Browser-compatible) - * - * This is the RUNTIME code that gets copied to generated output. - * Uses native W3C fetch API for browser compatibility. - * - * NOTE: This file is read at codegen time and written to output. - * Any changes here will affect all generated clients. - */ - -// ============================================================================ -// Configuration -// ============================================================================ - -export interface GraphQLClientConfig { - /** GraphQL endpoint URL */ - endpoint: string; - /** Default headers to include in all requests */ - headers?: Record; - /** Dynamic headers callback called on every request */ - getHeaders?: () => Record; -} - -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 ', - * }, - * }); - * ``` - */ -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 '); - * ``` - */ -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 ', 'X-Custom': 'value' }); - * ``` - */ -export function setHeaders(headers: Record): 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; - extensions?: Record; -} - -export class GraphQLClientError extends Error { - constructor( - message: string, - public errors: GraphQLError[], - public response?: Response - ) { - super(message); - this.name = 'GraphQLClientError'; - } -} - -// ============================================================================ -// Execution -// ============================================================================ - -export interface ExecuteOptions { - /** Override headers for this request */ - headers?: Record; - /** AbortSignal for request cancellation */ - signal?: AbortSignal; -} - -/** - * Execute a GraphQL operation - * - * @example - * ```ts - * const result = await execute( - * carsQueryDocument, - * { first: 10 } - * ); - * ``` - */ -export async function execute< - TData = unknown, - TVariables = Record, ->( - document: string, - variables?: TVariables, - options?: ExecuteOptions -): Promise { - const config = getConfig(); - const dynamicHeaders = config.getHeaders?.() ?? {}; - - const response = await fetch(config.endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...config.headers, - ...dynamicHeaders, - ...options?.headers, - }, - body: JSON.stringify({ - query: document, - variables, - }), - signal: options?.signal, - }); - - 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 - ); - } - - return json.data as TData; -} - -/** - * Execute a GraphQL operation with full response (data + errors) - * Useful when you want to handle partial data with errors - */ -export async function executeWithErrors< - TData = unknown, - TVariables = Record, ->( - document: string, - variables?: TVariables, - options?: ExecuteOptions -): Promise<{ data: TData | null; errors: GraphQLError[] | null }> { - const config = getConfig(); - const dynamicHeaders = config.getHeaders?.() ?? {}; - - const response = await fetch(config.endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...config.headers, - ...dynamicHeaders, - ...options?.headers, - }, - body: JSON.stringify({ - query: document, - variables, - }), - signal: options?.signal, - }); - - const json = await response.json(); - - return { - data: json.data ?? null, - errors: json.errors ?? null, - }; -} - -// ============================================================================ -// 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 - */ -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 -// }, -// }, -// }); diff --git a/graphql/codegen/src/core/codegen/templates/client.node.ts b/graphql/codegen/src/core/codegen/templates/client.node.ts deleted file mode 100644 index 156f41ceb..000000000 --- a/graphql/codegen/src/core/codegen/templates/client.node.ts +++ /dev/null @@ -1,337 +0,0 @@ -/** - * GraphQL client configuration and execution (Node.js with native http/https) - * - * This is the RUNTIME code that gets copied to generated output. - * Uses native Node.js http/https modules. - * - * NOTE: This file is read at codegen time and written to output. - * Any changes here will affect all generated clients. - */ - -import http from 'node:http'; -import https from 'node:https'; - -// ============================================================================ -// HTTP Request Helper -// ============================================================================ - -interface HttpResponse { - statusCode: number; - statusMessage: string; - data: string; -} - -/** - * Make an HTTP/HTTPS request using native Node modules - */ -function makeRequest( - url: URL, - options: http.RequestOptions, - body: string -): Promise { - return new Promise((resolve, reject) => { - const protocol = url.protocol === 'https:' ? https : http; - - const req = protocol.request(url, options, (res) => { - let data = ''; - res.setEncoding('utf8'); - res.on('data', (chunk: string) => { - data += chunk; - }); - res.on('end', () => { - resolve({ - statusCode: res.statusCode || 0, - statusMessage: res.statusMessage || '', - data, - }); - }); - }); - - req.on('error', reject); - req.write(body); - req.end(); - }); -} - -// ============================================================================ -// Configuration -// ============================================================================ - -export interface GraphQLClientConfig { - /** GraphQL endpoint URL */ - endpoint: string; - /** Default headers to include in all requests */ - headers?: Record; - /** Dynamic headers callback called on every request */ - getHeaders?: () => Record; -} - -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 ', - * }, - * }); - * ``` - */ -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 '); - * ``` - */ -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 ', 'X-Custom': 'value' }); - * ``` - */ -export function setHeaders(headers: Record): 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; - extensions?: Record; -} - -export class GraphQLClientError extends Error { - constructor( - message: string, - public errors: GraphQLError[], - public statusCode?: number - ) { - super(message); - this.name = 'GraphQLClientError'; - } -} - -// ============================================================================ -// Execution -// ============================================================================ - -export interface ExecuteOptions { - /** Override headers for this request */ - headers?: Record; -} - -/** - * Execute a GraphQL operation - * - * @example - * ```ts - * const result = await execute( - * carsQueryDocument, - * { first: 10 } - * ); - * ``` - */ -export async function execute< - TData = unknown, - TVariables = Record, ->( - document: string, - variables?: TVariables, - options?: ExecuteOptions -): Promise { - const config = getConfig(); - const url = new URL(config.endpoint); - const dynamicHeaders = config.getHeaders?.() ?? {}; - - const body = JSON.stringify({ - query: document, - variables, - }); - - const requestOptions: http.RequestOptions = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...config.headers, - ...dynamicHeaders, - ...options?.headers, - }, - }; - - const response = await makeRequest(url, requestOptions, body); - - if (response.statusCode < 200 || response.statusCode >= 300) { - throw new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`); - } - - const json = JSON.parse(response.data) as { - data?: TData; - errors?: GraphQLError[]; - }; - - if (json.errors && json.errors.length > 0) { - throw new GraphQLClientError( - json.errors[0].message || 'GraphQL request failed', - json.errors, - response.statusCode - ); - } - - return json.data as TData; -} - -/** - * Execute a GraphQL operation with full response (data + errors) - * Useful when you want to handle partial data with errors - */ -export async function executeWithErrors< - TData = unknown, - TVariables = Record, ->( - document: string, - variables?: TVariables, - options?: ExecuteOptions -): Promise<{ data: TData | null; errors: GraphQLError[] | null }> { - const config = getConfig(); - const url = new URL(config.endpoint); - const dynamicHeaders = config.getHeaders?.() ?? {}; - - const body = JSON.stringify({ - query: document, - variables, - }); - - const requestOptions: http.RequestOptions = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...config.headers, - ...dynamicHeaders, - ...options?.headers, - }, - }; - - const response = await makeRequest(url, requestOptions, body); - - if (response.statusCode < 200 || response.statusCode >= 300) { - return { - data: null, - errors: [{ message: `HTTP ${response.statusCode}: ${response.statusMessage}` }], - }; - } - - const json = JSON.parse(response.data) as { - data?: TData; - errors?: GraphQLError[]; - }; - - return { - data: json.data ?? null, - errors: json.errors ?? null, - }; -} - -// ============================================================================ -// 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 - */ -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 -// }, -// }, -// }); diff --git a/graphql/codegen/src/core/codegen/templates/orm-client.ts b/graphql/codegen/src/core/codegen/templates/orm-client.ts index a71633d67..c6993a338 100644 --- a/graphql/codegen/src/core/codegen/templates/orm-client.ts +++ b/graphql/codegen/src/core/codegen/templates/orm-client.ts @@ -11,13 +11,13 @@ import type { GraphQLAdapter, GraphQLError, - QueryResult, + QueryResult } from '@constructive-io/graphql-types'; export type { GraphQLAdapter, GraphQLError, - QueryResult, + QueryResult } from '@constructive-io/graphql-types'; /** @@ -43,19 +43,19 @@ export class FetchAdapter implements GraphQLAdapter { headers: { 'Content-Type': 'application/json', Accept: 'application/json', - ...this.headers, + ...this.headers }, body: JSON.stringify({ query: document, - variables: variables ?? {}, - }), + variables: variables ?? {} + }) }); if (!response.ok) { return { ok: false, data: null, - errors: [{ message: `HTTP ${response.status}: ${response.statusText}` }], + errors: [{ message: `HTTP ${response.status}: ${response.statusText}` }] }; } @@ -68,14 +68,14 @@ export class FetchAdapter implements GraphQLAdapter { return { ok: false, data: null, - errors: json.errors, + errors: json.errors }; } return { ok: true, data: json.data as T, - errors: undefined, + errors: undefined }; } diff --git a/graphql/codegen/src/core/codegen/templates/query-builder.ts b/graphql/codegen/src/core/codegen/templates/query-builder.ts index 869e210e8..a678fbbd0 100644 --- a/graphql/codegen/src/core/codegen/templates/query-builder.ts +++ b/graphql/codegen/src/core/codegen/templates/query-builder.ts @@ -8,15 +8,16 @@ * Any changes here will affect all generated ORM clients. */ -import * as t from 'gql-ast'; import { parseType, print } from '@0no-co/graphql.web'; +import * as t from 'gql-ast'; import type { ArgumentNode, - FieldNode, - VariableDefinitionNode, EnumValueNode, + FieldNode, + VariableDefinitionNode } from 'graphql'; -import { OrmClient, QueryResult, GraphQLRequestError } from './client'; + +import { GraphQLRequestError,OrmClient, QueryResult } from './client'; export interface QueryBuilderConfig { client: OrmClient; @@ -133,7 +134,7 @@ export function buildSelections( nested.filter ? t.argument({ name: 'filter', value: buildValueAst(nested.filter) }) : null, - buildEnumListArg('orderBy', nested.orderBy), + buildEnumListArg('orderBy', nested.orderBy) ]); if (isConnection) { @@ -142,8 +143,8 @@ export function buildSelections( name: key, args, selectionSet: t.selectionSet({ - selections: buildConnectionSelections(nestedSelections), - }), + selections: buildConnectionSelections(nestedSelections) + }) }) ); } else { @@ -151,7 +152,7 @@ export function buildSelections( t.field({ name: key, args, - selectionSet: t.selectionSet({ selections: nestedSelections }), + selectionSet: t.selectionSet({ selections: nestedSelections }) }) ); } @@ -200,7 +201,7 @@ export function buildFindManyDocument( { varName: 'orderBy', typeName: '[' + orderByTypeName + '!]', - value: args.orderBy?.length ? args.orderBy : undefined, + value: args.orderBy?.length ? args.orderBy : undefined }, variableDefinitions, queryArgs, @@ -249,13 +250,13 @@ export function buildFindManyDocument( name: queryField, args: queryArgs.length ? queryArgs : undefined, selectionSet: t.selectionSet({ - selections: buildConnectionSelections(selections), - }), - }), - ], - }), - }), - ], + selections: buildConnectionSelections(selections) + }) + }) + ] + }) + }) + ] }); return { document: print(document), variables }; @@ -305,15 +306,15 @@ export function buildFindFirstDocument( selections: [ t.field({ name: 'nodes', - selectionSet: t.selectionSet({ selections }), - }), - ], - }), - }), - ], - }), - }), - ], + selectionSet: t.selectionSet({ selections }) + }) + ] + }) + }) + ] + }) + }) + ] }); return { document: print(document), variables }; @@ -339,15 +340,15 @@ export function buildCreateDocument( resultSelections: [ t.field({ name: entityField, - selectionSet: t.selectionSet({ selections }), - }), - ], + selectionSet: t.selectionSet({ selections }) + }) + ] }), variables: { input: { - [entityField]: data, - }, - }, + [entityField]: data + } + } }; } @@ -372,26 +373,117 @@ export function buildUpdateDocument( +export function buildUpdateByPkDocument( + operationName: string, + mutationField: string, + entityField: string, + select: TSelect, + id: string | number, + data: TData, + inputTypeName: string, + idFieldName: string +): { document: string; variables: Record } { + const selections = select + ? buildSelections(select as Record) + : [t.field({ name: 'id' })]; + + return { + document: buildInputMutationDocument({ + operationName, + mutationField, + inputTypeName, + resultSelections: [ + t.field({ + name: entityField, + selectionSet: t.selectionSet({ selections }) + }) + ] + }), + variables: { + input: { + [idFieldName]: id, + patch: data + } + } + }; +} + +export function buildFindOneDocument( + operationName: string, + queryField: string, + id: string | number, + select: TSelect, + idArgName: string, + idTypeName: string +): { document: string; variables: Record } { + const selections = select + ? buildSelections(select as Record) + : [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: '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 + inputTypeName: string, + select?: TSelect ): { document: string; variables: Record } { + const entitySelections = select + ? buildSelections(select as Record) + : [t.field({ name: 'id' })]; + return { document: buildInputMutationDocument({ operationName, @@ -401,16 +493,49 @@ export function buildDeleteDocument( t.field({ name: entityField, selectionSet: t.selectionSet({ - selections: [t.field({ name: 'id' })], - }), - }), - ], + selections: entitySelections + }) + }) + ] }), variables: { input: { - id: where.id, - }, - }, + id: where.id + } + } + }; +} + +export function buildDeleteByPkDocument( + operationName: string, + mutationField: string, + entityField: string, + id: string | number, + inputTypeName: string, + idFieldName: string, + select?: TSelect +): { document: string; variables: Record } { + const entitySelections = select + ? buildSelections(select as Record) + : [t.field({ name: 'id' })]; + + return { + document: buildInputMutationDocument({ + operationName, + mutationField, + inputTypeName, + resultSelections: [ + t.field({ + name: entityField, + selectionSet: t.selectionSet({ selections: entitySelections }) + }) + ] + }), + variables: { + input: { + [idFieldName]: id + } + } }; } @@ -440,13 +565,13 @@ export function buildCustomDocument( const variableDefs = variableDefinitions.map((definition) => t.variableDefinition({ variable: t.variable({ name: definition.name }), - type: parseType(definition.type), + type: parseType(definition.type) }) ); const fieldArgs = variableDefinitions.map((definition) => t.argument({ name: definition.name, - value: t.variable({ name: definition.name }), + value: t.variable({ name: definition.name }) }) ); @@ -467,17 +592,17 @@ export function buildCustomDocument( args: fieldArgs.length ? fieldArgs : undefined, selectionSet: fieldSelections.length ? t.selectionSet({ selections: fieldSelections }) - : undefined, - }), - ], - }), - }), - ], + : undefined + }) + ] + }) + }) + ] }); return { document: print(document), - variables: (args ?? {}) as Record, + variables: (args ?? {}) as Record }; } @@ -513,15 +638,15 @@ function buildEnumListArg( return t.argument({ name, value: t.listValue({ - values: values.map((value) => buildEnumValue(value)), - }), + values: values.map((value) => buildEnumValue(value)) + }) }); } function buildEnumValue(value: string): EnumValueNode { return { kind: 'EnumValue', - value, + value }; } @@ -530,7 +655,7 @@ function buildPageInfoSelections(): FieldNode[] { t.field({ name: 'hasNextPage' }), t.field({ name: 'hasPreviousPage' }), t.field({ name: 'startCursor' }), - t.field({ name: 'endCursor' }), + t.field({ name: 'endCursor' }) ]; } @@ -538,13 +663,13 @@ function buildConnectionSelections(nodeSelections: FieldNode[]): FieldNode[] { return [ t.field({ name: 'nodes', - selectionSet: t.selectionSet({ selections: nodeSelections }), + selectionSet: t.selectionSet({ selections: nodeSelections }) }), t.field({ name: 'totalCount' }), t.field({ name: 'pageInfo', - selectionSet: t.selectionSet({ selections: buildPageInfoSelections() }), - }), + selectionSet: t.selectionSet({ selections: buildPageInfoSelections() }) + }) ]; } @@ -571,8 +696,8 @@ function buildInputMutationDocument(config: InputMutationConfig): string { variableDefinitions: [ t.variableDefinition({ variable: t.variable({ name: 'input' }), - type: parseType(config.inputTypeName + '!'), - }), + type: parseType(config.inputTypeName + '!') + }) ], selectionSet: t.selectionSet({ selections: [ @@ -581,17 +706,17 @@ function buildInputMutationDocument(config: InputMutationConfig): string { args: [ t.argument({ name: 'input', - value: t.variable({ name: 'input' }), - }), + value: t.variable({ name: 'input' }) + }) ], selectionSet: t.selectionSet({ - selections: config.resultSelections, - }), - }), - ], - }), - }), - ], + selections: config.resultSelections + }) + }) + ] + }) + }) + ] }); return print(document); } @@ -607,13 +732,13 @@ function addVariable( definitions.push( t.variableDefinition({ variable: t.variable({ name: spec.varName }), - type: parseType(spec.typeName), + type: parseType(spec.typeName) }) ); args.push( t.argument({ name: spec.argName ?? spec.varName, - value: t.variable({ name: spec.varName }), + value: t.variable({ name: spec.varName }) }) ); variables[spec.varName] = spec.value; @@ -650,7 +775,7 @@ function buildValueAst( if (Array.isArray(value)) { return t.listValue({ - values: value.map((item) => buildValueAst(item)), + values: value.map((item) => buildValueAst(item)) }); } @@ -660,9 +785,9 @@ function buildValueAst( fields: Object.entries(obj).map(([key, val]) => t.objectField({ name: key, - value: buildValueAst(val), + value: buildValueAst(val) }) - ), + ) }); } diff --git a/graphql/codegen/src/core/codegen/templates/select-types.ts b/graphql/codegen/src/core/codegen/templates/select-types.ts index 4f0360a77..df14aac7b 100644 --- a/graphql/codegen/src/core/codegen/templates/select-types.ts +++ b/graphql/codegen/src/core/codegen/templates/select-types.ts @@ -48,8 +48,17 @@ export interface UpdateArgs { select?: TSelect; } -export interface DeleteArgs { +export type FindOneArgs< + TSelect, + TIdName extends string = 'id', + TId = string +> = { + select?: TSelect; +} & Record; + +export interface DeleteArgs { where: TWhere; + select?: TSelect; } /** diff --git a/graphql/codegen/src/core/codegen/type-resolver.ts b/graphql/codegen/src/core/codegen/type-resolver.ts index f358cff12..9e8856aa3 100644 --- a/graphql/codegen/src/core/codegen/type-resolver.ts +++ b/graphql/codegen/src/core/codegen/type-resolver.ts @@ -5,11 +5,10 @@ * into TypeScript type strings and interface definitions. */ import type { - CleanTypeRef, - CleanArgument, CleanObjectField, + CleanTypeRef } from '../../types/schema'; -import { scalarToTsType as resolveScalarToTs, SCALAR_NAMES } from './scalars'; +import { SCALAR_NAMES,scalarToTsType as resolveScalarToTs } from './scalars'; // ============================================================================ // Type Tracker for Collecting Referenced Types @@ -31,7 +30,7 @@ const SKIP_TYPE_TRACKING = new Set([ '__EnumValue', '__Directive', // Connection types (handled separately) - 'PageInfo', + 'PageInfo' ]); /** @@ -88,7 +87,7 @@ export function createTypeTracker(options?: TypeTrackerOptions): TypeTracker { }, reset() { referencedTypes.clear(); - }, + } }; } @@ -116,42 +115,42 @@ export function scalarToTsType(scalarName: string): string { */ export function typeRefToTsType(typeRef: CleanTypeRef, tracker?: TypeTracker): string { switch (typeRef.kind) { - case 'NON_NULL': - // Non-null wrapper - unwrap and return the inner type - if (typeRef.ofType) { - return typeRefToTsType(typeRef.ofType, tracker); - } - return 'unknown'; + case 'NON_NULL': + // Non-null wrapper - unwrap and return the inner type + if (typeRef.ofType) { + return typeRefToTsType(typeRef.ofType, tracker); + } + return 'unknown'; - case 'LIST': - // List wrapper - wrap inner type in array - if (typeRef.ofType) { - const innerType = typeRefToTsType(typeRef.ofType, tracker); - return `${innerType}[]`; - } - return 'unknown[]'; + case 'LIST': + // List wrapper - wrap inner type in array + if (typeRef.ofType) { + const innerType = typeRefToTsType(typeRef.ofType, tracker); + return `${innerType}[]`; + } + return 'unknown[]'; - case 'SCALAR': - // Scalar type - map to TS type - return scalarToTsType(typeRef.name ?? 'unknown'); + case 'SCALAR': + // Scalar type - map to TS type + return scalarToTsType(typeRef.name ?? 'unknown'); - case 'ENUM': { - // Enum type - use the GraphQL enum name and track it - const typeName = typeRef.name ?? 'string'; - tracker?.track(typeName); - return typeName; - } + case 'ENUM': { + // Enum type - use the GraphQL enum name and track it + const typeName = typeRef.name ?? 'string'; + tracker?.track(typeName); + return typeName; + } - case 'OBJECT': - case 'INPUT_OBJECT': { - // Object types - use the GraphQL type name and track it - const typeName = typeRef.name ?? 'unknown'; - tracker?.track(typeName); - return typeName; - } + case 'OBJECT': + case 'INPUT_OBJECT': { + // Object types - use the GraphQL type name and track it + const typeName = typeRef.name ?? 'unknown'; + tracker?.track(typeName); + return typeName; + } - default: - return 'unknown'; + default: + return 'unknown'; } } diff --git a/graphql/codegen/src/core/codegen/types.ts b/graphql/codegen/src/core/codegen/types.ts index 4491ed972..1fb93e70c 100644 --- a/graphql/codegen/src/core/codegen/types.ts +++ b/graphql/codegen/src/core/codegen/types.ts @@ -1,10 +1,11 @@ /** * Types generator - generates types.ts with entity interfaces using Babel AST */ -import type { CleanTable } from '../../types/schema'; import * as t from '@babel/types'; + +import type { CleanTable } from '../../types/schema'; import { generateCode } from './babel-ast'; -import { getScalarFields, fieldTypeToTs, getGeneratedFileHeader } from './utils'; +import { fieldTypeToTs, getGeneratedFileHeader,getScalarFields } from './utils'; interface InterfaceProperty { name: string; @@ -37,7 +38,7 @@ const FILTER_CONFIGS: Array<{ name: string; tsType: string; operators: FilterOps // List filters { name: 'StringListFilter', tsType: 'string[]', operators: ['equality', 'distinct', 'comparison', 'listArray'] }, { name: 'IntListFilter', tsType: 'number[]', operators: ['equality', 'distinct', 'comparison', 'listArray'] }, - { name: 'UUIDListFilter', tsType: 'string[]', operators: ['equality', 'distinct', 'comparison', 'listArray'] }, + { name: 'UUIDListFilter', tsType: 'string[]', operators: ['equality', 'distinct', 'comparison', 'listArray'] } ]; /** Build filter properties based on operator sets */ @@ -49,7 +50,7 @@ function buildFilterProperties(tsType: string, operators: FilterOps[]): Interfac props.push( { name: 'isNull', type: 'boolean', optional: true }, { name: 'equalTo', type: tsType, optional: true }, - { name: 'notEqualTo', type: tsType, optional: true }, + { name: 'notEqualTo', type: tsType, optional: true } ); } @@ -57,7 +58,7 @@ function buildFilterProperties(tsType: string, operators: FilterOps[]): Interfac if (operators.includes('distinct')) { props.push( { name: 'distinctFrom', type: tsType, optional: true }, - { name: 'notDistinctFrom', type: tsType, optional: true }, + { name: 'notDistinctFrom', type: tsType, optional: true } ); } @@ -65,7 +66,7 @@ function buildFilterProperties(tsType: string, operators: FilterOps[]): Interfac if (operators.includes('inArray')) { props.push( { name: 'in', type: `${tsType}[]`, optional: true }, - { name: 'notIn', type: `${tsType}[]`, optional: true }, + { name: 'notIn', type: `${tsType}[]`, optional: true } ); } @@ -75,7 +76,7 @@ function buildFilterProperties(tsType: string, operators: FilterOps[]): Interfac { name: 'lessThan', type: tsType, optional: true }, { name: 'lessThanOrEqualTo', type: tsType, optional: true }, { name: 'greaterThan', type: tsType, optional: true }, - { name: 'greaterThanOrEqualTo', type: tsType, optional: true }, + { name: 'greaterThanOrEqualTo', type: tsType, optional: true } ); } @@ -97,7 +98,7 @@ function buildFilterProperties(tsType: string, operators: FilterOps[]): Interfac { name: 'like', type: 'string', optional: true }, { name: 'notLike', type: 'string', optional: true }, { name: 'likeInsensitive', type: 'string', optional: true }, - { name: 'notLikeInsensitive', type: 'string', optional: true }, + { name: 'notLikeInsensitive', type: 'string', optional: true } ); } @@ -108,7 +109,7 @@ function buildFilterProperties(tsType: string, operators: FilterOps[]): Interfac { name: 'containedBy', type: 'unknown', optional: true }, { name: 'containsKey', type: 'string', optional: true }, { name: 'containsAllKeys', type: 'string[]', optional: true }, - { name: 'containsAnyKeys', type: 'string[]', optional: true }, + { name: 'containsAnyKeys', type: 'string[]', optional: true } ); } @@ -117,7 +118,7 @@ function buildFilterProperties(tsType: string, operators: FilterOps[]): Interfac props.push( { name: 'contains', type: 'string', optional: true }, { name: 'containedBy', type: 'string', optional: true }, - { name: 'containsOrContainedBy', type: 'string', optional: true }, + { name: 'containsOrContainedBy', type: 'string', optional: true } ); } @@ -139,7 +140,7 @@ function buildFilterProperties(tsType: string, operators: FilterOps[]): Interfac { name: 'anyLessThan', type: baseType, optional: true }, { name: 'anyLessThanOrEqualTo', type: baseType, optional: true }, { name: 'anyGreaterThan', type: baseType, optional: true }, - { name: 'anyGreaterThanOrEqualTo', type: baseType, optional: true }, + { name: 'anyGreaterThanOrEqualTo', type: baseType, optional: true } ); } @@ -237,7 +238,7 @@ export function generateTypesFile( const properties: InterfaceProperty[] = scalarFields.map((field) => ({ name: field.name, - type: `${fieldTypeToTs(field.type)} | null`, + type: `${fieldTypeToTs(field.type)} | null` })); statements.push(createInterfaceDeclaration(table.name, properties)); diff --git a/graphql/codegen/src/core/codegen/utils.ts b/graphql/codegen/src/core/codegen/utils.ts index dc46b8703..859a67900 100644 --- a/graphql/codegen/src/core/codegen/utils.ts +++ b/graphql/codegen/src/core/codegen/utils.ts @@ -1,13 +1,14 @@ /** * Codegen utilities - naming conventions, type mapping, and helpers */ +import { pluralize } from 'inflekt'; + import type { - CleanTable, CleanField, CleanFieldType, + CleanTable } from '../../types/schema'; -import { scalarToTsType, scalarToFilterType } from './scalars'; -import { pluralize } from 'inflekt'; +import { scalarToFilterType,scalarToTsType } from './scalars'; // ============================================================================ // String manipulation @@ -77,7 +78,7 @@ export function getTableNames(table: CleanTable): TableNames { typeName, singularName, pluralName, - pluralTypeName, + pluralTypeName }; } @@ -360,8 +361,8 @@ export function getPrimaryKeyInfo(table: CleanTable): PrimaryKeyField[] { { name: idField.name, gqlType: idField.type.gqlType, - tsType: fieldTypeToTs(idField.type), - }, + tsType: fieldTypeToTs(idField.type) + } ]; } // Last resort: assume 'id' of type string (UUID) @@ -370,7 +371,7 @@ export function getPrimaryKeyInfo(table: CleanTable): PrimaryKeyField[] { return pk.fields.map((f) => ({ name: f.name, gqlType: f.type.gqlType, - tsType: fieldTypeToTs(f.type), + tsType: fieldTypeToTs(f.type) })); } @@ -400,6 +401,36 @@ export function hasValidPrimaryKey(table: CleanTable): boolean { return false; } +/** + * Get the best field name for a defaultSelect literal. + * Prefers PK field if valid, then 'id'/'nodeId', then first scalar field. + * Unlike getPrimaryKeyInfo(), never returns a fictional 'id' fallback. + */ +export function getDefaultSelectFieldName(table: CleanTable): string { + // 1. Try the actual primary key + const pk = table.constraints?.primaryKey?.[0]; + if (pk && pk.fields.length >= 1) { + return pk.fields[0].name; + } + // 2. Try id / nodeId fields + const idField = table.fields.find((f) => f.name === 'id' || f.name === 'nodeId'); + if (idField) { + return idField.name; + } + // 3. First non-array scalar field + const scalarField = table.fields.find( + (f) => !f.type.isArray && scalarToTsType(f.type.gqlType) !== f.type.gqlType + ); + if (scalarField) { + return scalarField.name; + } + // 4. First field of any kind + if (table.fields.length > 0) { + return table.fields[0].name; + } + return 'id'; +} + // ============================================================================ // Query key generation // ============================================================================ diff --git a/graphql/codegen/src/core/config/index.ts b/graphql/codegen/src/core/config/index.ts index 239e5581b..99dea07ad 100644 --- a/graphql/codegen/src/core/config/index.ts +++ b/graphql/codegen/src/core/config/index.ts @@ -6,12 +6,11 @@ export { CONFIG_FILENAME, findConfigFile, loadConfigFile, - type LoadConfigFileResult, + type LoadConfigFileResult } from './loader'; - export { - loadAndResolveConfig, - loadWatchConfig, type ConfigOverrideOptions, + loadAndResolveConfig, type LoadConfigResult, + loadWatchConfig } from './resolver'; diff --git a/graphql/codegen/src/core/config/loader.ts b/graphql/codegen/src/core/config/loader.ts index 2a80524c5..668b3adc9 100644 --- a/graphql/codegen/src/core/config/loader.ts +++ b/graphql/codegen/src/core/config/loader.ts @@ -6,6 +6,7 @@ */ import * as fs from 'node:fs'; import * as path from 'node:path'; + import { createJiti } from 'jiti'; export const CONFIG_FILENAME = 'graphql-codegen.config.ts'; @@ -55,7 +56,7 @@ export async function loadConfigFile( if (!fs.existsSync(resolvedPath)) { return { success: false, - error: `Config file not found: ${resolvedPath}`, + error: `Config file not found: ${resolvedPath}` }; } @@ -64,7 +65,7 @@ export async function loadConfigFile( // jiti handles .ts, .js, .mjs, .cjs and ESM/CJS interop const jiti = createJiti(__filename, { interopDefault: true, - debug: process.env.JITI_DEBUG === '1', + debug: process.env.JITI_DEBUG === '1' }); // jiti.import() with { default: true } returns mod?.default ?? mod @@ -73,19 +74,19 @@ export async function loadConfigFile( if (!config || typeof config !== 'object') { return { success: false, - error: 'Config file must export a configuration object', + error: 'Config file must export a configuration object' }; } return { success: true, - config, + config }; } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error'; return { success: false, - error: `Failed to load config file: ${message}`, + error: `Failed to load config file: ${message}` }; } } diff --git a/graphql/codegen/src/core/config/resolver.ts b/graphql/codegen/src/core/config/resolver.ts index 56c551e3b..aa71fe60b 100644 --- a/graphql/codegen/src/core/config/resolver.ts +++ b/graphql/codegen/src/core/config/resolver.ts @@ -6,9 +6,9 @@ */ import type { GraphQLSDKConfig, - GraphQLSDKConfigTarget, + GraphQLSDKConfigTarget } from '../../types/config'; -import { mergeConfig, getConfigOptions } from '../../types/config'; +import { getConfigOptions,mergeConfig } from '../../types/config'; import { findConfigFile, loadConfigFile } from './loader'; /** @@ -47,13 +47,13 @@ export async function loadAndResolveConfig( const sources = [ overrides.endpoint, overrides.schemaFile, - overrides.db, + overrides.db ].filter(Boolean); if (sources.length > 1) { return { success: false, error: - 'Multiple sources specified. Use only one of: endpoint, schemaFile, or db.', + 'Multiple sources specified. Use only one of: endpoint, schemaFile, or db.' }; } @@ -85,13 +85,13 @@ export async function loadAndResolveConfig( return { success: false, error: - 'No source specified. Use --endpoint, --schema-file, or --db, or create a config file with "graphql-codegen init".', + 'No source specified. Use --endpoint, --schema-file, or --db, or create a config file with "graphql-codegen init".' }; } return { success: true, - config: getConfigOptions(mergedConfig), + config: getConfigOptions(mergedConfig) }; } @@ -134,12 +134,12 @@ export async function loadWatchConfig(options: { const watchOverrides: GraphQLSDKConfigTarget = { watch: { ...(options.pollInterval !== undefined && { - pollInterval: options.pollInterval, + pollInterval: options.pollInterval }), ...(options.debounce !== undefined && { debounce: options.debounce }), ...(options.touch !== undefined && { touchFile: options.touch }), - ...(options.clear !== undefined && { clearScreen: options.clear }), - }, + ...(options.clear !== undefined && { clearScreen: options.clear }) + } }; let mergedConfig = mergeConfig(baseConfig, sourceOverrides); diff --git a/graphql/codegen/src/core/custom-ast.ts b/graphql/codegen/src/core/custom-ast.ts index 5d9a1428e..02ea4642b 100644 --- a/graphql/codegen/src/core/custom-ast.ts +++ b/graphql/codegen/src/core/custom-ast.ts @@ -22,7 +22,7 @@ export function getCustomAst(fieldDefn?: MetaField): FieldNode | null { } return t.field({ - name: fieldDefn.name, + name: fieldDefn.name }); } @@ -57,7 +57,7 @@ export function getCustomAstForCleanField(field: CleanField): FieldNode { // Return simple field for scalar types return t.field({ - name, + name }); } @@ -72,7 +72,7 @@ export function requiresSubfieldSelection(field: CleanField): boolean { 'GeometryPoint', 'Interval', 'GeometryGeometryCollection', - 'GeoJSON', + 'GeoJSON' ]; return complexTypes.includes(gqlType); @@ -85,8 +85,8 @@ export function geometryPointAst(name: string): FieldNode { return t.field({ name, selectionSet: t.selectionSet({ - selections: toFieldArray(['x', 'y']), - }), + selections: toFieldArray(['x', 'y']) + }) }); } @@ -101,8 +101,8 @@ export function geometryCollectionAst(name: string): FieldNode { kind: 'NamedType', name: { kind: 'Name', - value: 'GeometryPoint', - }, + value: 'GeometryPoint' + } }, selectionSet: { kind: 'SelectionSet', @@ -111,18 +111,18 @@ export function geometryCollectionAst(name: string): FieldNode { kind: 'Field', name: { kind: 'Name', - value: 'x', - }, + value: 'x' + } }, { kind: 'Field', name: { kind: 'Name', - value: 'y', - }, - }, - ], - }, + value: 'y' + } + } + ] + } }; return t.field({ @@ -133,11 +133,11 @@ export function geometryCollectionAst(name: string): FieldNode { name: 'geometries', selectionSet: t.selectionSet({ // eslint-disable-next-line @typescript-eslint/no-explicit-any - selections: [inlineFragment as any], // gql-ast limitation with inline fragments - }), - }), - ], - }), + selections: [inlineFragment as any] // gql-ast limitation with inline fragments + }) + }) + ] + }) }); } @@ -148,8 +148,8 @@ export function geometryAst(name: string): FieldNode { return t.field({ name, selectionSet: t.selectionSet({ - selections: toFieldArray(['geojson']), - }), + selections: toFieldArray(['geojson']) + }) }); } @@ -166,9 +166,9 @@ export function intervalAst(name: string): FieldNode { 'minutes', 'months', 'seconds', - 'years', - ]), - }), + 'years' + ]) + }) }); } diff --git a/graphql/codegen/src/core/database/index.ts b/graphql/codegen/src/core/database/index.ts index 74c41cb03..0ca12ff14 100644 --- a/graphql/codegen/src/core/database/index.ts +++ b/graphql/codegen/src/core/database/index.ts @@ -6,6 +6,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; + import { buildSchemaSDL } from '@constructive-io/graphql-server'; export interface BuildSchemaFromDatabaseOptions { @@ -47,7 +48,7 @@ export async function buildSchemaFromDatabase( const sdl = await buildSchemaSDL({ database, schemas, - graphile: { pgSettings: async () => ({ role: 'administrator' }) }, + graphile: { pgSettings: async () => ({ role: 'administrator' }) } }); // Write schema to file @@ -74,6 +75,6 @@ export async function buildSchemaSDLFromDatabase(options: { return buildSchemaSDL({ database, schemas, - graphile: { pgSettings: async () => ({ role: 'administrator' }) }, + graphile: { pgSettings: async () => ({ role: 'administrator' }) } }); } diff --git a/graphql/codegen/src/core/generate.ts b/graphql/codegen/src/core/generate.ts index 51e679a2d..e73b34edc 100644 --- a/graphql/codegen/src/core/generate.ts +++ b/graphql/codegen/src/core/generate.ts @@ -6,15 +6,15 @@ */ import path from 'path'; -import { createSchemaSource, validateSourceOptions } from './introspect'; -import { runCodegenPipeline, validateTablesFound } from './pipeline'; +import type { GraphQLSDKConfigTarget } from '../types/config'; +import { getConfigOptions } from '../types/config'; import { generate as generateReactQueryFiles } from './codegen'; import { generateRootBarrel } from './codegen/barrel'; import { generateOrm as generateOrmFiles } from './codegen/orm'; import { generateSharedTypes } from './codegen/shared'; +import { createSchemaSource, validateSourceOptions } from './introspect'; import { writeGeneratedFiles } from './output'; -import type { GraphQLSDKConfigTarget } from '../types/config'; -import { getConfigOptions } from '../types/config'; +import { runCodegenPipeline, validateTablesFound } from './pipeline'; export interface GenerateOptions extends GraphQLSDKConfigTarget { authorization?: string; @@ -44,14 +44,16 @@ export async function generate(options: GenerateOptions = {}): Promise t.name), - filesWritten: allFilesWritten, + filesWritten: allFilesWritten }; } diff --git a/graphql/codegen/src/core/index.ts b/graphql/codegen/src/core/index.ts index 5dbf3eb23..86a01af13 100644 --- a/graphql/codegen/src/core/index.ts +++ b/graphql/codegen/src/core/index.ts @@ -6,8 +6,8 @@ */ // Main generate function (orchestrates the entire pipeline) -export { generate } from './generate'; export type { GenerateOptions, GenerateResult } from './generate'; +export { generate } from './generate'; // Types export * from './types'; @@ -17,10 +17,10 @@ export * from './ast'; export * from './custom-ast'; // Query builder -export { QueryBuilder, MetaObject } from './query-builder'; +export { MetaObject,QueryBuilder } from './query-builder'; // Meta object utilities -export { validateMetaObject, convertFromMetaSchema } from './meta-object'; +export { convertFromMetaSchema,validateMetaObject } from './meta-object'; // Configuration loading and resolution export * from './config'; diff --git a/graphql/codegen/src/core/introspect/fetch-schema.ts b/graphql/codegen/src/core/introspect/fetch-schema.ts index 81cd7b5bb..7cd561125 100644 --- a/graphql/codegen/src/core/introspect/fetch-schema.ts +++ b/graphql/codegen/src/core/introspect/fetch-schema.ts @@ -4,8 +4,9 @@ */ import http from 'node:http'; import https from 'node:https'; -import { SCHEMA_INTROSPECTION_QUERY } from './schema-query'; + import type { IntrospectionQueryResponse } from '../../types/introspection'; +import { SCHEMA_INTROSPECTION_QUERY } from './schema-query'; interface HttpResponse { statusCode: number; @@ -35,7 +36,7 @@ function makeRequest( resolve({ statusCode: res.statusCode || 0, statusMessage: res.statusMessage || '', - data, + data }); }); }); @@ -83,7 +84,7 @@ export async function fetchSchema( const requestHeaders: Record = { 'Content-Type': 'application/json', Accept: 'application/json', - ...headers, + ...headers }; if (authorization) { @@ -92,12 +93,12 @@ export async function fetchSchema( const body = JSON.stringify({ query: SCHEMA_INTROSPECTION_QUERY, - variables: {}, + variables: {} }); const requestOptions: http.RequestOptions = { method: 'POST', - headers: requestHeaders, + headers: requestHeaders }; try { @@ -107,7 +108,7 @@ export async function fetchSchema( return { success: false, error: `HTTP ${response.statusCode}: ${response.statusMessage}`, - statusCode: response.statusCode, + statusCode: response.statusCode }; } @@ -121,7 +122,7 @@ export async function fetchSchema( return { success: false, error: `GraphQL errors: ${errorMessages}`, - statusCode: response.statusCode, + statusCode: response.statusCode }; } @@ -130,21 +131,21 @@ export async function fetchSchema( success: false, error: 'No __schema field in response. Introspection may be disabled on this endpoint.', - statusCode: response.statusCode, + statusCode: response.statusCode }; } return { success: true, data: json.data, - statusCode: response.statusCode, + statusCode: response.statusCode }; } catch (err) { if (err instanceof Error) { if (err.message.includes('timeout')) { return { success: false, - error: `Request timeout after ${timeout}ms`, + error: `Request timeout after ${timeout}ms` }; } @@ -152,31 +153,31 @@ export async function fetchSchema( if (errorCode === 'ECONNREFUSED') { return { success: false, - error: `Connection refused - is the server running at ${endpoint}?`, + error: `Connection refused - is the server running at ${endpoint}?` }; } if (errorCode === 'ENOTFOUND') { return { success: false, - error: `DNS lookup failed for ${url.hostname} - check the endpoint URL`, + error: `DNS lookup failed for ${url.hostname} - check the endpoint URL` }; } if (errorCode === 'ECONNRESET') { return { success: false, - error: `Connection reset by server at ${endpoint}`, + error: `Connection reset by server at ${endpoint}` }; } return { success: false, - error: err.message, + error: err.message }; } return { success: false, - error: 'Unknown error occurred', + error: 'Unknown error occurred' }; } } diff --git a/graphql/codegen/src/core/introspect/index.ts b/graphql/codegen/src/core/introspect/index.ts index baaf7ebc2..5ddf28346 100644 --- a/graphql/codegen/src/core/introspect/index.ts +++ b/graphql/codegen/src/core/introspect/index.ts @@ -3,29 +3,29 @@ */ // Table inference from introspection -export { inferTablesFromIntrospection } from './infer-tables'; export type { InferTablesOptions } from './infer-tables'; +export { inferTablesFromIntrospection } from './infer-tables'; // Pluralization utilities (from inflekt) -export { singularize, pluralize } from 'inflekt'; +export { pluralize,singularize } from 'inflekt'; // Schema sources +export type { + CreateSchemaSourceOptions, + SchemaSource, + SchemaSourceResult +} from './source'; export { createSchemaSource, - validateSourceOptions, EndpointSchemaSource, FileSchemaSource, SchemaSourceError, -} from './source'; -export type { - SchemaSource, - SchemaSourceResult, - CreateSchemaSourceOptions, + validateSourceOptions } from './source'; // Schema fetching (still used by watch mode) -export { fetchSchema } from './fetch-schema'; export type { FetchSchemaOptions, FetchSchemaResult } from './fetch-schema'; +export { fetchSchema } from './fetch-schema'; // Transform utilities (only filterTables, getTableNames, findTable are still useful) -export { getTableNames, findTable, filterTables } from './transform'; +export { filterTables,findTable, getTableNames } from './transform'; diff --git a/graphql/codegen/src/core/introspect/infer-tables.ts b/graphql/codegen/src/core/introspect/infer-tables.ts index 9f5ab631a..ef53092be 100644 --- a/graphql/codegen/src/core/introspect/infer-tables.ts +++ b/graphql/codegen/src/core/introspect/infer-tables.ts @@ -12,27 +12,28 @@ * - Query operations: {pluralName} (list), {singularName} (single) * - Mutation operations: create{Name}, update{Name}, delete{Name} */ +import { lcFirst, pluralize, singularize, ucFirst } from 'inflekt'; + import type { + IntrospectionField, IntrospectionQueryResponse, IntrospectionType, - IntrospectionField, - IntrospectionTypeRef, + IntrospectionTypeRef } from '../../types/introspection'; -import { unwrapType, getBaseTypeName, isList } from '../../types/introspection'; +import { getBaseTypeName, isList,unwrapType } from '../../types/introspection'; import type { - CleanTable, + CleanBelongsToRelation, CleanField, CleanFieldType, - CleanRelations, - CleanBelongsToRelation, CleanHasManyRelation, CleanManyToManyRelation, - TableInflection, - TableQueryNames, - TableConstraints, + CleanRelations, + CleanTable, ConstraintInfo, + TableConstraints, + TableInflection, + TableQueryNames } from '../../types/schema'; -import { singularize, pluralize, lcFirst, ucFirst } from 'inflekt'; // ============================================================================ // Pattern Matching Constants @@ -63,7 +64,7 @@ const PATTERNS = { // Mutation name patterns (camelCase) createMutation: /^create([A-Z][a-zA-Z0-9]*)$/, updateMutation: /^update([A-Z][a-zA-Z0-9]*)$/, - deleteMutation: /^delete([A-Z][a-zA-Z0-9]*)$/, + deleteMutation: /^delete([A-Z][a-zA-Z0-9]*)$/ }; /** @@ -88,7 +89,7 @@ const BUILTIN_TYPES = new Set([ 'Time', 'JSON', 'BigInt', - 'BigFloat', + 'BigFloat' ]); /** @@ -244,7 +245,7 @@ function buildCleanTable( one: queryOps.one ?? lcFirst(entityName), create: mutationOps.create ?? `create${entityName}`, update: mutationOps.update, - delete: mutationOps.delete, + delete: mutationOps.delete }; return { @@ -254,9 +255,9 @@ function buildCleanTable( relations, inflection, query, - constraints, + constraints }, - hasRealOperation, + hasRealOperation }; } @@ -295,7 +296,7 @@ function extractEntityFields( // Include scalar, enum, and other non-relation fields fields.push({ name: field.name, - type: convertToCleanFieldType(field.type), + type: convertToCleanFieldType(field.type) }); } @@ -324,7 +325,7 @@ function convertToCleanFieldType( return { gqlType: baseType.name ?? 'Unknown', - isArray, + isArray // PostgreSQL-specific fields are not available from introspection // They were optional anyway and not used by generators }; @@ -371,7 +372,7 @@ function inferRelations( isUnique: false, // Can't determine from introspection alone referencesTable: baseTypeName, type: baseTypeName, - keys: [], // Would need FK info to populate + keys: [] // Would need FK info to populate }); } } @@ -425,8 +426,8 @@ function inferHasManyOrManyToMany( fieldName: field.name, rightTable: actualEntityName, junctionTable, - type: connectionTypeName, - }, + type: connectionTypeName + } }; } @@ -437,8 +438,8 @@ function inferHasManyOrManyToMany( isUnique: false, referencedByTable: relatedEntityName, type: connectionTypeName, - keys: [], - }, + keys: [] + } }; } @@ -596,9 +597,9 @@ function inferConstraints( fields: [ { name: idField.name, - type: convertToCleanFieldType(idField.type), - }, - ], + type: convertToCleanFieldType(idField.type) + } + ] }); } } @@ -617,9 +618,9 @@ function inferConstraints( fields: [ { name: idField.name, - type: convertToCleanFieldType(idField.type), - }, - ], + type: convertToCleanFieldType(idField.type) + } + ] }); } } @@ -628,7 +629,7 @@ function inferConstraints( return { primaryKey, foreignKey: [], // Would need FK info to populate - unique: [], // Would need constraint info to populate + unique: [] // Would need constraint info to populate }; } @@ -681,7 +682,7 @@ function buildInflection( tableType: entityName, typeName: entityName, updateByPrimaryKey: `update${entityName}`, - updatePayloadType: hasUpdatePayload ? `Update${entityName}Payload` : null, + updatePayloadType: hasUpdatePayload ? `Update${entityName}Payload` : null }; } @@ -710,7 +711,7 @@ function findOrderByType( const candidates = [ `${entityName}sOrderBy`, // Simple 's' plural: User -> UsersOrderBy `${entityName}esOrderBy`, // 'es' plural: Address -> AddressesOrderBy - `${entityName}OrderBy`, // No change (already plural or singular OK) + `${entityName}OrderBy` // No change (already plural or singular OK) ]; // Check each candidate diff --git a/graphql/codegen/src/core/introspect/source/api-schemas.ts b/graphql/codegen/src/core/introspect/source/api-schemas.ts index 4ff79229f..4b81f5280 100644 --- a/graphql/codegen/src/core/introspect/source/api-schemas.ts +++ b/graphql/codegen/src/core/introspect/source/api-schemas.ts @@ -39,7 +39,7 @@ export async function validateServicesSchemas( if (apisCheck.rows.length === 0) { return { valid: false, - error: 'services_public.apis table not found. The database must have the services schema deployed.', + error: 'services_public.apis table not found. The database must have the services schema deployed.' }; } @@ -52,7 +52,7 @@ export async function validateServicesSchemas( if (apiSchemasCheck.rows.length === 0) { return { valid: false, - error: 'services_public.api_schemas table not found. The database must have the services schema deployed.', + error: 'services_public.api_schemas table not found. The database must have the services schema deployed.' }; } @@ -65,7 +65,7 @@ export async function validateServicesSchemas( if (metaschemaCheck.rows.length === 0) { return { valid: false, - error: 'metaschema_public.schema table not found. The database must have the metaschema deployed.', + error: 'metaschema_public.schema table not found. The database must have the metaschema deployed.' }; } @@ -73,7 +73,7 @@ export async function validateServicesSchemas( } catch (err) { return { valid: false, - error: `Failed to validate services schemas: ${err instanceof Error ? err.message : 'Unknown error'}`, + error: `Failed to validate services schemas: ${err instanceof Error ? err.message : 'Unknown error'}` }; } } @@ -142,7 +142,7 @@ export function createDatabasePool(database: string): Pool { port: parseInt(url.port || '5432', 10), user: url.username, password: url.password, - database: dbName, + database: dbName }); } diff --git a/graphql/codegen/src/core/introspect/source/database.ts b/graphql/codegen/src/core/introspect/source/database.ts index 4b77ea12e..55dfb0355 100644 --- a/graphql/codegen/src/core/introspect/source/database.ts +++ b/graphql/codegen/src/core/introspect/source/database.ts @@ -5,11 +5,12 @@ * introspection and converts it to introspection format. */ import { buildSchema, introspectionFromSchema } from 'graphql'; -import type { SchemaSource, SchemaSourceResult } from './types'; -import { SchemaSourceError } from './types'; + import type { IntrospectionQueryResponse } from '../../../types/introspection'; import { buildSchemaSDLFromDatabase } from '../../database'; import { createDatabasePool, resolveApiSchemas, validateServicesSchemas } from './api-schemas'; +import type { SchemaSource, SchemaSourceResult } from './types'; +import { SchemaSourceError } from './types'; export interface DatabaseSchemaSourceOptions { /** @@ -77,7 +78,7 @@ export class DatabaseSchemaSource implements SchemaSource { try { sdl = await buildSchemaSDLFromDatabase({ database, - schemas, + schemas }); } catch (err) { throw new SchemaSourceError( diff --git a/graphql/codegen/src/core/introspect/source/endpoint.ts b/graphql/codegen/src/core/introspect/source/endpoint.ts index a5c50ff84..20ec8d931 100644 --- a/graphql/codegen/src/core/introspect/source/endpoint.ts +++ b/graphql/codegen/src/core/introspect/source/endpoint.ts @@ -4,9 +4,9 @@ * Fetches GraphQL schema via introspection from a live endpoint. * Wraps the existing fetchSchema() function with the SchemaSource interface. */ +import { fetchSchema } from '../fetch-schema'; import type { SchemaSource, SchemaSourceResult } from './types'; import { SchemaSourceError } from './types'; -import { fetchSchema } from '../fetch-schema'; export interface EndpointSchemaSourceOptions { /** @@ -45,7 +45,7 @@ export class EndpointSchemaSource implements SchemaSource { endpoint: this.options.endpoint, authorization: this.options.authorization, headers: this.options.headers, - timeout: this.options.timeout, + timeout: this.options.timeout }); if (!result.success) { @@ -63,7 +63,7 @@ export class EndpointSchemaSource implements SchemaSource { } return { - introspection: result.data, + introspection: result.data }; } diff --git a/graphql/codegen/src/core/introspect/source/file.ts b/graphql/codegen/src/core/introspect/source/file.ts index 6ab828aac..58ffe6b33 100644 --- a/graphql/codegen/src/core/introspect/source/file.ts +++ b/graphql/codegen/src/core/introspect/source/file.ts @@ -6,10 +6,12 @@ */ import * as fs from 'node:fs'; import * as path from 'node:path'; + import { buildSchema, introspectionFromSchema } from 'graphql'; + +import type { IntrospectionQueryResponse } from '../../../types/introspection'; import type { SchemaSource, SchemaSourceResult } from './types'; import { SchemaSourceError } from './types'; -import type { IntrospectionQueryResponse } from '../../../types/introspection'; export interface FileSchemaSourceOptions { /** diff --git a/graphql/codegen/src/core/introspect/source/index.ts b/graphql/codegen/src/core/introspect/source/index.ts index 9642201c0..c96b498be 100644 --- a/graphql/codegen/src/core/introspect/source/index.ts +++ b/graphql/codegen/src/core/introspect/source/index.ts @@ -7,23 +7,21 @@ * - PostgreSQL databases (via PostGraphile introspection) * - PGPM modules (via ephemeral database deployment) */ -export * from './types'; +export * from './api-schemas'; +export * from './database'; export * from './endpoint'; export * from './file'; -export * from './database'; export * from './pgpm-module'; -export * from './api-schemas'; +export * from './types'; -import type { SchemaSource } from './types'; -import type { DbConfig, PgpmConfig } from '../../../types/config'; +import type { DbConfig } from '../../../types/config'; +import { DatabaseSchemaSource } from './database'; import { EndpointSchemaSource } from './endpoint'; import { FileSchemaSource } from './file'; -import { DatabaseSchemaSource } from './database'; import { - PgpmModuleSchemaSource, - isPgpmModulePathOptions, - isPgpmWorkspaceOptions, + PgpmModuleSchemaSource } from './pgpm-module'; +import type { SchemaSource } from './types'; /** * Options for endpoint-based schema source @@ -142,49 +140,49 @@ export function createSchemaSource( const mode = detectSourceMode(options); switch (mode) { - case 'schemaFile': - return new FileSchemaSource({ - schemaPath: options.schemaFile!, - }); + case 'schemaFile': + return new FileSchemaSource({ + schemaPath: options.schemaFile! + }); - case 'endpoint': - return new EndpointSchemaSource({ - endpoint: options.endpoint!, - authorization: options.authorization, - headers: options.headers, - timeout: options.timeout, - }); + case 'endpoint': + return new EndpointSchemaSource({ + endpoint: options.endpoint!, + authorization: options.authorization, + headers: options.headers, + timeout: options.timeout + }); - case 'database': - // Database mode uses db.config for connection (falls back to env vars) - // and db.schemas or db.apiNames for schema selection - return new DatabaseSchemaSource({ - database: options.db?.config?.database ?? '', - schemas: options.db?.schemas, - apiNames: options.db?.apiNames, - }); + case 'database': + // Database mode uses db.config for connection (falls back to env vars) + // and db.schemas or db.apiNames for schema selection + return new DatabaseSchemaSource({ + database: options.db?.config?.database ?? '', + schemas: options.db?.schemas, + apiNames: options.db?.apiNames + }); - case 'pgpm-module': - return new PgpmModuleSchemaSource({ - pgpmModulePath: options.db!.pgpm!.modulePath!, - schemas: options.db?.schemas, - apiNames: options.db?.apiNames, - keepDb: options.db?.keepDb, - }); + case 'pgpm-module': + return new PgpmModuleSchemaSource({ + pgpmModulePath: options.db!.pgpm!.modulePath!, + schemas: options.db?.schemas, + apiNames: options.db?.apiNames, + keepDb: options.db?.keepDb + }); - case 'pgpm-workspace': - return new PgpmModuleSchemaSource({ - pgpmWorkspacePath: options.db!.pgpm!.workspacePath!, - pgpmModuleName: options.db!.pgpm!.moduleName!, - schemas: options.db?.schemas, - apiNames: options.db?.apiNames, - keepDb: options.db?.keepDb, - }); + case 'pgpm-workspace': + return new PgpmModuleSchemaSource({ + pgpmWorkspacePath: options.db!.pgpm!.workspacePath!, + pgpmModuleName: options.db!.pgpm!.moduleName!, + schemas: options.db?.schemas, + apiNames: options.db?.apiNames, + keepDb: options.db?.keepDb + }); - default: - throw new Error( - 'No source specified. Use one of: endpoint, schemaFile, or db (with optional pgpm for module deployment).' - ); + default: + throw new Error( + 'No source specified. Use one of: endpoint, schemaFile, or db (with optional pgpm for module deployment).' + ); } } @@ -199,14 +197,14 @@ export function validateSourceOptions(options: CreateSchemaSourceOptions): { const sources = [ options.endpoint, options.schemaFile, - options.db, + options.db ].filter(Boolean); if (sources.length === 0) { return { valid: false, error: - 'No source specified. Use one of: endpoint, schemaFile, or db.', + 'No source specified. Use one of: endpoint, schemaFile, or db.' }; } @@ -214,7 +212,7 @@ export function validateSourceOptions(options: CreateSchemaSourceOptions): { return { valid: false, error: - 'Multiple sources specified. Use only one of: endpoint, schemaFile, or db.', + 'Multiple sources specified. Use only one of: endpoint, schemaFile, or db.' }; } @@ -224,14 +222,14 @@ export function validateSourceOptions(options: CreateSchemaSourceOptions): { if (pgpm.workspacePath && !pgpm.moduleName) { return { valid: false, - error: 'db.pgpm.workspacePath requires db.pgpm.moduleName to be specified.', + error: 'db.pgpm.workspacePath requires db.pgpm.moduleName to be specified.' }; } if (pgpm.moduleName && !pgpm.workspacePath) { return { valid: false, - error: 'db.pgpm.moduleName requires db.pgpm.workspacePath to be specified.', + error: 'db.pgpm.moduleName requires db.pgpm.workspacePath to be specified.' }; } @@ -239,7 +237,7 @@ export function validateSourceOptions(options: CreateSchemaSourceOptions): { if (!pgpm.modulePath && !(pgpm.workspacePath && pgpm.moduleName)) { return { valid: false, - error: 'db.pgpm requires either modulePath or both workspacePath and moduleName.', + error: 'db.pgpm requires either modulePath or both workspacePath and moduleName.' }; } } @@ -252,14 +250,14 @@ export function validateSourceOptions(options: CreateSchemaSourceOptions): { if (hasSchemas && hasApiNames) { return { valid: false, - error: 'Cannot specify both db.schemas and db.apiNames. Use one or the other.', + error: 'Cannot specify both db.schemas and db.apiNames. Use one or the other.' }; } if (!hasSchemas && !hasApiNames) { return { valid: false, - error: 'Must specify either db.schemas or db.apiNames for database mode.', + error: 'Must specify either db.schemas or db.apiNames for database mode.' }; } } diff --git a/graphql/codegen/src/core/introspect/source/pgpm-module.ts b/graphql/codegen/src/core/introspect/source/pgpm-module.ts index a246d769f..4bbc7e14e 100644 --- a/graphql/codegen/src/core/introspect/source/pgpm-module.ts +++ b/graphql/codegen/src/core/introspect/source/pgpm-module.ts @@ -7,17 +7,17 @@ * 3. Introspecting the database with PostGraphile * 4. Cleaning up the ephemeral database (unless keepDb is true) */ -import { buildSchema, introspectionFromSchema } from 'graphql'; import { PgpmPackage } from '@pgpmjs/core'; +import { buildSchema, introspectionFromSchema } from 'graphql'; +import { getPgPool } from 'pg-cache'; import { createEphemeralDb, type EphemeralDbResult } from 'pgsql-client'; import { deployPgpm } from 'pgsql-seed'; -import { getPgPool } from 'pg-cache'; -import type { SchemaSource, SchemaSourceResult } from './types'; -import { SchemaSourceError } from './types'; import type { IntrospectionQueryResponse } from '../../../types/introspection'; import { buildSchemaSDLFromDatabase } from '../../database'; import { resolveApiSchemas, validateServicesSchemas } from './api-schemas'; +import type { SchemaSource, SchemaSourceResult } from './types'; +import { SchemaSourceError } from './types'; /** * Options for PGPM module schema source using direct module path @@ -147,7 +147,7 @@ export class PgpmModuleSchemaSource implements SchemaSource { try { this.ephemeralDb = createEphemeralDb({ prefix: 'codegen_pgpm_', - verbose: false, + verbose: false }); } catch (err) { throw new SchemaSourceError( @@ -199,7 +199,7 @@ export class PgpmModuleSchemaSource implements SchemaSource { try { sdl = await buildSchemaSDLFromDatabase({ database: dbConfig.database, - schemas, + schemas }); } catch (err) { throw new SchemaSourceError( diff --git a/graphql/codegen/src/core/introspect/transform-schema.ts b/graphql/codegen/src/core/introspect/transform-schema.ts index 40cc4aa9f..a7edd5216 100644 --- a/graphql/codegen/src/core/introspect/transform-schema.ts +++ b/graphql/codegen/src/core/introspect/transform-schema.ts @@ -5,24 +5,24 @@ * format used by code generators. */ import type { - IntrospectionQueryResponse, - IntrospectionType, IntrospectionField, - IntrospectionTypeRef, IntrospectionInputValue, + IntrospectionQueryResponse, + IntrospectionType, + IntrospectionTypeRef } from '../../types/introspection'; import { - unwrapType, getBaseTypeName, isNonNull, + unwrapType } from '../../types/introspection'; import type { - CleanOperation, CleanArgument, - CleanTypeRef, CleanObjectField, - TypeRegistry, + CleanOperation, + CleanTypeRef, ResolvedType, + TypeRegistry } from '../../types/schema'; // ============================================================================ @@ -48,7 +48,7 @@ export function buildTypeRegistry(types: IntrospectionType[]): TypeRegistry { const resolvedType: ResolvedType = { kind: type.kind as ResolvedType['kind'], name: type.name, - description: type.description ?? undefined, + description: type.description ?? undefined }; // Resolve enum values for ENUM types (no circular refs possible) @@ -99,7 +99,7 @@ function transformFieldToCleanObjectFieldShallow( return { name: field.name, type: transformTypeRefShallow(field.type), - description: field.description ?? undefined, + description: field.description ?? undefined }; } @@ -113,7 +113,7 @@ function transformInputValueToCleanArgumentShallow( name: inputValue.name, type: transformTypeRefShallow(inputValue.type), defaultValue: inputValue.defaultValue ?? undefined, - description: inputValue.description ?? undefined, + description: inputValue.description ?? undefined }; } @@ -124,7 +124,7 @@ function transformInputValueToCleanArgumentShallow( function transformTypeRefShallow(typeRef: IntrospectionTypeRef): CleanTypeRef { const cleanRef: CleanTypeRef = { kind: typeRef.kind as CleanTypeRef['kind'], - name: typeRef.name, + name: typeRef.name }; if (typeRef.ofType) { @@ -165,15 +165,15 @@ export function transformSchemaToOperations( // Transform queries const queries: CleanOperation[] = queryTypeDef?.fields ? queryTypeDef.fields.map((field) => - transformFieldToCleanOperation(field, 'query', types) - ) + transformFieldToCleanOperation(field, 'query', types) + ) : []; // Transform mutations const mutations: CleanOperation[] = mutationTypeDef?.fields ? mutationTypeDef.fields.map((field) => - transformFieldToCleanOperation(field, 'mutation', types) - ) + transformFieldToCleanOperation(field, 'mutation', types) + ) : []; return { queries, mutations, typeRegistry }; @@ -200,7 +200,7 @@ function transformFieldToCleanOperation( returnType: transformTypeRefToCleanTypeRef(field.type, types), description: field.description ?? undefined, isDeprecated: field.isDeprecated, - deprecationReason: field.deprecationReason ?? undefined, + deprecationReason: field.deprecationReason ?? undefined }; } @@ -215,7 +215,7 @@ function transformInputValueToCleanArgument( name: inputValue.name, type: transformTypeRefToCleanTypeRef(inputValue.type, types), defaultValue: inputValue.defaultValue ?? undefined, - description: inputValue.description ?? undefined, + description: inputValue.description ?? undefined }; } @@ -237,7 +237,7 @@ function transformTypeRefToCleanTypeRef( ): CleanTypeRef { const cleanRef: CleanTypeRef = { kind: typeRef.kind as CleanTypeRef['kind'], - name: typeRef.name, + name: typeRef.name }; // Recursively transform ofType for wrappers (LIST, NON_NULL) @@ -400,4 +400,4 @@ export function getCustomOperations( } // Re-export utility functions from introspection types -export { unwrapType, getBaseTypeName, isNonNull }; +export { getBaseTypeName, isNonNull,unwrapType }; diff --git a/graphql/codegen/src/core/meta-object/convert.ts b/graphql/codegen/src/core/meta-object/convert.ts index 02bff2fe4..295ae7841 100644 --- a/graphql/codegen/src/core/meta-object/convert.ts +++ b/graphql/codegen/src/core/meta-object/convert.ts @@ -78,11 +78,11 @@ export function convertFromMetaSchema( metaSchema: MetaSchemaInput ): ConvertedMetaObject { const { - _meta: { tables }, + _meta: { tables } } = metaSchema; const result: ConvertedMetaObject = { - tables: [], + tables: [] }; for (const table of tables) { @@ -94,7 +94,7 @@ export function convertFromMetaSchema( foreignConstraints: pickForeignConstraint( table.foreignKeyConstraints, table.relations - ), + ) }); } @@ -136,7 +136,7 @@ function pickForeignConstraint( return { refTable: refTable.name, fromKey, - toKey, + toKey }; }); } @@ -144,13 +144,13 @@ function pickForeignConstraint( function pickField(field: MetaSchemaField): ConvertedField { return { name: field.name, - type: field.type, + type: field.type }; } function pickConstraintField(field: MetaSchemaField): ConvertedConstraint { return { name: field.name, - type: field.type, + type: field.type }; } diff --git a/graphql/codegen/src/core/meta-object/validate.ts b/graphql/codegen/src/core/meta-object/validate.ts index 1481e6b5d..eab7a346b 100644 --- a/graphql/codegen/src/core/meta-object/validate.ts +++ b/graphql/codegen/src/core/meta-object/validate.ts @@ -18,7 +18,7 @@ function getValidator() { return { ajv: cachedAjv, - validator: cachedValidator!, + validator: cachedValidator! }; } @@ -36,6 +36,6 @@ export function validateMetaObject( return { errors: validator.errors, - message: ajv.errorsText(validator.errors, { separator: '\n' }), + message: ajv.errorsText(validator.errors, { separator: '\n' }) }; } diff --git a/graphql/codegen/src/core/output/index.ts b/graphql/codegen/src/core/output/index.ts index 3d6344d9c..529f10e41 100644 --- a/graphql/codegen/src/core/output/index.ts +++ b/graphql/codegen/src/core/output/index.ts @@ -3,9 +3,9 @@ */ export { - writeGeneratedFiles, formatOutput, type GeneratedFile, - type WriteResult, + writeGeneratedFiles, type WriteOptions, + type WriteResult } from './writer'; diff --git a/graphql/codegen/src/core/output/writer.ts b/graphql/codegen/src/core/output/writer.ts index 6db930669..8b04f1613 100644 --- a/graphql/codegen/src/core/output/writer.ts +++ b/graphql/codegen/src/core/output/writer.ts @@ -62,7 +62,7 @@ async function formatFileContent( singleQuote: true, trailingComma: 'es5', tabWidth: 2, - semi: true, + semi: true }); return result.code; } catch { @@ -98,7 +98,7 @@ export async function writeGeneratedFiles( const message = err instanceof Error ? err.message : 'Unknown error'; return { success: false, - errors: [`Failed to create output directory: ${message}`], + errors: [`Failed to create output directory: ${message}`] }; } @@ -171,7 +171,7 @@ export async function writeGeneratedFiles( return { success: errors.length === 0, filesWritten: written, - errors: errors.length > 0 ? errors : undefined, + errors: errors.length > 0 ? errors : undefined }; } @@ -211,7 +211,7 @@ export async function formatOutput( if (!formatFn) { return { success: false, - error: 'oxfmt not available. Install it with: npm install oxfmt', + error: 'oxfmt not available. Install it with: npm install oxfmt' }; } diff --git a/graphql/codegen/src/core/pipeline/index.ts b/graphql/codegen/src/core/pipeline/index.ts index 65aba4e21..f2d33456d 100644 --- a/graphql/codegen/src/core/pipeline/index.ts +++ b/graphql/codegen/src/core/pipeline/index.ts @@ -10,18 +10,18 @@ */ import type { GraphQLSDKConfigTarget } from '../../types/config'; import type { - CleanTable, CleanOperation, - TypeRegistry, + CleanTable, + TypeRegistry } from '../../types/schema'; -import type { SchemaSource } from '../introspect/source'; import { inferTablesFromIntrospection } from '../introspect/infer-tables'; +import type { SchemaSource } from '../introspect/source'; import { filterTables } from '../introspect/transform'; import { - transformSchemaToOperations, filterOperations, - getTableOperationNames, getCustomOperations, + getTableOperationNames, + transformSchemaToOperations } from '../introspect/transform-schema'; // Re-export for convenience @@ -103,7 +103,7 @@ export async function runCodegenPipeline( source, config, verbose = false, - skipCustomOperations = false, + skipCustomOperations = false } = options; const log = verbose ? console.log : () => {}; @@ -120,7 +120,7 @@ export async function runCodegenPipeline( // 3. Filter tables by config (combine exclude and systemExclude) tables = filterTables(tables, config.tables.include, [ ...config.tables.exclude, - ...config.tables.systemExclude, + ...config.tables.systemExclude ]); const filteredTables = tables.length; log(` After filtering: ${filteredTables} tables`); @@ -130,7 +130,7 @@ export async function runCodegenPipeline( const { queries: allQueries, mutations: allMutations, - typeRegistry, + typeRegistry } = transformSchemaToOperations(introspection); const totalQueries = allQueries.length; @@ -178,7 +178,7 @@ export async function runCodegenPipeline( customOperations: { queries: customQueries, mutations: customMutations, - typeRegistry, + typeRegistry }, stats: { totalTables, @@ -186,8 +186,8 @@ export async function runCodegenPipeline( totalQueries, totalMutations, customQueries: customQueries.length, - customMutations: customMutations.length, - }, + customMutations: customMutations.length + } }; } @@ -206,7 +206,7 @@ export function validateTablesFound(tables: CleanTable[]): { return { valid: false, error: - 'No tables found after filtering. Check your include/exclude patterns.', + 'No tables found after filtering. Check your include/exclude patterns.' }; } return { valid: true }; diff --git a/graphql/codegen/src/core/query-builder.ts b/graphql/codegen/src/core/query-builder.ts index 8be4db4ca..403ed2e3b 100644 --- a/graphql/codegen/src/core/query-builder.ts +++ b/graphql/codegen/src/core/query-builder.ts @@ -1,5 +1,5 @@ import { DocumentNode, print as gqlPrint } from 'graphql'; -import { camelize, underscore, pluralize } from 'inflekt'; +import { camelize, pluralize,underscore } from 'inflekt'; import { createOne, @@ -8,11 +8,10 @@ import { getCount, getMany, getOne, - patchOne, + patchOne } from './ast'; import { validateMetaObject } from './meta-object'; import type { - QueryFieldSelection, IntrospectionSchema, MetaObject, MetaTable, @@ -20,7 +19,8 @@ import type { QueryBuilderOptions, QueryBuilderResult, QueryDefinition, - QuerySelectionOptions, + QueryFieldSelection, + QuerySelectionOptions } from './types'; export * as MetaObject from './meta-object'; @@ -46,7 +46,7 @@ export class QueryBuilder { constructor({ meta = {} as MetaObject, - introspection, + introspection }: QueryBuilderOptions) { this._introspection = introspection; this._meta = meta; @@ -144,17 +144,17 @@ export class QueryBuilder { // We only need deleteAction from all of [deleteAction, deleteActionBySlug, deleteActionByName] const getInputName = (mutationType: string): string => { switch (mutationType) { - case 'delete': { - return `Delete${camelize(this._model)}Input`; - } - case 'create': { - return `Create${camelize(this._model)}Input`; - } - case 'patch': { - return `Update${camelize(this._model)}Input`; - } - default: - throw new Error('Unhandled mutation type' + mutationType); + case 'delete': { + return `Delete${camelize(this._model)}Input`; + } + case 'create': { + return `Create${camelize(this._model)}Input`; + } + case 'patch': { + return `Update${camelize(this._model)}Input`; + } + default: + throw new Error('Unhandled mutation type' + mutationType); } }; @@ -208,7 +208,7 @@ export class QueryBuilder { queryName: this._queryName, operationName: this._key, query: defn, - selection: this._select, + selection: this._select }); return this; @@ -232,7 +232,7 @@ export class QueryBuilder { queryName: this._queryName, operationName: this._key, query: defn, - selection: this._select, + selection: this._select }); return this; @@ -254,7 +254,7 @@ export class QueryBuilder { this._ast = getCount({ queryName: this._queryName, operationName: this._key, - query: defn, + query: defn }); return this; @@ -278,7 +278,7 @@ export class QueryBuilder { queryName: this._queryName, operationName: this._key, query: defn, - selection: this._select, + selection: this._select }); return this; @@ -302,7 +302,7 @@ export class QueryBuilder { operationName: this._key, mutationName: this._queryName, mutation: defn, - selection: this._select, + selection: this._select }); return this; @@ -326,7 +326,7 @@ export class QueryBuilder { this._ast = deleteOne({ operationName: this._key, mutationName: this._queryName, - mutation: defn, + mutation: defn }); return this; @@ -351,7 +351,7 @@ export class QueryBuilder { operationName: this._key, mutationName: this._queryName, mutation: defn, - selection: this._select, + selection: this._select }); return this; @@ -370,7 +370,7 @@ export class QueryBuilder { return { _hash, _queryName: this._queryName, - _ast: this._ast, + _ast: this._ast }; } @@ -421,7 +421,7 @@ function pickScalarFields( .map((fieldName) => ({ name: fieldName, isObject: false, - fieldDefn: modelMeta.fields.find((f) => f.name === fieldName), + fieldDefn: modelMeta.fields.find((f) => f.name === fieldName) })); // This is for inferring the sub-selection of a mutation query @@ -498,7 +498,7 @@ function pickAllFields( isObject: true, isBelongTo, selection: subFields.map((name) => ({ name, isObject: false })), - variables: selectOptions.variables as QueryFieldSelection['variables'], + variables: selectOptions.variables as QueryFieldSelection['variables'] }; // Need to further expand selection of object fields, @@ -529,8 +529,8 @@ function pickAllFields( { name: fieldName, isObject: false, - fieldDefn: modelMeta.fields.find((f) => f.name === fieldName), - }, + fieldDefn: modelMeta.fields.find((f) => f.name === fieldName) + } ]; } } diff --git a/graphql/codegen/src/core/watch/cache.ts b/graphql/codegen/src/core/watch/cache.ts index f92764d02..2df07bc6d 100644 --- a/graphql/codegen/src/core/watch/cache.ts +++ b/graphql/codegen/src/core/watch/cache.ts @@ -7,6 +7,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; + import type { IntrospectionQueryResponse } from '../../types/introspection'; import { hashObject } from './hash'; diff --git a/graphql/codegen/src/core/watch/index.ts b/graphql/codegen/src/core/watch/index.ts index 6e220682f..737f372d6 100644 --- a/graphql/codegen/src/core/watch/index.ts +++ b/graphql/codegen/src/core/watch/index.ts @@ -2,17 +2,17 @@ * Watch mode module exports */ -export { SchemaPoller, computeSchemaHash } from './poller'; export { SchemaCache, touchFile } from './cache'; -export { sha256, hashObject, combineHashes } from './hash'; export { debounce, debounceAsync } from './debounce'; -export { WatchOrchestrator, startWatch } from './orchestrator'; +export { combineHashes,hashObject, sha256 } from './hash'; +export type { WatchOrchestratorOptions, WatchStatus } from './orchestrator'; +export { startWatch,WatchOrchestrator } from './orchestrator'; +export { computeSchemaHash,SchemaPoller } from './poller'; export type { - PollResult, - WatchOptions, - PollEventType, - PollEventHandler, - PollEvent, GeneratorType, + PollEvent, + PollEventHandler, + PollEventType, + PollResult, + WatchOptions } from './types'; -export type { WatchOrchestratorOptions, WatchStatus } from './orchestrator'; diff --git a/graphql/codegen/src/core/watch/orchestrator.ts b/graphql/codegen/src/core/watch/orchestrator.ts index c530c113e..dc986a441 100644 --- a/graphql/codegen/src/core/watch/orchestrator.ts +++ b/graphql/codegen/src/core/watch/orchestrator.ts @@ -5,9 +5,9 @@ */ import type { GraphQLSDKConfigTarget } from '../../types/config'; -import type { GeneratorType, WatchOptions, PollEvent } from './types'; -import { SchemaPoller } from './poller'; import { debounce } from './debounce'; +import { SchemaPoller } from './poller'; +import type { GeneratorType, PollEvent,WatchOptions } from './types'; // These will be injected by the CLI layer to avoid circular dependencies // The watch orchestrator doesn't need to know about the full generate commands @@ -82,7 +82,7 @@ export class WatchOrchestrator { lastPollTime: null, lastRegenTime: null, lastError: null, - currentHash: null, + currentHash: null }; // Create debounced regenerate function @@ -105,7 +105,7 @@ export class WatchOrchestrator { debounce: config.watch.debounce, touchFile: config.watch.touchFile, clearScreen: config.watch.clearScreen, - verbose, + verbose }; } @@ -217,18 +217,18 @@ export class WatchOrchestrator { let outputDir: string | undefined; switch (this.options.generatorType) { - case 'react-query': - generateFn = this.options.generateReactQuery; - // React Query hooks go to {output}/hooks - outputDir = this.options.outputDir ?? `${this.options.config.output}/hooks`; - break; - case 'orm': - generateFn = this.options.generateOrm; - // ORM client goes to {output}/orm - outputDir = this.options.outputDir ?? `${this.options.config.output}/orm`; - break; - default: - throw new Error(`Unknown generator type: ${this.options.generatorType}`); + case 'react-query': + generateFn = this.options.generateReactQuery; + // React Query hooks go to {output}/hooks + outputDir = this.options.outputDir ?? `${this.options.config.output}/hooks`; + break; + case 'orm': + generateFn = this.options.generateOrm; + // ORM client goes to {output}/orm + outputDir = this.options.outputDir ?? `${this.options.config.output}/orm`; + break; + default: + throw new Error(`Unknown generator type: ${this.options.generatorType}`); } const result = await generateFn({ @@ -238,7 +238,7 @@ export class WatchOrchestrator { output: outputDir, authorization: this.options.authorization, verbose: this.watchOptions.verbose, - skipCustomOperations: this.options.skipCustomOperations, + skipCustomOperations: this.options.skipCustomOperations }); const duration = Date.now() - startTime; @@ -287,14 +287,14 @@ export class WatchOrchestrator { private logHeader(): void { let generatorName: string; switch (this.options.generatorType) { - case 'react-query': - generatorName = 'React Query hooks'; - break; - case 'orm': - generatorName = 'ORM client'; - break; - default: - throw new Error(`Unknown generator type: ${this.options.generatorType}`); + case 'react-query': + generatorName = 'React Query hooks'; + break; + case 'orm': + generatorName = 'ORM client'; + break; + default: + throw new Error(`Unknown generator type: ${this.options.generatorType}`); } console.log(`\n${'─'.repeat(50)}`); console.log(`graphql-codegen watch mode (${generatorName})`); diff --git a/graphql/codegen/src/core/watch/poller.ts b/graphql/codegen/src/core/watch/poller.ts index c60583f85..78e2937ac 100644 --- a/graphql/codegen/src/core/watch/poller.ts +++ b/graphql/codegen/src/core/watch/poller.ts @@ -6,16 +6,17 @@ */ import { EventEmitter } from 'node:events'; + import type { IntrospectionQueryResponse } from '../../types/introspection'; import { fetchSchema } from '../introspect/fetch-schema'; import { SchemaCache, touchFile } from './cache'; +import { hashObject } from './hash'; import type { - PollResult, PollEvent, PollEventType, - WatchOptions, + PollResult, + WatchOptions } from './types'; -import { hashObject } from './hash'; /** * Schema poller that periodically introspects a GraphQL endpoint @@ -73,7 +74,7 @@ export class SchemaPoller extends EventEmitter { return { success: false, changed: false, - error: 'Poll already in progress', + error: 'Poll already in progress' }; } @@ -87,7 +88,7 @@ export class SchemaPoller extends EventEmitter { endpoint: this.options.endpoint, authorization: this.options.authorization, headers: this.options.headers, - timeout: 30000, + timeout: 30000 }); const duration = Date.now() - startTime; @@ -156,7 +157,7 @@ export class SchemaPoller extends EventEmitter { endpoint: this.options.endpoint, authorization: this.options.authorization, headers: this.options.headers, - timeout: 30000, + timeout: 30000 }); if (schemaResult.success) { @@ -218,7 +219,7 @@ export class SchemaPoller extends EventEmitter { return { type, timestamp: Date.now(), - ...extra, + ...extra }; } } diff --git a/graphql/codegen/src/generators/field-selector.ts b/graphql/codegen/src/generators/field-selector.ts index 8e42e5c9d..43ce4851c 100644 --- a/graphql/codegen/src/generators/field-selector.ts +++ b/graphql/codegen/src/generators/field-selector.ts @@ -7,7 +7,7 @@ import type { CleanTable } from '../types/schema'; import type { FieldSelection, FieldSelectionPreset, - SimpleFieldSelection, + SimpleFieldSelection } from '../types/selection'; /** @@ -39,47 +39,47 @@ function convertPresetToSelection( const options: QuerySelectionOptions = {}; switch (preset) { - case 'minimal': { - // Just id and first display field - const minimalFields = getMinimalFields(table); - minimalFields.forEach((field) => { - options[field] = true; - }); - break; - } + case 'minimal': { + // Just id and first display field + const minimalFields = getMinimalFields(table); + minimalFields.forEach((field) => { + options[field] = true; + }); + break; + } - case 'display': { - // Common display fields - const displayFields = getDisplayFields(table); - displayFields.forEach((field) => { - options[field] = true; - }); - break; - } + case 'display': { + // Common display fields + const displayFields = getDisplayFields(table); + displayFields.forEach((field) => { + options[field] = true; + }); + break; + } - case 'all': { - // All non-relational fields (includes complex fields like JSON, geometry, etc.) - const allFields = getNonRelationalFields(table); - allFields.forEach((field) => { - options[field] = true; - }); - break; - } + case 'all': { + // All non-relational fields (includes complex fields like JSON, geometry, etc.) + const allFields = getNonRelationalFields(table); + allFields.forEach((field) => { + options[field] = true; + }); + break; + } - case 'full': - // All fields including basic relations - table.fields.forEach((field) => { - options[field.name] = true; - }); - break; + case 'full': + // All fields including basic relations + table.fields.forEach((field) => { + options[field.name] = true; + }); + break; - default: { - // Default to display - const defaultFields = getDisplayFields(table); - defaultFields.forEach((field) => { - options[field] = true; - }); - } + default: { + // Default to display + const defaultFields = getDisplayFields(table); + defaultFields.forEach((field) => { + options[field] = true; + }); + } } return options; @@ -118,7 +118,7 @@ function convertCustomSelectionToOptions( // Include with dynamically determined scalar fields from the related table options[relationField] = { select: getRelatedTableScalarFields(relationField, table, allTables), - variables: {}, + variables: {} }; } }); @@ -137,7 +137,7 @@ function convertCustomSelectionToOptions( table, allTables ), - variables: {}, + variables: {} }; } else if (Array.isArray(relationSelection)) { // Include with specific fields @@ -147,7 +147,7 @@ function convertCustomSelectionToOptions( }); options[relationField] = { select: selectObj, - variables: {}, + variables: {} }; } } @@ -305,7 +305,7 @@ function getRelatedTableScalarFields( 'slug', 'code', 'createdAt', - 'updatedAt', + 'updatedAt' ]; const included: string[] = []; @@ -351,7 +351,7 @@ export function getAvailableRelations( relations.push({ fieldName: rel.fieldName, type: 'belongsTo', - referencedTable: rel.referencesTable || undefined, + referencedTable: rel.referencesTable || undefined }); } }); @@ -362,7 +362,7 @@ export function getAvailableRelations( relations.push({ fieldName: rel.fieldName, type: 'hasOne', - referencedTable: rel.referencedByTable || undefined, + referencedTable: rel.referencedByTable || undefined }); } }); @@ -373,7 +373,7 @@ export function getAvailableRelations( relations.push({ fieldName: rel.fieldName, type: 'hasMany', - referencedTable: rel.referencedByTable || undefined, + referencedTable: rel.referencedByTable || undefined }); } }); @@ -384,7 +384,7 @@ export function getAvailableRelations( relations.push({ fieldName: rel.fieldName, type: 'manyToMany', - referencedTable: rel.rightTable || undefined, + referencedTable: rel.rightTable || undefined }); } }); @@ -465,6 +465,6 @@ export function validateFieldSelection( return { isValid: errors.length === 0, - errors, + errors }; } diff --git a/graphql/codegen/src/generators/index.ts b/graphql/codegen/src/generators/index.ts index 1bc3c755e..4b38b3518 100644 --- a/graphql/codegen/src/generators/index.ts +++ b/graphql/codegen/src/generators/index.ts @@ -5,26 +5,26 @@ // Field selector utilities export { convertToSelectionOptions, - isRelationalField, getAvailableRelations, - validateFieldSelection, + isRelationalField, + validateFieldSelection } from './field-selector'; // Query generators export { - buildSelect, - buildFindOne, buildCount, - toCamelCasePlural, - toOrderByTypeName, + buildFindOne, + buildSelect, cleanTableToMetaObject, - generateIntrospectionSchema, createASTQueryBuilder, + generateIntrospectionSchema, + toCamelCasePlural, + toOrderByTypeName } from './select'; // Mutation generators export { buildPostGraphileCreate, - buildPostGraphileUpdate, buildPostGraphileDelete, + buildPostGraphileUpdate } from './mutations'; diff --git a/graphql/codegen/src/generators/mutations.ts b/graphql/codegen/src/generators/mutations.ts index a7d495e0b..3d86856e1 100644 --- a/graphql/codegen/src/generators/mutations.ts +++ b/graphql/codegen/src/generators/mutations.ts @@ -3,18 +3,17 @@ * Uses AST-based approach for PostGraphile-compatible mutations */ import * as t from 'gql-ast'; -import { print } from 'graphql'; import type { ArgumentNode, FieldNode, VariableDefinitionNode } from 'graphql'; +import { print } from 'graphql'; import { camelize } from 'inflekt'; import { TypedDocumentString } from '../client/typed-document'; import { getCustomAstForCleanField, - requiresSubfieldSelection, + requiresSubfieldSelection } from '../core/custom-ast'; -import type { CleanTable } from '../types/schema'; import type { MutationOptions } from '../types/mutation'; - +import type { CleanTable } from '../types/schema'; import { isRelationalField } from './field-selector'; /** @@ -55,17 +54,17 @@ export function buildPostGraphileCreate( t.variableDefinition({ variable: t.variable({ name: 'input' }), type: t.nonNullType({ - type: t.namedType({ type: `Create${table.name}Input` }), - }), - }), + type: t.namedType({ type: `Create${table.name}Input` }) + }) + }) ]; // Create the mutation arguments const mutationArgs: ArgumentNode[] = [ t.argument({ name: 'input', - value: t.variable({ name: 'input' }), - }), + value: t.variable({ name: 'input' }) + }) ]; // Get the field selections for the return value using custom AST logic @@ -88,23 +87,23 @@ export function buildPostGraphileCreate( t.field({ name: singularName, selectionSet: t.selectionSet({ - selections: fieldSelections, - }), - }), - ], - }), - }), - ], - }), - }), - ], + selections: fieldSelections + }) + }) + ] + }) + }) + ] + }) + }) + ] }); // Print the AST to get the query string const queryString = print(ast); return new TypedDocumentString(queryString, { - __ast: ast, + __ast: ast }) as TypedDocumentString< Record, { input: { [key: string]: Record } } @@ -131,17 +130,17 @@ export function buildPostGraphileUpdate( t.variableDefinition({ variable: t.variable({ name: 'input' }), type: t.nonNullType({ - type: t.namedType({ type: `Update${table.name}Input` }), - }), - }), + type: t.namedType({ type: `Update${table.name}Input` }) + }) + }) ]; // Create the mutation arguments const mutationArgs: ArgumentNode[] = [ t.argument({ name: 'input', - value: t.variable({ name: 'input' }), - }), + value: t.variable({ name: 'input' }) + }) ]; // Get the field selections for the return value using custom AST logic @@ -164,23 +163,23 @@ export function buildPostGraphileUpdate( t.field({ name: singularName, selectionSet: t.selectionSet({ - selections: fieldSelections, - }), - }), - ], - }), - }), - ], - }), - }), - ], + selections: fieldSelections + }) + }) + ] + }) + }) + ] + }) + }) + ] }); // Print the AST to get the query string const queryString = print(ast); return new TypedDocumentString(queryString, { - __ast: ast, + __ast: ast }) as TypedDocumentString< Record, { input: { id: string | number; patch: Record } } @@ -206,17 +205,17 @@ export function buildPostGraphileDelete( t.variableDefinition({ variable: t.variable({ name: 'input' }), type: t.nonNullType({ - type: t.namedType({ type: `Delete${table.name}Input` }), - }), - }), + type: t.namedType({ type: `Delete${table.name}Input` }) + }) + }) ]; // Create the mutation arguments const mutationArgs: ArgumentNode[] = [ t.argument({ name: 'input', - value: t.variable({ name: 'input' }), - }), + value: t.variable({ name: 'input' }) + }) ]; // PostGraphile delete mutations typically return clientMutationId @@ -235,20 +234,20 @@ export function buildPostGraphileDelete( name: mutationName, args: mutationArgs, selectionSet: t.selectionSet({ - selections: fieldSelections, - }), - }), - ], - }), - }), - ], + selections: fieldSelections + }) + }) + ] + }) + }) + ] }); // Print the AST to get the query string const queryString = print(ast); return new TypedDocumentString(queryString, { - __ast: ast, + __ast: ast }) as TypedDocumentString< Record, { input: { id: string | number } } diff --git a/graphql/codegen/src/generators/select.ts b/graphql/codegen/src/generators/select.ts index edf2cace7..8bd58ccf8 100644 --- a/graphql/codegen/src/generators/select.ts +++ b/graphql/codegen/src/generators/select.ts @@ -3,14 +3,14 @@ * Uses AST-based approach for all query generation */ import * as t from 'gql-ast'; -import { print } from 'graphql'; import type { ArgumentNode, FieldNode, VariableDefinitionNode } from 'graphql'; +import { print } from 'graphql'; import { camelize, pluralize } from 'inflekt'; import { TypedDocumentString } from '../client/typed-document'; import { getCustomAstForCleanField, - requiresSubfieldSelection, + requiresSubfieldSelection } from '../core/custom-ast'; import { QueryBuilder } from '../core/query-builder'; import type { @@ -20,12 +20,11 @@ import type { MetaObject, MutationDefinition, QueryDefinition, - QuerySelectionOptions, + QuerySelectionOptions } from '../core/types'; -import type { CleanTable } from '../types/schema'; import type { QueryOptions } from '../types/query'; +import type { CleanTable } from '../types/schema'; import type { FieldSelection } from '../types/selection'; - import { convertToSelectionOptions, isRelationalField } from './field-selector'; /** @@ -67,8 +66,8 @@ export function cleanTableToMetaObject(tables: CleanTable[]): MetaObject { pgAlias: field.type.pgAlias, pgType: field.type.pgType, subtype: field.type.subtype, - typmod: field.type.typmod, - }, + typmod: field.type.typmod + } })), primaryConstraints: [] as MetaConstraint[], // Would need to be derived from schema uniqueConstraints: [] as MetaConstraint[], // Would need to be derived from schema @@ -83,9 +82,9 @@ export function cleanTableToMetaObject(tables: CleanTable[]): MetaObject { pgAlias: null, pgType: null, subtype: null, - typmod: null, + typmod: null } as MetaFieldType, - alias: rel.fieldName || '', + alias: rel.fieldName || '' }, toKey: { name: 'id', @@ -96,11 +95,11 @@ export function cleanTableToMetaObject(tables: CleanTable[]): MetaObject { pgAlias: null, pgType: null, subtype: null, - typmod: null, - } as MetaFieldType, - }, - })), - })), + typmod: null + } as MetaFieldType + } + })) + })) }; } @@ -125,7 +124,7 @@ export function generateIntrospectionSchema( qtype: 'getMany', model: modelName, selection, - properties: convertFieldsToProperties(table.fields), + properties: convertFieldsToProperties(table.fields) } as QueryDefinition; // Add getOne query (by ID) @@ -134,7 +133,7 @@ export function generateIntrospectionSchema( qtype: 'getOne', model: modelName, selection, - properties: convertFieldsToProperties(table.fields), + properties: convertFieldsToProperties(table.fields) } as QueryDefinition; // Add create mutation @@ -157,11 +156,11 @@ export function generateIntrospectionSchema( isNotNull: true, isArray: false, isArrayNotNull: false, - properties: convertFieldsToNestedProperties(table.fields), - }, - }, - }, - }, + properties: convertFieldsToNestedProperties(table.fields) + } + } + } + } } as MutationDefinition; // Add update mutation @@ -184,11 +183,11 @@ export function generateIntrospectionSchema( isNotNull: true, isArray: false, isArrayNotNull: false, - properties: convertFieldsToNestedProperties(table.fields), - }, - }, - }, - }, + properties: convertFieldsToNestedProperties(table.fields) + } + } + } + } } as MutationDefinition; // Add delete mutation @@ -210,11 +209,11 @@ export function generateIntrospectionSchema( type: 'UUID', isNotNull: true, isArray: false, - isArrayNotNull: false, - }, - }, - }, - }, + isArrayNotNull: false + } + } + } + } } as MutationDefinition; } @@ -233,7 +232,7 @@ function convertFieldsToProperties(fields: CleanTable['fields']) { type: field.type.gqlType, isNotNull: !field.type.gqlType.endsWith('!'), isArray: field.type.isArray, - isArrayNotNull: false, + isArrayNotNull: false }; }); @@ -252,7 +251,7 @@ function convertFieldsToNestedProperties(fields: CleanTable['fields']) { type: field.type.gqlType, isNotNull: false, // Mutations typically allow optional fields isArray: field.type.isArray, - isArrayNotNull: false, + isArrayNotNull: false }; }); @@ -268,7 +267,7 @@ export function createASTQueryBuilder(tables: CleanTable[]): QueryBuilder { return new QueryBuilder({ meta: metaObject, - introspection: introspectionSchema, + introspection: introspectionSchema }); } @@ -370,13 +369,13 @@ function generateSelectQueryAST( variableDefinitions.push( t.variableDefinition({ variable: t.variable({ name: 'first' }), - type: t.namedType({ type: 'Int' }), + type: t.namedType({ type: 'Int' }) }) ); queryArgs.push( t.argument({ name: 'first', - value: t.variable({ name: 'first' }), + value: t.variable({ name: 'first' }) }) ); } @@ -385,13 +384,13 @@ function generateSelectQueryAST( variableDefinitions.push( t.variableDefinition({ variable: t.variable({ name: 'offset' }), - type: t.namedType({ type: 'Int' }), + type: t.namedType({ type: 'Int' }) }) ); queryArgs.push( t.argument({ name: 'offset', - value: t.variable({ name: 'offset' }), + value: t.variable({ name: 'offset' }) }) ); } @@ -401,13 +400,13 @@ function generateSelectQueryAST( variableDefinitions.push( t.variableDefinition({ variable: t.variable({ name: 'after' }), - type: t.namedType({ type: 'Cursor' }), + type: t.namedType({ type: 'Cursor' }) }) ); queryArgs.push( t.argument({ name: 'after', - value: t.variable({ name: 'after' }), + value: t.variable({ name: 'after' }) }) ); } @@ -416,13 +415,13 @@ function generateSelectQueryAST( variableDefinitions.push( t.variableDefinition({ variable: t.variable({ name: 'before' }), - type: t.namedType({ type: 'Cursor' }), + type: t.namedType({ type: 'Cursor' }) }) ); queryArgs.push( t.argument({ name: 'before', - value: t.variable({ name: 'before' }), + value: t.variable({ name: 'before' }) }) ); } @@ -432,13 +431,13 @@ function generateSelectQueryAST( variableDefinitions.push( t.variableDefinition({ variable: t.variable({ name: 'filter' }), - type: t.namedType({ type: `${table.name}Filter` }), + type: t.namedType({ type: `${table.name}Filter` }) }) ); queryArgs.push( t.argument({ name: 'filter', - value: t.variable({ name: 'filter' }), + value: t.variable({ name: 'filter' }) }) ); } @@ -451,15 +450,15 @@ function generateSelectQueryAST( // PostGraphile expects [ProductsOrderBy!] - list of non-null enum values type: t.listType({ type: t.nonNullType({ - type: t.namedType({ type: toOrderByTypeName(table.name) }), - }), - }), + type: t.namedType({ type: toOrderByTypeName(table.name) }) + }) + }) }) ); queryArgs.push( t.argument({ name: 'orderBy', - value: t.variable({ name: 'orderBy' }), + value: t.variable({ name: 'orderBy' }) }) ); } @@ -470,9 +469,9 @@ function generateSelectQueryAST( t.field({ name: 'nodes', selectionSet: t.selectionSet({ - selections: fieldSelections, - }), - }), + selections: fieldSelections + }) + }) ]; // Add pageInfo if requested (for cursor-based pagination / infinite scroll) @@ -489,9 +488,9 @@ function generateSelectQueryAST( t.field({ name: 'hasNextPage' }), t.field({ name: 'hasPreviousPage' }), t.field({ name: 'startCursor' }), - t.field({ name: 'endCursor' }), - ], - }), + t.field({ name: 'endCursor' }) + ] + }) }) ); } @@ -508,13 +507,13 @@ function generateSelectQueryAST( name: pluralName, args: queryArgs, selectionSet: t.selectionSet({ - selections: connectionSelections, - }), - }), - ], - }), - }), - ], + selections: connectionSelections + }) + }) + ] + }) + }) + ] }); return print(ast); @@ -595,20 +594,20 @@ function generateFieldSelectionsFromOptions( t.argument({ name: 'first', value: t.intValue({ - value: DEFAULT_NESTED_RELATION_FIRST.toString(), - }), - }), + value: DEFAULT_NESTED_RELATION_FIRST.toString() + }) + }) ], selectionSet: t.selectionSet({ selections: [ t.field({ name: 'nodes', selectionSet: t.selectionSet({ - selections: nestedSelections, - }), - }), - ], - }), + selections: nestedSelections + }) + }) + ] + }) }) ); } else { @@ -617,8 +616,8 @@ function generateFieldSelectionsFromOptions( t.field({ name: fieldName, selectionSet: t.selectionSet({ - selections: nestedSelections, - }), + selections: nestedSelections + }) }) ); } @@ -749,9 +748,9 @@ function generateFindOneQueryAST(table: CleanTable): string { t.variableDefinition({ variable: t.variable({ name: 'id' }), type: t.nonNullType({ - type: t.namedType({ type: 'UUID' }), - }), - }), + type: t.namedType({ type: 'UUID' }) + }) + }) ], selectionSet: t.selectionSet({ selections: [ @@ -760,17 +759,17 @@ function generateFindOneQueryAST(table: CleanTable): string { args: [ t.argument({ name: 'id', - value: t.variable({ name: 'id' }), - }), + value: t.variable({ name: 'id' }) + }) ], selectionSet: t.selectionSet({ - selections: fieldSelections, - }), - }), - ], - }), - }), - ], + selections: fieldSelections + }) + }) + ] + }) + }) + ] }); return print(ast); @@ -790,8 +789,8 @@ function generateCountQueryAST(table: CleanTable): string { variableDefinitions: [ t.variableDefinition({ variable: t.variable({ name: 'filter' }), - type: t.namedType({ type: `${table.name}Filter` }), - }), + type: t.namedType({ type: `${table.name}Filter` }) + }) ], selectionSet: t.selectionSet({ selections: [ @@ -800,17 +799,17 @@ function generateCountQueryAST(table: CleanTable): string { args: [ t.argument({ name: 'filter', - value: t.variable({ name: 'filter' }), - }), + value: t.variable({ name: 'filter' }) + }) ], selectionSet: t.selectionSet({ - selections: [t.field({ name: 'totalCount' })], - }), - }), - ], - }), - }), - ], + selections: [t.field({ name: 'totalCount' })] + }) + }) + ] + }) + }) + ] }); return print(ast); diff --git a/graphql/codegen/src/index.ts b/graphql/codegen/src/index.ts index 19ae82473..9d794f364 100644 --- a/graphql/codegen/src/index.ts +++ b/graphql/codegen/src/index.ts @@ -22,22 +22,22 @@ export * from './client'; export { defineConfig } from './types/config'; // Main generate function (orchestrates the entire pipeline) -export { generate } from './core/generate'; export type { GenerateOptions, GenerateResult } from './core/generate'; +export { generate } from './core/generate'; // Config utilities export { findConfigFile, loadConfigFile } from './core/config'; // CLI shared utilities (for packages/cli to import) -export { codegenQuestions, splitCommas, printResult, camelizeArgv } from './cli/shared'; export type { CodegenAnswers } from './cli/shared'; +export { camelizeArgv,codegenQuestions, printResult, splitCommas } from './cli/shared'; // Database schema utilities (re-exported from core for convenience) -export { - buildSchemaFromDatabase, - buildSchemaSDLFromDatabase, -} from './core/database'; export type { BuildSchemaFromDatabaseOptions, - BuildSchemaFromDatabaseResult, + BuildSchemaFromDatabaseResult +} from './core/database'; +export { + buildSchemaFromDatabase, + buildSchemaSDLFromDatabase } from './core/database'; diff --git a/graphql/codegen/src/types/config.ts b/graphql/codegen/src/types/config.ts index bae0a864f..09e457006 100644 --- a/graphql/codegen/src/types/config.ts +++ b/graphql/codegen/src/types/config.ts @@ -356,7 +356,7 @@ export const DEFAULT_WATCH_CONFIG: WatchConfig = { pollInterval: 3000, debounce: 800, touchFile: undefined, - clearScreen: true, + clearScreen: true }; /** @@ -367,7 +367,7 @@ export const DEFAULT_QUERY_KEY_CONFIG: QueryKeyConfig = { relationships: {}, generateScopedKeys: true, generateCascadeHelpers: true, - generateMutationKeys: true, + generateMutationKeys: true }; /** @@ -380,36 +380,36 @@ export const DEFAULT_CONFIG: GraphQLSDKConfigTarget = { tables: { include: ['*'], exclude: [], - systemExclude: [], + systemExclude: [] }, queries: { include: ['*'], exclude: [], - systemExclude: ['_meta', 'query'], // Internal PostGraphile queries + systemExclude: ['_meta', 'query'] // Internal PostGraphile queries }, mutations: { include: ['*'], exclude: [], - systemExclude: [], + systemExclude: [] }, excludeFields: [], hooks: { queries: true, mutations: true, - queryKeyPrefix: 'graphql', + queryKeyPrefix: 'graphql' }, postgraphile: { - schema: 'public', + schema: 'public' }, codegen: { maxFieldDepth: 2, - skipQueryField: true, + skipQueryField: true }, orm: false, reactQuery: false, browserCompatible: true, queryKeys: DEFAULT_QUERY_KEY_CONFIG, - watch: DEFAULT_WATCH_CONFIG, + watch: DEFAULT_WATCH_CONFIG }; diff --git a/graphql/codegen/src/types/index.ts b/graphql/codegen/src/types/index.ts index 8e23aad73..83a920137 100644 --- a/graphql/codegen/src/types/index.ts +++ b/graphql/codegen/src/types/index.ts @@ -4,59 +4,58 @@ // Schema types export type { - CleanTable, + CleanBelongsToRelation, CleanField, CleanFieldType, - CleanRelations, - CleanBelongsToRelation, - CleanHasOneRelation, CleanHasManyRelation, + CleanHasOneRelation, CleanManyToManyRelation, - TableInflection, - TableQueryNames, - TableConstraints, + CleanRelations, + CleanTable, ConstraintInfo, ForeignKeyConstraint, + TableConstraints, + TableInflection, + TableQueryNames } from './schema'; // Query types export type { - PageInfo, ConnectionResult, - QueryOptions, - OrderByItem, - FilterOperator, FieldFilter, - RelationalFilter, Filter, + FilterOperator, + OrderByItem, + PageInfo, + QueryOptions, + RelationalFilter } from './query'; // Mutation types export type { - MutationOptions, CreateInput, - UpdateInput, DeleteInput, + MutationOptions, MutationResult, + UpdateInput } from './mutation'; // Selection types export type { - SimpleFieldSelection, - FieldSelectionPreset, FieldSelection, + FieldSelectionPreset, SelectionOptions, + SimpleFieldSelection } from './selection'; // Config types export type { GraphQLSDKConfig, - GraphQLSDKConfigTarget, + GraphQLSDKConfigTarget } from './config'; - export { + DEFAULT_CONFIG, defineConfig, getConfigOptions, - mergeConfig, - DEFAULT_CONFIG, + mergeConfig } from './config';