@vantreeseba/drizzle-graphql 15.1.2 → 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 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
 
@@ -1495,6 +1514,12 @@ field, and passing it back as `after` resumes strictly after that row.
1495
1514
  millisecond JS `Date` that `mode: 'date'` reads it into, so the cursor carries the
1496
1515
  database's own text of the value instead, and rows a microsecond apart are still distinct
1497
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
1498
1523
  - A cursor is only valid under the ordering it was issued for — reusing it with a
1499
1524
  different `orderBy` (or a malformed/corrupted cursor) returns a `GraphQLError`
1500
1525
  - `after` cannot be combined with `distinct`, with `matchFilterOrder`, or with an `orderBy`
@@ -2519,12 +2544,13 @@ import { resolveSelection, selectionToWith } from '@vantreeseba/drizzle-graphql'
2519
2544
  const rows = await resolveSelection(info, {
2520
2545
  db,
2521
2546
  table: 'Users',
2547
+ context, // the resolver's context — required when the build configures `scope`
2522
2548
  where: { id: { eq: userId } },
2523
2549
  executor: tx, // defaults to `db`; pass a transaction to see its own writes
2524
2550
  })
2525
2551
 
2526
2552
  // Or just the `with:` tree, if you are building the query yourself.
2527
- const withTree = selectionToWith(info, { db, table: 'Users' })
2553
+ const withTree = selectionToWith(info, { db, table: 'Users', context })
2528
2554
  const row = await tx.query.Users.findFirst({ where: { id: userId }, with: withTree })
2529
2555
  ```
2530
2556
 
@@ -2535,8 +2561,27 @@ relation they select, compiles each relation's `where` / `orderBy` / `limit` / `
2535
2561
  into its nested entry, and leaves `<relation>Aggregate` selections out — they are not
2536
2562
  `with:` entries. It returns `undefined` when the selection asks for no relations.
2537
2563
 
2538
- `resolveSelection` additionally accepts `single`, `orderBy`, `limit`, `offset` and `after`,
2539
- 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.
2540
2585
 
2541
2586
  Both take the naming options the build used — `typeNameMapper`, `derivedTypeNameMapper`,
2542
2587
  `typeNamePrefix` and `typeNameSuffix`. The translation matches selections against the
package/dist/index.cjs CHANGED
@@ -2007,6 +2007,13 @@ const extractUniqueKeyFilter = (table, tableName, fieldName, members, value) =>
2007
2007
  */
2008
2008
  const narrowedCtxCache = /* @__PURE__ */ new WeakMap();
2009
2009
  /**
2010
+ * One alias counter per build, shared by every table's narrowed context. A statement can
2011
+ * combine subqueries compiled from different contexts — a relation filter on one table whose
2012
+ * target's scope is a filter object reaching through another relation, say — and nested
2013
+ * subqueries that drew the same alias from two separate counters would shadow each other.
2014
+ */
2015
+ const aliasCounters = /* @__PURE__ */ new WeakMap();
2016
+ /**
2010
2017
  * Narrows the build-scoped relation filter context to the table a resolver is filtering.
2011
2018
  *
2012
2019
  * Cached rather than rebuilt: the base carries the build's whole `tables` and `relationMap`,
@@ -2020,7 +2027,7 @@ const narrowedCtxCache = /* @__PURE__ */ new WeakMap();
2020
2027
  * aliases only ever have to be unique *within* one statement, which a number that never
2021
2028
  * repeats gives more surely than one that restarts.
2022
2029
  */
