@vantreeseba/drizzle-graphql 4.2.0 → 5.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
@@ -100,9 +100,10 @@ const { schema } = buildSchema(db, {
100
100
  relationAggregates: false, // <relation>Aggregate fields on object types
101
101
  distinct: false, // the `distinct` argument on list queries
102
102
  insert: false, // create<Table> / create<Table>Single mutations
103
- update: false, // update<Table> mutations
104
- delete: false, // delete<Table> mutations
103
+ update: false, // update<Table> / update<Table>Single mutations
104
+ delete: false, // delete<Table> / delete<Table>Single mutations
105
105
  upsert: true, // upsert<Table> / upsert<Table>Single mutations (off by default)
106
+ requireWhere: true, // make `where` non-null on plural update/delete (off by default)
106
107
  },
107
108
  })
108
109
  ```
@@ -204,8 +205,10 @@ A **to-many** relation takes a `some` / `none` / `every` wrapper
204
205
  - `some: {}` means "at least one related row exists"; `none: {}` means "none exist"
205
206
  - Several modes may be given at once and are `AND`ed together
206
207
  - A relation whose name collides with a column name is skipped — the column keeps the field
207
- - Many-to-many relations declared with `.through()` are not filterable yet and are left out
208
- of the filter input
208
+ - Many-to-many relations declared with `.through()` are filterable too: the `EXISTS`
209
+ subquery joins the junction table to the target, so
210
+ `users(where: { roles: { some: { name: { eq: "admin" } } } })` works the same as a direct
211
+ to-many relation
209
212
 
210
213
  ## Aggregate queries
211
214
 
@@ -465,6 +468,37 @@ The build-wide `conflictDoNothing` option is deprecated in favour of this: it ap
465
468
  `create*` mutation with no way for a request to opt out. `onConflict: { action: NOTHING }` is
466
469
  the per-request replacement.
467
470
 
471
+ ## Single-row update & delete
472
+
473
+ Alongside the plural `update<Table>` / `delete<Table>` mutations, every table gets an
474
+ `update<Table>Single` / `delete<Table>Single` variant that targets exactly one row:
475
+
476
+ ```graphql
477
+ mutation {
478
+ updateUsersSingle(where: { id: { eq: 1 } }, set: { name: "Dan" }) {
479
+ id
480
+ name
481
+ }
482
+ }
483
+ ```
484
+
485
+ - `where` is **non-null** and must contain at least one filter — `where: {}` is rejected at
486
+ resolve time, so a Single write can never become an unbounded one
487
+ - The return type is the single (nullable) row type, not a list: the affected row comes back
488
+ directly, and **no match returns `null`** instead of an empty list
489
+ - If `where` matches **more than one row**, the mutation throws a `GraphQLError` and writes
490
+ nothing — the match is checked (`LIMIT 2`) before the write runs
491
+ - **MySQL** cannot return the affected row (no `RETURNING`), so its Single variants keep the
492
+ single-match and non-empty-`where` guarantees but return `MutationReturn` like every other
493
+ MySQL mutation
494
+ - The variants are generated under `features.update` / `features.delete`, share the plural
495
+ mutations' input types, and appear in `entities.mutations` for custom schemas
496
+
497
+ The plural mutations still accept a missing `where` (a full-table write) by default. To rule
498
+ that out at the type level, `features: { requireWhere: true }` makes `where` non-null on
499
+ `update<Table>` / `delete<Table>` and rejects a `where` with no filters, exactly like the
500
+ Single variants. It defaults to `false` for backwards compatibility.
501
+
468
502
  ## Query cost
469
503
 
470
504
  Generated fields carry a `complexity` hint in their GraphQL extensions, ready for
@@ -623,6 +657,28 @@ Generated schemas resolve nested relations without N+1 query explosions:
623
657
  > **SQLite 3.25+**. Relations without pagination, and all other query/mutation paths,
624
658
  > have no such requirement.
625
659
 
660
+ ### Relation nullability
661
+
662
+ A **to-many** relation field is always `[Target!]!`. A **to-one** relation field is
663
+ nullable by default, but honors the relation's declared optionality: declaring
664
+ `optional: false` on a Drizzle `one` relation — the assertion that the related row always
665
+ exists, i.e. a `NOT NULL` foreign key — emits the field as `Target!`:
666
+
667
+ ```Typescript
668
+ const relations = buildRelations({ Users, Posts }, {
669
+ Posts: {
670
+ // posts.author_id is NOT NULL → author: Users!
671
+ author: r.one.Users({ from: r.Posts.authorId, to: r.Users.id, optional: false }),
672
+ },
673
+ })
674
+ ```
675
+
676
+ Only the explicit `optional: false` declaration is honored — required-ness is never
677
+ inferred from column nullability, since a `NOT NULL` `from` column does not guarantee a
678
+ related row exists when the constraint lives on the other side of the join. Note that on a
679
+ required relation, a `where` argument that filters out the related row (or a dangling
680
+ foreign key) resolves to `null` and therefore surfaces as a GraphQL non-null error.
681
+
626
682
  ### Overriding a relation's resolver without overfetching
627
683
 
628
684
  By default the eager `with:` pre-fetch is driven purely by the GraphQL selection set:
package/dist/README.md CHANGED
@@ -100,9 +100,10 @@ const { schema } = buildSchema(db, {
100
100
  relationAggregates: false, // <relation>Aggregate fields on object types
101
101
  distinct: false, // the `distinct` argument on list queries
102
102
  insert: false, // create<Table> / create<Table>Single mutations
103
- update: false, // update<Table> mutations
104
- delete: false, // delete<Table> mutations
103
+ update: false, // update<Table> / update<Table>Single mutations
104
+ delete: false, // delete<Table> / delete<Table>Single mutations
105
105
  upsert: true, // upsert<Table> / upsert<Table>Single mutations (off by default)
106
+ requireWhere: true, // make `where` non-null on plural update/delete (off by default)
106
107
  },
107
108
  })
108
109
  ```
