@vantreeseba/drizzle-graphql 11.0.1 → 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.cjs CHANGED
@@ -544,11 +544,12 @@ const isJsonColumn = (column) => (column.dataType ?? "").includes("json") && col
544
544
  * StringArray, …) so arrays with different element types never share
545
545
  * one filter input; membership operators instead of scalar comparisons
546
546
  * - "Id" → uuid-typed columns (no string pattern operators)
547
- * - "Boolean" → boolean columns
547
+ * - "Boolean" → boolean columns (equality and membership only — no ordering, no patterns)
548
548
  * - "BigInt" → bigint columns (BigInt-scalar-typed operators, no string pattern operators)
549
549
  * - "Decimal" → numeric/decimal columns (Decimal-scalar-typed operators, no string pattern operators)
550
- * - the enum GraphQL type name → enum columns (still unique per enum)
551
- * - "DateTime" → timestamp and date columns
550
+ * - the enum GraphQL type name → enum columns (still unique per enum; equality and
551
+ * membership only, like Boolean)
552
+ * - "DateTime" → timestamp and date columns (ordering kept, no string pattern operators)
552
553
  * - "Int" → integer/serial columns (no string pattern operators)
553
554
  * - "Float" → real/double columns (no string pattern operators)
554
555
  * - "String" → all other text/varchar columns
@@ -576,7 +577,8 @@ const resolveGenericFilterDescriptor = (column, columnGraphQLType) => {
576
577
  };
577
578
  if (columnGraphQLType.type instanceof graphql.GraphQLEnumType) return {
578
579
  name: columnGraphQLType.type.name,
579
- kind: "scalar"
580
+ kind: "scalar",
581
+ isEnum: true
580
582
  };
