export function updateSubgraph()

in controlplane/src/core/bufservices/subgraph/updateSubgraph.ts [21:269]


export function updateSubgraph(
  opts: RouterOptions,
  req: UpdateSubgraphRequest,
  ctx: HandlerContext,
): Promise<PlainMessage<UpdateSubgraphResponse>> {
  let logger = getLogger(ctx, opts.logger);

  return handleError<PlainMessage<UpdateSubgraphResponse>>(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 auditLogRepo = new AuditLogRepository(opts.db);
    const orgWebhooks = new OrganizationWebhookService(
      opts.db,
      authContext.organizationId,
      opts.logger,
      opts.billingDefaultPlanId,
    );

    req.namespace = req.namespace || DefaultNamespace;

    if (!authContext.hasWriteAccess) {
      return {
        response: {
          code: EnumStatusCode.ERR,
          details: `The user doesnt have the permissions to perform this operation`,
        },
        compositionErrors: [],
        deploymentErrors: [],
        compositionWarnings: [],
      };
    }

    const subgraph = await subgraphRepo.byName(req.name, req.namespace);
    if (!subgraph) {
      return {
        response: {
          code: EnumStatusCode.ERR_NOT_FOUND,
          details: `The subgraph "${req.name}" was not found.`,
        },
        compositionErrors: [],
        deploymentErrors: [],
        compositionWarnings: [],
      };
    }

    if (subgraph.isFeatureSubgraph && ((req.labels && req.labels.length > 0) || req.unsetLabels)) {
      return {
        response: {
          code: EnumStatusCode.ERR,
          details:
            `Feature subgraph labels cannot be changed directly.` +
            ` Feature subgraph labels are determined by the feature flag they compose.`,
        },
        compositionErrors: [],
        deploymentErrors: [],
        compositionWarnings: [],
      };
    }

    if (!isValidLabels(req.labels)) {
      return {
        response: {
          code: EnumStatusCode.ERR_INVALID_LABELS,
          details: `One ore more labels were found to be invalid`,
        },
        compositionErrors: [],
        deploymentErrors: [],
        compositionWarnings: [],
      };
    }

    // If the graph is an EDG, it should never define a routing URL nor a subscription URL
    if (subgraph.isEventDrivenGraph) {
      if (req.routingUrl !== undefined) {
        return {
          response: {
            code: EnumStatusCode.ERR,
            details: `Event-Driven Graphs must not define a routing URL`,
          },
          compositionErrors: [],
          deploymentErrors: [],
          compositionWarnings: [],
        };
      }
      if (req.subscriptionUrl !== undefined) {
        return {
          response: {
            code: EnumStatusCode.ERR,
            details: `Event-Driven Graphs must not define a subscription URL`,
          },
          compositionErrors: [],
          deploymentErrors: [],
          compositionWarnings: [],
        };
      }
      if (req.subscriptionProtocol !== undefined) {
        return {
          response: {
            code: EnumStatusCode.ERR,
            details: `Event-Driven Graphs must not define a subscription protocol`,
          },
          compositionErrors: [],
          deploymentErrors: [],
          compositionWarnings: [],
        };
      }
      if (req.websocketSubprotocol !== undefined) {
        return {
          response: {
            code: EnumStatusCode.ERR,
            details: `Event-Driven Graphs must not define a websocket subprotocol`,
          },
          compositionErrors: [],
          deploymentErrors: [],
          compositionWarnings: [],
        };
      }
    } else {
      // Routing URL should never be an empty string, so check explicitly for undefined
      if (req.routingUrl !== undefined && !isValidUrl(req.routingUrl)) {
        return {
          response: {
            code: EnumStatusCode.ERR,
            details: `Routing URL "${req.routingUrl}" is not a valid URL.`,
          },
          compositionErrors: [],
          deploymentErrors: [],
          compositionWarnings: [],
        };
      }
      // When un-setting the url, the url can be an empty string
      if (req.subscriptionUrl && !isValidUrl(req.subscriptionUrl)) {
        return {
          response: {
            code: EnumStatusCode.ERR,
            details: `Subscription URL "${req.subscriptionUrl}" is not a valid URL.`,
          },
          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,
    });

    const { compositionErrors, updatedFederatedGraphs, deploymentErrors, compositionWarnings } =
      await subgraphRepo.update(
        {
          targetId: subgraph.targetId,
          labels: req.labels,
          unsetLabels: req.unsetLabels ?? false,
          subscriptionUrl: req.subscriptionUrl,
          routingUrl: req.routingUrl,
          subscriptionProtocol:
            req.subscriptionProtocol === undefined ? undefined : formatSubscriptionProtocol(req.subscriptionProtocol),
          websocketSubprotocol:
            req.websocketSubprotocol === undefined ? undefined : formatWebsocketSubprotocol(req.websocketSubprotocol),
          updatedBy: authContext.userId,
          readme: req.readme,
          namespaceId: subgraph.namespaceId,
        },
        opts.blobStorage,
        {
          cdnBaseUrl: opts.cdnBaseUrl,
          webhookJWTSecret: opts.admissionWebhookJWTSecret,
        },
        opts.chClient!,
      );

    await auditLogRepo.addAuditLog({
      organizationId: authContext.organizationId,
      auditAction: subgraph.isFeatureSubgraph ? 'feature_subgraph.updated' : 'subgraph.updated',
      action: 'updated',
      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,
    });

    for (const graph of updatedFederatedGraphs) {
      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,
        compositionWarnings,
        deploymentErrors: [],
      };
    }

    if (deploymentErrors.length > 0) {
      return {
        response: {
          code: EnumStatusCode.ERR_DEPLOYMENT_FAILED,
        },
        compositionErrors: [],
        compositionWarnings,
        deploymentErrors,
      };
    }

    return {
      response: {
        code: EnumStatusCode.OK,
      },
      compositionErrors: [],
      deploymentErrors: [],
      compositionWarnings,
    };
  });
}