2023
- const relationFilterCtx = (base, tableKey) => {
2030
+ const relationFilterCtx = (base, tableKey, scope) => {
2024
2031
  if (!base) return;
2025
2032
  let byTable = narrowedCtxCache.get(base);
2026
2033
  if (!byTable) {
@@ -2029,13 +2036,33 @@ const relationFilterCtx = (base, tableKey) => {
2029
2036
  }
2030
2037
  let ctx = byTable.get(tableKey);
2031
2038
  if (!ctx) {
2039
+ let aliases = aliasCounters.get(base);
2040
+ if (!aliases) {
2041
+ aliases = { n: 0 };
2042
+ aliasCounters.set(base, aliases);
2043
+ }
2032
2044
  ctx = {
2033
2045
  ...base,
2034
- tableKey
2046
+ tableKey,
2047
+ aliases
2035
2048
  };
2036
2049
  byTable.set(tableKey, ctx);
2037
2050
  }
2038
- return ctx;
2051
+ return scope ? {
2052
+ ...ctx,
2053
+ scope
2054
+ } : ctx;
2055
+ };
2056
+ /**
2057
+ * The row-policy predicate a subquery over a relation's target applies: the target's scope
2058
+ * and soft-delete condition, in the `deleted` mode the relation field defaults to — so a
2059
+ * relation filter or a relation ordering sees exactly the rows the relation field returns.
2060
+ */
2061
+ const relationTargetPolicy = (ctx, relation, targetTableName, aliasedTarget) => {
2062
+ const scope = ctx.scope;
2063
+ if (!scope) return;
2064
+ const mode = scope.relationDefault(targetTableName, (0, drizzle_orm.is)(relation, drizzle_orm.One) && relation.optional === false);
2065
+ return scope.has(targetTableName, mode) ? scope.on(targetTableName, aliasedTarget, mode) : void 0;
2039
2066
  };
2040
2067
  /**
2041
2068
  * Correlates the parent row with the aliased target table using the relation's own join
@@ -2126,8 +2153,11 @@ const buildRelationExists = (parentTable, relationName, relEntry, innerFilters,
2126
2153
  const throughTable = relation.throughTable;
2127
2154
  let fromClause;
2128
2155
  let joinCondition;
2156
+ let junctionPolicy;
2129
2157
  if (throughTable) {
2130
2158
  const aliasedThrough = (0, drizzle_orm.aliasedTable)(throughTable, `dgql_rel_${aliases.n++}`);
2159
+ const throughTableName = ctx.scope ? Object.keys(ctx.tables).find((name) => ctx.tables[name] === throughTable) : void 0;
2160
+ if (throughTableName && ctx.scope.has(throughTableName)) junctionPolicy = ctx.scope.on(throughTableName, aliasedThrough);
2131
2161
  const { correlation, junctionJoin } = buildThroughJoinConditions(parentTable, relation, aliasedThrough, aliasedTarget, relationName);
2132
2162
  fromClause = drizzle_orm.sql`${(0, drizzle_orm.getTableAsAliasSQL)(aliasedTarget)} inner join ${(0, drizzle_orm.getTableAsAliasSQL)(aliasedThrough)} on ${junctionJoin}`;
2133
2163
  joinCondition = correlation;
@@ -2137,6 +2167,7 @@ const buildRelationExists = (parentTable, relationName, relEntry, innerFilters,
2137
2167
  }
2138
2168
  const ownWhere = relationOwnWhere(relation);
2139
2169
  const relationWhere = ownWhere ? (0, drizzle_orm.relationsFilterToSQL)(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
2170
+ const policy = (0, drizzle_orm.and)(relationTargetPolicy(ctx, relation, targetTableName, aliasedTarget), junctionPolicy);
2140
2171
  const inner = innerFilters ? extractFilters(aliasedTarget, targetTableName, innerFilters, {
2141
2172
  ...ctx,
2142
2173
  tableKey: targetTableName,
@@ -2144,9 +2175,9 @@ const buildRelationExists = (parentTable, relationName, relEntry, innerFilters,
2144
2175
  }) : void 0;
2145
2176
  if (mode === "every") {
2146
2177
  if (!inner) return;
2147
- return drizzle_orm.sql`not exists (select 1 from ${fromClause} where ${(0, drizzle_orm.and)(joinCondition, relationWhere, (0, drizzle_orm.not)(inner))})`;
2178
+ return drizzle_orm.sql`not exists (select 1 from ${fromClause} where ${(0, drizzle_orm.and)(joinCondition, relationWhere, policy, (0, drizzle_orm.not)(inner))})`;
2148
2179
  }
2149
- const condition = (0, drizzle_orm.and)(joinCondition, relationWhere, inner);
2180
+ const condition = (0, drizzle_orm.and)(joinCondition, relationWhere, policy, inner);
2150
2181
  return mode === "none" ? drizzle_orm.sql`not exists (select 1 from ${fromClause} where ${condition})` : drizzle_orm.sql`exists (select 1 from ${fromClause} where ${condition})`;
2151
2182
  };
2152
2183
  /**
@@ -2672,12 +2703,14 @@ const extendRelationOrderChain = (parentTable, relationName, relEntry, ctx, chai
2672
2703
  const joinCondition = buildRelationJoinCondition(parentTable, relation, aliasedTarget, relationName);
2673
2704
  const ownWhere = relationOwnWhere(relation);
2674
2705
  const relationWhere = ownWhere ? (0, drizzle_orm.relationsFilterToSQL)(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
2706
+ const policy = relationTargetPolicy(ctx, relation, relEntry.targetTableName, aliasedTarget);
2675
2707
  return {
2676
2708
  fromParts: [...chain?.fromParts ?? [], (0, drizzle_orm.getTableAsAliasSQL)(aliasedTarget)],
2677
2709
  conditions: [
2678
2710
  ...chain?.conditions ?? [],
2679
2711
  ...joinCondition ? [joinCondition] : [],
2680
- ...relationWhere ? [relationWhere] : []
2712
+ ...relationWhere ? [relationWhere] : [],
2713
+ ...policy ? [policy] : []
2681
2714
  ],
2682
2715
  table: aliasedTarget
2683
2716
  };
@@ -3907,10 +3940,10 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
3907
3940
  });
3908
3941
  const relWhere = relationArgs?.where;
3909
3942
  const relDeleted = relationArgs?.deleted ?? scope?.relationDefault(targetTableName, (0, drizzle_orm.is)(relEntry.relation, drizzle_orm.One) && relEntry.relation.optional === false);
3910
- thisRecord.where = relWhere || scope?.has(targetTableName, relDeleted) ? { RAW: (aliasedTable) => withScope(scope, targetTableName, aliasedTable, relWhere ? extractFilters(aliasedTable, relName, relWhere, relationFilterCtx(filterCtx, targetTableName)) : void 0, relDeleted) } : void 0;
3943
+ thisRecord.where = relWhere || scope?.has(targetTableName, relDeleted) ? { RAW: (aliasedTable) => withScope(scope, targetTableName, aliasedTable, relWhere ? extractFilters(aliasedTable, relName, relWhere, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, relDeleted) } : void 0;
3911
3944
  const hasPagination = offset != null || limit != null;
3912
3945
  const pkNames = targetPkNames ?? [];
3913
- thisRecord.orderBy = relationArgs?.orderBy ? (aliasedTable) => extractOrderBy(aliasedTable, relationArgs.orderBy, relationFilterCtx(filterCtx, targetTableName), relWhere) : hasPagination && pkNames.length ? (aliasedTable) => primaryKeyOrderExprs(aliasedTable, pkNames) : void 0;
3946
+ thisRecord.orderBy = relationArgs?.orderBy ? (aliasedTable) => extractOrderBy(aliasedTable, relationArgs.orderBy, relationFilterCtx(filterCtx, targetTableName, scope), relWhere) : hasPagination && pkNames.length ? (aliasedTable) => primaryKeyOrderExprs(aliasedTable, pkNames) : void 0;
3914
3947
  thisRecord.offset = offset;
3915
3948
  thisRecord.limit = limit;
3916
3949
  thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
@@ -3968,7 +4001,7 @@ const runRelationalSelect = async (opts) => {
3968
4001
  tableName,
3969
4002
  distinct,
3970
4003
  pkNames: pkNames ?? [],
3971
- where: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0, deleted),
4004
+ where: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0, deleted),
3972
4005
  orderBy,
3973
4006
  limit: single ? 1 : opts.limit,
3974
4007
  offset
@@ -3983,8 +4016,8 @@ const runRelationalSelect = async (opts) => {
3983
4016
  allRelations: filterCtx?.relationMap
3984
4017
  }),
3985
4018
  offset: distinctKeys ? void 0 : offset,
3986
- orderBy: distinctKeys ? (aliasedTable) => [...orderBy ? extractOrderBy(aliasedTable, orderBy, relationFilterCtx(filterCtx, tableName), where) : [], ...primaryKeyOrderExprs(aliasedTable, pkNames)] : cursorEntries ? (aliasedTable) => cursorOrderExprs(aliasedTable, cursorEntries) : orderBy ? (aliasedTable) => extractOrderBy(aliasedTable, orderBy, relationFilterCtx(filterCtx, tableName), where) : needsDefaultOrder && pkNames?.length ? (aliasedTable) => primaryKeyOrderExprs(aliasedTable, pkNames) : void 0,
3987
- where: distinctKeys ? { RAW: (aliasedTable) => primaryKeyRestriction(aliasedTable, pkNames, distinctKeys) } : where || cursorValues || scope?.has(tableName, deleted) ? { RAW: (aliasedTable) => withScope(scope, tableName, aliasedTable, (0, drizzle_orm.and)(where ? extractFilters(aliasedTable, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0, cursorValues ? buildCursorCondition(aliasedTable, cursorEntries, cursorValues, opts.nullOrdering ?? "nulls-smallest") : void 0), deleted) } : void 0,
4019
+ orderBy: distinctKeys ? (aliasedTable) => [...orderBy ? extractOrderBy(aliasedTable, orderBy, relationFilterCtx(filterCtx, tableName, scope), where) : [], ...primaryKeyOrderExprs(aliasedTable, pkNames)] : cursorEntries ? (aliasedTable) => cursorOrderExprs(aliasedTable, cursorEntries) : orderBy ? (aliasedTable) => extractOrderBy(aliasedTable, orderBy, relationFilterCtx(filterCtx, tableName, scope), where) : needsDefaultOrder && pkNames?.length ? (aliasedTable) => primaryKeyOrderExprs(aliasedTable, pkNames) : void 0,
4020
+ where: distinctKeys ? { RAW: (aliasedTable) => primaryKeyRestriction(aliasedTable, pkNames, distinctKeys) } : where || cursorValues || scope?.has(tableName, deleted) ? { RAW: (aliasedTable) => withScope(scope, tableName, aliasedTable, (0, drizzle_orm.and)(where ? extractFilters(aliasedTable, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0, cursorValues ? buildCursorCondition(aliasedTable, cursorEntries, cursorValues, opts.nullOrdering ?? "nulls-smallest") : void 0), deleted) } : void 0,
3988
4021
  with: relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, {
3989
4022
  typeNameMapper,
3990
4023
  filterCtx,
@@ -4566,7 +4599,7 @@ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, f
4566
4599
  tableName,
4567
4600
  table,
4568
4601
  where,
4569
- relationCtx: relationFilterCtx(filterCtx, tableName),
4602
+ relationCtx: relationFilterCtx(filterCtx, tableName, scope),
4570
4603
  required: requireWhere,
4571
4604
  deleted: hard ? "INCLUDE" : void 0
4572
4605
  });
@@ -4972,7 +5005,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4972
5005
  const executor = resolveExecutor(db, context);
4973
5006
  const uniqueIds = [...new Set(parentIds)];
4974
5007
  const scope = resolveScope(policies, context, filterCtx);
4975
- let whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
5008
+ let whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
4976
5009
  if (distinct) {
4977
5010
  const keys = await selectDistinctKeys({
4978
5011
  db: executor,
@@ -4997,10 +5030,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4997
5030
  [PARENT_KEY]: groupCol
4998
5031
  } : columns;
4999
5032
  let rows;
5000
- if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName), whereArg, junction);
5033
+ if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName, scope), whereArg, junction);
5001
5034
  else {
5002
5035
  let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
5003
- const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName), whereArg) : [];
5036
+ const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName, scope), whereArg) : [];
5004
5037
  if (orderExprs.length) q = q.orderBy(...orderExprs);
5005
5038
  rows = await q;
5006
5039
  }
@@ -5680,6 +5713,7 @@ const resolveBuildConfig = (config, schema) => {
5680
5713
  const normalized = {};
5681
5714
  for (const [columnName, entry] of Object.entries(orderBy)) {
5682
5715
  if (!columns[columnName]) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which is not a column of '${tableName}'.`);
5716
+ if (config?.exclude?.columns?.[tableName]?.includes(columnName)) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which config.exclude.columns hides. A default ordering is encoded into every row's cursor, so it can only use columns the schema exposes.`);
5683
5717
  const direction = typeof entry === "string" ? entry : entry?.direction;
5684
5718
  if (direction !== "asc" && direction !== "desc") throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy.${columnName} must be 'asc', 'desc', or { direction, priority? }.`);
5685
5719
  const priority = typeof entry === "string" ? void 0 : entry?.priority;
@@ -6060,7 +6094,8 @@ const generateAggregate = (db, tableName, table, typeName, fieldName, filterArgs
6060
6094
  const request = parseAggregateRequest(info, target);
6061
6095
  if (!Object.keys(request.selection).length) return {};
6062
6096
  let query = resolveExecutor(db, context).select(request.selection).from(table);
6063
- const where = withScope(resolveScope(policies, context, filterCtx), tableName, table, args.where ? extractFilters(table, tableName, args.where, relationFilterCtx(filterCtx, tableName)) : void 0, args.deleted);
6097
+ const scope = resolveScope(policies, context, filterCtx);
6098
+ const where = withScope(scope, tableName, table, args.where ? extractFilters(table, tableName, args.where, relationFilterCtx(filterCtx, tableName, scope)) : void 0, args.deleted);
6064
6099
  if (where) query = query.where(where);
6065
6100
  const rows = await query;
6066
6101
  return assembleAggregateRow(rows[0] ?? {}, request, target);
@@ -6137,7 +6172,8 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6137
6172
  const loader = getOrCreateLoader(context, loaderKey, async (parentIds) => {
6138
6173
  const executor = resolveExecutor(db, context);
6139
6174
  const uniqueIds = [...new Set(parentIds)];
6140
- const whereCondition = withScope(resolveScope(policies, context, filterCtx), targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName)) : void 0), deleted ?? defaultDeleted);
6175
+ const scope = resolveScope(policies, context, filterCtx);
6176
+ const whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
6141
6177
  const selected = executor.select({
6142
6178
  [GROUP_KEY]: groupCol,
6143
6179
  ...request.selection
@@ -6353,7 +6389,8 @@ const generateGroupBy = (db, tableName, table, typeName, fieldName, filterArgs,
6353
6389
  ...request.selection
6354
6390
  };
6355
6391
  let query = resolveExecutor(db, context).select(selection).from(table);
6356
- const where = withScope(resolveScope(policies, context, filterCtx), tableName, table, args.where ? extractFilters(table, tableName, args.where, relationFilterCtx(filterCtx, tableName)) : void 0, args.deleted);
6392
+ const scope = resolveScope(policies, context, filterCtx);
6393
+ const where = withScope(scope, tableName, table, args.where ? extractFilters(table, tableName, args.where, relationFilterCtx(filterCtx, tableName, scope)) : void 0, args.deleted);
6357
6394
  if (where) query = query.where(where);
6358
6395
  query = query.groupBy(...keyColumns.map(([, column]) => column));
6359
6396
  const havingCondition = buildHavingCondition(args.having, columns);
@@ -6392,6 +6429,54 @@ const generateGroupBy = (db, tableName, table, typeName, fieldName, filterArgs,
6392
6429
  };
6393
6430
  };
6394
6431
  //#endregion
6432
+ //#region src/util/builders/common/build-registry.ts
6433
+ /**
6434
+ * Keyed by the `extensions.drizzle` object of every object type the build generated, rather
6435
+ * than by the type: a schema composed from `entities.types` keeps the types, and one rebuilt
6436
+ * by a schema-transforming tool (`mapSchema`, stitching) keeps their extensions.
6437
+ */
6438
+ const runtimeByTypeMeta = /* @__PURE__ */ new WeakMap();
6439
+ /**
6440
+ * What is known about the builds of one drizzle instance, for a caller with no resolve info to
6441
+ * look its build up by. Only a lone build is kept — with two or more there is no telling which
6442
+ * one a selection belongs to — so rebuilding a schema repeatedly holds on to nothing extra.
6443
+ */
6444
+ const buildsByDb = /* @__PURE__ */ new WeakMap();
6445
+ /** Whether a build changes what a read returns at all: a row policy, a limit or a default ordering. */
6446
+ const hasPolicies = (runtime) => !!(runtime.policies?.scope || runtime.policies?.defaultOrderBy || runtime.limits);
6447
+ /** Records a finished build against the object types it generated and the instance it read. */
6448
+ const registerBuildRuntime = (runtime, types) => {
6449
+ for (const type of types) {
6450
+ const meta = drizzleExtension(type);
6451
+ if (meta) runtimeByTypeMeta.set(meta, runtime);
6452
+ }
6453
+ const previous = buildsByDb.get(runtime.db);
6454
+ buildsByDb.set(runtime.db, previous ? {
6455
+ count: previous.count + 1,
6456
+ anyPolicies: previous.anyPolicies || hasPolicies(runtime)
6457
+ } : {
6458
+ count: 1,
6459
+ only: runtime,
6460
+ anyPolicies: hasPolicies(runtime)
6461
+ });
6462
+ };
6463
+ /** The build that generated `type`, when one did. */
6464
+ const runtimeForType = (type) => {
6465
+ const meta = type && typeof type === "object" ? drizzleExtension(type) : void 0;
6466
+ return meta ? runtimeByTypeMeta.get(meta) : void 0;
6467
+ };
6468
+ /**
6469
+ * The build of `db` to use when no generated type identifies one: the only build, or none
6470
+ * when no build applies any policy. Throws when several builds exist and one of them would
6471
+ * change the read, since picking either could apply the wrong policies — or none.
6472
+ */
6473
+ const runtimeForDb = (db, helper) => {
6474
+ const builds = buildsByDb.get(db);
6475
+ if (!builds) return;
6476
+ if (builds.count === 1) return builds.only;
6477
+ if (builds.anyPolicies) throw new Error(`Drizzle-GraphQL Error: ${helper} cannot tell which of the ${builds.count} schemas built over this database the selection belongs to, and at least one applies row policies (scope, softDelete, limits or defaults). Pass the resolver's \`info\` rather than a parsed selection tree, from a field whose type the build generated.`);
6478
+ };
6479
+ //#endregion
6395
6480
  //#region src/util/builders/nested-writes.ts
6396
6481
  const ALL_OPS = [
6397
6482
  "create",
@@ -6884,7 +6969,7 @@ const createNestedWriteRuntime = (params) => {
6884
6969
  * nothing is rejected: `connect: {}` would otherwise attach every row in the table.
6885
6970
  */
6886
6971
  const conditionOf = (plan, filter, operation, scope) => {
6887
- const condition = extractFilters(plan.targetTable, plan.targetTableName, filter, relationFilterCtx(filterCtx, plan.targetTableName));
6972
+ const condition = extractFilters(plan.targetTable, plan.targetTableName, filter, relationFilterCtx(filterCtx, plan.targetTableName, scope));
6888
6973
  if (!condition) throw drizzleError(`Drizzle-GraphQL Error: '${operation}' on '${plan.relationName}' needs a filter that selects rows — it was given one that matches everything.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
6889
6974
  return withScope(scope, plan.targetTableName, plan.targetTable, condition);
6890
6975
  };
@@ -7495,6 +7580,14 @@ const createSchemaBuilder = (adapter) => {
7495
7580
  }
7496
7581
  if (Object.keys(relResolvers).length > 0) fieldResolvers[tableName] = relResolvers;
7497
7582
  }
7583
+ registerBuildRuntime({
7584
+ db: ctx.db,
7585
+ filterCtx: ctx.filterCtx,
7586
+ policies: ctx.policies,
7587
+ scoped: !!ctx.tablePolicies?.scope,
7588
+ limits: ctx.limits,
7589
+ nullOrdering: adapter.nullOrdering
7590
+ }, Object.values(ctx.outputs));
7498
7591
  return {
7499
7592
  queries: ctx.queries,
7500
7593
  mutations: ctx.mutations,
@@ -7617,7 +7710,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
7617
7710
  const input = stripContextValues(remapUpdateInput(set, table, tableName), policies?.contextValues?.(tableName));
7618
7711
  if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
7619
7712
  await before();
7620
- const relationCtx = relationFilterCtx(filterCtx, tableName);
7713
+ const relationCtx = relationFilterCtx(filterCtx, tableName, scope);
7621
7714
  const filters = scopedWhere({
7622
7715
  scope,
7623
7716
  tableName,
@@ -7663,7 +7756,7 @@ const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName,
7663
7756
  if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
7664
7757
  return {
7665
7758
  set: input,
7666
- filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0)
7759
+ filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0)
7667
7760
  };
7668
7761
  });
7669
7762
  await executor.transaction(async (tx) => {
@@ -7706,7 +7799,7 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
7706
7799
  const { where } = args;
7707
7800
  const hard = canHardDelete && args.hard === true;
7708
7801
  const scope = policies?.scope?.(context);
7709
- const relationCtx = relationFilterCtx(filterCtx, tableName);
7802
+ const relationCtx = relationFilterCtx(filterCtx, tableName, scope);
7710
7803
  const filters = scopedWhere({
7711
7804
  scope,
7712
7805
  tableName,
@@ -8048,7 +8141,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8048
8141
  excludedRef: excludedColumnRef,
8049
8142
  withTarget: true,
8050
8143
  serverOwned: Object.keys(contextColumns ?? {}),
8051
- buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName))
8144
+ buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope))
8052
8145
  });
8053
8146
  const runBatch = async (executor, { values: rows, plan }) => {
8054
8147
  let query = executor.insert(table).values(rows).returning(returning);
@@ -8130,7 +8223,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8130
8223
  const nestedOps = entry && nested.hasOps(entry.ops) ? entry.ops : void 0;
8131
8224
  const input = stripContextValues(remapUpdateInput(entry ? entry.columns : set, table, tableName), policies?.contextValues?.(tableName));
8132
8225
  if (!Object.keys(input).length && !nestedOps) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
8133
- const relationCtx = relationFilterCtx(filterCtx, tableName);
8226
+ const relationCtx = relationFilterCtx(filterCtx, tableName, scope);
8134
8227
  const filters = scopedWhere({
8135
8228
  scope,
8136
8229
  tableName,
@@ -8209,7 +8302,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8209
8302
  await before();
8210
8303
  const parsedInfo = parseResolveInfo(info, { deep: true });
8211
8304
  const columns = extractSelectedColumnsFromTreeSQLFormat(parsedInfo.fieldsByTypeName[applyObjectTypeName(typeName, tableName, resolveName)], table, selectionCtx);
8212
- const relationCtx = relationFilterCtx(filterCtx, tableName);
8305
+ const relationCtx = relationFilterCtx(filterCtx, tableName, scope);
8213
8306
  const filters = scopedWhere({
8214
8307
  scope,
8215
8308
  tableName,
@@ -8660,7 +8753,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
8660
8753
  return {
8661
8754
  set: input,
8662
8755
  ops,
8663
- filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0)
8756
+ filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0)
8664
8757
  };
8665
8758
  });
8666
8759
  const anyNested = entries.some((entry) => entry.ops);
@@ -8830,6 +8923,36 @@ const namingRuleOf = (options) => resolveGeneratedTypeNames({
8830
8923
  typeNameSuffix: options.typeNameSuffix
8831
8924
  });
8832
8925
  /**
8926
+ * The build whose policies the read applies. Looked up through the generated types the
8927
+ * resolve info reaches — the field's return type, the root table's type in the executing
8928
+ * schema, the parent type — so a resolver over a generated type finds its own build. Without
8929
+ * resolve info (a parsed tree was passed), the instance's only build is used.
8930
+ */
8931
+ const runtimeFor = (info, db, typeName, helper) => {
8932
+ if (!("fieldsByTypeName" in info)) {
8933
+ const candidates = [
8934
+ info.returnType ? (0, graphql.getNamedType)(info.returnType) : void 0,
8935
+ info.schema?.getType(typeName),
8936
+ info.parentType
8937
+ ];
8938
+ for (const candidate of candidates) {
8939
+ const runtime = runtimeForType(candidate);
8940
+ if (runtime && runtime.db === db) return runtime;
8941
+ }
8942
+ }
8943
+ return runtimeForDb(db, helper);
8944
+ };
8945
+ /**
8946
+ * The request's row policies under `runtime`, bound to the context the caller passed. A build
8947
+ * with `scope` hooks cannot be read without that context: the hooks would be called with
8948
+ * nothing, and running the read unscoped is the leak the option exists to prevent.
8949
+ */
8950
+ const scopeFor = (runtime, options, helper) => {
8951
+ if (!runtime?.policies?.scope) return;
8952
+ if (runtime.scoped && !("context" in options)) throw new Error(`Drizzle-GraphQL Error: the schema this selection belongs to configures \`scope\`, so ${helper} needs the request context to apply it. Pass the resolver's context as the \`context\` option.`);
8953
+ return runtime.policies.scope(options.context);
8954
+ };
8955
+ /**
8833
8956
  * Reads the parsed selection tree out of a resolver's `info`, resolving fragments and
8834
8957
  * inline fragments. Accepts an already-parsed tree so a caller that has one (or is chaining
8835
8958
  * these helpers) does not pay for the walk twice.
@@ -8856,6 +8979,10 @@ const parseInfo = (info) => {
8856
8979
  * const row = await tx.query.Users.findFirst({ where: { id: { eq } }, with: withTree });
8857
8980
  * ```
8858
8981
  *
8982
+ * Every nested entry carries the build's row policies — `scope`, `softDelete`, `limits` and
8983
+ * `defaults.orderBy` — exactly as the generated `with:` clause does. The build is found
8984
+ * through the resolve info; pass `context` when it configures `scope`.
8985
+ *
8859
8986
  * Returns `undefined` when the selection asks for no relations, which is the value the
8860
8987
  * query builder wants for "no `with` clause".
8861
8988
  */
@@ -8865,11 +8992,16 @@ const selectionToWith = (info, options) => {
8865
8992
  const { tables, relationMap, filterCtx } = contextFor(db);
8866
8993
  if (!tables[table]) throw new Error(`Drizzle-GraphQL Error: '${table}' is not a table in the Drizzle schema.`);
8867
8994
  if (!relationMap[table]) return;
8995
+ const typeName = resolveObjectTypeName(table, typeNameMapper, resolveName);
8996
+ const runtime = runtimeFor(info, db, typeName, "selectionToWith");
8868
8997
  const parsed = parseInfo(info);
8869
- const params = extractRelationsParams(relationMap, tables, table, parsed, resolveObjectTypeName(table, typeNameMapper, resolveName), {
8998
+ const params = extractRelationsParams(relationMap, tables, table, parsed, typeName, {
8870
8999
  typeNameMapper,
8871
- filterCtx,
8872
- resolveName
9000
+ filterCtx: runtime?.filterCtx ?? filterCtx,
9001
+ resolveName,
9002
+ scope: scopeFor(runtime, options, "selectionToWith"),
9003
+ limits: runtime?.limits,
9004
+ defaultOrderBy: runtime?.policies?.defaultOrderBy
8873
9005
  });
8874
9006
  return params && Object.keys(params).length ? params : void 0;
8875
9007
  };
@@ -8881,14 +9013,16 @@ const selectionToWith = (info, options) => {
8881
9013
  * This is what an override usually wants after it has done its own work:
8882
9014
  *
8883
9015
  * ```ts
8884
- * const rows = await resolveSelection(info, { db, table: 'Users', where, executor: tx });
9016
+ * const rows = await resolveSelection(info, { db, table: 'Users', context, where, executor: tx });
8885
9017
  * ```
8886
9018
  *
8887
9019
  * Relation arguments and aggregate fields keep behaving as they do in the generated schema,
8888
- * because it is the generated schema's own code path.
9020
+ * because it is the generated schema's own code path. So do the build's row policies: the
9021
+ * root table and every nested relation are scoped and soft-delete-filtered, and the limit
9022
+ * policy and default ordering apply. Pass `context` when the build configures `scope`.
8889
9023
  */
8890
9024
  const resolveSelection = async (info, options) => {
8891
- const { db, table: tableName, typeNameMapper, executor, single = false, where, orderBy, limit, offset, after } = options;
9025
+ const { db, table: tableName, typeNameMapper, executor, single = false, where, orderBy, limit, offset, after, deleted } = options;
8892
9026
  const resolveName = namingRuleOf(options);
8893
9027
  const { tables, relationMap, filterCtx, primaryKeyOf } = contextFor(db);
8894
9028
  const table = tables[tableName];
@@ -8898,25 +9032,34 @@ const resolveSelection = async (info, options) => {
8898
9032
  const source = executor ?? db;
8899
9033
  const queryBase = source.query?.[tableName];
8900
9034
  if (!queryBase) throw new Error(`Drizzle-GraphQL Error: the executor passed to resolveSelection has no relational query builder for '${tableName}'.`);
9035
+ const typeName = resolveObjectTypeName(tableName, typeNameMapper, resolveName);
9036
+ const runtime = runtimeFor(info, db, typeName, "resolveSelection");
9037
+ const scope = scopeFor(runtime, options, "resolveSelection");
9038
+ const defaultOrderBy = runtime?.policies?.defaultOrderBy;
8901
9039
  return runRelationalSelect({
8902
9040
  queryBase,
8903
9041
  tables,
8904
9042
  tableName,
8905
9043
  table,
8906
9044
  relationMap,
8907
- typeName: resolveObjectTypeName(tableName, typeNameMapper, resolveName),
9045
+ typeName,
8908
9046
  typeNameMapper,
8909
9047
  resolveName,
8910
9048
  parsedInfo,
8911
9049
  single,
8912
9050
  where,
8913
- orderBy,
8914
- limit,
9051
+ orderBy: withDefaultOrderBy({ orderBy }, tableName, defaultOrderBy).orderBy,
9052
+ limit: single ? limit : applyLimitPolicy(limit, runtime?.limits?.(tableName)),
8915
9053
  offset,
8916
9054
  after,
8917
- filterCtx,
9055
+ filterCtx: runtime?.filterCtx ?? filterCtx,
8918
9056
  pkNames: primaryKeyOf(tableName),
8919
- db: source
9057
+ db: source,
9058
+ scope,
9059
+ deleted,
9060
+ limits: runtime?.limits,
9061
+ defaultOrderBy,
9062
+ nullOrdering: runtime?.nullOrdering
8920
9063
  });
8921
9064
  };
8922
9065
  //#endregion