Skip to content

OpenAPI: Migrate member endpoints#4173

Draft
laevandus wants to merge 1 commit into
developfrom
open-api-members
Draft

OpenAPI: Migrate member endpoints#4173
laevandus wants to merge 1 commit into
developfrom
open-api-members

Conversation

@laevandus

@laevandus laevandus commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🔗 Issue Links

Resolves: IOS-1829

🎯 Goal

Migrate the member query and partial member update endpoints from the hand-written v1 implementation to the OpenAPI-generated v2 endpoints and models.

📝 Summary

  • Point channelMembersqueryMembers (GET /api/v2/chat/members) and partialMemberUpdateupdateMemberPartial (PATCH /api/v2/chat/channels/{type}/{id}/member) at the generated v2 operations, returning the generated MembersResponse / ChannelMemberResponse / UpdateMemberPartialResponse.
  • Persist the generated responses through new DatabaseSession save methods (saveUser(response:), saveMember(response:), saveMembers(response:)), alongside the existing MemberPayload/UserPayload ones which are still used by WebSocket events.
  • Delete the hand-written MemberEndpoints and MemberUpdatePayload; callers now build the generated UpdateMemberPartialRequest directly.
  • Convert ChannelMemberListQuery into the generated QueryMembersPayload via a new Filter.toRawJSONDictionary() helper.

🛠 Implementation

  • Two representations kept intentionally. WebSocket events still decode the v1 flattened MemberPayload/UserPayload, so those types remain; only the REST endpoints switch to the generated models. Public types (ChatChannelMember, MemberRole, MemberInfo) are unchanged.
  • updateMemberPartial self-membership narrowing. The v2 path drops userId (channels/{type}/{id}/member); the public/worker partialUpdate(...) signatures keep userId for source compatibility but no longer send it, matching the v2 contract.
  • Filter[String: RawJSON]. Ported toRawJSONDictionary() / FilterValue.rawJSON / MemberRole.filterRawValue and added ChannelMemberListSortingKey.remoteKey, so ChannelMemberListQuery.asQueryMembersPayload() produces the generated request. This is the same conversion the wider OpenAPI migration branch uses.
  • SortParamRequest collision. The generated SortParamRequest collided with the hand-written poll one; renamed the poll-local precursor to PollsSortParamRequest.
  • Test infrastructure. queryMembers serializes its request as a payload query-item string with non-deterministic key order, so CompareJSONEqual now compares string values that are themselves JSON objects structurally (order-independent) — keeping full AnyEndpoint equality in endpoint tests. Added Filter tests for the new toRawJSONDictionary conversion.

🎨 Showcase

Not applicable — no UI changes.

🧪 Manual Testing Notes

Open debug menu and use show channel members item, members appear.
Try archiving a channel using the debug menu, channel appears under archived channel list query (button in the toolbar for channel list queries).

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change should be manually QAed
  • Changelog is updated with client-facing changes
  • Changelog is updated with new localization keys
  • New code is covered by unit tests
  • Documentation has been updated in the docs-content repo

@laevandus
laevandus requested a review from a team as a code owner July 16, 2026 12:55
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR migrates member listing and partial member updates to OpenAPI-generated request and response models, updates database persistence and offline endpoint handling, and revises controllers, workers, mocks, fixtures, and tests accordingly.

Changes

Members API migration

Layer / File(s) Summary
OpenAPI contracts and query serialization
Scripts/openapi_generate.sh, Sources/StreamChat/APIClient/Endpoints/..., Sources/StreamChat/Query/..., TestTools/StreamChatTestTools/Extensions/...
OpenAPI allowlists retain the new member models and endpoints; member queries serialize filters, sorting, pagination, and channel identity into QueryMembersPayload.
Response-based persistence
Sources/StreamChat/Database/...
Database sessions and DTO mappings accept UserResponse, ChannelMemberResponse, and MembersResponse, including custom data and relationships.
Member request and update flow
Sources/StreamChat/Controllers/..., Sources/StreamChat/Workers/...
Controllers and workers use UpdateMemberPartialRequest, .queryMembers, and .updateMemberPartial, then persist returned response models.
Test contracts and fixtures
TestTools/..., Tests/StreamChatTests/...
Mocks, dummy responses, endpoint assertions, filter serialization tests, member-list tests, and pin/archive/update tests use the new contracts.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: ✅ Feature

Suggested reviewers: nuno-vieira, martinmitrevski

Sequence Diagram(s)

sequenceDiagram
  participant MemberList
  participant ChannelMemberListUpdater
  participant APIClient
  participant DatabaseSession
  MemberList->>ChannelMemberListUpdater: load(query)
  ChannelMemberListUpdater->>APIClient: queryMembers(payload)
  APIClient-->>ChannelMemberListUpdater: MembersResponse
  ChannelMemberListUpdater->>DatabaseSession: saveMembers(response)
  DatabaseSession-->>MemberList: updated member state
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main change: migrating member endpoints to OpenAPI-generated implementations.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch open-api-members

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

