Skip to content

Commit bf6061d

Browse files
committed
feat: add codegen support for bulk mutations (ORM methods, React Query hooks, CLI commands)
Phase 6 of the bulk mutations feature: - Extend introspection to detect bulkCreate/bulkUpsert/bulkUpdate/bulkDelete mutations from the GraphQL schema (infer-tables.ts, transform-schema.ts) - Add bulk document builders to query-builder template (buildBulkInsertDocument, buildBulkUpsertDocument, etc.) - Add bulk arg types and BulkMutationResult to select-types template - Generate ORM bulk methods on model classes (bulkCreate, bulkUpsert, bulkUpdate, bulkDelete) via Babel AST in model-generator.ts - Generate React Query mutation hooks (useBulkCreateX, useBulkUpsertX, useBulkUpdateX, useBulkDeleteX) in mutations.ts - Generate CLI subcommands (bulk-create, bulk-upsert, bulk-update, bulk-delete) in table-command-generator.ts - Add bulk mutation keys to mutation-keys.ts for tracking in-flight mutations - Add bulk naming helper functions to utils.ts
1 parent a19d207 commit bf6061d

11 files changed

Lines changed: 1230 additions & 1 deletion

File tree

graphql/codegen/src/core/codegen/cli/table-command-generator.ts

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,6 +1322,275 @@ function buildMutationHandler(
13221322
);
13231323
}
13241324

