diff --git a/cli/src/commands/router/commands/compose.ts b/cli/src/commands/router/commands/compose.ts index 0feda11d9f..4da637a3b1 100644 --- a/cli/src/commands/router/commands/compose.ts +++ b/cli/src/commands/router/commands/compose.ts @@ -171,6 +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.action(async (options) => { const inputFile = resolve(options.input); @@ -208,7 +209,7 @@ export default (opts: BaseCommandOptions) => { }; }), { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys: options.ignoreExternalKeys, disableResolvabilityValidation: options.disableResolvabilityValidation, }, ); @@ -591,7 +592,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..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'; @@ -104,6 +105,10 @@ export function createContract( organizationId: authContext.organizationId, featureId: 'federated-graphs', }); + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }); 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: 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 7cfb751a3f..12036cc10b 100644 --- a/controlplane/src/core/bufservices/contract/updateContract.ts +++ b/controlplane/src/core/bufservices/contract/updateContract.ts @@ -9,10 +9,12 @@ 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'; 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 +35,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 +119,14 @@ export function updateContract( }; } + 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, excludeTags: req.excludeTags, @@ -141,7 +152,7 @@ export function updateContract( labelMatchers: [], chClient: opts.chClient!, compositionOptions: { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys, disableResolvabilityValidation: req.disableResolvabilityValidation, }, }); @@ -159,7 +170,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..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'; @@ -189,6 +190,10 @@ export function createFeatureFlag( namespaceId: namespace.id, excludeDisabled: true, }); + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }); const compositionErrors: PlainMessage[] = []; const deploymentErrors: PlainMessage[] = []; @@ -206,7 +211,7 @@ export function createFeatureFlag( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - // @TODO 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 9cfb20e079..5235ad2417 100644 --- a/controlplane/src/core/bufservices/feature-flag/deleteFeatureFlag.ts +++ b/controlplane/src/core/bufservices/feature-flag/deleteFeatureFlag.ts @@ -9,10 +9,12 @@ 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'; 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 +33,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 +105,11 @@ export function deleteFeatureFlag( }); } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }); + 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: 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 9aea579dd0..055c410fa5 100644 --- a/controlplane/src/core/bufservices/feature-flag/enableFeatureFlag.ts +++ b/controlplane/src/core/bufservices/feature-flag/enableFeatureFlag.ts @@ -9,10 +9,12 @@ 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'; 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 +33,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 +106,10 @@ 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_FEATURE_ID, + }); const compositionErrors: PlainMessage[] = []; const deploymentErrors: PlainMessage[] = []; @@ -120,7 +127,7 @@ export function enableFeatureFlag( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - // @TODO 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 ac8bf5f972..0d4e736d19 100644 --- a/controlplane/src/core/bufservices/feature-flag/updateFeatureFlag.ts +++ b/controlplane/src/core/bufservices/feature-flag/updateFeatureFlag.ts @@ -9,11 +9,12 @@ 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'; 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,11 @@ export function updateFeatureFlag( allFederatedGraphsToCompose.push(newFederatedGraph); } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }); + const compositionErrors: PlainMessage[] = []; const deploymentErrors: PlainMessage[] = []; const compositionWarnings: PlainMessage[] = []; @@ -175,7 +182,7 @@ export function updateFeatureFlag( blobStorage: opts.blobStorage, chClient: opts.chClient!, compositionOptions: { - // @TODO 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 4ed48d8bbf..3d84acca3e 100644 --- a/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts +++ b/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts @@ -9,9 +9,11 @@ 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'; +import { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; import { SubgraphRepository } from '../../repositories/SubgraphRepository.js'; import type { RouterOptions } from '../../routes.js'; import { @@ -38,6 +40,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 +103,11 @@ export function checkFederatedGraph( type: convertToSubgraphType(s.type), })); + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }); + const result = composeSubgraphs( subgraphsUsedForComposition.map((s) => ({ id: s.id, @@ -109,7 +117,7 @@ export function checkFederatedGraph( })), federatedGraph.routerCompatibilityVersion, { - // @TODO 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 596aa61c71..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'; @@ -210,6 +211,11 @@ export function createFederatedGraph( }; } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }); + 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: 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 d9089182af..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'; @@ -131,6 +131,11 @@ export function migrateFromApollo( } } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }); + await opts.db.transaction(async (tx) => { const fedGraphRepo = new FederatedGraphRepository(logger, tx, authContext.organizationId); @@ -157,6 +162,7 @@ export function migrateFromApollo( }, chClient: opts.chClient!, compositionOptions: { + 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 c94c6d255b..399cfa548d 100644 --- a/controlplane/src/core/bufservices/federated-graph/updateFederatedGraph.ts +++ b/controlplane/src/core/bufservices/federated-graph/updateFederatedGraph.ts @@ -10,9 +10,11 @@ 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'; +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 +32,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 +109,11 @@ export function updateFederatedGraph( }; } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }); + 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: 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 466cfecce9..e12cf99a6c 100644 --- a/controlplane/src/core/bufservices/graph/setGraphRouterCompatibilityVersion.ts +++ b/controlplane/src/core/bufservices/graph/setGraphRouterCompatibilityVersion.ts @@ -6,8 +6,10 @@ 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'; import type { RouterOptions } from '../../routes.js'; import { enrichLogger, getLogger, handleError } from '../../util.js'; import { AuditLogRepository } from '../../repositories/AuditLogRepository.js'; @@ -30,6 +32,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 +113,11 @@ export function setGraphRouterCompatibilityVersion( }; } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }); + 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: 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 9fd48dfeba..bb30418df7 100644 --- a/controlplane/src/core/bufservices/monograph/publishMonograph.ts +++ b/controlplane/src/core/bufservices/monograph/publishMonograph.ts @@ -7,7 +7,6 @@ import { PublishMonographResponse, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; 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'; @@ -34,7 +33,6 @@ 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); @@ -70,7 +68,12 @@ export function publishMonograph( let isV2Graph: boolean | undefined; try { - // Here we check if the schema is valid as a subgraph SDL + /* 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 { diff --git a/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts b/controlplane/src/core/bufservices/subgraph/checkSubgraphSchema.ts index b37d3ae0cb..7807f22263 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'; @@ -189,6 +190,13 @@ export function checkSubgraphSchema( } const subgraphName = subgraph?.name || req.subgraphName; + 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, @@ -205,7 +213,12 @@ export function checkSubgraphSchema( let newGraphQLSchema: GraphQLSchema | undefined; if (newSchemaSDL) { try { - // Here we check if the schema is valid as a subgraph SDL + /* 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 { @@ -281,7 +294,10 @@ export function checkSubgraphSchema( limit, chClient: opts.chClient, newGraphQLSchema, - disableResolvabilityValidation: req.disableResolvabilityValidation, + compositionOptions: { + disableResolvabilityValidation: req.disableResolvabilityValidation, + ignoreExternalKeys, + }, webhookService, }); @@ -449,7 +465,10 @@ export function checkSubgraphSchema( limit: targetLimit, chClient: opts.chClient, newGraphQLSchema: targetNewGraphQLSchema, - disableResolvabilityValidation: req.disableResolvabilityValidation, + compositionOptions: { + 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..394985b6d7 100644 --- a/controlplane/src/core/bufservices/subgraph/deleteFederatedSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/deleteFederatedSubgraph.ts @@ -6,10 +6,12 @@ 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'; 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 +34,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 +124,11 @@ export function deleteFederatedSubgraph( } } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }); + 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: 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 4fa100bcdf..950197cbb9 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'; @@ -110,6 +111,10 @@ export function fixSubgraphSchema( organizationId: authContext.organizationId, featureId: 'ai', }); + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }); if (!feature?.enabled) { return { @@ -129,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, @@ -169,7 +179,7 @@ export function fixSubgraphSchema( subgraph.namespaceId, newSchemaSDL, { - // @TODO 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 b50ea791ef..2fff2508d7 100644 --- a/controlplane/src/core/bufservices/subgraph/moveSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/moveSubgraph.ts @@ -3,10 +3,12 @@ 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'; 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 +27,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 +85,11 @@ export function moveSubgraph( authContext, }); + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }); + 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: 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 3902421083..3c58df3938 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'; @@ -68,6 +69,11 @@ export function publishFederatedSubgraph( throw new UnauthorizedError(); } + 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); if (!namespace) { @@ -97,7 +103,12 @@ 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 + /* 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 { @@ -588,7 +599,7 @@ export function publishFederatedSubgraph( }, opts.chClient!, { - // @TODO 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 6374ea01fb..3567045124 100644 --- a/controlplane/src/core/bufservices/subgraph/updateSubgraph.ts +++ b/controlplane/src/core/bufservices/subgraph/updateSubgraph.ts @@ -8,8 +8,10 @@ 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'; import { SubgraphRepository } from '../../repositories/SubgraphRepository.js'; import type { RouterOptions } from '../../routes.js'; import { @@ -37,6 +39,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 +188,11 @@ export function updateSubgraph( throw new UnauthorizedError(); } + const ignoreExternalKeysFeature = await orgRepo.getFeature({ + organizationId: authContext.organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }); + const { compositionErrors, updatedFederatedGraphs, deploymentErrors, compositionWarnings } = await subgraphRepo.update( { @@ -208,7 +216,7 @@ export function updateSubgraph( }, opts.chClient!, { - // @TODO ignoreExternalKeys: ?, + ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, disableResolvabilityValidation: req.disableResolvabilityValidation, }, ); diff --git a/controlplane/src/core/repositories/OrganizationRepository.ts b/controlplane/src/core/repositories/OrganizationRepository.ts index a374b860fd..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,6 +1397,7 @@ export class OrganizationRepository { scim: false, 'cache-warmer': false, proposals: 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 81cdd0c234..77107956c8 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, @@ -751,6 +752,13 @@ export class SchemaCheckRepository { organizationId, featureId: 'breaking-change-retention', }); + const ignoreExternalKeys = + ( + await orgRepo.getFeature({ + organizationId, + featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, + }) + )?.enabled ?? false; const limit = changeRetention?.limit ?? defaultRetentionLimitInDays; @@ -819,7 +827,12 @@ export class SchemaCheckRepository { let newGraphQLSchema: GraphQLSchema | undefined; if (newSchemaSDL) { try { - // Here we check if the schema is valid as a subgraph SDL + /* 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({ @@ -851,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) { @@ -1109,6 +1122,10 @@ export class SchemaCheckRepository { } const { composedGraphs } = await composer.composeWithProposedSchemas({ + compositionOptions: { + disableResolvabilityValidation: false, + ignoreExternalKeys, + }, inputSubgraphs: checkSubgraphs, graphs: federatedGraphs.filter((g) => !g.contract), }); @@ -1368,7 +1385,10 @@ export class SchemaCheckRepository { limit: targetLimit, chClient, newGraphQLSchema: targetNewGraphQLSchema, - disableResolvabilityValidation: false, + compositionOptions: { + disableResolvabilityValidation: false, + ignoreExternalKeys, + }, webhookService, }); diff --git a/controlplane/src/core/repositories/SubgraphRepository.ts b/controlplane/src/core/repositories/SubgraphRepository.ts index 1e32180bbb..eaadcd5653 100644 --- a/controlplane/src/core/repositories/SubgraphRepository.ts +++ b/controlplane/src/core/repositories/SubgraphRepository.ts @@ -1861,7 +1861,7 @@ export class SubgraphRepository { limit, chClient, newGraphQLSchema, - disableResolvabilityValidation, + compositionOptions, webhookService, }: { actorId: string; @@ -1885,7 +1885,7 @@ export class SubgraphRepository { limit: number; chClient?: ClickHouseClient; newGraphQLSchema?: GraphQLSchema; - disableResolvabilityValidation?: boolean; + compositionOptions?: CompositionOptions; webhookService: OrganizationWebhookService; }): Promise< PlainMessage & { @@ -2069,10 +2069,7 @@ export class SubgraphRepository { }); const { composedGraphs } = await composer.composeWithProposedSchemas({ - compositionOptions: { - // @TODO ignoreExternalKeys: ?, - disableResolvabilityValidation, - }, + compositionOptions, graphs: federatedGraphs.filter((g) => !g.contract), inputSubgraphs: checkSubgraphs, }); diff --git a/controlplane/src/types/index.ts b/controlplane/src/types/index.ts index 9d335e65fc..8f2e9ce77c 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,6 +27,7 @@ export type FeatureIds = | 'cache-warmer' | 'proposals' | 'plugins' + | 'composition-ignore-external-keys' // 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 440bf6f822..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 = ''; @@ -265,4 +266,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_FEATURE_ID], + }); + 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(); + }); });