Skip to content

Commit b08a92c

Browse files
authored
fix: monolith promotion to empty target (#8376)
1 parent d2a20f7 commit b08a92c

4 files changed

Lines changed: 178 additions & 56 deletions

File tree

.changeset/dry-nails-prove.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'hive': patch
3+
---
4+
5+
Fix internal server error when promoting a monolith schema to an empty target.

integration-tests/tests/api/schema/promotion.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,3 +1131,57 @@ test.concurrent('promote monolith schema version succeeds', async ({ expect }) =
11311131
});
11321132
expect(promotedVersionDetails.sdl).toContain('b: String!');
11331133
});
1134+
1135+
test.concurrent('promote monolith version to empty target', async () => {
1136+
const { createOrg } = await initSeed().createOwner();
1137+
const { createProject, createOrganizationAccessToken } = await createOrg();
1138+
const { target, fetchVersions, createTarget } = await createProject(ProjectType.Single);
1139+
const otherTarget = await createTarget().then(r => r.expectNoGraphQLErrors());
1140+
const { privateAccessKey } = await createOrganizationAccessToken({
1141+
resources: {
1142+
mode: ResourceAssignmentModeType.All,
1143+
},
1144+
permissions: [
1145+
'schemaVersion:publish',
1146+
'target:modifySettings',
1147+
'project:describe',
1148+
'schemaVersion:promote',
1149+
],
1150+
});
1151+
1152+
await publishSchema(
1153+
{
1154+
author: 'a',
1155+
commit: 'a',
1156+
sdl: /* GraphQL */ `
1157+
type Query {
1158+
a: String!
1159+
}
1160+
`,
1161+
target: {
1162+
byId: target.id,
1163+
},
1164+
},
1165+
privateAccessKey,
1166+
).then(r => r.expectNoGraphQLErrors());
1167+
1168+
const [versionToPromote] = await fetchVersions(1);
1169+
1170+
assertNonNullish(otherTarget.createTarget.ok);
1171+
let promoteResult = await schemaVersionPromote(
1172+
{
1173+
source: {
1174+
fromSchemaVersionById: versionToPromote.id,
1175+
},
1176+
target: {
1177+
toTarget: {
1178+
byId: otherTarget.createTarget.ok.createdTarget.id,
1179+
},
1180+
},
1181+
},
1182+
privateAccessKey,
1183+
).then(r => r.expectNoGraphQLErrors());
1184+
1185+
expect(promoteResult.schemaVersionPromote.error).toEqual(null);
1186+
assertNonNullish(promoteResult.schemaVersionPromote.ok);
1187+
});

packages/services/api/src/modules/schema/providers/schema-publisher.ts

Lines changed: 87 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2400,8 +2400,43 @@ export class SchemaPublisher {
24002400
});
24012401
}
24022402