1325+
type BulkCliOp = 'bulk-create' | 'bulk-upsert' | 'bulk-update' | 'bulk-delete';
1326+
1327+
function buildBulkMutationHandler(
1328+
table: Table,
1329+
operation: BulkCliOp,
1330+
targetName?: string,
1331+
): t.FunctionDeclaration {
1332+
const { singularName } = getTableNames(table);
1333+
const selectObj = buildSelectObject(table);
1334+
1335+
// Map CLI op name to ORM method name
1336+
const ormMethod = (() => {
1337+
switch (operation) {
1338+
case 'bulk-create': return 'bulkCreate';
1339+
case 'bulk-upsert': return 'bulkUpsert';
1340+
case 'bulk-update': return 'bulkUpdate';
1341+
case 'bulk-delete': return 'bulkDelete';
1342+
}
1343+
})();
1344+
1345+
const tryBody: t.Statement[] = [];
1346+
1347+
if (operation === 'bulk-create' || operation === 'bulk-upsert') {
1348+
// Parse --data (JSON array) from argv
1349+
tryBody.push(
1350+
t.variableDeclaration('const', [
1351+
t.variableDeclarator(
1352+
t.identifier('dataRaw'),
1353+
t.memberExpression(t.identifier('argv'), t.identifier('data')),
1354+
),
1355+
]),
1356+
);
1357+
tryBody.push(
1358+
t.ifStatement(
1359+
t.unaryExpression('!', t.identifier('dataRaw')),
1360+
t.blockStatement([
1361+
t.expressionStatement(
1362+
t.callExpression(
1363+
t.memberExpression(t.identifier('console'), t.identifier('error')),
1364+
[t.stringLiteral(`--data is required for ${operation}. Provide a JSON array.`)],
1365+
),
1366+
),
1367+
t.expressionStatement(
1368+
t.callExpression(
1369+
t.memberExpression(t.identifier('process'), t.identifier('exit')),
1370+
[t.numericLiteral(1)],
1371+
),
1372+
),
1373+
]),
1374+
),
1375+
);
1376+
tryBody.push(
1377+
t.variableDeclaration('const', [
1378+
t.variableDeclarator(
1379+
t.identifier('data'),
1380+
t.callExpression(
1381+
t.memberExpression(t.identifier('JSON'), t.identifier('parse')),
1382+
[t.tsAsExpression(t.identifier('dataRaw'), t.tsStringKeyword())],
1383+
),
1384+
),
1385+
]),
1386+
);
1387+
1388+
let ormArgs: t.ObjectExpression;
1389+
if (operation === 'bulk-upsert') {
1390+
// Also parse --on-conflict
1391+
tryBody.push(
1392+
t.variableDeclaration('const', [
1393+
t.variableDeclarator(
1394+
t.identifier('onConflictRaw'),
1395+
t.memberExpression(t.identifier('argv'), t.identifier('on-conflict')),
1396+
),
1397+
]),
1398+
);
1399+
tryBody.push(
1400+
t.variableDeclaration('const', [
1401+
t.variableDeclarator(
1402+
t.identifier('onConflict'),
1403+
t.conditionalExpression(
1404+
t.identifier('onConflictRaw'),
1405+
t.callExpression(
1406+
t.memberExpression(t.identifier('JSON'), t.identifier('parse')),
1407+
[t.tsAsExpression(t.identifier('onConflictRaw'), t.tsStringKeyword())],
1408+
),
1409+
t.objectExpression([]),
1410+
),
1411+
),
1412+
]),
1413+
);
1414+
ormArgs = t.objectExpression([
1415+
t.objectProperty(t.identifier('data'), t.identifier('data')),
1416+
t.objectProperty(t.identifier('onConflict'), t.identifier('onConflict')),
1417+
t.objectProperty(t.identifier('select'), selectObj),
1418+
]);
1419+
} else {
1420+
ormArgs = t.objectExpression([
1421+
t.objectProperty(t.identifier('data'), t.identifier('data')),
1422+
t.objectProperty(t.identifier('select'), selectObj),
1423+
]);
1424+
}
1425+
1426+
tryBody.push(buildGetClientStatement(targetName));
1427+
tryBody.push(
1428+
t.variableDeclaration('const', [
1429+
t.variableDeclarator(
1430+
t.identifier('result'),
1431+
t.awaitExpression(buildOrmCall(singularName, ormMethod, ormArgs)),
1432+
),
1433+
]),
1434+
);
1435+
} else if (operation === 'bulk-update') {
1436+
// Parse --where (JSON) and --data (JSON)
1437+
tryBody.push(
1438+
t.variableDeclaration('const', [
1439+
t.variableDeclarator(
1440+
t.identifier('whereRaw'),
1441+
t.memberExpression(t.identifier('argv'), t.identifier('where')),
1442+
),
1443+
]),
1444+
);
1445+
tryBody.push(
1446+
t.variableDeclaration('const', [
1447+
t.variableDeclarator(
1448+
t.identifier('dataRaw'),
1449+
t.memberExpression(t.identifier('argv'), t.identifier('data')),
1450+
),
1451+
]),
1452+
);
1453+
tryBody.push(
1454+
t.ifStatement(
1455+
t.logicalExpression(
1456+
'||',
1457+
t.unaryExpression('!', t.identifier('whereRaw')),
1458+
t.unaryExpression('!', t.identifier('dataRaw')),
1459+
),
1460+
t.blockStatement([
1461+
t.expressionStatement(
1462+
t.callExpression(
1463+
t.memberExpression(t.identifier('console'), t.identifier('error')),
1464+
[t.stringLiteral('--where and --data are required for bulk-update. Provide JSON objects.')],
1465+
),
1466+
),
1467+
t.expressionStatement(
1468+
t.callExpression(
1469+
t.memberExpression(t.identifier('process'), t.identifier('exit')),
1470+
[t.numericLiteral(1)],
1471+
),
1472+
),
1473+
]),
1474+
),
1475+
);
1476+
tryBody.push(
1477+
t.variableDeclaration('const', [
1478+
t.variableDeclarator(
1479+
t.identifier('where'),
1480+
t.callExpression(
1481+
t.memberExpression(t.identifier('JSON'), t.identifier('parse')),
1482+
[t.tsAsExpression(t.identifier('whereRaw'), t.tsStringKeyword())],
1483+
),
1484+
),
1485+
]),
1486+
);
1487+
tryBody.push(
1488+
t.variableDeclaration('const', [
1489+
t.variableDeclarator(
1490+
t.identifier('data'),
1491+
t.callExpression(
1492+
t.memberExpression(t.identifier('JSON'), t.identifier('parse')),
1493+
[t.tsAsExpression(t.identifier('dataRaw'), t.tsStringKeyword())],
1494+
),
1495+
),
1496+
]),
1497+
);
1498+
tryBody.push(buildGetClientStatement(targetName));
1499+
tryBody.push(
1500+
t.variableDeclaration('const', [
1501+
t.variableDeclarator(
1502+
t.identifier('result'),
1503+
t.awaitExpression(
1504+
buildOrmCall(singularName, ormMethod, t.objectExpression([
1505+
t.objectProperty(t.identifier('where'), t.identifier('where')),
1506+
t.objectProperty(t.identifier('data'), t.identifier('data')),
1507+
t.objectProperty(t.identifier('select'), selectObj),
1508+
])),
1509+
),
1510+
),
1511+
]),
1512+
);
1513+
} else {
1514+
// bulk-delete: parse --where (JSON)
1515+
tryBody.push(
1516+
t.variableDeclaration('const', [
1517+
t.variableDeclarator(
1518+
t.identifier('whereRaw'),
1519+
t.memberExpression(t.identifier('argv'), t.identifier('where')),
1520+
),
1521+
]),
1522+
);
1523+
tryBody.push(
1524+
t.ifStatement(
1525+
t.unaryExpression('!', t.identifier('whereRaw')),
1526+
t.blockStatement([
1527+
t.expressionStatement(
1528+
t.callExpression(
1529+
t.memberExpression(t.identifier('console'), t.identifier('error')),
1530+
[t.stringLiteral('--where is required for bulk-delete. Provide a JSON object.')],
1531+
),
1532+
),
1533+
t.expressionStatement(
1534+
t.callExpression(
1535+
t.memberExpression(t.identifier('process'), t.identifier('exit')),
1536+
[t.numericLiteral(1)],
1537+
),
1538+
),
1539+
]),
1540+
),
1541+
);
1542+
tryBody.push(
1543+
t.variableDeclaration('const', [
1544+
t.variableDeclarator(
1545+
t.identifier('where'),
1546+
t.callExpression(
1547+
t.memberExpression(t.identifier('JSON'), t.identifier('parse')),
1548+
[t.tsAsExpression(t.identifier('whereRaw'), t.tsStringKeyword())],
1549+
),
1550+
),
1551+
]),
1552+
);
1553+
tryBody.push(buildGetClientStatement(targetName));
1554+
tryBody.push(
1555+
t.variableDeclaration('const', [
1556+
t.variableDeclarator(
1557+
t.identifier('result'),
1558+
t.awaitExpression(
1559+
buildOrmCall(singularName, ormMethod, t.objectExpression([
1560+
t.objectProperty(t.identifier('where'), t.identifier('where')),
1561+
t.objectProperty(t.identifier('select'), selectObj),
1562+
])),
1563+
),
1564+
),
1565+
]),
1566+
);
1567+
}
1568+
1569+
tryBody.push(buildJsonLog(t.identifier('result')));
1570+
1571+
const argvParam = t.identifier('argv');
1572+
argvParam.typeAnnotation = buildArgvType();
1573+
const prompterParam = t.identifier('prompter');
1574+
prompterParam.typeAnnotation = t.tsTypeAnnotation(
1575+
t.tsTypeReference(t.identifier('Inquirerer')),
1576+
);
1577+
1578+
const handlerName = `handle${toPascalCase(operation)}`;
1579+
1580+
return t.functionDeclaration(
1581+
t.identifier(handlerName),
1582+
[argvParam, prompterParam],
1583+
t.blockStatement([
1584+
t.tryStatement(
1585+
t.blockStatement(tryBody),
1586+
buildErrorCatch(`Failed to ${operation}.`),
1587+
),
1588+
]),
1589+
false,
1590+
true,
1591+
);
1592+
}
1593+
13251594
export interface TableCommandOptions {
13261595
targetName?: string;
13271596
executorImportPath?: string;
@@ -1439,12 +1708,22 @@ export function generateTableCommand(table: Table, options?: TableCommandOptions
14391708
);
14401709
}
14411710

