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,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BulkUpsertPlugin = void 0;
|
|
4
|
+
require("../augmentations");
|
|
5
|
+
const pg_1 = require("@dataplan/pg");
|
|
6
|
+
const sql_builder_1 = require("../utils/sql-builder");
|
|
7
|
+
const version = '0.1.0';
|
|
8
|
+
/**
|
|
9
|
+
* BulkUpsertPlugin
|
|
10
|
+
*
|
|
11
|
+
* Registers `bulkUpsertX` mutation fields for each resource with the
|
|
12
|
+
* `bulkUpsert` behavior. Uses INSERT ... ON CONFLICT DO UPDATE SET.
|
|
13
|
+
*
|
|
14
|
+
* Uses `RETURNING <pk_columns>` (not `RETURNING *`) + follow-up SELECT
|
|
15
|
+
* to respect column-level SELECT grants.
|
|
16
|
+
*/
|
|
17
|
+
exports.BulkUpsertPlugin = {
|
|
18
|
+
name: 'BulkUpsertPlugin',
|
|
19
|
+
version,
|
|
20
|
+
description: 'Adds bulk upsert (bulkUpsertX) mutations',
|
|
21
|
+
after: ['BulkTypesPlugin'],
|
|
22
|
+
schema: {
|
|
23
|
+
hooks: {
|
|
24
|
+
GraphQLObjectType_fields(fields, build, context) {
|
|
25
|
+
const { scope: { isRootMutation } } = context;
|
|
26
|
+
if (!isRootMutation)
|
|
27
|
+
return fields;
|
|
28
|
+
const { inflection, sql, graphql: { GraphQLNonNull }, options: { bulkUpsert: enableUpsert = true, bulkMaxRows = 1000 } } = build;
|
|
29
|
+
if (!enableUpsert)
|
|
30
|
+
return fields;
|
|
31
|
+
const pgRegistry = build.input.pgRegistry;
|
|
32
|
+
for (const [resourceName, resource] of Object.entries(pgRegistry.pgResources)) {
|
|
33
|
+
if (resource.parameters)
|
|
34
|
+
continue;
|
|
35
|
+
if (!resource.codec.attributes)
|
|
36
|
+
continue;
|
|
37
|
+
if (resource.codec.polymorphism)
|
|
38
|
+
continue;
|
|
39
|
+
if (resource.codec.isAnonymous)
|
|
40
|
+
continue;
|
|
41
|
+
if (!resource.uniques?.length)
|
|
42
|
+
continue;
|
|
43
|
+
if (!build.behavior.pgResourceMatches(resource, 'bulkUpsert'))
|
|
44
|
+
continue;
|
|
45
|
+
const typeName = inflection.tableType(resource.codec);
|
|
46
|
+
const fieldName = inflection.bulkUpsertField(resourceName);
|
|
47
|
+
const inputTypeName = inflection.bulkUpsertInputType(typeName);
|
|
48
|
+
const payloadTypeName = inflection.bulkUpsertPayloadType(typeName);
|
|
49
|
+
const inputType = build.getTypeByName(inputTypeName);
|
|
50
|
+
const payloadType = build.getTypeByName(payloadTypeName);
|
|
51
|
+
if (!inputType || !payloadType)
|
|
52
|
+
continue;
|
|
53
|
+
const columnSpecs = [];
|
|
54
|
+
const fieldToAttr = {};
|
|
55
|
+
for (const [attrName, attr] of Object.entries(resource.codec.attributes)) {
|
|
56
|
+
if (attr.extensions?.isInsertable === false)
|
|
57
|
+
continue;
|
|
58
|
+
const gqlFieldName = inflection.attribute({
|
|
59
|
+
attributeName: attrName,
|
|
60
|
+
codec: resource.codec
|
|
61
|
+
});
|
|
62
|
+
fieldToAttr[gqlFieldName] = attrName;
|
|
63
|
+
columnSpecs.push({
|
|
64
|
+
name: attrName,
|
|
65
|
+
sqlType: sql.compile(attr.codec.sqlType).text
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// Collect unique constraint attributes for excluding from updateColumns
|
|
69
|
+
const uniqueAttrs = new Set();
|
|
70
|
+
for (const unique of resource.uniques) {
|
|
71
|
+
for (const attr of unique.attributes) {
|
|
72
|
+
uniqueAttrs.add(attr);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Extract primary key columns for RETURNING clause
|
|
76
|
+
const primaryUnique = resource.uniques.find((u) => u.isPrimary) ?? resource.uniques[0];
|
|
77
|
+
const pkColumns = primaryUnique.attributes;
|
|
78
|
+
const compiledFrom = sql.compile(resource.from).text;
|
|
79
|
+
// Capture for plan closure
|
|
80
|
+
const executor = resource.executor;
|
|
81
|
+
const maxRows = bulkMaxRows;
|
|
82
|
+
fields = build.extend(fields, {
|
|
83
|
+
[fieldName]: context.fieldWithHooks({ fieldName }, {
|
|
84
|
+
description: `Bulk upsert rows into ${typeName}. Requires an ON CONFLICT constraint.`,
|
|
85
|
+
type: payloadType,
|
|
86
|
+
args: {
|
|
87
|
+
input: {
|
|
88
|
+
type: new GraphQLNonNull(inputType)
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
plan(_$root, args) {
|
|
92
|
+
const $input = args.getRaw('input');
|
|
93
|
+
const $result = (0, pg_1.sideEffectWithPgClient)(executor, $input, async (pgClient, input) => {
|
|
94
|
+
const values = input.values;
|
|
95
|
+
if (!values || !Array.isArray(values) || values.length === 0) {
|
|
96
|
+
return { affectedCount: 0, returning: [] };
|
|
97
|
+
}
|
|
98
|
+
if (values.length > maxRows) {
|
|
99
|
+
throw new Error(`Bulk upsert exceeds maximum of ${maxRows} rows (got ${values.length})`);
|
|
100
|
+
}
|
|
101
|
+
if (!input.onConflict?.constraint) {
|
|
102
|
+
throw new Error('bulkUpsert requires onConflict.constraint');
|
|
103
|
+
}
|
|
104
|
+
const rows = values.map((row) => {
|
|
105
|
+
const mapped = {};
|
|
106
|
+
for (const [key, val] of Object.entries(row)) {
|
|
107
|
+
const attrName = fieldToAttr[key];
|
|
108
|
+
if (attrName) {
|
|
109
|
+
mapped[attrName] = val;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return mapped;
|
|
113
|
+
});
|
|
114
|
+
// Determine which columns to update
|
|
115
|
+
let updateColumns;
|
|
116
|
+
if (input.onConflict.updateColumns &&
|
|
117
|
+
input.onConflict.updateColumns.length > 0) {
|
|
118
|
+
updateColumns = input.onConflict.updateColumns;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// Default: all non-key columns
|
|
122
|
+
updateColumns = columnSpecs
|
|
123
|
+
.map((c) => c.name)
|
|
124
|
+
.filter((n) => !uniqueAttrs.has(n));
|
|
125
|
+
}
|
|
126
|
+
const batches = (0, sql_builder_1.buildBulkInsertSQL)(compiledFrom, columnSpecs, rows, pkColumns, {
|
|
127
|
+
conflictColumns: input.onConflict.constraint.split(','),
|
|
128
|
+
action: 'UPDATE',
|
|
129
|
+
updateColumns
|
|
130
|
+
});
|
|
131
|
+
let totalAffected = 0;
|
|
132
|
+
const allPkRows = [];
|
|
133
|
+
for (const batch of batches) {
|
|
134
|
+
const result = await pgClient.query(batch.text, batch.values);
|
|
135
|
+
totalAffected += result.rowCount ?? 0;
|
|
136
|
+
if (result.rows) {
|
|
137
|
+
allPkRows.push(...result.rows);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Follow-up SELECT using PKs to respect column-level grants
|
|
141
|
+
let returning = [];
|
|
142
|
+
if (allPkRows.length > 0) {
|
|
143
|
+
const pkConditions = allPkRows.map((pkRow, rowIdx) => {
|
|
144
|
+
return pkColumns.map((col, colIdx) => {
|
|
145
|
+
const paramIdx = rowIdx * pkColumns.length + colIdx + 1;
|
|
146
|
+
return `"${col}" = $${paramIdx}`;
|
|
147
|
+
}).join(' AND ');
|
|
148
|
+
});
|
|
149
|
+
const whereClause = pkConditions.map((c) => `(${c})`).join(' OR ');
|
|
150
|
+
const selectParams = allPkRows.flatMap((pkRow) => pkColumns.map((col) => pkRow[col]));
|
|
151
|
+
const selectResult = await pgClient.query(`SELECT * FROM ${compiledFrom} WHERE ${whereClause}`, selectParams);
|
|
152
|
+
returning = selectResult.rows || [];
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
affectedCount: totalAffected,
|
|
156
|
+
returning
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
return $result;
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
}, `Adding bulk upsert field for ${typeName}`);
|
|
163
|
+
}
|
|
164
|
+
return fields;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
@@ -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';
|
package/plugins/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BulkUpsertPlugin = exports.BulkUpdatePlugin = exports.BulkTypesPlugin = exports.BulkRelationalPlugin = exports.BulkInsertPlugin = exports.BulkInflectionPlugin = exports.BulkDeletePlugin = void 0;
|
|
4
|
+
var BulkDeletePlugin_1 = require("./BulkDeletePlugin");
|
|
5
|
+
Object.defineProperty(exports, "BulkDeletePlugin", { enumerable: true, get: function () { return BulkDeletePlugin_1.BulkDeletePlugin; } });
|
|
6
|
+
var BulkInflectionPlugin_1 = require("./BulkInflectionPlugin");
|
|
7
|
+
Object.defineProperty(exports, "BulkInflectionPlugin", { enumerable: true, get: function () { return BulkInflectionPlugin_1.BulkInflectionPlugin; } });
|
|
8
|
+
var BulkInsertPlugin_1 = require("./BulkInsertPlugin");
|
|
9
|
+
Object.defineProperty(exports, "BulkInsertPlugin", { enumerable: true, get: function () { return BulkInsertPlugin_1.BulkInsertPlugin; } });
|
|
10
|
+
var BulkRelationalPlugin_1 = require("./BulkRelationalPlugin");
|
|
11
|
+
Object.defineProperty(exports, "BulkRelationalPlugin", { enumerable: true, get: function () { return BulkRelationalPlugin_1.BulkRelationalPlugin; } });
|
|
12
|
+
var BulkTypesPlugin_1 = require("./BulkTypesPlugin");
|
|
13
|
+
Object.defineProperty(exports, "BulkTypesPlugin", { enumerable: true, get: function () { return BulkTypesPlugin_1.BulkTypesPlugin; } });
|
|
14
|
+
var BulkUpdatePlugin_1 = require("./BulkUpdatePlugin");
|
|
15
|
+
Object.defineProperty(exports, "BulkUpdatePlugin", { enumerable: true, get: function () { return BulkUpdatePlugin_1.BulkUpdatePlugin; } });
|
|
16
|
+
var BulkUpsertPlugin_1 = require("./BulkUpsertPlugin");
|
|
17
|
+
Object.defineProperty(exports, "BulkUpsertPlugin", { enumerable: true, get: function () { return BulkUpsertPlugin_1.BulkUpsertPlugin; } });
|
package/preset.d.ts
ADDED
|
@@ -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/preset.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bulk Mutations Preset
|
|
4
|
+
*
|
|
5
|
+
* Provides a convenient preset factory for including bulk mutations
|
|
6
|
+
* in PostGraphile v5.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { BulkMutationPreset } from 'graphile-bulk-mutations';
|
|
11
|
+
*
|
|
12
|
+
* const preset = {
|
|
13
|
+
* extends: [
|
|
14
|
+
* BulkMutationPreset(),
|
|
15
|
+
* // or with options:
|
|
16
|
+
* BulkMutationPreset({
|
|
17
|
+
* bulkNaming: 'pluralized',
|
|
18
|
+
* bulkRelational: true,
|
|
19
|
+
* }),
|
|
20
|
+
* ],
|
|
21
|
+
* };
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.BulkMutationPreset = BulkMutationPreset;
|
|
26
|
+
require("./augmentations");
|
|
27
|
+
const plugins_1 = require("./plugins");
|
|
28
|
+
/**
|
|
29
|
+
* Default schema options for bulk mutations.
|
|
30
|
+
*/
|
|
31
|
+
const defaultSchemaOptions = {
|
|
32
|
+
bulkNaming: 'bulk',
|
|
33
|
+
bulkInsert: true,
|
|
34
|
+
bulkUpsert: true,
|
|
35
|
+
bulkUpdate: true,
|
|
36
|
+
bulkDelete: true,
|
|
37
|
+
bulkRelational: false,
|
|
38
|
+
bulkReturning: true,
|
|
39
|
+
bulkMaxRows: 1000,
|
|
40
|
+
bulkMaxNestingDepth: 3,
|
|
41
|
+
bulkRequireWhere: true
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Creates a preset that includes the bulk mutation plugins with the given options.
|
|
45
|
+
*
|
|
46
|
+
* Behavior is OFF by default for all tables. Tables must opt in via smart tags:
|
|
47
|
+
* COMMENT ON TABLE users IS E'@behavior +bulkInsert +bulkUpsert';
|
|
48
|
+
*
|
|
49
|
+
* The `enable_bulk` database setting must also be true for the plugin to load.
|
|
50
|
+
*/
|
|
51
|
+
function BulkMutationPreset(options = {}) {
|
|
52
|
+
const mergedOptions = { ...defaultSchemaOptions, ...options };
|
|
53
|
+
const plugins = [
|
|
54
|
+
plugins_1.BulkInflectionPlugin,
|
|
55
|
+
plugins_1.BulkTypesPlugin
|
|
56
|
+
];
|
|
57
|
+
if (mergedOptions.bulkInsert) {
|
|
58
|
+
plugins.push(plugins_1.BulkInsertPlugin);
|
|
59
|
+
}
|
|
60
|
+
if (mergedOptions.bulkUpsert) {
|
|
61
|
+
plugins.push(plugins_1.BulkUpsertPlugin);
|
|
62
|
+
}
|
|
63
|
+
if (mergedOptions.bulkUpdate) {
|
|
64
|
+
plugins.push(plugins_1.BulkUpdatePlugin);
|
|
65
|
+
}
|
|
66
|
+
if (mergedOptions.bulkDelete) {
|
|
67
|
+
plugins.push(plugins_1.BulkDeletePlugin);
|
|
68
|
+
}
|
|
69
|
+
if (mergedOptions.bulkRelational) {
|
|
70
|
+
plugins.push(plugins_1.BulkRelationalPlugin);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
plugins,
|
|
74
|
+
schema: mergedOptions
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
exports.default = BulkMutationPreset;
|
package/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/types.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Configuration options and shared types for graphile-bulk-mutations.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PG_MAX_PARAMS = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Max PostgreSQL parameters per query. Auto-batch splits at this limit.
|
|
9
|
+
*/
|
|
10
|
+
exports.PG_MAX_PARAMS = 32_000;
|
package/utils/index.d.ts
ADDED
package/utils/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildBulkUpdateSQL = exports.buildBulkInsertSQL = exports.buildBulkDeleteSQL = exports.discoverNestedRelations = void 0;
|
|
4
|
+
var relations_1 = require("./relations");
|
|
5
|
+
Object.defineProperty(exports, "discoverNestedRelations", { enumerable: true, get: function () { return relations_1.discoverNestedRelations; } });
|
|
6
|
+
var sql_builder_1 = require("./sql-builder");
|
|
7
|
+
Object.defineProperty(exports, "buildBulkDeleteSQL", { enumerable: true, get: function () { return sql_builder_1.buildBulkDeleteSQL; } });
|
|
8
|
+
Object.defineProperty(exports, "buildBulkInsertSQL", { enumerable: true, get: function () { return sql_builder_1.buildBulkInsertSQL; } });
|
|
9
|
+
Object.defineProperty(exports, "buildBulkUpdateSQL", { enumerable: true, get: function () { return sql_builder_1.buildBulkUpdateSQL; } });
|
|
@@ -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[];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Relation discovery utilities for bulk relational/nested inserts.
|
|
4
|
+
*
|
|
5
|
+
* Inspects the PostGraphile registry to find "has many" (isReferencee)
|
|
6
|
+
* relations for a given resource. For each qualifying relation, computes
|
|
7
|
+
* column metadata needed to build child INSERT statements.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.discoverNestedRelations = discoverNestedRelations;
|
|
11
|
+
/**
|
|
12
|
+
* Discover "has many" relations for a resource that qualify for nested
|
|
13
|
+
* inserts. A relation qualifies when:
|
|
14
|
+
*
|
|
15
|
+
* 1. isReferencee is true (the remote table references the local table)
|
|
16
|
+
* 2. isUnique is false (one-to-many, not one-to-one)
|
|
17
|
+
* 3. The remote resource is a regular table (has attributes, uniques, etc.)
|
|
18
|
+
*/
|
|
19
|
+
function discoverNestedRelations(resource, pgRegistry, inflection, sql) {
|
|
20
|
+
const codecName = resource.codec.name;
|
|
21
|
+
const relations = pgRegistry.pgRelations[codecName];
|
|
22
|
+
if (!relations)
|
|
23
|
+
return [];
|
|
24
|
+
const result = [];
|
|
25
|
+
for (const [relationName, relation] of Object.entries(relations)) {
|
|
26
|
+
// Only reverse relations (child references parent)
|
|
27
|
+
if (!relation.isReferencee)
|
|
28
|
+
continue;
|
|
29
|
+
// Only one-to-many (skip one-to-one)
|
|
30
|
+
if (relation.isUnique)
|
|
31
|
+
continue;
|
|
32
|
+
const remoteResource = relation.remoteResource;
|
|
33
|
+
if (!remoteResource?.codec?.attributes)
|
|
34
|
+
continue;
|
|
35
|
+
if (remoteResource.codec.polymorphism)
|
|
36
|
+
continue;
|
|
37
|
+
if (remoteResource.codec.isAnonymous)
|
|
38
|
+
continue;
|
|
39
|
+
if (!remoteResource.uniques?.length)
|
|
40
|
+
continue;
|
|
41
|
+
// Build child column metadata (all insertable columns)
|
|
42
|
+
const childFieldToAttr = {};
|
|
43
|
+
const childColumnSpecs = [];
|
|
44
|
+
for (const [attrName, attr] of Object.entries(remoteResource.codec.attributes)) {
|
|
45
|
+
if (attr.extensions?.isInsertable === false)
|
|
46
|
+
continue;
|
|
47
|
+
const gqlFieldName = inflection.attribute({
|
|
48
|
+
attributeName: attrName,
|
|
49
|
+
codec: remoteResource.codec
|
|
50
|
+
});
|
|
51
|
+
childFieldToAttr[gqlFieldName] = attrName;
|
|
52
|
+
childColumnSpecs.push({
|
|
53
|
+
name: attrName,
|
|
54
|
+
sqlType: sql.compile(attr.codec.sqlType).text
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// Find PK columns for child
|
|
58
|
+
const childPrimaryUnique = remoteResource.uniques.find((u) => u.isPrimary) ??
|
|
59
|
+
remoteResource.uniques[0];
|
|
60
|
+
const childPkColumns = [...childPrimaryUnique.attributes];
|
|
61
|
+
const childCompiledFrom = sql.compile(remoteResource.from).text;
|
|
62
|
+
// Find the registry key of the remote resource
|
|
63
|
+
let remoteResourceName = '';
|
|
64
|
+
for (const [name, res] of Object.entries(pgRegistry.pgResources)) {
|
|
65
|
+
if (res === remoteResource) {
|
|
66
|
+
remoteResourceName = name;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Derive a clean GraphQL field name from the remote resource name
|
|
71
|
+
const fieldName = inflection.camelCase(remoteResourceName);
|
|
72
|
+
result.push({
|
|
73
|
+
relationName,
|
|
74
|
+
fieldName,
|
|
75
|
+
remoteResource,
|
|
76
|
+
remoteResourceName,
|
|
77
|
+
localAttributes: [...relation.localAttributes],
|
|
78
|
+
remoteAttributes: [...relation.remoteAttributes],
|
|
79
|
+
childFieldToAttr,
|
|
80
|
+
childColumnSpecs,
|
|
81
|
+
childCompiledFrom,
|
|
82
|
+
childPkColumns
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL builder utilities for bulk mutations.
|
|
3
|
+
*
|
|
4
|
+
* Provides parameterized SQL generation for VALUES clauses, ON CONFLICT
|
|
5
|
+
* clauses, and SET patches. Handles auto-batching when the parameter
|
|
6
|
+
* count would exceed PostgreSQL's 32K limit.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: All mutation queries use `RETURNING <pk_columns>` instead of
|
|
9
|
+
* `RETURNING *` to respect column-level SELECT grants. The caller is
|
|
10
|
+
* responsible for doing a follow-up SELECT using the returned PKs to
|
|
11
|
+
* resolve the full row through PostGraphile's grant-aware query pipeline.
|
|
12
|
+
* See: https://github.com/pyramation/graphile-column-privileges-mutations
|
|
13
|
+
*/
|
|
14
|
+
export interface ColumnSpec {
|
|
15
|
+
name: string;
|
|
16
|
+
sqlType: string;
|
|
17
|
+
}
|
|
18
|
+
export interface InsertBatch {
|
|
19
|
+
text: string;
|
|
20
|
+
values: unknown[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Builds parameterized INSERT ... VALUES SQL with optional ON CONFLICT clause.
|
|
24
|
+
* Auto-splits into multiple batches when parameter count exceeds PG_MAX_PARAMS.
|
|
25
|
+
*
|
|
26
|
+
* Uses `RETURNING <returningColumns>` (typically primary key columns) instead
|
|
27
|
+
* of `RETURNING *` to avoid permission errors with column-level SELECT grants.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildBulkInsertSQL(tableName: string, columns: ColumnSpec[], rows: Record<string, unknown>[], returningColumns: string[], onConflict?: {
|
|
30
|
+
conflictColumns?: string[];
|
|
31
|
+
action: 'IGNORE';
|
|
32
|
+
} | {
|
|
33
|
+
conflictColumns: string[];
|
|
34
|
+
action: 'UPDATE';
|
|
35
|
+
updateColumns?: string[];
|
|
36
|
+
}): InsertBatch[];
|
|
37
|
+
/**
|
|
38
|
+
* Builds a parameterized UPDATE ... SET ... WHERE SQL statement.
|
|
39
|
+
*
|
|
40
|
+
* Uses `RETURNING <returningColumns>` (typically primary key columns) instead
|
|
41
|
+
* of `RETURNING *` to avoid permission errors with column-level SELECT grants.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildBulkUpdateSQL(tableName: string, patch: Record<string, unknown>, columns: ColumnSpec[], returningColumns: string[], whereClause: string, whereParams: unknown[]): {
|
|
44
|
+
text: string;
|
|
45
|
+
values: unknown[];
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Builds a parameterized DELETE ... WHERE SQL statement.
|
|
49
|
+
*
|
|
50
|
+
* Uses `RETURNING <returningColumns>` (typically primary key columns) instead
|
|
51
|
+
* of `RETURNING *` to avoid permission errors with column-level SELECT grants.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildBulkDeleteSQL(tableName: string, returningColumns: string[], whereClause: string, whereParams: unknown[]): {
|
|
54
|
+
text: string;
|
|
55
|
+
values: unknown[];
|
|
56
|
+
};
|