@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 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 `nestedWrites` are the exceptions: both default to `false`, so the upsert
115
- mutations and the relation fields on the write inputs only exist if you ask for them
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 list queries only — a single query returns one row either way
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. Put a depth limit in front of a publicly exposed schema as well — e.g.
737
- [`graphql-depth-limit`](https://github.com/stems/graphql-depth-limit) — or set
738
- `relationsDepthLimit: 0` to generate no relation fields at all.
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. A request that
786
- fires several mutations therefore commits each one separately, and a failure halfway leaves
787
- the earlier ones in place. To run a whole request as one unit, open a transaction yourself
788
- and put it on the GraphQL context under the exported `drizzleExecutorKey`:
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 |