@vantreeseba/drizzle-graphql 15.1.1 → 15.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -7
- package/dist/index.cjs +334 -96
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -7
- package/dist/index.d.ts +68 -7
- package/dist/index.js +335 -97
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Column, One, SQL, Table, aliasedTable, and, arrayContains, arrayOverlap
|
|
|
2
2
|
import { MySqlAsyncDatabase, MySqlDate, MySqlDateString, MySqlDateTime, MySqlDateTimeString, MySqlInt, MySqlSerial, MySqlTable, MySqlTimestamp, MySqlTimestampString, getTableConfig } from "drizzle-orm/mysql-core";
|
|
3
3
|
import { PgAsyncDatabase, PgDate, PgDateString, PgInteger, PgSerial, PgTable, PgTimestamp, PgTimestampString, PgUUID, getTableConfig as getTableConfig$1 } from "drizzle-orm/pg-core";
|
|
4
4
|
import { SQLiteAsyncDatabase, SQLiteInteger, SQLiteTable, SQLiteTimestamp, getTableConfig as getTableConfig$2 } from "drizzle-orm/sqlite-core";
|
|
5
|
-
import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, Kind, getArgumentValues } from "graphql";
|
|
5
|
+
import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, Kind, getArgumentValues, getNamedType } from "graphql";
|
|
6
6
|
import { GraphQLDateTime, GraphQLJSON, GraphQLUUID } from "graphql-scalars";
|
|
7
7
|
import pluralize from "pluralize";
|
|
8
8
|
//#region src/util/builders/common/errors.ts
|
|
@@ -1983,6 +1983,13 @@ const extractUniqueKeyFilter = (table, tableName, fieldName, members, value) =>
|
|
|
1983
1983
|
*/
|
|
1984
1984
|
const narrowedCtxCache = /* @__PURE__ */ new WeakMap();
|
|
1985
1985
|
/**
|
|
1986
|
+
* One alias counter per build, shared by every table's narrowed context. A statement can
|
|
1987
|
+
* combine subqueries compiled from different contexts — a relation filter on one table whose
|
|
1988
|
+
* target's scope is a filter object reaching through another relation, say — and nested
|
|
1989
|
+
* subqueries that drew the same alias from two separate counters would shadow each other.
|
|
1990
|
+
*/
|
|
1991
|
+
const aliasCounters = /* @__PURE__ */ new WeakMap();
|
|
1992
|
+
/**
|
|
1986
1993
|
* Narrows the build-scoped relation filter context to the table a resolver is filtering.
|
|
1987
1994
|
*
|
|
1988
1995
|
* Cached rather than rebuilt: the base carries the build's whole `tables` and `relationMap`,
|
|
@@ -1996,7 +2003,7 @@ const narrowedCtxCache = /* @__PURE__ */ new WeakMap();
|
|
|
1996
2003
|
* aliases only ever have to be unique *within* one statement, which a number that never
|
|
1997
2004
|
* repeats gives more surely than one that restarts.
|
|
1998
2005
|
*/
|
|
1999
|
-
const relationFilterCtx = (base, tableKey) => {
|
|
2006
|
+
const relationFilterCtx = (base, tableKey, scope) => {
|
|
2000
2007
|
if (!base) return;
|
|
2001
2008
|
let byTable = narrowedCtxCache.get(base);
|
|
2002
2009
|
if (!byTable) {
|
|
@@ -2005,13 +2012,33 @@ const relationFilterCtx = (base, tableKey) => {
|
|
|
2005
2012
|
}
|
|
2006
2013
|
let ctx = byTable.get(tableKey);
|
|
2007
2014
|
if (!ctx) {
|
|
2015
|
+
let aliases = aliasCounters.get(base);
|
|
2016
|
+
if (!aliases) {
|
|
2017
|
+
aliases = { n: 0 };
|
|
2018
|
+
aliasCounters.set(base, aliases);
|
|
2019
|
+
}
|
|
2008
2020
|
ctx = {
|
|
2009
2021
|
...base,
|
|
2010
|
-
tableKey
|
|
2022
|
+
tableKey,
|
|
2023
|
+
aliases
|
|
2011
2024
|
};
|
|
2012
2025
|
byTable.set(tableKey, ctx);
|
|
2013
2026
|
}
|
|
2014
|
-
return
|
|
2027
|
+
return scope ? {
|
|
2028
|
+
...ctx,
|
|
2029
|
+
scope
|
|
2030
|
+
} : ctx;
|
|
2031
|
+
};
|
|
2032
|
+
/**
|
|
2033
|
+
* The row-policy predicate a subquery over a relation's target applies: the target's scope
|
|
2034
|
+
* and soft-delete condition, in the `deleted` mode the relation field defaults to — so a
|
|
2035
|
+
* relation filter or a relation ordering sees exactly the rows the relation field returns.
|
|
2036
|
+
*/
|
|
2037
|
+
const relationTargetPolicy = (ctx, relation, targetTableName, aliasedTarget) => {
|
|
2038
|
+
const scope = ctx.scope;
|
|
2039
|
+
if (!scope) return;
|
|
2040
|
+
const mode = scope.relationDefault(targetTableName, is(relation, One) && relation.optional === false);
|
|
2041
|
+
return scope.has(targetTableName, mode) ? scope.on(targetTableName, aliasedTarget, mode) : void 0;
|
|
2015
2042
|
};
|
|
2016
2043
|
/**
|
|
2017
2044
|
* Correlates the parent row with the aliased target table using the relation's own join
|
|
@@ -2102,8 +2129,11 @@ const buildRelationExists = (parentTable, relationName, relEntry, innerFilters,
|
|
|
2102
2129
|
const throughTable = relation.throughTable;
|
|
2103
2130
|
let fromClause;
|
|
2104
2131
|
let joinCondition;
|
|
2132
|
+
let junctionPolicy;
|
|
2105
2133
|
if (throughTable) {
|
|
2106
2134
|
const aliasedThrough = aliasedTable(throughTable, `dgql_rel_${aliases.n++}`);
|
|
2135
|
+
const throughTableName = ctx.scope ? Object.keys(ctx.tables).find((name) => ctx.tables[name] === throughTable) : void 0;
|
|
2136
|
+
if (throughTableName && ctx.scope.has(throughTableName)) junctionPolicy = ctx.scope.on(throughTableName, aliasedThrough);
|
|
2107
2137
|
const { correlation, junctionJoin } = buildThroughJoinConditions(parentTable, relation, aliasedThrough, aliasedTarget, relationName);
|
|
2108
2138
|
fromClause = sql`${getTableAsAliasSQL(aliasedTarget)} inner join ${getTableAsAliasSQL(aliasedThrough)} on ${junctionJoin}`;
|
|
2109
2139
|
joinCondition = correlation;
|
|
@@ -2113,6 +2143,7 @@ const buildRelationExists = (parentTable, relationName, relEntry, innerFilters,
|
|
|
2113
2143
|
}
|
|
2114
2144
|
const ownWhere = relationOwnWhere(relation);
|
|
2115
2145
|
const relationWhere = ownWhere ? relationsFilterToSQL(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
|
|
2146
|
+
const policy = and(relationTargetPolicy(ctx, relation, targetTableName, aliasedTarget), junctionPolicy);
|
|
2116
2147
|
const inner = innerFilters ? extractFilters(aliasedTarget, targetTableName, innerFilters, {
|
|
2117
2148
|
...ctx,
|
|
2118
2149
|
tableKey: targetTableName,
|
|
@@ -2120,9 +2151,9 @@ const buildRelationExists = (parentTable, relationName, relEntry, innerFilters,
|
|
|
2120
2151
|
}) : void 0;
|
|
2121
2152
|
if (mode === "every") {
|
|
2122
2153
|
if (!inner) return;
|
|
2123
|
-
return sql`not exists (select 1 from ${fromClause} where ${and(joinCondition, relationWhere, not(inner))})`;
|
|
2154
|
+
return sql`not exists (select 1 from ${fromClause} where ${and(joinCondition, relationWhere, policy, not(inner))})`;
|
|
2124
2155
|
}
|
|
2125
|
-
const condition = and(joinCondition, relationWhere, inner);
|
|
2156
|
+
const condition = and(joinCondition, relationWhere, policy, inner);
|
|
2126
2157
|
return mode === "none" ? sql`not exists (select 1 from ${fromClause} where ${condition})` : sql`exists (select 1 from ${fromClause} where ${condition})`;
|
|
2127
2158
|
};
|
|
2128
2159
|
/**
|
|
@@ -2394,8 +2425,8 @@ const columnEnumCache = /* @__PURE__ */ new WeakMap();
|
|
|
2394
2425
|
* input field the enum would have typed, rather than emitting an empty enum, which is
|
|
2395
2426
|
* invalid GraphQL.
|
|
2396
2427
|
*/
|
|
2397
|
-
const generateColumnEnum = (table, enumName, description, predicate = () => true) => {
|
|
2398
|
-
const cacheKey = `${enumName}\u0000${excludedColumnsKey(table)}`;
|
|
2428
|
+
const generateColumnEnum = (table, enumName, description, predicate = () => true, variant = "") => {
|
|
2429
|
+
const cacheKey = `${enumName}\u0000${excludedColumnsKey(table)}\u0000${variant}`;
|
|
2399
2430
|
let tableCache = columnEnumCache.get(table);
|
|
2400
2431
|
const cached = tableCache?.get(cacheKey);
|
|
2401
2432
|
if (cached) return cached;
|
|
@@ -2455,6 +2486,9 @@ const conflictActionEnumType = (typeName) => sharedType(typeName, {
|
|
|
2455
2486
|
* `ON DUPLICATE KEY UPDATE` fires on any unique key and takes no predicate, so offering
|
|
2456
2487
|
* either there would mean silently ignoring it.
|
|
2457
2488
|
*
|
|
2489
|
+
* `update` never offers a `contextValues` column: the server owns it, and the conflict
|
|
2490
|
+
* branch is an update, which must not hand the row to another owner.
|
|
2491
|
+
*
|
|
2458
2492
|
* Returns `undefined` when the table has nothing to conflict on (`withTarget` dialects
|
|
2459
2493
|
* only) — the caller then generates no upsert mutations for that table at all, rather than
|
|
2460
2494
|
* an operation whose every call is a database error.
|
|
@@ -2462,12 +2496,13 @@ const conflictActionEnumType = (typeName) => sharedType(typeName, {
|
|
|
2462
2496
|
const generateOnConflictInput = (params) => {
|
|
2463
2497
|
const { table, tableName, typeName, uniqueSets, tableFilters, withTarget, cacheCtx } = params;
|
|
2464
2498
|
const resolveName = cacheCtx.typeName;
|
|
2499
|
+
const serverOwned = Object.keys(cacheCtx.contextValuesOf?.(tableName) ?? {}).sort();
|
|
2465
2500
|
const updateEnum = generateColumnEnum(table, resolveName({
|
|
2466
2501
|
kind: "columnEnum",
|
|
2467
2502
|
defaultName: `${typeName}UpdateColumn`,
|
|
2468
2503
|
table: tableName,
|
|
2469
2504
|
operation: "conflictUpdate"
|
|
2470
|
-
}), `Columns of ${typeName} that an upsert can overwrite
|
|
2505
|
+
}), `Columns of ${typeName} that an upsert can overwrite`, (_column, columnName) => !serverOwned.includes(columnName), serverOwned.join(","));
|
|
2471
2506
|
if (!updateEnum) return;
|
|
2472
2507
|
const fields = {
|
|
2473
2508
|
action: {
|
|
@@ -2477,11 +2512,11 @@ const generateOnConflictInput = (params) => {
|
|
|
2477
2512
|
},
|
|
2478
2513
|
update: {
|
|
2479
2514
|
type: new GraphQLList(new GraphQLNonNull(updateEnum)),
|
|
2480
|
-
description:
|
|
2515
|
+
description: `Columns to overwrite on conflict. Defaults to every column the request supplied, minus the ${withTarget ? "conflict target" : "primary key"}. Columns the request did not supply cannot be listed here — there would be no value to write.`
|
|
2481
2516
|
}
|
|
2482
2517
|
};
|
|
2483
2518
|
if (withTarget) {
|
|
2484
|
-
const uniqueColumns = new Set(uniqueSets.
|
|
2519
|
+
const uniqueColumns = new Set(uniqueSets.flatMap((set) => set.columns));
|
|
2485
2520
|
const targetEnum = generateColumnEnum(table, resolveName({
|
|
2486
2521
|
kind: "columnEnum",
|
|
2487
2522
|
defaultName: `${typeName}ConflictTarget`,
|
|
@@ -2509,55 +2544,88 @@ const generateOnConflictInput = (params) => {
|
|
|
2509
2544
|
});
|
|
2510
2545
|
};
|
|
2511
2546
|
/**
|
|
2512
|
-
* Turns the request's `onConflict` argument and the rows it is inserting into the
|
|
2513
|
-
* dialect should
|
|
2547
|
+
* Turns the request's `onConflict` argument and the rows it is inserting into the
|
|
2548
|
+
* statements a dialect should run — one per run of consecutive rows that write the same
|
|
2549
|
+
* columns on conflict.
|
|
2550
|
+
*
|
|
2551
|
+
* The split is what keeps a partial row partial. A multi-row insert fills a key one row
|
|
2552
|
+
* left out with DEFAULT, and `excluded.<column>` would carry that default onto the existing
|
|
2553
|
+
* row, so rows that supply different columns cannot share a `SET` list. Splitting into
|
|
2554
|
+
* *consecutive* runs, rather than grouping, keeps the rows a caller gets back in the order
|
|
2555
|
+
* it sent them. A batch whose rows agree — the usual case — is still one statement.
|
|
2514
2556
|
*
|
|
2515
2557
|
* `excludedRef` names the row that failed to insert in the dialect's own terms
|
|
2516
2558
|
* (`excluded.col` on PostgreSQL and SQLite, `values(col)` on MySQL), which is what makes a
|
|
2517
2559
|
* batch upsert update each row with its own values instead of the last row's.
|
|
2518
2560
|
*
|
|
2561
|
+
* Never written on conflict, even when supplied: the conflict target (it already matches);
|
|
2562
|
+
* the primary key on a dialect with no target (MySQL's conflict can be on another unique
|
|
2563
|
+
* key, and copying a generated id over the existing row's would move it); and `serverOwned`
|
|
2564
|
+
* — the `contextValues` columns, which an update never reassigns.
|
|
2565
|
+
*
|
|
2519
2566
|
* An UPDATE with nothing left to write degrades to NOTHING: `DO UPDATE SET` with an empty
|
|
2520
2567
|
* body is not valid SQL, and doing nothing is what the request asked for anyway.
|
|
2521
2568
|
*/
|
|
2522
2569
|
const resolveConflictPlan = (params) => {
|
|
2523
2570
|
const { table, values, onConflict, pkNames, uniqueSets, excludedRef, withTarget, buildWhere } = params;
|
|
2524
2571
|
const columns = getColumns(table);
|
|
2572
|
+
const serverOwned = new Set(params.serverOwned ?? []);
|
|
2525
2573
|
let target;
|
|
2574
|
+
let targetWhere;
|
|
2526
2575
|
if (withTarget) {
|
|
2527
2576
|
const targetNames = onConflict?.target?.length ? onConflict.target : [...pkNames];
|
|
2528
2577
|
if (!targetNames.length) throw drizzleError("Unable to upsert: no conflict target was given and this table has no primary key. Pass onConflict.target.", { code: "DRIZZLE_INVALID_ON_CONFLICT" });
|
|
2529
2578
|
const requested = [...targetNames].sort().join(",");
|
|
2530
|
-
|
|
2579
|
+
const matched = uniqueSets.find((set) => [...set.columns].sort().join(",") === requested);
|
|
2580
|
+
if (!matched) throw drizzleError(`Unable to upsert: [${targetNames.join(", ")}] is not a unique constraint on this table. Valid conflict targets: ${uniqueSets.map((set) => `[${set.columns.join(", ")}]`).join(", ")}.`, { code: "DRIZZLE_INVALID_ON_CONFLICT" });
|
|
2531
2581
|
target = targetNames.map((name) => columns[name]);
|
|
2582
|
+
targetWhere = matched.where;
|
|
2532
2583
|
}
|
|
2533
|
-
|
|
2584
|
+
const nothing = {
|
|
2534
2585
|
action: "NOTHING",
|
|
2535
2586
|
target,
|
|
2587
|
+
targetWhere,
|
|
2536
2588
|
set: {},
|
|
2537
2589
|
setWhere: void 0
|
|
2538
2590
|
};
|
|
2591
|
+
if ((onConflict?.action ?? "UPDATE") === "NOTHING") return [{
|
|
2592
|
+
values,
|
|
2593
|
+
plan: nothing
|
|
2594
|
+
}];
|
|
2539
2595
|
const supplied = new Set(values.flatMap((row) => Object.keys(row)));
|
|
2540
|
-
const
|
|
2541
|
-
let
|
|
2596
|
+
const neverWritten = new Set(withTarget && onConflict?.target?.length ? onConflict.target : pkNames);
|
|
2597
|
+
let candidates;
|
|
2542
2598
|
if (onConflict?.update?.length) {
|
|
2599
|
+
const owned = onConflict.update.filter((name) => serverOwned.has(name));
|
|
2600
|
+
if (owned.length) throw drizzleError(`Unable to upsert: onConflict.update lists ${owned.join(", ")}, which the server sets and an update never overwrites.`, { code: "DRIZZLE_INVALID_ON_CONFLICT" });
|
|
2543
2601
|
const unsupplied = onConflict.update.filter((name) => !supplied.has(name));
|
|
2544
2602
|
if (unsupplied.length) throw drizzleError(`Unable to upsert: onConflict.update lists ${unsupplied.join(", ")}, which the values do not supply.`, { code: "DRIZZLE_INVALID_ON_CONFLICT" });
|
|
2545
|
-
|
|
2546
|
-
} else
|
|
2547
|
-
if (!updateNames.length) return {
|
|
2548
|
-
action: "NOTHING",
|
|
2549
|
-
target,
|
|
2550
|
-
set: {},
|
|
2551
|
-
setWhere: void 0
|
|
2552
|
-
};
|
|
2553
|
-
const set = Object.fromEntries(updateNames.map((name) => [name, excludedRef(columns[name].name)]));
|
|
2603
|
+
candidates = onConflict.update;
|
|
2604
|
+
} else candidates = [...supplied].filter((name) => !neverWritten.has(name) && !serverOwned.has(name));
|
|
2554
2605
|
const setWhere = onConflict?.where && buildWhere ? buildWhere(onConflict.where) : void 0;
|
|
2555
|
-
|
|
2606
|
+
const planFor = (updateNames) => updateNames.length ? {
|
|
2556
2607
|
action: "UPDATE",
|
|
2557
2608
|
target,
|
|
2558
|
-
|
|
2609
|
+
targetWhere,
|
|
2610
|
+
set: Object.fromEntries(updateNames.map((name) => [name, excludedRef(columns[name].name)])),
|
|
2559
2611
|
setWhere
|
|
2560
|
-
};
|
|
2612
|
+
} : nothing;
|
|
2613
|
+
const batches = [];
|
|
2614
|
+
for (const row of values) {
|
|
2615
|
+
const updateNames = candidates.filter((name) => name in row);
|
|
2616
|
+
const key = updateNames.join(",");
|
|
2617
|
+
const last = batches[batches.length - 1];
|
|
2618
|
+
if (last?.key === key) last.values.push(row);
|
|
2619
|
+
else batches.push({
|
|
2620
|
+
key,
|
|
2621
|
+
values: [row],
|
|
2622
|
+
plan: planFor(updateNames)
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
return batches.map(({ values: rows, plan }) => ({
|
|
2626
|
+
values: rows,
|
|
2627
|
+
plan
|
|
2628
|
+
}));
|
|
2561
2629
|
};
|
|
2562
2630
|
/** `excluded.<column>` — PostgreSQL and SQLite name the rejected row this way. */
|
|
2563
2631
|
const excludedColumnRef = (columnName) => sql`excluded.${sql.identifier(columnName)}`;
|
|
@@ -2611,12 +2679,14 @@ const extendRelationOrderChain = (parentTable, relationName, relEntry, ctx, chai
|
|
|
2611
2679
|
const joinCondition = buildRelationJoinCondition(parentTable, relation, aliasedTarget, relationName);
|
|
2612
2680
|
const ownWhere = relationOwnWhere(relation);
|
|
2613
2681
|
const relationWhere = ownWhere ? relationsFilterToSQL(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
|
|
2682
|
+
const policy = relationTargetPolicy(ctx, relation, relEntry.targetTableName, aliasedTarget);
|
|
2614
2683
|
return {
|
|
2615
2684
|
fromParts: [...chain?.fromParts ?? [], getTableAsAliasSQL(aliasedTarget)],
|
|
2616
2685
|
conditions: [
|
|
2617
2686
|
...chain?.conditions ?? [],
|
|
2618
2687
|
...joinCondition ? [joinCondition] : [],
|
|
2619
|
-
...relationWhere ? [relationWhere] : []
|
|
2688
|
+
...relationWhere ? [relationWhere] : [],
|
|
2689
|
+
...policy ? [policy] : []
|
|
2620
2690
|
],
|
|
2621
2691
|
table: aliasedTarget
|
|
2622
2692
|
};
|
|
@@ -3001,9 +3071,9 @@ const primaryKeyOrderExprs = (table, pkNames) => {
|
|
|
3001
3071
|
return pkNames.map((n) => cols[n]).filter(Boolean).map((col) => asc(col));
|
|
3002
3072
|
};
|
|
3003
3073
|
/**
|
|
3004
|
-
* Every set of columns that
|
|
3005
|
-
*
|
|
3006
|
-
*
|
|
3074
|
+
* Every set of columns that is unique on `table`: the primary key first, then each unique
|
|
3075
|
+
* constraint and unique index, then each column declared `.unique()` inline. Sets are
|
|
3076
|
+
* property names (what GraphQL inputs use), not database column names.
|
|
3007
3077
|
*
|
|
3008
3078
|
* `getTableConfig` is the dialect's own — the three dialects expose the same
|
|
3009
3079
|
* `{ primaryKeys, uniqueConstraints, indexes }` shape but from different modules, so the
|
|
@@ -3012,9 +3082,13 @@ const primaryKeyOrderExprs = (table, pkNames) => {
|
|
|
3012
3082
|
* Index entries whose columns are SQL expressions rather than plain columns are skipped:
|
|
3013
3083
|
* an expression index is a valid conflict target in the database but cannot be named by a
|
|
3014
3084
|
* column enum. Deduplicated, order-insensitive — a column that is both the primary key and
|
|
3015
|
-
* a unique constraint yields one set
|
|
3085
|
+
* a unique constraint yields one set, and a set that is unique both outright and under a
|
|
3086
|
+
* partial index keeps the outright one.
|
|
3087
|
+
*
|
|
3088
|
+
* A partial unique index carries its predicate as `where`: it is a conflict target only
|
|
3089
|
+
* together with that predicate, and it does not make the set unique across the whole table.
|
|
3016
3090
|
*/
|
|
3017
|
-
const
|
|
3091
|
+
const getUniqueTargets = (table, getTableConfig) => {
|
|
3018
3092
|
const cols = visibleColumns(table);
|
|
3019
3093
|
const propNameByColumnName = new Map(Object.entries(cols).map(([propName, col]) => [col.name, propName]));
|
|
3020
3094
|
const toPropNames = (columnNames) => {
|
|
@@ -3026,25 +3100,40 @@ const getUniqueColumnSets = (table, getTableConfig) => {
|
|
|
3026
3100
|
}
|
|
3027
3101
|
return propNames.length ? propNames : void 0;
|
|
3028
3102
|
};
|
|
3103
|
+
const whole = (columns) => columns ? { columns } : void 0;
|
|
3029
3104
|
const config = getTableConfig(table);
|
|
3030
3105
|
const candidates = [
|
|
3031
|
-
Object.entries(cols).filter(([, col]) => col.primary).map(([propName]) => propName),
|
|
3032
|
-
...config.primaryKeys.map((pk) => toPropNames(pk.columns.map((c) => c.name))),
|
|
3033
|
-
...(config.uniqueConstraints ?? []).map((uc) => toPropNames(uc.columns.map((c) => c.name))),
|
|
3034
|
-
...(config.indexes ?? []).filter((index) => index.config.unique).map((index) =>
|
|
3035
|
-
|
|
3106
|
+
whole(Object.entries(cols).filter(([, col]) => col.primary).map(([propName]) => propName)),
|
|
3107
|
+
...config.primaryKeys.map((pk) => whole(toPropNames(pk.columns.map((c) => c.name)))),
|
|
3108
|
+
...(config.uniqueConstraints ?? []).map((uc) => whole(toPropNames(uc.columns.map((c) => c.name)))),
|
|
3109
|
+
...(config.indexes ?? []).filter((index) => index.config.unique).map((index) => {
|
|
3110
|
+
const columns = toPropNames(index.config.columns.map((c) => c?.name));
|
|
3111
|
+
return columns ? {
|
|
3112
|
+
columns,
|
|
3113
|
+
...index.config.where ? { where: index.config.where } : {}
|
|
3114
|
+
} : void 0;
|
|
3115
|
+
}),
|
|
3116
|
+
...Object.entries(cols).filter(([, col]) => col.isUnique).map(([propName]) => ({ columns: [propName] }))
|
|
3036
3117
|
];
|
|
3037
|
-
const
|
|
3038
|
-
const
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
const
|
|
3042
|
-
if (seen.
|
|
3043
|
-
seen.
|
|
3044
|
-
sets.push(set);
|
|
3118
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
3119
|
+
for (const candidate of candidates) {
|
|
3120
|
+
if (!candidate?.columns.length) continue;
|
|
3121
|
+
const key = [...candidate.columns].sort().join(",");
|
|
3122
|
+
const seen = byKey.get(key);
|
|
3123
|
+
if (!seen) byKey.set(key, candidate);
|
|
3124
|
+
else if (seen.where && !candidate.where) byKey.set(key, { columns: seen.columns });
|
|
3045
3125
|
}
|
|
3046
|
-
return
|
|
3126
|
+
return [...byKey.values()];
|
|
3047
3127
|
};
|
|
3128
|
+
/**
|
|
3129
|
+
* The column sets that identify at most one row of `table` — {@link getUniqueTargets}
|
|
3130
|
+
* without the partial unique indexes, which only constrain the rows their predicate
|
|
3131
|
+
* matches (a soft-deleted duplicate is allowed), and so do not.
|
|
3132
|
+
*
|
|
3133
|
+
* Pass `{ partial: true }` to keep them, for callers that only need to know a set is
|
|
3134
|
+
* unique among the rows that matter.
|
|
3135
|
+
*/
|
|
3136
|
+
const getUniqueColumnSets = (table, getTableConfig, options = {}) => getUniqueTargets(table, getTableConfig).filter((target) => options.partial || !target.where).map((target) => target.columns);
|
|
3048
3137
|
//#endregion
|
|
3049
3138
|
//#region src/util/builders/common/distinct.ts
|
|
3050
3139
|
/**
|
|
@@ -3827,10 +3916,10 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
|
|
|
3827
3916
|
});
|
|
3828
3917
|
const relWhere = relationArgs?.where;
|
|
3829
3918
|
const relDeleted = relationArgs?.deleted ?? scope?.relationDefault(targetTableName, is(relEntry.relation, One) && relEntry.relation.optional === false);
|
|
3830
|
-
thisRecord.where = relWhere || scope?.has(targetTableName, relDeleted) ? { RAW: (aliasedTable) => withScope(scope, targetTableName, aliasedTable, relWhere ? extractFilters(aliasedTable, relName, relWhere, relationFilterCtx(filterCtx, targetTableName)) : void 0, relDeleted) } : void 0;
|
|
3919
|
+
thisRecord.where = relWhere || scope?.has(targetTableName, relDeleted) ? { RAW: (aliasedTable) => withScope(scope, targetTableName, aliasedTable, relWhere ? extractFilters(aliasedTable, relName, relWhere, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, relDeleted) } : void 0;
|
|
3831
3920
|
const hasPagination = offset != null || limit != null;
|
|
3832
3921
|
const pkNames = targetPkNames ?? [];
|
|
3833
|
-
thisRecord.orderBy = relationArgs?.orderBy ? (aliasedTable) => extractOrderBy(aliasedTable, relationArgs.orderBy, relationFilterCtx(filterCtx, targetTableName), relWhere) : hasPagination && pkNames.length ? (aliasedTable) => primaryKeyOrderExprs(aliasedTable, pkNames) : void 0;
|
|
3922
|
+
thisRecord.orderBy = relationArgs?.orderBy ? (aliasedTable) => extractOrderBy(aliasedTable, relationArgs.orderBy, relationFilterCtx(filterCtx, targetTableName, scope), relWhere) : hasPagination && pkNames.length ? (aliasedTable) => primaryKeyOrderExprs(aliasedTable, pkNames) : void 0;
|
|
3834
3923
|
thisRecord.offset = offset;
|
|
3835
3924
|
thisRecord.limit = limit;
|
|
3836
3925
|
thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
|
|
@@ -3888,7 +3977,7 @@ const runRelationalSelect = async (opts) => {
|
|
|
3888
3977
|
tableName,
|
|
3889
3978
|
distinct,
|
|
3890
3979
|
pkNames: pkNames ?? [],
|
|
3891
|
-
where: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0, deleted),
|
|
3980
|
+
where: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0, deleted),
|
|
3892
3981
|
orderBy,
|
|
3893
3982
|
limit: single ? 1 : opts.limit,
|
|
3894
3983
|
offset
|
|
@@ -3903,8 +3992,8 @@ const runRelationalSelect = async (opts) => {
|
|
|
3903
3992
|
allRelations: filterCtx?.relationMap
|
|
3904
3993
|
}),
|
|
3905
3994
|
offset: distinctKeys ? void 0 : offset,
|
|
3906
|
-
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,
|
|
3907
|
-
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,
|
|
3995
|
+
orderBy: distinctKeys ? (aliasedTable) => [...orderBy ? extractOrderBy(aliasedTable, orderBy, relationFilterCtx(filterCtx, tableName, scope), where) : [], ...primaryKeyOrderExprs(aliasedTable, pkNames)] : cursorEntries ? (aliasedTable) => cursorOrderExprs(aliasedTable, cursorEntries) : orderBy ? (aliasedTable) => extractOrderBy(aliasedTable, orderBy, relationFilterCtx(filterCtx, tableName, scope), where) : needsDefaultOrder && pkNames?.length ? (aliasedTable) => primaryKeyOrderExprs(aliasedTable, pkNames) : void 0,
|
|
3996
|
+
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, scope)) : void 0, cursorValues ? buildCursorCondition(aliasedTable, cursorEntries, cursorValues, opts.nullOrdering ?? "nulls-smallest") : void 0), deleted) } : void 0,
|
|
3908
3997
|
with: relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, {
|
|
3909
3998
|
typeNameMapper,
|
|
3910
3999
|
filterCtx,
|
|
@@ -4486,7 +4575,7 @@ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, f
|
|
|
4486
4575
|
tableName,
|
|
4487
4576
|
table,
|
|
4488
4577
|
where,
|
|
4489
|
-
relationCtx: relationFilterCtx(filterCtx, tableName),
|
|
4578
|
+
relationCtx: relationFilterCtx(filterCtx, tableName, scope),
|
|
4490
4579
|
required: requireWhere,
|
|
4491
4580
|
deleted: hard ? "INCLUDE" : void 0
|
|
4492
4581
|
});
|
|
@@ -4674,6 +4763,19 @@ const keysFromWhere = (where, primaryKey) => {
|
|
|
4674
4763
|
return none;
|
|
4675
4764
|
};
|
|
4676
4765
|
/**
|
|
4766
|
+
* Whether an upsert's `onConflict` leaves the primary-key values in `values` naming exactly
|
|
4767
|
+
* the rows it writes. It does not when the conflict is detected on another unique key — the
|
|
4768
|
+
* row updated is whichever one holds that key, not the one the supplied primary key names —
|
|
4769
|
+
* nor when conflicting rows can be skipped: `action: NOTHING`, or a `where` guard.
|
|
4770
|
+
*/
|
|
4771
|
+
const conflictsOnKeyAlone = (onConflict, primaryKey) => {
|
|
4772
|
+
if (!onConflict || typeof onConflict !== "object") return true;
|
|
4773
|
+
const { target, action, where } = onConflict;
|
|
4774
|
+
if (action === "NOTHING" || where !== void 0 && where !== null) return false;
|
|
4775
|
+
if (!Array.isArray(target) || !target.length) return true;
|
|
4776
|
+
return [...target].sort().join(",") === [...primaryKey].sort().join(",");
|
|
4777
|
+
};
|
|
4778
|
+
/**
|
|
4677
4779
|
* The rows a generated field's arguments are about, as primary-key values.
|
|
4678
4780
|
*
|
|
4679
4781
|
* The row identity of a mutation lives in a different argument for every operation —
|
|
@@ -4709,7 +4811,7 @@ const identifyRows = (field, args) => {
|
|
|
4709
4811
|
table,
|
|
4710
4812
|
primaryKey,
|
|
4711
4813
|
rows: rows.filter((row) => !!row),
|
|
4712
|
-
complete: entries.length > 0 && rows.every(Boolean)
|
|
4814
|
+
complete: entries.length > 0 && rows.every(Boolean) && (meta.operation !== "upsert" || conflictsOnKeyAlone(args?.["onConflict"], primaryKey))
|
|
4713
4815
|
};
|
|
4714
4816
|
}
|
|
4715
4817
|
if (targetArg === "updates") {
|
|
@@ -4879,7 +4981,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4879
4981
|
const executor = resolveExecutor(db, context);
|
|
4880
4982
|
const uniqueIds = [...new Set(parentIds)];
|
|
4881
4983
|
const scope = resolveScope(policies, context, filterCtx);
|
|
4882
|
-
let whereCondition = withScope(scope, targetTableName, targetTable, and(inArray(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
|
|
4984
|
+
let whereCondition = withScope(scope, targetTableName, targetTable, and(inArray(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
|
|
4883
4985
|
if (distinct) {
|
|
4884
4986
|
const keys = await selectDistinctKeys({
|
|
4885
4987
|
db: executor,
|
|
@@ -4904,10 +5006,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4904
5006
|
[PARENT_KEY]: groupCol
|
|
4905
5007
|
} : columns;
|
|
4906
5008
|
let rows;
|
|
4907
|
-
if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName), whereArg, junction);
|
|
5009
|
+
if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName, scope), whereArg, junction);
|
|
4908
5010
|
else {
|
|
4909
5011
|
let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
|
|
4910
|
-
const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName), whereArg) : [];
|
|
5012
|
+
const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName, scope), whereArg) : [];
|
|
4911
5013
|
if (orderExprs.length) q = q.orderBy(...orderExprs);
|
|
4912
5014
|
rows = await q;
|
|
4913
5015
|
}
|
|
@@ -5587,6 +5689,7 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5587
5689
|
const normalized = {};
|
|
5588
5690
|
for (const [columnName, entry] of Object.entries(orderBy)) {
|
|
5589
5691
|
if (!columns[columnName]) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which is not a column of '${tableName}'.`);
|
|
5692
|
+
if (config?.exclude?.columns?.[tableName]?.includes(columnName)) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which config.exclude.columns hides. A default ordering is encoded into every row's cursor, so it can only use columns the schema exposes.`);
|
|
5590
5693
|
const direction = typeof entry === "string" ? entry : entry?.direction;
|
|
5591
5694
|
if (direction !== "asc" && direction !== "desc") throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy.${columnName} must be 'asc', 'desc', or { direction, priority? }.`);
|
|
5592
5695
|
const priority = typeof entry === "string" ? void 0 : entry?.priority;
|
|
@@ -5967,7 +6070,8 @@ const generateAggregate = (db, tableName, table, typeName, fieldName, filterArgs
|
|
|
5967
6070
|
const request = parseAggregateRequest(info, target);
|
|
5968
6071
|
if (!Object.keys(request.selection).length) return {};
|
|
5969
6072
|
let query = resolveExecutor(db, context).select(request.selection).from(table);
|
|
5970
|
-
const
|
|
6073
|
+
const scope = resolveScope(policies, context, filterCtx);
|
|
6074
|
+
const where = withScope(scope, tableName, table, args.where ? extractFilters(table, tableName, args.where, relationFilterCtx(filterCtx, tableName, scope)) : void 0, args.deleted);
|
|
5971
6075
|
if (where) query = query.where(where);
|
|
5972
6076
|
const rows = await query;
|
|
5973
6077
|
return assembleAggregateRow(rows[0] ?? {}, request, target);
|
|
@@ -6044,7 +6148,8 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
6044
6148
|
const loader = getOrCreateLoader(context, loaderKey, async (parentIds) => {
|
|
6045
6149
|
const executor = resolveExecutor(db, context);
|
|
6046
6150
|
const uniqueIds = [...new Set(parentIds)];
|
|
6047
|
-
const
|
|
6151
|
+
const scope = resolveScope(policies, context, filterCtx);
|
|
6152
|
+
const whereCondition = withScope(scope, targetTableName, targetTable, and(inArray(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
|
|
6048
6153
|
const selected = executor.select({
|
|
6049
6154
|
[GROUP_KEY]: groupCol,
|
|
6050
6155
|
...request.selection
|
|
@@ -6260,7 +6365,8 @@ const generateGroupBy = (db, tableName, table, typeName, fieldName, filterArgs,
|
|
|
6260
6365
|
...request.selection
|
|
6261
6366
|
};
|
|
6262
6367
|
let query = resolveExecutor(db, context).select(selection).from(table);
|
|
6263
|
-
const
|
|
6368
|
+
const scope = resolveScope(policies, context, filterCtx);
|
|
6369
|
+
const where = withScope(scope, tableName, table, args.where ? extractFilters(table, tableName, args.where, relationFilterCtx(filterCtx, tableName, scope)) : void 0, args.deleted);
|
|
6264
6370
|
if (where) query = query.where(where);
|
|
6265
6371
|
query = query.groupBy(...keyColumns.map(([, column]) => column));
|
|
6266
6372
|
const havingCondition = buildHavingCondition(args.having, columns);
|
|
@@ -6299,6 +6405,54 @@ const generateGroupBy = (db, tableName, table, typeName, fieldName, filterArgs,
|
|
|
6299
6405
|
};
|
|
6300
6406
|
};
|
|
6301
6407
|
//#endregion
|
|
6408
|
+
//#region src/util/builders/common/build-registry.ts
|
|
6409
|
+
/**
|
|
6410
|
+
* Keyed by the `extensions.drizzle` object of every object type the build generated, rather
|
|
6411
|
+
* than by the type: a schema composed from `entities.types` keeps the types, and one rebuilt
|
|
6412
|
+
* by a schema-transforming tool (`mapSchema`, stitching) keeps their extensions.
|
|
6413
|
+
*/
|
|
6414
|
+
const runtimeByTypeMeta = /* @__PURE__ */ new WeakMap();
|
|
6415
|
+
/**
|
|
6416
|
+
* What is known about the builds of one drizzle instance, for a caller with no resolve info to
|
|
6417
|
+
* look its build up by. Only a lone build is kept — with two or more there is no telling which
|
|
6418
|
+
* one a selection belongs to — so rebuilding a schema repeatedly holds on to nothing extra.
|
|
6419
|
+
*/
|
|
6420
|
+
const buildsByDb = /* @__PURE__ */ new WeakMap();
|
|
6421
|
+
/** Whether a build changes what a read returns at all: a row policy, a limit or a default ordering. */
|
|
6422
|
+
const hasPolicies = (runtime) => !!(runtime.policies?.scope || runtime.policies?.defaultOrderBy || runtime.limits);
|
|
6423
|
+
/** Records a finished build against the object types it generated and the instance it read. */
|
|
6424
|
+
const registerBuildRuntime = (runtime, types) => {
|
|
6425
|
+
for (const type of types) {
|
|
6426
|
+
const meta = drizzleExtension(type);
|
|
6427
|
+
if (meta) runtimeByTypeMeta.set(meta, runtime);
|
|
6428
|
+
}
|
|
6429
|
+
const previous = buildsByDb.get(runtime.db);
|
|
6430
|
+
buildsByDb.set(runtime.db, previous ? {
|
|
6431
|
+
count: previous.count + 1,
|
|
6432
|
+
anyPolicies: previous.anyPolicies || hasPolicies(runtime)
|
|
6433
|
+
} : {
|
|
6434
|
+
count: 1,
|
|
6435
|
+
only: runtime,
|
|
6436
|
+
anyPolicies: hasPolicies(runtime)
|
|
6437
|
+
});
|
|
6438
|
+
};
|
|
6439
|
+
/** The build that generated `type`, when one did. */
|
|
6440
|
+
const runtimeForType = (type) => {
|
|
6441
|
+
const meta = type && typeof type === "object" ? drizzleExtension(type) : void 0;
|
|
6442
|
+
return meta ? runtimeByTypeMeta.get(meta) : void 0;
|
|
6443
|
+
};
|
|
6444
|
+
/**
|
|
6445
|
+
* The build of `db` to use when no generated type identifies one: the only build, or none
|
|
6446
|
+
* when no build applies any policy. Throws when several builds exist and one of them would
|
|
6447
|
+
* change the read, since picking either could apply the wrong policies — or none.
|
|
6448
|
+
*/
|
|
6449
|
+
const runtimeForDb = (db, helper) => {
|
|
6450
|
+
const builds = buildsByDb.get(db);
|
|
6451
|
+
if (!builds) return;
|
|
6452
|
+
if (builds.count === 1) return builds.only;
|
|
6453
|
+
if (builds.anyPolicies) throw new Error(`Drizzle-GraphQL Error: ${helper} cannot tell which of the ${builds.count} schemas built over this database the selection belongs to, and at least one applies row policies (scope, softDelete, limits or defaults). Pass the resolver's \`info\` rather than a parsed selection tree, from a field whose type the build generated.`);
|
|
6454
|
+
};
|
|
6455
|
+
//#endregion
|
|
6302
6456
|
//#region src/util/builders/nested-writes.ts
|
|
6303
6457
|
const ALL_OPS = [
|
|
6304
6458
|
"create",
|
|
@@ -6791,7 +6945,7 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6791
6945
|
* nothing is rejected: `connect: {}` would otherwise attach every row in the table.
|
|
6792
6946
|
*/
|
|
6793
6947
|
const conditionOf = (plan, filter, operation, scope) => {
|
|
6794
|
-
const condition = extractFilters(plan.targetTable, plan.targetTableName, filter, relationFilterCtx(filterCtx, plan.targetTableName));
|
|
6948
|
+
const condition = extractFilters(plan.targetTable, plan.targetTableName, filter, relationFilterCtx(filterCtx, plan.targetTableName, scope));
|
|
6795
6949
|
if (!condition) throw drizzleError(`Drizzle-GraphQL Error: '${operation}' on '${plan.relationName}' needs a filter that selects rows — it was given one that matches everything.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
6796
6950
|
return withScope(scope, plan.targetTableName, plan.targetTable, condition);
|
|
6797
6951
|
};
|
|
@@ -7186,7 +7340,7 @@ const createSchemaBuilder = (adapter) => {
|
|
|
7186
7340
|
uniqueKeysOf: (tableName) => uniqueKeys[tableName]
|
|
7187
7341
|
};
|
|
7188
7342
|
adapter.preflight?.(db, options);
|
|
7189
|
-
const nestedPlans = features.nestedWrites ? buildNestedWritePlans(tables, namedRelations, (target) =>
|
|
7343
|
+
const nestedPlans = features.nestedWrites ? buildNestedWritePlans(tables, namedRelations, (target) => getUniqueColumnSets(target, adapter.getTableConfig, { partial: true }), (target) => primaryKeyPropNames(target), extractRelationJoinColumns, (junction) => schemaEntries.find(([, value]) => value === junction)?.[0]) : void 0;
|
|
7190
7344
|
if (nestedPlans) {
|
|
7191
7345
|
const omitted = gateNestedWritePlans(nestedPlans, featureOf, (tableName) => excludedTables.has(tableName));
|
|
7192
7346
|
if (omitted.length) {
|
|
@@ -7402,6 +7556,14 @@ const createSchemaBuilder = (adapter) => {
|
|
|
7402
7556
|
}
|
|
7403
7557
|
if (Object.keys(relResolvers).length > 0) fieldResolvers[tableName] = relResolvers;
|
|
7404
7558
|
}
|
|
7559
|
+
registerBuildRuntime({
|
|
7560
|
+
db: ctx.db,
|
|
7561
|
+
filterCtx: ctx.filterCtx,
|
|
7562
|
+
policies: ctx.policies,
|
|
7563
|
+
scoped: !!ctx.tablePolicies?.scope,
|
|
7564
|
+
limits: ctx.limits,
|
|
7565
|
+
nullOrdering: adapter.nullOrdering
|
|
7566
|
+
}, Object.values(ctx.outputs));
|
|
7405
7567
|
return {
|
|
7406
7568
|
queries: ctx.queries,
|
|
7407
7569
|
mutations: ctx.mutations,
|
|
@@ -7474,20 +7636,33 @@ const generateUpsert = (db, tableName, table, baseType, onConflictType, fieldNam
|
|
|
7474
7636
|
}
|
|
7475
7637
|
},
|
|
7476
7638
|
run: async ({ executor, args, context, before, after }) => {
|
|
7477
|
-
const
|
|
7639
|
+
const contextColumns = policies?.contextValues?.(tableName);
|
|
7640
|
+
const input = applyContextValuesAll(single ? [remapFromGraphQLSingleInput(args.values, table)] : remapFromGraphQLArrayInput(args.values, table), contextColumns, context);
|
|
7478
7641
|
if (!input.length) throw drizzleError("No values were provided!", { code: "DRIZZLE_NO_VALUES" });
|
|
7479
7642
|
await before();
|
|
7480
|
-
const
|
|
7643
|
+
const batches = resolveConflictPlan({
|
|
7481
7644
|
table,
|
|
7482
7645
|
values: input,
|
|
7483
7646
|
onConflict: args.onConflict,
|
|
7484
7647
|
pkNames,
|
|
7485
7648
|
uniqueSets: [],
|
|
7486
7649
|
excludedRef: mysqlValuesColumnRef,
|
|
7487
|
-
withTarget: false
|
|
7650
|
+
withTarget: false,
|
|
7651
|
+
serverOwned: Object.keys(contextColumns ?? {})
|
|
7652
|
+
});
|
|
7653
|
+
const requestedNothing = args.onConflict?.action === "NOTHING";
|
|
7654
|
+
const runBatch = async (target, { values, plan }) => {
|
|
7655
|
+
if (plan.action === "NOTHING" && !requestedNothing) {
|
|
7656
|
+
const columns = getColumns(table);
|
|
7657
|
+
const keyName = pkNames[0] ?? Object.keys(columns)[0];
|
|
7658
|
+
await target.insert(table).values(values).onDuplicateKeyUpdate({ set: { [keyName]: sql`${sql.identifier(columns[keyName].name)}` } });
|
|
7659
|
+
} else if (plan.action === "NOTHING") await target.insert(table).ignore().values(values);
|
|
7660
|
+
else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
|
|
7661
|
+
};
|
|
7662
|
+
if (batches.length === 1) await runBatch(executor, batches[0]);
|
|
7663
|
+
else await executor.transaction(async (tx) => {
|
|
7664
|
+
for (const batch of batches) await runBatch(tx, batch);
|
|
7488
7665
|
});
|
|
7489
|
-
if (plan.action === "NOTHING") await executor.insert(table).ignore().values(input);
|
|
7490
|
-
else await executor.insert(table).values(input).onDuplicateKeyUpdate({ set: plan.set });
|
|
7491
7666
|
await after();
|
|
7492
7667
|
return isSuccess;
|
|
7493
7668
|
}
|
|
@@ -7511,7 +7686,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
|
|
|
7511
7686
|
const input = stripContextValues(remapUpdateInput(set, table, tableName), policies?.contextValues?.(tableName));
|
|
7512
7687
|
if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
|
|
7513
7688
|
await before();
|
|
7514
|
-
const relationCtx = relationFilterCtx(filterCtx, tableName);
|
|
7689
|
+
const relationCtx = relationFilterCtx(filterCtx, tableName, scope);
|
|
7515
7690
|
const filters = scopedWhere({
|
|
7516
7691
|
scope,
|
|
7517
7692
|
tableName,
|
|
@@ -7557,7 +7732,7 @@ const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName,
|
|
|
7557
7732
|
if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
|
|
7558
7733
|
return {
|
|
7559
7734
|
set: input,
|
|
7560
|
-
filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0)
|
|
7735
|
+
filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0)
|
|
7561
7736
|
};
|
|
7562
7737
|
});
|
|
7563
7738
|
await executor.transaction(async (tx) => {
|
|
@@ -7600,7 +7775,7 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
|
|
|
7600
7775
|
const { where } = args;
|
|
7601
7776
|
const hard = canHardDelete && args.hard === true;
|
|
7602
7777
|
const scope = policies?.scope?.(context);
|
|
7603
|
-
const relationCtx = relationFilterCtx(filterCtx, tableName);
|
|
7778
|
+
const relationCtx = relationFilterCtx(filterCtx, tableName, scope);
|
|
7604
7779
|
const filters = scopedWhere({
|
|
7605
7780
|
scope,
|
|
7606
7781
|
tableName,
|
|
@@ -7933,7 +8108,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
7933
8108
|
});
|
|
7934
8109
|
const returning = withHookReturning(nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns);
|
|
7935
8110
|
const runUpsert = async (target, values) => {
|
|
7936
|
-
const
|
|
8111
|
+
const batches = resolveConflictPlan({
|
|
7937
8112
|
table,
|
|
7938
8113
|
values,
|
|
7939
8114
|
onConflict: args.onConflict,
|
|
@@ -7941,16 +8116,29 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
7941
8116
|
uniqueSets,
|
|
7942
8117
|
excludedRef: excludedColumnRef,
|
|
7943
8118
|
withTarget: true,
|
|
7944
|
-
|
|
8119
|
+
serverOwned: Object.keys(contextColumns ?? {}),
|
|
8120
|
+
buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope))
|
|
7945
8121
|
});
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
8122
|
+
const runBatch = async (executor, { values: rows, plan }) => {
|
|
8123
|
+
let query = executor.insert(table).values(rows).returning(returning);
|
|
8124
|
+
const setWhere = withScope(scope, tableName, table, plan.setWhere);
|
|
8125
|
+
query = plan.action === "NOTHING" ? query.onConflictDoNothing(plan.target ? {
|
|
8126
|
+
target: plan.target,
|
|
8127
|
+
where: plan.targetWhere
|
|
8128
|
+
} : void 0) : query.onConflictDoUpdate({
|
|
8129
|
+
target: plan.target,
|
|
8130
|
+
targetWhere: plan.targetWhere,
|
|
8131
|
+
set: plan.set,
|
|
8132
|
+
setWhere
|
|
8133
|
+
});
|
|
8134
|
+
return await query;
|
|
8135
|
+
};
|
|
8136
|
+
if (batches.length === 1) return runBatch(target, batches[0]);
|
|
8137
|
+
return target.transaction(async (tx) => {
|
|
8138
|
+
const rows = [];
|
|
8139
|
+
for (const batch of batches) rows.push(...await runBatch(tx, batch));
|
|
8140
|
+
return rows;
|
|
7952
8141
|
});
|
|
7953
|
-
return await query;
|
|
7954
8142
|
};
|
|
7955
8143
|
const written = nestedEntries ? await writeWithNestedOps({
|
|
7956
8144
|
executor,
|
|
@@ -8011,7 +8199,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8011
8199
|
const nestedOps = entry && nested.hasOps(entry.ops) ? entry.ops : void 0;
|
|
8012
8200
|
const input = stripContextValues(remapUpdateInput(entry ? entry.columns : set, table, tableName), policies?.contextValues?.(tableName));
|
|
8013
8201
|
if (!Object.keys(input).length && !nestedOps) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
|
|
8014
|
-
const relationCtx = relationFilterCtx(filterCtx, tableName);
|
|
8202
|
+
const relationCtx = relationFilterCtx(filterCtx, tableName, scope);
|
|
8015
8203
|
const filters = scopedWhere({
|
|
8016
8204
|
scope,
|
|
8017
8205
|
tableName,
|
|
@@ -8090,7 +8278,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8090
8278
|
await before();
|
|
8091
8279
|
const parsedInfo = parseResolveInfo(info, { deep: true });
|
|
8092
8280
|
const columns = extractSelectedColumnsFromTreeSQLFormat(parsedInfo.fieldsByTypeName[applyObjectTypeName(typeName, tableName, resolveName)], table, selectionCtx);
|
|
8093
|
-
const relationCtx = relationFilterCtx(filterCtx, tableName);
|
|
8281
|
+
const relationCtx = relationFilterCtx(filterCtx, tableName, scope);
|
|
8094
8282
|
const filters = scopedWhere({
|
|
8095
8283
|
scope,
|
|
8096
8284
|
tableName,
|
|
@@ -8130,7 +8318,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8130
8318
|
*/
|
|
8131
8319
|
const createSchemaDataGenerator = (adapter) => {
|
|
8132
8320
|
const primaryKeyPropNames = adapter.primaryKeyPropNames;
|
|
8133
|
-
const
|
|
8321
|
+
const uniqueTargets = (table) => getUniqueTargets(table, adapter.getTableConfig);
|
|
8134
8322
|
const { generateInsert, generateUpsert, generateUpdate, generateDelete } = buildWriteResolvers(primaryKeyPropNames);
|
|
8135
8323
|
const { prepareBuild, forEachTable, finalizeBuild } = createSchemaBuilder(adapter);
|
|
8136
8324
|
return (db, schema, relations, options) => {
|
|
@@ -8171,7 +8359,7 @@ const createSchemaDataGenerator = (adapter) => {
|
|
|
8171
8359
|
single: true,
|
|
8172
8360
|
conflictDoNothing
|
|
8173
8361
|
}) : void 0;
|
|
8174
|
-
const uniqueSets = tableFeatures.upsert ?
|
|
8362
|
+
const uniqueSets = tableFeatures.upsert ? uniqueTargets(schema[tableName]) : [];
|
|
8175
8363
|
const onConflictInput = tableFeatures.upsert ? generateOnConflictInput({
|
|
8176
8364
|
table: schema[tableName],
|
|
8177
8365
|
tableName,
|
|
@@ -8541,7 +8729,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
|
|
|
8541
8729
|
return {
|
|
8542
8730
|
set: input,
|
|
8543
8731
|
ops,
|
|
8544
|
-
filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0)
|
|
8732
|
+
filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0)
|
|
8545
8733
|
};
|
|
8546
8734
|
});
|
|
8547
8735
|
const anyNested = entries.some((entry) => entry.ops);
|
|
@@ -8711,6 +8899,36 @@ const namingRuleOf = (options) => resolveGeneratedTypeNames({
|
|
|
8711
8899
|
typeNameSuffix: options.typeNameSuffix
|
|
8712
8900
|
});
|
|
8713
8901
|
/**
|
|
8902
|
+
* The build whose policies the read applies. Looked up through the generated types the
|
|
8903
|
+
* resolve info reaches — the field's return type, the root table's type in the executing
|
|
8904
|
+
* schema, the parent type — so a resolver over a generated type finds its own build. Without
|
|
8905
|
+
* resolve info (a parsed tree was passed), the instance's only build is used.
|
|
8906
|
+
*/
|
|
8907
|
+
const runtimeFor = (info, db, typeName, helper) => {
|
|
8908
|
+
if (!("fieldsByTypeName" in info)) {
|
|
8909
|
+
const candidates = [
|
|
8910
|
+
info.returnType ? getNamedType(info.returnType) : void 0,
|
|
8911
|
+
info.schema?.getType(typeName),
|
|
8912
|
+
info.parentType
|
|
8913
|
+
];
|
|
8914
|
+
for (const candidate of candidates) {
|
|
8915
|
+
const runtime = runtimeForType(candidate);
|
|
8916
|
+
if (runtime && runtime.db === db) return runtime;
|
|
8917
|
+
}
|
|
8918
|
+
}
|
|
8919
|
+
return runtimeForDb(db, helper);
|
|
8920
|
+
};
|
|
8921
|
+
/**
|
|
8922
|
+
* The request's row policies under `runtime`, bound to the context the caller passed. A build
|
|
8923
|
+
* with `scope` hooks cannot be read without that context: the hooks would be called with
|
|
8924
|
+
* nothing, and running the read unscoped is the leak the option exists to prevent.
|
|
8925
|
+
*/
|
|
8926
|
+
const scopeFor = (runtime, options, helper) => {
|
|
8927
|
+
if (!runtime?.policies?.scope) return;
|
|
8928
|
+
if (runtime.scoped && !("context" in options)) throw new Error(`Drizzle-GraphQL Error: the schema this selection belongs to configures \`scope\`, so ${helper} needs the request context to apply it. Pass the resolver's context as the \`context\` option.`);
|
|
8929
|
+
return runtime.policies.scope(options.context);
|
|
8930
|
+
};
|
|
8931
|
+
/**
|
|
8714
8932
|
* Reads the parsed selection tree out of a resolver's `info`, resolving fragments and
|
|
8715
8933
|
* inline fragments. Accepts an already-parsed tree so a caller that has one (or is chaining
|
|
8716
8934
|
* these helpers) does not pay for the walk twice.
|
|
@@ -8737,6 +8955,10 @@ const parseInfo = (info) => {
|
|
|
8737
8955
|
* const row = await tx.query.Users.findFirst({ where: { id: { eq } }, with: withTree });
|
|
8738
8956
|
* ```
|
|
8739
8957
|
*
|
|
8958
|
+
* Every nested entry carries the build's row policies — `scope`, `softDelete`, `limits` and
|
|
8959
|
+
* `defaults.orderBy` — exactly as the generated `with:` clause does. The build is found
|
|
8960
|
+
* through the resolve info; pass `context` when it configures `scope`.
|
|
8961
|
+
*
|
|
8740
8962
|
* Returns `undefined` when the selection asks for no relations, which is the value the
|
|
8741
8963
|
* query builder wants for "no `with` clause".
|
|
8742
8964
|
*/
|
|
@@ -8746,11 +8968,16 @@ const selectionToWith = (info, options) => {
|
|
|
8746
8968
|
const { tables, relationMap, filterCtx } = contextFor(db);
|
|
8747
8969
|
if (!tables[table]) throw new Error(`Drizzle-GraphQL Error: '${table}' is not a table in the Drizzle schema.`);
|
|
8748
8970
|
if (!relationMap[table]) return;
|
|
8971
|
+
const typeName = resolveObjectTypeName(table, typeNameMapper, resolveName);
|
|
8972
|
+
const runtime = runtimeFor(info, db, typeName, "selectionToWith");
|
|
8749
8973
|
const parsed = parseInfo(info);
|
|
8750
|
-
const params = extractRelationsParams(relationMap, tables, table, parsed,
|
|
8974
|
+
const params = extractRelationsParams(relationMap, tables, table, parsed, typeName, {
|
|
8751
8975
|
typeNameMapper,
|
|
8752
|
-
filterCtx,
|
|
8753
|
-
resolveName
|
|
8976
|
+
filterCtx: runtime?.filterCtx ?? filterCtx,
|
|
8977
|
+
resolveName,
|
|
8978
|
+
scope: scopeFor(runtime, options, "selectionToWith"),
|
|
8979
|
+
limits: runtime?.limits,
|
|
8980
|
+
defaultOrderBy: runtime?.policies?.defaultOrderBy
|
|
8754
8981
|
});
|
|
8755
8982
|
return params && Object.keys(params).length ? params : void 0;
|
|
8756
8983
|
};
|
|
@@ -8762,14 +8989,16 @@ const selectionToWith = (info, options) => {
|
|
|
8762
8989
|
* This is what an override usually wants after it has done its own work:
|
|
8763
8990
|
*
|
|
8764
8991
|
* ```ts
|
|
8765
|
-
* const rows = await resolveSelection(info, { db, table: 'Users', where, executor: tx });
|
|
8992
|
+
* const rows = await resolveSelection(info, { db, table: 'Users', context, where, executor: tx });
|
|
8766
8993
|
* ```
|
|
8767
8994
|
*
|
|
8768
8995
|
* Relation arguments and aggregate fields keep behaving as they do in the generated schema,
|
|
8769
|
-
* because it is the generated schema's own code path.
|
|
8996
|
+
* because it is the generated schema's own code path. So do the build's row policies: the
|
|
8997
|
+
* root table and every nested relation are scoped and soft-delete-filtered, and the limit
|
|
8998
|
+
* policy and default ordering apply. Pass `context` when the build configures `scope`.
|
|
8770
8999
|
*/
|
|
8771
9000
|
const resolveSelection = async (info, options) => {
|
|
8772
|
-
const { db, table: tableName, typeNameMapper, executor, single = false, where, orderBy, limit, offset, after } = options;
|
|
9001
|
+
const { db, table: tableName, typeNameMapper, executor, single = false, where, orderBy, limit, offset, after, deleted } = options;
|
|
8773
9002
|
const resolveName = namingRuleOf(options);
|
|
8774
9003
|
const { tables, relationMap, filterCtx, primaryKeyOf } = contextFor(db);
|
|
8775
9004
|
const table = tables[tableName];
|
|
@@ -8779,25 +9008,34 @@ const resolveSelection = async (info, options) => {
|
|
|
8779
9008
|
const source = executor ?? db;
|
|
8780
9009
|
const queryBase = source.query?.[tableName];
|
|
8781
9010
|
if (!queryBase) throw new Error(`Drizzle-GraphQL Error: the executor passed to resolveSelection has no relational query builder for '${tableName}'.`);
|
|
9011
|
+
const typeName = resolveObjectTypeName(tableName, typeNameMapper, resolveName);
|
|
9012
|
+
const runtime = runtimeFor(info, db, typeName, "resolveSelection");
|
|
9013
|
+
const scope = scopeFor(runtime, options, "resolveSelection");
|
|
9014
|
+
const defaultOrderBy = runtime?.policies?.defaultOrderBy;
|
|
8782
9015
|
return runRelationalSelect({
|
|
8783
9016
|
queryBase,
|
|
8784
9017
|
tables,
|
|
8785
9018
|
tableName,
|
|
8786
9019
|
table,
|
|
8787
9020
|
relationMap,
|
|
8788
|
-
typeName
|
|
9021
|
+
typeName,
|
|
8789
9022
|
typeNameMapper,
|
|
8790
9023
|
resolveName,
|
|
8791
9024
|
parsedInfo,
|
|
8792
9025
|
single,
|
|
8793
9026
|
where,
|
|
8794
|
-
orderBy,
|
|
8795
|
-
limit,
|
|
9027
|
+
orderBy: withDefaultOrderBy({ orderBy }, tableName, defaultOrderBy).orderBy,
|
|
9028
|
+
limit: single ? limit : applyLimitPolicy(limit, runtime?.limits?.(tableName)),
|
|
8796
9029
|
offset,
|
|
8797
9030
|
after,
|
|
8798
|
-
filterCtx,
|
|
9031
|
+
filterCtx: runtime?.filterCtx ?? filterCtx,
|
|
8799
9032
|
pkNames: primaryKeyOf(tableName),
|
|
8800
|
-
db: source
|
|
9033
|
+
db: source,
|
|
9034
|
+
scope,
|
|
9035
|
+
deleted,
|
|
9036
|
+
limits: runtime?.limits,
|
|
9037
|
+
defaultOrderBy,
|
|
9038
|
+
nullOrdering: runtime?.nullOrdering
|
|
8801
9039
|
});
|
|
8802
9040
|
};
|
|
8803
9041
|
//#endregion
|