Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Subscription price schedules no longer drop manual prices after the first 50** — `subscription-price-schedule get` (and the read-back after `set-batch`) fetched `/v1/subscriptions/{id}/prices` without a `limit`, so only Apple's default first page came back and the other territories were filled with the first price's equalizations, showing wrong prices where custom per-territory prices were set. It now requests `limit=200` and follows the pagination cursor.
- **The other per-territory price lists no longer stop at 50** — the same truncation affected `iap-price-schedule get` (manual prices), `iap-offer-codes prices list`, `subscription-offer-codes prices list`, `subscription-promotional-offers prices list` and `win-back-offers prices list`. They called Apple without a `limit` and read one page, so any territory past Apple's default 50 was missing. They now request `limit=200` and follow every page through a shared `APIClient.requestAllPages(_:nextCursor:)` helper, which `subscription-price-schedule get` uses too.
- **Offer price lists now show each price's territory and price point** — `iap-offer-codes prices list`, `subscription-offer-codes prices list`, `subscription-promotional-offers prices list` and `win-back-offers prices list` returned entries with only `id` and the offer ID, because Apple sends a price's territory and price point only when the request asks for them with `include=`. The requests now include both, so `territory` and the price-point ID are filled in, over CLI and REST.
- **More list commands no longer stop at Apple's first page** — `reviews list`, `devices list`, `bundle-ids list`, `profiles list` (with and without `--bundle-id-id`), `users list`, `versions list`, `builds uploads list`, `xcode-cloud builds list` and `diagnostics list` read a single page with no `limit`, so anything past Apple's default page size (50 for most endpoints, 20 for bundle IDs) was silently dropped. They now request `limit=200` and follow every page through `APIClient.requestAllPages`. On a real account, `builds uploads list` went from 50 to 122 entries and `bundle-ids list` from 20 to 85.

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,11 @@ public struct SDKBuildUploadRepository: BuildUploadRepository, @unchecked Sendab
}

public func listBuildUploads(appId: String) async throws -> [Domain.BuildUpload] {
let request = APIEndpoint.v1.apps.id(appId).buildUploads.get()
let response = try await client.request(request)
return response.data.map { mapBuildUpload($0, appId: appId) }
let pages = try await client.requestAllPages(
APIEndpoint.v1.apps.id(appId).buildUploads.get(parameters: .init(limit: 200)),
nextCursor: { $0.meta?.paging.nextCursor }
)
return pages.flatMap(\.data).map { mapBuildUpload($0, appId: appId) }
}

public func getBuildUpload(id: String) async throws -> Domain.BuildUpload {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ public struct SDKDiagnosticsRepository: DiagnosticsRepository, @unchecked Sendab

public func listSignatures(buildId: String, diagnosticType: DiagnosticType?) async throws -> [DiagnosticSignatureInfo] {
let filterType = diagnosticType.flatMap { mapDiagnosticTypeFilter($0) }.map { [$0] }
let request = APIEndpoint.v1.builds.id(buildId).diagnosticSignatures.get(parameters: .init(
filterDiagnosticType: filterType
))
let response = try await client.request(request)
return response.data.map { mapSignature($0, buildId: buildId) }
let pages = try await client.requestAllPages(
APIEndpoint.v1.builds.id(buildId).diagnosticSignatures.get(parameters: .init(
filterDiagnosticType: filterType,
limit: 200
)),
nextCursor: { $0.meta?.paging.nextCursor }
)
return pages.flatMap(\.data).map { mapSignature($0, buildId: buildId) }
}

public func listLogs(signatureId: String) async throws -> [DiagnosticLogEntry] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ public struct SDKCustomerReviewRepository: CustomerReviewRepository, @unchecked
}

