controlplane/src/core/bufservices/feature-flag/deleteFeatureFlag.ts (175 lines of code) (raw):

import { PlainMessage } from '@bufbuild/protobuf'; import { HandlerContext } from '@connectrpc/connect'; import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; import { OrganizationEventName } from '@wundergraph/cosmo-connect/dist/notifications/events_pb'; import { CompositionError, CompositionWarning, DeleteFeatureFlagRequest, DeleteFeatureFlagResponse, DeploymentError, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FeatureFlagRepository } from '../../repositories/FeatureFlagRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; import { DefaultNamespace, NamespaceRepository } from '../../repositories/NamespaceRepository.js'; import type { RouterOptions } from '../../routes.js'; import { enrichLogger, getLogger, handleError } from '../../util.js'; import { OrganizationWebhookService } from '../../webhooks/OrganizationWebhookService.js'; export function deleteFeatureFlag( opts: RouterOptions, req: DeleteFeatureFlagRequest, ctx: HandlerContext, ): Promise<PlainMessage<DeleteFeatureFlagResponse>> { let logger = getLogger(ctx, opts.logger); return handleError<PlainMessage<DeleteFeatureFlagResponse>>(ctx, logger, async () => { const authContext = await opts.authenticator.authenticate(ctx.requestHeader); logger = enrichLogger(ctx, logger, authContext); const featureFlagRepo = new FeatureFlagRepository(logger, opts.db, authContext.organizationId); const namespaceRepo = new NamespaceRepository(opts.db, authContext.organizationId); 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 does not have the permissions to perform this operation.`, }, compositionErrors: [], deploymentErrors: [], compositionWarnings: [], }; } 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 featureFlag = await featureFlagRepo.getFeatureFlagByName({ featureFlagName: req.name, namespaceId: namespace.id, }); if (!featureFlag) { return { response: { code: EnumStatusCode.ERR_NOT_FOUND, details: `The feature flag "${req.name}" was not found.`, }, compositionErrors: [], deploymentErrors: [], compositionWarnings: [], }; } // Collect the federated graph DTOs that have the feature flag enabled because they will be re-composed const federatedGraphs = await featureFlagRepo.getFederatedGraphsByFeatureFlag({ featureFlagId: featureFlag.id, namespaceId: namespace.id, // if deleting when already disabled, there are no compositions to be done. excludeDisabled: true, }); /* Check that the user is authorized to delete the feature flag * The user must have authorization for each related federated graph * */ for (const federatedGraph of federatedGraphs) { // check if the user is authorized to perform the action await opts.authorizer.authorize({ db: opts.db, graph: { targetId: federatedGraph.targetId, targetType: 'federatedGraph', }, headers: ctx.requestHeader, authContext, }); } const compositionErrors: PlainMessage<CompositionError>[] = []; const deploymentErrors: PlainMessage<DeploymentError>[] = []; const compositionWarnings: PlainMessage<CompositionWarning>[] = []; await opts.db.transaction(async (tx) => { const fedGraphRepo = new FederatedGraphRepository(logger, tx, authContext.organizationId); const auditLogRepo = new AuditLogRepository(tx); await featureFlagRepo.delete(featureFlag.id); await auditLogRepo.addAuditLog({ organizationId: authContext.organizationId, auditAction: 'feature_flag.deleted', action: 'deleted', actorId: authContext.userId, auditableType: 'feature_flag', auditableDisplayName: featureFlag.name, apiKeyName: authContext.apiKeyName, actorDisplayName: authContext.userDisplayName, actorType: authContext.auth === 'api_key' ? 'api_key' : 'user', targetNamespaceId: namespace.id, targetNamespaceDisplayName: namespace.name, }); const composition = await fedGraphRepo.composeAndDeployGraphs({ federatedGraphs, 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); }); for (const graph of federatedGraphs) { 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, }, compositionErrors, deploymentErrors, compositionWarnings, }; }); }