graphile-bulk-mutations 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +197 -0
- package/augmentations.d.ts +102 -0
- package/augmentations.js +11 -0
- package/esm/augmentations.d.ts +102 -0
- package/esm/augmentations.js +9 -0
- package/esm/index.d.ts +30 -0
- package/esm/index.js +29 -0
- package/esm/plugins/BulkDeletePlugin.d.ts +13 -0
- package/esm/plugins/BulkDeletePlugin.js +181 -0
- package/esm/plugins/BulkInflectionPlugin.d.ts +9 -0
- package/esm/plugins/BulkInflectionPlugin.js +103 -0
- package/esm/plugins/BulkInsertPlugin.d.ts +21 -0
- package/esm/plugins/BulkInsertPlugin.js +214 -0
- package/esm/plugins/BulkRelationalPlugin.d.ts +31 -0
- package/esm/plugins/BulkRelationalPlugin.js +127 -0
- package/esm/plugins/BulkTypesPlugin.d.ts +18 -0
- package/esm/plugins/BulkTypesPlugin.js +391 -0
- package/esm/plugins/BulkUpdatePlugin.d.ts +13 -0
- package/esm/plugins/BulkUpdatePlugin.js +211 -0
- package/esm/plugins/BulkUpsertPlugin.d.ts +12 -0
- package/esm/plugins/BulkUpsertPlugin.js +165 -0
- package/esm/plugins/index.d.ts +7 -0
- package/esm/plugins/index.js +7 -0
- package/esm/preset.d.ts +35 -0
- package/esm/preset.js +74 -0
- package/esm/types.d.ts +40 -0
- package/esm/types.js +7 -0
- package/esm/utils/index.d.ts +4 -0
- package/esm/utils/index.js +2 -0
- package/esm/utils/relations.d.ts +42 -0
- package/esm/utils/relations.js +83 -0
- package/esm/utils/sql-builder.d.ts +56 -0
- package/esm/utils/sql-builder.js +121 -0
- package/index.d.ts +30 -0
- package/index.js +44 -0
- package/package.json +61 -0
- package/plugins/BulkDeletePlugin.d.ts +13 -0
- package/plugins/BulkDeletePlugin.js +184 -0
- package/plugins/BulkInflectionPlugin.d.ts +9 -0
- package/plugins/BulkInflectionPlugin.js +106 -0
- package/plugins/BulkInsertPlugin.d.ts +21 -0
- package/plugins/BulkInsertPlugin.js +217 -0
- package/plugins/BulkRelationalPlugin.d.ts +31 -0
- package/plugins/BulkRelationalPlugin.js +130 -0
- package/plugins/BulkTypesPlugin.d.ts +18 -0
- package/plugins/BulkTypesPlugin.js +394 -0
- package/plugins/BulkUpdatePlugin.d.ts +13 -0
- package/plugins/BulkUpdatePlugin.js +214 -0
- package/plugins/BulkUpsertPlugin.d.ts +12 -0
- package/plugins/BulkUpsertPlugin.js +168 -0
- package/plugins/index.d.ts +7 -0
- package/plugins/index.js +17 -0
- package/preset.d.ts +35 -0
- package/preset.js +77 -0
- package/types.d.ts +40 -0
- package/types.js +10 -0
- package/utils/index.d.ts +4 -0
- package/utils/index.js +9 -0
- package/utils/relations.d.ts +42 -0
- package/utils/relations.js +86 -0
- package/utils/sql-builder.d.ts +56 -0
- package/utils/sql-builder.js +126 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import '../augmentations';
|
|
2
|
+
import { sideEffectWithPgClient } from '@dataplan/pg';
|
|
3
|
+
const version = '0.1.0';
|
|
4
|
+
/**
|
|
5
|
+
* BulkUpdatePlugin
|
|
6
|
+
*
|
|
7
|
+
* Registers `bulkUpdateX` mutation fields for each resource with the
|
|
8
|
+
* `bulkUpdate` behavior. Uses UPDATE ... SET ... WHERE with conditions
|
|
9
|
+
* from graphile-connection-filter (or PostGraphile's built-in condition type).
|
|
10
|
+
*
|
|
11
|
+
* Uses `RETURNING <pk_columns>` (not `RETURNING *`) + follow-up SELECT
|
|
12
|
+
* to respect column-level SELECT grants.
|
|
13
|
+
*/
|
|
14
|
+
export const BulkUpdatePlugin = {
|
|
15
|
+
name: 'BulkUpdatePlugin',
|
|
16
|
+
version,
|
|
17
|
+
description: 'Adds bulk update (bulkUpdateX) mutations',
|
|
18
|
+
after: ['BulkTypesPlugin'],
|
|
19
|
+
schema: {
|
|
20
|
+
hooks: {
|
|
21
|
+
GraphQLObjectType_fields(fields, build, context) {
|
|
22
|
+
const { scope: { isRootMutation } } = context;
|
|
23
|
+
if (!isRootMutation)
|
|
24
|
+
return fields;
|
|
25
|
+
const { inflection, sql, graphql: { GraphQLNonNull }, options: { bulkUpdate: enableUpdate = true, bulkRequireWhere = true } } = build;
|
|
26
|
+
if (!enableUpdate)
|
|
27
|
+
return fields;
|
|
28
|
+
const pgRegistry = build.input.pgRegistry;
|
|
29
|
+
for (const [resourceName, resource] of Object.entries(pgRegistry.pgResources)) {
|
|
30
|
+
if (resource.parameters)
|
|
31
|
+
continue;
|
|
32
|
+
if (!resource.codec.attributes)
|
|
33
|
+
continue;
|
|
34
|
+
if (resource.codec.polymorphism)
|
|
35
|
+
continue;
|
|
36
|
+
if (resource.codec.isAnonymous)
|
|
37
|
+
continue;
|
|
38
|
+
if (!resource.uniques?.length)
|
|
39
|
+
continue;
|
|
40
|
+
if (!build.behavior.pgResourceMatches(resource, 'bulkUpdate'))
|
|
41
|
+
continue;
|
|
42
|
+
const typeName = inflection.tableType(resource.codec);
|
|
43
|
+
const fieldName = inflection.bulkUpdateField(resourceName);
|
|
44
|
+
const inputTypeName = inflection.bulkUpdateInputType(typeName);
|
|
45
|
+
const payloadTypeName = inflection.bulkUpdatePayloadType(typeName);
|
|
46
|
+
const inputType = build.getTypeByName(inputTypeName);
|
|
47
|
+
const payloadType = build.getTypeByName(payloadTypeName);
|
|
48
|
+
if (!inputType || !payloadType)
|
|
49
|
+
continue;
|
|
50
|
+
// Build field-to-attribute mapping for patch resolution
|
|
51
|
+
const fieldToAttr = {};
|
|
52
|
+
const attrToSqlType = {};
|
|
53
|
+
for (const [attrName, attr] of Object.entries(resource.codec.attributes)) {
|
|
54
|
+
const gqlFieldName = inflection.attribute({
|
|
55
|
+
attributeName: attrName,
|
|
56
|
+
codec: resource.codec
|
|
57
|
+
});
|
|
58
|
+
fieldToAttr[gqlFieldName] = attrName;
|
|
59
|
+
attrToSqlType[attrName] = sql.compile(attr.codec.sqlType).text;
|
|
60
|
+
}
|
|
61
|
+
// Extract primary key columns for RETURNING clause
|
|
62
|
+
const primaryUnique = resource.uniques.find((u) => u.isPrimary) ?? resource.uniques[0];
|
|
63
|
+
const pkColumns = primaryUnique.attributes;
|
|
64
|
+
const pkReturning = pkColumns.map((c) => `"${c}"`).join(', ');
|
|
65
|
+
const compiledFrom = sql.compile(resource.from).text;
|
|
66
|
+
// Capture for plan closure
|
|
67
|
+
const executor = resource.executor;
|
|
68
|
+
const requireWhere = bulkRequireWhere;
|
|
69
|
+
fields = build.extend(fields, {
|
|
70
|
+
[fieldName]: context.fieldWithHooks({ fieldName }, {
|
|
71
|
+
description: `Bulk update ${typeName} rows matching the given condition.`,
|
|
72
|
+
type: payloadType,
|
|
73
|
+
args: {
|
|
74
|
+
input: {
|
|
75
|
+
type: new GraphQLNonNull(inputType)
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
plan(_$root, args) {
|
|
79
|
+
const $input = args.getRaw('input');
|
|
80
|
+
const $result = sideEffectWithPgClient(executor, $input, async (pgClient, input) => {
|
|
81
|
+
if (requireWhere && (!input.where || Object.keys(input.where).length === 0)) {
|
|
82
|
+
throw new Error('Bulk update requires a non-empty where condition. Set bulkRequireWhere: false to allow unrestricted updates.');
|
|
83
|
+
}
|
|
84
|
+
if (!input.patch || Object.keys(input.patch).length === 0) {
|
|
85
|
+
throw new Error('Bulk update requires a non-empty patch.');
|
|
86
|
+
}
|
|
87
|
+
// Build SET clause from patch
|
|
88
|
+
const setClauses = [];
|
|
89
|
+
const values = [];
|
|
90
|
+
for (const [key, val] of Object.entries(input.patch)) {
|
|
91
|
+
if (val === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
const attrName = fieldToAttr[key];
|
|
94
|
+
if (!attrName)
|
|
95
|
+
continue;
|
|
96
|
+
const sqlType = attrToSqlType[attrName];
|
|
97
|
+
values.push(val);
|
|
98
|
+
setClauses.push(`"${attrName}" = $${values.length}::${sqlType}`);
|
|
99
|
+
}
|
|
100
|
+
if (setClauses.length === 0) {
|
|
101
|
+
return { affectedCount: 0, returning: [] };
|
|
102
|
+
}
|
|
103
|
+
// Build WHERE clause from condition
|
|
104
|
+
// Supports both simple equality (Condition type: { name: "X" })
|
|
105
|
+
// and operator-based (Filter type: { name: { equalTo: "X" } })
|
|
106
|
+
const whereClauses = [];
|
|
107
|
+
if (input.where) {
|
|
108
|
+
for (const [key, spec] of Object.entries(input.where)) {
|
|
109
|
+
const attrName = fieldToAttr[key];
|
|
110
|
+
if (!attrName)
|
|
111
|
+
continue;
|
|
112
|
+
const sqlType = attrToSqlType[attrName];
|
|
113
|
+
if (spec === null) {
|
|
114
|
+
whereClauses.push(`"${attrName}" IS NULL`);
|
|
115
|
+
}
|
|
116
|
+
else if (spec !== undefined && typeof spec !== 'object') {
|
|
117
|
+
// Simple equality (Condition type)
|
|
118
|
+
values.push(spec);
|
|
119
|
+
whereClauses.push(`"${attrName}" = $${values.length}::${sqlType}`);
|
|
120
|
+
}
|
|
121
|
+
else if (spec && typeof spec === 'object') {
|
|
122
|
+
// Operator-based (Filter type)
|
|
123
|
+
for (const [op, val] of Object.entries(spec)) {
|
|
124
|
+
values.push(val);
|
|
125
|
+
const paramRef = `$${values.length}::${sqlType}`;
|
|
126
|
+
switch (op) {
|
|
127
|
+
case 'equalTo':
|
|
128
|
+
whereClauses.push(`"${attrName}" = ${paramRef}`);
|
|
129
|
+
break;
|
|
130
|
+
case 'notEqualTo':
|
|
131
|
+
whereClauses.push(`"${attrName}" != ${paramRef}`);
|
|
132
|
+
break;
|
|
133
|
+
case 'greaterThan':
|
|
134
|
+
whereClauses.push(`"${attrName}" > ${paramRef}`);
|
|
135
|
+
break;
|
|
136
|
+
case 'greaterThanOrEqualTo':
|
|
137
|
+
whereClauses.push(`"${attrName}" >= ${paramRef}`);
|
|
138
|
+
break;
|
|
139
|
+
case 'lessThan':
|
|
140
|
+
whereClauses.push(`"${attrName}" < ${paramRef}`);
|
|
141
|
+
break;
|
|
142
|
+
case 'lessThanOrEqualTo':
|
|
143
|
+
whereClauses.push(`"${attrName}" <= ${paramRef}`);
|
|
144
|
+
break;
|
|
145
|
+
case 'in':
|
|
146
|
+
if (Array.isArray(val)) {
|
|
147
|
+
const placeholders = val.map((v) => {
|
|
148
|
+
values.push(v);
|
|
149
|
+
return `$${values.length}::${sqlType}`;
|
|
150
|
+
});
|
|
151
|
+
values.pop();
|
|
152
|
+
whereClauses.push(`"${attrName}" IN (${placeholders.join(', ')})`);
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
case 'isNull':
|
|
156
|
+
values.pop();
|
|
157
|
+
if (val) {
|
|
158
|
+
whereClauses.push(`"${attrName}" IS NULL`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
whereClauses.push(`"${attrName}" IS NOT NULL`);
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
default:
|
|
165
|
+
values.pop();
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (whereClauses.length === 0 && requireWhere) {
|
|
173
|
+
throw new Error('Bulk update: no valid where conditions resolved. At least one condition is required.');
|
|
174
|
+
}
|
|
175
|
+
const whereStr = whereClauses.length > 0
|
|
176
|
+
? whereClauses.join(' AND ')
|
|
177
|
+
: 'TRUE';
|
|
178
|
+
// Use RETURNING <pk_columns> instead of RETURNING *
|
|
179
|
+
const text = `UPDATE ${compiledFrom}\nSET ${setClauses.join(', ')}\nWHERE ${whereStr}\nRETURNING ${pkReturning}`;
|
|
180
|
+
const mutationResult = await pgClient.query(text, values);
|
|
181
|
+
const affectedCount = mutationResult.rowCount ?? 0;
|
|
182
|
+
// Follow-up SELECT using PKs to respect column-level grants
|
|
183
|
+
let returning = [];
|
|
184
|
+
if (mutationResult.rows && mutationResult.rows.length > 0) {
|
|
185
|
+
const pkRows = mutationResult.rows;
|
|
186
|
+
const pkConditions = pkRows.map((pkRow, rowIdx) => {
|
|
187
|
+
return pkColumns.map((col, colIdx) => {
|
|
188
|
+
const paramIdx = rowIdx * pkColumns.length + colIdx + 1;
|
|
189
|
+
return `"${col}" = $${paramIdx}`;
|
|
190
|
+
}).join(' AND ');
|
|
191
|
+
});
|
|
192
|
+
const selectWhere = pkConditions.map((c) => `(${c})`).join(' OR ');
|
|
193
|
+
const selectParams = pkRows.flatMap((pkRow) => pkColumns.map((col) => pkRow[col]));
|
|
194
|
+
const selectResult = await pgClient.query(`SELECT * FROM ${compiledFrom} WHERE ${selectWhere}`, selectParams);
|
|
195
|
+
returning = selectResult.rows || [];
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
affectedCount,
|
|
199
|
+
returning
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
return $result;
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
}, `Adding bulk update field for ${typeName}`);
|
|
206
|
+
}
|
|
207
|
+
return fields;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import '../augmentations';
|
|
2
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
3
|
+
/**
|
|
4
|
+
* BulkUpsertPlugin
|
|
5
|
+
*
|
|
6
|
+
* Registers `bulkUpsertX` mutation fields for each resource with the
|
|
7
|
+
* `bulkUpsert` behavior. Uses INSERT ... ON CONFLICT DO UPDATE SET.
|
|
8
|
+
*
|
|
9
|
+
* Uses `RETURNING <pk_columns>` (not `RETURNING *`) + follow-up SELECT
|
|
10
|
+
* to respect column-level SELECT grants.
|
|
11
|
+
*/
|
|
12
|
+
export declare const BulkUpsertPlugin: GraphileConfig.Plugin;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import '../augmentations';
|
|
2
|
+
import { sideEffectWithPgClient } from '@dataplan/pg';
|
|
3
|
+
import { buildBulkInsertSQL } from '../utils/sql-builder';
|
|
4
|
+
const version = '0.1.0';
|
|
5
|
+
/**
|
|
6
|
+
* BulkUpsertPlugin
|
|
7
|
+
*
|
|
8
|
+
* Registers `bulkUpsertX` mutation fields for each resource with the
|
|
9
|
+
* `bulkUpsert` behavior. Uses INSERT ... ON CONFLICT DO UPDATE SET.
|
|
10
|
+
*
|
|
11
|
+
* Uses `RETURNING <pk_columns>` (not `RETURNING *`) + follow-up SELECT
|
|
12
|
+
* to respect column-level SELECT grants.
|
|
13
|
+
*/
|
|
14
|
+
export const BulkUpsertPlugin = {
|
|
15
|
+
name: 'BulkUpsertPlugin',
|
|
16
|
+
version,
|
|
17
|
+
description: 'Adds bulk upsert (bulkUpsertX) mutations',
|
|
18
|
+
after: ['BulkTypesPlugin'],
|
|
19
|
+
schema: {
|
|
20
|
+
hooks: {
|
|
21
|
+
GraphQLObjectType_fields(fields, build, context) {
|
|
22
|
+
const { scope: { isRootMutation } } = context;
|
|
23
|
+
if (!isRootMutation)
|
|
24
|
+
return fields;
|
|
25
|
+
const { inflection, sql, graphql: { GraphQLNonNull }, options: { bulkUpsert: enableUpsert = true, bulkMaxRows = 1000 } } = build;
|
|
26
|
+
if (!enableUpsert)
|
|
27
|
+
return fields;
|
|
28
|
+
const pgRegistry = build.input.pgRegistry;
|
|
29
|
+
for (const [resourceName, resource] of Object.entries(pgRegistry.pgResources)) {
|
|
30
|
+
if (resource.parameters)
|
|
31
|
+
continue;
|
|
32
|
+
if (!resource.codec.attributes)
|
|
33
|
+
continue;
|
|
34
|
+
if (resource.codec.polymorphism)
|
|
35
|
+
continue;
|
|
36
|
+
if (resource.codec.isAnonymous)
|
|
37
|
+
continue;
|
|
38
|
+
if (!resource.uniques?.length)
|
|
39
|
+
continue;
|
|
40
|
+
if (!build.behavior.pgResourceMatches(resource, 'bulkUpsert'))
|
|
41
|
+
continue;
|
|
42
|
+
const typeName = inflection.tableType(resource.codec);
|
|
43
|
+
const fieldName = inflection.bulkUpsertField(resourceName);
|
|
44
|
+
const inputTypeName = inflection.bulkUpsertInputType(typeName);
|
|
45
|
+
const payloadTypeName = inflection.bulkUpsertPayloadType(typeName);
|
|
46
|
+
const inputType = build.getTypeByName(inputTypeName);
|
|
47
|
+
const payloadType = build.getTypeByName(payloadTypeName);
|
|
48
|
+
if (!inputType || !payloadType)
|
|
49
|
+
continue;
|
|
50
|
+
const columnSpecs = [];
|
|
51
|
+
const fieldToAttr = {};
|
|
52
|
+
for (const [attrName, attr] of Object.entries(resource.codec.attributes)) {
|
|
53
|
+
if (attr.extensions?.isInsertable === false)
|
|
54
|
+
continue;
|
|
55
|
+
const gqlFieldName = inflection.attribute({
|
|
56
|
+
attributeName: attrName,
|
|
57
|
+
codec: resource.codec
|
|
58
|
+
});
|
|
59
|
+
fieldToAttr[gqlFieldName] = attrName;
|
|
60
|
+
columnSpecs.push({
|
|
61
|
+
name: attrName,
|
|
62
|
+
sqlType: sql.compile(attr.codec.sqlType).text
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// Collect unique constraint attributes for excluding from updateColumns
|
|
66
|
+
const uniqueAttrs = new Set();
|
|
67
|
+
for (const unique of resource.uniques) {
|
|
68
|
+
for (const attr of unique.attributes) {
|
|
69
|
+
uniqueAttrs.add(attr);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Extract primary key columns for RETURNING clause
|
|
73
|
+
const primaryUnique = resource.uniques.find((u) => u.isPrimary) ?? resource.uniques[0];
|
|
74
|
+
const pkColumns = primaryUnique.attributes;
|
|
75
|
+
const compiledFrom = sql.compile(resource.from).text;
|
|
76
|
+
// Capture for plan closure
|
|
77
|
+
const executor = resource.executor;
|
|
78
|
+
const maxRows = bulkMaxRows;
|
|
79
|
+
fields = build.extend(fields, {
|
|
80
|
+
[fieldName]: context.fieldWithHooks({ fieldName }, {
|
|
81
|
+
description: `Bulk upsert rows into ${typeName}. Requires an ON CONFLICT constraint.`,
|
|
82
|
+
type: payloadType,
|
|
83
|
+
args: {
|
|
84
|
+
input: {
|
|
85
|
+
type: new GraphQLNonNull(inputType)
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
plan(_$root, args) {
|
|
89
|
+
const $input = args.getRaw('input');
|
|
90
|
+
const $result = sideEffectWithPgClient(executor, $input, async (pgClient, input) => {
|
|
91
|
+
const values = input.values;
|
|
92
|
+
if (!values || !Array.isArray(values) || values.length === 0) {
|
|
93
|
+
return { affectedCount: 0, returning: [] };
|
|
94
|
+
}
|
|
95
|
+
if (values.length > maxRows) {
|
|
96
|
+
throw new Error(`Bulk upsert exceeds maximum of ${maxRows} rows (got ${values.length})`);
|
|
97
|
+
}
|
|
98
|
+
if (!input.onConflict?.constraint) {
|
|
99
|
+
throw new Error('bulkUpsert requires onConflict.constraint');
|
|
100
|
+
}
|
|
101
|
+
const rows = values.map((row) => {
|
|
102
|
+
const mapped = {};
|
|
103
|
+
for (const [key, val] of Object.entries(row)) {
|
|
104
|
+
const attrName = fieldToAttr[key];
|
|
105
|
+
if (attrName) {
|
|
106
|
+
mapped[attrName] = val;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return mapped;
|
|
110
|
+
});
|
|
111
|
+
// Determine which columns to update
|
|
112
|
+
let updateColumns;
|
|
113
|
+
if (input.onConflict.updateColumns &&
|
|
114
|
+
input.onConflict.updateColumns.length > 0) {
|
|
115
|
+
updateColumns = input.onConflict.updateColumns;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// Default: all non-key columns
|
|
119
|
+
updateColumns = columnSpecs
|
|
120
|
+
.map((c) => c.name)
|
|
121
|
+
.filter((n) => !uniqueAttrs.has(n));
|
|
122
|
+
}
|
|
123
|
+
const batches = buildBulkInsertSQL(compiledFrom, columnSpecs, rows, pkColumns, {
|
|
124
|
+
conflictColumns: input.onConflict.constraint.split(','),
|
|
125
|
+
action: 'UPDATE',
|
|
126
|
+
updateColumns
|
|
127
|
+
});
|
|
128
|
+
let totalAffected = 0;
|
|
129
|
+
const allPkRows = [];
|
|
130
|
+
for (const batch of batches) {
|
|
131
|
+
const result = await pgClient.query(batch.text, batch.values);
|
|
132
|
+
totalAffected += result.rowCount ?? 0;
|
|
133
|
+
if (result.rows) {
|
|
134
|
+
allPkRows.push(...result.rows);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Follow-up SELECT using PKs to respect column-level grants
|
|
138
|
+
let returning = [];
|
|
139
|
+
if (allPkRows.length > 0) {
|
|
140
|
+
const pkConditions = allPkRows.map((pkRow, rowIdx) => {
|
|
141
|
+
return pkColumns.map((col, colIdx) => {
|
|
142
|
+
const paramIdx = rowIdx * pkColumns.length + colIdx + 1;
|
|
143
|
+
return `"${col}" = $${paramIdx}`;
|
|
144
|
+
}).join(' AND ');
|
|
145
|
+
});
|
|
146
|
+
const whereClause = pkConditions.map((c) => `(${c})`).join(' OR ');
|
|
147
|
+
const selectParams = allPkRows.flatMap((pkRow) => pkColumns.map((col) => pkRow[col]));
|
|
148
|
+
const selectResult = await pgClient.query(`SELECT * FROM ${compiledFrom} WHERE ${whereClause}`, selectParams);
|
|
149
|
+
returning = selectResult.rows || [];
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
affectedCount: totalAffected,
|
|
153
|
+
returning
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
return $result;
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
}, `Adding bulk upsert field for ${typeName}`);
|
|
160
|
+
}
|
|
161
|
+
return fields;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { BulkDeletePlugin } from './BulkDeletePlugin';
|
|
2
|
+
export { BulkInflectionPlugin } from './BulkInflectionPlugin';
|
|
3
|
+
export { BulkInsertPlugin } from './BulkInsertPlugin';
|
|
4
|
+
export { BulkRelationalPlugin } from './BulkRelationalPlugin';
|
|
5
|
+
export { BulkTypesPlugin } from './BulkTypesPlugin';
|
|
6
|
+
export { BulkUpdatePlugin } from './BulkUpdatePlugin';
|
|
7
|
+
export { BulkUpsertPlugin } from './BulkUpsertPlugin';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { BulkDeletePlugin } from './BulkDeletePlugin';
|
|
2
|
+
export { BulkInflectionPlugin } from './BulkInflectionPlugin';
|
|
3
|
+
export { BulkInsertPlugin } from './BulkInsertPlugin';
|
|
4
|
+
export { BulkRelationalPlugin } from './BulkRelationalPlugin';
|
|
5
|
+
export { BulkTypesPlugin } from './BulkTypesPlugin';
|
|
6
|
+
export { BulkUpdatePlugin } from './BulkUpdatePlugin';
|
|
7
|
+
export { BulkUpsertPlugin } from './BulkUpsertPlugin';
|
package/esm/preset.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulk Mutations Preset
|
|
3
|
+
*
|
|
4
|
+
* Provides a convenient preset factory for including bulk mutations
|
|
5
|
+
* in PostGraphile v5.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { BulkMutationPreset } from 'graphile-bulk-mutations';
|
|
10
|
+
*
|
|
11
|
+
* const preset = {
|
|
12
|
+
* extends: [
|
|
13
|
+
* BulkMutationPreset(),
|
|
14
|
+
* // or with options:
|
|
15
|
+
* BulkMutationPreset({
|
|
16
|
+
* bulkNaming: 'pluralized',
|
|
17
|
+
* bulkRelational: true,
|
|
18
|
+
* }),
|
|
19
|
+
* ],
|
|
20
|
+
* };
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import './augmentations';
|
|
24
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
25
|
+
import type { BulkMutationOptions } from './types';
|
|
26
|
+
/**
|
|
27
|
+
* Creates a preset that includes the bulk mutation plugins with the given options.
|
|
28
|
+
*
|
|
29
|
+
* Behavior is OFF by default for all tables. Tables must opt in via smart tags:
|
|
30
|
+
* COMMENT ON TABLE users IS E'@behavior +bulkInsert +bulkUpsert';
|
|
31
|
+
*
|
|
32
|
+
* The `enable_bulk` database setting must also be true for the plugin to load.
|
|
33
|
+
*/
|
|
34
|
+
export declare function BulkMutationPreset(options?: BulkMutationOptions): GraphileConfig.Preset;
|
|
35
|
+
export default BulkMutationPreset;
|
package/esm/preset.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulk Mutations Preset
|
|
3
|
+
*
|
|
4
|
+
* Provides a convenient preset factory for including bulk mutations
|
|
5
|
+
* in PostGraphile v5.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { BulkMutationPreset } from 'graphile-bulk-mutations';
|
|
10
|
+
*
|
|
11
|
+
* const preset = {
|
|
12
|
+
* extends: [
|
|
13
|
+
* BulkMutationPreset(),
|
|
14
|
+
* // or with options:
|
|
15
|
+
* BulkMutationPreset({
|
|
16
|
+
* bulkNaming: 'pluralized',
|
|
17
|
+
* bulkRelational: true,
|
|
18
|
+
* }),
|
|
19
|
+
* ],
|
|
20
|
+
* };
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import './augmentations';
|
|
24
|
+
import { BulkDeletePlugin, BulkInflectionPlugin, BulkInsertPlugin, BulkRelationalPlugin, BulkTypesPlugin, BulkUpdatePlugin, BulkUpsertPlugin } from './plugins';
|
|
25
|
+
/**
|
|
26
|
+
* Default schema options for bulk mutations.
|
|
27
|
+
*/
|
|
28
|
+
const defaultSchemaOptions = {
|
|
29
|
+
bulkNaming: 'bulk',
|
|
30
|
+
bulkInsert: true,
|
|
31
|
+
bulkUpsert: true,
|
|
32
|
+
bulkUpdate: true,
|
|
33
|
+
bulkDelete: true,
|
|
34
|
+
bulkRelational: false,
|
|
35
|
+
bulkReturning: true,
|
|
36
|
+
bulkMaxRows: 1000,
|
|
37
|
+
bulkMaxNestingDepth: 3,
|
|
38
|
+
bulkRequireWhere: true
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Creates a preset that includes the bulk mutation plugins with the given options.
|
|
42
|
+
*
|
|
43
|
+
* Behavior is OFF by default for all tables. Tables must opt in via smart tags:
|
|
44
|
+
* COMMENT ON TABLE users IS E'@behavior +bulkInsert +bulkUpsert';
|
|
45
|
+
*
|
|
46
|
+
* The `enable_bulk` database setting must also be true for the plugin to load.
|
|
47
|
+
*/
|
|
48
|
+
export function BulkMutationPreset(options = {}) {
|
|
49
|
+
const mergedOptions = { ...defaultSchemaOptions, ...options };
|
|
50
|
+
const plugins = [
|
|
51
|
+
BulkInflectionPlugin,
|
|
52
|
+
BulkTypesPlugin
|
|
53
|
+
];
|
|
54
|
+
if (mergedOptions.bulkInsert) {
|
|
55
|
+
plugins.push(BulkInsertPlugin);
|
|
56
|
+
}
|
|
57
|
+
if (mergedOptions.bulkUpsert) {
|
|
58
|
+
plugins.push(BulkUpsertPlugin);
|
|
59
|
+
}
|
|
60
|
+
if (mergedOptions.bulkUpdate) {
|
|
61
|
+
plugins.push(BulkUpdatePlugin);
|
|
62
|
+
}
|
|
63
|
+
if (mergedOptions.bulkDelete) {
|
|
64
|
+
plugins.push(BulkDeletePlugin);
|
|
65
|
+
}
|
|
66
|
+
if (mergedOptions.bulkRelational) {
|
|
67
|
+
plugins.push(BulkRelationalPlugin);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
plugins,
|
|
71
|
+
schema: mergedOptions
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export default BulkMutationPreset;
|
package/esm/types.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options and shared types for graphile-bulk-mutations.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Naming strategy for bulk mutation fields.
|
|
6
|
+
*
|
|
7
|
+
* - 'bulk': bulkCreateUser, bulkUpsertUser, bulkUpdateUser, bulkDeleteUser
|
|
8
|
+
* - 'pluralized': createUsers, upsertUsers, updateUsers, deleteUsers
|
|
9
|
+
* - 'many': createManyUsers, upsertManyUsers, updateManyUsers, deleteManyUsers
|
|
10
|
+
*/
|
|
11
|
+
export type BulkNamingStrategy = 'bulk' | 'pluralized' | 'many';
|
|
12
|
+
/**
|
|
13
|
+
* Configuration options for the bulk mutations preset.
|
|
14
|
+
*/
|
|
15
|
+
export interface BulkMutationOptions {
|
|
16
|
+
/** Naming strategy for bulk mutation fields. Default: 'bulk' */
|
|
17
|
+
bulkNaming?: BulkNamingStrategy;
|
|
18
|
+
/** Enable bulk insert mutations. Default: true */
|
|
19
|
+
bulkInsert?: boolean;
|
|
20
|
+
/** Enable bulk upsert mutations. Default: true */
|
|
21
|
+
bulkUpsert?: boolean;
|
|
22
|
+
/** Enable bulk update mutations. Default: true */
|
|
23
|
+
bulkUpdate?: boolean;
|
|
24
|
+
/** Enable bulk delete mutations. Default: true */
|
|
25
|
+
bulkDelete?: boolean;
|
|
26
|
+
/** Enable relational/nested inserts on bulk create. Default: false */
|
|
27
|
+
bulkRelational?: boolean;
|
|
28
|
+
/** Include returning rows in payload. Default: true */
|
|
29
|
+
bulkReturning?: boolean;
|
|
30
|
+
/** Max items in values array (insert/upsert). Default: 1000 */
|
|
31
|
+
bulkMaxRows?: number;
|
|
32
|
+
/** Max nesting depth for relational inserts. Default: 3 */
|
|
33
|
+
bulkMaxNestingDepth?: number;
|
|
34
|
+
/** Require non-empty where on bulk update/delete. Default: true */
|
|
35
|
+
bulkRequireWhere?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Max PostgreSQL parameters per query. Auto-batch splits at this limit.
|
|
39
|
+
*/
|
|
40
|
+
export declare const PG_MAX_PARAMS = 32000;
|
package/esm/types.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relation discovery utilities for bulk relational/nested inserts.
|
|
3
|
+
*
|
|
4
|
+
* Inspects the PostGraphile registry to find "has many" (isReferencee)
|
|
5
|
+
* relations for a given resource. For each qualifying relation, computes
|
|
6
|
+
* column metadata needed to build child INSERT statements.
|
|
7
|
+
*/
|
|
8
|
+
import type { ColumnSpec } from './sql-builder';
|
|
9
|
+
/**
|
|
10
|
+
* Metadata for a single "has many" relation eligible for nested inserts.
|
|
11
|
+
*/
|
|
12
|
+
export interface NestedRelationInfo {
|
|
13
|
+
/** Registry key of the relation (e.g. "productsByCategoryId") */
|
|
14
|
+
relationName: string;
|
|
15
|
+
/** GraphQL field name on the parent values item type */
|
|
16
|
+
fieldName: string;
|
|
17
|
+
/** The remote (child) PgResource */
|
|
18
|
+
remoteResource: any;
|
|
19
|
+
/** Registry key of the remote resource */
|
|
20
|
+
remoteResourceName: string;
|
|
21
|
+
/** PK columns on the parent that the FK references */
|
|
22
|
+
localAttributes: string[];
|
|
23
|
+
/** FK columns on the child table */
|
|
24
|
+
remoteAttributes: string[];
|
|
25
|
+
/** GraphQL field name → SQL column name for child */
|
|
26
|
+
childFieldToAttr: Record<string, string>;
|
|
27
|
+
/** Column specs for building child INSERT SQL */
|
|
28
|
+
childColumnSpecs: ColumnSpec[];
|
|
29
|
+
/** Compiled SQL table name for the child */
|
|
30
|
+
childCompiledFrom: string;
|
|
31
|
+
/** PK columns on the child table */
|
|
32
|
+
childPkColumns: string[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Discover "has many" relations for a resource that qualify for nested
|
|
36
|
+
* inserts. A relation qualifies when:
|
|
37
|
+
*
|
|
38
|
+
* 1. isReferencee is true (the remote table references the local table)
|
|
39
|
+
* 2. isUnique is false (one-to-many, not one-to-one)
|
|
40
|
+
* 3. The remote resource is a regular table (has attributes, uniques, etc.)
|
|
41
|
+
*/
|
|
42
|
+
export declare function discoverNestedRelations(resource: any, pgRegistry: any, inflection: any, sql: any): NestedRelationInfo[];
|