Skip to content

feat: global variable sets#902

Open
adityachoudhari26 wants to merge 7 commits intomainfrom
global-variable-sets
Open

feat: global variable sets#902
adityachoudhari26 wants to merge 7 commits intomainfrom
global-variable-sets

Conversation

@adityachoudhari26
Copy link
Copy Markdown
Member

@adityachoudhari26 adityachoudhari26 commented Apr 1, 2026

Summary by CodeRabbit

  • New Features

    • Variable Sets Management: workspace-scoped APIs to create, list, retrieve, update, and delete variable sets with CEL selectors and priority ordering.
    • Variable Resolution Integration: variable sets are evaluated and applied during deployment variable resolution as a prioritized fallback.
  • Chores

    • Updated createWorkflow success response from 202 to 201 (resource creation).

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

📝 Walkthrough

Walkthrough

Adds a "variable sets" feature: DB tables and migrations, Drizzle/pg/sqlc mappings, OpenAPI schemas and endpoints, API route handlers, backend conversion/querying, resolver integration (CEL-based filtering + priority resolution), tests and SDK types.

Changes

Cohort / File(s) Summary
Database schema & migrations
packages/db/drizzle/0182_silent_blazing_skull.sql, packages/db/drizzle/meta/_journal.json, apps/workspace-engine/pkg/db/queries/schema.sql, packages/db/src/schema/variable-set.ts, packages/db/src/schema/index.ts
Added variable_set and variable_set_variable tables, constraints, Drizzle schema exports, and migration journal entry.
SQL queries & sqlc config
apps/workspace-engine/pkg/db/queries/variable_sets.sql, apps/workspace-engine/pkg/db/sqlc.yaml, apps/workspace-engine/pkg/db/variable_sets.sql.go
New query to list variable sets with aggregated variables; sqlc config/type overrides and generated Go query/row types.
DB models & conversion
apps/workspace-engine/pkg/db/models.go, apps/workspace-engine/pkg/db/convert.go
Added DB model types for VariableSet/VariableSetVariable and converter to build oapi.VariableSetWithVariables from query rows, including JSON value unmarshalling and UUID parsing.
OpenAPI schemas & paths
apps/workspace-engine/oapi/spec/schemas/variablesets.jsonnet, apps/workspace-engine/oapi/spec/main.jsonnet, apps/workspace-engine/oapi/openapi.json, apps/api/openapi/schemas/variablesets.jsonnet, apps/api/openapi/paths/variablesets.jsonnet, apps/api/openapi/main.jsonnet, apps/api/openapi/openapi.json
Added OpenAPI schemas (Create/Update/VariableSet/VariableSetVariable/VariableSetWithVariables) and REST path definitions for workspace-scoped CRUD endpoints.
API routes & types
apps/api/src/routes/v1/workspaces/variable-sets.ts, apps/api/src/routes/v1/workspaces/index.ts, apps/api/src/types/openapi.ts, apps/web/app/api/openapi.ts
Implemented Express CRUD router for variable sets (list/create/get/update/delete), wiring transactions, nested variables handling, and updated OpenAPI/TS types.
Service resolver integration
apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters.go, apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters_postgres.go, apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go, apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go
Extended Getter interface and Postgres implementation to load variable sets; integrated fetching, CEL selector filtering, priority-sorted matching, a new fallback resolveFromVariableSets stage, and added tests covering resolution behavior.
Tests & test mocks
apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go, apps/workspace-engine/test/controllers/harness/mocks.go
Updated test mocks to expose GetVariableSetsWithVariables and include variable-sets in mocks.
SDK types
packages/workspace-engine-sdk/src/schema.ts
Added exported TypeScript types for new OpenAPI-derived schemas.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API
    participant DB
    participant Resolver
    participant CEL

    Client->>API: POST /v1/workspaces/{id}/variable-sets (create)
    API->>DB: INSERT variable_set + variable_set_variable rows
    DB-->>API: Created rows
    API-->>Client: 201 Created

    Client->>Resolver: Resolve variables (workspace scope)
    Resolver->>DB: ListVariableSetsWithVariablesByWorkspaceID(workspaceID)
    DB-->>Resolver: [VariableSetWithVariables...]

    loop evaluate selectors
        Resolver->>CEL: compile & eval selector against scope
        CEL-->>Resolver: true / false
        alt true
            Resolver->>Resolver: include set in filtered list
        end
    end

    loop for each variable key
        Resolver->>Resolver: resolveFromResource()
        alt found
            Resolver-->>Client: use resource value
        else
            Resolver->>Resolver: resolveFromValues()
            alt found
                Resolver-->>Client: use value
            else
                Resolver->>Resolver: resolveFromVariableSets(filtered)
                alt found
                    Resolver-->>Client: use highest-priority variable-set value
                else
                    Resolver-->>Client: unresolved
                end
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • jsbroks

Poem

🐰 Soft paws tap the schema tree,

Variables gathered, wild and free,
CEL whispers which ones belong,
Priority hums the winning song,
Hooray — variable sets hop along! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: global variable sets' clearly and concisely summarizes the main change—the introduction of a new feature for global/workspace-scoped variable sets across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch global-variable-sets

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@adityachoudhari26 adityachoudhari26 marked this pull request as ready for review April 1, 2026 20:29
Copilot AI review requested due to automatic review settings April 1, 2026 20:29
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go (1)

