@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 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 `after`,
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