in controlplane/src/core/bufservices/contract/updateContract.ts [20:243]
export function updateContract(
opts: RouterOptions,
req: UpdateContractRequest,
ctx: HandlerContext,
): Promise<PlainMessage<UpdateContractResponse>> {
let logger = getLogger(ctx, opts.logger);
return handleError<PlainMessage<UpdateContractResponse>>(ctx, logger, async () => {
req.namespace = req.namespace || DefaultNamespace;
const authContext = await opts.authenticator.authenticate(ctx.requestHeader);
logger = enrichLogger(ctx, logger, authContext);
const fedGraphRepo = new FederatedGraphRepository(logger, opts.db, authContext.organizationId);
const contractRepo = new ContractRepository(logger, opts.db, authContext.organizationId);
const auditLogRepo = new AuditLogRepository(opts.db);
const orgWebhooks = new OrganizationWebhookService(
opts.db,
authContext.organizationId,
opts.logger,
opts.billingDefaultPlanId,
);
req.excludeTags = [...new Set(req.excludeTags)];
req.includeTags = [...new Set(req.includeTags)];
if (!authContext.hasWriteAccess) {
return {
response: {
code: EnumStatusCode.ERR,
details: `The user doesn't have the permissions to perform this operation`,
},
compositionErrors: [],
deploymentErrors: [],
compositionWarnings: [],
};
}
if (req.includeTags.length > 0 && req.excludeTags.length > 0) {
return {
response: {
code: EnumStatusCode.ERR,
details:
`The "exclude" and "include" options for tags are currently mutually exclusive.` +
` Both options have been provided, but one of the options must be empty or unset.`,
},
compositionErrors: [],
deploymentErrors: [],
compositionWarnings: [],
};
}
if (!isValidSchemaTags(req.excludeTags)) {
return {
response: {
code: EnumStatusCode.ERR,
details: `Provided exclude tags are invalid`,
},
compositionErrors: [],
deploymentErrors: [],
compositionWarnings: [],
};
}
if (!isValidSchemaTags(req.includeTags)) {
return {
response: {
code: EnumStatusCode.ERR,
details: `Provided include tags are invalid`,
},
compositionErrors: [],
deploymentErrors: [],
compositionWarnings: [],
};
}
const graph = await fedGraphRepo.byName(req.name, req.namespace);
if (!graph) {
return {
response: {
code: EnumStatusCode.ERR_NOT_FOUND,
details: `Could not find contract graph ${req.name} in namespace ${req.namespace}`,
},
compositionErrors: [],
deploymentErrors: [],
compositionWarnings: [],
};
}
if (!graph.contract?.id) {
return {
response: {
code: EnumStatusCode.ERR_NOT_FOUND,
details: `The graph is not a contract`,
},
compositionErrors: [],
deploymentErrors: [],
compositionWarnings: [],
};
}
const updatedContractDetails = await contractRepo.update({
id: graph.contract.id,
excludeTags: req.excludeTags,
includeTags: req.includeTags,
actorId: authContext.userId,
});
await fedGraphRepo.update({
targetId: graph.targetId,
// if the routingUrl is not provided, it will be set to an empty string.
// As the routing url wont be updated in this case.
routingUrl: req.routingUrl || '',
updatedBy: authContext.userId,
readme: req.readme,
blobStorage: opts.blobStorage,
namespaceId: graph.namespaceId,
admissionWebhookURL: req.admissionWebhookUrl,
admissionWebhookSecret: req.admissionWebhookSecret,
admissionConfig: {
cdnBaseUrl: opts.cdnBaseUrl,
jwtSecret: opts.admissionWebhookJWTSecret,
},
labelMatchers: [],
chClient: opts.chClient!,
});
const compositionErrors: PlainMessage<CompositionError>[] = [];
const deploymentErrors: PlainMessage<DeploymentError>[] = [];
const compositionWarnings: PlainMessage<CompositionWarning>[] = [];
const composition = await fedGraphRepo.composeAndDeployGraphs({
federatedGraphs: [
{
...graph,
routingUrl: req.routingUrl || graph.routingUrl,
admissionWebhookURL: req.admissionWebhookUrl || graph.admissionWebhookURL,
admissionWebhookSecret: req.admissionWebhookSecret || graph.admissionWebhookSecret,
readme: req.readme || graph.readme,
contract: {
...graph.contract,
...updatedContractDetails,
},
},
],
actorId: authContext.userId,
blobStorage: opts.blobStorage,
admissionConfig: {
cdnBaseUrl: opts.cdnBaseUrl,
webhookJWTSecret: opts.admissionWebhookJWTSecret,
},
chClient: opts.chClient!,
});
compositionErrors.push(...composition.compositionErrors);
deploymentErrors.push(...composition.deploymentErrors);
compositionWarnings.push(...composition.compositionWarnings);
await auditLogRepo.addAuditLog({
organizationId: authContext.organizationId,
auditAction: 'federated_graph.updated',
action: 'updated',
actorId: authContext.userId,
auditableType: 'federated_graph',
auditableDisplayName: graph.name,
actorDisplayName: authContext.userDisplayName,
apiKeyName: authContext.apiKeyName,
actorType: authContext.auth === 'api_key' ? 'api_key' : 'user',
targetNamespaceId: graph.namespaceId,
targetNamespaceDisplayName: graph.namespace,
});
orgWebhooks.send(
{
eventName: OrganizationEventName.FEDERATED_GRAPH_SCHEMA_UPDATED,
payload: {
federated_graph: {
id: graph.id,
name: graph.name,
namespace: graph.namespace,
},
organization: {
id: authContext.organizationId,
slug: authContext.organizationSlug,
},
errors: compositionErrors.length > 0 || deploymentErrors.length > 0,
actor_id: authContext.userId,
},
},
authContext.userId,
);
if (compositionErrors.length > 0) {
return {
response: {
code: EnumStatusCode.ERR_SUBGRAPH_COMPOSITION_FAILED,
},
compositionErrors,
deploymentErrors: [],
compositionWarnings,
};
}
if (deploymentErrors.length > 0) {
return {
response: {
code: EnumStatusCode.ERR_DEPLOYMENT_FAILED,
},
compositionErrors: [],
deploymentErrors,
compositionWarnings,
};
}
return {
response: {
code: EnumStatusCode.OK,
},
deploymentErrors,
compositionErrors,
compositionWarnings,
};
});
}