Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions Examples/MistDemo/Sources/MistDemoKit/Resources/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ <h3>Discover <span class="endpoint-label">users/discover (POST) <em>(CloudKit JS
<div class="panel-row">
<textarea id="users-discover-emails" placeholder="emails, comma-separated"></textarea>
</div>
<div class="panel-row">
<textarea id="users-discover-phone-numbers" placeholder="phone numbers, comma-separated"></textarea>
</div>
<div class="panel-row">
<textarea id="users-discover-record-names" placeholder="user record names, comma-separated"></textarea>
<button id="users-discover-btn" type="button">Discover</button>
Expand Down
19 changes: 14 additions & 5 deletions Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// users/caller · users/discover panel handlers. The deprecated
// users/lookup/email and users/lookup/id primitives are not exposed —
// users/discover is Apple's supported replacement and handles both email
// and record-name lookups (phone-number support tracked in #398).
// users/discover is Apple's supported replacement and handles email,
// phone-number, and record-name lookups.

const usersCallerStatus = document.getElementById('users-caller-status');
const usersCallerRaw = document.getElementById('users-caller-raw');
Expand All @@ -24,9 +24,10 @@ document.getElementById('users-caller-btn').addEventListener('click', async () =

document.getElementById('users-discover-btn').addEventListener('click', async () => {
const emails = csv(document.getElementById('users-discover-emails').value);
const phoneNumbers = csv(document.getElementById('users-discover-phone-numbers').value);
const userRecordNames = csv(document.getElementById('users-discover-record-names').value);
if (emails.length === 0 && userRecordNames.length === 0) {
setStatus(usersDiscoverStatus, 'Provide at least one email or record name.', 'error');
if (emails.length === 0 && phoneNumbers.length === 0 && userRecordNames.length === 0) {
setStatus(usersDiscoverStatus, 'Provide at least one email, phone number, or record name.', 'error');
return;
}
await runPanelOperation({
Expand All @@ -35,7 +36,7 @@ document.getElementById('users-discover-btn').addEventListener('click', async ()
label: 'Discover users',
fn: async () => {
if (currentMode === 'mistkit') {
return await postJSON('/api/users/discover', { emails, userRecordNames });
return await postJSON('/api/users/discover', { emails, phoneNumbers, userRecordNames });
}
// CloudKit JS exposes per-item primitives — loop and aggregate
// to match the REST endpoint's batch shape.
Expand All @@ -48,6 +49,14 @@ document.getElementById('users-discover-btn').addEventListener('click', async ()
results.push({ email, error: error.message });
}
}
for (const phoneNumber of phoneNumbers) {
try {
const identity = await ckJsContainer().discoverUserIdentityWithPhoneNumber(phoneNumber);
results.push({ phoneNumber, identity });
} catch (error) {
results.push({ phoneNumber, error: error.message });
}
}
for (const recordName of userRecordNames) {
try {
const identity = await ckJsContainer().discoverUserIdentityWithUserRecordName(recordName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ extension CloudKitService {

internal func webDiscoverUsers(
emails: [String],
phoneNumbers: [String],
userRecordNames: [String]
) async throws -> [UserIdentity] {
let lookupInfos =
emails.map { UserIdentityLookupInfo(emailAddress: $0) }
+ phoneNumbers.map { UserIdentityLookupInfo(phoneNumber: $0) }
+ userRecordNames.map { UserIdentityLookupInfo(userRecordName: $0) }
return try await discoverUserIdentities(lookupInfos: lookupInfos)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ internal protocol WebBackend: Sendable {

func webDiscoverUsers(
emails: [String],
phoneNumbers: [String],
userRecordNames: [String]
) async throws -> [UserIdentity]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,28 @@ internal import Foundation
// wrapper (`discoverUserIdentities`) operates on the public database with
// web-auth credentials regardless of the request's selected database.
extension WebRequests {
/// `POST /api/users/discover` — discover user identities by email address
/// and/or user record name. Either list may be omitted; an absent key
/// decodes to an empty array.
/// `POST /api/users/discover` — discover user identities by email address,
/// phone number, and/or user record name. Any list may be omitted; an
/// absent key decodes to an empty array.
internal struct DiscoverUsers: Decodable {
private enum CodingKeys: String, CodingKey {
case emails
case phoneNumbers
case userRecordNames
}

internal let emails: [String]
internal let phoneNumbers: [String]
internal let userRecordNames: [String]

internal init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.emails =
try container.decodeIfPresent([String].self, forKey: .emails) ?? []
self.phoneNumbers =
try container.decodeIfPresent(
[String].self, forKey: .phoneNumbers
) ?? []
self.userRecordNames =
try container.decodeIfPresent(
[String].self, forKey: .userRecordNames
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
/// operate on the public database with web-auth credentials, so neither
/// carries a `database` selector. The deprecated `lookup/email` and
/// `lookup/id` primitives are intentionally not exposed — `discover` is
/// Apple's supported replacement and handles email + record-name lookups.
/// Apple's supported replacement and handles email, phone-number, and
/// record-name lookups.
internal func addUsersEndpoints(
api: RouterGroup<BasicRequestContext>
) {
Expand Down Expand Up @@ -66,7 +67,7 @@
}

/// `POST /api/users/discover` — discover user identities by email
/// address and/or user record name.
/// address, phone number, and/or user record name.
private func addUsersDiscoverEndpoint(
api: RouterGroup<BasicRequestContext>
) {
Expand All @@ -83,6 +84,7 @@
let backend = try backendFactory.make(token)
let users = try await backend.webDiscoverUsers(
emails: body.emails,
phoneNumbers: body.phoneNumbers,
userRecordNames: body.userRecordNames
)
return try WebJSON.encoder().encode(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
/// Captured arguments from the most recent `webDiscoverUsers` call.
internal struct DiscoverUsersCall: Sendable {
internal let emails: [String]
internal let phoneNumbers: [String]
internal let userRecordNames: [String]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,23 @@

internal func webDiscoverUsers(
emails: [String],
phoneNumbers: [String],
userRecordNames: [String]
) async throws -> [UserIdentity] {
lastDiscoverUsers = DiscoverUsersCall(
emails: emails,
phoneNumbers: phoneNumbers,
userRecordNames: userRecordNames
)
try consumePendingError()
return emails.map { email in
UserIdentity(lookupInfo: UserIdentityLookupInfo(emailAddress: email))
}
+ phoneNumbers.map { phoneNumber in
UserIdentity(
lookupInfo: UserIdentityLookupInfo(phoneNumber: phoneNumber)
)
}
+ userRecordNames.map { name in
UserIdentity(userRecordName: .recordName(name))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,17 @@
}

@Test(
"POST /api/users/discover forwards emails and record names to the backend"
"""
POST /api/users/discover forwards emails, phone numbers, \
and record names to the backend
"""
)
internal func usersDiscoverForwards() async throws {
let fixture = Self.makeFixture(authenticated: true)
let app = Application(router: try fixture.server.makeRouter())
let jsonBody = """
{"emails":["a@example.com","b@example.com"],\
"phoneNumbers":["+15555550123"],\
"userRecordNames":["_user-1"]}
"""

Expand All @@ -113,12 +117,13 @@
UsersPayload.self,
from: Data(response.body.readableBytesView)
)
#expect(payload.users.count == 3)
#expect(payload.users.count == 4)
}
}

let captured = await fixture.backend.lastDiscoverUsers
#expect(captured?.emails == ["a@example.com", "b@example.com"])
#expect(captured?.phoneNumbers == ["+15555550123"])
#expect(captured?.userRecordNames == ["_user-1"])
}

Expand Down
Loading