@vantreeseba/drizzle-graphql 15.1.1 → 15.1.3
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 +70 -7
- package/dist/index.cjs +334 -96
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -7
- package/dist/index.d.ts +68 -7
- package/dist/index.js +335 -97
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -249,6 +249,12 @@ Two details worth stating plainly:
|
|
|
249
249
|
- **Nothing changes below the schema.** An excluded column is still written by its
|
|
250
250
|
default, still read by the primary-key and cursor machinery, and still usable by your
|
|
251
251
|
own resolvers. Only the generated schema stops mentioning it.
|
|
252
|
+
- **A cursor carries the primary key in readable form.** A [cursor](#cursor-pagination)
|
|
253
|
+
is base64url JSON of the row's ordering values, and the primary key is always one of
|
|
254
|
+
them. Excluding the primary key removes the field, but anyone holding a cursor can still
|
|
255
|
+
decode the key's value. If the key itself must stay secret, wrap the cursors in your own
|
|
256
|
+
opaque or encrypted form. A `defaults.<table>.orderBy` that names an excluded column
|
|
257
|
+
throws at build time, because its values would ride along in every cursor too.
|
|
252
258
|
|
|
253
259
|
Names that match nothing throw at build time, so renaming a column fails the build instead
|
|
254
260
|
of quietly un-hiding it. Excluding a `NOT NULL` column with no default is allowed — the
|
|
@@ -291,6 +297,8 @@ narrow the scope, never widen it. It reaches every path that touches the table:
|
|
|
291
297
|
| `postsAggregate`, `postsGroupBy` | counted over in-scope rows only |
|
|
292
298
|
| `user.posts`, `user.postsAggregate` | scoped on both the batched and the eager (`with:`) path |
|
|
293
299
|
| cursor pages | the keyset page is taken within the scope |
|
|
300
|
+
| relation filters — `users(where: { posts: { some: … } })`, `none`, `every`, to-one, `.through()` | only in-scope posts can match, block `none` or fail `every` — on reads, aggregates, `groupBy`, and the `where` of updates and deletes |
|
|
301
|
+
| `orderBy` through a to-one — `comments(orderBy: { post: { title: … } })` | an out-of-scope post sorts as `NULL` |
|
|
294
302
|
| `updatePosts`, `updatePostsMany`, `deletePosts` | an out-of-scope row is simply not matched |
|
|
295
303
|
| `upsertPosts` (conflict branch) | a conflicting row outside the scope is left alone, not taken over |
|
|
296
304
|
| nested `connect` / `disconnect` / `set` | only in-scope rows can be attached or detached |
|
|
@@ -303,6 +311,11 @@ Two things it deliberately does not do:
|
|
|
303
311
|
- **It does not apply to a plain insert.** There is no existing row to filter. `contextValues`
|
|
304
312
|
is the write-side half.
|
|
305
313
|
|
|
314
|
+
A relation filter or an ordering through a relation sees exactly the rows the relation field
|
|
315
|
+
would return. Through a `.through()` relation, the junction table's own `scope` and soft delete
|
|
316
|
+
apply as well. A scope that returns a filter object is the server's own predicate, so the
|
|
317
|
+
relations it reaches through are read without their targets' scopes.
|
|
318
|
+
|
|
306
319
|
Returning `undefined` means *no restriction for this context*, so one hook can let an
|
|
307
320
|
administrator through:
|
|
308
321
|
|
|
@@ -335,7 +348,9 @@ const { schema } = buildSchema(db, {
|
|
|
335
348
|
send it at all — the SDL itself says the column is server-owned. It stays readable on the
|
|
336
349
|
object type.
|
|
337
350
|
- Every insert stamps it, including rows created by a nested `create`.
|
|
338
|
-
- Updates never write it, so an update cannot hand a row to another owner.
|
|
351
|
+
- Updates never write it, so an update cannot hand a row to another owner. Neither does the
|
|
352
|
+
conflict branch of an upsert: it is left out of the default overwrite and of the columns
|
|
353
|
+
`onConflict.update` can name.
|
|
339
354
|
|
|
340
355
|
Together the two close the loop: `contextValues` decides which tenant a new row belongs to, and
|
|
341
356
|
`scope` decides which rows a request can see and change afterwards.
|
|
@@ -475,6 +490,8 @@ What a declared table gets:
|
|
|
475
490
|
| `posts`, `postsSingle`, cursor pages | marked rows are not returned |
|
|
476
491
|
| `postsAggregate`, `postsGroupBy` | counted over unmarked rows only |
|
|
477
492
|
| `user.posts`, `user.postsAggregate` | hidden on both the batched and the eager (`with:`) path |
|
|
493
|
+
| relation filters — `users(where: { posts: { some: {} } })`, `none`, `every`, `.through()` junctions | a marked post neither matches nor blocks `none`, with the same default as the relation field (see below) |
|
|
494
|
+
| `orderBy` through a to-one | a marked target the relation field hides sorts as `NULL` |
|
|
478
495
|
| `updatePosts`, `updatePostsMany`, a second `deletePosts` | a marked row is simply not matched |
|
|
479
496
|
| nested `connect` / `disconnect` / `set` | a marked row cannot be attached or detached |
|
|
480
497
|
| `CreatePostsInput`, `UpdatePostsInput` | the marker column is **removed** — only the mutations write it |
|
|
@@ -538,6 +555,10 @@ The `deleted` argument is generated on relation fields either way, so a request
|
|
|
538
555
|
whichever default applies — `altKind(deleted: EXCLUDE)` under `'root'`, `altKind(deleted:
|
|
539
556
|
INCLUDE)` under `'all'`.
|
|
540
557
|
|
|
558
|
+
A relation filter (`items(where: { kind: { name: … } })`) and an `orderBy` through a to-one
|
|
559
|
+
use the same default as the relation field, so they match and sort over exactly the rows the
|
|
560
|
+
field shows. They take no `deleted` argument of their own.
|
|
561
|
+
|
|
541
562
|
`scope` changes reads only. A nested `connect` / `disconnect` / `set` still refuses to attach a
|
|
542
563
|
marked row under either setting, and the table's own writes are unaffected.
|
|
543
564
|
|
|
@@ -984,6 +1005,9 @@ input StockItemIdLocationIdKey {
|
|
|
984
1005
|
composes with column filters, relation filters, `OR` / `AND` / `NOT`
|
|
985
1006
|
- Composite primary keys get a field like any other unique constraint
|
|
986
1007
|
- Single-column constraints do not: `eq` on the column already names the row
|
|
1008
|
+
- Partial unique indexes (`uniqueIndex().on(…).where(…)`) do not either: they are unique
|
|
1009
|
+
only among the rows the predicate matches, so with soft-deleted rows included
|
|
1010
|
+
(`deleted: INCLUDE`) the key can match more than one
|
|
987
1011
|
- A field name a column or relation already holds is skipped, the same way a relation
|
|
988
1012
|
yields to a same-named column
|
|
989
1013
|
- It compiles to exactly the equalities you would have written by hand; the guarantee is in
|
|
@@ -1490,6 +1514,12 @@ field, and passing it back as `after` resumes strictly after that row.
|
|
|
1490
1514
|
millisecond JS `Date` that `mode: 'date'` reads it into, so the cursor carries the
|
|
1491
1515
|
database's own text of the value instead, and rows a microsecond apart are still distinct
|
|
1492
1516
|
pages
|
|
1517
|
+
- A cursor is opaque by convention only. It is base64url-encoded JSON of the row's
|
|
1518
|
+
ordering values, primary key included, so a client can decode it and read those values,
|
|
1519
|
+
even for a primary key hidden by [`exclude`](#excluding-tables-and-columns). It is not
|
|
1520
|
+
signed or encrypted. If the ordering values must stay private, wrap the cursor in your
|
|
1521
|
+
own opaque or encrypted token before it leaves the server, and unwrap it on the way back
|
|
1522
|
+
in
|
|
1493
1523
|
- A cursor is only valid under the ordering it was issued for — reusing it with a
|
|
1494
1524
|
different `orderBy` (or a malformed/corrupted cursor) returns a `GraphQLError`
|
|
1495
1525
|
- `after` cannot be combined with `distinct`, with `matchFilterOrder`, or with an `orderBy`
|
|
@@ -1566,9 +1596,17 @@ mutation {
|
|
|
1566
1596
|
- Columns the request did not supply are never overwritten — a partial upsert does not null
|
|
1567
1597
|
out the rest of the row. Listing an unsupplied column in `update` is an error rather than
|
|
1568
1598
|
a silent no-op
|
|
1599
|
+
- That holds per row: a batch whose rows supply different columns is split into one
|
|
1600
|
+
statement per run of consecutive rows that write the same columns, run in one transaction,
|
|
1601
|
+
so a row that left a column out keeps it. The rows come back in the order they were sent
|
|
1569
1602
|
- `target` must match one of the table's unique constraints exactly; anything else is
|
|
1570
1603
|
rejected with the list of valid targets, instead of the database's opaque "no unique or
|
|
1571
1604
|
exclusion constraint matching" error
|
|
1605
|
+
- `target` can be a partial unique index — the `WHERE deleted_at IS NULL` companion of
|
|
1606
|
+
[soft delete](#soft-delete), say. Its predicate goes into `ON CONFLICT (…) WHERE …`, so a
|
|
1607
|
+
conflict is detected only against the rows the index covers
|
|
1608
|
+
- `contextValues` columns are never overwritten on conflict — see
|
|
1609
|
+
[columns the server owns](#columns-the-server-owns)
|
|
1572
1610
|
- `action: NOTHING` inserts nothing and returns nothing for the conflicting row. An `UPDATE`
|
|
1573
1611
|
with no columns left to write degrades to the same thing
|
|
1574
1612
|
- `values` is the same `Create<Type>Input` the insert mutations take, so turning `insert`
|
|
@@ -1603,7 +1641,9 @@ Dialect differences:
|
|
|
1603
1641
|
- **PostgreSQL** and **SQLite** — the full surface above. A table with no primary key and no
|
|
1604
1642
|
unique constraint has nothing to conflict on, so it gets no upsert mutations at all
|
|
1605
1643
|
- **MySQL** — `ON DUPLICATE KEY UPDATE` fires on whichever unique key was violated and takes
|
|
1606
|
-
no predicate, so `<Type>OnConflict` there has only `action` and `update`.
|
|
1644
|
+
no predicate, so `<Type>OnConflict` there has only `action` and `update`. Because the
|
|
1645
|
+
conflict can be on any unique key, the default overwrite leaves out the primary key as well
|
|
1646
|
+
— copying a new row's key over the conflicting row's would move it. `action: NOTHING`
|
|
1607
1647
|
becomes `INSERT IGNORE`, and the mutations return `MutationReturn` like every other MySQL
|
|
1608
1648
|
mutation
|
|
1609
1649
|
|
|
@@ -2504,12 +2544,13 @@ import { resolveSelection, selectionToWith } from '@vantreeseba/drizzle-graphql'
|
|
|
2504
2544
|
const rows = await resolveSelection(info, {
|
|
2505
2545
|
db,
|
|
2506
2546
|
table: 'Users',
|
|
2547
|
+
context, // the resolver's context — required when the build configures `scope`
|
|
2507
2548
|
where: { id: { eq: userId } },
|
|
2508
2549
|
executor: tx, // defaults to `db`; pass a transaction to see its own writes
|
|
2509
2550
|
})
|
|
2510
2551
|
|
|
2511
2552
|
// Or just the `with:` tree, if you are building the query yourself.
|
|
2512
|
-
const withTree = selectionToWith(info, { db, table: 'Users' })
|
|
2553
|
+
const withTree = selectionToWith(info, { db, table: 'Users', context })
|
|
2513
2554
|
const row = await tx.query.Users.findFirst({ where: { id: userId }, with: withTree })
|
|
2514
2555
|
```
|
|
2515
2556
|
|
|
@@ -2520,8 +2561,27 @@ relation they select, compiles each relation's `where` / `orderBy` / `limit` / `
|
|
|
2520
2561
|
into its nested entry, and leaves `<relation>Aggregate` selections out — they are not
|
|
2521
2562
|
`with:` entries. It returns `undefined` when the selection asks for no relations.
|
|
2522
2563
|
|
|
2523
|
-
`resolveSelection` additionally accepts `single`, `orderBy`, `limit`, `offset` and
|
|
2524
|
-
and returns the rows already remapped to their GraphQL transport forms.
|
|
2564
|
+
`resolveSelection` additionally accepts `single`, `orderBy`, `limit`, `offset`, `after` and
|
|
2565
|
+
`deleted`, and returns the rows already remapped to their GraphQL transport forms.
|
|
2566
|
+
|
|
2567
|
+
Both apply the build's row policies the way the generated fields do, so an override is not a
|
|
2568
|
+
way around them. `resolveSelection` scopes and soft-delete-filters the root table, applies the
|
|
2569
|
+
table's `limits` and `defaults.orderBy` to the root list, and applies all four to every nested
|
|
2570
|
+
relation. `selectionToWith` applies them to the nested relations in the tree it returns. The
|
|
2571
|
+
root query you run with it is yours to scope.
|
|
2572
|
+
|
|
2573
|
+
They find the build through the resolve info: the field's return type, the root table's type in
|
|
2574
|
+
the executing schema, or the parent type, whichever the build generated. That works for a schema
|
|
2575
|
+
assembled from `entities.types`, and for one a tool such as `mapSchema` rebuilt, because the
|
|
2576
|
+
build is found through each type's `extensions.drizzle`. Two things follow:
|
|
2577
|
+
|
|
2578
|
+
- **Pass `context`.** Scope hooks take the request context, which resolve info does not
|
|
2579
|
+
carry. When the build configures `scope`, a call without `context` throws instead of
|
|
2580
|
+
reading unscoped. Soft delete, limits and default ordering need no context.
|
|
2581
|
+
- **Pass `info`, not a parsed tree, when one database has several builds.** A parsed
|
|
2582
|
+
selection tree carries no type to find the build by. Over a database with exactly one
|
|
2583
|
+
build, that build's policies apply. With several builds, the call throws if any of them
|
|
2584
|
+
has policies, since picking one could apply the wrong policies.
|
|
2525
2585
|
|
|
2526
2586
|
Both take the naming options the build used — `typeNameMapper`, `derivedTypeNameMapper`,
|
|
2527
2587
|
`typeNamePrefix` and `typeNameSuffix`. The translation matches selections against the
|
|
@@ -2580,8 +2640,11 @@ It understands `values` (one object or a list), `where` (`eq` for one row, `inAr
|
|
|
2580
2640
|
many, an equality on every column of a composite key), and the per-entry `where` inside a
|
|
2581
2641
|
batch update's `updates`. Check `complete` before treating `rows` as the affected set: it
|
|
2582
2642
|
is `false` when the arguments do not pin the rows down — an insert whose key the database
|
|
2583
|
-
generates, a filter on something other than the primary key, an unbounded update,
|
|
2584
|
-
table with no primary key at all
|
|
2643
|
+
generates, a filter on something other than the primary key, an unbounded update, a
|
|
2644
|
+
table with no primary key at all, or an upsert whose `onConflict` targets another unique key
|
|
2645
|
+
or can skip a conflicting row (`action: NOTHING`, a `where`). On MySQL, where an upsert has
|
|
2646
|
+
no `target` and conflicts on whichever unique key it hits, `complete` assumes the primary
|
|
2647
|
+
key: do not rely on it for a table with other unique keys.
|
|
2585
2648
|
|
|
2586
2649
|
> [!NOTE]
|
|
2587
2650
|
> `extensions.drizzle` is stable API. The point of publishing it is that consumers stop
|