Skip to content
70 changes: 62 additions & 8 deletions controlplane/src/core/repositories/SchemaCheckRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CompositionWarning,
GraphPruningIssue,
LintIssue,
LintSeverity,
ProposalSubgraph,
SchemaChange,
VCSContext,
Expand Down Expand Up @@ -744,8 +745,13 @@ export class SchemaCheckRepository {
const compositionErrors: PlainMessage<CompositionError>[] = [];
const compositionWarnings: PlainMessage<CompositionWarning>[] = [];

type ExtendedCheckSubgraph = CheckSubgraph & {
lintIssues: SchemaLintIssues;
pruneIssues: SchemaGraphPruningIssues;
};

const federatedGraphs: FederatedGraphDTO[] = [];
const checkSubgraphs: Map<string, CheckSubgraph> = new Map();
const checkSubgraphs: Map<string, ExtendedCheckSubgraph> = new Map();
const fedGraphIdToCheckFedGraphId = new Map<string, string>();

const changeRetention = await orgRepo.getFeature({
Expand Down Expand Up @@ -943,6 +949,8 @@ export class SchemaCheckRepository {
checkSubgraphId: schemaCheckSubgraphId,
routerCompatibilityVersion,
labels: s.isNew ? s.labels : undefined,
lintIssues: { warnings: [], errors: [] },
pruneIssues: { warnings: [], errors: [] },
});
}

Expand Down Expand Up @@ -1029,13 +1037,6 @@ export class SchemaCheckRepository {
storedBreakingChanges,
);

checkSubgraphs.set(subgraphName, {
...checkSubgraph,
inspectorChanges,
storedBreakingChanges,
checkSubgraphId: schemaCheckSubgraphId,
});

const lintIssues: SchemaLintIssues = await schemaLintRepo.performSchemaLintCheck({
schemaCheckID,
newSchemaSDL,
Expand Down Expand Up @@ -1119,6 +1120,15 @@ export class SchemaCheckRepository {
}),
),
);

checkSubgraphs.set(subgraphName, {
...checkSubgraph,
inspectorChanges,
storedBreakingChanges,
checkSubgraphId: schemaCheckSubgraphId,
lintIssues,
pruneIssues: graphPruningIssues,
});
}

