@vantreeseba/drizzle-graphql 7.6.0 → 7.7.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
@@ -362,6 +362,81 @@ const { schema } = buildSchema(db, {
362
362
 
363
363
  Return `undefined` for any table that should keep its default naming.
364
364
 
365
+ ## Soft delete
366
+
367
+ `softDelete` turns a table's `delete` mutation into an UPDATE that marks the row, and hides
368
+ marked rows from every read:
369
+
370
+ ```Typescript
371
+ const { schema } = buildSchema(db, {
372
+ softDelete: {
373
+ posts: 'deletedAt',
374
+ // The NOT NULL form has to spell out both values:
375
+ flags: { column: 'isArchived', deletedValue: true, restoredValue: false },
376
+ },
377
+ })
378
+ ```
379
+
380
+ Keyed by the **Drizzle schema key**, like `scope` and `exclude`. A function form is also
381
+ accepted, for a convention applied across the schema:
382
+
383
+ ```Typescript
384
+ softDelete: (table, tableName) => ('deletedAt' in getTableColumns(table) ? 'deletedAt' : undefined)
385
+ ```
386
+
387
+ What a declared table gets:
388
+
389
+ | Path | Effect |
390
+ | --- | --- |
391
+ | `deletePosts`, `deletePostsSingle` | UPDATE that writes the marker; returns the rows as they now stand |
392
+ | `restorePosts`, `restorePostsSingle` | the inverse — matches **only** marked rows and clears the marker |
393
+ | `posts`, `postsSingle`, cursor pages | marked rows are not returned |
394
+ | `postsAggregate`, `postsGroupBy` | counted over unmarked rows only |
395
+ | `user.posts`, `user.postsAggregate` | hidden on both the batched and the eager (`with:`) path |
396
+ | `updatePosts`, `updatePostsMany`, a second `deletePosts` | a marked row is simply not matched |
397
+ | nested `connect` / `disconnect` / `set` | a marked row cannot be attached or detached |
398
+ | `CreatePostsInput`, `UpdatePostsInput` | the marker column is **removed** — only the mutations write it |
399
+
400
+ Every read path also gains a `deleted` argument for the cases the default hides:
401
+
402
+ ```graphql
403
+ {
404
+ live: posts { id } # the default — EXCLUDE
405
+ trash: posts(deleted: ONLY) { id } # the trash view, no second field needed
406
+ all: posts(deleted: INCLUDE) { id }
407
+ }
408
+ ```
409
+
410
+ The marker column stays **readable and filterable** — it is only the write inputs that lose it,
411
+ the same trade `contextValues` makes.
412
+
413
+ Defaults come from the column's type: `new Date()` for a timestamp, an ISO string for text,
414
+ `Date.now()` for a number, `true` for a boolean. A NOT NULL column has no "absent" state, so
415
+ the predicate has to compare against a constant: any NOT NULL column other than a boolean must
416
+ give `deletedValue` (a constant, not a function) and `restoredValue`, or the build throws.
417
+
418
+ Three things it deliberately does not do:
419
+
420
+ - **It does not cascade.** Marking a post does not mark its comments. A comment whose post is
421
+ marked is still returned by `comments`; its `post` field resolves to `null`. Declare the
422
+ child table too if that is not what you want.
423
+ - **It does not free the row's unique keys.** A marked row still occupies its primary key and
424
+ every unique index, so inserting a new row with the same natural key fails the constraint.
425
+ A partial unique index (`WHERE deleted_at IS NULL`) is the usual answer.
426
+ - **It does not refuse.** Like `scope`, an unreachable row answers the same as a row that does
427
+ not exist.
428
+
429
+ The soft-delete predicate is ANDed **before** a `scope` predicate, both after the client's
430
+ `where` — so a scope still confines what `deleted: INCLUDE` can reach.
431
+
432
+ > **MySQL caveat.** `ON DUPLICATE KEY UPDATE` takes no predicate, so an upsert whose conflict
433
+ > target hits a marked row updates it — reviving it — rather than leaving it alone. On
434
+ > PostgreSQL and SQLite the conflict branch carries the predicate and updates nothing.
435
+
436
+ A table with no declaration keeps a real `DELETE`, gets no `restore` mutation and no `deleted`
437
+ argument, and a build with no `softDelete` at all emits exactly the SQL it did before.
438
+
439
+
365
440
  ## Scalars
366
441
 
367
442
  Columns whose values don't fit a built-in GraphQL scalar get a named custom scalar, so the
@@ -1637,7 +1712,8 @@ const meta = drizzleExtension(field)
1637
1712
  // table: 'Users', // the Drizzle schema key
1638
1713
  // kind: 'mutation', // 'query' | 'mutation' | 'relation' | 'aggregate' | 'type'
1639
1714
  // operation: 'update', // 'select' | 'insert' | 'update' | 'updateMany' | 'upsert'
1640
- // // | 'delete' | 'aggregate' | 'groupBy' | 'relation'
1715
+ // // | 'delete' | 'restore' | 'aggregate' | 'groupBy'
1716
+ // // | 'relation'
1641
1717
  // // | 'relationAggregate'
1642
1718
  // single: true, // one row, or a list
1643
1719
  // targetArg: 'where', // 'where' | 'values' | 'updates' — where the rows live
package/dist/README.md CHANGED
@@ -362,6 +362,81 @@ const { schema } = buildSchema(db, {
362
362
 
363
363
  Return `undefined` for any table that should keep its default naming.
364
364
 
365
+ ## Soft delete
366
+
367
+ `softDelete` turns a table's `delete` mutation into an UPDATE that marks the row, and hides
368
+ marked rows from every read:
369
+
370
+ ```Typescript
371
+ const { schema } = buildSchema(db, {
372
+ softDelete: {
373
+ posts: 'deletedAt',
374
+ // The NOT NULL form has to spell out both values:
375
+ flags: { column: 'isArchived', deletedValue: true, restoredValue: false },
376
+ },
377
+ })
378
+ ```
379
+
380
+ Keyed by the **Drizzle schema key**, like `scope` and `exclude`. A function form is also
381
+ accepted, for a convention applied across the schema:
382
+
383
+ ```Typescript
384
+ softDelete: (table, tableName) => ('deletedAt' in getTableColumns(table) ? 'deletedAt' : undefined)
385
+ ```
386
+
387
+ What a declared table gets:
388
+
389
+ | Path | Effect |
390
+ | --- | --- |
391
+ | `deletePosts`, `deletePostsSingle` | UPDATE that writes the marker; returns the rows as they now stand |
392
+ | `restorePosts`, `restorePostsSingle` | the inverse — matches **only** marked rows and clears the marker |
393
+ | `posts`, `postsSingle`, cursor pages | marked rows are not returned |
394
+ | `postsAggregate`, `postsGroupBy` | counted over unmarked rows only |
395
+ | `user.posts`, `user.postsAggregate` | hidden on both the batched and the eager (`with:`) path |
396
+ | `updatePosts`, `updatePostsMany`, a second `deletePosts` | a marked row is simply not matched |
397
+ | nested `connect` / `disconnect` / `set` | a marked row cannot be attached or detached |
398
+ | `CreatePostsInput`, `UpdatePostsInput` | the marker column is **removed** — only the mutations write it |
399
+
400
+ Every read path also gains a `deleted` argument for the cases the default hides:
401
+
402
+ ```graphql
403
+ {
404
+ live: posts { id } # the default — EXCLUDE
405
+ trash: posts(deleted: ONLY) { id } # the trash view, no second field needed
406
+ all: posts(deleted: INCLUDE) { id }
407
+ }
408
+ ```
409
+
410
+ The marker column stays **readable and filterable** — it is only the write inputs that lose it,
411
+ the same trade `contextValues` makes.
412
+
413
+ Defaults come from the column's type: `new Date()` for a timestamp, an ISO string for text,
414
+ `Date.now()` for a number, `true` for a boolean. A NOT NULL column has no "absent" state, so
415
+ the predicate has to compare against a constant: any NOT NULL column other than a boolean must
416
+ give `deletedValue` (a constant, not a function) and `restoredValue`, or the build throws.
417
+
418
+ Three things it deliberately does not do:
419
+
420
+ - **It does not cascade.** Marking a post does not mark its comments. A comment whose post is
421
+ marked is still returned by `comments`; its `post` field resolves to `null`. Declare the
422
+ child table too if that is not what you want.
423
+ - **It does not free the row's unique keys.** A marked row still occupies its primary key and
424
+ every unique index, so inserting a new row with the same natural key fails the constraint.
425
+ A partial unique index (`WHERE deleted_at IS NULL`) is the usual answer.
426
+ - **It does not refuse.** Like `scope`, an unreachable row answers the same as a row that does
427
+ not exist.
428
+
429
+ The soft-delete predicate is ANDed **before** a `scope` predicate, both after the client's
430
+ `where` — so a scope still confines what `deleted: INCLUDE` can reach.
431
+
432
+ > **MySQL caveat.** `ON DUPLICATE KEY UPDATE` takes no predicate, so an upsert whose conflict
433
+ > target hits a marked row updates it — reviving it — rather than leaving it alone. On
434
+ > PostgreSQL and SQLite the conflict branch carries the predicate and updates nothing.
435
+
436
+ A table with no declaration keeps a real `DELETE`, gets no `restore` mutation and no `deleted`
437
+ argument, and a build with no `softDelete` at all emits exactly the SQL it did before.
438
+
439
+
365
440
  ## Scalars
366
441
 
367
442
  Columns whose values don't fit a built-in GraphQL scalar get a named custom scalar, so the
@@ -1637,7 +1712,8 @@ const meta = drizzleExtension(field)
1637
1712
  // table: 'Users', // the Drizzle schema key
1638
1713
  // kind: 'mutation', // 'query' | 'mutation' | 'relation' | 'aggregate' | 'type'
1639
1714
  // operation: 'update', // 'select' | 'insert' | 'update' | 'updateMany' | 'upsert'
1640
- // // | 'delete' | 'aggregate' | 'groupBy' | 'relation'
1715
+ // // | 'delete' | 'restore' | 'aggregate' | 'groupBy'
1716
+ // // | 'relation'
1641
1717
  // // | 'relationAggregate'
1642
1718
  // single: true, // one row, or a list
1643
1719
  // targetArg: 'where', // 'where' | 'values' | 'updates' — where the rows live