@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.
Files changed (301) hide show
  1. package/README.md +1818 -113
  2. package/__tests__/codegen/input-types-generator.test.d.ts +1 -0
  3. package/__tests__/codegen/input-types-generator.test.js +635 -0
  4. package/cli/codegen/barrel.d.ts +27 -0
  5. package/cli/codegen/barrel.js +163 -0
  6. package/cli/codegen/client.d.ts +4 -0
  7. package/cli/codegen/client.js +170 -0
  8. package/cli/codegen/custom-mutations.d.ts +38 -0
  9. package/cli/codegen/custom-mutations.js +149 -0
  10. package/cli/codegen/custom-queries.d.ts +38 -0
  11. package/cli/codegen/custom-queries.js +358 -0
  12. package/cli/codegen/filters.d.ts +27 -0
  13. package/cli/codegen/filters.js +357 -0
  14. package/cli/codegen/gql-ast.d.ts +41 -0
  15. package/cli/codegen/gql-ast.js +329 -0
  16. package/cli/codegen/index.d.ts +71 -0
  17. package/cli/codegen/index.js +147 -0
  18. package/cli/codegen/mutations.d.ts +30 -0
  19. package/cli/codegen/mutations.js +410 -0
  20. package/cli/codegen/orm/barrel.d.ts +18 -0
  21. package/cli/codegen/orm/barrel.js +48 -0
  22. package/cli/codegen/orm/client-generator.d.ts +45 -0
  23. package/cli/codegen/orm/client-generator.js +646 -0
  24. package/cli/codegen/orm/custom-ops-generator.d.ts +30 -0
  25. package/cli/codegen/orm/custom-ops-generator.js +350 -0
  26. package/cli/codegen/orm/index.d.ts +38 -0
  27. package/cli/codegen/orm/index.js +88 -0
  28. package/cli/codegen/orm/input-types-generator.d.ts +21 -0
  29. package/cli/codegen/orm/input-types-generator.js +705 -0
  30. package/cli/codegen/orm/input-types-generator.test.d.ts +1 -0
  31. package/cli/codegen/orm/input-types-generator.test.js +75 -0
  32. package/cli/codegen/orm/model-generator.d.ts +32 -0
  33. package/cli/codegen/orm/model-generator.js +264 -0
  34. package/cli/codegen/orm/query-builder.d.ts +161 -0
  35. package/cli/codegen/orm/query-builder.js +366 -0
  36. package/cli/codegen/orm/select-types.d.ts +169 -0
  37. package/cli/codegen/orm/select-types.js +16 -0
  38. package/cli/codegen/orm/select-types.test.d.ts +11 -0
  39. package/cli/codegen/orm/select-types.test.js +22 -0
  40. package/cli/codegen/queries.d.ts +25 -0
  41. package/cli/codegen/queries.js +438 -0
  42. package/cli/codegen/scalars.d.ts +12 -0
  43. package/cli/codegen/scalars.js +71 -0
  44. package/cli/codegen/schema-gql-ast.d.ts +51 -0
  45. package/cli/codegen/schema-gql-ast.js +385 -0
  46. package/cli/codegen/ts-ast.d.ts +122 -0
  47. package/cli/codegen/ts-ast.js +280 -0
  48. package/cli/codegen/type-resolver.d.ts +96 -0
  49. package/cli/codegen/type-resolver.js +246 -0
  50. package/cli/codegen/types.d.ts +12 -0
  51. package/cli/codegen/types.js +69 -0
  52. package/cli/codegen/utils.d.ts +163 -0
  53. package/cli/codegen/utils.js +326 -0
  54. package/cli/commands/generate-orm.d.ts +37 -0
  55. package/cli/commands/generate-orm.js +195 -0
  56. package/cli/commands/generate.d.ts +39 -0
  57. package/cli/commands/generate.js +299 -0
  58. package/cli/commands/index.d.ts +7 -0
  59. package/cli/commands/index.js +12 -0
  60. package/cli/commands/init.d.ts +35 -0
  61. package/cli/commands/init.js +176 -0
  62. package/cli/index.d.ts +4 -0
  63. package/cli/index.js +291 -0
  64. package/cli/introspect/fetch-meta.d.ts +31 -0
  65. package/cli/introspect/fetch-meta.js +108 -0
  66. package/cli/introspect/fetch-schema.d.ts +21 -0
  67. package/cli/introspect/fetch-schema.js +86 -0
  68. package/cli/introspect/index.d.ts +8 -0
  69. package/cli/introspect/index.js +16 -0
  70. package/cli/introspect/meta-query.d.ts +111 -0
  71. package/cli/introspect/meta-query.js +191 -0
  72. package/cli/introspect/schema-query.d.ts +20 -0
  73. package/cli/introspect/schema-query.js +123 -0
  74. package/cli/introspect/transform-schema.d.ts +74 -0
  75. package/cli/introspect/transform-schema.js +269 -0
  76. package/cli/introspect/transform-schema.test.d.ts +1 -0
  77. package/cli/introspect/transform-schema.test.js +67 -0
  78. package/cli/introspect/transform.d.ts +21 -0
  79. package/cli/introspect/transform.js +216 -0
  80. package/cli/watch/cache.d.ts +45 -0
  81. package/cli/watch/cache.js +111 -0
  82. package/cli/watch/debounce.d.ts +19 -0
  83. package/cli/watch/debounce.js +89 -0
  84. package/cli/watch/hash.d.ts +17 -0
  85. package/cli/watch/hash.js +48 -0
  86. package/cli/watch/index.d.ts +10 -0
  87. package/cli/watch/index.js +22 -0
  88. package/cli/watch/orchestrator.d.ts +63 -0
  89. package/cli/watch/orchestrator.js +228 -0
  90. package/cli/watch/poller.d.ts +65 -0
  91. package/cli/watch/poller.js +203 -0
  92. package/cli/watch/types.d.ts +67 -0
  93. package/cli/watch/types.js +5 -0
  94. package/client/error.d.ts +95 -0
  95. package/client/error.js +255 -0
  96. package/client/execute.d.ts +57 -0
  97. package/client/execute.js +124 -0
  98. package/client/index.d.ts +6 -0
  99. package/client/index.js +18 -0
  100. package/client/typed-document.d.ts +31 -0
  101. package/client/typed-document.js +44 -0
  102. package/core/ast.d.ts +10 -0
  103. package/core/ast.js +593 -0
  104. package/core/custom-ast.d.ts +35 -0
  105. package/core/custom-ast.js +204 -0
  106. package/core/index.d.ts +8 -0
  107. package/core/index.js +33 -0
  108. package/core/meta-object/convert.d.ts +65 -0
  109. package/core/meta-object/convert.js +63 -0
  110. package/core/meta-object/format.json +93 -0
  111. package/core/meta-object/index.d.ts +2 -0
  112. package/core/meta-object/index.js +18 -0
  113. package/core/meta-object/validate.d.ts +9 -0
  114. package/core/meta-object/validate.js +34 -0
  115. package/core/query-builder.d.ts +46 -0
  116. package/core/query-builder.js +412 -0
  117. package/core/types.d.ts +139 -0
  118. package/core/types.js +28 -0
  119. package/esm/__tests__/codegen/input-types-generator.test.d.ts +1 -0
  120. package/esm/__tests__/codegen/input-types-generator.test.js +633 -0
  121. package/esm/cli/codegen/barrel.d.ts +27 -0
  122. package/esm/cli/codegen/barrel.js +156 -0
  123. package/esm/cli/codegen/client.d.ts +4 -0
  124. package/esm/cli/codegen/client.js +167 -0
  125. package/esm/cli/codegen/custom-mutations.d.ts +38 -0
  126. package/esm/cli/codegen/custom-mutations.js +145 -0
  127. package/esm/cli/codegen/custom-queries.d.ts +38 -0
  128. package/esm/cli/codegen/custom-queries.js +354 -0
  129. package/esm/cli/codegen/filters.d.ts +27 -0
  130. package/esm/cli/codegen/filters.js +351 -0
  131. package/esm/cli/codegen/gql-ast.d.ts +41 -0
  132. package/esm/cli/codegen/gql-ast.js +288 -0
  133. package/esm/cli/codegen/index.d.ts +71 -0
  134. package/esm/cli/codegen/index.js +124 -0
  135. package/esm/cli/codegen/mutations.d.ts +30 -0
  136. package/esm/cli/codegen/mutations.js +404 -0
  137. package/esm/cli/codegen/orm/barrel.d.ts +18 -0
  138. package/esm/cli/codegen/orm/barrel.js +44 -0
  139. package/esm/cli/codegen/orm/client-generator.d.ts +45 -0
  140. package/esm/cli/codegen/orm/client-generator.js +640 -0
  141. package/esm/cli/codegen/orm/custom-ops-generator.d.ts +30 -0
  142. package/esm/cli/codegen/orm/custom-ops-generator.js +346 -0
  143. package/esm/cli/codegen/orm/index.d.ts +38 -0
  144. package/esm/cli/codegen/orm/index.js +75 -0
  145. package/esm/cli/codegen/orm/input-types-generator.d.ts +21 -0
  146. package/esm/cli/codegen/orm/input-types-generator.js +700 -0
  147. package/esm/cli/codegen/orm/input-types-generator.test.d.ts +1 -0
  148. package/esm/cli/codegen/orm/input-types-generator.test.js +73 -0
  149. package/esm/cli/codegen/orm/model-generator.d.ts +32 -0
  150. package/esm/cli/codegen/orm/model-generator.js +260 -0
  151. package/esm/cli/codegen/orm/query-builder.d.ts +161 -0
  152. package/esm/cli/codegen/orm/query-builder.js +353 -0
  153. package/esm/cli/codegen/orm/select-types.d.ts +169 -0
  154. package/esm/cli/codegen/orm/select-types.js +15 -0
  155. package/esm/cli/codegen/orm/select-types.test.d.ts +11 -0
  156. package/esm/cli/codegen/orm/select-types.test.js +21 -0
  157. package/esm/cli/codegen/queries.d.ts +25 -0
  158. package/esm/cli/codegen/queries.js +433 -0
  159. package/esm/cli/codegen/scalars.d.ts +12 -0
  160. package/esm/cli/codegen/scalars.js +66 -0
  161. package/esm/cli/codegen/schema-gql-ast.d.ts +51 -0
  162. package/esm/cli/codegen/schema-gql-ast.js +343 -0
  163. package/esm/cli/codegen/ts-ast.d.ts +122 -0
  164. package/esm/cli/codegen/ts-ast.js +260 -0
  165. package/esm/cli/codegen/type-resolver.d.ts +96 -0
  166. package/esm/cli/codegen/type-resolver.js +224 -0
  167. package/esm/cli/codegen/types.d.ts +12 -0
  168. package/esm/cli/codegen/types.js +65 -0
  169. package/esm/cli/codegen/utils.d.ts +163 -0
  170. package/esm/cli/codegen/utils.js +288 -0
  171. package/esm/cli/commands/generate-orm.d.ts +37 -0
  172. package/esm/cli/commands/generate-orm.js +192 -0
  173. package/esm/cli/commands/generate.d.ts +39 -0
  174. package/esm/cli/commands/generate.js +262 -0
  175. package/esm/cli/commands/index.d.ts +7 -0
  176. package/esm/cli/commands/index.js +5 -0
  177. package/esm/cli/commands/init.d.ts +35 -0
  178. package/esm/cli/commands/init.js +138 -0
  179. package/esm/cli/index.d.ts +4 -0
  180. package/esm/cli/index.js +256 -0
  181. package/esm/cli/introspect/fetch-meta.d.ts +31 -0
  182. package/esm/cli/introspect/fetch-meta.js +104 -0
  183. package/esm/cli/introspect/fetch-schema.d.ts +21 -0
  184. package/esm/cli/introspect/fetch-schema.js +83 -0
  185. package/esm/cli/introspect/index.d.ts +8 -0
  186. package/esm/cli/introspect/index.js +6 -0
  187. package/esm/cli/introspect/meta-query.d.ts +111 -0
  188. package/esm/cli/introspect/meta-query.js +188 -0
  189. package/esm/cli/introspect/schema-query.d.ts +20 -0
  190. package/esm/cli/introspect/schema-query.js +120 -0
  191. package/esm/cli/introspect/transform-schema.d.ts +74 -0
  192. package/esm/cli/introspect/transform-schema.js +259 -0
  193. package/esm/cli/introspect/transform-schema.test.d.ts +1 -0
  194. package/esm/cli/introspect/transform-schema.test.js +65 -0
  195. package/esm/cli/introspect/transform.d.ts +21 -0
  196. package/esm/cli/introspect/transform.js +210 -0
  197. package/esm/cli/watch/cache.d.ts +45 -0
  198. package/esm/cli/watch/cache.js +73 -0
  199. package/esm/cli/watch/debounce.d.ts +19 -0
  200. package/esm/cli/watch/debounce.js +85 -0
  201. package/esm/cli/watch/hash.d.ts +17 -0
  202. package/esm/cli/watch/hash.js +43 -0
  203. package/esm/cli/watch/index.d.ts +10 -0
  204. package/esm/cli/watch/index.js +8 -0
  205. package/esm/cli/watch/orchestrator.d.ts +63 -0
  206. package/esm/cli/watch/orchestrator.js +223 -0
  207. package/esm/cli/watch/poller.d.ts +65 -0
  208. package/esm/cli/watch/poller.js +198 -0
  209. package/esm/cli/watch/types.d.ts +67 -0
  210. package/esm/cli/watch/types.js +4 -0
  211. package/esm/client/error.d.ts +95 -0
  212. package/esm/client/error.js +249 -0
  213. package/esm/client/execute.d.ts +57 -0
  214. package/esm/client/execute.js +120 -0
  215. package/esm/client/index.d.ts +6 -0
  216. package/esm/client/index.js +6 -0
  217. package/esm/client/typed-document.d.ts +31 -0
  218. package/esm/client/typed-document.js +40 -0
  219. package/esm/core/ast.d.ts +10 -0
  220. package/esm/core/ast.js +549 -0
  221. package/esm/core/custom-ast.d.ts +35 -0
  222. package/esm/core/custom-ast.js +161 -0
  223. package/esm/core/index.d.ts +8 -0
  224. package/esm/core/index.js +12 -0
  225. package/esm/core/meta-object/convert.d.ts +65 -0
  226. package/esm/core/meta-object/convert.js +60 -0
  227. package/esm/core/meta-object/format.json +93 -0
  228. package/esm/core/meta-object/index.d.ts +2 -0
  229. package/esm/core/meta-object/index.js +2 -0
  230. package/esm/core/meta-object/validate.d.ts +9 -0
  231. package/esm/core/meta-object/validate.js +28 -0
  232. package/esm/core/query-builder.d.ts +46 -0
  233. package/esm/core/query-builder.js +375 -0
  234. package/esm/core/types.d.ts +139 -0
  235. package/esm/core/types.js +24 -0
  236. package/esm/generators/field-selector.d.ts +30 -0
  237. package/esm/generators/field-selector.js +355 -0
  238. package/esm/generators/index.d.ts +6 -0
  239. package/esm/generators/index.js +9 -0
  240. package/esm/generators/mutations.d.ts +31 -0
  241. package/esm/generators/mutations.js +197 -0
  242. package/esm/generators/select.d.ts +50 -0
  243. package/esm/generators/select.js +636 -0
  244. package/esm/index.d.ts +12 -0
  245. package/esm/index.js +17 -3
  246. package/esm/react/index.d.ts +5 -0
  247. package/esm/react/index.js +6 -0
  248. package/esm/types/config.d.ts +199 -0
  249. package/esm/types/config.js +106 -0
  250. package/esm/types/index.d.ts +9 -0
  251. package/esm/types/index.js +4 -0
  252. package/esm/types/introspection.d.ts +121 -0
  253. package/esm/types/introspection.js +54 -0
  254. package/esm/types/mutation.d.ts +45 -0
  255. package/esm/types/mutation.js +4 -0
  256. package/esm/types/query.d.ts +82 -0
  257. package/esm/types/query.js +4 -0
  258. package/esm/types/schema.d.ts +253 -0
  259. package/esm/types/schema.js +5 -0
  260. package/esm/types/selection.d.ts +43 -0
  261. package/esm/types/selection.js +4 -0
  262. package/esm/utils/index.d.ts +4 -0
  263. package/esm/utils/index.js +4 -0
  264. package/generators/field-selector.d.ts +30 -0
  265. package/generators/field-selector.js +361 -0
  266. package/generators/index.d.ts +6 -0
  267. package/generators/index.js +27 -0
  268. package/generators/mutations.d.ts +31 -0
  269. package/generators/mutations.js +235 -0
  270. package/generators/select.d.ts +50 -0
  271. package/generators/select.js +679 -0
  272. package/index.d.ts +12 -3
  273. package/index.js +19 -3
  274. package/package.json +59 -38
  275. package/react/index.d.ts +5 -0
  276. package/react/index.js +9 -0
  277. package/types/config.d.ts +199 -0
  278. package/types/config.js +111 -0
  279. package/types/index.d.ts +9 -0
  280. package/types/index.js +10 -0
  281. package/types/introspection.d.ts +121 -0
  282. package/types/introspection.js +62 -0
  283. package/types/mutation.d.ts +45 -0
  284. package/types/mutation.js +5 -0
  285. package/types/query.d.ts +82 -0
  286. package/types/query.js +5 -0
  287. package/types/schema.d.ts +253 -0
  288. package/types/schema.js +6 -0
  289. package/types/selection.d.ts +43 -0
  290. package/types/selection.js +5 -0
  291. package/utils/index.d.ts +4 -0
  292. package/utils/index.js +7 -0
  293. package/codegen.d.ts +0 -13
  294. package/codegen.js +0 -293
  295. package/esm/codegen.js +0 -253
  296. package/esm/gql.js +0 -939
  297. package/esm/options.js +0 -27
  298. package/gql.d.ts +0 -188
  299. package/gql.js +0 -992
  300. package/options.d.ts +0 -45
  301. package/options.js +0 -31
