export function updateContract()

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,
    };
  });
}