Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1,080 changes: 544 additions & 536 deletions connect-go/gen/proto/wg/cosmo/platform/v1/platform.pb.go

Large diffs are not rendered by default.

15 changes: 10 additions & 5 deletions connect/src/wg/cosmo/platform/v1/platform_pb.ts

Large diffs are not rendered by default.

169 changes: 80 additions & 89 deletions controlplane/src/core/services/WorkspaceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,33 +40,82 @@ export class WorkspaceService {
id: ns.id,
name: ns.name,
graphs: [],
subgraphs: [],
}),
)
.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }));

// Step 3 - Retrieve all the federated graphs the actor has access to, based on the namespaces
const federatedGraphs = await this.fetchFederatedGraphs(result);
if (federatedGraphs === 0) {
return result;
}
// Step 3 - Retrieve all the subgraphs the actor has access to, based on the namespace
await this.fetchSubgraphs(result);

// Step 4 - Retrieve all the subgraphs the actor has access to, based on the federated graphs
await this.fetchSubgraphsForFederatedGraphs(result);
// Step 4 - Retrieve all the federated graphs the actor has access to, based on the namespaces
await this.fetchFederatedGraphs(result);

// Step 5 - Retrieve all the feature subgraphs the actor has access to, based on the namespaces
await this.fetchFeatureSubgraphs(result);
// Step 5 - Retrieve all the subgraphs the actor has access to, based on the federated graphs
await this.fetchSubgraphsForFederatedGraphs(result);

return result;
}

private async fetchSubgraphs(namespaces: PlainMessage<WorkspaceNamespace>[]): Promise<void> {
if (namespaces.length === 0) {
return;
}

const conditions: (SQL<unknown> | undefined)[] = [
eq(schema.targets.type, 'subgraph'),
eq(schema.targets.organizationId, this.organizationId),
inArray(
schema.targets.namespaceId,
namespaces.map((ns) => ns.id),
),
];

if (!SubgraphRepository.applyRbacConditionsToQuery(this.rbac, conditions)) {
return;
}

const subgraphs = await this.db
.select({
id: schema.subgraphs.id,
targetId: schema.targets.id,
name: schema.targets.name,
namespaceId: schema.targets.namespaceId,
baseSubgraphId: schema.featureSubgraphsToBaseSubgraphs.baseSubgraphId,
isFeatureSubgraph: schema.subgraphs.isFeatureSubgraph,
})
.from(schema.targets)
.innerJoin(schema.subgraphs, eq(schema.subgraphs.targetId, schema.targets.id))
.leftJoin(
schema.featureSubgraphsToBaseSubgraphs,
eq(schema.featureSubgraphsToBaseSubgraphs.featureSubgraphId, schema.subgraphs.id),
)
.where(and(...conditions))
.execute();

for (const namespace of namespaces) {
namespace.subgraphs = subgraphs
.filter((fsg) => fsg.namespaceId === namespace.id)
.map((fsg) =>
fromJson(WorkspaceSubgraphSchema, {
id: fsg.id,
targetId: fsg.targetId,
name: fsg.name,
isFeatureSubgraph: fsg.isFeatureSubgraph,
baseSubgraphId: fsg.baseSubgraphId,
}),
)
.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }));
}
}