const { composedGraphs } = await composer.composeWithProposedSchemas({
Expand Down Expand Up @@ -1322,13 +1332,57 @@ export class SchemaCheckRepository {
}
}

// Execute the subgraph check extension webhook
const sceResult = await webhookService.sendSubgraphCheckExtension({
actorId,
schemaCheckID,
blobStorage,
admissionConfig,
organization: { id: organizationId, slug: organizationSlug },
namespace,
vcsContext,
subgraphs: [...checkSubgraphs.entries()].map(([subgraphName, { subgraph, ...check }]) => ({
id: subgraph?.id ?? '',
name: subgraphName,
labels: subgraph?.labels ?? [],
schemaSDL: subgraph?.schemaSDL ?? '',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
schemaChanges: check.schemaChanges,
lintIssues: check.lintIssues,
pruneIssues: check.pruneIssues,
newSchemaSDL: check.newSchemaSDL,
isDeleted: check.newSchemaSDL === '',
})),
affectedGraphs: federatedGraphs,
composedGraphs,
inspectedOperations,
});
Comment thread
wilsonrivera marked this conversation as resolved.

if (sceResult?.lintIssuesBySubgraph) {
for (const [subgraphName, check] of checkSubgraphs.entries()) {
const sceLintIssues = sceResult.lintIssuesBySubgraph.get(subgraphName);
if (sceLintIssues && sceLintIssues.length > 0) {
check.lintIssues.warnings.push(...sceLintIssues.filter((issue) => issue.severity === LintSeverity.warn));
check.lintIssues.errors.push(...sceLintIssues.filter((issue) => issue.severity === LintSeverity.error));

// Then, we need to add the overwritten lint issues
await schemaLintRepo.addSchemaCheckLintIssues({
schemaCheckId: schemaCheckID,
lintIssues: sceLintIssues,
schemaCheckSubgraphId: check.checkSubgraphId,
});
}
}
}

// Update the overall schema check with the results
await this.update({
schemaCheckID,
hasClientTraffic,
hasBreakingChanges: breakingChanges.length > 0 || composedSchemaBreakingChanges.length > 0,
hasLintErrors: lintErrors.length > 0,
hasGraphPruningErrors: graphPruneErrors.length > 0,
checkExtensionDeliveryId: sceResult?.deliveryInfo?.id,
checkExtensionErrorMessage: sceResult?.deliveryInfo?.errorMessage ?? undefined,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

let isLinkedTrafficCheckFailed = false;
Expand Down
44 changes: 25 additions & 19 deletions controlplane/src/core/repositories/SubgraphRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2299,32 +2299,38 @@ export class SubgraphRepository {
organization: { id: this.organizationId, slug: organizationSlug },
namespace,
vcsContext,
subgraph,
newSchemaSDL,
isDeleted,
subgraphs: [
{
id: subgraph?.id ?? '',
name: subgraph?.name ?? subgraphName,
labels: subgraph?.labels ?? labels ?? [],
schemaSDL: subgraph?.schemaSDL ?? '',
schemaChanges,
lintIssues,
pruneIssues: graphPruningIssues,
newSchemaSDL,
isDeleted,
},
],
affectedGraphs: federatedGraphs,
composedGraphs,
schemaChanges,
lintIssues,
pruneIssues: graphPruningIssues,
inspectedOperations,
});

if (sceResult && sceResult.additionalLintIssues.length > 0) {
const additionalLintIssues: SchemaLintIssues = {
warnings: sceResult.additionalLintIssues.filter((issue) => issue.severity === LintSeverity.warn),
errors: sceResult.additionalLintIssues.filter((issue) => issue.severity === LintSeverity.error),
};
if (sceResult?.lintIssuesBySubgraph) {
const sceLintIssues = sceResult.lintIssuesBySubgraph.get(subgraph?.name ?? subgraphName);
Comment thread
wilsonrivera marked this conversation as resolved.

lintIssues.warnings.push(...additionalLintIssues.warnings);
lintIssues.errors.push(...additionalLintIssues.errors);
if (sceLintIssues && sceLintIssues.length > 0) {
lintIssues.warnings.push(...sceLintIssues.filter((issue) => issue.severity === LintSeverity.warn));
lintIssues.errors.push(...sceLintIssues.filter((issue) => issue.severity === LintSeverity.error));

// Then, we need to add the overwritten lint issues
await schemaLintRepo.addSchemaCheckLintIssues({
schemaCheckId: schemaCheckID,
lintIssues: sceResult.additionalLintIssues,
schemaCheckSubgraphId,
});
// Then, we need to add the overwritten lint issues
await schemaLintRepo.addSchemaCheckLintIssues({
schemaCheckId: schemaCheckID,
lintIssues: sceLintIssues,
schemaCheckSubgraphId,
});
}
}

// Update the overall schema check with the results
Expand Down
117 changes: 59 additions & 58 deletions controlplane/src/core/webhooks/OrganizationWebhookService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { eq } from 'drizzle-orm';
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import pino from 'pino';
import { v4 } from 'uuid';
import z from 'zod';
import * as z from 'zod';
import { LintSeverity, VCSContext } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb';
import * as schema from '../../db/schema.js';
import { FederatedGraphRepository } from '../repositories/FederatedGraphRepository.js';
Expand All @@ -20,7 +20,6 @@ import {
NamespaceDTO,
SchemaGraphPruningIssues,
SchemaLintIssues,
SubgraphDTO,
} from '../../types/index.js';
import { ComposedFederatedGraph } from '../composition/composer.js';
import { GetDiffBetweenGraphsSuccess } from '../composition/schemaCheck.js';
Expand All @@ -33,18 +32,21 @@ import { makeWebhookRequest } from './utils.js';
const subgraphCheckExtensionSchema = z.object({
errors: z.array(z.string()).optional(),
lintIssues: z
.array(
z.object({
lintRuleType: z.string().trim(),
severity: z.nativeEnum(LintSeverity),
message: z.string().trim(),
issueLocation: z.object({
line: z.number().int().positive(),
column: z.number().int().positive(),
endLine: z.number().int().positive().optional(),
endColumn: z.number().int().positive().optional(),
.record(
z.string(),
z.array(
z.object({
lintRuleType: z.string().trim(),
severity: z.nativeEnum(LintSeverity),
message: z.string().trim(),
issueLocation: z.object({
line: z.number().int().positive(),
column: z.number().int().positive(),
endLine: z.number().int().positive().optional(),
endColumn: z.number().int().positive().optional(),
}),
}),
}),
),
)
.optional(),
});
Expand Down Expand Up @@ -585,19 +587,24 @@ export class OrganizationWebhookService {
organization: { id: string; slug: string };
namespace: NamespaceDTO;
vcsContext: VCSContext | undefined;
subgraph: SubgraphDTO | undefined;
newSchemaSDL: string;
isDeleted: boolean;
subgraphs: {
id: string;
name: string;
labels: Label[];
schemaChanges: GetDiffBetweenGraphsSuccess;
lintIssues: SchemaLintIssues;
pruneIssues: SchemaGraphPruningIssues;
schemaSDL: string;
newSchemaSDL: string;
isDeleted: boolean;
}[];
affectedGraphs: FederatedGraphDTO[];
composedGraphs: ComposedFederatedGraph[];
schemaChanges: GetDiffBetweenGraphsSuccess;
lintIssues: SchemaLintIssues;
pruneIssues: SchemaGraphPruningIssues;
inspectedOperations: InspectorOperationResult[];
}): Promise<
| {
deliveryInfo: WebhookDeliveryInfo;
additionalLintIssues: LintIssueResult[];
lintIssuesBySubgraph: Map<string, LintIssueResult[]>;
}
| undefined
> {
Expand Down Expand Up @@ -629,15 +636,17 @@ export class OrganizationWebhookService {

// Compose the contents of the file that we'll provide to the webhook
const fileContent: Record<string, unknown> = {};
if (input.subgraph) {
fileContent.subgraphs = [
{
id: input.subgraph.id,
name: input.subgraph.name,
newComposedSdl: sceConfig.includeLintingIssues ? input.newSchemaSDL : undefined,
oldComposedSdl: sceConfig.includeLintingIssues ? input.subgraph.schemaSDL : undefined,
},
];
if (input.subgraphs.length > 0) {
fileContent.subgraphs = input.subgraphs.map((subgraph) => ({
id: subgraph.id,
name: subgraph.name,
labels: subgraph.labels,
newComposedSdl: sceConfig.includeComposedSdl ? subgraph.newSchemaSDL : undefined,
oldComposedSdl: sceConfig.includeComposedSdl ? subgraph.schemaSDL : undefined,
lintIssues: sceConfig.includeLintingIssues ? subgraph.lintIssues : undefined,
pruningIssues: sceConfig.includePruningIssues ? subgraph.pruneIssues : undefined,
schemaChanges: sceConfig.includeSchemaChanges ? subgraph.schemaChanges : undefined,
}));
}

if (sceConfig.includeComposedSdl) {
Expand All @@ -649,18 +658,6 @@ export class OrganizationWebhookService {
}));
}

if (sceConfig.includeLintingIssues) {
fileContent.lintIssues = [...input.lintIssues.warnings, ...input.lintIssues.errors];
}

if (sceConfig.includePruningIssues) {
fileContent.pruningIssues = [...input.pruneIssues.warnings, ...input.pruneIssues.errors];
}

if (sceConfig.includeSchemaChanges) {
fileContent.schemaChanges = input.schemaChanges.changes;
}

if (sceConfig.includeAffectedOperations) {
fileContent.affectedOperations = input.inspectedOperations;
}
Expand All @@ -669,11 +666,11 @@ export class OrganizationWebhookService {
let url: string | undefined;
if (Object.keys(fileContent).length > 0) {
// Only upload the file if at least one option is enabled
const blobKey = `/${input.organization.id}/subgraph_checks/${v4()}.json`;
const blobKey = `${input.organization.id}/subgraph_checks/${v4()}.json`;
const blobContent = JSON.stringify(fileContent);
await input.blobStorage.putObject({
key: blobKey,
contentType: 'application/json',
contentType: 'application/json; charset=utf-8',
body: Buffer.from(blobContent, 'utf8'),
});

Expand All @@ -686,14 +683,14 @@ export class OrganizationWebhookService {
},
});

url = `${input.admissionConfig.cdnBaseUrl}${blobKey}?token=${token}`;
url = `${input.admissionConfig.cdnBaseUrl}/${blobKey}?token=${token}`;
}

// Compose the webhook payload
const payload: Record<string, unknown> = {
actorId: input.actorId,
checkId: input.schemaCheckID,
labels: input.subgraph ? undefined : input.labels,
labels: input.labels,
organization: input.organization,
namespace: { id: input.namespace.id, name: input.namespace.name },
vcsContext: input.vcsContext,
Expand All @@ -704,14 +701,13 @@ export class OrganizationWebhookService {
url,
};

if (input.subgraph) {
payload.subgraphs = [
{
id: input.subgraph.id,
name: input.subgraph.name,
isDeleted: input.isDeleted,
},
];
if (input.subgraphs.length > 0) {
payload.subgraphs = input.subgraphs.map((sg) => ({
id: sg.id,
name: sg.name,
labels: sg.labels,
isDeleted: sg.isDeleted,
}));
}

// Deliver the webhook
Expand All @@ -733,13 +729,13 @@ export class OrganizationWebhookService {
this.logger,
);

let lintIssuesBySubgraph: Map<string, LintIssueResult[]> = new Map<string, LintIssueResult[]>();
if (!response?.data) {
return { deliveryInfo, additionalLintIssues: [] };
return { deliveryInfo, lintIssuesBySubgraph };
}

// Validate the response and maybe overwrite the error message based on it
let overwriteErrorMessage = false;
let additionalLintIssues: LintIssueResult[] = [];

if (response?.status !== 200 && response?.status !== 204) {
// We expect the response status to be either 200 (OK) or 204 (No Content)
Expand All @@ -755,8 +751,13 @@ export class OrganizationWebhookService {
deliveryInfo.errorMessage = parsedResponse.data.errors.filter(Boolean).join('\n');
}

if (Array.isArray(parsedResponse.data.lintIssues)) {
additionalLintIssues = parsedResponse.data.lintIssues as LintIssueResult[];
if (typeof parsedResponse.data.lintIssues === 'object') {
try {
const lintIssuesRecord = parsedResponse.data.lintIssues as Record<string, LintIssueResult[]>;
lintIssuesBySubgraph = new Map(Object.entries(lintIssuesRecord));
} catch {
// ignore
}
}
} else {
overwriteErrorMessage = true;
Expand All @@ -775,6 +776,6 @@ export class OrganizationWebhookService {
}

// We are done!
return { deliveryInfo, additionalLintIssues };
return { deliveryInfo, lintIssuesBySubgraph };
}
}
Loading
Loading