Skip to content

Commit e81b5fa

Browse files
authored
feat: resolve feature subgraphs in federated graph field usage (#3144)
1 parent 5edbee2 commit e81b5fa

8 files changed

Lines changed: 944 additions & 877 deletions

File tree

connect-go/gen/proto/wg/cosmo/platform/v1/platform.pb.go

Lines changed: 862 additions & 851 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

connect/src/wg/cosmo/platform/v1/platform_pb.ts

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

controlplane/src/core/bufservices/federated-graph/getFederatedGraphByName.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import {
77
} from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb';
88
import { PlainMessage } from '../../../types/index.js';
99
import { UnauthorizedError } from '../../errors/errors.js';
10+
import { FeatureFlagRepository } from '../../repositories/FeatureFlagRepository.js';
1011
import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js';
1112
import { DefaultNamespace } from '../../repositories/NamespaceRepository.js';
1213
import { SubgraphRepository } from '../../repositories/SubgraphRepository.js';
1314
import { AnalyticsDashboardViewRepository } from '../../repositories/analytics/AnalyticsDashboardViewRepository.js';
1415
import type { RouterOptions } from '../../routes.js';
15-
import { convertToSubgraphType, enrichLogger, getLogger, handleError } from '../../util.js';
16+
import { convertToSubgraphProto, enrichLogger, getLogger, handleError } from '../../util.js';
1617

1718
export function getFederatedGraphByName(
1819
opts: RouterOptions,
@@ -27,13 +28,15 @@ export function getFederatedGraphByName(
2728

2829
const fedRepo = new FederatedGraphRepository(logger, opts.db, authContext.organizationId);
2930
const subgraphRepo = new SubgraphRepository(logger, opts.db, authContext.organizationId);
31+
const featureFlagRepo = new FeatureFlagRepository(logger, opts.db, authContext.organizationId);
3032

3133
req.namespace = req.namespace || DefaultNamespace;
3234

3335
const federatedGraph = await fedRepo.byName(req.name, req.namespace);
3436
if (!federatedGraph) {
3537
return {
3638
subgraphs: [],
39+
featureSubgraphs: [],
3740
graphRequestToken: '',
3841
response: {
3942
code: EnumStatusCode.ERR_NOT_FOUND,
@@ -58,6 +61,16 @@ export function getFederatedGraphByName(
5861
rbac: authContext.rbac,
5962
});
6063

64+
// Feature subgraphs of the feature flags that match this federated graph
65+
const { featureSubgraphs } = await featureFlagRepo.getFeatureSubgraphsByFederatedGraph({
66+
federatedGraphId: federatedGraph.id,
67+
namespaceId: federatedGraph.namespaceId,
68+
fedGraphLabelMatchers: federatedGraph.labelMatchers,
69+
limit: 0,
70+
offset: 0,
71+
rbac: authContext.rbac,
72+
});
73+
6174
const routerRequestToken = await fedRepo.getGraphSignedToken({
6275
federatedGraphId: federatedGraph.id,
6376
organizationId: authContext.organizationId,
@@ -66,6 +79,7 @@ export function getFederatedGraphByName(
6679
if (!routerRequestToken) {
6780
return {
6881
subgraphs: [],
82+
featureSubgraphs: [],
6983
graphRequestToken: '',
7084
response: {
7185
code: EnumStatusCode.ERR,
@@ -94,22 +108,8 @@ export function getFederatedGraphByName(
94108
admissionWebhookUrl: federatedGraph.admissionWebhookURL,
95109
routerCompatibilityVersion: federatedGraph.routerCompatibilityVersion,
96110
},
97-
subgraphs: list.map((g) => ({
98-
id: g.id,
99-
name: g.name,
100-
routingURL: g.routingUrl,
101-
lastUpdatedAt: g.lastUpdatedAt,
102-
labels: g.labels,
103-
targetId: g.targetId,
104-
subscriptionUrl: g.subscriptionUrl,
105-
namespace: g.namespace,
106-
subscriptionProtocol: g.subscriptionProtocol,
107-
isEventDrivenGraph: g.isEventDrivenGraph,
108-
isV2Graph: g.isV2Graph,
109-
websocketSubprotocol: g.websocketSubprotocol || '',
110-
isFeatureSubgraph: g.isFeatureSubgraph,
111-
type: convertToSubgraphType(g.type),
112-
})),
111+
subgraphs: list.map((g) => convertToSubgraphProto(g)),
112+
featureSubgraphs: featureSubgraphs.map((g) => convertToSubgraphProto(g)),
113113
graphRequestToken: routerRequestToken,
114114
response: {
115115
code: EnumStatusCode.OK,

controlplane/src/core/repositories/FeatureFlagRepository.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,13 +461,15 @@ export class FeatureFlagRepository {
461461
limit,
462462
offset,
463463
query,
464+
rbac,
464465
}: {
465466
federatedGraphId: string;
466467
namespaceId: string;
467468
fedGraphLabelMatchers: string[];
468469
limit: number;
469470
offset: number;
470471
query?: string;
472+
rbac?: RBACEvaluator;
471473
}): Promise<{ featureSubgraphs: FeatureSubgraphDTO[]; totalCount: number }> {
472474
const subgraphRepo = new SubgraphRepository(this.logger, this.db, this.organizationId);
473475

@@ -495,6 +497,14 @@ export class FeatureFlagRepository {
495497
conditions.push(isValidUuid(query) ? eq(subgraphs.id, query) : like(targets.name, `%${query}%`));
496498
}
497499

500+
if (
501+
rbac &&
502+
(!applyIdpNamespaceGate(rbac, targets.namespaceId, conditions) ||
503+
!this.applyRbacConditionsToQuery(rbac, conditions))
504+
) {
505+
return { featureSubgraphs: [], totalCount: 0 };
506+
}
507+
498508
const baseSubgraphs = alias(subgraphs, 'base_subgraphs');
499509
const baseTargets = alias(targets, 'base_targets');
500510
const baseQuery = this.db
@@ -552,7 +562,7 @@ export class FeatureFlagRepository {
552562
const pendingFeatureSubgraphs = featureSubgraphTargets.map((target) => target.targetId);
553563
while (pendingFeatureSubgraphs.length > 0) {
554564
const chunkOfIdsToFetch = pendingFeatureSubgraphs.splice(0, 100);
555-
const chunkOfSubgraphs = await subgraphRepo.getSubgraphsByTargetIds(chunkOfIdsToFetch);
565+
const chunkOfSubgraphs = await subgraphRepo.getSubgraphsByTargetIds(chunkOfIdsToFetch, rbac);
556566
a.push(...chunkOfSubgraphs);
557567
}
558568

controlplane/src/core/util.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { parse, visit } from 'graphql';
1818
import { uid } from 'uid/secure';
1919
import DOMPurify from 'isomorphic-dompurify';
2020
import { LATEST_ROUTER_COMPATIBILITY_VERSION } from '@wundergraph/composition';
21-
import { ProposalOrigin, SubgraphType } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb';
21+
import { ProposalOrigin, Subgraph, SubgraphType } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb';
2222
import { MemberRole, ProposalOrigin as ProposalOriginEnum, WebsocketSubprotocol } from '../db/models.js';
2323
import {
2424
AuthContext,
@@ -27,10 +27,12 @@ import {
2727
Label,
2828
LoginMethod,
2929
NamespaceAccess,
30+
PlainMessage,
3031
ResponseMessage,
3132
S3StorageOptions,
3233
SOCIAL_LOGIN_PROVIDERS,
3334
SocialLoginProvider,
35+
SubgraphDTO,
3436
} from '../types/index.js';
3537
import { paginationDefaults } from './constants.js';
3638
import {
@@ -700,6 +702,28 @@ export const convertToSubgraphType = (type: string) => {
700702
}
701703
};
702704

705+
/**
706+
* Maps a subgraph (or feature subgraph) DTO to its proto representation.
707+
*/
708+
export function convertToSubgraphProto(subgraph: SubgraphDTO): PlainMessage<Subgraph> {
709+
return {
710+
id: subgraph.id,
711+
name: subgraph.name,
712+
routingURL: subgraph.routingUrl,
713+
lastUpdatedAt: subgraph.lastUpdatedAt,
714+
labels: subgraph.labels,
715+
targetId: subgraph.targetId,
716+
subscriptionUrl: subgraph.subscriptionUrl,
717+
namespace: subgraph.namespace,
718+
subscriptionProtocol: subgraph.subscriptionProtocol,
719+
isEventDrivenGraph: subgraph.isEventDrivenGraph,
720+
isV2Graph: subgraph.isV2Graph,
721+
websocketSubprotocol: subgraph.websocketSubprotocol || '',
722+
isFeatureSubgraph: subgraph.isFeatureSubgraph,
723+
type: convertToSubgraphType(subgraph.type),
724+
};
725+
}
726+
703727
export function toProposalOriginEnum(value: ProposalOrigin): ProposalOriginEnum {
704728
switch (value) {
705729
case ProposalOrigin.EXTERNAL: {

proto/wg/cosmo/platform/v1/platform.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,8 @@ message GetFederatedGraphByNameResponse {
518518
FederatedGraph graph = 2;
519519
repeated Subgraph subgraphs = 3;
520520
string graphRequestToken = 4;
521+
// The feature subgraphs that are part of the feature flags which match this federated graph.
522+
repeated Subgraph featureSubgraphs = 5;
521523
}
522524

523525
message GetFederatedGraphSDLByNameRequest {

studio/src/components/analytics/field-usage.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ export const FieldUsage = ({ usageData }: { usageData: GetFieldUsageResponse })
3939
const organizationSlug = useCurrentOrganization()?.slug;
4040
const slug = router.query.slug;
4141

42-
const subgraphs = useContext(GraphContext)?.subgraphs ?? [];
42+
const graphContext = useContext(GraphContext);
43+
const subgraphs = graphContext?.subgraphs ?? [];
44+
const featureSubgraphs = graphContext?.featureSubgraphs ?? [];
4345

4446
const { range, dateRange } = useAnalyticsQueryState();
4547

@@ -243,9 +245,21 @@ export const FieldUsage = ({ usageData }: { usageData: GetFieldUsageResponse })
243245
<h2 className="text-lg font-semibold">Subgraphs: </h2>
244246
<div className="mt-[2px] grid w-max grid-cols-3 gap-x-8">
245247
{usageData.meta.subgraphIds.map((id) => {
246-
const subgraph = subgraphs.find((s) => s.id === id);
248+
const subgraph = [...subgraphs, ...featureSubgraphs].find((s) => s.id === id);
247249
if (!subgraph) return null;
248250

251+
const content = (
252+
<div className="flex items-start gap-x-1">
253+
<CubeIcon className="mt-1.5 flex-shrink-0 break-all" />
254+
{subgraph.name}
255+
</div>
256+
);
257+
258+
// Feature subgraphs are not part of the federated graph, so there is no page to link to
259+
if (subgraph.isFeatureSubgraph) {
260+
return <div key={id}>{content}</div>;
261+
}
262+
249263
return (
250264
<Link
251265
key={id}
@@ -257,10 +271,7 @@ export const FieldUsage = ({ usageData }: { usageData: GetFieldUsageResponse })
257271
})}
258272
className="text-primary"
259273
>
260-
<div className="flex items-start gap-x-1">
261-
<CubeIcon className="mt-1.5 flex-shrink-0 break-all" />
262-
{subgraph.name}
263-
</div>
274+
{content}
264275
</Link>
265276
);
266277
})}

studio/src/components/layout/graph-layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { buildUrl } from '@/lib/build-url';
3939
export interface GraphContextProps {
4040
graph: GetFederatedGraphByNameResponse['graph'];
4141
subgraphs: GetFederatedGraphByNameResponse['subgraphs'];
42+
featureSubgraphs: GetFederatedGraphByNameResponse['featureSubgraphs'];
4243
graphs: GetFederatedGraphsResponse['graphs'];
4344
graphRequestToken: string;
4445
}
@@ -205,6 +206,7 @@ export const GraphLayout = ({ children }: LayoutProps) => {
205206
return {
206207
graph: data.graph,
207208
subgraphs: data.subgraphs,
209+
featureSubgraphs: data.featureSubgraphs,
208210
graphRequestToken: data.graphRequestToken,
209211
graphs: graphsData.graphs,
210212
};

0 commit comments

Comments
 (0)