2403+
private diffSingleSchemaLogs(args: {
2404+
logs: {
2405+
target: Array<SchemaLogWithEdges>;
2406+
origin: Array<SchemaLogWithEdges>;
2407+
};
2408+
target: Target;
2409+
}): SchemaLogDiffInput {
2410+
// we do not need to diff the services
2411+
// the "main" diff already covers all changes
2412+
invariant(args.logs.origin.length === 1, 'In a monolith project there can only be one log.');
2413+
invariant(
2414+
args.logs.target.length <= 1,
2415+
'In a monolith project there can only be up to one log.',
2416+
);
2417+
2418+
return {
2419+
removed: [],
2420+
added: [],
2421+
changed: [
2422+
{
2423+
id: args.logs.origin[0].actionId,
2424+
// we do not need a direct link to the previous log
2425+
previousId: null,
2426+
serviceName: null,
2427+
// we can omit the type for a monolith schema; there is always only one "subgraph"
2428+
type: null,
2429+
// there are no service specific changes
2430+
// the changes are already covered via the main graph
2431+
changes: null,
2432+
},
2433+
],
2434+
unchanged: [],
2435+
};
2436+
}
2437+
24032438
@traceFn('SchemaPublisher.diffSchemaLogs')
2404-
private async diffSchemaLogs(args: {
2439+
private async diffCompositeSchemaLogs(args: {
24052440
logs: {
24062441
target: Array<SchemaLogWithEdges>;
24072442
origin: Array<SchemaLogWithEdges>;
@@ -2436,14 +2471,15 @@ export class SchemaPublisher {
24362471
continue;
24372472
}
24382473

2474+
invariant(targetLogEdge.node.service_name !== null, 'A service name must exist.');
2475+
24392476
// Note: we use fallback value of '' to support single schema workflows.
2440-
const serviceName = targetLogEdge.node.service_name ?? '';
24412477
invariant(
2442-
diffMap.has(serviceName) === false,
2478+
diffMap.has(targetLogEdge.node.service_name) === false,
24432479
'Invalid database state. A log for the same service can not appear more than once.',
24442480
);
24452481

2446-
diffMap.set(serviceName, {
2482+
diffMap.set(targetLogEdge.node.service_name, {
24472483
type: 'removed',
24482484
previousLog: targetLogEdge.node,
24492485
});
@@ -2454,13 +2490,11 @@ export class SchemaPublisher {
24542490
continue;
24552491
}
24562492

2457-
const serviceName = originLogEdge.node.service_name ?? '';
2493+
invariant(originLogEdge.node.service_name !== null, 'A service name must exist.');
24582494

2459-
let record = diffMap.get(serviceName);
2495+
let record = diffMap.get(originLogEdge.node.service_name);
24602496

24612497
if (!record) {
2462-
invariant(originLogEdge.node.service_name !== null, 'A service name must exist.');
2463-
24642498
diffMap.set(originLogEdge.node.service_name, {
24652499
type: 'added',
24662500
newLog: originLogEdge.node,
@@ -2471,15 +2505,15 @@ export class SchemaPublisher {
24712505
invariant(record.type === 'removed', 'At this point the type can only be removed.');
24722506

24732507
if (record.previousLog.id === originLogEdge.node.id) {
2474-
diffMap.set(serviceName, {
2508+
diffMap.set(originLogEdge.node.service_name, {
24752509
type: 'unchanged',
24762510
log: record.previousLog,
24772511
});
24782512
continue;
24792513
}
24802514

24812515
if (record.previousLog.id !== originLogEdge.node.id) {
2482-
diffMap.set(serviceName, {
2516+
diffMap.set(originLogEdge.node.service_name, {
24832517
type: 'changed',
24842518
newLog: originLogEdge.node,
24852519
previousLog: record.previousLog,
@@ -2492,7 +2526,7 @@ export class SchemaPublisher {
24922526
// Let's create the new Graph version edges and delete logs (if needed)
24932527

24942528
const schemaLogs: SchemaLogDiffInput = {
2495-
deleted: [],
2529+
removed: [],
24962530
added: [],
24972531
changed: [],
24982532
unchanged: [],
@@ -2504,12 +2538,13 @@ export class SchemaPublisher {
25042538

25052539
// Note: we seed the ID here so we do not need to map some more within the logic within `SchemaVersions.promoteSchemaVersionToTarget`
25062540
const logId = crypto.randomUUID();
2507-
schemaLogs.deleted.push({
2541+
schemaLogs.removed.push({
25082542
id: logId,
25092543
previousId: diff.previousLog.id,
25102544
serviceName: diff.previousLog.service_name,
25112545
targetId: args.target.id,
25122546
projectId: args.target.projectId,
2547+
type: 'removed',
25132548
});
25142549
continue;
25152550
}
@@ -2522,42 +2557,45 @@ export class SchemaPublisher {
25222557
serviceName: diff.newLog.service_name,
25232558
projectId: args.target.projectId,
25242559
targetId: args.target.id,
2560+
type: 'added',
25252561
});
25262562
continue;
25272563
}
25282564

25292565
if (diff.type === 'unchanged') {
2530-
schemaLogs.unchanged.push({ id: diff.log.id, serviceName: diff.log.service_name });
2566+
schemaLogs.unchanged.push({
2567+
id: diff.log.id,
2568+
serviceName: diff.log.service_name,
2569+
type: 'unchanged',
2570+
});
25312571
continue;
25322572
}
25332573

25342574
if (diff.type === 'changed') {
2535-
let changes = null;
2536-
2537-
// We only want a diff for non-monolith schemas
2538-
if (diff.newLog.service_name) {
2539-
changes = await this.registryChecks
2540-
.diff({
2541-
existingSdl: diff.previousLog.sdl ?? null,
2542-
incomingSdl: diff.newLog.sdl ?? null,
2543-
approvedChanges: null,
2544-
conditionalBreakingChangeConfig: null,
2545-
includeUrlChanges: false,
2546-
filterOutFederationChanges: false,
2547-
failDiffOnDangerousChange: false,
2548-
failAllDangerousChanges: false,
2549-
failDangerousChangeTypes: [],
2550-
filterNestedChanges: true,
2551-
getAffectedAppDeployments: null,
2552-
})
2553-
.then(r => r.result?.all ?? r.reason?.all ?? null);
2554-
}
2575+
invariant(diff.newLog.service_name != null, 'Changed logs require a service name.');
2576+
2577+
const changes = await this.registryChecks
2578+
.diff({
2579+
existingSdl: diff.previousLog.sdl ?? null,
2580+
incomingSdl: diff.newLog.sdl ?? null,
2581+
approvedChanges: null,
2582+
conditionalBreakingChangeConfig: null,
2583+
includeUrlChanges: false,
2584+
filterOutFederationChanges: false,
2585+
failDiffOnDangerousChange: false,
2586+
failAllDangerousChanges: false,
2587+
failDangerousChangeTypes: [],
2588+
filterNestedChanges: true,
2589+
getAffectedAppDeployments: null,
2590+
})
2591+
.then(r => r.result?.all ?? r.reason?.all ?? null);
25552592

25562593
schemaLogs.changed.push({
25572594
id: diff.newLog.id,
25582595
previousId: diff.previousLog.id,
25592596
serviceName: diff.newLog.service_name,
25602597
changes,
2598+
type: 'changed',
25612599
});
25622600
continue;
25632601
}
@@ -2569,7 +2607,7 @@ export class SchemaPublisher {
25692607
this.logger.debug(
25702608
'producing schema log diff finished (addedCount=%d, deletedCount=%d, changedCount=%d).',
25712609
schemaLogs.added.length,
2572-
schemaLogs.deleted.length,
2610+
schemaLogs.removed.length,
25732611
schemaLogs.changed.length,
25742612
schemaLogs.unchanged.length,
25752613
);
@@ -2937,13 +2975,21 @@ export class SchemaPublisher {
29372975
contracts,
29382976
conditionalBreakingChangeMetadata,
29392977
] = await Promise.all([
2940-
this.diffSchemaLogs({
2941-
logs: {
2942-
target: targetLogEdges,
2943-
origin: originLogEdges,
2944-
},
2945-
target,
2946-
}),
2978+
project.type === Types.ProjectType.SINGLE
2979+
? this.diffSingleSchemaLogs({
2980+
logs: {
2981+
target: targetLogEdges,
2982+
origin: originLogEdges,
2983+
},
2984+
target,
2985+
})
2986+
: this.diffCompositeSchemaLogs({
2987+
logs: {
2988+
target: targetLogEdges,
2989+
origin: originLogEdges,
2990+
},
2991+
target,
2992+
}),
29472993
this.registryChecks
29482994
.diff({
29492995
existingSdl: targetLatestValidSchemaVersion?.compositeSchemaSDL ?? null,

0 commit comments

Comments
 (0)