@vantreeseba/drizzle-graphql 1.0.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,3 +84,78 @@ Automatically create GraphQL schema or customizable schema config fields from Dr
84
84
  console.info('Server is running on http://localhost:4000/graphql')
85
85
  })
86
86
  ```
87
+
88
+ ## Relations & N+1 handling
89
+
90
+ Generated schemas resolve nested relations without N+1 query explosions:
91
+
92
+ - **Queries** — root queries (`entities.queries.*`) eagerly load every requested
93
+ relation in a single round-trip using Drizzle's relational query builder
94
+ (`with:`), including nested relations and per-relation `where` / `orderBy` /
95
+ `limit` / `offset` arguments.
96
+ - **Mutations (PostgreSQL & SQLite)** — after an insert or update, if the selection set
97
+ includes relation fields, the affected rows are re-fetched by primary key through one
98
+ relational query so their relations are eagerly loaded (single- and composite-column
99
+ primary keys are supported). `delete` mutations keep using the request-scoped batch
100
+ loader, since the rows no longer exist to re-fetch. MySQL mutations return only a
101
+ success flag (no row payload), so this step does not apply there.
102
+ - **Custom schemas** — each relation field also has a standalone resolver, exported as
103
+ `entities.fieldResolvers[TableName][relationName]`. When you wire generated types
104
+ into your own schema and your root resolver does **not** pre-fetch relations, these
105
+ resolvers batch all sibling loads in a request into a single `IN (…)` query
106
+ (request-scoped, keyed on the GraphQL `context`), preventing N+1. Per-parent
107
+ `limit` / `offset` on a to-many relation is applied across the whole batch using a
108
+ `ROW_NUMBER() OVER (PARTITION BY …)` window function — still one query, not one per
109
+ parent.
110
+
111
+ ```Typescript
112
+ // entities.fieldResolvers is keyed by table name, then relation name
113
+ const usersPostsResolver = entities.fieldResolvers.Users.posts
114
+ ```
115
+
116
+ > [!IMPORTANT]
117
+ > Per-parent paginated relations (a to-many relation field with `limit` or `offset`)
118
+ > rely on SQL window functions. These require **PostgreSQL**, **MySQL 8.0+**, or
119
+ > **SQLite 3.25+**. Relations without pagination, and all other query/mutation paths,
120
+ > have no such requirement.
121
+
122
+ ### Overriding a relation's resolver without overfetching
123
+
124
+ By default the eager `with:` pre-fetch is driven purely by the GraphQL selection set:
125
+ any selected relation is fetched from the database, even if you intend to resolve it
126
+ yourself (from a cache, another service, a dataloader, …). To override a relation's
127
+ resolver, first opt it out of eager loading with `eagerLoadRelations` so the parent
128
+ query stops fetching it, then supply your resolver with the standard
129
+ [`@graphql-tools/schema`](https://the-guild.dev/graphql/tools) utilities:
130
+
131
+ ```Typescript
132
+ import { addResolversToSchema } from '@graphql-tools/schema'
133
+
134
+ const { schema } = buildSchema(db, {
135
+ // Exclude Users.posts from the parent query's `with:` clause.
136
+ // Other relations keep eager-loading as usual.
137
+ eagerLoadRelations: (table, relation) => !(table === 'Users' && relation === 'posts'),
138
+ })
139
+
140
+ const finalSchema = addResolversToSchema({
141
+ schema,
142
+ resolvers: {
143
+ Users: {
144
+ posts: (parent, args, context) => context.postsLoader.load(parent.id),
145
+ },
146
+ },
147
+ })
148
+ ```
149
+
150
+ `eagerLoadRelations` accepts:
151
+
152
+ - `true` (default) — eager-load every relation.
153
+ - `false` — never eager-load; every relation resolves lazily through its
154
+ (request-batched) field resolver.
155
+ - `(tableName, relationName) => boolean` — decide per relation. Returning `false`
156
+ excludes that relation from `with:` (and from the mutation eager re-fetch).
157
+
158
+ Opting a relation out does **not** remove its field — it keeps resolving lazily via the
159
+ request-scoped batch loader, so the field still works even before you override it. Table
160
+ and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys
161
+ of `entities.fieldResolvers`.
package/dist/README.md CHANGED
@@ -84,3 +84,78 @@ Automatically create GraphQL schema or customizable schema config fields from Dr
84
84
  console.info('Server is running on http://localhost:4000/graphql')
85
85
  })
86
86
  ```
