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,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BulkInflectionPlugin = void 0;
4
+ require("../augmentations");
5
+ /**
6
+ * Builds a mutation field name based on the naming strategy.
7
+ *
8
+ * - 'bulk': bulkCreateUser
9
+ * - 'pluralized': createUsers
10
+ * - 'many': createManyUsers
11
+ */
12
+ function buildFieldName(inflection, verb, resourceName, naming) {
13
+ const typeName = inflection.upperCamelCase(resourceName);
14
+ switch (naming) {
15
+ case 'pluralized':
16
+ return inflection.camelCase(`${verb}_${inflection.pluralize(resourceName)}`);
17
+ case 'many':
18
+ return inflection.camelCase(`${verb}_many_${inflection.pluralize(resourceName)}`);
19
+ case 'bulk':
20
+ default:
21
+ return inflection.camelCase(`bulk_${verb}_${typeName}`);
22
+ }
23
+ }
24
+ /**
25
+ * BulkInflectionPlugin
26
+ *
27
+ * Adds inflection methods for naming bulk mutation types, fields, and enums.
28
+ * Naming is configurable via the `bulkNaming` schema option.
29
+ */
30
+ exports.BulkInflectionPlugin = {
31
+ name: 'BulkInflectionPlugin',
32
+ version: '0.1.0',
33
+ description: 'Adds inflection methods for bulk mutation type naming',
34
+ inflection: {
35
+ add: {
36
+ // Mutation field names — read naming from preset schema options
37
+ bulkInsertField(_preset, resourceName) {
38
+ const naming = (_preset?.schema?.bulkNaming ?? 'bulk');
39
+ return buildFieldName(this, 'create', resourceName, naming);
40
+ },
41
+ bulkUpsertField(_preset, resourceName) {
42
+ const naming = (_preset?.schema?.bulkNaming ?? 'bulk');
43
+ return buildFieldName(this, 'upsert', resourceName, naming);
44
+ },
45
+ bulkUpdateField(_preset, resourceName) {
46
+ const naming = (_preset?.schema?.bulkNaming ?? 'bulk');
47
+ return buildFieldName(this, 'update', resourceName, naming);
48
+ },
49
+ bulkDeleteField(_preset, resourceName) {
50
+ const naming = (_preset?.schema?.bulkNaming ?? 'bulk');
51
+ return buildFieldName(this, 'delete', resourceName, naming);
52
+ },
53
+ // Payload type names
54
+ bulkInsertPayloadType(_preset, typeName) {
55
+ return `BulkCreate${typeName}Payload`;
56
+ },
57
+ bulkUpsertPayloadType(_preset, typeName) {
58
+ return `BulkUpsert${typeName}Payload`;
59
+ },
60
+ bulkUpdatePayloadType(_preset, typeName) {
61
+ return `BulkUpdate${typeName}Payload`;
62
+ },
63
+ bulkDeletePayloadType(_preset, typeName) {
64
+ return `BulkDelete${typeName}Payload`;
65
+ },
66
+ // Input type names
67
+ bulkInsertInputType(_preset, typeName) {
68
+ return `BulkCreate${typeName}Input`;
69
+ },
70
+ bulkUpsertInputType(_preset, typeName) {
71
+ return `BulkUpsert${typeName}Input`;
72
+ },
73
+ bulkUpdateInputType(_preset, typeName) {
74
+ return `BulkUpdate${typeName}Input`;
75
+ },
76
+ bulkDeleteInputType(_preset, typeName) {
77
+ return `BulkDelete${typeName}Input`;
78
+ },
79
+ // Per-table enum types
80
+ uniqueConstraintEnumType(_preset, typeName) {
81
+ return `${typeName}UniqueConstraint`;
82
+ },
83
+ columnEnumType(_preset, typeName) {
84
+ return `${typeName}Column`;
85
+ },
86
+ // ON CONFLICT input types
87
+ bulkInsertOnConflictInputType(_preset, typeName) {
88
+ return `BulkCreate${typeName}OnConflictInput`;
89
+ },
90
+ bulkUpsertOnConflictInputType(_preset, typeName) {
91
+ return `BulkUpsert${typeName}OnConflictInput`;
92
+ },
93
+ // Values item types
94
+ bulkInsertValuesItemType(_preset, typeName) {
95
+ return `${typeName}BulkCreateItem`;
96
+ },
97
+ bulkUpsertValuesItemType(_preset, typeName) {
98
+ return `${typeName}BulkUpsertItem`;
99
+ },
100
+ // Relational nested input type names
101
+ bulkNestedValuesItemType(_preset, parentTypeName, childTypeName) {
102
+ return `${childTypeName}BulkNestedIn${parentTypeName}Input`;
103
+ }
104
+ }
105
+ }
106
+ };
@@ -0,0 +1,21 @@
1
+ import '../augmentations';
2
+ import type { GraphileConfig } from 'graphile-config';
3
+ /**
4
+ * BulkInsertPlugin
5
+ *
6
+ * Registers `bulkCreateX` mutation fields on the root mutation type for
7
+ * each resource that has the `bulkInsert` behavior.
8
+ *
9
+ * Uses `sideEffectWithPgClient` for raw SQL execution. The mutation
10
+ * SQL uses `RETURNING <pk_columns>` (not `RETURNING *`) to respect
11
+ * column-level SELECT grants. A follow-up SELECT retrieves the full
12
+ * rows using only the returned PKs, ensuring the query runs through
13
+ * the normal grant-aware pipeline.
14
+ *
15
+ * When `bulkRelational` is enabled, nested child records are detected
16
+ * in the input and inserted in a layered fashion: parent rows first,
17
+ * then child rows with FK values auto-populated from the parent PKs.
18
+ *
19
+ * Pattern from: https://github.com/pyramation/graphile-column-privileges-mutations
20
+ */
21
+ export declare const BulkInsertPlugin: GraphileConfig.Plugin;
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BulkInsertPlugin = void 0;
4
+ require("../augmentations");
5
+ const pg_1 = require("@dataplan/pg");
6
+ const relations_1 = require("../utils/relations");
7
+ const sql_builder_1 = require("../utils/sql-builder");
8
+ const version = '0.1.0';
9
+ /**
10
+ * BulkInsertPlugin
11
+ *
12
+ * Registers `bulkCreateX` mutation fields on the root mutation type for
13
+ * each resource that has the `bulkInsert` behavior.
14
+ *
15
+ * Uses `sideEffectWithPgClient` for raw SQL execution. The mutation
16
+ * SQL uses `RETURNING <pk_columns>` (not `RETURNING *`) to respect
17
+ * column-level SELECT grants. A follow-up SELECT retrieves the full
18
+ * rows using only the returned PKs, ensuring the query runs through
19
+ * the normal grant-aware pipeline.
20
+ *
21
+ * When `bulkRelational` is enabled, nested child records are detected
22
+ * in the input and inserted in a layered fashion: parent rows first,
23
+ * then child rows with FK values auto-populated from the parent PKs.
24
+ *
25
+ * Pattern from: https://github.com/pyramation/graphile-column-privileges-mutations
26
+ */
27
+ exports.BulkInsertPlugin = {
28
+ name: 'BulkInsertPlugin',
29
+ version,
30
+ description: 'Adds bulk insert (bulkCreateX) mutations',
31
+ after: ['BulkTypesPlugin'],
32
+ schema: {
33
+ hooks: {
34
+ GraphQLObjectType_fields(fields, build, context) {
35
+ const { scope: { isRootMutation } } = context;
36
+ if (!isRootMutation)
37
+ return fields;
38
+ const { inflection, sql, graphql: { GraphQLNonNull }, options: { bulkInsert: enableInsert = true, bulkRelational: enableRelational = false, bulkMaxRows = 1000 } } = build;
39
+ if (!enableInsert)
40
+ return fields;
41
+ const pgRegistry = build.input.pgRegistry;
42
+ for (const [resourceName, resource] of Object.entries(pgRegistry.pgResources)) {
43
+ if (resource.parameters)
44
+ continue;
45
+ if (!resource.codec.attributes)
46
+ continue;
47
+ if (resource.codec.polymorphism)
48
+ continue;
49
+ if (resource.codec.isAnonymous)
50
+ continue;
51
+ if (!resource.uniques?.length)
52
+ continue;
53
+ if (!build.behavior.pgResourceMatches(resource, 'bulkInsert'))
54
+ continue;
55
+ const typeName = inflection.tableType(resource.codec);
56
+ const fieldName = inflection.bulkInsertField(resourceName);
57
+ const inputTypeName = inflection.bulkInsertInputType(typeName);
58
+ const payloadTypeName = inflection.bulkInsertPayloadType(typeName);
59
+ const inputType = build.getTypeByName(inputTypeName);
60
+ const payloadType = build.getTypeByName(payloadTypeName);
61
+ if (!inputType || !payloadType)
62
+ continue;
63
+ // Pre-compute column specs for SQL generation
64
+ const columnSpecs = [];
65
+ const fieldToAttr = {};
66
+ for (const [attrName, attr] of Object.entries(resource.codec.attributes)) {
67
+ if (attr.extensions?.isInsertable === false)
68
+ continue;
69
+ const gqlFieldName = inflection.attribute({
70
+ attributeName: attrName,
71
+ codec: resource.codec
72
+ });
73
+ fieldToAttr[gqlFieldName] = attrName;
74
+ columnSpecs.push({
75
+ name: attrName,
76
+ sqlType: sql.compile(attr.codec.sqlType).text
77
+ });
78
+ }
79
+ // Extract primary key columns for RETURNING clause
80
+ const primaryUnique = resource.uniques.find((u) => u.isPrimary) ?? resource.uniques[0];
81
+ const pkColumns = primaryUnique.attributes;
82
+ const compiledFrom = sql.compile(resource.from).text;
83
+ // Discover nested relations at schema build time
84
+ const nestedRelations = enableRelational
85
+ ? (0, relations_1.discoverNestedRelations)(resource, pgRegistry, inflection, sql)
86
+ : [];
87
+ const nestedFieldNames = new Set(nestedRelations.map((r) => r.fieldName));
88
+ // Capture executor for plan closure
89
+ const executor = resource.executor;
90
+ const maxRows = bulkMaxRows;
91
+ fields = build.extend(fields, {
92
+ [fieldName]: context.fieldWithHooks({ fieldName }, {
93
+ description: `Bulk insert rows into ${typeName}.`,
94
+ type: payloadType,
95
+ args: {
96
+ input: {
97
+ type: new GraphQLNonNull(inputType)
98
+ }
99
+ },
100
+ plan(_$root, args) {
101
+ const $input = args.getRaw('input');
102
+ const $result = (0, pg_1.sideEffectWithPgClient)(executor, $input, async (pgClient, input) => {
103
+ const values = input.values;
104
+ if (!values || !Array.isArray(values) || values.length === 0) {
105
+ return { affectedCount: 0, returning: [] };
106
+ }
107
+ if (values.length > maxRows) {
108
+ throw new Error(`Bulk insert exceeds maximum of ${maxRows} rows (got ${values.length})`);
109
+ }
110
+ // Check for nested data
111
+ const hasNestedData = nestedRelations.length > 0 &&
112
+ values.some((row) => nestedRelations.some((rel) => Array.isArray(row[rel.fieldName]) &&
113
+ row[rel.fieldName].length > 0));
114
+ // Validate: ON CONFLICT not allowed with nested inserts
115
+ if (hasNestedData && input.onConflict) {
116
+ throw new Error('ON CONFLICT cannot be used with nested/relational inserts. ' +
117
+ 'Remove the onConflict option or remove nested records from values.');
118
+ }
119
+ // Map GraphQL field names to SQL column names,
120
+ // separating parent data from nested data
121
+ const rows = values.map((row) => {
122
+ const mapped = {};
123
+ for (const [key, val] of Object.entries(row)) {
124
+ if (nestedFieldNames.has(key))
125
+ continue;
126
+ const attrName = fieldToAttr[key];
127
+ if (attrName) {
128
+ mapped[attrName] = val;
129
+ }
130
+ }
131
+ return mapped;
132
+ });
133
+ // Build ON CONFLICT clause
134
+ let onConflict;
135
+ if (input.onConflict) {
136
+ const conflictColumns = input.onConflict.constraint
137
+ ? input.onConflict.constraint.split(',')
138
+ : undefined;
139
+ onConflict = {
140
+ conflictColumns,
141
+ action: input.onConflict.action
142
+ };
143
+ }
144
+ const batches = (0, sql_builder_1.buildBulkInsertSQL)(compiledFrom, columnSpecs, rows, pkColumns, onConflict);
145
+ let totalAffected = 0;
146
+ const allPkRows = [];
147
+ for (const batch of batches) {
148
+ const result = await pgClient.query(batch.text, batch.values);
149
+ totalAffected += result.rowCount ?? 0;
150
+ if (result.rows) {
151
+ allPkRows.push(...result.rows);
152
+ }
153
+ }
154
+ // Layered nested inserts: insert child rows with FK
155
+ // values auto-populated from parent PKs
156
+ if (hasNestedData) {
157
+ for (let i = 0; i < values.length && i < allPkRows.length; i++) {
158
+ const row = values[i];
159
+ const parentPk = allPkRows[i];
160
+ for (const rel of nestedRelations) {
161
+ const childInputRows = row[rel.fieldName];
162
+ if (!childInputRows?.length)
163
+ continue;
164
+ // Map child GraphQL fields to SQL columns
165
+ // and inject FK values from parent PK
166
+ const childRows = childInputRows.map((childRow) => {
167
+ const mapped = {};
168
+ for (const [key, val] of Object.entries(childRow)) {
169
+ const attrName = rel.childFieldToAttr[key];
170
+ if (attrName) {
171
+ mapped[attrName] = val;
172
+ }
173
+ }
174
+ // Fill FK columns from parent PK
175
+ for (let j = 0; j < rel.localAttributes.length; j++) {
176
+ mapped[rel.remoteAttributes[j]] =
177
+ parentPk[rel.localAttributes[j]];
178
+ }
179
+ return mapped;
180
+ });
181
+ const childBatches = (0, sql_builder_1.buildBulkInsertSQL)(rel.childCompiledFrom, rel.childColumnSpecs, childRows, rel.childPkColumns);
182
+ for (const batch of childBatches) {
183
+ const result = await pgClient.query(batch.text, batch.values);
184
+ totalAffected += result.rowCount ?? 0;
185
+ }
186
+ }
187
+ }
188
+ }
189
+ // Follow-up SELECT using PKs to respect column-level grants
190
+ let returning = [];
191
+ if (allPkRows.length > 0) {
192
+ const pkConditions = allPkRows.map((pkRow, rowIdx) => {
193
+ return pkColumns.map((col, colIdx) => {
194
+ const paramIdx = rowIdx * pkColumns.length + colIdx + 1;
195
+ return `"${col}" = $${paramIdx}`;
196
+ }).join(' AND ');
197
+ });
198
+ const whereClause = pkConditions.map((c) => `(${c})`).join(' OR ');
199
+ const selectParams = allPkRows.flatMap((pkRow) => pkColumns.map((col) => pkRow[col]));
200
+ const selectResult = await pgClient.query(`SELECT * FROM ${compiledFrom} WHERE ${whereClause}`, selectParams);
201
+ returning = selectResult.rows || [];
202
+ }
203
+ return {
204
+ affectedCount: totalAffected,
205
+ returning
206
+ };
207
+ });
208
+ return $result;
209
+ }
210
+ })
211
+ }, `Adding bulk insert field for ${typeName}`);
212
+ }
213
+ return fields;
214
+ }
215
+ }
216
+ }
217
+ };
@@ -0,0 +1,31 @@
1
+ import '../augmentations';
2
+ import type { GraphileConfig } from 'graphile-config';
3
+ /**
4
+ * BulkRelationalPlugin
5
+ *
6
+ * Adds relational/nested insert support to bulk mutations. When enabled
7
+ * via `bulkRelational: true`, parent values item input types gain optional
8
+ * fields for each "has many" relation, allowing creation of parent and
9
+ * child rows in a single mutation.
10
+ *
11
+ * Example:
12
+ * mutation {
13
+ * bulkCreateCategories(input: {
14
+ * values: [{
15
+ * name: "Electronics"
16
+ * products: [
17
+ * { name: "Laptop", price: "999.99" }
18
+ * ]
19
+ * }]
20
+ * }) { affectedCount }
21
+ * }
22
+ *
23
+ * The FK column on the child table (e.g. category_id) is automatically
24
+ * populated from the parent's returned primary key. Nested input types
25
+ * exclude FK columns so the user does not need to provide them.
26
+ *
27
+ * Limitations:
28
+ * - ON CONFLICT cannot be used with nested inserts (throws an error)
29
+ * - Only one level of nesting is supported in the initial implementation
30
+ */
31
+ export declare const BulkRelationalPlugin: GraphileConfig.Plugin;
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BulkRelationalPlugin = void 0;
4
+ require("../augmentations");
5
+ const relations_1 = require("../utils/relations");
6
+ const version = '0.1.0';
7
+ /**
8
+ * BulkRelationalPlugin
9
+ *
10
+ * Adds relational/nested insert support to bulk mutations. When enabled
11
+ * via `bulkRelational: true`, parent values item input types gain optional
12
+ * fields for each "has many" relation, allowing creation of parent and
13
+ * child rows in a single mutation.
14
+ *
15
+ * Example:
16
+ * mutation {
17
+ * bulkCreateCategories(input: {
18
+ * values: [{
19
+ * name: "Electronics"
20
+ * products: [
21
+ * { name: "Laptop", price: "999.99" }
22
+ * ]
23
+ * }]
24
+ * }) { affectedCount }
25
+ * }
26
+ *
27
+ * The FK column on the child table (e.g. category_id) is automatically
28
+ * populated from the parent's returned primary key. Nested input types
29
+ * exclude FK columns so the user does not need to provide them.
30
+ *
31
+ * Limitations:
32
+ * - ON CONFLICT cannot be used with nested inserts (throws an error)
33
+ * - Only one level of nesting is supported in the initial implementation
34
+ */
35
+ exports.BulkRelationalPlugin = {
36
+ name: 'BulkRelationalPlugin',
37
+ version,
38
+ description: 'Adds relational/nested insert support to bulk mutations',
39
+ after: ['BulkTypesPlugin', 'BulkInsertPlugin'],
40
+ schema: {
41
+ hooks: {
42
+ init(_, build) {
43
+ const { inflection, sql, options: { bulkRelational: enableRelational = false } } = build;
44
+ if (!enableRelational)
45
+ return _;
46
+ const pgRegistry = build.input.pgRegistry;
47
+ for (const [_resourceName, resource] of Object.entries(pgRegistry.pgResources)) {
48
+ if (resource.parameters)
49
+ continue;
50
+ if (!resource.codec.attributes)
51
+ continue;
52
+ if (resource.codec.polymorphism)
53
+ continue;
54
+ if (resource.codec.isAnonymous)
55
+ continue;
56
+ if (!resource.uniques?.length)
57
+ continue;
58
+ if (!build.behavior.pgResourceMatches(resource, 'bulkInsert'))
59
+ continue;
60
+ const parentTypeName = inflection.tableType(resource.codec);
61
+ const nestedRelations = (0, relations_1.discoverNestedRelations)(resource, pgRegistry, inflection, sql);
62
+ for (const rel of nestedRelations) {
63
+ const childTypeName = inflection.tableType(rel.remoteResource.codec);
64
+ const nestedInputTypeName = inflection.bulkNestedValuesItemType(parentTypeName, childTypeName);
65
+ // FK columns to exclude from nested input type
66
+ const fkAttrSet = new Set(rel.remoteAttributes);
67
+ build.registerInputObjectType(nestedInputTypeName, { isBulkRelationalNestedInput: true }, () => ({
68
+ description: `A row to nest-create for ${childTypeName} when bulk creating ${parentTypeName}. FK columns are auto-populated from the parent.`,
69
+ fields: () => {
70
+ const result = {};
71
+ for (const [attrName, attr] of Object.entries(rel.remoteResource.codec.attributes)) {
72
+ if (attr.extensions?.isInsertable === false)
73
+ continue;
74
+ // Exclude FK columns — they are auto-filled from parent PK
75
+ if (fkAttrSet.has(attrName))
76
+ continue;
77
+ const fieldName = inflection.attribute({
78
+ attributeName: attrName,
79
+ codec: rel.remoteResource.codec
80
+ });
81
+ const inputType = build.getGraphQLTypeByPgCodec(attr.codec, 'input');
82
+ if (!inputType)
83
+ continue;
84
+ const isRequired = !!attr.notNull && !attr.hasDefault;
85
+ result[fieldName] = {
86
+ description: `Value for ${attrName}`,
87
+ type: isRequired
88
+ ? new build.graphql.GraphQLNonNull(inputType)
89
+ : inputType
90
+ };
91
+ }
92
+ return result;
93
+ }
94
+ }), `Nested input for ${childTypeName} in ${parentTypeName}`);
95
+ }
96
+ }
97
+ return _;
98
+ },
99
+ // Add nested relation fields to existing values item input types
100
+ GraphQLInputObjectType_fields(fields, build, context) {
101
+ const { scope: { isBulkMutationValuesItem, bulkMutationResourceName } } = context;
102
+ if (!isBulkMutationValuesItem || !bulkMutationResourceName)
103
+ return fields;
104
+ const { inflection, sql, options: { bulkRelational: enableRelational = false } } = build;
105
+ if (!enableRelational)
106
+ return fields;
107
+ const pgRegistry = build.input.pgRegistry;
108
+ const resource = pgRegistry.pgResources[bulkMutationResourceName];
109
+ if (!resource)
110
+ return fields;
111
+ const parentTypeName = inflection.tableType(resource.codec);
112
+ const nestedRelations = (0, relations_1.discoverNestedRelations)(resource, pgRegistry, inflection, sql);
113
+ for (const rel of nestedRelations) {
114
+ const childTypeName = inflection.tableType(rel.remoteResource.codec);
115
+ const nestedInputTypeName = inflection.bulkNestedValuesItemType(parentTypeName, childTypeName);
116
+ const nestedInputType = build.getTypeByName(nestedInputTypeName);
117
+ if (!nestedInputType)
118
+ continue;
119
+ fields = build.extend(fields, {
120
+ [rel.fieldName]: {
121
+ description: `Nested ${childTypeName} rows to create for this ${parentTypeName}.`,
122
+ type: new build.graphql.GraphQLList(new build.graphql.GraphQLNonNull(nestedInputType))
123
+ }
124
+ }, `Adding nested field ${rel.fieldName} to ${parentTypeName} values item`);
125
+ }
126
+ return fields;
127
+ }
128
+ }
129
+ }
130
+ };
@@ -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;