Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b72648a
fix: show feature flags for a federated graph under split config
JivusAyrus Jul 30, 2026
c81127e
fix: decouple feature flag compositions from base composition in getC…
JivusAyrus Jul 31, 2026
5bb5a70
fix: update feature flag retrieval method to use latest composition
JivusAyrus Jul 31, 2026
d08ab07
fix: add test for feature flag retrieval when latest composition fails
JivusAyrus Jul 31, 2026
de989c8
fix: lint
JivusAyrus Jul 31, 2026
516d932
Merge branch 'main' into suvij/cosmo-315-controlplane-feature-flags-n…
JivusAyrus Jul 31, 2026
656969d
fix: update feature flag response validation to use Set for uniqueness
JivusAyrus Jul 31, 2026
8d31b00
Merge branches 'suvij/cosmo-315-controlplane-feature-flags-not-shown-…
JivusAyrus Jul 31, 2026
ac3cad5
Merge branch 'main' into suvij/cosmo-315-controlplane-feature-flags-n…
JivusAyrus Jul 31, 2026
a2e8028
fix(controlplane): report feature flags only when they have a valid c…
JivusAyrus Aug 4, 2026
3a81098
fix(controlplane): handle disabled feature flags in getFederatedGraph…
JivusAyrus Aug 5, 2026
147d5a1
chore: add tests
JivusAyrus Aug 5, 2026
06f8b07
feat(controlplane): enhance CompositionErrorsBanner to include view c…
JivusAyrus Aug 5, 2026
24d989a
Merge branch 'main' into suvij/cosmo-315-controlplane-feature-flags-n…
JivusAyrus Aug 5, 2026
727a670
fix: lint
JivusAyrus Aug 5, 2026
1fef1c0
Merge branch 'suvij/cosmo-315-controlplane-feature-flags-not-shown-fo…
JivusAyrus Aug 5, 2026
63c3e2f
fix: pr suggestions
JivusAyrus Aug 11, 2026
d685de5
Merge branch 'main' of github.com:wundergraph/cosmo into suvij/cosmo-…
JivusAyrus Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 24 additions & 13 deletions connect-go/gen/proto/wg/cosmo/platform/v1/platform.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion connect/src/wg/cosmo/platform/v1/platform_pb.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,10 @@ export function getFeatureFlagsInLatestCompositionByFederatedGraph(
throw new UnauthorizedError();
}

if (!federatedGraph.schemaVersionId) {
return {
response: {
code: EnumStatusCode.OK,
},
featureFlags: [],
};
}