package/README.md CHANGED
@@ -1,174 +1,1879 @@
1
- # @constructive-io/graphql-codegen
1
+ # @constructive-io/graphql-sdk
2
2
 
3
- <p align="center" width="100%">
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
- <p align="center" width="100%">
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
- Generate GraphQL mutations/queries
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
- ```sh
18
- npm install @constructive-io/graphql-codegen
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
- ## introspecting via GraphQL
53
+ Creates a `graphql-sdk.config.ts` file:
22
54
 
23
- ```js
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
- const gen = generate(resultOfIntrospectionQuery);
30
- const output = Object.keys(gen).reduce((m, key) => {
31
- m[key] = print(gen[key].ast);
32
- return m;
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
- console.log(output);
73
+ # Generate ORM client
74
+ npx graphql-sdk generate-orm -e https://api.example.com/graphql -o ./generated/orm
36
75
  ```
37
76
 
38
- # output
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
- which will output the entire API as an object with the mutations and queries as values
131
+ ### `graphql-sdk init`
41
132
 
42
- ```json
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
- "createApiTokenMutation": "mutation createApiTokenMutation($id: UUID, $userId: UUID!, $accessToken: String, $accessTokenExpiresAt: Datetime) {
45
- createApiToken(input: {apiToken: {id: $id, userId: $userId, accessToken: $accessToken, accessTokenExpiresAt: $accessTokenExpiresAt}}) {
46
- apiToken {
47
- id
48
- userId
49
- accessToken
50
- accessTokenExpiresAt
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
- ## Codegen (types, operations, SDK, React Query)
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
- Programmatic codegen generates files to disk from a schema SDL file or from a live endpoint via introspection.
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
- ```js
61
- import { runCodegen, defaultGraphQLCodegenOptions } from '@constructive-io/graphql-codegen'
987
+ // Connection result (for list queries)
988
+ interface CarsConnection {
989
+ nodes: Car[];
990
+ totalCount: number;
991
+ pageInfo: PageInfo;
992
+ }
62
993
 
63
- await runCodegen({
64
- input: { schema: './schema.graphql' }, // or: { endpoint: 'http://localhost:3000/graphql', headers: { Host: 'meta8.localhost' } }
65
- output: defaultGraphQLCodegenOptions.output, // root/typesFile/operationsDir/sdkFile/reactQueryFile
66
- documents: defaultGraphQLCodegenOptions.documents, // format: 'gql'|'ts', naming convention
67
- features: { emitTypes: true, emitOperations: true, emitSdk: true, emitReactQuery: true },
68
- reactQuery: { fetcher: 'graphql-request' }
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
- Outputs under `output.root`:
73
- - `types/generated.ts` (schema types)
74
- - `operations/*` (queries/mutations/Fragments)
75
- - `sdk.ts` (typed GraphQL Request client)
76
- - `react-query.ts` (typed React Query hooks; generated when `emitReactQuery: true`)
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
- Documents options:
79
- - `format`: `'gql' | 'ts'`
80
- - `convention`: `'dashed' | 'underscore' | 'camelcase' | 'camelUpper'`
81
- - `allowQueries`, `excludeQueries`, `excludePatterns` to control which root fields become operations
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
- Endpoint introspection:
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
- ## Selection Options
1032
+ ```typescript
1033
+ import { createClient } from './generated/orm';
88
1034
 
89
- Configure mutation input style and connection pagination shape.
1035
+ // Create client instance
1036
+ const db = createClient({
1037
+ endpoint: 'https://api.example.com/graphql',
1038
+ headers: { Authorization: 'Bearer <token>' },
1039
+ });
90
1040
 
91
- ```ts
92
- selection: {
93
- mutationInputMode?: 'expanded' | 'model' | 'raw' | 'patchCollapsed'
94
- connectionStyle?: 'nodes' | 'edges'
95
- forceModelOutput?: boolean
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
- - `mutationInputMode`
100
- - Controls how mutation variables and `input` are generated.
101
- - `expanded`: one variable per input property; `input` is a flat object of those variables.
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
- - `connectionStyle`
107
- - Standardizes connection queries and nested many selections.
108
- - `nodes`: emits `totalCount` and `nodes { ... }`.
109
- - `edges`: emits `totalCount`, `pageInfo { ... }`, and `edges { cursor node { ... } }`.
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
- - `forceModelOutput`
112
- - When `true`, ensures the object payload selection is emitted to avoid a payload with only `clientMutationId`.
1088
+ // TypeScript knows the exact shape:
1089
+ // users.users.nodes[0] is { id: string; username: string | null }
113
1090
 
114
- ### Examples
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
- Create mutation with raw input:
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
- ```graphql
119
- mutation createDomain($input: CreateDomainInput!) {
120
- createDomain(input: $input) {
121
- domain { id, domain, subdomain }
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
- Patch mutation with collapsed patch:
1372
+ ### Error Handling
127
1373
 
128
- ```graphql
129
- mutation updateDomain($id: UUID!, $patch: DomainPatch!) {
130
- updateDomain(input: { id: $id, patch: $patch }) {
131
- domain { id }
132
- clientMutationId
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
- Edges‑style connection query:
1414
+ #### `.unwrapOr()` - Default Value on Error
138
1415
 
139
- ```graphql
140
- query getDomainsPaginated($first: Int, $after: Cursor) {
141
- domains(first: $first, after: $after) {
142
- totalCount
143
- pageInfo { hasNextPage hasPreviousPage endCursor startCursor }
144
- edges { cursor node { id domain subdomain } }
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
- ## Custom Scalars and Type Overrides
1473
+ ### Custom Operations
150
1474
 
151
- - When your schema exposes custom scalars that are not available in the printed SDL or differ across environments, you can configure both TypeScript scalar typings and GraphQL type names used in generated operations.
1475
+ Custom queries and mutations (like `login`, `currentUser`, etc.) are available on `db.query` and `db.mutation`:
152
1476
 
153
- - Add these to your config object:
1477
+ #### Custom Queries
154
1478
 
155
- ```json
156
- {
157
- "scalars": {
158
- "LaunchqlInternalTypeHostname": "string",
159
- "PgpmInternalTypeHostname": "string"
160
- },
161
- "typeNameOverrides": {
162
- "LaunchqlInternalTypeHostname": "String",
163
- "PgpmInternalTypeHostname": "String"
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
- - `scalars`: maps GraphQL scalar names to TypeScript types for `typescript`/`typescript-operations`/`typescript-graphql-request`/`typescript-react-query` plugins.
169
- - `typeNameOverrides`: rewrites scalar names in generated GraphQL AST so variable definitions and input fields use compatible built‑in GraphQL types.
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
- - These options are also available programmatically through `LaunchQLGenOptions`.
1876
+ MIT
172
1877
 
173
1878
  ---
174
1879