@@ -204,8 +205,10 @@ A **to-many** relation takes a `some` / `none` / `every` wrapper
204
205
  - `some: {}` means "at least one related row exists"; `none: {}` means "none exist"
205
206
  - Several modes may be given at once and are `AND`ed together
206
207
  - A relation whose name collides with a column name is skipped — the column keeps the field
207
- - Many-to-many relations declared with `.through()` are not filterable yet and are left out
208
- of the filter input
208
+ - Many-to-many relations declared with `.through()` are filterable too: the `EXISTS`
209
+ subquery joins the junction table to the target, so
210
+ `users(where: { roles: { some: { name: { eq: "admin" } } } })` works the same as a direct
211
+ to-many relation
209
212
 
210
213
  ## Aggregate queries
211
214
 
@@ -465,6 +468,37 @@ The build-wide `conflictDoNothing` option is deprecated in favour of this: it ap
465
468
  `create*` mutation with no way for a request to opt out. `onConflict: { action: NOTHING }` is
466
469
  the per-request replacement.
467
470
 
471
+ ## Single-row update & delete
472
+
473
+ Alongside the plural `update<Table>` / `delete<Table>` mutations, every table gets an
474
+ `update<Table>Single` / `delete<Table>Single` variant that targets exactly one row:
475
+
476
+ ```graphql
477
+ mutation {
478
+ updateUsersSingle(where: { id: { eq: 1 } }, set: { name: "Dan" }) {
479
+ id
480
+ name
481
+ }
482
+ }
483
+ ```
484
+
485
+ - `where` is **non-null** and must contain at least one filter — `where: {}` is rejected at
486
+ resolve time, so a Single write can never become an unbounded one
487
+ - The return type is the single (nullable) row type, not a list: the affected row comes back
488
+ directly, and **no match returns `null`** instead of an empty list
489
+ - If `where` matches **more than one row**, the mutation throws a `GraphQLError` and writes
490
+ nothing — the match is checked (`LIMIT 2`) before the write runs
491
+ - **MySQL** cannot return the affected row (no `RETURNING`), so its Single variants keep the
492
+ single-match and non-empty-`where` guarantees but return `MutationReturn` like every other
493
+ MySQL mutation
494
+ - The variants are generated under `features.update` / `features.delete`, share the plural
495
+ mutations' input types, and appear in `entities.mutations` for custom schemas
496
+
497
+ The plural mutations still accept a missing `where` (a full-table write) by default. To rule
498
+ that out at the type level, `features: { requireWhere: true }` makes `where` non-null on
499
+ `update<Table>` / `delete<Table>` and rejects a `where` with no filters, exactly like the
500
+ Single variants. It defaults to `false` for backwards compatibility.
501
+
468
502
  ## Query cost
469
503
 
470
504
  Generated fields carry a `complexity` hint in their GraphQL extensions, ready for
@@ -623,6 +657,28 @@ Generated schemas resolve nested relations without N+1 query explosions:
623
657
  > **SQLite 3.25+**. Relations without pagination, and all other query/mutation paths,
624
658
  > have no such requirement.
625
659
 
660
+ ### Relation nullability
661
+
662
+ A **to-many** relation field is always `[Target!]!`. A **to-one** relation field is
663
+ nullable by default, but honors the relation's declared optionality: declaring
664
+ `optional: false` on a Drizzle `one` relation — the assertion that the related row always
665
+ exists, i.e. a `NOT NULL` foreign key — emits the field as `Target!`:
666
+
667
+ ```Typescript
668
+ const relations = buildRelations({ Users, Posts }, {
669
+ Posts: {
670
+ // posts.author_id is NOT NULL → author: Users!
671
+ author: r.one.Users({ from: r.Posts.authorId, to: r.Users.id, optional: false }),
672
+ },
673
+ })
674
+ ```
675
+
676
+ Only the explicit `optional: false` declaration is honored — required-ness is never
677
+ inferred from column nullability, since a `NOT NULL` `from` column does not guarantee a
678
+ related row exists when the constraint lives on the other side of the join. Note that on a
679
+ required relation, a `where` argument that filters out the related row (or a dangling
680
+ foreign key) resolves to `null` and therefore surfaces as a GraphQL non-null error.
681
+
626
682
  ### Overriding a relation's resolver without overfetching
627
683
 
628
684
  By default the eager `with:` pre-fetch is driven purely by the GraphQL selection set: