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