581
583
  if (columnGraphQLType.type === GraphQLBigIntString) return {
582
584
  name: "BigInt",
@@ -800,18 +802,32 @@ const arrayFilterFields = (colType, colArr) => {
800
802
  };
801
803
  /**
802
804
  * Filters whose fields omit the string pattern operators (like/notLike/ilike/notIlike/
803
- * startsWith/contains/…): they are nonsensical on opaque uuids and invalid SQL on numeric
804
- * columns. Keyed on the generic filter name (i.e. on column type) — a string-typed column
805
- * keeps the string operators whatever it is called.
805
+ * startsWith/contains/… and the `insensitive` flag that modifies them): they are nonsensical
806
+ * on opaque uuids, booleans and enum members, invalid SQL on numeric columns, and on a
807
+ * timestamp they match whatever the session happens to render the value as rather than
808
+ * anything in the query. Keyed on the generic filter name (i.e. on column type) — a
809
+ * string-typed column keeps the string operators whatever it is called. Enum filters are
810
+ * named after their enum and are recognised by `isEnum` instead.
806
811
  */
807
812
  const FILTERS_WITHOUT_STRING_OPS = /* @__PURE__ */ new Set([
808
813
  "Id",
809
814
  "Int",
810
815
  "Float",
811
816
  "BigInt",
812
- "Decimal"
817
+ "Decimal",
818
+ "Boolean",
819
+ "Date",
820
+ "DateTime"
813
821
  ]);
814
822
  /**
823
+ * Filters that additionally omit the ordering operators (lt/lte/gt/gte). Ordering two
824
+ * booleans is `eq` spelled as a puzzle (`gt: false` is `eq: true`), and ordering enum members
825
+ * compares their declaration order — an artefact of how the column was written rather than a
826
+ * fact about the domain. Timestamps and the numerics keep them: ranges are the point of
827
+ * having them. Enum filters are recognised by `isEnum`, as above.
828
+ */
829
+ const FILTERS_WITHOUT_ORDERING_OPS = /* @__PURE__ */ new Set(["Boolean"]);
830
+ /**
815
831
  * Scalars the built-in detection itself can produce. A scalar override to one of these
816
832
  * shares the built-in filter type for that scalar, so its shape must stay exactly what a
817
833
  * natural column of the scalar gets — whichever column builds the cached type first.
@@ -854,14 +870,15 @@ const resolveOverrideFilterDescriptor = (scalar) => {
854
870
  const generateColumnFilterValues = (column, tableName, columnName, cacheCtx) => {
855
871
  const columnGraphQLType = drizzleColumnToGraphQLType(column, columnName, tableName, true, false, true);
856
872
  const inputOverride = getColumnScalarOverride(column, true);
857
- const { name: genericName, kind } = inputOverride ? resolveOverrideFilterDescriptor(inputOverride) : resolveGenericFilterDescriptor(column, columnGraphQLType);
873
+ const { name: genericName, kind, isEnum } = inputOverride ? resolveOverrideFilterDescriptor(inputOverride) : resolveGenericFilterDescriptor(column, columnGraphQLType);
858
874
  const cached = cacheCtx.genericFilterCache.get(genericName);
859
875
  if (cached) return cached;
860
876
  const colType = columnGraphQLType.type;
861
877
  const colArr = new graphql.GraphQLList(new graphql.GraphQLNonNull(colType));
862
878
  const customOverrideFilter = inputOverride !== void 0 && !BUILTIN_FILTER_SCALARS.has(inputOverride);
863
879
  const underlying = (0, drizzle_orm.extractExtendedColumnType)(column);
864
- const omitStringOps = customOverrideFilter ? underlying.type !== "string" || underlying.constraint === "numeric" : FILTERS_WITHOUT_STRING_OPS.has(genericName);
880
+ const omitStringOps = customOverrideFilter ? underlying.type !== "string" || underlying.constraint === "numeric" : isEnum || FILTERS_WITHOUT_STRING_OPS.has(genericName);
881
+ const omitOrderingOps = !customOverrideFilter && (isEnum || FILTERS_WITHOUT_ORDERING_OPS.has(genericName));
865
882
  const baseFields = kind === "json" ? jsonFilterFields(column, colType, cacheCtx.typeName) : kind === "array" ? arrayFilterFields(colType, colArr) : {
866
883
  eq: {
867
884
  type: colType,
@@ -871,21 +888,23 @@ const generateColumnFilterValues = (column, tableName, columnName, cacheCtx) =>
871
888
  type: colType,
872
889
  description: "Not equal to"
873
890
  },
874
- lt: {
875
- type: colType,
876
- description: "Less than"
877
- },
878
- lte: {
879
- type: colType,
880
- description: "Less than or equal to"
881
- },
882
- gt: {
883
- type: colType,
884
- description: "Greater than"
885
- },
886
- gte: {
887
- type: colType,
888
- description: "Greater than or equal to"
891
+ ...omitOrderingOps ? {} : {
892
+ lt: {
893
+ type: colType,
894
+ description: "Less than"
895
+ },
896
+ lte: {
897
+ type: colType,
898
+ description: "Less than or equal to"
899
+ },
900
+ gt: {
901
+ type: colType,
902
+ description: "Greater than"
903
+ },
904
+ gte: {
905
+ type: colType,
906
+ description: "Greater than or equal to"
907
+ }
889
908
  },
890
909
  ...omitStringOps ? {} : {
891
910
  like: { type: graphql.GraphQLString },
@@ -2907,6 +2926,154 @@ const listFieldComplexity = (options, policy) => ({ args, childComplexity }) =>
2907
2926
  */
2908
2927
  const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
2909
2928
  //#endregion
2929
+ //#region src/util/parse-resolve-info.ts
2930
+ /** Unwraps `[T]!`-style wrappers down to the named type. `getNamedType` without the realm tie. */
2931
+ const namedTypeOf = (type) => {
2932
+ let current = type;
2933
+ while (current?.ofType) current = current.ofType;
2934
+ return current;
2935
+ };
2936
+ /**
2937
+ * Whether a field's named output type has sub-selections. Object types and interfaces expose
2938
+ * `getFields`, unions expose `getTypes`; scalars and enums expose neither. Input object types
2939
+ * also expose `getFields`, but an input type can never be a field's output type, so there is
2940
+ * nothing here for that to collide with.
2941
+ */
2942
+ const isCompositeLike = (type) => !!type && (typeof type.getFields === "function" || typeof type.getTypes === "function");
2943
+ /**
2944
+ * The fields declared on a type, or `undefined` when it has none to declare. A union is the
2945
+ * case that matters: it has no fields of its own, so a selection directly against one resolves
2946
+ * to nothing and is dropped — only the inline fragments underneath contribute.
2947
+ */
2948
+ const fieldsOf = (type) => typeof type?.getFields === "function" ? type.getFields() : void 0;
2949
+ /**
2950
+ * The runtime variable values, in the plain-map form the directive check wants. graphql 17
2951
+ * wraps them in `{ sources, coerced }`; graphql 16 hands over the map itself.
2952
+ */
2953
+ const coercedVariables = (info) => {
2954
+ const values = info.variableValues;
2955
+ if (values && typeof values === "object" && "coerced" in values) return values.coerced ?? {};
2956
+ return values ?? {};
2957
+ };
2958
+ /** The value of a `@skip` / `@include` `if:` argument — a literal or a variable reference. */
2959
+ const directiveIfValue = (info, value) => {
2960
+ if (value.kind === graphql.Kind.VARIABLE) return coercedVariables(info)[value.name.value];
2961
+ if (value.kind === graphql.Kind.BOOLEAN) return value.value;
2962
+ };
2963
+ /**
2964
+ * Whether `@skip` / `@include` on this selection exclude it. Applies to fields, fragment
2965
+ * spreads and inline fragments alike, which is where graphql puts them.
2966
+ */
2967
+ const isExcluded = (info, node) => {
2968
+ for (const directive of node.directives ?? []) {
2969
+ const directiveName = directive.name.value;
2970
+ if (directiveName !== "skip" && directiveName !== "include") continue;
2971
+ const ifArgument = directive.arguments?.find((argument) => argument.name.value === "if");
2972
+ if (!ifArgument) continue;
2973
+ const value = directiveIfValue(info, ifArgument.value);
2974
+ if (directiveName === "skip" ? !!value : !value) return true;
2975
+ }
2976
+ return false;
2977
+ };
2978
+ /** The type a fragment's `on Type` condition names, looked up in the executing schema. */
2979
+ const typeFromCondition = (info, condition) => info.schema.getType(condition.name.value);
2980
+ /**
2981
+ * Introspection meta-fields are not schema fields and never reach the database. `__typename` is
2982
+ * the one that turns up in practice; `__id` is excluded from the rule because a schema is free
2983
+ * to declare a field by that name.
2984
+ */
2985
+ const isMetaField = (name) => name.startsWith("__") && name !== "__id";
2986
+ /**
2987
+ * Merges one selection set into `tree`, which is a `fieldsByTypeName` map — the caller owns the
2988
+ * level, this owns the entries under `parentType`'s name.
2989
+ *
2990
+ * `activeFragments` is the set of fragment names on the current spread path. A document that
2991
+ * passed validation cannot contain a fragment cycle, but this parser also runs on trees a
2992
+ * caller assembled by hand, and an unbounded walk would hang rather than fail.
2993
+ */
2994
+ const collectSelections = (selections, info, tree, parentType, deep, activeFragments) => {
2995
+ const parentTypeName = parentType?.name;
2996
+ if (!parentTypeName) return tree;
2997
+ let fields = tree[parentTypeName];
2998
+ if (!fields) {
2999
+ fields = {};
3000
+ tree[parentTypeName] = fields;
3001
+ }
3002
+ const parentFields = fieldsOf(parentType);
3003
+ for (const selection of selections) {
3004
+ if (isExcluded(info, selection)) continue;
3005
+ if (selection.kind === graphql.Kind.FIELD) {
3006
+ const name = selection.name.value;
3007
+ if (isMetaField(name)) continue;
3008
+ const fieldDef = parentFields?.[name];
3009
+ if (!fieldDef) continue;
3010
+ const fieldType = namedTypeOf(fieldDef.type);
3011
+ if (!fieldType) continue;
3012
+ const composite = isCompositeLike(fieldType);
3013
+ const alias = selection.alias?.value || name;
3014
+ let node = fields[alias];
3015
+ if (!node) {
3016
+ node = {
3017
+ name,
3018
+ alias,
3019
+ args: { ...(0, graphql.getArgumentValues)(fieldDef, selection, info.variableValues) },
3020
+ fieldsByTypeName: composite && fieldType.name ? { [fieldType.name]: {} } : {}
3021
+ };
3022
+ fields[alias] = node;
3023
+ }
3024
+ if (selection.selectionSet && deep && composite) collectSelections(selection.selectionSet.selections, info, node.fieldsByTypeName, fieldType, deep, activeFragments);
3025
+ continue;
3026
+ }
3027
+ if (!deep) continue;
3028
+ if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
3029
+ const fragmentName = selection.name.value;
3030
+ const fragment = info.fragments?.[fragmentName];
3031
+ if (!fragment) throw new Error(`Drizzle-GraphQL Error: unknown fragment '${fragmentName}' — it is spread in the query but not defined in the document.`);
3032
+ if (activeFragments.has(fragmentName)) continue;
3033
+ const fragmentType = fragment.typeCondition ? typeFromCondition(info, fragment.typeCondition) : parentType;
3034
+ if (!isCompositeLike(fragmentType)) continue;
3035
+ activeFragments.add(fragmentName);
3036
+ try {
3037
+ collectSelections(fragment.selectionSet.selections, info, tree, fragmentType, deep, activeFragments);
3038
+ } finally {
3039
+ activeFragments.delete(fragmentName);
3040
+ }
3041
+ continue;
3042
+ }
3043
+ if (selection.kind === graphql.Kind.INLINE_FRAGMENT) {
3044
+ const fragmentType = selection.typeCondition ? typeFromCondition(info, selection.typeCondition) : parentType;
3045
+ if (!isCompositeLike(fragmentType)) continue;
3046
+ collectSelections(selection.selectionSet.selections, info, tree, fragmentType, deep, activeFragments);
3047
+ }
3048
+ }
3049
+ return tree;
3050
+ };
3051
+ /** The first key of an object, without materializing the whole key array to discard it. */
3052
+ const firstKey = (object) => {
3053
+ for (const key in object) return key;
3054
+ };
3055
+ /**
3056
+ * Reads the selection under a resolver's own field out of its `info`, as a tree of
3057
+ * {@link ResolveTree} nodes.
3058
+ *
3059
+ * ```ts
3060
+ * const parsed = parseResolveInfo(info, { deep: true })!;
3061
+ * const selected = parsed.fieldsByTypeName['Users']; // keyed by response key
3062
+ * ```
3063
+ *
3064
+ * Returns `undefined` when the resolver's field produced no node at all, which in practice
3065
+ * means `info` carried no field nodes.
3066
+ */
3067
+ const parseResolveInfo = (info, options = {}) => {
3068
+ const fieldNodes = info.fieldNodes;
3069
+ if (!fieldNodes) throw new Error("Drizzle-GraphQL Error: resolve info carries no fieldNodes, so no selection could be read from it.");
3070
+ const tree = collectSelections(fieldNodes, info, {}, info.parentType, options.deep ?? true, /* @__PURE__ */ new Set());
3071
+ const typeKey = firstKey(tree);
3072
+ const fields = typeKey === void 0 ? void 0 : tree[typeKey];
3073
+ const fieldKey = fields && firstKey(fields);
3074
+ return fieldKey === void 0 ? void 0 : fields?.[fieldKey];
3075
+ };
3076
+ //#endregion
2910
3077
  //#region src/util/builders/field-updates.ts
