graphile-bulk-mutations 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +197 -0
  3. package/augmentations.d.ts +102 -0
  4. package/augmentations.js +11 -0
  5. package/esm/augmentations.d.ts +102 -0
  6. package/esm/augmentations.js +9 -0
  7. package/esm/index.d.ts +30 -0
  8. package/esm/index.js +29 -0
  9. package/esm/plugins/BulkDeletePlugin.d.ts +13 -0
  10. package/esm/plugins/BulkDeletePlugin.js +181 -0
  11. package/esm/plugins/BulkInflectionPlugin.d.ts +9 -0
  12. package/esm/plugins/BulkInflectionPlugin.js +103 -0
  13. package/esm/plugins/BulkInsertPlugin.d.ts +21 -0
  14. package/esm/plugins/BulkInsertPlugin.js +214 -0
  15. package/esm/plugins/BulkRelationalPlugin.d.ts +31 -0
  16. package/esm/plugins/BulkRelationalPlugin.js +127 -0
  17. package/esm/plugins/BulkTypesPlugin.d.ts +18 -0
  18. package/esm/plugins/BulkTypesPlugin.js +391 -0
  19. package/esm/plugins/BulkUpdatePlugin.d.ts +13 -0
  20. package/esm/plugins/BulkUpdatePlugin.js +211 -0
  21. package/esm/plugins/BulkUpsertPlugin.d.ts +12 -0
  22. package/esm/plugins/BulkUpsertPlugin.js +165 -0
  23. package/esm/plugins/index.d.ts +7 -0
  24. package/esm/plugins/index.js +7 -0
  25. package/esm/preset.d.ts +35 -0
  26. package/esm/preset.js +74 -0
  27. package/esm/types.d.ts +40 -0
  28. package/esm/types.js +7 -0
  29. package/esm/utils/index.d.ts +4 -0
  30. package/esm/utils/index.js +2 -0
  31. package/esm/utils/relations.d.ts +42 -0
  32. package/esm/utils/relations.js +83 -0
  33. package/esm/utils/sql-builder.d.ts +56 -0
  34. package/esm/utils/sql-builder.js +121 -0
  35. package/index.d.ts +30 -0
  36. package/index.js +44 -0
  37. package/package.json +61 -0
  38. package/plugins/BulkDeletePlugin.d.ts +13 -0
  39. package/plugins/BulkDeletePlugin.js +184 -0
  40. package/plugins/BulkInflectionPlugin.d.ts +9 -0
  41. package/plugins/BulkInflectionPlugin.js +106 -0
  42. package/plugins/BulkInsertPlugin.d.ts +21 -0
  43. package/plugins/BulkInsertPlugin.js +217 -0
  44. package/plugins/BulkRelationalPlugin.d.ts +31 -0
  45. package/plugins/BulkRelationalPlugin.js +130 -0
  46. package/plugins/BulkTypesPlugin.d.ts +18 -0
  47. package/plugins/BulkTypesPlugin.js +394 -0
  48. package/plugins/BulkUpdatePlugin.d.ts +13 -0
  49. package/plugins/BulkUpdatePlugin.js +214 -0
  50. package/plugins/BulkUpsertPlugin.d.ts +12 -0
  51. package/plugins/BulkUpsertPlugin.js +168 -0
  52. package/plugins/index.d.ts +7 -0
  53. package/plugins/index.js +17 -0
  54. package/preset.d.ts +35 -0
  55. package/preset.js +77 -0
  56. package/types.d.ts +40 -0
  57. package/types.js +10 -0
  58. package/utils/index.d.ts +4 -0
  59. package/utils/index.js +9 -0
  60. package/utils/relations.d.ts +42 -0
  61. package/utils/relations.js +86 -0
  62. package/utils/sql-builder.d.ts +56 -0
  63. package/utils/sql-builder.js +126 -0
@@ -0,0 +1,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';
@@ -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;
@@ -0,0 +1,4 @@
1
+ export type { NestedRelationInfo } from './relations';
2
+ export { discoverNestedRelations } from './relations';
3
+ export type { ColumnSpec, InsertBatch } from './sql-builder';
4
+ export { buildBulkDeleteSQL, buildBulkInsertSQL, buildBulkUpdateSQL } from './sql-builder';
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
+ };