❤️ Share

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

@github-actions

Copy link
Copy Markdown
2 Warnings
⚠️ Please be sure to complete the Contributor Checklist in the Pull Request description
⚠️ Big PR
1 Message
📖 There seems to be app changes but CHANGELOG wasn't modified.
Please include an entry if the PR includes user-facing changes.
You can find it at CHANGELOG.md.

Generated by 🚫 Danger

@github-actions

Copy link
Copy Markdown

Public Interface

🚀 No changes affecting the public interface.

memberUpdater.partialUpdate(
userId: userId,
in: cid,
updates: MemberUpdatePayload(extraData: extraData),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I did not touch public interfaces at the moment because there are 2 different arguments instead of single argument with request type - set and unset as arguments. Alternative is to add new functions which allow creating the UpdateMemberPartialRequest on the public layer. I think it is OK to keep this as is.

func partialUpdate(
        extraData: [String: RawJSON]?,
        unsetProperties: [String]? = nil,
        completion: (@MainActor (Result<ChatChannelMember, Error>) -> Void)? = nil
    ) {

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberUpdater_Mock.swift (2)

80-82: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add @Sendable to the overridden completion parameter.

As per coding guidelines, maintain Swift 6.0 strict-concurrency compatibility. The base class signature requires the completion closure to be @Sendable, and omitting it here creates a strict-concurrency mismatch.

♻️ Proposed refactor
         request: UpdateMemberPartialRequest,
-        completion: `@escaping` ((Result<ChatChannelMember, Error>) -> Void)
+        completion: `@escaping` (`@Sendable` (Result<ChatChannelMember, Error>) -> Void)
     ) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberUpdater_Mock.swift`
around lines 80 - 82, The overridden completion parameter in
ChannelMemberUpdater_Mock’s update-member method must match the base class’s
strict-concurrency signature. Add `@Sendable` to the Result<ChatChannelMember,
Error> completion closure while preserving the existing method behavior.

Source: Coding guidelines


25-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add @Sendable to the completion closure to match the base class.

As per coding guidelines, maintain Swift 6.0 strict-concurrency compatibility by using Sendable where needed. The ChannelMemberUpdater base class defines the completion closure as @Sendable, so the mock should match it exactly.

♻️ Proposed refactor
     `@Atomic` var partialUpdate_request: UpdateMemberPartialRequest?
-    `@Atomic` var partialUpdate_completion: ((Result<ChatChannelMember, Error>) -> Void)?
+    `@Atomic` var partialUpdate_completion: (`@Sendable` (Result<ChatChannelMember, Error>) -> Void)?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberUpdater_Mock.swift`
around lines 25 - 26, Update the partialUpdate_completion closure property in
the ChannelMemberUpdater mock to include `@Sendable`, matching the completion
closure declaration in the base ChannelMemberUpdater class while preserving its
existing Result type and behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/StreamChat/Database/DTOs/MemberModelDTO.swift`:
- Around line 237-239: Update the member-list persistence flow around saveMember
so it pre-warms and reuses a cache mapping for all response.members, rather than
passing cache: nil for each member. Extend MembersResponse with a mapping
mechanism analogous to ChannelMemberListPayload.getPayloadToModelIdMappings,
then pass the resulting cache into each saveMember call while preserving the
existing compactMapLoggingError behavior.

In `@Sources/StreamChat/Query/Filter.swift`:
- Around line 678-679: Remove the documentation comment immediately above
Filter.toRawJSONDictionary() and leave the method implementation unchanged.

---

Nitpick comments:
In
`@TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberUpdater_Mock.swift`:
- Around line 80-82: The overridden completion parameter in
ChannelMemberUpdater_Mock’s update-member method must match the base class’s
strict-concurrency signature. Add `@Sendable` to the Result<ChatChannelMember,
Error> completion closure while preserving the existing method behavior.
- Around line 25-26: Update the partialUpdate_completion closure property in the
ChannelMemberUpdater mock to include `@Sendable`, matching the completion closure
declaration in the base ChannelMemberUpdater class while preserving its existing
Result type and behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2b7c266e-5a60-4f6a-9d5d-efb25865aa47

📥 Commits

Reviewing files that changed from the base of the PR and between 1cc5a3f and 63b7d58.

⛔ Files ignored due to path filters (8)
  • Sources/StreamChat/Generated/OpenAPI/APIs/DefaultEndpoints.swift is excluded by !**/generated/**
  • Sources/StreamChat/Generated/OpenAPI/models/ChannelMemberRequest.swift is excluded by !**/generated/**
  • Sources/StreamChat/Generated/OpenAPI/models/ChannelMemberResponse.swift is excluded by !**/generated/**
  • Sources/StreamChat/Generated/OpenAPI/models/MembersResponse.swift is excluded by !**/generated/**
  • Sources/StreamChat/Generated/OpenAPI/models/QueryMembersPayload.swift is excluded by !**/generated/**
  • Sources/StreamChat/Generated/OpenAPI/models/SortParamRequest.swift is excluded by !**/generated/**
  • Sources/StreamChat/Generated/OpenAPI/models/UpdateMemberPartialRequest.swift is excluded by !**/generated/**
  • Sources/StreamChat/Generated/OpenAPI/models/UpdateMemberPartialResponse.swift is excluded by !**/generated/**
📒 Files selected for processing (31)
  • Scripts/openapi_generate.sh
  • Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift
  • Sources/StreamChat/APIClient/Endpoints/MemberEndpoints.swift
  • Sources/StreamChat/APIClient/Endpoints/Payloads/MemberUpdatePayload.swift
  • Sources/StreamChat/APIClient/Endpoints/Requests/QueryPollsRequestBody.swift
  • Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift
  • Sources/StreamChat/Controllers/MemberController/MemberController.swift
  • Sources/StreamChat/Database/DTOs/MemberModelDTO.swift
  • Sources/StreamChat/Database/DTOs/UserDTO.swift
  • Sources/StreamChat/Database/DatabaseSession.swift
  • Sources/StreamChat/Models/Member.swift
  • Sources/StreamChat/Query/ChannelMemberListQuery.swift
  • Sources/StreamChat/Query/Filter.swift
  • Sources/StreamChat/Query/Sorting/ChannelMemberListSortingKey.swift
  • Sources/StreamChat/Workers/ChannelMemberListUpdater.swift
  • Sources/StreamChat/Workers/ChannelMemberUpdater.swift
  • TestTools/StreamChatTestTools/Assertions/AssertJSONEqual.swift
  • TestTools/StreamChatTestTools/Extensions/EndpoinPath+Equatable.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberUpdater_Mock.swift
  • TestTools/StreamChatTestTools/TestData/DummyData/ChannelMemberResponse+Dummy.swift
  • TestTools/StreamChatTestTools/TestData/FilterTestScope.swift
  • Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift
  • Tests/StreamChatTests/APIClient/Endpoints/MemberEndpoints_Tests.swift
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • Tests/StreamChatTests/Controllers/MemberController/MemberController_Tests.swift
  • Tests/StreamChatTests/Query/Filter_Tests.swift
  • Tests/StreamChatTests/StateLayer/Chat_Tests.swift
  • Tests/StreamChatTests/StateLayer/MemberList_Tests.swift
  • Tests/StreamChatTests/Workers/ChannelMemberListUpdater_Tests.swift
  • Tests/StreamChatTests/Workers/ChannelMemberUpdater_Tests.swift
💤 Files with no reviewable changes (3)
  • Sources/StreamChat/APIClient/Endpoints/Payloads/MemberUpdatePayload.swift
  • Sources/StreamChat/APIClient/Endpoints/MemberEndpoints.swift
  • Sources/StreamChat/APIClient/Endpoints/Requests/QueryPollsRequestBody.swift

Comment on lines +237 to +239
return response.members.compactMapLoggingError {
try saveMember(response: $0, channelId: channelId, query: query, cache: nil)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Pre-warm the cache to prevent N+1 queries.

Passing cache: nil bypasses Core Data pre-warming. When saving a paginated list of members, this will cause a separate fetch query for every individual member, leading to an N+1 performance regression.

Consider extending MembersResponse with a cache mapping mechanism analogous to getPayloadToModelIdMappings (used by ChannelMemberListPayload) to restore this database optimization.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/StreamChat/Database/DTOs/MemberModelDTO.swift` around lines 237 -
239, Update the member-list persistence flow around saveMember so it pre-warms
and reuses a cache mapping for all response.members, rather than passing cache:
nil for each member. Extend MembersResponse with a mapping mechanism analogous
to ChannelMemberListPayload.getPayloadToModelIdMappings, then pass the resulting
cache into each saveMember call while preserving the existing
compactMapLoggingError behavior.

Comment on lines +678 to +679
/// Converts the filter to a `RawJSON` dictionary for OpenAPI request bodies.
func toRawJSONDictionary() -> [String: RawJSON] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove doc comment from internal method.

As per coding guidelines, do not add /// doc comments to internal, private, or test code. toRawJSONDictionary() is an internal method, so a standard comment should be used instead.

♻️ Proposed fix
-    /// Converts the filter to a `RawJSON` dictionary for OpenAPI request bodies.
+    // Converts the filter to a `RawJSON` dictionary for OpenAPI request bodies.
     func toRawJSONDictionary() -> [String: RawJSON] {
📝 Committable suggestion

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

Suggested change
/// Converts the filter to a `RawJSON` dictionary for OpenAPI request bodies.
func toRawJSONDictionary() -> [String: RawJSON] {
// Converts the filter to a `RawJSON` dictionary for OpenAPI request bodies.
func toRawJSONDictionary() -> [String: RawJSON] {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/StreamChat/Query/Filter.swift` around lines 678 - 679, Remove the
documentation comment immediately above Filter.toRawJSONDictionary() and leave
the method implementation unchanged.

Source: Coding guidelines

Comment on lines +156 to +161
func saveMember(
response: ChannelMemberResponse,
channelId: ChannelId,
query: ChannelMemberListQuery?,
cache: PreWarmedCache?
) throws -> MemberDTO {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Decided to add additional save methods instead of converting new ChannelMemberResponse to MemberPayload just for reusing the save. At the end of the migration, saveMember with MemberPayload argument would get deleted. Felt cleaner to keep them separate. WDYT

@Stream-SDK-Bot

Copy link
Copy Markdown
Collaborator

SDK Size

title develop branch diff status
StreamChat 7.15 MB 7.19 MB +45 KB 🟢
StreamChatUI 3.62 MB 3.62 MB 0 KB 🟢
StreamChatCommonUI 0.94 MB 0.94 MB 0 KB 🟢


import Foundation

final class ChannelMemberResponse: Sendable, Codable, JSONEncodable {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I should consider making this public instead of ChatChannelMember public type. Is it possible?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not yet possible, these are too different at the moment. OpenAPI has UserResponse, not ChatUser. So ChatUser should be replaced first with OpenAPI type.

}

/// Converts the filter to a `RawJSON` dictionary for OpenAPI request bodies.
func toRawJSONDictionary() -> [String: RawJSON] {

@laevandus laevandus Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is what OpenAPI models require, filter is fed in with [String: RawJSON]

let filterConditions: [String: RawJSON]

Alternative is going into generator and making it to accept Encodable, but then it has implications to other SDKs as well (Feeds) or it should be configurable on the generator level. WDYT?

This is what currently is used for encoding filters. Now we are moving toward using [String: RawJSON] which itself is encodable.

// pre this PR
extension Filter: Codable {
    public func encode(to encoder: Encoder) throws {
        if self.operator.isGroupOperator {
            // Filters with group operators are encoded in the following form:
            //  { $<operator>: [ <filter 1>, <filter 2> ] }
            try [self.operator: AnyEncodable(value)].encode(to: encoder)
            return

        } else if let key = self.key {
            // Normal filters are encoded in the following form:
            //  { key: { $<operator>: <value> } }
            try [key: [self.operator: AnyEncodable(value)]].encode(to: encoder)
            return

        } else {
            throw EncodingError.invalidValue(
                self,
                EncodingError.Context(
                    codingPath: encoder.codingPath,
                    debugDescription: "Filter must have the `key` value when the operator is not a group operator."
                )
            )
        }
    }

@Stream-SDK-Bot

Copy link
Copy Markdown
Collaborator

StreamChat XCSize

Object Diff (bytes)
ChannelMemberResponse.o +11597
MemberEndpoints.o -8937
QueryMembersPayload.o +7163
ChannelMemberRequest.o +6119
MembersResponse.o +5719
Show 26 more objects
Object Diff (bytes)
SortParamRequest.o +5538
UpdateMemberPartialRequest.o +5510
UpdateMemberPartialResponse.o +5480
MemberUpdatePayload.o -4235
MemberModelDTO.o +3286
Filter.o +2639
ChannelMemberListQuery.o +2638
UserDTO.o +2116
AttachmentDownloader.o +1490
StreamCDNStorage.o +1296
AnyAttachmentPayload.o +924
ThreadDTO.o -828
UnknownChannelEvent.o +468
DefaultEndpoints.o -456
ChannelMemberUpdater.o -394
RequestEncoder.o -348
AttachmentTypes.o +308
AudioAnalysisEngine.o +234
Member.o +204
DatabaseSession.o +188
ChannelQuery.o +164
MessageDTO.o -108
APIClient.o -76
AppSettings+Extensions.o +68
ChannelListPayload.o +64
ChannelMemberListUpdater.o -54

Comment on lines +716 to +719
case let value as ChannelId:
return .string(value.rawValue)
case let value as ChannelType:
return .string(value.rawValue)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These should probably be RawRepresentable

@laevandus
laevandus marked this pull request as draft July 16, 2026 13:14
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants