@vantreeseba/drizzle-graphql 11.0.2 → 12.0.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/dist/index.js CHANGED
@@ -520,11 +520,12 @@ const isJsonColumn = (column) => (column.dataType ?? "").includes("json") && col
520
520
  * StringArray, …) so arrays with different element types never share
521
521
  * one filter input; membership operators instead of scalar comparisons
522
522
  * - "Id" → uuid-typed columns (no string pattern operators)
523
- * - "Boolean" → boolean columns
523
+ * - "Boolean" → boolean columns (equality and membership only — no ordering, no patterns)
524
524
  * - "BigInt" → bigint columns (BigInt-scalar-typed operators, no string pattern operators)
525
525
  * - "Decimal" → numeric/decimal columns (Decimal-scalar-typed operators, no string pattern operators)
526
- * - the enum GraphQL type name → enum columns (still unique per enum)
527
- * - "DateTime" → timestamp and date columns
526
+ * - the enum GraphQL type name → enum columns (still unique per enum; equality and
527
+ * membership only, like Boolean)
528
+ * - "DateTime" → timestamp and date columns (ordering kept, no string pattern operators)
528
529
  * - "Int" → integer/serial columns (no string pattern operators)
529
530
  * - "Float" → real/double columns (no string pattern operators)
530
531
  * - "String" → all other text/varchar columns
@@ -552,7 +553,8 @@ const resolveGenericFilterDescriptor = (column, columnGraphQLType) => {
552
553
  };
553
554
  if (columnGraphQLType.type instanceof GraphQLEnumType) return {
554
555
  name: columnGraphQLType.type.name,
555
- kind: "scalar"
556
+ kind: "scalar",
557
+ isEnum: true
556
558
  };