/**
* Fetches all the federated graphs the actor has access to based on the provided namespaces.
*
* @private
* @param namespaces The namespaces to fetch the federated graphs for
* @returns The number of federated graphs fetched
*/
private async fetchFederatedGraphs(namespaces: PlainMessage<WorkspaceNamespace>[]): Promise<number> {
private async fetchFederatedGraphs(namespaces: PlainMessage<WorkspaceNamespace>[]): Promise<void> {
const conditions: SQL<unknown>[] = [
eq(schema.targets.type, 'federated'),
eq(schema.targets.organizationId, this.organizationId),
Expand All @@ -78,7 +127,7 @@ export class WorkspaceService {

if (!FederatedGraphRepository.applyRbacConditionsToQuery(this.rbac, conditions)) {
// The actor doesn't have access to any federated graph, no need to continue
return 0;
return;
}

// Retrieve the federated graphs from the database
Expand All @@ -97,39 +146,37 @@ export class WorkspaceService {
.execute();

// Map the federated graphs to the corresponding namespace
let numberOfFetchedGraphs = 0;
for (const namespace of namespaces) {
const namespaceGraphs = federatedGraphs.filter((graph) => graph.namespaceId === namespace.id);
if (namespaceGraphs.length === 0) {
continue;
}

numberOfFetchedGraphs += namespaceGraphs.length;
namespace.graphs = namespaceGraphs
.map((graph) =>
fromJson(WorkspaceFederatedGraphSchema, {
id: graph.id,
targetId: graph.targetId,
name: graph.name,
isContract: !!graph.contractId,
subgraphs: [],
subgraphTargetIds: [],
}),
)
.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }));
}

return numberOfFetchedGraphs;
}

private async fetchSubgraphsForFederatedGraphs(namespaces: PlainMessage<WorkspaceNamespace>[]): Promise<void> {
const federatedGraphIds = namespaces.flatMap((ns) => ns.graphs.map((graph) => graph.id));
if (federatedGraphIds.length === 0) {
return;
}

const conditions: (SQL<unknown> | undefined)[] = [
eq(schema.targets.organizationId, this.organizationId),
eq(schema.targets.type, 'subgraph'),
eq(schema.subgraphs.isFeatureSubgraph, false),
inArray(
schema.subgraphsToFederatedGraph.federatedGraphId,
namespaces.flatMap((ns) => ns.graphs.map((graph) => graph.id)),
),
inArray(schema.subgraphsToFederatedGraph.federatedGraphId, federatedGraphIds),
];

if (!SubgraphRepository.applyRbacConditionsToQuery(this.rbac, conditions)) {
Expand All @@ -138,84 +185,28 @@ export class WorkspaceService {

const targetSubgraphs = await this.db
.selectDistinct({
id: schema.subgraphs.id,
targetId: schema.targets.id,
federatedGraphId: schema.subgraphsToFederatedGraph.federatedGraphId,
name: schema.targets.name,
isFeatureSubgraph: schema.subgraphs.isFeatureSubgraph,
})
.from(schema.targets)
.innerJoin(schema.subgraphs, eq(schema.subgraphs.targetId, schema.targets.id))
.innerJoin(schema.subgraphsToFederatedGraph, eq(schema.subgraphsToFederatedGraph.subgraphId, schema.subgraphs.id))
.where(and(...conditions))
.execute();

const federatedGraphs = namespaces.flatMap((ns) => ns.graphs);
for (const graph of federatedGraphs) {
const subgraphs = targetSubgraphs.filter((sg) => sg.federatedGraphId === graph.id);
if (subgraphs.length === 0) {
continue;
for (const ns of namespaces) {
for (const graph of ns.graphs) {
const subgraphs = targetSubgraphs.filter((sg) => sg.federatedGraphId === graph.id);
if (subgraphs.length === 0) {
continue;
}

graph.subgraphTargetIds = subgraphs
.map((sg) => ns.subgraphs.find((nssg) => nssg.targetId === sg.targetId)!)
.filter(Boolean)
.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }))
.map((sg) => sg.targetId);
}

graph.subgraphs = subgraphs
.map((sg) =>
fromJson(WorkspaceSubgraphSchema, {
id: sg.id,
targetId: sg.targetId,
name: sg.name,
}),
)
.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }));
}
}

