diff --git a/apps/api/openapi/openapi.json b/apps/api/openapi/openapi.json index 1860ff727..8f92c07d6 100644 --- a/apps/api/openapi/openapi.json +++ b/apps/api/openapi/openapi.json @@ -5006,6 +5006,16 @@ ], "type": "string" } + }, + { + "allowReserved": true, + "description": "CEL expression to filter the results", + "in": "query", + "name": "cel", + "required": false, + "schema": { + "type": "string" + } } ], "responses": { diff --git a/apps/api/openapi/paths/deploymentversions.jsonnet b/apps/api/openapi/paths/deploymentversions.jsonnet index 3004b5981..74a94d931 100644 --- a/apps/api/openapi/paths/deploymentversions.jsonnet +++ b/apps/api/openapi/paths/deploymentversions.jsonnet @@ -11,6 +11,7 @@ local openapi = import '../lib/openapi.libsonnet'; openapi.limitParam(), openapi.offsetParam(), openapi.orderParam(), + openapi.celParam(), ], responses: openapi.paginatedResponse(openapi.schemaRef('DeploymentVersion')) + openapi.notFoundResponse() diff --git a/apps/api/src/routes/v1/workspaces/deployments.ts b/apps/api/src/routes/v1/workspaces/deployments.ts index d0c91cd1b..67b91ecbb 100644 --- a/apps/api/src/routes/v1/workspaces/deployments.ts +++ b/apps/api/src/routes/v1/workspaces/deployments.ts @@ -1,5 +1,6 @@ import type { AsyncTypedHandler } from "@/types/api.js"; import { ApiError, asyncHandler } from "@/types/api.js"; +import { evaluate } from "cel-js"; import { Router } from "express"; import { v4 as uuidv4 } from "uuid"; @@ -258,6 +259,19 @@ const deleteDeployment: AsyncTypedHandler< .json({ id: deploymentId, message: "Deployment delete requested" }); }; +function filterDeploymentVersions( + versions: (typeof schema.deploymentVersion.$inferSelect)[], + cel: string, +) { + return versions.filter((version) => { + try { + return evaluate(cel, { deploymentVersion: version }); + } catch { + return false; + } + }); +} + const listDeploymentVersions: AsyncTypedHandler< "/v1/workspaces/{workspaceId}/deployments/{deploymentId}/versions", "get" @@ -266,29 +280,54 @@ const listDeploymentVersions: AsyncTypedHandler< const limit = req.query.limit ?? 50; const offset = req.query.offset ?? 0; const order = req.query.order ?? "desc"; + const { cel } = req.query; + + const orderBy = + order === "asc" + ? asc(schema.deploymentVersion.createdAt) + : desc(schema.deploymentVersion.createdAt); + + if (cel == null) { + const { total } = await db + .select({ total: count() }) + .from(schema.deploymentVersion) + .where(eq(schema.deploymentVersion.deploymentId, deploymentId)) + .then(takeFirst); - const [countResult] = await db - .select({ total: count() }) - .from(schema.deploymentVersion) - .where(eq(schema.deploymentVersion.deploymentId, deploymentId)); + const versions = await db + .select() + .from(schema.deploymentVersion) + .where(eq(schema.deploymentVersion.deploymentId, deploymentId)) + .orderBy(orderBy) + .limit(limit) + .offset(offset); + + res.status(200).json({ + items: versions.map(formatDeploymentVersion), + total, + limit, + offset, + }); + return; + } - const total = countResult?.total ?? 0; + if (!validResourceSelector(cel)) + throw new ApiError("Invalid CEL expression", 400); - const versions = await db + // CEL is evaluated in-memory, so cap the candidate set to bound cost. + // Filtering applies to the 1000 most-recent (or oldest, for asc) versions. + const candidates = await db .select() .from(schema.deploymentVersion) .where(eq(schema.deploymentVersion.deploymentId, deploymentId)) - .orderBy( - order === "asc" - ? asc(schema.deploymentVersion.createdAt) - : desc(schema.deploymentVersion.createdAt), - ) - .limit(limit) - .offset(offset); + .orderBy(orderBy) + .limit(1000); + + const filtered = filterDeploymentVersions(candidates, cel); res.status(200).json({ - items: versions.map(formatDeploymentVersion), - total, + items: filtered.slice(offset, offset + limit).map(formatDeploymentVersion), + total: filtered.length, limit, offset, }); diff --git a/apps/api/src/types/openapi.ts b/apps/api/src/types/openapi.ts index c2d090ab0..15a356629 100644 --- a/apps/api/src/types/openapi.ts +++ b/apps/api/src/types/openapi.ts @@ -3285,6 +3285,8 @@ export interface operations { offset?: number; /** @description Sort order for results */ order?: "asc" | "desc"; + /** @description CEL expression to filter the results */ + cel?: string; }; header?: never; path: {