public func listReviews(appId: String) async throws -> [Domain.CustomerReview] {
let request = APIEndpoint.v1.apps.id(appId).customerReviews.get(
parameters: .init(sort: [.minuscreatedDate])
let pages = try await client.requestAllPages(
APIEndpoint.v1.apps.id(appId).customerReviews.get(
parameters: .init(sort: [.minuscreatedDate], limit: 200)
),
nextCursor: { $0.meta?.paging.nextCursor }
)
let response = try await client.request(request)
return response.data.map { mapReview($0, appId: appId) }
return pages.flatMap(\.data).map { mapReview($0, appId: appId) }
}

public func getReview(reviewId: String) async throws -> Domain.CustomerReview {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ public struct SDKVersionRepository: VersionRepository, @unchecked Sendable {
}

public func listVersions(appId: String) async throws -> [Domain.AppStoreVersion] {
let request = APIEndpoint.v1.apps.id(appId).appStoreVersions.get()
let response = try await client.request(request)
return response.data.compactMap { mapVersion($0, appId: appId) }
let pages = try await client.requestAllPages(
APIEndpoint.v1.apps.id(appId).appStoreVersions.get(parameters: .init(limit: 200)),
nextCursor: { $0.meta?.paging.nextCursor }
)
return pages.flatMap(\.data).compactMap { mapVersion($0, appId: appId) }
}

public func getVersion(id: String) async throws -> Domain.AppStoreVersion {
Expand Down
15 changes: 9 additions & 6 deletions Sources/Infrastructure/CodeSigning/SDKBundleIDRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ public struct SDKBundleIDRepository: BundleIDRepository, @unchecked Sendable {
let filterPlatform = platform.flatMap {
APIEndpoint.V1.BundleIDs.GetParameters.FilterPlatform(rawValue: $0.rawValue)
}
let request = APIEndpoint.v1.bundleIDs.get(parameters: .init(
filterPlatform: filterPlatform.map { [$0] },
filterIdentifier: identifier.map { [$0] }
))
let response = try await client.request(request)
return response.data.map(mapBundleID)
let pages = try await client.requestAllPages(
APIEndpoint.v1.bundleIDs.get(parameters: .init(
filterPlatform: filterPlatform.map { [$0] },
filterIdentifier: identifier.map { [$0] },
limit: 200
)),
nextCursor: { $0.meta?.paging.nextCursor }
)
return pages.flatMap(\.data).map(mapBundleID)
}

public func createBundleID(name: String, identifier: String, platform: Domain.BundleIDPlatform) async throws -> Domain.BundleID {
Expand Down
13 changes: 8 additions & 5 deletions Sources/Infrastructure/CodeSigning/SDKDeviceRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ public struct SDKDeviceRepository: DeviceRepository, @unchecked Sendable {
let filterPlatform = platform.flatMap {
APIEndpoint.V1.Devices.GetParameters.FilterPlatform(rawValue: $0.rawValue)
}
let request = APIEndpoint.v1.devices.get(parameters: .init(
filterPlatform: filterPlatform.map { [$0] }
))
let response = try await client.request(request)
return response.data.map(mapDevice)
let pages = try await client.requestAllPages(
APIEndpoint.v1.devices.get(parameters: .init(
filterPlatform: filterPlatform.map { [$0] },
limit: 200
)),
nextCursor: { $0.meta?.paging.nextCursor }
)
return pages.flatMap(\.data).map(mapDevice)
}

public func registerDevice(name: String, udid: String, platform: Domain.BundleIDPlatform) async throws -> Domain.Device {
Expand Down
20 changes: 12 additions & 8 deletions Sources/Infrastructure/CodeSigning/SDKProfileRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,23 @@ public struct SDKProfileRepository: ProfileRepository, @unchecked Sendable {
public func listProfiles(bundleIdId: String?, profileType: Domain.ProfileType?) async throws -> [Domain.Profile] {
if let bundleIdId {
// Use the bundle ID relationship endpoint for server-side filtering
let response = try await client.request(
APIEndpoint.v1.bundleIDs.id(bundleIdId).profiles.get()
let pages = try await client.requestAllPages(
APIEndpoint.v1.bundleIDs.id(bundleIdId).profiles.get(limit: 200),
nextCursor: { $0.meta?.paging.nextCursor }
)
return response.data.map { mapProfile($0, bundleIdId: bundleIdId) }
return pages.flatMap(\.data).map { mapProfile($0, bundleIdId: bundleIdId) }
} else {
let filterType = profileType.flatMap {
APIEndpoint.V1.Profiles.GetParameters.FilterProfileType(rawValue: $0.rawValue)
}
let request = APIEndpoint.v1.profiles.get(parameters: .init(
filterProfileType: filterType.map { [$0] }
))
let response = try await client.request(request)
return response.data.map { sdkProfile in
let pages = try await client.requestAllPages(
APIEndpoint.v1.profiles.get(parameters: .init(
filterProfileType: filterType.map { [$0] },
limit: 200
)),
nextCursor: { $0.meta?.paging.nextCursor }
)
return pages.flatMap(\.data).map { sdkProfile in
let parentId = sdkProfile.relationships?.bundleID?.data?.id ?? ""
return mapProfile(sdkProfile, bundleIdId: parentId)
}
Expand Down
13 changes: 8 additions & 5 deletions Sources/Infrastructure/Users/SDKUserRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ public struct SDKUserRepository: UserRepository, @unchecked Sendable {
let filterRole = role.flatMap {
APIEndpoint.V1.Users.GetParameters.FilterRoles(rawValue: $0.rawValue)
}
let request = APIEndpoint.v1.users.get(parameters: .init(
filterRoles: filterRole.map { [$0] }
))
let response = try await client.request(request)
return response.data.map(mapTeamMember)
let pages = try await client.requestAllPages(
APIEndpoint.v1.users.get(parameters: .init(
filterRoles: filterRole.map { [$0] },
limit: 200
)),
nextCursor: { $0.meta?.paging.nextCursor }
)
return pages.flatMap(\.data).map(mapTeamMember)
}

public func updateUser(id: String, roles: [Domain.UserRole]) async throws -> Domain.TeamMember {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ public struct SDKXcodeCloudBuildRunRepository: XcodeCloudBuildRunRepository, @un
}

public func listBuildRuns(workflowId: String) async throws -> [XcodeCloudBuildRun] {
let request = APIEndpoint.v1.ciWorkflows.id(workflowId).buildRuns.get()
let response = try await client.request(request)
return response.data.map { mapBuildRun($0, workflowId: workflowId) }
let pages = try await client.requestAllPages(
APIEndpoint.v1.ciWorkflows.id(workflowId).buildRuns.get(parameters: .init(limit: 200)),
nextCursor: { $0.meta?.paging.nextCursor }
)
return pages.flatMap(\.data).map { mapBuildRun($0, workflowId: workflowId) }
}

public func getBuildRun(id: String) async throws -> XcodeCloudBuildRun {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,22 @@ struct SDKBuildUploadRepositoryTests {
)
)
}

@Test func `build uploads list includes every upload beyond the first page`() async throws {
func page(_ range: Range<Int>, nextCursor: String?) -> BuildUploadsResponse {
BuildUploadsResponse(
data: range.map { i in makeSdkBuildUpload(id: "item-\(i)", state: .complete) },
links: .init(this: ""),
meta: .init(paging: .init(total: 75, limit: 50, nextCursor: nextCursor))
)
}
let stub = StubAPIClient()
stub.willReturnPages([page(0..<50, nextCursor: "page-2"), page(50..<75, nextCursor: nil)])

let repo = SDKBuildUploadRepository(client: stub)
let result = try await repo.listBuildUploads(appId: "app-1")

#expect(result.count == 75)
#expect(result.last?.id == "item-74")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,22 @@ struct SDKDiagnosticsRepositoryTests {

#expect(result.isEmpty)
}

@Test func `diagnostic signatures list includes every signature beyond the first page`() async throws {
func page(_ range: Range<Int>, nextCursor: String?) -> DiagnosticSignaturesResponse {
DiagnosticSignaturesResponse(
data: range.map { i in makeSDKSignature(id: "item-\(i)") },
links: .init(this: ""),
meta: .init(paging: .init(total: 75, limit: 50, nextCursor: nextCursor))
)
}
let stub = StubAPIClient()
stub.willReturnPages([page(0..<50, nextCursor: "page-2"), page(50..<75, nextCursor: nil)])

let repo = SDKDiagnosticsRepository(client: stub)
let result = try await repo.listSignatures(buildId: "build-1", diagnosticType: nil)

#expect(result.count == 75)
#expect(result.last?.id == "item-74")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,22 @@ struct SDKCustomerReviewRepositoryDeleteResponseTests {

#expect(stub.voidRequestCalled)
}

@Test func `reviews list includes every review beyond the first page`() async throws {
func page(_ range: Range<Int>, nextCursor: String?) -> CustomerReviewsResponse {
CustomerReviewsResponse(
data: range.map { i in CustomerReview(type: .customerReviews, id: "item-\(i)", attributes: .init(rating: 5)) },
links: .init(this: ""),
meta: .init(paging: .init(total: 75, limit: 50, nextCursor: nextCursor))
)
}
let stub = StubAPIClient()
stub.willReturnPages([page(0..<50, nextCursor: "page-2"), page(50..<75, nextCursor: nil)])

let repo = SDKCustomerReviewRepository(client: stub)
let result = try await repo.listReviews(appId: "app-1")

#expect(result.count == 75)
#expect(result.last?.id == "item-74")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,22 @@ struct SDKVersionRepositoryTests {
try await repo.setBuild(versionId: "v-1", buildId: "build-42")
// No error thrown = success
}

@Test func `versions list includes every version beyond the first page`() async throws {
func page(_ range: Range<Int>, nextCursor: String?) -> AppStoreVersionsResponse {
AppStoreVersionsResponse(
data: range.map { i in AppStoreVersion(type: .appStoreVersions, id: "item-\(i)", attributes: .init(platform: .ios, versionString: "1.\(i)", appStoreState: .readyForSale)) },
links: .init(this: ""),
meta: .init(paging: .init(total: 75, limit: 50, nextCursor: nextCursor))
)
}
let stub = StubAPIClient()
stub.willReturnPages([page(0..<50, nextCursor: "page-2"), page(50..<75, nextCursor: nil)])

let repo = SDKVersionRepository(client: stub)
let result = try await repo.listVersions(appId: "app-1")

#expect(result.count == 75)
#expect(result.last?.id == "item-74")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,22 @@ struct SDKBundleIDRepositoryTests {

#expect(stub.voidRequestCalled == true)
}

@Test func `bundle ids list includes every bundle id beyond the first page`() async throws {
func page(_ range: Range<Int>, nextCursor: String?) -> BundleIDsResponse {
BundleIDsResponse(
data: range.map { i in AppStoreConnect_Swift_SDK.BundleID(type: .bundleIDs, id: "item-\(i)", attributes: .init(name: "B\(i)", platform: .ios, identifier: "com.example.b\(i)")) },
links: .init(this: ""),
meta: .init(paging: .init(total: 75, limit: 50, nextCursor: nextCursor))
)
}
let stub = StubAPIClient()
stub.willReturnPages([page(0..<50, nextCursor: "page-2"), page(50..<75, nextCursor: nil)])

let repo = SDKBundleIDRepository(client: stub)
let result = try await repo.listBundleIDs(platform: nil, identifier: nil)

#expect(result.count == 75)
#expect(result.last?.id == "item-74")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,22 @@ struct SDKDeviceRepositoryTests {

#expect(result[0].isEnabled == false)
}

@Test func `devices list includes every device beyond the first page`() async throws {
func page(_ range: Range<Int>, nextCursor: String?) -> DevicesResponse {
DevicesResponse(
data: range.map { i in AppStoreConnect_Swift_SDK.Device(type: .devices, id: "item-\(i)", attributes: .init(name: "D\(i)", platform: .ios, udid: "U\(i)", deviceClass: .iphone, status: .enabled)) },
links: .init(this: ""),
meta: .init(paging: .init(total: 75, limit: 50, nextCursor: nextCursor))
)
}
let stub = StubAPIClient()
stub.willReturnPages([page(0..<50, nextCursor: "page-2"), page(50..<75, nextCursor: nil)])

let repo = SDKDeviceRepository(client: stub)
let result = try await repo.listDevices(platform: nil)

#expect(result.count == 75)
#expect(result.last?.id == "item-74")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,40 @@ struct SDKProfileRepositoryTests {

#expect(stub.voidRequestCalled == true)
}

@Test func `profiles list for a bundle id includes every profile beyond the first page`() async throws {
func page(_ range: Range<Int>, nextCursor: String?) -> ProfilesWithoutIncludesResponse {
ProfilesWithoutIncludesResponse(
data: range.map { i in AppStoreConnect_Swift_SDK.Profile(type: .profiles, id: "item-\(i)", attributes: .init(name: "P\(i)", profileType: .iosAppStore, profileState: .active)) },
links: .init(this: ""),
meta: .init(paging: .init(total: 75, limit: 50, nextCursor: nextCursor))
)
}
let stub = StubAPIClient()
stub.willReturnPages([page(0..<50, nextCursor: "page-2"), page(50..<75, nextCursor: nil)])

let repo = SDKProfileRepository(client: stub)
let result = try await repo.listProfiles(bundleIdId: "bid-1", profileType: nil)

#expect(result.count == 75)
#expect(result.last?.id == "item-74")
}

@Test func `profiles list includes every profile beyond the first page`() async throws {
func page(_ range: Range<Int>, nextCursor: String?) -> ProfilesResponse {
ProfilesResponse(
data: range.map { i in AppStoreConnect_Swift_SDK.Profile(type: .profiles, id: "item-\(i)", attributes: .init(name: "P\(i)", profileType: .iosAppStore, profileState: .active)) },
links: .init(this: ""),
meta: .init(paging: .init(total: 75, limit: 50, nextCursor: nextCursor))
)
}
let stub = StubAPIClient()
stub.willReturnPages([page(0..<50, nextCursor: "page-2"), page(50..<75, nextCursor: nil)])

let repo = SDKProfileRepository(client: stub)
let result = try await repo.listProfiles(bundleIdId: nil, profileType: nil)

#expect(result.count == 75)
#expect(result.last?.id == "item-74")
}
}
Loading
Loading