From 32aee9ea4c9120323bbaa697d59ada5241983e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Banaszewski?= Date: Tue, 2 Jun 2026 13:25:44 +0200 Subject: [PATCH] feat: add DB queries for ai_gateway_coderd_keys (#25564) Adds Insert, List and Delete queries for `ai_gateway_coderd_keys ` table. --- coderd/database/dbauthz/dbauthz.go | 21 +++ coderd/database/dbauthz/dbauthz_test.go | 17 ++ coderd/database/dbmetrics/querymetrics.go | 24 +++ coderd/database/dbmock/dbmock.go | 45 +++++ coderd/database/querier.go | 3 + coderd/database/querier_test.go | 186 ++++++++++++++++++++ coderd/database/queries.sql.go | 106 +++++++++++ coderd/database/queries/ai_gateway_keys.sql | 13 ++ 8 files changed, 415 insertions(+) create mode 100644 coderd/database/queries/ai_gateway_keys.sql diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index a1a7497153..d084514dd8 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1907,6 +1907,13 @@ func (q *querier) CustomRoles(ctx context.Context, arg database.CustomRolesParam return q.db.CustomRoles(ctx, arg) } +func (q *querier) DeleteAIGatewayKey(ctx context.Context, id uuid.UUID) (database.DeleteAIGatewayKeyRow, error) { + if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAIGatewayKey); err != nil { + return database.DeleteAIGatewayKeyRow{}, err + } + return q.db.DeleteAIGatewayKey(ctx, id) +} + func (q *querier) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAIProvider); err != nil { return err @@ -5463,6 +5470,13 @@ func (q *querier) InsertAIBridgeUserPrompt(ctx context.Context, arg database.Ins return q.db.InsertAIBridgeUserPrompt(ctx, arg) } +func (q *querier) InsertAIGatewayKey(ctx context.Context, arg database.InsertAIGatewayKeyParams) (database.InsertAIGatewayKeyRow, error) { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAIGatewayKey); err != nil { + return database.InsertAIGatewayKeyRow{}, err + } + return q.db.InsertAIGatewayKey(ctx, arg) +} + func (q *querier) InsertAIProvider(ctx context.Context, arg database.InsertAIProviderParams) (database.AIProvider, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAIProvider); err != nil { return database.AIProvider{}, err @@ -6238,6 +6252,13 @@ func (q *querier) ListAIBridgeUserPromptsByInterceptionIDs(ctx context.Context, return q.db.ListAIBridgeUserPromptsByInterceptionIDs(ctx, interceptionIDs) } +func (q *querier) ListAIGatewayKeys(ctx context.Context) ([]database.ListAIGatewayKeysRow, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIGatewayKey); err != nil { + return nil, err + } + return q.db.ListAIGatewayKeys(ctx) +} + func (q *querier) ListBoundaryLogsBySessionID(ctx context.Context, arg database.ListBoundaryLogsBySessionIDParams) ([]database.BoundaryLog, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceBoundaryLog); err != nil { return nil, err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index f788fa71e2..1d7f7a62c3 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -6638,6 +6638,23 @@ func (s *MethodTestSuite) TestAIBridge() { dbm.EXPECT().UpdateEncryptedUserAIProviderKey(gomock.Any(), arg).Return(key, nil).AnyTimes() check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionUpdate).Returns(key) })) + + s.Run("InsertAIGatewayKey", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + params := database.InsertAIGatewayKeyParams{} + row := database.InsertAIGatewayKeyRow{} + dbm.EXPECT().InsertAIGatewayKey(gomock.Any(), params).Return(row, nil).AnyTimes() + check.Args(params).Asserts(rbac.ResourceAIGatewayKey, policy.ActionCreate).Returns(row) + })) + s.Run("ListAIGatewayKeys", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + rows := []database.ListAIGatewayKeysRow{} + dbm.EXPECT().ListAIGatewayKeys(gomock.Any()).Return(rows, nil).AnyTimes() + check.Args().Asserts(rbac.ResourceAIGatewayKey, policy.ActionRead).Returns(rows) + })) + s.Run("DeleteAIGatewayKey", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + id := uuid.New() + dbm.EXPECT().DeleteAIGatewayKey(gomock.Any(), id).Return(database.DeleteAIGatewayKeyRow{}, nil).AnyTimes() + check.Args(id).Asserts(rbac.ResourceAIGatewayKey, policy.ActionDelete).Returns(database.DeleteAIGatewayKeyRow{}) + })) } func (s *MethodTestSuite) TestTelemetry() { diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index e7120ec588..7f68852baf 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -377,6 +377,14 @@ func (m queryMetricsStore) CustomRoles(ctx context.Context, arg database.CustomR return r0, r1 } +func (m queryMetricsStore) DeleteAIGatewayKey(ctx context.Context, id uuid.UUID) (database.DeleteAIGatewayKeyRow, error) { + start := time.Now() + r0, r1 := m.s.DeleteAIGatewayKey(ctx, id) + m.queryLatencies.WithLabelValues("DeleteAIGatewayKey").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteAIGatewayKey").Inc() + return r0, r1 +} + func (m queryMetricsStore) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error { start := time.Now() r0 := m.s.DeleteAIProviderByID(ctx, id) @@ -3721,6 +3729,14 @@ func (m queryMetricsStore) InsertAIBridgeUserPrompt(ctx context.Context, arg dat return r0, r1 } +func (m queryMetricsStore) InsertAIGatewayKey(ctx context.Context, arg database.InsertAIGatewayKeyParams) (database.InsertAIGatewayKeyRow, error) { + start := time.Now() + r0, r1 := m.s.InsertAIGatewayKey(ctx, arg) + m.queryLatencies.WithLabelValues("InsertAIGatewayKey").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertAIGatewayKey").Inc() + return r0, r1 +} + func (m queryMetricsStore) InsertAIProvider(ctx context.Context, arg database.InsertAIProviderParams) (database.AIProvider, error) { start := time.Now() r0, r1 := m.s.InsertAIProvider(ctx, arg) @@ -4417,6 +4433,14 @@ func (m queryMetricsStore) ListAIBridgeUserPromptsByInterceptionIDs(ctx context. return r0, r1 } +func (m queryMetricsStore) ListAIGatewayKeys(ctx context.Context) ([]database.ListAIGatewayKeysRow, error) { + start := time.Now() + r0, r1 := m.s.ListAIGatewayKeys(ctx) + m.queryLatencies.WithLabelValues("ListAIGatewayKeys").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "ListAIGatewayKeys").Inc() + return r0, r1 +} + func (m queryMetricsStore) ListBoundaryLogsBySessionID(ctx context.Context, arg database.ListBoundaryLogsBySessionIDParams) ([]database.BoundaryLog, error) { start := time.Now() r0, r1 := m.s.ListBoundaryLogsBySessionID(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 0f6799e638..8321983028 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -603,6 +603,21 @@ func (mr *MockStoreMockRecorder) CustomRoles(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CustomRoles", reflect.TypeOf((*MockStore)(nil).CustomRoles), ctx, arg) } +// DeleteAIGatewayKey mocks base method. +func (m *MockStore) DeleteAIGatewayKey(ctx context.Context, id uuid.UUID) (database.DeleteAIGatewayKeyRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteAIGatewayKey", ctx, id) + ret0, _ := ret[0].(database.DeleteAIGatewayKeyRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteAIGatewayKey indicates an expected call of DeleteAIGatewayKey. +func (mr *MockStoreMockRecorder) DeleteAIGatewayKey(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAIGatewayKey", reflect.TypeOf((*MockStore)(nil).DeleteAIGatewayKey), ctx, id) +} + // DeleteAIProviderByID mocks base method. func (m *MockStore) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error { m.ctrl.T.Helper() @@ -6989,6 +7004,21 @@ func (mr *MockStoreMockRecorder) InsertAIBridgeUserPrompt(ctx, arg any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIBridgeUserPrompt", reflect.TypeOf((*MockStore)(nil).InsertAIBridgeUserPrompt), ctx, arg) } +// InsertAIGatewayKey mocks base method. +func (m *MockStore) InsertAIGatewayKey(ctx context.Context, arg database.InsertAIGatewayKeyParams) (database.InsertAIGatewayKeyRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertAIGatewayKey", ctx, arg) + ret0, _ := ret[0].(database.InsertAIGatewayKeyRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InsertAIGatewayKey indicates an expected call of InsertAIGatewayKey. +func (mr *MockStoreMockRecorder) InsertAIGatewayKey(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIGatewayKey", reflect.TypeOf((*MockStore)(nil).InsertAIGatewayKey), ctx, arg) +} + // InsertAIProvider mocks base method. func (m *MockStore) InsertAIProvider(ctx context.Context, arg database.InsertAIProviderParams) (database.AIProvider, error) { m.ctrl.T.Helper() @@ -8279,6 +8309,21 @@ func (mr *MockStoreMockRecorder) ListAIBridgeUserPromptsByInterceptionIDs(ctx, i return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAIBridgeUserPromptsByInterceptionIDs", reflect.TypeOf((*MockStore)(nil).ListAIBridgeUserPromptsByInterceptionIDs), ctx, interceptionIds) } +// ListAIGatewayKeys mocks base method. +func (m *MockStore) ListAIGatewayKeys(ctx context.Context) ([]database.ListAIGatewayKeysRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAIGatewayKeys", ctx) + ret0, _ := ret[0].([]database.ListAIGatewayKeysRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListAIGatewayKeys indicates an expected call of ListAIGatewayKeys. +func (mr *MockStoreMockRecorder) ListAIGatewayKeys(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAIGatewayKeys", reflect.TypeOf((*MockStore)(nil).ListAIGatewayKeys), ctx) +} + // ListAuthorizedAIBridgeClients mocks base method. func (m *MockStore) ListAuthorizedAIBridgeClients(ctx context.Context, arg database.ListAIBridgeClientsParams, prepared rbac.PreparedAuthorized) ([]string, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index a6c8f3e7db..4b9fa58e01 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -101,6 +101,7 @@ type sqlcQuerier interface { CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) CreateUserSecret(ctx context.Context, arg CreateUserSecretParams) (UserSecret, error) CustomRoles(ctx context.Context, arg CustomRolesParams) ([]CustomRole, error) + DeleteAIGatewayKey(ctx context.Context, id uuid.UUID) (DeleteAIGatewayKeyRow, error) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error DeleteAPIKeyByID(ctx context.Context, id string) error @@ -914,6 +915,7 @@ type sqlcQuerier interface { InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) (AIBridgeTokenUsage, error) InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) (AIBridgeToolUsage, error) InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIBridgeUserPromptParams) (AIBridgeUserPrompt, error) + InsertAIGatewayKey(ctx context.Context, arg InsertAIGatewayKeyParams) (InsertAIGatewayKeyRow, error) InsertAIProvider(ctx context.Context, arg InsertAIProviderParams) (AIProvider, error) InsertAIProviderKey(ctx context.Context, arg InsertAIProviderKeyParams) (AIProviderKey, error) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) @@ -1048,6 +1050,7 @@ type sqlcQuerier interface { ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeTokenUsage, error) ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeToolUsage, error) ListAIBridgeUserPromptsByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeUserPrompt, error) + ListAIGatewayKeys(ctx context.Context) ([]ListAIGatewayKeysRow, error) // Lists boundary logs for a session, sorted by sequence number ascending. // Supports optional exclusive sequence number bounds (seq_after, seq_before) // for fetching events between two known interceptions. diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index cefe6a866e..984dd8a79a 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -14733,3 +14733,189 @@ func TestSoftDeleteWorkspaceAgentsByWorkspaceID(t *testing.T) { err = db.SoftDeleteWorkspaceAgentsByWorkspaceID(ctx, wsEmpty) require.NoError(t, err) } + +func TestAIGatewayKeysTableConstraints(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + ctx := testutil.Context(t, testutil.WaitMedium) + + preExsiting := database.InsertAIGatewayKeyParams{ + ID: uuid.New(), + Name: "name", + SecretPrefix: "cgw_test__1", + HashedSecret: []byte("first-secret"), + } + _, err := db.InsertAIGatewayKey(ctx, preExsiting) + require.NoError(t, err) + + tests := []struct { + name string + params database.InsertAIGatewayKeyParams + expectUniqueErr database.UniqueConstraint + expectCheckErr database.CheckConstraint + }{ + { + name: "duplicate name", + params: aiGatewayKeyParams(preExsiting.Name, "cgw_test002"), + expectUniqueErr: database.UniqueAiGatewayKeysNameIndex, + }, + { + name: "duplicate secret prefix", + params: aiGatewayKeyParams("different-key", preExsiting.SecretPrefix), + expectUniqueErr: database.UniqueAiGatewayKeysSecretPrefixIndex, + }, + { + name: "duplicate hashed secret", + params: database.InsertAIGatewayKeyParams{ID: uuid.New(), Name: "other-name", SecretPrefix: "cgw_1234567", HashedSecret: preExsiting.HashedSecret}, + expectUniqueErr: database.UniqueAiGatewayKeysHashedSecretIndex, + }, + { + name: "empty name", + params: aiGatewayKeyParams("", "cgw_1234567"), + expectCheckErr: database.CheckAiGatewayKeysNameCheck, + }, + { + name: "name with trailing dash", + params: aiGatewayKeyParams("other-name-", "cgw_1234567"), + expectCheckErr: database.CheckAiGatewayKeysNameCheck, + }, + { + name: "name with consecutive dashes", + params: aiGatewayKeyParams("other--name", "cgw_1234567"), + expectCheckErr: database.CheckAiGatewayKeysNameCheck, + }, + { + name: "name with underscore", + params: aiGatewayKeyParams("other_name", "cgw_1234567"), + expectCheckErr: database.CheckAiGatewayKeysNameCheck, + }, + { + name: "name with space", + params: aiGatewayKeyParams("other name", "cgw_1234567"), + expectCheckErr: database.CheckAiGatewayKeysNameCheck, + }, + { + name: "name with leading dash", + params: aiGatewayKeyParams("-other-name", "cgw_1234567"), + expectCheckErr: database.CheckAiGatewayKeysNameCheck, + }, + { + name: "name longer than 64 characters", + params: aiGatewayKeyParams(strings.Repeat("a", 65), "cgw_1234567"), + expectCheckErr: database.CheckAiGatewayKeysNameCheck, + }, + { + name: "empty secret prefix", + params: aiGatewayKeyParams("other-name", ""), + expectCheckErr: database.CheckAiGatewayKeysSecretPrefixCheck, + }, + { + name: "invalid secret prefix length", + params: aiGatewayKeyParams("other-name", "cgw_short"), + expectCheckErr: database.CheckAiGatewayKeysSecretPrefixCheck, + }, + { + name: "empty hashed secret", + params: database.InsertAIGatewayKeyParams{ID: uuid.New(), Name: "other-name", SecretPrefix: "cgw_1234567"}, + expectCheckErr: database.CheckAiGatewayKeysHashedSecretCheck, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + + _, err = db.InsertAIGatewayKey(ctx, tc.params) + require.Error(t, err) + requireAIGatewayKeysViolation(t, err, tc.expectUniqueErr, tc.expectCheckErr) + }) + } +} + +func TestAIGatewayKeysQueries(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + ctx := testutil.Context(t, testutil.WaitLong) + + first := aiGatewayKeyParams("first-key", "cgw_first__") + second := aiGatewayKeyParams("second-key", "cgw_second_") + second.HashedSecret = []byte("second-secret") + + firstRow, err := db.InsertAIGatewayKey(ctx, first) + require.NoError(t, err) + require.Equal(t, first.ID, firstRow.ID) + + require.Equal(t, "first-key", firstRow.Name) + require.Equal(t, first.SecretPrefix, firstRow.SecretPrefix) + + secondRow, err := db.InsertAIGatewayKey(ctx, second) + require.NoError(t, err) + require.Equal(t, second.ID, secondRow.ID) + + require.Equal(t, "second-key", secondRow.Name) + require.Equal(t, second.SecretPrefix, secondRow.SecretPrefix) + + keys, err := db.ListAIGatewayKeys(ctx) + require.NoError(t, err) + require.Len(t, keys, 2) + + requireAIGatewayKeysRow(t, keys[0], first, firstRow.CreatedAt) + require.False(t, keys[0].LastUsedAt.Valid) + requireAIGatewayKeysRow(t, keys[1], second, secondRow.CreatedAt) + require.False(t, keys[1].LastUsedAt.Valid) + + deleted, err := db.DeleteAIGatewayKey(ctx, first.ID) + require.NoError(t, err) + require.Equal(t, first.ID, deleted.ID) + require.Equal(t, first.Name, deleted.Name) + require.Equal(t, first.SecretPrefix, deleted.SecretPrefix) + require.Equal(t, firstRow.CreatedAt, deleted.CreatedAt) + + _, err = db.DeleteAIGatewayKey(ctx, first.ID) + require.ErrorIs(t, err, sql.ErrNoRows) + + keys, err = db.ListAIGatewayKeys(ctx) + require.NoError(t, err) + require.Len(t, keys, 1) + requireAIGatewayKeysRow(t, keys[0], second, secondRow.CreatedAt) +} + +func aiGatewayKeyParams(name string, secretPrefix string) database.InsertAIGatewayKeyParams { + return database.InsertAIGatewayKeyParams{ + ID: uuid.New(), + Name: name, + SecretPrefix: secretPrefix, + HashedSecret: []byte("secret"), + } +} + +func requireAIGatewayKeysRow(t *testing.T, listRow database.ListAIGatewayKeysRow, insertParams database.InsertAIGatewayKeyParams, insertCreatedAt time.Time) { + t.Helper() + + require.Equal(t, insertParams.ID, listRow.ID) + require.Equal(t, insertParams.Name, listRow.Name) + require.Equal(t, insertParams.SecretPrefix, listRow.SecretPrefix) + require.Equal(t, insertCreatedAt, listRow.CreatedAt) +} + +func requireAIGatewayKeysViolation( + t *testing.T, + err error, + uniqueConstraint database.UniqueConstraint, + checkConstraint database.CheckConstraint, +) { + t.Helper() + + switch { + case uniqueConstraint != "": + require.True(t, database.IsUniqueViolation(err, uniqueConstraint), "expected %q unique violation, got %v", uniqueConstraint, err) + case checkConstraint != "": + require.True(t, database.IsCheckViolation(err, checkConstraint), "expected %q check violation, got %v", checkConstraint, err) + default: + require.FailNow(t, "test case must expect a constraint error") + } +} diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index dc646121dc..28b11009a3 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -111,6 +111,112 @@ func (q *sqlQuerier) ActivityBumpWorkspace(ctx context.Context, arg ActivityBump return err } +const deleteAIGatewayKey = `-- name: DeleteAIGatewayKey :one +DELETE FROM ai_gateway_keys WHERE id = $1 +RETURNING id, name, secret_prefix, created_at, last_used_at +` + +type DeleteAIGatewayKeyRow struct { + ID uuid.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` + SecretPrefix string `db:"secret_prefix" json:"secret_prefix"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LastUsedAt sql.NullTime `db:"last_used_at" json:"last_used_at"` +} + +func (q *sqlQuerier) DeleteAIGatewayKey(ctx context.Context, id uuid.UUID) (DeleteAIGatewayKeyRow, error) { + row := q.db.QueryRowContext(ctx, deleteAIGatewayKey, id) + var i DeleteAIGatewayKeyRow + err := row.Scan( + &i.ID, + &i.Name, + &i.SecretPrefix, + &i.CreatedAt, + &i.LastUsedAt, + ) + return i, err +} + +const insertAIGatewayKey = `-- name: InsertAIGatewayKey :one +INSERT INTO ai_gateway_keys (id, name, secret_prefix, hashed_secret, created_at) +VALUES ($1, $4, $2, $3, NOW()) +RETURNING id, name, secret_prefix, created_at +` + +type InsertAIGatewayKeyParams struct { + ID uuid.UUID `db:"id" json:"id"` + SecretPrefix string `db:"secret_prefix" json:"secret_prefix"` + HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"` + Name string `db:"name" json:"name"` +} + +type InsertAIGatewayKeyRow struct { + ID uuid.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` + SecretPrefix string `db:"secret_prefix" json:"secret_prefix"` + CreatedAt time.Time `db:"created_at" json:"created_at"` +} + +func (q *sqlQuerier) InsertAIGatewayKey(ctx context.Context, arg InsertAIGatewayKeyParams) (InsertAIGatewayKeyRow, error) { + row := q.db.QueryRowContext(ctx, insertAIGatewayKey, + arg.ID, + arg.SecretPrefix, + arg.HashedSecret, + arg.Name, + ) + var i InsertAIGatewayKeyRow + err := row.Scan( + &i.ID, + &i.Name, + &i.SecretPrefix, + &i.CreatedAt, + ) + return i, err +} + +const listAIGatewayKeys = `-- name: ListAIGatewayKeys :many +SELECT id, name, secret_prefix, created_at, last_used_at +FROM ai_gateway_keys +ORDER BY created_at ASC +` + +type ListAIGatewayKeysRow struct { + ID uuid.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` + SecretPrefix string `db:"secret_prefix" json:"secret_prefix"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LastUsedAt sql.NullTime `db:"last_used_at" json:"last_used_at"` +} + +func (q *sqlQuerier) ListAIGatewayKeys(ctx context.Context) ([]ListAIGatewayKeysRow, error) { + rows, err := q.db.QueryContext(ctx, listAIGatewayKeys) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListAIGatewayKeysRow + for rows.Next() { + var i ListAIGatewayKeysRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.SecretPrefix, + &i.CreatedAt, + &i.LastUsedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const deleteAIProviderKey = `-- name: DeleteAIProviderKey :exec DELETE FROM ai_provider_keys diff --git a/coderd/database/queries/ai_gateway_keys.sql b/coderd/database/queries/ai_gateway_keys.sql new file mode 100644 index 0000000000..308d0cb89d --- /dev/null +++ b/coderd/database/queries/ai_gateway_keys.sql @@ -0,0 +1,13 @@ +-- name: InsertAIGatewayKey :one +INSERT INTO ai_gateway_keys (id, name, secret_prefix, hashed_secret, created_at) +VALUES ($1, @name, $2, $3, NOW()) +RETURNING id, name, secret_prefix, created_at; + +-- name: ListAIGatewayKeys :many +SELECT id, name, secret_prefix, created_at, last_used_at +FROM ai_gateway_keys +ORDER BY created_at ASC; + +-- name: DeleteAIGatewayKey :one +DELETE FROM ai_gateway_keys WHERE id = $1 +RETURNING id, name, secret_prefix, created_at, last_used_at;