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