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.
Files changed (63) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +197 -0
  3. package/augmentations.d.ts +102 -0
  4. package/augmentations.js +11 -0
  5. package/esm/augmentations.d.ts +102 -0
  6. package/esm/augmentations.js +9 -0
  7. package/esm/index.d.ts +30 -0
  8. package/esm/index.js +29 -0
  9. package/esm/plugins/BulkDeletePlugin.d.ts +13 -0
  10. package/esm/plugins/BulkDeletePlugin.js +181 -0
  11. package/esm/plugins/BulkInflectionPlugin.d.ts +9 -0
  12. package/esm/plugins/BulkInflectionPlugin.js +103 -0
  13. package/esm/plugins/BulkInsertPlugin.d.ts +21 -0
  14. package/esm/plugins/BulkInsertPlugin.js +214 -0
  15. package/esm/plugins/BulkRelationalPlugin.d.ts +31 -0
  16. package/esm/plugins/BulkRelationalPlugin.js +127 -0
  17. package/esm/plugins/BulkTypesPlugin.d.ts +18 -0
  18. package/esm/plugins/BulkTypesPlugin.js +391 -0
  19. package/esm/plugins/BulkUpdatePlugin.d.ts +13 -0
  20. package/esm/plugins/BulkUpdatePlugin.js +211 -0
  21. package/esm/plugins/BulkUpsertPlugin.d.ts +12 -0
  22. package/esm/plugins/BulkUpsertPlugin.js +165 -0
  23. package/esm/plugins/index.d.ts +7 -0
  24. package/esm/plugins/index.js +7 -0
  25. package/esm/preset.d.ts +35 -0
  26. package/esm/preset.js +74 -0
  27. package/esm/types.d.ts +40 -0
  28. package/esm/types.js +7 -0
  29. package/esm/utils/index.d.ts +4 -0
  30. package/esm/utils/index.js +2 -0
  31. package/esm/utils/relations.d.ts +42 -0
  32. package/esm/utils/relations.js +83 -0
  33. package/esm/utils/sql-builder.d.ts +56 -0
  34. package/esm/utils/sql-builder.js +121 -0
  35. package/index.d.ts +30 -0
  36. package/index.js +44 -0
  37. package/package.json +61 -0
  38. package/plugins/BulkDeletePlugin.d.ts +13 -0
  39. package/plugins/BulkDeletePlugin.js +184 -0
  40. package/plugins/BulkInflectionPlugin.d.ts +9 -0
  41. package/plugins/BulkInflectionPlugin.js +106 -0
  42. package/plugins/BulkInsertPlugin.d.ts +21 -0
  43. package/plugins/BulkInsertPlugin.js +217 -0
  44. package/plugins/BulkRelationalPlugin.d.ts +31 -0
  45. package/plugins/BulkRelationalPlugin.js +130 -0
  46. package/plugins/BulkTypesPlugin.d.ts +18 -0
  47. package/plugins/BulkTypesPlugin.js +394 -0
  48. package/plugins/BulkUpdatePlugin.d.ts +13 -0
  49. package/plugins/BulkUpdatePlugin.js +214 -0
  50. package/plugins/BulkUpsertPlugin.d.ts +12 -0
  51. package/plugins/BulkUpsertPlugin.js +168 -0
  52. package/plugins/index.d.ts +7 -0
  53. package/plugins/index.js +17 -0
  54. package/preset.d.ts +35 -0
  55. package/preset.js +77 -0
  56. package/types.d.ts +40 -0
  57. package/types.js +10 -0
  58. package/utils/index.d.ts +4 -0
  59. package/utils/index.js +9 -0
  60. package/utils/relations.d.ts +42 -0
  61. package/utils/relations.js +86 -0
  62. package/utils/sql-builder.d.ts +56 -0
  63. package/utils/sql-builder.js +126 -0