1711+
// Detect bulk mutations
1712+
const hasBulkCreate = !!table.query?.bulkInsert;
1713+
const hasBulkUpsert = !!table.query?.bulkUpsert;
1714+
const hasBulkUpdate = !!table.query?.bulkUpdate;
1715+
const hasBulkDelete = !!table.query?.bulkDelete;
1716+
14421717
const subcommands: string[] = ['list', 'find-first'];
14431718
if (hasSearchFields) subcommands.push('search');
14441719
if (hasGet) subcommands.push('get');
14451720
subcommands.push('create');
14461721
if (hasUpdate) subcommands.push('update');
14471722
if (hasDelete) subcommands.push('delete');
1723+
if (hasBulkCreate) subcommands.push('bulk-create');
1724+
if (hasBulkUpsert) subcommands.push('bulk-upsert');
1725+
if (hasBulkUpdate) subcommands.push('bulk-update');
1726+
if (hasBulkDelete) subcommands.push('bulk-delete');
14481727

14491728
const usageLines = [
14501729
'',
@@ -1466,6 +1745,10 @@ export function generateTableCommand(table: Table, options?: TableCommandOptions
14661745
);
14671746
}
14681747
if (hasDelete) usageLines.push(` delete Delete a ${singularName}`);
1748+
if (hasBulkCreate) usageLines.push(` bulk-create Bulk create ${singularName} records`);
1749+
if (hasBulkUpsert) usageLines.push(` bulk-upsert Bulk upsert ${singularName} records`);
1750+
if (hasBulkUpdate) usageLines.push(` bulk-update Bulk update ${singularName} records`);
1751+
if (hasBulkDelete) usageLines.push(` bulk-delete Bulk delete ${singularName} records`);
14691752
usageLines.push(
14701753
'',
14711754
'List Options:',
@@ -1684,6 +1967,10 @@ export function generateTableCommand(table: Table, options?: TableCommandOptions
16841967
statements.push(buildMutationHandler(table, 'create', vectorFieldNames, tn, options?.typeRegistry, ormTypes));
16851968
if (hasUpdate) statements.push(buildMutationHandler(table, 'update', vectorFieldNames, tn, options?.typeRegistry, ormTypes));
16861969
if (hasDelete) statements.push(buildMutationHandler(table, 'delete', vectorFieldNames, tn, options?.typeRegistry, ormTypes));
1970+
if (hasBulkCreate) statements.push(buildBulkMutationHandler(table, 'bulk-create', tn));
1971+
if (hasBulkUpsert) statements.push(buildBulkMutationHandler(table, 'bulk-upsert', tn));
1972+
if (hasBulkUpdate) statements.push(buildBulkMutationHandler(table, 'bulk-update', tn));
1973+
if (hasBulkDelete) statements.push(buildBulkMutationHandler(table, 'bulk-delete', tn));
16871974

16881975
const header = getGeneratedFileHeader(`CLI commands for ${table.name}`);
16891976
const code = generateCode(statements);

graphql/codegen/src/core/codegen/mutation-keys.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,27 @@ function generateEntityMutationKeysDeclaration(
140140
addJSDocComment(deleteProp, [`Delete ${singularName} mutation key`]);
141141
properties.push(deleteProp);
142142

143+
// Bulk mutation keys (only if table has bulk operations)
144+
const bulkOps: Array<{ key: string; queryField: string | null | undefined }> = [
145+
{ key: 'bulkCreate', queryField: table.query?.bulkInsert },
146+
{ key: 'bulkUpsert', queryField: table.query?.bulkUpsert },
147+
{ key: 'bulkUpdate', queryField: table.query?.bulkUpdate },
148+
{ key: 'bulkDelete', queryField: table.query?.bulkDelete },
149+
];
150+
for (const { key, queryField } of bulkOps) {
151+
if (!queryField) continue;
152+
const arrowFn = t.arrowFunctionExpression(
153+
[],
154+
constArray([
155+
t.stringLiteral('mutation'),
156+
t.stringLiteral(entityKey),
157+
t.stringLiteral(key),
158+
]),
159+
);
160+
const prop = t.objectProperty(t.identifier(key), arrowFn);
161+
properties.push(prop);
162+
}
163+
143164
return t.exportNamedDeclaration(
144165
t.variableDeclaration('const', [
145166
t.variableDeclarator(

0 commit comments

Comments
 (0)