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,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';
@@ -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,7 @@
1
+ /**
2
+ * Configuration options and shared types for graphile-bulk-mutations.
3
+ */
4
+ /**
5
+ * Max PostgreSQL parameters per query. Auto-batch splits at this limit.
6
+ */
7
+ export const PG_MAX_PARAMS = 32_000;
@@ -0,0 +1,4 @@
1
+ export type { NestedRelationInfo } from './relations';
2
+ export { discoverNestedRelations } from './relations';
3
+ export type { ColumnSpec, InsertBatch } from './sql-builder';
4
+ export { buildBulkDeleteSQL, buildBulkInsertSQL, buildBulkUpdateSQL } from './sql-builder';
@@ -0,0 +1,2 @@
1
+ export { discoverNestedRelations } from './relations';
2
+ export { buildBulkDeleteSQL, buildBulkInsertSQL, buildBulkUpdateSQL } from './sql-builder';
@@ -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[];