328-351: Silent error handling may hide selector configuration issues.

filterVariableSets silently skips variable sets with invalid CEL selectors (lines 338-341). While this prevents errors from blocking resolution, it may hide configuration issues from operators. Consider logging compilation failures at debug/warn level.

🔍 Suggested logging for debugging
+import "github.com/charmbracelet/log"
+
 func filterVariableSets(
 	scope *Scope,
 	variableSets []oapi.VariableSetWithVariables,
 ) []oapi.VariableSetWithVariables {
 	celCtx := cel.BuildEntityContext(scope.Resource, scope.Deployment, scope.Environment)
 	matched := make([]oapi.VariableSetWithVariables, 0, len(variableSets))
 	for _, vs := range variableSets {
 		if vs.Selector == "" {
 			continue
 		}
 		program, err := cel.CompileProgram(vs.Selector)
 		if err != nil {
+			log.Debug("failed to compile variable set selector", "variable_set_id", vs.Id, "selector", vs.Selector, "error", err)
 			continue
 		}
 		result, _ := celutil.EvalBool(program, celCtx)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go`
around lines 328 - 351, In filterVariableSets, do not silently ignore compile
errors for vs.Selector; capture the error returned by
cel.CompileProgram(vs.Selector) and log it (at debug or warn) including the
selector value, the error, and identifying context (e.g.,
scope.Resource/Deployment/Environment or variable set ID/name) so operators can
detect misconfigured selectors; keep the existing behavior of skipping the
invalid selector after logging.
apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go (1)

1007-1211: Good test coverage for variable set resolution.

The test cases effectively validate the key behaviors:

  • Selector matching and value injection
  • Precedence hierarchy (resource vars > deployment vars > variable sets)
  • Priority-based resolution among multiple matching sets
  • Non-matching selectors are ignored

Consider adding edge case tests for:

  1. Variable set with reference values (not just literals)
  2. Error handling when GetVariableSetsWithVariables fails
  3. Empty selector string behavior (matches all vs matches none)

These would strengthen the test suite but aren't blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go`
around lines 1007 - 1211, Add three focused tests to the Resolve suite: (1) a
test that uses makeVariableSet to inject a non-literal/reference value (e.g., a
reference like "${other_var}") and ensures Resolve resolves references correctly
(use mockGetter with deploymentVars/resourceVars containing the referenced key);
(2) a test that simulates GetVariableSetsWithVariables failing by having
mockGetter return an error for that method and asserting Resolve returns the
error (or appropriate handling); and (3) a test for empty selector behavior by
creating a VariableSetWithVariables with an empty Selector string via
makeVariableSet and asserting whether Resolve treats it as matching-all or
matching-none per the intended contract. Ensure each test invokes
Resolve(context.Background(), getter, scope, scope.Deployment.Id,
scope.Resource.Id) and checks outcomes for keys like "log_level" as in existing
tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/workspace-engine/pkg/db/convert.go`:
- Around line 350-351: The code uses uuid.MustParse on fields Id and
VariableSetId which will panic on malformed UUIDs; replace these calls with
uuid.Parse and handle the error in the surrounding conversion function (e.g.,
the converter that constructs the struct from v) by returning a descriptive
error (or skipping the record if that is preferred) instead of panicking; update
both occurrences (Id and VariableSetId) and any other uuid.MustParse uses in
convert.go to parse, check err, and propagate or handle it cleanly so malformed
DB values don't crash the process.

In `@apps/workspace-engine/test/controllers/harness/mocks.go`:
- Around line 467-469: The mock method
DesiredReleaseGetter.GetVariableSetsWithVariables currently returns nil, nil
instead of the stored mock data; change it to return the struct's VariableSets
field and the mock error field (e.g., return g.VariableSets, g.Err) so it
behaves like other mocks (for example GetDeploymentVariables) and honors the
VariableSets test data on the DesiredReleaseGetter type.

---

Nitpick comments:
In
`@apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go`:
- Around line 1007-1211: Add three focused tests to the Resolve suite: (1) a
test that uses makeVariableSet to inject a non-literal/reference value (e.g., a
reference like "${other_var}") and ensures Resolve resolves references correctly
(use mockGetter with deploymentVars/resourceVars containing the referenced key);
(2) a test that simulates GetVariableSetsWithVariables failing by having
mockGetter return an error for that method and asserting Resolve returns the
error (or appropriate handling); and (3) a test for empty selector behavior by
creating a VariableSetWithVariables with an empty Selector string via
makeVariableSet and asserting whether Resolve treats it as matching-all or
matching-none per the intended contract. Ensure each test invokes
Resolve(context.Background(), getter, scope, scope.Deployment.Id,
scope.Resource.Id) and checks outcomes for keys like "log_level" as in existing
tests.

In
`@apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go`:
- Around line 328-351: In filterVariableSets, do not silently ignore compile
errors for vs.Selector; capture the error returned by
cel.CompileProgram(vs.Selector) and log it (at debug or warn) including the
selector value, the error, and identifying context (e.g.,
scope.Resource/Deployment/Environment or variable set ID/name) so operators can
detect misconfigured selectors; keep the existing behavior of skipping the
invalid selector after logging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c188170b-ff9a-4b4f-a1cf-39a965cf9c7a

📥 Commits

Reviewing files that changed from the base of the PR and between b10a04c and c81d4b2.

📒 Files selected for processing (21)
  • apps/workspace-engine/oapi/openapi.json
  • apps/workspace-engine/oapi/spec/main.jsonnet
  • apps/workspace-engine/oapi/spec/schemas/variablesets.jsonnet
  • apps/workspace-engine/pkg/db/convert.go
  • apps/workspace-engine/pkg/db/models.go
  • apps/workspace-engine/pkg/db/queries/schema.sql
  • apps/workspace-engine/pkg/db/queries/variable_sets.sql
  • apps/workspace-engine/pkg/db/sqlc.yaml
  • apps/workspace-engine/pkg/db/variable_sets.sql.go
  • apps/workspace-engine/pkg/oapi/oapi.gen.go
  • apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go
  • apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters.go
  • apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters_postgres.go
  • apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go
  • apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go
  • apps/workspace-engine/test/controllers/harness/mocks.go
  • packages/db/drizzle/0182_silent_blazing_skull.sql
  • packages/db/drizzle/meta/0182_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/src/schema/index.ts
  • packages/db/src/schema/variable-set.ts

Comment on lines +467 to +469
func (g *DesiredReleaseGetter) GetVariableSetsWithVariables(ctx context.Context, workspaceID uuid.UUID) ([]oapi.VariableSetWithVariables, error) {
return nil, nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Mock method doesn't use the VariableSets field.

The GetVariableSetsWithVariables method always returns nil, nil, ignoring the VariableSets field added at line 122. This is inconsistent with other mock methods like GetDeploymentVariables which return their corresponding field values.

🔧 Proposed fix
 func (g *DesiredReleaseGetter) GetVariableSetsWithVariables(ctx context.Context, workspaceID uuid.UUID) ([]oapi.VariableSetWithVariables, error) {
-	return nil, nil
+	return g.VariableSets, nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (g *DesiredReleaseGetter) GetVariableSetsWithVariables(ctx context.Context, workspaceID uuid.UUID) ([]oapi.VariableSetWithVariables, error) {
return nil, nil
}
func (g *DesiredReleaseGetter) GetVariableSetsWithVariables(ctx context.Context, workspaceID uuid.UUID) ([]oapi.VariableSetWithVariables, error) {
return g.VariableSets, nil
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/workspace-engine/test/controllers/harness/mocks.go` around lines 467 -
469, The mock method DesiredReleaseGetter.GetVariableSetsWithVariables currently
returns nil, nil instead of the stored mock data; change it to return the
struct's VariableSets field and the mock error field (e.g., return
g.VariableSets, g.Err) so it behaves like other mocks (for example
GetDeploymentVariables) and honors the VariableSets test data on the
DesiredReleaseGetter type.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds “variable sets” as a new workspace-scoped data model and integrates them into the workspace-engine variable resolution flow so release-target variables can be provided by prioritized, selector-matched sets.

Changes:

  • Introduces new DB tables variable_set and variable_set_variable (Drizzle schema + migration + snapshot/journal).
  • Extends desired-release variable resolution to load, filter (CEL selector), and apply variable sets (between deployment variable values and defaults), with tests.
  • Adds sqlc query + Go DB conversion and OAPI schema/models for variable sets.

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/db/src/schema/variable-set.ts Defines new Drizzle tables for variable sets and their variables.
packages/db/src/schema/index.ts Exports the new schema module.
packages/db/drizzle/meta/0182_snapshot.json Drizzle snapshot updated to include new tables.
packages/db/drizzle/meta/_journal.json Drizzle journal updated with migration entry.
packages/db/drizzle/0182_silent_blazing_skull.sql Creates the new tables and constraints in SQL migration.
apps/workspace-engine/test/controllers/harness/mocks.go Updates test harness getter to satisfy new interface method.
apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go Loads, filters, and resolves variables from variable sets.
apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go Adds unit tests covering variable-set resolution precedence and priority.
apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters.go Extends Getter interface with variable set retrieval.
apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters_postgres.go Implements variable set retrieval via sqlc query + conversion.
apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go Updates reconcile test mock to satisfy new Getter interface.
apps/workspace-engine/pkg/oapi/oapi.gen.go Adds generated OAPI models for variable sets.
apps/workspace-engine/pkg/db/variable_sets.sql.go Adds generated sqlc code for listing variable sets with variables.
apps/workspace-engine/pkg/db/sqlc.yaml Registers new query file and type overrides for variable set columns.
apps/workspace-engine/pkg/db/queries/variable_sets.sql Adds sqlc query to fetch variable sets and aggregated variables for a workspace.
apps/workspace-engine/pkg/db/queries/schema.sql Adds table definitions for sqlc schema context.
apps/workspace-engine/pkg/db/models.go Adds DB model structs for variable sets.
apps/workspace-engine/pkg/db/convert.go Adds row→OAPI conversion for variable sets with variables.
apps/workspace-engine/oapi/spec/schemas/variablesets.jsonnet Adds OpenAPI schema definitions for variable sets.
apps/workspace-engine/oapi/spec/main.jsonnet Includes the new variable set schemas into the spec build.
apps/workspace-engine/oapi/openapi.json Generated OpenAPI updated with variable set schemas.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

func (g *DesiredReleaseGetter) GetVariableSetsWithVariables(ctx context.Context, workspaceID uuid.UUID) ([]oapi.VariableSetWithVariables, error) {
return nil, nil
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetVariableSetsWithVariables currently returns (nil, nil) and ignores the DesiredReleaseGetter.VariableSets field. With variable-set resolution now part of variable resolving, this mock should return the configured VariableSets so harness-based tests can exercise the behavior (and to avoid surprising empty results when VariableSets is populated).

Suggested change
return nil, nil
if g == nil {
return nil, nil
}
return g.VariableSets, nil

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +37
} from "drizzle-orm/pg-core";

import { workspace } from "./workspace.js";

export const variableSet = pgTable("variable_set", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
description: text("description").notNull().default(""),
selector: text("selector").notNull(),

metadata: jsonb("metadata")
.notNull()
.default("{}")
.$type<Record<string, string>>(),

priority: integer("priority").notNull().default(0),

workspaceId: uuid("workspace_id")
.notNull()
.references(() => workspace.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
});
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

variable_set is queried by workspace_id (e.g., ListVariableSetsWithVariablesByWorkspaceID) but the table definition does not add an index on workspaceId. Consider adding an index (or a composite index like (workspaceId, priority DESC)) to avoid full table scans as variable sets grow; this matches the pattern used by other workspace-scoped tables (e.g., system has index().on(workspaceId)).

Suggested change
} from "drizzle-orm/pg-core";
import { workspace } from "./workspace.js";
export const variableSet = pgTable("variable_set", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
description: text("description").notNull().default(""),
selector: text("selector").notNull(),
metadata: jsonb("metadata")
.notNull()
.default("{}")
.$type<Record<string, string>>(),
priority: integer("priority").notNull().default(0),
workspaceId: uuid("workspace_id")
.notNull()
.references(() => workspace.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
});
index,
} from "drizzle-orm/pg-core";
import { workspace } from "./workspace.js";
export const variableSet = pgTable(
"variable_set",
{
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
description: text("description").notNull().default(""),
selector: text("selector").notNull(),
metadata: jsonb("metadata")
.notNull()
.default("{}")
.$type<Record<string, string>>(),
priority: integer("priority").notNull().default(0),
workspaceId: uuid("workspace_id")
.notNull()
.references(() => workspace.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
},
(t) => [index("variable_set_workspace_id_idx").on(t.workspaceId)],
);

Copilot uses AI. Check for mistakes.
"workspace_id" uuid NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration creates variable_set without an index on workspace_id. Since reads filter by workspace_id, add an index (optionally composite with priority) to avoid full scans as data grows.

Suggested change
);
);
CREATE INDEX "variable_set_workspace_id_priority_idx" ON "variable_set" ("workspace_id","priority");

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go (1)

