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
2 changes: 2 additions & 0 deletions services/cubejs/src/routes/discover.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ export default async function discover(req, res) {
"WebSocket endpoint for GraphQL subscriptions (proxied to Hasura). Same auth as /v1/graphql.",
"GET /api/v1/meta":
"Cube metadata — lists all cubes, measures, dimensions, and segments for the selected datasource.",
"GET /api/v1/meta-all":
"Aggregated cube summaries across every visible datasource. No datasource headers required. Returns cube name/title/description, measure/dimension/segment names, and cube-level meta.",
"POST /api/v1/load":
"Run a Cube.js query. Supports format=csv|jsonstat|arrow|json (default json).",
"GET /api/v1/sql":
Expand Down
6 changes: 6 additions & 0 deletions services/cubejs/src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import smartGenerate from "./smartGenerate.js";
import columnValues from "./columnValues.js";
import discoverNested from "./discoverNested.js";
import discover from "./discover.js";
import metaAll from "./metaAll.js";
import testConnection from "./testConnection.js";
import validate from "./validate.js";
import version from "./version.js";
Expand Down Expand Up @@ -269,6 +270,11 @@ export default ({ basePath, cubejs }) => {
// Discovery endpoint — WorkOS auth only, no datasource selection required
router.get(`${basePath}/v1/discover`, async (req, res) => discover(req, res));

// Aggregated meta across all visible datasources — WorkOS/FraiOS auth only
router.get(`${basePath}/v1/meta-all`, async (req, res) =>
metaAll(req, res, cubejs)
);

// Version endpoint is public — returns only the schema-compiler version string
router.get(`${basePath}/v1/version`, (req, res) => version(req, res));

Expand Down
189 changes: 189 additions & 0 deletions services/cubejs/src/routes/metaAll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { randomUUID } from "crypto";

import {
detectTokenType,
verifyWorkOSToken,
verifyFraiOSToken,
} from "../utils/workosAuth.js";
import {
findUser,
provisionUserFromWorkOS,
provisionUserFromFraiOS,
} from "../utils/dataSourceHelpers.js";
import defineUserScope from "../utils/defineUserScope.js";
import { resolvePartitionTeamIds } from "./discover.js";

function getRequestId(req) {
return (
req.get?.("x-request-id") ||
req.get?.("traceparent") ||
`${randomUUID()}-span-1`
);
}

function summarizeCube(cube, ds, branchId, versionId) {
const node = cube?.config || cube || {};
return {
datasource_id: ds.id,
datasource_name: ds.name,
db_type: ds.db_type,
team_id: ds.team_id,
branch_id: branchId,
version_id: versionId,
name: node.name,
title: node.title || null,
description: node.description || null,
public: node.public !== false,
measures: (node.measures || []).map((m) => m.name),
dimensions: (node.dimensions || []).map((d) => d.name),
segments: (node.segments || []).map((s) => s.name),
meta: node.meta || null,
};
}

async function metaForDatasource(apiGateway, req, userId, authToken, ds, allMembers) {
const activeBranch =
ds.branches?.find((b) => b.status === "active") || ds.branches?.[0];
if (!activeBranch) {
throw Object.assign(new Error("no active branch"), { status: 400 });
}
const latestVersion = activeBranch.versions?.[0] || null;

const userScope = defineUserScope(
[ds],
allMembers,
ds.id,
activeBranch.id,
latestVersion?.id
);

const securityContext = { authToken, userId, userScope };
const context = await apiGateway.contextByReq(
req,
securityContext,
getRequestId(req)
);

const compilerApi = await apiGateway.getCompilerApi(context);
let metaConfig = await compilerApi.metaConfig(context, {
requestId: context.requestId,
});
metaConfig = apiGateway.filterVisibleItemsInMeta(context, metaConfig);

const cubes = (metaConfig || []).map((entry) =>
summarizeCube(entry, ds, activeBranch.id, latestVersion?.id || null)
);

return {
datasource_id: ds.id,
datasource_name: ds.name,
db_type: ds.db_type,
team_id: ds.team_id,
branch_id: activeBranch.id,
version_id: latestVersion?.id || null,
cubes,
};
}

/**
* GET /api/v1/meta-all
*
* Aggregated cube catalog across every datasource the caller can see.
* One request walks all partition-filtered datasources, resolves their
* active branch + latest version, compiles each, and returns a summary
* per cube (name, title, description, measures, dimensions, segments, meta).
*
* Auth: WorkOS RS256 or FraiOS HS256 Bearer token (same as /discover).
*
* Response:
* {
* datasources: [
* {
* datasource_id, datasource_name, db_type, team_id,
* branch_id, version_id,
* cubes: [{ datasource_id, ..., name, title, description, public,
* measures: string[], dimensions: string[], segments: string[],
* meta: object | null }]
* }
* ],
* errors?: [{ datasource_id, error }]
* }
*/
export default async function metaAll(req, res, cubejs) {
try {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(403).json({ error: "Authorization header required" });
}

const token = authHeader.startsWith("Bearer ")
? authHeader.split(" ")[1]
: authHeader;

if (!token) {
return res.status(403).json({ error: "Bearer token required" });
}

const tokenType = detectTokenType(token);
let payload;
let userId;

if (tokenType === "workos") {
payload = await verifyWorkOSToken(token);
userId = await provisionUserFromWorkOS(payload);
} else if (tokenType === "fraios") {
payload = await verifyFraiOSToken(token);
userId = await provisionUserFromFraiOS(payload);
} else {
return res
.status(403)
.json({ error: "WorkOS or FraiOS token required" });
}

const user = await findUser({ userId });
if (!user.dataSources?.length) {
return res.json({ datasources: [] });
}

const partitionTeamIds = resolvePartitionTeamIds(
user.members,
payload.partition
);
const filtered = partitionTeamIds
? user.dataSources.filter((ds) => partitionTeamIds.has(ds.team_id))
: user.dataSources;

const apiGateway = cubejs.apiGateway();

const settled = await Promise.allSettled(
filtered.map((ds) =>
metaForDatasource(apiGateway, req, userId, token, ds, user.members)
)
);

const datasources = [];
const errors = [];
settled.forEach((r, i) => {
if (r.status === "fulfilled") {
datasources.push(r.value);
} else {
errors.push({
datasource_id: filtered[i].id,
error: r.reason?.message || "unknown error",
});
}
});

const body = { datasources };
if (errors.length) body.errors = errors;
res.json(body);
} catch (err) {
const status = err.status || 500;
if (status >= 500) {
console.error("meta-all error:", err);
}
res.status(status).json({
error: status >= 500 ? "Internal server error" : err.message,
});
}
}
Loading