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,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;
|