337-341: Surface invalid selector failures in telemetry.

A selector that fails to compile or evaluate is silently treated as a non-match here, which makes a broken variable set indistinguishable from a legitimate miss. Since Resolve is already instrumented, a span event or counter for invalid selectors would make these fallbacks much easier to debug.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go`
around lines 337 - 341, The selector compilation/evaluation currently swallows
errors (in the cel.CompileProgram(vs.Selector) and celutil.EvalBool(program,
celCtx) paths) so invalid selectors look like non-matches; update Resolve to
record telemetry when a selector fails by adding a span event or incrementing a
counter/tag on the active span for both compile failures (err from
cel.CompileProgram) and evaluation failures (error returned from
celutil.EvalBool), and include the selector (vs.Selector) and error message in
the telemetry to make broken variable sets diagnosable.
apps/api/openapi/paths/variablesets.jsonnet (1)

52-71: Use patch here if omitted fields are preserved.

UpdateVariableSet is modeled as a partial body downstream, so documenting this as put makes the contract read like full replacement. If the handler merges fields, switch this to patch; otherwise the update schema should make every field required.

Suggested change
-    put: {
+    patch: {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/openapi/paths/variablesets.jsonnet` around lines 52 - 71, The
OpenAPI operation currently uses put with operationId 'updateVariableSet' and
request schema 'UpdateVariableSet', but that schema is a partial/merge-style
update; change the HTTP verb from put to patch (replace the key "put" with
"patch") so the contract reflects a partial update, and keep the same parameters
and requestBody referencing openapi.schemaRef('UpdateVariableSet');
alternatively, if you intend full replacement, make the 'UpdateVariableSet'
schema require all fields instead of being partial.
apps/api/openapi/openapi.json (1)

