From 0849f41f3a055f486935b4266fce5b4e50bc2a3b Mon Sep 17 00:00:00 2001 From: Alessandro Pagnin Date: Fri, 6 Mar 2026 10:22:05 +0100 Subject: [PATCH 01/12] feat: add an organization feature to allow to ignore checks on @external fields --- cli/src/commands/router/commands/compose.ts | 8 ++++++-- .../core/bufservices/contract/createContract.ts | 7 ++++++- .../core/bufservices/contract/updateContract.ts | 12 ++++++++++-- .../bufservices/feature-flag/createFeatureFlag.ts | 7 ++++++- .../bufservices/feature-flag/deleteFeatureFlag.ts | 10 +++++++++- .../bufservices/feature-flag/enableFeatureFlag.ts | 9 ++++++++- .../bufservices/feature-flag/updateFeatureFlag.ts | 10 +++++++++- .../federated-graph/checkFederatedGraph.ts | 10 +++++++++- .../federated-graph/createFederatedGraph.ts | 8 +++++++- .../federated-graph/migrateFromApollo.ts | 7 +++++++ .../federated-graph/updateFederatedGraph.ts | 10 +++++++++- .../graph/setGraphRouterCompatibilityVersion.ts | 10 +++++++++- .../core/bufservices/monograph/publishMonograph.ts | 14 ++++++++++++-- .../core/bufservices/monograph/updateMonograph.ts | 13 +++++++++++++ .../bufservices/subgraph/checkSubgraphSchema.ts | 11 ++++++++++- .../subgraph/deleteFederatedSubgraph.ts | 10 +++++++++- .../core/bufservices/subgraph/fixSubgraphSchema.ts | 10 +++++++++- .../src/core/bufservices/subgraph/moveSubgraph.ts | 10 +++++++++- .../subgraph/publishFederatedSubgraph.ts | 12 ++++++++++-- .../core/bufservices/subgraph/updateSubgraph.ts | 10 +++++++++- .../core/repositories/OrganizationRepository.ts | 1 + .../src/core/repositories/SchemaCheckRepository.ts | 13 ++++++++++++- .../src/core/repositories/SubgraphRepository.ts | 4 +++- controlplane/src/types/index.ts | 1 + 24 files changed, 193 insertions(+), 24 deletions(-) diff --git a/cli/src/commands/router/commands/compose.ts b/cli/src/commands/router/commands/compose.ts index 0feda11d9f..8979c1f0f1 100644 --- a/cli/src/commands/router/commands/compose.ts +++ b/cli/src/commands/router/commands/compose.ts @@ -171,6 +171,10 @@ export default (opts: BaseCommandOptions) => { '--disable-resolvability-validation', 'This flag will disable the validation for whether all nodes of the federated graph are resolvable. Do NOT use unless troubleshooting.', ); + command.option( + '--ignore-external-keys', + 'This flag ignores resolvability checks on external fields during composition.', + ); command.action(async (options) => { const inputFile = resolve(options.input); @@ -208,7 +212,7 @@ export default (opts: BaseCommandOptions) => { }; }), { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys: options.ignoreExternalKeys, disableResolvabilityValidation: options.disableResolvabilityValidation, }, ); @@ -591,7 +595,7 @@ async function buildFeatureFlagsConfig( definitions: parse(s.sdl), })), { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys: options.ignoreExternalKeys, disableResolvabilityValidation: options.disableResolvabilityValidation, }, ); diff --git a/controlplane/src/core/bufservices/contract/createContract.ts b/controlplane/src/core/bufservices/contract/createContract.ts index 59afedf93d..3f9ef8975e 100644 --- a/controlplane/src/core/bufservices/contract/createContract.ts +++ b/controlplane/src/core/bufservices/contract/createContract.ts @@ -104,6 +104,11 @@ export function createContract( organizationId: authContext.organizationId, featureId: 'federated-graphs', }); + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const limit = feature?.limit === -1 ? undefined : feature?.limit; @@ -198,7 +203,7 @@ export function createContract( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs: [{ ...contractGraph, contract }], diff --git a/controlplane/src/core/bufservices/contract/updateContract.ts b/controlplane/src/core/bufservices/contract/updateContract.ts index 7cfb751a3f..219e2a19fe 100644 --- a/controlplane/src/core/bufservices/contract/updateContract.ts +++ b/controlplane/src/core/bufservices/contract/updateContract.ts @@ -13,6 +13,7 @@ import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { ContractRepository } from '../../repositories/ContractRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; import { DefaultNamespace } from '../../repositories/NamespaceRepository.js'; +import { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; import type { RouterOptions } from '../../routes.js'; import { enrichLogger, getLogger, handleError, isValidSchemaTags } from '../../util.js'; import { OrganizationWebhookService } from '../../webhooks/OrganizationWebhookService.js'; @@ -33,6 +34,7 @@ export function updateContract( const fedGraphRepo = new FederatedGraphRepository(logger, opts.db, authContext.organizationId); const contractRepo = new ContractRepository(logger, opts.db, authContext.organizationId); + const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId); const auditLogRepo = new AuditLogRepository(opts.db); const orgWebhooks = new OrganizationWebhookService( opts.db, @@ -116,6 +118,12 @@ export function updateContract( }; } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; + const updatedContractDetails = await contractRepo.update({ id: graph.contract.id, excludeTags: req.excludeTags, @@ -141,7 +149,7 @@ export function updateContract( labelMatchers: [], chClient: opts.chClient!, compositionOptions: { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, }); @@ -159,7 +167,7 @@ export function updateContract( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs: [ diff --git a/controlplane/src/core/bufservices/feature-flag/createFeatureFlag.ts b/controlplane/src/core/bufservices/feature-flag/createFeatureFlag.ts index 167243e2d8..6fbca462a2 100644 --- a/controlplane/src/core/bufservices/feature-flag/createFeatureFlag.ts +++ b/controlplane/src/core/bufservices/feature-flag/createFeatureFlag.ts @@ -189,6 +189,11 @@ export function createFeatureFlag( namespaceId: namespace.id, excludeDisabled: true, }); + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const compositionErrors: PlainMessage[] = []; const deploymentErrors: PlainMessage[] = []; @@ -206,7 +211,7 @@ export function createFeatureFlag( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs, diff --git a/controlplane/src/core/bufservices/feature-flag/deleteFeatureFlag.ts b/controlplane/src/core/bufservices/feature-flag/deleteFeatureFlag.ts index 9cfb20e079..6cbf8ec038 100644 --- a/controlplane/src/core/bufservices/feature-flag/deleteFeatureFlag.ts +++ b/controlplane/src/core/bufservices/feature-flag/deleteFeatureFlag.ts @@ -13,6 +13,7 @@ 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 { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; import type { RouterOptions } from '../../routes.js'; import { enrichLogger, getLogger, handleError } from '../../util.js'; import { OrganizationWebhookService } from '../../webhooks/OrganizationWebhookService.js'; @@ -31,6 +32,7 @@ export function deleteFeatureFlag( const featureFlagRepo = new FeatureFlagRepository(logger, opts.db, authContext.organizationId); const namespaceRepo = new NamespaceRepository(opts.db, authContext.organizationId); + const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId); const orgWebhooks = new OrganizationWebhookService( opts.db, authContext.organizationId, @@ -102,6 +104,12 @@ export function deleteFeatureFlag( }); } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; + const compositionErrors: PlainMessage[] = []; const deploymentErrors: PlainMessage[] = []; const compositionWarnings: PlainMessage[] = []; @@ -136,7 +144,7 @@ export function deleteFeatureFlag( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs, diff --git a/controlplane/src/core/bufservices/feature-flag/enableFeatureFlag.ts b/controlplane/src/core/bufservices/feature-flag/enableFeatureFlag.ts index 9aea579dd0..5784345e98 100644 --- a/controlplane/src/core/bufservices/feature-flag/enableFeatureFlag.ts +++ b/controlplane/src/core/bufservices/feature-flag/enableFeatureFlag.ts @@ -13,6 +13,7 @@ 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 { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; import type { RouterOptions } from '../../routes.js'; import { enrichLogger, getLogger, handleError } from '../../util.js'; import { OrganizationWebhookService } from '../../webhooks/OrganizationWebhookService.js'; @@ -31,6 +32,7 @@ export function enableFeatureFlag( const featureFlagRepo = new FeatureFlagRepository(logger, opts.db, authContext.organizationId); const namespaceRepo = new NamespaceRepository(opts.db, authContext.organizationId); + const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId); const auditLogRepo = new AuditLogRepository(opts.db); const orgWebhooks = new OrganizationWebhookService( opts.db, @@ -103,6 +105,11 @@ export function enableFeatureFlag( // fetch the federated graphs based on the state that has just been set for the feature flag above excludeDisabled: req.enabled, }); + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const compositionErrors: PlainMessage[] = []; const deploymentErrors: PlainMessage[] = []; @@ -120,7 +127,7 @@ export function enableFeatureFlag( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs, diff --git a/controlplane/src/core/bufservices/feature-flag/updateFeatureFlag.ts b/controlplane/src/core/bufservices/feature-flag/updateFeatureFlag.ts index ac8bf5f972..a930741810 100644 --- a/controlplane/src/core/bufservices/feature-flag/updateFeatureFlag.ts +++ b/controlplane/src/core/bufservices/feature-flag/updateFeatureFlag.ts @@ -14,6 +14,7 @@ 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 { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; import type { RouterOptions } from '../../routes.js'; import { enrichLogger, getLogger, handleError, isValidLabels } from '../../util.js'; import { OrganizationWebhookService } from '../../webhooks/OrganizationWebhookService.js'; @@ -32,6 +33,7 @@ export function updateFeatureFlag( const featureFlagRepo = new FeatureFlagRepository(logger, opts.db, authContext.organizationId); const namespaceRepo = new NamespaceRepository(opts.db, authContext.organizationId); + const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId); const orgWebhooks = new OrganizationWebhookService( opts.db, authContext.organizationId, @@ -159,6 +161,12 @@ export function updateFeatureFlag( allFederatedGraphsToCompose.push(newFederatedGraph); } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; + const compositionErrors: PlainMessage[] = []; const deploymentErrors: PlainMessage[] = []; const compositionWarnings: PlainMessage[] = []; @@ -175,7 +183,7 @@ export function updateFeatureFlag( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs: allFederatedGraphsToCompose, diff --git a/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts b/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts index 4ed48d8bbf..5368788253 100644 --- a/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts +++ b/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts @@ -12,6 +12,7 @@ import { parse } from 'graphql'; import { composeSubgraphs } from '../../composition/composition.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; import { DefaultNamespace } from '../../repositories/NamespaceRepository.js'; +import { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; import { SubgraphRepository } from '../../repositories/SubgraphRepository.js'; import type { RouterOptions } from '../../routes.js'; import { @@ -38,6 +39,7 @@ export function checkFederatedGraph( const fedGraphRepo = new FederatedGraphRepository(logger, opts.db, authContext.organizationId); const subgraphRepo = new SubgraphRepository(logger, opts.db, authContext.organizationId); + const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId); req.namespace = req.namespace || DefaultNamespace; @@ -100,6 +102,12 @@ export function checkFederatedGraph( type: convertToSubgraphType(s.type), })); + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; + const result = composeSubgraphs( subgraphsUsedForComposition.map((s) => ({ id: s.id, @@ -109,7 +117,7 @@ export function checkFederatedGraph( })), federatedGraph.routerCompatibilityVersion, { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, ); diff --git a/controlplane/src/core/bufservices/federated-graph/createFederatedGraph.ts b/controlplane/src/core/bufservices/federated-graph/createFederatedGraph.ts index 596aa61c71..be6a8b1da3 100644 --- a/controlplane/src/core/bufservices/federated-graph/createFederatedGraph.ts +++ b/controlplane/src/core/bufservices/federated-graph/createFederatedGraph.ts @@ -210,6 +210,12 @@ export function createFederatedGraph( }; } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; + const compositionErrors: PlainMessage[] = []; const deploymentErrors: PlainMessage[] = []; const compositionWarnings: PlainMessage[] = []; @@ -226,7 +232,7 @@ export function createFederatedGraph( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs: [federatedGraph], diff --git a/controlplane/src/core/bufservices/federated-graph/migrateFromApollo.ts b/controlplane/src/core/bufservices/federated-graph/migrateFromApollo.ts index d9089182af..6ec2242c8a 100644 --- a/controlplane/src/core/bufservices/federated-graph/migrateFromApollo.ts +++ b/controlplane/src/core/bufservices/federated-graph/migrateFromApollo.ts @@ -131,6 +131,12 @@ export function migrateFromApollo( } } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; + await opts.db.transaction(async (tx) => { const fedGraphRepo = new FederatedGraphRepository(logger, tx, authContext.organizationId); @@ -157,6 +163,7 @@ export function migrateFromApollo( }, chClient: opts.chClient!, compositionOptions: { + ignoreExternalKeys, disableResolvabilityValidation: true, }, }); diff --git a/controlplane/src/core/bufservices/federated-graph/updateFederatedGraph.ts b/controlplane/src/core/bufservices/federated-graph/updateFederatedGraph.ts index c94c6d255b..97396f43a3 100644 --- a/controlplane/src/core/bufservices/federated-graph/updateFederatedGraph.ts +++ b/controlplane/src/core/bufservices/federated-graph/updateFederatedGraph.ts @@ -13,6 +13,7 @@ import { isValidUrl } from '@wundergraph/cosmo-shared'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; import { DefaultNamespace } from '../../repositories/NamespaceRepository.js'; +import { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; import type { RouterOptions } from '../../routes.js'; import { enrichLogger, getLogger, handleError, isValidLabelMatchers } from '../../util.js'; import { OrganizationWebhookService } from '../../webhooks/OrganizationWebhookService.js'; @@ -30,6 +31,7 @@ export function updateFederatedGraph( logger = enrichLogger(ctx, logger, authContext); const fedGraphRepo = new FederatedGraphRepository(logger, opts.db, authContext.organizationId); + const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId); const auditLogRepo = new AuditLogRepository(opts.db); const orgWebhooks = new OrganizationWebhookService( opts.db, @@ -106,6 +108,12 @@ export function updateFederatedGraph( }; } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; + const deploymentErrors: PlainMessage[] = []; let compositionErrors: PlainMessage[] = []; const compositionWarnings: PlainMessage[] = []; @@ -120,7 +128,7 @@ export function updateFederatedGraph( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, labelMatchers: req.labelMatchers, diff --git a/controlplane/src/core/bufservices/graph/setGraphRouterCompatibilityVersion.ts b/controlplane/src/core/bufservices/graph/setGraphRouterCompatibilityVersion.ts index 466cfecce9..3bb2083a20 100644 --- a/controlplane/src/core/bufservices/graph/setGraphRouterCompatibilityVersion.ts +++ b/controlplane/src/core/bufservices/graph/setGraphRouterCompatibilityVersion.ts @@ -8,6 +8,7 @@ import { import { ROUTER_COMPATIBILITY_VERSIONS, SupportedRouterCompatibilityVersion } from '@wundergraph/composition'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; import { DefaultNamespace } from '../../repositories/NamespaceRepository.js'; +import { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; import type { RouterOptions } from '../../routes.js'; import { enrichLogger, getLogger, handleError } from '../../util.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; @@ -30,6 +31,7 @@ export function setGraphRouterCompatibilityVersion( } const fedGraphRepo = new FederatedGraphRepository(logger, opts.db, authContext.organizationId); + const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId); req.namespace = req.namespace || DefaultNamespace; @@ -110,6 +112,12 @@ export function setGraphRouterCompatibilityVersion( }; } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; + await opts.db.transaction(async (tx) => { const fedGraphRepo = new FederatedGraphRepository(logger, tx, authContext.organizationId); @@ -141,7 +149,7 @@ export function setGraphRouterCompatibilityVersion( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs: [federatedGraph], diff --git a/controlplane/src/core/bufservices/monograph/publishMonograph.ts b/controlplane/src/core/bufservices/monograph/publishMonograph.ts index 9fd48dfeba..2fb666fdec 100644 --- a/controlplane/src/core/bufservices/monograph/publishMonograph.ts +++ b/controlplane/src/core/bufservices/monograph/publishMonograph.ts @@ -38,6 +38,7 @@ export function publishMonograph( const namespaceRepo = new NamespaceRepository(opts.db, authContext.organizationId); const subgraphRepo = new SubgraphRepository(logger, opts.db, authContext.organizationId); const federatedGraphRepo = new FederatedGraphRepository(logger, opts.db, authContext.organizationId); + const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId); if (authContext.organizationDeactivated) { throw new UnauthorizedError(); @@ -66,12 +67,19 @@ export function publishMonograph( } const subgraphSchemaSDL = req.schema; + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; let isV2Graph: boolean | undefined; try { // Here we check if the schema is valid as a subgraph SDL - const result = buildSchema(subgraphSchemaSDL, true, graph.routerCompatibilityVersion); + const result = buildSchema(subgraphSchemaSDL, true, graph.routerCompatibilityVersion, { + ignoreExternalKeys, + }); if (!result.success) { return { response: { @@ -152,6 +160,9 @@ export function publishMonograph( webhookJWTSecret: opts.admissionWebhookJWTSecret, }, opts.chClient!, + { + ignoreExternalKeys, + }, ); for (const graph of updatedFederatedGraphs) { @@ -181,7 +192,6 @@ export function publishMonograph( // Best effort approach. This way of counting tokens is not accurate. subgraphSchemaSDL.length <= 10_000 ) { - const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId); const feature = await orgRepo.getFeature({ organizationId: authContext.organizationId, featureId: 'ai', diff --git a/controlplane/src/core/bufservices/monograph/updateMonograph.ts b/controlplane/src/core/bufservices/monograph/updateMonograph.ts index 9443ede21c..5ee6068ecd 100644 --- a/controlplane/src/core/bufservices/monograph/updateMonograph.ts +++ b/controlplane/src/core/bufservices/monograph/updateMonograph.ts @@ -10,6 +10,7 @@ import { isValidUrl } from '@wundergraph/cosmo-shared'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; import { DefaultNamespace } from '../../repositories/NamespaceRepository.js'; +import { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; import { SubgraphRepository } from '../../repositories/SubgraphRepository.js'; import type { RouterOptions } from '../../routes.js'; import { @@ -41,6 +42,7 @@ export function updateMonograph( return opts.db.transaction(async (tx) => { const fedGraphRepo = new FederatedGraphRepository(logger, tx, authContext.organizationId); const subgraphRepo = new SubgraphRepository(logger, tx, authContext.organizationId); + const orgRepo = new OrganizationRepository(logger, tx, opts.billingDefaultPlanId); const auditLogRepo = new AuditLogRepository(tx); const orgWebhooks = new OrganizationWebhookService( tx, @@ -112,6 +114,11 @@ export function updateMonograph( } const subgraph = subgraphs[0]; + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; // check if the user is authorized to perform the action await opts.authorizer.authorize({ @@ -150,6 +157,9 @@ export function updateMonograph( admissionWebhookURL: req.admissionWebhookURL, admissionWebhookSecret: req.admissionWebhookSecret, chClient: opts.chClient!, + compositionOptions: { + ignoreExternalKeys, + }, }); await subgraphRepo.update( @@ -173,6 +183,9 @@ export function updateMonograph( webhookJWTSecret: opts.admissionWebhookJWTSecret, }, opts.chClient!, + { + ignoreExternalKeys, + }, ); await auditLogRepo.addAuditLog({ diff --git a/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts b/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts index b37d3ae0cb..1aee3b6d20 100644 --- a/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts +++ b/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts @@ -189,6 +189,11 @@ export function checkSubgraphSchema( } const subgraphName = subgraph?.name || req.subgraphName; + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const federatedGraphs = await fedGraphRepo.bySubgraphLabels({ labels: subgraph ? subgraph.labels : req.labels, @@ -206,7 +211,9 @@ export function checkSubgraphSchema( if (newSchemaSDL) { try { // Here we check if the schema is valid as a subgraph SDL - const result = buildSchema(newSchemaSDL, true, routerCompatibilityVersion); + const result = buildSchema(newSchemaSDL, true, routerCompatibilityVersion, { + ignoreExternalKeys, + }); if (!result.success) { return { response: { @@ -282,6 +289,7 @@ export function checkSubgraphSchema( chClient: opts.chClient, newGraphQLSchema, disableResolvabilityValidation: req.disableResolvabilityValidation, + ignoreExternalKeys, webhookService, }); @@ -450,6 +458,7 @@ export function checkSubgraphSchema( chClient: opts.chClient, newGraphQLSchema: targetNewGraphQLSchema, disableResolvabilityValidation: req.disableResolvabilityValidation, + ignoreExternalKeys, webhookService, }); diff --git a/controlplane/src/core/bufservices/subgraph/deleteFederatedSubgraph.ts b/controlplane/src/core/bufservices/subgraph/deleteFederatedSubgraph.ts index 687bdc2c1c..9ac3e0b5f6 100644 --- a/controlplane/src/core/bufservices/subgraph/deleteFederatedSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/deleteFederatedSubgraph.ts @@ -10,6 +10,7 @@ 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 { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; import { SubgraphRepository } from '../../repositories/SubgraphRepository.js'; import type { RouterOptions } from '../../routes.js'; import { enrichLogger, getFederatedGraphRouterCompatibilityVersion, getLogger, handleError } from '../../util.js'; @@ -32,6 +33,7 @@ export function deleteFederatedSubgraph( const proposalRepo = new ProposalRepository(opts.db, authContext.organizationId); const namespaceRepo = new NamespaceRepository(opts.db, authContext.organizationId); const fedGraphRepo = new FederatedGraphRepository(logger, opts.db, authContext.organizationId); + const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId); const orgWebhooks = new OrganizationWebhookService( opts.db, authContext.organizationId, @@ -121,6 +123,12 @@ export function deleteFederatedSubgraph( } } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; + const { affectedFederatedGraphs, compositionErrors, deploymentErrors, compositionWarnings } = await opts.db.transaction(async (tx) => { const fedGraphRepo = new FederatedGraphRepository(logger, tx, authContext.organizationId); @@ -177,7 +185,7 @@ export function deleteFederatedSubgraph( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs: affectedFederatedGraphs, diff --git a/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts b/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts index 4fa100bcdf..29d1fa4d5f 100644 --- a/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts +++ b/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts @@ -110,6 +110,11 @@ export function fixSubgraphSchema( organizationId: authContext.organizationId, featureId: 'ai', }); + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; if (!feature?.enabled) { return { @@ -140,6 +145,9 @@ export function fixSubgraphSchema( * compatibility version. */ getFederatedGraphRouterCompatibilityVersion(federatedGraphs), + { + ignoreExternalKeys, + }, ); if (!result.success) { return { @@ -169,7 +177,7 @@ export function fixSubgraphSchema( subgraph.namespaceId, newSchemaSDL, { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, ); diff --git a/controlplane/src/core/bufservices/subgraph/moveSubgraph.ts b/controlplane/src/core/bufservices/subgraph/moveSubgraph.ts index b50ea791ef..9a039def46 100644 --- a/controlplane/src/core/bufservices/subgraph/moveSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/moveSubgraph.ts @@ -7,6 +7,7 @@ import { PublicError } from '../../errors/errors.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FeatureFlagRepository } from '../../repositories/FeatureFlagRepository.js'; import { NamespaceRepository } from '../../repositories/NamespaceRepository.js'; +import { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; import { SubgraphRepository } from '../../repositories/SubgraphRepository.js'; import type { RouterOptions } from '../../routes.js'; import { enrichLogger, getLogger, handleError } from '../../util.js'; @@ -25,6 +26,7 @@ export function moveSubgraph( const subgraphRepo = new SubgraphRepository(logger, opts.db, authContext.organizationId); const featureFlagRepo = new FeatureFlagRepository(logger, opts.db, authContext.organizationId); + const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId); const orgWebhooks = new OrganizationWebhookService( opts.db, authContext.organizationId, @@ -82,6 +84,12 @@ export function moveSubgraph( authContext, }); + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; + const { compositionErrors, updatedFederatedGraphs, deploymentErrors, compositionWarnings } = await opts.db.transaction(async (tx) => { const auditLogRepo = new AuditLogRepository(tx); @@ -118,7 +126,7 @@ export function moveSubgraph( }, opts.chClient!, { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, ); diff --git a/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts b/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts index 3902421083..79fd6a3bc4 100644 --- a/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts @@ -68,6 +68,12 @@ export function publishFederatedSubgraph( throw new UnauthorizedError(); } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; + const subgraphSchemaSDL = req.schema; const namespace = await namespaceRepo.byName(req.namespace); if (!namespace) { @@ -98,7 +104,9 @@ export function publishFederatedSubgraph( * compatibility version. */ // Here we check if the schema is valid as a subgraph SDL - const result = buildSchema(subgraphSchemaSDL, true, routerCompatibilityVersion); + const result = buildSchema(subgraphSchemaSDL, true, routerCompatibilityVersion, { + ignoreExternalKeys, + }); if (!result.success) { return { response: { @@ -588,7 +596,7 @@ export function publishFederatedSubgraph( }, opts.chClient!, { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, ); diff --git a/controlplane/src/core/bufservices/subgraph/updateSubgraph.ts b/controlplane/src/core/bufservices/subgraph/updateSubgraph.ts index 6374ea01fb..b623185dd8 100644 --- a/controlplane/src/core/bufservices/subgraph/updateSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/updateSubgraph.ts @@ -10,6 +10,7 @@ import { import { isValidUrl } from '@wundergraph/cosmo-shared'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { DefaultNamespace } from '../../repositories/NamespaceRepository.js'; +import { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; import { SubgraphRepository } from '../../repositories/SubgraphRepository.js'; import type { RouterOptions } from '../../routes.js'; import { @@ -37,6 +38,7 @@ export function updateSubgraph( logger = enrichLogger(ctx, logger, authContext); const subgraphRepo = new SubgraphRepository(logger, opts.db, authContext.organizationId); + const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId); const auditLogRepo = new AuditLogRepository(opts.db); const orgWebhooks = new OrganizationWebhookService( opts.db, @@ -185,6 +187,12 @@ export function updateSubgraph( throw new UnauthorizedError(); } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; + const { compositionErrors, updatedFederatedGraphs, deploymentErrors, compositionWarnings } = await subgraphRepo.update( { @@ -208,7 +216,7 @@ export function updateSubgraph( }, opts.chClient!, { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, ); diff --git a/controlplane/src/core/repositories/OrganizationRepository.ts b/controlplane/src/core/repositories/OrganizationRepository.ts index a374b860fd..73253ca80b 100644 --- a/controlplane/src/core/repositories/OrganizationRepository.ts +++ b/controlplane/src/core/repositories/OrganizationRepository.ts @@ -1396,6 +1396,7 @@ export class OrganizationRepository { scim: false, 'cache-warmer': false, proposals: false, + 'composition-ignore-external-keys': false, 'subgraph-check-extensions': false, }; diff --git a/controlplane/src/core/repositories/SchemaCheckRepository.ts b/controlplane/src/core/repositories/SchemaCheckRepository.ts index 81cdd0c234..341c5656b2 100644 --- a/controlplane/src/core/repositories/SchemaCheckRepository.ts +++ b/controlplane/src/core/repositories/SchemaCheckRepository.ts @@ -751,6 +751,11 @@ export class SchemaCheckRepository { organizationId, featureId: 'breaking-change-retention', }); + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId, + featureId: 'composition-ignore-external-keys', + }); + const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const limit = changeRetention?.limit ?? defaultRetentionLimitInDays; @@ -820,7 +825,9 @@ export class SchemaCheckRepository { if (newSchemaSDL) { try { // Here we check if the schema is valid as a subgraph SDL - const result = buildSchema(newSchemaSDL, true, routerCompatibilityVersion); + const result = buildSchema(newSchemaSDL, true, routerCompatibilityVersion, { + ignoreExternalKeys, + }); if (!result.success) { await this.update({ schemaCheckID, @@ -1109,6 +1116,9 @@ export class SchemaCheckRepository { } const { composedGraphs } = await composer.composeWithProposedSchemas({ + compositionOptions: { + ignoreExternalKeys, + }, inputSubgraphs: checkSubgraphs, graphs: federatedGraphs.filter((g) => !g.contract), }); @@ -1369,6 +1379,7 @@ export class SchemaCheckRepository { chClient, newGraphQLSchema: targetNewGraphQLSchema, disableResolvabilityValidation: false, + ignoreExternalKeys, webhookService, }); diff --git a/controlplane/src/core/repositories/SubgraphRepository.ts b/controlplane/src/core/repositories/SubgraphRepository.ts index 1e32180bbb..9dc207fd8c 100644 --- a/controlplane/src/core/repositories/SubgraphRepository.ts +++ b/controlplane/src/core/repositories/SubgraphRepository.ts @@ -1862,6 +1862,7 @@ export class SubgraphRepository { chClient, newGraphQLSchema, disableResolvabilityValidation, + ignoreExternalKeys, webhookService, }: { actorId: string; @@ -1886,6 +1887,7 @@ export class SubgraphRepository { chClient?: ClickHouseClient; newGraphQLSchema?: GraphQLSchema; disableResolvabilityValidation?: boolean; + ignoreExternalKeys?: boolean; webhookService: OrganizationWebhookService; }): Promise< PlainMessage & { @@ -2070,7 +2072,7 @@ export class SubgraphRepository { const { composedGraphs } = await composer.composeWithProposedSchemas({ compositionOptions: { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation, }, graphs: federatedGraphs.filter((g) => !g.contract), diff --git a/controlplane/src/types/index.ts b/controlplane/src/types/index.ts index 9d335e65fc..9e4e05ab06 100644 --- a/controlplane/src/types/index.ts +++ b/controlplane/src/types/index.ts @@ -25,6 +25,7 @@ export type FeatureIds = | 'cache-warmer' | 'proposals' | 'plugins' + | 'composition-ignore-external-keys' | 'subgraph-check-extensions'; export type Features = { From 3a9abde1fea4f6b7ce6c8dc3499017489d8fef86 Mon Sep 17 00:00:00 2001 From: Alessandro Pagnin Date: Fri, 6 Mar 2026 17:05:48 +0100 Subject: [PATCH 02/12] fix: sovled issues --- cli/src/commands/router/commands/compose.ts | 2 +- .../core/bufservices/contract/createContract.ts | 3 +-- .../core/bufservices/contract/updateContract.ts | 5 ++--- .../bufservices/feature-flag/createFeatureFlag.ts | 3 +-- .../bufservices/feature-flag/deleteFeatureFlag.ts | 3 +-- .../bufservices/feature-flag/enableFeatureFlag.ts | 3 +-- .../bufservices/feature-flag/updateFeatureFlag.ts | 3 +-- .../federated-graph/checkFederatedGraph.ts | 3 +-- .../federated-graph/createFederatedGraph.ts | 3 +-- .../federated-graph/migrateFromApollo.ts | 3 +-- .../federated-graph/updateFederatedGraph.ts | 3 +-- .../graph/setGraphRouterCompatibilityVersion.ts | 3 +-- .../bufservices/monograph/publishMonograph.ts | 8 ++------ .../core/bufservices/monograph/updateMonograph.ts | 14 -------------- .../bufservices/subgraph/checkSubgraphSchema.ts | 15 +++++++++------ .../subgraph/deleteFederatedSubgraph.ts | 3 +-- .../bufservices/subgraph/fixSubgraphSchema.ts | 5 ++--- .../src/core/bufservices/subgraph/moveSubgraph.ts | 3 +-- .../subgraph/publishFederatedSubgraph.ts | 5 ++--- .../core/bufservices/subgraph/updateSubgraph.ts | 3 +-- .../core/repositories/SchemaCheckRepository.ts | 11 ++++++----- .../src/core/repositories/SubgraphRepository.ts | 11 +++-------- 22 files changed, 40 insertions(+), 75 deletions(-) diff --git a/cli/src/commands/router/commands/compose.ts b/cli/src/commands/router/commands/compose.ts index 8979c1f0f1..14ee40e5c4 100644 --- a/cli/src/commands/router/commands/compose.ts +++ b/cli/src/commands/router/commands/compose.ts @@ -173,7 +173,7 @@ export default (opts: BaseCommandOptions) => { ); command.option( '--ignore-external-keys', - 'This flag ignores resolvability checks on external fields during composition.', + 'This flag ignores errors related to true external entity keys.', ); command.action(async (options) => { diff --git a/controlplane/src/core/bufservices/contract/createContract.ts b/controlplane/src/core/bufservices/contract/createContract.ts index 3f9ef8975e..7b7ca376f5 100644 --- a/controlplane/src/core/bufservices/contract/createContract.ts +++ b/controlplane/src/core/bufservices/contract/createContract.ts @@ -108,7 +108,6 @@ export function createContract( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const limit = feature?.limit === -1 ? undefined : feature?.limit; @@ -203,7 +202,7 @@ export function createContract( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs: [{ ...contractGraph, contract }], diff --git a/controlplane/src/core/bufservices/contract/updateContract.ts b/controlplane/src/core/bufservices/contract/updateContract.ts index 219e2a19fe..e9f7cfd7b4 100644 --- a/controlplane/src/core/bufservices/contract/updateContract.ts +++ b/controlplane/src/core/bufservices/contract/updateContract.ts @@ -122,7 +122,6 @@ export function updateContract( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const updatedContractDetails = await contractRepo.update({ id: graph.contract.id, @@ -149,7 +148,7 @@ export function updateContract( labelMatchers: [], chClient: opts.chClient!, compositionOptions: { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, }); @@ -167,7 +166,7 @@ export function updateContract( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs: [ diff --git a/controlplane/src/core/bufservices/feature-flag/createFeatureFlag.ts b/controlplane/src/core/bufservices/feature-flag/createFeatureFlag.ts index 6fbca462a2..66e0a070a2 100644 --- a/controlplane/src/core/bufservices/feature-flag/createFeatureFlag.ts +++ b/controlplane/src/core/bufservices/feature-flag/createFeatureFlag.ts @@ -193,7 +193,6 @@ export function createFeatureFlag( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const compositionErrors: PlainMessage[] = []; const deploymentErrors: PlainMessage[] = []; @@ -211,7 +210,7 @@ export function createFeatureFlag( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs, diff --git a/controlplane/src/core/bufservices/feature-flag/deleteFeatureFlag.ts b/controlplane/src/core/bufservices/feature-flag/deleteFeatureFlag.ts index 6cbf8ec038..c90d9a47b5 100644 --- a/controlplane/src/core/bufservices/feature-flag/deleteFeatureFlag.ts +++ b/controlplane/src/core/bufservices/feature-flag/deleteFeatureFlag.ts @@ -108,7 +108,6 @@ export function deleteFeatureFlag( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const compositionErrors: PlainMessage[] = []; const deploymentErrors: PlainMessage[] = []; @@ -144,7 +143,7 @@ export function deleteFeatureFlag( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs, diff --git a/controlplane/src/core/bufservices/feature-flag/enableFeatureFlag.ts b/controlplane/src/core/bufservices/feature-flag/enableFeatureFlag.ts index 5784345e98..61f1033160 100644 --- a/controlplane/src/core/bufservices/feature-flag/enableFeatureFlag.ts +++ b/controlplane/src/core/bufservices/feature-flag/enableFeatureFlag.ts @@ -109,7 +109,6 @@ export function enableFeatureFlag( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const compositionErrors: PlainMessage[] = []; const deploymentErrors: PlainMessage[] = []; @@ -127,7 +126,7 @@ export function enableFeatureFlag( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs, diff --git a/controlplane/src/core/bufservices/feature-flag/updateFeatureFlag.ts b/controlplane/src/core/bufservices/feature-flag/updateFeatureFlag.ts index a930741810..1532b30cd2 100644 --- a/controlplane/src/core/bufservices/feature-flag/updateFeatureFlag.ts +++ b/controlplane/src/core/bufservices/feature-flag/updateFeatureFlag.ts @@ -165,7 +165,6 @@ export function updateFeatureFlag( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const compositionErrors: PlainMessage[] = []; const deploymentErrors: PlainMessage[] = []; @@ -183,7 +182,7 @@ export function updateFeatureFlag( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs: allFederatedGraphsToCompose, diff --git a/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts b/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts index 5368788253..9f318565c6 100644 --- a/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts +++ b/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts @@ -106,7 +106,6 @@ export function checkFederatedGraph( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const result = composeSubgraphs( subgraphsUsedForComposition.map((s) => ({ @@ -117,7 +116,7 @@ export function checkFederatedGraph( })), federatedGraph.routerCompatibilityVersion, { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, ); diff --git a/controlplane/src/core/bufservices/federated-graph/createFederatedGraph.ts b/controlplane/src/core/bufservices/federated-graph/createFederatedGraph.ts index be6a8b1da3..4d3a4fed6f 100644 --- a/controlplane/src/core/bufservices/federated-graph/createFederatedGraph.ts +++ b/controlplane/src/core/bufservices/federated-graph/createFederatedGraph.ts @@ -214,7 +214,6 @@ export function createFederatedGraph( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const compositionErrors: PlainMessage[] = []; const deploymentErrors: PlainMessage[] = []; @@ -232,7 +231,7 @@ export function createFederatedGraph( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs: [federatedGraph], diff --git a/controlplane/src/core/bufservices/federated-graph/migrateFromApollo.ts b/controlplane/src/core/bufservices/federated-graph/migrateFromApollo.ts index 6ec2242c8a..b9a30bc32c 100644 --- a/controlplane/src/core/bufservices/federated-graph/migrateFromApollo.ts +++ b/controlplane/src/core/bufservices/federated-graph/migrateFromApollo.ts @@ -135,7 +135,6 @@ export function migrateFromApollo( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; await opts.db.transaction(async (tx) => { const fedGraphRepo = new FederatedGraphRepository(logger, tx, authContext.organizationId); @@ -163,7 +162,7 @@ export function migrateFromApollo( }, chClient: opts.chClient!, compositionOptions: { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: true, }, }); diff --git a/controlplane/src/core/bufservices/federated-graph/updateFederatedGraph.ts b/controlplane/src/core/bufservices/federated-graph/updateFederatedGraph.ts index 97396f43a3..4520af8703 100644 --- a/controlplane/src/core/bufservices/federated-graph/updateFederatedGraph.ts +++ b/controlplane/src/core/bufservices/federated-graph/updateFederatedGraph.ts @@ -112,7 +112,6 @@ export function updateFederatedGraph( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const deploymentErrors: PlainMessage[] = []; let compositionErrors: PlainMessage[] = []; @@ -128,7 +127,7 @@ export function updateFederatedGraph( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, labelMatchers: req.labelMatchers, diff --git a/controlplane/src/core/bufservices/graph/setGraphRouterCompatibilityVersion.ts b/controlplane/src/core/bufservices/graph/setGraphRouterCompatibilityVersion.ts index 3bb2083a20..eeebdac6ab 100644 --- a/controlplane/src/core/bufservices/graph/setGraphRouterCompatibilityVersion.ts +++ b/controlplane/src/core/bufservices/graph/setGraphRouterCompatibilityVersion.ts @@ -116,7 +116,6 @@ export function setGraphRouterCompatibilityVersion( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; await opts.db.transaction(async (tx) => { const fedGraphRepo = new FederatedGraphRepository(logger, tx, authContext.organizationId); @@ -149,7 +148,7 @@ export function setGraphRouterCompatibilityVersion( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs: [federatedGraph], diff --git a/controlplane/src/core/bufservices/monograph/publishMonograph.ts b/controlplane/src/core/bufservices/monograph/publishMonograph.ts index 2fb666fdec..32ecfc460d 100644 --- a/controlplane/src/core/bufservices/monograph/publishMonograph.ts +++ b/controlplane/src/core/bufservices/monograph/publishMonograph.ts @@ -71,14 +71,13 @@ export function publishMonograph( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; let isV2Graph: boolean | undefined; try { // Here we check if the schema is valid as a subgraph SDL const result = buildSchema(subgraphSchemaSDL, true, graph.routerCompatibilityVersion, { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, }); if (!result.success) { return { @@ -159,10 +158,7 @@ export function publishMonograph( cdnBaseUrl: opts.cdnBaseUrl, webhookJWTSecret: opts.admissionWebhookJWTSecret, }, - opts.chClient!, - { - ignoreExternalKeys, - }, + opts.chClient! ); for (const graph of updatedFederatedGraphs) { diff --git a/controlplane/src/core/bufservices/monograph/updateMonograph.ts b/controlplane/src/core/bufservices/monograph/updateMonograph.ts index 5ee6068ecd..cd8bb14028 100644 --- a/controlplane/src/core/bufservices/monograph/updateMonograph.ts +++ b/controlplane/src/core/bufservices/monograph/updateMonograph.ts @@ -10,7 +10,6 @@ import { isValidUrl } from '@wundergraph/cosmo-shared'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; import { DefaultNamespace } from '../../repositories/NamespaceRepository.js'; -import { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; import { SubgraphRepository } from '../../repositories/SubgraphRepository.js'; import type { RouterOptions } from '../../routes.js'; import { @@ -42,7 +41,6 @@ export function updateMonograph( return opts.db.transaction(async (tx) => { const fedGraphRepo = new FederatedGraphRepository(logger, tx, authContext.organizationId); const subgraphRepo = new SubgraphRepository(logger, tx, authContext.organizationId); - const orgRepo = new OrganizationRepository(logger, tx, opts.billingDefaultPlanId); const auditLogRepo = new AuditLogRepository(tx); const orgWebhooks = new OrganizationWebhookService( tx, @@ -114,12 +112,6 @@ export function updateMonograph( } const subgraph = subgraphs[0]; - const ignoreExternalKeysFeature = await orgRepo.getFeature({ - organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', - }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; - // check if the user is authorized to perform the action await opts.authorizer.authorize({ db: opts.db, @@ -157,9 +149,6 @@ export function updateMonograph( admissionWebhookURL: req.admissionWebhookURL, admissionWebhookSecret: req.admissionWebhookSecret, chClient: opts.chClient!, - compositionOptions: { - ignoreExternalKeys, - }, }); await subgraphRepo.update( @@ -183,9 +172,6 @@ export function updateMonograph( webhookJWTSecret: opts.admissionWebhookJWTSecret, }, opts.chClient!, - { - ignoreExternalKeys, - }, ); await auditLogRepo.addAuditLog({ diff --git a/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts b/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts index 1aee3b6d20..a7dce8c9b8 100644 --- a/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts +++ b/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts @@ -193,7 +193,6 @@ export function checkSubgraphSchema( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const federatedGraphs = await fedGraphRepo.bySubgraphLabels({ labels: subgraph ? subgraph.labels : req.labels, @@ -212,7 +211,7 @@ export function checkSubgraphSchema( try { // Here we check if the schema is valid as a subgraph SDL const result = buildSchema(newSchemaSDL, true, routerCompatibilityVersion, { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, }); if (!result.success) { return { @@ -288,8 +287,10 @@ export function checkSubgraphSchema( limit, chClient: opts.chClient, newGraphQLSchema, - disableResolvabilityValidation: req.disableResolvabilityValidation, - ignoreExternalKeys, + compositionOptions: { + disableResolvabilityValidation: req.disableResolvabilityValidation, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, + }, webhookService, }); @@ -457,8 +458,10 @@ export function checkSubgraphSchema( limit: targetLimit, chClient: opts.chClient, newGraphQLSchema: targetNewGraphQLSchema, - disableResolvabilityValidation: req.disableResolvabilityValidation, - ignoreExternalKeys, + compositionOptions: { + disableResolvabilityValidation: req.disableResolvabilityValidation, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, + }, webhookService, }); diff --git a/controlplane/src/core/bufservices/subgraph/deleteFederatedSubgraph.ts b/controlplane/src/core/bufservices/subgraph/deleteFederatedSubgraph.ts index 9ac3e0b5f6..71d0cedfa6 100644 --- a/controlplane/src/core/bufservices/subgraph/deleteFederatedSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/deleteFederatedSubgraph.ts @@ -127,7 +127,6 @@ export function deleteFederatedSubgraph( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const { affectedFederatedGraphs, compositionErrors, deploymentErrors, compositionWarnings } = await opts.db.transaction(async (tx) => { @@ -185,7 +184,7 @@ export function deleteFederatedSubgraph( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs: affectedFederatedGraphs, diff --git a/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts b/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts index 29d1fa4d5f..a1d2ed0b1b 100644 --- a/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts +++ b/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts @@ -114,7 +114,6 @@ export function fixSubgraphSchema( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; if (!feature?.enabled) { return { @@ -146,7 +145,7 @@ export function fixSubgraphSchema( */ getFederatedGraphRouterCompatibilityVersion(federatedGraphs), { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, }, ); if (!result.success) { @@ -177,7 +176,7 @@ export function fixSubgraphSchema( subgraph.namespaceId, newSchemaSDL, { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, ); diff --git a/controlplane/src/core/bufservices/subgraph/moveSubgraph.ts b/controlplane/src/core/bufservices/subgraph/moveSubgraph.ts index 9a039def46..5ed705cb91 100644 --- a/controlplane/src/core/bufservices/subgraph/moveSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/moveSubgraph.ts @@ -88,7 +88,6 @@ export function moveSubgraph( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const { compositionErrors, updatedFederatedGraphs, deploymentErrors, compositionWarnings } = await opts.db.transaction(async (tx) => { @@ -126,7 +125,7 @@ export function moveSubgraph( }, opts.chClient!, { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, ); diff --git a/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts b/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts index 79fd6a3bc4..60974124f3 100644 --- a/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts @@ -72,7 +72,6 @@ export function publishFederatedSubgraph( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const subgraphSchemaSDL = req.schema; const namespace = await namespaceRepo.byName(req.namespace); @@ -105,7 +104,7 @@ export function publishFederatedSubgraph( */ // Here we check if the schema is valid as a subgraph SDL const result = buildSchema(subgraphSchemaSDL, true, routerCompatibilityVersion, { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, }); if (!result.success) { return { @@ -596,7 +595,7 @@ export function publishFederatedSubgraph( }, opts.chClient!, { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, ); diff --git a/controlplane/src/core/bufservices/subgraph/updateSubgraph.ts b/controlplane/src/core/bufservices/subgraph/updateSubgraph.ts index b623185dd8..a3ca28e21a 100644 --- a/controlplane/src/core/bufservices/subgraph/updateSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/updateSubgraph.ts @@ -191,7 +191,6 @@ export function updateSubgraph( organizationId: authContext.organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const { compositionErrors, updatedFederatedGraphs, deploymentErrors, compositionWarnings } = await subgraphRepo.update( @@ -216,7 +215,7 @@ export function updateSubgraph( }, opts.chClient!, { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, ); diff --git a/controlplane/src/core/repositories/SchemaCheckRepository.ts b/controlplane/src/core/repositories/SchemaCheckRepository.ts index 341c5656b2..1dc5921531 100644 --- a/controlplane/src/core/repositories/SchemaCheckRepository.ts +++ b/controlplane/src/core/repositories/SchemaCheckRepository.ts @@ -755,7 +755,6 @@ export class SchemaCheckRepository { organizationId, featureId: 'composition-ignore-external-keys', }); - const ignoreExternalKeys = ignoreExternalKeysFeature?.enabled === true; const limit = changeRetention?.limit ?? defaultRetentionLimitInDays; @@ -826,7 +825,7 @@ export class SchemaCheckRepository { try { // Here we check if the schema is valid as a subgraph SDL const result = buildSchema(newSchemaSDL, true, routerCompatibilityVersion, { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, }); if (!result.success) { await this.update({ @@ -1117,7 +1116,7 @@ export class SchemaCheckRepository { const { composedGraphs } = await composer.composeWithProposedSchemas({ compositionOptions: { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, }, inputSubgraphs: checkSubgraphs, graphs: federatedGraphs.filter((g) => !g.contract), @@ -1378,8 +1377,10 @@ export class SchemaCheckRepository { limit: targetLimit, chClient, newGraphQLSchema: targetNewGraphQLSchema, - disableResolvabilityValidation: false, - ignoreExternalKeys, + compositionOptions: { + disableResolvabilityValidation: false, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, + }, webhookService, }); diff --git a/controlplane/src/core/repositories/SubgraphRepository.ts b/controlplane/src/core/repositories/SubgraphRepository.ts index 9dc207fd8c..eaadcd5653 100644 --- a/controlplane/src/core/repositories/SubgraphRepository.ts +++ b/controlplane/src/core/repositories/SubgraphRepository.ts @@ -1861,8 +1861,7 @@ export class SubgraphRepository { limit, chClient, newGraphQLSchema, - disableResolvabilityValidation, - ignoreExternalKeys, + compositionOptions, webhookService, }: { actorId: string; @@ -1886,8 +1885,7 @@ export class SubgraphRepository { limit: number; chClient?: ClickHouseClient; newGraphQLSchema?: GraphQLSchema; - disableResolvabilityValidation?: boolean; - ignoreExternalKeys?: boolean; + compositionOptions?: CompositionOptions; webhookService: OrganizationWebhookService; }): Promise< PlainMessage & { @@ -2071,10 +2069,7 @@ export class SubgraphRepository { }); const { composedGraphs } = await composer.composeWithProposedSchemas({ - compositionOptions: { - ignoreExternalKeys, - disableResolvabilityValidation, - }, + compositionOptions, graphs: federatedGraphs.filter((g) => !g.contract), inputSubgraphs: checkSubgraphs, }); From d27c4e00f945bf59081a9bae3fa754e4de9f9798 Mon Sep 17 00:00:00 2001 From: Alessandro Pagnin Date: Sat, 7 Mar 2026 20:54:31 +0100 Subject: [PATCH 03/12] chore: add test --- controlplane/test/integrations.test.ts | 93 ++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/controlplane/test/integrations.test.ts b/controlplane/test/integrations.test.ts index 440bf6f822..dc70a05a65 100644 --- a/controlplane/test/integrations.test.ts +++ b/controlplane/test/integrations.test.ts @@ -265,4 +265,97 @@ describe('Federated Graph', (ctx) => { await server.close(); }); + + test('that true external entity key errors can be ignored with the composition feature flag', async () => { + const namespace = genID('namespace').toLowerCase(); + const label = genUniqueLabel(); + const graphName = genID('fedGraph'); + const externalKeySubgraphName = genID('external-key'); + const keySourceSubgraphName = genID('key-source'); + const externalKeySDL = ` + type Entity @key(fields: "id") { + id: ID! @external + } + + type Query { + entities: [Entity!]! + } + `; + const keySourceSDL = ` + type Entity @key(fields: "id") { + id: ID! + name: String! + } + `; + + const { client, server } = await SetupTest({ dbname }); + await createNamespace(client, namespace); + + const publishExternalKeySubgraph = await client.publishFederatedSubgraph({ + name: externalKeySubgraphName, + namespace, + labels: [label], + routingUrl: 'http://localhost:4001', + schema: externalKeySDL, + }); + expect(publishExternalKeySubgraph.response?.code).toBe(EnumStatusCode.OK); + + const publishKeySourceSubgraph = await client.publishFederatedSubgraph({ + name: keySourceSubgraphName, + namespace, + labels: [label], + routingUrl: 'http://localhost:4002', + schema: keySourceSDL, + }); + expect(publishKeySourceSubgraph.response?.code).toBe(EnumStatusCode.OK); + + const createGraphWithoutFeature = await client.createFederatedGraph({ + name: graphName, + namespace, + routingUrl: 'http://localhost:8080', + labelMatchers: [joinLabel(label)], + }); + expect(createGraphWithoutFeature.response?.code).toBe(EnumStatusCode.ERR_SUBGRAPH_COMPOSITION_FAILED); + expect(createGraphWithoutFeature.compositionErrors).toHaveLength(3); + + await server.close(); + + const { client: featureClient, server: featureServer } = await SetupTest({ + dbname, + enabledFeatures: ['composition-ignore-external-keys'], + }); + const featureNamespace = genID('namespace').toLowerCase(); + const featureLabel = genUniqueLabel(); + + await createNamespace(featureClient, featureNamespace); + + const featureExternalKeySubgraph = await featureClient.publishFederatedSubgraph({ + name: genID('external-key'), + namespace: featureNamespace, + labels: [featureLabel], + routingUrl: 'http://localhost:4001', + schema: externalKeySDL, + }); + expect(featureExternalKeySubgraph.response?.code).toBe(EnumStatusCode.OK); + + const featureKeySourceSubgraph = await featureClient.publishFederatedSubgraph({ + name: genID('key-source'), + namespace: featureNamespace, + labels: [featureLabel], + routingUrl: 'http://localhost:4002', + schema: keySourceSDL, + }); + expect(featureKeySourceSubgraph.response?.code).toBe(EnumStatusCode.OK); + + const createGraphWithFeature = await featureClient.createFederatedGraph({ + name: genID('fedGraph'), + namespace: featureNamespace, + routingUrl: 'http://localhost:8080', + labelMatchers: [joinLabel(featureLabel)], + }); + expect(createGraphWithFeature.response?.code).toBe(EnumStatusCode.OK); + expect(createGraphWithFeature.compositionErrors).toHaveLength(0); + + await featureServer.close(); + }); }); From f53ac87314e97791e012e44d2ec766078676ba4f Mon Sep 17 00:00:00 2001 From: Alessandro Pagnin Date: Mon, 9 Mar 2026 15:16:19 +0100 Subject: [PATCH 04/12] chore: improve uniformity on checkMultipleSchemas --- controlplane/src/core/repositories/SchemaCheckRepository.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/controlplane/src/core/repositories/SchemaCheckRepository.ts b/controlplane/src/core/repositories/SchemaCheckRepository.ts index 1dc5921531..6ddf8a4a91 100644 --- a/controlplane/src/core/repositories/SchemaCheckRepository.ts +++ b/controlplane/src/core/repositories/SchemaCheckRepository.ts @@ -825,6 +825,7 @@ export class SchemaCheckRepository { try { // Here we check if the schema is valid as a subgraph SDL const result = buildSchema(newSchemaSDL, true, routerCompatibilityVersion, { + disableResolvabilityValidation: false, ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, }); if (!result.success) { @@ -1116,6 +1117,7 @@ export class SchemaCheckRepository { const { composedGraphs } = await composer.composeWithProposedSchemas({ compositionOptions: { + disableResolvabilityValidation: false, ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, }, inputSubgraphs: checkSubgraphs, From b2b9d3dc079afbc0c82d5a7bd82c63525521fde2 Mon Sep 17 00:00:00 2001 From: Alessandro Pagnin Date: Mon, 9 Mar 2026 15:56:07 +0100 Subject: [PATCH 05/12] chore: readd dangling comma --- controlplane/src/core/bufservices/monograph/publishMonograph.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controlplane/src/core/bufservices/monograph/publishMonograph.ts b/controlplane/src/core/bufservices/monograph/publishMonograph.ts index 32ecfc460d..08eec7596e 100644 --- a/controlplane/src/core/bufservices/monograph/publishMonograph.ts +++ b/controlplane/src/core/bufservices/monograph/publishMonograph.ts @@ -158,7 +158,7 @@ export function publishMonograph( cdnBaseUrl: opts.cdnBaseUrl, webhookJWTSecret: opts.admissionWebhookJWTSecret, }, - opts.chClient! + opts.chClient!, ); for (const graph of updatedFederatedGraphs) { From 1bf7db73f2389b1042efcfe896f154f0ac6eb467 Mon Sep 17 00:00:00 2001 From: Alessandro Pagnin Date: Mon, 9 Mar 2026 16:05:45 +0100 Subject: [PATCH 06/12] chore: linting --- cli/src/commands/router/commands/compose.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cli/src/commands/router/commands/compose.ts b/cli/src/commands/router/commands/compose.ts index 14ee40e5c4..4da637a3b1 100644 --- a/cli/src/commands/router/commands/compose.ts +++ b/cli/src/commands/router/commands/compose.ts @@ -171,10 +171,7 @@ export default (opts: BaseCommandOptions) => { '--disable-resolvability-validation', 'This flag will disable the validation for whether all nodes of the federated graph are resolvable. Do NOT use unless troubleshooting.', ); - command.option( - '--ignore-external-keys', - 'This flag ignores errors related to true external entity keys.', - ); + command.option('--ignore-external-keys', 'This flag ignores errors related to true external entity keys.'); command.action(async (options) => { const inputFile = resolve(options.input); From 4b080f1d935d8bfb5362fb812873f3d8f5f446e2 Mon Sep 17 00:00:00 2001 From: Alessandro Pagnin Date: Mon, 9 Mar 2026 16:20:27 +0100 Subject: [PATCH 07/12] chore: undo change --- controlplane/src/core/bufservices/monograph/updateMonograph.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/controlplane/src/core/bufservices/monograph/updateMonograph.ts b/controlplane/src/core/bufservices/monograph/updateMonograph.ts index cd8bb14028..9443ede21c 100644 --- a/controlplane/src/core/bufservices/monograph/updateMonograph.ts +++ b/controlplane/src/core/bufservices/monograph/updateMonograph.ts @@ -112,6 +112,7 @@ export function updateMonograph( } const subgraph = subgraphs[0]; + // check if the user is authorized to perform the action await opts.authorizer.authorize({ db: opts.db, From 0d3fd78871b4fec46252190ec77171b7453b8b56 Mon Sep 17 00:00:00 2001 From: Alessandro Pagnin Date: Mon, 9 Mar 2026 16:25:41 +0100 Subject: [PATCH 08/12] chore: added missing disableResolvabilityValidation --- .../src/core/bufservices/subgraph/checkSubgraphSchema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts b/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts index a7dce8c9b8..874c550c82 100644 --- a/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts +++ b/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts @@ -211,6 +211,7 @@ export function checkSubgraphSchema( try { // Here we check if the schema is valid as a subgraph SDL const result = buildSchema(newSchemaSDL, true, routerCompatibilityVersion, { + disableResolvabilityValidation: req.disableResolvabilityValidation, ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, }); if (!result.success) { From 810d3c037a86cc7ad72b53e66be9f37e6651eb18 Mon Sep 17 00:00:00 2001 From: Alessandro Pagnin Date: Mon, 9 Mar 2026 16:55:49 +0100 Subject: [PATCH 09/12] chore: use featureId constant --- controlplane/src/core/bufservices/contract/createContract.ts | 3 ++- controlplane/src/core/bufservices/contract/updateContract.ts | 3 ++- .../src/core/bufservices/feature-flag/createFeatureFlag.ts | 3 ++- .../src/core/bufservices/feature-flag/deleteFeatureFlag.ts | 3 ++- .../src/core/bufservices/feature-flag/enableFeatureFlag.ts | 3 ++- .../src/core/bufservices/feature-flag/updateFeatureFlag.ts | 4 ++-- .../core/bufservices/federated-graph/checkFederatedGraph.ts | 3 ++- .../core/bufservices/federated-graph/createFederatedGraph.ts | 3 ++- .../src/core/bufservices/federated-graph/migrateFromApollo.ts | 4 ++-- .../core/bufservices/federated-graph/updateFederatedGraph.ts | 3 ++- .../bufservices/graph/setGraphRouterCompatibilityVersion.ts | 3 ++- .../src/core/bufservices/monograph/publishMonograph.ts | 3 ++- .../src/core/bufservices/subgraph/checkSubgraphSchema.ts | 3 ++- .../src/core/bufservices/subgraph/deleteFederatedSubgraph.ts | 3 ++- .../src/core/bufservices/subgraph/fixSubgraphSchema.ts | 3 ++- controlplane/src/core/bufservices/subgraph/moveSubgraph.ts | 3 ++- .../src/core/bufservices/subgraph/publishFederatedSubgraph.ts | 3 ++- controlplane/src/core/bufservices/subgraph/updateSubgraph.ts | 3 ++- controlplane/src/core/repositories/OrganizationRepository.ts | 3 ++- controlplane/src/core/repositories/SchemaCheckRepository.ts | 3 ++- controlplane/src/types/index.ts | 4 +++- controlplane/test/integrations.test.ts | 3 ++- 22 files changed, 45 insertions(+), 24 deletions(-) diff --git a/controlplane/src/core/bufservices/contract/createContract.ts b/controlplane/src/core/bufservices/contract/createContract.ts index 7b7ca376f5..e77ffb689c 100644 --- a/controlplane/src/core/bufservices/contract/createContract.ts +++ b/controlplane/src/core/bufservices/contract/createContract.ts @@ -9,6 +9,7 @@ import { DeploymentError, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; import { isValidUrl } from '@wundergraph/cosmo-shared'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { PublicError, UnauthorizedError } from '../../errors/errors.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { ContractRepository } from '../../repositories/ContractRepository.js'; @@ -106,7 +107,7 @@ export function createContract( }); const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); const limit = feature?.limit === -1 ? undefined : feature?.limit; diff --git a/controlplane/src/core/bufservices/contract/updateContract.ts b/controlplane/src/core/bufservices/contract/updateContract.ts index e9f7cfd7b4..ea77050ef3 100644 --- a/controlplane/src/core/bufservices/contract/updateContract.ts +++ b/controlplane/src/core/bufservices/contract/updateContract.ts @@ -9,6 +9,7 @@ import { UpdateContractRequest, UpdateContractResponse, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { ContractRepository } from '../../repositories/ContractRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; @@ -120,7 +121,7 @@ export function updateContract( const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); const updatedContractDetails = await contractRepo.update({ diff --git a/controlplane/src/core/bufservices/feature-flag/createFeatureFlag.ts b/controlplane/src/core/bufservices/feature-flag/createFeatureFlag.ts index 66e0a070a2..27c845de7b 100644 --- a/controlplane/src/core/bufservices/feature-flag/createFeatureFlag.ts +++ b/controlplane/src/core/bufservices/feature-flag/createFeatureFlag.ts @@ -9,6 +9,7 @@ import { CreateFeatureFlagResponse, DeploymentError, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FeatureFlagRepository } from '../../repositories/FeatureFlagRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; @@ -191,7 +192,7 @@ export function createFeatureFlag( }); const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); const compositionErrors: PlainMessage[] = []; diff --git a/controlplane/src/core/bufservices/feature-flag/deleteFeatureFlag.ts b/controlplane/src/core/bufservices/feature-flag/deleteFeatureFlag.ts index c90d9a47b5..5235ad2417 100644 --- a/controlplane/src/core/bufservices/feature-flag/deleteFeatureFlag.ts +++ b/controlplane/src/core/bufservices/feature-flag/deleteFeatureFlag.ts @@ -9,6 +9,7 @@ import { DeleteFeatureFlagResponse, DeploymentError, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FeatureFlagRepository } from '../../repositories/FeatureFlagRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; @@ -106,7 +107,7 @@ export function deleteFeatureFlag( const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); const compositionErrors: PlainMessage[] = []; diff --git a/controlplane/src/core/bufservices/feature-flag/enableFeatureFlag.ts b/controlplane/src/core/bufservices/feature-flag/enableFeatureFlag.ts index 61f1033160..055c410fa5 100644 --- a/controlplane/src/core/bufservices/feature-flag/enableFeatureFlag.ts +++ b/controlplane/src/core/bufservices/feature-flag/enableFeatureFlag.ts @@ -9,6 +9,7 @@ import { EnableFeatureFlagRequest, EnableFeatureFlagResponse, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FeatureFlagRepository } from '../../repositories/FeatureFlagRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; @@ -107,7 +108,7 @@ export function enableFeatureFlag( }); const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); const compositionErrors: PlainMessage[] = []; diff --git a/controlplane/src/core/bufservices/feature-flag/updateFeatureFlag.ts b/controlplane/src/core/bufservices/feature-flag/updateFeatureFlag.ts index 1532b30cd2..0d4e736d19 100644 --- a/controlplane/src/core/bufservices/feature-flag/updateFeatureFlag.ts +++ b/controlplane/src/core/bufservices/feature-flag/updateFeatureFlag.ts @@ -9,7 +9,7 @@ import { UpdateFeatureFlagRequest, UpdateFeatureFlagResponse, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; -import { FederatedGraphDTO } from '../../../types/index.js'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, FederatedGraphDTO } from '../../../types/index.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FeatureFlagRepository } from '../../repositories/FeatureFlagRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; @@ -163,7 +163,7 @@ export function updateFeatureFlag( const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); const compositionErrors: PlainMessage[] = []; diff --git a/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts b/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts index 9f318565c6..3d84acca3e 100644 --- a/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts +++ b/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts @@ -9,6 +9,7 @@ import { Subgraph, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; import { parse } from 'graphql'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { composeSubgraphs } from '../../composition/composition.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; import { DefaultNamespace } from '../../repositories/NamespaceRepository.js'; @@ -104,7 +105,7 @@ export function checkFederatedGraph( const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); const result = composeSubgraphs( diff --git a/controlplane/src/core/bufservices/federated-graph/createFederatedGraph.ts b/controlplane/src/core/bufservices/federated-graph/createFederatedGraph.ts index 4d3a4fed6f..56b40660e2 100644 --- a/controlplane/src/core/bufservices/federated-graph/createFederatedGraph.ts +++ b/controlplane/src/core/bufservices/federated-graph/createFederatedGraph.ts @@ -10,6 +10,7 @@ import { DeploymentError, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; import { isValidUrl } from '@wundergraph/cosmo-shared'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; import { DefaultNamespace, NamespaceRepository } from '../../repositories/NamespaceRepository.js'; @@ -212,7 +213,7 @@ export function createFederatedGraph( const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); const compositionErrors: PlainMessage[] = []; diff --git a/controlplane/src/core/bufservices/federated-graph/migrateFromApollo.ts b/controlplane/src/core/bufservices/federated-graph/migrateFromApollo.ts index b9a30bc32c..7383d9571c 100644 --- a/controlplane/src/core/bufservices/federated-graph/migrateFromApollo.ts +++ b/controlplane/src/core/bufservices/federated-graph/migrateFromApollo.ts @@ -6,7 +6,7 @@ import { MigrateFromApolloRequest, MigrateFromApolloResponse, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; -import { GraphApiKeyJwtPayload } from '../../../types/index.js'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, GraphApiKeyJwtPayload } from '../../../types/index.js'; import { audiences, signJwtHS256 } from '../../crypto/jwt.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; @@ -133,7 +133,7 @@ export function migrateFromApollo( const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); await opts.db.transaction(async (tx) => { diff --git a/controlplane/src/core/bufservices/federated-graph/updateFederatedGraph.ts b/controlplane/src/core/bufservices/federated-graph/updateFederatedGraph.ts index 4520af8703..399cfa548d 100644 --- a/controlplane/src/core/bufservices/federated-graph/updateFederatedGraph.ts +++ b/controlplane/src/core/bufservices/federated-graph/updateFederatedGraph.ts @@ -10,6 +10,7 @@ import { UpdateFederatedGraphResponse, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; import { isValidUrl } from '@wundergraph/cosmo-shared'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; import { DefaultNamespace } from '../../repositories/NamespaceRepository.js'; @@ -110,7 +111,7 @@ export function updateFederatedGraph( const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); const deploymentErrors: PlainMessage[] = []; diff --git a/controlplane/src/core/bufservices/graph/setGraphRouterCompatibilityVersion.ts b/controlplane/src/core/bufservices/graph/setGraphRouterCompatibilityVersion.ts index eeebdac6ab..e12cf99a6c 100644 --- a/controlplane/src/core/bufservices/graph/setGraphRouterCompatibilityVersion.ts +++ b/controlplane/src/core/bufservices/graph/setGraphRouterCompatibilityVersion.ts @@ -6,6 +6,7 @@ import { SetGraphRouterCompatibilityVersionResponse, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; import { ROUTER_COMPATIBILITY_VERSIONS, SupportedRouterCompatibilityVersion } from '@wundergraph/composition'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; import { DefaultNamespace } from '../../repositories/NamespaceRepository.js'; import { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; @@ -114,7 +115,7 @@ export function setGraphRouterCompatibilityVersion( const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); await opts.db.transaction(async (tx) => { diff --git a/controlplane/src/core/bufservices/monograph/publishMonograph.ts b/controlplane/src/core/bufservices/monograph/publishMonograph.ts index 08eec7596e..9ada85730a 100644 --- a/controlplane/src/core/bufservices/monograph/publishMonograph.ts +++ b/controlplane/src/core/bufservices/monograph/publishMonograph.ts @@ -6,6 +6,7 @@ import { PublishMonographRequest, PublishMonographResponse, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { buildSchema } from '../../composition/composition.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; @@ -69,7 +70,7 @@ export function publishMonograph( const subgraphSchemaSDL = req.schema; const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); let isV2Graph: boolean | undefined; diff --git a/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts b/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts index 874c550c82..2164fb4f6a 100644 --- a/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts +++ b/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts @@ -7,6 +7,7 @@ import { CheckSubgraphSchemaResponse, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; import { GraphQLSchema, parse } from 'graphql'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { buildSchema } from '../../composition/composition.js'; import { UnauthorizedError } from '../../errors/errors.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; @@ -191,7 +192,7 @@ export function checkSubgraphSchema( const subgraphName = subgraph?.name || req.subgraphName; const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); const federatedGraphs = await fedGraphRepo.bySubgraphLabels({ diff --git a/controlplane/src/core/bufservices/subgraph/deleteFederatedSubgraph.ts b/controlplane/src/core/bufservices/subgraph/deleteFederatedSubgraph.ts index 71d0cedfa6..394985b6d7 100644 --- a/controlplane/src/core/bufservices/subgraph/deleteFederatedSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/deleteFederatedSubgraph.ts @@ -6,6 +6,7 @@ import { DeleteFederatedSubgraphRequest, DeleteFederatedSubgraphResponse, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FeatureFlagRepository } from '../../repositories/FeatureFlagRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; @@ -125,7 +126,7 @@ export function deleteFederatedSubgraph( const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); const { affectedFederatedGraphs, compositionErrors, deploymentErrors, compositionWarnings } = diff --git a/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts b/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts index a1d2ed0b1b..24d2cf3176 100644 --- a/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts +++ b/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts @@ -6,6 +6,7 @@ import { FixSubgraphSchemaRequest, FixSubgraphSchemaResponse, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { Composer } from '../../composition/composer.js'; import { buildSchema } from '../../composition/composition.js'; import { OpenAIGraphql } from '../../openai-graphql/index.js'; @@ -112,7 +113,7 @@ export function fixSubgraphSchema( }); const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); if (!feature?.enabled) { diff --git a/controlplane/src/core/bufservices/subgraph/moveSubgraph.ts b/controlplane/src/core/bufservices/subgraph/moveSubgraph.ts index 5ed705cb91..2fff2508d7 100644 --- a/controlplane/src/core/bufservices/subgraph/moveSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/moveSubgraph.ts @@ -3,6 +3,7 @@ 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 { MoveGraphRequest, MoveGraphResponse } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { PublicError } from '../../errors/errors.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FeatureFlagRepository } from '../../repositories/FeatureFlagRepository.js'; @@ -86,7 +87,7 @@ export function moveSubgraph( const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); const { compositionErrors, updatedFederatedGraphs, deploymentErrors, compositionWarnings } = diff --git a/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts b/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts index 60974124f3..f5083d600e 100644 --- a/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts @@ -8,6 +8,7 @@ import { SubgraphType, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; import { isValidUrl } from '@wundergraph/cosmo-shared'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { buildSchema } from '../../composition/composition.js'; import { UnauthorizedError } from '../../errors/errors.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; @@ -70,7 +71,7 @@ export function publishFederatedSubgraph( const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); const subgraphSchemaSDL = req.schema; diff --git a/controlplane/src/core/bufservices/subgraph/updateSubgraph.ts b/controlplane/src/core/bufservices/subgraph/updateSubgraph.ts index a3ca28e21a..3567045124 100644 --- a/controlplane/src/core/bufservices/subgraph/updateSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/updateSubgraph.ts @@ -8,6 +8,7 @@ import { UpdateSubgraphResponse, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; import { isValidUrl } from '@wundergraph/cosmo-shared'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { DefaultNamespace } from '../../repositories/NamespaceRepository.js'; import { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; @@ -189,7 +190,7 @@ export function updateSubgraph( const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId: authContext.organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); const { compositionErrors, updatedFederatedGraphs, deploymentErrors, compositionWarnings } = diff --git a/controlplane/src/core/repositories/OrganizationRepository.ts b/controlplane/src/core/repositories/OrganizationRepository.ts index 73253ca80b..1808dfb57c 100644 --- a/controlplane/src/core/repositories/OrganizationRepository.ts +++ b/controlplane/src/core/repositories/OrganizationRepository.ts @@ -26,6 +26,7 @@ import { users, } from '../../db/schema.js'; import { + COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, Feature, FeatureIds, OrganizationDTO, @@ -1396,7 +1397,7 @@ export class OrganizationRepository { scim: false, 'cache-warmer': false, proposals: false, - 'composition-ignore-external-keys': false, + [COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID]: false, 'subgraph-check-extensions': false, }; diff --git a/controlplane/src/core/repositories/SchemaCheckRepository.ts b/controlplane/src/core/repositories/SchemaCheckRepository.ts index 6ddf8a4a91..b7e2e48dc0 100644 --- a/controlplane/src/core/repositories/SchemaCheckRepository.ts +++ b/controlplane/src/core/repositories/SchemaCheckRepository.ts @@ -27,6 +27,7 @@ import { } from '../../db/schema.js'; import { CheckedSubgraphDTO, + COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, FederatedGraphDTO, Label, NamespaceDTO, @@ -753,7 +754,7 @@ export class SchemaCheckRepository { }); const ignoreExternalKeysFeature = await orgRepo.getFeature({ organizationId, - featureId: 'composition-ignore-external-keys', + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); const limit = changeRetention?.limit ?? defaultRetentionLimitInDays; diff --git a/controlplane/src/types/index.ts b/controlplane/src/types/index.ts index 9e4e05ab06..c32769fb70 100644 --- a/controlplane/src/types/index.ts +++ b/controlplane/src/types/index.ts @@ -3,6 +3,8 @@ import { JWTPayload } from 'jose'; import { DBSubgraphType, GraphPruningRuleEnum, OrganizationRole, ProposalMatch, ProposalOrigin } from '../db/models.js'; import { RBACEvaluator } from '../core/services/RBACEvaluator.js'; +export const COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID = 'composition-ignore-external-keys'; + export type FeatureIds = | 'users' | 'federated-graphs' @@ -25,7 +27,7 @@ export type FeatureIds = | 'cache-warmer' | 'proposals' | 'plugins' - | 'composition-ignore-external-keys' + | typeof COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID | 'subgraph-check-extensions'; export type Features = { diff --git a/controlplane/test/integrations.test.ts b/controlplane/test/integrations.test.ts index dc70a05a65..aeeaa84bf2 100644 --- a/controlplane/test/integrations.test.ts +++ b/controlplane/test/integrations.test.ts @@ -6,6 +6,7 @@ import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; import { afterAllSetup, beforeAllSetup, genID, genUniqueLabel } from '../src/core/test-util.js'; +import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../src/types/index.js'; import { createNamespace, resolvabilitySDLOne, resolvabilitySDLTwo, SetupTest } from './test-util.js'; let dbname = ''; @@ -322,7 +323,7 @@ describe('Federated Graph', (ctx) => { const { client: featureClient, server: featureServer } = await SetupTest({ dbname, - enabledFeatures: ['composition-ignore-external-keys'], + enabledFeatures: [COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID], }); const featureNamespace = genID('namespace').toLowerCase(); const featureLabel = genUniqueLabel(); From b5ee103e6032fd4a9521073f059986394c56355a Mon Sep 17 00:00:00 2001 From: Alessandro Pagnin Date: Mon, 9 Mar 2026 17:16:35 +0100 Subject: [PATCH 10/12] fix: wrong constant usage in the union --- controlplane/src/types/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controlplane/src/types/index.ts b/controlplane/src/types/index.ts index c32769fb70..8f2e9ce77c 100644 --- a/controlplane/src/types/index.ts +++ b/controlplane/src/types/index.ts @@ -27,7 +27,7 @@ export type FeatureIds = | 'cache-warmer' | 'proposals' | 'plugins' - | typeof COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID + | 'composition-ignore-external-keys' // COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID | 'subgraph-check-extensions'; export type Features = { From 45ac4818a98530fbc8240d897e1bc8910cd621e7 Mon Sep 17 00:00:00 2001 From: Aenimus Date: Mon, 9 Mar 2026 17:24:53 +0000 Subject: [PATCH 11/12] chore: clean up --- .../bufservices/contract/updateContract.ts | 15 ++++++---- .../bufservices/monograph/publishMonograph.ts | 11 ++++--- .../subgraph/checkSubgraphSchema.ts | 27 ++++++++++------- .../bufservices/subgraph/fixSubgraphSchema.ts | 15 ++++++---- .../subgraph/publishFederatedSubgraph.ts | 26 ++++++++++------- .../repositories/SchemaCheckRepository.ts | 29 +++++++++++-------- 6 files changed, 74 insertions(+), 49 deletions(-) diff --git a/controlplane/src/core/bufservices/contract/updateContract.ts b/controlplane/src/core/bufservices/contract/updateContract.ts index ea77050ef3..12036cc10b 100644 --- a/controlplane/src/core/bufservices/contract/updateContract.ts +++ b/controlplane/src/core/bufservices/contract/updateContract.ts @@ -119,10 +119,13 @@ export function updateContract( }; } - const ignoreExternalKeysFeature = await orgRepo.getFeature({ - organizationId: authContext.organizationId, - featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, - }); + const ignoreExternalKeys = + ( + await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }) + )?.enabled ?? false; const updatedContractDetails = await contractRepo.update({ id: graph.contract.id, @@ -149,7 +152,7 @@ export function updateContract( labelMatchers: [], chClient: opts.chClient!, compositionOptions: { - ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, }); @@ -167,7 +170,7 @@ export function updateContract( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, federatedGraphs: [ diff --git a/controlplane/src/core/bufservices/monograph/publishMonograph.ts b/controlplane/src/core/bufservices/monograph/publishMonograph.ts index 9ada85730a..1e888c9a8a 100644 --- a/controlplane/src/core/bufservices/monograph/publishMonograph.ts +++ b/controlplane/src/core/bufservices/monograph/publishMonograph.ts @@ -76,10 +76,13 @@ export function publishMonograph( let isV2Graph: boolean | undefined; try { - // Here we check if the schema is valid as a subgraph SDL - const result = buildSchema(subgraphSchemaSDL, true, graph.routerCompatibilityVersion, { - ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, - }); + /* Here we check if the schema is valid as a subgraph SDL + * `buildSchema` only calls normalization in isolation. + * The `disableResolvabilityChecks` flag is only used in the federation step. + * The `ignoreExternalKeys` flag is propagated in normalization but only used in the federation step. + * Consequently, there is currently no reason to propagate the options within `buildSchema`. + */ + const result = buildSchema(subgraphSchemaSDL, true, graph.routerCompatibilityVersion); if (!result.success) { return { response: { diff --git a/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts b/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts index 2164fb4f6a..7807f22263 100644 --- a/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts +++ b/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts @@ -190,10 +190,13 @@ export function checkSubgraphSchema( } const subgraphName = subgraph?.name || req.subgraphName; - const ignoreExternalKeysFeature = await orgRepo.getFeature({ - organizationId: authContext.organizationId, - featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, - }); + const ignoreExternalKeys = + ( + await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }) + )?.enabled ?? false; const federatedGraphs = await fedGraphRepo.bySubgraphLabels({ labels: subgraph ? subgraph.labels : req.labels, @@ -210,11 +213,13 @@ export function checkSubgraphSchema( let newGraphQLSchema: GraphQLSchema | undefined; if (newSchemaSDL) { try { - // Here we check if the schema is valid as a subgraph SDL - const result = buildSchema(newSchemaSDL, true, routerCompatibilityVersion, { - disableResolvabilityValidation: req.disableResolvabilityValidation, - ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, - }); + /* Here we check if the schema is valid as a subgraph SDL + * `buildSchema` only calls normalization in isolation. + * The `disableResolvabilityChecks` flag is only used in the federation step. + * The `ignoreExternalKeys` flag is propagated in normalization but only used in the federation step. + * Consequently, there is currently no reason to propagate the options within `buildSchema`. + */ + const result = buildSchema(newSchemaSDL, true, routerCompatibilityVersion); if (!result.success) { return { response: { @@ -291,7 +296,7 @@ export function checkSubgraphSchema( newGraphQLSchema, compositionOptions: { disableResolvabilityValidation: req.disableResolvabilityValidation, - ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, + ignoreExternalKeys, }, webhookService, }); @@ -462,7 +467,7 @@ export function checkSubgraphSchema( newGraphQLSchema: targetNewGraphQLSchema, compositionOptions: { disableResolvabilityValidation: req.disableResolvabilityValidation, - ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, + ignoreExternalKeys, }, webhookService, }); diff --git a/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts b/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts index 24d2cf3176..9a5fd6d71b 100644 --- a/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts +++ b/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts @@ -111,10 +111,13 @@ export function fixSubgraphSchema( organizationId: authContext.organizationId, featureId: 'ai', }); - const ignoreExternalKeysFeature = await orgRepo.getFeature({ - organizationId: authContext.organizationId, - featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, - }); + const ignoreExternalKeys = + ( + await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }) + )?.enabled ?? false; if (!feature?.enabled) { return { @@ -146,7 +149,7 @@ export function fixSubgraphSchema( */ getFederatedGraphRouterCompatibilityVersion(federatedGraphs), { - ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, + ignoreExternalKeys, }, ); if (!result.success) { @@ -177,7 +180,7 @@ export function fixSubgraphSchema( subgraph.namespaceId, newSchemaSDL, { - ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, ); diff --git a/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts b/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts index f5083d600e..4a21d45783 100644 --- a/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts @@ -69,10 +69,13 @@ export function publishFederatedSubgraph( throw new UnauthorizedError(); } - const ignoreExternalKeysFeature = await orgRepo.getFeature({ - organizationId: authContext.organizationId, - featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, - }); + const ignoreExternalKeys = + ( + await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }) + )?.enabled ?? false; const subgraphSchemaSDL = req.schema; const namespace = await namespaceRepo.byName(req.namespace); @@ -103,10 +106,13 @@ export function publishFederatedSubgraph( * If no federated graphs have yet been created, the subgraph will be validated against the latest router * compatibility version. */ - // Here we check if the schema is valid as a subgraph SDL - const result = buildSchema(subgraphSchemaSDL, true, routerCompatibilityVersion, { - ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, - }); + /* Here we check if the schema is valid as a subgraph SDL + * `buildSchema` only calls normalization in isolation. + * The `disableResolvabilityChecks` flag is only used in the federation step. + * The `ignoreExternalKeys` flag is propagated in normalization but only used in the federation step. + * Consequently, there is currently no reason to propagate the options within `buildSchema`. + */ + const result = buildSchema(subgraphSchemaSDL, true, routerCompatibilityVersion); if (!result.success) { return { response: { @@ -445,7 +451,7 @@ export function publishFederatedSubgraph( organizationId: authContext.organizationId, featureId: 'plugins', }); - const limit = feature?.limit === -1 ? 0 : (feature?.limit ?? 0); + const limit = feature?.limit === -1 ? 0 : feature?.limit ?? 0; if (count >= limit) { return { response: { @@ -596,7 +602,7 @@ export function publishFederatedSubgraph( }, opts.chClient!, { - ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, ); diff --git a/controlplane/src/core/repositories/SchemaCheckRepository.ts b/controlplane/src/core/repositories/SchemaCheckRepository.ts index b7e2e48dc0..77107956c8 100644 --- a/controlplane/src/core/repositories/SchemaCheckRepository.ts +++ b/controlplane/src/core/repositories/SchemaCheckRepository.ts @@ -752,10 +752,13 @@ export class SchemaCheckRepository { organizationId, featureId: 'breaking-change-retention', }); - const ignoreExternalKeysFeature = await orgRepo.getFeature({ - organizationId, - featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, - }); + const ignoreExternalKeys = + ( + await orgRepo.getFeature({ + organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }) + )?.enabled ?? false; const limit = changeRetention?.limit ?? defaultRetentionLimitInDays; @@ -824,11 +827,13 @@ export class SchemaCheckRepository { let newGraphQLSchema: GraphQLSchema | undefined; if (newSchemaSDL) { try { - // Here we check if the schema is valid as a subgraph SDL - const result = buildSchema(newSchemaSDL, true, routerCompatibilityVersion, { - disableResolvabilityValidation: false, - ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, - }); + /* Here we check if the schema is valid as a subgraph SDL + * `buildSchema` only calls normalization in isolation. + * The `disableResolvabilityChecks` flag is only used in the federation step. + * The `ignoreExternalKeys` flag is propagated in normalization but only used in the federation step. + * Consequently, there is currently no reason to propagate the options within `buildSchema`. + */ + const result = buildSchema(newSchemaSDL, true, routerCompatibilityVersion); if (!result.success) { await this.update({ schemaCheckID, @@ -859,7 +864,7 @@ export class SchemaCheckRepository { } if (namespace.enableGraphPruning) { const parsedSchema = parse(newSchemaSDL); - // this new GraphQL schema conatins the location info + // this new GraphQL schema contains the location info newGraphQLSchema = buildASTSchema(parsedSchema, { assumeValid: true, assumeValidSDL: true }); } } catch (e: any) { @@ -1119,7 +1124,7 @@ export class SchemaCheckRepository { const { composedGraphs } = await composer.composeWithProposedSchemas({ compositionOptions: { disableResolvabilityValidation: false, - ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, + ignoreExternalKeys, }, inputSubgraphs: checkSubgraphs, graphs: federatedGraphs.filter((g) => !g.contract), @@ -1382,7 +1387,7 @@ export class SchemaCheckRepository { newGraphQLSchema: targetNewGraphQLSchema, compositionOptions: { disableResolvabilityValidation: false, - ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, + ignoreExternalKeys, }, webhookService, }); From cb51f7bebb67d91817b15ce150b23e5ee4728cf6 Mon Sep 17 00:00:00 2001 From: Aenimus Date: Mon, 9 Mar 2026 19:01:25 +0000 Subject: [PATCH 12/12] chore: clean up --- .../bufservices/monograph/publishMonograph.ts | 9 +------- .../bufservices/subgraph/fixSubgraphSchema.ts | 23 +++++++++---------- .../subgraph/publishFederatedSubgraph.ts | 15 +++++------- 3 files changed, 18 insertions(+), 29 deletions(-) diff --git a/controlplane/src/core/bufservices/monograph/publishMonograph.ts b/controlplane/src/core/bufservices/monograph/publishMonograph.ts index 1e888c9a8a..bb30418df7 100644 --- a/controlplane/src/core/bufservices/monograph/publishMonograph.ts +++ b/controlplane/src/core/bufservices/monograph/publishMonograph.ts @@ -6,9 +6,7 @@ import { PublishMonographRequest, PublishMonographResponse, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; -import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; import { buildSchema } from '../../composition/composition.js'; -import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; import { DefaultNamespace, NamespaceRepository } from '../../repositories/NamespaceRepository.js'; import { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; @@ -35,11 +33,9 @@ export function publishMonograph( opts.logger, opts.billingDefaultPlanId, ); - const auditLogRepo = new AuditLogRepository(opts.db); const namespaceRepo = new NamespaceRepository(opts.db, authContext.organizationId); const subgraphRepo = new SubgraphRepository(logger, opts.db, authContext.organizationId); const federatedGraphRepo = new FederatedGraphRepository(logger, opts.db, authContext.organizationId); - const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId); if (authContext.organizationDeactivated) { throw new UnauthorizedError(); @@ -68,10 +64,6 @@ export function publishMonograph( } const subgraphSchemaSDL = req.schema; - const ignoreExternalKeysFeature = await orgRepo.getFeature({ - organizationId: authContext.organizationId, - featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, - }); let isV2Graph: boolean | undefined; @@ -192,6 +184,7 @@ export function publishMonograph( // Best effort approach. This way of counting tokens is not accurate. subgraphSchemaSDL.length <= 10_000 ) { + const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId); const feature = await orgRepo.getFeature({ organizationId: authContext.organizationId, featureId: 'ai', diff --git a/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts b/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts index 9a5fd6d71b..950197cbb9 100644 --- a/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts +++ b/controlplane/src/core/bufservices/subgraph/fixSubgraphSchema.ts @@ -111,13 +111,10 @@ export function fixSubgraphSchema( organizationId: authContext.organizationId, featureId: 'ai', }); - const ignoreExternalKeys = - ( - await orgRepo.getFeature({ - organizationId: authContext.organizationId, - featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, - }) - )?.enabled ?? false; + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }); if (!feature?.enabled) { return { @@ -137,7 +134,12 @@ export function fixSubgraphSchema( labels: subgraph.labels, namespaceId: namespace.id, }); - // Here we check if the schema is valid as a subgraph + /* Here we check if the schema is valid as a subgraph SDL + * `buildSchema` only calls normalization in isolation. + * The `disableResolvabilityChecks` flag is only used in the federation step. + * The `ignoreExternalKeys` flag is propagated in normalization but only used in the federation step. + * Consequently, there is currently no reason to propagate the options within `buildSchema`. + */ const result = buildSchema( newSchemaSDL, true, @@ -148,9 +150,6 @@ export function fixSubgraphSchema( * compatibility version. */ getFederatedGraphRouterCompatibilityVersion(federatedGraphs), - { - ignoreExternalKeys, - }, ); if (!result.success) { return { @@ -180,7 +179,7 @@ export function fixSubgraphSchema( subgraph.namespaceId, newSchemaSDL, { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, ); diff --git a/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts b/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts index 4a21d45783..3c58df3938 100644 --- a/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts @@ -69,13 +69,10 @@ export function publishFederatedSubgraph( throw new UnauthorizedError(); } - const ignoreExternalKeys = - ( - await orgRepo.getFeature({ - organizationId: authContext.organizationId, - featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, - }) - )?.enabled ?? false; + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }); const subgraphSchemaSDL = req.schema; const namespace = await namespaceRepo.byName(req.namespace); @@ -451,7 +448,7 @@ export function publishFederatedSubgraph( organizationId: authContext.organizationId, featureId: 'plugins', }); - const limit = feature?.limit === -1 ? 0 : feature?.limit ?? 0; + const limit = feature?.limit === -1 ? 0 : (feature?.limit ?? 0); if (count >= limit) { return { response: { @@ -602,7 +599,7 @@ export function publishFederatedSubgraph( }, opts.chClient!, { - ignoreExternalKeys, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, );