From 6bfb9cf801c165749083adf8c63767b1b29aad61 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 25 Feb 2026 10:35:08 -0800 Subject: [PATCH 1/3] refactor: move approval records to db --- apps/workspace-engine/pkg/db/models.go | 9 + .../pkg/db/queries/schema.sql | 10 + .../pkg/db/queries/user_approval_records.sql | 25 + apps/workspace-engine/pkg/db/sqlc.yaml | 1 + .../pkg/db/user_approval_records.sql.go | 155 + .../pkg/persistence/integration_test.go | 8 +- .../pkg/workspace/store/repository/db/repo.go | 7 + .../db/userapprovalrecords/mapper.go | 83 + .../repository/db/userapprovalrecords/repo.go | 90 + .../workspace/store/repository/interfaces.go | 9 + .../workspace/store/repository/memory/repo.go | 43 +- .../pkg/workspace/store/repository/repo.go | 1 + .../pkg/workspace/store/store.go | 19 + .../workspace/store/user_approval_records.go | 48 +- .../svc/workspaceconsumer/consumer.go | 1 + .../test/e2e/engine_approval_records_test.go | 49 +- .../test/e2e/engine_policy_approval_test.go | 131 +- .../test/e2e/engine_policy_conflicts_test.go | 24 +- .../e2e/engine_policy_dynamic_changes_test.go | 12 +- .../test/e2e/engine_policy_skip_test.go | 3 +- .../engine_policy_version_cooldown_test.go | 5 +- .../e2e/engine_policy_versionselector_test.go | 3 +- .../test/e2e/engine_trace_test.go | 3 +- .../test/integration/dbtest.go | 1 + packages/db/drizzle/0149_milky_hammerhead.sql | 9 + packages/db/drizzle/meta/0149_snapshot.json | 4120 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/schema/index.ts | 1 + .../db/src/schema/user-approval-record.ts | 22 + 29 files changed, 4805 insertions(+), 94 deletions(-) create mode 100644 apps/workspace-engine/pkg/db/queries/user_approval_records.sql create mode 100644 apps/workspace-engine/pkg/db/user_approval_records.sql.go create mode 100644 apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/mapper.go create mode 100644 apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/repo.go create mode 100644 packages/db/drizzle/0149_milky_hammerhead.sql create mode 100644 packages/db/drizzle/meta/0149_snapshot.json create mode 100644 packages/db/src/schema/user-approval-record.ts diff --git a/apps/workspace-engine/pkg/db/models.go b/apps/workspace-engine/pkg/db/models.go index c3fe2f85a..64c890a0b 100644 --- a/apps/workspace-engine/pkg/db/models.go +++ b/apps/workspace-engine/pkg/db/models.go @@ -268,6 +268,15 @@ type SystemEnvironment struct { CreatedAt pgtype.Timestamptz } +type UserApprovalRecord struct { + VersionID uuid.UUID + UserID uuid.UUID + EnvironmentID uuid.UUID + Status string + Reason pgtype.Text + CreatedAt pgtype.Timestamptz +} + type Workspace struct { ID uuid.UUID Name string diff --git a/apps/workspace-engine/pkg/db/queries/schema.sql b/apps/workspace-engine/pkg/db/queries/schema.sql index 3721319a2..2e430ae45 100644 --- a/apps/workspace-engine/pkg/db/queries/schema.sql +++ b/apps/workspace-engine/pkg/db/queries/schema.sql @@ -221,4 +221,14 @@ CREATE TABLE policy_rule_version_selector ( description TEXT, selector TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE user_approval_record ( + version_id UUID NOT NULL, + user_id UUID NOT NULL, + environment_id UUID NOT NULL, + status TEXT NOT NULL, + reason TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (version_id, user_id, environment_id) ); \ No newline at end of file diff --git a/apps/workspace-engine/pkg/db/queries/user_approval_records.sql b/apps/workspace-engine/pkg/db/queries/user_approval_records.sql new file mode 100644 index 000000000..0d4e0aab5 --- /dev/null +++ b/apps/workspace-engine/pkg/db/queries/user_approval_records.sql @@ -0,0 +1,25 @@ +-- name: GetUserApprovalRecord :one +SELECT version_id, user_id, environment_id, status, reason, created_at +FROM user_approval_record +WHERE version_id = $1 AND user_id = $2 AND environment_id = $3; + +-- name: UpsertUserApprovalRecord :exec +INSERT INTO user_approval_record (version_id, user_id, environment_id, status, reason, created_at) +VALUES ($1, $2, $3, $4, $5, COALESCE(sqlc.narg('created_at')::timestamptz, NOW())) +ON CONFLICT (version_id, user_id, environment_id) DO UPDATE +SET status = EXCLUDED.status, reason = EXCLUDED.reason; + +-- name: DeleteUserApprovalRecord :exec +DELETE FROM user_approval_record +WHERE version_id = $1 AND user_id = $2 AND environment_id = $3; + +-- name: ListApprovedRecordsByVersionAndEnvironment :many +SELECT version_id, user_id, environment_id, status, reason, created_at +FROM user_approval_record +WHERE version_id = $1 AND environment_id = $2 AND status = 'approved' +ORDER BY created_at ASC; + +-- name: ListUserApprovalRecordsByVersionID :many +SELECT version_id, user_id, environment_id, status, reason, created_at +FROM user_approval_record +WHERE version_id = $1; diff --git a/apps/workspace-engine/pkg/db/sqlc.yaml b/apps/workspace-engine/pkg/db/sqlc.yaml index 385c435f4..36fed78c5 100644 --- a/apps/workspace-engine/pkg/db/sqlc.yaml +++ b/apps/workspace-engine/pkg/db/sqlc.yaml @@ -19,6 +19,7 @@ sql: - queries/releases.sql - queries/changelog.sql - queries/policies.sql + - queries/user_approval_records.sql database: uri: "postgresql://ctrlplane:ctrlplane@127.0.0.1:5432/ctrlplane?sslmode=disable" gen: diff --git a/apps/workspace-engine/pkg/db/user_approval_records.sql.go b/apps/workspace-engine/pkg/db/user_approval_records.sql.go new file mode 100644 index 000000000..c8c8ff67d --- /dev/null +++ b/apps/workspace-engine/pkg/db/user_approval_records.sql.go @@ -0,0 +1,155 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: user_approval_records.sql + +package db + +import ( + "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +const deleteUserApprovalRecord = `-- name: DeleteUserApprovalRecord :exec +DELETE FROM user_approval_record +WHERE version_id = $1 AND user_id = $2 AND environment_id = $3 +` + +type DeleteUserApprovalRecordParams struct { + VersionID uuid.UUID + UserID uuid.UUID + EnvironmentID uuid.UUID +} + +func (q *Queries) DeleteUserApprovalRecord(ctx context.Context, arg DeleteUserApprovalRecordParams) error { + _, err := q.db.Exec(ctx, deleteUserApprovalRecord, arg.VersionID, arg.UserID, arg.EnvironmentID) + return err +} + +const getUserApprovalRecord = `-- name: GetUserApprovalRecord :one +SELECT version_id, user_id, environment_id, status, reason, created_at +FROM user_approval_record +WHERE version_id = $1 AND user_id = $2 AND environment_id = $3 +` + +type GetUserApprovalRecordParams struct { + VersionID uuid.UUID + UserID uuid.UUID + EnvironmentID uuid.UUID +} + +func (q *Queries) GetUserApprovalRecord(ctx context.Context, arg GetUserApprovalRecordParams) (UserApprovalRecord, error) { + row := q.db.QueryRow(ctx, getUserApprovalRecord, arg.VersionID, arg.UserID, arg.EnvironmentID) + var i UserApprovalRecord + err := row.Scan( + &i.VersionID, + &i.UserID, + &i.EnvironmentID, + &i.Status, + &i.Reason, + &i.CreatedAt, + ) + return i, err +} + +const listApprovedRecordsByVersionAndEnvironment = `-- name: ListApprovedRecordsByVersionAndEnvironment :many +SELECT version_id, user_id, environment_id, status, reason, created_at +FROM user_approval_record +WHERE version_id = $1 AND environment_id = $2 AND status = 'approved' +ORDER BY created_at ASC +` + +type ListApprovedRecordsByVersionAndEnvironmentParams struct { + VersionID uuid.UUID + EnvironmentID uuid.UUID +} + +func (q *Queries) ListApprovedRecordsByVersionAndEnvironment(ctx context.Context, arg ListApprovedRecordsByVersionAndEnvironmentParams) ([]UserApprovalRecord, error) { + rows, err := q.db.Query(ctx, listApprovedRecordsByVersionAndEnvironment, arg.VersionID, arg.EnvironmentID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []UserApprovalRecord + for rows.Next() { + var i UserApprovalRecord + if err := rows.Scan( + &i.VersionID, + &i.UserID, + &i.EnvironmentID, + &i.Status, + &i.Reason, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listUserApprovalRecordsByVersionID = `-- name: ListUserApprovalRecordsByVersionID :many +SELECT version_id, user_id, environment_id, status, reason, created_at +FROM user_approval_record +WHERE version_id = $1 +` + +func (q *Queries) ListUserApprovalRecordsByVersionID(ctx context.Context, versionID uuid.UUID) ([]UserApprovalRecord, error) { + rows, err := q.db.Query(ctx, listUserApprovalRecordsByVersionID, versionID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []UserApprovalRecord + for rows.Next() { + var i UserApprovalRecord + if err := rows.Scan( + &i.VersionID, + &i.UserID, + &i.EnvironmentID, + &i.Status, + &i.Reason, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const upsertUserApprovalRecord = `-- name: UpsertUserApprovalRecord :exec +INSERT INTO user_approval_record (version_id, user_id, environment_id, status, reason, created_at) +VALUES ($1, $2, $3, $4, $5, COALESCE($6::timestamptz, NOW())) +ON CONFLICT (version_id, user_id, environment_id) DO UPDATE +SET status = EXCLUDED.status, reason = EXCLUDED.reason +` + +type UpsertUserApprovalRecordParams struct { + VersionID uuid.UUID + UserID uuid.UUID + EnvironmentID uuid.UUID + Status string + Reason pgtype.Text + CreatedAt pgtype.Timestamptz +} + +func (q *Queries) UpsertUserApprovalRecord(ctx context.Context, arg UpsertUserApprovalRecordParams) error { + _, err := q.db.Exec(ctx, upsertUserApprovalRecord, + arg.VersionID, + arg.UserID, + arg.EnvironmentID, + arg.Status, + arg.Reason, + arg.CreatedAt, + ) + return err +} diff --git a/apps/workspace-engine/pkg/persistence/integration_test.go b/apps/workspace-engine/pkg/persistence/integration_test.go index d36e7e0c8..ded0367f3 100644 --- a/apps/workspace-engine/pkg/persistence/integration_test.go +++ b/apps/workspace-engine/pkg/persistence/integration_test.go @@ -500,7 +500,7 @@ func TestPersistence_AllEntityTypes(t *testing.T) { _, ok = testStore.Repo().GithubEntities.Get(githubEntityKey) assert.True(t, ok, "GithubEntity should be restored") - _, ok = testStore.Repo().UserApprovalRecords.Get(userApprovalRecord.Key()) + _, ok = testStore.Repo().UserApprovalRecords().Get(userApprovalRecord.Key()) assert.True(t, ok, "UserApprovalRecord should be restored") } @@ -757,13 +757,13 @@ func TestUserApprovalRecordKeyIncludesEnvironment(t *testing.T) { assert.NotEqual(t, recordOne.Key(), recordTwo.Key()) - _, ok := testStore.Repo().UserApprovalRecords.Get(recordOne.Key()) + _, ok := testStore.Repo().UserApprovalRecords().Get(recordOne.Key()) assert.True(t, ok, "first user approval record should be stored") - _, ok = testStore.Repo().UserApprovalRecords.Get(recordTwo.Key()) + _, ok = testStore.Repo().UserApprovalRecords().Get(recordTwo.Key()) assert.True(t, ok, "second user approval record should be stored") - assert.Equal(t, 2, len(testStore.Repo().UserApprovalRecords.Items())) + assert.Equal(t, 2, len(testStore.Repo().UserApprovalRecords().Items())) } // Helper function to create string pointers diff --git a/apps/workspace-engine/pkg/workspace/store/repository/db/repo.go b/apps/workspace-engine/pkg/workspace/store/repository/db/repo.go index 7458b5303..961cc8662 100644 --- a/apps/workspace-engine/pkg/workspace/store/repository/db/repo.go +++ b/apps/workspace-engine/pkg/workspace/store/repository/db/repo.go @@ -8,6 +8,7 @@ import ( "workspace-engine/pkg/workspace/store/repository/db/environments" "workspace-engine/pkg/workspace/store/repository/db/jobagents" "workspace-engine/pkg/workspace/store/repository/db/policies" + "workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords" "workspace-engine/pkg/workspace/store/repository/db/releases" "workspace-engine/pkg/workspace/store/repository/db/resourceproviders" "workspace-engine/pkg/workspace/store/repository/db/resources" @@ -28,6 +29,7 @@ type DBRepo struct { systemDeployments repository.SystemDeploymentRepo systemEnvironments repository.SystemEnvironmentRepo policies repository.PolicyRepo + userApprovalRecords repository.UserApprovalRecordRepo } func (d *DBRepo) DeploymentVersions() repository.DeploymentVersionRepo { @@ -74,6 +76,10 @@ func (d *DBRepo) Policies() repository.PolicyRepo { return d.policies } +func (d *DBRepo) UserApprovalRecords() repository.UserApprovalRecordRepo { + return d.userApprovalRecords +} + func NewDBRepo(ctx context.Context, workspaceID string) *DBRepo { return &DBRepo{ deploymentVersions: deploymentversions.NewRepo(ctx, workspaceID), @@ -87,5 +93,6 @@ func NewDBRepo(ctx context.Context, workspaceID string) *DBRepo { systemDeployments: systemdeployments.NewRepo(ctx), systemEnvironments: systemenvironments.NewRepo(ctx), policies: policies.NewRepo(ctx, workspaceID), + userApprovalRecords: userapprovalrecords.NewRepo(ctx), } } diff --git a/apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/mapper.go b/apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/mapper.go new file mode 100644 index 000000000..675a709cf --- /dev/null +++ b/apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/mapper.go @@ -0,0 +1,83 @@ +package userapprovalrecords + +import ( + "fmt" + "time" + "workspace-engine/pkg/db" + "workspace-engine/pkg/oapi" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +func ToOapi(row db.UserApprovalRecord) *oapi.UserApprovalRecord { + var reason *string + if row.Reason.Valid { + reason = &row.Reason.String + } + + return &oapi.UserApprovalRecord{ + VersionId: row.VersionID.String(), + UserId: row.UserID.String(), + EnvironmentId: row.EnvironmentID.String(), + Status: oapi.ApprovalStatus(row.Status), + Reason: reason, + CreatedAt: row.CreatedAt.Time.Format(time.RFC3339), + } +} + +func ToUpsertParams(e *oapi.UserApprovalRecord) (db.UpsertUserApprovalRecordParams, error) { + vid, err := uuid.Parse(e.VersionId) + if err != nil { + return db.UpsertUserApprovalRecordParams{}, fmt.Errorf("parse version_id: %w", err) + } + uid, err := uuid.Parse(e.UserId) + if err != nil { + return db.UpsertUserApprovalRecordParams{}, fmt.Errorf("parse user_id: %w", err) + } + eid, err := uuid.Parse(e.EnvironmentId) + if err != nil { + return db.UpsertUserApprovalRecordParams{}, fmt.Errorf("parse environment_id: %w", err) + } + + var reason pgtype.Text + if e.Reason != nil { + reason = pgtype.Text{String: *e.Reason, Valid: true} + } + + var createdAt pgtype.Timestamptz + if e.CreatedAt != "" { + t, err := time.Parse(time.RFC3339, e.CreatedAt) + if err == nil { + createdAt = pgtype.Timestamptz{Time: t, Valid: true} + } + } + + return db.UpsertUserApprovalRecordParams{ + VersionID: vid, + UserID: uid, + EnvironmentID: eid, + Status: string(e.Status), + Reason: reason, + CreatedAt: createdAt, + }, nil +} + +func parseKey(key string) (uuid.UUID, uuid.UUID, uuid.UUID, error) { + if len(key) < 108 { + return uuid.Nil, uuid.Nil, uuid.Nil, fmt.Errorf("key too short: %q", key) + } + vid, err := uuid.Parse(key[:36]) + if err != nil { + return uuid.Nil, uuid.Nil, uuid.Nil, fmt.Errorf("parse version_id from key: %w", err) + } + uid, err := uuid.Parse(key[36:72]) + if err != nil { + return uuid.Nil, uuid.Nil, uuid.Nil, fmt.Errorf("parse user_id from key: %w", err) + } + eid, err := uuid.Parse(key[72:108]) + if err != nil { + return uuid.Nil, uuid.Nil, uuid.Nil, fmt.Errorf("parse environment_id from key: %w", err) + } + return vid, uid, eid, nil +} diff --git a/apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/repo.go b/apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/repo.go new file mode 100644 index 000000000..b80fff0cd --- /dev/null +++ b/apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/repo.go @@ -0,0 +1,90 @@ +package userapprovalrecords + +import ( + "context" + "fmt" + "workspace-engine/pkg/db" + "workspace-engine/pkg/oapi" + + "github.com/charmbracelet/log" + "github.com/google/uuid" +) + +type Repo struct { + ctx context.Context +} + +func NewRepo(ctx context.Context) *Repo { + return &Repo{ctx: ctx} +} + +func (r *Repo) Get(key string) (*oapi.UserApprovalRecord, bool) { + versionID, userID, environmentID, err := parseKey(key) + if err != nil { + log.Warn("Failed to parse user approval record key", "key", key, "error", err) + return nil, false + } + + row, err := db.GetQueries(r.ctx).GetUserApprovalRecord(r.ctx, db.GetUserApprovalRecordParams{ + VersionID: versionID, + UserID: userID, + EnvironmentID: environmentID, + }) + if err != nil { + return nil, false + } + + return ToOapi(row), true +} + +func (r *Repo) Set(entity *oapi.UserApprovalRecord) error { + params, err := ToUpsertParams(entity) + if err != nil { + return fmt.Errorf("convert to upsert params: %w", err) + } + + return db.GetQueries(r.ctx).UpsertUserApprovalRecord(r.ctx, params) +} + +func (r *Repo) Remove(key string) error { + versionID, userID, environmentID, err := parseKey(key) + if err != nil { + return fmt.Errorf("parse key: %w", err) + } + + return db.GetQueries(r.ctx).DeleteUserApprovalRecord(r.ctx, db.DeleteUserApprovalRecordParams{ + VersionID: versionID, + UserID: userID, + EnvironmentID: environmentID, + }) +} + +func (r *Repo) Items() map[string]*oapi.UserApprovalRecord { + log.Warn("UserApprovalRecords.Items() called on DB repo — not scoped, returning empty map") + return make(map[string]*oapi.UserApprovalRecord) +} + +func (r *Repo) GetApprovedByVersionAndEnvironment(versionID, environmentID string) ([]*oapi.UserApprovalRecord, error) { + vid, err := uuid.Parse(versionID) + if err != nil { + return nil, fmt.Errorf("parse version_id: %w", err) + } + eid, err := uuid.Parse(environmentID) + if err != nil { + return nil, fmt.Errorf("parse environment_id: %w", err) + } + + rows, err := db.GetQueries(r.ctx).ListApprovedRecordsByVersionAndEnvironment(r.ctx, db.ListApprovedRecordsByVersionAndEnvironmentParams{ + VersionID: vid, + EnvironmentID: eid, + }) + if err != nil { + return nil, fmt.Errorf("list approved records: %w", err) + } + + records := make([]*oapi.UserApprovalRecord, len(rows)) + for i, row := range rows { + records[i] = ToOapi(row) + } + return records, nil +} diff --git a/apps/workspace-engine/pkg/workspace/store/repository/interfaces.go b/apps/workspace-engine/pkg/workspace/store/repository/interfaces.go index 3ca231da6..ae4da04a1 100644 --- a/apps/workspace-engine/pkg/workspace/store/repository/interfaces.go +++ b/apps/workspace-engine/pkg/workspace/store/repository/interfaces.go @@ -116,3 +116,12 @@ type PolicyRepo interface { Remove(id string) error Items() map[string]*oapi.Policy } + +// UserApprovalRecordRepo defines the contract for user approval record storage. +type UserApprovalRecordRepo interface { + Get(key string) (*oapi.UserApprovalRecord, bool) + Set(entity *oapi.UserApprovalRecord) error + Remove(key string) error + Items() map[string]*oapi.UserApprovalRecord + GetApprovedByVersionAndEnvironment(versionID, environmentID string) ([]*oapi.UserApprovalRecord, error) +} diff --git a/apps/workspace-engine/pkg/workspace/store/repository/memory/repo.go b/apps/workspace-engine/pkg/workspace/store/repository/memory/repo.go index 138dfcff7..937236f67 100644 --- a/apps/workspace-engine/pkg/workspace/store/repository/memory/repo.go +++ b/apps/workspace-engine/pkg/workspace/store/repository/memory/repo.go @@ -62,7 +62,7 @@ func New(wsId string) *InMemory { releases: createMemDBStore[*oapi.Release](router, "release", memdb), Jobs: createMemDBStore[*oapi.Job](router, "job", memdb), jobAgents: createTypedStore[*oapi.JobAgent](router, "job_agent"), - UserApprovalRecords: createTypedStore[*oapi.UserApprovalRecord](router, "user_approval_record"), + userApprovalRecords: createTypedStore[*oapi.UserApprovalRecord](router, "user_approval_record"), RelationshipRules: createTypedStore[*oapi.RelationshipRule](router, "relationship_rule"), GithubEntities: createTypedStore[*oapi.GithubEntity](router, "github_entity"), Workflows: createTypedStore[*oapi.Workflow](router, "workflow"), @@ -104,7 +104,7 @@ type InMemory struct { jobAgents cmap.ConcurrentMap[string, *oapi.JobAgent] GithubEntities cmap.ConcurrentMap[string, *oapi.GithubEntity] - UserApprovalRecords cmap.ConcurrentMap[string, *oapi.UserApprovalRecord] + userApprovalRecords cmap.ConcurrentMap[string, *oapi.UserApprovalRecord] RelationshipRules cmap.ConcurrentMap[string, *oapi.RelationshipRule] Workflows cmap.ConcurrentMap[string, *oapi.Workflow] @@ -434,6 +434,45 @@ func (s *InMemory) Policies() repository.PolicyRepo { return &cmapRepoAdapter[*oapi.Policy]{store: &s.policies} } +// userApprovalRecordRepoAdapter wraps a cmap to satisfy UserApprovalRecordRepo. +type userApprovalRecordRepoAdapter struct { + store *cmap.ConcurrentMap[string, *oapi.UserApprovalRecord] +} + +func (a *userApprovalRecordRepoAdapter) Get(key string) (*oapi.UserApprovalRecord, bool) { + return a.store.Get(key) +} + +func (a *userApprovalRecordRepoAdapter) Set(entity *oapi.UserApprovalRecord) error { + a.store.Set(entity.Key(), entity) + return nil +} + +func (a *userApprovalRecordRepoAdapter) Remove(key string) error { + a.store.Remove(key) + return nil +} + +func (a *userApprovalRecordRepoAdapter) Items() map[string]*oapi.UserApprovalRecord { + return a.store.Items() +} + +func (a *userApprovalRecordRepoAdapter) GetApprovedByVersionAndEnvironment(versionID, environmentID string) ([]*oapi.UserApprovalRecord, error) { + var records []*oapi.UserApprovalRecord + for item := range a.store.IterBuffered() { + r := item.Val + if r.VersionId == versionID && r.EnvironmentId == environmentID && r.Status == oapi.ApprovalStatusApproved { + records = append(records, r) + } + } + return records, nil +} + +// UserApprovalRecords implements repository.Repo. +func (s *InMemory) UserApprovalRecords() repository.UserApprovalRecordRepo { + return &userApprovalRecordRepoAdapter{store: &s.userApprovalRecords} +} + // SystemEnvironments implements repository.Repo. func (s *InMemory) SystemEnvironments() repository.SystemEnvironmentRepo { return &systemEnvironmentRepoAdapter{ diff --git a/apps/workspace-engine/pkg/workspace/store/repository/repo.go b/apps/workspace-engine/pkg/workspace/store/repository/repo.go index 830ff3ba6..59287aa8c 100644 --- a/apps/workspace-engine/pkg/workspace/store/repository/repo.go +++ b/apps/workspace-engine/pkg/workspace/store/repository/repo.go @@ -12,4 +12,5 @@ type Repo interface { SystemDeployments() SystemDeploymentRepo SystemEnvironments() SystemEnvironmentRepo Policies() PolicyRepo + UserApprovalRecords() UserApprovalRecordRepo } diff --git a/apps/workspace-engine/pkg/workspace/store/store.go b/apps/workspace-engine/pkg/workspace/store/store.go index d4549b45f..89d7247c3 100644 --- a/apps/workspace-engine/pkg/workspace/store/store.go +++ b/apps/workspace-engine/pkg/workspace/store/store.go @@ -115,6 +115,15 @@ func WithDBPolicies(ctx context.Context) StoreOption { } } +// WithDBUserApprovalRecords replaces the default in-memory UserApprovalRecordRepo +// with a DB-backed implementation. +func WithDBUserApprovalRecords(ctx context.Context) StoreOption { + return func(s *Store) { + dbRepo := dbrepo.NewDBRepo(ctx, s.id) + s.UserApprovalRecords.SetRepo(dbRepo.UserApprovalRecords()) + } +} + func New(wsId string, changeset *statechange.ChangeSet[any], opts ...StoreOption) *Store { repo := memory.New(wsId) store := &Store{id: wsId, repo: repo, changeset: changeset} @@ -296,6 +305,16 @@ func (s *Store) Restore(ctx context.Context, changes persistence.Changes, setSta } } + if setStatus != nil { + setStatus("Migrating legacy user approval records") + } + for _, uar := range s.repo.UserApprovalRecords().Items() { + if err := s.UserApprovalRecords.repo.Set(uar); err != nil { + log.Warn("Failed to migrate legacy user approval record", + "key", uar.Key(), "error", err) + } + } + if setStatus != nil { setStatus("Computing release targets") } diff --git a/apps/workspace-engine/pkg/workspace/store/user_approval_records.go b/apps/workspace-engine/pkg/workspace/store/user_approval_records.go index 40dab7f0c..42e314ea4 100644 --- a/apps/workspace-engine/pkg/workspace/store/user_approval_records.go +++ b/apps/workspace-engine/pkg/workspace/store/user_approval_records.go @@ -5,56 +5,70 @@ import ( "sort" "time" "workspace-engine/pkg/oapi" - "workspace-engine/pkg/workspace/store/repository/memory" + "workspace-engine/pkg/workspace/store/repository" + + "github.com/charmbracelet/log" ) type UserApprovalRecords struct { - repo *memory.InMemory + repo repository.UserApprovalRecordRepo store *Store } func NewUserApprovalRecords(store *Store) *UserApprovalRecords { return &UserApprovalRecords{ - repo: store.repo, + repo: store.repo.UserApprovalRecords(), store: store, } } +func (u *UserApprovalRecords) SetRepo(repo repository.UserApprovalRecordRepo) { + u.repo = repo +} + func (u *UserApprovalRecords) Upsert(ctx context.Context, userApprovalRecord *oapi.UserApprovalRecord) { - u.repo.UserApprovalRecords.Set(userApprovalRecord.Key(), userApprovalRecord) + if err := u.repo.Set(userApprovalRecord); err != nil { + log.Error("Failed to upsert user approval record", "error", err) + return + } u.store.changeset.RecordUpsert(userApprovalRecord) } func (u *UserApprovalRecords) Get(versionId, userId string) (*oapi.UserApprovalRecord, bool) { - return u.repo.UserApprovalRecords.Get(versionId + userId) + return u.repo.Get(versionId + userId) } func (u *UserApprovalRecords) Remove(ctx context.Context, key string) { - userApprovalRecord, ok := u.repo.UserApprovalRecords.Get(key) + userApprovalRecord, ok := u.repo.Get(key) if !ok || userApprovalRecord == nil { return } - u.repo.UserApprovalRecords.Remove(key) + if err := u.repo.Remove(key); err != nil { + log.Error("Failed to remove user approval record", "error", err) + return + } u.store.changeset.RecordDelete(userApprovalRecord) } func (u *UserApprovalRecords) GetApprovers(versionId, environmentId string) []string { - approvers := make([]string, 0) - for _, record := range u.repo.UserApprovalRecords.Items() { - if record.VersionId == versionId && record.EnvironmentId == environmentId && record.Status == oapi.ApprovalStatusApproved { - approvers = append(approvers, record.UserId) - } + records, err := u.repo.GetApprovedByVersionAndEnvironment(versionId, environmentId) + if err != nil { + log.Warn("Failed to get approvers", "version_id", versionId, "environment_id", environmentId, "error", err) + return nil + } + approvers := make([]string, len(records)) + for i, r := range records { + approvers[i] = r.UserId } return approvers } func (u *UserApprovalRecords) GetApprovalRecords(versionId, environmentId string) []*oapi.UserApprovalRecord { - records := make([]*oapi.UserApprovalRecord, 0) - for _, record := range u.repo.UserApprovalRecords.Items() { - if record.VersionId == versionId && record.EnvironmentId == environmentId && record.Status == oapi.ApprovalStatusApproved { - records = append(records, record) - } + records, err := u.repo.GetApprovedByVersionAndEnvironment(versionId, environmentId) + if err != nil { + log.Warn("Failed to get approval records", "version_id", versionId, "environment_id", environmentId, "error", err) + return nil } sort.Slice(records, func(i, j int) bool { ti, ei := time.Parse(time.RFC3339, records[i].CreatedAt) diff --git a/apps/workspace-engine/svc/workspaceconsumer/consumer.go b/apps/workspace-engine/svc/workspaceconsumer/consumer.go index 704cef3a8..a65a207bf 100644 --- a/apps/workspace-engine/svc/workspaceconsumer/consumer.go +++ b/apps/workspace-engine/svc/workspaceconsumer/consumer.go @@ -95,6 +95,7 @@ func (s *Service) configureManager() error { wsstore.WithDBResources(bgCtx), wsstore.WithDBJobAgents(bgCtx), wsstore.WithDBPolicies(bgCtx), + wsstore.WithDBUserApprovalRecords(bgCtx), ), ), ) diff --git a/apps/workspace-engine/test/e2e/engine_approval_records_test.go b/apps/workspace-engine/test/e2e/engine_approval_records_test.go index 0cd6ad85d..1e8ea1755 100644 --- a/apps/workspace-engine/test/e2e/engine_approval_records_test.go +++ b/apps/workspace-engine/test/e2e/engine_approval_records_test.go @@ -51,25 +51,28 @@ func TestEngine_ApprovalRecords_GetApprovers(t *testing.T) { version.Tag = "v1.0.0" engine.PushEvent(ctx, handler.DeploymentVersionCreate, version) + user1ID := uuid.New().String() + user2ID := uuid.New().String() + // Add approvals engine.PushEvent(ctx, handler.UserApprovalRecordCreate, &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-alice", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, }) engine.PushEvent(ctx, handler.UserApprovalRecordCreate, &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-bob", + UserId: user2ID, Status: oapi.ApprovalStatusApproved, }) // Verify GetApprovers() approvers := engine.Workspace().UserApprovalRecords().GetApprovers(version.Id, environmentID) assert.Len(t, approvers, 2) - assert.Contains(t, approvers, "user-alice") - assert.Contains(t, approvers, "user-bob") + assert.Contains(t, approvers, user1ID) + assert.Contains(t, approvers, user2ID) } func TestEngine_ApprovalRecords_GetApproversMultipleEnvironments(t *testing.T) { @@ -110,11 +113,14 @@ func TestEngine_ApprovalRecords_GetApproversMultipleEnvironments(t *testing.T) { version.Tag = "v1.0.0" engine.PushEvent(ctx, handler.DeploymentVersionCreate, version) + user1ID := uuid.New().String() + user2ID := uuid.New().String() + // Approve for staging engine.PushEvent(ctx, handler.UserApprovalRecordCreate, &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: env1ID, - UserId: "user-charlie", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, }) @@ -122,18 +128,18 @@ func TestEngine_ApprovalRecords_GetApproversMultipleEnvironments(t *testing.T) { engine.PushEvent(ctx, handler.UserApprovalRecordCreate, &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: env2ID, - UserId: "user-delta", + UserId: user2ID, Status: oapi.ApprovalStatusApproved, }) // Verify environment-scoped GetApprovers stagingApprovers := engine.Workspace().UserApprovalRecords().GetApprovers(version.Id, env1ID) assert.Len(t, stagingApprovers, 1) - assert.Contains(t, stagingApprovers, "user-charlie") + assert.Contains(t, stagingApprovers, user1ID) prodApprovers := engine.Workspace().UserApprovalRecords().GetApprovers(version.Id, env2ID) assert.Len(t, prodApprovers, 1) - assert.Contains(t, prodApprovers, "user-delta") + assert.Contains(t, prodApprovers, user2ID) } func TestEngine_ApprovalRecords_UpdateStatus(t *testing.T) { @@ -167,27 +173,29 @@ func TestEngine_ApprovalRecords_UpdateStatus(t *testing.T) { version.Tag = "v1.0.0" engine.PushEvent(ctx, handler.DeploymentVersionCreate, version) + userID := uuid.New().String() + engine.PushEvent(ctx, handler.UserApprovalRecordCreate, &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-dave", + UserId: userID, Status: oapi.ApprovalStatusApproved, }) // user-dave should be an approver approvers := engine.Workspace().UserApprovalRecords().GetApprovers(version.Id, environmentID) - assert.Contains(t, approvers, "user-dave") + assert.Contains(t, approvers, userID) // Update to rejected — should no longer be an approver engine.PushEvent(ctx, handler.UserApprovalRecordUpdate, &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-dave", + UserId: userID, Status: oapi.ApprovalStatusRejected, }) approvers = engine.Workspace().UserApprovalRecords().GetApprovers(version.Id, environmentID) - assert.NotContains(t, approvers, "user-dave") + assert.NotContains(t, approvers, userID) } func TestEngine_ApprovalRecords_Delete(t *testing.T) { @@ -221,25 +229,27 @@ func TestEngine_ApprovalRecords_Delete(t *testing.T) { version.Tag = "v1.0.0" engine.PushEvent(ctx, handler.DeploymentVersionCreate, version) + userID := uuid.New().String() + engine.PushEvent(ctx, handler.UserApprovalRecordCreate, &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-eve", + UserId: userID, Status: oapi.ApprovalStatusApproved, }) approvers := engine.Workspace().UserApprovalRecords().GetApprovers(version.Id, environmentID) - assert.Contains(t, approvers, "user-eve") + assert.Contains(t, approvers, userID) engine.PushEvent(ctx, handler.UserApprovalRecordDelete, &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-eve", + UserId: userID, Status: oapi.ApprovalStatusApproved, }) approvers = engine.Workspace().UserApprovalRecords().GetApprovers(version.Id, environmentID) - assert.NotContains(t, approvers, "user-eve") + assert.NotContains(t, approvers, userID) } func TestEngine_ApprovalRecords_GetApprovalRecords(t *testing.T) { @@ -273,16 +283,19 @@ func TestEngine_ApprovalRecords_GetApprovalRecords(t *testing.T) { version.Tag = "v1.0.0" engine.PushEvent(ctx, handler.DeploymentVersionCreate, version) + user1ID := uuid.New().String() + user2ID := uuid.New().String() + engine.PushEvent(ctx, handler.UserApprovalRecordCreate, &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-a", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, }) engine.PushEvent(ctx, handler.UserApprovalRecordCreate, &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-b", + UserId: user2ID, Status: oapi.ApprovalStatusApproved, }) diff --git a/apps/workspace-engine/test/e2e/engine_policy_approval_test.go b/apps/workspace-engine/test/e2e/engine_policy_approval_test.go index 852b8cc96..a743a5f67 100644 --- a/apps/workspace-engine/test/e2e/engine_policy_approval_test.go +++ b/apps/workspace-engine/test/e2e/engine_policy_approval_test.go @@ -2,7 +2,6 @@ package e2e import ( "context" - "fmt" "testing" "time" "workspace-engine/pkg/events/handler" @@ -78,10 +77,13 @@ func TestEngine_ApprovalPolicy_BasicFlow(t *testing.T) { } // Add first approval + user1ID := uuid.New().String() + user2ID := uuid.New().String() + approval1 := &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval1) @@ -96,7 +98,7 @@ func TestEngine_ApprovalPolicy_BasicFlow(t *testing.T) { approval2 := &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-2", + UserId: user2ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval2) @@ -182,11 +184,14 @@ func TestEngine_ApprovalPolicy_UnapprovalFlow(t *testing.T) { version1.Tag = "v1.0.0" engine.PushEvent(ctx, handler.DeploymentVersionCreate, version1) + user1ID := uuid.New().String() + user2ID := uuid.New().String() + // Add 2 approvals for v1.0.0 approval1 := &oapi.UserApprovalRecord{ VersionId: version1.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval1) @@ -194,7 +199,7 @@ func TestEngine_ApprovalPolicy_UnapprovalFlow(t *testing.T) { approval2 := &oapi.UserApprovalRecord{ VersionId: version1.Id, EnvironmentId: environmentID, - UserId: "user-2", + UserId: user2ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval2) @@ -228,7 +233,7 @@ func TestEngine_ApprovalPolicy_UnapprovalFlow(t *testing.T) { approval3 := &oapi.UserApprovalRecord{ VersionId: version2.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval3) @@ -236,7 +241,7 @@ func TestEngine_ApprovalPolicy_UnapprovalFlow(t *testing.T) { approval4 := &oapi.UserApprovalRecord{ VersionId: version2.Id, EnvironmentId: environmentID, - UserId: "user-2", + UserId: user2ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval4) @@ -271,7 +276,7 @@ func TestEngine_ApprovalPolicy_UnapprovalFlow(t *testing.T) { approval5 := &oapi.UserApprovalRecord{ VersionId: version3.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval5) @@ -356,10 +361,14 @@ func TestEngine_ApprovalPolicy_MultipleVersions(t *testing.T) { version2.Tag = "v2.0.0" engine.PushEvent(ctx, handler.DeploymentVersionCreate, version2) + user1ID := uuid.New().String() + user2ID := uuid.New().String() + user3ID := uuid.New().String() + approval1 := &oapi.UserApprovalRecord{ VersionId: version2.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval1) @@ -373,7 +382,7 @@ func TestEngine_ApprovalPolicy_MultipleVersions(t *testing.T) { approval2 := &oapi.UserApprovalRecord{ VersionId: version3.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval2) @@ -381,7 +390,7 @@ func TestEngine_ApprovalPolicy_MultipleVersions(t *testing.T) { approval3 := &oapi.UserApprovalRecord{ VersionId: version3.Id, EnvironmentId: environmentID, - UserId: "user-2", + UserId: user2ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval3) @@ -414,7 +423,7 @@ func TestEngine_ApprovalPolicy_MultipleVersions(t *testing.T) { approval4 := &oapi.UserApprovalRecord{ VersionId: version4.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval4) @@ -422,7 +431,7 @@ func TestEngine_ApprovalPolicy_MultipleVersions(t *testing.T) { approval5 := &oapi.UserApprovalRecord{ VersionId: version4.Id, EnvironmentId: environmentID, - UserId: "user-2", + UserId: user2ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval5) @@ -430,7 +439,7 @@ func TestEngine_ApprovalPolicy_MultipleVersions(t *testing.T) { approval6 := &oapi.UserApprovalRecord{ VersionId: version4.Id, EnvironmentId: environmentID, - UserId: "user-3", + UserId: user3ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval6) @@ -524,16 +533,34 @@ func TestEngine_ApprovalPolicy_ExactMinimum(t *testing.T) { version.Tag = "v1.0.0" engine.PushEvent(ctx, handler.DeploymentVersionCreate, version) + user1ID := uuid.New().String() + user2ID := uuid.New().String() + user3ID := uuid.New().String() + // Add exactly 3 approvals (the minimum) - for i := 1; i <= 3; i++ { - approval := &oapi.UserApprovalRecord{ - VersionId: version.Id, - EnvironmentId: environmentID, - UserId: fmt.Sprintf("user-%d", i), - Status: oapi.ApprovalStatusApproved, - } - engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval) + approval1 := &oapi.UserApprovalRecord{ + VersionId: version.Id, + EnvironmentId: environmentID, + UserId: user1ID, + Status: oapi.ApprovalStatusApproved, + } + engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval1) + + approval2 := &oapi.UserApprovalRecord{ + VersionId: version.Id, + EnvironmentId: environmentID, + UserId: user2ID, + Status: oapi.ApprovalStatusApproved, + } + engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval2) + + approval3 := &oapi.UserApprovalRecord{ + VersionId: version.Id, + EnvironmentId: environmentID, + UserId: user3ID, + Status: oapi.ApprovalStatusApproved, } + engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval3) // Should be deployed with exactly 3 approvals allJobs := engine.Workspace().Jobs().Items() @@ -689,16 +716,44 @@ func TestEngine_ApprovalPolicy_PartialApprovalBlocks(t *testing.T) { version.Tag = "v1.0.0" engine.PushEvent(ctx, handler.DeploymentVersionCreate, version) + user1ID := uuid.New().String() + user2ID := uuid.New().String() + user3ID := uuid.New().String() + user4ID := uuid.New().String() + user5ID := uuid.New().String() + // Add 4 approvals (1 short of requirement) - for i := 1; i <= 4; i++ { - approval := &oapi.UserApprovalRecord{ - VersionId: version.Id, - EnvironmentId: environmentID, - UserId: fmt.Sprintf("user-%d", i), - Status: oapi.ApprovalStatusApproved, - } - engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval) + approval1 := &oapi.UserApprovalRecord{ + VersionId: version.Id, + EnvironmentId: environmentID, + UserId: user1ID, + Status: oapi.ApprovalStatusApproved, + } + engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval1) + + approval2 := &oapi.UserApprovalRecord{ + VersionId: version.Id, + EnvironmentId: environmentID, + UserId: user2ID, + Status: oapi.ApprovalStatusApproved, + } + engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval2) + + approval3 := &oapi.UserApprovalRecord{ + VersionId: version.Id, + EnvironmentId: environmentID, + UserId: user3ID, + Status: oapi.ApprovalStatusApproved, + } + engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval3) + + approval4 := &oapi.UserApprovalRecord{ + VersionId: version.Id, + EnvironmentId: environmentID, + UserId: user4ID, + Status: oapi.ApprovalStatusApproved, } + engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval4) // Should NOT be deployed (4 < 5) allJobs := engine.Workspace().Jobs().Items() @@ -710,7 +765,7 @@ func TestEngine_ApprovalPolicy_PartialApprovalBlocks(t *testing.T) { approval5 := &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-5", + UserId: user5ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval5) @@ -762,9 +817,10 @@ func TestEngine_ApprovalPolicy_ApprovalDeletion(t *testing.T) { // Add approval rule requiring 2 approvals policy, _ := engine.Workspace().Policies().Get(policyID) + rule1Id := uuid.New().String() policy.Rules = []oapi.PolicyRule{ { - Id: "rule-1", + Id: rule1Id, PolicyId: policyID, CreatedAt: "2024-01-01T00:00:00Z", AnyApproval: &oapi.AnyApprovalRule{MinApprovals: 2}, @@ -778,10 +834,13 @@ func TestEngine_ApprovalPolicy_ApprovalDeletion(t *testing.T) { version1.Tag = "v1.0.0" engine.PushEvent(ctx, handler.DeploymentVersionCreate, version1) + user1ID := uuid.New().String() + user2ID := uuid.New().String() + approval1 := &oapi.UserApprovalRecord{ VersionId: version1.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval1) @@ -789,7 +848,7 @@ func TestEngine_ApprovalPolicy_ApprovalDeletion(t *testing.T) { approval2 := &oapi.UserApprovalRecord{ VersionId: version1.Id, EnvironmentId: environmentID, - UserId: "user-2", + UserId: user2ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval2) @@ -822,7 +881,7 @@ func TestEngine_ApprovalPolicy_ApprovalDeletion(t *testing.T) { approval3 := &oapi.UserApprovalRecord{ VersionId: version2.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval3) @@ -830,7 +889,7 @@ func TestEngine_ApprovalPolicy_ApprovalDeletion(t *testing.T) { approval4 := &oapi.UserApprovalRecord{ VersionId: version2.Id, EnvironmentId: environmentID, - UserId: "user-2", + UserId: user2ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval4) diff --git a/apps/workspace-engine/test/e2e/engine_policy_conflicts_test.go b/apps/workspace-engine/test/e2e/engine_policy_conflicts_test.go index 99ccd786d..24c291740 100644 --- a/apps/workspace-engine/test/e2e/engine_policy_conflicts_test.go +++ b/apps/workspace-engine/test/e2e/engine_policy_conflicts_test.go @@ -22,6 +22,9 @@ func TestEngine_PolicyConflict_MultipleApprovalRequirements(t *testing.T) { resourceID := uuid.New().String() policy1ID := uuid.New().String() policy2ID := uuid.New().String() + user1ID := uuid.New().String() + user2ID := uuid.New().String() + user3ID := uuid.New().String() engine := integration.NewTestWorkspace(t, integration.WithJobAgent( @@ -81,7 +84,7 @@ func TestEngine_PolicyConflict_MultipleApprovalRequirements(t *testing.T) { approval1 := &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval1) @@ -89,7 +92,7 @@ func TestEngine_PolicyConflict_MultipleApprovalRequirements(t *testing.T) { approval2 := &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-2", + UserId: user2ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval2) @@ -104,7 +107,7 @@ func TestEngine_PolicyConflict_MultipleApprovalRequirements(t *testing.T) { approval3 := &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-3", + UserId: user3ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval3) @@ -125,6 +128,8 @@ func TestEngine_PolicyConflict_OverlappingSelectors(t *testing.T) { resourceID := uuid.New().String() policy1ID := uuid.New().String() policy2ID := uuid.New().String() + user1ID := uuid.New().String() + user2ID := uuid.New().String() engine := integration.NewTestWorkspace(t, integration.WithJobAgent( @@ -183,7 +188,7 @@ func TestEngine_PolicyConflict_OverlappingSelectors(t *testing.T) { approval1 := &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval1) @@ -197,7 +202,7 @@ func TestEngine_PolicyConflict_OverlappingSelectors(t *testing.T) { approval2 := &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-2", + UserId: user2ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval2) @@ -217,6 +222,7 @@ func TestEngine_PolicyConflict_PriorityOrdering(t *testing.T) { resourceID := uuid.New().String() highPriorityPolicyID := uuid.New().String() lowPriorityPolicyID := uuid.New().String() + user1ID := uuid.New().String() engine := integration.NewTestWorkspace(t, integration.WithJobAgent( @@ -273,7 +279,7 @@ func TestEngine_PolicyConflict_PriorityOrdering(t *testing.T) { approval := &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval) @@ -311,6 +317,7 @@ func TestEngine_PolicyConflict_ApprovalPlusRetry(t *testing.T) { resourceID := uuid.New().String() approvalPolicyID := uuid.New().String() retryPolicyID := uuid.New().String() + user1ID := uuid.New().String() engine := integration.NewTestWorkspace(t, integration.WithJobAgent( @@ -367,7 +374,7 @@ func TestEngine_PolicyConflict_ApprovalPlusRetry(t *testing.T) { approval := &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval) @@ -398,6 +405,7 @@ func TestEngine_PolicyConflict_AllRulesMerged(t *testing.T) { environmentID := uuid.New().String() policy1ID := uuid.New().String() policy2ID := uuid.New().String() + user1ID := uuid.New().String() engine := integration.NewTestWorkspace(t, integration.WithJobAgent( @@ -460,7 +468,7 @@ func TestEngine_PolicyConflict_AllRulesMerged(t *testing.T) { approval := &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, CreatedAt: time.Now().Format(time.RFC3339), } diff --git a/apps/workspace-engine/test/e2e/engine_policy_dynamic_changes_test.go b/apps/workspace-engine/test/e2e/engine_policy_dynamic_changes_test.go index d4367dd8d..74b8aba8d 100644 --- a/apps/workspace-engine/test/e2e/engine_policy_dynamic_changes_test.go +++ b/apps/workspace-engine/test/e2e/engine_policy_dynamic_changes_test.go @@ -21,6 +21,7 @@ func TestEngine_PolicyPriorityChange_MidDeployment(t *testing.T) { environmentID := uuid.New().String() resourceID := uuid.New().String() policyID := uuid.New().String() + userID := uuid.New().String() engine := integration.NewTestWorkspace(t, integration.WithJobAgent( @@ -78,7 +79,7 @@ func TestEngine_PolicyPriorityChange_MidDeployment(t *testing.T) { approval := &oapi.UserApprovalRecord{ VersionId: version1.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: userID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval) @@ -166,6 +167,8 @@ func TestEngine_PolicyRulesUpdate_ExistingApprovals(t *testing.T) { environmentID := uuid.New().String() resourceID := uuid.New().String() policyID := uuid.New().String() + user1ID := uuid.New().String() + user2ID := uuid.New().String() engine := integration.NewTestWorkspace(t, integration.WithJobAgent( @@ -206,7 +209,7 @@ func TestEngine_PolicyRulesUpdate_ExistingApprovals(t *testing.T) { approval1 := &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval1) @@ -214,7 +217,7 @@ func TestEngine_PolicyRulesUpdate_ExistingApprovals(t *testing.T) { approval2 := &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-2", + UserId: user2ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval2) @@ -372,6 +375,7 @@ func TestEngine_PolicyDelete_WithPendingApprovals(t *testing.T) { environmentID := uuid.New().String() resourceID := uuid.New().String() policyID := uuid.New().String() + user1ID := uuid.New().String() engine := integration.NewTestWorkspace(t, integration.WithJobAgent( @@ -418,7 +422,7 @@ func TestEngine_PolicyDelete_WithPendingApprovals(t *testing.T) { approval := &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval) diff --git a/apps/workspace-engine/test/e2e/engine_policy_skip_test.go b/apps/workspace-engine/test/e2e/engine_policy_skip_test.go index 9bbe7c960..78733749f 100644 --- a/apps/workspace-engine/test/e2e/engine_policy_skip_test.go +++ b/apps/workspace-engine/test/e2e/engine_policy_skip_test.go @@ -354,6 +354,7 @@ func TestEngine_PolicyBypass_PolicySpecific(t *testing.T) { policy2ID := uuid.New().String() rule1ID := uuid.New().String() rule2ID := uuid.New().String() + user1ID := uuid.New().String() engine := integration.NewTestWorkspace(t, integration.WithJobAgent( @@ -433,7 +434,7 @@ func TestEngine_PolicyBypass_PolicySpecific(t *testing.T) { approval := &oapi.UserApprovalRecord{ VersionId: version.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, CreatedAt: time.Now().Format(time.RFC3339), } diff --git a/apps/workspace-engine/test/e2e/engine_policy_version_cooldown_test.go b/apps/workspace-engine/test/e2e/engine_policy_version_cooldown_test.go index da1327baf..7c984d8dd 100644 --- a/apps/workspace-engine/test/e2e/engine_policy_version_cooldown_test.go +++ b/apps/workspace-engine/test/e2e/engine_policy_version_cooldown_test.go @@ -453,6 +453,7 @@ func TestEngine_VersionCooldown_CombinedWithApproval(t *testing.T) { environmentID := uuid.New().String() cooldownPolicyID := uuid.New().String() approvalPolicyID := uuid.New().String() + user1ID := uuid.New().String() engine := integration.NewTestWorkspace(t, integration.WithJobAgent( @@ -511,7 +512,7 @@ func TestEngine_VersionCooldown_CombinedWithApproval(t *testing.T) { approval := &oapi.UserApprovalRecord{ VersionId: v1.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval) @@ -537,7 +538,7 @@ func TestEngine_VersionCooldown_CombinedWithApproval(t *testing.T) { approval2 := &oapi.UserApprovalRecord{ VersionId: v2.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval2) diff --git a/apps/workspace-engine/test/e2e/engine_policy_versionselector_test.go b/apps/workspace-engine/test/e2e/engine_policy_versionselector_test.go index a4865f84b..589a19341 100644 --- a/apps/workspace-engine/test/e2e/engine_policy_versionselector_test.go +++ b/apps/workspace-engine/test/e2e/engine_policy_versionselector_test.go @@ -208,6 +208,7 @@ func TestEngine_VersionSelectorPolicy_CombinedWithOtherPolicies(t *testing.T) { approvalPolicyID := uuid.New().String() ruleVersionID := uuid.New().String() ruleApprovalID := uuid.New().String() + user1ID := uuid.New().String() engine := integration.NewTestWorkspace(t, integration.WithJobAgent( @@ -300,7 +301,7 @@ func TestEngine_VersionSelectorPolicy_CombinedWithOtherPolicies(t *testing.T) { approval := &oapi.UserApprovalRecord{ VersionId: version2.Id, EnvironmentId: environmentID, - UserId: "user-1", + UserId: user1ID, Status: oapi.ApprovalStatusApproved, } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval) diff --git a/apps/workspace-engine/test/e2e/engine_trace_test.go b/apps/workspace-engine/test/e2e/engine_trace_test.go index 81f8f68da..d507bae5b 100644 --- a/apps/workspace-engine/test/e2e/engine_trace_test.go +++ b/apps/workspace-engine/test/e2e/engine_trace_test.go @@ -195,11 +195,12 @@ func TestEngine_Trace_MultiplePolicyEvaluations(t *testing.T) { engine.PushEvent(ctx, handler.DeploymentVersionCreate, dv) // Add approval so the approval policy passes and gradual rollout can be evaluated + user1ID := uuid.New().String() approval := &oapi.UserApprovalRecord{ VersionId: dv.Id, EnvironmentId: environmentId, Status: oapi.ApprovalStatusApproved, - UserId: "test-user", + UserId: user1ID, CreatedAt: "2024-01-01T00:00:00Z", } engine.PushEvent(ctx, handler.UserApprovalRecordCreate, approval) diff --git a/apps/workspace-engine/test/integration/dbtest.go b/apps/workspace-engine/test/integration/dbtest.go index 17fa37c5f..61bedbfd6 100644 --- a/apps/workspace-engine/test/integration/dbtest.go +++ b/apps/workspace-engine/test/integration/dbtest.go @@ -79,6 +79,7 @@ func newDBTestWorkspace(t *testing.T, options ...WorkspaceOption) *TestWorkspace store.WithDBSystemEnvironments(ctx), store.WithDBResources(ctx), store.WithDBPolicies(ctx), + store.WithDBUserApprovalRecords(ctx), // store.WithDBReleases(ctx), ), ) diff --git a/packages/db/drizzle/0149_milky_hammerhead.sql b/packages/db/drizzle/0149_milky_hammerhead.sql new file mode 100644 index 000000000..5343f9549 --- /dev/null +++ b/packages/db/drizzle/0149_milky_hammerhead.sql @@ -0,0 +1,9 @@ +CREATE TABLE "user_approval_record" ( + "version_id" uuid NOT NULL, + "user_id" uuid NOT NULL, + "environment_id" uuid NOT NULL, + "status" text NOT NULL, + "reason" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "user_approval_record_version_id_user_id_environment_id_pk" PRIMARY KEY("version_id","user_id","environment_id") +); diff --git a/packages/db/drizzle/meta/0149_snapshot.json b/packages/db/drizzle/meta/0149_snapshot.json new file mode 100644 index 000000000..61f1242ff --- /dev/null +++ b/packages/db/drizzle/meta/0149_snapshot.json @@ -0,0 +1,4120 @@ +{ + "id": "b64e903a-273c-4574-b965-e91e509ce092", + "prevId": "89cc8df0-f5a6-4e8b-99c6-6f31e96a822b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_session_token_unique": { + "name": "session_session_token_unique", + "nullsNotDistinct": false, + "columns": ["session_token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "active_workspace_id": { + "name": "active_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "system_role": { + "name": "system_role", + "type": "system_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_active_workspace_id_workspace_id_fk": { + "name": "user_active_workspace_id_workspace_id_fk", + "tableFrom": "user", + "tableTo": "workspace", + "columnsFrom": ["active_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_api_key": { + "name": "user_api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key_preview": { + "name": "key_preview", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_api_key_key_prefix_key_hash_index": { + "name": "user_api_key_key_prefix_key_hash_index", + "columns": [ + { + "expression": "key_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_api_key_user_id_user_id_fk": { + "name": "user_api_key_user_id_user_id_fk", + "tableFrom": "user_api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.changelog_entry": { + "name": "changelog_entry", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_data": { + "name": "entity_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "changelog_entry_workspace_id_workspace_id_fk": { + "name": "changelog_entry_workspace_id_workspace_id_fk", + "tableFrom": "changelog_entry", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "changelog_entry_workspace_id_entity_type_entity_id_pk": { + "name": "changelog_entry_workspace_id_entity_type_entity_id_pk", + "columns": ["workspace_id", "entity_type", "entity_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard": { + "name": "dashboard", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_workspace_id_workspace_id_fk": { + "name": "dashboard_workspace_id_workspace_id_fk", + "tableFrom": "dashboard", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_widget": { + "name": "dashboard_widget", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "dashboard_id": { + "name": "dashboard_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "widget": { + "name": "widget", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "x": { + "name": "x", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "y": { + "name": "y", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "w": { + "name": "w", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "h": { + "name": "h", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_widget_dashboard_id_dashboard_id_fk": { + "name": "dashboard_widget_dashboard_id_dashboard_id_fk", + "tableFrom": "dashboard_widget", + "tableTo": "dashboard", + "columnsFrom": ["dashboard_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_trace_span": { + "name": "deployment_trace_span", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "span_id": { + "name": "span_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_span_id": { + "name": "parent_span_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_time": { + "name": "start_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "release_target_key": { + "name": "release_target_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_trace_id": { + "name": "parent_trace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "node_type": { + "name": "node_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "events": { + "name": "events", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "deployment_trace_span_trace_span_idx": { + "name": "deployment_trace_span_trace_span_idx", + "columns": [ + { + "expression": "trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "span_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_trace_id_idx": { + "name": "deployment_trace_span_trace_id_idx", + "columns": [ + { + "expression": "trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_parent_span_id_idx": { + "name": "deployment_trace_span_parent_span_id_idx", + "columns": [ + { + "expression": "parent_span_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_workspace_id_idx": { + "name": "deployment_trace_span_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_release_target_key_idx": { + "name": "deployment_trace_span_release_target_key_idx", + "columns": [ + { + "expression": "release_target_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_release_id_idx": { + "name": "deployment_trace_span_release_id_idx", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_job_id_idx": { + "name": "deployment_trace_span_job_id_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_parent_trace_id_idx": { + "name": "deployment_trace_span_parent_trace_id_idx", + "columns": [ + { + "expression": "parent_trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_created_at_idx": { + "name": "deployment_trace_span_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_phase_idx": { + "name": "deployment_trace_span_phase_idx", + "columns": [ + { + "expression": "phase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_node_type_idx": { + "name": "deployment_trace_span_node_type_idx", + "columns": [ + { + "expression": "node_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_status_idx": { + "name": "deployment_trace_span_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_trace_span_workspace_id_workspace_id_fk": { + "name": "deployment_trace_span_workspace_id_workspace_id_fk", + "tableFrom": "deployment_trace_span", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_version": { + "name": "deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "deployment_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_version_deployment_id_tag_index": { + "name": "deployment_version_deployment_id_tag_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_version_created_at_idx": { + "name": "deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_version_workspace_id_workspace_id_fk": { + "name": "deployment_version_workspace_id_workspace_id_fk", + "tableFrom": "deployment_version", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computed_deployment_resource": { + "name": "computed_deployment_resource", + "schema": "", + "columns": { + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "computed_deployment_resource_deployment_id_deployment_id_fk": { + "name": "computed_deployment_resource_deployment_id_deployment_id_fk", + "tableFrom": "computed_deployment_resource", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_deployment_resource_resource_id_resource_id_fk": { + "name": "computed_deployment_resource_resource_id_resource_id_fk", + "tableFrom": "computed_deployment_resource", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "computed_deployment_resource_deployment_id_resource_id_pk": { + "name": "computed_deployment_resource_deployment_id_resource_id_pk", + "columns": ["deployment_id", "resource_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_agent_id": { + "name": "job_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "job_agents": { + "name": "job_agents", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "resource_selector": { + "name": "resource_selector", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'false'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_workspace_id_workspace_id_fk": { + "name": "deployment_workspace_id_workspace_id_fk", + "tableFrom": "deployment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computed_environment_resource": { + "name": "computed_environment_resource", + "schema": "", + "columns": { + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "computed_environment_resource_environment_id_environment_id_fk": { + "name": "computed_environment_resource_environment_id_environment_id_fk", + "tableFrom": "computed_environment_resource", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_environment_resource_resource_id_resource_id_fk": { + "name": "computed_environment_resource_resource_id_resource_id_fk", + "tableFrom": "computed_environment_resource", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "computed_environment_resource_environment_id_resource_id_pk": { + "name": "computed_environment_resource_environment_id_resource_id_pk", + "columns": ["environment_id", "resource_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "resource_selector": { + "name": "resource_selector", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'false'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "environment_workspace_id_workspace_id_fk": { + "name": "environment_workspace_id_workspace_id_fk", + "tableFrom": "environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.event": { + "name": "event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "event_workspace_id_workspace_id_fk": { + "name": "event_workspace_id_workspace_id_fk", + "tableFrom": "event", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource": { + "name": "resource", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resource_identifier_workspace_id_index": { + "name": "resource_identifier_workspace_id_index", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_provider_id_resource_provider_id_fk": { + "name": "resource_provider_id_resource_provider_id_fk", + "tableFrom": "resource", + "tableTo": "resource_provider", + "columnsFrom": ["provider_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "resource_workspace_id_workspace_id_fk": { + "name": "resource_workspace_id_workspace_id_fk", + "tableFrom": "resource", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_schema": { + "name": "resource_schema", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "json_schema": { + "name": "json_schema", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "resource_schema_version_kind_workspace_id_index": { + "name": "resource_schema_version_kind_workspace_id_index", + "columns": [ + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_schema_workspace_id_workspace_id_fk": { + "name": "resource_schema_workspace_id_workspace_id_fk", + "tableFrom": "resource_schema", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_provider": { + "name": "resource_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "resource_provider_workspace_id_name_index": { + "name": "resource_provider_workspace_id_name_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_provider_workspace_id_workspace_id_fk": { + "name": "resource_provider_workspace_id_workspace_id_fk", + "tableFrom": "resource_provider", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system": { + "name": "system", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": { + "system_workspace_id_workspace_id_fk": { + "name": "system_workspace_id_workspace_id_fk", + "tableFrom": "system", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_deployment": { + "name": "system_deployment", + "schema": "", + "columns": { + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "system_deployment_system_id_system_id_fk": { + "name": "system_deployment_system_id_system_id_fk", + "tableFrom": "system_deployment", + "tableTo": "system", + "columnsFrom": ["system_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "system_deployment_deployment_id_deployment_id_fk": { + "name": "system_deployment_deployment_id_deployment_id_fk", + "tableFrom": "system_deployment", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "system_deployment_system_id_deployment_id_pk": { + "name": "system_deployment_system_id_deployment_id_pk", + "columns": ["system_id", "deployment_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_environment": { + "name": "system_environment", + "schema": "", + "columns": { + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "system_environment_system_id_system_id_fk": { + "name": "system_environment_system_id_system_id_fk", + "tableFrom": "system_environment", + "tableTo": "system", + "columnsFrom": ["system_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "system_environment_environment_id_environment_id_fk": { + "name": "system_environment_environment_id_environment_id_fk", + "tableFrom": "system_environment", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "system_environment_system_id_environment_id_pk": { + "name": "system_environment_system_id_environment_id_pk", + "columns": ["system_id", "environment_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team": { + "name": "team", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "team_workspace_id_workspace_id_fk": { + "name": "team_workspace_id_workspace_id_fk", + "tableFrom": "team", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_member": { + "name": "team_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "team_member_team_id_user_id_index": { + "name": "team_member_team_id_user_id_index", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_member_team_id_team_id_fk": { + "name": "team_member_team_id_team_id_fk", + "tableFrom": "team_member", + "tableTo": "team", + "columnsFrom": ["team_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_member_user_id_user_id_fk": { + "name": "team_member_user_id_user_id_fk", + "tableFrom": "team_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job": { + "name": "job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_agent_id": { + "name": "job_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "job_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'policy_passing'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_created_at_idx": { + "name": "job_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_status_idx": { + "name": "job_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_external_id_idx": { + "name": "job_external_id_idx", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_job_agent_id_job_agent_id_fk": { + "name": "job_job_agent_id_job_agent_id_fk", + "tableFrom": "job", + "tableTo": "job_agent", + "columnsFrom": ["job_agent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_metadata": { + "name": "job_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "job_metadata_key_job_id_index": { + "name": "job_metadata_key_job_id_index", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_metadata_job_id_idx": { + "name": "job_metadata_job_id_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_metadata_job_id_job_id_fk": { + "name": "job_metadata_job_id_job_id_fk", + "tableFrom": "job_metadata", + "tableTo": "job", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_variable": { + "name": "job_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "job_variable_job_id_key_index": { + "name": "job_variable_job_id_key_index", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_variable_job_id_job_id_fk": { + "name": "job_variable_job_id_job_id_fk", + "tableFrom": "job_variable", + "tableTo": "job", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_slug_unique": { + "name": "workspace_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_email_domain_matching": { + "name": "workspace_email_domain_matching", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verification_code": { + "name": "verification_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification_email": { + "name": "verification_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_email_domain_matching_workspace_id_domain_index": { + "name": "workspace_email_domain_matching_workspace_id_domain_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_email_domain_matching_workspace_id_workspace_id_fk": { + "name": "workspace_email_domain_matching_workspace_id_workspace_id_fk", + "tableFrom": "workspace_email_domain_matching", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_email_domain_matching_role_id_role_id_fk": { + "name": "workspace_email_domain_matching_role_id_role_id_fk", + "tableFrom": "workspace_email_domain_matching", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invite_token": { + "name": "workspace_invite_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invite_token_role_id_role_id_fk": { + "name": "workspace_invite_token_role_id_role_id_fk", + "tableFrom": "workspace_invite_token", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invite_token_workspace_id_workspace_id_fk": { + "name": "workspace_invite_token_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invite_token", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invite_token_created_by_user_id_fk": { + "name": "workspace_invite_token_created_by_user_id_fk", + "tableFrom": "workspace_invite_token", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invite_token_token_unique": { + "name": "workspace_invite_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entity_role": { + "name": "entity_role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "scope_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "entity_role_role_id_entity_type_entity_id_scope_id_scope_type_index": { + "name": "entity_role_role_id_entity_type_entity_id_scope_id_scope_type_index", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entity_role_role_id_role_id_fk": { + "name": "entity_role_role_id_role_id_fk", + "tableFrom": "entity_role", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role": { + "name": "role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "role_workspace_id_workspace_id_fk": { + "name": "role_workspace_id_workspace_id_fk", + "tableFrom": "role", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_permission": { + "name": "role_permission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "role_permission_role_id_permission_index": { + "name": "role_permission_role_id_permission_index", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "role_permission_role_id_role_id_fk": { + "name": "role_permission_role_id_role_id_fk", + "tableFrom": "role_permission", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.release": { + "name": "release", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "release_resource_id_resource_id_fk": { + "name": "release_resource_id_resource_id_fk", + "tableFrom": "release", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "release_environment_id_environment_id_fk": { + "name": "release_environment_id_environment_id_fk", + "tableFrom": "release", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "release_deployment_id_deployment_id_fk": { + "name": "release_deployment_id_deployment_id_fk", + "tableFrom": "release", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "release_version_id_deployment_version_id_fk": { + "name": "release_version_id_deployment_version_id_fk", + "tableFrom": "release", + "tableTo": "deployment_version", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.release_variable": { + "name": "release_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "release_id": { + "name": "release_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "release_variable_release_id_key_index": { + "name": "release_variable_release_id_key_index", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "release_variable_release_id_release_id_fk": { + "name": "release_variable_release_id_release_id_fk", + "tableFrom": "release_variable", + "tableTo": "release", + "columnsFrom": ["release_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reconcile_work_payload": { + "name": "reconcile_work_payload", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "reconcile_work_payload_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "scope_ref": { + "name": "scope_ref", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "payload_type": { + "name": "payload_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "payload_key": { + "name": "payload_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reconcile_work_payload_scope_ref_payload_type_payload_key_index": { + "name": "reconcile_work_payload_scope_ref_payload_type_payload_key_index", + "columns": [ + { + "expression": "scope_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "payload_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "payload_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reconcile_work_payload_scope_ref_index": { + "name": "reconcile_work_payload_scope_ref_index", + "columns": [ + { + "expression": "scope_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reconcile_work_payload_scope_ref_reconcile_work_scope_id_fk": { + "name": "reconcile_work_payload_scope_ref_reconcile_work_scope_id_fk", + "tableFrom": "reconcile_work_payload", + "tableTo": "reconcile_work_scope", + "columnsFrom": ["scope_ref"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reconcile_work_scope": { + "name": "reconcile_work_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "reconcile_work_scope_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "event_ts": { + "name": "event_ts", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "priority": { + "name": "priority", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "not_before": { + "name": "not_before", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_until": { + "name": "claimed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reconcile_work_scope_workspace_id_kind_scope_type_scope_id_index": { + "name": "reconcile_work_scope_workspace_id_kind_scope_type_scope_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reconcile_work_scope_kind_not_before_priority_event_ts_claimed_until_index": { + "name": "reconcile_work_scope_kind_not_before_priority_event_ts_claimed_until_index", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "not_before", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_ts", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy": { + "name": "policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "selector": { + "name": "selector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'true'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_workspace_id_workspace_id_fk": { + "name": "policy_workspace_id_workspace_id_fk", + "tableFrom": "policy", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_any_approval": { + "name": "policy_rule_any_approval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "min_approvals": { + "name": "min_approvals", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_any_approval_policy_id_policy_id_fk": { + "name": "policy_rule_any_approval_policy_id_policy_id_fk", + "tableFrom": "policy_rule_any_approval", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_deployment_dependency": { + "name": "policy_rule_deployment_dependency", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "depends_on": { + "name": "depends_on", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_deployment_dependency_policy_id_policy_id_fk": { + "name": "policy_rule_deployment_dependency_policy_id_policy_id_fk", + "tableFrom": "policy_rule_deployment_dependency", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_deployment_window": { + "name": "policy_rule_deployment_window", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allow_window": { + "name": "allow_window", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rrule": { + "name": "rrule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_deployment_window_policy_id_policy_id_fk": { + "name": "policy_rule_deployment_window_policy_id_policy_id_fk", + "tableFrom": "policy_rule_deployment_window", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_environment_progression": { + "name": "policy_rule_environment_progression", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "depends_on_environment_selector": { + "name": "depends_on_environment_selector", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "maximum_age_hours": { + "name": "maximum_age_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "minimum_soak_time_minutes": { + "name": "minimum_soak_time_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "minimum_success_percentage": { + "name": "minimum_success_percentage", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "success_statuses": { + "name": "success_statuses", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_environment_progression_policy_id_policy_id_fk": { + "name": "policy_rule_environment_progression_policy_id_policy_id_fk", + "tableFrom": "policy_rule_environment_progression", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_gradual_rollout": { + "name": "policy_rule_gradual_rollout", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rollout_type": { + "name": "rollout_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time_scale_interval": { + "name": "time_scale_interval", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_gradual_rollout_policy_id_policy_id_fk": { + "name": "policy_rule_gradual_rollout_policy_id_policy_id_fk", + "tableFrom": "policy_rule_gradual_rollout", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_retry": { + "name": "policy_rule_retry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "max_retries": { + "name": "max_retries", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "backoff_seconds": { + "name": "backoff_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "backoff_strategy": { + "name": "backoff_strategy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_backoff_seconds": { + "name": "max_backoff_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "retry_on_statuses": { + "name": "retry_on_statuses", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_retry_policy_id_policy_id_fk": { + "name": "policy_rule_retry_policy_id_policy_id_fk", + "tableFrom": "policy_rule_retry", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_rollback": { + "name": "policy_rule_rollback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "on_job_statuses": { + "name": "on_job_statuses", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "on_verification_failure": { + "name": "on_verification_failure", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_rollback_policy_id_policy_id_fk": { + "name": "policy_rule_rollback_policy_id_policy_id_fk", + "tableFrom": "policy_rule_rollback", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_verification": { + "name": "policy_rule_verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "trigger_on": { + "name": "trigger_on", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_verification_policy_id_policy_id_fk": { + "name": "policy_rule_verification_policy_id_policy_id_fk", + "tableFrom": "policy_rule_verification", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_version_cooldown": { + "name": "policy_rule_version_cooldown", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_version_cooldown_policy_id_policy_id_fk": { + "name": "policy_rule_version_cooldown_policy_id_policy_id_fk", + "tableFrom": "policy_rule_version_cooldown", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_version_selector": { + "name": "policy_rule_version_selector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "selector": { + "name": "selector", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_version_selector_policy_id_policy_id_fk": { + "name": "policy_rule_version_selector_policy_id_policy_id_fk", + "tableFrom": "policy_rule_version_selector", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_approval_record": { + "name": "user_approval_record", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_approval_record_version_id_user_id_environment_id_pk": { + "name": "user_approval_record_version_id_user_id_environment_id_pk", + "columns": ["version_id", "user_id", "environment_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_agent": { + "name": "job_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "job_agent_workspace_id_name_index": { + "name": "job_agent_workspace_id_name_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_agent_workspace_id_workspace_id_fk": { + "name": "job_agent_workspace_id_workspace_id_fk", + "tableFrom": "job_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.system_role": { + "name": "system_role", + "schema": "public", + "values": ["user", "admin"] + }, + "public.deployment_version_status": { + "name": "deployment_version_status", + "schema": "public", + "values": [ + "unspecified", + "building", + "ready", + "failed", + "rejected", + "paused" + ] + }, + "public.job_reason": { + "name": "job_reason", + "schema": "public", + "values": [ + "policy_passing", + "policy_override", + "env_policy_override", + "config_policy_override" + ] + }, + "public.job_status": { + "name": "job_status", + "schema": "public", + "values": [ + "cancelled", + "skipped", + "in_progress", + "action_required", + "pending", + "failure", + "invalid_job_agent", + "invalid_integration", + "external_run_not_found", + "successful" + ] + }, + "public.entity_type": { + "name": "entity_type", + "schema": "public", + "values": ["user", "team"] + }, + "public.scope_type": { + "name": "scope_type", + "schema": "public", + "values": [ + "deploymentVersion", + "resource", + "resourceProvider", + "workspace", + "environment", + "system", + "deployment" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 7d22e0ef5..8d0c9dc64 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -1044,6 +1044,13 @@ "when": 1771972725019, "tag": "0148_jittery_wild_child", "breakpoints": true + }, + { + "idx": 149, + "version": "7", + "when": 1772042525109, + "tag": "0149_milky_hammerhead", + "breakpoints": true } ] } diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index 486a8f718..3982656b9 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -17,3 +17,4 @@ export * from "./changelog-entry.js"; export * from "./release.js"; export * from "./reconcile.js"; export * from "./policy.js"; +export * from "./user-approval-record.js"; diff --git a/packages/db/src/schema/user-approval-record.ts b/packages/db/src/schema/user-approval-record.ts new file mode 100644 index 000000000..974668ea1 --- /dev/null +++ b/packages/db/src/schema/user-approval-record.ts @@ -0,0 +1,22 @@ +import { + pgTable, + primaryKey, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; + +export const userApprovalRecord = pgTable( + "user_approval_record", + { + versionId: uuid("version_id").notNull(), + userId: uuid("user_id").notNull(), + environmentId: uuid("environment_id").notNull(), + status: text("status").notNull(), + reason: text("reason"), + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + }, + (t) => [primaryKey({ columns: [t.versionId, t.userId, t.environmentId] })], +); From 149ff5c512eb87de6f19bd4184482b20da76745c Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 25 Feb 2026 11:20:55 -0800 Subject: [PATCH 2/3] cleanup --- .../store/repository/db/userapprovalrecords/mapper.go | 5 +++-- .../store/repository/db/userapprovalrecords/repo.go | 5 +++++ .../pkg/workspace/store/user_approval_records.go | 4 ++-- apps/workspace-engine/test/integration/dbtest.go | 1 + 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/mapper.go b/apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/mapper.go index 675a709cf..b5b0e2d07 100644 --- a/apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/mapper.go +++ b/apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/mapper.go @@ -48,9 +48,10 @@ func ToUpsertParams(e *oapi.UserApprovalRecord) (db.UpsertUserApprovalRecordPara var createdAt pgtype.Timestamptz if e.CreatedAt != "" { t, err := time.Parse(time.RFC3339, e.CreatedAt) - if err == nil { - createdAt = pgtype.Timestamptz{Time: t, Valid: true} + if err != nil { + return db.UpsertUserApprovalRecordParams{}, fmt.Errorf("parse created_at %q: %w", e.CreatedAt, err) } + createdAt = pgtype.Timestamptz{Time: t, Valid: true} } return db.UpsertUserApprovalRecordParams{ diff --git a/apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/repo.go b/apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/repo.go index b80fff0cd..b21436f8b 100644 --- a/apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/repo.go +++ b/apps/workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords/repo.go @@ -2,12 +2,14 @@ package userapprovalrecords import ( "context" + "errors" "fmt" "workspace-engine/pkg/db" "workspace-engine/pkg/oapi" "github.com/charmbracelet/log" "github.com/google/uuid" + "github.com/jackc/pgx/v5" ) type Repo struct { @@ -31,6 +33,9 @@ func (r *Repo) Get(key string) (*oapi.UserApprovalRecord, bool) { EnvironmentID: environmentID, }) if err != nil { + if !errors.Is(err, pgx.ErrNoRows) { + log.Warn("Failed to get user approval record", "key", key, "error", err) + } return nil, false } diff --git a/apps/workspace-engine/pkg/workspace/store/user_approval_records.go b/apps/workspace-engine/pkg/workspace/store/user_approval_records.go index 42e314ea4..7faced610 100644 --- a/apps/workspace-engine/pkg/workspace/store/user_approval_records.go +++ b/apps/workspace-engine/pkg/workspace/store/user_approval_records.go @@ -34,8 +34,8 @@ func (u *UserApprovalRecords) Upsert(ctx context.Context, userApprovalRecord *oa u.store.changeset.RecordUpsert(userApprovalRecord) } -func (u *UserApprovalRecords) Get(versionId, userId string) (*oapi.UserApprovalRecord, bool) { - return u.repo.Get(versionId + userId) +func (u *UserApprovalRecords) Get(versionId, userId, environmentId string) (*oapi.UserApprovalRecord, bool) { + return u.repo.Get(versionId + "-" + userId + "-" + environmentId) } func (u *UserApprovalRecords) Remove(ctx context.Context, key string) { diff --git a/apps/workspace-engine/test/integration/dbtest.go b/apps/workspace-engine/test/integration/dbtest.go index 61bedbfd6..1b271e96b 100644 --- a/apps/workspace-engine/test/integration/dbtest.go +++ b/apps/workspace-engine/test/integration/dbtest.go @@ -106,6 +106,7 @@ func newDBTestWorkspace(t *testing.T, options ...WorkspaceOption) *TestWorkspace }{ {"release variables", "DELETE FROM release_variable WHERE release_id IN (SELECT r.id FROM release r JOIN deployment d ON d.id = r.deployment_id WHERE d.workspace_id = $1)"}, {"releases", "DELETE FROM release WHERE deployment_id IN (SELECT id FROM deployment WHERE workspace_id = $1)"}, + {"user approval records", "DELETE FROM user_approval_record WHERE version_id IN (SELECT id FROM deployment_version WHERE workspace_id = $1)"}, {"deployment versions", "DELETE FROM deployment_version WHERE workspace_id = $1"}, {"system deployments", "DELETE FROM system_deployment WHERE deployment_id IN (SELECT id FROM deployment WHERE workspace_id = $1)"}, {"system environments", "DELETE FROM system_environment WHERE environment_id IN (SELECT id FROM environment WHERE workspace_id = $1)"}, From 1aee9fe4ebc116cfa06a21a4c95a28feda2040cf Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 25 Feb 2026 11:31:13 -0800 Subject: [PATCH 3/3] cleanup --- .../pkg/workspace/store/repository/db/repo.go | 46 +++++++++---------- .../workspace/store/user_approval_records.go | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/apps/workspace-engine/pkg/workspace/store/repository/db/repo.go b/apps/workspace-engine/pkg/workspace/store/repository/db/repo.go index 961cc8662..4ab8c7684 100644 --- a/apps/workspace-engine/pkg/workspace/store/repository/db/repo.go +++ b/apps/workspace-engine/pkg/workspace/store/repository/db/repo.go @@ -8,27 +8,27 @@ import ( "workspace-engine/pkg/workspace/store/repository/db/environments" "workspace-engine/pkg/workspace/store/repository/db/jobagents" "workspace-engine/pkg/workspace/store/repository/db/policies" - "workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords" "workspace-engine/pkg/workspace/store/repository/db/releases" "workspace-engine/pkg/workspace/store/repository/db/resourceproviders" "workspace-engine/pkg/workspace/store/repository/db/resources" "workspace-engine/pkg/workspace/store/repository/db/systemdeployments" "workspace-engine/pkg/workspace/store/repository/db/systemenvironments" "workspace-engine/pkg/workspace/store/repository/db/systems" + "workspace-engine/pkg/workspace/store/repository/db/userapprovalrecords" ) type DBRepo struct { - deploymentVersions repository.DeploymentVersionRepo - deployments repository.DeploymentRepo - environments repository.EnvironmentRepo - resources repository.ResourceRepo - systems repository.SystemRepo - jobAgents repository.JobAgentRepo - resourceProviders repository.ResourceProviderRepo - releases repository.ReleaseRepo - systemDeployments repository.SystemDeploymentRepo - systemEnvironments repository.SystemEnvironmentRepo - policies repository.PolicyRepo + deploymentVersions repository.DeploymentVersionRepo + deployments repository.DeploymentRepo + environments repository.EnvironmentRepo + resources repository.ResourceRepo + systems repository.SystemRepo + jobAgents repository.JobAgentRepo + resourceProviders repository.ResourceProviderRepo + releases repository.ReleaseRepo + systemDeployments repository.SystemDeploymentRepo + systemEnvironments repository.SystemEnvironmentRepo + policies repository.PolicyRepo userApprovalRecords repository.UserApprovalRecordRepo } @@ -82,17 +82,17 @@ func (d *DBRepo) UserApprovalRecords() repository.UserApprovalRecordRepo { func NewDBRepo(ctx context.Context, workspaceID string) *DBRepo { return &DBRepo{ - deploymentVersions: deploymentversions.NewRepo(ctx, workspaceID), - deployments: deployments.NewRepo(ctx, workspaceID), - environments: environments.NewRepo(ctx, workspaceID), - resources: resources.NewRepo(ctx, workspaceID), - systems: systems.NewRepo(ctx, workspaceID), - jobAgents: jobagents.NewRepo(ctx, workspaceID), - resourceProviders: resourceproviders.NewRepo(ctx, workspaceID), - releases: releases.NewRepo(ctx, workspaceID), - systemDeployments: systemdeployments.NewRepo(ctx), - systemEnvironments: systemenvironments.NewRepo(ctx), - policies: policies.NewRepo(ctx, workspaceID), + deploymentVersions: deploymentversions.NewRepo(ctx, workspaceID), + deployments: deployments.NewRepo(ctx, workspaceID), + environments: environments.NewRepo(ctx, workspaceID), + resources: resources.NewRepo(ctx, workspaceID), + systems: systems.NewRepo(ctx, workspaceID), + jobAgents: jobagents.NewRepo(ctx, workspaceID), + resourceProviders: resourceproviders.NewRepo(ctx, workspaceID), + releases: releases.NewRepo(ctx, workspaceID), + systemDeployments: systemdeployments.NewRepo(ctx), + systemEnvironments: systemenvironments.NewRepo(ctx), + policies: policies.NewRepo(ctx, workspaceID), userApprovalRecords: userapprovalrecords.NewRepo(ctx), } } diff --git a/apps/workspace-engine/pkg/workspace/store/user_approval_records.go b/apps/workspace-engine/pkg/workspace/store/user_approval_records.go index 7faced610..c4fa22854 100644 --- a/apps/workspace-engine/pkg/workspace/store/user_approval_records.go +++ b/apps/workspace-engine/pkg/workspace/store/user_approval_records.go @@ -35,7 +35,7 @@ func (u *UserApprovalRecords) Upsert(ctx context.Context, userApprovalRecord *oa } func (u *UserApprovalRecords) Get(versionId, userId, environmentId string) (*oapi.UserApprovalRecord, bool) { - return u.repo.Get(versionId + "-" + userId + "-" + environmentId) + return u.repo.Get(versionId + userId + environmentId) } func (u *UserApprovalRecords) Remove(ctx context.Context, key string) {