2442-2464: Make this update contract clearly patch-like or replacement-like.

UpdateVariableSet makes every field optional, but the operation is exposed as PUT. Right now callers cannot tell whether omitted fields are preserved or cleared. Either switch this to PATCH or require the full replacement shape in UpdateVariableSet.

Also applies to: 8925-8957

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/openapi/openapi.json` around lines 2442 - 2464, The
UpdateVariableSet schema currently presents an object with all optional fields
but is used by a PUT operation, causing ambiguity whether omitted fields are
preserved or cleared; update the API to be explicit by either (A) changing the
operation from PUT to PATCH and keeping UpdateVariableSet as a partial/patch
schema (document semantics as partial update), or (B) making UpdateVariableSet a
full replacement schema by marking required properties (e.g., name, variables,
priority, selector, description as needed) and updating any endpoints that
reference UpdateVariableSet to use the replacement semantics; ensure you update
the OpenAPI components/schemas entry for "UpdateVariableSet" and the
corresponding path operation(s) that reference it so the contract clearly
indicates patch-like or replacement behavior.
apps/api/src/routes/v1/workspaces/variable-sets.ts (1)

24-29: Refactor mixed .then() chains to pure await for consistency.

The queries mix await with .then() chains, which violates the guideline to prefer async/await over raw promises. Use intermediate variables or helper functions with pure await instead:

// Before
const total = await db
  .select({ total: count() })
  .from(schema.variableSet)
  .where(eq(schema.variableSet.workspaceId, workspaceId))
  .then(takeFirst)
  .then(({ total }) => total);

// After
const result = await db
  .select({ total: count() })
  .from(schema.variableSet)
  .where(eq(schema.variableSet.workspaceId, workspaceId))
  .then(takeFirst);
const total = result.total;

Apply to lines: 24–29, 62–67, 89–98, 117–128, 160–169.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/workspaces/variable-sets.ts` around lines 24 - 29,
Replace the mixed await/.then() promise chains in the variable-set queries with
pure async/await using intermediate variables: for each occurrence where you
call
db.select(...).from(schema.variableSet).where(eq(schema.variableSet.workspaceId,
workspaceId)).then(takeFirst).then(({ total }) => total) (and the similar blocks
at the other line ranges), await the query once into a result variable (e.g.,
const result = await
db.select(...).from(schema.variableSet).where(...).then(takeFirst);) and then
extract the value with const total = result.total; ensure you update all
instances referencing count(), takeFirst, schema.variableSet and workspaceId to
follow this pattern for consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/openapi/openapi.json`:
- Around line 268-295: The OpenAPI schemas are inconsistent: the
CreateVariableSet write schema (CreateVariableSet) allows omitting description
and priority while the VariableSet response schema requires them; update the
contract so clients aren’t confused by missing fields. Either make description
and priority optional in the VariableSet response schema (VariableSet) or add
defaulting/documentation and mark them required in CreateVariableSet;
specifically adjust the "required" arrays for CreateVariableSet and VariableSet
(and mirror changes where referenced around the other occurrences you noted) and
add clear default values/descriptions for description and priority if the server
populates them automatically.

In `@apps/api/src/routes/v1/workspaces/variable-sets.ts`:
- Around line 31-36: The query that fetches sets via schema.variableSet
currently uses .limit(limit).offset(offset) without ordering, which makes
pagination unstable; update the db query (the select from schema.variableSet
where eq(schema.variableSet.workspaceId, workspaceId)) to include an explicit
.orderBy(...) using the intended sort key (e.g., schema.variableSet.priority)
and add a stable tie-breaker (e.g., schema.variableSet.id or createdAt) to
ensure deterministic ordering across pages.
- Around line 60-76: The code dereferences variables.length without guarding for
null/undefined; before using variables in the POST handler (where created and
tx.insert(schema.variableSetVariable) are used) normalize it to an array (e.g.,
const variables = Array.isArray(req.body.variables) ? req.body.variables : [])
so subsequent checks like variables.length and the variables.map call are safe;
update the code that builds setData to use the normalized variables variable,
and ensure the same defensive pattern is applied where variables is read in this
file (e.g., around created, tx.insert(schema.variableSetVariable), and any PUT
handlers).

In
`@apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go`:
- Around line 82-91: Update the exported Resolve function doc comment to reflect
the new precedence chain that includes variable sets before defaults: locate the
Resolve docblock (and the related exported comment around lines covering the
Resolve declaration and the block referred to at 136-160) and change the
description from the old "three-step precedence chain" to explicitly list the
current precedence order (including variable sets, e.g., runtime
inputs/overrides -> variable sets -> defaults or whatever the implemented order
is), and mention any scope/filtering behavior (referencing symbols like Resolve,
filterVariableSets, and getter.GetVariableSetsWithVariables) so the exported
contract matches the implementation.
- Around line 306-325: The resolution is nondeterministic when variable sets
share the same Priority; update the ordering in both the SQL query
(variable_sets.sql) and in the Go-side sort used by filterVariableSets to add a
deterministic secondary key (e.g., CreatedAt DESC then Id ASC, or Name) so
equal-priority sets are consistently ordered; specifically, modify the SQL ORDER
BY to include the secondary column(s) after Priority and update
filterVariableSets to sort variableSets by the same fields (referencing the
VariableSet struct fields like Priority, CreatedAt, Id or Name) before
resolveFromVariableSets iterates.

---

Nitpick comments:
In `@apps/api/openapi/openapi.json`:
- Around line 2442-2464: The UpdateVariableSet schema currently presents an
object with all optional fields but is used by a PUT operation, causing
ambiguity whether omitted fields are preserved or cleared; update the API to be
explicit by either (A) changing the operation from PUT to PATCH and keeping
UpdateVariableSet as a partial/patch schema (document semantics as partial
update), or (B) making UpdateVariableSet a full replacement schema by marking
required properties (e.g., name, variables, priority, selector, description as
needed) and updating any endpoints that reference UpdateVariableSet to use the
replacement semantics; ensure you update the OpenAPI components/schemas entry
for "UpdateVariableSet" and the corresponding path operation(s) that reference
it so the contract clearly indicates patch-like or replacement behavior.

In `@apps/api/openapi/paths/variablesets.jsonnet`:
- Around line 52-71: The OpenAPI operation currently uses put with operationId
'updateVariableSet' and request schema 'UpdateVariableSet', but that schema is a
partial/merge-style update; change the HTTP verb from put to patch (replace the
key "put" with "patch") so the contract reflects a partial update, and keep the
same parameters and requestBody referencing
openapi.schemaRef('UpdateVariableSet'); alternatively, if you intend full
replacement, make the 'UpdateVariableSet' schema require all fields instead of
being partial.

In `@apps/api/src/routes/v1/workspaces/variable-sets.ts`:
- Around line 24-29: Replace the mixed await/.then() promise chains in the
variable-set queries with pure async/await using intermediate variables: for
each occurrence where you call
db.select(...).from(schema.variableSet).where(eq(schema.variableSet.workspaceId,
workspaceId)).then(takeFirst).then(({ total }) => total) (and the similar blocks
at the other line ranges), await the query once into a result variable (e.g.,
const result = await
db.select(...).from(schema.variableSet).where(...).then(takeFirst);) and then
extract the value with const total = result.total; ensure you update all
instances referencing count(), takeFirst, schema.variableSet and workspaceId to
follow this pattern for consistency.

In
`@apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go`:
- Around line 337-341: The selector compilation/evaluation currently swallows
errors (in the cel.CompileProgram(vs.Selector) and celutil.EvalBool(program,
celCtx) paths) so invalid selectors look like non-matches; update Resolve to
record telemetry when a selector fails by adding a span event or incrementing a
counter/tag on the active span for both compile failures (err from
cel.CompileProgram) and evaluation failures (error returned from
celutil.EvalBool), and include the selector (vs.Selector) and error message in
the telemetry to make broken variable sets diagnosable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 698140c8-1475-4619-b0aa-56e0b0563617

📥 Commits

Reviewing files that changed from the base of the PR and between c81d4b2 and 48e29a3.

📒 Files selected for processing (15)
  • apps/api/openapi/main.jsonnet
  • apps/api/openapi/openapi.json
  • apps/api/openapi/paths/variablesets.jsonnet
  • apps/api/openapi/schemas/variablesets.jsonnet
  • apps/api/src/routes/v1/workspaces/index.ts
  • apps/api/src/routes/v1/workspaces/variable-sets.ts
  • apps/api/src/types/openapi.ts
  • apps/workspace-engine/pkg/db/convert.go
  • apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go
  • apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters.go
  • apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters_postgres.go
  • apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go
  • apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go
  • apps/workspace-engine/test/controllers/harness/mocks.go
  • packages/workspace-engine-sdk/src/schema.ts
✅ Files skipped from review due to trivial changes (3)
  • apps/api/openapi/main.jsonnet
  • apps/workspace-engine/pkg/db/convert.go
  • apps/api/openapi/schemas/variablesets.jsonnet
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters.go
  • apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go
  • apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go
  • apps/workspace-engine/test/controllers/harness/mocks.go

Comment on lines +268 to +295
"CreateVariableSet": {
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"priority": {
"type": "integer"
},
"selector": {
"description": "A CEL expression to select which release targets this variable set applies to",
"type": "string"
},
"variables": {
"items": {
"$ref": "#/components/schemas/VariableSetVariable"
},
"type": "array"
}
},
"required": [
"name",
"selector",
"variables"
],
"type": "object"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Align the write schemas with the required response fields.

Lines 270-277 and 2444-2451 let callers omit description and priority, but Lines 2984-2986 mark them as always present on VariableSet. That leaves the contract ambiguous for generated clients: either the server is defaulting these fields and the defaults need to be documented here, or the input schemas are too permissive / the response schema is too strict.

Also applies to: 2442-2464, 2981-2989

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/openapi/openapi.json` around lines 268 - 295, The OpenAPI schemas
are inconsistent: the CreateVariableSet write schema (CreateVariableSet) allows
omitting description and priority while the VariableSet response schema requires
them; update the contract so clients aren’t confused by missing fields. Either
make description and priority optional in the VariableSet response schema
(VariableSet) or add defaulting/documentation and mark them required in
CreateVariableSet; specifically adjust the "required" arrays for
CreateVariableSet and VariableSet (and mirror changes where referenced around
the other occurrences you noted) and add clear default values/descriptions for
description and priority if the server populates them automatically.

