Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 53 additions & 9 deletions coderd/database/db2sdk/db2sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -1609,6 +1609,11 @@ func Chat(c database.Chat, diffStatus *database.ChatDiffStatus, files []database
parentChatID := c.ParentChatID.UUID
chat.ParentChatID = &parentChatID
}
// Always initialize Children to an empty slice so the JSON
// field serializes as [] rather than null. Root chats may
// later have children populated; child chats remain empty
// because nesting depth is capped at 1.
Comment thread
mafredri marked this conversation as resolved.
chat.Children = []codersdk.Chat{}
switch {
case c.RootChatID.Valid:
rootChatID := c.RootChatID.UUID
Expand Down Expand Up @@ -1756,19 +1761,21 @@ func ChatDebugStep(s database.ChatDebugStep) codersdk.ChatDebugStep {
}
}

// ChatRows converts a slice of database.GetChatsRow (which embeds
// Chat plus HasUnread) to codersdk.Chat, looking up diff statuses
// from the provided map. When diffStatusesByChatID is non-nil,
// chats without an entry receive an empty DiffStatus.
func ChatRows(rows []database.GetChatsRow, diffStatusesByChatID map[uuid.UUID]database.ChatDiffStatus) []codersdk.Chat {
result := make([]codersdk.Chat, len(rows))
for i, row := range rows {
diffStatus, ok := diffStatusesByChatID[row.Chat.ID]
// ChildChatRows converts child chat rows to codersdk.Chat values,
// resolving diff statuses from the shared map. When diffStatuses
// is non-nil, children without an entry receive an empty DiffStatus.
func ChildChatRows(
Comment thread
mafredri marked this conversation as resolved.
children []database.GetChildChatsByParentIDsRow,
diffStatuses map[uuid.UUID]database.ChatDiffStatus,
) []codersdk.Chat {
result := make([]codersdk.Chat, len(children))
for i, row := range children {
diffStatus, ok := diffStatuses[row.Chat.ID]
if ok {
result[i] = Chat(row.Chat, &diffStatus, nil)
} else {
result[i] = Chat(row.Chat, nil, nil)
if diffStatusesByChatID != nil {
if diffStatuses != nil {
emptyDiffStatus := ChatDiffStatus(row.Chat.ID, nil)
result[i].DiffStatus = &emptyDiffStatus
}
Expand All @@ -1778,6 +1785,43 @@ func ChatRows(rows []database.GetChatsRow, diffStatusesByChatID map[uuid.UUID]da
return result
}

// ChatRowsWithChildren converts root chat rows and their child rows
// into codersdk.Chat values with children embedded under each parent.
// Both root and child diff statuses are resolved from the shared map.
func ChatRowsWithChildren(
roots []database.GetChatsRow,
children []database.GetChildChatsByParentIDsRow,
diffStatuses map[uuid.UUID]database.ChatDiffStatus,
) []codersdk.Chat {
// Group children by parent ID.
childrenByParent := make(map[uuid.UUID][]database.GetChildChatsByParentIDsRow, len(children))
for _, row := range children {
parentID := row.Chat.ParentChatID.UUID
childrenByParent[parentID] = append(childrenByParent[parentID], row)
}

result := make([]codersdk.Chat, len(roots))
for i, row := range roots {
diffStatus, ok := diffStatuses[row.Chat.ID]
if ok {
result[i] = Chat(row.Chat, &diffStatus, nil)
} else {
result[i] = Chat(row.Chat, nil, nil)
if diffStatuses != nil {
emptyDiffStatus := ChatDiffStatus(row.Chat.ID, nil)
result[i].DiffStatus = &emptyDiffStatus
}
}
result[i].HasUnread = row.HasUnread

// Embed child chats.
if childRows, ok := childrenByParent[row.Chat.ID]; ok {
result[i].Children = ChildChatRows(childRows, diffStatuses)
}
}
return result
}

// ChatDiffStatus converts a database.ChatDiffStatus to a
// codersdk.ChatDiffStatus. When status is nil an empty value
// containing only the chatID is returned.
Expand Down
2 changes: 1 addition & 1 deletion coderd/database/db2sdk/db2sdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,7 @@ func TestChat_AllFieldsPopulated(t *testing.T) {

v := reflect.ValueOf(got)
typ := v.Type()
// HasUnread is populated by ChatRows (which joins the
// HasUnread is populated by ChatRowsWithChildren (which joins the
// read-cursor query), not by Chat. Warnings is a transient
// field populated by handlers, not the converter. Both are
// expected to remain zero here.
Expand Down
8 changes: 8 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -2944,6 +2944,14 @@ func (q *querier) GetChatsUpdatedAfter(ctx context.Context, updatedAfter time.Ti
return q.db.GetChatsUpdatedAfter(ctx, updatedAfter)
}

func (q *querier) GetChildChatsByParentIDs(ctx context.Context, arg database.GetChildChatsByParentIDsParams) ([]database.GetChildChatsByParentIDsRow, error) {
Comment thread
mafredri marked this conversation as resolved.
Comment thread
mafredri marked this conversation as resolved.
// Each child is independently authorized via post-filter.
// The handler calls this after GetChats already authorized
// the parent chats, but we still verify read access on
// every child row for defense in depth.
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetChildChatsByParentIDs)(ctx, arg)
}

func (q *querier) GetConnectionLogsOffset(ctx context.Context, arg database.GetConnectionLogsOffsetParams) ([]database.GetConnectionLogsOffsetRow, error) {
// Just like with the audit logs query, shortcut if the user is an owner.
err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceConnectionLog)
Expand Down
21 changes: 21 additions & 0 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,27 @@ func (s *MethodTestSuite) TestChats() {
// No asserts here because SQLFilter.
check.Args(params).Asserts()
}))
s.Run("GetChildChatsByParentIDs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
parentA := testutil.Fake(s.T(), faker, database.Chat{})
parentB := testutil.Fake(s.T(), faker, database.Chat{})
childA := testutil.Fake(s.T(), faker, database.Chat{
ParentChatID: uuid.NullUUID{UUID: parentA.ID, Valid: true},
})
childB := testutil.Fake(s.T(), faker, database.Chat{
ParentChatID: uuid.NullUUID{UUID: parentB.ID, Valid: true},
})
parentIDs := []uuid.UUID{parentA.ID, parentB.ID}
params := database.GetChildChatsByParentIDsParams{
ParentIds: parentIDs,
Archived: sql.NullBool{Bool: false, Valid: true},
}
rows := []database.GetChildChatsByParentIDsRow{
{Chat: childA},
{Chat: childB},
}
dbm.EXPECT().GetChildChatsByParentIDs(gomock.Any(), params).Return(rows, nil).AnyTimes()
check.Args(params).Asserts(childA, policy.ActionRead, childB, policy.ActionRead).Returns(rows)
}))
s.Run("GetAuthorizedChats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
params := database.GetChatsParams{}
dbm.EXPECT().GetAuthorizedChats(gomock.Any(), params, gomock.Any()).Return([]database.GetChatsRow{}, nil).AnyTimes()
Expand Down
8 changes: 8 additions & 0 deletions coderd/database/dbmetrics/querymetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions coderd/database/dbmock/dbmock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions coderd/database/modelmethods.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ func (r GetChatsRow) RBACObject() rbac.Object {
return r.Chat.RBACObject()
}

func (r GetChildChatsByParentIDsRow) RBACObject() rbac.Object {
return r.Chat.RBACObject()
}

func (c ChatFile) RBACObject() rbac.Object {
return rbac.ResourceChat.WithID(c.ID).WithOwner(c.OwnerID.String()).InOrg(c.OrganizationID)
}
Expand Down
18 changes: 18 additions & 0 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

107 changes: 107 additions & 0 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions coderd/database/queries/chats.sql
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,11 @@ WHERE
WHEN sqlc.narg('label_filter')::jsonb IS NOT NULL THEN chats.labels @> sqlc.narg('label_filter')::jsonb
ELSE true
END
-- Paginate over root chats only. Children are fetched
-- separately via GetChildChatsByParentIDs and embedded under
-- each parent. Other callers that need the full set should
-- use a narrower query (e.g. GetChatsByWorkspaceIDs).
AND chats.parent_chat_id IS NULL
Comment thread
mafredri marked this conversation as resolved.
-- Authorize Filter clause will be injected below in GetAuthorizedChats
-- @authorize_filter
ORDER BY
Expand All @@ -390,6 +395,45 @@ LIMIT
-- Default to 50 to prevent accidental excessively large queries.
COALESCE(NULLIF(@limit_opt :: int, 0), 50);

-- name: GetChildChatsByParentIDs :many
-- Fetches child chats for the given parent IDs. Used by the list
-- handler and singular getChat to embed children under each root
-- chat response.
--
-- The archived narg is three-state: NULL returns every child,
-- true returns archived children only, false returns active
-- children only. Callers pass the archive state that matches the
-- parent list they are rendering, so sidebar views never surface
-- children whose archive state differs from the parent.
--
-- The archive invariant (parent archived implies child archived)
-- is enforced at write time, not here: ArchiveChatByID cascades
-- through root_chat_id, patchChat allows individual child
-- archive, and chatd.UnarchiveChildChatAtomic rejects unarchive
-- of a child while the parent is archived. A stale read during a
-- concurrent cascade can momentarily return an archive-state
-- mismatch; the caller's next refetch converges.
SELECT
sqlc.embed(chats),
EXISTS (
SELECT 1 FROM chat_messages cm
WHERE cm.chat_id = chats.id
AND cm.role = 'assistant'
AND cm.deleted = false
AND cm.id > COALESCE(chats.last_read_message_id, 0)
) AS has_unread
FROM
chats
WHERE
chats.parent_chat_id = ANY(@parent_ids :: uuid[])
Comment thread
mafredri marked this conversation as resolved.
AND CASE
WHEN sqlc.narg('archived') :: boolean IS NULL THEN true
ELSE chats.archived = sqlc.narg('archived') :: boolean
Comment thread
mafredri marked this conversation as resolved.
END
ORDER BY
chats.created_at ASC,
chats.id ASC;

-- name: InsertChat :one
INSERT INTO chats (
organization_id,
Expand Down
Loading
Loading