in controlplane/src/core/bufservices/subgraph/deleteFederatedSubgraph.ts [26:294]
export function deleteFederatedSubgraph(
opts: RouterOptions,
req: DeleteFederatedSubgraphRequest,
ctx: HandlerContext,
): Promise<PlainMessage<DeleteFederatedSubgraphResponse>> {
let logger = getLogger(ctx, opts.logger);
return handleError<PlainMessage<DeleteFederatedSubgraphResponse>>(ctx, logger, async () => {
const authContext = await opts.authenticator.authenticate(ctx.requestHeader);
logger = enrichLogger(ctx, logger, authContext);
const subgraphRepo = new SubgraphRepository(logger, opts.db, authContext.organizationId);
const proposalRepo = new ProposalRepository(opts.db);
const namespaceRepo = new NamespaceRepository(opts.db, authContext.organizationId);
const fedGraphRepo = new FederatedGraphRepository(logger, opts.db, authContext.organizationId);
const orgWebhooks = new OrganizationWebhookService(
opts.db,
authContext.organizationId,
opts.logger,
opts.billingDefaultPlanId,
);
req.namespace = req.namespace || DefaultNamespace;
if (authContext.organizationDeactivated) {
throw new UnauthorizedError();
}
const namespace = await namespaceRepo.byName(req.namespace);
if (!namespace) {
return {
response: {
code: EnumStatusCode.ERR_NOT_FOUND,
details: `Could not find namespace ${req.namespace}`,
},
compositionErrors: [],
deploymentErrors: [],
compositionWarnings: [],
};
}
const subgraph = await subgraphRepo.byName(req.subgraphName, req.namespace);
if (!subgraph) {
return {
response: {
code: EnumStatusCode.ERR_NOT_FOUND,
details: `The subgraph "${req.subgraphName}" was not found.`,
},
compositionErrors: [],
deploymentErrors: [],
compositionWarnings: [],
};
}
// check if the user is authorized to perform the action
await opts.authorizer.authorize({
db: opts.db,
graph: {
targetId: subgraph.targetId,
targetType: 'subgraph',
},
headers: ctx.requestHeader,
authContext,
isDeleteOperation: true,
});
let proposalMatchMessage: string | undefined;
let matchedEntity:
| {
proposalId: string;
proposalSubgraphId: string;
}
| undefined;
if (namespace.enableProposals) {
const federatedGraphs = await fedGraphRepo.bySubgraphLabels({
labels: subgraph.labels,
namespaceId: namespace.id,
});
const proposalConfig = await proposalRepo.getProposalConfig({ namespaceId: namespace.id });
if (proposalConfig) {
const match = await proposalRepo.matchSchemaWithProposal({
subgraphName: subgraph.name,
namespaceId: namespace.id,
schemaSDL: '',
routerCompatibilityVersion: getFederatedGraphRouterCompatibilityVersion(federatedGraphs),
isDeleted: true,
});
if (!match) {
if (proposalConfig.publishSeverityLevel === 'warn') {
proposalMatchMessage = `The subgraph ${subgraph.name} is not proposed to be deleted in any of the approved proposals.`;
} else {
return {
response: {
code: EnumStatusCode.ERR_SCHEMA_MISMATCH_WITH_APPROVED_PROPOSAL,
details: `The subgraph ${subgraph.name} is not proposed to be deleted in any of the approved proposals.`,
},
compositionErrors: [],
deploymentErrors: [],
compositionWarnings: [],
proposalMatchMessage: `The subgraph ${subgraph.name} is not proposed to be deleted in any of the approved proposals.`,
};
}
}
matchedEntity = match;
}
}
const { affectedFederatedGraphs, compositionErrors, deploymentErrors, compositionWarnings } =
await opts.db.transaction(async (tx) => {
const fedGraphRepo = new FederatedGraphRepository(logger, tx, authContext.organizationId);
const subgraphRepo = new SubgraphRepository(logger, tx, authContext.organizationId);
const featureFlagRepo = new FeatureFlagRepository(logger, tx, authContext.organizationId);
const auditLogRepo = new AuditLogRepository(tx);
let labels = subgraph.labels;
if (subgraph.isFeatureSubgraph) {
const baseSubgraph = await featureFlagRepo.getBaseSubgraphByFeatureSubgraphId({ id: subgraph.id });
if (baseSubgraph) {
labels = baseSubgraph.labels;
}
} else {
await featureFlagRepo.deleteFeatureSubgraphsByBaseSubgraphId({
subgraphId: subgraph.id,
namespaceId: subgraph.namespaceId,
});
}
// Collect all federated graphs that used this subgraph before deleting subgraph to include them in the composition
const affectedFederatedGraphs = await fedGraphRepo.bySubgraphLabels({
labels,
namespaceId: subgraph.namespaceId,
excludeContracts: true,
});
// Delete the subgraph
await subgraphRepo.delete(subgraph.targetId);
await auditLogRepo.addAuditLog({
organizationId: authContext.organizationId,
organizationSlug: authContext.organizationSlug,
auditAction: subgraph.isFeatureSubgraph ? 'feature_subgraph.deleted' : 'subgraph.deleted',
action: 'deleted',
actorId: authContext.userId,
auditableType: subgraph.isFeatureSubgraph ? 'feature_subgraph' : 'subgraph',
auditableDisplayName: subgraph.name,
actorDisplayName: authContext.userDisplayName,
apiKeyName: authContext.apiKeyName,
actorType: authContext.auth === 'api_key' ? 'api_key' : 'user',
targetNamespaceId: subgraph.namespaceId,
targetNamespaceDisplayName: subgraph.namespace,
});
// Recompose and deploy all affected federated graphs and their respective contracts.
// Collects all composition and deployment errors if any.
const { compositionErrors, deploymentErrors, compositionWarnings } = await fedGraphRepo.composeAndDeployGraphs({
actorId: authContext.userId,
admissionConfig: {
webhookJWTSecret: opts.admissionWebhookJWTSecret,
cdnBaseUrl: opts.cdnBaseUrl,
},
blobStorage: opts.blobStorage,
chClient: opts.chClient!,
compositionOptions: newCompositionOptions(req.disableResolvabilityValidation),
federatedGraphs: affectedFederatedGraphs,
});
return { affectedFederatedGraphs, compositionErrors, deploymentErrors, compositionWarnings };
});
for (const affectedFederatedGraph of affectedFederatedGraphs) {
const hasErrors =
compositionErrors.some((error) => error.federatedGraphName === affectedFederatedGraph.name) ||
deploymentErrors.some((error) => error.federatedGraphName === affectedFederatedGraph.name);
orgWebhooks.send(
{
eventName: OrganizationEventName.FEDERATED_GRAPH_SCHEMA_UPDATED,
payload: {
federated_graph: {
id: affectedFederatedGraph.id,
name: affectedFederatedGraph.name,
namespace: affectedFederatedGraph.namespace,
},
organization: {
id: authContext.organizationId,
slug: authContext.organizationSlug,
},
errors: hasErrors,
actor_id: authContext.userId,
},
},
authContext.userId,
);
}
// if this subgraph is part of a proposal, mark the proposal subgraph as published
// and if all proposal subgraphs are published, update the proposal state to PUBLISHED
if (matchedEntity) {
const { allSubgraphsPublished } = await proposalRepo.markProposalSubgraphAsPublished({
proposalSubgraphId: matchedEntity.proposalSubgraphId,
proposalId: matchedEntity.proposalId,
});
if (allSubgraphsPublished) {
const proposal = await proposalRepo.ById(matchedEntity.proposalId);
if (proposal) {
const federatedGraph = await fedGraphRepo.byId(proposal.proposal.federatedGraphId);
if (federatedGraph) {
orgWebhooks.send(
{
eventName: OrganizationEventName.PROPOSAL_STATE_UPDATED,
payload: {
federated_graph: {
id: federatedGraph.id,
name: federatedGraph.name,
namespace: federatedGraph.namespace,
},
organization: {
id: authContext.organizationId,
slug: authContext.organizationSlug,
},
proposal: {
id: proposal.proposal.id,
name: proposal.proposal.name,
namespace: req.namespace,
state: 'PUBLISHED',
},
actor_id: authContext.userId,
},
},
authContext.userId,
);
}
}
}
}
if (compositionErrors.length > 0) {
return {
response: {
code: EnumStatusCode.ERR_SUBGRAPH_COMPOSITION_FAILED,
},
deploymentErrors: [],
compositionErrors,
compositionWarnings,
proposalMatchMessage,
};
}
if (deploymentErrors.length > 0) {
return {
response: {
code: EnumStatusCode.ERR_DEPLOYMENT_FAILED,
},
deploymentErrors,
compositionErrors: [],
compositionWarnings,
proposalMatchMessage,
};
}
return {
response: {
code: EnumStatusCode.OK,
},
deploymentErrors: [],
compositionErrors: [],
compositionWarnings,
proposalMatchMessage,
};
});
}