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,83 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Discover "has many" relations for a resource that qualify for nested
|
|
10
|
+
* inserts. A relation qualifies when:
|
|
11
|
+
*
|
|
12
|
+
* 1. isReferencee is true (the remote table references the local table)
|
|
13
|
+
* 2. isUnique is false (one-to-many, not one-to-one)
|
|
14
|
+
* 3. The remote resource is a regular table (has attributes, uniques, etc.)
|
|
15
|
+
*/
|
|
16
|
+
export function discoverNestedRelations(resource, pgRegistry, inflection, sql) {
|
|
17
|
+
const codecName = resource.codec.name;
|
|
18
|
+
const relations = pgRegistry.pgRelations[codecName];
|
|
19
|
+
if (!relations)
|
|
20
|
+
return [];
|
|
21
|
+
const result = [];
|
|
22
|
+
for (const [relationName, relation] of Object.entries(relations)) {
|
|
23
|
+
// Only reverse relations (child references parent)
|
|
24
|
+
if (!relation.isReferencee)
|
|
25
|
+
continue;
|
|
26
|
+
// Only one-to-many (skip one-to-one)
|
|
27
|
+
if (relation.isUnique)
|
|
28
|
+
continue;
|
|
29
|
+
const remoteResource = relation.remoteResource;
|
|
30
|
+
if (!remoteResource?.codec?.attributes)
|
|
31
|
+
continue;
|
|
32
|
+
if (remoteResource.codec.polymorphism)
|
|
33
|
+
continue;
|
|
34
|
+
if (remoteResource.codec.isAnonymous)
|
|
35
|
+
continue;
|
|
36
|
+
if (!remoteResource.uniques?.length)
|
|
37
|
+
continue;
|
|
38
|
+
// Build child column metadata (all insertable columns)
|
|
39
|
+
const childFieldToAttr = {};
|
|
40
|
+
const childColumnSpecs = [];
|
|
41
|
+
for (const [attrName, attr] of Object.entries(remoteResource.codec.attributes)) {
|
|
42
|
+
if (attr.extensions?.isInsertable === false)
|
|
43
|
+
continue;
|
|
44
|
+
const gqlFieldName = inflection.attribute({
|
|
45
|
+
attributeName: attrName,
|
|
46
|
+
codec: remoteResource.codec
|
|
47
|
+
});
|
|
48
|
+
childFieldToAttr[gqlFieldName] = attrName;
|
|
49
|
+
childColumnSpecs.push({
|
|
50
|
+
name: attrName,
|
|
51
|
+
sqlType: sql.compile(attr.codec.sqlType).text
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// Find PK columns for child
|
|
55
|
+
const childPrimaryUnique = remoteResource.uniques.find((u) => u.isPrimary) ??
|
|
56
|
+
remoteResource.uniques[0];
|
|
57
|
+
const childPkColumns = [...childPrimaryUnique.attributes];
|
|
58
|
+
const childCompiledFrom = sql.compile(remoteResource.from).text;
|
|
59
|
+
// Find the registry key of the remote resource
|
|
60
|
+
let remoteResourceName = '';
|
|
61
|
+
for (const [name, res] of Object.entries(pgRegistry.pgResources)) {
|
|
62
|
+
if (res === remoteResource) {
|
|
63
|
+
remoteResourceName = name;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Derive a clean GraphQL field name from the remote resource name
|
|
68
|
+
const fieldName = inflection.camelCase(remoteResourceName);
|
|
69
|
+
result.push({
|
|
70
|
+
relationName,
|
|
71
|
+
fieldName,
|
|
72
|
+
remoteResource,
|
|
73
|
+
remoteResourceName,
|
|
74
|
+
localAttributes: [...relation.localAttributes],
|
|
75
|
+
remoteAttributes: [...relation.remoteAttributes],
|
|
76
|
+
childFieldToAttr,
|
|
77
|
+
childColumnSpecs,
|
|
78
|
+
childCompiledFrom,
|
|
79
|
+
childPkColumns
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
import { PG_MAX_PARAMS } from '../types';
|
|
15
|
+
/**
|
|
16
|
+
* Builds parameterized INSERT ... VALUES SQL with optional ON CONFLICT clause.
|
|
17
|
+
* Auto-splits into multiple batches when parameter count exceeds PG_MAX_PARAMS.
|
|
18
|
+
*
|
|
19
|
+
* Uses `RETURNING <returningColumns>` (typically primary key columns) instead
|
|
20
|
+
* of `RETURNING *` to avoid permission errors with column-level SELECT grants.
|
|
21
|
+
*/
|
|
22
|
+
export function buildBulkInsertSQL(tableName, columns, rows, returningColumns, onConflict) {
|
|
23
|
+
const colNames = columns.map((c) => `"${c.name}"`);
|
|
24
|
+
const colsPerRow = columns.length;
|
|
25
|
+
const maxRowsPerBatch = Math.floor(PG_MAX_PARAMS / colsPerRow);
|
|
26
|
+
const returningClause = returningColumns.length > 0
|
|
27
|
+
? returningColumns.map((c) => `"${c}"`).join(', ')
|
|
28
|
+
: '*';
|
|
29
|
+
const batches = [];
|
|
30
|
+
for (let offset = 0; offset < rows.length; offset += maxRowsPerBatch) {
|
|
31
|
+
const batchRows = rows.slice(offset, offset + maxRowsPerBatch);
|
|
32
|
+
const values = [];
|
|
33
|
+
const valuePlaceholders = [];
|
|
34
|
+
for (const row of batchRows) {
|
|
35
|
+
const placeholders = [];
|
|
36
|
+
for (const col of columns) {
|
|
37
|
+
const val = row[col.name];
|
|
38
|
+
if (val === undefined) {
|
|
39
|
+
placeholders.push('DEFAULT');
|
|
40
|
+
}
|
|
41
|
+
else if (val === null) {
|
|
42
|
+
placeholders.push('NULL');
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
values.push(val);
|
|
46
|
+
placeholders.push(`$${values.length}::${col.sqlType}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
valuePlaceholders.push(`(${placeholders.join(', ')})`);
|
|
50
|
+
}
|
|
51
|
+
let text = `INSERT INTO ${tableName} (${colNames.join(', ')})\nVALUES ${valuePlaceholders.join(',\n ')}`;
|
|
52
|
+
if (onConflict) {
|
|
53
|
+
if (onConflict.conflictColumns && onConflict.conflictColumns.length > 0) {
|
|
54
|
+
const colList = onConflict.conflictColumns.map((c) => `"${c}"`).join(', ');
|
|
55
|
+
text += `\nON CONFLICT (${colList})`;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
text += '\nON CONFLICT';
|
|
59
|
+
}
|
|
60
|
+
if (onConflict.action === 'IGNORE') {
|
|
61
|
+
text += ' DO NOTHING';
|
|
62
|
+
}
|
|
63
|
+
else if (onConflict.action === 'UPDATE') {
|
|
64
|
+
const setCols = onConflict.updateColumns && onConflict.updateColumns.length > 0
|
|
65
|
+
? onConflict.updateColumns
|
|
66
|
+
: columns.map((c) => c.name);
|
|
67
|
+
const setClause = setCols
|
|
68
|
+
.map((c) => `"${c}" = EXCLUDED."${c}"`)
|
|
69
|
+
.join(', ');
|
|
70
|
+
text += ` DO UPDATE SET ${setClause}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
text += `\nRETURNING ${returningClause}`;
|
|
74
|
+
batches.push({ text, values });
|
|
75
|
+
}
|
|
76
|
+
return batches;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Builds a parameterized UPDATE ... SET ... WHERE SQL statement.
|
|
80
|
+
*
|
|
81
|
+
* Uses `RETURNING <returningColumns>` (typically primary key columns) instead
|
|
82
|
+
* of `RETURNING *` to avoid permission errors with column-level SELECT grants.
|
|
83
|
+
*/
|
|
84
|
+
export function buildBulkUpdateSQL(tableName, patch, columns, returningColumns, whereClause, whereParams) {
|
|
85
|
+
const values = [];
|
|
86
|
+
const setClauses = [];
|
|
87
|
+
for (const [fieldName, value] of Object.entries(patch)) {
|
|
88
|
+
const col = columns.find((c) => c.name === fieldName);
|
|
89
|
+
if (!col)
|
|
90
|
+
continue;
|
|
91
|
+
if (value === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
values.push(value);
|
|
94
|
+
setClauses.push(`"${col.name}" = $${values.length}::${col.sqlType}`);
|
|
95
|
+
}
|
|
96
|
+
if (setClauses.length === 0) {
|
|
97
|
+
throw new Error('No columns to update in patch');
|
|
98
|
+
}
|
|
99
|
+
// Renumber where params to follow set params
|
|
100
|
+
const offset = values.length;
|
|
101
|
+
const renumberedWhere = whereClause.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
|
|
102
|
+
values.push(...whereParams);
|
|
103
|
+
const returningClause = returningColumns.length > 0
|
|
104
|
+
? returningColumns.map((c) => `"${c}"`).join(', ')
|
|
105
|
+
: '*';
|
|
106
|
+
const text = `UPDATE ${tableName}\nSET ${setClauses.join(', ')}\nWHERE ${renumberedWhere}\nRETURNING ${returningClause}`;
|
|
107
|
+
return { text, values };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Builds a parameterized DELETE ... WHERE SQL statement.
|
|
111
|
+
*
|
|
112
|
+
* Uses `RETURNING <returningColumns>` (typically primary key columns) instead
|
|
113
|
+
* of `RETURNING *` to avoid permission errors with column-level SELECT grants.
|
|
114
|
+
*/
|
|
115
|
+
export function buildBulkDeleteSQL(tableName, returningColumns, whereClause, whereParams) {
|
|
116
|
+
const returningClause = returningColumns.length > 0
|
|
117
|
+
? returningColumns.map((c) => `"${c}"`).join(', ')
|
|
118
|
+
: '*';
|
|
119
|
+
const text = `DELETE FROM ${tableName}\nWHERE ${whereClause}\nRETURNING ${returningClause}`;
|
|
120
|
+
return { text, values: [...whereParams] };
|
|
121
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graphile-bulk-mutations
|
|
3
|
+
*
|
|
4
|
+
* A PostGraphile v5 plugin for bulk insert, upsert, update, and delete
|
|
5
|
+
* mutations with ON CONFLICT handling.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { BulkMutationPreset } from 'graphile-bulk-mutations';
|
|
10
|
+
*
|
|
11
|
+
* const preset = {
|
|
12
|
+
* extends: [
|
|
13
|
+
* BulkMutationPreset(),
|
|
14
|
+
* ],
|
|
15
|
+
* };
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Tables must opt in via smart tags:
|
|
19
|
+
* ```sql
|
|
20
|
+
* COMMENT ON TABLE users IS E'@behavior +bulkInsert +bulkUpsert';
|
|
21
|
+
* COMMENT ON TABLE orders IS E'@behavior +bulkInsert +bulkUpdate +bulkDelete';
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
import './augmentations';
|
|
25
|
+
export { BulkMutationPreset } from './preset';
|
|
26
|
+
export { BulkDeletePlugin, BulkInflectionPlugin, BulkInsertPlugin, BulkRelationalPlugin, BulkTypesPlugin, BulkUpdatePlugin, BulkUpsertPlugin } from './plugins';
|
|
27
|
+
export type { BulkMutationOptions, BulkNamingStrategy } from './types';
|
|
28
|
+
export { PG_MAX_PARAMS } from './types';
|
|
29
|
+
export type { ColumnSpec, InsertBatch } from './utils';
|
|
30
|
+
export { buildBulkDeleteSQL, buildBulkInsertSQL, buildBulkUpdateSQL } from './utils';
|
package/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* graphile-bulk-mutations
|
|
4
|
+
*
|
|
5
|
+
* A PostGraphile v5 plugin for bulk insert, upsert, update, and delete
|
|
6
|
+
* mutations with ON CONFLICT handling.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { BulkMutationPreset } from 'graphile-bulk-mutations';
|
|
11
|
+
*
|
|
12
|
+
* const preset = {
|
|
13
|
+
* extends: [
|
|
14
|
+
* BulkMutationPreset(),
|
|
15
|
+
* ],
|
|
16
|
+
* };
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Tables must opt in via smart tags:
|
|
20
|
+
* ```sql
|
|
21
|
+
* COMMENT ON TABLE users IS E'@behavior +bulkInsert +bulkUpsert';
|
|
22
|
+
* COMMENT ON TABLE orders IS E'@behavior +bulkInsert +bulkUpdate +bulkDelete';
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.buildBulkUpdateSQL = exports.buildBulkInsertSQL = exports.buildBulkDeleteSQL = exports.PG_MAX_PARAMS = exports.BulkUpsertPlugin = exports.BulkUpdatePlugin = exports.BulkTypesPlugin = exports.BulkRelationalPlugin = exports.BulkInsertPlugin = exports.BulkInflectionPlugin = exports.BulkDeletePlugin = exports.BulkMutationPreset = void 0;
|
|
27
|
+
require("./augmentations");
|
|
28
|
+
var preset_1 = require("./preset");
|
|
29
|
+
Object.defineProperty(exports, "BulkMutationPreset", { enumerable: true, get: function () { return preset_1.BulkMutationPreset; } });
|
|
30
|
+
// Re-export all plugins for granular use
|
|
31
|
+
var plugins_1 = require("./plugins");
|
|
32
|
+
Object.defineProperty(exports, "BulkDeletePlugin", { enumerable: true, get: function () { return plugins_1.BulkDeletePlugin; } });
|
|
33
|
+
Object.defineProperty(exports, "BulkInflectionPlugin", { enumerable: true, get: function () { return plugins_1.BulkInflectionPlugin; } });
|
|
34
|
+
Object.defineProperty(exports, "BulkInsertPlugin", { enumerable: true, get: function () { return plugins_1.BulkInsertPlugin; } });
|
|
35
|
+
Object.defineProperty(exports, "BulkRelationalPlugin", { enumerable: true, get: function () { return plugins_1.BulkRelationalPlugin; } });
|
|
36
|
+
Object.defineProperty(exports, "BulkTypesPlugin", { enumerable: true, get: function () { return plugins_1.BulkTypesPlugin; } });
|
|
37
|
+
Object.defineProperty(exports, "BulkUpdatePlugin", { enumerable: true, get: function () { return plugins_1.BulkUpdatePlugin; } });
|
|
38
|
+
Object.defineProperty(exports, "BulkUpsertPlugin", { enumerable: true, get: function () { return plugins_1.BulkUpsertPlugin; } });
|
|
39
|
+
var types_1 = require("./types");
|
|
40
|
+
Object.defineProperty(exports, "PG_MAX_PARAMS", { enumerable: true, get: function () { return types_1.PG_MAX_PARAMS; } });
|
|
41
|
+
var utils_1 = require("./utils");
|
|
42
|
+
Object.defineProperty(exports, "buildBulkDeleteSQL", { enumerable: true, get: function () { return utils_1.buildBulkDeleteSQL; } });
|
|
43
|
+
Object.defineProperty(exports, "buildBulkInsertSQL", { enumerable: true, get: function () { return utils_1.buildBulkInsertSQL; } });
|
|
44
|
+
Object.defineProperty(exports, "buildBulkUpdateSQL", { enumerable: true, get: function () { return utils_1.buildBulkUpdateSQL; } });
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "graphile-bulk-mutations",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "PostGraphile v5 plugin for bulk insert, upsert, update, and delete mutations with ON CONFLICT handling",
|
|
5
|
+
"author": "Constructive <developers@constructive.io>",
|
|
6
|
+
"homepage": "https://github.com/constructive-io/constructive",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"module": "esm/index.js",
|
|
10
|
+
"types": "index.d.ts",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"clean": "makage clean",
|
|
13
|
+
"prepack": "npm run build",
|
|
14
|
+
"build": "makage build",
|
|
15
|
+
"build:dev": "makage build --dev",
|
|
16
|
+
"lint": "eslint . --fix",
|
|
17
|
+
"test": "jest",
|
|
18
|
+
"test:watch": "jest --watch"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"directory": "dist"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/constructive-io/constructive"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"postgraphile",
|
|
30
|
+
"graphile",
|
|
31
|
+
"constructive",
|
|
32
|
+
"plugin",
|
|
33
|
+
"postgres",
|
|
34
|
+
"graphql",
|
|
35
|
+
"bulk",
|
|
36
|
+
"mutations",
|
|
37
|
+
"upsert",
|
|
38
|
+
"on-conflict",
|
|
39
|
+
"v5"
|
|
40
|
+
],
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/constructive-io/constructive/issues"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.19.11",
|
|
46
|
+
"graphile-test": "^4.14.1",
|
|
47
|
+
"makage": "^0.3.0",
|
|
48
|
+
"pgsql-test": "^4.13.1"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@dataplan/pg": "1.0.0",
|
|
52
|
+
"grafast": "1.0.0",
|
|
53
|
+
"graphile-build": "5.0.0",
|
|
54
|
+
"graphile-build-pg": "5.0.0",
|
|
55
|
+
"graphile-config": "1.0.0",
|
|
56
|
+
"graphql": "16.13.0",
|
|
57
|
+
"pg-sql2": "5.0.0",
|
|
58
|
+
"postgraphile": "5.0.0"
|
|
59
|
+
},
|
|
60
|
+
"gitHead": "a8badcda8c3aa551194622b866fc61d4ab3e4eda"
|
|
61
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import '../augmentations';
|
|
2
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
3
|
+
/**
|
|
4
|
+
* BulkDeletePlugin
|
|
5
|
+
*
|
|
6
|
+
* Registers `bulkDeleteX` mutation fields for each resource with the
|
|
7
|
+
* `bulkDelete` behavior. Uses DELETE ... WHERE with conditions from
|
|
8
|
+
* 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 BulkDeletePlugin: GraphileConfig.Plugin;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BulkDeletePlugin = void 0;
|
|
4
|
+
require("../augmentations");
|
|
5
|
+
const pg_1 = require("@dataplan/pg");
|
|
6
|
+
const version = '0.1.0';
|
|
7
|
+
/**
|
|
8
|
+
* BulkDeletePlugin
|
|
9
|
+
*
|
|
10
|
+
* Registers `bulkDeleteX` mutation fields for each resource with the
|
|
11
|
+
* `bulkDelete` behavior. Uses DELETE ... WHERE with conditions from
|
|
12
|
+
* graphile-connection-filter (or PostGraphile's built-in condition type).
|
|
13
|
+
*
|
|
14
|
+
* Uses `RETURNING <pk_columns>` (not `RETURNING *`) + follow-up SELECT
|
|
15
|
+
* to respect column-level SELECT grants.
|
|
16
|
+
*/
|
|
17
|
+
exports.BulkDeletePlugin = {
|
|
18
|
+
name: 'BulkDeletePlugin',
|
|
19
|
+
version,
|
|
20
|
+
description: 'Adds bulk delete (bulkDeleteX) 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: { bulkDelete: enableDelete = true, bulkRequireWhere = true } } = build;
|
|
29
|
+
if (!enableDelete)
|
|
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, 'bulkDelete'))
|
|
44
|
+
continue;
|
|
45
|
+
const typeName = inflection.tableType(resource.codec);
|
|
46
|
+
const fieldName = inflection.bulkDeleteField(resourceName);
|
|
47
|
+
const inputTypeName = inflection.bulkDeleteInputType(typeName);
|
|
48
|
+
const payloadTypeName = inflection.bulkDeletePayloadType(typeName);
|
|
49
|
+
const inputType = build.getTypeByName(inputTypeName);
|
|
50
|
+
const payloadType = build.getTypeByName(payloadTypeName);
|
|
51
|
+
if (!inputType || !payloadType)
|
|
52
|
+
continue;
|
|
53
|
+
const fieldToAttr = {};
|
|
54
|
+
const attrToSqlType = {};
|
|
55
|
+
for (const [attrName, attr] of Object.entries(resource.codec.attributes)) {
|
|
56
|
+
const gqlFieldName = inflection.attribute({
|
|
57
|
+
attributeName: attrName,
|
|
58
|
+
codec: resource.codec
|
|
59
|
+
});
|
|
60
|
+
fieldToAttr[gqlFieldName] = attrName;
|
|
61
|
+
attrToSqlType[attrName] = sql.compile(attr.codec.sqlType).text;
|
|
62
|
+
}
|
|
63
|
+
// Extract primary key columns for RETURNING clause
|
|
64
|
+
const primaryUnique = resource.uniques.find((u) => u.isPrimary) ?? resource.uniques[0];
|
|
65
|
+
const pkColumns = primaryUnique.attributes;
|
|
66
|
+
const pkReturning = pkColumns.map((c) => `"${c}"`).join(', ');
|
|
67
|
+
const compiledFrom = sql.compile(resource.from).text;
|
|
68
|
+
// Capture for plan closure
|
|
69
|
+
const executor = resource.executor;
|
|
70
|
+
const requireWhere = bulkRequireWhere;
|
|
71
|
+
fields = build.extend(fields, {
|
|
72
|
+
[fieldName]: context.fieldWithHooks({ fieldName }, {
|
|
73
|
+
description: `Bulk delete ${typeName} rows matching the given condition.`,
|
|
74
|
+
type: payloadType,
|
|
75
|
+
args: {
|
|
76
|
+
input: {
|
|
77
|
+
type: new GraphQLNonNull(inputType)
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
plan(_$root, args) {
|
|
81
|
+
const $input = args.getRaw('input');
|
|
82
|
+
const $result = (0, pg_1.sideEffectWithPgClient)(executor, $input, async (pgClient, input) => {
|
|
83
|
+
if (requireWhere && (!input.where || Object.keys(input.where).length === 0)) {
|
|
84
|
+
throw new Error('Bulk delete requires a non-empty where condition. Set bulkRequireWhere: false to allow unrestricted deletes.');
|
|
85
|
+
}
|
|
86
|
+
// Build WHERE clause from condition
|
|
87
|
+
// Supports both simple equality (Condition type: { name: "X" })
|
|
88
|
+
// and operator-based (Filter type: { name: { equalTo: "X" } })
|
|
89
|
+
const values = [];
|
|
90
|
+
const whereClauses = [];
|
|
91
|
+
if (input.where) {
|
|
92
|
+
for (const [key, spec] of Object.entries(input.where)) {
|
|
93
|
+
const attrName = fieldToAttr[key];
|
|
94
|
+
if (!attrName)
|
|
95
|
+
continue;
|
|
96
|
+
const sqlType = attrToSqlType[attrName];
|
|
97
|
+
if (spec === null) {
|
|
98
|
+
whereClauses.push(`"${attrName}" IS NULL`);
|
|
99
|
+
}
|
|
100
|
+
else if (spec !== undefined && typeof spec !== 'object') {
|
|
101
|
+
// Simple equality (Condition type)
|
|
102
|
+
values.push(spec);
|
|
103
|
+
whereClauses.push(`"${attrName}" = $${values.length}::${sqlType}`);
|
|
104
|
+
}
|
|
105
|
+
else if (spec && typeof spec === 'object') {
|
|
106
|
+
// Operator-based (Filter type)
|
|
107
|
+
for (const [op, val] of Object.entries(spec)) {
|
|
108
|
+
values.push(val);
|
|
109
|
+
const paramRef = `$${values.length}::${sqlType}`;
|
|
110
|
+
switch (op) {
|
|
111
|
+
case 'equalTo':
|
|
112
|
+
whereClauses.push(`"${attrName}" = ${paramRef}`);
|
|
113
|
+
break;
|
|
114
|
+
case 'notEqualTo':
|
|
115
|
+
whereClauses.push(`"${attrName}" != ${paramRef}`);
|
|
116
|
+
break;
|
|
117
|
+
case 'greaterThan':
|
|
118
|
+
whereClauses.push(`"${attrName}" > ${paramRef}`);
|
|
119
|
+
break;
|
|
120
|
+
case 'greaterThanOrEqualTo':
|
|
121
|
+
whereClauses.push(`"${attrName}" >= ${paramRef}`);
|
|
122
|
+
break;
|
|
123
|
+
case 'lessThan':
|
|
124
|
+
whereClauses.push(`"${attrName}" < ${paramRef}`);
|
|
125
|
+
break;
|
|
126
|
+
case 'lessThanOrEqualTo':
|
|
127
|
+
whereClauses.push(`"${attrName}" <= ${paramRef}`);
|
|
128
|
+
break;
|
|
129
|
+
case 'in':
|
|
130
|
+
if (Array.isArray(val)) {
|
|
131
|
+
const placeholders = val.map((v) => {
|
|
132
|
+
values.push(v);
|
|
133
|
+
return `$${values.length}::${sqlType}`;
|
|
134
|
+
});
|
|
135
|
+
values.pop();
|
|
136
|
+
whereClauses.push(`"${attrName}" IN (${placeholders.join(', ')})`);
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
case 'isNull':
|
|
140
|
+
values.pop();
|
|
141
|
+
if (val) {
|
|
142
|
+
whereClauses.push(`"${attrName}" IS NULL`);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
whereClauses.push(`"${attrName}" IS NOT NULL`);
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
default:
|
|
149
|
+
values.pop();
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (whereClauses.length === 0 && requireWhere) {
|
|
157
|
+
throw new Error('Bulk delete: no valid where conditions resolved. At least one condition is required.');
|
|
158
|
+
}
|
|
159
|
+
const whereStr = whereClauses.length > 0
|
|
160
|
+
? whereClauses.join(' AND ')
|
|
161
|
+
: 'TRUE';
|
|
162
|
+
// Use RETURNING <pk_columns> instead of RETURNING *
|
|
163
|
+
// For delete, we capture PKs before rows are gone
|
|
164
|
+
const text = `DELETE FROM ${compiledFrom}\nWHERE ${whereStr}\nRETURNING ${pkReturning}`;
|
|
165
|
+
const mutationResult = await pgClient.query(text, values);
|
|
166
|
+
const affectedCount = mutationResult.rowCount ?? 0;
|
|
167
|
+
// For delete, rows no longer exist so we can't do a
|
|
168
|
+
// follow-up SELECT. Return the PK values directly.
|
|
169
|
+
const returning = mutationResult.rows || [];
|
|
170
|
+
return {
|
|
171
|
+
affectedCount,
|
|
172
|
+
returning
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
return $result;
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
}, `Adding bulk delete field for ${typeName}`);
|
|
179
|
+
}
|
|
180
|
+
return fields;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import '../augmentations';
|
|
2
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
3
|
+
/**
|
|
4
|
+
* BulkInflectionPlugin
|
|
5
|
+
*
|
|
6
|
+
* Adds inflection methods for naming bulk mutation types, fields, and enums.
|
|
7
|
+
* Naming is configurable via the `bulkNaming` schema option.
|
|
8
|
+
*/
|
|
9
|
+
export declare const BulkInflectionPlugin: GraphileConfig.Plugin;
|