-
Notifications
You must be signed in to change notification settings - Fork 18
feat: version list endpoint can filter with CEL expression #1047
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this be sufficient? What if you looped pages of 1000 until you got enough results? |
||
|
|
||
| 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, | ||
| }); | ||
|
Comment on lines
+317
to
333
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hard 1000-candidate cap silently truncates When
At minimum, surface the cap to the caller (e.g., an additional flag like 🤖 Prompt for AI Agents |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silent
catch { return false }makes invalid CEL expressions indistinguishable from empty results.validResourceSelectoronly validates parse-time syntax (viacel-jsparse), not runtime evaluation. A syntactically valid expression that references an unknown field, mis-types a comparison, or otherwise throws at evaluate-time will cause every candidate to be dropped and the endpoint will return{ items: [], total: 0 }with HTTP 200 — users will have no way to tell their expression is wrong.Two options, either is fine:
Proposed change
function filterDeploymentVersions( versions: (typeof schema.deploymentVersion.$inferSelect)[], cel: string, ) { + let loggedError = false; return versions.filter((version) => { try { return evaluate(cel, { deploymentVersion: version }); - } catch { + } catch (err) { + if (!loggedError) { + console.warn("CEL evaluation failed for deployment version", { + cel, + versionId: version.id, + error: err, + }); + loggedError = true; + } return false; } }); }📝 Committable suggestion
🤖 Prompt for AI Agents