@graphql-suite/client 0.8.3 → 0.9.1

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 CHANGED
@@ -1,191 +1,259 @@
1
- [![Monthly Downloads](https://img.shields.io/npm/dm/drizzle-graphql-suite.svg)](https://www.npmjs.com/package/drizzle-graphql-suite)
2
- [![NPM](https://img.shields.io/npm/v/drizzle-graphql-suite.svg 'NPM package version')](https://www.npmjs.com/package/drizzle-graphql-suite)
3
- [![CI](https://github.com/annexare/drizzle-graphql-suite/actions/workflows/ci.yml/badge.svg)](https://github.com/annexare/drizzle-graphql-suite/actions/workflows/ci.yml)
1
+ # @graphql-suite/client
4
2
 
5
- # drizzle-graphql-suite
3
+ > Part of [`graphql-suite`](https://github.com/annexare/graphql-suite).
4
+ > See also: [`schema`](https://github.com/annexare/graphql-suite/tree/main/packages/schema) | [`query`](https://github.com/annexare/graphql-suite/tree/main/packages/query)
6
5
 
7
- Auto-generated GraphQL CRUD, type-safe clients, and React Query hooks from Drizzle PostgreSQL schemas.
8
-
9
- ## Overview
10
-
11
- `drizzle-graphql-suite` is a three-layer toolkit that turns your Drizzle ORM schema into a fully working GraphQL API with end-to-end type safety:
12
-
13
- 1. **Schema builder** — generates a complete GraphQL schema with CRUD operations, relation-level filtering, per-operation hooks, and runtime permissions from Drizzle table definitions.
14
- 2. **Client** — provides a type-safe GraphQL client that infers query/mutation types directly from your Drizzle schema, with full TypeScript support for filters, relations, and results.
15
- 3. **React Query hooks** — wraps the client in TanStack React Query hooks for caching, pagination, and mutations with automatic cache invalidation.
16
-
17
- Inspired by [`drizzle-graphql`](https://github.com/drizzle-team/drizzle-graphql), rewritten with significant improvements including relation-level filtering, hooks, count queries, configurable schema generation, and code generation.
18
-
19
- ## Packages
20
-
21
- | Subpath | Package | Description |
22
- |---------|---------|-------------|
23
- | `drizzle-graphql-suite/schema` | [`@drizzle-graphql-suite/schema`](packages/schema/README.md) | GraphQL schema builder with CRUD, filtering, hooks, permissions, and codegen |
24
- | `drizzle-graphql-suite/client` | [`@drizzle-graphql-suite/client`](packages/client/README.md) | Type-safe GraphQL client with full Drizzle type inference |
25
- | `drizzle-graphql-suite/query` | [`@drizzle-graphql-suite/query`](packages/query/README.md) | TanStack React Query hooks for the client |
6
+ Type-safe GraphQL client auto-generated from Drizzle schemas, with full TypeScript inference for queries, mutations, filters, and relations.
26
7
 
27
8
  ## Installation
28
9
 
29
10
  ```bash
30
- bun add drizzle-graphql-suite
11
+ bun add @graphql-suite/client
31
12
  ```
32
13
 
33
14
  ```bash
34
- npm install drizzle-graphql-suite
15
+ npm install @graphql-suite/client
35
16
  ```
36
17
 
37
- ## Peer Dependencies
18
+ Or install the full suite:
38
19
 
39
- Each subpath import has its own peer dependency requirements:
20
+ ```bash
21
+ bun add graphql-suite
22
+ ```
40
23
 
41
- | Subpath | Peer Dependencies |
42
- |---------|-------------------|
43
- | `./schema` | `drizzle-orm` >=0.44.0, `graphql` >=16.3.0 |
44
- | `./client` | `drizzle-orm` >=0.44.0 |
45
- | `./query` | `react` >=18.0.0, `@tanstack/react-query` >=5.0.0 |
24
+ ```bash
25
+ npm install graphql-suite
26
+ ```
46
27
 
47
28
  ## Quick Start
48
29
 
49
- ### 1. Server Build GraphQL Schema
30
+ ### From Drizzle Schema (recommended)
31
+
32
+ Use `createDrizzleClient` to create a client that infers all types directly from your Drizzle schema module — no code generation needed.
50
33
 
51
34
  ```ts
52
- import { buildSchema } from 'drizzle-graphql-suite/schema'
53
- import { createYoga } from 'graphql-yoga'
54
- import { createServer } from 'node:http'
55
- import { db } from './db'
56
-
57
- const { schema, withPermissions } = buildSchema(db, {
58
- tables: { exclude: ['session', 'verification'] },
59
- hooks: {
60
- user: {
61
- query: {
62
- before: async ({ context }) => {
63
- if (!context.user) throw new Error('Unauthorized')
64
- },
65
- },
66
- },
35
+ import { createDrizzleClient } from '@graphql-suite/client'
36
+ import * as schema from './db/schema'
37
+
38
+ const client = createDrizzleClient({
39
+ schema,
40
+ config: {
41
+ suffixes: { list: 's' },
42
+ tables: { exclude: ['session', 'verification'] },
67
43
  },
44
+ url: '/api/graphql',
45
+ headers: { Authorization: 'Bearer ...' },
68
46
  })
69
-
70
- const yoga = createYoga({ schema })
71
- const server = createServer(yoga)
72
- server.listen(4000)
73
47
  ```
74
48
 
75
- #### Per-Role Schemas (Optional)
49
+ ### From Schema Descriptor (separate-repo setups)
50
+
51
+ Use `createClient` with a codegen-generated schema descriptor when the client is in a separate repository that can't import the Drizzle schema directly.
76
52
 
77
53
  ```ts
78
- import { permissive, restricted, readOnly } from 'drizzle-graphql-suite/schema'
54
+ import { createClient } from '@graphql-suite/client'
55
+ import { schema, type EntityDefs } from './generated/entity-defs'
79
56
 
80
- // Cached per id call withPermissions on each request
81
- const schemas = {
82
- admin: schema,
83
- editor: withPermissions(permissive('editor', { audit: false, user: readOnly() })),
84
- viewer: withPermissions(restricted('viewer', { post: { query: true } })),
85
- }
57
+ const client = createClient<typeof schema, EntityDefs>({
58
+ schema,
59
+ url: '/api/graphql',
60
+ })
86
61
  ```
87
62
 
88
- ### 2. Client — Type-Safe Queries
63
+ ## EntityClient API
64
+
65
+ Access a typed entity client via `client.entity('name')`:
89
66
 
90
67
  ```ts
91
- import { createDrizzleClient } from 'drizzle-graphql-suite/client'
92
- import * as schema from './db/schema'
68
+ const user = client.entity('user')
69
+ ```
93
70
 
94
- const client = createDrizzleClient({
95
- schema,
96
- config: { suffixes: { list: 's' } },
97
- url: '/api/graphql',
98
- })
71
+ Each entity client provides the following methods:
72
+
73
+ ### `query(params)`
74
+
75
+ Fetch a list of records with optional filtering, pagination, and ordering. Returns `T[]`.
99
76
 
100
- const users = await client.entity('user').query({
77
+ ```ts
78
+ const users = await user.query({
101
79
  select: {
102
80
  id: true,
103
81
  name: true,
104
- posts: { id: true, title: true },
82
+ email: true,
83
+ posts: {
84
+ id: true,
85
+ title: true,
86
+ comments: { id: true, body: true },
87
+ },
105
88
  },
106
- where: { name: { ilike: '%john%' } },
107
- limit: 10,
89
+ where: { email: { ilike: '%@example.com' } },
90
+ orderBy: { name: { direction: 'asc', priority: 1 } },
91
+ limit: 20,
92
+ offset: 0,
108
93
  })
109
94
  ```
110
95
 
111
- ### 3. React — Query Hooks
96
+ ### `querySingle(params)`
112
97
 
113
- ```tsx
114
- import { GraphQLProvider, useEntity, useEntityList } from 'drizzle-graphql-suite/query'
115
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
98
+ Fetch a single record. Returns `T | null`.
116
99
 
117
- const queryClient = new QueryClient()
100
+ ```ts
101
+ const found = await user.querySingle({
102
+ select: { id: true, name: true, email: true },
103
+ where: { id: { eq: 'some-uuid' } },
104
+ })
105
+ ```
118
106
 
119
- function App() {
120
- return (
121
- <QueryClientProvider client={queryClient}>
122
- <GraphQLProvider client={graphqlClient}>
123
- <UserList />
124
- </GraphQLProvider>
125
- </QueryClientProvider>
126
- )
127
- }
107
+ ### `count(params?)`
128
108
 
129
- function UserList() {
130
- const user = useEntity('user')
131
- const { data, isLoading } = useEntityList(user, {
132
- select: { id: true, name: true, email: true },
133
- limit: 20,
134
- })
109
+ Count matching records. Returns `number`.
135
110
 
136
- if (isLoading) return <div>Loading...</div>
137
- return <ul>{data?.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
138
- }
111
+ ```ts
112
+ const total = await user.count({
113
+ where: { role: { eq: 'admin' } },
114
+ })
139
115
  ```
140
116
 
141
- ## Framework Integration Examples
117
+ ### `insert(params)`
118
+
119
+ Insert multiple records. Returns `T[]` of inserted rows.
142
120
 
143
- `buildSchema()` returns a standard `GraphQLSchema` — here's how to serve it from popular frameworks.
121
+ ```ts
122
+ const created = await user.insert({
123
+ values: [
124
+ { name: 'Alice', email: 'alice@example.com' },
125
+ { name: 'Bob', email: 'bob@example.com' },
126
+ ],
127
+ returning: { id: true, name: true },
128
+ })
129
+ ```
144
130
 
145
- ### Next.js App Router
131
+ ### `insertSingle(params)`
132
+
133
+ Insert a single record. Returns `T | null`.
146
134
 
147
135
  ```ts
148
- // app/api/graphql/route.ts
149
- import { createYoga } from 'graphql-yoga'
150
- import { buildSchema } from 'drizzle-graphql-suite/schema'
151
- import { db } from '@/db'
136
+ const created = await user.insertSingle({
137
+ values: { name: 'Alice', email: 'alice@example.com' },
138
+ returning: { id: true, name: true },
139
+ })
140
+ ```
152
141
 
153
- const { schema } = buildSchema(db)
142
+ ### `update(params)`
154
143
 
155
- const { handleRequest } = createYoga({
156
- schema,
157
- graphqlEndpoint: '/api/graphql',
158
- fetchAPI: { Response },
144
+ Update records matching a filter. Returns `T[]` of updated rows.
145
+
146
+ ```ts
147
+ const updated = await user.update({
148
+ set: { role: 'admin' },
149
+ where: { id: { eq: 'some-uuid' } },
150
+ returning: { id: true, role: true },
159
151
  })
152
+ ```
153
+
154
+ ### `delete(params)`
155
+
156
+ Delete records matching a filter. Returns `T[]` of deleted rows.
160
157
 
161
- export { handleRequest as GET, handleRequest as POST }
158
+ ```ts
159
+ const deleted = await user.delete({
160
+ where: { deletedAt: { isNotNull: true } },
161
+ returning: { id: true },
162
+ })
162
163
  ```
163
164
 
164
- ### ElysiaJS
165
+ ## Schema Descriptor
166
+
167
+ A `SchemaDescriptor` is a runtime object mapping entity names to their operation names, fields, and relations. It tells the client how to build GraphQL queries.
168
+
169
+ ### `buildSchemaDescriptor(schema, config?)`
170
+
171
+ Builds a `SchemaDescriptor` from a Drizzle schema module. This is called internally by `createDrizzleClient`, but can be used directly if you need the descriptor.
165
172
 
166
173
  ```ts
167
- // server.ts
168
- import { Elysia } from 'elysia'
169
- import { yoga } from '@elysiajs/graphql-yoga'
170
- import { buildSchema } from 'drizzle-graphql-suite/schema'
171
- import { db } from './db'
174
+ import { buildSchemaDescriptor } from '@graphql-suite/client'
175
+ import * as schema from './db/schema'
176
+
177
+ const descriptor = buildSchemaDescriptor(schema, {
178
+ suffixes: { list: 's' },
179
+ tables: { exclude: ['session'] },
180
+ pruneRelations: { 'user.sessions': false },
181
+ })
182
+ ```
172
183
 
173
- const { schema } = buildSchema(db)
184
+ Config options mirror the schema package: `mutations`, `suffixes`, `tables.exclude`, and `pruneRelations`.
174
185
 
175
- new Elysia()
176
- .use(yoga({ schema }))
177
- .listen(3000)
186
+ ## Type Inference
187
+
188
+ The client provides end-to-end type inference from Drizzle schema to query results:
189
+
190
+ ### `InferEntityDefs<TSchema, TConfig>`
191
+
192
+ Infers the complete entity type definitions from a Drizzle schema module, including fields (with Date → string wire conversion), relations, filters, insert inputs, update inputs, and orderBy types.
193
+
194
+ ```ts
195
+ import type { InferEntityDefs } from '@graphql-suite/client'
196
+ import type * as schema from './db/schema'
197
+
198
+ type MyEntityDefs = InferEntityDefs<typeof schema, { tables: { exclude: ['session'] } }>
178
199
  ```
179
200
 
180
- ## AI Agent Skill
201
+ ### `InferResult<TDefs, TEntity, TSelect>`
181
202
 
182
- This repo includes a [skills.sh](https://skills.sh) skill that provides AI coding agents (Claude Code, Cursor, etc.) with accurate, up-to-date guidance for all three packages.
203
+ Infers the return type of a query from the `select` object. Only selected scalar fields and relations are included in the result type. Relations resolve to arrays or `T | null` based on their cardinality.
183
204
 
184
- ```bash
185
- bunx skills add annexare/drizzle-graphql-suite
186
- # or: npx skills add annexare/drizzle-graphql-suite
205
+ ### `SelectInput<TDefs, TEntity>`
206
+
207
+ Describes the valid shape of a `select` parameter — `true` for scalar fields, nested objects for relations.
208
+
209
+ ## Dynamic URL and Headers
210
+
211
+ Both `url` and `headers` support static values or functions (sync or async) that are called per-request:
212
+
213
+ ```ts
214
+ const client = createDrizzleClient({
215
+ schema,
216
+ config: {},
217
+ // Dynamic URL
218
+ url: () => `${getApiBase()}/graphql`,
219
+ // Async headers (e.g., refresh token)
220
+ headers: async () => ({
221
+ Authorization: `Bearer ${await getAccessToken()}`,
222
+ }),
223
+ })
187
224
  ```
188
225
 
189
- ## License
226
+ ## Error Handling
227
+
228
+ The client throws two error types:
190
229
 
191
- MIT
230
+ ### `GraphQLClientError`
231
+
232
+ Thrown when the server returns GraphQL errors in the response body.
233
+
234
+ - **`errors`** — `Array<{ message, locations?, path?, extensions? }>` — individual GraphQL errors
235
+ - **`status`** — HTTP status code (usually `200`)
236
+ - **`message`** — concatenated error messages
237
+
238
+ ### `NetworkError`
239
+
240
+ Thrown when the HTTP request fails (network error or non-2xx status).
241
+
242
+ - **`status`** — HTTP status code (`0` for network failures)
243
+ - **`message`** — error description
244
+
245
+ ```ts
246
+ import { GraphQLClientError, NetworkError } from '@graphql-suite/client'
247
+
248
+ try {
249
+ const users = await client.entity('user').query({
250
+ select: { id: true, name: true },
251
+ })
252
+ } catch (e) {
253
+ if (e instanceof GraphQLClientError) {
254
+ console.error('GraphQL errors:', e.errors)
255
+ } else if (e instanceof NetworkError) {
256
+ console.error('Network error:', e.status, e.message)
257
+ }
258
+ }
259
+ ```
package/client.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { BuildSchemaConfig } from '@graphql-suite/schema';
2
+ import { type EntityClient } from './entity';
3
+ import type { InferEntityDefs } from './infer';
4
+ import type { AnyEntityDefs, ClientConfig, EntityDefsRef, SchemaDescriptor } from './types';
5
+ export declare class GraphQLClient<TSchema extends SchemaDescriptor, TDefs extends AnyEntityDefs = AnyEntityDefs> {
6
+ private url;
7
+ private schema;
8
+ private headers?;
9
+ constructor(config: ClientConfig<TSchema>);
10
+ entity<TEntityName extends string & keyof TSchema & keyof TDefs>(entityName: TEntityName): EntityClient<EntityDefsRef<TDefs>, TEntityName>;
11
+ execute(query: string, variables?: Record<string, unknown>): Promise<Record<string, unknown>>;
12
+ }
13
+ export type DrizzleClientConfig<TSchema extends Record<string, unknown>, TConfig extends BuildSchemaConfig> = {
14
+ schema: TSchema;
15
+ config: TConfig;
16
+ url: string | (() => string);
17
+ headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
18
+ };
19
+ export declare function createDrizzleClient<TSchema extends Record<string, unknown>, const TConfig extends BuildSchemaConfig>(options: DrizzleClientConfig<TSchema, TConfig>): GraphQLClient<SchemaDescriptor, InferEntityDefs<TSchema, TConfig>>;
package/entity.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ import type { AnyEntityDefs, EntityDef, EntityDefsRef, EntityDescriptor, InferResult, SchemaDescriptor } from './types';
2
+ type ResolveEntity<TRef extends EntityDefsRef<AnyEntityDefs>, TEntityName extends string> = TRef['__defs'][TEntityName] & EntityDef;
3
+ export interface EntityClient<TRef extends EntityDefsRef<AnyEntityDefs>, TEntityName extends string> {
4
+ query<S extends Record<string, unknown>>(params: {
5
+ select: S;
6
+ where?: ResolveEntity<TRef, TEntityName> extends {
7
+ filters: infer F;
8
+ } ? F : never;
9
+ limit?: number;
10
+ offset?: number;
11
+ orderBy?: ResolveEntity<TRef, TEntityName> extends {
12
+ orderBy: infer O;
13
+ } ? O : never;
14
+ }): Promise<InferResult<TRef['__defs'], ResolveEntity<TRef, TEntityName>, S>[]>;
15
+ querySingle<S extends Record<string, unknown>>(params: {
16
+ select: S;
17
+ where?: ResolveEntity<TRef, TEntityName> extends {
18
+ filters: infer F;
19
+ } ? F : never;
20
+ offset?: number;
21
+ orderBy?: ResolveEntity<TRef, TEntityName> extends {
22
+ orderBy: infer O;
23
+ } ? O : never;
24
+ }): Promise<InferResult<TRef['__defs'], ResolveEntity<TRef, TEntityName>, S> | null>;
25
+ count(params?: {
26
+ where?: ResolveEntity<TRef, TEntityName> extends {
27
+ filters: infer F;
28
+ } ? F : never;
29
+ }): Promise<number>;
30
+ insert<S extends Record<string, unknown>>(params: {
31
+ values: ResolveEntity<TRef, TEntityName> extends {
32
+ insertInput: infer I;
33
+ } ? I[] : never;
34
+ returning?: S;
35
+ }): Promise<InferResult<TRef['__defs'], ResolveEntity<TRef, TEntityName>, S>[]>;
36
+ insertSingle<S extends Record<string, unknown>>(params: {
37
+ values: ResolveEntity<TRef, TEntityName> extends {
38
+ insertInput: infer I;
39
+ } ? I : never;
40
+ returning?: S;
41
+ }): Promise<InferResult<TRef['__defs'], ResolveEntity<TRef, TEntityName>, S> | null>;
42
+ update<S extends Record<string, unknown>>(params: {
43
+ set: ResolveEntity<TRef, TEntityName> extends {
44
+ updateInput: infer U;
45
+ } ? U : never;
46
+ where?: ResolveEntity<TRef, TEntityName> extends {
47
+ filters: infer F;
48
+ } ? F : never;
49
+ returning?: S;
50
+ }): Promise<InferResult<TRef['__defs'], ResolveEntity<TRef, TEntityName>, S>[]>;
51
+ delete<S extends Record<string, unknown>>(params: {
52
+ where?: ResolveEntity<TRef, TEntityName> extends {
53
+ filters: infer F;
54
+ } ? F : never;
55
+ returning?: S;
56
+ }): Promise<InferResult<TRef['__defs'], ResolveEntity<TRef, TEntityName>, S>[]>;
57
+ }
58
+ export declare function createEntityClient<TRef extends EntityDefsRef<AnyEntityDefs>, TEntityName extends string>(entityName: string, entityDef: EntityDescriptor, schema: SchemaDescriptor, executeGraphQL: (query: string, variables: Record<string, unknown>) => Promise<Record<string, unknown>>): EntityClient<TRef, TEntityName>;
59
+ export {};
package/errors.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ export type GraphQLErrorLocation = {
2
+ line: number;
3
+ column: number;
4
+ };
5
+ export type GraphQLErrorEntry = {
6
+ message: string;
7
+ locations?: GraphQLErrorLocation[];
8
+ path?: (string | number)[];
9
+ extensions?: Record<string, unknown>;
10
+ };
11
+ export declare class GraphQLClientError extends Error {
12
+ readonly errors: GraphQLErrorEntry[];
13
+ readonly status: number;
14
+ constructor(errors: GraphQLErrorEntry[], status?: number);
15
+ }
16
+ export declare class NetworkError extends Error {
17
+ readonly status: number;
18
+ constructor(message: string, status: number);
19
+ }
package/index.d.ts CHANGED
@@ -1 +1,11 @@
1
- export * from '@drizzle-graphql-suite/client'
1
+ import { GraphQLClient } from './client';
2
+ import type { AnyEntityDefs, ClientConfig, SchemaDescriptor } from './types';
3
+ export declare function createClient<TSchema extends SchemaDescriptor, TDefs extends AnyEntityDefs = AnyEntityDefs>(config: ClientConfig<TSchema>): GraphQLClient<TSchema, TDefs>;
4
+ export type { BuildSchemaConfig } from '@graphql-suite/schema';
5
+ export type { DrizzleClientConfig } from './client';
6
+ export { createDrizzleClient, GraphQLClient } from './client';
7
+ export type { EntityClient } from './entity';
8
+ export { GraphQLClientError, NetworkError } from './errors';
9
+ export type { InferEntityDefs } from './infer';
10
+ export { buildSchemaDescriptor } from './schema-builder';
11
+ export type { AnyEntityDefs, ClientConfig, EntityDef, EntityDefsRef, EntityDescriptor, InferResult, SchemaDescriptor, SelectInput, } from './types';
package/index.js CHANGED
@@ -1 +1,25 @@
1
- export * from '@drizzle-graphql-suite/client'
1
+ /** @graphql-suite/client v0.9.1 | MIT */
2
+ var j=(E)=>E.charAt(0).toUpperCase()+E.slice(1);function G(E,x,$,Y=4){let H=" ".repeat(Y),C=[];for(let[R,J]of Object.entries(E)){if(J===!0){C.push(`${H}${R}`);continue}if(typeof J==="object"&&J!==null){let X=$.relations[R];if(!X)continue;let A=x[X.entity];if(!A)continue;let Z=G(J,x,A,Y+2);C.push(`${H}${R} {`),C.push(Z),C.push(`${H}}`)}}return C.join(`
3
+ `)}function L(E){return`${j(E)}Filters`}function S(E){return`${j(E)}OrderBy`}function p(E){return`${j(E)}InsertInput`}function c(E){return`${j(E)}UpdateInput`}function T(E,x,$,Y,H,C,R,J){let X=x.queryListName;if(!X)throw Error(`Entity '${E}' has no list query`);let A=L(E),Z=S(E),_=`${j(X)}Query`,V=[],I=[];if(H)V.push(`$where: ${A}`),I.push("where: $where");if(C)V.push(`$orderBy: ${Z}`),I.push("orderBy: $orderBy");if(R)V.push("$limit: Int"),I.push("limit: $limit");if(J)V.push("$offset: Int"),I.push("offset: $offset");let q=V.length?`(${V.join(", ")})`:"",U=I.length?`(${I.join(", ")})`:"",F=G(Y,$,x);return{query:`query ${_}${q} {
4
+ ${X}${U} {
5
+ ${F}
6
+ }
7
+ }`,variables:{},operationName:_}}function d(E,x,$,Y,H,C,R){let J=x.queryName;if(!J)throw Error(`Entity '${E}' has no single query`);let X=L(E),A=S(E),Z=`${j(J)}SingleQuery`,_=[],V=[];if(H)_.push(`$where: ${X}`),V.push("where: $where");if(C)_.push(`$orderBy: ${A}`),V.push("orderBy: $orderBy");if(R)_.push("$offset: Int"),V.push("offset: $offset");let I=_.length?`(${_.join(", ")})`:"",q=V.length?`(${V.join(", ")})`:"",U=G(Y,$,x);return{query:`query ${Z}${I} {
8
+ ${J}${q} {
9
+ ${U}
10
+ }
11
+ }`,variables:{},operationName:Z}}function b(E,x,$){let Y=x.countName;if(!Y)throw Error(`Entity '${E}' has no count query`);let H=L(E),C=`${j(Y)}Query`,R=[],J=[];if($)R.push(`$where: ${H}`),J.push("where: $where");let X=R.length?`(${R.join(", ")})`:"",A=J.length?`(${J.join(", ")})`:"";return{query:`query ${C}${X} {
12
+ ${Y}${A}
13
+ }`,variables:{},operationName:C}}function w(E,x,$,Y,H){let C=H?x.insertSingleName:x.insertName;if(!C)throw Error(`Entity '${E}' has no ${H?"insertSingle":"insert"} mutation`);let R=p(E),J=`${j(C)}Mutation`,A=`($values: ${H?`${R}!`:`[${R}!]!`})`,Z="(values: $values)",_="";if(Y)_=` {
14
+ ${G(Y,$,x)}
15
+ }`;return{query:`mutation ${J}${A} {
16
+ ${C}${Z}${_}
17
+ }`,variables:{},operationName:J}}function v(E,x,$,Y,H){let C=x.updateName;if(!C)throw Error(`Entity '${E}' has no update mutation`);let R=c(E),J=L(E),X=`${j(C)}Mutation`,A=[`$set: ${R}!`],Z=["set: $set"];if(H)A.push(`$where: ${J}`),Z.push("where: $where");let _=`(${A.join(", ")})`,V=`(${Z.join(", ")})`,I="";if(Y)I=` {
18
+ ${G(Y,$,x)}
19
+ }`;return{query:`mutation ${X}${_} {
20
+ ${C}${V}${I}
21
+ }`,variables:{},operationName:X}}function g(E,x,$,Y,H){let C=x.deleteName;if(!C)throw Error(`Entity '${E}' has no delete mutation`);let R=L(E),J=`${j(C)}Mutation`,X=[],A=[];if(H)X.push(`$where: ${R}`),A.push("where: $where");let Z=X.length?`(${X.join(", ")})`:"",_=A.length?`(${A.join(", ")})`:"",V="";if(Y)V=` {
22
+ ${G(Y,$,x)}
23
+ }`;return{query:`mutation ${J}${Z} {
24
+ ${C}${_}${V}
25
+ }`,variables:{},operationName:J}}function D(E,x,$,Y){return{async query(H){let{select:C,where:R,limit:J,offset:X,orderBy:A}=H,Z=T(E,x,$,C,R!=null,A!=null,J!=null,X!=null),_={};if(R!=null)_.where=R;if(A!=null)_.orderBy=A;if(J!=null)_.limit=J;if(X!=null)_.offset=X;return(await Y(Z.query,_))[x.queryListName]},async querySingle(H){let{select:C,where:R,offset:J,orderBy:X}=H,A=d(E,x,$,C,R!=null,X!=null,J!=null),Z={};if(R!=null)Z.where=R;if(X!=null)Z.orderBy=X;if(J!=null)Z.offset=J;return(await Y(A.query,Z))[x.queryName]??null},async count(H){let C=H?.where,R=b(E,x,C!=null),J={};if(C!=null)J.where=C;return(await Y(R.query,J))[x.countName]},async insert(H){let{values:C,returning:R}=H,J=w(E,x,$,R,!1),X={values:C};return(await Y(J.query,X))[x.insertName]},async insertSingle(H){let{values:C,returning:R}=H,J=w(E,x,$,R,!0),X={values:C};return(await Y(J.query,X))[x.insertSingleName]??null},async update(H){let{set:C,where:R,returning:J}=H,X=v(E,x,$,J,R!=null),A={set:C};if(R!=null)A.where=R;return(await Y(X.query,A))[x.updateName]},async delete(H){let{where:C,returning:R}=H,J=g(E,x,$,R,C!=null),X={};if(C!=null)X.where=C;return(await Y(J.query,X))[x.deleteName]}}}class K extends Error{errors;status;constructor(E,x=200){let $=E.map((Y)=>Y.message).join("; ");super($);this.name="GraphQLClientError",this.errors=E,this.status=x}}class W extends Error{status;constructor(E,x){super(E);this.name="NetworkError",this.status=x}}import{getTableColumns as f,getTableName as h,is as z,Many as N,One as u,Relations as m,Table as l}from"drizzle-orm";var O=(E)=>E.charAt(0).toUpperCase()+E.slice(1);function k(E,x={}){let $=new Set(x.tables?.exclude??[]),Y=x.suffixes?.list??"s",H=new Map,C=new Map;for(let[A,Z]of Object.entries(E))if(z(Z,l)){if($.has(A))continue;let _=h(Z),V=Object.keys(f(Z));H.set(A,{table:Z,dbName:_,columns:V}),C.set(_,A)}let R=new Map;for(let A of Object.values(E)){if(!z(A,m))continue;let Z=h(A.table),_=C.get(Z);if(!_||!H.has(_))continue;let V={one:(U,F)=>{return new u(A.table,U,F,!1)},many:(U,F)=>{return new N(A.table,U,F)}},I=A.config(V),q={};for(let[U,F]of Object.entries(I)){let Q=F,P=Q.referencedTableName;if(!P)continue;let B=C.get(P);if(!B)continue;let o=z(Q,u)?"one":"many";q[U]={entity:B,type:o}}R.set(_,q)}let J=x.pruneRelations??{};for(let[A,Z]of R)for(let _ of Object.keys(Z)){let V=`${A}.${_}`;if(J[V]===!1)delete Z[_]}let X={};for(let[A,{columns:Z}]of H){let _=R.get(A)??{};X[A]={queryName:A,queryListName:`${A}${Y}`,countName:`${A}Count`,insertName:`insertInto${O(A)}`,insertSingleName:`insertInto${O(A)}Single`,updateName:`update${O(A)}`,deleteName:`deleteFrom${O(A)}`,fields:Z,relations:_}}return X}class M{url;schema;headers;constructor(E){this.url=E.url,this.schema=E.schema,this.headers=E.headers}entity(E){let x=this.schema[E];if(!x)throw Error(`Entity '${E}' not found in schema`);return D(E,x,this.schema,($,Y)=>this.execute($,Y))}async execute(E,x={}){let $=typeof this.url==="function"?this.url():this.url,Y={"Content-Type":"application/json",...typeof this.headers==="function"?await this.headers():this.headers??{}},H;try{H=await fetch($,{method:"POST",headers:Y,body:JSON.stringify({query:E,variables:x})})}catch(R){throw new W(R instanceof Error?R.message:"Network request failed",0)}if(!H.ok)throw new W(`HTTP ${H.status}: ${H.statusText}`,H.status);let C=await H.json();if(C.errors?.length)throw new K(C.errors,H.status);if(!C.data)throw new K([{message:"No data in response"}],H.status);return C.data}}function y(E){let x=k(E.schema,E.config);return new M({url:E.url,schema:x,headers:E.headers})}function RE(E){return new M(E)}export{y as createDrizzleClient,RE as createClient,k as buildSchemaDescriptor,W as NetworkError,K as GraphQLClientError,M as GraphQLClient};
package/infer.d.ts ADDED
@@ -0,0 +1,107 @@
1
+ import type { Many, One, Relations, Table } from 'drizzle-orm';
2
+ type ToWire<T> = T extends Date ? string : T;
3
+ type WireFormat<T> = {
4
+ [K in keyof T]: ToWire<T[K]>;
5
+ };
6
+ type ExtractTables<TSchema> = {
7
+ [K in keyof TSchema as TSchema[K] extends Table ? K : never]: TSchema[K];
8
+ };
9
+ type FindRelationConfig<TSchema, TTableName extends string> = {
10
+ [K in keyof TSchema]: TSchema[K] extends Relations<TTableName, infer TConfig> ? TConfig : never;
11
+ }[keyof TSchema];
12
+ type MapRelation<T> = T extends One<infer N, infer TIsNullable> ? {
13
+ entity: N;
14
+ type: 'one';
15
+ required: TIsNullable;
16
+ } : T extends Many<infer N> ? {
17
+ entity: N;
18
+ type: 'many';
19
+ } : never;
20
+ type InferRelationDefs<TSchema, TTableName extends string> = {
21
+ [K in keyof FindRelationConfig<TSchema, TTableName>]: MapRelation<FindRelationConfig<TSchema, TTableName>[K]>;
22
+ };
23
+ type ScalarFilterOps<T> = {
24
+ eq?: T | null;
25
+ ne?: T | null;
26
+ lt?: T | null;
27
+ lte?: T | null;
28
+ gt?: T | null;
29
+ gte?: T | null;
30
+ like?: string | null;
31
+ notLike?: string | null;
32
+ ilike?: string | null;
33
+ notIlike?: string | null;
34
+ inArray?: T[] | null;
35
+ notInArray?: T[] | null;
36
+ isNull?: boolean | null;
37
+ isNotNull?: boolean | null;
38
+ };
39
+ type ScalarColumnFilters<TFields> = {
40
+ [K in keyof TFields]?: ScalarFilterOps<NonNullable<TFields[K]>>;
41
+ };
42
+ type ManyRelationFilter<TFilter> = {
43
+ some?: TFilter;
44
+ every?: TFilter;
45
+ none?: TFilter;
46
+ };
47
+ type IsResolvableRelation<TSchema, TRel> = TRel extends {
48
+ entity: infer E;
49
+ } ? E extends string ? KeyForDbName<TSchema, E> extends keyof ExtractTables<TSchema> ? true : false : false : false;
50
+ type Prev<T extends unknown[]> = T extends [unknown, ...infer Rest] ? Rest : [];
51
+ type InferRelationFilterFields<TSchema, TRels, TDepth extends unknown[]> = {
52
+ [K in keyof TRels as IsResolvableRelation<TSchema, TRels[K]> extends true ? K : never]?: TRels[K] extends {
53
+ entity: infer E;
54
+ type: infer TRelType;
55
+ } ? E extends string ? KeyForDbName<TSchema, E> extends infer RK ? RK extends keyof ExtractTables<TSchema> ? ExtractTables<TSchema>[RK] extends infer TTarget ? TTarget extends Table ? TRelType extends 'many' ? ManyRelationFilter<InferEntityFilters<TSchema, TTarget, Prev<TDepth>>> : InferEntityFilters<TSchema, TTarget, Prev<TDepth>> : never : never : never : never : never : never;
56
+ };
57
+ type InferEntityFilters<TSchema, T extends Table, TDepth extends unknown[] = [0]> = ScalarColumnFilters<WireFormat<T['$inferSelect']>> & (TDepth extends [] ? {} : InferRelationFilterFields<TSchema, InferRelationDefs<TSchema, TableDbName<T>>, TDepth>) & {
58
+ OR?: InferEntityFilters<TSchema, T, TDepth>[];
59
+ };
60
+ type InferInsertInput<T> = T extends Table ? WireFormat<T['$inferInsert']> : never;
61
+ type InferUpdateInput<T> = T extends Table ? {
62
+ [K in keyof T['$inferInsert']]?: ToWire<T['$inferInsert'][K]> | null;
63
+ } : never;
64
+ type InferOrderBy<T> = T extends Table ? {
65
+ [K in keyof T['$inferSelect']]?: {
66
+ direction: 'asc' | 'desc';
67
+ priority: number;
68
+ };
69
+ } : never;
70
+ type ExcludedNames<TConfig> = TConfig extends {
71
+ tables: {
72
+ exclude: readonly (infer T)[];
73
+ };
74
+ } ? T : never;
75
+ type NumberToTuple<N extends number, T extends unknown[] = []> = T['length'] extends N ? T : T['length'] extends 5 ? T : NumberToTuple<N, [...T, 0]>;
76
+ type ExtractFilterDepth<TConfig> = TConfig extends {
77
+ limitRelationDepth: infer D;
78
+ } ? D extends number ? number extends D ? [0] : NumberToTuple<D> : [0] : [0];
79
+ type TableDbName<T> = T extends Table<infer TConfig> ? TConfig['name'] extends string ? TConfig['name'] : string : string;
80
+ type DbNameToKey<TSchema> = {
81
+ [K in keyof ExtractTables<TSchema>]: TableDbName<ExtractTables<TSchema>[K]>;
82
+ };
83
+ type KeyForDbName<TSchema, TDbName extends string> = {
84
+ [K in keyof DbNameToKey<TSchema>]: DbNameToKey<TSchema>[K] extends TDbName ? K : never;
85
+ }[keyof DbNameToKey<TSchema>];
86
+ type ResolveRelationEntity<TSchema, TDbName extends string> = KeyForDbName<TSchema, TDbName> extends infer K ? (K extends string ? K : TDbName) : TDbName;
87
+ type ResolveRelationDefs<TSchema, TRels> = {
88
+ [K in keyof TRels]: TRels[K] extends {
89
+ entity: infer E;
90
+ type: infer T;
91
+ } ? E extends string ? Omit<TRels[K], 'entity'> & {
92
+ entity: ResolveRelationEntity<TSchema, E>;
93
+ type: T;
94
+ } : TRels[K] : TRels[K];
95
+ };
96
+ type BuildEntityDef<TSchema, T, TDepth extends unknown[]> = T extends Table ? {
97
+ fields: WireFormat<T['$inferSelect']>;
98
+ relations: ResolveRelationDefs<TSchema, InferRelationDefs<TSchema, TableDbName<T>>>;
99
+ filters: InferEntityFilters<TSchema, T, TDepth>;
100
+ insertInput: InferInsertInput<T>;
101
+ updateInput: InferUpdateInput<T>;
102
+ orderBy: InferOrderBy<T>;
103
+ } : never;
104
+ export type InferEntityDefs<TSchema, TConfig = Record<string, never>> = {
105
+ [K in keyof ExtractTables<TSchema> as K extends ExcludedNames<TConfig> ? never : K extends string ? K : never]: BuildEntityDef<TSchema, ExtractTables<TSchema>[K], ExtractFilterDepth<TConfig>>;
106
+ };
107
+ export {};
package/package.json CHANGED
@@ -1,14 +1,24 @@
1
1
  {
2
2
  "name": "@graphql-suite/client",
3
- "version": "0.8.3",
3
+ "version": "0.9.1",
4
4
  "description": "Type-safe GraphQL client with entity-based API and full Drizzle type inference",
5
5
  "license": "MIT",
6
6
  "author": "https://github.com/dmythro",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/annexare/drizzle-graphql-suite.git",
9
+ "url": "git+https://github.com/annexare/graphql-suite.git",
10
10
  "directory": "packages/client"
11
11
  },
12
+ "homepage": "https://graphql-suite.annexare.com/client/overview/",
13
+ "keywords": [
14
+ "drizzle",
15
+ "graphql",
16
+ "client",
17
+ "type-safe",
18
+ "typescript",
19
+ "orm",
20
+ "query-builder"
21
+ ],
12
22
  "type": "module",
13
23
  "publishConfig": {
14
24
  "access": "public"
@@ -21,11 +31,8 @@
21
31
  "import": "./index.js"
22
32
  }
23
33
  },
24
- "dependencies": {
25
- "@drizzle-graphql-suite/client": "0.8.3"
26
- },
27
34
  "peerDependencies": {
28
- "@drizzle-graphql-suite/schema": ">=0.8.0",
35
+ "@graphql-suite/schema": ">=0.9.0",
29
36
  "drizzle-orm": ">=0.44.0"
30
37
  }
31
38
  }
@@ -0,0 +1,12 @@
1
+ import type { EntityDescriptor, SchemaDescriptor } from './types';
2
+ export type BuiltQuery = {
3
+ query: string;
4
+ variables: Record<string, unknown>;
5
+ operationName: string;
6
+ };
7
+ export declare function buildListQuery(entityName: string, entityDef: EntityDescriptor, schema: SchemaDescriptor, select: Record<string, unknown>, hasWhere: boolean, hasOrderBy: boolean, hasLimit: boolean, hasOffset: boolean): BuiltQuery;
8
+ export declare function buildSingleQuery(entityName: string, entityDef: EntityDescriptor, schema: SchemaDescriptor, select: Record<string, unknown>, hasWhere: boolean, hasOrderBy: boolean, hasOffset: boolean): BuiltQuery;
9
+ export declare function buildCountQuery(entityName: string, entityDef: EntityDescriptor, hasWhere: boolean): BuiltQuery;
10
+ export declare function buildInsertMutation(entityName: string, entityDef: EntityDescriptor, schema: SchemaDescriptor, returning: Record<string, unknown> | undefined, isSingle: boolean): BuiltQuery;
11
+ export declare function buildUpdateMutation(entityName: string, entityDef: EntityDescriptor, schema: SchemaDescriptor, returning: Record<string, unknown> | undefined, hasWhere: boolean): BuiltQuery;
12
+ export declare function buildDeleteMutation(entityName: string, entityDef: EntityDescriptor, schema: SchemaDescriptor, returning: Record<string, unknown> | undefined, hasWhere: boolean): BuiltQuery;
@@ -0,0 +1,3 @@
1
+ import type { BuildSchemaConfig } from '@graphql-suite/schema';
2
+ import type { SchemaDescriptor } from './types';
3
+ export declare function buildSchemaDescriptor(schema: Record<string, unknown>, config?: BuildSchemaConfig): SchemaDescriptor;
package/types.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ export type RelationDef = {
2
+ entity: string;
3
+ type: 'one' | 'many';
4
+ required?: boolean;
5
+ };
6
+ export type EntityDef = {
7
+ fields: Record<string, unknown>;
8
+ relations: Record<string, RelationDef>;
9
+ filters?: Record<string, unknown>;
10
+ insertInput?: Record<string, unknown>;
11
+ updateInput?: Record<string, unknown>;
12
+ orderBy?: Record<string, unknown>;
13
+ };
14
+ export type AnyEntityDefs = Record<string, EntityDef>;
15
+ /** Opaque wrapper that prevents TS from expanding entity defs during serialization */
16
+ export interface EntityDefsRef<TDefs extends AnyEntityDefs> {
17
+ readonly __defs: TDefs;
18
+ }
19
+ export type EntityDescriptor = {
20
+ queryName: string;
21
+ queryListName: string;
22
+ countName: string;
23
+ insertName: string;
24
+ insertSingleName: string;
25
+ updateName: string;
26
+ deleteName: string;
27
+ fields: readonly string[];
28
+ relations: Record<string, {
29
+ entity: string;
30
+ type: 'one' | 'many';
31
+ }>;
32
+ };
33
+ export type SchemaDescriptor = Record<string, EntityDescriptor>;
34
+ export type SelectInput<TDefs extends AnyEntityDefs, TEntity extends EntityDef> = {
35
+ [K in keyof TEntity['fields'] | keyof TEntity['relations']]?: K extends keyof TEntity['relations'] ? TEntity['relations'][K] extends RelationDef ? TEntity['relations'][K]['entity'] extends keyof TDefs ? SelectInput<TDefs, TDefs[TEntity['relations'][K]['entity']]> : never : never : K extends keyof TEntity['fields'] ? true : never;
36
+ };
37
+ type Simplify<T> = {
38
+ [K in keyof T]: T[K];
39
+ } & {};
40
+ export type InferResult<TDefs extends AnyEntityDefs, TEntity extends EntityDef, TSelect> = Simplify<InferScalars<TEntity, TSelect> & InferRelations<TDefs, TEntity, TSelect>>;
41
+ type InferScalars<TEntity extends EntityDef, TSelect> = Pick<TEntity['fields'], keyof TSelect & keyof TEntity['fields']>;
42
+ type InferRelations<TDefs extends AnyEntityDefs, TEntity extends EntityDef, TSelect> = {
43
+ [K in keyof TSelect & keyof TEntity['relations'] as TSelect[K] extends Record<string, unknown> ? K : never]: TEntity['relations'][K] extends RelationDef ? TEntity['relations'][K]['entity'] extends keyof TDefs ? TEntity['relations'][K]['type'] extends 'many' ? InferResult<TDefs, TDefs[TEntity['relations'][K]['entity']], TSelect[K]>[] : InferResult<TDefs, TDefs[TEntity['relations'][K]['entity']], TSelect[K]> | null : never : never;
44
+ };
45
+ export type ClientConfig<TSchema extends SchemaDescriptor = SchemaDescriptor> = {
46
+ url: string | (() => string);
47
+ schema: TSchema;
48
+ headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
49
+ };
50
+ export {};