Comment on lines +306 to +325
func resolveFromVariableSets(
ctx context.Context,
key string,
variableSets []oapi.VariableSetWithVariables,
resolver RelatedEntityResolver,
resourceID string,
entity *oapi.RelatableEntity,
) *oapi.LiteralValue {
for _, vs := range variableSets {
for _, v := range vs.Variables {
if v.Key == key {
lv, err := ResolveValue(ctx, resolver, resourceID, entity, &v.Value)
if err == nil && lv != nil {
return lv
}
}
}
}
return nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's explore the file structure and find the relevant files
cd apps/workspace-engine || exit 1
find . -name "resolve.go" -type f | head -5

Repository: ctrlplanedev/ctrlplane

Length of output: 126


🏁 Script executed:

# Let's read the resolve.go file to understand the complete context
cat -n svc/controllers/desiredrelease/variableresolver/resolve.go | head -100

Repository: ctrlplanedev/ctrlplane

Length of output: 156


🏁 Script executed:

# Also find the variable_sets.sql file
find . -name "variable_sets.sql" -type f

Repository: ctrlplanedev/ctrlplane

Length of output: 122


🏁 Script executed:

# Search for filterVariableSets function
rg "func.*filterVariableSets" --type go

Repository: ctrlplanedev/ctrlplane

Length of output: 171


🏁 Script executed:

# Read the resolve.go file from the correct path
cat -n apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go

Repository: ctrlplanedev/ctrlplane

Length of output: 14025


🏁 Script executed:

# Read the SQL file to check the query
cat -n apps/workspace-engine/pkg/db/queries/variable_sets.sql

Repository: ctrlplanedev/ctrlplane

Length of output: 634


🏁 Script executed:

# Search for any comments or documentation about priority or tie-breaking
rg -i "priority|tie.*break|same.*priority|deterministic" apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/ --type go

Repository: ctrlplanedev/ctrlplane

Length of output: 4144


🏁 Script executed:

# Check if there's any validation that rejects same-priority variable sets
rg -i "priority.*collision|same.*priority.*reject|validation.*priority" apps/workspace-engine/ --type go

Repository: ctrlplanedev/ctrlplane

Length of output: 48


🏁 Script executed:

# Also check the oapi.VariableSetWithVariables structure to understand the fields
rg -A 10 "type VariableSetWithVariables" apps/workspace-engine/ --type go

Repository: ctrlplanedev/ctrlplane

Length of output: 896


🏁 Script executed:

# Check the test file for same-priority scenarios
cat -n apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go | grep -A 30 "VariableSet_HighestPriorityWins"

Repository: ctrlplanedev/ctrlplane

Length of output: 1133


🏁 Script executed:

# Look for any test that has same-priority scenarios
rg -A 20 "Priority.*Priority" apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go | head -80

Repository: ctrlplanedev/ctrlplane

Length of output: 48


🏁 Script executed:

# Check the full VariableSetWithVariables structure
rg -A 20 "type VariableSetWithVariables struct" apps/workspace-engine/pkg/oapi/oapi.gen.go

Repository: ctrlplanedev/ctrlplane

Length of output: 791


Add a secondary sort key to ensure deterministic ordering of equal-priority variable sets.

When two variable sets have the same priority and match the same variable key, resolveFromVariableSets returns whichever appears first in the input list. However, the SQL query (lines 1-8 of variable_sets.sql) and filterVariableSets (lines 346-348) sort only by Priority with no secondary order, leaving the order undefined for equal-priority sets. This causes nondeterministic resolution. Add a deterministic secondary sort key (e.g., by CreatedAt, Id, or Name) to both the SQL query and the sort in filterVariableSets.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go`
around lines 306 - 325, The resolution is nondeterministic when variable sets
share the same Priority; update the ordering in both the SQL query
(variable_sets.sql) and in the Go-side sort used by filterVariableSets to add a
deterministic secondary key (e.g., CreatedAt DESC then Id ASC, or Name) so
equal-priority sets are consistently ordered; specifically, modify the SQL ORDER
BY to include the secondary column(s) after Priority and update
filterVariableSets to sort variableSets by the same fields (referencing the
VariableSet struct fields like Priority, CreatedAt, Id or Name) before
resolveFromVariableSets iterates.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
apps/api/src/routes/v1/workspaces/variable-sets.ts (1)

32-38: ⚠️ Potential issue | 🟠 Major

Finish the page sort with a unique tiebreaker.

priority DESC, name ASC is still unstable when two sets share the same priority and name, so limit/offset can still produce duplicates or gaps across pages. Add a final unique column such as id.

📐 Suggested change
   const sets = await db
     .select()
     .from(schema.variableSet)
     .where(eq(schema.variableSet.workspaceId, workspaceId))
-    .orderBy(desc(schema.variableSet.priority), schema.variableSet.name)
+    .orderBy(
+      desc(schema.variableSet.priority),
+      schema.variableSet.name,
+      schema.variableSet.id,
+    )
     .limit(limit)
     .offset(offset);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/workspaces/variable-sets.ts` around lines 32 - 38, The
ordering for the variable set query is unstable for pagination; update the query
that builds sets (the db.select() -> .from(schema.variableSet) chain) to include
a final unique tiebreaker column by appending schema.variableSet.id to the
.orderBy (e.g. .orderBy(desc(schema.variableSet.priority),
schema.variableSet.name, schema.variableSet.id)) so ties on priority and name
become deterministic and prevent duplicate/missing rows across pages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/routes/v1/workspaces/variable-sets.ts`:
- Around line 49-54: The handler is returning raw DB rows (using sets.map and
allVariables) which exposes internal columns; update the projection so you
construct the published API shapes instead of spreading DB rows: for each set in
sets build a VariableSet object (pick only the public fields declared in
VariableSet in openapi.ts—e.g., id/name/description/createdAt/updatedAt/etc.,
omit workspaceId and metadata) and for each variable in allVariables build a
VariableSetVariable object (pick only the public fields, omit internal id and
variableSetId), then assemble items and return res.status(200).json({ items,
total, limit, offset }); apply the same mapping logic to the other handler
blocks that use sets, allVariables, and the items response.
- Around line 21-29: The handlers read req.params.workspaceId (assigned to
workspaceId) and use it to query DB without authorization; add an explicit
workspace access check at the top of each handler that uses workspaceId (the
occurrences around the initial total count and the other blocks noted) by
calling the project's existing authorization helper (e.g., a function like
ensureWorkspaceMembership, checkWorkspaceAccess, or similar) with the current
user/context and workspaceId, and return 403 if the check fails; do this before
any DB queries or mutations that reference workspaceId so every branch (count,
list, get, create, update, delete) verifies membership/ownership first.

---

Duplicate comments:
In `@apps/api/src/routes/v1/workspaces/variable-sets.ts`:
- Around line 32-38: The ordering for the variable set query is unstable for
pagination; update the query that builds sets (the db.select() ->
.from(schema.variableSet) chain) to include a final unique tiebreaker column by
appending schema.variableSet.id to the .orderBy (e.g.
.orderBy(desc(schema.variableSet.priority), schema.variableSet.name,
schema.variableSet.id)) so ties on priority and name become deterministic and
prevent duplicate/missing rows across pages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f9f033f2-fa78-4b60-8c69-6a7113e16f41

📥 Commits

Reviewing files that changed from the base of the PR and between 48e29a3 and 9b0a894.

📒 Files selected for processing (8)
  • apps/api/openapi/openapi.json
  • apps/api/openapi/schemas/variablesets.jsonnet
  • apps/api/src/routes/v1/workspaces/variable-sets.ts
  • apps/api/src/types/openapi.ts
  • apps/web/app/api/openapi.ts
  • apps/workspace-engine/pkg/db/queries/variable_sets.sql
  • apps/workspace-engine/pkg/db/variable_sets.sql.go
  • apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go
✅ Files skipped from review due to trivial changes (3)
  • apps/workspace-engine/pkg/db/queries/variable_sets.sql
  • apps/api/openapi/schemas/variablesets.jsonnet
  • apps/workspace-engine/pkg/db/variable_sets.sql.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/openapi/openapi.json

Comment on lines +21 to +29
const { workspaceId } = req.params;
const limit = req.query.limit ?? 50;
const offset = req.query.offset ?? 0;

const total = await db
.select({ total: count() })
.from(schema.variableSet)
.where(eq(schema.variableSet.workspaceId, workspaceId))
.then(takeFirst)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Authorize workspaceId before using it.

apps/api/src/routes/v1/workspaces/index.ts mounts this router directly under /:workspaceId/variable-sets, and every handler here trusts that path param as the authorization boundary. That lets a caller read or mutate another workspace’s variable sets by swapping the UUID in the URL unless you do an explicit membership/ownership check first.

Also applies to: 61-69, 91-100, 121-129, 163-171

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/workspaces/variable-sets.ts` around lines 21 - 29, The
handlers read req.params.workspaceId (assigned to workspaceId) and use it to
query DB without authorization; add an explicit workspace access check at the
top of each handler that uses workspaceId (the occurrences around the initial
total count and the other blocks noted) by calling the project's existing
authorization helper (e.g., a function like ensureWorkspaceMembership,
checkWorkspaceAccess, or similar) with the current user/context and workspaceId,
and return 403 if the check fails; do this before any DB queries or mutations
that reference workspaceId so every branch (count, list, get, create, update,
delete) verifies membership/ownership first.

Comment on lines +49 to +54
const items = sets.map((s) => ({
...s,
variables: allVariables.filter((v) => v.variableSetId === s.id),
}));

res.status(200).json({ items, total, limit, offset });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Project DB rows into the published API shape.

These handlers currently return raw schema.variableSet / schema.variableSetVariable rows. That exposes workspaceId and metadata on sets plus id and variableSetId on variables, but apps/api/src/types/openapi.ts declares VariableSet / VariableSetVariable without those fields. This leaks internal columns and makes the implementation diverge from the generated SDK contract.

🧩 Suggested mapping
+const toVariableSet = ({
+  metadata: _metadata,
+  workspaceId: _workspaceId,
+  ...variableSet
+}: typeof schema.variableSet.$inferSelect) => variableSet;
+
+const toVariableSetVariable = ({
+  id: _id,
+  variableSetId: _variableSetId,
+  ...variable
+}: typeof schema.variableSetVariable.$inferSelect) => variable;
+
   const items = sets.map((s) => ({
-    ...s,
-    variables: allVariables.filter((v) => v.variableSetId === s.id),
+    ...toVariableSet(s),
+    variables: allVariables
+      .filter((v) => v.variableSetId === s.id)
+      .map(toVariableSetVariable),
   }));
 
-  res.status(201).json(created);
+  res.status(201).json(toVariableSet(created));
 
-  res.json({ ...vs, variables });
+  res.json({
+    ...toVariableSet(vs),
+    variables: variables.map(toVariableSetVariable),
+  });
 
-  res.status(202).json(updated);
+  res.status(202).json(toVariableSet(updated));
 
-  res.status(202).json(deleted);
+  res.status(202).json(toVariableSet(deleted));

Also applies to: 64-83, 104-109, 119-155, 162-174

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/workspaces/variable-sets.ts` around lines 49 - 54, The
handler is returning raw DB rows (using sets.map and allVariables) which exposes
internal columns; update the projection so you construct the published API
shapes instead of spreading DB rows: for each set in sets build a VariableSet
object (pick only the public fields declared in VariableSet in openapi.ts—e.g.,
id/name/description/createdAt/updatedAt/etc., omit workspaceId and metadata) and
for each variable in allVariables build a VariableSetVariable object (pick only
the public fields, omit internal id and variableSetId), then assemble items and
return res.status(200).json({ items, total, limit, offset }); apply the same
mapping logic to the other handler blocks that use sets, allVariables, and the
items response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants