Skip to content

Commit 6f3a443

Browse files
authored
Merge pull request #540 from stytchauth/search-member-fixes
Fix the organization search member function
2 parents 34123cf + 07a4e10 commit 6f3a443

File tree

5 files changed

+497
-77
lines changed

5 files changed

+497
-77
lines changed

Sources/StytchCore/StytchB2BClient/StytchB2BClient+Organizations/StytchB2BClient+Organizations+SearchParameters.swift

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import Foundation
33

44
public extension StytchB2BClient.Organizations {
55
struct SearchParameters: Codable, Sendable {
6-
let query: SearchQuery
6+
let query: SearchQuery?
77
let cursor: String?
8-
let limit: String?
8+
let limit: Int?
99

1010
/// * Data class used for wrapping the parameters necessary to search members
1111
/// - Parameters:
@@ -23,9 +23,9 @@ public extension StytchB2BClient.Organizations {
2323
/// The default limit is 100. A maximum of 1000 results can be returned by a single search request.
2424
/// If the total size of your result set is greater than one page size, you must paginate the response. See the cursor field.
2525
public init(
26-
query: SearchQuery,
26+
query: SearchQuery? = nil,
2727
cursor: String? = nil,
28-
limit: String? = nil
28+
limit: Int? = nil
2929
) {
3030
self.query = query
3131
self.cursor = cursor
@@ -49,7 +49,8 @@ public extension StytchB2BClient.Organizations.SearchParameters {
4949
/// - searchOperands: An array of SearchQueryOperand(s) created via 'SearchParameters.searchQueryOperand(...)'
5050
public init(searchOperator: SearchOperator, searchOperands: [any SearchQueryOperand]) {
5151
self.searchOperator = searchOperator
52-
searchOperandsJSON = JSON(arrayLiteral: searchOperands.map(\.json))
52+
let searchOperandsJSONArray = searchOperands.map(\.json)
53+
searchOperandsJSON = JSON(searchOperandsJSONArray)
5354
}
5455
}
5556
}
@@ -80,11 +81,8 @@ public extension StytchB2BClient.Organizations.SearchParameters {
8081
let filterValue: [String]
8182

8283
public var filterValueJSON: JSON {
83-
var filterValueJSON = [JSON]()
84-
for string in filterValue {
85-
filterValueJSON.append(JSON(stringLiteral: string))
86-
}
87-
return JSON(arrayLiteral: filterValueJSON)
84+
let filterValues = filterValue.map { JSON(stringLiteral: $0) }
85+
return JSON(filterValues)
8886
}
8987

9088
init(filterName: String, filterValue: [String]) {
@@ -132,7 +130,7 @@ public extension StytchB2BClient.Organizations.SearchParameters {
132130
case memberIsBreakglass = "member_is_breakglass"
133131
case statuses
134132
case memberMfaPhoneNumbers = "member_mfa_phone_numbers"
135-
case memberMfaPhoneNumbeFuzzy = "member_mfa_phone_number_fuzzy"
133+
case memberMfaPhoneNumberFuzzy = "member_mfa_phone_number_fuzzy"
136134
case memberPasswordExists = "member_password_exists"
137135
case memberRoles = "member_roles"
138136

@@ -141,7 +139,7 @@ public extension StytchB2BClient.Organizations.SearchParameters {
141139
}
142140

143141
var isSearchQueryOperandString: Bool {
144-
self == .memberEmailFuzzy || self == .memberMfaPhoneNumbeFuzzy
142+
self == .memberEmailFuzzy || self == .memberMfaPhoneNumberFuzzy
145143
}
146144

147145
var isSearchQueryOperandBool: Bool {
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import Combine
2+
import StytchCore
3+
import UIKit
4+
5+
final class OrganizationMemberSearchViewController: UIViewController {
6+
private let stackView = UIStackView.stytchB2BStackView()
7+
8+
private lazy var searchAllMembersButton: UIButton = .init(title: "Search all members", primaryAction: .init { [weak self] _ in
9+
self?.searchAllMembers()
10+
})
11+
12+
private lazy var searchBySingleEmailButton: UIButton = .init(title: "Search by single email", primaryAction: .init { [weak self] _ in
13+
self?.searchBySingleEmail()
14+
})
15+
private lazy var searchByMultipleEmailsButton: UIButton = .init(title: "Search by multiple emails", primaryAction: .init { [weak self] _ in
16+
self?.searchByMultipleEmails()
17+
})
18+
private lazy var searchByEmailFuzzyButton: UIButton = .init(title: "Search by email fuzzy", primaryAction: .init { [weak self] _ in
19+
self?.searchByEmailFuzzy()
20+
})
21+
private lazy var searchByBreakglassTrueButton: UIButton = .init(title: "Search breakglass true", primaryAction: .init { [weak self] _ in
22+
self?.searchByBreakglassTrue()
23+
})
24+
private lazy var searchByPasswordExistsFalseButton: UIButton = .init(title: "Search password exists false", primaryAction: .init { [weak self] _ in
25+
self?.searchByPasswordExistsFalse()
26+
})
27+
private lazy var searchByStatusesButton: UIButton = .init(title: "Search by statuses", primaryAction: .init { [weak self] _ in
28+
self?.searchByStatuses()
29+
})
30+
private lazy var searchByRolesButton: UIButton = .init(title: "Search by roles", primaryAction: .init { [weak self] _ in
31+
self?.searchByRoles()
32+
})
33+
private lazy var searchAndEmailsAndPasswordButton: UIButton = .init(title: "Search AND emails and password", primaryAction: .init { [weak self] _ in
34+
self?.searchAndEmailsAndPassword()
35+
})
36+
private lazy var searchOrFuzzyEmailOrFuzzyPhoneButton: UIButton = .init(title: "Search OR fuzzy email or fuzzy phone", primaryAction: .init { [weak self] _ in
37+
self?.searchOrFuzzyEmailOrFuzzyPhone()
38+
})
39+
private lazy var searchWithPaginationSmallLimitButton: UIButton = .init(title: "Search with small limit for pagination", primaryAction: .init { [weak self] _ in
40+
self?.searchWithPaginationSmallLimit()
41+
})
42+
43+
override func viewDidLoad() {
44+
super.viewDidLoad()
45+
46+
title = "Organization Member Search"
47+
view.backgroundColor = .systemBackground
48+
49+
view.addSubview(stackView)
50+
stackView.translatesAutoresizingMaskIntoConstraints = false
51+
NSLayoutConstraint.activate([
52+
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
53+
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
54+
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
55+
])
56+
57+
// Buttons
58+
stackView.addArrangedSubview(searchAllMembersButton)
59+
stackView.addArrangedSubview(searchBySingleEmailButton)
60+
stackView.addArrangedSubview(searchByMultipleEmailsButton)
61+
stackView.addArrangedSubview(searchByEmailFuzzyButton)
62+
stackView.addArrangedSubview(searchByBreakglassTrueButton)
63+
stackView.addArrangedSubview(searchByPasswordExistsFalseButton)
64+
stackView.addArrangedSubview(searchByStatusesButton)
65+
stackView.addArrangedSubview(searchByRolesButton)
66+
stackView.addArrangedSubview(searchAndEmailsAndPasswordButton)
67+
stackView.addArrangedSubview(searchOrFuzzyEmailOrFuzzyPhoneButton)
68+
stackView.addArrangedSubview(searchWithPaginationSmallLimitButton)
69+
}
70+
71+
// MARK: - Demo search builders
72+
73+
private func searchAllMembers() {
74+
Task {
75+
let operands: [any SearchQueryOperand] = []
76+
let queryOperator: StytchB2BClient.Organizations.SearchParameters.SearchOperator = .AND
77+
await performSearch(operands: operands, operator: queryOperator, cursor: nil, limit: nil, label: "all members")
78+
}
79+
}
80+
81+
private func searchBySingleEmail() {
82+
Task {
83+
let operands = makeOperands([
84+
(.memberEmails, ["foo@stytch.com"]),
85+
])
86+
let queryOperator: StytchB2BClient.Organizations.SearchParameters.SearchOperator = .AND
87+
await performSearch(operands: operands, operator: queryOperator, cursor: nil, limit: nil, label: "single email")
88+
}
89+
}
90+
91+
private func searchByMultipleEmails() {
92+
Task {
93+
let operands = makeOperands([
94+
(.memberEmails, ["alpha@example.com", "beta@example.com"]),
95+
])
96+
let queryOperator: StytchB2BClient.Organizations.SearchParameters.SearchOperator = .OR
97+
await performSearch(operands: operands, operator: queryOperator, cursor: nil, limit: nil, label: "multiple emails OR")
98+
}
99+
}
100+
101+
private func searchByEmailFuzzy() {
102+
Task {
103+
let operands = makeOperands([
104+
(.memberEmailFuzzy, "ali"),
105+
])
106+
let queryOperator: StytchB2BClient.Organizations.SearchParameters.SearchOperator = .AND
107+
await performSearch(operands: operands, operator: queryOperator, cursor: nil, limit: nil, label: "email fuzzy")
108+
}
109+
}
110+
111+
private func searchByBreakglassTrue() {
112+
Task {
113+
let operands = makeOperands([
114+
(.memberIsBreakglass, true),
115+
])
116+
let queryOperator: StytchB2BClient.Organizations.SearchParameters.SearchOperator = .AND
117+
await performSearch(operands: operands, operator: queryOperator, cursor: nil, limit: nil, label: "breakglass true")
118+
}
119+
}
120+
121+
private func searchByPasswordExistsFalse() {
122+
Task {
123+
let operands = makeOperands([
124+
(.memberPasswordExists, false),
125+
])
126+
let queryOperator: StytchB2BClient.Organizations.SearchParameters.SearchOperator = .AND
127+
await performSearch(operands: operands, operator: queryOperator, cursor: nil, limit: nil, label: "password exists false")
128+
}
129+
}
130+
131+
private func searchByStatuses() {
132+
Task {
133+
let operands = makeOperands([
134+
(.statuses, ["active", "invited"]),
135+
])
136+
let queryOperator: StytchB2BClient.Organizations.SearchParameters.SearchOperator = .OR
137+
await performSearch(operands: operands, operator: queryOperator, cursor: nil, limit: nil, label: "statuses")
138+
}
139+
}
140+
141+
private func searchByRoles() {
142+
Task {
143+
let operands = makeOperands([
144+
(.memberRoles, ["admin", "member"]),
145+
])
146+
let queryOperator: StytchB2BClient.Organizations.SearchParameters.SearchOperator = .OR
147+
await performSearch(operands: operands, operator: queryOperator, cursor: nil, limit: nil, label: "roles")
148+
}
149+
}
150+
151+
private func searchAndEmailsAndPassword() {
152+
Task {
153+
let operands = makeOperands([
154+
(.memberEmails, ["alpha@example.com"]),
155+
(.memberPasswordExists, true),
156+
])
157+
let queryOperator: StytchB2BClient.Organizations.SearchParameters.SearchOperator = .AND
158+
await performSearch(operands: operands, operator: queryOperator, cursor: nil, limit: nil, label: "AND emails and password")
159+
}
160+
}
161+
162+
private func searchOrFuzzyEmailOrFuzzyPhone() {
163+
Task {
164+
let operands = makeOperands([
165+
(.memberEmailFuzzy, "ali"),
166+
(.memberMfaPhoneNumberFuzzy, "401"),
167+
])
168+
let queryOperator: StytchB2BClient.Organizations.SearchParameters.SearchOperator = .OR
169+
await performSearch(operands: operands, operator: queryOperator, cursor: nil, limit: nil, label: "OR email fuzzy or phone fuzzy")
170+
}
171+
}
172+
173+
private func searchWithPaginationSmallLimit() {
174+
Task {
175+
let operands = makeOperands([
176+
(.memberEmails, ["alpha@example.com", "beta@example.com", "gamma@example.com"]),
177+
])
178+
let queryOperator: StytchB2BClient.Organizations.SearchParameters.SearchOperator = .OR
179+
await performSearch(operands: operands, operator: queryOperator, cursor: nil, limit: 2, label: "pagination with small limit")
180+
}
181+
}
182+
183+
// MARK: - Common helpers
184+
185+
private func performSearch(
186+
operands: [any SearchQueryOperand],
187+
operator searchOperator: StytchB2BClient.Organizations.SearchParameters.SearchOperator,
188+
cursor: String?,
189+
limit: Int?,
190+
label: String
191+
) async {
192+
do {
193+
let query = StytchB2BClient.Organizations.SearchParameters.SearchQuery(
194+
searchOperator: searchOperator,
195+
searchOperands: operands
196+
)
197+
let parameters = StytchB2BClient.Organizations.SearchParameters(
198+
query: query,
199+
cursor: cursor,
200+
limit: limit
201+
)
202+
let response = try await StytchB2BClient.organizations.searchMembers(parameters: parameters)
203+
print("search \(label) members count: \(response.members.count)")
204+
if let nextCursor = response.resultsMetadata.nextCursor {
205+
print("search \(label) next_cursor: \(nextCursor)")
206+
}
207+
presentAlertAndLogMessage(description: "search \(label) success", object: response)
208+
} catch {
209+
presentAlertAndLogMessage(description: "search \(label) error", object: error)
210+
}
211+
}
212+
213+
private func makeOperands(_ inputs: [(StytchB2BClient.Organizations.SearchParameters.SearchQueryOperandFilterNames, Any)]) -> [any SearchQueryOperand] {
214+
var builtOperands = [any SearchQueryOperand]()
215+
for input in inputs {
216+
if let operand = StytchB2BClient.Organizations.SearchParameters.searchQueryOperand(
217+
filterName: input.0,
218+
filterValue: input.1
219+
) {
220+
builtOperands.append(operand)
221+
} else {
222+
print("Invalid operand skipped for filter \(input.0.rawValue)")
223+
}
224+
}
225+
return builtOperands
226+
}
227+
}

Stytch/DemoApps/B2BWorkbench/ViewControllers/AuthMethodControllers/OrganizationViewController.swift

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ final class OrganizationViewController: UIViewController {
2727
})
2828

2929
lazy var searchMembersButton: UIButton = .init(title: "Search Members", primaryAction: .init { [weak self] _ in
30-
self?.searchMembers()
30+
self?.navigationController?.pushViewController(OrganizationMemberSearchViewController(), animated: true)
3131
})
3232

3333
override func viewDidLoad() {
@@ -127,34 +127,4 @@ final class OrganizationViewController: UIViewController {
127127
}
128128
}
129129
}
130-
131-
func searchMembers() {
132-
Task {
133-
do {
134-
var operands = [any SearchQueryOperand]()
135-
if let operand = StytchB2BClient.Organizations.SearchParameters.searchQueryOperand(
136-
filterName: .memberEmails,
137-
filterValue: ["foo@stytch.com"]
138-
) {
139-
operands.append(operand)
140-
}
141-
142-
let query = StytchB2BClient.Organizations.SearchParameters.SearchQuery(
143-
searchOperator: .AND,
144-
searchOperands: operands
145-
)
146-
147-
let parameters = StytchB2BClient.Organizations.SearchParameters(
148-
query: query,
149-
cursor: nil,
150-
limit: nil
151-
)
152-
153-
let response = try await StytchB2BClient.organizations.searchMembers(parameters: parameters)
154-
presentAlertAndLogMessage(description: "search organization member success!", object: response)
155-
} catch {
156-
presentAlertAndLogMessage(description: "search organization members error", object: error)
157-
}
158-
}
159-
}
160130
}

Stytch/Stytch.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
74BCCBDD2D2C2A97001A1C0A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 74BCCBD32D2C2A97001A1C0A /* Main.storyboard */; };
6969
74BCCBDF2D2C2DE7001A1C0A /* StytchCore in Frameworks */ = {isa = PBXBuildFile; productRef = 74BCCBDE2D2C2DE7001A1C0A /* StytchCore */; };
7070
74C427992C9A25E700EFA1A1 /* SCIMViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C427982C9A25E700EFA1A1 /* SCIMViewController.swift */; };
71+
74DC4C7A2E6F5571008D0152 /* OrganizationMemberSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74DC4C792E6F5570008D0152 /* OrganizationMemberSearchViewController.swift */; };
7172
74DD38CB2C2A2ABA00BEB0DD /* OAuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74DD38CA2C2A2ABA00BEB0DD /* OAuthViewController.swift */; };
7273
74F0088D2C24C05100E0F863 /* RecoveryCodesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F0088C2C24C05000E0F863 /* RecoveryCodesViewController.swift */; };
7374
74F3702A2C07B7F400AED9C9 /* OrganizationMemberViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F370292C07B7F400AED9C9 /* OrganizationMemberViewController.swift */; };
@@ -152,6 +153,7 @@
152153
74BF2AFB2CF4EF7200798DCE /* Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigation.swift; sourceTree = "<group>"; };
153154
74C427982C9A25E700EFA1A1 /* SCIMViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCIMViewController.swift; sourceTree = "<group>"; };
154155
74D1157E2BFBDFFD002EAB79 /* B2BWorkbench-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "B2BWorkbench-Bridging-Header.h"; sourceTree = "<group>"; };
156+
74DC4C792E6F5570008D0152 /* OrganizationMemberSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizationMemberSearchViewController.swift; sourceTree = "<group>"; };
155157
74DD38CA2C2A2ABA00BEB0DD /* OAuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthViewController.swift; sourceTree = "<group>"; };
156158
74F0088C2C24C05000E0F863 /* RecoveryCodesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryCodesViewController.swift; sourceTree = "<group>"; };
157159
74F370292C07B7F400AED9C9 /* OrganizationMemberViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizationMemberViewController.swift; sourceTree = "<group>"; };
@@ -236,6 +238,7 @@
236238
2254779729A05D08003DF229 /* MemberViewController.swift */,
237239
74DD38CA2C2A2ABA00BEB0DD /* OAuthViewController.swift */,
238240
2254779929A05D37003DF229 /* OrganizationViewController.swift */,
241+
74DC4C792E6F5570008D0152 /* OrganizationMemberSearchViewController.swift */,
239242
74F370292C07B7F400AED9C9 /* OrganizationMemberViewController.swift */,
240243
74A2D1902C21FDF8007F8F20 /* OTPViewController.swift */,
241244
224E6F2429E8CAEB0031D37A /* PasswordsViewController.swift */,
@@ -654,6 +657,7 @@
654657
2254779A29A05D37003DF229 /* OrganizationViewController.swift in Sources */,
655658
2254779C29A05D4E003DF229 /* SessionsViewController.swift in Sources */,
656659
22ABFABB29F7690100927518 /* SSOViewController.swift in Sources */,
660+
74DC4C7A2E6F5571008D0152 /* OrganizationMemberSearchViewController.swift in Sources */,
657661
74A2D1912C21FDF8007F8F20 /* OTPViewController.swift in Sources */,
658662
22ABFAB929F7628E00927518 /* DiscoveryViewController.swift in Sources */,
659663
2254779829A05D08003DF229 /* MemberViewController.swift in Sources */,

0 commit comments

Comments
 (0)