2911
3078
  /**
2912
3079
  * The scalars arithmetic is offered on. Decimal and BigInt transport as strings but are
@@ -3239,6 +3406,127 @@ const pruneNonEagerRelations = (relationMap, shouldEagerLoad) => {
3239
3406
  for (const [tableName, rels] of Object.entries(relationMap)) out[tableName] = Object.fromEntries(Object.entries(rels).filter(([relationName]) => shouldEagerLoad(tableName, relationName)));
3240
3407
  return out;
3241
3408
  };
3409
+ //#endregion
3410
+ //#region src/util/builders/common/select-runtime.ts
3411
+ /**
3412
+ * Runs the relational-query-builder select shared by every dialect's `generateSelect*`
3413
+ * resolver: selected columns + offset/limit + aliased orderBy/where callbacks + the eager
3414
+ * `with:` relation params, then remaps the result. `single` switches between
3415
+ * findFirst/findMany (and the single path omits `limit`). The PG fallback for tables
3416
+ * without RQB support stays in pg.ts; this covers the common RQB path for all three.
3417
+ */
3418
+ const runRelationalSelect = async (opts) => {
3419
+ const { queryBase, tables, tableName, table, relationMap, typeName, typeNameMapper, parsedInfo, offset, orderBy, where, single, filterCtx, pkNames, after, scope, deleted } = opts;
3420
+ const distinct = opts.distinct?.length ? opts.distinct : void 0;
3421
+ const cursorSelected = !single && isCursorFieldSelected(parsedInfo.fieldsByTypeName[typeName], table);
3422
+ let cursorEntries;
3423
+ if (!single && (after != null || cursorSelected)) {
3424
+ if (after != null && distinct) throw drizzleError("'after' cannot be combined with 'distinct'.", { code: "DRIZZLE_INVALID_CURSOR" });
3425
+ const cursorObstacle = orderByCursorObstacle(orderBy);
3426
+ if (cursorObstacle) {
3427
+ if (after != null) throw drizzleError(cursorObstacle, { code: "DRIZZLE_INVALID_CURSOR" });
3428
+ } else if (!pkNames?.length) {
3429
+ if (after != null) throw drizzleError(`Table ${tableName} has no primary key, so cursor pagination cannot be used on it.`, { code: "DRIZZLE_INVALID_CURSOR" });
3430
+ } else cursorEntries = cursorOrderingEntries(orderBy, pkNames);
3431
+ }
3432
+ const cursorValues = after != null && cursorEntries ? decodeCursor(after, cursorEntries) : void 0;
3433
+ const needsDefaultOrder = single || offset != null || opts.limit != null;
3434
+ let distinctKeys;
3435
+ if (distinct) {
3436
+ distinctKeys = await selectDistinctKeys({
3437
+ db: opts.db,
3438
+ table,
3439
+ tableName,
3440
+ distinct,
3441
+ pkNames: pkNames ?? [],
3442
+ where: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0, deleted),
3443
+ orderBy,
3444
+ limit: single ? 1 : opts.limit,
3445
+ offset
3446
+ });
3447
+ if (!distinctKeys.length) return single ? void 0 : [];
3448
+ }
3449
+ const params = {
3450
+ columns: extractSelectedColumnsFromTree(parsedInfo.fieldsByTypeName[typeName], table, {
3451
+ tableName,
3452
+ relationMap,
3453
+ tables,
3454
+ allRelations: filterCtx?.relationMap
3455
+ }),
3456
+ offset: distinctKeys ? void 0 : offset,
3457
+ 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,
3458
+ 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,
3459
+ with: relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, {
3460
+ typeNameMapper,
3461
+ filterCtx,
3462
+ limits: opts.limits,
3463
+ scope,
3464
+ defaultOrderBy: opts.defaultOrderBy,
3465
+ resolveName: opts.resolveName
3466
+ }) : void 0
3467
+ };
3468
+ if (single) {
3469
+ const result = await queryBase.findFirst(params);
3470
+ return result ? remapToGraphQLSingleOutput(result, tableName, table, relationMap) : void 0;
3471
+ }
3472
+ if (cursorEntries && cursorSelected) for (const [column] of cursorEntries) params.columns[column] = true;
3473
+ params.limit = distinctKeys ? void 0 : opts.limit;
3474
+ const result = await queryBase.findMany(params);
3475
+ if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
3476
+ return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
3477
+ };
3478
+ /**
3479
+ * After a mutation, re-fetch the mutated rows through the relational query builder so the
3480
+ * selected relations are eagerly loaded in a single query, then merge those relations onto
3481
+ * the `.returning()` rows — making the per-field BatchLoader fallback unnecessary.
3482
+ *
3483
+ * `withParams` is the pre-computed relation selection (from extractRelationsParams); the
3484
+ * caller only invokes this when relations are actually selected, so it also gates whether
3485
+ * the PK was forced into RETURNING.
3486
+ *
3487
+ * Falls back to the original `.returning()` rows (relations then resolve via the
3488
+ * field-level BatchLoader) when the table has no RQB support, no primary key columns can be
3489
+ * determined, or the re-fetch fails. Supports single- and multi-column primary keys.
3490
+ */
3491
+ const eagerLoadMutationRelations = async (db, tableName, rows, pkNames, withParams) => {
3492
+ if (!rows.length || !pkNames.length || !withParams || !Object.keys(withParams).length) return rows;
3493
+ const queryBase = db.query?.[tableName];
3494
+ if (!queryBase) return rows;
3495
+ const keyableRows = rows.filter((row) => pkNames.every((n) => row[n] != null));
3496
+ if (!keyableRows.length) return rows;
3497
+ const pkColumns = {};
3498
+ for (const pk of pkNames) pkColumns[pk] = true;
3499
+ const relationNames = Object.keys(withParams);
3500
+ const keyOf = (row) => JSON.stringify(pkNames.map((n) => typeof row[n] === "bigint" ? row[n].toString() : row[n]));
3501
+ let whereRaw;
3502
+ if (pkNames.length === 1) {
3503
+ const pkName = pkNames[0];
3504
+ const ids = keyableRows.map((r) => r[pkName]);
3505
+ whereRaw = (aliased) => (0, drizzle_orm.inArray)(aliased[pkName], ids);
3506
+ } else whereRaw = (aliased) => {
3507
+ const lhs = drizzle_orm.sql.join(pkNames.map((n) => drizzle_orm.sql`${aliased[n]}`), drizzle_orm.sql`, `);
3508
+ const tuples = drizzle_orm.sql.join(keyableRows.map((row) => drizzle_orm.sql`(${drizzle_orm.sql.join(pkNames.map((n) => drizzle_orm.sql`${row[n]}`), drizzle_orm.sql`, `)})`), drizzle_orm.sql`, `);
3509
+ return drizzle_orm.sql`(${lhs}) in (${tuples})`;
3510
+ };
3511
+ let enriched;
3512
+ try {
3513
+ enriched = await queryBase.findMany({
3514
+ columns: pkColumns,
3515
+ where: { RAW: whereRaw },
3516
+ with: withParams
3517
+ });
3518
+ } catch (err) {
3519
+ console.warn(`[drizzle-graphql] eager-loading relations for a "${tableName}" mutation failed; falling back to lazy resolution.`, err);
3520
+ return rows;
3521
+ }
3522
+ const byKey = new Map(enriched.map((e) => [keyOf(e), e]));
3523
+ for (const row of rows) {
3524
+ const match = byKey.get(keyOf(row));
3525
+ if (!match) continue;
3526
+ for (const rel of relationNames) row[rel] = match[rel];
3527
+ }
3528
+ return rows;
3529
+ };
3242
3530
  const createMutationTxCtx = (options) => options ? {
3243
3531
  timeoutMs: options.timeoutMs,
3244
3532
  fieldNames: /* @__PURE__ */ new Set(),
@@ -3417,12 +3705,6 @@ const hardDeleteArg = {
3417
3705
  defaultValue: false,
3418
3706
  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."
3419
3707
  };
3420
- /**
3421
- * Computes the RETURNING columns and relation selection for a mutation resolver: extracts
3422
- * the selected scalar columns, determines whether any relations were selected, and only
3423
- * then forces the primary key into the column set (so the post-mutation eager-load can
3424
- * re-key rows). Returns everything the resolver needs to decide whether to eager-load.
3425
- */
3426
3708
  const prepareMutationRelationColumns = (params) => {
3427
3709
  const { relationMap, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
3428
3710
  const typeName = resolveObjectTypeName(tableName, typeNameMapper, resolveName);
@@ -3446,6 +3728,38 @@ const prepareMutationRelationColumns = (params) => {
3446
3728
  };
3447
3729
  };
3448
3730
  /**
3731
+ * The prologue every write resolver runs: parse the resolve info, then work out the columns
3732
+ * to return and the relations to eager-load from it. `parsedInfo` comes back too — the delete
3733
+ * resolver reads its own columns off the tree rather than taking the prepared set.
3734
+ */
3735
+ const mutationSelection = (info, params) => {
3736
+ const parsedInfo = parseResolveInfo(info, { deep: true });
3737
+ return {
3738
+ parsedInfo,
3739
+ ...prepareMutationRelationColumns({
3740
+ ...params,
3741
+ parsedInfo
3742
+ })
3743
+ };
3744
+ };
3745
+ /**
3746
+ * The `where` a write runs under: the caller's filters — required when the mutation refuses to
3747
+ * run unbounded — with the row scope ANDed on last, so a supplied filter can only narrow the
3748
+ * scope and never widen it. An out-of-scope row is simply not matched.
3749
+ *
3750
+ * `deleted` states which soft-delete mode the match reads at, for the writes that need one:
3751
+ * `restore` sees only marked rows, a hard delete has to see them too.
3752
+ */
3753
+ const scopedWhere = (params) => {
3754
+ const { scope, tableName, table, where, relationCtx, required, deleted } = params;
3755
+ return withScope(scope, tableName, table, required ? extractRequiredFilters(table, tableName, where, relationCtx) : where ? extractFilters(table, tableName, where, relationCtx) : void 0, deleted);
3756
+ };
3757
+ /**
3758
+ * The epilogue: the written rows with their selected relations attached, or the rows as they
3759
+ * came back when the selection asked for no relation at all.
3760
+ */
3761
+ const withEagerRelations = (executor, tableName, rows, pkNames, withParams, hasRelations) => hasRelations ? eagerLoadMutationRelations(executor, tableName, rows, pkNames, withParams) : rows;
3762
+ /**
3449
3763
  * The per-entry input of `update<Table>Many`: `{ where, set }`, reusing the table's
3450
3764
  * update `set` input and filter input. Shared by all three dialect builders.
3451
3765
  */
@@ -3545,8 +3859,14 @@ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, f
3545
3859
  try {
3546
3860
  return await runMutation(db, context, info, txCtx, async (executor) => {
3547
3861
  const { where, set } = args;
3548
- const relationCtx = relationFilterCtx(filterCtx, tableName);
3549
- const filters = requireWhere ? extractRequiredFilters(table, tableName, where, relationCtx) : where ? extractFilters(table, tableName, where, relationCtx) : void 0;
3862
+ const filters = scopedWhere({
3863
+ scope: void 0,
3864
+ tableName,
3865
+ table,
3866
+ where,
3867
+ relationCtx: relationFilterCtx(filterCtx, tableName),
3868
+ required: requireWhere
3869
+ });
3550
3870
  let query;
3551
3871
  if (kind === "delete") query = executor.delete(table);
3552
3872
  else {
@@ -3804,8 +4124,8 @@ const identifyRows = (field, args) => {
3804
4124
  * Window functions require PostgreSQL, MySQL >= 8.0, or SQLite >= 3.25.
3805
4125
  * Returns raw rows (NOT remapped); the caller groups + remaps them.
3806
4126
  */
3807
- const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereCondition, orderByArg, limit, offset, pkNames, orderCtx, whereArgs) => {
3808
- const cols = (0, drizzle_orm.getColumns)(targetTable);
4127
+ const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereCondition, orderByArg, limit, offset, pkNames, columns, orderCtx, whereArgs) => {
4128
+ const cols = columns ?? (0, drizzle_orm.getColumns)(targetTable);
3809
4129
  const orderExprs = [...orderByArg ? extractOrderBy(targetTable, orderByArg, orderCtx, whereArgs) : [], ...primaryKeyOrderExprs(targetTable, pkNames)];
3810
4130
  const orderClause = orderExprs.length ? drizzle_orm.sql` order by ${drizzle_orm.sql.join(orderExprs, drizzle_orm.sql`, `)}` : drizzle_orm.sql``;
3811
4131
  const RN = "__drizzle_graphql_rn";
@@ -3851,12 +4171,39 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
3851
4171
  operation: "relation",
3852
4172
  relation: relationName
3853
4173
  };
4174
+ /**
4175
+ * The columns one batch reads: the ones the selection names, plus the ones the batch needs
4176
+ * whatever the client asked for — the key it groups the rows by, and the ordering columns a
4177
+ * row cursor is encoded from. Nested relation fields contribute their own join column
4178
+ * through the selection context, the same way the root select path narrows.
4179
+ *
4180
+ * `undefined` means "every column", which is what a resolver with no readable resolve info
4181
+ * has to fall back to.
4182
+ */
4183
+ const selectedColumns = (info, cursorEntries) => {
4184
+ const parsed = info ? parseResolveInfo(info) : void 0;
4185
+ if (!parsed) return;
4186
+ const tree = Object.assign({}, ...Object.values(parsed.fieldsByTypeName));
4187
+ const columns = extractSelectedColumnsFromTreeSQLFormat(tree, targetTable, {
4188
+ tableName: targetTableName,
4189
+ relationMap: filterCtx?.relationMap ?? {},
4190
+ tables,
4191
+ allRelations: filterCtx?.relationMap
4192
+ });
4193
+ const targetColumns = (0, drizzle_orm.getColumns)(targetTable);
4194
+ columns[foreignColPropName] = foreignCol;
4195
+ for (const [columnName] of cursorEntries ?? []) {
4196
+ const column = targetColumns[columnName];
4197
+ if (column) columns[columnName] = column;
4198
+ }
4199
+ return columns;
4200
+ };
3854
4201
  const resolve = async (parent, args, context, info) => {
3855
4202
  if (parent[relationName] !== void 0) return parent[relationName];
3856
4203
  const localValue = parent[localColPropName];
3857
4204
  if (localValue == null) return isOne ? null : [];
3858
4205
  const { where: whereArg, limit: requestedLimit, offset, after, deleted } = args ?? {};
3859
- const { orderByArg, limit, distinct, cursorEntries, cursorValues, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `relation:${tableName}::${relationName}`, () => {
4206
+ const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `relation:${tableName}::${relationName}`, () => {
3860
4207
  const orderByArg = isOne ? args?.orderBy : withDefaultOrderBy(args ?? {}, targetTableName, policies?.defaultOrderBy).orderBy;
3861
4208
  const limit = applyLimitPolicy(requestedLimit, limitPolicy, errorCtx);
3862
4209
  const distinct = (args ?? {}).distinct?.length ? (args ?? {}).distinct : void 0;
@@ -3871,6 +4218,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
3871
4218
  } else cursorEntries = cursorOrderingEntries(orderByArg, targetPkNames);
3872
4219
  }
3873
4220
  const cursorValues = after != null && cursorEntries ? decodeCursor(after, cursorEntries) : void 0;
4221
+ const columns = selectedColumns(info, cursorEntries);
3874
4222
  const argsKey = JSON.stringify({
3875
4223
  where: whereArg ?? null,
3876
4224
  orderBy: orderByArg ?? null,
@@ -3879,7 +4227,8 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
3879
4227
  deleted: deleted ?? defaultDeleted ?? null,
3880
4228
  after: after ?? null,
3881
4229
  distinct: distinct ?? null,
3882
- cursor: cursorEntries ? 1 : 0
4230
+ cursor: cursorEntries ? 1 : 0,
4231
+ columns: columns ? Object.keys(columns).sort() : null
3883
4232
  });
3884
4233
  return {
3885
4234
  orderByArg,
@@ -3887,6 +4236,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
3887
4236
  distinct,
3888
4237
  cursorEntries,
3889
4238
  cursorValues,
4239
+ columns,
3890
4240
  loaderKey: `${tableName}::${relationName}::${argsKey}`
3891
4241
  };
3892
4242
  });
@@ -3910,9 +4260,9 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
3910
4260
  whereCondition = (0, drizzle_orm.and)(whereCondition, primaryKeyRestriction(targetTable, targetPkNames, keys));
3911
4261
  }
3912
4262
  let rows;
3913
- if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, relationFilterCtx(filterCtx, targetTableName), whereArg);
4263
+ if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, columns, relationFilterCtx(filterCtx, targetTableName), whereArg);
3914
4264
  else {
3915
- let q = executor.select().from(targetTable).where(whereCondition);
4265
+ let q = (columns ? executor.select(columns) : executor.select()).from(targetTable).where(whereCondition);
3916
4266
  const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName), whereArg) : [];
3917
4267
  if (orderExprs.length) q = q.orderBy(...orderExprs);
3918
4268
  rows = await q;
@@ -3983,165 +4333,44 @@ const computeResolverFieldNames = (tableName, typeNameMapper, prefixes, suffixes
3983
4333
  const restoreSingleFieldName = `${restoreBase}${writeSingleSuffix}`;
3984
4334
  return {
3985
4335
  typeName,
3986
- listFieldName,
3987
- singleFieldName,
3988
- aggregateFieldName,
3989
- groupByFieldName,
3990
- createArrayFieldName,
3991
- createSingleFieldName,
3992
- upsertArrayFieldName,
3993
- upsertSingleFieldName,
3994
- updateFieldName,
3995
- updateManyFieldName,
3996
- updateSingleFieldName,
3997
- updateCountFieldName: `${prefixes.update}${pluralNoun}Count`,
3998
- deleteFieldName,
3999
- deleteSingleFieldName,
4000
- restoreFieldName,
4001
- restoreSingleFieldName,
4002
- deleteCountFieldName: `${prefixes.delete}${pluralNoun}Count`
4003
- };
4004
- };
4005
- //#endregion
4006
- //#region src/util/builders/common/root-fields.ts
4007
- /**
4008
- * Registers one generated root field, refusing to overwrite a name already taken.
4009
- *
4010
- * A generated name is the table name run through `typeNameMapper`, a prefix and a suffix,
4011
- * and the pluralization each operation applies — so whether two operations collide is a
4012
- * property of the finished names, not of any one piece of the config. `buildSchema` checks
4013
- * what it can up front (identical list and single suffixes with no mapper), but that check
4014
- * compares the suffix *strings*, which cannot see a collision the rest of the naming rules
4015
- * produce: `{ list: 'All', single: '' }` used to give both insert mutations the name
4016
- * `createUsers`, and the array insert simply disappeared from the schema.
4017
- *
4018
- * Assigning into the record silently kept the last registration. Throwing here means a
4019
- * naming config that cannot work says so at build time, naming the field it lost.
4020
- */
4021
- const defineRootField = (fields, kind, name, config) => {
4022
- if (Object.getOwnPropertyDescriptor(fields, name) !== void 0) throw new Error(`Drizzle-GraphQL Error: two generated ${kind} fields are both named '${name}'. Generated names come from the table name plus config.prefixes, config.suffixes and config.typeNameMapper — some combination of those gives two operations the same name. Adjust one of them so each operation gets its own.`);
4023
- fields[name] = config;
4024
- };
4025
- //#endregion
4026
- //#region src/util/builders/common/select-runtime.ts
4027
- /**
4028
- * Runs the relational-query-builder select shared by every dialect's `generateSelect*`
4029
- * resolver: selected columns + offset/limit + aliased orderBy/where callbacks + the eager
4030
- * `with:` relation params, then remaps the result. `single` switches between
4031
- * findFirst/findMany (and the single path omits `limit`). The PG fallback for tables
4032
- * without RQB support stays in pg.ts; this covers the common RQB path for all three.
4033
- */
4034
- const runRelationalSelect = async (opts) => {
4035
- const { queryBase, tables, tableName, table, relationMap, typeName, typeNameMapper, parsedInfo, offset, orderBy, where, single, filterCtx, pkNames, after, scope, deleted } = opts;
4036
- const distinct = opts.distinct?.length ? opts.distinct : void 0;
4037
- const cursorSelected = !single && isCursorFieldSelected(parsedInfo.fieldsByTypeName[typeName], table);
4038
- let cursorEntries;
4039
- if (!single && (after != null || cursorSelected)) {
4040
- if (after != null && distinct) throw drizzleError("'after' cannot be combined with 'distinct'.", { code: "DRIZZLE_INVALID_CURSOR" });
4041
- const cursorObstacle = orderByCursorObstacle(orderBy);
4042
- if (cursorObstacle) {
4043
- if (after != null) throw drizzleError(cursorObstacle, { code: "DRIZZLE_INVALID_CURSOR" });
4044
- } else if (!pkNames?.length) {
4045
- if (after != null) throw drizzleError(`Table ${tableName} has no primary key, so cursor pagination cannot be used on it.`, { code: "DRIZZLE_INVALID_CURSOR" });
4046
- } else cursorEntries = cursorOrderingEntries(orderBy, pkNames);
4047
- }
4048
- const cursorValues = after != null && cursorEntries ? decodeCursor(after, cursorEntries) : void 0;
4049
- const needsDefaultOrder = single || offset != null || opts.limit != null;
4050
- let distinctKeys;
4051
- if (distinct) {
4052
- distinctKeys = await selectDistinctKeys({
4053
- db: opts.db,
4054
- table,
4055
- tableName,
4056
- distinct,
4057
- pkNames: pkNames ?? [],
4058
- where: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0, deleted),
4059
- orderBy,
4060
- limit: single ? 1 : opts.limit,
4061
- offset
4062
- });
4063
- if (!distinctKeys.length) return single ? void 0 : [];
4064
- }
4065
- const params = {
4066
- columns: extractSelectedColumnsFromTree(parsedInfo.fieldsByTypeName[typeName], table, {
4067
- tableName,
4068
- relationMap,
4069
- tables,
4070
- allRelations: filterCtx?.relationMap
4071
- }),
4072
- offset: distinctKeys ? void 0 : offset,
4073
- 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,
4074
- 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,
4075
- with: relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, {
4076
- typeNameMapper,
4077
- filterCtx,
4078
- limits: opts.limits,
4079
- scope,
4080
- defaultOrderBy: opts.defaultOrderBy,
4081
- resolveName: opts.resolveName
4082
- }) : void 0
4336
+ listFieldName,
4337
+ singleFieldName,
4338
+ aggregateFieldName,
4339
+ groupByFieldName,
4340
+ createArrayFieldName,
4341
+ createSingleFieldName,
4342
+ upsertArrayFieldName,
4343
+ upsertSingleFieldName,
4344
+ updateFieldName,
4345
+ updateManyFieldName,
4346
+ updateSingleFieldName,
4347
+ updateCountFieldName: `${prefixes.update}${pluralNoun}Count`,
4348
+ deleteFieldName,
4349
+ deleteSingleFieldName,
4350
+ restoreFieldName,
4351
+ restoreSingleFieldName,
4352
+ deleteCountFieldName: `${prefixes.delete}${pluralNoun}Count`
4083
4353
  };
4084
- if (single) {
4085
- const result = await queryBase.findFirst(params);
4086
- return result ? remapToGraphQLSingleOutput(result, tableName, table, relationMap) : void 0;
4087
- }
4088
- if (cursorEntries && cursorSelected) for (const [column] of cursorEntries) params.columns[column] = true;
4089
- params.limit = distinctKeys ? void 0 : opts.limit;
4090
- const result = await queryBase.findMany(params);
4091
- if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
4092
- return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
4093
4354
  };
