@constructive-io/graphql-codegen 2.32.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +429 -1691
- package/cli/index.d.ts +5 -2
- package/cli/index.js +98 -581
- package/cli/shared.d.ts +35 -0
- package/cli/shared.js +106 -0
- package/{esm/cli → core}/codegen/barrel.d.ts +1 -1
- package/{cli → core}/codegen/barrel.js +1 -4
- package/{esm/cli → core}/codegen/index.d.ts +15 -5
- package/{cli → core}/codegen/index.js +44 -24
- package/{cli → core}/codegen/invalidation.d.ts +2 -2
- package/{esm/cli → core}/codegen/mutation-keys.d.ts +2 -2
- package/{cli → core}/codegen/orm/client-generator.js +2 -3
- package/{esm/cli → core}/codegen/orm/index.d.ts +9 -2
- package/{cli → core}/codegen/orm/index.js +3 -2
- package/{cli → core}/codegen/query-keys.d.ts +2 -2
- package/core/codegen/shared/index.d.ts +39 -0
- package/core/codegen/shared/index.js +118 -0
- package/core/config/index.d.ts +5 -0
- package/core/config/index.js +13 -0
- package/core/config/loader.d.ts +18 -0
- package/{cli/commands/init.js → core/config/loader.js} +7 -94
- package/core/config/resolver.d.ts +46 -0
- package/core/config/resolver.js +104 -0
- package/core/database/index.d.ts +43 -0
- package/core/database/index.js +85 -0
- package/core/generate.d.ts +22 -0
- package/core/generate.js +192 -0
- package/core/index.d.ts +13 -1
- package/core/index.js +22 -2
- package/{cli → core}/introspect/fetch-schema.js +58 -9
- package/core/introspect/source/api-schemas.d.ts +44 -0
- package/core/introspect/source/api-schemas.js +122 -0
- package/core/introspect/source/database.d.ts +32 -0
- package/core/introspect/source/database.js +91 -0
- package/core/introspect/source/index.d.ts +112 -0
- package/core/introspect/source/index.js +173 -0
- package/core/introspect/source/pgpm-module.d.ts +83 -0
- package/core/introspect/source/pgpm-module.js +200 -0
- package/core/output/index.d.ts +4 -0
- package/core/output/index.js +9 -0
- package/core/output/writer.d.ts +38 -0
- package/core/output/writer.js +156 -0
- package/{cli/commands/shared.d.ts → core/pipeline/index.d.ts} +5 -3
- package/{cli/commands/shared.js → core/pipeline/index.js} +4 -0
- package/{cli → core}/watch/orchestrator.d.ts +25 -3
- package/{cli → core}/watch/orchestrator.js +35 -27
- package/{cli → core}/watch/types.d.ts +1 -1
- package/esm/cli/index.d.ts +5 -2
- package/esm/cli/index.js +97 -547
- package/esm/cli/shared.d.ts +35 -0
- package/esm/cli/shared.js +101 -0
- package/{cli → esm/core}/codegen/barrel.d.ts +1 -1
- package/esm/{cli → core}/codegen/barrel.js +1 -4
- package/{cli → esm/core}/codegen/index.d.ts +15 -5
- package/esm/{cli → core}/codegen/index.js +44 -24
- package/esm/{cli → core}/codegen/invalidation.d.ts +2 -2
- package/{cli → esm/core}/codegen/mutation-keys.d.ts +2 -2
- package/esm/{cli → core}/codegen/orm/client-generator.js +2 -3
- package/{cli → esm/core}/codegen/orm/index.d.ts +9 -2
- package/esm/{cli → core}/codegen/orm/index.js +3 -2
- package/esm/{cli → core}/codegen/query-keys.d.ts +2 -2
- package/esm/core/codegen/shared/index.d.ts +39 -0
- package/esm/core/codegen/shared/index.js +79 -0
- package/esm/core/config/index.d.ts +5 -0
- package/esm/core/config/index.js +5 -0
- package/esm/core/config/loader.d.ts +18 -0
- package/esm/core/config/loader.js +71 -0
- package/esm/core/config/resolver.d.ts +46 -0
- package/esm/core/config/resolver.js +100 -0
- package/esm/core/database/index.d.ts +43 -0
- package/esm/core/database/index.js +48 -0
- package/esm/core/generate.d.ts +22 -0
- package/esm/core/generate.js +186 -0
- package/esm/core/index.d.ts +13 -1
- package/esm/core/index.js +20 -1
- package/esm/{cli → core}/introspect/fetch-schema.js +55 -9
- package/esm/core/introspect/source/api-schemas.d.ts +44 -0
- package/esm/core/introspect/source/api-schemas.js +117 -0
- package/esm/core/introspect/source/database.d.ts +32 -0
- package/esm/core/introspect/source/database.js +87 -0
- package/esm/core/introspect/source/index.d.ts +112 -0
- package/esm/core/introspect/source/index.js +154 -0
- package/esm/core/introspect/source/pgpm-module.d.ts +83 -0
- package/esm/core/introspect/source/pgpm-module.js +194 -0
- package/esm/core/output/index.d.ts +4 -0
- package/esm/core/output/index.js +4 -0
- package/esm/core/output/writer.d.ts +38 -0
- package/esm/core/output/writer.js +119 -0
- package/esm/{cli/commands/shared.d.ts → core/pipeline/index.d.ts} +5 -3
- package/esm/{cli/commands/shared.js → core/pipeline/index.js} +1 -0
- package/esm/{cli → core}/watch/orchestrator.d.ts +25 -3
- package/esm/{cli → core}/watch/orchestrator.js +35 -27
- package/esm/{cli → core}/watch/types.d.ts +1 -1
- package/esm/index.d.ts +8 -3
- package/esm/index.js +9 -3
- package/esm/types/config.d.ts +101 -138
- package/esm/types/config.js +8 -35
- package/esm/types/index.d.ts +2 -2
- package/esm/types/index.js +1 -1
- package/index.d.ts +8 -3
- package/index.js +18 -8
- package/package.json +18 -11
- package/types/config.d.ts +101 -138
- package/types/config.js +9 -38
- package/types/index.d.ts +2 -2
- package/types/index.js +3 -3
- package/cli/commands/generate-orm.d.ts +0 -53
- package/cli/commands/generate-orm.js +0 -292
- package/cli/commands/generate.d.ts +0 -66
- package/cli/commands/generate.js +0 -431
- package/cli/commands/index.d.ts +0 -9
- package/cli/commands/index.js +0 -14
- package/cli/commands/init.d.ts +0 -35
- package/cli/introspect/source/index.d.ts +0 -48
- package/cli/introspect/source/index.js +0 -72
- package/esm/cli/commands/generate-orm.d.ts +0 -53
- package/esm/cli/commands/generate-orm.js +0 -289
- package/esm/cli/commands/generate.d.ts +0 -66
- package/esm/cli/commands/generate.js +0 -393
- package/esm/cli/commands/index.d.ts +0 -9
- package/esm/cli/commands/index.js +0 -6
- package/esm/cli/commands/init.d.ts +0 -35
- package/esm/cli/commands/init.js +0 -158
- package/esm/cli/introspect/source/index.d.ts +0 -48
- package/esm/cli/introspect/source/index.js +0 -54
- /package/{cli → core}/codegen/babel-ast.d.ts +0 -0
- /package/{cli → core}/codegen/babel-ast.js +0 -0
- /package/{cli → core}/codegen/client.d.ts +0 -0
- /package/{cli → core}/codegen/client.js +0 -0
- /package/{cli → core}/codegen/custom-mutations.d.ts +0 -0
- /package/{cli → core}/codegen/custom-mutations.js +0 -0
- /package/{cli → core}/codegen/custom-queries.d.ts +0 -0
- /package/{cli → core}/codegen/custom-queries.js +0 -0
- /package/{cli → core}/codegen/gql-ast.d.ts +0 -0
- /package/{cli → core}/codegen/gql-ast.js +0 -0
- /package/{cli → core}/codegen/invalidation.js +0 -0
- /package/{cli → core}/codegen/mutation-keys.js +0 -0
- /package/{cli → core}/codegen/mutations.d.ts +0 -0
- /package/{cli → core}/codegen/mutations.js +0 -0
- /package/{cli → core}/codegen/orm/barrel.d.ts +0 -0
- /package/{cli → core}/codegen/orm/barrel.js +0 -0
- /package/{cli → core}/codegen/orm/client-generator.d.ts +0 -0
- /package/{cli → core}/codegen/orm/client.d.ts +0 -0
- /package/{cli → core}/codegen/orm/client.js +0 -0
- /package/{cli → core}/codegen/orm/custom-ops-generator.d.ts +0 -0
- /package/{cli → core}/codegen/orm/custom-ops-generator.js +0 -0
- /package/{cli → core}/codegen/orm/input-types-generator.d.ts +0 -0
- /package/{cli → core}/codegen/orm/input-types-generator.js +0 -0
- /package/{cli → core}/codegen/orm/model-generator.d.ts +0 -0
- /package/{cli → core}/codegen/orm/model-generator.js +0 -0
- /package/{cli → core}/codegen/orm/query-builder.d.ts +0 -0
- /package/{cli → core}/codegen/orm/query-builder.js +0 -0
- /package/{cli → core}/codegen/orm/query-builder.ts +0 -0
- /package/{cli → core}/codegen/orm/select-types.d.ts +0 -0
- /package/{cli → core}/codegen/orm/select-types.js +0 -0
- /package/{cli → core}/codegen/queries.d.ts +0 -0
- /package/{cli → core}/codegen/queries.js +0 -0
- /package/{cli → core}/codegen/query-keys.js +0 -0
- /package/{cli → core}/codegen/scalars.d.ts +0 -0
- /package/{cli → core}/codegen/scalars.js +0 -0
- /package/{cli → core}/codegen/schema-gql-ast.d.ts +0 -0
- /package/{cli → core}/codegen/schema-gql-ast.js +0 -0
- /package/{cli → core}/codegen/schema-types-generator.d.ts +0 -0
- /package/{cli → core}/codegen/schema-types-generator.js +0 -0
- /package/{cli → core}/codegen/type-resolver.d.ts +0 -0
- /package/{cli → core}/codegen/type-resolver.js +0 -0
- /package/{cli → core}/codegen/types.d.ts +0 -0
- /package/{cli → core}/codegen/types.js +0 -0
- /package/{cli → core}/codegen/utils.d.ts +0 -0
- /package/{cli → core}/codegen/utils.js +0 -0
- /package/{cli → core}/introspect/fetch-schema.d.ts +0 -0
- /package/{cli → core}/introspect/index.d.ts +0 -0
- /package/{cli → core}/introspect/index.js +0 -0
- /package/{cli → core}/introspect/infer-tables.d.ts +0 -0
- /package/{cli → core}/introspect/infer-tables.js +0 -0
- /package/{cli → core}/introspect/schema-query.d.ts +0 -0
- /package/{cli → core}/introspect/schema-query.js +0 -0
- /package/{cli → core}/introspect/source/endpoint.d.ts +0 -0
- /package/{cli → core}/introspect/source/endpoint.js +0 -0
- /package/{cli → core}/introspect/source/file.d.ts +0 -0
- /package/{cli → core}/introspect/source/file.js +0 -0
- /package/{cli → core}/introspect/source/types.d.ts +0 -0
- /package/{cli → core}/introspect/source/types.js +0 -0
- /package/{cli → core}/introspect/transform-schema.d.ts +0 -0
- /package/{cli → core}/introspect/transform-schema.js +0 -0
- /package/{cli → core}/introspect/transform.d.ts +0 -0
- /package/{cli → core}/introspect/transform.js +0 -0
- /package/{cli → core}/watch/cache.d.ts +0 -0
- /package/{cli → core}/watch/cache.js +0 -0
- /package/{cli → core}/watch/debounce.d.ts +0 -0
- /package/{cli → core}/watch/debounce.js +0 -0
- /package/{cli → core}/watch/hash.d.ts +0 -0
- /package/{cli → core}/watch/hash.js +0 -0
- /package/{cli → core}/watch/index.d.ts +0 -0
- /package/{cli → core}/watch/index.js +0 -0
- /package/{cli → core}/watch/poller.d.ts +0 -0
- /package/{cli → core}/watch/poller.js +0 -0
- /package/{cli → core}/watch/types.js +0 -0
- /package/esm/{cli → core}/codegen/babel-ast.d.ts +0 -0
- /package/esm/{cli → core}/codegen/babel-ast.js +0 -0
- /package/esm/{cli → core}/codegen/client.d.ts +0 -0
- /package/esm/{cli → core}/codegen/client.js +0 -0
- /package/esm/{cli → core}/codegen/custom-mutations.d.ts +0 -0
- /package/esm/{cli → core}/codegen/custom-mutations.js +0 -0
- /package/esm/{cli → core}/codegen/custom-queries.d.ts +0 -0
- /package/esm/{cli → core}/codegen/custom-queries.js +0 -0
- /package/esm/{cli → core}/codegen/gql-ast.d.ts +0 -0
- /package/esm/{cli → core}/codegen/gql-ast.js +0 -0
- /package/esm/{cli → core}/codegen/invalidation.js +0 -0
- /package/esm/{cli → core}/codegen/mutation-keys.js +0 -0
- /package/esm/{cli → core}/codegen/mutations.d.ts +0 -0
- /package/esm/{cli → core}/codegen/mutations.js +0 -0
- /package/esm/{cli → core}/codegen/orm/barrel.d.ts +0 -0
- /package/esm/{cli → core}/codegen/orm/barrel.js +0 -0
- /package/esm/{cli → core}/codegen/orm/client-generator.d.ts +0 -0
- /package/esm/{cli → core}/codegen/orm/client.d.ts +0 -0
- /package/esm/{cli → core}/codegen/orm/client.js +0 -0
- /package/esm/{cli → core}/codegen/orm/custom-ops-generator.d.ts +0 -0
- /package/esm/{cli → core}/codegen/orm/custom-ops-generator.js +0 -0
- /package/esm/{cli → core}/codegen/orm/input-types-generator.d.ts +0 -0
- /package/esm/{cli → core}/codegen/orm/input-types-generator.js +0 -0
- /package/esm/{cli → core}/codegen/orm/model-generator.d.ts +0 -0
- /package/esm/{cli → core}/codegen/orm/model-generator.js +0 -0
- /package/esm/{cli → core}/codegen/orm/query-builder.d.ts +0 -0
- /package/esm/{cli → core}/codegen/orm/query-builder.js +0 -0
- /package/esm/{cli → core}/codegen/orm/select-types.d.ts +0 -0
- /package/esm/{cli → core}/codegen/orm/select-types.js +0 -0
- /package/esm/{cli → core}/codegen/queries.d.ts +0 -0
- /package/esm/{cli → core}/codegen/queries.js +0 -0
- /package/esm/{cli → core}/codegen/query-keys.js +0 -0
- /package/esm/{cli → core}/codegen/scalars.d.ts +0 -0
- /package/esm/{cli → core}/codegen/scalars.js +0 -0
- /package/esm/{cli → core}/codegen/schema-gql-ast.d.ts +0 -0
- /package/esm/{cli → core}/codegen/schema-gql-ast.js +0 -0
- /package/esm/{cli → core}/codegen/schema-types-generator.d.ts +0 -0
- /package/esm/{cli → core}/codegen/schema-types-generator.js +0 -0
- /package/esm/{cli → core}/codegen/type-resolver.d.ts +0 -0
- /package/esm/{cli → core}/codegen/type-resolver.js +0 -0
- /package/esm/{cli → core}/codegen/types.d.ts +0 -0
- /package/esm/{cli → core}/codegen/types.js +0 -0
- /package/esm/{cli → core}/codegen/utils.d.ts +0 -0
- /package/esm/{cli → core}/codegen/utils.js +0 -0
- /package/esm/{cli → core}/introspect/fetch-schema.d.ts +0 -0
- /package/esm/{cli → core}/introspect/index.d.ts +0 -0
- /package/esm/{cli → core}/introspect/index.js +0 -0
- /package/esm/{cli → core}/introspect/infer-tables.d.ts +0 -0
- /package/esm/{cli → core}/introspect/infer-tables.js +0 -0
- /package/esm/{cli → core}/introspect/schema-query.d.ts +0 -0
- /package/esm/{cli → core}/introspect/schema-query.js +0 -0
- /package/esm/{cli → core}/introspect/source/endpoint.d.ts +0 -0
- /package/esm/{cli → core}/introspect/source/endpoint.js +0 -0
- /package/esm/{cli → core}/introspect/source/file.d.ts +0 -0
- /package/esm/{cli → core}/introspect/source/file.js +0 -0
- /package/esm/{cli → core}/introspect/source/types.d.ts +0 -0
- /package/esm/{cli → core}/introspect/source/types.js +0 -0
- /package/esm/{cli → core}/introspect/transform-schema.d.ts +0 -0
- /package/esm/{cli → core}/introspect/transform-schema.js +0 -0
- /package/esm/{cli → core}/introspect/transform.d.ts +0 -0
- /package/esm/{cli → core}/introspect/transform.js +0 -0
- /package/esm/{cli → core}/watch/cache.d.ts +0 -0
- /package/esm/{cli → core}/watch/cache.js +0 -0
- /package/esm/{cli → core}/watch/debounce.d.ts +0 -0
- /package/esm/{cli → core}/watch/debounce.js +0 -0
- /package/esm/{cli → core}/watch/hash.d.ts +0 -0
- /package/esm/{cli → core}/watch/hash.js +0 -0
- /package/esm/{cli → core}/watch/index.d.ts +0 -0
- /package/esm/{cli → core}/watch/index.js +0 -0
- /package/esm/{cli → core}/watch/poller.d.ts +0 -0
- /package/esm/{cli → core}/watch/poller.js +0 -0
- /package/esm/{cli → core}/watch/types.js +0 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<a href="https://www.npmjs.com/package/@constructive-io/graphql-codegen"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=graphql%2Fcodegen%2Fpackage.json"/></a>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
GraphQL SDK generator for Constructive databases. Generate type-safe React Query hooks or a Prisma-like ORM client from your GraphQL schema.
|
|
16
16
|
|
|
17
17
|
## Features
|
|
18
18
|
|
|
@@ -24,29 +24,21 @@ CLI-based GraphQL SDK generator for PostGraphile endpoints. Generate type-safe R
|
|
|
24
24
|
- **Advanced Type Inference**: Const generics for narrowed return types based on select clauses
|
|
25
25
|
- **Relation Support**: Typed nested selects for belongsTo, hasMany, and manyToMany relations
|
|
26
26
|
- **Error Handling**: Discriminated unions with `.unwrap()`, `.unwrapOr()`, `.unwrapOrElse()` methods
|
|
27
|
-
- **AST-Based Generation**: Uses
|
|
27
|
+
- **AST-Based Generation**: Uses Babel for reliable code generation
|
|
28
28
|
- **Configurable**: Filter tables, queries, and mutations with glob patterns
|
|
29
29
|
- **Type-Safe**: Full TypeScript support with generated interfaces
|
|
30
30
|
|
|
31
31
|
## Table of Contents
|
|
32
32
|
|
|
33
33
|
- [Installation](#installation)
|
|
34
|
-
- [
|
|
35
|
-
- [CLI Commands](#cli-commands)
|
|
36
|
-
- [Configuration](#configuration)
|
|
34
|
+
- [Programmatic API](#programmatic-api)
|
|
37
35
|
- [React Query Hooks](#react-query-hooks)
|
|
38
36
|
- [ORM Client](#orm-client)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- [Relations](#relations)
|
|
42
|
-
- [Filtering & Ordering](#filtering--ordering)
|
|
43
|
-
- [Pagination](#pagination)
|
|
44
|
-
- [Error Handling](#error-handling)
|
|
45
|
-
- [Custom Operations](#custom-operations)
|
|
37
|
+
- [Configuration](#configuration)
|
|
38
|
+
- [CLI Commands](#cli-commands)
|
|
46
39
|
- [Architecture](#architecture)
|
|
47
40
|
- [Generated Types](#generated-types)
|
|
48
41
|
- [Development](#development)
|
|
49
|
-
- [Roadmap](#roadmap)
|
|
50
42
|
|
|
51
43
|
## Installation
|
|
52
44
|
|
|
@@ -54,217 +46,195 @@ CLI-based GraphQL SDK generator for PostGraphile endpoints. Generate type-safe R
|
|
|
54
46
|
pnpm add @constructive-io/graphql-codegen
|
|
55
47
|
```
|
|
56
48
|
|
|
57
|
-
##
|
|
49
|
+
## Programmatic API
|
|
58
50
|
|
|
59
|
-
|
|
51
|
+
The primary way to use this package is through the programmatic API. Import the `generate` function and call it with your configuration.
|
|
60
52
|
|
|
61
|
-
|
|
62
|
-
npx graphql-sdk init
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Creates a `graphql-sdk.config.ts` file:
|
|
53
|
+
### Generate from Endpoint
|
|
66
54
|
|
|
67
55
|
```typescript
|
|
68
|
-
import {
|
|
56
|
+
import { generate } from '@constructive-io/graphql-codegen';
|
|
69
57
|
|
|
70
|
-
|
|
58
|
+
// Generate React Query hooks from a GraphQL endpoint
|
|
59
|
+
await generate({
|
|
71
60
|
endpoint: 'https://api.example.com/graphql',
|
|
72
|
-
output: './generated
|
|
73
|
-
headers: {
|
|
74
|
-
|
|
75
|
-
},
|
|
61
|
+
output: './generated',
|
|
62
|
+
headers: { Authorization: 'Bearer <token>' },
|
|
63
|
+
reactQuery: true,
|
|
76
64
|
});
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### 2. Generate SDK
|
|
80
65
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
66
|
+
// Generate ORM client from a GraphQL endpoint
|
|
67
|
+
await generate({
|
|
68
|
+
endpoint: 'https://api.example.com/graphql',
|
|
69
|
+
output: './generated',
|
|
70
|
+
headers: { Authorization: 'Bearer <token>' },
|
|
71
|
+
orm: true,
|
|
72
|
+
});
|
|
84
73
|
|
|
85
|
-
|
|
86
|
-
|
|
74
|
+
// Generate both React Query hooks and ORM client
|
|
75
|
+
await generate({
|
|
76
|
+
endpoint: 'https://api.example.com/graphql',
|
|
77
|
+
output: './generated',
|
|
78
|
+
reactQuery: true,
|
|
79
|
+
orm: true,
|
|
80
|
+
});
|
|
87
81
|
```
|
|
88
82
|
|
|
89
|
-
###
|
|
90
|
-
|
|
91
|
-
```typescript
|
|
92
|
-
// ORM Client
|
|
93
|
-
import { createClient } from './generated/orm';
|
|
94
|
-
|
|
95
|
-
const db = createClient({ endpoint: 'https://api.example.com/graphql' });
|
|
83
|
+
### Generate from Database
|
|
96
84
|
|
|
97
|
-
|
|
98
|
-
select: { id: true, username: true },
|
|
99
|
-
first: 10,
|
|
100
|
-
}).execute();
|
|
85
|
+
Connect directly to a PostgreSQL database to generate code:
|
|
101
86
|
|
|
102
|
-
|
|
103
|
-
import {
|
|
104
|
-
|
|
105
|
-
function CarList() {
|
|
106
|
-
const { data } = useCarsQuery({ first: 10 });
|
|
107
|
-
return <ul>{data?.cars.nodes.map(car => <li key={car.id}>{car.name}</li>)}</ul>;
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
## CLI Commands
|
|
87
|
+
```typescript
|
|
88
|
+
import { generate } from '@constructive-io/graphql-codegen';
|
|
112
89
|
|
|
113
|
-
|
|
90
|
+
// Generate from database with explicit schemas
|
|
91
|
+
await generate({
|
|
92
|
+
db: {
|
|
93
|
+
schemas: ['public', 'app_public'],
|
|
94
|
+
},
|
|
95
|
+
output: './generated',
|
|
96
|
+
reactQuery: true,
|
|
97
|
+
});
|
|
114
98
|
|
|
115
|
-
Generate
|
|
99
|
+
// Generate from database using API names for automatic schema discovery
|
|
100
|
+
await generate({
|
|
101
|
+
db: {
|
|
102
|
+
apiNames: ['my_api'],
|
|
103
|
+
},
|
|
104
|
+
output: './generated',
|
|
105
|
+
orm: true,
|
|
106
|
+
});
|
|
116
107
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
108
|
+
// Generate with explicit database config (overrides environment variables)
|
|
109
|
+
await generate({
|
|
110
|
+
db: {
|
|
111
|
+
config: {
|
|
112
|
+
host: 'localhost',
|
|
113
|
+
port: 5432,
|
|
114
|
+
database: 'mydb',
|
|
115
|
+
user: 'postgres',
|
|
116
|
+
},
|
|
117
|
+
schemas: ['public'],
|
|
118
|
+
},
|
|
119
|
+
output: './generated',
|
|
120
|
+
reactQuery: true,
|
|
121
|
+
});
|
|
127
122
|
```
|
|
128
123
|
|
|
129
|
-
###
|
|
124
|
+
### Generate from PGPM Module
|
|
130
125
|
|
|
131
|
-
Generate
|
|
126
|
+
Generate code from a PGPM module path:
|
|
132
127
|
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
-e, --endpoint <url> GraphQL endpoint URL
|
|
136
|
-
-t, --target <name> Target name in config file
|
|
137
|
-
-o, --output <dir> Output directory (default: ./generated/orm)
|
|
138
|
-
-c, --config <path> Path to config file
|
|
139
|
-
-a, --authorization <token> Authorization header value
|
|
140
|
-
--skip-custom-operations Only generate table models
|
|
141
|
-
--dry-run Preview without writing files
|
|
142
|
-
-v, --verbose Show detailed output
|
|
143
|
-
```
|
|
128
|
+
```typescript
|
|
129
|
+
import { generate } from '@constructive-io/graphql-codegen';
|
|
144
130
|
|
|
145
|
-
|
|
131
|
+
// Generate from a PGPM module directory
|
|
132
|
+
await generate({
|
|
133
|
+
db: {
|
|
134
|
+
pgpm: { modulePath: './packages/my-module' },
|
|
135
|
+
schemas: ['public'],
|
|
136
|
+
},
|
|
137
|
+
output: './generated',
|
|
138
|
+
reactQuery: true,
|
|
139
|
+
});
|
|
146
140
|
|
|
147
|
-
|
|
141
|
+
// Generate from a PGPM workspace with module name
|
|
142
|
+
await generate({
|
|
143
|
+
db: {
|
|
144
|
+
pgpm: {
|
|
145
|
+
workspacePath: '/path/to/workspace',
|
|
146
|
+
moduleName: 'my-module',
|
|
147
|
+
},
|
|
148
|
+
schemas: ['public'],
|
|
149
|
+
},
|
|
150
|
+
output: './generated',
|
|
151
|
+
orm: true,
|
|
152
|
+
});
|
|
148
153
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
// Keep the ephemeral database after generation (for debugging)
|
|
155
|
+
await generate({
|
|
156
|
+
db: {
|
|
157
|
+
pgpm: { modulePath: './packages/my-module' },
|
|
158
|
+
schemas: ['public'],
|
|
159
|
+
keepDb: true,
|
|
160
|
+
},
|
|
161
|
+
output: './generated',
|
|
162
|
+
reactQuery: true,
|
|
163
|
+
});
|
|
153
164
|
```
|
|
154
165
|
|
|
155
|
-
###
|
|
156
|
-
|
|
157
|
-
Inspect schema without generating code.
|
|
158
|
-
|
|
159
|
-
```bash
|
|
160
|
-
Options:
|
|
161
|
-
-e, --endpoint <url> GraphQL endpoint URL
|
|
162
|
-
--json Output as JSON
|
|
163
|
-
-v, --verbose Show detailed output
|
|
164
|
-
```
|
|
166
|
+
### Configuration Options
|
|
165
167
|
|
|
166
|
-
|
|
168
|
+
The `generate` function accepts a configuration object with the following options:
|
|
167
169
|
|
|
168
170
|
```typescript
|
|
169
171
|
interface GraphQLSDKConfigTarget {
|
|
170
|
-
//
|
|
171
|
-
endpoint?: string;
|
|
172
|
-
|
|
172
|
+
// Source (choose one)
|
|
173
|
+
endpoint?: string; // GraphQL endpoint URL
|
|
174
|
+
schemaFile?: string; // Path to GraphQL schema file (.graphql)
|
|
175
|
+
db?: DbConfig; // Database configuration (see below)
|
|
173
176
|
|
|
174
177
|
// Output
|
|
175
|
-
output?: string;
|
|
178
|
+
output?: string; // Output directory (default: './generated/graphql')
|
|
176
179
|
|
|
177
180
|
// Authentication
|
|
178
|
-
headers?: Record<string, string>;
|
|
181
|
+
headers?: Record<string, string>; // HTTP headers for endpoint requests
|
|
182
|
+
|
|
183
|
+
// Generator flags
|
|
184
|
+
reactQuery?: boolean; // Generate React Query hooks (output: {output}/hooks)
|
|
185
|
+
orm?: boolean; // Generate ORM client (output: {output}/orm)
|
|
179
186
|
|
|
180
187
|
// Table filtering (for CRUD operations from _meta)
|
|
181
188
|
tables?: {
|
|
182
|
-
include?: string[];
|
|
183
|
-
exclude?: string[];
|
|
189
|
+
include?: string[]; // Glob patterns (default: ['*'])
|
|
190
|
+
exclude?: string[]; // Glob patterns (default: [])
|
|
184
191
|
};
|
|
185
192
|
|
|
186
193
|
// Query filtering (for ALL queries from __schema)
|
|
187
194
|
queries?: {
|
|
188
|
-
include?: string[];
|
|
189
|
-
exclude?: string[];
|
|
195
|
+
include?: string[]; // Glob patterns (default: ['*'])
|
|
196
|
+
exclude?: string[]; // Glob patterns (default: ['_meta', 'query'])
|
|
190
197
|
};
|
|
191
198
|
|
|
192
199
|
// Mutation filtering (for ALL mutations from __schema)
|
|
193
200
|
mutations?: {
|
|
194
|
-
include?: string[];
|
|
195
|
-
exclude?: string[];
|
|
201
|
+
include?: string[]; // Glob patterns (default: ['*'])
|
|
202
|
+
exclude?: string[]; // Glob patterns (default: [])
|
|
196
203
|
};
|
|
197
204
|
|
|
198
205
|
// Code generation options
|
|
199
206
|
codegen?: {
|
|
200
|
-
maxFieldDepth?: number;
|
|
201
|
-
skipQueryField?: boolean;
|
|
207
|
+
maxFieldDepth?: number; // Max depth for nested fields (default: 2)
|
|
208
|
+
skipQueryField?: boolean; // Skip 'query' field (default: true)
|
|
202
209
|
};
|
|
203
210
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
// Query key generation
|
|
212
|
+
queryKeys?: {
|
|
213
|
+
generateScopedKeys?: boolean; // Generate scope-aware keys (default: true)
|
|
214
|
+
generateMutationKeys?: boolean; // Generate mutation keys (default: true)
|
|
215
|
+
generateCascadeHelpers?: boolean; // Generate invalidation helpers (default: true)
|
|
216
|
+
relationships?: Record<string, { parent: string; foreignKey: string }>;
|
|
208
217
|
};
|
|
209
218
|
}
|
|
210
219
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
type GraphQLSDKConfig = GraphQLSDKConfigTarget | GraphQLSDKMultiConfig;
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
### Multi-target Configuration
|
|
220
|
-
|
|
221
|
-
Configure multiple schema sources and outputs in one file:
|
|
222
|
-
|
|
223
|
-
```typescript
|
|
224
|
-
export default defineConfig({
|
|
225
|
-
defaults: {
|
|
226
|
-
headers: { Authorization: 'Bearer <token>' },
|
|
227
|
-
},
|
|
228
|
-
targets: {
|
|
229
|
-
public: {
|
|
230
|
-
endpoint: 'https://api.example.com/graphql',
|
|
231
|
-
output: './generated/public',
|
|
232
|
-
},
|
|
233
|
-
admin: {
|
|
234
|
-
schema: './admin.schema.graphql',
|
|
235
|
-
output: './generated/admin',
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
});
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
CLI behavior:
|
|
242
|
-
|
|
243
|
-
- `graphql-codegen generate` runs all targets
|
|
244
|
-
- `graphql-codegen generate --target admin` runs a single target
|
|
245
|
-
- `--output` requires `--target` when multiple targets exist
|
|
220
|
+
// Database configuration for direct database introspection or PGPM module
|
|
221
|
+
interface DbConfig {
|
|
222
|
+
// PostgreSQL connection config (falls back to PGHOST, PGPORT, etc. env vars)
|
|
223
|
+
config?: Partial<PgConfig>;
|
|
246
224
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
225
|
+
// PGPM module configuration for ephemeral database creation
|
|
226
|
+
pgpm?: {
|
|
227
|
+
modulePath?: string; // Path to PGPM module directory
|
|
228
|
+
workspacePath?: string; // Path to PGPM workspace (with moduleName)
|
|
229
|
+
moduleName?: string; // Module name within workspace
|
|
230
|
+
};
|
|
253
231
|
|
|
254
|
-
|
|
232
|
+
// Schema selection (choose one)
|
|
233
|
+
schemas?: string[]; // Explicit PostgreSQL schema names
|
|
234
|
+
apiNames?: string[]; // API names for automatic schema discovery
|
|
255
235
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
tables: {
|
|
259
|
-
include: ['User', 'Product', 'Order*'],
|
|
260
|
-
exclude: ['*_archive', 'temp_*'],
|
|
261
|
-
},
|
|
262
|
-
queries: {
|
|
263
|
-
exclude: ['_meta', 'query', '*Debug*'],
|
|
264
|
-
},
|
|
265
|
-
mutations: {
|
|
266
|
-
include: ['create*', 'update*', 'delete*', 'login', 'register', 'logout'],
|
|
267
|
-
},
|
|
236
|
+
// Debugging
|
|
237
|
+
keepDb?: boolean; // Keep ephemeral database after generation
|
|
268
238
|
}
|
|
269
239
|
```
|
|
270
240
|
|
|
@@ -279,138 +249,74 @@ generated/hooks/
|
|
|
279
249
|
├── index.ts # Main barrel export (configure, hooks, types)
|
|
280
250
|
├── client.ts # configure() and execute() functions
|
|
281
251
|
├── types.ts # Entity interfaces, filter types, enums
|
|
282
|
-
├── hooks.ts # All hooks re-exported
|
|
283
252
|
├── queries/
|
|
284
253
|
│ ├── index.ts # Query hooks barrel
|
|
285
254
|
│ ├── useCarsQuery.ts # Table list query (findMany)
|
|
286
255
|
│ ├── useCarQuery.ts # Table single item query (findOne)
|
|
287
|
-
│ ├── useCurrentUserQuery.ts # Custom query
|
|
288
256
|
│ └── ...
|
|
289
257
|
└── mutations/
|
|
290
258
|
├── index.ts # Mutation hooks barrel
|
|
291
259
|
├── useCreateCarMutation.ts
|
|
292
260
|
├── useUpdateCarMutation.ts
|
|
293
261
|
├── useDeleteCarMutation.ts
|
|
294
|
-
├── useLoginMutation.ts # Custom mutation
|
|
295
262
|
└── ...
|
|
296
263
|
```
|
|
297
264
|
|
|
298
265
|
### Setup & Configuration
|
|
299
266
|
|
|
300
|
-
#### 1. Configure the Client
|
|
301
|
-
|
|
302
267
|
Configure the GraphQL client once at your app's entry point:
|
|
303
268
|
|
|
304
269
|
```tsx
|
|
305
|
-
// App.tsx or main.tsx
|
|
306
270
|
import { configure } from './generated/hooks';
|
|
307
271
|
|
|
308
|
-
// Basic configuration
|
|
309
|
-
configure({
|
|
310
|
-
endpoint: 'https://api.example.com/graphql',
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// With authentication
|
|
314
272
|
configure({
|
|
315
273
|
endpoint: 'https://api.example.com/graphql',
|
|
316
274
|
headers: {
|
|
317
275
|
Authorization: 'Bearer <token>',
|
|
318
|
-
'X-Custom-Header': 'value',
|
|
319
276
|
},
|
|
320
277
|
});
|
|
321
278
|
```
|
|
322
279
|
|
|
323
|
-
#### 2. Update Headers at Runtime
|
|
324
|
-
|
|
325
|
-
```tsx
|
|
326
|
-
import { configure } from './generated/hooks';
|
|
327
|
-
|
|
328
|
-
// After login, update the authorization header
|
|
329
|
-
function handleLoginSuccess(token: string) {
|
|
330
|
-
configure({
|
|
331
|
-
endpoint: 'https://api.example.com/graphql',
|
|
332
|
-
headers: {
|
|
333
|
-
Authorization: `Bearer ${token}`,
|
|
334
|
-
},
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
```
|
|
338
|
-
|
|
339
280
|
### Table Query Hooks
|
|
340
281
|
|
|
341
282
|
For each table, two query hooks are generated:
|
|
342
283
|
|
|
343
|
-
#### List Query (`use{Table}sQuery`)
|
|
344
|
-
|
|
345
|
-
Fetches multiple records with pagination, filtering, and ordering:
|
|
346
|
-
|
|
347
284
|
```tsx
|
|
348
|
-
import { useCarsQuery } from './generated/hooks';
|
|
285
|
+
import { useCarsQuery, useCarQuery } from './generated/hooks';
|
|
349
286
|
|
|
287
|
+
// List query with filtering, pagination, and ordering
|
|
350
288
|
function CarList() {
|
|
351
|
-
const { data, isLoading, isError, error
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
// Filtering
|
|
361
|
-
filter: {
|
|
362
|
-
brand: { equalTo: 'Tesla' },
|
|
363
|
-
price: { greaterThan: 50000 },
|
|
364
|
-
},
|
|
365
|
-
|
|
366
|
-
// Ordering
|
|
367
|
-
orderBy: ['CREATED_AT_DESC', 'NAME_ASC'],
|
|
368
|
-
}
|
|
369
|
-
);
|
|
289
|
+
const { data, isLoading, isError, error } = useCarsQuery({
|
|
290
|
+
first: 10,
|
|
291
|
+
filter: {
|
|
292
|
+
brand: { equalTo: 'Tesla' },
|
|
293
|
+
price: { greaterThan: 50000 },
|
|
294
|
+
},
|
|
295
|
+
orderBy: ['CREATED_AT_DESC'],
|
|
296
|
+
});
|
|
370
297
|
|
|
371
298
|
if (isLoading) return <div>Loading...</div>;
|
|
372
299
|
if (isError) return <div>Error: {error.message}</div>;
|
|
373
300
|
|
|
374
301
|
return (
|
|
375
|
-
<
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
{car.brand} - ${car.price}
|
|
381
|
-
</li>
|
|
382
|
-
))}
|
|
383
|
-
</ul>
|
|
384
|
-
|
|
385
|
-
{/* Pagination info */}
|
|
386
|
-
{data?.cars.pageInfo.hasNextPage && (
|
|
387
|
-
<button onClick={() => refetch()}>Load More</button>
|
|
388
|
-
)}
|
|
389
|
-
</div>
|
|
302
|
+
<ul>
|
|
303
|
+
{data?.cars.nodes.map((car) => (
|
|
304
|
+
<li key={car.id}>{car.brand} - ${car.price}</li>
|
|
305
|
+
))}
|
|
306
|
+
</ul>
|
|
390
307
|
);
|
|
391
308
|
}
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
#### Single Item Query (`use{Table}Query`)
|
|
395
|
-
|
|
396
|
-
Fetches a single record by ID:
|
|
397
|
-
|
|
398
|
-
```tsx
|
|
399
|
-
import { useCarQuery } from './generated/hooks';
|
|
400
309
|
|
|
310
|
+
// Single item query by ID
|
|
401
311
|
function CarDetails({ carId }: { carId: string }) {
|
|
402
|
-
const { data, isLoading
|
|
403
|
-
id: carId,
|
|
404
|
-
});
|
|
312
|
+
const { data, isLoading } = useCarQuery({ id: carId });
|
|
405
313
|
|
|
406
314
|
if (isLoading) return <div>Loading...</div>;
|
|
407
|
-
if (isError) return <div>Car not found</div>;
|
|
408
315
|
|
|
409
316
|
return (
|
|
410
317
|
<div>
|
|
411
318
|
<h1>{data?.car?.brand}</h1>
|
|
412
319
|
<p>Price: ${data?.car?.price}</p>
|
|
413
|
-
<p>Created: {data?.car?.createdAt}</p>
|
|
414
320
|
</div>
|
|
415
321
|
);
|
|
416
322
|
}
|
|
@@ -420,1472 +326,421 @@ function CarDetails({ carId }: { carId: string }) {
|
|
|
420
326
|
|
|
421
327
|
For each table, three mutation hooks are generated:
|
|
422
328
|
|
|
423
|
-
#### Create Mutation (`useCreate{Table}Mutation`)
|
|
424
|
-
|
|
425
329
|
```tsx
|
|
426
|
-
import {
|
|
330
|
+
import {
|
|
331
|
+
useCreateCarMutation,
|
|
332
|
+
useUpdateCarMutation,
|
|
333
|
+
useDeleteCarMutation,
|
|
334
|
+
} from './generated/hooks';
|
|
427
335
|
|
|
428
|
-
function
|
|
336
|
+
function CarForm() {
|
|
429
337
|
const createCar = useCreateCarMutation({
|
|
430
338
|
onSuccess: (data) => {
|
|
431
339
|
console.log('Created car:', data.createCar.car.id);
|
|
432
|
-
// Invalidate queries, redirect, show toast, etc.
|
|
433
|
-
},
|
|
434
|
-
onError: (error) => {
|
|
435
|
-
console.error('Failed to create car:', error);
|
|
436
|
-
},
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
const handleSubmit = (formData: { brand: string; price: number }) => {
|
|
440
|
-
createCar.mutate({
|
|
441
|
-
input: {
|
|
442
|
-
car: {
|
|
443
|
-
brand: formData.brand,
|
|
444
|
-
price: formData.price,
|
|
445
|
-
},
|
|
446
|
-
},
|
|
447
|
-
});
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
return (
|
|
451
|
-
<form
|
|
452
|
-
onSubmit={(e) => {
|
|
453
|
-
e.preventDefault();
|
|
454
|
-
handleSubmit({ brand: 'Tesla', price: 80000 });
|
|
455
|
-
}}
|
|
456
|
-
>
|
|
457
|
-
{/* form fields */}
|
|
458
|
-
<button type="submit" disabled={createCar.isPending}>
|
|
459
|
-
{createCar.isPending ? 'Creating...' : 'Create Car'}
|
|
460
|
-
</button>
|
|
461
|
-
{createCar.isError && <p>Error: {createCar.error.message}</p>}
|
|
462
|
-
</form>
|
|
463
|
-
);
|
|
464
|
-
}
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
#### Update Mutation (`useUpdate{Table}Mutation`)
|
|
468
|
-
|
|
469
|
-
```tsx
|
|
470
|
-
import { useUpdateCarMutation } from './generated/hooks';
|
|
471
|
-
|
|
472
|
-
function EditCarForm({
|
|
473
|
-
carId,
|
|
474
|
-
currentBrand,
|
|
475
|
-
}: {
|
|
476
|
-
carId: string;
|
|
477
|
-
currentBrand: string;
|
|
478
|
-
}) {
|
|
479
|
-
const updateCar = useUpdateCarMutation({
|
|
480
|
-
onSuccess: (data) => {
|
|
481
|
-
console.log('Updated car:', data.updateCar.car.brand);
|
|
482
|
-
},
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
const handleUpdate = (newBrand: string) => {
|
|
486
|
-
updateCar.mutate({
|
|
487
|
-
input: {
|
|
488
|
-
id: carId,
|
|
489
|
-
patch: {
|
|
490
|
-
brand: newBrand,
|
|
491
|
-
},
|
|
492
|
-
},
|
|
493
|
-
});
|
|
494
|
-
};
|
|
495
|
-
|
|
496
|
-
return (
|
|
497
|
-
<button
|
|
498
|
-
onClick={() => handleUpdate('Updated Brand')}
|
|
499
|
-
disabled={updateCar.isPending}
|
|
500
|
-
>
|
|
501
|
-
Update
|
|
502
|
-
</button>
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
#### Delete Mutation (`useDelete{Table}Mutation`)
|
|
508
|
-
|
|
509
|
-
```tsx
|
|
510
|
-
import { useDeleteCarMutation } from './generated/hooks';
|
|
511
|
-
|
|
512
|
-
function DeleteCarButton({ carId }: { carId: string }) {
|
|
513
|
-
const deleteCar = useDeleteCarMutation({
|
|
514
|
-
onSuccess: () => {
|
|
515
|
-
console.log('Car deleted');
|
|
516
|
-
// Navigate away, refetch list, etc.
|
|
517
340
|
},
|
|
518
341
|
});
|
|
519
342
|
|
|
520
343
|
return (
|
|
521
344
|
<button
|
|
522
|
-
onClick={() =>
|
|
523
|
-
|
|
345
|
+
onClick={() =>
|
|
346
|
+
createCar.mutate({
|
|
347
|
+
input: { car: { brand: 'Tesla', price: 80000 } },
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
disabled={createCar.isPending}
|
|
524
351
|
>
|
|
525
|
-
|
|
352
|
+
Create
|
|
526
353
|
</button>
|
|
527
354
|
);
|
|
528
355
|
}
|
|
529
356
|
```
|
|
530
357
|
|
|
531
|
-
### Custom Query Hooks
|
|
358
|
+
### Custom Query and Mutation Hooks
|
|
532
359
|
|
|
533
|
-
Custom queries from your schema
|
|
360
|
+
Custom queries and mutations from your schema get their own hooks:
|
|
534
361
|
|
|
535
362
|
```tsx
|
|
536
|
-
import { useCurrentUserQuery,
|
|
363
|
+
import { useCurrentUserQuery, useLoginMutation } from './generated/hooks';
|
|
537
364
|
|
|
538
|
-
// Simple custom query
|
|
539
365
|
function UserProfile() {
|
|
540
|
-
const { data
|
|
541
|
-
|
|
542
|
-
if (isLoading) return <div>Loading...</div>;
|
|
543
|
-
if (!data?.currentUser) return <div>Not logged in</div>;
|
|
544
|
-
|
|
545
|
-
return (
|
|
546
|
-
<div>
|
|
547
|
-
<h1>Welcome, {data.currentUser.username}</h1>
|
|
548
|
-
<p>Email: {data.currentUser.email}</p>
|
|
549
|
-
</div>
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Custom query with arguments
|
|
554
|
-
function NodeViewer({ nodeId }: { nodeId: string }) {
|
|
555
|
-
const { data } = useNodeByIdQuery({
|
|
556
|
-
id: nodeId,
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
return <pre>{JSON.stringify(data?.node, null, 2)}</pre>;
|
|
366
|
+
const { data } = useCurrentUserQuery();
|
|
367
|
+
return <h1>Welcome, {data?.currentUser?.username}</h1>;
|
|
560
368
|
}
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
### Custom Mutation Hooks
|
|
564
|
-
|
|
565
|
-
Custom mutations (like `login`, `register`, `logout`) get dedicated hooks:
|
|
566
369
|
|
|
567
|
-
```tsx
|
|
568
|
-
import {
|
|
569
|
-
useLoginMutation,
|
|
570
|
-
useRegisterMutation,
|
|
571
|
-
useLogoutMutation,
|
|
572
|
-
useForgotPasswordMutation,
|
|
573
|
-
} from './generated/hooks';
|
|
574
|
-
|
|
575
|
-
// Login
|
|
576
370
|
function LoginForm() {
|
|
577
371
|
const login = useLoginMutation({
|
|
578
372
|
onSuccess: (data) => {
|
|
579
373
|
const token = data.login.apiToken?.accessToken;
|
|
580
|
-
if (token)
|
|
581
|
-
localStorage.setItem('token', token);
|
|
582
|
-
// Reconfigure client with new token
|
|
583
|
-
configure({
|
|
584
|
-
endpoint: 'https://api.example.com/graphql',
|
|
585
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
},
|
|
589
|
-
onError: (error) => {
|
|
590
|
-
alert('Login failed: ' + error.message);
|
|
591
|
-
},
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
const handleLogin = (email: string, password: string) => {
|
|
595
|
-
login.mutate({
|
|
596
|
-
input: { email, password },
|
|
597
|
-
});
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
return (
|
|
601
|
-
<form
|
|
602
|
-
onSubmit={(e) => {
|
|
603
|
-
e.preventDefault();
|
|
604
|
-
handleLogin('user@example.com', 'password');
|
|
605
|
-
}}
|
|
606
|
-
>
|
|
607
|
-
{/* email and password inputs */}
|
|
608
|
-
<button disabled={login.isPending}>
|
|
609
|
-
{login.isPending ? 'Logging in...' : 'Login'}
|
|
610
|
-
</button>
|
|
611
|
-
</form>
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Register
|
|
616
|
-
function RegisterForm() {
|
|
617
|
-
const register = useRegisterMutation({
|
|
618
|
-
onSuccess: () => {
|
|
619
|
-
alert('Registration successful! Please check your email.');
|
|
620
|
-
},
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
const handleRegister = (data: {
|
|
624
|
-
email: string;
|
|
625
|
-
password: string;
|
|
626
|
-
username: string;
|
|
627
|
-
}) => {
|
|
628
|
-
register.mutate({
|
|
629
|
-
input: {
|
|
630
|
-
email: data.email,
|
|
631
|
-
password: data.password,
|
|
632
|
-
username: data.username,
|
|
633
|
-
},
|
|
634
|
-
});
|
|
635
|
-
};
|
|
636
|
-
|
|
637
|
-
return (
|
|
638
|
-
<button
|
|
639
|
-
onClick={() =>
|
|
640
|
-
handleRegister({
|
|
641
|
-
email: 'new@example.com',
|
|
642
|
-
password: 'secret',
|
|
643
|
-
username: 'newuser',
|
|
644
|
-
})
|
|
645
|
-
}
|
|
646
|
-
>
|
|
647
|
-
Register
|
|
648
|
-
</button>
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// Logout
|
|
653
|
-
function LogoutButton() {
|
|
654
|
-
const logout = useLogoutMutation({
|
|
655
|
-
onSuccess: () => {
|
|
656
|
-
localStorage.removeItem('token');
|
|
657
|
-
window.location.href = '/login';
|
|
658
|
-
},
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
return <button onClick={() => logout.mutate({ input: {} })}>Logout</button>;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Forgot Password
|
|
665
|
-
function ForgotPasswordForm() {
|
|
666
|
-
const forgotPassword = useForgotPasswordMutation({
|
|
667
|
-
onSuccess: () => {
|
|
668
|
-
alert('Password reset email sent!');
|
|
374
|
+
if (token) localStorage.setItem('token', token);
|
|
669
375
|
},
|
|
670
376
|
});
|
|
671
377
|
|
|
672
378
|
return (
|
|
673
|
-
<button
|
|
674
|
-
|
|
675
|
-
forgotPassword.mutate({ input: { email: 'user@example.com' } })
|
|
676
|
-
}
|
|
677
|
-
>
|
|
678
|
-
Reset Password
|
|
379
|
+
<button onClick={() => login.mutate({ input: { email: 'user@example.com', password: 'secret' } })}>
|
|
380
|
+
Login
|
|
679
381
|
</button>
|
|
680
382
|
);
|
|
681
383
|
}
|
|
682
384
|
```
|
|
683
385
|
|
|
684
|
-
###
|
|
386
|
+
### Centralized Query Keys
|
|
685
387
|
|
|
686
|
-
|
|
388
|
+
The codegen generates a centralized query key factory for type-safe cache management:
|
|
687
389
|
|
|
688
390
|
```tsx
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
filter: {
|
|
692
|
-
brand: {
|
|
693
|
-
equalTo: 'Tesla',
|
|
694
|
-
notEqualTo: 'Ford',
|
|
695
|
-
in: ['Tesla', 'BMW', 'Mercedes'],
|
|
696
|
-
notIn: ['Unknown'],
|
|
697
|
-
contains: 'es', // LIKE '%es%'
|
|
698
|
-
startsWith: 'Tes', // LIKE 'Tes%'
|
|
699
|
-
endsWith: 'la', // LIKE '%la'
|
|
700
|
-
includesInsensitive: 'TESLA', // Case-insensitive
|
|
701
|
-
},
|
|
702
|
-
},
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
// Number filters
|
|
706
|
-
useProductsQuery({
|
|
707
|
-
filter: {
|
|
708
|
-
price: {
|
|
709
|
-
equalTo: 100,
|
|
710
|
-
greaterThan: 50,
|
|
711
|
-
greaterThanOrEqualTo: 50,
|
|
712
|
-
lessThan: 200,
|
|
713
|
-
lessThanOrEqualTo: 200,
|
|
714
|
-
},
|
|
715
|
-
},
|
|
716
|
-
});
|
|
391
|
+
import { userKeys, invalidate } from './generated/hooks';
|
|
392
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
717
393
|
|
|
718
|
-
//
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
});
|
|
394
|
+
// Query key structure
|
|
395
|
+
userKeys.all; // ['user']
|
|
396
|
+
userKeys.lists(); // ['user', 'list']
|
|
397
|
+
userKeys.list({ first: 10 }); // ['user', 'list', { first: 10 }]
|
|
398
|
+
userKeys.details(); // ['user', 'detail']
|
|
399
|
+
userKeys.detail('user-123'); // ['user', 'detail', 'user-123']
|
|
725
400
|
|
|
726
|
-
//
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
},
|
|
733
|
-
},
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
// Null checks
|
|
737
|
-
useUsersQuery({
|
|
738
|
-
filter: {
|
|
739
|
-
deletedAt: { isNull: true }, // Only non-deleted
|
|
740
|
-
},
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
// Logical operators
|
|
744
|
-
useUsersQuery({
|
|
745
|
-
filter: {
|
|
746
|
-
// AND (implicit)
|
|
747
|
-
isActive: { equalTo: true },
|
|
748
|
-
role: { equalTo: 'ADMIN' },
|
|
749
|
-
},
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
useUsersQuery({
|
|
753
|
-
filter: {
|
|
754
|
-
// OR
|
|
755
|
-
or: [{ role: { equalTo: 'ADMIN' } }, { role: { equalTo: 'MODERATOR' } }],
|
|
756
|
-
},
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
useUsersQuery({
|
|
760
|
-
filter: {
|
|
761
|
-
// Complex: active AND (admin OR moderator)
|
|
762
|
-
and: [
|
|
763
|
-
{ isActive: { equalTo: true } },
|
|
764
|
-
{
|
|
765
|
-
or: [
|
|
766
|
-
{ role: { equalTo: 'ADMIN' } },
|
|
767
|
-
{ role: { equalTo: 'MODERATOR' } },
|
|
768
|
-
],
|
|
769
|
-
},
|
|
770
|
-
],
|
|
771
|
-
},
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
useUsersQuery({
|
|
775
|
-
filter: {
|
|
776
|
-
// NOT
|
|
777
|
-
not: { status: { equalTo: 'DELETED' } },
|
|
778
|
-
},
|
|
779
|
-
});
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
### Ordering
|
|
783
|
-
|
|
784
|
-
```tsx
|
|
785
|
-
// Single order
|
|
786
|
-
useCarsQuery({
|
|
787
|
-
orderBy: ['CREATED_AT_DESC'],
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
// Multiple orders (fallback)
|
|
791
|
-
useCarsQuery({
|
|
792
|
-
orderBy: ['BRAND_ASC', 'CREATED_AT_DESC'],
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
// Available OrderBy values per table:
|
|
796
|
-
// - PRIMARY_KEY_ASC / PRIMARY_KEY_DESC
|
|
797
|
-
// - NATURAL
|
|
798
|
-
// - {FIELD_NAME}_ASC / {FIELD_NAME}_DESC
|
|
799
|
-
```
|
|
800
|
-
|
|
801
|
-
### Pagination
|
|
802
|
-
|
|
803
|
-
```tsx
|
|
804
|
-
// First N records
|
|
805
|
-
useCarsQuery({ first: 10 });
|
|
806
|
-
|
|
807
|
-
// Last N records
|
|
808
|
-
useCarsQuery({ last: 10 });
|
|
809
|
-
|
|
810
|
-
// Offset pagination
|
|
811
|
-
useCarsQuery({ first: 10, offset: 20 }); // Skip 20, take 10
|
|
812
|
-
|
|
813
|
-
// Cursor-based pagination
|
|
814
|
-
function PaginatedList() {
|
|
815
|
-
const [cursor, setCursor] = useState<string | null>(null);
|
|
816
|
-
|
|
817
|
-
const { data } = useCarsQuery({
|
|
818
|
-
first: 10,
|
|
819
|
-
after: cursor,
|
|
820
|
-
});
|
|
821
|
-
|
|
822
|
-
return (
|
|
823
|
-
<div>
|
|
824
|
-
{data?.cars.nodes.map((car) => (
|
|
825
|
-
<div key={car.id}>{car.brand}</div>
|
|
826
|
-
))}
|
|
827
|
-
|
|
828
|
-
{data?.cars.pageInfo.hasNextPage && (
|
|
829
|
-
<button onClick={() => setCursor(data.cars.pageInfo.endCursor)}>
|
|
830
|
-
Load More
|
|
831
|
-
</button>
|
|
832
|
-
)}
|
|
833
|
-
</div>
|
|
834
|
-
);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// PageInfo structure
|
|
838
|
-
// {
|
|
839
|
-
// hasNextPage: boolean;
|
|
840
|
-
// hasPreviousPage: boolean;
|
|
841
|
-
// startCursor: string | null;
|
|
842
|
-
// endCursor: string | null;
|
|
843
|
-
// }
|
|
844
|
-
```
|
|
845
|
-
|
|
846
|
-
### React Query Options
|
|
847
|
-
|
|
848
|
-
All hooks accept standard React Query options:
|
|
849
|
-
|
|
850
|
-
```tsx
|
|
851
|
-
// Query hooks
|
|
852
|
-
useCarsQuery(
|
|
853
|
-
{ first: 10 }, // Variables
|
|
854
|
-
{
|
|
855
|
-
// React Query options
|
|
856
|
-
enabled: isAuthenticated, // Conditional fetching
|
|
857
|
-
refetchInterval: 30000, // Poll every 30s
|
|
858
|
-
refetchOnWindowFocus: true, // Refetch on tab focus
|
|
859
|
-
staleTime: 5 * 60 * 1000, // Consider fresh for 5 min
|
|
860
|
-
gcTime: 30 * 60 * 1000, // Keep in cache for 30 min
|
|
861
|
-
retry: 3, // Retry failed requests
|
|
862
|
-
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
|
863
|
-
placeholderData: previousData, // Show previous data while loading
|
|
864
|
-
select: (data) => data.cars.nodes, // Transform data
|
|
865
|
-
}
|
|
866
|
-
);
|
|
867
|
-
|
|
868
|
-
// Mutation hooks
|
|
869
|
-
useCreateCarMutation({
|
|
870
|
-
onSuccess: (data, variables, context) => {
|
|
871
|
-
console.log('Created:', data);
|
|
872
|
-
queryClient.invalidateQueries({ queryKey: ['cars'] });
|
|
873
|
-
},
|
|
874
|
-
onError: (error, variables, context) => {
|
|
875
|
-
console.error('Error:', error);
|
|
876
|
-
},
|
|
877
|
-
onSettled: (data, error, variables, context) => {
|
|
878
|
-
console.log('Mutation completed');
|
|
879
|
-
},
|
|
880
|
-
onMutate: async (variables) => {
|
|
881
|
-
// Optimistic update
|
|
882
|
-
await queryClient.cancelQueries({ queryKey: ['cars'] });
|
|
883
|
-
const previousCars = queryClient.getQueryData(['cars']);
|
|
884
|
-
queryClient.setQueryData(['cars'], (old) => ({
|
|
885
|
-
...old,
|
|
886
|
-
cars: {
|
|
887
|
-
...old.cars,
|
|
888
|
-
nodes: [...old.cars.nodes, { id: 'temp', ...variables.input.car }],
|
|
889
|
-
},
|
|
890
|
-
}));
|
|
891
|
-
return { previousCars };
|
|
892
|
-
},
|
|
893
|
-
});
|
|
894
|
-
```
|
|
895
|
-
|
|
896
|
-
### Cache Invalidation
|
|
897
|
-
|
|
898
|
-
```tsx
|
|
899
|
-
import { useQueryClient } from '@tanstack/react-query';
|
|
900
|
-
import { useCreateCarMutation, useCarsQuery } from './generated/hooks';
|
|
901
|
-
|
|
902
|
-
function CreateCarWithInvalidation() {
|
|
903
|
-
const queryClient = useQueryClient();
|
|
904
|
-
|
|
905
|
-
const createCar = useCreateCarMutation({
|
|
906
|
-
onSuccess: () => {
|
|
907
|
-
// Invalidate all car queries to refetch
|
|
908
|
-
queryClient.invalidateQueries({ queryKey: ['cars'] });
|
|
909
|
-
|
|
910
|
-
// Or invalidate specific queries
|
|
911
|
-
queryClient.invalidateQueries({ queryKey: ['cars', { first: 10 }] });
|
|
912
|
-
},
|
|
913
|
-
});
|
|
914
|
-
|
|
915
|
-
// ...
|
|
916
|
-
}
|
|
917
|
-
```
|
|
918
|
-
|
|
919
|
-
### Centralized Query Keys
|
|
920
|
-
|
|
921
|
-
The codegen generates a centralized query key factory following the [lukemorales query-key-factory](https://tanstack.com/query/docs/framework/react/community/lukemorales-query-key-factory) pattern. This provides type-safe cache management with autocomplete support.
|
|
922
|
-
|
|
923
|
-
#### Generated Files
|
|
924
|
-
|
|
925
|
-
| File | Purpose |
|
|
926
|
-
| ------------------ | ------------------------------------------------------- |
|
|
927
|
-
| `query-keys.ts` | Query key factories for all entities |
|
|
928
|
-
| `mutation-keys.ts` | Mutation key factories for tracking in-flight mutations |
|
|
929
|
-
| `invalidation.ts` | Type-safe cache invalidation helpers |
|
|
930
|
-
|
|
931
|
-
#### Using Query Keys
|
|
932
|
-
|
|
933
|
-
```tsx
|
|
934
|
-
import { userKeys, invalidate } from './generated/hooks';
|
|
935
|
-
import { useQueryClient } from '@tanstack/react-query';
|
|
936
|
-
|
|
937
|
-
// Query key structure
|
|
938
|
-
userKeys.all; // ['user']
|
|
939
|
-
userKeys.lists(); // ['user', 'list']
|
|
940
|
-
userKeys.list({ first: 10 }); // ['user', 'list', { first: 10 }]
|
|
941
|
-
userKeys.details(); // ['user', 'detail']
|
|
942
|
-
userKeys.detail('user-123'); // ['user', 'detail', 'user-123']
|
|
943
|
-
|
|
944
|
-
// Granular cache invalidation
|
|
945
|
-
const queryClient = useQueryClient();
|
|
946
|
-
|
|
947
|
-
// Invalidate ALL user queries
|
|
948
|
-
queryClient.invalidateQueries({ queryKey: userKeys.all });
|
|
949
|
-
|
|
950
|
-
// Invalidate only list queries
|
|
951
|
-
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
|
|
952
|
-
|
|
953
|
-
// Invalidate a specific user
|
|
954
|
-
queryClient.invalidateQueries({ queryKey: userKeys.detail(userId) });
|
|
955
|
-
```
|
|
956
|
-
|
|
957
|
-
#### Invalidation Helpers
|
|
958
|
-
|
|
959
|
-
Type-safe invalidation utilities:
|
|
960
|
-
|
|
961
|
-
```tsx
|
|
962
|
-
import { invalidate, remove } from './generated/hooks';
|
|
963
|
-
|
|
964
|
-
// Invalidate queries (triggers refetch)
|
|
965
|
-
invalidate.user.all(queryClient);
|
|
966
|
-
invalidate.user.lists(queryClient);
|
|
967
|
-
invalidate.user.detail(queryClient, userId);
|
|
968
|
-
|
|
969
|
-
// Remove from cache (for delete operations)
|
|
970
|
-
remove.user(queryClient, userId);
|
|
971
|
-
```
|
|
972
|
-
|
|
973
|
-
#### Mutation Key Tracking
|
|
974
|
-
|
|
975
|
-
Track in-flight mutations with `useIsMutating`:
|
|
976
|
-
|
|
977
|
-
```tsx
|
|
978
|
-
import { useIsMutating } from '@tanstack/react-query';
|
|
979
|
-
import { userMutationKeys } from './generated/hooks';
|
|
980
|
-
|
|
981
|
-
function UserList() {
|
|
982
|
-
// Check if any user mutations are in progress
|
|
983
|
-
const isMutating = useIsMutating({ mutationKey: userMutationKeys.all });
|
|
984
|
-
|
|
985
|
-
// Check if a specific user is being deleted
|
|
986
|
-
const isDeleting = useIsMutating({
|
|
987
|
-
mutationKey: userMutationKeys.delete(userId),
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
return (
|
|
991
|
-
<div>
|
|
992
|
-
{isMutating > 0 && <Spinner />}
|
|
993
|
-
<button disabled={isDeleting > 0}>Delete</button>
|
|
994
|
-
</div>
|
|
995
|
-
);
|
|
996
|
-
}
|
|
997
|
-
```
|
|
998
|
-
|
|
999
|
-
#### Optimistic Updates with Query Keys
|
|
1000
|
-
|
|
1001
|
-
```tsx
|
|
1002
|
-
import { useCreateUserMutation, userKeys } from './generated/hooks';
|
|
1003
|
-
|
|
1004
|
-
const createUser = useCreateUserMutation({
|
|
1005
|
-
onMutate: async (newUser) => {
|
|
1006
|
-
// Cancel outgoing refetches
|
|
1007
|
-
await queryClient.cancelQueries({ queryKey: userKeys.lists() });
|
|
1008
|
-
|
|
1009
|
-
// Snapshot previous value
|
|
1010
|
-
const previous = queryClient.getQueryData(userKeys.list());
|
|
1011
|
-
|
|
1012
|
-
// Optimistically update cache
|
|
1013
|
-
queryClient.setQueryData(userKeys.list(), (old) => ({
|
|
1014
|
-
...old,
|
|
1015
|
-
users: {
|
|
1016
|
-
...old.users,
|
|
1017
|
-
nodes: [...old.users.nodes, { id: 'temp', ...newUser.input.user }],
|
|
1018
|
-
},
|
|
1019
|
-
}));
|
|
1020
|
-
|
|
1021
|
-
return { previous };
|
|
1022
|
-
},
|
|
1023
|
-
onError: (err, variables, context) => {
|
|
1024
|
-
// Rollback on error
|
|
1025
|
-
queryClient.setQueryData(userKeys.list(), context.previous);
|
|
1026
|
-
},
|
|
1027
|
-
onSettled: () => {
|
|
1028
|
-
// Refetch after mutation
|
|
1029
|
-
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
|
|
1030
|
-
},
|
|
1031
|
-
});
|
|
1032
|
-
```
|
|
1033
|
-
|
|
1034
|
-
#### Configuration
|
|
1035
|
-
|
|
1036
|
-
Query key generation is enabled by default. Configure in your config file:
|
|
1037
|
-
|
|
1038
|
-
```typescript
|
|
1039
|
-
// graphql-sdk.config.ts
|
|
1040
|
-
export default defineConfig({
|
|
1041
|
-
endpoint: 'https://api.example.com/graphql',
|
|
1042
|
-
|
|
1043
|
-
queryKeys: {
|
|
1044
|
-
// Generate scope-aware keys (default: true)
|
|
1045
|
-
generateScopedKeys: true,
|
|
1046
|
-
|
|
1047
|
-
// Generate mutation keys (default: true)
|
|
1048
|
-
generateMutationKeys: true,
|
|
1049
|
-
|
|
1050
|
-
// Generate invalidation helpers (default: true)
|
|
1051
|
-
generateCascadeHelpers: true,
|
|
1052
|
-
|
|
1053
|
-
// Define entity relationships for cascade invalidation
|
|
1054
|
-
relationships: {
|
|
1055
|
-
table: { parent: 'database', foreignKey: 'databaseId' },
|
|
1056
|
-
field: { parent: 'table', foreignKey: 'tableId' },
|
|
1057
|
-
},
|
|
1058
|
-
},
|
|
1059
|
-
});
|
|
1060
|
-
```
|
|
1061
|
-
|
|
1062
|
-
For detailed documentation on query key factory design and implementation, see [docs/QUERY-KEY-FACTORY.md](./docs/QUERY-KEY-FACTORY.md).
|
|
1063
|
-
|
|
1064
|
-
### Prefetching
|
|
1065
|
-
|
|
1066
|
-
```tsx
|
|
1067
|
-
import { useQueryClient } from '@tanstack/react-query';
|
|
1068
|
-
|
|
1069
|
-
function CarListItem({ car }: { car: Car }) {
|
|
1070
|
-
const queryClient = useQueryClient();
|
|
1071
|
-
|
|
1072
|
-
// Prefetch details on hover
|
|
1073
|
-
const handleHover = () => {
|
|
1074
|
-
queryClient.prefetchQuery({
|
|
1075
|
-
queryKey: ['car', { id: car.id }],
|
|
1076
|
-
queryFn: () => execute(carQuery, { id: car.id }),
|
|
1077
|
-
});
|
|
1078
|
-
};
|
|
1079
|
-
|
|
1080
|
-
return (
|
|
1081
|
-
<Link to={`/cars/${car.id}`} onMouseEnter={handleHover}>
|
|
1082
|
-
{car.brand}
|
|
1083
|
-
</Link>
|
|
1084
|
-
);
|
|
1085
|
-
}
|
|
1086
|
-
```
|
|
1087
|
-
|
|
1088
|
-
### Type Exports
|
|
1089
|
-
|
|
1090
|
-
All generated types are exported for use in your application:
|
|
1091
|
-
|
|
1092
|
-
```tsx
|
|
1093
|
-
import type {
|
|
1094
|
-
// Entity types
|
|
1095
|
-
Car,
|
|
1096
|
-
User,
|
|
1097
|
-
Product,
|
|
1098
|
-
Order,
|
|
1099
|
-
|
|
1100
|
-
// Filter types
|
|
1101
|
-
CarFilter,
|
|
1102
|
-
UserFilter,
|
|
1103
|
-
StringFilter,
|
|
1104
|
-
IntFilter,
|
|
1105
|
-
UUIDFilter,
|
|
1106
|
-
DatetimeFilter,
|
|
1107
|
-
|
|
1108
|
-
// OrderBy types
|
|
1109
|
-
CarsOrderBy,
|
|
1110
|
-
UsersOrderBy,
|
|
1111
|
-
|
|
1112
|
-
// Input types
|
|
1113
|
-
CreateCarInput,
|
|
1114
|
-
UpdateCarInput,
|
|
1115
|
-
CarPatch,
|
|
1116
|
-
LoginInput,
|
|
1117
|
-
|
|
1118
|
-
// Payload types
|
|
1119
|
-
LoginPayload,
|
|
1120
|
-
CreateCarPayload,
|
|
1121
|
-
} from './generated/hooks';
|
|
1122
|
-
|
|
1123
|
-
// Use in your components
|
|
1124
|
-
interface CarListProps {
|
|
1125
|
-
filter?: CarFilter;
|
|
1126
|
-
orderBy?: CarsOrderBy[];
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
function CarList({ filter, orderBy }: CarListProps) {
|
|
1130
|
-
const { data } = useCarsQuery({ filter, orderBy, first: 10 });
|
|
1131
|
-
// ...
|
|
1132
|
-
}
|
|
1133
|
-
```
|
|
1134
|
-
|
|
1135
|
-
### Error Handling
|
|
1136
|
-
|
|
1137
|
-
```tsx
|
|
1138
|
-
function CarList() {
|
|
1139
|
-
const { data, isLoading, isError, error, failureCount } = useCarsQuery({
|
|
1140
|
-
first: 10,
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
if (isLoading) {
|
|
1144
|
-
return <div>Loading...</div>;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
if (isError) {
|
|
1148
|
-
// error is typed as Error
|
|
1149
|
-
return (
|
|
1150
|
-
<div>
|
|
1151
|
-
<p>Error: {error.message}</p>
|
|
1152
|
-
<p>Failed {failureCount} times</p>
|
|
1153
|
-
<button onClick={() => refetch()}>Retry</button>
|
|
1154
|
-
</div>
|
|
1155
|
-
);
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
return <div>{/* render data */}</div>;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
// Global error handling
|
|
1162
|
-
const queryClient = new QueryClient({
|
|
1163
|
-
defaultOptions: {
|
|
1164
|
-
queries: {
|
|
1165
|
-
onError: (error) => {
|
|
1166
|
-
console.error('Query error:', error);
|
|
1167
|
-
// Show toast, log to monitoring, etc.
|
|
1168
|
-
},
|
|
1169
|
-
},
|
|
1170
|
-
mutations: {
|
|
1171
|
-
onError: (error) => {
|
|
1172
|
-
console.error('Mutation error:', error);
|
|
1173
|
-
},
|
|
1174
|
-
},
|
|
1175
|
-
},
|
|
1176
|
-
});
|
|
1177
|
-
```
|
|
1178
|
-
|
|
1179
|
-
### Generated Types Reference
|
|
1180
|
-
|
|
1181
|
-
```typescript
|
|
1182
|
-
// Query hook return type
|
|
1183
|
-
type UseQueryResult<TData> = {
|
|
1184
|
-
data: TData | undefined;
|
|
1185
|
-
error: Error | null;
|
|
1186
|
-
isLoading: boolean;
|
|
1187
|
-
isFetching: boolean;
|
|
1188
|
-
isError: boolean;
|
|
1189
|
-
isSuccess: boolean;
|
|
1190
|
-
refetch: () => Promise<QueryObserverResult<TData>>;
|
|
1191
|
-
// ... more React Query properties
|
|
1192
|
-
};
|
|
1193
|
-
|
|
1194
|
-
// Mutation hook return type
|
|
1195
|
-
type UseMutationResult<TData, TVariables> = {
|
|
1196
|
-
data: TData | undefined;
|
|
1197
|
-
error: Error | null;
|
|
1198
|
-
isLoading: boolean; // deprecated, use isPending
|
|
1199
|
-
isPending: boolean;
|
|
1200
|
-
isError: boolean;
|
|
1201
|
-
isSuccess: boolean;
|
|
1202
|
-
mutate: (variables: TVariables) => void;
|
|
1203
|
-
mutateAsync: (variables: TVariables) => Promise<TData>;
|
|
1204
|
-
reset: () => void;
|
|
1205
|
-
// ... more React Query properties
|
|
1206
|
-
};
|
|
1207
|
-
|
|
1208
|
-
// Connection result (for list queries)
|
|
1209
|
-
interface CarsConnection {
|
|
1210
|
-
nodes: Car[];
|
|
1211
|
-
totalCount: number;
|
|
1212
|
-
pageInfo: PageInfo;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
interface PageInfo {
|
|
1216
|
-
hasNextPage: boolean;
|
|
1217
|
-
hasPreviousPage: boolean;
|
|
1218
|
-
startCursor: string | null;
|
|
1219
|
-
endCursor: string | null;
|
|
1220
|
-
}
|
|
1221
|
-
```
|
|
1222
|
-
|
|
1223
|
-
---
|
|
401
|
+
// Invalidation helpers
|
|
402
|
+
const queryClient = useQueryClient();
|
|
403
|
+
invalidate.user.all(queryClient);
|
|
404
|
+
invalidate.user.lists(queryClient);
|
|
405
|
+
invalidate.user.detail(queryClient, userId);
|
|
406
|
+
```
|
|
1224
407
|
|
|
1225
408
|
## ORM Client
|
|
1226
409
|
|
|
1227
|
-
The ORM client provides a Prisma-like fluent API for GraphQL operations without React dependencies.
|
|
1228
|
-
|
|
1229
|
-
### Generated Output Structure
|
|
1230
|
-
|
|
1231
|
-
```
|
|
1232
|
-
generated/orm/
|
|
1233
|
-
├── index.ts # createClient() factory + re-exports
|
|
1234
|
-
├── client.ts # OrmClient class (GraphQL executor)
|
|
1235
|
-
├── query-builder.ts # QueryBuilder with execute(), unwrap(), etc.
|
|
1236
|
-
├── select-types.ts # Type utilities for select inference
|
|
1237
|
-
├── input-types.ts # All generated types (entities, filters, inputs, etc.)
|
|
1238
|
-
├── types.ts # Re-exports from input-types
|
|
1239
|
-
├── models/
|
|
1240
|
-
│ ├── index.ts # Barrel export for all models
|
|
1241
|
-
│ ├── user.ts # UserModel class
|
|
1242
|
-
│ ├── product.ts # ProductModel class
|
|
1243
|
-
│ ├── order.ts # OrderModel class
|
|
1244
|
-
│ └── ...
|
|
1245
|
-
├── query/
|
|
1246
|
-
│ └── index.ts # Custom query operations (currentUser, etc.)
|
|
1247
|
-
└── mutation/
|
|
1248
|
-
└── index.ts # Custom mutation operations (login, register, etc.)
|
|
1249
|
-
```
|
|
1250
|
-
|
|
1251
|
-
### Basic Usage
|
|
1252
|
-
|
|
1253
|
-
```typescript
|
|
1254
|
-
import { createClient } from './generated/orm';
|
|
1255
|
-
|
|
1256
|
-
// Create client instance
|
|
1257
|
-
const db = createClient({
|
|
1258
|
-
endpoint: 'https://api.example.com/graphql',
|
|
1259
|
-
headers: { Authorization: 'Bearer <token>' },
|
|
1260
|
-
});
|
|
1261
|
-
|
|
1262
|
-
// Query users
|
|
1263
|
-
const result = await db.user
|
|
1264
|
-
.findMany({
|
|
1265
|
-
select: { id: true, username: true, email: true },
|
|
1266
|
-
first: 20,
|
|
1267
|
-
})
|
|
1268
|
-
.execute();
|
|
1269
|
-
|
|
1270
|
-
if (result.ok) {
|
|
1271
|
-
console.log(result.data.users.nodes);
|
|
1272
|
-
} else {
|
|
1273
|
-
console.error(result.errors);
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
// Find first matching user
|
|
1277
|
-
const user = await db.user
|
|
1278
|
-
.findFirst({
|
|
1279
|
-
select: { id: true, username: true },
|
|
1280
|
-
where: { username: { equalTo: 'john' } },
|
|
1281
|
-
})
|
|
1282
|
-
.execute();
|
|
1283
|
-
|
|
1284
|
-
// Create a user
|
|
1285
|
-
const newUser = await db.user
|
|
1286
|
-
.create({
|
|
1287
|
-
data: { username: 'john', email: 'john@example.com' },
|
|
1288
|
-
select: { id: true, username: true },
|
|
1289
|
-
})
|
|
1290
|
-
.execute();
|
|
1291
|
-
|
|
1292
|
-
// Update a user
|
|
1293
|
-
const updated = await db.user
|
|
1294
|
-
.update({
|
|
1295
|
-
where: { id: 'user-id' },
|
|
1296
|
-
data: { displayName: 'John Doe' },
|
|
1297
|
-
select: { id: true, displayName: true },
|
|
1298
|
-
})
|
|
1299
|
-
.execute();
|
|
1300
|
-
|
|
1301
|
-
// Delete a user
|
|
1302
|
-
const deleted = await db.user
|
|
1303
|
-
.delete({
|
|
1304
|
-
where: { id: 'user-id' },
|
|
1305
|
-
})
|
|
1306
|
-
.execute();
|
|
1307
|
-
```
|
|
1308
|
-
|
|
1309
|
-
### Select & Type Inference
|
|
1310
|
-
|
|
1311
|
-
The ORM uses **const generics** to infer return types based on your select clause. Only the fields you select will be in the return type.
|
|
1312
|
-
|
|
1313
|
-
```typescript
|
|
1314
|
-
// Select specific fields - return type is narrowed
|
|
1315
|
-
const users = await db.user
|
|
1316
|
-
.findMany({
|
|
1317
|
-
select: { id: true, username: true }, // Only id and username
|
|
1318
|
-
})
|
|
1319
|
-
.unwrap();
|
|
1320
|
-
|
|
1321
|
-
// TypeScript knows the exact shape:
|
|
1322
|
-
// users.users.nodes[0] is { id: string; username: string | null }
|
|
1323
|
-
|
|
1324
|
-
// If you try to access a field you didn't select, TypeScript will error:
|
|
1325
|
-
// users.users.nodes[0].email // Error: Property 'email' does not exist
|
|
1326
|
-
|
|
1327
|
-
// Without select, you get the full entity type
|
|
1328
|
-
const allFields = await db.user.findMany({}).unwrap();
|
|
1329
|
-
// allFields.users.nodes[0] has all User fields
|
|
1330
|
-
```
|
|
1331
|
-
|
|
1332
|
-
### Relations
|
|
1333
|
-
|
|
1334
|
-
Relations are fully typed in Select types. The ORM supports all PostGraphile relation types:
|
|
1335
|
-
|
|
1336
|
-
#### BelongsTo Relations (Single Entity)
|
|
1337
|
-
|
|
1338
|
-
```typescript
|
|
1339
|
-
// Order.customer is a belongsTo relation to User
|
|
1340
|
-
const orders = await db.order
|
|
1341
|
-
.findMany({
|
|
1342
|
-
select: {
|
|
1343
|
-
id: true,
|
|
1344
|
-
orderNumber: true,
|
|
1345
|
-
// Nested select for belongsTo relation
|
|
1346
|
-
customer: {
|
|
1347
|
-
select: { id: true, username: true, displayName: true },
|
|
1348
|
-
},
|
|
1349
|
-
},
|
|
1350
|
-
})
|
|
1351
|
-
.unwrap();
|
|
1352
|
-
|
|
1353
|
-
// TypeScript knows:
|
|
1354
|
-
// orders.orders.nodes[0].customer is { id: string; username: string | null; displayName: string | null }
|
|
1355
|
-
```
|
|
1356
|
-
|
|
1357
|
-
#### HasMany Relations (Connection/Collection)
|
|
1358
|
-
|
|
1359
|
-
```typescript
|
|
1360
|
-
// Order.orderItems is a hasMany relation to OrderItem
|
|
1361
|
-
const orders = await db.order
|
|
1362
|
-
.findMany({
|
|
1363
|
-
select: {
|
|
1364
|
-
id: true,
|
|
1365
|
-
// HasMany with pagination and filtering
|
|
1366
|
-
orderItems: {
|
|
1367
|
-
select: { id: true, quantity: true, price: true },
|
|
1368
|
-
first: 10, // Pagination
|
|
1369
|
-
filter: { quantity: { greaterThan: 0 } }, // Filtering
|
|
1370
|
-
orderBy: ['QUANTITY_DESC'], // Ordering
|
|
1371
|
-
},
|
|
1372
|
-
},
|
|
1373
|
-
})
|
|
1374
|
-
.unwrap();
|
|
1375
|
-
|
|
1376
|
-
// orders.orders.nodes[0].orderItems is a connection:
|
|
1377
|
-
// { nodes: Array<{ id: string; quantity: number | null; price: number | null }>, totalCount: number, pageInfo: PageInfo }
|
|
1378
|
-
```
|
|
1379
|
-
|
|
1380
|
-
#### ManyToMany Relations
|
|
1381
|
-
|
|
1382
|
-
```typescript
|
|
1383
|
-
// Order.productsByOrderItemOrderIdAndProductId is a manyToMany through OrderItem
|
|
1384
|
-
const orders = await db.order
|
|
1385
|
-
.findMany({
|
|
1386
|
-
select: {
|
|
1387
|
-
id: true,
|
|
1388
|
-
productsByOrderItemOrderIdAndProductId: {
|
|
1389
|
-
select: { id: true, name: true, price: true },
|
|
1390
|
-
first: 5,
|
|
1391
|
-
},
|
|
1392
|
-
},
|
|
1393
|
-
})
|
|
1394
|
-
.unwrap();
|
|
1395
|
-
```
|
|
1396
|
-
|
|
1397
|
-
#### Deeply Nested Relations
|
|
1398
|
-
|
|
1399
|
-
```typescript
|
|
1400
|
-
// Multiple levels of nesting
|
|
1401
|
-
const products = await db.product
|
|
1402
|
-
.findMany({
|
|
1403
|
-
select: {
|
|
1404
|
-
id: true,
|
|
1405
|
-
name: true,
|
|
1406
|
-
// BelongsTo: Product -> User (seller)
|
|
1407
|
-
seller: {
|
|
1408
|
-
select: {
|
|
1409
|
-
id: true,
|
|
1410
|
-
username: true,
|
|
1411
|
-
// Even deeper nesting if needed
|
|
1412
|
-
},
|
|
1413
|
-
},
|
|
1414
|
-
// BelongsTo: Product -> Category
|
|
1415
|
-
category: {
|
|
1416
|
-
select: { id: true, name: true },
|
|
1417
|
-
},
|
|
1418
|
-
// HasMany: Product -> Review
|
|
1419
|
-
reviews: {
|
|
1420
|
-
select: {
|
|
1421
|
-
id: true,
|
|
1422
|
-
rating: true,
|
|
1423
|
-
comment: true,
|
|
1424
|
-
},
|
|
1425
|
-
first: 5,
|
|
1426
|
-
orderBy: ['CREATED_AT_DESC'],
|
|
1427
|
-
},
|
|
1428
|
-
},
|
|
1429
|
-
})
|
|
1430
|
-
.unwrap();
|
|
1431
|
-
```
|
|
1432
|
-
|
|
1433
|
-
### Filtering & Ordering
|
|
1434
|
-
|
|
1435
|
-
#### Filter Types
|
|
1436
|
-
|
|
1437
|
-
Each entity has a generated Filter type with field-specific operators:
|
|
1438
|
-
|
|
1439
|
-
```typescript
|
|
1440
|
-
// String filters
|
|
1441
|
-
where: {
|
|
1442
|
-
username: {
|
|
1443
|
-
equalTo: 'john',
|
|
1444
|
-
notEqualTo: 'jane',
|
|
1445
|
-
in: ['john', 'jane', 'bob'],
|
|
1446
|
-
notIn: ['admin'],
|
|
1447
|
-
contains: 'oh', // LIKE '%oh%'
|
|
1448
|
-
startsWith: 'j', // LIKE 'j%'
|
|
1449
|
-
endsWith: 'n', // LIKE '%n'
|
|
1450
|
-
includesInsensitive: 'OH', // Case-insensitive
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
// Number filters (Int, Float, BigInt, BigFloat)
|
|
1455
|
-
where: {
|
|
1456
|
-
price: {
|
|
1457
|
-
equalTo: 100,
|
|
1458
|
-
greaterThan: 50,
|
|
1459
|
-
greaterThanOrEqualTo: 50,
|
|
1460
|
-
lessThan: 200,
|
|
1461
|
-
lessThanOrEqualTo: 200,
|
|
1462
|
-
in: [100, 200, 300],
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
// Boolean filters
|
|
1467
|
-
where: {
|
|
1468
|
-
isActive: { equalTo: true }
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
// UUID filters
|
|
1472
|
-
where: {
|
|
1473
|
-
id: {
|
|
1474
|
-
equalTo: 'uuid-string',
|
|
1475
|
-
in: ['uuid-1', 'uuid-2'],
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
// DateTime filters
|
|
1480
|
-
where: {
|
|
1481
|
-
createdAt: {
|
|
1482
|
-
greaterThan: '2024-01-01T00:00:00Z',
|
|
1483
|
-
lessThan: '2024-12-31T23:59:59Z',
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
// JSON filters
|
|
1488
|
-
where: {
|
|
1489
|
-
metadata: {
|
|
1490
|
-
contains: { key: 'value' },
|
|
1491
|
-
containsKey: 'key',
|
|
1492
|
-
containsAllKeys: ['key1', 'key2'],
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
410
|
+
The ORM client provides a Prisma-like fluent API for GraphQL operations without React dependencies.
|
|
1495
411
|
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
412
|
+
### Generated Output Structure
|
|
413
|
+
|
|
414
|
+
```
|
|
415
|
+
generated/orm/
|
|
416
|
+
├── index.ts # createClient() factory + re-exports
|
|
417
|
+
├── client.ts # OrmClient class (GraphQL executor)
|
|
418
|
+
├── query-builder.ts # QueryBuilder with execute(), unwrap(), etc.
|
|
419
|
+
├── select-types.ts # Type utilities for select inference
|
|
420
|
+
├── input-types.ts # All generated types
|
|
421
|
+
├── models/
|
|
422
|
+
│ ├── index.ts # Barrel export for all models
|
|
423
|
+
│ ├── user.ts # UserModel class
|
|
424
|
+
│ └── ...
|
|
425
|
+
├── query/
|
|
426
|
+
│ └── index.ts # Custom query operations
|
|
427
|
+
└── mutation/
|
|
428
|
+
└── index.ts # Custom mutation operations
|
|
1500
429
|
```
|
|
1501
430
|
|
|
1502
|
-
|
|
431
|
+
### Basic Usage
|
|
1503
432
|
|
|
1504
433
|
```typescript
|
|
1505
|
-
|
|
1506
|
-
where: {
|
|
1507
|
-
isActive: { equalTo: true },
|
|
1508
|
-
username: { startsWith: 'j' }
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
// AND (explicit)
|
|
1512
|
-
where: {
|
|
1513
|
-
and: [
|
|
1514
|
-
{ isActive: { equalTo: true } },
|
|
1515
|
-
{ username: { startsWith: 'j' } }
|
|
1516
|
-
]
|
|
1517
|
-
}
|
|
434
|
+
import { createClient } from './generated/orm';
|
|
1518
435
|
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
{ status: { equalTo: 'PENDING' } }
|
|
1524
|
-
]
|
|
1525
|
-
}
|
|
436
|
+
const db = createClient({
|
|
437
|
+
endpoint: 'https://api.example.com/graphql',
|
|
438
|
+
headers: { Authorization: 'Bearer <token>' },
|
|
439
|
+
});
|
|
1526
440
|
|
|
1527
|
-
//
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
}
|
|
441
|
+
// Query users
|
|
442
|
+
const result = await db.user
|
|
443
|
+
.findMany({
|
|
444
|
+
select: { id: true, username: true, email: true },
|
|
445
|
+
first: 20,
|
|
446
|
+
})
|
|
447
|
+
.execute();
|
|
1531
448
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
{
|
|
1537
|
-
or: [
|
|
1538
|
-
{ role: { equalTo: 'ADMIN' } },
|
|
1539
|
-
{ role: { equalTo: 'MODERATOR' } }
|
|
1540
|
-
]
|
|
1541
|
-
}
|
|
1542
|
-
]
|
|
449
|
+
if (result.ok) {
|
|
450
|
+
console.log(result.data.users.nodes);
|
|
451
|
+
} else {
|
|
452
|
+
console.error(result.errors);
|
|
1543
453
|
}
|
|
1544
|
-
```
|
|
1545
454
|
|
|
1546
|
-
|
|
455
|
+
// Create a user
|
|
456
|
+
const newUser = await db.user
|
|
457
|
+
.create({
|
|
458
|
+
data: { username: 'john', email: 'john@example.com' },
|
|
459
|
+
select: { id: true, username: true },
|
|
460
|
+
})
|
|
461
|
+
.execute();
|
|
1547
462
|
|
|
1548
|
-
|
|
1549
|
-
const
|
|
1550
|
-
.
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
'USERNAME_ASC', // Then alphabetical
|
|
1555
|
-
],
|
|
463
|
+
// Update a user
|
|
464
|
+
const updated = await db.user
|
|
465
|
+
.update({
|
|
466
|
+
where: { id: 'user-id' },
|
|
467
|
+
data: { displayName: 'John Doe' },
|
|
468
|
+
select: { id: true, displayName: true },
|
|
1556
469
|
})
|
|
1557
|
-
.
|
|
470
|
+
.execute();
|
|
1558
471
|
|
|
1559
|
-
//
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
472
|
+
// Delete a user
|
|
473
|
+
const deleted = await db.user
|
|
474
|
+
.delete({ where: { id: 'user-id' } })
|
|
475
|
+
.execute();
|
|
1563
476
|
```
|
|
1564
477
|
|
|
1565
|
-
###
|
|
478
|
+
### Select & Type Inference
|
|
1566
479
|
|
|
1567
|
-
The ORM
|
|
480
|
+
The ORM uses const generics to infer return types based on your select clause:
|
|
1568
481
|
|
|
1569
482
|
```typescript
|
|
1570
|
-
|
|
1571
|
-
const first10 = await db.user
|
|
483
|
+
const users = await db.user
|
|
1572
484
|
.findMany({
|
|
1573
|
-
select: { id: true },
|
|
1574
|
-
first: 10,
|
|
485
|
+
select: { id: true, username: true },
|
|
1575
486
|
})
|
|
1576
487
|
.unwrap();
|
|
1577
488
|
|
|
1578
|
-
//
|
|
1579
|
-
|
|
1580
|
-
.findMany({
|
|
1581
|
-
select: { id: true },
|
|
1582
|
-
last: 10,
|
|
1583
|
-
})
|
|
1584
|
-
.unwrap();
|
|
489
|
+
// TypeScript knows the exact shape:
|
|
490
|
+
// users.users.nodes[0] is { id: string; username: string | null }
|
|
1585
491
|
|
|
1586
|
-
//
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
})
|
|
1592
|
-
.unwrap();
|
|
492
|
+
// Accessing unselected fields is a compile error:
|
|
493
|
+
// users.users.nodes[0].email // Error: Property 'email' does not exist
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Relations
|
|
1593
497
|
|
|
1594
|
-
|
|
498
|
+
Relations are fully typed in Select types:
|
|
1595
499
|
|
|
1596
|
-
|
|
500
|
+
```typescript
|
|
501
|
+
// BelongsTo relation
|
|
502
|
+
const orders = await db.order
|
|
1597
503
|
.findMany({
|
|
1598
|
-
select: {
|
|
1599
|
-
|
|
1600
|
-
|
|
504
|
+
select: {
|
|
505
|
+
id: true,
|
|
506
|
+
customer: {
|
|
507
|
+
select: { id: true, username: true },
|
|
508
|
+
},
|
|
509
|
+
},
|
|
1601
510
|
})
|
|
1602
511
|
.unwrap();
|
|
1603
512
|
|
|
1604
|
-
//
|
|
1605
|
-
const
|
|
513
|
+
// HasMany relation with pagination
|
|
514
|
+
const users = await db.user
|
|
1606
515
|
.findMany({
|
|
1607
|
-
select: {
|
|
1608
|
-
|
|
1609
|
-
|
|
516
|
+
select: {
|
|
517
|
+
id: true,
|
|
518
|
+
orders: {
|
|
519
|
+
select: { id: true, total: true },
|
|
520
|
+
first: 10,
|
|
521
|
+
orderBy: ['CREATED_AT_DESC'],
|
|
522
|
+
},
|
|
523
|
+
},
|
|
1610
524
|
})
|
|
1611
525
|
.unwrap();
|
|
1612
|
-
|
|
1613
|
-
// PageInfo structure
|
|
1614
|
-
// {
|
|
1615
|
-
// hasNextPage: boolean;
|
|
1616
|
-
// hasPreviousPage: boolean;
|
|
1617
|
-
// startCursor: string | null;
|
|
1618
|
-
// endCursor: string | null;
|
|
1619
|
-
// }
|
|
1620
|
-
|
|
1621
|
-
// Total count is always included
|
|
1622
|
-
console.log(page1.users.totalCount); // Total matching records
|
|
1623
526
|
```
|
|
1624
527
|
|
|
1625
528
|
### Error Handling
|
|
1626
529
|
|
|
1627
530
|
The ORM provides multiple ways to handle errors:
|
|
1628
531
|
|
|
1629
|
-
#### Discriminated Union (Recommended)
|
|
1630
|
-
|
|
1631
532
|
```typescript
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
select: { id: true },
|
|
1635
|
-
})
|
|
1636
|
-
.execute();
|
|
533
|
+
// Discriminated union (recommended)
|
|
534
|
+
const result = await db.user.findMany({ select: { id: true } }).execute();
|
|
1637
535
|
|
|
1638
536
|
if (result.ok) {
|
|
1639
|
-
// TypeScript knows result.data is non-null
|
|
1640
537
|
console.log(result.data.users.nodes);
|
|
1641
|
-
// result.errors is undefined in this branch
|
|
1642
538
|
} else {
|
|
1643
|
-
|
|
1644
|
-
console.error(result.errors[0].message);
|
|
1645
|
-
// result.data is null in this branch
|
|
539
|
+
console.error(result.errors);
|
|
1646
540
|
}
|
|
1647
|
-
```
|
|
1648
|
-
|
|
1649
|
-
#### `.unwrap()` - Throw on Error
|
|
1650
|
-
|
|
1651
|
-
```typescript
|
|
1652
|
-
import { GraphQLRequestError } from './generated/orm';
|
|
1653
541
|
|
|
542
|
+
// .unwrap() - throws on error
|
|
1654
543
|
try {
|
|
1655
|
-
|
|
1656
|
-
const data = await db.user
|
|
1657
|
-
.findMany({
|
|
1658
|
-
select: { id: true },
|
|
1659
|
-
})
|
|
1660
|
-
.unwrap();
|
|
1661
|
-
|
|
1662
|
-
console.log(data.users.nodes);
|
|
544
|
+
const data = await db.user.findMany({ select: { id: true } }).unwrap();
|
|
1663
545
|
} catch (error) {
|
|
1664
546
|
if (error instanceof GraphQLRequestError) {
|
|
1665
547
|
console.error('GraphQL errors:', error.errors);
|
|
1666
|
-
console.error('Message:', error.message);
|
|
1667
548
|
}
|
|
1668
549
|
}
|
|
1669
|
-
```
|
|
1670
|
-
|
|
1671
|
-
#### `.unwrapOr()` - Default Value on Error
|
|
1672
550
|
|
|
1673
|
-
|
|
1674
|
-
// Returns default value if query fails (no throwing)
|
|
551
|
+
// .unwrapOr() - returns default on error
|
|
1675
552
|
const data = await db.user
|
|
1676
|
-
.findMany({
|
|
1677
|
-
|
|
1678
|
-
})
|
|
1679
|
-
.unwrapOr({
|
|
1680
|
-
users: {
|
|
1681
|
-
nodes: [],
|
|
1682
|
-
totalCount: 0,
|
|
1683
|
-
pageInfo: { hasNextPage: false, hasPreviousPage: false },
|
|
1684
|
-
},
|
|
1685
|
-
});
|
|
1686
|
-
|
|
1687
|
-
// Always returns data (either real or default)
|
|
1688
|
-
console.log(data.users.nodes);
|
|
1689
|
-
```
|
|
1690
|
-
|
|
1691
|
-
#### `.unwrapOrElse()` - Callback on Error
|
|
553
|
+
.findMany({ select: { id: true } })
|
|
554
|
+
.unwrapOr({ users: { nodes: [], totalCount: 0, pageInfo: { hasNextPage: false, hasPreviousPage: false } } });
|
|
1692
555
|
|
|
1693
|
-
|
|
1694
|
-
// Call a function to handle errors and return fallback
|
|
556
|
+
// .unwrapOrElse() - callback on error
|
|
1695
557
|
const data = await db.user
|
|
1696
|
-
.findMany({
|
|
1697
|
-
select: { id: true },
|
|
1698
|
-
})
|
|
558
|
+
.findMany({ select: { id: true } })
|
|
1699
559
|
.unwrapOrElse((errors) => {
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
// Return fallback data
|
|
1704
|
-
return {
|
|
1705
|
-
users: {
|
|
1706
|
-
nodes: [],
|
|
1707
|
-
totalCount: 0,
|
|
1708
|
-
pageInfo: { hasNextPage: false, hasPreviousPage: false },
|
|
1709
|
-
},
|
|
1710
|
-
};
|
|
560
|
+
console.error('Query failed:', errors);
|
|
561
|
+
return { users: { nodes: [], totalCount: 0, pageInfo: { hasNextPage: false, hasPreviousPage: false } } };
|
|
1711
562
|
});
|
|
1712
563
|
```
|
|
1713
564
|
|
|
1714
|
-
|
|
565
|
+
### Custom Operations
|
|
566
|
+
|
|
567
|
+
Custom queries and mutations are available on `db.query` and `db.mutation`:
|
|
1715
568
|
|
|
1716
569
|
```typescript
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
extensions?: Record<string, unknown>;
|
|
1722
|
-
}
|
|
570
|
+
// Custom query
|
|
571
|
+
const currentUser = await db.query
|
|
572
|
+
.currentUser({ select: { id: true, username: true } })
|
|
573
|
+
.unwrap();
|
|
1723
574
|
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
}
|
|
575
|
+
// Custom mutation
|
|
576
|
+
const login = await db.mutation
|
|
577
|
+
.login(
|
|
578
|
+
{ input: { email: 'user@example.com', password: 'secret' } },
|
|
579
|
+
{ select: { apiToken: { select: { accessToken: true } } } }
|
|
580
|
+
)
|
|
581
|
+
.unwrap();
|
|
1728
582
|
|
|
1729
|
-
|
|
1730
|
-
| { ok: true; data: T; errors: undefined }
|
|
1731
|
-
| { ok: false; data: null; errors: GraphQLError[] };
|
|
583
|
+
console.log(login.login.apiToken?.accessToken);
|
|
1732
584
|
```
|
|
1733
585
|
|
|
1734
|
-
|
|
586
|
+
## Configuration
|
|
1735
587
|
|
|
1736
|
-
|
|
588
|
+
### Config File
|
|
1737
589
|
|
|
1738
|
-
|
|
590
|
+
Create a `graphql-codegen.config.ts` file:
|
|
1739
591
|
|
|
1740
592
|
```typescript
|
|
1741
|
-
|
|
1742
|
-
const currentUser = await db.query
|
|
1743
|
-
.currentUser({
|
|
1744
|
-
select: { id: true, username: true, email: true },
|
|
1745
|
-
})
|
|
1746
|
-
.unwrap();
|
|
1747
|
-
|
|
1748
|
-
// Query without select (returns full type)
|
|
1749
|
-
const me = await db.query.currentUser({}).unwrap();
|
|
593
|
+
import type { GraphQLSDKConfig } from '@constructive-io/graphql-codegen';
|
|
1750
594
|
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
)
|
|
1761
|
-
.unwrap();
|
|
595
|
+
export default {
|
|
596
|
+
endpoint: 'https://api.example.com/graphql',
|
|
597
|
+
output: './generated/graphql',
|
|
598
|
+
headers: {
|
|
599
|
+
Authorization: 'Bearer <token>',
|
|
600
|
+
},
|
|
601
|
+
reactQuery: true,
|
|
602
|
+
orm: true,
|
|
603
|
+
} satisfies GraphQLSDKConfig;
|
|
1762
604
|
```
|
|
1763
605
|
|
|
1764
|
-
|
|
606
|
+
### Multi-target Configuration
|
|
607
|
+
|
|
608
|
+
For multiple schema sources, export a record of named configs:
|
|
1765
609
|
|
|
1766
610
|
```typescript
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
611
|
+
import type { GraphQLSDKMultiConfig } from '@constructive-io/graphql-codegen';
|
|
612
|
+
|
|
613
|
+
export default {
|
|
614
|
+
public: {
|
|
615
|
+
endpoint: 'https://api.example.com/graphql',
|
|
616
|
+
output: './generated/public',
|
|
617
|
+
headers: { Authorization: 'Bearer <token>' },
|
|
618
|
+
reactQuery: true,
|
|
619
|
+
},
|
|
620
|
+
admin: {
|
|
621
|
+
schemaFile: './admin.schema.graphql',
|
|
622
|
+
output: './generated/admin',
|
|
623
|
+
orm: true,
|
|
624
|
+
},
|
|
625
|
+
database: {
|
|
626
|
+
db: {
|
|
627
|
+
pgpm: { modulePath: './packages/my-module' },
|
|
628
|
+
schemas: ['public'],
|
|
1775
629
|
},
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
accessTokenExpiresAt: true,
|
|
1783
|
-
},
|
|
1784
|
-
},
|
|
1785
|
-
},
|
|
1786
|
-
}
|
|
1787
|
-
)
|
|
1788
|
-
.unwrap();
|
|
630
|
+
output: './generated/db',
|
|
631
|
+
reactQuery: true,
|
|
632
|
+
orm: true,
|
|
633
|
+
},
|
|
634
|
+
} satisfies GraphQLSDKMultiConfig;
|
|
635
|
+
```
|
|
1789
636
|
|
|
1790
|
-
|
|
637
|
+
Run all targets with `graphql-codegen` or a specific target with `graphql-codegen --target public`.
|
|
1791
638
|
|
|
1792
|
-
|
|
1793
|
-
const register = await db.mutation
|
|
1794
|
-
.register({
|
|
1795
|
-
input: {
|
|
1796
|
-
email: 'new@example.com',
|
|
1797
|
-
password: 'secret123',
|
|
1798
|
-
username: 'newuser',
|
|
1799
|
-
},
|
|
1800
|
-
})
|
|
1801
|
-
.unwrap();
|
|
639
|
+
### Glob Patterns
|
|
1802
640
|
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
641
|
+
Filter patterns support wildcards:
|
|
642
|
+
|
|
643
|
+
```typescript
|
|
644
|
+
{
|
|
645
|
+
tables: {
|
|
646
|
+
include: ['User', 'Product', 'Order*'],
|
|
647
|
+
exclude: ['*_archive', 'temp_*'],
|
|
648
|
+
},
|
|
649
|
+
queries: {
|
|
650
|
+
exclude: ['_meta', 'query', '*Debug*'],
|
|
651
|
+
},
|
|
652
|
+
mutations: {
|
|
653
|
+
include: ['create*', 'update*', 'delete*', 'login', 'register', 'logout'],
|
|
654
|
+
},
|
|
655
|
+
}
|
|
1809
656
|
```
|
|
1810
657
|
|
|
1811
|
-
|
|
658
|
+
## CLI Commands
|
|
659
|
+
|
|
660
|
+
The CLI provides a convenient way to run code generation from the command line.
|
|
1812
661
|
|
|
1813
|
-
|
|
662
|
+
### `graphql-sdk generate`
|
|
1814
663
|
|
|
1815
|
-
|
|
1816
|
-
const query = db.user.findMany({
|
|
1817
|
-
select: { id: true, username: true },
|
|
1818
|
-
where: { isActive: { equalTo: true } },
|
|
1819
|
-
first: 10,
|
|
1820
|
-
});
|
|
664
|
+
Generate React Query hooks and/or ORM client from various sources.
|
|
1821
665
|
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
666
|
+
```bash
|
|
667
|
+
Source Options (choose one):
|
|
668
|
+
-c, --config <path> Path to config file (graphql-sdk.config.ts)
|
|
669
|
+
-e, --endpoint <url> GraphQL endpoint URL
|
|
670
|
+
-s, --schema-file <path> Path to GraphQL schema file (.graphql)
|
|
671
|
+
--pgpm-module-path <path> Path to PGPM module directory
|
|
672
|
+
--pgpm-workspace-path <path> Path to PGPM workspace (requires --pgpm-module-name)
|
|
673
|
+
--pgpm-module-name <name> PGPM module name in workspace
|
|
674
|
+
|
|
675
|
+
Database Options (for pgpm modes):
|
|
676
|
+
--schemas <list> Comma-separated list of PostgreSQL schemas to introspect
|
|
677
|
+
--api-names <list> Comma-separated API names for automatic schema discovery
|
|
678
|
+
(mutually exclusive with --schemas)
|
|
679
|
+
|
|
680
|
+
Generator Options:
|
|
681
|
+
--react-query Generate React Query hooks
|
|
682
|
+
--orm Generate ORM client
|
|
683
|
+
-t, --target <name> Target name in config file
|
|
684
|
+
-o, --output <dir> Output directory
|
|
685
|
+
-a, --authorization <token> Authorization header value
|
|
686
|
+
--skip-custom-operations Only generate table CRUD operations
|
|
687
|
+
--dry-run Preview without writing files
|
|
688
|
+
--keep-db Keep ephemeral database after generation (pgpm modes)
|
|
689
|
+
-v, --verbose Show detailed output
|
|
690
|
+
|
|
691
|
+
Watch Mode Options:
|
|
692
|
+
-w, --watch Watch for schema changes and regenerate
|
|
693
|
+
--poll-interval <ms> Polling interval in milliseconds (default: 5000)
|
|
694
|
+
--debounce <ms> Debounce delay in milliseconds (default: 500)
|
|
695
|
+
--touch <path> Touch file after regeneration
|
|
696
|
+
--no-clear Don't clear console on regeneration
|
|
1839
697
|
```
|
|
1840
698
|
|
|
1841
|
-
|
|
699
|
+
Examples:
|
|
1842
700
|
|
|
1843
|
-
```
|
|
1844
|
-
|
|
701
|
+
```bash
|
|
702
|
+
# Generate React Query hooks from an endpoint
|
|
703
|
+
npx graphql-sdk generate --endpoint https://api.example.com/graphql --output ./generated --react-query
|
|
1845
704
|
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
endpoint: 'https://api.example.com/graphql',
|
|
1849
|
-
});
|
|
705
|
+
# Generate ORM client from an endpoint
|
|
706
|
+
npx graphql-sdk generate --endpoint https://api.example.com/graphql --output ./generated --orm
|
|
1850
707
|
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
endpoint: 'https://api.example.com/graphql',
|
|
1854
|
-
headers: {
|
|
1855
|
-
Authorization: 'Bearer <token>',
|
|
1856
|
-
'X-Custom-Header': 'value',
|
|
1857
|
-
},
|
|
1858
|
-
});
|
|
708
|
+
# Generate both React Query hooks and ORM client
|
|
709
|
+
npx graphql-sdk generate --endpoint https://api.example.com/graphql --output ./generated --react-query --orm
|
|
1859
710
|
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
Authorization: 'Bearer <new-token>',
|
|
1863
|
-
});
|
|
711
|
+
# Generate from a PGPM module
|
|
712
|
+
npx graphql-sdk generate --pgpm-module-path ./packages/my-module --schemas public --react-query
|
|
1864
713
|
|
|
1865
|
-
|
|
1866
|
-
|
|
714
|
+
# Generate using apiNames for automatic schema discovery
|
|
715
|
+
npx graphql-sdk generate --pgpm-module-path ./packages/my-module --api-names my_api --react-query --orm
|
|
1867
716
|
```
|
|
1868
717
|
|
|
1869
|
-
|
|
718
|
+
### `graphql-sdk init`
|
|
1870
719
|
|
|
1871
|
-
|
|
720
|
+
Create a configuration file.
|
|
1872
721
|
|
|
1873
|
-
|
|
722
|
+
```bash
|
|
723
|
+
Options:
|
|
724
|
+
-f, --format <format> Config format: ts, js, json (default: ts)
|
|
725
|
+
-o, --output <path> Output path for config file
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### `graphql-sdk introspect`
|
|
729
|
+
|
|
730
|
+
Inspect schema without generating code.
|
|
1874
731
|
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
732
|
+
```bash
|
|
733
|
+
Options:
|
|
734
|
+
-e, --endpoint <url> GraphQL endpoint URL
|
|
735
|
+
--json Output as JSON
|
|
736
|
+
-v, --verbose Show detailed output
|
|
737
|
+
```
|
|
1880
738
|
|
|
1881
|
-
|
|
1882
|
-
- All queries (including custom ones like `currentUser`)
|
|
1883
|
-
- All mutations (including custom ones like `login`, `register`)
|
|
1884
|
-
- All types (entities, inputs, enums, scalars)
|
|
739
|
+
## Architecture
|
|
1885
740
|
|
|
1886
|
-
|
|
741
|
+
### How It Works
|
|
1887
742
|
|
|
1888
|
-
|
|
743
|
+
The codegen fetches `_meta` for table metadata (names, fields, relations, constraints) and `__schema` for full schema introspection (all queries, mutations, types). It then filters operations to avoid duplicates and generates type-safe code using Babel AST.
|
|
1889
744
|
|
|
1890
745
|
### Code Generation Pipeline
|
|
1891
746
|
|
|
@@ -1924,68 +779,16 @@ PostGraphile Endpoint
|
|
|
1924
779
|
└───────────────────┘
|
|
1925
780
|
```
|
|
1926
781
|
|
|
1927
|
-
###
|
|
1928
|
-
|
|
1929
|
-
#### Type Inference with Const Generics
|
|
782
|
+
### Type Inference with Const Generics
|
|
1930
783
|
|
|
1931
784
|
The ORM uses TypeScript const generics to infer return types:
|
|
1932
785
|
|
|
1933
786
|
```typescript
|
|
1934
|
-
// Model method signature
|
|
1935
787
|
findMany<const S extends UserSelect>(
|
|
1936
788
|
args?: FindManyArgs<S, UserFilter, UsersOrderBy>
|
|
1937
789
|
): QueryBuilder<{ users: ConnectionResult<InferSelectResult<User, S>> }>
|
|
1938
|
-
|
|
1939
|
-
// InferSelectResult maps select object to result type
|
|
1940
|
-
type InferSelectResult<TEntity, TSelect> = {
|
|
1941
|
-
[K in keyof TSelect & keyof TEntity as TSelect[K] extends false | undefined
|
|
1942
|
-
? never
|
|
1943
|
-
: K]: TSelect[K] extends true
|
|
1944
|
-
? TEntity[K]
|
|
1945
|
-
: TSelect[K] extends { select: infer NestedSelect }
|
|
1946
|
-
? /* handle nested select */
|
|
1947
|
-
: TEntity[K];
|
|
1948
|
-
};
|
|
1949
|
-
```
|
|
1950
|
-
|
|
1951
|
-
#### Select Types with Relations
|
|
1952
|
-
|
|
1953
|
-
Select types include relation fields with proper typing:
|
|
1954
|
-
|
|
1955
|
-
```typescript
|
|
1956
|
-
export type OrderSelect = {
|
|
1957
|
-
// Scalar fields
|
|
1958
|
-
id?: boolean;
|
|
1959
|
-
orderNumber?: boolean;
|
|
1960
|
-
status?: boolean;
|
|
1961
|
-
|
|
1962
|
-
// BelongsTo relation
|
|
1963
|
-
customer?: boolean | { select?: UserSelect };
|
|
1964
|
-
|
|
1965
|
-
// HasMany relation
|
|
1966
|
-
orderItems?:
|
|
1967
|
-
| boolean
|
|
1968
|
-
| {
|
|
1969
|
-
select?: OrderItemSelect;
|
|
1970
|
-
first?: number;
|
|
1971
|
-
filter?: OrderItemFilter;
|
|
1972
|
-
orderBy?: OrderItemsOrderBy[];
|
|
1973
|
-
};
|
|
1974
|
-
|
|
1975
|
-
// ManyToMany relation
|
|
1976
|
-
productsByOrderItemOrderIdAndProductId?:
|
|
1977
|
-
| boolean
|
|
1978
|
-
| {
|
|
1979
|
-
select?: ProductSelect;
|
|
1980
|
-
first?: number;
|
|
1981
|
-
filter?: ProductFilter;
|
|
1982
|
-
orderBy?: ProductsOrderBy[];
|
|
1983
|
-
};
|
|
1984
|
-
};
|
|
1985
790
|
```
|
|
1986
791
|
|
|
1987
|
-
---
|
|
1988
|
-
|
|
1989
792
|
## Generated Types
|
|
1990
793
|
|
|
1991
794
|
### Entity Types
|
|
@@ -2024,7 +827,6 @@ export interface StringFilter {
|
|
|
2024
827
|
contains?: string;
|
|
2025
828
|
startsWith?: string;
|
|
2026
829
|
endsWith?: string;
|
|
2027
|
-
// ... more operators
|
|
2028
830
|
}
|
|
2029
831
|
```
|
|
2030
832
|
|
|
@@ -2060,35 +862,8 @@ export interface UpdateUserInput {
|
|
|
2060
862
|
id: string;
|
|
2061
863
|
patch: UserPatch;
|
|
2062
864
|
}
|
|
2063
|
-
|
|
2064
|
-
export interface UserPatch {
|
|
2065
|
-
username?: string | null;
|
|
2066
|
-
email?: string | null;
|
|
2067
|
-
displayName?: string | null;
|
|
2068
|
-
}
|
|
2069
|
-
```
|
|
2070
|
-
|
|
2071
|
-
### Payload Types (Custom Operations)
|
|
2072
|
-
|
|
2073
|
-
```typescript
|
|
2074
|
-
export interface LoginPayload {
|
|
2075
|
-
clientMutationId?: string | null;
|
|
2076
|
-
apiToken?: ApiToken | null;
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
export interface ApiToken {
|
|
2080
|
-
accessToken: string;
|
|
2081
|
-
accessTokenExpiresAt?: string | null;
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
export type LoginPayloadSelect = {
|
|
2085
|
-
clientMutationId?: boolean;
|
|
2086
|
-
apiToken?: boolean | { select?: ApiTokenSelect };
|
|
2087
|
-
};
|
|
2088
865
|
```
|
|
2089
866
|
|
|
2090
|
-
---
|
|
2091
|
-
|
|
2092
867
|
## Development
|
|
2093
868
|
|
|
2094
869
|
```bash
|
|
@@ -2101,27 +876,6 @@ pnpm build
|
|
|
2101
876
|
# Run in watch mode
|
|
2102
877
|
pnpm dev
|
|
2103
878
|
|
|
2104
|
-
# Test React Query hooks generation
|
|
2105
|
-
node bin/graphql-sdk.js generate \
|
|
2106
|
-
-e http://public-0e394519.localhost:3000/graphql \
|
|
2107
|
-
-o ./output-rq \
|
|
2108
|
-
--verbose
|
|
2109
|
-
|
|
2110
|
-
# Test ORM client generation
|
|
2111
|
-
node bin/graphql-sdk.js generate-orm \
|
|
2112
|
-
-e http://public-0e394519.localhost:3000/graphql \
|
|
2113
|
-
-o ./output-orm \
|
|
2114
|
-
--verbose
|
|
2115
|
-
|
|
2116
|
-
# Type check generated output
|
|
2117
|
-
npx tsc --noEmit output-orm/*.ts output-orm/**/*.ts \
|
|
2118
|
-
--skipLibCheck --target ES2022 --module ESNext \
|
|
2119
|
-
--moduleResolution bundler --strict
|
|
2120
|
-
|
|
2121
|
-
# Run example tests
|
|
2122
|
-
npx tsx examples/test-orm.ts
|
|
2123
|
-
npx tsx examples/type-inference-test.ts
|
|
2124
|
-
|
|
2125
879
|
# Type check
|
|
2126
880
|
pnpm lint:types
|
|
2127
881
|
|
|
@@ -2129,22 +883,6 @@ pnpm lint:types
|
|
|
2129
883
|
pnpm test
|
|
2130
884
|
```
|
|
2131
885
|
|
|
2132
|
-
---
|
|
2133
|
-
|
|
2134
|
-
## Roadmap
|
|
2135
|
-
|
|
2136
|
-
- [x] **Relations**: Typed nested select with relation loading
|
|
2137
|
-
- [x] **Type Inference**: Const generics for narrowed return types
|
|
2138
|
-
- [x] **Error Handling**: Discriminated unions with unwrap methods
|
|
2139
|
-
- [ ] **Aggregations**: Count, sum, avg operations
|
|
2140
|
-
- [ ] **Batch Operations**: Bulk create/update/delete
|
|
2141
|
-
- [ ] **Transactions**: Transaction support where available
|
|
2142
|
-
- [ ] **Subscriptions**: Real-time subscription support
|
|
2143
|
-
- [ ] **Custom Scalars**: Better handling of PostGraphile custom types
|
|
2144
|
-
- [ ] **Query Caching**: Optional caching layer for ORM client
|
|
2145
|
-
- [ ] **Middleware**: Request/response interceptors
|
|
2146
|
-
- [ ] **Connection Pooling**: For high-throughput scenarios
|
|
2147
|
-
|
|
2148
886
|
## Requirements
|
|
2149
887
|
|
|
2150
888
|
- Node.js >= 18
|