557
559
  if (columnGraphQLType.type === GraphQLBigIntString) return {
558
560
  name: "BigInt",
@@ -776,18 +778,32 @@ const arrayFilterFields = (colType, colArr) => {
776
778
  };
777
779
  /**
778
780
  * Filters whose fields omit the string pattern operators (like/notLike/ilike/notIlike/
779
- * startsWith/contains/…): they are nonsensical on opaque uuids and invalid SQL on numeric
780
- * columns. Keyed on the generic filter name (i.e. on column type) — a string-typed column
781
- * keeps the string operators whatever it is called.
781
+ * startsWith/contains/… and the `insensitive` flag that modifies them): they are nonsensical
782
+ * on opaque uuids, booleans and enum members, invalid SQL on numeric columns, and on a
783
+ * timestamp they match whatever the session happens to render the value as rather than
784
+ * anything in the query. Keyed on the generic filter name (i.e. on column type) — a
785
+ * string-typed column keeps the string operators whatever it is called. Enum filters are
786
+ * named after their enum and are recognised by `isEnum` instead.
782
787
  */
783
788
  const FILTERS_WITHOUT_STRING_OPS = /* @__PURE__ */ new Set([
784
789
  "Id",
785
790
  "Int",
786
791
  "Float",
787
792
  "BigInt",
788
- "Decimal"
793
+ "Decimal",
794
+ "Boolean",
795
+ "Date",
796
+ "DateTime"
789
797
  ]);
790
798
  /**
799
+ * Filters that additionally omit the ordering operators (lt/lte/gt/gte). Ordering two
800
+ * booleans is `eq` spelled as a puzzle (`gt: false` is `eq: true`), and ordering enum members
801
+ * compares their declaration order — an artefact of how the column was written rather than a
802
+ * fact about the domain. Timestamps and the numerics keep them: ranges are the point of
803
+ * having them. Enum filters are recognised by `isEnum`, as above.
804
+ */
805
+ const FILTERS_WITHOUT_ORDERING_OPS = /* @__PURE__ */ new Set(["Boolean"]);
806
+ /**
791
807
  * Scalars the built-in detection itself can produce. A scalar override to one of these
792
808
  * shares the built-in filter type for that scalar, so its shape must stay exactly what a
793
809
  * natural column of the scalar gets — whichever column builds the cached type first.
@@ -830,14 +846,15 @@ const resolveOverrideFilterDescriptor = (scalar) => {
830
846
  const generateColumnFilterValues = (column, tableName, columnName, cacheCtx) => {
831
847
  const columnGraphQLType = drizzleColumnToGraphQLType(column, columnName, tableName, true, false, true);
832
848
  const inputOverride = getColumnScalarOverride(column, true);
833
- const { name: genericName, kind } = inputOverride ? resolveOverrideFilterDescriptor(inputOverride) : resolveGenericFilterDescriptor(column, columnGraphQLType);
849
+ const { name: genericName, kind, isEnum } = inputOverride ? resolveOverrideFilterDescriptor(inputOverride) : resolveGenericFilterDescriptor(column, columnGraphQLType);
834
850
  const cached = cacheCtx.genericFilterCache.get(genericName);
835
851
  if (cached) return cached;
836
852
  const colType = columnGraphQLType.type;
837
853
  const colArr = new GraphQLList(new GraphQLNonNull(colType));
838
854
  const customOverrideFilter = inputOverride !== void 0 && !BUILTIN_FILTER_SCALARS.has(inputOverride);
839
855
  const underlying = extractExtendedColumnType(column);
840
- const omitStringOps = customOverrideFilter ? underlying.type !== "string" || underlying.constraint === "numeric" : FILTERS_WITHOUT_STRING_OPS.has(genericName);
856
+ const omitStringOps = customOverrideFilter ? underlying.type !== "string" || underlying.constraint === "numeric" : isEnum || FILTERS_WITHOUT_STRING_OPS.has(genericName);
857
+ const omitOrderingOps = !customOverrideFilter && (isEnum || FILTERS_WITHOUT_ORDERING_OPS.has(genericName));
841
858
  const baseFields = kind === "json" ? jsonFilterFields(column, colType, cacheCtx.typeName) : kind === "array" ? arrayFilterFields(colType, colArr) : {
842
859
  eq: {
843
860
  type: colType,
@@ -847,21 +864,23 @@ const generateColumnFilterValues = (column, tableName, columnName, cacheCtx) =>
847
864
  type: colType,
848
865
  description: "Not equal to"
849
866
  },
850
- lt: {
851
- type: colType,
852
- description: "Less than"
853
- },
854
- lte: {
855
- type: colType,
856
- description: "Less than or equal to"
857
- },
858
- gt: {
859
- type: colType,
860
- description: "Greater than"
861
- },
862
- gte: {
863
- type: colType,
864
- description: "Greater than or equal to"
867
+ ...omitOrderingOps ? {} : {
868
+ lt: {
869
+ type: colType,
870
+ description: "Less than"
871
+ },
872
+ lte: {
873
+ type: colType,
874
+ description: "Less than or equal to"
875
+ },
876
+ gt: {
877
+ type: colType,
878
+ description: "Greater than"
879
+ },
880
+ gte: {
881
+ type: colType,
882
+ description: "Greater than or equal to"
883
+ }
865
884
  },
866
885
  ...omitStringOps ? {} : {
867
886
  like: { type: GraphQLString },
@@ -2883,6 +2902,154 @@ const listFieldComplexity = (options, policy) => ({ args, childComplexity }) =>
2883
2902
  */
2884
2903
  const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
2885
2904
  //#endregion
2905
+ //#region src/util/parse-resolve-info.ts
2906
+ /** Unwraps `[T]!`-style wrappers down to the named type. `getNamedType` without the realm tie. */
2907
+ const namedTypeOf = (type) => {
2908
+ let current = type;
2909
+ while (current?.ofType) current = current.ofType;
2910
+ return current;
2911
+ };
2912
+ /**
2913
+ * Whether a field's named output type has sub-selections. Object types and interfaces expose
2914
+ * `getFields`, unions expose `getTypes`; scalars and enums expose neither. Input object types
2915
+ * also expose `getFields`, but an input type can never be a field's output type, so there is
2916
+ * nothing here for that to collide with.
2917
+ */
2918
+ const isCompositeLike = (type) => !!type && (typeof type.getFields === "function" || typeof type.getTypes === "function");
2919
+ /**
2920
+ * The fields declared on a type, or `undefined` when it has none to declare. A union is the
2921
+ * case that matters: it has no fields of its own, so a selection directly against one resolves
2922
+ * to nothing and is dropped — only the inline fragments underneath contribute.
2923
+ */
2924
+ const fieldsOf = (type) => typeof type?.getFields === "function" ? type.getFields() : void 0;
2925
+ /**
2926
+ * The runtime variable values, in the plain-map form the directive check wants. graphql 17
2927
+ * wraps them in `{ sources, coerced }`; graphql 16 hands over the map itself.
2928
+ */
2929
+ const coercedVariables = (info) => {
2930
+ const values = info.variableValues;
2931
+ if (values && typeof values === "object" && "coerced" in values) return values.coerced ?? {};
2932
+ return values ?? {};
2933
+ };
2934
+ /** The value of a `@skip` / `@include` `if:` argument — a literal or a variable reference. */
2935
+ const directiveIfValue = (info, value) => {
2936
+ if (value.kind === Kind.VARIABLE) return coercedVariables(info)[value.name.value];
2937
+ if (value.kind === Kind.BOOLEAN) return value.value;
2938
+ };
2939
+ /**
2940
+ * Whether `@skip` / `@include` on this selection exclude it. Applies to fields, fragment
2941
+ * spreads and inline fragments alike, which is where graphql puts them.
2942
+ */
2943
+ const isExcluded = (info, node) => {
2944
+ for (const directive of node.directives ?? []) {
2945
+ const directiveName = directive.name.value;
2946
+ if (directiveName !== "skip" && directiveName !== "include") continue;
2947
+ const ifArgument = directive.arguments?.find((argument) => argument.name.value === "if");
2948
+ if (!ifArgument) continue;
2949
+ const value = directiveIfValue(info, ifArgument.value);
2950
+ if (directiveName === "skip" ? !!value : !value) return true;
2951
+ }
2952
+ return false;
2953
+ };
2954
+ /** The type a fragment's `on Type` condition names, looked up in the executing schema. */
2955
+ const typeFromCondition = (info, condition) => info.schema.getType(condition.name.value);
2956
+ /**
2957
+ * Introspection meta-fields are not schema fields and never reach the database. `__typename` is
2958
+ * the one that turns up in practice; `__id` is excluded from the rule because a schema is free
2959
+ * to declare a field by that name.
2960
+ */
2961
+ const isMetaField = (name) => name.startsWith("__") && name !== "__id";
2962
+ /**
2963
+ * Merges one selection set into `tree`, which is a `fieldsByTypeName` map — the caller owns the
2964
+ * level, this owns the entries under `parentType`'s name.
2965
+ *
2966
+ * `activeFragments` is the set of fragment names on the current spread path. A document that
2967
+ * passed validation cannot contain a fragment cycle, but this parser also runs on trees a
2968
+ * caller assembled by hand, and an unbounded walk would hang rather than fail.
2969
+ */
2970
+ const collectSelections = (selections, info, tree, parentType, deep, activeFragments) => {
2971
+ const parentTypeName = parentType?.name;
2972
+ if (!parentTypeName) return tree;
2973
+ let fields = tree[parentTypeName];
2974
+ if (!fields) {
2975
+ fields = {};
2976
+ tree[parentTypeName] = fields;
2977
+ }
2978
+ const parentFields = fieldsOf(parentType);
2979
+ for (const selection of selections) {
2980
+ if (isExcluded(info, selection)) continue;
2981
+ if (selection.kind === Kind.FIELD) {
2982
+ const name = selection.name.value;
2983
+ if (isMetaField(name)) continue;
2984
+ const fieldDef = parentFields?.[name];
2985
+ if (!fieldDef) continue;
2986
+ const fieldType = namedTypeOf(fieldDef.type);
2987
+ if (!fieldType) continue;
2988
+ const composite = isCompositeLike(fieldType);
2989
+ const alias = selection.alias?.value || name;
2990
+ let node = fields[alias];
2991
+ if (!node) {
2992
+ node = {
2993
+ name,
2994
+ alias,
2995
+ args: { ...getArgumentValues(fieldDef, selection, info.variableValues) },
2996
+ fieldsByTypeName: composite && fieldType.name ? { [fieldType.name]: {} } : {}
2997
+ };
2998
+ fields[alias] = node;
2999
+ }
3000
+ if (selection.selectionSet && deep && composite) collectSelections(selection.selectionSet.selections, info, node.fieldsByTypeName, fieldType, deep, activeFragments);
3001
+ continue;
3002
+ }
3003
+ if (!deep) continue;
3004
+ if (selection.kind === Kind.FRAGMENT_SPREAD) {
3005
+ const fragmentName = selection.name.value;
3006
+ const fragment = info.fragments?.[fragmentName];
3007
+ if (!fragment) throw new Error(`Drizzle-GraphQL Error: unknown fragment '${fragmentName}' — it is spread in the query but not defined in the document.`);
3008
+ if (activeFragments.has(fragmentName)) continue;
3009
+ const fragmentType = fragment.typeCondition ? typeFromCondition(info, fragment.typeCondition) : parentType;
3010
+ if (!isCompositeLike(fragmentType)) continue;
3011
+ activeFragments.add(fragmentName);
3012
+ try {
3013
+ collectSelections(fragment.selectionSet.selections, info, tree, fragmentType, deep, activeFragments);
3014
+ } finally {
3015
+ activeFragments.delete(fragmentName);
3016
+ }
3017
+ continue;
3018
+ }
3019
+ if (selection.kind === Kind.INLINE_FRAGMENT) {
3020
+ const fragmentType = selection.typeCondition ? typeFromCondition(info, selection.typeCondition) : parentType;
3021
+ if (!isCompositeLike(fragmentType)) continue;
3022
+ collectSelections(selection.selectionSet.selections, info, tree, fragmentType, deep, activeFragments);
3023
+ }
3024
+ }
3025
+ return tree;
3026
+ };
3027
+ /** The first key of an object, without materializing the whole key array to discard it. */
3028
+ const firstKey = (object) => {
3029
+ for (const key in object) return key;
3030
+ };
3031
+ /**
3032
+ * Reads the selection under a resolver's own field out of its `info`, as a tree of
3033
+ * {@link ResolveTree} nodes.
3034
+ *
3035
+ * ```ts
3036
+ * const parsed = parseResolveInfo(info, { deep: true })!;
3037
+ * const selected = parsed.fieldsByTypeName['Users']; // keyed by response key
3038
+ * ```
3039
+ *
3040
+ * Returns `undefined` when the resolver's field produced no node at all, which in practice
3041
+ * means `info` carried no field nodes.
3042
+ */
3043
+ const parseResolveInfo = (info, options = {}) => {
3044
+ const fieldNodes = info.fieldNodes;
3045
+ if (!fieldNodes) throw new Error("Drizzle-GraphQL Error: resolve info carries no fieldNodes, so no selection could be read from it.");
3046
+ const tree = collectSelections(fieldNodes, info, {}, info.parentType, options.deep ?? true, /* @__PURE__ */ new Set());
3047
+ const typeKey = firstKey(tree);
3048
+ const fields = typeKey === void 0 ? void 0 : tree[typeKey];
3049
+ const fieldKey = fields && firstKey(fields);
3050
+ return fieldKey === void 0 ? void 0 : fields?.[fieldKey];
3051
+ };
3052
+ //#endregion
2886
3053
  //#region src/util/builders/field-updates.ts
2887
3054
  /**
2888
3055
  * The scalars arithmetic is offered on. Decimal and BigInt transport as strings but are
@@ -3215,75 +3382,196 @@ const pruneNonEagerRelations = (relationMap, shouldEagerLoad) => {
3215
3382
  for (const [tableName, rels] of Object.entries(relationMap)) out[tableName] = Object.fromEntries(Object.entries(rels).filter(([relationName]) => shouldEagerLoad(tableName, relationName)));
3216
3383
  return out;
3217
3384
  };
3218
- const createMutationTxCtx = (options) => options ? {
3219
- timeoutMs: options.timeoutMs,
3220
- fieldNames: /* @__PURE__ */ new Set(),
3221
- states: /* @__PURE__ */ new WeakMap()
3222
- } : void 0;
3223
- const includedByDirectives = (node, variables) => {
3224
- for (const directive of node.directives ?? []) {
3225
- const name = directive.name.value;
3226
- if (name !== "skip" && name !== "include") continue;
3227
- const ifArg = directive.arguments?.find((arg) => arg.name.value === "if");
3228
- let value;
3229
- if (ifArg?.value.kind === "BooleanValue") value = ifArg.value.value;
3230
- else if (ifArg?.value.kind === "Variable") value = variables?.[ifArg.value.name.value];
3231
- if (name === "skip" && value === true) return false;
3232
- if (name === "include" && value !== true) return false;
3233
- }
3234
- return true;
3235
- };
3385
+ //#endregion
3386
+ //#region src/util/builders/common/select-runtime.ts
3236
3387
  /**
3237
- * Collects the response keys of every root mutation field the executor will run, expanding
3238
- * fragments and honoring `@skip` / `@include`. Distinct response keys (alias ?? name) are
3239
- * counted, matching graphql-js field collection, so a field selected twice under one key is
3240
- * still one resolver call. Returns `false` when the document selects a root mutation field
3241
- * this build did not generate — its completion cannot be tracked, so the caller must not
3242
- * open a transaction it might never be able to close.
3388
+ * Runs the relational-query-builder select shared by every dialect's `generateSelect*`
3389
+ * resolver: selected columns + offset/limit + aliased orderBy/where callbacks + the eager
3390
+ * `with:` relation params, then remaps the result. `single` switches between
3391
+ * findFirst/findMany (and the single path omits `limit`). The PG fallback for tables
3392
+ * without RQB support stays in pg.ts; this covers the common RQB path for all three.
3243
3393
  */