4355
+ //#endregion
4356
+ //#region src/util/builders/common/root-fields.ts
4094
4357
  /**
4095
- * After a mutation, re-fetch the mutated rows through the relational query builder so the
4096
- * selected relations are eagerly loaded in a single query, then merge those relations onto
4097
- * the `.returning()` rows — making the per-field BatchLoader fallback unnecessary.
4358
+ * Registers one generated root field, refusing to overwrite a name already taken.
4098
4359
  *
4099
- * `withParams` is the pre-computed relation selection (from extractRelationsParams); the
4100
- * caller only invokes this when relations are actually selected, so it also gates whether
4101
- * the PK was forced into RETURNING.
4360
+ * A generated name is the table name run through `typeNameMapper`, a prefix and a suffix,
4361
+ * and the pluralization each operation applies — so whether two operations collide is a
4362
+ * property of the finished names, not of any one piece of the config. `buildSchema` checks
4363
+ * what it can up front (identical list and single suffixes with no mapper), but that check
4364
+ * compares the suffix *strings*, which cannot see a collision the rest of the naming rules
4365
+ * produce: `{ list: 'All', single: '' }` used to give both insert mutations the name
4366
+ * `createUsers`, and the array insert simply disappeared from the schema.
4102
4367
  *
4103
- * Falls back to the original `.returning()` rows (relations then resolve via the
4104
- * field-level BatchLoader) when the table has no RQB support, no primary key columns can be
4105
- * determined, or the re-fetch fails. Supports single- and multi-column primary keys.
4368
+ * Assigning into the record silently kept the last registration. Throwing here means a
4369
+ * naming config that cannot work says so at build time, naming the field it lost.
4106
4370
  */