87
+
88
+ ## Relations & N+1 handling
89
+
90
+ Generated schemas resolve nested relations without N+1 query explosions:
91
+
92
+ - **Queries** — root queries (`entities.queries.*`) eagerly load every requested
93
+ relation in a single round-trip using Drizzle's relational query builder
94
+ (`with:`), including nested relations and per-relation `where` / `orderBy` /
95
+ `limit` / `offset` arguments.
96
+ - **Mutations (PostgreSQL & SQLite)** — after an insert or update, if the selection set
97
+ includes relation fields, the affected rows are re-fetched by primary key through one
98
+ relational query so their relations are eagerly loaded (single- and composite-column
99
+ primary keys are supported). `delete` mutations keep using the request-scoped batch
100
+ loader, since the rows no longer exist to re-fetch. MySQL mutations return only a
101
+ success flag (no row payload), so this step does not apply there.
102
+ - **Custom schemas** — each relation field also has a standalone resolver, exported as
103
+ `entities.fieldResolvers[TableName][relationName]`. When you wire generated types
104
+ into your own schema and your root resolver does **not** pre-fetch relations, these
105
+ resolvers batch all sibling loads in a request into a single `IN (…)` query
106
+ (request-scoped, keyed on the GraphQL `context`), preventing N+1. Per-parent
107
+ `limit` / `offset` on a to-many relation is applied across the whole batch using a
108
+ `ROW_NUMBER() OVER (PARTITION BY …)` window function — still one query, not one per
109
+ parent.
110
+
111
+ ```Typescript
112
+ // entities.fieldResolvers is keyed by table name, then relation name
113
+ const usersPostsResolver = entities.fieldResolvers.Users.posts
114
+ ```
115
+
116
+ > [!IMPORTANT]
117
+ > Per-parent paginated relations (a to-many relation field with `limit` or `offset`)
118
+ > rely on SQL window functions. These require **PostgreSQL**, **MySQL 8.0+**, or
119
+ > **SQLite 3.25+**. Relations without pagination, and all other query/mutation paths,
120
+ > have no such requirement.
121
+
122
+ ### Overriding a relation's resolver without overfetching
123
+
124
+ By default the eager `with:` pre-fetch is driven purely by the GraphQL selection set:
125
+ any selected relation is fetched from the database, even if you intend to resolve it
126
+ yourself (from a cache, another service, a dataloader, …). To override a relation's
127
+ resolver, first opt it out of eager loading with `eagerLoadRelations` so the parent
128
+ query stops fetching it, then supply your resolver with the standard
129
+ [`@graphql-tools/schema`](https://the-guild.dev/graphql/tools) utilities:
130
+
131
+ ```Typescript
132
+ import { addResolversToSchema } from '@graphql-tools/schema'
133
+
134
+ const { schema } = buildSchema(db, {
135
+ // Exclude Users.posts from the parent query's `with:` clause.
136
+ // Other relations keep eager-loading as usual.
137
+ eagerLoadRelations: (table, relation) => !(table === 'Users' && relation === 'posts'),
138
+ })
139
+
140
+ const finalSchema = addResolversToSchema({
141
+ schema,
142
+ resolvers: {
143
+ Users: {
144
+ posts: (parent, args, context) => context.postsLoader.load(parent.id),
145
+ },
146
+ },
147
+ })
148
+ ```
149
+
150
+ `eagerLoadRelations` accepts:
151
+
152
+ - `true` (default) — eager-load every relation.
153
+ - `false` — never eager-load; every relation resolves lazily through its
154
+ (request-batched) field resolver.
155
+ - `(tableName, relationName) => boolean` — decide per relation. Returning `false`
156
+ excludes that relation from `with:` (and from the mutation eager re-fetch).
157
+
158
+ Opting a relation out does **not** remove its field — it keeps resolving lazily via the
159
+ request-scoped batch loader, so the field still works even before you override it. Table
160
+ and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys
161
+ of `entities.fieldResolvers`.