3244
- const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments) => {
3245
- for (const selection of selectionSet.selections) {
3246
- if (!includedByDirectives(selection, variables)) continue;
3247
- if (selection.kind === "Field") {
3248
- const name = selection.name.value;
3249
- if (name === "__typename") continue;
3250
- if (!known.has(name)) return false;
3251
- keys.add(selection.alias?.value ?? name);
3252
- } else if (selection.kind === "InlineFragment") {
3253
- if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
3254
- } else if (selection.kind === "FragmentSpread") {
3255
- const fragmentName = selection.name.value;
3256
- if (visitedFragments.has(fragmentName)) continue;
3257
- visitedFragments.add(fragmentName);
3258
- const fragment = fragments?.[fragmentName];
3259
- if (!fragment) return false;
3260
- if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
3261
- }
3394
+ const runRelationalSelect = async (opts) => {
3395
+ const { queryBase, tables, tableName, table, relationMap, typeName, typeNameMapper, parsedInfo, offset, orderBy, where, single, filterCtx, pkNames, after, scope, deleted } = opts;
3396
+ const distinct = opts.distinct?.length ? opts.distinct : void 0;
3397
+ const cursorSelected = !single && isCursorFieldSelected(parsedInfo.fieldsByTypeName[typeName], table);
3398
+ let cursorEntries;
3399
+ if (!single && (after != null || cursorSelected)) {
3400
+ if (after != null && distinct) throw drizzleError("'after' cannot be combined with 'distinct'.", { code: "DRIZZLE_INVALID_CURSOR" });
3401
+ const cursorObstacle = orderByCursorObstacle(orderBy);
3402
+ if (cursorObstacle) {
3403
+ if (after != null) throw drizzleError(cursorObstacle, { code: "DRIZZLE_INVALID_CURSOR" });
3404
+ } else if (!pkNames?.length) {
3405
+ if (after != null) throw drizzleError(`Table ${tableName} has no primary key, so cursor pagination cannot be used on it.`, { code: "DRIZZLE_INVALID_CURSOR" });
3406
+ } else cursorEntries = cursorOrderingEntries(orderBy, pkNames);
3262
3407
  }
3263
- return true;
3264
- };
3265
- const abortedBatchError = () => drizzleError("Drizzle-GraphQL Error: Mutation was not executed because an earlier mutation in the same request failed and the shared transaction was rolled back.", { code: "DRIZZLE_TRANSACTION_ABORTED" });
3266
- /** Opens the request-wide transaction and wires up latch, inactivity timer and cleanup. */
3267
- const openSharedTx = (db, context, txCtx, stateKey, expected) => {
3268
- let release;
3269
- let rejectLatch;
3270
- const latch = new Promise((resolve, reject) => {
3271
- release = resolve;
3272
- rejectLatch = reject;
3273
- });
3274
- let readyResolve;
3275
- let readyReject;
3276
- const ready = new Promise((resolve, reject) => {
3277
- readyResolve = resolve;
3278
- readyReject = reject;
3279
- });
3280
- ready.catch(() => {});
3281
- const canStash = context !== null && typeof context === "object" && !Object.isFrozen(context);
3282
- const settled = db.transaction(async (tx) => {
3283
- if (canStash) context[drizzleExecutorKey] = tx;
3284
- readyResolve(tx);
3285
- await latch;
3286
- });
3408
+ const cursorValues = after != null && cursorEntries ? decodeCursor(after, cursorEntries) : void 0;
3409
+ const needsDefaultOrder = single || offset != null || opts.limit != null;
3410
+ let distinctKeys;
3411
+ if (distinct) {
3412
+ distinctKeys = await selectDistinctKeys({
3413
+ db: opts.db,
3414
+ table,
3415
+ tableName,
3416
+ distinct,
3417
+ pkNames: pkNames ?? [],
3418
+ where: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0, deleted),
3419
+ orderBy,
3420
+ limit: single ? 1 : opts.limit,
3421
+ offset
3422
+ });
3423
+ if (!distinctKeys.length) return single ? void 0 : [];
3424
+ }
3425
+ const params = {
3426
+ columns: extractSelectedColumnsFromTree(parsedInfo.fieldsByTypeName[typeName], table, {
3427
+ tableName,
3428
+ relationMap,
3429
+ tables,
3430
+ allRelations: filterCtx?.relationMap
3431
+ }),
3432
+ offset: distinctKeys ? void 0 : offset,
3433
+ 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,
3434
+ where: distinctKeys ? { RAW: (aliasedTable) => primaryKeyRestriction(aliasedTable, pkNames, distinctKeys) } : where || cursorValues || scope?.has(tableName, deleted) ? { RAW: (aliasedTable) => withScope(scope, tableName, aliasedTable, 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,
3435
+ with: relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, {
3436
+ typeNameMapper,
3437
+ filterCtx,
3438
+ limits: opts.limits,
3439
+ scope,
3440
+ defaultOrderBy: opts.defaultOrderBy,
3441
+ resolveName: opts.resolveName
3442
+ }) : void 0
3443
+ };
3444
+ if (single) {
3445
+ const result = await queryBase.findFirst(params);
3446
+ return result ? remapToGraphQLSingleOutput(result, tableName, table, relationMap) : void 0;
3447
+ }
3448
+ if (cursorEntries && cursorSelected) for (const [column] of cursorEntries) params.columns[column] = true;
3449
+ params.limit = distinctKeys ? void 0 : opts.limit;
3450
+ const result = await queryBase.findMany(params);
3451
+ if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
3452
+ return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
3453
+ };
3454
+ /**
3455
+ * After a mutation, re-fetch the mutated rows through the relational query builder so the
3456
+ * selected relations are eagerly loaded in a single query, then merge those relations onto
3457
+ * the `.returning()` rows — making the per-field BatchLoader fallback unnecessary.
3458
+ *
3459
+ * `withParams` is the pre-computed relation selection (from extractRelationsParams); the
3460
+ * caller only invokes this when relations are actually selected, so it also gates whether
3461
+ * the PK was forced into RETURNING.
3462
+ *
3463
+ * Falls back to the original `.returning()` rows (relations then resolve via the
3464
+ * field-level BatchLoader) when the table has no RQB support, no primary key columns can be
3465
+ * determined, or the re-fetch fails. Supports single- and multi-column primary keys.
3466
+ */
3467
+ const eagerLoadMutationRelations = async (db, tableName, rows, pkNames, withParams) => {
3468
+ if (!rows.length || !pkNames.length || !withParams || !Object.keys(withParams).length) return rows;
3469
+ const queryBase = db.query?.[tableName];
3470
+ if (!queryBase) return rows;
3471
+ const keyableRows = rows.filter((row) => pkNames.every((n) => row[n] != null));
3472
+ if (!keyableRows.length) return rows;
3473
+ const pkColumns = {};
3474
+ for (const pk of pkNames) pkColumns[pk] = true;
3475
+ const relationNames = Object.keys(withParams);
3476
+ const keyOf = (row) => JSON.stringify(pkNames.map((n) => typeof row[n] === "bigint" ? row[n].toString() : row[n]));
3477
+ let whereRaw;
3478
+ if (pkNames.length === 1) {
3479
+ const pkName = pkNames[0];
3480
+ const ids = keyableRows.map((r) => r[pkName]);
3481
+ whereRaw = (aliased) => inArray(aliased[pkName], ids);
3482
+ } else whereRaw = (aliased) => {
3483
+ const lhs = sql.join(pkNames.map((n) => sql`${aliased[n]}`), sql`, `);
3484
+ const tuples = sql.join(keyableRows.map((row) => sql`(${sql.join(pkNames.map((n) => sql`${row[n]}`), sql`, `)})`), sql`, `);
3485
+ return sql`(${lhs}) in (${tuples})`;
3486
+ };
3487
+ let enriched;
3488
+ try {
3489
+ enriched = await queryBase.findMany({
3490
+ columns: pkColumns,
3491
+ where: { RAW: whereRaw },
3492
+ with: withParams
3493
+ });
3494
+ } catch (err) {
3495
+ console.warn(`[drizzle-graphql] eager-loading relations for a "${tableName}" mutation failed; falling back to lazy resolution.`, err);
3496
+ return rows;
3497
+ }
3498
+ const byKey = new Map(enriched.map((e) => [keyOf(e), e]));
3499
+ for (const row of rows) {
3500
+ const match = byKey.get(keyOf(row));
3501
+ if (!match) continue;
3502
+ for (const rel of relationNames) row[rel] = match[rel];
3503
+ }
3504
+ return rows;
3505
+ };
3506
+ const createMutationTxCtx = (options) => options ? {
3507
+ timeoutMs: options.timeoutMs,
3508
+ fieldNames: /* @__PURE__ */ new Set(),
3509
+ states: /* @__PURE__ */ new WeakMap()
3510
+ } : void 0;
3511
+ const includedByDirectives = (node, variables) => {
3512
+ for (const directive of node.directives ?? []) {
3513
+ const name = directive.name.value;
3514
+ if (name !== "skip" && name !== "include") continue;
3515
+ const ifArg = directive.arguments?.find((arg) => arg.name.value === "if");
3516
+ let value;
3517
+ if (ifArg?.value.kind === "BooleanValue") value = ifArg.value.value;
3518
+ else if (ifArg?.value.kind === "Variable") value = variables?.[ifArg.value.name.value];
3519
+ if (name === "skip" && value === true) return false;
3520
+ if (name === "include" && value !== true) return false;
3521
+ }
3522
+ return true;
3523
+ };
3524
+ /**
3525
+ * Collects the response keys of every root mutation field the executor will run, expanding
3526
+ * fragments and honoring `@skip` / `@include`. Distinct response keys (alias ?? name) are
3527
+ * counted, matching graphql-js field collection, so a field selected twice under one key is
3528
+ * still one resolver call. Returns `false` when the document selects a root mutation field
3529
+ * this build did not generate — its completion cannot be tracked, so the caller must not
3530
+ * open a transaction it might never be able to close.
3531
+ */
3532
+ const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments) => {
3533
+ for (const selection of selectionSet.selections) {
3534
+ if (!includedByDirectives(selection, variables)) continue;
3535
+ if (selection.kind === "Field") {
3536
+ const name = selection.name.value;
3537
+ if (name === "__typename") continue;
3538
+ if (!known.has(name)) return false;
3539
+ keys.add(selection.alias?.value ?? name);
3540
+ } else if (selection.kind === "InlineFragment") {
3541
+ if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
3542
+ } else if (selection.kind === "FragmentSpread") {
3543
+ const fragmentName = selection.name.value;
3544
+ if (visitedFragments.has(fragmentName)) continue;
3545
+ visitedFragments.add(fragmentName);
3546
+ const fragment = fragments?.[fragmentName];
3547
+ if (!fragment) return false;
3548
+ if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
3549
+ }
3550
+ }
3551
+ return true;
3552
+ };
3553
+ const abortedBatchError = () => drizzleError("Drizzle-GraphQL Error: Mutation was not executed because an earlier mutation in the same request failed and the shared transaction was rolled back.", { code: "DRIZZLE_TRANSACTION_ABORTED" });
3554
+ /** Opens the request-wide transaction and wires up latch, inactivity timer and cleanup. */
3555
+ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
3556
+ let release;
3557
+ let rejectLatch;
3558
+ const latch = new Promise((resolve, reject) => {
3559
+ release = resolve;
3560
+ rejectLatch = reject;
3561
+ });
3562
+ let readyResolve;
3563
+ let readyReject;
3564
+ const ready = new Promise((resolve, reject) => {
3565
+ readyResolve = resolve;
3566
+ readyReject = reject;
3567
+ });
3568
+ ready.catch(() => {});
3569
+ const canStash = context !== null && typeof context === "object" && !Object.isFrozen(context);
3570
+ const settled = db.transaction(async (tx) => {
3571
+ if (canStash) context[drizzleExecutorKey] = tx;
3572
+ readyResolve(tx);
3573
+ await latch;
3574
+ });
3287
3575
  let timer;
3288
3576
  const clearTimer = () => {
3289
3577
  if (timer !== void 0) {
@@ -3393,12 +3681,6 @@ const hardDeleteArg = {
3393
3681
  defaultValue: false,
3394
3682
  description: "Remove the matched rows instead of marking them deleted. Reads at `INCLUDE`, so it reaches rows that are already marked; a `scope` still confines what it can reach."
3395
3683
  };
3396
- /**
3397
- * Computes the RETURNING columns and relation selection for a mutation resolver: extracts
3398
- * the selected scalar columns, determines whether any relations were selected, and only
3399
- * then forces the primary key into the column set (so the post-mutation eager-load can
3400
- * re-key rows). Returns everything the resolver needs to decide whether to eager-load.
3401
- */
3402
3684
  const prepareMutationRelationColumns = (params) => {
3403
3685
  const { relationMap, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
3404
3686
  const typeName = resolveObjectTypeName(tableName, typeNameMapper, resolveName);
@@ -3422,6 +3704,38 @@ const prepareMutationRelationColumns = (params) => {
3422
3704
  };
3423
3705
  };
3424
3706
  /**
3707
+ * The prologue every write resolver runs: parse the resolve info, then work out the columns
3708
+ * to return and the relations to eager-load from it. `parsedInfo` comes back too — the delete
3709
+ * resolver reads its own columns off the tree rather than taking the prepared set.
3710
+ */
3711
+ const mutationSelection = (info, params) => {
3712
+ const parsedInfo = parseResolveInfo(info, { deep: true });
3713
+ return {
3714
+ parsedInfo,
3715
+ ...prepareMutationRelationColumns({
3716
+ ...params,
3717
+ parsedInfo
3718
+ })
3719
+ };
3720
+ };
3721
+ /**
3722
+ * The `where` a write runs under: the caller's filters — required when the mutation refuses to
3723
+ * run unbounded — with the row scope ANDed on last, so a supplied filter can only narrow the
3724
+ * scope and never widen it. An out-of-scope row is simply not matched.
3725
+ *
3726
+ * `deleted` states which soft-delete mode the match reads at, for the writes that need one:
3727
+ * `restore` sees only marked rows, a hard delete has to see them too.
3728
+ */
3729
+ const scopedWhere = (params) => {
3730
+ const { scope, tableName, table, where, relationCtx, required, deleted } = params;
3731
+ return withScope(scope, tableName, table, required ? extractRequiredFilters(table, tableName, where, relationCtx) : where ? extractFilters(table, tableName, where, relationCtx) : void 0, deleted);
3732
+ };
3733
+ /**
3734
+ * The epilogue: the written rows with their selected relations attached, or the rows as they
3735
+ * came back when the selection asked for no relation at all.
3736
+ */
3737
+ const withEagerRelations = (executor, tableName, rows, pkNames, withParams, hasRelations) => hasRelations ? eagerLoadMutationRelations(executor, tableName, rows, pkNames, withParams) : rows;
3738
+ /**
3425
3739
  * The per-entry input of `update<Table>Many`: `{ where, set }`, reusing the table's
3426
3740
  * update `set` input and filter input. Shared by all three dialect builders.
3427
3741
  */
@@ -3521,8 +3835,14 @@ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, f
3521
3835
  try {
3522
3836
  return await runMutation(db, context, info, txCtx, async (executor) => {
3523
3837
  const { where, set } = args;
3524
- const relationCtx = relationFilterCtx(filterCtx, tableName);
3525
- const filters = requireWhere ? extractRequiredFilters(table, tableName, where, relationCtx) : where ? extractFilters(table, tableName, where, relationCtx) : void 0;
3838
+ const filters = scopedWhere({
3839
+ scope: void 0,
3840
+ tableName,
3841
+ table,
3842
+ where,
3843
+ relationCtx: relationFilterCtx(filterCtx, tableName),
3844
+ required: requireWhere
3845
+ });
3526
3846
  let query;
3527
3847
  if (kind === "delete") query = executor.delete(table);
3528
3848
  else {
@@ -3660,260 +3980,112 @@ const relationFieldExtension = (params) => ({
3660
3980
  });
3661
3981
  /** Pulls the primary-key values out of one `values` entry; `undefined` if any are missing. */
3662
3982
  const keyFromValues = (entry, primaryKey) => {
3663
- if (!entry || typeof entry !== "object") return;
3664
- const row = {};
3665
- for (const column of primaryKey) {
3666
- const value = entry[column];
3667
- if (value === void 0 || value === null) return;
3668
- row[column] = value;
3669
- }
3670
- return row;
3671
- };
3672
- /**
3673
- * Pulls primary-key values out of a filter argument.
3674
- *
3675
- * Only top-level equality counts: `{ id: { eq } }` names one row, `{ id: { inArray } }`
3676
- * names a list, and a composite key needs `eq` on every one of its columns. A boolean
3677
- * group (`OR` / `AND` / `NOT`) or any other operator on a key column means the filter is
3678
- * not a key lookup, and the rows are reported as not identified.
3679
- */
3680
- const keysFromWhere = (where, primaryKey) => {
3681
- const none = {
3682
- rows: [],
3683
- complete: false
3684
- };
3685
- if (!where || typeof where !== "object" || Array.isArray(where)) return none;
3686
- const filters = where;
3687
- if (filters.OR || filters.AND || filters.NOT) return none;
3688
- if (primaryKey.length > 1) {
3689
- const row = {};
3690
- for (const column of primaryKey) {
3691
- const operators = filters[column];
3692
- if (!operators || typeof operators !== "object" || operators.eq === void 0) return none;
3693
- row[column] = operators.eq;
3694
- }
3695
- return {
3696
- rows: [row],
3697
- complete: Object.keys(filters).length === primaryKey.length
3698
- };
3699
- }
3700
- const column = primaryKey[0];
3701
- if (!column) return none;
3702
- const operators = filters[column];
3703
- if (!operators || typeof operators !== "object") return none;
3704
- const exact = Object.keys(filters).length === 1 && Object.keys(operators).length === 1;
3705
- if (operators.eq !== void 0) return {
3706
- rows: [{ [column]: operators.eq }],
3707
- complete: exact
3708
- };
3709
- if (Array.isArray(operators.inArray)) return {
3710
- rows: operators.inArray.map((value) => ({ [column]: value })),
3711
- complete: exact
3712
- };
3713
- return none;
3714
- };
3715
- /**
3716
- * The rows a generated field's arguments are about, as primary-key values.
3717
- *
3718
- * The row identity of a mutation lives in a different argument for every operation —
3719
- * `where` for update and delete, `values` for insert and upsert, a per-entry `where` inside
3720
- * `updates` for the batch update — and each of those has both a one-row and a many-row
3721
- * spelling. This reads whichever one applies:
3722
- *
3723
- * ```ts
3724
- * const field = info.parentType.getFields()[info.fieldName];
3725
- * const target = identifyRows(field, args);
3726
- * if (target?.complete) await invalidate(target.table, target.rows);
3727
- * ```
3728
- *
3729
- * Returns `undefined` for a field this library did not generate, or an object type. Check
3730
- * `complete` before treating `rows` as the affected set — see {@link IdentifiedRows}.
3731
- */
3732
- const identifyRows = (field, args) => {
3733
- const meta = drizzleExtension(field);
3734
- if (!isDrizzleFieldExtension(meta)) return;
3735
- const { table, primaryKey, targetArg } = meta;
3736
- const empty = {
3737
- table,
3738
- primaryKey,
3739
- rows: [],
3740
- complete: false
3741
- };
3742
- if (!primaryKey.length || !targetArg) return empty;
3743
- const target = args?.[targetArg];
3744
- if (targetArg === "values") {
3745
- const entries = Array.isArray(target) ? target : target === void 0 ? [] : [target];
3746
- const rows = entries.map((entry) => keyFromValues(entry, primaryKey));
3747
- return {
3748
- table,
3749
- primaryKey,
3750
- rows: rows.filter((row) => !!row),
3751
- complete: entries.length > 0 && rows.every(Boolean)
3752
- };
3753
- }
3754
- if (targetArg === "updates") {
3755
- if (!Array.isArray(target)) return empty;
3756
- const perEntry = target.map((entry) => keysFromWhere(entry?.where, primaryKey));
3757
- return {
3758
- table,
3759
- primaryKey,
3760
- rows: perEntry.flatMap((entry) => entry.rows),
3761
- complete: target.length > 0 && perEntry.every((entry) => entry.complete)
3762
- };
3763
- }
3764
- return {
3765
- table,
3766
- primaryKey,
3767
- ...keysFromWhere(target, primaryKey)
3768
- };
3769
- };
3770
- //#endregion
3771
- //#region src/util/parse-resolve-info.ts
3772
- /** Unwraps `[T]!`-style wrappers down to the named type. `getNamedType` without the realm tie. */
3773
- const namedTypeOf = (type) => {
3774
- let current = type;
3775
- while (current?.ofType) current = current.ofType;
3776
- return current;
3777
- };
3778
- /**
3779
- * Whether a field's named output type has sub-selections. Object types and interfaces expose
3780
- * `getFields`, unions expose `getTypes`; scalars and enums expose neither. Input object types
3781
- * also expose `getFields`, but an input type can never be a field's output type, so there is
3782
- * nothing here for that to collide with.
3783
- */
3784
- const isCompositeLike = (type) => !!type && (typeof type.getFields === "function" || typeof type.getTypes === "function");
3785
- /**
3786
- * The fields declared on a type, or `undefined` when it has none to declare. A union is the
3787
- * case that matters: it has no fields of its own, so a selection directly against one resolves
3788
- * to nothing and is dropped — only the inline fragments underneath contribute.
3789
- */
3790
- const fieldsOf = (type) => typeof type?.getFields === "function" ? type.getFields() : void 0;
3791
- /**
3792
- * The runtime variable values, in the plain-map form the directive check wants. graphql 17
3793
- * wraps them in `{ sources, coerced }`; graphql 16 hands over the map itself.
3794
- */
3795
- const coercedVariables = (info) => {
3796
- const values = info.variableValues;
3797
- if (values && typeof values === "object" && "coerced" in values) return values.coerced ?? {};
3798
- return values ?? {};
3799
- };
3800
- /** The value of a `@skip` / `@include` `if:` argument — a literal or a variable reference. */
3801
- const directiveIfValue = (info, value) => {
3802
- if (value.kind === Kind.VARIABLE) return coercedVariables(info)[value.name.value];
3803
- if (value.kind === Kind.BOOLEAN) return value.value;
3804
- };
3805
- /**
3806
- * Whether `@skip` / `@include` on this selection exclude it. Applies to fields, fragment
3807
- * spreads and inline fragments alike, which is where graphql puts them.
3808
- */
3809
- const isExcluded = (info, node) => {
3810
- for (const directive of node.directives ?? []) {
3811
- const directiveName = directive.name.value;
3812
- if (directiveName !== "skip" && directiveName !== "include") continue;
3813
- const ifArgument = directive.arguments?.find((argument) => argument.name.value === "if");
3814
- if (!ifArgument) continue;
3815
- const value = directiveIfValue(info, ifArgument.value);
3816
- if (directiveName === "skip" ? !!value : !value) return true;
3817
- }
3818
- return false;
3819
- };
3820
- /** The type a fragment's `on Type` condition names, looked up in the executing schema. */
3821
- const typeFromCondition = (info, condition) => info.schema.getType(condition.name.value);
3822
- /**
3823
- * Introspection meta-fields are not schema fields and never reach the database. `__typename` is
3824
- * the one that turns up in practice; `__id` is excluded from the rule because a schema is free
3825
- * to declare a field by that name.
3826
- */
3827
- const isMetaField = (name) => name.startsWith("__") && name !== "__id";
3828
- /**
3829
- * Merges one selection set into `tree`, which is a `fieldsByTypeName` map — the caller owns the
3830
- * level, this owns the entries under `parentType`'s name.
3831
- *
3832
- * `activeFragments` is the set of fragment names on the current spread path. A document that
3833
- * passed validation cannot contain a fragment cycle, but this parser also runs on trees a
3834
- * caller assembled by hand, and an unbounded walk would hang rather than fail.
3835
- */
3836
- const collectSelections = (selections, info, tree, parentType, deep, activeFragments) => {
3837
- const parentTypeName = parentType?.name;
3838
- if (!parentTypeName) return tree;
3839
- let fields = tree[parentTypeName];
3840
- if (!fields) {
3841
- fields = {};
3842
- tree[parentTypeName] = fields;
3843
- }
3844
- const parentFields = fieldsOf(parentType);
3845
- for (const selection of selections) {
3846
- if (isExcluded(info, selection)) continue;
3847
- if (selection.kind === Kind.FIELD) {
3848
- const name = selection.name.value;
3849
- if (isMetaField(name)) continue;
3850
- const fieldDef = parentFields?.[name];
3851
- if (!fieldDef) continue;
3852
- const fieldType = namedTypeOf(fieldDef.type);
3853
- if (!fieldType) continue;
3854
- const composite = isCompositeLike(fieldType);
3855
- const alias = selection.alias?.value || name;
3856
- let node = fields[alias];
3857
- if (!node) {
3858
- node = {
3859
- name,
3860
- alias,
3861
- args: { ...getArgumentValues(fieldDef, selection, info.variableValues) },
3862
- fieldsByTypeName: composite && fieldType.name ? { [fieldType.name]: {} } : {}
3863
- };
3864
- fields[alias] = node;
3865
- }
3866
- if (selection.selectionSet && deep && composite) collectSelections(selection.selectionSet.selections, info, node.fieldsByTypeName, fieldType, deep, activeFragments);
3867
- continue;
3868
- }
3869
- if (!deep) continue;
3870
- if (selection.kind === Kind.FRAGMENT_SPREAD) {
3871
- const fragmentName = selection.name.value;
3872
- const fragment = info.fragments?.[fragmentName];
3873
- if (!fragment) throw new Error(`Drizzle-GraphQL Error: unknown fragment '${fragmentName}' — it is spread in the query but not defined in the document.`);
3874
- if (activeFragments.has(fragmentName)) continue;
3875
- const fragmentType = fragment.typeCondition ? typeFromCondition(info, fragment.typeCondition) : parentType;
3876
- if (!isCompositeLike(fragmentType)) continue;
3877
- activeFragments.add(fragmentName);
3878
- try {
3879
- collectSelections(fragment.selectionSet.selections, info, tree, fragmentType, deep, activeFragments);
3880
- } finally {
3881
- activeFragments.delete(fragmentName);
3882
- }
3883
- continue;
3884
- }
3885
- if (selection.kind === Kind.INLINE_FRAGMENT) {
3886
- const fragmentType = selection.typeCondition ? typeFromCondition(info, selection.typeCondition) : parentType;
3887
- if (!isCompositeLike(fragmentType)) continue;
3888
- collectSelections(selection.selectionSet.selections, info, tree, fragmentType, deep, activeFragments);
3889
- }
3983
+ if (!entry || typeof entry !== "object") return;
3984
+ const row = {};
3985
+ for (const column of primaryKey) {
3986
+ const value = entry[column];
3987
+ if (value === void 0 || value === null) return;
3988
+ row[column] = value;
3890
3989
  }
3891
- return tree;
3990
+ return row;
3892
3991
  };
3893
- /** The first key of an object, without materializing the whole key array to discard it. */
3894
- const firstKey = (object) => {
3895
- for (const key in object) return key;
3992
+ /**
3993
+ * Pulls primary-key values out of a filter argument.
3994
+ *
3995
+ * Only top-level equality counts: `{ id: { eq } }` names one row, `{ id: { inArray } }`
3996
+ * names a list, and a composite key needs `eq` on every one of its columns. A boolean
3997
+ * group (`OR` / `AND` / `NOT`) or any other operator on a key column means the filter is
3998
+ * not a key lookup, and the rows are reported as not identified.
3999
+ */
4000
+ const keysFromWhere = (where, primaryKey) => {
4001
+ const none = {
4002
+ rows: [],
4003
+ complete: false
4004
+ };
4005
+ if (!where || typeof where !== "object" || Array.isArray(where)) return none;
4006
+ const filters = where;
4007
+ if (filters.OR || filters.AND || filters.NOT) return none;
4008
+ if (primaryKey.length > 1) {
4009
+ const row = {};
4010
+ for (const column of primaryKey) {
4011
+ const operators = filters[column];
4012
+ if (!operators || typeof operators !== "object" || operators.eq === void 0) return none;
4013
+ row[column] = operators.eq;
4014
+ }
4015
+ return {
4016
+ rows: [row],
4017
+ complete: Object.keys(filters).length === primaryKey.length
4018
+ };
4019
+ }
4020
+ const column = primaryKey[0];
4021
+ if (!column) return none;
4022
+ const operators = filters[column];
4023
+ if (!operators || typeof operators !== "object") return none;
4024
+ const exact = Object.keys(filters).length === 1 && Object.keys(operators).length === 1;
4025
+ if (operators.eq !== void 0) return {
4026
+ rows: [{ [column]: operators.eq }],
4027
+ complete: exact
4028
+ };
4029
+ if (Array.isArray(operators.inArray)) return {
4030
+ rows: operators.inArray.map((value) => ({ [column]: value })),
4031
+ complete: exact
4032
+ };
4033
+ return none;
3896
4034
  };
3897
4035
  /**
3898
- * Reads the selection under a resolver's own field out of its `info`, as a tree of
3899
- * {@link ResolveTree} nodes.
4036
+ * The rows a generated field's arguments are about, as primary-key values.
4037
+ *
4038
+ * The row identity of a mutation lives in a different argument for every operation —
4039
+ * `where` for update and delete, `values` for insert and upsert, a per-entry `where` inside
4040
+ * `updates` for the batch update — and each of those has both a one-row and a many-row
4041
+ * spelling. This reads whichever one applies:
3900
4042
  *
3901
4043
  * ```ts
3902
- * const parsed = parseResolveInfo(info, { deep: true })!;
3903
- * const selected = parsed.fieldsByTypeName['Users']; // keyed by response key
4044
+ * const field = info.parentType.getFields()[info.fieldName];
4045
+ * const target = identifyRows(field, args);
4046
+ * if (target?.complete) await invalidate(target.table, target.rows);
3904
4047
  * ```
3905
4048
  *
3906
- * Returns `undefined` when the resolver's field produced no node at all, which in practice
3907
- * means `info` carried no field nodes.
4049
+ * Returns `undefined` for a field this library did not generate, or an object type. Check
4050
+ * `complete` before treating `rows` as the affected set — see {@link IdentifiedRows}.
3908
4051
  */
3909
- const parseResolveInfo = (info, options = {}) => {
3910
- const fieldNodes = info.fieldNodes;
3911
- if (!fieldNodes) throw new Error("Drizzle-GraphQL Error: resolve info carries no fieldNodes, so no selection could be read from it.");
3912
- const tree = collectSelections(fieldNodes, info, {}, info.parentType, options.deep ?? true, /* @__PURE__ */ new Set());
3913
- const typeKey = firstKey(tree);
3914
- const fields = typeKey === void 0 ? void 0 : tree[typeKey];
3915
- const fieldKey = fields && firstKey(fields);
3916
- return fieldKey === void 0 ? void 0 : fields?.[fieldKey];
4052
+ const identifyRows = (field, args) => {
4053
+ const meta = drizzleExtension(field);
4054
+ if (!isDrizzleFieldExtension(meta)) return;
4055
+ const { table, primaryKey, targetArg } = meta;
4056
+ const empty = {
4057
+ table,
4058
+ primaryKey,
4059
+ rows: [],
4060
+ complete: false
4061
+ };
4062
+ if (!primaryKey.length || !targetArg) return empty;
4063
+ const target = args?.[targetArg];
4064
+ if (targetArg === "values") {
4065
+ const entries = Array.isArray(target) ? target : target === void 0 ? [] : [target];
4066
+ const rows = entries.map((entry) => keyFromValues(entry, primaryKey));
4067
+ return {
4068
+ table,
4069
+ primaryKey,
4070
+ rows: rows.filter((row) => !!row),
4071
+ complete: entries.length > 0 && rows.every(Boolean)
4072
+ };
4073
+ }
4074
+ if (targetArg === "updates") {
4075
+ if (!Array.isArray(target)) return empty;
4076
+ const perEntry = target.map((entry) => keysFromWhere(entry?.where, primaryKey));
4077
+ return {
4078
+ table,
4079
+ primaryKey,
4080
+ rows: perEntry.flatMap((entry) => entry.rows),
4081
+ complete: target.length > 0 && perEntry.every((entry) => entry.complete)
4082
+ };
4083
+ }
4084
+ return {
4085
+ table,
4086
+ primaryKey,
4087
+ ...keysFromWhere(target, primaryKey)
4088
+ };
3917
4089
  };
3918
4090
  //#endregion
3919
4091
  //#region src/util/builders/common/relation-resolvers.ts
@@ -4177,127 +4349,6 @@ const defineRootField = (fields, kind, name, config) => {
4177
4349
  fields[name] = config;
4178
4350
  };
4179
4351
  //#endregion
4180
- //#region src/util/builders/common/select-runtime.ts
4181
- /**
4182
- * Runs the relational-query-builder select shared by every dialect's `generateSelect*`
4183
- * resolver: selected columns + offset/limit + aliased orderBy/where callbacks + the eager
4184
- * `with:` relation params, then remaps the result. `single` switches between
4185
- * findFirst/findMany (and the single path omits `limit`). The PG fallback for tables
4186
- * without RQB support stays in pg.ts; this covers the common RQB path for all three.
4187
- */
4188
- const runRelationalSelect = async (opts) => {
4189
- const { queryBase, tables, tableName, table, relationMap, typeName, typeNameMapper, parsedInfo, offset, orderBy, where, single, filterCtx, pkNames, after, scope, deleted } = opts;
4190
- const distinct = opts.distinct?.length ? opts.distinct : void 0;
4191
- const cursorSelected = !single && isCursorFieldSelected(parsedInfo.fieldsByTypeName[typeName], table);
4192
- let cursorEntries;
4193
- if (!single && (after != null || cursorSelected)) {
4194
- if (after != null && distinct) throw drizzleError("'after' cannot be combined with 'distinct'.", { code: "DRIZZLE_INVALID_CURSOR" });
4195
- const cursorObstacle = orderByCursorObstacle(orderBy);
4196
- if (cursorObstacle) {
4197
- if (after != null) throw drizzleError(cursorObstacle, { code: "DRIZZLE_INVALID_CURSOR" });
4198
- } else if (!pkNames?.length) {
4199
- if (after != null) throw drizzleError(`Table ${tableName} has no primary key, so cursor pagination cannot be used on it.`, { code: "DRIZZLE_INVALID_CURSOR" });
4200
- } else cursorEntries = cursorOrderingEntries(orderBy, pkNames);
4201
- }
4202
- const cursorValues = after != null && cursorEntries ? decodeCursor(after, cursorEntries) : void 0;
4203
- const needsDefaultOrder = single || offset != null || opts.limit != null;
4204
- let distinctKeys;
4205
- if (distinct) {
4206
- distinctKeys = await selectDistinctKeys({
4207
- db: opts.db,
4208
- table,
4209
- tableName,
4210
- distinct,
4211
- pkNames: pkNames ?? [],
4212
- where: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0, deleted),
4213
- orderBy,
4214
- limit: single ? 1 : opts.limit,
4215
- offset
4216
- });
4217
- if (!distinctKeys.length) return single ? void 0 : [];
4218
- }
4219
- const params = {
4220
- columns: extractSelectedColumnsFromTree(parsedInfo.fieldsByTypeName[typeName], table, {
4221
- tableName,
4222
- relationMap,
4223
- tables,
4224
- allRelations: filterCtx?.relationMap
4225
- }),
4226
- offset: distinctKeys ? void 0 : offset,
4227
- 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,
4228
- where: distinctKeys ? { RAW: (aliasedTable) => primaryKeyRestriction(aliasedTable, pkNames, distinctKeys) } : where || cursorValues || scope?.has(tableName, deleted) ? { RAW: (aliasedTable) => withScope(scope, tableName, aliasedTable, 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,
4229
- with: relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, {
4230
- typeNameMapper,
4231
- filterCtx,
4232
- limits: opts.limits,
4233
- scope,
4234
- defaultOrderBy: opts.defaultOrderBy,
4235
- resolveName: opts.resolveName
4236
- }) : void 0
4237
- };
4238
- if (single) {
4239
- const result = await queryBase.findFirst(params);
4240
- return result ? remapToGraphQLSingleOutput(result, tableName, table, relationMap) : void 0;
4241
- }
4242
- if (cursorEntries && cursorSelected) for (const [column] of cursorEntries) params.columns[column] = true;
4243
- params.limit = distinctKeys ? void 0 : opts.limit;
4244
- const result = await queryBase.findMany(params);
4245
- if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
4246
- return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
4247
- };
4248
- /**
4249
- * After a mutation, re-fetch the mutated rows through the relational query builder so the
4250
- * selected relations are eagerly loaded in a single query, then merge those relations onto
4251
- * the `.returning()` rows — making the per-field BatchLoader fallback unnecessary.
4252
- *
4253
- * `withParams` is the pre-computed relation selection (from extractRelationsParams); the
4254
- * caller only invokes this when relations are actually selected, so it also gates whether
4255
- * the PK was forced into RETURNING.
4256
- *
4257
- * Falls back to the original `.returning()` rows (relations then resolve via the
4258
- * field-level BatchLoader) when the table has no RQB support, no primary key columns can be
4259
- * determined, or the re-fetch fails. Supports single- and multi-column primary keys.
4260
- */
4261
- const eagerLoadMutationRelations = async (db, tableName, rows, pkNames, withParams) => {
4262
- if (!rows.length || !pkNames.length || !withParams || !Object.keys(withParams).length) return rows;
4263
- const queryBase = db.query?.[tableName];
4264
- if (!queryBase) return rows;
4265
- const keyableRows = rows.filter((row) => pkNames.every((n) => row[n] != null));
4266
- if (!keyableRows.length) return rows;
4267
- const pkColumns = {};
4268
- for (const pk of pkNames) pkColumns[pk] = true;
4269
- const relationNames = Object.keys(withParams);
4270
- const keyOf = (row) => JSON.stringify(pkNames.map((n) => typeof row[n] === "bigint" ? row[n].toString() : row[n]));
4271
- let whereRaw;
4272
- if (pkNames.length === 1) {
4273
- const pkName = pkNames[0];
4274
- const ids = keyableRows.map((r) => r[pkName]);
4275
- whereRaw = (aliased) => inArray(aliased[pkName], ids);
4276
- } else whereRaw = (aliased) => {
4277
- const lhs = sql.join(pkNames.map((n) => sql`${aliased[n]}`), sql`, `);
4278
- const tuples = sql.join(keyableRows.map((row) => sql`(${sql.join(pkNames.map((n) => sql`${row[n]}`), sql`, `)})`), sql`, `);
4279
- return sql`(${lhs}) in (${tuples})`;
4280
- };
4281
- let enriched;
4282
- try {
4283
- enriched = await queryBase.findMany({
4284
- columns: pkColumns,
4285
- where: { RAW: whereRaw },
4286
- with: withParams
4287
- });
4288
- } catch (err) {
4289
- console.warn(`[drizzle-graphql] eager-loading relations for a "${tableName}" mutation failed; falling back to lazy resolution.`, err);
4290
- return rows;
4291
- }
4292
- const byKey = new Map(enriched.map((e) => [keyOf(e), e]));
4293
- for (const row of rows) {
4294
- const match = byKey.get(keyOf(row));
4295
- if (!match) continue;
4296
- for (const rel of relationNames) row[rel] = match[rel];
4297
- }
4298
- return rows;
4299
- };
4300
- //#endregion
4301
4352
  //#region src/util/builders/common/table-inputs.ts
4302
4353
  const orderMap = /* @__PURE__ */ new WeakMap();
4303
4354
  const generateTableOrderCached = (table, innerOrder) => {
@@ -6583,7 +6634,14 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
6583
6634
  if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
6584
6635
  await before();
6585
6636
  const relationCtx = relationFilterCtx(filterCtx, tableName);
6586
- const filters = withScope(scope, tableName, table, single || requireWhere ? extractRequiredFilters(table, tableName, where, relationCtx) : where ? extractFilters(table, tableName, where, relationCtx) : void 0);
6637
+ const filters = scopedWhere({
6638
+ scope,
6639
+ tableName,
6640
+ table,
6641
+ where,
6642
+ relationCtx,
6643
+ required: single || requireWhere
6644
+ });
6587
6645
  if (single) await assertSingleMatch(executor, table, filters);
6588
6646
  let query = executor.update(table).set(input);
6589
6647
  if (filters) query = query.where(filters);
@@ -6665,7 +6723,15 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
6665
6723
  const hard = canHardDelete && args.hard === true;
6666
6724
  const scope = policies?.scope?.(context);
6667
6725
  const relationCtx = relationFilterCtx(filterCtx, tableName);
6668
- const filters = withScope(scope, tableName, table, single || requireWhere ? extractRequiredFilters(table, tableName, where, relationCtx) : where ? extractFilters(table, tableName, where, relationCtx) : void 0, restore ? "ONLY" : hard ? "INCLUDE" : void 0);
6726
+ const filters = scopedWhere({
6727
+ scope,
6728
+ tableName,
6729
+ table,
6730
+ where,
6731
+ relationCtx,
6732
+ required: single || requireWhere,
6733
+ deleted: restore ? "ONLY" : hard ? "INCLUDE" : void 0
6734
+ });
6669
6735
  if (single) await assertSingleMatch(executor, table, filters);
6670
6736
  let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
6671
6737
  if (filters) query = query.where(filters);
@@ -6893,8 +6959,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
6893
6959
  const contextColumns = policies?.contextValues?.(tableName);
6894
6960
  const scope = policies?.scope?.(context);
6895
6961
  const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : args.values, table), contextColumns, context);
6896
- const parsedInfo = parseResolveInfo(info, { deep: true });
6897
- const { columns, hasRelations, withParams } = prepareMutationRelationColumns({
6962
+ const { columns, hasRelations, withParams } = mutationSelection(info, {
6898
6963
  relationMap,
6899
6964
  tables,
6900
6965
  tableName,
@@ -6903,7 +6968,6 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
6903
6968
  resolveName,
6904
6969
  table,
6905
6970
  pkNames,
6906
- parsedInfo,
6907
6971
  limits,
6908
6972
  scope,
6909
6973
  defaultOrderBy: policies?.defaultOrderBy
@@ -6924,7 +6988,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
6924
6988
  context
6925
6989
  }) : await runInsert(executor, input);
6926
6990
  await after(result);
6927
- const enriched = hasRelations ? await eagerLoadMutationRelations(executor, tableName, result, pkNames, withParams) : result;
6991
+ const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
6928
6992
  return remapToGraphQLArrayOutput(enriched, tableName, table, relationMap);
6929
6993
  }
6930
6994
  });
@@ -6948,8 +7012,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
6948
7012
  const contextColumns = policies?.contextValues?.(tableName);
6949
7013
  const scope = policies?.scope?.(context);
6950
7014
  const input = nestedEntry ? {} : applyContextValues(remapFromGraphQLSingleInput(entry ? entry.columns : args.values, table), contextColumns, context);
6951
- const parsedInfo = parseResolveInfo(info, { deep: true });
6952
- const { columns, hasRelations, withParams } = prepareMutationRelationColumns({
7015
+ const { columns, hasRelations, withParams } = mutationSelection(info, {
6953
7016
  relationMap,
6954
7017
  tables,
6955
7018
  tableName,
@@ -6958,7 +7021,6 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
6958
7021
  resolveName,
6959
7022
  table,
6960
7023
  pkNames,
6961
- parsedInfo,
6962
7024
  limits,
6963
7025
  scope,
6964
7026
  defaultOrderBy: policies?.defaultOrderBy
@@ -6983,7 +7045,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
6983
7045
  if (!conflictDoNothing) throw drizzleError("The insert returned no row.", { code: "DRIZZLE_NO_ROW_RETURNED" });
6984
7046
  return;
6985
7047
  }
6986
- const enriched = hasRelations ? await eagerLoadMutationRelations(executor, tableName, result, pkNames, withParams) : result;
7048
+ const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
6987
7049
  return remapToGraphQLSingleOutput(enriched[0], tableName, table, relationMap);
6988
7050
  }
6989
7051
  });
@@ -7021,8 +7083,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7021
7083
  const contextColumns = policies?.contextValues?.(tableName);
7022
7084
  const scope = policies?.scope?.(context);
7023
7085
  const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
7024
- const parsedInfo = parseResolveInfo(info, { deep: true });
7025
- const { columns, hasRelations, withParams } = prepareMutationRelationColumns({
7086
+ const { columns, hasRelations, withParams } = mutationSelection(info, {
7026
7087
  relationMap,
7027
7088
  tables,
7028
7089
  tableName,
@@ -7031,7 +7092,6 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7031
7092
  resolveName,
7032
7093
  table,
7033
7094
  pkNames,
7034
- parsedInfo,
7035
7095
  limits,
7036
7096
  scope,
7037
7097
  defaultOrderBy: policies?.defaultOrderBy
@@ -7068,7 +7128,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7068
7128
  }) : await runUpsert(executor, input);
7069
7129
  await after(result);
7070
7130
  if (single && !result[0]) return;
7071
- const enriched = hasRelations ? await eagerLoadMutationRelations(executor, tableName, result, pkNames, withParams) : result;
7131
+ const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
7072
7132
  return single ? remapToGraphQLSingleOutput(enriched[0], tableName, table, relationMap) : remapToGraphQLArrayOutput(enriched, tableName, table, relationMap);
7073
7133
  }
7074
7134
  });
@@ -7092,8 +7152,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7092
7152
  const { where, set } = args;
7093
7153
  const scope = policies?.scope?.(context);
7094
7154
  await before();
7095
- const parsedInfo = parseResolveInfo(info, { deep: true });
7096
- const { columns, hasRelations, withParams } = prepareMutationRelationColumns({
7155
+ const { columns, hasRelations, withParams } = mutationSelection(info, {
7097
7156
  relationMap,
7098
7157
  tables,
7099
7158
  tableName,
@@ -7102,7 +7161,6 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7102
7161
  resolveName,
7103
7162
  table,
7104
7163
  pkNames,
7105
- parsedInfo,
7106
7164
  limits,
7107
7165
  scope,
7108
7166
  defaultOrderBy: policies?.defaultOrderBy
@@ -7112,7 +7170,14 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7112
7170
  const input = stripContextValues(remapUpdateInput(entry ? entry.columns : set, table, tableName), policies?.contextValues?.(tableName));
7113
7171
  if (!Object.keys(input).length && !nestedOps) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
7114
7172
  const relationCtx = relationFilterCtx(filterCtx, tableName);
7115
- const filters = withScope(scope, tableName, table, single || requireWhere ? extractRequiredFilters(table, tableName, where, relationCtx) : where ? extractFilters(table, tableName, where, relationCtx) : void 0);
7173
+ const filters = scopedWhere({
7174
+ scope,
7175
+ tableName,
7176
+ table,
7177
+ where,
7178
+ relationCtx,
7179
+ required: single || requireWhere
7180
+ });
7116
7181
  if (single) await assertSingleMatch(executor, table, filters);
7117
7182
  const returning = nestedOps ? nested.withJoinColumns(tableName, nestedOps, { ...columns }, table) : columns;
7118
7183
  const runUpdate = async (target, values) => {
@@ -7134,7 +7199,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7134
7199
  await after(result);
7135
7200
  if (single && result.length > 1) throw drizzleError("'where' matched more than one row!", { code: "DRIZZLE_MULTI_ROW_MATCH" });
7136
7201
  if (single && !result[0]) return;
7137
- const enriched = hasRelations ? await eagerLoadMutationRelations(executor, tableName, result, pkNames, withParams) : result;
7202
+ const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
7138
7203
  return single ? remapToGraphQLSingleOutput(enriched[0], tableName, table, relationMap) : remapToGraphQLArrayOutput(enriched, tableName, table, relationMap);
7139
7204
  }
7140
7205
  });
@@ -7176,7 +7241,15 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7176
7241
  const parsedInfo = parseResolveInfo(info, { deep: true });
7177
7242
  const columns = extractSelectedColumnsFromTreeSQLFormat(parsedInfo.fieldsByTypeName[applyObjectTypeName(typeName, tableName, resolveName)], table, selectionCtx);
7178
7243
  const relationCtx = relationFilterCtx(filterCtx, tableName);
7179
- const filters = withScope(scope, tableName, table, single || requireWhere ? extractRequiredFilters(table, tableName, where, relationCtx) : where ? extractFilters(table, tableName, where, relationCtx) : void 0, restore ? "ONLY" : hard ? "INCLUDE" : void 0);
7244
+ const filters = scopedWhere({
7245
+ scope,
7246
+ tableName,
7247
+ table,
7248
+ where,
7249
+ relationCtx,
7250
+ required: single || requireWhere,
7251
+ deleted: restore ? "ONLY" : hard ? "INCLUDE" : void 0
7252
+ });
7180
7253
  if (single) await assertSingleMatch(executor, table, filters);
7181
7254
  let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
7182
7255
  if (filters) query = query.where(filters);
@@ -7505,8 +7578,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7505
7578
  await before();
7506
7579
  const scope = policies?.scope?.(context);
7507
7580
  const contextColumns = policies?.contextValues?.(tableName);
7508
- const parsedInfo = parseResolveInfo(info, { deep: true });
7509
- const { columns, hasRelations, withParams } = prepareMutationRelationColumns({
7581
+ const { columns, hasRelations, withParams } = mutationSelection(info, {
7510
7582
  relationMap,
7511
7583
  tables,
7512
7584
  tableName,
@@ -7515,7 +7587,6 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7515
7587
  resolveName,
7516
7588
  table,
7517
7589
  pkNames,
7518
- parsedInfo,
7519
7590
  limits,
7520
7591
  scope,
7521
7592
  defaultOrderBy: policies?.defaultOrderBy
@@ -7544,7 +7615,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7544
7615
  });
7545
7616
  const flatRows = perEntry.flat();
7546
7617
  await after(flatRows);
7547
- const enriched = hasRelations ? await eagerLoadMutationRelations(executor, tableName, flatRows, pkNames, withParams) : flatRows;
7618
+ const enriched = await withEagerRelations(executor, tableName, flatRows, pkNames, withParams, hasRelations);
7548
7619
  const output = [];
7549
7620
  let offset = 0;
7550
7621
  for (const rows of perEntry) {