4107
- const eagerLoadMutationRelations = async (db, tableName, rows, pkNames, withParams) => {
4108
- if (!rows.length || !pkNames.length || !withParams || !Object.keys(withParams).length) return rows;
4109
- const queryBase = db.query?.[tableName];
4110
- if (!queryBase) return rows;
4111
- const keyableRows = rows.filter((row) => pkNames.every((n) => row[n] != null));
4112
- if (!keyableRows.length) return rows;
4113
- const pkColumns = {};
4114
- for (const pk of pkNames) pkColumns[pk] = true;
4115
- const relationNames = Object.keys(withParams);
4116
- const keyOf = (row) => JSON.stringify(pkNames.map((n) => typeof row[n] === "bigint" ? row[n].toString() : row[n]));
4117
- let whereRaw;
4118
- if (pkNames.length === 1) {
4119
- const pkName = pkNames[0];
4120
- const ids = keyableRows.map((r) => r[pkName]);
4121
- whereRaw = (aliased) => (0, drizzle_orm.inArray)(aliased[pkName], ids);
4122
- } else whereRaw = (aliased) => {
4123
- const lhs = drizzle_orm.sql.join(pkNames.map((n) => drizzle_orm.sql`${aliased[n]}`), drizzle_orm.sql`, `);
4124
- const tuples = drizzle_orm.sql.join(keyableRows.map((row) => drizzle_orm.sql`(${drizzle_orm.sql.join(pkNames.map((n) => drizzle_orm.sql`${row[n]}`), drizzle_orm.sql`, `)})`), drizzle_orm.sql`, `);
4125
- return drizzle_orm.sql`(${lhs}) in (${tuples})`;
4126
- };
4127
- let enriched;
4128
- try {
4129
- enriched = await queryBase.findMany({
4130
- columns: pkColumns,
4131
- where: { RAW: whereRaw },
4132
- with: withParams
4133
- });
4134
- } catch (err) {
4135
- console.warn(`[drizzle-graphql] eager-loading relations for a "${tableName}" mutation failed; falling back to lazy resolution.`, err);
4136
- return rows;
4137
- }
4138
- const byKey = new Map(enriched.map((e) => [keyOf(e), e]));
4139
- for (const row of rows) {
4140
- const match = byKey.get(keyOf(row));
4141
- if (!match) continue;
4142
- for (const rel of relationNames) row[rel] = match[rel];
4143
- }
4144
- return rows;
4371
+ const defineRootField = (fields, kind, name, config) => {
4372
+ if (Object.getOwnPropertyDescriptor(fields, name) !== void 0) throw new Error(`Drizzle-GraphQL Error: two generated ${kind} fields are both named '${name}'. Generated names come from the table name plus config.prefixes, config.suffixes and config.typeNameMapper — some combination of those gives two operations the same name. Adjust one of them so each operation gets its own.`);
4373
+ fields[name] = config;
4145
4374
  };
4146
4375
  //#endregion
4147
4376
  //#region src/util/builders/common/table-inputs.ts
@@ -4927,154 +5156,6 @@ const resolveBuildConfig = (config, schema) => {
4927
5156
  };
4928
5157
  };
