@vantreeseba/drizzle-graphql 15.1.2 → 15.1.4
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 +54 -4
- package/dist/index.cjs +275 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -4
- package/dist/index.d.ts +62 -4
- package/dist/index.js +276 -82
- 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
|
|
|
@@ -477,6 +490,8 @@ What a declared table gets:
|
|
|
477
490
|
| `posts`, `postsSingle`, cursor pages | marked rows are not returned |
|
|
478
491
|
| `postsAggregate`, `postsGroupBy` | counted over unmarked rows only |
|
|
479
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` |
|
|
480
495
|
| `updatePosts`, `updatePostsMany`, a second `deletePosts` | a marked row is simply not matched |
|
|
481
496
|
| nested `connect` / `disconnect` / `set` | a marked row cannot be attached or detached |
|
|
482
497
|
| `CreatePostsInput`, `UpdatePostsInput` | the marker column is **removed** — only the mutations write it |
|
|
@@ -540,6 +555,10 @@ The `deleted` argument is generated on relation fields either way, so a request
|
|
|
540
555
|
whichever default applies — `altKind(deleted: EXCLUDE)` under `'root'`, `altKind(deleted:
|
|
541
556
|
INCLUDE)` under `'all'`.
|
|
542
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
|
+
|
|
543
562
|
`scope` changes reads only. A nested `connect` / `disconnect` / `set` still refuses to attach a
|
|
544
563
|
marked row under either setting, and the table's own writes are unaffected.
|
|
545
564
|
|
|
@@ -1342,6 +1361,9 @@ can count or summarise related rows without fetching them:
|
|
|
1342
1361
|
and applies only to the related rows
|
|
1343
1362
|
- A parent with no related rows gets `count: 0` and `null` for every other aggregate
|
|
1344
1363
|
- To-one relations get no aggregate field — there is nothing to aggregate over
|
|
1364
|
+
- A relation joined on several columns (`from: [a, b], to: [x, y]`) is grouped by all of
|
|
1365
|
+
them, so each parent counts only the rows that match it on every column. A many-to-many
|
|
1366
|
+
(`.through()`) relation whose junction has a composite key gets no aggregate field
|
|
1345
1367
|
- The field is skipped if its name would collide with a column or another relation
|
|
1346
1368
|
- Set `features: { relationAggregates: false }` (globally or per table) to leave these
|
|
1347
1369
|
fields out
|
|
@@ -1495,6 +1517,12 @@ field, and passing it back as `after` resumes strictly after that row.
|
|
|
1495
1517
|
millisecond JS `Date` that `mode: 'date'` reads it into, so the cursor carries the
|
|
1496
1518
|
database's own text of the value instead, and rows a microsecond apart are still distinct
|
|
1497
1519
|
pages
|
|
1520
|
+
- A cursor is opaque by convention only. It is base64url-encoded JSON of the row's
|
|
1521
|
+
ordering values, primary key included, so a client can decode it and read those values,
|
|
1522
|
+
even for a primary key hidden by [`exclude`](#excluding-tables-and-columns). It is not
|
|
1523
|
+
signed or encrypted. If the ordering values must stay private, wrap the cursor in your
|
|
1524
|
+
own opaque or encrypted token before it leaves the server, and unwrap it on the way back
|
|
1525
|
+
in
|
|
1498
1526
|
- A cursor is only valid under the ordering it was issued for — reusing it with a
|
|
1499
1527
|
different `orderBy` (or a malformed/corrupted cursor) returns a `GraphQLError`
|
|
1500
1528
|
- `after` cannot be combined with `distinct`, with `matchFilterOrder`, or with an `orderBy`
|
|
@@ -2422,7 +2450,9 @@ Generated schemas resolve nested relations without N+1 query explosions:
|
|
|
2422
2450
|
(request-scoped, keyed on the GraphQL `context`), preventing N+1. Per-parent
|
|
2423
2451
|
`limit` / `offset` on a to-many relation is applied across the whole batch using a
|
|
2424
2452
|
`ROW_NUMBER() OVER (PARTITION BY …)` window function — still one query, not one per
|
|
2425
|
-
parent.
|
|
2453
|
+
parent. A relation joined on several columns (`from: [a, b], to: [x, y]`) is batched,
|
|
2454
|
+
grouped and partitioned by all of them, matching the parents with
|
|
2455
|
+
`(a = … AND b = …) OR …` instead of `IN (…)`.
|
|
2426
2456
|
|
|
2427
2457
|
```Typescript
|
|
2428
2458
|
// entities.fieldResolvers is keyed by table name, then relation name
|
|
@@ -2519,12 +2549,13 @@ import { resolveSelection, selectionToWith } from '@vantreeseba/drizzle-graphql'
|
|
|
2519
2549
|
const rows = await resolveSelection(info, {
|
|
2520
2550
|
db,
|
|
2521
2551
|
table: 'Users',
|
|
2552
|
+
context, // the resolver's context — required when the build configures `scope`
|
|
2522
2553
|
where: { id: { eq: userId } },
|
|
2523
2554
|
executor: tx, // defaults to `db`; pass a transaction to see its own writes
|
|
2524
2555
|
})
|
|
2525
2556
|
|
|
2526
2557
|
// Or just the `with:` tree, if you are building the query yourself.
|
|
2527
|
-
const withTree = selectionToWith(info, { db, table: 'Users' })
|
|
2558
|
+
const withTree = selectionToWith(info, { db, table: 'Users', context })
|
|
2528
2559
|
const row = await tx.query.Users.findFirst({ where: { id: userId }, with: withTree })
|
|
2529
2560
|
```
|
|
2530
2561
|
|
|
@@ -2535,8 +2566,27 @@ relation they select, compiles each relation's `where` / `orderBy` / `limit` / `
|
|
|
2535
2566
|
into its nested entry, and leaves `<relation>Aggregate` selections out — they are not
|
|
2536
2567
|
`with:` entries. It returns `undefined` when the selection asks for no relations.
|
|
2537
2568
|
|
|
2538
|
-
`resolveSelection` additionally accepts `single`, `orderBy`, `limit`, `offset` and
|
|
2539
|
-
and returns the rows already remapped to their GraphQL transport forms.
|
|
2569
|
+
`resolveSelection` additionally accepts `single`, `orderBy`, `limit`, `offset`, `after` and
|
|
2570
|
+
`deleted`, and returns the rows already remapped to their GraphQL transport forms.
|
|
2571
|
+
|
|
2572
|
+
Both apply the build's row policies the way the generated fields do, so an override is not a
|
|
2573
|
+
way around them. `resolveSelection` scopes and soft-delete-filters the root table, applies the
|
|
2574
|
+
table's `limits` and `defaults.orderBy` to the root list, and applies all four to every nested
|
|
2575
|
+
relation. `selectionToWith` applies them to the nested relations in the tree it returns. The
|
|
2576
|
+
root query you run with it is yours to scope.
|
|
2577
|
+
|
|
2578
|
+
They find the build through the resolve info: the field's return type, the root table's type in
|
|
2579
|
+
the executing schema, or the parent type, whichever the build generated. That works for a schema
|
|
2580
|
+
assembled from `entities.types`, and for one a tool such as `mapSchema` rebuilt, because the
|
|
2581
|
+
build is found through each type's `extensions.drizzle`. Two things follow:
|
|
2582
|
+
|
|
2583
|
+
- **Pass `context`.** Scope hooks take the request context, which resolve info does not
|
|
2584
|
+
carry. When the build configures `scope`, a call without `context` throws instead of
|
|
2585
|
+
reading unscoped. Soft delete, limits and default ordering need no context.
|
|
2586
|
+
- **Pass `info`, not a parsed tree, when one database has several builds.** A parsed
|
|
2587
|
+
selection tree carries no type to find the build by. Over a database with exactly one
|
|
2588
|
+
build, that build's policies apply. With several builds, the call throws if any of them
|
|
2589
|
+
has policies, since picking one could apply the wrong policies.
|
|
2540
2590
|
|
|
2541
2591
|
Both take the naming options the build used — `typeNameMapper`, `derivedTypeNameMapper`,
|
|
2542
2592
|
`typeNamePrefix` and `typeNameSuffix`. The translation matches selections against the
|