// Get feature flag IDs from the latest valid composition
const ffsInLatestValidComposition = await featureFlagRepo.getFeatureFlagSchemaVersionsByBaseSchemaVersion({
baseSchemaVersionId: federatedGraph.schemaVersionId,
const ffsInLatestValidComposition = await featureFlagRepo.getFeatureFlagSchemaVersionsInLatestComposition({
federatedGraphId: federatedGraph.id,
federatedGraphTargetId: federatedGraph.targetId,
});

const featureFlags: FeatureFlagDTO[] = [];
Expand All @@ -83,7 +75,8 @@ export function getFeatureFlagsInLatestCompositionByFederatedGraph(
// A disabled feature flag is no longer served in the latest composition (its router config is
// removed without recomposing), so exclude it even though its schema version rows still exist.
if (flag && flag.isEnabled) {
featureFlags.push(flag);
// True means the composition reported for this flag is its last successful one, not its latest.
featureFlags.push({ ...flag, hasFailedLatestComposition: ff.hasFailedLatestComposition });
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,22 @@ export function getCompositionDetails(
};
}

if (composition.targetId) {
const graph = await fedRepo.byTargetId(composition.targetId);
if (graph && !authContext.rbac.hasFederatedGraphReadAccess(graph)) {
throw new UnauthorizedError();
}
// A composition is always tied to a federated graph (its target is only nulled once the graph is deleted), so an
// unresolvable graph means the composition is orphaned — treat it as not found rather than serving it without an
// authorization check.
const graph = composition.targetId ? await fedRepo.byTargetId(composition.targetId) : undefined;
if (!graph) {
return {
response: {
code: EnumStatusCode.ERR_NOT_FOUND,
details: `Federated graph for composition '${req.compositionId}' not found`,
},
compositionSubgraphs: [],
featureFlagCompositions: [],
};
}
if (!authContext.rbac.hasFederatedGraphReadAccess(graph)) {
throw new UnauthorizedError();
}

const compositionSubgraphs = await compositionRepo.getCompositionSubgraphs({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,15 @@ export function getFederatedGraphById(

const featureFlagsInLatestValidComposition: FeatureFlagDTO[] = [];

if (federatedGraph.schemaVersionId) {
const ffsInLatestValidComposition = await featureFlagRepo.getFeatureFlagSchemaVersionsByBaseSchemaVersion({
baseSchemaVersionId: federatedGraph.schemaVersionId,
});
if (ffsInLatestValidComposition) {
for (const ff of ffsInLatestValidComposition) {
const flag = featureFlags.find((f) => f.id === ff.featureFlagId);
if (flag) {
featureFlagsInLatestValidComposition.push(flag);
}
const ffsInLatestValidComposition = await featureFlagRepo.getFeatureFlagSchemaVersionsInLatestComposition({
federatedGraphId: federatedGraph.id,
federatedGraphTargetId: federatedGraph.targetId,
});
if (ffsInLatestValidComposition) {
for (const ff of ffsInLatestValidComposition) {
const flag = featureFlags.find((f) => f.id === ff.featureFlagId);
if (flag) {
featureFlagsInLatestValidComposition.push(flag);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ export function getFederatedGraphSDLByName(
};
}

if (!featureFlag.isEnabled) {
return {
response: {
code: EnumStatusCode.ERR_NOT_FOUND,
details: `Feature flag ${req.featureFlagName} is disabled`,
},
};
}

const ffSchemaVersion = await featureFlagRepo.getFeatureFlagSchemaVersionByBaseSchemaVersion({
baseSchemaVersionId: schemaVersion.schemaVersionId,
federatedGraphId: federatedGraph.id,
Expand Down
97 changes: 83 additions & 14 deletions controlplane/src/core/repositories/FeatureFlagRepository.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Subgraph } from '@wundergraph/composition';
import { joinLabel, splitLabel } from '@wundergraph/cosmo-shared';
import { SQL, and, asc, count, desc, eq, inArray, like, or, sql, arrayOverlaps, isNull } from 'drizzle-orm';
import { SQL, and, asc, count, desc, eq, inArray, like, or, sql, arrayOverlaps, isNull, isNotNull } from 'drizzle-orm';
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import { FastifyBaseLogger } from 'fastify';
import { validate as isValidUuid } from 'uuid';
Expand Down Expand Up @@ -1493,17 +1493,44 @@ export class FeatureFlagRepository {
return featureFlagCompositions;
}

// return all the feature flag schema versions associated with the base schema version
// input: base schema version id
public async getFeatureFlagSchemaVersionsByBaseSchemaVersion({
baseSchemaVersionId,
public async getFeatureFlagSchemaVersionsInLatestComposition({
federatedGraphId,
federatedGraphTargetId,
}: {
baseSchemaVersionId: string;
federatedGraphId: string;
federatedGraphTargetId: string;
}) {
// A feature flag can have multiple composed schema versions against the same base schema version
// (e.g. recomposing the feature flag recomposes it against the unchanged base, so rows accumulate).
// Deduplicate by feature flag, keeping the latest composed version, so callers get one entry per flag.
const ffSchemaVersions = await this.db
const orgRepo = new OrganizationRepository(this.logger, this.db);
const splitConfigFeature = await orgRepo.getFeature({
organizationId: this.organizationId,
featureId: 'split-config-loading',
});

let baseLinkageCondition;
if (splitConfigFeature?.enabled) {
// Flag compositions are decoupled from the base composition, so the base schema version is irrelevant here.
baseLinkageCondition = and(
isNull(federatedGraphsToFeatureFlagSchemaVersions.baseCompositionSchemaVersionId),
eq(federatedGraphsToFeatureFlagSchemaVersions.federatedGraphId, federatedGraphId),
);
} else {
const federatedGraphRepo = new FederatedGraphRepository(this.logger, this.db, this.organizationId);
const latestValidBaseSchemaVersion = await federatedGraphRepo.getLatestValidSchemaVersion({
targetId: federatedGraphTargetId,
});

if (!latestValidBaseSchemaVersion) {
return;
}

baseLinkageCondition = eq(
federatedGraphsToFeatureFlagSchemaVersions.baseCompositionSchemaVersionId,
latestValidBaseSchemaVersion.schemaVersionId,
);
}

// The latest composition per flag that actually succeeded and deployed; this is what we return.
const validSchemaVersions = await this.db
.selectDistinctOn([federatedGraphsToFeatureFlagSchemaVersions.featureFlagId], {
id: federatedGraphsToFeatureFlagSchemaVersions.composedSchemaVersionId,
featureFlagId: federatedGraphsToFeatureFlagSchemaVersions.featureFlagId,
Expand All @@ -1513,15 +1540,51 @@ export class FeatureFlagRepository {
schemaVersion,
eq(schemaVersion.id, federatedGraphsToFeatureFlagSchemaVersions.composedSchemaVersionId),
)
.where(eq(federatedGraphsToFeatureFlagSchemaVersions.baseCompositionSchemaVersionId, baseSchemaVersionId))
.innerJoin(graphCompositions, eq(graphCompositions.schemaVersionId, schemaVersion.id))
.where(
and(
baseLinkageCondition,
isNotNull(federatedGraphsToFeatureFlagSchemaVersions.featureFlagId),
eq(graphCompositions.isComposable, true),
or(isNull(graphCompositions.deploymentError), eq(graphCompositions.deploymentError, '')),
or(isNull(graphCompositions.admissionError), eq(graphCompositions.admissionError, '')),
),
)
.orderBy(federatedGraphsToFeatureFlagSchemaVersions.featureFlagId, desc(schemaVersion.createdAt))
.execute();

if (ffSchemaVersions.length === 0) {
if (validSchemaVersions.length === 0) {
return;
}

return ffSchemaVersions;
if (!splitConfigFeature?.enabled) {
return validSchemaVersions.map((version) => ({
...version,
hasFailedLatestComposition: false,
}));
}

// The latest composition per flag regardless of status, used only to detect a newer failed composition.
const latestSchemaVersions = await this.db
.selectDistinctOn([federatedGraphsToFeatureFlagSchemaVersions.featureFlagId], {
id: federatedGraphsToFeatureFlagSchemaVersions.composedSchemaVersionId,
featureFlagId: federatedGraphsToFeatureFlagSchemaVersions.featureFlagId,
})
.from(federatedGraphsToFeatureFlagSchemaVersions)
.innerJoin(
schemaVersion,
eq(schemaVersion.id, federatedGraphsToFeatureFlagSchemaVersions.composedSchemaVersionId),
)
.where(and(baseLinkageCondition, isNotNull(federatedGraphsToFeatureFlagSchemaVersions.featureFlagId)))
.orderBy(federatedGraphsToFeatureFlagSchemaVersions.featureFlagId, desc(schemaVersion.createdAt))
.execute();

const latestIdByFeatureFlagId = new Map(latestSchemaVersions.map((version) => [version.featureFlagId, version.id]));

return validSchemaVersions.map((version) => ({
...version,
hasFailedLatestComposition: latestIdByFeatureFlagId.get(version.featureFlagId) !== version.id,
}));
}

/*
Expand Down Expand Up @@ -1598,16 +1661,22 @@ export class FeatureFlagRepository {
schemaVersion,
eq(schemaVersion.id, federatedGraphsToFeatureFlagSchemaVersions.composedSchemaVersionId),
)
.innerJoin(graphCompositions, eq(graphCompositions.schemaVersionId, schemaVersion.id))
.where(
/**
* When split config is enabled, the feature flag composition will not be tied to the base schema version, so
* we need to check that the baseCompositionSchemaVersionId is null
* we need to check that the baseCompositionSchemaVersionId is null. We also require the composition to have
* succeeded, otherwise a failed latest composition resolves to a null SDL and the caller 404s instead of
* serving the last valid schema.
*/
splitConfigFeature?.enabled
? and(
isNull(federatedGraphsToFeatureFlagSchemaVersions.baseCompositionSchemaVersionId),
eq(federatedGraphsToFeatureFlagSchemaVersions.featureFlagId, featureFlagId),
eq(federatedGraphsToFeatureFlagSchemaVersions.federatedGraphId, federatedGraphId),
eq(graphCompositions.isComposable, true),
or(isNull(graphCompositions.deploymentError), eq(graphCompositions.deploymentError, '')),
or(isNull(graphCompositions.admissionError), eq(graphCompositions.admissionError, '')),
)
: and(
eq(federatedGraphsToFeatureFlagSchemaVersions.baseCompositionSchemaVersionId, baseSchemaVersionId),
Expand Down
1 change: 1 addition & 0 deletions controlplane/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export interface FeatureFlagDTO {
createdAt: string;
updatedAt: string;
featureSubgraphs: FeatureSubgraphDTO[];
hasFailedLatestComposition?: boolean;
}

export interface MigrationSubgraph {
Expand Down
Loading
Loading