4929
5158
  //#endregion
4930
- //#region src/util/parse-resolve-info.ts
4931
- /** Unwraps `[T]!`-style wrappers down to the named type. `getNamedType` without the realm tie. */
4932
- const namedTypeOf = (type) => {
4933
- let current = type;
4934
- while (current?.ofType) current = current.ofType;
4935
- return current;
4936
- };
4937
- /**
4938
- * Whether a field's named output type has sub-selections. Object types and interfaces expose
4939
- * `getFields`, unions expose `getTypes`; scalars and enums expose neither. Input object types
4940
- * also expose `getFields`, but an input type can never be a field's output type, so there is
4941
- * nothing here for that to collide with.
4942
- */
4943
- const isCompositeLike = (type) => !!type && (typeof type.getFields === "function" || typeof type.getTypes === "function");
4944
- /**
4945
- * The fields declared on a type, or `undefined` when it has none to declare. A union is the
4946
- * case that matters: it has no fields of its own, so a selection directly against one resolves
4947
- * to nothing and is dropped — only the inline fragments underneath contribute.
4948
- */
4949
- const fieldsOf = (type) => typeof type?.getFields === "function" ? type.getFields() : void 0;
4950
- /**
4951
- * The runtime variable values, in the plain-map form the directive check wants. graphql 17
4952
- * wraps them in `{ sources, coerced }`; graphql 16 hands over the map itself.
4953
- */
4954
- const coercedVariables = (info) => {
4955
- const values = info.variableValues;
4956
- if (values && typeof values === "object" && "coerced" in values) return values.coerced ?? {};
4957
- return values ?? {};
4958
- };
4959
- /** The value of a `@skip` / `@include` `if:` argument — a literal or a variable reference. */
4960
- const directiveIfValue = (info, value) => {
4961
- if (value.kind === graphql.Kind.VARIABLE) return coercedVariables(info)[value.name.value];
4962
- if (value.kind === graphql.Kind.BOOLEAN) return value.value;
4963
- };
4964
- /**
4965
- * Whether `@skip` / `@include` on this selection exclude it. Applies to fields, fragment
4966
- * spreads and inline fragments alike, which is where graphql puts them.
4967
- */
4968
- const isExcluded = (info, node) => {
4969
- for (const directive of node.directives ?? []) {
4970
- const directiveName = directive.name.value;
4971
- if (directiveName !== "skip" && directiveName !== "include") continue;
4972
- const ifArgument = directive.arguments?.find((argument) => argument.name.value === "if");
4973
- if (!ifArgument) continue;
4974
- const value = directiveIfValue(info, ifArgument.value);
4975
- if (directiveName === "skip" ? !!value : !value) return true;
4976
- }
4977
- return false;
4978
- };
4979
- /** The type a fragment's `on Type` condition names, looked up in the executing schema. */
4980
- const typeFromCondition = (info, condition) => info.schema.getType(condition.name.value);
4981
- /**
4982
- * Introspection meta-fields are not schema fields and never reach the database. `__typename` is
4983
- * the one that turns up in practice; `__id` is excluded from the rule because a schema is free
4984
- * to declare a field by that name.
4985
- */
4986
- const isMetaField = (name) => name.startsWith("__") && name !== "__id";
4987
- /**
4988
- * Merges one selection set into `tree`, which is a `fieldsByTypeName` map — the caller owns the
4989
- * level, this owns the entries under `parentType`'s name.
4990
- *
4991
- * `activeFragments` is the set of fragment names on the current spread path. A document that
4992
- * passed validation cannot contain a fragment cycle, but this parser also runs on trees a
4993
- * caller assembled by hand, and an unbounded walk would hang rather than fail.
4994
- */
4995
- const collectSelections = (selections, info, tree, parentType, deep, activeFragments) => {
4996
- const parentTypeName = parentType?.name;
4997
- if (!parentTypeName) return tree;
4998
- let fields = tree[parentTypeName];
4999
- if (!fields) {
5000
- fields = {};
5001
- tree[parentTypeName] = fields;
5002
- }
5003
- const parentFields = fieldsOf(parentType);
5004
- for (const selection of selections) {
5005
- if (isExcluded(info, selection)) continue;
5006
- if (selection.kind === graphql.Kind.FIELD) {
5007
- const name = selection.name.value;
5008
- if (isMetaField(name)) continue;
5009
- const fieldDef = parentFields?.[name];
5010
- if (!fieldDef) continue;
5011
- const fieldType = namedTypeOf(fieldDef.type);
5012
- if (!fieldType) continue;
5013
- const composite = isCompositeLike(fieldType);
5014
- const alias = selection.alias?.value || name;
5015
- let node = fields[alias];
5016
- if (!node) {
5017
- node = {
5018
- name,
5019
- alias,
5020
- args: { ...(0, graphql.getArgumentValues)(fieldDef, selection, info.variableValues) },
5021
- fieldsByTypeName: composite && fieldType.name ? { [fieldType.name]: {} } : {}
5022
- };
5023
- fields[alias] = node;
5024
- }
5025
- if (selection.selectionSet && deep && composite) collectSelections(selection.selectionSet.selections, info, node.fieldsByTypeName, fieldType, deep, activeFragments);
5026
- continue;
5027
- }
5028
- if (!deep) continue;
5029
- if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
5030
- const fragmentName = selection.name.value;
5031
- const fragment = info.fragments?.[fragmentName];
5032
- if (!fragment) throw new Error(`Drizzle-GraphQL Error: unknown fragment '${fragmentName}' — it is spread in the query but not defined in the document.`);
5033
- if (activeFragments.has(fragmentName)) continue;
5034
- const fragmentType = fragment.typeCondition ? typeFromCondition(info, fragment.typeCondition) : parentType;
5035
- if (!isCompositeLike(fragmentType)) continue;
5036
- activeFragments.add(fragmentName);
5037
- try {
5038
- collectSelections(fragment.selectionSet.selections, info, tree, fragmentType, deep, activeFragments);
5039
- } finally {
5040
- activeFragments.delete(fragmentName);
5041
- }
5042
- continue;
5043
- }
5044
- if (selection.kind === graphql.Kind.INLINE_FRAGMENT) {
5045
- const fragmentType = selection.typeCondition ? typeFromCondition(info, selection.typeCondition) : parentType;
5046
- if (!isCompositeLike(fragmentType)) continue;
5047
- collectSelections(selection.selectionSet.selections, info, tree, fragmentType, deep, activeFragments);
5048
- }
5049
- }
5050
- return tree;
5051
- };
5052
- /** The first key of an object, without materializing the whole key array to discard it. */
5053
- const firstKey = (object) => {
5054
- for (const key in object) return key;
5055
- };
5056
- /**
5057
- * Reads the selection under a resolver's own field out of its `info`, as a tree of
5058
- * {@link ResolveTree} nodes.
5059
- *
5060
- * ```ts
5061
- * const parsed = parseResolveInfo(info, { deep: true })!;
5062
- * const selected = parsed.fieldsByTypeName['Users']; // keyed by response key
5063
- * ```
5064
- *
5065
- * Returns `undefined` when the resolver's field produced no node at all, which in practice
5066
- * means `info` carried no field nodes.
5067
- */
5068
- const parseResolveInfo = (info, options = {}) => {
5069
- const fieldNodes = info.fieldNodes;
5070
- if (!fieldNodes) throw new Error("Drizzle-GraphQL Error: resolve info carries no fieldNodes, so no selection could be read from it.");
5071
- const tree = collectSelections(fieldNodes, info, {}, info.parentType, options.deep ?? true, /* @__PURE__ */ new Set());
5072
- const typeKey = firstKey(tree);
5073
- const fields = typeKey === void 0 ? void 0 : tree[typeKey];
5074
- const fieldKey = fields && firstKey(fields);
5075
- return fieldKey === void 0 ? void 0 : fields?.[fieldKey];
5076
- };
5077
- //#endregion
5078
5159
  //#region src/util/builders/aggregates.ts
