@vantreeseba/drizzle-graphql 15.1.4 → 15.1.5
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 +44 -10
- package/dist/index.cjs +194 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +194 -54
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -301,7 +301,7 @@ narrow the scope, never widen it. It reaches every path that touches the table:
|
|
|
301
301
|
| `orderBy` through a to-one — `comments(orderBy: { post: { title: … } })` | an out-of-scope post sorts as `NULL` |
|
|
302
302
|
| `updatePosts`, `updatePostsMany`, `deletePosts` | an out-of-scope row is simply not matched |
|
|
303
303
|
| `upsertPosts` (conflict branch) | a conflicting row outside the scope is left alone, not taken over |
|
|
304
|
-
| nested `connect` / `disconnect` / `set` | only in-scope rows can be attached or detached |
|
|
304
|
+
| nested `connect` / `disconnect` / `set` | only in-scope rows can be attached or detached; a many-to-many `set` keeps the links to out-of-scope rows |
|
|
305
305
|
|
|
306
306
|
Two things it deliberately does not do:
|
|
307
307
|
|
|
@@ -347,10 +347,14 @@ const { schema } = buildSchema(db, {
|
|
|
347
347
|
- The column is **removed from `CreatePostsInput` and `UpdatePostsInput`**, so a client cannot
|
|
348
348
|
send it at all — the SDL itself says the column is server-owned. It stays readable on the
|
|
349
349
|
object type.
|
|
350
|
-
- Every insert stamps it, including rows created by a nested `create
|
|
350
|
+
- Every insert stamps it, including rows created by a nested `create` and junction rows a
|
|
351
|
+
many-to-many `connect` / `set` inserts.
|
|
351
352
|
- Updates never write it, so an update cannot hand a row to another owner. Neither does the
|
|
352
353
|
conflict branch of an upsert: it is left out of the default overwrite and of the columns
|
|
353
354
|
`onConflict.update` can name.
|
|
355
|
+
- A relation whose foreign key is such a column gets no
|
|
356
|
+
[nested-write](#nested-writes) field from either side: every nested operation on it would
|
|
357
|
+
write that column. The build notes which relations it left out.
|
|
354
358
|
|
|
355
359
|
Together the two close the loop: `contextValues` decides which tenant a new row belongs to, and
|
|
356
360
|
`scope` decides which rows a request can see and change afterwards.
|
|
@@ -493,8 +497,8 @@ What a declared table gets:
|
|
|
493
497
|
| relation filters — `users(where: { posts: { some: {} } })`, `none`, `every`, `.through()` junctions | a marked post neither matches nor blocks `none`, with the same default as the relation field (see below) |
|
|
494
498
|
| `orderBy` through a to-one | a marked target the relation field hides sorts as `NULL` |
|
|
495
499
|
| `updatePosts`, `updatePostsMany`, a second `deletePosts` | a marked row is simply not matched |
|
|
496
|
-
| nested `connect` / `disconnect` / `set` | a marked row cannot be attached or detached |
|
|
497
|
-
| `CreatePostsInput`, `UpdatePostsInput` | the marker column is **removed** — only the mutations write it |
|
|
500
|
+
| nested `connect` / `disconnect` / `set` | a marked row cannot be attached or detached; a many-to-many `set` keeps the links to marked rows |
|
|
501
|
+
| `CreatePostsInput`, `UpdatePostsInput`, nested `create` payloads | the marker column is **removed** — only the mutations write it |
|
|
498
502
|
|
|
499
503
|
Every read path also gains a `deleted` argument for the cases the default hides:
|
|
500
504
|
|
|
@@ -1692,9 +1696,10 @@ mutation {
|
|
|
1692
1696
|
|
|
1693
1697
|
- `connect`, `disconnect` and `set` take the target's own `Filters` input — a list of them
|
|
1694
1698
|
on a to-many relation, a single one that must match exactly one row on a to-one — so they
|
|
1695
|
-
select rows the same way a query does. A to-one `
|
|
1696
|
-
|
|
1697
|
-
|
|
1699
|
+
select rows the same way a query does. A to-one `connect` that matches no row, or several,
|
|
1700
|
+
fails with `DRIZZLE_NESTED_WRITE_INVALID` before anything is detached. A to-one
|
|
1701
|
+
`disconnect` is just `disconnect: true`, since there is only one row to detach. A filter
|
|
1702
|
+
that matches everything is rejected rather than attaching the whole table
|
|
1698
1703
|
- `set` replaces the relation wholesale — everything attached is detached first, then the
|
|
1699
1704
|
filters attach. It runs before `create`, so a row created in the same write survives it
|
|
1700
1705
|
- A to-one relation takes one operation at a time (`connect` **or** `disconnect` **or**
|
|
@@ -1716,14 +1721,42 @@ mutation {
|
|
|
1716
1721
|
— so a failure anywhere leaves nothing behind
|
|
1717
1722
|
- A `set` that carries only relation fields is a valid update: it changes what the row is
|
|
1718
1723
|
attached to without rewriting the row
|
|
1724
|
+
- When the parent write produces no row — an update whose `where` matched nothing, an
|
|
1725
|
+
insert or upsert a conflict turned into a no-op — whatever a parent-side `create` inserted
|
|
1726
|
+
for it is rolled back too, so no orphan is left. In a batch only that entry is undone
|
|
1727
|
+
- The returned rows reflect the nested write: on a self-referencing relation
|
|
1728
|
+
(`reports: { connect: … }` on the row itself), where a child-side statement rewrites the
|
|
1729
|
+
parent row, the parent rows are read back after it
|
|
1730
|
+
- A relation joining on more than one column per side gets no nested-write field: setting
|
|
1731
|
+
one key would leave the rest of the join (a tenant column, say) unmatched and unwritten
|
|
1732
|
+
|
|
1733
|
+
### A relation's own `where`
|
|
1734
|
+
|
|
1735
|
+
A relation declared with a `where` (`publishedPosts: r.many.posts({ …, where: { published: true } })`)
|
|
1736
|
+
is written through the same way it is read: only the rows it shows.
|
|
1737
|
+
|
|
1738
|
+
- `disconnect`, `set`, and the detaching a to-one attach does, reach only rows that satisfy
|
|
1739
|
+
the `where` — `publishedPosts: { set: [] }` leaves the drafts attached
|
|
1740
|
+
- `connect` and `set` attach only rows that satisfy it, so the relation shows what was
|
|
1741
|
+
attached. A to-one `connect` must match exactly one such row
|
|
1742
|
+
- There is no `create`: the inserted row is attached whether or not it satisfies the
|
|
1743
|
+
`where`, and the relation could come back without it. Create it with its own mutation, or
|
|
1744
|
+
through a relation with no `where`
|
|
1745
|
+
- A many-to-many `set` unlinks only the links to rows the `where` shows
|
|
1746
|
+
- A relation that inherits its `where` from the other side (declared without `from` / `to`)
|
|
1747
|
+
filters the parent rather than the target. Writing through it on a parent row it is
|
|
1748
|
+
hidden for fails with `DRIZZLE_NESTED_WRITE_INVALID`
|
|
1719
1749
|
|
|
1720
1750
|
### The target table's own rules
|
|
1721
1751
|
|
|
1722
1752
|
A nested write is a write to another table, so it follows that table's rules, not just the
|
|
1723
1753
|
root field's:
|
|
1724
1754
|
|
|
1725
|
-
- **`scope`**
|
|
1726
|
-
|
|
1755
|
+
- **`scope`** and **`softDelete`** narrow the rows `connect` / `disconnect` / `set` can
|
|
1756
|
+
reach — a many-to-many `set` keeps the links to rows it cannot reach — and
|
|
1757
|
+
**`contextValues`** stamp the rows a nested `create` inserts. A relation keyed on a
|
|
1758
|
+
`contextValues` column is not offered at all, and the payload of a nested `create` leaves
|
|
1759
|
+
out the context columns and the soft-delete marker, as the target's own create input does
|
|
1727
1760
|
- **`features`**: each nested input is generated only when the table it writes has that
|
|
1728
1761
|
write turned on. Whatever is left out is noted on the console at build time, and a
|
|
1729
1762
|
relation with nothing left loses its field altogether
|
|
@@ -1767,7 +1800,8 @@ mutation {
|
|
|
1767
1800
|
re-connecting is a no-op rather than a unique-constraint error
|
|
1768
1801
|
- `disconnect` deletes only the links, never the rows they pointed at
|
|
1769
1802
|
- `set: []` unlinks everything and links nothing back, which is how a many-to-many relation
|
|
1770
|
-
is cleared
|
|
1803
|
+
is cleared. "Everything" is everything the request could have unlinked: links to rows
|
|
1804
|
+
outside the target's `scope`, soft-deleted, or hidden by the relation's own `where` stay
|
|
1771
1805
|
- There is no `create`: inserting a row *and* linking it is a write to two tables, and the
|
|
1772
1806
|
link is what the relation field is for. Create the row with its own mutation, then
|
|
1773
1807
|
`connect` it
|
package/dist/index.cjs
CHANGED
|
@@ -6540,6 +6540,20 @@ const ALL_OPS = [
|
|
|
6540
6540
|
* accepts NULL.
|
|
6541
6541
|
*/
|
|
6542
6542
|
const isOptionalOnInsert = (column) => !column.notNull || !!column.hasDefault || !!column.defaultFn;
|
|
6543
|
+
/**
|
|
6544
|
+
* The plan fields that carry a relation's own `where` (see {@link relationOwnCondition}).
|
|
6545
|
+
*
|
|
6546
|
+
* A `where` on the target narrows every row selection the write makes, and takes `create`
|
|
6547
|
+
* away: the row it inserts is attached whether or not it satisfies the `where`, so the
|
|
6548
|
+
* relation could come back without it. A `where` inherited from the reverse relation filters
|
|
6549
|
+
* the parent instead, and becomes a per-parent check.
|
|
6550
|
+
*/
|
|
6551
|
+
const ownWherePlan = (relEntry, parentTable, targetTable, localCol) => {
|
|
6552
|
+
const condition = () => relationOwnCondition(relEntry, parentTable, targetTable, [localCol], [localCol]);
|
|
6553
|
+
if (!condition()) return {};
|
|
6554
|
+
if (!isRelationFilterReversed(relEntry.relation ?? relEntry)) return { ownCondition: () => condition() };
|
|
6555
|
+
return { parentShows: async (executor, key) => (await executor.select({ key: localCol }).from(parentTable).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(localCol, key), condition())).limit(1)).length > 0 };
|
|
6556
|
+
};
|
|
6543
6557
|
/** The property name a column is stored under on its table, matched by identity. */
|
|
6544
6558
|
const propNameOf = (table, column) => {
|
|
6545
6559
|
if (!column) return;
|
|
@@ -6560,7 +6574,7 @@ const propNameOf = (table, column) => {
|
|
|
6560
6574
|
* `onUnresolved` is called when the relation's columns are of a supported shape but cannot be
|
|
6561
6575
|
* matched to the three tables — a drizzle-orm mismatch rather than an unsupported relation.
|
|
6562
6576
|
*/
|
|
6563
|
-
const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable, tableNameOf, onUnresolved) => {
|
|
6577
|
+
const buildThroughPlan = (relationName, relEntry, relation, table, targetTableName, targetTable, tableNameOf, onUnresolved) => {
|
|
6564
6578
|
if ((0, drizzle_orm.is)(relation, drizzle_orm.One)) return;
|
|
6565
6579
|
const junction = relation.throughTable;
|
|
6566
6580
|
const sourceEntries = relation.through?.source;
|
|
@@ -6601,6 +6615,7 @@ const buildThroughPlan = (relationName, relation, table, targetTableName, target
|
|
|
6601
6615
|
targetCol
|
|
6602
6616
|
},
|
|
6603
6617
|
targetKeyColumns: { [foreignColPropName]: foreignCol },
|
|
6618
|
+
...ownWherePlan(relEntry, table, targetTable, localCol),
|
|
6604
6619
|
allowed: /* @__PURE__ */ new Set([
|
|
6605
6620
|
"connect",
|
|
6606
6621
|
"disconnect",
|
|
@@ -6615,8 +6630,9 @@ const buildThroughPlan = (relationName, relation, table, targetTableName, target
|
|
|
6615
6630
|
* rather than a foreign-key one — see {@link buildThroughPlan}.
|
|
6616
6631
|
*
|
|
6617
6632
|
* Skipped, and so absent from the generated inputs rather than failing at runtime:
|
|
6618
|
-
* relations whose join columns cannot be read off the drizzle relation;
|
|
6619
|
-
*
|
|
6633
|
+
* relations whose join columns cannot be read off the drizzle relation; relations joining on
|
|
6634
|
+
* more than one column per side; and relations where neither join column is unique, which
|
|
6635
|
+
* leaves no way to tell which side stores the key.
|
|
6620
6636
|
*/
|
|
6621
6637
|
const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePrimaryKeys, extractJoinColumns, tableNameOf = () => void 0) => {
|
|
6622
6638
|
const uniqueCache = /* @__PURE__ */ new Map();
|
|
@@ -6648,10 +6664,11 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
6648
6664
|
if (!targetTable) continue;
|
|
6649
6665
|
const omitted = `the '${relationName}' field of its nested-write create and update inputs`;
|
|
6650
6666
|
if (relation.through) {
|
|
6651
|
-
const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable, tableNameOf, () => warnUnresolvedJoin(relEntry, tableName, relationName, omitted));
|
|
6667
|
+
const throughPlan = buildThroughPlan(relationName, relEntry, relation, table, targetTableName, targetTable, tableNameOf, () => warnUnresolvedJoin(relEntry, tableName, relationName, omitted));
|
|
6652
6668
|
if (throughPlan) tablePlans[relationName] = throughPlan;
|
|
6653
6669
|
continue;
|
|
6654
6670
|
}
|
|
6671
|
+
if ((relationColumns(relation, "source")?.length ?? 0) > 1 || (relationColumns(relation, "target")?.length ?? 0) > 1) continue;
|
|
6655
6672
|
const joinColumns = extractJoinColumns(relEntry, table, targetTable);
|
|
6656
6673
|
if (!joinColumns) {
|
|
6657
6674
|
warnUnresolvedJoin(relEntry, tableName, relationName, omitted);
|
|
@@ -6674,8 +6691,15 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
6674
6691
|
else continue;
|
|
6675
6692
|
const fkColumn = fkSide === "child" ? foreignCol : localCol;
|
|
6676
6693
|
const targetColumns = (0, drizzle_orm.getColumns)(targetTable);
|
|
6677
|
-
const
|
|
6694
|
+
const targetPrimaryKey = resolvePrimaryKeys(targetTable).filter((name) => targetColumns[name]);
|
|
6695
|
+
const targetKeyColumns = Object.fromEntries(targetPrimaryKey.map((name) => [name, targetColumns[name]]));
|
|
6678
6696
|
targetKeyColumns[foreignColPropName] = foreignCol;
|
|
6697
|
+
const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol);
|
|
6698
|
+
const ops = !fkColumn.notNull ? isOne ? [
|
|
6699
|
+
"create",
|
|
6700
|
+
"connect",
|
|
6701
|
+
"disconnect"
|
|
6702
|
+
] : ALL_OPS : ["create", "connect"];
|
|
6679
6703
|
tablePlans[relationName] = {
|
|
6680
6704
|
relationName,
|
|
6681
6705
|
targetTableName,
|
|
@@ -6688,11 +6712,9 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
6688
6712
|
fkSide,
|
|
6689
6713
|
canDetach: !fkColumn.notNull,
|
|
6690
6714
|
targetKeyColumns,
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
"disconnect"
|
|
6695
|
-
] : ALL_OPS : ["create", "connect"])
|
|
6715
|
+
targetPrimaryKey,
|
|
6716
|
+
...ownWhere,
|
|
6717
|
+
allowed: new Set(ownWhere.ownCondition ? ops.filter((op) => op !== "create") : ops)
|
|
6696
6718
|
};
|
|
6697
6719
|
}
|
|
6698
6720
|
if (Object.keys(tablePlans).length) plans[tableName] = tablePlans;
|
|
@@ -6767,6 +6789,35 @@ const gateNestedWritePlans = (plans, featureOf, isExcluded) => {
|
|
|
6767
6789
|
}
|
|
6768
6790
|
return omitted;
|
|
6769
6791
|
};
|
|
6792
|
+
/**
|
|
6793
|
+
* Takes out every relation whose foreign key is a `contextValues` column of the table storing
|
|
6794
|
+
* it — the parent's own column for a parent-side relation, the target's for a child-side one,
|
|
6795
|
+
* either junction column for a many-to-many.
|
|
6796
|
+
*
|
|
6797
|
+
* Every operation on such a relation writes that column: `connect`, `disconnect` and `set`
|
|
6798
|
+
* by definition, and a child-side `create` too, since it fills the key in from the parent row
|
|
6799
|
+
* rather than from context. That column is the server's to set — an insert stamps it and an
|
|
6800
|
+
* update never touches it — so writing it from a nested input would let a client pick, or
|
|
6801
|
+
* change, the owner the server assigns. Whole relations are dropped, not single inputs: no
|
|
6802
|
+
* operation on them is left that does not write the key.
|
|
6803
|
+
*
|
|
6804
|
+
* Returns one line per relation taken out, for the build note.
|
|
6805
|
+
*/
|
|
6806
|
+
const dropContextValueRelations = (plans, contextValuesOf) => {
|
|
6807
|
+
const omitted = [];
|
|
6808
|
+
if (!contextValuesOf) return omitted;
|
|
6809
|
+
for (const [tableName, tablePlans] of Object.entries(plans)) {
|
|
6810
|
+
for (const [relationName, plan] of Object.entries(tablePlans)) {
|
|
6811
|
+
const stamped = (plan.through ? [[plan.through.tableName, plan.through.sourceColPropName], [plan.through.tableName, plan.through.targetColPropName]] : plan.fkSide === "parent" ? [[tableName, plan.localColPropName]] : [[plan.targetTableName, plan.foreignColPropName]]).find(([owner, column]) => owner !== void 0 && contextValuesOf(owner)?.[column]);
|
|
6812
|
+
if (stamped) {
|
|
6813
|
+
delete tablePlans[relationName];
|
|
6814
|
+
omitted.push(`${tableName}.${relationName} (${stamped[0]}.${stamped[1]} is a contextValues column)`);
|
|
6815
|
+
}
|
|
6816
|
+
}
|
|
6817
|
+
if (!Object.keys(tablePlans).length) delete plans[tableName];
|
|
6818
|
+
}
|
|
6819
|
+
return omitted;
|
|
6820
|
+
};
|
|
6770
6821
|
const NO_COLUMNS = /* @__PURE__ */ new Set();
|
|
6771
6822
|
/**
|
|
6772
6823
|
* Builds the nested-write input types for a schema.
|
|
@@ -6797,7 +6848,8 @@ const createNestedWriteTypes = (params) => {
|
|
|
6797
6848
|
if (cached) return cached;
|
|
6798
6849
|
const omitted = plan.fkSide === "child" ? plan.foreignColPropName : void 0;
|
|
6799
6850
|
const contextColumns = cacheCtx.contextValuesOf?.(plan.targetTableName);
|
|
6800
|
-
const
|
|
6851
|
+
const markerColumn = cacheCtx.softDeleteOf?.(plan.targetTableName)?.columnName;
|
|
6852
|
+
const fields = Object.fromEntries(Object.entries(visibleColumns(plan.targetTable)).filter(([columnName]) => columnName !== omitted && !contextColumns?.[columnName] && columnName !== markerColumn).map(([columnName, column]) => [columnName, drizzleColumnToGraphQLType(column, columnName, plan.targetTableName, false, true, true)]));
|
|
6801
6853
|
const type = new graphql.GraphQLInputObjectType({
|
|
6802
6854
|
name: cacheCtx.typeName({
|
|
6803
6855
|
kind: "nestedWriteInput",
|
|
@@ -6921,6 +6973,8 @@ const createNestedWriteTypes = (params) => {
|
|
|
6921
6973
|
* join columns the whole batch's RETURNING clause needs, without inspecting each row's write.
|
|
6922
6974
|
*/
|
|
6923
6975
|
const mergedOps = (entries) => Object.assign({}, ...entries.map((entry) => entry.ops));
|
|
6976
|
+
/** A to-one `connect` whose filter did not match exactly one row. */
|
|
6977
|
+
const connectMatchError = (relationName, matched) => drizzleError(`Drizzle-GraphQL Error: 'connect' on '${relationName}' matched ${matched} rows — it attaches a single row, so its filter must match exactly one.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
6924
6978
|
const asArray = (value) => Array.isArray(value) ? value : value === void 0 ? [] : [value];
|
|
6925
6979
|
const supplied = (value) => value !== void 0 && value !== null;
|
|
6926
6980
|
/**
|
|
@@ -7012,6 +7066,8 @@ const registerChildSteps = (steps, plan, op) => {
|
|
|
7012
7066
|
};
|
|
7013
7067
|
/** The input that attaches a row to a to-one, which detaches whatever was attached before. */
|
|
7014
7068
|
const attachingOp = (op) => op["connect"] !== void 0 ? "connect" : op["create"] !== void 0 ? "create" : void 0;
|
|
7069
|
+
/** The child-side relation in `ops` whose target is the parent's own table, if there is one. */
|
|
7070
|
+
const selfTargetingPlan = (tablePlans, tableName, ops) => Object.keys(ops).map((relationName) => tablePlans?.[relationName]).find((plan) => plan?.fkSide === "child" && !plan.through && plan.targetTableName === tableName);
|
|
7015
7071
|
const createNestedWriteRuntime = (params) => {
|
|
7016
7072
|
const { plans, filterCtx, policies, contextValues } = params;
|
|
7017
7073
|
const onWrite = policies?.onWrite;
|
|
@@ -7022,7 +7078,7 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7022
7078
|
const conditionOf = (plan, filter, operation, scope) => {
|
|
7023
7079
|
const condition = extractFilters(plan.targetTable, plan.targetTableName, filter, relationFilterCtx(filterCtx, plan.targetTableName, scope));
|
|
7024
7080
|
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" });
|
|
7025
|
-
return withScope(scope, plan.targetTableName, plan.targetTable, condition);
|
|
7081
|
+
return withScope(scope, plan.targetTableName, plan.targetTable, (0, drizzle_orm.and)(condition, plan.ownCondition?.()));
|
|
7026
7082
|
};
|
|
7027
7083
|
const assertSingleOperation = (plan, op) => {
|
|
7028
7084
|
const given = [
|
|
@@ -7045,22 +7101,24 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7045
7101
|
* error on some of them — the read and the write share the surrounding transaction, so no
|
|
7046
7102
|
* other writer can slip a duplicate in between them.
|
|
7047
7103
|
*/
|
|
7048
|
-
const applyThroughOps = async (executor, plan, key, op, scope, steps) => {
|
|
7104
|
+
const applyThroughOps = async (executor, plan, key, op, scope, steps, context) => {
|
|
7049
7105
|
const through = plan.through;
|
|
7050
7106
|
const link = async (operands, operation) => {
|
|
7051
7107
|
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]));
|
|
7052
7108
|
const rows = [];
|
|
7053
7109
|
for (const filter of operands) for (const targetKey of await selectedTargetKeys(executor, plan, filter, operation, scope)) if (!existing.has(targetKey)) {
|
|
7054
7110
|
existing.add(targetKey);
|
|
7055
|
-
rows.push({
|
|
7111
|
+
rows.push(applyContextValues({
|
|
7056
7112
|
[through.sourceColPropName]: key,
|
|
7057
7113
|
[through.targetColPropName]: targetKey
|
|
7058
|
-
});
|
|
7114
|
+
}, through.tableName ? contextValues?.(through.tableName) : void 0, context));
|
|
7059
7115
|
}
|
|
7060
7116
|
if (rows.length) await steps.run(executor.insert(through.table).values(rows), operation, "insert");
|
|
7061
7117
|
};
|
|
7062
7118
|
if (supplied(op["set"])) {
|
|
7063
|
-
|
|
7119
|
+
const reachable = withScope(scope, plan.targetTableName, plan.targetTable, plan.ownCondition?.());
|
|
7120
|
+
const unlink = reachable ? (0, drizzle_orm.and)((0, drizzle_orm.eq)(through.sourceCol, key), (0, drizzle_orm.inArray)(through.targetCol, executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(reachable))) : (0, drizzle_orm.eq)(through.sourceCol, key);
|
|
7121
|
+
await steps.run(executor.delete(through.table).where(unlink), "set", "delete");
|
|
7064
7122
|
await link(asArray(op["set"]), "set");
|
|
7065
7123
|
}
|
|
7066
7124
|
if (supplied(op["disconnect"])) for (const filter of asArray(op["disconnect"])) {
|
|
@@ -7069,7 +7127,7 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7069
7127
|
}
|
|
7070
7128
|
if (supplied(op["connect"])) await link(asArray(op["connect"]), "connect");
|
|
7071
7129
|
};
|
|
7072
|
-
const detachAll = (executor, plan, key, scope, steps, op) => steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where(withScope(scope, plan.targetTableName, plan.targetTable, (0, drizzle_orm.eq)(plan.foreignCol, key))), op, "update");
|
|
7130
|
+
const detachAll = (executor, plan, key, scope, steps, op) => steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where(withScope(scope, plan.targetTableName, plan.targetTable, (0, drizzle_orm.and)((0, drizzle_orm.eq)(plan.foreignCol, key), plan.ownCondition?.()))), op, "update");
|
|
7073
7131
|
return {
|
|
7074
7132
|
enabled: (tableName) => !!plans[tableName],
|
|
7075
7133
|
split: (tableName, values) => {
|
|
@@ -7096,8 +7154,24 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7096
7154
|
const plan = tablePlans[relationName];
|
|
7097
7155
|
if (plan?.fkSide === "child" && !(plan.localColPropName in columns) && allColumns[plan.localColPropName]) columns[plan.localColPropName] = allColumns[plan.localColPropName];
|
|
7098
7156
|
}
|
|
7157
|
+
for (const name of selfTargetingPlan(tablePlans, tableName, ops)?.targetPrimaryKey ?? []) if (!(name in columns) && allColumns[name]) columns[name] = allColumns[name];
|
|
7099
7158
|
return columns;
|
|
7100
7159
|
},
|
|
7160
|
+
createsParentSide: (tableName, ops) => Object.entries(ops).some(([relationName, op]) => plans[tableName]?.[relationName]?.fkSide === "parent" && supplied(op?.["create"])),
|
|
7161
|
+
refreshed: async (executor, tableName, ops, rows) => {
|
|
7162
|
+
const plan = selfTargetingPlan(plans[tableName], tableName, ops);
|
|
7163
|
+
const pk = plan?.targetPrimaryKey ?? [];
|
|
7164
|
+
if (!plan || !pk.length || !rows.length || !pk.every((name) => name in rows[0])) return rows;
|
|
7165
|
+
const columns = (0, drizzle_orm.getColumns)(plan.targetTable);
|
|
7166
|
+
const selection = Object.fromEntries(Object.keys(rows[0]).filter((name) => columns[name]).map((name) => [name, columns[name]]));
|
|
7167
|
+
const keyOf = (row) => pk.map((name) => String(row[name])).join("\0");
|
|
7168
|
+
const match = pk.length === 1 ? (0, drizzle_orm.inArray)(columns[pk[0]], rows.map((row) => row[pk[0]])) : (0, drizzle_orm.or)(...rows.map((row) => (0, drizzle_orm.and)(...pk.map((name) => (0, drizzle_orm.eq)(columns[name], row[name])))));
|
|
7169
|
+
const fresh = new Map((await executor.select(selection).from(plan.targetTable).where(match)).map((row) => [keyOf(row), row]));
|
|
7170
|
+
return rows.map((row) => ({
|
|
7171
|
+
...row,
|
|
7172
|
+
...fresh.get(keyOf(row))
|
|
7173
|
+
}));
|
|
7174
|
+
},
|
|
7101
7175
|
applyParentSide: async (executor, tableName, ops, request) => {
|
|
7102
7176
|
const tablePlans = plans[tableName];
|
|
7103
7177
|
const patch = {};
|
|
@@ -7122,10 +7196,11 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7122
7196
|
patch[plan.localColPropName] = created[plan.foreignColPropName];
|
|
7123
7197
|
} else if (op["connect"] !== void 0) {
|
|
7124
7198
|
const rows = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
|
|
7125
|
-
if (rows.length !== 1) throw
|
|
7199
|
+
if (rows.length !== 1) throw connectMatchError(relationName, rows.length);
|
|
7126
7200
|
patch[plan.localColPropName] = rows[0][plan.foreignColPropName];
|
|
7127
7201
|
} else if (op["disconnect"] === true) patch[plan.localColPropName] = null;
|
|
7128
7202
|
}
|
|
7203
|
+
for (const columnName of Object.keys(contextValues?.(tableName) ?? {})) delete patch[columnName];
|
|
7129
7204
|
return patch;
|
|
7130
7205
|
},
|
|
7131
7206
|
applyChildSide: async (executor, tableName, ops, parentRows, request) => {
|
|
@@ -7143,8 +7218,9 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7143
7218
|
for (const parentRow of parentRows) {
|
|
7144
7219
|
const key = parentRow?.[plan.localColPropName];
|
|
7145
7220
|
if (key === void 0 || key === null) throw drizzleError(`Drizzle-GraphQL Error: cannot write through '${relationName}': the ${tableName} row has no '${plan.localColPropName}' value to attach to.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
7221
|
+
if (plan.parentShows && !await plan.parentShows(executor, key)) throw drizzleError(`Drizzle-GraphQL Error: cannot write through '${relationName}': its \`where\` hides the relation on this ${tableName} row.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
7146
7222
|
if (plan.through) {
|
|
7147
|
-
await applyThroughOps(executor, plan, key, op, scope, steps);
|
|
7223
|
+
await applyThroughOps(executor, plan, key, op, scope, steps, context);
|
|
7148
7224
|
continue;
|
|
7149
7225
|
}
|
|
7150
7226
|
if (op["set"] !== void 0) {
|
|
@@ -7156,14 +7232,18 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7156
7232
|
if (op["disconnect"] === true) await detachAll(executor, plan, key, scope, steps, "disconnect");
|
|
7157
7233
|
} else for (const filter of asArray(op["disconnect"])) await steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(plan.foreignCol, key), conditionOf(plan, filter, "disconnect", scope))), "disconnect", "update");
|
|
7158
7234
|
}
|
|
7235
|
+
if (plan.isOne && op["connect"] !== void 0) {
|
|
7236
|
+
const matched = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
|
|
7237
|
+
if (matched.length !== 1) throw connectMatchError(relationName, matched.length);
|
|
7238
|
+
}
|
|
7159
7239
|
const attachedBy = attachingOp(op);
|
|
7160
7240
|
if (plan.isOne && attachedBy && plan.canDetach && op["set"] === void 0) await detachAll(executor, plan, key, scope, steps, attachedBy);
|
|
7161
7241
|
if (op["connect"] !== void 0) for (const filter of asArray(op["connect"])) await steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: key }).where(conditionOf(plan, filter, "connect", scope)), "connect", "update");
|
|
7162
7242
|
if (op["create"] !== void 0) {
|
|
7163
|
-
const rows = asArray(op["create"]).map((row) => ({
|
|
7164
|
-
...
|
|
7243
|
+
const rows = asArray(op["create"]).map((row) => applyContextValues({
|
|
7244
|
+
...remapFromGraphQLSingleInput({ ...row }, plan.targetTable),
|
|
7165
7245
|
[plan.foreignColPropName]: key
|
|
7166
|
-
}));
|
|
7246
|
+
}, contextValues?.(plan.targetTableName), context));
|
|
7167
7247
|
if (rows.length) await steps.run(executor.insert(plan.targetTable).values(rows), "create", "insert");
|
|
7168
7248
|
}
|
|
7169
7249
|
}
|
|
@@ -7172,6 +7252,39 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7172
7252
|
}
|
|
7173
7253
|
};
|
|
7174
7254
|
};
|
|
7255
|
+
/** Thrown inside an entry's savepoint to undo a parent-side `create` whose parent was never written. */
|
|
7256
|
+
var NoParentRow = class extends Error {};
|
|
7257
|
+
/**
|
|
7258
|
+
* One parent write with its nested operations around it: the parent-side operations, whose
|
|
7259
|
+
* values `write` receives as a patch, then the write, then the child-side operations — and
|
|
7260
|
+
* the rows it returns read back when a child-side statement may have changed them.
|
|
7261
|
+
*
|
|
7262
|
+
* A parent write can come back empty: an update whose `where` matched nothing, an insert or
|
|
7263
|
+
* upsert that a conflict turned into a no-op. Anything a parent-side `create` inserted would
|
|
7264
|
+
* then be left with nothing pointing at it, so such an entry runs in a savepoint of its own
|
|
7265
|
+
* that is rolled back when the write produced no row, and the entry returns no rows — as it
|
|
7266
|
+
* would have without the nested operation. The rest of the batch is unaffected.
|
|
7267
|
+
*
|
|
7268
|
+
* `executor` must already be a transaction.
|
|
7269
|
+
*/
|
|
7270
|
+
const writeNestedEntry = async (params) => {
|
|
7271
|
+
const { executor, runtime, tableName, ops, write, request } = params;
|
|
7272
|
+
const guarded = runtime.createsParentSide(tableName, ops);
|
|
7273
|
+
const run = async (tx) => {
|
|
7274
|
+
const patch = await runtime.applyParentSide(tx, tableName, ops, request);
|
|
7275
|
+
const rows = await write(tx, patch);
|
|
7276
|
+
if (guarded && !rows.length) throw new NoParentRow();
|
|
7277
|
+
await runtime.applyChildSide(tx, tableName, ops, rows, request);
|
|
7278
|
+
return runtime.refreshed(tx, tableName, ops, rows);
|
|
7279
|
+
};
|
|
7280
|
+
if (!guarded) return run(executor);
|
|
7281
|
+
try {
|
|
7282
|
+
return await executor.transaction(run);
|
|
7283
|
+
} catch (error) {
|
|
7284
|
+
if (error instanceof NoParentRow) return [];
|
|
7285
|
+
throw error;
|
|
7286
|
+
}
|
|
7287
|
+
};
|
|
7175
7288
|
/**
|
|
7176
7289
|
* Runs one parent write per input entry, with that entry's nested operations around it, all
|
|
7177
7290
|
* inside a single transaction — a savepoint when the caller already runs on one, so a failing
|
|
@@ -7190,12 +7303,17 @@ const writeWithNestedOps = async (params) => {
|
|
|
7190
7303
|
return executor.transaction(async (tx) => {
|
|
7191
7304
|
const rows = [];
|
|
7192
7305
|
for (const entry of entries) {
|
|
7193
|
-
const
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7306
|
+
const written = await writeNestedEntry({
|
|
7307
|
+
executor: tx,
|
|
7308
|
+
runtime,
|
|
7309
|
+
tableName,
|
|
7310
|
+
ops: entry.ops,
|
|
7311
|
+
write: (target, patch) => write(target, {
|
|
7312
|
+
...remapValues(entry.columns),
|
|
7313
|
+
...patch
|
|
7314
|
+
}),
|
|
7315
|
+
request
|
|
7197
7316
|
});
|
|
7198
|
-
await runtime.applyChildSide(tx, tableName, entry.ops, written, request);
|
|
7199
7317
|
rows.push(...written);
|
|
7200
7318
|
}
|
|
7201
7319
|
return rows;
|
|
@@ -7208,15 +7326,17 @@ const writeWithNestedOps = async (params) => {
|
|
|
7208
7326
|
*/
|
|
7209
7327
|
const updateWithNestedOps = async (params) => {
|
|
7210
7328
|
const { executor, runtime, tableName, columns, ops, remapValues, write, request } = params;
|
|
7211
|
-
return executor.transaction(
|
|
7212
|
-
|
|
7213
|
-
|
|
7329
|
+
return executor.transaction((tx) => writeNestedEntry({
|
|
7330
|
+
executor: tx,
|
|
7331
|
+
runtime,
|
|
7332
|
+
tableName,
|
|
7333
|
+
ops,
|
|
7334
|
+
write: (target, patch) => write(target, {
|
|
7214
7335
|
...remapValues(columns),
|
|
7215
7336
|
...patch
|
|
7216
|
-
})
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
});
|
|
7337
|
+
}),
|
|
7338
|
+
request
|
|
7339
|
+
}));
|
|
7220
7340
|
};
|
|
7221
7341
|
//#endregion
|
|
7222
7342
|
//#region src/util/builders/errors.ts
|
|
@@ -7423,6 +7543,10 @@ const createSchemaBuilder = (adapter) => {
|
|
|
7423
7543
|
console.warn(`Drizzle-GraphQL Warning: config.features — nestedWrites leaves out the nested inputs whose table has that write turned off: ${listed}.`);
|
|
7424
7544
|
}
|
|
7425
7545
|
}
|
|
7546
|
+
if (nestedPlans) {
|
|
7547
|
+
const omitted = dropContextValueRelations(nestedPlans, contextValuesOf);
|
|
7548
|
+
if (omitted.length) console.warn(`Drizzle-GraphQL Warning: config.contextValues — nestedWrites leaves out the relations whose foreign key the server stamps from context: ${omitted.join(", ")}.`);
|
|
7549
|
+
}
|
|
7426
7550
|
const nestedTypes = nestedPlans ? createNestedWriteTypes({
|
|
7427
7551
|
plans: nestedPlans,
|
|
7428
7552
|
cacheCtx,
|
|
@@ -8736,16 +8860,24 @@ const createSchemaDataGenerator = (adapter) => {
|
|
|
8736
8860
|
const runUpdateManyBatch = ({ executor, entries, table, returning, nested, tableName, request }) => executor.transaction(async (tx) => {
|
|
8737
8861
|
const results = [];
|
|
8738
8862
|
for (const entry of entries) {
|
|
8739
|
-
const
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8863
|
+
const write = async (target, patch) => {
|
|
8864
|
+
const values = {
|
|
8865
|
+
...entry.set,
|
|
8866
|
+
...patch
|
|
8867
|
+
};
|
|
8868
|
+
const writes = Object.keys(values).length > 0;
|
|
8869
|
+
let query = writes ? target.update(table).set(values) : target.select(returning).from(table);
|
|
8870
|
+
if (entry.filters) query = query.where(entry.filters);
|
|
8871
|
+
return await (writes ? query.returning(returning) : query);
|
|
8872
|
+
};
|
|
8873
|
+
results.push(entry.ops ? await writeNestedEntry({
|
|
8874
|
+
executor: tx,
|
|
8875
|
+
runtime: nested,
|
|
8876
|
+
tableName,
|
|
8877
|
+
ops: entry.ops,
|
|
8878
|
+
write,
|
|
8879
|
+
request
|
|
8880
|
+
}) : await write(tx, {}));
|
|
8749
8881
|
}
|
|
8750
8882
|
return results;
|
|
8751
8883
|
});
|
|
@@ -8877,16 +9009,24 @@ const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNest
|
|
|
8877
9009
|
return query.returning(returning).all();
|
|
8878
9010
|
};
|
|
8879
9011
|
const runNestedEntry = async (tx, entry) => {
|
|
8880
|
-
const
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
9012
|
+
const write = async (target, patch) => {
|
|
9013
|
+
const values = {
|
|
9014
|
+
...entry.set,
|
|
9015
|
+
...patch
|
|
9016
|
+
};
|
|
9017
|
+
const writes = Object.keys(values).length > 0;
|
|
9018
|
+
let query = writes ? target.update(table).set(values) : target.select(returning).from(table);
|
|
9019
|
+
if (entry.filters) query = query.where(entry.filters);
|
|
9020
|
+
return await (writes ? query.returning(returning) : query);
|
|
9021
|
+
};
|
|
9022
|
+
return entry.ops ? writeNestedEntry({
|
|
9023
|
+
executor: tx,
|
|
9024
|
+
runtime: nested,
|
|
9025
|
+
tableName,
|
|
9026
|
+
ops: entry.ops,
|
|
9027
|
+
write,
|
|
9028
|
+
request
|
|
9029
|
+
}) : write(tx, {});
|
|
8890
9030
|
};
|
|
8891
9031
|
return executor.transaction((tx) => {
|
|
8892
9032
|
if (anyNested) return (async () => {
|