private async fetchFeatureSubgraphs(namespaces: PlainMessage<WorkspaceNamespace>[]): Promise<void> {
if (namespaces.length === 0) {
return;
}

const conditions: (SQL<unknown> | undefined)[] = [
eq(schema.targets.type, 'subgraph'),
eq(schema.targets.organizationId, this.organizationId),
eq(schema.subgraphs.isFeatureSubgraph, true),
inArray(
schema.targets.namespaceId,
namespaces.map((ns) => ns.id),
),
];

if (!SubgraphRepository.applyRbacConditionsToQuery(this.rbac, conditions)) {
return;
}

const featureSubgraphs = await this.db
.select({
id: schema.subgraphs.id,
targetId: schema.targets.id,
name: schema.targets.name,
namespaceId: schema.targets.namespaceId,
baseSubgraphId: schema.featureSubgraphsToBaseSubgraphs.baseSubgraphId,
})
.from(schema.targets)
.innerJoin(schema.subgraphs, eq(schema.subgraphs.targetId, schema.targets.id))
.innerJoin(
schema.featureSubgraphsToBaseSubgraphs,
eq(schema.featureSubgraphsToBaseSubgraphs.featureSubgraphId, schema.subgraphs.id),
)
.where(and(...conditions))
.execute();

for (const namespace of namespaces) {
namespace.featureSubgraphs = featureSubgraphs
.filter((fsg) => fsg.namespaceId === namespace.id)
.map((fsg) =>
fromJson(WorkspaceSubgraphSchema, {
id: fsg.id,
targetId: fsg.targetId,
name: fsg.name,
baseSubgraphId: fsg.baseSubgraphId,
}),
);
}
}
}
7 changes: 5 additions & 2 deletions controlplane/test/workspace/get-workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,13 @@ describe('GetWorkspace', () => {
expect(response.response?.code).toBe(EnumStatusCode.OK);
const defaultNs = response.namespaces.find((ns) => ns.name === DEFAULT_NAMESPACE);
const graph = defaultNs?.graphs.find((g) => g.name === graphName);
const subgraph = defaultNs?.subgraphs.find((sg) => sg.name === subgraphName);

expect(graph?.name).toBe(graphName);
expect(subgraph?.name).toBe(subgraphName);
// Published subgraph with matching label should be associated with the graph
expect(graph?.subgraphs.length).toBe(1);
expect(graph?.subgraphs[0].name).toBe(subgraphName);
expect(graph?.subgraphTargetIds.length).toBe(1);
expect(graph?.subgraphTargetIds[0]).toBe(subgraph?.targetId);
});