5079
5160
  /** Operations that aggregate over a set of column values. `count` is handled separately (whole rows). */
5080
5161
  const AGGREGATE_OPS = [
@@ -5959,7 +6040,7 @@ const createNestedWriteRuntime = (params) => {
5959
6040
  };
5960
6041
  /** The values the junction stores for the target rows one operand selects. */
5961
6042
  const selectedTargetKeys = async (executor, plan, filter, operation, scope) => {
5962
- return (await executor.select().from(plan.targetTable).where(conditionOf(plan, filter, operation, scope))).map((row) => row[plan.foreignColPropName]).filter((value) => value !== void 0 && value !== null);
6043
+ return (await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, filter, operation, scope))).map((row) => row[plan.foreignColPropName]).filter((value) => value !== void 0 && value !== null);
5963
6044
  };
5964
6045
  /**
5965
6046
  * The link half of a many-to-many write: junction rows are inserted and deleted, and both
@@ -5974,7 +6055,7 @@ const createNestedWriteRuntime = (params) => {
5974
6055
  const through = plan.through;
5975
6056
  const unlinkAll = () => executor.delete(through.table).where((0, drizzle_orm.eq)(through.sourceCol, key));
5976
6057
  const link = async (operands, operation) => {
5977
- const existing = new Set((await executor.select().from(through.table).where((0, drizzle_orm.eq)(through.sourceCol, key))).map((row) => row[through.targetColPropName]));
6058
+ const existing = new Set((await executor.select({ [through.targetColPropName]: through.targetCol }).from(through.table).where((0, drizzle_orm.eq)(through.sourceCol, key))).map((row) => row[through.targetColPropName]));
5978
6059
  const rows = [];
5979
6060
  for (const filter of operands) for (const targetKey of await selectedTargetKeys(executor, plan, filter, operation, scope)) if (!existing.has(targetKey)) {
5980
6061
  existing.add(targetKey);
@@ -6035,11 +6116,11 @@ const createNestedWriteRuntime = (params) => {
6035
6116
  assertSingleOperation(plan, op);
6036
6117
  if (op["create"] !== void 0) {
6037
6118
  const values = applyContextValues(remapFromGraphQLSingleInput({ ...op["create"] }, plan.targetTable), contextValues?.(plan.targetTableName), context);
6038
- const created = (await executor.insert(plan.targetTable).values(values).returning())[0];
6119
+ const created = (await executor.insert(plan.targetTable).values(values).returning({ [plan.foreignColPropName]: plan.foreignCol }))[0];
6039
6120
  if (!created) throw drizzleError(`Drizzle-GraphQL Error: 'create' on '${relationName}' inserted no row, so there is nothing to attach.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
6040
6121
  patch[plan.localColPropName] = created[plan.foreignColPropName];
6041
6122
  } else if (op["connect"] !== void 0) {
6042
- const rows = await executor.select().from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
6123
+ const rows = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
6043
6124
  if (rows.length !== 1) throw drizzleError(`Drizzle-GraphQL Error: 'connect' on '${relationName}' matched ${rows.length} rows — it attaches a single row, so its filter must match exactly one.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
6044
6125
  patch[plan.localColPropName] = rows[0][plan.foreignColPropName];
6045
6126
  } else if (op["disconnect"] === true) patch[plan.localColPropName] = null;
@@ -6577,7 +6658,14 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
6577
6658
  if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
6578
6659
  await before();
6579
6660
  const relationCtx = relationFilterCtx(filterCtx, tableName);
6580
- const filters = withScope(scope, tableName, table, single || requireWhere ? extractRequiredFilters(table, tableName, where, relationCtx) : where ? extractFilters(table, tableName, where, relationCtx) : void 0);
6661
+ const filters = scopedWhere({
6662
+ scope,
6663
+ tableName,
6664
+ table,
6665
+ where,
6666
+ relationCtx,
6667
+ required: single || requireWhere
6668
+ });
6581
6669
  if (single) await assertSingleMatch(executor, table, filters);
6582
6670
  let query = executor.update(table).set(input);
6583
6671
  if (filters) query = query.where(filters);
@@ -6659,7 +6747,15 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
6659
6747
  const hard = canHardDelete && args.hard === true;
6660
6748
  const scope = policies?.scope?.(context);
6661
6749
  const relationCtx = relationFilterCtx(filterCtx, tableName);
6662
- 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);
6750
+ const filters = scopedWhere({
6751
+ scope,
6752
+ tableName,
6753
+ table,
6754
+ where,
6755
+ relationCtx,
6756
+ required: single || requireWhere,
6757
+ deleted: restore ? "ONLY" : hard ? "INCLUDE" : void 0
6758
+ });
6663
6759
  if (single) await assertSingleMatch(executor, table, filters);
6664
6760
  let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
6665
6761
  if (filters) query = query.where(filters);
@@ -6887,8 +6983,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
6887
6983
  const contextColumns = policies?.contextValues?.(tableName);
6888
6984
  const scope = policies?.scope?.(context);
6889
6985
  const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : args.values, table), contextColumns, context);
6890
- const parsedInfo = parseResolveInfo(info, { deep: true });
6891
- const { columns, hasRelations, withParams } = prepareMutationRelationColumns({
6986
+ const { columns, hasRelations, withParams } = mutationSelection(info, {
6892
6987
  relationMap,
6893
6988
  tables,
6894
6989
  tableName,
@@ -6897,7 +6992,6 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
6897
6992
  resolveName,
6898
6993
  table,
6899
6994
  pkNames,
6900
- parsedInfo,
6901
6995
  limits,
6902
6996
  scope,
6903
6997
  defaultOrderBy: policies?.defaultOrderBy
@@ -6918,7 +7012,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
6918
7012
  context
6919
7013
  }) : await runInsert(executor, input);
6920
7014
  await after(result);
6921
- const enriched = hasRelations ? await eagerLoadMutationRelations(executor, tableName, result, pkNames, withParams) : result;
7015
+ const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
6922
7016
  return remapToGraphQLArrayOutput(enriched, tableName, table, relationMap);
6923
7017
  }
6924
7018
  });
@@ -6942,8 +7036,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
6942
7036
  const contextColumns = policies?.contextValues?.(tableName);
6943
7037
  const scope = policies?.scope?.(context);
6944
7038
  const input = nestedEntry ? {} : applyContextValues(remapFromGraphQLSingleInput(entry ? entry.columns : args.values, table), contextColumns, context);
6945
- const parsedInfo = parseResolveInfo(info, { deep: true });
6946
- const { columns, hasRelations, withParams } = prepareMutationRelationColumns({
7039
+ const { columns, hasRelations, withParams } = mutationSelection(info, {
6947
7040
  relationMap,
6948
7041
  tables,
6949
7042
  tableName,
@@ -6952,7 +7045,6 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
6952
7045
  resolveName,
6953
7046
  table,
6954
7047
  pkNames,
6955
- parsedInfo,
6956
7048
  limits,
6957
7049
  scope,
6958
7050
  defaultOrderBy: policies?.defaultOrderBy
@@ -6977,7 +7069,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
6977
7069
  if (!conflictDoNothing) throw drizzleError("The insert returned no row.", { code: "DRIZZLE_NO_ROW_RETURNED" });
6978
7070
  return;
6979
7071
  }
6980
- const enriched = hasRelations ? await eagerLoadMutationRelations(executor, tableName, result, pkNames, withParams) : result;
7072
+ const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
6981
7073
  return remapToGraphQLSingleOutput(enriched[0], tableName, table, relationMap);
6982
7074
  }
6983
7075
  });
@@ -7015,8 +7107,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7015
7107
  const contextColumns = policies?.contextValues?.(tableName);
7016
7108
  const scope = policies?.scope?.(context);
7017
7109
  const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
7018
- const parsedInfo = parseResolveInfo(info, { deep: true });
7019
- const { columns, hasRelations, withParams } = prepareMutationRelationColumns({
7110
+ const { columns, hasRelations, withParams } = mutationSelection(info, {
7020
7111
  relationMap,
7021
7112
  tables,
7022
7113
  tableName,
@@ -7025,7 +7116,6 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7025
7116
  resolveName,
7026
7117
  table,
7027
7118
  pkNames,
7028
- parsedInfo,
7029
7119
  limits,
7030
7120
  scope,
7031
7121
  defaultOrderBy: policies?.defaultOrderBy
@@ -7062,7 +7152,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7062
7152
  }) : await runUpsert(executor, input);
7063
7153
  await after(result);
7064
7154
  if (single && !result[0]) return;
7065
- const enriched = hasRelations ? await eagerLoadMutationRelations(executor, tableName, result, pkNames, withParams) : result;
7155
+ const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
7066
7156
  return single ? remapToGraphQLSingleOutput(enriched[0], tableName, table, relationMap) : remapToGraphQLArrayOutput(enriched, tableName, table, relationMap);
7067
7157
  }
7068
7158
  });
@@ -7086,8 +7176,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7086
7176
  const { where, set } = args;
7087
7177
  const scope = policies?.scope?.(context);
7088
7178
  await before();
7089
- const parsedInfo = parseResolveInfo(info, { deep: true });
7090
- const { columns, hasRelations, withParams } = prepareMutationRelationColumns({
7179
+ const { columns, hasRelations, withParams } = mutationSelection(info, {
7091
7180
  relationMap,
7092
7181
  tables,
7093
7182
  tableName,
@@ -7096,7 +7185,6 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7096
7185
  resolveName,
7097
7186
  table,
7098
7187
  pkNames,
7099
- parsedInfo,
7100
7188
  limits,
7101
7189
  scope,
7102
7190
  defaultOrderBy: policies?.defaultOrderBy
@@ -7106,7 +7194,14 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7106
7194
  const input = stripContextValues(remapUpdateInput(entry ? entry.columns : set, table, tableName), policies?.contextValues?.(tableName));
7107
7195
  if (!Object.keys(input).length && !nestedOps) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
7108
7196
  const relationCtx = relationFilterCtx(filterCtx, tableName);
7109
- const filters = withScope(scope, tableName, table, single || requireWhere ? extractRequiredFilters(table, tableName, where, relationCtx) : where ? extractFilters(table, tableName, where, relationCtx) : void 0);
7197
+ const filters = scopedWhere({
7198
+ scope,
7199
+ tableName,
7200
+ table,
7201
+ where,
7202
+ relationCtx,
7203
+ required: single || requireWhere
7204
+ });
7110
7205
  if (single) await assertSingleMatch(executor, table, filters);
7111
7206
  const returning = nestedOps ? nested.withJoinColumns(tableName, nestedOps, { ...columns }, table) : columns;
7112
7207
  const runUpdate = async (target, values) => {
@@ -7128,7 +7223,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7128
7223
  await after(result);
7129
7224
  if (single && result.length > 1) throw drizzleError("'where' matched more than one row!", { code: "DRIZZLE_MULTI_ROW_MATCH" });
7130
7225
  if (single && !result[0]) return;
7131
- const enriched = hasRelations ? await eagerLoadMutationRelations(executor, tableName, result, pkNames, withParams) : result;
7226
+ const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
7132
7227
  return single ? remapToGraphQLSingleOutput(enriched[0], tableName, table, relationMap) : remapToGraphQLArrayOutput(enriched, tableName, table, relationMap);
7133
7228
  }
7134
7229
  });
@@ -7170,7 +7265,15 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7170
7265
  const parsedInfo = parseResolveInfo(info, { deep: true });
7171
7266
  const columns = extractSelectedColumnsFromTreeSQLFormat(parsedInfo.fieldsByTypeName[applyObjectTypeName(typeName, tableName, resolveName)], table, selectionCtx);
7172
7267
  const relationCtx = relationFilterCtx(filterCtx, tableName);
7173
- 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);
7268
+ const filters = scopedWhere({
7269
+ scope,
7270
+ tableName,
7271
+ table,
7272
+ where,
7273
+ relationCtx,
7274
+ required: single || requireWhere,
7275
+ deleted: restore ? "ONLY" : hard ? "INCLUDE" : void 0
7276
+ });
7174
7277
  if (single) await assertSingleMatch(executor, table, filters);
7175
7278
  let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
7176
7279
  if (filters) query = query.where(filters);
@@ -7499,8 +7602,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7499
7602
  await before();
7500
7603
  const scope = policies?.scope?.(context);
7501
7604
  const contextColumns = policies?.contextValues?.(tableName);
7502
- const parsedInfo = parseResolveInfo(info, { deep: true });
7503
- const { columns, hasRelations, withParams } = prepareMutationRelationColumns({
7605
+ const { columns, hasRelations, withParams } = mutationSelection(info, {
7504
7606
  relationMap,
7505
7607
  tables,
7506
7608
  tableName,
@@ -7509,7 +7611,6 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7509
7611
  resolveName,
7510
7612
  table,
7511
7613
  pkNames,
7512
- parsedInfo,
7513
7614
  limits,
7514
7615
  scope,
7515
7616
  defaultOrderBy: policies?.defaultOrderBy
@@ -7538,7 +7639,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7538
7639
  });
7539
7640
  const flatRows = perEntry.flat();
7540
7641
  await after(flatRows);
7541
- const enriched = hasRelations ? await eagerLoadMutationRelations(executor, tableName, flatRows, pkNames, withParams) : flatRows;
7642
+ const enriched = await withEagerRelations(executor, tableName, flatRows, pkNames, withParams, hasRelations);
7542
7643
  const output = [];
7543
7644
  let offset = 0;
7544
7645
  for (const rows of perEntry) {