@vantreeseba/drizzle-graphql 6.0.0 → 7.1.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 +372 -10
- package/dist/README.md +372 -10
- package/dist/index.cjs +1029 -447
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +132 -2
- package/dist/index.d.ts +132 -2
- package/dist/index.js +935 -343
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,9 +101,12 @@ const { schema } = buildSchema(db, {
|
|
|
101
101
|
distinct: false, // the `distinct` argument on list queries
|
|
102
102
|
insert: false, // create<Table> / create<Table>Single mutations
|
|
103
103
|
update: false, // update<Table> / update<Table>Single mutations
|
|
104
|
+
updateMany: false, // update<Table>Many batch mutation (needs `update`)
|
|
104
105
|
delete: false, // delete<Table> / delete<Table>Single mutations
|
|
105
106
|
upsert: true, // upsert<Table> / upsert<Table>Single mutations (off by default)
|
|
106
107
|
nestedWrites: true, // relation fields on the create/update inputs (off by default)
|
|
108
|
+
fieldUpdateOperations: true, // increment/push operations on the update input (off by default)
|
|
109
|
+
countMutations: true, // update<Table>Count / delete<Table>Count mutations (off by default)
|
|
107
110
|
requireWhere: true, // make `where` non-null on plural update/delete (off by default)
|
|
108
111
|
},
|
|
109
112
|
})
|
|
@@ -111,8 +114,8 @@ const { schema } = buildSchema(db, {
|
|
|
111
114
|
|
|
112
115
|
- Any flag left out keeps its default of `true`, so `{ features: { delete: false } }`
|
|
113
116
|
changes nothing else
|
|
114
|
-
- `upsert` and `
|
|
115
|
-
|
|
117
|
+
- `upsert`, `nestedWrites`, `fieldUpdateOperations` and `countMutations` are the
|
|
118
|
+
exceptions: all four default to `false`, so they only exist if you ask for them
|
|
116
119
|
- `groupBy` needs `aggregates`: it reuses those output types, so turning `aggregates` off
|
|
117
120
|
turns the group-by queries off with it
|
|
118
121
|
- Turning off `insert` or `update` also drops the input type that only that mutation
|
|
@@ -121,6 +124,38 @@ const { schema } = buildSchema(db, {
|
|
|
121
124
|
`mutations: false`
|
|
122
125
|
- List and single queries are always generated, so `Query` is never empty
|
|
123
126
|
|
|
127
|
+
## Naming
|
|
128
|
+
|
|
129
|
+
Type and field names are derived from the Drizzle schema keys. Three options change them.
|
|
130
|
+
|
|
131
|
+
`prefixes` and `suffixes` rename operations without touching type names:
|
|
132
|
+
|
|
133
|
+
```Typescript
|
|
134
|
+
const { schema } = buildSchema(db, {
|
|
135
|
+
prefixes: { insert: 'add', update: 'edit', delete: 'remove' }, // default: create / update / delete
|
|
136
|
+
suffixes: { list: 'All', single: '' }, // default: '' / 'Single'
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The list and single suffixes cannot be identical unless a `typeNameMapper` is also given —
|
|
141
|
+
`users` and `usersSingle` would otherwise collapse onto one field name, and `buildSchema`
|
|
142
|
+
throws rather than generating a schema that silently drops one of them.
|
|
143
|
+
|
|
144
|
+
`typeNameMapper` renames the table itself, which is what a plural-keyed schema usually wants:
|
|
145
|
+
`export const users = pgTable('users', …)` otherwise produces `type Users` and a
|
|
146
|
+
`usersSingle` query.
|
|
147
|
+
|
|
148
|
+
```Typescript
|
|
149
|
+
import pluralize from 'pluralize'
|
|
150
|
+
|
|
151
|
+
const { schema } = buildSchema(db, {
|
|
152
|
+
typeNameMapper: (table) => ({ singular: pluralize.singular(table), plural: table }),
|
|
153
|
+
})
|
|
154
|
+
// type User; queries `users` / `user`; mutations `createUsers` / `createUser`
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Return `undefined` for any table that should keep its default naming.
|
|
158
|
+
|
|
124
159
|
## Scalars
|
|
125
160
|
|
|
126
161
|
Columns whose values don't fit a built-in GraphQL scalar get a named custom scalar, so the
|
|
@@ -160,6 +195,25 @@ import { GraphQLBigIntString, GraphQLDate, GraphQLDateTime, GraphQLJSON, GraphQL
|
|
|
160
195
|
`GraphQLBigIntString` is the `BigInt` scalar — named for what it does rather than what it is
|
|
161
196
|
called in SDL, to avoid clashing with the language's own `BigInt`.
|
|
162
197
|
|
|
198
|
+
### Overriding a column's scalar
|
|
199
|
+
|
|
200
|
+
Built-in detection is a default, not a ceiling. `scalars` overrides named columns, and
|
|
201
|
+
`mapColumnType` applies a rule across every column at build time — the tool for a convention
|
|
202
|
+
("every `numeric` column is `Money`") or for a custom column type detection does not know:
|
|
203
|
+
|
|
204
|
+
```Typescript
|
|
205
|
+
const { schema } = buildSchema(db, {
|
|
206
|
+
scalars: {
|
|
207
|
+
users: { email: EmailAddressScalar }, // by table key, then column key
|
|
208
|
+
},
|
|
209
|
+
mapColumnType: (column) => (column.columnType === 'PgNumeric' ? MoneyScalar : undefined),
|
|
210
|
+
})
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Either may return a single scalar, or an `{ output, input }` pair when the read and write
|
|
214
|
+
types differ. `mapColumnType` returning `undefined` keeps the default, and a column matched
|
|
215
|
+
by both is decided by `scalars` alone.
|
|
216
|
+
|
|
163
217
|
## Enum types
|
|
164
218
|
|
|
165
219
|
A column declared with a database enum becomes a GraphQL enum named after **the enum**, not
|
|
@@ -467,7 +521,10 @@ enum. Rows sharing the same combination of those columns collapse to one:
|
|
|
467
521
|
- `where` is applied **before** rows are collapsed; `limit` and `offset` are applied
|
|
468
522
|
**after**, so `limit: 10` returns ten distinct rows
|
|
469
523
|
- Several columns are treated as one combined key, not as independent ones
|
|
470
|
-
- `distinct` is available on
|
|
524
|
+
- `distinct` is also available on to-many relation fields, where it means one row per
|
|
525
|
+
distinct value **per parent** — the window is partitioned by the foreign key as well as
|
|
526
|
+
the requested columns
|
|
527
|
+
- `distinct` is not on single queries — those return one row either way
|
|
471
528
|
|
|
472
529
|
It runs as an extra `row_number() over (partition by …)` query that picks the surviving
|
|
473
530
|
rows' primary keys, after which the main query is narrowed to them — so it needs the same
|
|
@@ -536,6 +593,32 @@ field, and passing it back as `after` resumes strictly after that row.
|
|
|
536
593
|
- If a table has a real column named `cursor`, the column wins and the meta field is
|
|
537
594
|
skipped
|
|
538
595
|
|
|
596
|
+
To-many relation fields take `after` and `distinct` too, with the same meaning they have on
|
|
597
|
+
a root list of the same table — a relation field is a list of the target table, so it gets
|
|
598
|
+
that table's whole pagination surface:
|
|
599
|
+
|
|
600
|
+
```graphql
|
|
601
|
+
{
|
|
602
|
+
users {
|
|
603
|
+
id
|
|
604
|
+
posts(orderBy: { createdAt: { direction: desc, priority: 1 } }, limit: 10, after: "…") {
|
|
605
|
+
id
|
|
606
|
+
cursor
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
Drizzle's `with:` clause cannot express either one — keyset needs a predicate over the
|
|
613
|
+
request's own total order, `distinct` needs a window pass, and a `cursor` has to be computed
|
|
614
|
+
from the raw row. So a relation field that is passed `after` or `distinct`, or that selects
|
|
615
|
+
`cursor`, drops out of the eager fetch and resolves through the request-scoped batch loader
|
|
616
|
+
instead, which implements all three. That is still **one query per relation** across every
|
|
617
|
+
parent in the batch (`distinct` adds its key-selection pass, as on a root list) — never one
|
|
618
|
+
per parent. Nothing about the arguments changes; only how many round-trips the request
|
|
619
|
+
costs.
|
|
620
|
+
|
|
621
|
+
|
|
539
622
|
## Upsert
|
|
540
623
|
|
|
541
624
|
`features.upsert` adds a pair of mutations per table that insert rows, or update the ones
|
|
@@ -660,6 +743,71 @@ and interleaves awaited statements inside a transaction, which a synchronous SQL
|
|
|
660
743
|
cannot do — `buildSchema` rejects both combinations rather than generating something that
|
|
661
744
|
half-works.
|
|
662
745
|
|
|
746
|
+
## Atomic column updates
|
|
747
|
+
|
|
748
|
+
`Update<Table>Input` sets columns to values, so "increment the view count" is a read followed
|
|
749
|
+
by a write of the number the read returned — two round-trips, and a lost update whenever two
|
|
750
|
+
clients read the same number. `features.fieldUpdateOperations` replaces qualifying columns'
|
|
751
|
+
update fields with an operations input, so the arithmetic happens in the database:
|
|
752
|
+
|
|
753
|
+
```graphql
|
|
754
|
+
mutation {
|
|
755
|
+
updatePostsSingle(where: { id: { eq: 1 } }, set: { views: { increment: 1 } }) {
|
|
756
|
+
views
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
compiles to `SET views = views + 1`. The flag changes the update inputs only; insert inputs
|
|
762
|
+
are untouched.
|
|
763
|
+
|
|
764
|
+
- **Numeric columns** (`Int`, `Float`, `BigInt`, `Decimal`) take
|
|
765
|
+
`set` / `increment` / `decrement` / `multiply` / `divide`
|
|
766
|
+
- **Array columns** take `set` / `push`, where `push` appends to the existing array
|
|
767
|
+
- Exactly one operation per column. Passing two, or `{}`, is a `GraphQLError` naming the
|
|
768
|
+
column — graphql-js has no `@oneOf` on the version range this library supports, so it is
|
|
769
|
+
enforced at resolve time rather than by the type
|
|
770
|
+
- `set` is the explicit spelling of the plain assignment the field had before the flag, so
|
|
771
|
+
every existing update has a one-key rewrite: `{ views: 5 }` → `{ views: { set: 5 } }`
|
|
772
|
+
- The input types are named for the **scalar**, not the column — every `Int` column in the
|
|
773
|
+
schema shares one `IntFieldUpdate` — and a column claimed by a `scalarOverrides` entry
|
|
774
|
+
keeps its plain field, since the override decides what that column accepts
|
|
775
|
+
|
|
776
|
+
Two things follow the database's own semantics rather than being guarded:
|
|
777
|
+
|
|
778
|
+
- `increment`/`multiply` on an integer column can overflow, and `divide` on one **rounds**
|
|
779
|
+
the way the dialect rounds (PostgreSQL truncates toward zero; use a `Float` or `Decimal`
|
|
780
|
+
column if you need the fraction)
|
|
781
|
+
- `divide: 0` raises the driver's division-by-zero error, surfaced as a `GraphQLError`
|
|
782
|
+
|
|
783
|
+
## Row counts instead of rows
|
|
784
|
+
|
|
785
|
+
The plural `update<Table>` / `delete<Table>` mutations always `RETURNING *`, which is wasted
|
|
786
|
+
work — and wasted bandwidth — when a client only wants to know how many rows were touched.
|
|
787
|
+
`features.countMutations` adds a count-returning variant of each:
|
|
788
|
+
|
|
789
|
+
```graphql
|
|
790
|
+
mutation {
|
|
791
|
+
deletePostsCount(where: { status: { eq: "spam" } })
|
|
792
|
+
updatePostsCount(where: { draft: { eq: true } }, set: { draft: false })
|
|
793
|
+
}
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
- Both return `Int!` — the number of rows the write affected — and add no new output type
|
|
797
|
+
to the schema
|
|
798
|
+
- Arguments are exactly the plural mutation's, so a client swaps one field name for the
|
|
799
|
+
other; `features.requireWhere` applies to them the same way
|
|
800
|
+
- They follow the feature switch of the write they mirror: `updateCount` needs
|
|
801
|
+
`features.update`, `deleteCount` needs `features.delete`
|
|
802
|
+
- **MySQL** gets them too, and they are arguably most useful there: MySQL's other mutations
|
|
803
|
+
return only `MutationReturn { isSuccess }`, so a row count is strictly more information
|
|
804
|
+
- `update<Table>Count` **rejects** nested writes with a `GraphQLError` — a nested write
|
|
805
|
+
needs the parent rows this mutation deliberately does not read back. Use the plural
|
|
806
|
+
mutation for those
|
|
807
|
+
- The count is read from whichever field the driver reports it in (`rowCount`,
|
|
808
|
+
`affectedRows`, `rowsAffected`, `changes`, `count`); a driver that reports none is a
|
|
809
|
+
`GraphQLError` rather than a silent `0`
|
|
810
|
+
|
|
663
811
|
## Single-row update & delete
|
|
664
812
|
|
|
665
813
|
Alongside the plural `update<Table>` / `delete<Table>` mutations, every table gets an
|
|
@@ -686,11 +834,67 @@ mutation {
|
|
|
686
834
|
- The variants are generated under `features.update` / `features.delete`, share the plural
|
|
687
835
|
mutations' input types, and appear in `entities.mutations` for custom schemas
|
|
688
836
|
|
|
837
|
+
### Mutation return shapes
|
|
838
|
+
|
|
839
|
+
Every mutation that returns rows returns them non-null, with two exceptions that are
|
|
840
|
+
deliberate rather than accidental:
|
|
841
|
+
|
|
842
|
+
| Mutation | Returns | Why |
|
|
843
|
+
| --- | --- | --- |
|
|
844
|
+
| `create<Table>Single` | `Table!`, or `Table` under `conflictDoNothing` | An insert of one row returns that row. It can only fail to produce one when the `conflictDoNothing` build option swallows a conflict, so it is nullable exactly when that option is set |
|
|
845
|
+
| `update<Table>Many` | `[Table]!` | Each entry's updated rows **in entry order**. An entry whose `where` matched nothing contributes `null` in its slot, so the response lines up with the request positionally — the nullable element is the whole point |
|
|
846
|
+
| everything else | `[Table!]!` / `Table!` / `Table` | — |
|
|
847
|
+
|
|
689
848
|
The plural mutations still accept a missing `where` (a full-table write) by default. To rule
|
|
690
849
|
that out at the type level, `features: { requireWhere: true }` makes `where` non-null on
|
|
691
850
|
`update<Table>` / `delete<Table>` and rejects a `where` with no filters, exactly like the
|
|
692
851
|
Single variants. It defaults to `false` for backwards compatibility.
|
|
693
852
|
|
|
853
|
+
## Bounding page size
|
|
854
|
+
|
|
855
|
+
Nothing stops a client asking a list query for every row it has. `limits` sets a page size for
|
|
856
|
+
a request that names none, a ceiling for one that does, or both:
|
|
857
|
+
|
|
858
|
+
```ts
|
|
859
|
+
buildSchema(db, {
|
|
860
|
+
limits: {
|
|
861
|
+
defaultLimit: 25,
|
|
862
|
+
maxLimit: 100,
|
|
863
|
+
tables: {
|
|
864
|
+
Posts: { defaultLimit: 50 },
|
|
865
|
+
AuditLog: { maxLimit: 10, clampToMax: true },
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
});
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
| Key | Effect |
|
|
872
|
+
| --- | --- |
|
|
873
|
+
| `defaultLimit` | The `limit` applied when the request passes none |
|
|
874
|
+
| `maxLimit` | The largest `limit` a request may ask for |
|
|
875
|
+
| `clampToMax` | `false` (default) rejects a `limit` above `maxLimit`; `true` reduces it |
|
|
876
|
+
| `tables` | Per-table overrides, keyed by the table's key in your Drizzle schema |
|
|
877
|
+
|
|
878
|
+
A per-table policy overrides the global one key by key, so `AuditLog` above still inherits
|
|
879
|
+
`defaultLimit: 25`. Everything is optional; a build with no `limits` behaves exactly as before.
|
|
880
|
+
|
|
881
|
+
Two details worth stating plainly:
|
|
882
|
+
|
|
883
|
+
- **`maxLimit` alone also bounds a request that passes no `limit`.** No limit means every
|
|
884
|
+
row, which is above any maximum, so such a request runs at `maxLimit`.
|
|
885
|
+
- **A rejection is the default because truncation lies.** Silently cutting a page down to
|
|
886
|
+
`maxLimit` tells a paginating client it has reached the end when it has not. `clampToMax:
|
|
887
|
+
true` opts into that when you would rather never fail a query.
|
|
888
|
+
|
|
889
|
+
The policy applies to root list queries and to to-many relation fields, on both the eager and
|
|
890
|
+
the lazy relation paths. A relation takes the policy of the table it *reads*, not its parent's:
|
|
891
|
+
with `tables: { Posts: { maxLimit: 10 } }`, `users { posts(limit: 50) }` is rejected. Cursor
|
|
892
|
+
pagination inherits it — a page defaults to `defaultLimit` and the `cursor` values stay usable.
|
|
893
|
+
|
|
894
|
+
What it does not touch: `<tableName>Single` queries, which are already one row; aggregate,
|
|
895
|
+
group-by and `<relation>Aggregate` fields, which count over the whole table rather than
|
|
896
|
+
returning rows; and to-one relation fields, which take no `limit` at all.
|
|
897
|
+
|
|
694
898
|
## Query cost
|
|
695
899
|
|
|
696
900
|
Generated fields carry a `complexity` hint in their GraphQL extensions, ready for
|
|
@@ -708,6 +912,10 @@ fields it mentions:
|
|
|
708
912
|
So `users(limit: 20) { id posts(limit: 5) { id } }` costs `20 * (1 + 5 * 1)` = 120, while the
|
|
709
913
|
same query without limits costs `10 * (1 + 10 * 1)` = 110.
|
|
710
914
|
|
|
915
|
+
A [limit policy](#bounding-page-size) feeds into the hint: a field with no `limit` is priced at
|
|
916
|
+
its `defaultLimit` rather than `defaultListSize`, and nothing is priced above `maxLimit`, so the
|
|
917
|
+
cost matches the rows the query can actually pull.
|
|
918
|
+
|
|
711
919
|
The hints do nothing until you install a complexity rule, so they are generated by default:
|
|
712
920
|
|
|
713
921
|
```ts
|
|
@@ -733,9 +941,108 @@ buildSchema(db, { complexity: false });
|
|
|
733
941
|
|
|
734
942
|
Cost is not a depth bound. A cyclic relation graph (`user -> posts -> author -> posts -> …`)
|
|
735
943
|
lets a client nest as deep as it likes, and a deep query over cheap fields can stay under any
|
|
736
|
-
complexity ceiling.
|
|
737
|
-
|
|
738
|
-
|
|
944
|
+
complexity ceiling. Set `relationsDepthLimit: 0` to generate no relation fields at all, or —
|
|
945
|
+
more usefully — bound depth at the edge, along with the rest of the document-shape defences
|
|
946
|
+
below.
|
|
947
|
+
|
|
948
|
+
### Hardening a public endpoint
|
|
949
|
+
|
|
950
|
+
A generated schema is wide by construction: every table is reachable, every relation is
|
|
951
|
+
traversable, and the relation graph is usually cyclic. The `complexity` hints price that
|
|
952
|
+
surface, but only for the shapes the generator knows about, and nothing in this library
|
|
953
|
+
inspects the incoming document. For a schema exposed to untrusted clients, put
|
|
954
|
+
[GraphQL Armor](https://github.com/Escape-Technologies/graphql-armor) in front of it — it
|
|
955
|
+
bundles the document-level defences this library deliberately does not generate:
|
|
956
|
+
|
|
957
|
+
| Protection | What it bounds |
|
|
958
|
+
| --- | --- |
|
|
959
|
+
| `maxDepth` | nesting through a cyclic relation graph |
|
|
960
|
+
| `maxAliases` | the same expensive list requested many times under many aliases |
|
|
961
|
+
| `maxDirectives` | directive-count amplification |
|
|
962
|
+
| `maxTokens` | parser blow-up, rejected before validation runs |
|
|
963
|
+
| `costLimit` | a static per-document cost ceiling |
|
|
964
|
+
| `blockFieldSuggestion` | `Did you mean "keyHash"?` leaking field names on a typo |
|
|
965
|
+
|
|
966
|
+
```ts
|
|
967
|
+
import { EnvelopArmor } from '@escape.tech/graphql-armor';
|
|
968
|
+
import { createYoga } from 'graphql-yoga';
|
|
969
|
+
|
|
970
|
+
const { schema } = buildSchema(db);
|
|
971
|
+
const armor = new EnvelopArmor({
|
|
972
|
+
maxDepth: { n: 8 }, // roughly three relation hops into the generated graph
|
|
973
|
+
maxAliases: { n: 15 },
|
|
974
|
+
costLimit: { maxCost: 5000 },
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
const yoga = createYoga({ schema, plugins: armor.protect().plugins });
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
`ApolloArmor` is the Apollo Server equivalent — its `protect()` returns `plugins` plus
|
|
981
|
+
`validationRules`.
|
|
982
|
+
|
|
983
|
+
Armor's `costLimit` is its own document-shape estimate and does not read the `complexity`
|
|
984
|
+
extensions above; the two compose, if you want a cheap static ceiling alongside row-count
|
|
985
|
+
aware pricing. Neither one bounds an individual `limit` argument, so a single unbounded list
|
|
986
|
+
query is still a full table scan — cap `limit` yourself if that matters.
|
|
987
|
+
|
|
988
|
+
## Authorization
|
|
989
|
+
|
|
990
|
+
`buildSchema` is handed a Drizzle instance, not a request. It has no notion of who is asking,
|
|
991
|
+
so it generates no authorization: every table it knows about is queryable and writable by
|
|
992
|
+
anyone who can reach the endpoint. Auth belongs to a layer above the schema, where the
|
|
993
|
+
request context actually lives.
|
|
994
|
+
|
|
995
|
+
- **[graphql-shield](https://github.com/maticzav/graphql-shield)** — permission rules
|
|
996
|
+
mapped onto fields and applied with
|
|
997
|
+
[`graphql-middleware`](https://github.com/maticzav/graphql-middleware). Rules attach to
|
|
998
|
+
root fields *and* to fields of a generated object type, which is what makes it an answer
|
|
999
|
+
to "this column must never leave the server" and not just "this query is blocked":
|
|
1000
|
+
|
|
1001
|
+
```ts
|
|
1002
|
+
import { applyMiddleware } from 'graphql-middleware';
|
|
1003
|
+
import { allow, deny, rule, shield } from 'graphql-shield';
|
|
1004
|
+
|
|
1005
|
+
const isAuthenticated = rule()((parent, args, ctx) => Boolean(ctx.userId));
|
|
1006
|
+
|
|
1007
|
+
const { schema } = buildSchema(db);
|
|
1008
|
+
const guarded = applyMiddleware(
|
|
1009
|
+
schema,
|
|
1010
|
+
shield(
|
|
1011
|
+
{
|
|
1012
|
+
Query: { users: isAuthenticated, usersSingle: isAuthenticated },
|
|
1013
|
+
Mutation: { '*': isAuthenticated },
|
|
1014
|
+
ApiKeys: { keyHash: deny }, // field stays in the schema, never resolves
|
|
1015
|
+
},
|
|
1016
|
+
{ fallbackRule: allow },
|
|
1017
|
+
),
|
|
1018
|
+
);
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
- **[CASL](https://casl.js.org)** (`@casl/ability`) — one ability definition, conditions and
|
|
1022
|
+
field lists included (`can('read', 'Posts', ['title', 'body'])`), shared between the
|
|
1023
|
+
GraphQL layer and the rest of the app instead of a permission model that exists only in
|
|
1024
|
+
the API. CASL is not GraphQL middleware itself: wire it in through the shield rules above
|
|
1025
|
+
or through your own resolver wrapper, and let the ability answer the questions.
|
|
1026
|
+
|
|
1027
|
+
What neither can do is **narrow** a query. They permit or deny a field; they do not append
|
|
1028
|
+
`WHERE user_id = …` to the SQL a generated resolver runs, and a permitted `users` query still
|
|
1029
|
+
returns every tenant's rows. Until row scoping is a first-class option here, the ways to get
|
|
1030
|
+
it are, in increasing order of effort:
|
|
1031
|
+
|
|
1032
|
+
- supply the tenant predicate as an ordinary `where` argument from a trusted gateway, and
|
|
1033
|
+
deny the raw generated fields to everyone else;
|
|
1034
|
+
- build the schema by hand from `entities`, exposing only the generated resolvers you have
|
|
1035
|
+
wrapped;
|
|
1036
|
+
- put the scoped tables behind database row-level security and give the driver the
|
|
1037
|
+
request's identity, so the narrowing happens below Drizzle entirely.
|
|
1038
|
+
|
|
1039
|
+
None of this removes anything from introspection either — `deny` hides the data, not the
|
|
1040
|
+
shape. If the schema must not so much as mention a table, keep that table out of the Drizzle
|
|
1041
|
+
instance you pass to `buildSchema` (a second instance over a narrowed schema object is
|
|
1042
|
+
cheap).
|
|
1043
|
+
|
|
1044
|
+
Nor is it a row bound: a complexity ceiling prices the `limit` a client asks for, it does not
|
|
1045
|
+
cap it. Use [`limits`](#bounding-page-size) for that.
|
|
739
1046
|
|
|
740
1047
|
## Error handling
|
|
741
1048
|
|
|
@@ -782,10 +1089,44 @@ want to fall back to it explicitly.
|
|
|
782
1089
|
|
|
783
1090
|
## Transactions
|
|
784
1091
|
|
|
785
|
-
Each resolver runs its statements on the database the schema was built from
|
|
786
|
-
fires several mutations
|
|
787
|
-
the earlier ones in place.
|
|
788
|
-
|
|
1092
|
+
Each resolver runs its statements on the database the schema was built from, so by default a
|
|
1093
|
+
request that fires several mutations commits each one separately and a failure halfway leaves
|
|
1094
|
+
the earlier ones in place. There are two ways to make a request atomic.
|
|
1095
|
+
|
|
1096
|
+
### Letting the library open one
|
|
1097
|
+
|
|
1098
|
+
`transactions: 'auto'` opens `db.transaction()` once per request whose document selects more
|
|
1099
|
+
than one root mutation field, and runs every mutation field — plus the reads nested under
|
|
1100
|
+
them — inside it:
|
|
1101
|
+
|
|
1102
|
+
```Typescript
|
|
1103
|
+
const { schema } = buildSchema(db, { transactions: 'auto' })
|
|
1104
|
+
|
|
1105
|
+
// or, to change the safety timeout:
|
|
1106
|
+
const { schema } = buildSchema(db, { transactions: { mode: 'auto', timeoutMs: 10_000 } })
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
The transaction commits when the last mutation field completes and rolls back if any of them
|
|
1110
|
+
fails; mutation fields after a failure fail fast instead of running against a rolled-back
|
|
1111
|
+
transaction. A document with one mutation field or none opens nothing, so ordinary requests
|
|
1112
|
+
are untouched.
|
|
1113
|
+
|
|
1114
|
+
- The driver must support `db.transaction()` — `neon-http` does not. For SQLite only an
|
|
1115
|
+
asynchronous driver can hold a transaction open across resolvers, and `buildSchema`
|
|
1116
|
+
throws for a synchronous one rather than failing at request time
|
|
1117
|
+
- The GraphQL context value must be a fresh object per request (every mainstream server
|
|
1118
|
+
does this) — it keys the per-request transaction state
|
|
1119
|
+
- `timeoutMs` (default `30000`) rolls back an abandoned transaction, e.g. when the host
|
|
1120
|
+
server stopped calling resolvers because of a non-null completion error, instead of
|
|
1121
|
+
leaking the connection
|
|
1122
|
+
- A document that mixes in mutation fields this build did not generate is left alone: their
|
|
1123
|
+
completion cannot be tracked, so no transaction is opened
|
|
1124
|
+
- It never nests — an executor supplied on the context wins, as below
|
|
1125
|
+
|
|
1126
|
+
### Supplying your own executor
|
|
1127
|
+
|
|
1128
|
+
To control the boundary yourself, open a transaction and put it on the GraphQL context under
|
|
1129
|
+
the exported `drizzleExecutorKey`:
|
|
789
1130
|
|
|
790
1131
|
```Typescript
|
|
791
1132
|
import { buildSchema, drizzleExecutorKey } from 'drizzle-graphql'
|
|
@@ -911,3 +1252,24 @@ Opting a relation out does **not** remove its field — it keeps resolving lazil
|
|
|
911
1252
|
request-scoped batch loader, so the field still works even before you override it. Table
|
|
912
1253
|
and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys
|
|
913
1254
|
of `entities.fieldResolvers`.
|
|
1255
|
+
|
|
1256
|
+
## Configuration reference
|
|
1257
|
+
|
|
1258
|
+
Every key of the second argument to `buildSchema(db, config?)`. Each links to the section
|
|
1259
|
+
that explains it, where there is one.
|
|
1260
|
+
|
|
1261
|
+
| Key | Default | What it does |
|
|
1262
|
+
| --- | --- | --- |
|
|
1263
|
+
| `mutations` | `true` | `false` omits the `Mutation` type entirely |
|
|
1264
|
+
| `features` | see [Choosing what gets generated](#choosing-what-gets-generated) | which operations are generated, per operation |
|
|
1265
|
+
| `relationsDepthLimit` | unlimited | how deep relation fields are generated; `0` gives a flat, columns-only schema |
|
|
1266
|
+
| `prefixes` / `suffixes` / `typeNameMapper` | see [Naming](#naming) | operation and type names |
|
|
1267
|
+
| `scalars` / `mapColumnType` | built-in detection | [override a column's GraphQL type](#overriding-a-columns-scalar), by name or by rule |
|
|
1268
|
+
| `enumNameMapper` | shared `<EnumName>Enum` per database enum | [names the enum a column maps to](#enum-types) |
|
|
1269
|
+
| `describeColumn` / `describeTable` / `describeRelation` | none | [GraphQL descriptions](#documenting-the-schema) |
|
|
1270
|
+
| `deprecateColumn` | none | `@deprecated` on a column's output and optional input fields |
|
|
1271
|
+
| `conflictDoNothing` | `false` | PostgreSQL only — `ON CONFLICT DO NOTHING` on inserts, which makes `create<Table>Single` nullable |
|
|
1272
|
+
| `complexity` | on | [cost hints](#query-cost) in field extensions; `false` turns them off |
|
|
1273
|
+
| `transactions` | `'none'` | `'auto'` wraps a multi-mutation request in one [transaction](#transactions) |
|
|
1274
|
+
| `onError` | replaces driver errors | [error handling](#error-handling) hook — log, or surface a different error |
|
|
1275
|
+
| `eagerLoadRelations` | `true` | [which relations are pre-fetched](#overriding-a-relations-resolver-without-overfetching) with the parent query |
|