@@ -0,0 +1,127 @@
1
+ import '../augmentations';
2
+ import { discoverNestedRelations } from '../utils/relations';
3
+ const version = '0.1.0';
4
+ /**
5
+ * BulkRelationalPlugin
6
+ *
7
+ * Adds relational/nested insert support to bulk mutations. When enabled
8
+ * via `bulkRelational: true`, parent values item input types gain optional
9
+ * fields for each "has many" relation, allowing creation of parent and
10
+ * child rows in a single mutation.
11
+ *
12
+ * Example:
13
+ * mutation {
14
+ * bulkCreateCategories(input: {
15
+ * values: [{
16
+ * name: "Electronics"
17
+ * products: [
18
+ * { name: "Laptop", price: "999.99" }
19
+ * ]
20
+ * }]
21
+ * }) { affectedCount }
22
+ * }
23
+ *
24
+ * The FK column on the child table (e.g. category_id) is automatically
25
+ * populated from the parent's returned primary key. Nested input types
26
+ * exclude FK columns so the user does not need to provide them.
27
+ *
28
+ * Limitations:
29
+ * - ON CONFLICT cannot be used with nested inserts (throws an error)
30
+ * - Only one level of nesting is supported in the initial implementation
31
+ */
32
+ export const BulkRelationalPlugin = {
33
+ name: 'BulkRelationalPlugin',
34
+ version,
35
+ description: 'Adds relational/nested insert support to bulk mutations',
36
+ after: ['BulkTypesPlugin', 'BulkInsertPlugin'],
37
+ schema: {
38
+ hooks: {
39
+ init(_, build) {
40
+ const { inflection, sql, options: { bulkRelational: enableRelational = false } } = build;
41
+ if (!enableRelational)
42
+ return _;
43
+ const pgRegistry = build.input.pgRegistry;
44
+ for (const [_resourceName, resource] of Object.entries(pgRegistry.pgResources)) {
45
+ if (resource.parameters)
46
+ continue;
47
+ if (!resource.codec.attributes)
48
+ continue;
49
+ if (resource.codec.polymorphism)
50
+ continue;
51
+ if (resource.codec.isAnonymous)
52
+ continue;
53
+ if (!resource.uniques?.length)
54
+ continue;
55
+ if (!build.behavior.pgResourceMatches(resource, 'bulkInsert'))
56
+ continue;
57
+ const parentTypeName = inflection.tableType(resource.codec);
58
+ const nestedRelations = discoverNestedRelations(resource, pgRegistry, inflection, sql);
59
+ for (const rel of nestedRelations) {
60
+ const childTypeName = inflection.tableType(rel.remoteResource.codec);
61
+ const nestedInputTypeName = inflection.bulkNestedValuesItemType(parentTypeName, childTypeName);
62
+ // FK columns to exclude from nested input type
63
+ const fkAttrSet = new Set(rel.remoteAttributes);
64
+ build.registerInputObjectType(nestedInputTypeName, { isBulkRelationalNestedInput: true }, () => ({
65
+ description: `A row to nest-create for ${childTypeName} when bulk creating ${parentTypeName}. FK columns are auto-populated from the parent.`,
66
+ fields: () => {
67
+ const result = {};
68
+ for (const [attrName, attr] of Object.entries(rel.remoteResource.codec.attributes)) {
69
+ if (attr.extensions?.isInsertable === false)
70
+ continue;
71
+ // Exclude FK columns — they are auto-filled from parent PK
72
+ if (fkAttrSet.has(attrName))
73
+ continue;
74
+ const fieldName = inflection.attribute({
75
+ attributeName: attrName,
76
+ codec: rel.remoteResource.codec
77
+ });
78
+ const inputType = build.getGraphQLTypeByPgCodec(attr.codec, 'input');
79
+ if (!inputType)
80
+ continue;
81
+ const isRequired = !!attr.notNull && !attr.hasDefault;
82
+ result[fieldName] = {
83
+ description: `Value for ${attrName}`,
84
+ type: isRequired
85
+ ? new build.graphql.GraphQLNonNull(inputType)
86
+ : inputType
87
+ };
88
+ }
89
+ return result;
90
+ }
91
+ }), `Nested input for ${childTypeName} in ${parentTypeName}`);
92
+ }
93
+ }
94
+ return _;
95
+ },
96
+ // Add nested relation fields to existing values item input types
97
+ GraphQLInputObjectType_fields(fields, build, context) {
98
+ const { scope: { isBulkMutationValuesItem, bulkMutationResourceName } } = context;
99
+ if (!isBulkMutationValuesItem || !bulkMutationResourceName)
100
+ return fields;
101
+ const { inflection, sql, options: { bulkRelational: enableRelational = false } } = build;
102
+ if (!enableRelational)
103
+ return fields;
104
+ const pgRegistry = build.input.pgRegistry;
105
+ const resource = pgRegistry.pgResources[bulkMutationResourceName];
106
+ if (!resource)
107
+ return fields;
108
+ const parentTypeName = inflection.tableType(resource.codec);
109
+ const nestedRelations = discoverNestedRelations(resource, pgRegistry, inflection, sql);
110
+ for (const rel of nestedRelations) {
111
+ const childTypeName = inflection.tableType(rel.remoteResource.codec);
112
+ const nestedInputTypeName = inflection.bulkNestedValuesItemType(parentTypeName, childTypeName);
113
+ const nestedInputType = build.getTypeByName(nestedInputTypeName);
114
+ if (!nestedInputType)
115
+ continue;
116
+ fields = build.extend(fields, {
117
+ [rel.fieldName]: {
118
+ description: `Nested ${childTypeName} rows to create for this ${parentTypeName}.`,
119
+ type: new build.graphql.GraphQLList(new build.graphql.GraphQLNonNull(nestedInputType))
120
+ }
121
+ }, `Adding nested field ${rel.fieldName} to ${parentTypeName} values item`);
122
+ }
123
+ return fields;
124
+ }
125
+ }
126
+ }
127
+ };
@@ -0,0 +1,18 @@
1
+ import '../augmentations';
2
+ import type { GraphileConfig } from 'graphile-config';
3
+ /**
4
+ * BulkTypesPlugin
5
+ *
6
+ * Registers all shared types for bulk mutations:
7
+ * 1. ConflictAction enum (IGNORE)
8
+ * 2. Per-table unique constraint enums (e.g. UserUniqueConstraint)
9
+ * 3. Per-table column enums (e.g. UserColumn)
10
+ * 4. Per-table ON CONFLICT input types
11
+ * 5. Per-table values item input types
12
+ * 6. Per-table input types (wrapping values + onConflict)
13
+ * 7. Per-table payload types (affectedCount + returning)
14
+ *
15
+ * Behavior: OFF by default. Tables must opt in via smart tags:
16
+ * @behavior +bulkInsert +bulkUpsert +bulkUpdate +bulkDelete
17
+ */
18
+ export declare const BulkTypesPlugin: GraphileConfig.Plugin;
@@ -0,0 +1,391 @@
1
+ import '../augmentations';
2
+ import { pgSelectFromRecords } from '@dataplan/pg';
3
+ import { access } from 'grafast';
4
+ const version = '0.1.0';
5
+ /**
6
+ * Checks if a resource is suitable for bulk mutations:
7
+ * - Must have attributes (is a table)
8
+ * - Must not be polymorphic
9
+ * - Must not be anonymous
10
+ * - Must have at least one unique constraint (for ON CONFLICT)
11
+ * - Must not have parameters (not a function)
12
+ */
13
+ function isBulkMutationCandidate(resource) {
14
+ return (!resource.parameters &&
15
+ !!resource.codec.attributes &&
16
+ !resource.codec.polymorphism &&
17
+ !resource.codec.isAnonymous &&
18
+ !!resource.uniques?.length);
19
+ }
20
+ /**
21
+ * BulkTypesPlugin
22
+ *
23
+ * Registers all shared types for bulk mutations:
24
+ * 1. ConflictAction enum (IGNORE)
25
+ * 2. Per-table unique constraint enums (e.g. UserUniqueConstraint)
26
+ * 3. Per-table column enums (e.g. UserColumn)
27
+ * 4. Per-table ON CONFLICT input types
28
+ * 5. Per-table values item input types
29
+ * 6. Per-table input types (wrapping values + onConflict)
30
+ * 7. Per-table payload types (affectedCount + returning)
31
+ *
32
+ * Behavior: OFF by default. Tables must opt in via smart tags:
33
+ * @behavior +bulkInsert +bulkUpsert +bulkUpdate +bulkDelete
34
+ */
35
+ export const BulkTypesPlugin = {
36
+ name: 'BulkTypesPlugin',
37
+ version,
38
+ description: 'Registers bulk mutation types (enums, inputs, payloads)',
39
+ after: ['PgMutationCreatePlugin', 'PgMutationUpdateDeletePlugin', 'BulkInflectionPlugin'],
40
+ schema: {
41
+ behaviorRegistry: {
42
+ add: {
43
+ bulkInsert: {
44
+ description: 'Enable bulk insert mutation for this resource',
45
+ entities: ['pgResource']
46
+ },
47
+ bulkUpsert: {
48
+ description: 'Enable bulk upsert mutation for this resource',
49
+ entities: ['pgResource']
50
+ },
51
+ bulkUpdate: {
52
+ description: 'Enable bulk update mutation for this resource',
53
+ entities: ['pgResource']
54
+ },
55
+ bulkDelete: {
56
+ description: 'Enable bulk delete mutation for this resource',
57
+ entities: ['pgResource']
58
+ }
59
+ }
60
+ },
61
+ // Default: OFF. Tables must explicitly opt in via smart tags.
62
+ entityBehavior: {
63
+ pgResource: {
64
+ inferred(behavior) {
65
+ return [behavior, '-bulkInsert -bulkUpsert -bulkUpdate -bulkDelete'];
66
+ }
67
+ }
68
+ },
69
+ hooks: {
70
+ init(_, build) {
71
+ const { inflection, options: { bulkInsert: enableInsert = true, bulkUpsert: enableUpsert = true, bulkUpdate: enableUpdate = true, bulkDelete: enableDelete = true } } = build;
72
+ // Register the ConflictAction enum (shared across all tables)
73
+ build.registerEnumType('ConflictAction', {}, () => ({
74
+ description: 'Action to take when a conflict occurs during bulk insert.',
75
+ values: {
76
+ IGNORE: {
77
+ value: 'IGNORE',
78
+ description: 'Skip the conflicting row (ON CONFLICT DO NOTHING).'
79
+ }
80
+ }
81
+ }), 'ConflictAction enum for bulk mutations');
82
+ // Iterate over all resources to register per-table types
83
+ const pgRegistry = build.input.pgRegistry;
84
+ for (const [resourceName, resource] of Object.entries(pgRegistry.pgResources)) {
85
+ if (!isBulkMutationCandidate(resource))
86
+ continue;
87
+ const hasInsert = enableInsert &&
88
+ build.behavior.pgResourceMatches(resource, 'bulkInsert');
89
+ const hasUpsert = enableUpsert &&
90
+ build.behavior.pgResourceMatches(resource, 'bulkUpsert');
91
+ const hasUpdate = enableUpdate &&
92
+ build.behavior.pgResourceMatches(resource, 'bulkUpdate');
93
+ const hasDelete = enableDelete &&
94
+ build.behavior.pgResourceMatches(resource, 'bulkDelete');
95
+ if (!hasInsert && !hasUpsert && !hasUpdate && !hasDelete)
96
+ continue;
97
+ const typeName = inflection.tableType(resource.codec);
98
+ // Per-table unique constraint enum
99
+ if (hasInsert || hasUpsert) {
100
+ const constraintEnumName = inflection.uniqueConstraintEnumType(typeName);
101
+ const values = {};
102
+ for (const unique of resource.uniques) {
103
+ // Use column-based conflict targeting (ON CONFLICT (col1, col2))
104
+ // rather than constraint names, since PgResourceUnique doesn't
105
+ // store the actual PostgreSQL constraint name.
106
+ const enumKey = unique.isPrimary
107
+ ? `${resourceName}_pkey`
108
+ : `${resourceName}_${unique.attributes.join('_')}_key`;
109
+ const enumValue = inflection
110
+ .constantCase(enumKey)
111
+ .toUpperCase();
112
+ // Store comma-separated column list as the enum value
113
+ values[enumValue] = {
114
+ value: unique.attributes.join(','),
115
+ description: unique.isPrimary
116
+ ? `Primary key constraint on ${resourceName}`
117
+ : `Unique constraint on (${unique.attributes.join(', ')})`
118
+ };
119
+ }
120
+ build.registerEnumType(constraintEnumName, { isBulkMutationConstraintEnum: true }, () => ({
121
+ description: `Unique constraints on ${typeName} for ON CONFLICT targeting.`,
122
+ values
123
+ }), `Unique constraint enum for ${typeName}`);
124
+ }
125
+ // Per-table column enum
126
+ if (hasUpsert) {
127
+ const columnEnumName = inflection.columnEnumType(typeName);
128
+ const values = {};
129
+ for (const [attrName, attr] of Object.entries(resource.codec.attributes)) {
130
+ if (attr.extensions?.isInsertable === false)
131
+ continue;
132
+ const enumValue = inflection
133
+ .constantCase(attrName)
134
+ .toUpperCase();
135
+ values[enumValue] = {
136
+ value: attrName,
137
+ description: `Column ${attrName}`
138
+ };
139
+ }
140
+ build.registerEnumType(columnEnumName, { isBulkMutationColumnEnum: true }, () => ({
141
+ description: `Columns of ${typeName} available for upsert SET clause.`,
142
+ values
143
+ }), `Column enum for ${typeName}`);
144
+ }
145
+ // Per-table ON CONFLICT input for insert
146
+ if (hasInsert) {
147
+ const onConflictName = inflection.bulkInsertOnConflictInputType(typeName);
148
+ const constraintEnumName = inflection.uniqueConstraintEnumType(typeName);
149
+ build.registerInputObjectType(onConflictName, { isBulkMutationOnConflictInput: true }, () => ({
150
+ description: `ON CONFLICT options for bulk creating ${typeName}.`,
151
+ fields: ({ fieldWithHooks }) => ({
152
+ constraint: fieldWithHooks({ fieldName: 'constraint' }, () => ({
153
+ description: 'The unique constraint to use for conflict detection.',
154
+ type: build.getTypeByName(constraintEnumName)
155
+ })),
156
+ action: fieldWithHooks({ fieldName: 'action' }, () => ({
157
+ description: 'The action to take on conflict.',
158
+ type: new build.graphql.GraphQLNonNull(build.getTypeByName('ConflictAction'))
159
+ }))
160
+ })
161
+ }), `ON CONFLICT input for bulk insert ${typeName}`);
162
+ }
163
+ // Per-table ON CONFLICT input for upsert
164
+ if (hasUpsert) {
165
+ const onConflictName = inflection.bulkUpsertOnConflictInputType(typeName);
166
+ const constraintEnumName = inflection.uniqueConstraintEnumType(typeName);
167
+ const columnEnumName = inflection.columnEnumType(typeName);
168
+ build.registerInputObjectType(onConflictName, { isBulkMutationOnConflictInput: true }, () => ({
169
+ description: `ON CONFLICT options for bulk upserting ${typeName}.`,
170
+ fields: ({ fieldWithHooks }) => ({
171
+ constraint: fieldWithHooks({ fieldName: 'constraint' }, () => ({
172
+ description: 'The unique constraint to use for conflict detection.',
173
+ type: new build.graphql.GraphQLNonNull(build.getTypeByName(constraintEnumName))
174
+ })),
175
+ updateColumns: fieldWithHooks({ fieldName: 'updateColumns' }, () => ({
176
+ description: 'Columns to update on conflict. If omitted, all non-key columns are updated.',
177
+ type: new build.graphql.GraphQLList(new build.graphql.GraphQLNonNull(build.getTypeByName(columnEnumName)))
178
+ }))
179
+ })
180
+ }), `ON CONFLICT input for bulk upsert ${typeName}`);
181
+ }
182
+ // Per-table values item input type (for insert)
183
+ if (hasInsert || hasUpsert) {
184
+ const itemTypeName = hasInsert
185
+ ? inflection.bulkInsertValuesItemType(typeName)
186
+ : inflection.bulkUpsertValuesItemType(typeName);
187
+ build.registerInputObjectType(itemTypeName, {
188
+ isBulkMutationValuesItem: true,
189
+ bulkMutationResourceName: resourceName
190
+ }, () => ({
191
+ description: `A single row to insert for ${typeName}.`,
192
+ fields: () => {
193
+ const result = {};
194
+ for (const [attrName, attr] of Object.entries(resource.codec.attributes)) {
195
+ if (attr.extensions?.isInsertable === false)
196
+ continue;
197
+ const fieldName = inflection.attribute({
198
+ attributeName: attrName,
199
+ codec: resource.codec
200
+ });
201
+ const inputType = build.getGraphQLTypeByPgCodec(attr.codec, 'input');
202
+ if (!inputType)
203
+ continue;
204
+ const isRequired = !!attr.notNull && !attr.hasDefault;
205
+ result[fieldName] = {
206
+ description: `Value for ${attrName}`,
207
+ type: isRequired
208
+ ? new build.graphql.GraphQLNonNull(inputType)
209
+ : inputType
210
+ };
211
+ }
212
+ return result;
213
+ }
214
+ }), `Values item input for bulk insert ${typeName}`);
215
+ // Register upsert item separately if both exist
216
+ if (hasInsert && hasUpsert) {
217
+ const upsertItemTypeName = inflection.bulkUpsertValuesItemType(typeName);
218
+ build.registerInputObjectType(upsertItemTypeName, {
219
+ isBulkMutationValuesItem: true,
220
+ bulkMutationResourceName: resourceName
221
+ }, () => ({
222
+ description: `A single row to upsert for ${typeName}.`,
223
+ fields: () => {
224
+ const result = {};
225
+ for (const [attrName, attr] of Object.entries(resource.codec.attributes)) {
226
+ if (attr.extensions?.isInsertable === false)
227
+ continue;
228
+ const fieldName = inflection.attribute({
229
+ attributeName: attrName,
230
+ codec: resource.codec
231
+ });
232
+ const inputType = build.getGraphQLTypeByPgCodec(attr.codec, 'input');
233
+ if (!inputType)
234
+ continue;
235
+ const isRequired = !!attr.notNull && !attr.hasDefault;
236
+ result[fieldName] = {
237
+ description: `Value for ${attrName}`,
238
+ type: isRequired
239
+ ? new build.graphql.GraphQLNonNull(inputType)
240
+ : inputType
241
+ };
242
+ }
243
+ return result;
244
+ }
245
+ }), `Values item input for bulk upsert ${typeName}`);
246
+ }
247
+ }
248
+ // Per-table wrapper input types
249
+ if (hasInsert) {
250
+ const inputName = inflection.bulkInsertInputType(typeName);
251
+ const itemTypeName = inflection.bulkInsertValuesItemType(typeName);
252
+ const onConflictName = inflection.bulkInsertOnConflictInputType(typeName);
253
+ build.registerInputObjectType(inputName, { isBulkMutationInput: true }, () => ({
254
+ description: `Input for bulk creating ${typeName} rows.`,
255
+ fields: ({ fieldWithHooks }) => ({
256
+ values: fieldWithHooks({ fieldName: 'values' }, () => ({
257
+ description: 'The rows to insert.',
258
+ type: new build.graphql.GraphQLNonNull(new build.graphql.GraphQLList(new build.graphql.GraphQLNonNull(build.getTypeByName(itemTypeName))))
259
+ })),
260
+ onConflict: fieldWithHooks({ fieldName: 'onConflict' }, () => ({
261
+ description: 'Optional ON CONFLICT handling for duplicate keys.',
262
+ type: build.getTypeByName(onConflictName)
263
+ }))
264
+ })
265
+ }), `Input for bulk insert ${typeName}`);
266
+ }
267
+ if (hasUpsert) {
268
+ const inputName = inflection.bulkUpsertInputType(typeName);
269
+ const itemTypeName = inflection.bulkUpsertValuesItemType(typeName);
270
+ const onConflictName = inflection.bulkUpsertOnConflictInputType(typeName);
271
+ build.registerInputObjectType(inputName, { isBulkMutationInput: true }, () => ({
272
+ description: `Input for bulk upserting ${typeName} rows.`,
273
+ fields: ({ fieldWithHooks }) => ({
274
+ values: fieldWithHooks({ fieldName: 'values' }, () => ({
275
+ description: 'The rows to upsert.',
276
+ type: new build.graphql.GraphQLNonNull(new build.graphql.GraphQLList(new build.graphql.GraphQLNonNull(build.getTypeByName(itemTypeName))))
277
+ })),
278
+ onConflict: fieldWithHooks({ fieldName: 'onConflict' }, () => ({
279
+ description: 'The unique constraint to target and which columns to update.',
280
+ type: new build.graphql.GraphQLNonNull(build.getTypeByName(onConflictName))
281
+ }))
282
+ })
283
+ }), `Input for bulk upsert ${typeName}`);
284
+ }
285
+ if (hasUpdate) {
286
+ const inputName = inflection.bulkUpdateInputType(typeName);
287
+ const patchTypeName = inflection.patchType(typeName);
288
+ build.registerInputObjectType(inputName, { isBulkMutationInput: true }, () => ({
289
+ description: `Input for bulk updating ${typeName} rows.`,
290
+ fields: ({ fieldWithHooks }) => ({
291
+ where: fieldWithHooks({ fieldName: 'where' }, () => {
292
+ // Try to use connection-filter type if available
293
+ const filterTypeName = `${typeName}Filter`;
294
+ const filterType = build.getTypeByName(filterTypeName);
295
+ // Fall back to PostGraphile's built-in condition type
296
+ const conditionTypeName = inflection.conditionType(typeName);
297
+ const conditionType = build.getTypeByName(conditionTypeName);
298
+ const whereType = filterType || conditionType;
299
+ return {
300
+ description: 'Condition to select which rows to update.',
301
+ type: whereType
302
+ ? new build.graphql.GraphQLNonNull(whereType)
303
+ : build.graphql.GraphQLString
304
+ };
305
+ }),
306
+ patch: fieldWithHooks({ fieldName: 'patch' }, () => ({
307
+ description: 'The fields to update.',
308
+ type: new build.graphql.GraphQLNonNull(build.getTypeByName(patchTypeName))
309
+ }))
310
+ })
311
+ }), `Input for bulk update ${typeName}`);
312
+ }
313
+ if (hasDelete) {
314
+ const inputName = inflection.bulkDeleteInputType(typeName);
315
+ build.registerInputObjectType(inputName, { isBulkMutationInput: true }, () => ({
316
+ description: `Input for bulk deleting ${typeName} rows.`,
317
+ fields: ({ fieldWithHooks }) => ({
318
+ where: fieldWithHooks({ fieldName: 'where' }, () => {
319
+ const filterTypeName = `${typeName}Filter`;
320
+ const filterType = build.getTypeByName(filterTypeName);
321
+ const conditionTypeName = inflection.conditionType(typeName);
322
+ const conditionType = build.getTypeByName(conditionTypeName);
323
+ const whereType = filterType || conditionType;
324
+ return {
325
+ description: 'Condition to select which rows to delete.',
326
+ type: whereType
327
+ ? new build.graphql.GraphQLNonNull(whereType)
328
+ : build.graphql.GraphQLString
329
+ };
330
+ })
331
+ })
332
+ }), `Input for bulk delete ${typeName}`);
333
+ }
334
+ // Payload types
335
+ const registerPayload = (payloadName, description, payloadResource) => {
336
+ build.registerObjectType(payloadName, { isBulkMutationPayload: true }, () => ({
337
+ description,
338
+ fields: ({ fieldWithHooks }) => {
339
+ const outputType = build.getTypeByName(typeName);
340
+ const fields = {
341
+ clientMutationId: fieldWithHooks({ fieldName: 'clientMutationId' }, () => ({
342
+ description: 'The exact same clientMutationId that was provided in the mutation input.',
343
+ type: build.graphql.GraphQLString
344
+ })),
345
+ affectedCount: fieldWithHooks({
346
+ fieldName: 'affectedCount',
347
+ isBulkMutationPayloadAffectedCountField: true
348
+ }, () => ({
349
+ description: 'The number of rows affected by the mutation.',
350
+ type: new build.graphql.GraphQLNonNull(build.graphql.GraphQLInt)
351
+ }))
352
+ };
353
+ if (outputType) {
354
+ fields.returning = fieldWithHooks({
355
+ fieldName: 'returning',
356
+ isBulkMutationPayloadReturningField: true
357
+ }, () => ({
358
+ description: 'The rows affected by the mutation.',
359
+ type: new build.graphql.GraphQLNonNull(new build.graphql.GraphQLList(new build.graphql.GraphQLNonNull(outputType))),
360
+ plan($parent) {
361
+ const $records = access($parent, 'returning');
362
+ return pgSelectFromRecords(payloadResource, $records);
363
+ }
364
+ }));
365
+ }
366
+ fields.query = fieldWithHooks({ fieldName: 'query' }, () => ({
367
+ description: 'Access the root query type. Useful for re-fetching data after a mutation.',
368
+ type: build.getTypeByName('Query')
369
+ }));
370
+ return fields;
371
+ }
372
+ }), `${description} payload`);
373
+ };
374
+ if (hasInsert) {
375
+ registerPayload(inflection.bulkInsertPayloadType(typeName), `The output of the bulk create ${typeName} mutation.`, resource);
376
+ }
377
+ if (hasUpsert) {
378
+ registerPayload(inflection.bulkUpsertPayloadType(typeName), `The output of the bulk upsert ${typeName} mutation.`, resource);
379
+ }
380
+ if (hasUpdate) {
381
+ registerPayload(inflection.bulkUpdatePayloadType(typeName), `The output of the bulk update ${typeName} mutation.`, resource);
382
+ }
383
+ if (hasDelete) {
384
+ registerPayload(inflection.bulkDeletePayloadType(typeName), `The output of the bulk delete ${typeName} mutation.`, resource);
385
+ }
386
+ }
387
+ return _;
388
+ }
389
+ }
390
+ }
391
+ };
@@ -0,0 +1,13 @@
1
+ import '../augmentations';
2
+ import type { GraphileConfig } from 'graphile-config';
3
+ /**
4
+ * BulkUpdatePlugin
5
+ *
6
+ * Registers `bulkUpdateX` mutation fields for each resource with the
7
+ * `bulkUpdate` behavior. Uses UPDATE ... SET ... WHERE with conditions
8
+ * from graphile-connection-filter (or PostGraphile's built-in condition type).
9
+ *
10
+ * Uses `RETURNING <pk_columns>` (not `RETURNING *`) + follow-up SELECT
11
+ * to respect column-level SELECT grants.
12
+ */
13
+ export declare const BulkUpdatePlugin: GraphileConfig.Plugin;