@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 +77 -1
- package/dist/README.md +77 -1
- package/dist/index.cjs +384 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -3
- package/dist/index.d.ts +117 -3
- package/dist/index.js +384 -91
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
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' | '
|
|
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' | '
|
|
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
|