test.each([
Expand Down
5 changes: 3 additions & 2 deletions proto/wg/cosmo/platform/v1/platform.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2844,22 +2844,23 @@ message WorkspaceNamespace {
string id = 1;
string name = 2;
repeated WorkspaceFederatedGraph graphs = 3;
repeated WorkspaceSubgraph featureSubgraphs = 4;
repeated WorkspaceSubgraph subgraphs = 4;
}

message WorkspaceFederatedGraph {
string id = 1;
string targetId = 2;
string name = 3;
bool isContract = 4;
repeated WorkspaceSubgraph subgraphs = 5;
repeated string subgraphTargetIds = 5;
}

Comment thread
wilsonrivera marked this conversation as resolved.
message WorkspaceSubgraph {
string id = 1;
string targetId = 2;
string name = 3;
optional string baseSubgraphId = 4;
bool isFeatureSubgraph = 5;
}

message GetWorkspaceRequest {}
Expand Down
28 changes: 15 additions & 13 deletions studio/src/components/dashboard/graph-command-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function GraphCommandGroup({
}: GraphCommandGroupProps) {
return (
<CommandGroup key={`heading-${namespaceIndex}`} heading={namespace.name}>
{namespace.graphs.map(({ subgraphs, ...graph }, graphIndex) => (
{namespace.graphs.map(({ subgraphTargetIds, ...graph }, graphIndex) => (
<React.Fragment key={`graph-${namespaceIndex}-${graphIndex}`}>
<GraphCommandItem
namespace={namespace}
Expand All @@ -39,18 +39,20 @@ export function GraphCommandGroup({
/>

{(isFiltering || activeSubgraphId) &&
subgraphs.map((subgraph, subgraphIndex) => (
<GraphCommandItem
key={`subgraph-${namespaceIndex}-${graphIndex}-${subgraphIndex}`}
name={subgraph.name}
namespace={namespace}
isActive={activeSubgraphId === subgraph.id}
value={`${namespace.name}.${graph.id}.${subgraph.id}`}
isSubgraph
className="pl-8"
setNamespace={setNamespace}
/>
))}
namespace.subgraphs
.filter((sg) => subgraphTargetIds.includes(sg.targetId))
.map((subgraph, subgraphIndex) => (
<GraphCommandItem
key={`subgraph-${namespaceIndex}-${graphIndex}-${subgraphIndex}`}
name={subgraph.name}
namespace={namespace}
isActive={activeSubgraphId === subgraph.id}
value={`${namespace.name}.${graph.id}.${subgraph.id}`}
isSubgraph
className="pl-8"
setNamespace={setNamespace}
/>
))}
</React.Fragment>
))}
</CommandGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,14 @@ export function WorkspaceCommandWrapper({
}

// Only search for subgraphs if the graph contains subgraphs
if (!graph.subgraphs?.length) {
if (!graph.subgraphTargetIds?.length) {
// The federated graph doesn't contain any subgraphs, we don't need to perform the search here
continue;
}

// Apply the filter to the subgraphs, we need to find at least one to add the graph to the search results
fuse.setCollection(graph.subgraphs);
fuse.setCollection(clonedWns.subgraphs.filter((sg) => graph.subgraphTargetIds.includes(sg.targetId)));

const matchingSubgraphs = fuse.search(filterValue);
if (matchingSubgraphs.length === 0) {
// Only add the graph to the list of results if we found at least one matching subgraph
Expand All @@ -86,9 +87,9 @@ export function WorkspaceCommandWrapper({

// We need to clone the graph to avoid mutating the original object
const clonedGraph = clone(WorkspaceFederatedGraphSchema, graph);
clonedGraph.subgraphs = matchingSubgraphs
clonedGraph.subgraphTargetIds = matchingSubgraphs
.sort((a, b) => (a.score ?? 0) - (b.score ?? 0))
.map((match) => match.item as WorkspaceSubgraph);
.map((match) => (match.item as WorkspaceSubgraph).targetId);

clonedWns.graphs.push(clonedGraph);
}
Expand Down
15 changes: 6 additions & 9 deletions studio/src/components/dashboard/workspace-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,27 @@ export function WorkspaceSelector({ children, truncateNamespace = true }: Worksp
const currentSlug = (router.query.slug as string)?.toLowerCase();
const currentSubgraphSlug = (router.query.subgraphSlug as string)?.toLowerCase();

const nsGraphs = namespace.graphs;
const nsSubgraphs = nsGraphs.flatMap((graph) => graph.subgraphs);

const activeGraph =
routeSegment === 'graph' ? nsGraphs.find((graph) => graph.name.toLowerCase() === currentSlug) : undefined;
routeSegment === 'graph' ? namespace.graphs.find((graph) => graph.name.toLowerCase() === currentSlug) : undefined;

// Try to find the currently active subgraph by id
let baseSubgraph: WorkspaceSubgraph | undefined;
let activeSubgraph = !!subgraphContext?.subgraph?.id
? nsSubgraphs.find((subgraph) => subgraph.id === subgraphContext?.subgraph?.id)
? namespace.subgraphs.find((subgraph) => subgraph.id === subgraphContext?.subgraph?.id)
: undefined;

if (!activeGraph && !activeSubgraph && routeSegment === 'subgraph' && !!currentSubgraphSlug) {
// We couldn't find the subgraph, try to find it on the feature subgraphs
activeSubgraph = namespace.featureSubgraphs.find((fsg) => fsg.name.toLowerCase() === currentSubgraphSlug);
activeSubgraph = namespace.subgraphs.find((fsg) => fsg.name.toLowerCase() === currentSubgraphSlug);
if (activeSubgraph?.baseSubgraphId) {
// Find the base base subgraph by id
baseSubgraph = nsSubgraphs.find((subgraph) => subgraph.id === activeSubgraph?.baseSubgraphId);
// Find the base subgraph by id
baseSubgraph = namespace.subgraphs.find((subgraph) => subgraph.id === activeSubgraph?.baseSubgraphId);
}
}

return [activeGraph, activeSubgraph, baseSubgraph];
}, [
namespace.featureSubgraphs,
namespace.subgraphs,
namespace.graphs,
router.asPath,
router.query.slug,
Expand Down
Loading