From 56cdfae4eb1586b938c04184419fdda6993db3cc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Feb 2026 06:40:34 +0000 Subject: [PATCH 1/2] Add metadata support for workspace entities Co-authored-by: Justin Brooks --- apps/workspace-engine/oapi/openapi.json | 26 ++++++++- .../oapi/spec/schemas/deployments.jsonnet | 6 +- .../oapi/spec/schemas/entities.jsonnet | 18 +++++- apps/workspace-engine/pkg/db/deployments.go | 32 ++++++++++- .../pkg/db/deployments_test.go | 12 ++++ apps/workspace-engine/pkg/db/environments.go | 33 ++++++++++- .../pkg/db/environments_test.go | 13 +++++ apps/workspace-engine/pkg/db/job_agents.go | 31 +++++++++- .../pkg/db/job_agents_test.go | 13 +++++ apps/workspace-engine/pkg/db/metadata.go | 56 +++++++++++++++++++ apps/workspace-engine/pkg/db/systems.go | 31 +++++++++- apps/workspace-engine/pkg/db/systems_test.go | 13 +++++ apps/workspace-engine/pkg/oapi/oapi.gen.go | 50 +++++++++-------- .../pkg/selector/langs/cel/cel.go | 6 +- .../pkg/workspace/relationships/property.go | 22 ++++++++ .../pkg/workspace/store/deployments.go | 4 ++ .../workspace/store/diffcheck/deployment.go | 12 ++++ .../store/diffcheck/deployment_test.go | 28 ++++++++++ .../workspace/store/diffcheck/environment.go | 12 ++++ .../store/diffcheck/environment_test.go | 24 ++++++++ .../pkg/workspace/store/environments.go | 4 ++ .../pkg/workspace/store/job_agents.go | 4 ++ .../pkg/workspace/store/systems.go | 4 ++ .../db/drizzle/0130_add_entity_metadata.sql | 42 ++++++++++++++ packages/db/src/schema/deployment.ts | 46 +++++++++++---- packages/db/src/schema/job-agent.ts | 24 ++++++++ packages/db/src/schema/system.ts | 21 +++++++ 27 files changed, 541 insertions(+), 46 deletions(-) create mode 100644 apps/workspace-engine/pkg/db/metadata.go create mode 100644 packages/db/drizzle/0130_add_entity_metadata.sql diff --git a/apps/workspace-engine/oapi/openapi.json b/apps/workspace-engine/oapi/openapi.json index 9542daa37..966ceb28d 100644 --- a/apps/workspace-engine/oapi/openapi.json +++ b/apps/workspace-engine/oapi/openapi.json @@ -179,6 +179,12 @@ "name": { "type": "string" }, + "metadata": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, "resourceSelector": { "$ref": "#/components/schemas/Selector" }, @@ -192,6 +198,7 @@ "required": [ "id", "name", + "metadata", "slug", "systemId", "jobAgentConfig" @@ -459,6 +466,7 @@ "required": [ "id", "name", + "metadata", "systemId", "createdAt" ], @@ -737,6 +745,12 @@ "name": { "type": "string" }, + "metadata": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, "type": { "type": "string" }, @@ -749,7 +763,8 @@ "workspaceId", "name", "type", - "config" + "config", + "metadata" ], "type": "object" }, @@ -1764,6 +1779,12 @@ "name": { "type": "string" }, + "metadata": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, "workspaceId": { "type": "string" } @@ -1771,7 +1792,8 @@ "required": [ "id", "workspaceId", - "name" + "name", + "metadata" ], "type": "object" }, diff --git a/apps/workspace-engine/oapi/spec/schemas/deployments.jsonnet b/apps/workspace-engine/oapi/spec/schemas/deployments.jsonnet index e609154c3..cbcbc5dd9 100644 --- a/apps/workspace-engine/oapi/spec/schemas/deployments.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/deployments.jsonnet @@ -3,7 +3,7 @@ local openapi = import '../lib/openapi.libsonnet'; { Deployment: { type: 'object', - required: ['id', 'name', 'slug', 'systemId', 'jobAgentConfig'], + required: ['id', 'name', 'slug', 'systemId', 'jobAgentConfig', 'metadata'], properties: { id: { type: 'string' }, name: { type: 'string' }, @@ -13,6 +13,10 @@ local openapi = import '../lib/openapi.libsonnet'; jobAgentId: { type: 'string' }, jobAgentConfig: openapi.schemaRef('JobAgentConfig'), resourceSelector: openapi.schemaRef('Selector'), + metadata: { + type: 'object', + additionalProperties: { type: 'string' }, + }, }, }, diff --git a/apps/workspace-engine/oapi/spec/schemas/entities.jsonnet b/apps/workspace-engine/oapi/spec/schemas/entities.jsonnet index a3994ca3f..f4d824a93 100644 --- a/apps/workspace-engine/oapi/spec/schemas/entities.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/entities.jsonnet @@ -64,7 +64,7 @@ local openapi = import '../lib/openapi.libsonnet'; Environment: { type: 'object', - required: ['id', 'name', 'systemId', 'createdAt'], + required: ['id', 'name', 'systemId', 'createdAt', 'metadata'], properties: { id: { type: 'string' }, name: { type: 'string' }, @@ -72,29 +72,41 @@ local openapi = import '../lib/openapi.libsonnet'; systemId: { type: 'string' }, resourceSelector: openapi.schemaRef('Selector'), createdAt: { type: 'string', format: 'date-time' }, + metadata: { + type: 'object', + additionalProperties: { type: 'string' }, + }, }, }, System: { type: 'object', - required: ['id', 'workspaceId', 'name'], + required: ['id', 'workspaceId', 'name', 'metadata'], properties: { id: { type: 'string' }, workspaceId: { type: 'string' }, name: { type: 'string' }, description: { type: 'string' }, + metadata: { + type: 'object', + additionalProperties: { type: 'string' }, + }, }, }, JobAgent: { type: 'object', - required: ['id', 'workspaceId', 'name', 'type', 'config'], + required: ['id', 'workspaceId', 'name', 'type', 'config', 'metadata'], properties: { id: { type: 'string' }, workspaceId: { type: 'string' }, name: { type: 'string' }, type: { type: 'string' }, config: openapi.schemaRef('JobAgentConfig'), + metadata: { + type: 'object', + additionalProperties: { type: 'string' }, + }, }, }, diff --git a/apps/workspace-engine/pkg/db/deployments.go b/apps/workspace-engine/pkg/db/deployments.go index e203d5eb8..9929ca63d 100644 --- a/apps/workspace-engine/pkg/db/deployments.go +++ b/apps/workspace-engine/pkg/db/deployments.go @@ -17,10 +17,19 @@ const DEPLOYMENT_SELECT_QUERY = ` d.system_id, d.job_agent_id, d.job_agent_config, - d.resource_selector + d.resource_selector, + COALESCE( + json_object_agg( + COALESCE(dm.key, ''), + COALESCE(dm.value, '') + ) FILTER (WHERE dm.key IS NOT NULL), + '{}'::json + ) as metadata FROM deployment d INNER JOIN system s ON s.id = d.system_id + LEFT JOIN deployment_metadata dm ON dm.deployment_id = d.id WHERE s.workspace_id = $1 + GROUP BY d.id, d.name, d.slug, d.description, d.system_id, d.job_agent_id, d.job_agent_config, d.resource_selector ` func getDeployments(ctx context.Context, workspaceID string) ([]*oapi.Deployment, error) { @@ -40,6 +49,7 @@ func getDeployments(ctx context.Context, workspaceID string) ([]*oapi.Deployment for rows.Next() { var deployment oapi.Deployment var rawSelector map[string]interface{} + var metadataJSON []byte err := rows.Scan( &deployment.Id, @@ -50,6 +60,7 @@ func getDeployments(ctx context.Context, workspaceID string) ([]*oapi.Deployment &deployment.JobAgentId, &deployment.JobAgentConfig, &rawSelector, + &metadataJSON, ) if err != nil { return nil, err @@ -61,6 +72,12 @@ func getDeployments(ctx context.Context, workspaceID string) ([]*oapi.Deployment return nil, err } + metadata, err := parseMetadataJSON(metadataJSON) + if err != nil { + return nil, err + } + deployment.Metadata = metadata + deployments = append(deployments, &deployment) } if err := rows.Err(); err != nil { @@ -104,6 +121,19 @@ func writeDeployment(ctx context.Context, deployment *oapi.Deployment, tx pgx.Tx return err } + metadata := deployment.Metadata + if metadata == nil { + metadata = map[string]string{} + } + + if _, err := tx.Exec(ctx, "DELETE FROM deployment_metadata WHERE deployment_id = $1", deployment.Id); err != nil { + return err + } + + if err := writeMetadata(ctx, "deployment_metadata", "deployment_id", deployment.Id, metadata, tx); err != nil { + return err + } + return nil } diff --git a/apps/workspace-engine/pkg/db/deployments_test.go b/apps/workspace-engine/pkg/db/deployments_test.go index 9de330b59..33a8744c6 100644 --- a/apps/workspace-engine/pkg/db/deployments_test.go +++ b/apps/workspace-engine/pkg/db/deployments_test.go @@ -43,6 +43,17 @@ func validateRetrievedDeployments(t *testing.T, actualDeployments []*oapi.Deploy } compareStrPtr(t, actual.Description, expected.Description) compareStrPtr(t, actual.JobAgentId, expected.JobAgentId) + expectedMetadata := expected.Metadata + if expectedMetadata == nil { + expectedMetadata = map[string]string{} + } + actualMetadata := actual.Metadata + if actualMetadata == nil { + actualMetadata = map[string]string{} + } + if !reflect.DeepEqual(expectedMetadata, actualMetadata) { + t.Fatalf("expected deployment metadata %v, got %v", expectedMetadata, actualMetadata) + } // Note: ResourceSelector is *Selector (complex type), comparing as pointers only if (actual.ResourceSelector == nil) != (expected.ResourceSelector == nil) { t.Fatalf("resource_selector nil mismatch: expected %v, got %v", expected.ResourceSelector == nil, actual.ResourceSelector == nil) @@ -108,6 +119,7 @@ func TestDBDeployments_BasicWrite(t *testing.T) { SystemId: systemID, Description: &description, JobAgentConfig: oapi.JobAgentConfig{}, + Metadata: map[string]string{"team": "platform"}, ResourceSelector: nil, // Selector is complex type, skipping for basic test } diff --git a/apps/workspace-engine/pkg/db/environments.go b/apps/workspace-engine/pkg/db/environments.go index 6c68a328a..a08c83341 100644 --- a/apps/workspace-engine/pkg/db/environments.go +++ b/apps/workspace-engine/pkg/db/environments.go @@ -16,10 +16,19 @@ const ENVIRONMENT_SELECT_QUERY = ` e.system_id, e.created_at, e.description, - e.resource_selector + e.resource_selector, + COALESCE( + json_object_agg( + COALESCE(em.key, ''), + COALESCE(em.value, '') + ) FILTER (WHERE em.key IS NOT NULL), + '{}'::json + ) as metadata FROM environment e INNER JOIN system s ON s.id = e.system_id + LEFT JOIN environment_metadata em ON em.environment_id = e.id WHERE s.workspace_id = $1 + GROUP BY e.id, e.name, e.system_id, e.created_at, e.description, e.resource_selector ` func getEnvironments(ctx context.Context, workspaceID string) ([]*oapi.Environment, error) { @@ -40,6 +49,7 @@ func getEnvironments(ctx context.Context, workspaceID string) ([]*oapi.Environme var environment oapi.Environment var createdAt time.Time var rawSelector map[string]interface{} + var metadataJSON []byte err := rows.Scan( &environment.Id, @@ -48,6 +58,7 @@ func getEnvironments(ctx context.Context, workspaceID string) ([]*oapi.Environme &createdAt, &environment.Description, &rawSelector, + &metadataJSON, ) if err != nil { return nil, err @@ -60,6 +71,12 @@ func getEnvironments(ctx context.Context, workspaceID string) ([]*oapi.Environme return nil, err } + metadata, err := parseMetadataJSON(metadataJSON) + if err != nil { + return nil, err + } + environment.Metadata = metadata + environments = append(environments, &environment) } if err := rows.Err(); err != nil { @@ -96,6 +113,20 @@ func writeEnvironment(ctx context.Context, environment *oapi.Environment, tx pgx ); err != nil { return err } + + metadata := environment.Metadata + if metadata == nil { + metadata = map[string]string{} + } + + if _, err := tx.Exec(ctx, "DELETE FROM environment_metadata WHERE environment_id = $1", environment.Id); err != nil { + return err + } + + if err := writeMetadata(ctx, "environment_metadata", "environment_id", environment.Id, metadata, tx); err != nil { + return err + } + return nil } diff --git a/apps/workspace-engine/pkg/db/environments_test.go b/apps/workspace-engine/pkg/db/environments_test.go index a1afa421b..cbe941dd5 100644 --- a/apps/workspace-engine/pkg/db/environments_test.go +++ b/apps/workspace-engine/pkg/db/environments_test.go @@ -2,6 +2,7 @@ package db import ( "fmt" + "reflect" "strings" "testing" "workspace-engine/pkg/oapi" @@ -37,6 +38,17 @@ func validateRetrievedEnvironments(t *testing.T, actualEnvironments []*oapi.Envi t.Fatalf("expected environment system_id %s, got %s", expectedEnv.SystemId, actualEnv.SystemId) } compareStrPtr(t, actualEnv.Description, expectedEnv.Description) + expectedMetadata := expectedEnv.Metadata + if expectedMetadata == nil { + expectedMetadata = map[string]string{} + } + actualMetadata := actualEnv.Metadata + if actualMetadata == nil { + actualMetadata = map[string]string{} + } + if !reflect.DeepEqual(expectedMetadata, actualMetadata) { + t.Fatalf("expected environment metadata %v, got %v", expectedMetadata, actualMetadata) + } // Note: ResourceSelector is *Selector (complex type), comparing as pointers only if (actualEnv.ResourceSelector == nil) != (expectedEnv.ResourceSelector == nil) { t.Fatalf("resource_selector nil mismatch: expected %v, got %v", expectedEnv.ResourceSelector == nil, actualEnv.ResourceSelector == nil) @@ -81,6 +93,7 @@ func TestDBEnvironments_BasicWrite(t *testing.T) { Name: envName, SystemId: systemID, Description: &description, + Metadata: map[string]string{"tier": "backend"}, ResourceSelector: nil, // Selector is complex type, skipping for test } diff --git a/apps/workspace-engine/pkg/db/job_agents.go b/apps/workspace-engine/pkg/db/job_agents.go index d8cc46e68..d3513a1d1 100644 --- a/apps/workspace-engine/pkg/db/job_agents.go +++ b/apps/workspace-engine/pkg/db/job_agents.go @@ -14,9 +14,18 @@ const JOB_AGENT_SELECT_QUERY = ` j.workspace_id, j.name, j.type, - j.config + j.config, + COALESCE( + json_object_agg( + COALESCE(jm.key, ''), + COALESCE(jm.value, '') + ) FILTER (WHERE jm.key IS NOT NULL), + '{}'::json + ) as metadata FROM job_agent j + LEFT JOIN job_agent_metadata jm ON jm.job_agent_id = j.id WHERE j.workspace_id = $1 + GROUP BY j.id, j.workspace_id, j.name, j.type, j.config ` func getJobAgents(ctx context.Context, workspaceID string) ([]*oapi.JobAgent, error) { @@ -65,6 +74,7 @@ func runnerJobAgentConfig(m map[string]interface{}) oapi.JobAgentConfig { func scanJobAgentRow(rows pgx.Rows) (*oapi.JobAgent, error) { jobAgent := &oapi.JobAgent{} var config *map[string]interface{} + var metadataJSON []byte err := rows.Scan( &jobAgent.Id, @@ -72,6 +82,7 @@ func scanJobAgentRow(rows pgx.Rows) (*oapi.JobAgent, error) { &jobAgent.Name, &jobAgent.Type, &config, + &metadataJSON, ) if err != nil { return nil, err @@ -115,6 +126,11 @@ func scanJobAgentRow(rows pgx.Rows) (*oapi.JobAgent, error) { } jobAgent.Config = cfg + metadata, err := parseMetadataJSON(metadataJSON) + if err != nil { + return nil, err + } + jobAgent.Metadata = metadata return jobAgent, nil } @@ -140,6 +156,19 @@ func writeJobAgent(ctx context.Context, jobAgent *oapi.JobAgent, tx pgx.Tx) erro ); err != nil { return err } + + metadata := jobAgent.Metadata + if metadata == nil { + metadata = map[string]string{} + } + + if _, err := tx.Exec(ctx, "DELETE FROM job_agent_metadata WHERE job_agent_id = $1", jobAgent.Id); err != nil { + return err + } + + if err := writeMetadata(ctx, "job_agent_metadata", "job_agent_id", jobAgent.Id, metadata, tx); err != nil { + return err + } return nil } diff --git a/apps/workspace-engine/pkg/db/job_agents_test.go b/apps/workspace-engine/pkg/db/job_agents_test.go index a2c7e5c15..98e353a38 100644 --- a/apps/workspace-engine/pkg/db/job_agents_test.go +++ b/apps/workspace-engine/pkg/db/job_agents_test.go @@ -62,6 +62,18 @@ func validateRetrievedJobAgents(t *testing.T, actualJobAgents []*oapi.JobAgent, if !reflect.DeepEqual(expectedConfig, actualConfig) { t.Fatalf("expected config %v, got %v", expectedConfig, actualConfig) } + + expectedMetadata := expected.Metadata + if expectedMetadata == nil { + expectedMetadata = map[string]string{} + } + actualMetadata := actual.Metadata + if actualMetadata == nil { + actualMetadata = map[string]string{} + } + if !reflect.DeepEqual(expectedMetadata, actualMetadata) { + t.Fatalf("expected metadata %v, got %v", expectedMetadata, actualMetadata) + } } } @@ -86,6 +98,7 @@ func TestDBJobAgents_BasicWrite(t *testing.T) { Name: name, Type: "kubernetes", Config: config, + Metadata: map[string]string{"team": "release"}, } err = writeJobAgent(t.Context(), jobAgent, tx) diff --git a/apps/workspace-engine/pkg/db/metadata.go b/apps/workspace-engine/pkg/db/metadata.go new file mode 100644 index 000000000..1c667dc84 --- /dev/null +++ b/apps/workspace-engine/pkg/db/metadata.go @@ -0,0 +1,56 @@ +package db + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/jackc/pgx/v5" +) + +func parseMetadataJSON(metadataJSON []byte) (map[string]string, error) { + if len(metadataJSON) == 0 { + return map[string]string{}, nil + } + + var metadataMap map[string]string + if err := json.Unmarshal(metadataJSON, &metadataMap); err != nil { + return nil, err + } + if metadataMap == nil { + metadataMap = map[string]string{} + } + return metadataMap, nil +} + +func writeMetadata(ctx context.Context, table string, idColumn string, entityId string, metadata map[string]string, tx pgx.Tx) error { + if len(metadata) == 0 { + return nil + } + + valueStrings := make([]string, 0, len(metadata)) + valueArgs := make([]interface{}, 0, len(metadata)*3) + i := 1 + for k, v := range metadata { + valueStrings = append(valueStrings, + "($"+fmt.Sprintf("%d", i)+", $"+fmt.Sprintf("%d", i+1)+", $"+fmt.Sprintf("%d", i+2)+")", + ) + valueArgs = append(valueArgs, entityId, k, v) + i += 3 + } + + query := fmt.Sprintf( + "INSERT INTO %s (%s, key, value) VALUES %s ON CONFLICT (%s, key) DO UPDATE SET value = EXCLUDED.value", + table, + idColumn, + strings.Join(valueStrings, ", "), + idColumn, + ) + + _, err := tx.Exec(ctx, query, valueArgs...) + if err != nil { + return err + } + return nil +} diff --git a/apps/workspace-engine/pkg/db/systems.go b/apps/workspace-engine/pkg/db/systems.go index 90b38a052..f9f4b092d 100644 --- a/apps/workspace-engine/pkg/db/systems.go +++ b/apps/workspace-engine/pkg/db/systems.go @@ -12,9 +12,18 @@ const SYSTEM_SELECT_QUERY = ` s.id, s.workspace_id, s.name, - s.description + s.description, + COALESCE( + json_object_agg( + COALESCE(sm.key, ''), + COALESCE(sm.value, '') + ) FILTER (WHERE sm.key IS NOT NULL), + '{}'::json + ) as metadata FROM system s + LEFT JOIN system_metadata sm ON sm.system_id = s.id WHERE s.workspace_id = $1 + GROUP BY s.id, s.workspace_id, s.name, s.description ` func getSystems(ctx context.Context, workspaceID string) ([]*oapi.System, error) { @@ -43,15 +52,22 @@ func getSystems(ctx context.Context, workspaceID string) ([]*oapi.System, error) func scanSystemRow(rows pgx.Rows) (*oapi.System, error) { system := &oapi.System{} + var metadataJSON []byte err := rows.Scan( &system.Id, &system.WorkspaceId, &system.Name, &system.Description, + &metadataJSON, ) if err != nil { return nil, err } + metadata, err := parseMetadataJSON(metadataJSON) + if err != nil { + return nil, err + } + system.Metadata = metadata return system, nil } @@ -77,6 +93,19 @@ func writeSystem(ctx context.Context, system *oapi.System, tx pgx.Tx) error { ); err != nil { return err } + + metadata := system.Metadata + if metadata == nil { + metadata = map[string]string{} + } + + if _, err := tx.Exec(ctx, "DELETE FROM system_metadata WHERE system_id = $1", system.Id); err != nil { + return err + } + + if err := writeMetadata(ctx, "system_metadata", "system_id", system.Id, metadata, tx); err != nil { + return err + } return nil } diff --git a/apps/workspace-engine/pkg/db/systems_test.go b/apps/workspace-engine/pkg/db/systems_test.go index 1b1a7ab6c..ac8e64aef 100644 --- a/apps/workspace-engine/pkg/db/systems_test.go +++ b/apps/workspace-engine/pkg/db/systems_test.go @@ -2,6 +2,7 @@ package db import ( "fmt" + "reflect" "strings" "testing" "workspace-engine/pkg/oapi" @@ -35,6 +36,17 @@ func validateRetrievedSystems(t *testing.T, actualSystems []*oapi.System, expect if actualSystem.WorkspaceId != expectedSystem.WorkspaceId { t.Fatalf("expected system %v, got %v", expectedSystem, actualSystem) } + expectedMetadata := expectedSystem.Metadata + if expectedMetadata == nil { + expectedMetadata = map[string]string{} + } + actualMetadata := actualSystem.Metadata + if actualMetadata == nil { + actualMetadata = map[string]string{} + } + if !reflect.DeepEqual(expectedMetadata, actualMetadata) { + t.Fatalf("expected system metadata %v, got %v", expectedMetadata, actualMetadata) + } } } @@ -56,6 +68,7 @@ func TestDBSystems_BasicWrite(t *testing.T) { WorkspaceId: workspaceID, Name: name, Description: &description, + Metadata: map[string]string{"owner": "platform"}, } err = writeSystem(t.Context(), sys, tx) diff --git a/apps/workspace-engine/pkg/oapi/oapi.gen.go b/apps/workspace-engine/pkg/oapi/oapi.gen.go index 983163cf4..653747bc4 100644 --- a/apps/workspace-engine/pkg/oapi/oapi.gen.go +++ b/apps/workspace-engine/pkg/oapi/oapi.gen.go @@ -279,14 +279,15 @@ type DeployDecision struct { // Deployment defines model for Deployment. type Deployment struct { - Description *string `json:"description,omitempty"` - Id string `json:"id"` - JobAgentConfig JobAgentConfig `json:"jobAgentConfig"` - JobAgentId *string `json:"jobAgentId,omitempty"` - Name string `json:"name"` - ResourceSelector *Selector `json:"resourceSelector,omitempty"` - Slug string `json:"slug"` - SystemId string `json:"systemId"` + Description *string `json:"description,omitempty"` + Id string `json:"id"` + JobAgentConfig JobAgentConfig `json:"jobAgentConfig"` + JobAgentId *string `json:"jobAgentId,omitempty"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + ResourceSelector *Selector `json:"resourceSelector,omitempty"` + Slug string `json:"slug"` + SystemId string `json:"systemId"` } // DeploymentAndSystem defines model for DeploymentAndSystem. @@ -378,12 +379,13 @@ type EntityRelation struct { // Environment defines model for Environment. type Environment struct { - CreatedAt time.Time `json:"createdAt"` - Description *string `json:"description,omitempty"` - Id string `json:"id"` - Name string `json:"name"` - ResourceSelector *Selector `json:"resourceSelector,omitempty"` - SystemId string `json:"systemId"` + CreatedAt time.Time `json:"createdAt"` + Description *string `json:"description,omitempty"` + Id string `json:"id"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + ResourceSelector *Selector `json:"resourceSelector,omitempty"` + SystemId string `json:"systemId"` } // EnvironmentProgressionRule defines model for EnvironmentProgressionRule. @@ -502,11 +504,12 @@ type Job struct { // JobAgent defines model for JobAgent. type JobAgent struct { - Config JobAgentConfig `json:"config"` - Id string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - WorkspaceId string `json:"workspaceId"` + Config JobAgentConfig `json:"config"` + Id string `json:"id"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + Type string `json:"type"` + WorkspaceId string `json:"workspaceId"` } // JobAgentConfig defines model for JobAgentConfig. @@ -890,10 +893,11 @@ type StringValue = string // System defines model for System. type System struct { - Description *string `json:"description,omitempty"` - Id string `json:"id"` - Name string `json:"name"` - WorkspaceId string `json:"workspaceId"` + Description *string `json:"description,omitempty"` + Id string `json:"id"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + WorkspaceId string `json:"workspaceId"` } // TerraformCloudJobAgentConfig defines model for TerraformCloudJobAgentConfig. diff --git a/apps/workspace-engine/pkg/selector/langs/cel/cel.go b/apps/workspace-engine/pkg/selector/langs/cel/cel.go index 71194e4a6..d3db1205d 100644 --- a/apps/workspace-engine/pkg/selector/langs/cel/cel.go +++ b/apps/workspace-engine/pkg/selector/langs/cel/cel.go @@ -192,12 +192,13 @@ func resourceToMap(r *oapi.Resource) map[string]any { } func deploymentToMap(d *oapi.Deployment) map[string]any { - m := make(map[string]any, 8) + m := make(map[string]any, 9) m["id"] = d.Id m["name"] = d.Name m["slug"] = d.Slug m["systemId"] = d.SystemId m["jobAgentConfig"] = d.JobAgentConfig + m["metadata"] = d.Metadata if d.Description != nil { m["description"] = *d.Description } @@ -211,11 +212,12 @@ func deploymentToMap(d *oapi.Deployment) map[string]any { } func environmentToMap(e *oapi.Environment) map[string]any { - m := make(map[string]any, 6) + m := make(map[string]any, 7) m["id"] = e.Id m["name"] = e.Name m["systemId"] = e.SystemId m["createdAt"] = e.CreatedAt + m["metadata"] = e.Metadata if e.Description != nil { m["description"] = *e.Description } diff --git a/apps/workspace-engine/pkg/workspace/relationships/property.go b/apps/workspace-engine/pkg/workspace/relationships/property.go index 5c42e9708..9acfd39aa 100644 --- a/apps/workspace-engine/pkg/workspace/relationships/property.go +++ b/apps/workspace-engine/pkg/workspace/relationships/property.go @@ -125,6 +125,17 @@ func getDeploymentProperty(deployment *oapi.Deployment, propertyPath []string) ( return nil, err } return convertValue(value) + case "metadata": + if len(propertyPath) == 1 { + return convertValue(deployment.Metadata) + } + if len(propertyPath) == 2 { + if val, ok := deployment.Metadata[propertyPath[1]]; ok { + return convertValue(val) + } + return nil, fmt.Errorf("metadata key %s not found", propertyPath[1]) + } + return nil, fmt.Errorf("metadata path too deep: %v", propertyPath) default: return getPropertyReflection(deployment, propertyPath) } @@ -149,6 +160,17 @@ func getEnvironmentProperty(environment *oapi.Environment, propertyPath []string return nil, fmt.Errorf("description is nil") case "system_id", "systemid": return convertValue(environment.SystemId) + case "metadata": + if len(propertyPath) == 1 { + return convertValue(environment.Metadata) + } + if len(propertyPath) == 2 { + if val, ok := environment.Metadata[propertyPath[1]]; ok { + return convertValue(val) + } + return nil, fmt.Errorf("metadata key %s not found", propertyPath[1]) + } + return nil, fmt.Errorf("metadata path too deep: %v", propertyPath) default: return getPropertyReflection(environment, propertyPath) } diff --git a/apps/workspace-engine/pkg/workspace/store/deployments.go b/apps/workspace-engine/pkg/workspace/store/deployments.go index 457c5231c..f0a8628eb 100644 --- a/apps/workspace-engine/pkg/workspace/store/deployments.go +++ b/apps/workspace-engine/pkg/workspace/store/deployments.go @@ -34,6 +34,10 @@ func (e *Deployments) Upsert(ctx context.Context, deployment *oapi.Deployment) e _, span := deploymentsTracer.Start(ctx, "UpsertDeployment") defer span.End() + if deployment.Metadata == nil { + deployment.Metadata = map[string]string{} + } + e.repo.Deployments.Set(deployment.Id, deployment) e.store.changeset.RecordUpsert(deployment) diff --git a/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment.go b/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment.go index 083cf0abd..a0065ffbd 100644 --- a/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment.go +++ b/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment.go @@ -144,6 +144,18 @@ func hasDeploymentChangesBasic(old, new *oapi.Deployment) map[string]bool { } } + // Compare metadata + for key := range old.Metadata { + if newVal, exists := new.Metadata[key]; !exists || old.Metadata[key] != newVal { + changed["metadata."+key] = true + } + } + for key := range new.Metadata { + if _, exists := old.Metadata[key]; !exists { + changed["metadata."+key] = true + } + } + // Compare ResourceSelector using deep equality if !deepEqual(old.ResourceSelector, new.ResourceSelector) { changed["resourceselector"] = true diff --git a/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment_test.go b/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment_test.go index f3902acc8..093a39b03 100644 --- a/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment_test.go +++ b/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment_test.go @@ -413,6 +413,34 @@ func TestHasDeploymentChanges_ResourceSelectorChanged(t *testing.T) { assert.True(t, hasResourceSelectorChange, "Should detect resourceSelector change") } +func TestHasDeploymentChanges_MetadataChanged(t *testing.T) { + old := &oapi.Deployment{ + Name: "api-deployment", + Slug: "api-deployment", + SystemId: "sys-123", + JobAgentConfig: oapi.JobAgentConfig{}, + Metadata: map[string]string{ + "tier": "backend", + }, + Id: "deploy-123", + } + + new := &oapi.Deployment{ + Name: "api-deployment", + Slug: "api-deployment", + SystemId: "sys-123", + JobAgentConfig: oapi.JobAgentConfig{}, + Metadata: map[string]string{ + "tier": "frontend", + }, + Id: "deploy-123", + } + + changes := HasDeploymentChanges(old, new) + assert.Len(t, changes, 1, "Should detect metadata change") + assert.True(t, changes["metadata.tier"], "Should detect metadata.tier change") +} + func TestHasDeploymentChanges_MultipleChanges(t *testing.T) { oldDesc := "old description" newDesc := "new description" diff --git a/apps/workspace-engine/pkg/workspace/store/diffcheck/environment.go b/apps/workspace-engine/pkg/workspace/store/diffcheck/environment.go index 0e0a8c2ca..3be14105f 100644 --- a/apps/workspace-engine/pkg/workspace/store/diffcheck/environment.go +++ b/apps/workspace-engine/pkg/workspace/store/diffcheck/environment.go @@ -81,5 +81,17 @@ func hasEnvironmentChangesBasic(old, new *oapi.Environment) map[string]bool { changed["resourceselector"] = true } + // Compare metadata + for key := range old.Metadata { + if newVal, exists := new.Metadata[key]; !exists || old.Metadata[key] != newVal { + changed["metadata."+key] = true + } + } + for key := range new.Metadata { + if _, exists := old.Metadata[key]; !exists { + changed["metadata."+key] = true + } + } + return changed } diff --git a/apps/workspace-engine/pkg/workspace/store/diffcheck/environment_test.go b/apps/workspace-engine/pkg/workspace/store/diffcheck/environment_test.go index dd4e91947..20e4713d0 100644 --- a/apps/workspace-engine/pkg/workspace/store/diffcheck/environment_test.go +++ b/apps/workspace-engine/pkg/workspace/store/diffcheck/environment_test.go @@ -242,6 +242,30 @@ func TestHasEnvironmentChanges_ResourceSelectorChanged(t *testing.T) { assert.True(t, hasResourceSelectorChange, "Should detect resourceSelector change") } +func TestHasEnvironmentChanges_MetadataChanged(t *testing.T) { + old := &oapi.Environment{ + Name: "production", + SystemId: "sys-123", + Metadata: map[string]string{ + "region": "us-east-1", + }, + Id: "env-123", + } + + new := &oapi.Environment{ + Name: "production", + SystemId: "sys-123", + Metadata: map[string]string{ + "region": "us-west-2", + }, + Id: "env-123", + } + + changes := HasEnvironmentChanges(old, new) + assert.Len(t, changes, 1, "Should detect metadata change") + assert.True(t, changes["metadata.region"], "Should detect metadata.region change") +} + func TestHasEnvironmentChanges_ResourceSelectorNilToSet(t *testing.T) { newSelector := &oapi.Selector{} _ = newSelector.FromJsonSelector(oapi.JsonSelector{ diff --git a/apps/workspace-engine/pkg/workspace/store/environments.go b/apps/workspace-engine/pkg/workspace/store/environments.go index ecdf8075f..2127bcafb 100644 --- a/apps/workspace-engine/pkg/workspace/store/environments.go +++ b/apps/workspace-engine/pkg/workspace/store/environments.go @@ -29,6 +29,10 @@ func (e *Environments) Get(id string) (*oapi.Environment, bool) { } func (e *Environments) Upsert(ctx context.Context, environment *oapi.Environment) error { + if environment.Metadata == nil { + environment.Metadata = map[string]string{} + } + e.repo.Environments.Set(environment.Id, environment) e.store.changeset.RecordUpsert(environment) diff --git a/apps/workspace-engine/pkg/workspace/store/job_agents.go b/apps/workspace-engine/pkg/workspace/store/job_agents.go index b77559d75..653b95f3b 100644 --- a/apps/workspace-engine/pkg/workspace/store/job_agents.go +++ b/apps/workspace-engine/pkg/workspace/store/job_agents.go @@ -19,6 +19,10 @@ type JobAgents struct { } func (j *JobAgents) Upsert(ctx context.Context, jobAgent *oapi.JobAgent) { + if jobAgent.Metadata == nil { + jobAgent.Metadata = map[string]string{} + } + j.repo.JobAgents.Set(jobAgent.Id, jobAgent) j.store.changeset.RecordUpsert(jobAgent) } diff --git a/apps/workspace-engine/pkg/workspace/store/systems.go b/apps/workspace-engine/pkg/workspace/store/systems.go index 8c4c087f2..23d379a00 100644 --- a/apps/workspace-engine/pkg/workspace/store/systems.go +++ b/apps/workspace-engine/pkg/workspace/store/systems.go @@ -23,6 +23,10 @@ func (s *Systems) Get(id string) (*oapi.System, bool) { } func (s *Systems) Upsert(ctx context.Context, system *oapi.System) error { + if system.Metadata == nil { + system.Metadata = map[string]string{} + } + s.repo.Systems.Set(system.Id, system) s.store.changeset.RecordUpsert(system) diff --git a/packages/db/drizzle/0130_add_entity_metadata.sql b/packages/db/drizzle/0130_add_entity_metadata.sql new file mode 100644 index 000000000..0f6941ad0 --- /dev/null +++ b/packages/db/drizzle/0130_add_entity_metadata.sql @@ -0,0 +1,42 @@ +CREATE TABLE IF NOT EXISTS "deployment_metadata" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "deployment_id" uuid NOT NULL, + "key" text NOT NULL, + "value" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "system_metadata" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "system_id" uuid NOT NULL, + "key" text NOT NULL, + "value" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "job_agent_metadata" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "job_agent_id" uuid NOT NULL, + "key" text NOT NULL, + "value" text NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "deployment_metadata" ADD CONSTRAINT "deployment_metadata_deployment_id_deployment_id_fk" FOREIGN KEY ("deployment_id") REFERENCES "public"."deployment"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "system_metadata" ADD CONSTRAINT "system_metadata_system_id_system_id_fk" FOREIGN KEY ("system_id") REFERENCES "public"."system"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "job_agent_metadata" ADD CONSTRAINT "job_agent_metadata_job_agent_id_job_agent_id_fk" FOREIGN KEY ("job_agent_id") REFERENCES "public"."job_agent"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "deployment_metadata_key_deployment_id_index" ON "deployment_metadata" USING btree ("key","deployment_id");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "system_metadata_key_system_id_index" ON "system_metadata" USING btree ("key","system_id");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "job_agent_metadata_key_job_agent_id_index" ON "job_agent_metadata" USING btree ("key","job_agent_id");--> statement-breakpoint diff --git a/packages/db/src/schema/deployment.ts b/packages/db/src/schema/deployment.ts index ca0d2dbf9..62a133ed1 100644 --- a/packages/db/src/schema/deployment.ts +++ b/packages/db/src/schema/deployment.ts @@ -116,18 +116,28 @@ export const updateDeployment = deploymentInsert.partial(); export type UpdateDeployment = z.infer; export type Deployment = InferSelectModel; -export const deploymentRelations = relations(deployment, ({ one, many }) => ({ - system: one(system, { - fields: [deployment.systemId], - references: [system.id], - }), - jobAgent: one(jobAgent, { - fields: [deployment.jobAgentId], - references: [jobAgent.id], +export const deploymentMetadata = pgTable( + "deployment_metadata", + { + id: uuid("id").primaryKey().defaultRandom().notNull(), + deploymentId: uuid("deployment_id") + .references(() => deployment.id, { onDelete: "cascade" }) + .notNull(), + key: text("key").notNull(), + value: text("value").notNull(), + }, + (t) => ({ uniq: uniqueIndex().on(t.key, t.deploymentId) }), +); + +export const deploymentMetadataRelations = relations( + deploymentMetadata, + ({ one }) => ({ + deployment: one(deployment, { + fields: [deploymentMetadata.deploymentId], + references: [deployment.id], + }), }), - computedResources: many(computedDeploymentResource), - releaseTargets: many(releaseTarget), -})); +); export const computedDeploymentResource = pgTable( "computed_deployment_resource", @@ -156,6 +166,20 @@ export const computedDeploymentResourceRelations = relations( }), ); +export const deploymentRelations = relations(deployment, ({ one, many }) => ({ + system: one(system, { + fields: [deployment.systemId], + references: [system.id], + }), + jobAgent: one(jobAgent, { + fields: [deployment.jobAgentId], + references: [jobAgent.id], + }), + metadata: many(deploymentMetadata), + computedResources: many(computedDeploymentResource), + releaseTargets: many(releaseTarget), +})); + const buildCondition = (cond: DeploymentCondition): SQL => { if (cond.type === "name") return ColumnOperatorFn[cond.operator](deployment.name, cond.value); diff --git a/packages/db/src/schema/job-agent.ts b/packages/db/src/schema/job-agent.ts index b07a96e20..0abe8d5b1 100644 --- a/packages/db/src/schema/job-agent.ts +++ b/packages/db/src/schema/job-agent.ts @@ -21,8 +21,32 @@ export const jobAgent = pgTable( (t) => ({ uniq: uniqueIndex().on(t.workspaceId, t.name) }), ); +export const jobAgentMetadata = pgTable( + "job_agent_metadata", + { + id: uuid("id").primaryKey().defaultRandom().notNull(), + jobAgentId: uuid("job_agent_id") + .references(() => jobAgent.id, { onDelete: "cascade" }) + .notNull(), + key: text("key").notNull(), + value: text("value").notNull(), + }, + (t) => ({ uniq: uniqueIndex().on(t.key, t.jobAgentId) }), +); + +export const jobAgentMetadataRelations = relations( + jobAgentMetadata, + ({ one }) => ({ + jobAgent: one(jobAgent, { + fields: [jobAgentMetadata.jobAgentId], + references: [jobAgent.id], + }), + }), +); + export const jobAgentRelations = relations(jobAgent, ({ many }) => ({ jobs: many(job), + metadata: many(jobAgentMetadata), })); export const createJobAgent = createInsertSchema(jobAgent, { diff --git a/packages/db/src/schema/system.ts b/packages/db/src/schema/system.ts index 4c108ec23..dcab6b8aa 100644 --- a/packages/db/src/schema/system.ts +++ b/packages/db/src/schema/system.ts @@ -48,9 +48,30 @@ export const updateSystem = createSystem.partial(); export type System = InferSelectModel; +export const systemMetadata = pgTable( + "system_metadata", + { + id: uuid("id").primaryKey().defaultRandom().notNull(), + systemId: uuid("system_id") + .references(() => system.id, { onDelete: "cascade" }) + .notNull(), + key: text("key").notNull(), + value: text("value").notNull(), + }, + (t) => ({ uniq: uniqueIndex().on(t.key, t.systemId) }), +); + +export const systemMetadataRelations = relations(systemMetadata, ({ one }) => ({ + system: one(system, { + fields: [systemMetadata.systemId], + references: [system.id], + }), +})); + export const systemRelations = relations(system, ({ one, many }) => ({ environments: many(environment), deployments: many(deployment), + metadata: many(systemMetadata), workspace: one(workspace, { fields: [system.workspaceId], references: [workspace.id], From ff804a8e7c7e45364adcb0e0b0fbc3f0204be541 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Feb 2026 15:38:49 +0000 Subject: [PATCH 2/2] Revert db metadata changes; update api types Co-authored-by: Justin Brooks --- apps/api/src/types/openapi.ts | 27 +++++++++ apps/workspace-engine/pkg/db/deployments.go | 32 +---------- .../pkg/db/deployments_test.go | 12 ---- apps/workspace-engine/pkg/db/environments.go | 33 +---------- .../pkg/db/environments_test.go | 13 ----- apps/workspace-engine/pkg/db/job_agents.go | 31 +--------- .../pkg/db/job_agents_test.go | 13 ----- apps/workspace-engine/pkg/db/metadata.go | 56 ------------------- apps/workspace-engine/pkg/db/systems.go | 31 +--------- apps/workspace-engine/pkg/db/systems_test.go | 13 ----- .../db/drizzle/0130_add_entity_metadata.sql | 42 -------------- packages/db/src/schema/deployment.ts | 46 ++++----------- packages/db/src/schema/job-agent.ts | 24 -------- packages/db/src/schema/system.ts | 21 ------- 14 files changed, 42 insertions(+), 352 deletions(-) delete mode 100644 apps/workspace-engine/pkg/db/metadata.go delete mode 100644 packages/db/drizzle/0130_add_entity_metadata.sql diff --git a/apps/api/src/types/openapi.ts b/apps/api/src/types/openapi.ts index 35a42b56b..c38866f72 100644 --- a/apps/api/src/types/openapi.ts +++ b/apps/api/src/types/openapi.ts @@ -816,6 +816,9 @@ export interface components { [key: string]: unknown; }; jobAgentId?: string; + metadata?: { + [key: string]: string; + }; name: string; resourceSelector?: components["schemas"]["Selector"]; slug: string; @@ -839,6 +842,9 @@ export interface components { }; CreateEnvironmentRequest: { description?: string; + metadata?: { + [key: string]: string; + }; name: string; resourceSelector?: components["schemas"]["Selector"]; systemId: string; @@ -871,6 +877,9 @@ export interface components { }; CreateSystemRequest: { description?: string; + metadata?: { + [key: string]: string; + }; name: string; }; CreateWorkspaceRequest: { @@ -930,6 +939,9 @@ export interface components { [key: string]: unknown; }; jobAgentId?: string; + metadata: { + [key: string]: string; + }; name: string; resourceSelector?: components["schemas"]["Selector"]; slug: string; @@ -1007,6 +1019,9 @@ export interface components { createdAt: string; description?: string; id: string; + metadata: { + [key: string]: string; + }; name: string; resourceSelector?: components["schemas"]["Selector"]; systemId: string; @@ -1346,6 +1361,9 @@ export interface components { System: { description?: string; id: string; + metadata: { + [key: string]: string; + }; name: string; slug: string; workspaceId: string; @@ -1400,6 +1418,9 @@ export interface components { [key: string]: unknown; }; jobAgentId?: string; + metadata?: { + [key: string]: string; + }; name: string; resourceSelector?: components["schemas"]["Selector"]; slug: string; @@ -1435,6 +1456,9 @@ export interface components { }; UpsertEnvironmentRequest: { description?: string; + metadata?: { + [key: string]: string; + }; name: string; resourceSelector?: components["schemas"]["Selector"]; systemId: string; @@ -1485,6 +1509,9 @@ export interface components { }; UpsertSystemRequest: { description?: string; + metadata?: { + [key: string]: string; + }; name: string; }; UpsertUserApprovalRecordRequest: { diff --git a/apps/workspace-engine/pkg/db/deployments.go b/apps/workspace-engine/pkg/db/deployments.go index 9929ca63d..e203d5eb8 100644 --- a/apps/workspace-engine/pkg/db/deployments.go +++ b/apps/workspace-engine/pkg/db/deployments.go @@ -17,19 +17,10 @@ const DEPLOYMENT_SELECT_QUERY = ` d.system_id, d.job_agent_id, d.job_agent_config, - d.resource_selector, - COALESCE( - json_object_agg( - COALESCE(dm.key, ''), - COALESCE(dm.value, '') - ) FILTER (WHERE dm.key IS NOT NULL), - '{}'::json - ) as metadata + d.resource_selector FROM deployment d INNER JOIN system s ON s.id = d.system_id - LEFT JOIN deployment_metadata dm ON dm.deployment_id = d.id WHERE s.workspace_id = $1 - GROUP BY d.id, d.name, d.slug, d.description, d.system_id, d.job_agent_id, d.job_agent_config, d.resource_selector ` func getDeployments(ctx context.Context, workspaceID string) ([]*oapi.Deployment, error) { @@ -49,7 +40,6 @@ func getDeployments(ctx context.Context, workspaceID string) ([]*oapi.Deployment for rows.Next() { var deployment oapi.Deployment var rawSelector map[string]interface{} - var metadataJSON []byte err := rows.Scan( &deployment.Id, @@ -60,7 +50,6 @@ func getDeployments(ctx context.Context, workspaceID string) ([]*oapi.Deployment &deployment.JobAgentId, &deployment.JobAgentConfig, &rawSelector, - &metadataJSON, ) if err != nil { return nil, err @@ -72,12 +61,6 @@ func getDeployments(ctx context.Context, workspaceID string) ([]*oapi.Deployment return nil, err } - metadata, err := parseMetadataJSON(metadataJSON) - if err != nil { - return nil, err - } - deployment.Metadata = metadata - deployments = append(deployments, &deployment) } if err := rows.Err(); err != nil { @@ -121,19 +104,6 @@ func writeDeployment(ctx context.Context, deployment *oapi.Deployment, tx pgx.Tx return err } - metadata := deployment.Metadata - if metadata == nil { - metadata = map[string]string{} - } - - if _, err := tx.Exec(ctx, "DELETE FROM deployment_metadata WHERE deployment_id = $1", deployment.Id); err != nil { - return err - } - - if err := writeMetadata(ctx, "deployment_metadata", "deployment_id", deployment.Id, metadata, tx); err != nil { - return err - } - return nil } diff --git a/apps/workspace-engine/pkg/db/deployments_test.go b/apps/workspace-engine/pkg/db/deployments_test.go index 33a8744c6..9de330b59 100644 --- a/apps/workspace-engine/pkg/db/deployments_test.go +++ b/apps/workspace-engine/pkg/db/deployments_test.go @@ -43,17 +43,6 @@ func validateRetrievedDeployments(t *testing.T, actualDeployments []*oapi.Deploy } compareStrPtr(t, actual.Description, expected.Description) compareStrPtr(t, actual.JobAgentId, expected.JobAgentId) - expectedMetadata := expected.Metadata - if expectedMetadata == nil { - expectedMetadata = map[string]string{} - } - actualMetadata := actual.Metadata - if actualMetadata == nil { - actualMetadata = map[string]string{} - } - if !reflect.DeepEqual(expectedMetadata, actualMetadata) { - t.Fatalf("expected deployment metadata %v, got %v", expectedMetadata, actualMetadata) - } // Note: ResourceSelector is *Selector (complex type), comparing as pointers only if (actual.ResourceSelector == nil) != (expected.ResourceSelector == nil) { t.Fatalf("resource_selector nil mismatch: expected %v, got %v", expected.ResourceSelector == nil, actual.ResourceSelector == nil) @@ -119,7 +108,6 @@ func TestDBDeployments_BasicWrite(t *testing.T) { SystemId: systemID, Description: &description, JobAgentConfig: oapi.JobAgentConfig{}, - Metadata: map[string]string{"team": "platform"}, ResourceSelector: nil, // Selector is complex type, skipping for basic test } diff --git a/apps/workspace-engine/pkg/db/environments.go b/apps/workspace-engine/pkg/db/environments.go index a08c83341..6c68a328a 100644 --- a/apps/workspace-engine/pkg/db/environments.go +++ b/apps/workspace-engine/pkg/db/environments.go @@ -16,19 +16,10 @@ const ENVIRONMENT_SELECT_QUERY = ` e.system_id, e.created_at, e.description, - e.resource_selector, - COALESCE( - json_object_agg( - COALESCE(em.key, ''), - COALESCE(em.value, '') - ) FILTER (WHERE em.key IS NOT NULL), - '{}'::json - ) as metadata + e.resource_selector FROM environment e INNER JOIN system s ON s.id = e.system_id - LEFT JOIN environment_metadata em ON em.environment_id = e.id WHERE s.workspace_id = $1 - GROUP BY e.id, e.name, e.system_id, e.created_at, e.description, e.resource_selector ` func getEnvironments(ctx context.Context, workspaceID string) ([]*oapi.Environment, error) { @@ -49,7 +40,6 @@ func getEnvironments(ctx context.Context, workspaceID string) ([]*oapi.Environme var environment oapi.Environment var createdAt time.Time var rawSelector map[string]interface{} - var metadataJSON []byte err := rows.Scan( &environment.Id, @@ -58,7 +48,6 @@ func getEnvironments(ctx context.Context, workspaceID string) ([]*oapi.Environme &createdAt, &environment.Description, &rawSelector, - &metadataJSON, ) if err != nil { return nil, err @@ -71,12 +60,6 @@ func getEnvironments(ctx context.Context, workspaceID string) ([]*oapi.Environme return nil, err } - metadata, err := parseMetadataJSON(metadataJSON) - if err != nil { - return nil, err - } - environment.Metadata = metadata - environments = append(environments, &environment) } if err := rows.Err(); err != nil { @@ -113,20 +96,6 @@ func writeEnvironment(ctx context.Context, environment *oapi.Environment, tx pgx ); err != nil { return err } - - metadata := environment.Metadata - if metadata == nil { - metadata = map[string]string{} - } - - if _, err := tx.Exec(ctx, "DELETE FROM environment_metadata WHERE environment_id = $1", environment.Id); err != nil { - return err - } - - if err := writeMetadata(ctx, "environment_metadata", "environment_id", environment.Id, metadata, tx); err != nil { - return err - } - return nil } diff --git a/apps/workspace-engine/pkg/db/environments_test.go b/apps/workspace-engine/pkg/db/environments_test.go index cbe941dd5..a1afa421b 100644 --- a/apps/workspace-engine/pkg/db/environments_test.go +++ b/apps/workspace-engine/pkg/db/environments_test.go @@ -2,7 +2,6 @@ package db import ( "fmt" - "reflect" "strings" "testing" "workspace-engine/pkg/oapi" @@ -38,17 +37,6 @@ func validateRetrievedEnvironments(t *testing.T, actualEnvironments []*oapi.Envi t.Fatalf("expected environment system_id %s, got %s", expectedEnv.SystemId, actualEnv.SystemId) } compareStrPtr(t, actualEnv.Description, expectedEnv.Description) - expectedMetadata := expectedEnv.Metadata - if expectedMetadata == nil { - expectedMetadata = map[string]string{} - } - actualMetadata := actualEnv.Metadata - if actualMetadata == nil { - actualMetadata = map[string]string{} - } - if !reflect.DeepEqual(expectedMetadata, actualMetadata) { - t.Fatalf("expected environment metadata %v, got %v", expectedMetadata, actualMetadata) - } // Note: ResourceSelector is *Selector (complex type), comparing as pointers only if (actualEnv.ResourceSelector == nil) != (expectedEnv.ResourceSelector == nil) { t.Fatalf("resource_selector nil mismatch: expected %v, got %v", expectedEnv.ResourceSelector == nil, actualEnv.ResourceSelector == nil) @@ -93,7 +81,6 @@ func TestDBEnvironments_BasicWrite(t *testing.T) { Name: envName, SystemId: systemID, Description: &description, - Metadata: map[string]string{"tier": "backend"}, ResourceSelector: nil, // Selector is complex type, skipping for test } diff --git a/apps/workspace-engine/pkg/db/job_agents.go b/apps/workspace-engine/pkg/db/job_agents.go index d3513a1d1..d8cc46e68 100644 --- a/apps/workspace-engine/pkg/db/job_agents.go +++ b/apps/workspace-engine/pkg/db/job_agents.go @@ -14,18 +14,9 @@ const JOB_AGENT_SELECT_QUERY = ` j.workspace_id, j.name, j.type, - j.config, - COALESCE( - json_object_agg( - COALESCE(jm.key, ''), - COALESCE(jm.value, '') - ) FILTER (WHERE jm.key IS NOT NULL), - '{}'::json - ) as metadata + j.config FROM job_agent j - LEFT JOIN job_agent_metadata jm ON jm.job_agent_id = j.id WHERE j.workspace_id = $1 - GROUP BY j.id, j.workspace_id, j.name, j.type, j.config ` func getJobAgents(ctx context.Context, workspaceID string) ([]*oapi.JobAgent, error) { @@ -74,7 +65,6 @@ func runnerJobAgentConfig(m map[string]interface{}) oapi.JobAgentConfig { func scanJobAgentRow(rows pgx.Rows) (*oapi.JobAgent, error) { jobAgent := &oapi.JobAgent{} var config *map[string]interface{} - var metadataJSON []byte err := rows.Scan( &jobAgent.Id, @@ -82,7 +72,6 @@ func scanJobAgentRow(rows pgx.Rows) (*oapi.JobAgent, error) { &jobAgent.Name, &jobAgent.Type, &config, - &metadataJSON, ) if err != nil { return nil, err @@ -126,11 +115,6 @@ func scanJobAgentRow(rows pgx.Rows) (*oapi.JobAgent, error) { } jobAgent.Config = cfg - metadata, err := parseMetadataJSON(metadataJSON) - if err != nil { - return nil, err - } - jobAgent.Metadata = metadata return jobAgent, nil } @@ -156,19 +140,6 @@ func writeJobAgent(ctx context.Context, jobAgent *oapi.JobAgent, tx pgx.Tx) erro ); err != nil { return err } - - metadata := jobAgent.Metadata - if metadata == nil { - metadata = map[string]string{} - } - - if _, err := tx.Exec(ctx, "DELETE FROM job_agent_metadata WHERE job_agent_id = $1", jobAgent.Id); err != nil { - return err - } - - if err := writeMetadata(ctx, "job_agent_metadata", "job_agent_id", jobAgent.Id, metadata, tx); err != nil { - return err - } return nil } diff --git a/apps/workspace-engine/pkg/db/job_agents_test.go b/apps/workspace-engine/pkg/db/job_agents_test.go index 98e353a38..a2c7e5c15 100644 --- a/apps/workspace-engine/pkg/db/job_agents_test.go +++ b/apps/workspace-engine/pkg/db/job_agents_test.go @@ -62,18 +62,6 @@ func validateRetrievedJobAgents(t *testing.T, actualJobAgents []*oapi.JobAgent, if !reflect.DeepEqual(expectedConfig, actualConfig) { t.Fatalf("expected config %v, got %v", expectedConfig, actualConfig) } - - expectedMetadata := expected.Metadata - if expectedMetadata == nil { - expectedMetadata = map[string]string{} - } - actualMetadata := actual.Metadata - if actualMetadata == nil { - actualMetadata = map[string]string{} - } - if !reflect.DeepEqual(expectedMetadata, actualMetadata) { - t.Fatalf("expected metadata %v, got %v", expectedMetadata, actualMetadata) - } } } @@ -98,7 +86,6 @@ func TestDBJobAgents_BasicWrite(t *testing.T) { Name: name, Type: "kubernetes", Config: config, - Metadata: map[string]string{"team": "release"}, } err = writeJobAgent(t.Context(), jobAgent, tx) diff --git a/apps/workspace-engine/pkg/db/metadata.go b/apps/workspace-engine/pkg/db/metadata.go deleted file mode 100644 index 1c667dc84..000000000 --- a/apps/workspace-engine/pkg/db/metadata.go +++ /dev/null @@ -1,56 +0,0 @@ -package db - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - "github.com/jackc/pgx/v5" -) - -func parseMetadataJSON(metadataJSON []byte) (map[string]string, error) { - if len(metadataJSON) == 0 { - return map[string]string{}, nil - } - - var metadataMap map[string]string - if err := json.Unmarshal(metadataJSON, &metadataMap); err != nil { - return nil, err - } - if metadataMap == nil { - metadataMap = map[string]string{} - } - return metadataMap, nil -} - -func writeMetadata(ctx context.Context, table string, idColumn string, entityId string, metadata map[string]string, tx pgx.Tx) error { - if len(metadata) == 0 { - return nil - } - - valueStrings := make([]string, 0, len(metadata)) - valueArgs := make([]interface{}, 0, len(metadata)*3) - i := 1 - for k, v := range metadata { - valueStrings = append(valueStrings, - "($"+fmt.Sprintf("%d", i)+", $"+fmt.Sprintf("%d", i+1)+", $"+fmt.Sprintf("%d", i+2)+")", - ) - valueArgs = append(valueArgs, entityId, k, v) - i += 3 - } - - query := fmt.Sprintf( - "INSERT INTO %s (%s, key, value) VALUES %s ON CONFLICT (%s, key) DO UPDATE SET value = EXCLUDED.value", - table, - idColumn, - strings.Join(valueStrings, ", "), - idColumn, - ) - - _, err := tx.Exec(ctx, query, valueArgs...) - if err != nil { - return err - } - return nil -} diff --git a/apps/workspace-engine/pkg/db/systems.go b/apps/workspace-engine/pkg/db/systems.go index f9f4b092d..90b38a052 100644 --- a/apps/workspace-engine/pkg/db/systems.go +++ b/apps/workspace-engine/pkg/db/systems.go @@ -12,18 +12,9 @@ const SYSTEM_SELECT_QUERY = ` s.id, s.workspace_id, s.name, - s.description, - COALESCE( - json_object_agg( - COALESCE(sm.key, ''), - COALESCE(sm.value, '') - ) FILTER (WHERE sm.key IS NOT NULL), - '{}'::json - ) as metadata + s.description FROM system s - LEFT JOIN system_metadata sm ON sm.system_id = s.id WHERE s.workspace_id = $1 - GROUP BY s.id, s.workspace_id, s.name, s.description ` func getSystems(ctx context.Context, workspaceID string) ([]*oapi.System, error) { @@ -52,22 +43,15 @@ func getSystems(ctx context.Context, workspaceID string) ([]*oapi.System, error) func scanSystemRow(rows pgx.Rows) (*oapi.System, error) { system := &oapi.System{} - var metadataJSON []byte err := rows.Scan( &system.Id, &system.WorkspaceId, &system.Name, &system.Description, - &metadataJSON, ) if err != nil { return nil, err } - metadata, err := parseMetadataJSON(metadataJSON) - if err != nil { - return nil, err - } - system.Metadata = metadata return system, nil } @@ -93,19 +77,6 @@ func writeSystem(ctx context.Context, system *oapi.System, tx pgx.Tx) error { ); err != nil { return err } - - metadata := system.Metadata - if metadata == nil { - metadata = map[string]string{} - } - - if _, err := tx.Exec(ctx, "DELETE FROM system_metadata WHERE system_id = $1", system.Id); err != nil { - return err - } - - if err := writeMetadata(ctx, "system_metadata", "system_id", system.Id, metadata, tx); err != nil { - return err - } return nil } diff --git a/apps/workspace-engine/pkg/db/systems_test.go b/apps/workspace-engine/pkg/db/systems_test.go index ac8e64aef..1b1a7ab6c 100644 --- a/apps/workspace-engine/pkg/db/systems_test.go +++ b/apps/workspace-engine/pkg/db/systems_test.go @@ -2,7 +2,6 @@ package db import ( "fmt" - "reflect" "strings" "testing" "workspace-engine/pkg/oapi" @@ -36,17 +35,6 @@ func validateRetrievedSystems(t *testing.T, actualSystems []*oapi.System, expect if actualSystem.WorkspaceId != expectedSystem.WorkspaceId { t.Fatalf("expected system %v, got %v", expectedSystem, actualSystem) } - expectedMetadata := expectedSystem.Metadata - if expectedMetadata == nil { - expectedMetadata = map[string]string{} - } - actualMetadata := actualSystem.Metadata - if actualMetadata == nil { - actualMetadata = map[string]string{} - } - if !reflect.DeepEqual(expectedMetadata, actualMetadata) { - t.Fatalf("expected system metadata %v, got %v", expectedMetadata, actualMetadata) - } } } @@ -68,7 +56,6 @@ func TestDBSystems_BasicWrite(t *testing.T) { WorkspaceId: workspaceID, Name: name, Description: &description, - Metadata: map[string]string{"owner": "platform"}, } err = writeSystem(t.Context(), sys, tx) diff --git a/packages/db/drizzle/0130_add_entity_metadata.sql b/packages/db/drizzle/0130_add_entity_metadata.sql deleted file mode 100644 index 0f6941ad0..000000000 --- a/packages/db/drizzle/0130_add_entity_metadata.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE IF NOT EXISTS "deployment_metadata" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "deployment_id" uuid NOT NULL, - "key" text NOT NULL, - "value" text NOT NULL -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "system_metadata" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "system_id" uuid NOT NULL, - "key" text NOT NULL, - "value" text NOT NULL -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "job_agent_metadata" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "job_agent_id" uuid NOT NULL, - "key" text NOT NULL, - "value" text NOT NULL -); ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "deployment_metadata" ADD CONSTRAINT "deployment_metadata_deployment_id_deployment_id_fk" FOREIGN KEY ("deployment_id") REFERENCES "public"."deployment"("id") ON DELETE cascade ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "system_metadata" ADD CONSTRAINT "system_metadata_system_id_system_id_fk" FOREIGN KEY ("system_id") REFERENCES "public"."system"("id") ON DELETE cascade ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "job_agent_metadata" ADD CONSTRAINT "job_agent_metadata_job_agent_id_job_agent_id_fk" FOREIGN KEY ("job_agent_id") REFERENCES "public"."job_agent"("id") ON DELETE cascade ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -CREATE UNIQUE INDEX IF NOT EXISTS "deployment_metadata_key_deployment_id_index" ON "deployment_metadata" USING btree ("key","deployment_id");--> statement-breakpoint -CREATE UNIQUE INDEX IF NOT EXISTS "system_metadata_key_system_id_index" ON "system_metadata" USING btree ("key","system_id");--> statement-breakpoint -CREATE UNIQUE INDEX IF NOT EXISTS "job_agent_metadata_key_job_agent_id_index" ON "job_agent_metadata" USING btree ("key","job_agent_id");--> statement-breakpoint diff --git a/packages/db/src/schema/deployment.ts b/packages/db/src/schema/deployment.ts index 62a133ed1..ca0d2dbf9 100644 --- a/packages/db/src/schema/deployment.ts +++ b/packages/db/src/schema/deployment.ts @@ -116,28 +116,18 @@ export const updateDeployment = deploymentInsert.partial(); export type UpdateDeployment = z.infer; export type Deployment = InferSelectModel; -export const deploymentMetadata = pgTable( - "deployment_metadata", - { - id: uuid("id").primaryKey().defaultRandom().notNull(), - deploymentId: uuid("deployment_id") - .references(() => deployment.id, { onDelete: "cascade" }) - .notNull(), - key: text("key").notNull(), - value: text("value").notNull(), - }, - (t) => ({ uniq: uniqueIndex().on(t.key, t.deploymentId) }), -); - -export const deploymentMetadataRelations = relations( - deploymentMetadata, - ({ one }) => ({ - deployment: one(deployment, { - fields: [deploymentMetadata.deploymentId], - references: [deployment.id], - }), +export const deploymentRelations = relations(deployment, ({ one, many }) => ({ + system: one(system, { + fields: [deployment.systemId], + references: [system.id], }), -); + jobAgent: one(jobAgent, { + fields: [deployment.jobAgentId], + references: [jobAgent.id], + }), + computedResources: many(computedDeploymentResource), + releaseTargets: many(releaseTarget), +})); export const computedDeploymentResource = pgTable( "computed_deployment_resource", @@ -166,20 +156,6 @@ export const computedDeploymentResourceRelations = relations( }), ); -export const deploymentRelations = relations(deployment, ({ one, many }) => ({ - system: one(system, { - fields: [deployment.systemId], - references: [system.id], - }), - jobAgent: one(jobAgent, { - fields: [deployment.jobAgentId], - references: [jobAgent.id], - }), - metadata: many(deploymentMetadata), - computedResources: many(computedDeploymentResource), - releaseTargets: many(releaseTarget), -})); - const buildCondition = (cond: DeploymentCondition): SQL => { if (cond.type === "name") return ColumnOperatorFn[cond.operator](deployment.name, cond.value); diff --git a/packages/db/src/schema/job-agent.ts b/packages/db/src/schema/job-agent.ts index 0abe8d5b1..b07a96e20 100644 --- a/packages/db/src/schema/job-agent.ts +++ b/packages/db/src/schema/job-agent.ts @@ -21,32 +21,8 @@ export const jobAgent = pgTable( (t) => ({ uniq: uniqueIndex().on(t.workspaceId, t.name) }), ); -export const jobAgentMetadata = pgTable( - "job_agent_metadata", - { - id: uuid("id").primaryKey().defaultRandom().notNull(), - jobAgentId: uuid("job_agent_id") - .references(() => jobAgent.id, { onDelete: "cascade" }) - .notNull(), - key: text("key").notNull(), - value: text("value").notNull(), - }, - (t) => ({ uniq: uniqueIndex().on(t.key, t.jobAgentId) }), -); - -export const jobAgentMetadataRelations = relations( - jobAgentMetadata, - ({ one }) => ({ - jobAgent: one(jobAgent, { - fields: [jobAgentMetadata.jobAgentId], - references: [jobAgent.id], - }), - }), -); - export const jobAgentRelations = relations(jobAgent, ({ many }) => ({ jobs: many(job), - metadata: many(jobAgentMetadata), })); export const createJobAgent = createInsertSchema(jobAgent, { diff --git a/packages/db/src/schema/system.ts b/packages/db/src/schema/system.ts index dcab6b8aa..4c108ec23 100644 --- a/packages/db/src/schema/system.ts +++ b/packages/db/src/schema/system.ts @@ -48,30 +48,9 @@ export const updateSystem = createSystem.partial(); export type System = InferSelectModel; -export const systemMetadata = pgTable( - "system_metadata", - { - id: uuid("id").primaryKey().defaultRandom().notNull(), - systemId: uuid("system_id") - .references(() => system.id, { onDelete: "cascade" }) - .notNull(), - key: text("key").notNull(), - value: text("value").notNull(), - }, - (t) => ({ uniq: uniqueIndex().on(t.key, t.systemId) }), -); - -export const systemMetadataRelations = relations(systemMetadata, ({ one }) => ({ - system: one(system, { - fields: [systemMetadata.systemId], - references: [system.id], - }), -})); - export const systemRelations = relations(system, ({ one, many }) => ({ environments: many(environment), deployments: many(deployment), - metadata: many(systemMetadata), workspace: one(workspace, { fields: [system.workspaceId], references: [workspace.id],