From 6eb3abaf142c622f2b99a723ff909f8313796ca3 Mon Sep 17 00:00:00 2001 From: Oliver Jones Date: Mon, 18 Jul 2022 19:04:27 +1000 Subject: [PATCH 01/12] Switching to using Bagbutik Prior to this commit the project used AppStoreConnect_Swift_SDK for all API interactions. This commit starts refactoring the project to use Bagbutik instead. Bagbutik supports Linux and provides a better (async/await) based API and is generated from the AppStoreConnect Swagger spec. --- Package.resolved | 108 ++++++++ Package.swift | 16 +- .../AppStoreConnectCLI.swift | 2 +- .../Arguments/APIKeyID.swift | 10 +- .../Arguments/UserInfoArguments.swift | 4 +- .../Commands/BundleIds/BundleIdsCommand.swift | 2 +- .../BundleIds/DeleteBundleIdCommand.swift | 4 +- .../BundleIds/ListBundleIdsCommand.swift | 4 +- .../BundleIds/ModifyBundleIdCommand.swift | 4 +- .../BundleIds/ReadBundleIdCommand.swift | 4 +- .../DisableBundleIdCapabilityCommand.swift | 4 +- .../Capability/EnableBundleIdCapability.swift | 4 +- .../Commands/CommonParsableCommand.swift | 4 +- .../Profiles/CreateProfileCommand.swift | 4 +- .../ListProfilesByBundleIdCommand.swift | 4 +- .../TestFlight/Apps/ListAppsCommand.swift | 4 +- .../TestFlight/Apps/ReadAppCommand.swift | 4 +- .../BetaGroups/AddTestersToGroupCommand.swift | 4 +- .../BetaGroups/ReadBetaGroupCommand.swift | 4 +- .../AddTesterToGroupsCommand.swift | 4 +- .../ListBetaTesterByBuildsCommand.swift | 28 +- .../ListBetaTesterByGroupCommand.swift | 4 +- .../Builds/AddGroupsToBuildCommand.swift | 4 +- .../CreateBuildLocalizationsCommand.swift | 4 +- .../DeleteBuildLocalizationsCommand.swift | 4 +- .../ListBuildLocalizationsCommand.swift | 4 +- .../ReadBuildLocalizationCommand.swift | 4 +- .../UpdateBuildLocalizationsCommand.swift | 4 +- .../Builds/RemoveGroupsFromBuildCommand.swift | 4 +- .../Sync/TestFlightPullCommand.swift | 4 +- .../Sync/TestFlightPushCommand.swift | 4 +- .../Commands/Users/GetUserInfoCommand.swift | 2 +- .../CancelUserInvitationsCommand.swift | 11 +- .../Users/Invitations/InviteUserCommand.swift | 70 +++-- .../ListUserInvitationsCommand.swift | 6 +- .../Commands/Users/ListUsersCommand.swift | 20 +- .../AppStoreConnectService+Helpers.swift | 13 - .../Helpers/JWT+Helpers.swift | 23 ++ .../ListUserInvitationsV1.Filter.Roles.swift | 38 +++ ...ListUsers.Sort+ExpressibleByArgument.swift | 4 +- .../API/UserRole+ExpressibleByArgument.swift | 21 -- Sources/AppStoreConnectCLI/Model/App.swift | 44 +-- .../AppStoreConnectCLI/Model/BetaGroup.swift | 18 ++ .../AppStoreConnectCLI/Model/BetaTester.swift | 2 +- .../AppStoreConnectCLI/Model/BundleId.swift | 16 +- Sources/AppStoreConnectCLI/Model/User.swift | 21 +- .../Model/UserInvitation.swift | 70 ++--- .../AppStoreConnectCLI/Model/UserRole.swift | 114 ++++++++ .../Services/AppStoreConnectService.swift | 253 +++++++++++------- .../Services/EndpointRequestor.swift | 19 ++ .../Services/Operations/APIOperation.swift | 10 + .../Operations/ListAppsOperation.swift | 30 +-- .../Operations/ListBundleIdsOperation.swift | 26 +- .../ListUserInvitationsOperation.swift | 29 +- .../Operations/ListUsersOperation.swift | 52 ++-- .../Operations/ReadAppOperation.swift | 45 ++-- .../Operations/ReadBundleIdOperation.swift | 40 ++- Sources/Model/UserInvitation.swift | 31 +++ Sources/Model/UserRole.swift | 30 +++ Sources/appstoreconnect-cli/EntryPoint.swift | 7 + Sources/appstoreconnect-cli/main.swift | 3 - .../Operations/TestRequestors.swift | 8 + 62 files changed, 874 insertions(+), 468 deletions(-) delete mode 100755 Sources/AppStoreConnectCLI/Helpers/AppStoreConnectService+Helpers.swift create mode 100644 Sources/AppStoreConnectCLI/Helpers/JWT+Helpers.swift create mode 100644 Sources/AppStoreConnectCLI/Model/API/ListUserInvitationsV1.Filter.Roles.swift delete mode 100644 Sources/AppStoreConnectCLI/Model/API/UserRole+ExpressibleByArgument.swift create mode 100644 Sources/AppStoreConnectCLI/Model/UserRole.swift create mode 100644 Sources/Model/UserInvitation.swift create mode 100644 Sources/Model/UserRole.swift create mode 100644 Sources/appstoreconnect-cli/EntryPoint.swift delete mode 100644 Sources/appstoreconnect-cli/main.swift diff --git a/Package.resolved b/Package.resolved index fa232f43..14be877b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -9,6 +9,15 @@ "version" : "1.7.0" } }, + { + "identity" : "bagbutik", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MortenGregersen/Bagbutik.git", + "state" : { + "revision" : "a0752c08c0f3a20f6648f75be0b7267f96486e67", + "version" : "2.0.0" + } + }, { "identity" : "codablecsv", "kind" : "remoteSourceControl", @@ -18,6 +27,15 @@ "version" : "0.5.5" } }, + { + "identity" : "collectionconcurrencykit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git", + "state" : { + "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", + "version" : "0.2.0" + } + }, { "identity" : "files", "kind" : "remoteSourceControl", @@ -27,6 +45,69 @@ "version" : "4.1.1" } }, + { + "identity" : "komondor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/shibapm/Komondor.git", + "state" : { + "revision" : "90b087b1e39069684b1ff4bf915c2aae594f2d60", + "version" : "1.1.3" + } + }, + { + "identity" : "packageconfig", + "kind" : "remoteSourceControl", + "location" : "https://github.com/shibapm/PackageConfig.git", + "state" : { + "revision" : "58523193c26fb821ed1720dcd8a21009055c7cdb", + "version" : "1.1.3" + } + }, + { + "identity" : "pathkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/PathKit.git", + "state" : { + "revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574", + "version" : "1.0.1" + } + }, + { + "identity" : "shellout", + "kind" : "remoteSourceControl", + "location" : "https://github.com/JohnSundell/ShellOut.git", + "state" : { + "revision" : "e1577acf2b6e90086d01a6d5e2b8efdaae033568", + "version" : "2.3.0" + } + }, + { + "identity" : "spectre", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/Spectre.git", + "state" : { + "revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7", + "version" : "0.10.1" + } + }, + { + "identity" : "stencil", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stencilproject/Stencil.git", + "state" : { + "revision" : "ccd9402682f4c07dac9561befd207c8156e80e20", + "version" : "0.14.2" + } + }, + { + "identity" : "stencilswiftkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftGen/StencilSwiftKit", + "state" : { + "revision" : "54cbedcdbb4334e03930adcff7343ffaf317bf0f", + "version" : "2.8.0" + } + }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", @@ -36,6 +117,24 @@ "version" : "1.1.3" } }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto", + "state" : { + "revision" : "d9825fa541df64b1a7b182178d61b9a82730d01f", + "version" : "2.1.0" + } + }, + { + "identity" : "swiftformat", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/SwiftFormat", + "state" : { + "revision" : "665c3c58923ee8ac36d3e44b17dc185229cce301", + "version" : "0.49.13" + } + }, { "identity" : "swiftytexttable", "kind" : "remoteSourceControl", @@ -53,6 +152,15 @@ "revision" : "01835dc202670b5bb90d07f3eae41867e9ed29f6", "version" : "5.0.1" } + }, + { + "identity" : "zip", + "kind" : "remoteSourceControl", + "location" : "https://github.com/marmelroy/Zip.git", + "state" : { + "revision" : "67fa55813b9e7b3b9acee9c0ae501def28746d76", + "version" : "2.1.2" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 5996b741..d5900b0e 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "appstoreconnect-cli", platforms: [ - .macOS(.v10_15) + .macOS(.v12) ], products: [ .executable( @@ -38,7 +38,15 @@ let package = Package( .package( url: "https://github.com/dehesa/CodableCSV.git", from: "0.5.5" - ) + ), + .package( + url: "https://github.com/MortenGregersen/Bagbutik.git", + from: "2.0.0" + ), + .package( + url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", + from: "0.2.0" + ), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -59,10 +67,12 @@ let package = Package( .target(name: "Model"), .target(name: "FileSystem"), .product(name: "AppStoreConnect-Swift-SDK", package: "AppStoreConnect-Swift-SDK"), + .product(name: "Bagbutik", package: "Bagbutik"), .product(name: "Yams", package: "Yams"), .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "SwiftyTextTable", package: "SwiftyTextTable"), - .product(name: "CodableCSV", package: "CodableCSV") + .product(name: "CodableCSV", package: "CodableCSV"), + .product(name: "CollectionConcurrencyKit", package: "CollectionConcurrencyKit"), ] ), .testTarget( diff --git a/Sources/AppStoreConnectCLI/AppStoreConnectCLI.swift b/Sources/AppStoreConnectCLI/AppStoreConnectCLI.swift index 6fbc3bc4..87c49482 100755 --- a/Sources/AppStoreConnectCLI/AppStoreConnectCLI.swift +++ b/Sources/AppStoreConnectCLI/AppStoreConnectCLI.swift @@ -3,7 +3,7 @@ import ArgumentParser import Foundation -public struct AppStoreConnectCLI: ParsableCommand { +public struct AppStoreConnectCLI: AsyncParsableCommand { public static var configuration = CommandConfiguration( commandName: "asc", abstract: "A utility for interacting with the AppStore Connect API.", diff --git a/Sources/AppStoreConnectCLI/Arguments/APIKeyID.swift b/Sources/AppStoreConnectCLI/Arguments/APIKeyID.swift index a15d91b0..7b786b47 100644 --- a/Sources/AppStoreConnectCLI/Arguments/APIKeyID.swift +++ b/Sources/AppStoreConnectCLI/Arguments/APIKeyID.swift @@ -31,6 +31,13 @@ struct APIKeyID: EnvironmentLoadableArgument { } func load() throws -> String { + return loadPEM() + .components(separatedBy: .newlines) + .filter { $0.hasSuffix("PRIVATE KEY-----") == false } + .joined() + } + + func loadPEM() throws -> String { // TODO: validate the format of the env var content // TODO: validate format of file is correct (if found) @@ -43,8 +50,5 @@ struct APIKeyID: EnvironmentLoadableArgument { } return apiKeyFileContent - .components(separatedBy: .newlines) - .filter { $0.hasSuffix("PRIVATE KEY-----") == false } - .joined() } } diff --git a/Sources/AppStoreConnectCLI/Arguments/UserInfoArguments.swift b/Sources/AppStoreConnectCLI/Arguments/UserInfoArguments.swift index e18063a9..540ddcb8 100644 --- a/Sources/AppStoreConnectCLI/Arguments/UserInfoArguments.swift +++ b/Sources/AppStoreConnectCLI/Arguments/UserInfoArguments.swift @@ -1,15 +1,15 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd import ArgumentParser -import AppStoreConnect_Swift_SDK import Foundation +import Model struct UserInfoArguments: ParsableArguments { @Option( parsing: .upToNextOption, help: "Assigned user roles that determine the user's access to sections of App Store Connect and tasks they can perform. \(UserRole.allCases)" ) - var roles: [UserRole] = [] + var roles: [Model.UserRole] = [] @Flag(help: "Indicates that a user has access to all apps available to the team.") var allAppsVisible = false diff --git a/Sources/AppStoreConnectCLI/Commands/BundleIds/BundleIdsCommand.swift b/Sources/AppStoreConnectCLI/Commands/BundleIds/BundleIdsCommand.swift index 88f15dc8..d30ee57e 100644 --- a/Sources/AppStoreConnectCLI/Commands/BundleIds/BundleIdsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/BundleIds/BundleIdsCommand.swift @@ -3,7 +3,7 @@ import ArgumentParser import Foundation -struct BundleIdsCommand: ParsableCommand { +struct BundleIdsCommand: AsyncParsableCommand { static var configuration = CommandConfiguration( commandName: "bundle-ids", abstract: "Manage the bundle IDs that uniquely identify your apps.", diff --git a/Sources/AppStoreConnectCLI/Commands/BundleIds/DeleteBundleIdCommand.swift b/Sources/AppStoreConnectCLI/Commands/BundleIds/DeleteBundleIdCommand.swift index 454e8b6f..32c9c3ac 100644 --- a/Sources/AppStoreConnectCLI/Commands/BundleIds/DeleteBundleIdCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/BundleIds/DeleteBundleIdCommand.swift @@ -18,9 +18,9 @@ struct DeleteBundleIdCommand: CommonParsableCommand { @Argument(help: "The reverse-DNS bundle ID identifier to delete. Must be unique. (eg. com.example.app)") var identifier: String - func run() throws { + func run() async throws { let service = try makeService() - try service.deleteBundleId(bundleId: identifier) + try await service.deleteBundleId(bundleId: identifier) } } diff --git a/Sources/AppStoreConnectCLI/Commands/BundleIds/ListBundleIdsCommand.swift b/Sources/AppStoreConnectCLI/Commands/BundleIds/ListBundleIdsCommand.swift index c4444477..2166aceb 100644 --- a/Sources/AppStoreConnectCLI/Commands/BundleIds/ListBundleIdsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/BundleIds/ListBundleIdsCommand.swift @@ -33,10 +33,10 @@ struct ListBundleIdsCommand: CommonParsableCommand { @Option(parsing: .upToNextOption, help: "Filter the results by seed ID") var filterSeedId: [String] = [] - func run() throws { + func run() async throws { let service = try makeService() - let bundleIds = try service.listBundleIds( + let bundleIds = try await service.listBundleIds( identifiers: filterIdentifier, names: filterName, platforms: filterPlatform, diff --git a/Sources/AppStoreConnectCLI/Commands/BundleIds/ModifyBundleIdCommand.swift b/Sources/AppStoreConnectCLI/Commands/BundleIds/ModifyBundleIdCommand.swift index 191040be..aa9e22c7 100644 --- a/Sources/AppStoreConnectCLI/Commands/BundleIds/ModifyBundleIdCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/BundleIds/ModifyBundleIdCommand.swift @@ -22,10 +22,10 @@ struct ModifyBundleIdCommand: CommonParsableCommand { @Argument(help: "The new name for the bundle identifier.") var name: String - func run() throws { + func run() async throws { let service = try makeService() - let bundleId = try service + let bundleId = try await service .modifyBundleIdInformation(bundleId: identifier, name: name) bundleId.render(options: common.outputOptions) diff --git a/Sources/AppStoreConnectCLI/Commands/BundleIds/ReadBundleIdCommand.swift b/Sources/AppStoreConnectCLI/Commands/BundleIds/ReadBundleIdCommand.swift index dbd3bba7..0d476021 100644 --- a/Sources/AppStoreConnectCLI/Commands/BundleIds/ReadBundleIdCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/BundleIds/ReadBundleIdCommand.swift @@ -18,10 +18,10 @@ struct ReadBundleIdCommand: CommonParsableCommand { @Argument(help: "The reverse-DNS bundle ID identifier to read. Must be unique. (eg. com.example.app)") var identifier: String - func run() throws { + func run() async throws { let service = try makeService() - let bundleId = try service.readBundleIdInformation(bundleId: identifier) + let bundleId = try await service.readBundleIdInformation(bundleId: identifier) bundleId.render(options: common.outputOptions) } diff --git a/Sources/AppStoreConnectCLI/Commands/Capability/DisableBundleIdCapabilityCommand.swift b/Sources/AppStoreConnectCLI/Commands/Capability/DisableBundleIdCapabilityCommand.swift index e9ec5615..251962fe 100644 --- a/Sources/AppStoreConnectCLI/Commands/Capability/DisableBundleIdCapabilityCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/Capability/DisableBundleIdCapabilityCommand.swift @@ -19,9 +19,9 @@ struct DisableBundleIdCapabilityCommand: CommonParsableCommand { @Argument(help: ArgumentHelp("Bundle Id capability type.", discussion: "One of \(CapabilityType.allCases)")) var capabilityType: CapabilityType - func run() throws { + func run() async throws { let service = try makeService() - try service.disableBundleIdCapability(bundleId: bundleId, capabilityType: capabilityType) + try await service.disableBundleIdCapability(bundleId: bundleId, capabilityType: capabilityType) } } diff --git a/Sources/AppStoreConnectCLI/Commands/Capability/EnableBundleIdCapability.swift b/Sources/AppStoreConnectCLI/Commands/Capability/EnableBundleIdCapability.swift index bbce43b2..96ea5e54 100644 --- a/Sources/AppStoreConnectCLI/Commands/Capability/EnableBundleIdCapability.swift +++ b/Sources/AppStoreConnectCLI/Commands/Capability/EnableBundleIdCapability.swift @@ -21,10 +21,10 @@ struct EnableBundleIdCapabilityCommand: CommonParsableCommand { // TODO: CapabilitySetting - func run() throws { + func run() async throws { let service = try makeService() - try service.enableBundleIdCapability( + try await service.enableBundleIdCapability( bundleId: bundleId, capabilityType: capabilityType ) } diff --git a/Sources/AppStoreConnectCLI/Commands/CommonParsableCommand.swift b/Sources/AppStoreConnectCLI/Commands/CommonParsableCommand.swift index a25974ff..c3bc3a39 100755 --- a/Sources/AppStoreConnectCLI/Commands/CommonParsableCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/CommonParsableCommand.swift @@ -4,7 +4,7 @@ import AppStoreConnect_Swift_SDK import ArgumentParser import Foundation -protocol CommonParsableCommand: ParsableCommand { +protocol CommonParsableCommand: AsyncParsableCommand { var common: CommonOptions { get } func makeService() throws -> AppStoreConnectService @@ -20,7 +20,7 @@ enum PrintLevel { extension CommonParsableCommand { func makeService() throws -> AppStoreConnectService { - AppStoreConnectService(configuration: try APIConfiguration(common.authOptions)) + try AppStoreConnectService(configuration: .init(common.authOptions), jwt: .init(common.authOptions)) } } diff --git a/Sources/AppStoreConnectCLI/Commands/Profiles/CreateProfileCommand.swift b/Sources/AppStoreConnectCLI/Commands/Profiles/CreateProfileCommand.swift index 514a9e00..13e9f436 100644 --- a/Sources/AppStoreConnectCLI/Commands/Profiles/CreateProfileCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/Profiles/CreateProfileCommand.swift @@ -40,10 +40,10 @@ struct CreateProfileCommand: CommonParsableCommand { } } - func run() throws { + func run() async throws { let service = try makeService() - let profile = try service.createProfile( + let profile = try await service.createProfile( name: name, bundleId: bundleId, profileType: profileType, diff --git a/Sources/AppStoreConnectCLI/Commands/Profiles/ListProfilesByBundleIdCommand.swift b/Sources/AppStoreConnectCLI/Commands/Profiles/ListProfilesByBundleIdCommand.swift index 0fa77222..d9beefbd 100644 --- a/Sources/AppStoreConnectCLI/Commands/Profiles/ListProfilesByBundleIdCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/Profiles/ListProfilesByBundleIdCommand.swift @@ -37,10 +37,10 @@ struct ListProfilesByBundleIdCommand: CommonParsableCommand { ) var downloadPath: String? - func run() throws { + func run() async throws { let service = try makeService() - let profiles = try service.listProfilesByBundleId(bundleId, limit: limit) + let profiles = try await service.listProfilesByBundleId(bundleId, limit: limit) if let path = downloadPath { let processor = ProfileProcessor(path: .folder(path: path)) diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/Apps/ListAppsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/Apps/ListAppsCommand.swift index 5ec787c9..696c75ec 100755 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/Apps/ListAppsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/Apps/ListAppsCommand.swift @@ -27,10 +27,10 @@ struct ListAppsCommand: CommonParsableCommand { @Option(parsing: .upToNextOption, help: "Filter the results by the specified app SKUs") var filterSkus: [String] = [] - func run() throws { + func run() async throws { let service = try makeService() - let apps = try service.listApps( + let apps = try await service.listApps( bundleIds: filterBundleIds, names: filterNames, skus: filterSkus, diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/Apps/ReadAppCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/Apps/ReadAppCommand.swift index 9975f242..a5fd3665 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/Apps/ReadAppCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/Apps/ReadAppCommand.swift @@ -17,10 +17,10 @@ struct ReadAppCommand: CommonParsableCommand { @OptionGroup() var appLookupArgument: AppLookupArgument - func run() throws { + func run() async throws { let service = try makeService() - let app = try service.readApp(identifier: appLookupArgument.identifier) + let app = try await service.readApp(identifier: appLookupArgument.identifier) app.render(options: common.outputOptions) } diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/AddTestersToGroupCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/AddTestersToGroupCommand.swift index ebeedba9..0695478c 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/AddTestersToGroupCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/AddTestersToGroupCommand.swift @@ -26,9 +26,9 @@ struct AddTestersToGroupCommand: CommonParsableCommand { } } - func run() throws { + func run() async throws { let service = try makeService() - try service.addTestersToGroup(bundleId: bundleId, groupName: groupName, emails: emails) + try await service.addTestersToGroup(bundleId: bundleId, groupName: groupName, emails: emails) } } diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/ReadBetaGroupCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/ReadBetaGroupCommand.swift index 1caa2b47..7bc5bd54 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/ReadBetaGroupCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/ReadBetaGroupCommand.swift @@ -21,10 +21,10 @@ struct ReadBetaGroupCommand: CommonParsableCommand { @Argument(help: "The name of the beta group.") var groupName: String - func run() throws { + func run() async throws { let service = try makeService() - let betaGroup = try service.readBetaGroup(bundleId: appBundleId, groupName: groupName) + let betaGroup = try await service.readBetaGroup(bundleId: appBundleId, groupName: groupName) betaGroup.render(options: common.outputOptions) } diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaTesters/AddTesterToGroupsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaTesters/AddTesterToGroupsCommand.swift index b30ce96a..d3da6e21 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaTesters/AddTesterToGroupsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaTesters/AddTesterToGroupsCommand.swift @@ -26,9 +26,9 @@ struct AddTesterToGroupsCommand: CommonParsableCommand { } } - func run() throws { + func run() async throws { let service = try makeService() - try service.addTesterToGroups(email: email, bundleId: bundleId, groupNames: groupNames) + try await service.addTesterToGroups(email: email, bundleId: bundleId, groupNames: groupNames) } } diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaTesters/ListBetaTesterByBuildsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaTesters/ListBetaTesterByBuildsCommand.swift index 5036ddd8..6220f5d2 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaTesters/ListBetaTesterByBuildsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaTesters/ListBetaTesterByBuildsCommand.swift @@ -41,26 +41,22 @@ struct ListBetaTesterByBuildsCommand: CommonParsableCommand { } } - func run() throws { + func run() async throws { let service = try makeService() - let betaTesters = try service - .getAppResourceIdsFrom(bundleIds: [bundleId]) - .flatMap { [versions, preReleaseVersions] appIds -> AnyPublisher in - - var filters: [ListBuilds.Filter] = [.app(appIds)] - - if !versions.isEmpty { - filters.append(.version(versions)) - } + let appIds = try await service.appResourceIdsForBundleIds([bundleId]) + + var filters: [ListBuilds.Filter] = [.app(appIds)] + if !versions.isEmpty { + filters.append(.version(versions)) + } - if !preReleaseVersions.isEmpty { - filters.append(.preReleaseVersionVersion(preReleaseVersions)) - } + if !preReleaseVersions.isEmpty { + filters.append(.preReleaseVersionVersion(preReleaseVersions)) + } - return service.request(APIEndpoint.builds(filter: filters)) - .eraseToAnyPublisher() - } + let betaTesters = try service.request(APIEndpoint.builds(filter: filters)) + .eraseToAnyPublisher() .flatMap { [versions, preReleaseVersions] buildResponse -> AnyPublisher in guard !buildResponse.data.isEmpty else { let error = CommandError.noBuildsFound(preReleaseVersions: preReleaseVersions, versions: versions) diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaTesters/ListBetaTesterByGroupCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaTesters/ListBetaTesterByGroupCommand.swift index bb804e70..b44fab33 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaTesters/ListBetaTesterByGroupCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaTesters/ListBetaTesterByGroupCommand.swift @@ -22,10 +22,10 @@ struct ListBetaTesterByGroupCommand: CommonParsableCommand { ) var groupName: String - func run() throws { + func run() async throws { let service = try makeService() - let betaTesters = try service.listBetaTestersForGroup(identifier: appLookupArgument.identifier, groupName: groupName) + let betaTesters = try await service.listBetaTestersForGroup(identifier: appLookupArgument.identifier, groupName: groupName) betaTesters.render(options: common.outputOptions) } } diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/AddGroupsToBuildCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/AddGroupsToBuildCommand.swift index 5d6123a5..1663d38b 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/AddGroupsToBuildCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/AddGroupsToBuildCommand.swift @@ -24,10 +24,10 @@ struct AddGroupsToBuildCommand: CommonParsableCommand { } } - func run() throws { + func run() async throws { let service = try makeService() - try service.addGroupsToBuild( + try await service.addGroupsToBuild( bundleId: build.bundleId, version: build.preReleaseVersion, buildNumber: build.buildNumber, diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/CreateBuildLocalizationsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/CreateBuildLocalizationsCommand.swift index 43e62f45..d36c7ad1 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/CreateBuildLocalizationsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/CreateBuildLocalizationsCommand.swift @@ -25,10 +25,10 @@ struct CreateBuildLocalizationsCommand: CommonParsableCommand, CreateUpdateBuild try validateWhatsNewInput() } - func run() throws { + func run() async throws { let service = try makeService() - let buildLocalization = try service.createBuildLocalization( + let buildLocalization = try await service.createBuildLocalization( bundleId: build.bundleId, buildNumber: build.buildNumber, preReleaseVersion: build.preReleaseVersion, diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/DeleteBuildLocalizationsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/DeleteBuildLocalizationsCommand.swift index a50f0d1f..9e87f0de 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/DeleteBuildLocalizationsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/DeleteBuildLocalizationsCommand.swift @@ -17,10 +17,10 @@ struct DeleteBuildLocalizationsCommand: CommonParsableCommand { @Argument(help: "The locale information of the build localization resource. eg. (en-AU)") var locale: String - func run() throws { + func run() async throws { let service = try makeService() - try service.deleteBuildLocalization( + try await service.deleteBuildLocalization( bundleId: build.bundleId, buildNumber: build.buildNumber, preReleaseVersion: build.preReleaseVersion, diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/ListBuildLocalizationsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/ListBuildLocalizationsCommand.swift index 16d2ebba..c432ad5c 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/ListBuildLocalizationsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/ListBuildLocalizationsCommand.swift @@ -17,10 +17,10 @@ struct ListBuildLocalizationsCommand: CommonParsableCommand { @Option(help: "Limit the number of resources to return.") var limit: Int? - func run() throws { + func run() async throws { let service = try makeService() - let localizations = try service.listBuildsLocalizations( + let localizations = try await service.listBuildsLocalizations( bundleId: build.bundleId, buildNumber: build.buildNumber, preReleaseVersion: build.preReleaseVersion, diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/ReadBuildLocalizationCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/ReadBuildLocalizationCommand.swift index 5d86736b..c50ae960 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/ReadBuildLocalizationCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/ReadBuildLocalizationCommand.swift @@ -17,10 +17,10 @@ struct ReadBuildLocalizationCommand: CommonParsableCommand { @Argument(help: "The locale information of the build localization resource. eg. (en-AU)") var locale: String - func run() throws { + func run() async throws { let service = try makeService() - let buildLocalization = try service.readBuildLocaization( + let buildLocalization = try await service.readBuildLocaization( bundleId: build.bundleId, buildNumber: build.buildNumber, preReleaseVersion: build.preReleaseVersion, diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/UpdateBuildLocalizationsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/UpdateBuildLocalizationsCommand.swift index 2ab91914..5aa4ac87 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/UpdateBuildLocalizationsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/Localizations/UpdateBuildLocalizationsCommand.swift @@ -25,10 +25,10 @@ struct UpdateBuildLocalizationsCommand: CommonParsableCommand, CreateUpdateBuild try validateWhatsNewInput() } - func run() throws { + func run() async throws { let service = try makeService() - let buildLocalization = try service.upateBuildLocalization( + let buildLocalization = try await service.upateBuildLocalization( bundleId: build.bundleId, buildNumber: build.buildNumber, preReleaseVersion: build.preReleaseVersion, diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/RemoveGroupsFromBuildCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/RemoveGroupsFromBuildCommand.swift index e4b9a1a8..7bbb8731 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/RemoveGroupsFromBuildCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/Builds/RemoveGroupsFromBuildCommand.swift @@ -31,10 +31,10 @@ struct RemoveBuildFromGroupsCommand: CommonParsableCommand { } } - func run() throws { + func run() async throws { let service = try makeService() - try service.removeBuildFromGroups( + try await service.removeBuildFromGroups( bundleId: bundleId, version: preReleaseVersion, buildNumber: buildNumber, diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/Sync/TestFlightPullCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/Sync/TestFlightPullCommand.swift index 22bfdfb6..35061d56 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/Sync/TestFlightPullCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/Sync/TestFlightPullCommand.swift @@ -24,10 +24,10 @@ struct TestFlightPullCommand: CommonParsableCommand { @Option(help: "Path to output/write the TestFlight configuration.") var outputPath = "./config/apps" - func run() throws { + func run() async throws { let service = try makeService() - let testflightProgram = try service.getTestFlightProgram(bundleIds: filterBundleIds) + let testflightProgram = try await service.getTestFlightProgram(bundleIds: filterBundleIds) try FileSystem.writeTestFlightConfiguration(program: testflightProgram, to: outputPath) } diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/Sync/TestFlightPushCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/Sync/TestFlightPushCommand.swift index 49cf6580..8f26707d 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/Sync/TestFlightPushCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/Sync/TestFlightPushCommand.swift @@ -18,11 +18,11 @@ struct TestFlightPushCommand: CommonParsableCommand { @Option(help: "Path to read in the TestFlight configuration.") var inputPath = "./config/apps" - func run() throws { + func run() async throws { let service = try makeService() let local = try FileSystem.readTestFlightConfiguration(from: inputPath) - let remote = try service.getTestFlightProgram() + let remote = try await service.getTestFlightProgram() let difference = TestFlightProgramDifference(local: local, remote: remote) diff --git a/Sources/AppStoreConnectCLI/Commands/Users/GetUserInfoCommand.swift b/Sources/AppStoreConnectCLI/Commands/Users/GetUserInfoCommand.swift index cef7e1e7..f4b76f07 100755 --- a/Sources/AppStoreConnectCLI/Commands/Users/GetUserInfoCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/Users/GetUserInfoCommand.swift @@ -21,7 +21,7 @@ struct GetUserInfoCommand: CommonParsableCommand { func run() throws { let service = try makeService() - let user = try service.getUserInfo( + let user = try service.userInfo( with: email, includeVisibleApps: includeVisibleApps ) diff --git a/Sources/AppStoreConnectCLI/Commands/Users/Invitations/CancelUserInvitationsCommand.swift b/Sources/AppStoreConnectCLI/Commands/Users/Invitations/CancelUserInvitationsCommand.swift index 58b8f642..bd06f556 100755 --- a/Sources/AppStoreConnectCLI/Commands/Users/Invitations/CancelUserInvitationsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/Users/Invitations/CancelUserInvitationsCommand.swift @@ -16,14 +16,9 @@ struct CancelUserInvitationsCommand: CommonParsableCommand { @Argument(help: "The email address of a pending user invitation.") var email: String - public func run() throws { + public func run() async throws { let service = try makeService() - - let cancelInvitation = { service.request(APIEndpoint.cancel(userInvitationWithId: $0)) } - - try service - .invitationIdentifier(matching: email) - .flatMap(cancelInvitation) - .await() + + try await service.cancel(userInvitationWithId: service.invitationIdentifier(matching: email)) } } diff --git a/Sources/AppStoreConnectCLI/Commands/Users/Invitations/InviteUserCommand.swift b/Sources/AppStoreConnectCLI/Commands/Users/Invitations/InviteUserCommand.swift index 89eb666a..82b015d8 100755 --- a/Sources/AppStoreConnectCLI/Commands/Users/Invitations/InviteUserCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/Users/Invitations/InviteUserCommand.swift @@ -1,8 +1,10 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd import AppStoreConnect_Swift_SDK + import ArgumentParser import Foundation +import Model struct InviteUserCommand: CommonParsableCommand { static var configuration = CommandConfiguration( @@ -29,39 +31,57 @@ struct InviteUserCommand: CommonParsableCommand { @OptionGroup() var userInfo: UserInfoArguments - public func run() throws { + func validate() throws { + if userInfo.allAppsVisible == false && userInfo.bundleIds.isEmpty { + throw ValidationError.init("If you set allAppsVisible to false, you must provide at least one value for the visibleApps relationship.") + } + } + + public func run() async throws { let service = try makeService() + let invitation: Model.UserInvitation + if userInfo.allAppsVisible { - try inviteUserToTeam(by: service) - return - } - - if userInfo.bundleIds.isNotEmpty { - let resourceIds = try service - .getAppResourceIdsFrom(bundleIds: userInfo.bundleIds) - .await() + invitation = try await service.inviteUserToTeam( + email: email, + firstName: firstName, + lastName: lastName, + roles: userInfo.roles, + allAppsVisible: userInfo.allAppsVisible, + provisioningAllowed: userInfo.provisioningAllowed + ) + } else { + let resourceIds = try await service + .appResourceIdsForBundleIds(userInfo.bundleIds) - try inviteUserToTeam(with: resourceIds, by: service) + guard resourceIds.isEmpty == false else { + throw AppError.couldntFindApp(bundleId: userInfo.bundleIds) + } + + invitation = try await service.inviteUserToTeam( + email: email, + firstName: firstName, + lastName: lastName, + roles: userInfo.roles, + allAppsVisible: userInfo.allAppsVisible, + provisioningAllowed: userInfo.provisioningAllowed, + appsVisibleIds: resourceIds + ) } - - fatalError("Invalid Input: If you set allAppsVisible to false, you must provide at least one value for the visibleApps relationship.") + + invitation.render(options: common.outputOptions) } - func inviteUserToTeam(with appsVisibleIds: [String] = [], by service: AppStoreConnectService) throws { - let request = APIEndpoint.invite( - userWithEmail: email, - firstName: firstName, - lastName: lastName, - roles: userInfo.roles, - allAppsVisible: userInfo.allAppsVisible, - provisioningAllowed: userInfo.provisioningAllowed, - appsVisibleIds: appsVisibleIds) // appsVisibleIds should be empty when allAppsVisible is true +} - let invitation = try service.request(request) - .map { $0.data } - .await() +private enum AppError: LocalizedError { + case couldntFindApp(bundleId: [String]) - invitation.render(options: common.outputOptions) + var errorDescription: String? { + switch self { + case .couldntFindApp(let bundleIds): + return "No apps were found matching \(bundleIds)." + } } } diff --git a/Sources/AppStoreConnectCLI/Commands/Users/Invitations/ListUserInvitationsCommand.swift b/Sources/AppStoreConnectCLI/Commands/Users/Invitations/ListUserInvitationsCommand.swift index bb15402d..86f6ffab 100644 --- a/Sources/AppStoreConnectCLI/Commands/Users/Invitations/ListUserInvitationsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/Users/Invitations/ListUserInvitationsCommand.swift @@ -1,7 +1,7 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK import ArgumentParser +import Model struct ListUserInvitationsCommand: CommonParsableCommand { public static var configuration = CommandConfiguration( @@ -24,10 +24,10 @@ struct ListUserInvitationsCommand: CommonParsableCommand { @Flag(help: "Include visible apps in results.") var includeVisibleApps = false - public func run() throws { + public func run() async throws { let service = try makeService() - let invitations = try service.listUserInvitaions( + let invitations = try await service.listUserInvitaions( filterEmail: filterEmail, filterRole: filterRole, limitVisibleApps: limitVisibleApps, diff --git a/Sources/AppStoreConnectCLI/Commands/Users/ListUsersCommand.swift b/Sources/AppStoreConnectCLI/Commands/Users/ListUsersCommand.swift index fa47eea6..4b8494b9 100644 --- a/Sources/AppStoreConnectCLI/Commands/Users/ListUsersCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/Users/ListUsersCommand.swift @@ -1,9 +1,8 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK import ArgumentParser -import Combine import Foundation +import Model public struct ListUsersCommand: CommonParsableCommand { public static var configuration = CommandConfiguration( @@ -23,9 +22,9 @@ public struct ListUsersCommand: CommonParsableCommand { @Option( parsing: SingleValueParsingStrategy.unconditional, - help: "Sort the results using the provided key (\(ListUsers.Sort.allCases.map { $0.rawValue }.joined(separator: ", "))).\nThe `-` prefix indicates descending order." + help: "Sort the results using the provided key (\(Sort.allCases.map { $0.rawValue }.joined(separator: ", "))).\nThe `-` prefix indicates descending order." ) - var sort: ListUsers.Sort? + var sort: Sort? @Option( parsing: .upToNextOption, @@ -55,14 +54,21 @@ public struct ListUsersCommand: CommonParsableCommand { @Flag(help: "Include visible apps in results.") var includeVisibleApps = false - public func run() throws { + public enum Sort: String, CaseIterable, ExpressibleByArgument { + case lastNameAscending = "lastName" + case lastNameDescending = "-lastName" + case usernameAscending = "username" + case usernameDescending = "-username" + } + + public func run() async throws { let service = try makeService() - let users = try service + let users = try await service .listUsers( limitVisibleApps: limitVisibleApps, limitUsers: limitUsers, - sort: sort, + sort: sort?.rawValue, filterUsername: filterUsername, filterRole: filterRole, filterVisibleApps: filterVisibleApps, diff --git a/Sources/AppStoreConnectCLI/Helpers/AppStoreConnectService+Helpers.swift b/Sources/AppStoreConnectCLI/Helpers/AppStoreConnectService+Helpers.swift deleted file mode 100755 index d0646438..00000000 --- a/Sources/AppStoreConnectCLI/Helpers/AppStoreConnectService+Helpers.swift +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2020 Itty Bitty Apps Pty Ltd - -import Foundation -import AppStoreConnect_Swift_SDK -import Yams - -extension AppStoreConnectService { - convenience init(authenticationYmlPath: String) throws { - let authYml = try String(contentsOfFile: authenticationYmlPath) - let configuration: APIConfiguration = try YAMLDecoder().decode(from: authYml) - self.init(configuration: configuration) - } -} diff --git a/Sources/AppStoreConnectCLI/Helpers/JWT+Helpers.swift b/Sources/AppStoreConnectCLI/Helpers/JWT+Helpers.swift new file mode 100644 index 00000000..5d43fdcc --- /dev/null +++ b/Sources/AppStoreConnectCLI/Helpers/JWT+Helpers.swift @@ -0,0 +1,23 @@ +// Copyright 2022 Itty Bitty Apps Pty Ltd + +import Foundation +import Bagbutik + +extension JWT { + init(_ authOptions: AuthOptions) throws { + + guard authOptions.apiIssuer.value.isEmpty == false else { + throw AuthOptions.Error.issuerNotProvided + } + + guard authOptions.apiKeyId.value.isEmpty == false else { + throw AuthOptions.Error.apiKeyIdNotProvided + } + + try self.init( + keyId: authOptions.apiKeyId.value, + issuerId: authOptions.apiIssuer.value, + privateKey: try authOptions.apiKeyId.loadPEM() + ) + } +} diff --git a/Sources/AppStoreConnectCLI/Model/API/ListUserInvitationsV1.Filter.Roles.swift b/Sources/AppStoreConnectCLI/Model/API/ListUserInvitationsV1.Filter.Roles.swift new file mode 100644 index 00000000..3a5c4a2a --- /dev/null +++ b/Sources/AppStoreConnectCLI/Model/API/ListUserInvitationsV1.Filter.Roles.swift @@ -0,0 +1,38 @@ +// Copyright 2022 Itty Bitty Apps Pty Ltd + +import Bagbutik +import Foundation +import Model + +extension ListUserInvitationsV1.Filter.Roles { + init(_ role: Model.UserRole) { + switch role { + case .admin: + self = .admin + case .finance: + self = .finance + case .accountHolder: + self = .accountHolder + case .sales: + self = .sales + case .marketing: + self = .marketing + case .appManager: + self = .appManager + case .developer: + self = .developer + case .accessToReports: + self = .accessToReports + case .customerSupport: + self = .customerSupport + case .imageManager: + self = .imageManager + case .createApps: + self = .createApps + case .cloudManagedDeveloperId: + self = .cloudManagedDeveloperId + case .cloudManagedAppDistribution: + self = .cloudManagedAppDistribution + } + } +} diff --git a/Sources/AppStoreConnectCLI/Model/API/ListUsers.Sort+ExpressibleByArgument.swift b/Sources/AppStoreConnectCLI/Model/API/ListUsers.Sort+ExpressibleByArgument.swift index a16e96e0..617be5f0 100644 --- a/Sources/AppStoreConnectCLI/Model/API/ListUsers.Sort+ExpressibleByArgument.swift +++ b/Sources/AppStoreConnectCLI/Model/API/ListUsers.Sort+ExpressibleByArgument.swift @@ -1,8 +1,8 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd import ArgumentParser -import AppStoreConnect_Swift_SDK +import Bagbutik import Foundation -extension ListUsers.Sort: Codable, ExpressibleByArgument { +extension ListUsersV1.Sort: Codable, ExpressibleByArgument { } diff --git a/Sources/AppStoreConnectCLI/Model/API/UserRole+ExpressibleByArgument.swift b/Sources/AppStoreConnectCLI/Model/API/UserRole+ExpressibleByArgument.swift deleted file mode 100644 index 1ff53882..00000000 --- a/Sources/AppStoreConnectCLI/Model/API/UserRole+ExpressibleByArgument.swift +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2020 Itty Bitty Apps Pty Ltd - -import ArgumentParser -import AppStoreConnect_Swift_SDK -import Foundation - -extension UserRole: ExpressibleByArgument, CustomStringConvertible { - public typealias AllCases = [UserRole] - - public init?(argument: String) { - self.init(rawValue: argument.uppercased()) - } - - public static var allCases: AllCases { - [.accessToReports, .accountHolder, .admin, .appManager, .customerSupport, .developer, .finance, .marketing, .readOnly, .sales, .technical] - } - - public var description: String { - rawValue.lowercased() - } -} diff --git a/Sources/AppStoreConnectCLI/Model/App.swift b/Sources/AppStoreConnectCLI/Model/App.swift index 9b12fab9..ee605bd9 100644 --- a/Sources/AppStoreConnectCLI/Model/App.swift +++ b/Sources/AppStoreConnectCLI/Model/App.swift @@ -1,6 +1,7 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd import AppStoreConnect_Swift_SDK +import Bagbutik import Combine import Foundation import struct Model.App @@ -21,6 +22,17 @@ extension App { sku: attributes?.sku ) } + + init(_ apiApp: Bagbutik.App) { + let attributes = apiApp.attributes + self.init( + id: apiApp.id, + bundleId: attributes?.bundleId, + name: attributes?.name, + primaryLocale: attributes?.primaryLocale, + sku: attributes?.sku + ) + } } // MARK: - TextTable conveniences @@ -46,35 +58,3 @@ extension App: TableInfoProvider { ] } } - -extension AppStoreConnectService { - - private enum AppError: LocalizedError { - case couldntFindApp(bundleId: [String]) - - var errorDescription: String? { - switch self { - case .couldntFindApp(let bundleIds): - return "No apps were found matching \(bundleIds)." - } - } - } - - /// Find the opaque internal identifier for an application that related to this bundle ID. - func getAppResourceIdsFrom(bundleIds: [String]) -> AnyPublisher<[String], Error> { - let getAppResourceIdRequest = APIEndpoint.apps( - filters: [ListApps.Filter.bundleId(bundleIds)] - ) - - return self.request(getAppResourceIdRequest) - .tryMap { (response: AppsResponse) throws -> [AppStoreConnect_Swift_SDK.App] in - guard !response.data.isEmpty else { - throw AppError.couldntFindApp(bundleId: bundleIds) - } - - return response.data - } - .compactMap { $0.map { $0.id } } - .eraseToAnyPublisher() - } -} diff --git a/Sources/AppStoreConnectCLI/Model/BetaGroup.swift b/Sources/AppStoreConnectCLI/Model/BetaGroup.swift index 9eca62b9..4067df8e 100755 --- a/Sources/AppStoreConnectCLI/Model/BetaGroup.swift +++ b/Sources/AppStoreConnectCLI/Model/BetaGroup.swift @@ -1,6 +1,7 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd import AppStoreConnect_Swift_SDK +import Bagbutik import Combine import Foundation import Model @@ -56,4 +57,21 @@ extension Model.BetaGroup { creationDate: apiBetaGroup.attributes?.createdDate?.formattedDate ) } + + init( + _ apiApp: Bagbutik.App?, + _ apiBetaGroup: AppStoreConnect_Swift_SDK.BetaGroup + ) { + self.init( + id: apiBetaGroup.id, + app: apiApp.map(Model.App.init), + groupName: apiBetaGroup.attributes?.name, + isInternal: apiBetaGroup.attributes?.isInternalGroup, + publicLink: apiBetaGroup.attributes?.publicLink, + publicLinkEnabled: apiBetaGroup.attributes?.publicLinkEnabled, + publicLinkLimit: apiBetaGroup.attributes?.publicLinkLimit, + publicLinkLimitEnabled: apiBetaGroup.attributes?.publicLinkLimitEnabled, + creationDate: apiBetaGroup.attributes?.createdDate?.formattedDate + ) + } } diff --git a/Sources/AppStoreConnectCLI/Model/BetaTester.swift b/Sources/AppStoreConnectCLI/Model/BetaTester.swift index b2c55642..63094796 100644 --- a/Sources/AppStoreConnectCLI/Model/BetaTester.swift +++ b/Sources/AppStoreConnectCLI/Model/BetaTester.swift @@ -26,7 +26,7 @@ extension Model.BetaTester { firstName: betaTester.attributes?.firstName, lastName: betaTester.attributes?.lastName, inviteType: betaTester.attributes?.inviteType?.rawValue, - betaGroups: betaGroups.map { Model.BetaGroup(nil, $0) }, + betaGroups: betaGroups.map { Model.BetaGroup(Optional.none, $0) }, apps: apps.map(Model.App.init) ) } diff --git a/Sources/AppStoreConnectCLI/Model/BundleId.swift b/Sources/AppStoreConnectCLI/Model/BundleId.swift index 21c81816..b04f8316 100644 --- a/Sources/AppStoreConnectCLI/Model/BundleId.swift +++ b/Sources/AppStoreConnectCLI/Model/BundleId.swift @@ -1,6 +1,7 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd import AppStoreConnect_Swift_SDK +import Bagbutik import Combine import Foundation import Model @@ -17,14 +18,27 @@ extension Model.BundleId { seedId: attributes.seedId ) } + + init(_ attributes: Bagbutik.BundleId.Attributes) { + self.init( + identifier: attributes.identifier, + name: attributes.name, + platform: attributes.platform?.rawValue, + seedId: attributes.seedId + ) + } init(_ apiBundleId: AppStoreConnect_Swift_SDK.BundleId) { self.init(apiBundleId.attributes!) } + init(_ apiBundleId: Bagbutik.BundleId) { + self.init(apiBundleId.attributes!) + } + init(_ response: AppStoreConnect_Swift_SDK.BundleIdResponse) { self.init(response.data) - } + } } // MARK: - TextTable conveniences diff --git a/Sources/AppStoreConnectCLI/Model/User.swift b/Sources/AppStoreConnectCLI/Model/User.swift index 2025f127..446f0415 100644 --- a/Sources/AppStoreConnectCLI/Model/User.swift +++ b/Sources/AppStoreConnectCLI/Model/User.swift @@ -1,13 +1,12 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd import AppStoreConnect_Swift_SDK +import Bagbutik import Combine import Foundation import Model import SwiftyTextTable -// TODO: Extract these extensions somewhere that makes sense down the road - // MARK: - API conveniences extension Model.User { @@ -31,7 +30,7 @@ extension Model.User { ) } - static func fromAPIResponse(_ response: UsersResponse) -> [Model.User] { + static func fromAPIResponse(_ response: AppStoreConnect_Swift_SDK.UsersResponse) -> [Model.User] { let users: [AppStoreConnect_Swift_SDK.User] = response.data return users.compactMap { (user: AppStoreConnect_Swift_SDK.User) -> Model.User in @@ -57,6 +56,22 @@ extension Model.User { visibleApps: visibleApps?.compactMap { $0.attributes?.bundleId } ) } + + init(_ user: Bagbutik.User, visibleApps: [Bagbutik.App]? = nil) { + self.init(attributes: user.attributes!, visibleApps: visibleApps) + } + + init(attributes: Bagbutik.User.Attributes, visibleApps: [Bagbutik.App]? = nil) { + self.init( + username: attributes.username ?? "", + firstName: attributes.firstName ?? "", + lastName: attributes.lastName ?? "", + roles: attributes.roles?.map(\.rawValue) ?? [], + provisioningAllowed: attributes.provisioningAllowed ?? false, + allAppsVisible: attributes.allAppsVisible ?? false, + visibleApps: visibleApps?.compactMap { $0.attributes?.bundleId } + ) + } } // MARK: - TextTable conveniences diff --git a/Sources/AppStoreConnectCLI/Model/UserInvitation.swift b/Sources/AppStoreConnectCLI/Model/UserInvitation.swift index b9a0e02e..487b68a2 100755 --- a/Sources/AppStoreConnectCLI/Model/UserInvitation.swift +++ b/Sources/AppStoreConnectCLI/Model/UserInvitation.swift @@ -1,14 +1,37 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd +import AppStoreConnect_Swift_SDK +import Bagbutik import Foundation import Combine import struct Model.User +import struct Model.UserInvitation import SwiftyTextTable -import AppStoreConnect_Swift_SDK -extension UserInvitation: ResultRenderable { } +extension Model.UserInvitation { + init(_ apiInvitation: Bagbutik.UserInvitation) { + let attributes = apiInvitation.attributes! + + self.init(attributes) + } + + init(_ attributes: Bagbutik.UserInvitation.Attributes) { + self.init( + username: attributes.email!, + firstName: attributes.firstName!, + lastName: attributes.lastName!, + roles: (attributes.roles ?? []).map { .init($0) }, + provisioningAllowed: attributes.provisioningAllowed ?? false, + allAppsVisible: attributes.allAppsVisible ?? false, + expirationDate: attributes.expirationDate! + ) + } + +} + +extension Model.UserInvitation: ResultRenderable { } -extension UserInvitation: TableInfoProvider { +extension Model.UserInvitation: TableInfoProvider { static func tableColumns() -> [TextTableColumn] { return [ TextTableColumn(header: "Email"), @@ -23,19 +46,19 @@ extension UserInvitation: TableInfoProvider { var tableRow: [CustomStringConvertible] { return [ - attributes?.email ?? "", - attributes?.firstName ?? "", - attributes?.lastName ?? "", - attributes?.roles?.map { $0.rawValue }.joined(separator: ", ") ?? "", - attributes?.expirationDate ?? "", - attributes?.provisioningAllowed?.toYesNo() ?? "", - attributes?.allAppsVisible?.toYesNo() ?? "", + username, + firstName ?? "", + lastName ?? "", + roles.map { $0.rawValue }.joined(separator: ", "), + expirationDate, + provisioningAllowed.toYesNo(), + allAppsVisible.toYesNo(), ] } } -extension APIEndpoint where T == UserInvitationResponse { - static func invite(user: User) -> Self { +extension APIEndpoint where T == AppStoreConnect_Swift_SDK.UserInvitationResponse { + static func invite(user: Model.User) -> Self { invite( userWithEmail: user.username, firstName: user.firstName, @@ -47,26 +70,3 @@ extension APIEndpoint where T == UserInvitationResponse { ) } } - -extension AppStoreConnectService { - - /// Find the opaque internal identifier for this invitation; search by email address. - /// - /// This is an App Store Connect internal identifier - func invitationIdentifier(matching email: String) throws -> AnyPublisher { - let endpoint = APIEndpoint.invitedUsers( - filter: [.email([email])] - ) - - return self.request(endpoint) - .map { $0.data.filter { $0.attributes?.email == email } } - .compactMap { response -> String? in - if response.count == 1 { - return response.first?.id - } - fatalError("User with email address '\(email)' not unique or not found") - } - .eraseToAnyPublisher() - } - -} diff --git a/Sources/AppStoreConnectCLI/Model/UserRole.swift b/Sources/AppStoreConnectCLI/Model/UserRole.swift new file mode 100644 index 00000000..eb240584 --- /dev/null +++ b/Sources/AppStoreConnectCLI/Model/UserRole.swift @@ -0,0 +1,114 @@ +// Copyright 2022 Itty Bitty Apps Pty Ltd + +import ArgumentParser +import AppStoreConnect_Swift_SDK +import Bagbutik +import Foundation +import Model + +extension Model.UserRole: ExpressibleByArgument, CustomStringConvertible { + + public init?(argument: String) { + self.init(rawValue: argument.uppercased()) + } + + public var description: String { + rawValue.lowercased() + } + +} + +extension Model.UserRole { + init(_ apiUserRole: Bagbutik.UserRole) { + switch apiUserRole { + case .admin: + self = .admin + case .finance: + self = .finance + case .accountHolder: + self = .accountHolder + case .sales: + self = .sales + case .marketing: + self = .marketing + case .appManager: + self = .appManager + case .developer: + self = .developer + case .accessToReports: + self = .accessToReports + case .customerSupport: + self = .customerSupport + case .imageManager: + self = .imageManager + case .createApps: + self = .createApps + case .cloudManagedDeveloperId: + self = .cloudManagedDeveloperId + case .cloudManagedAppDistribution: + self = .cloudManagedAppDistribution + } + } +} + +extension Bagbutik.UserRole { + init(_ userRole: Model.UserRole) { + switch userRole { + case .admin: + self = .admin + case .finance: + self = .finance + case .accountHolder: + self = .accountHolder + case .sales: + self = .sales + case .marketing: + self = .marketing + case .appManager: + self = .appManager + case .developer: + self = .developer + case .accessToReports: + self = .accessToReports + case .customerSupport: + self = .customerSupport + case .imageManager: + self = .imageManager + case .createApps: + self = .createApps + case .cloudManagedDeveloperId: + self = .cloudManagedDeveloperId + case .cloudManagedAppDistribution: + self = .cloudManagedAppDistribution + } + } +} + +extension AppStoreConnect_Swift_SDK.UserRole { + init(_ userRole: Model.UserRole) { + switch userRole { + case .admin: + self = .admin + case .finance: + self = .finance + case .accountHolder: + self = .accountHolder + case .sales: + self = .sales + case .marketing: + self = .marketing + case .appManager: + self = .appManager + case .developer: + self = .developer + case .accessToReports: + self = .accessToReports + case .customerSupport: + self = .customerSupport + case .cloudManagedAppDistribution: + self = .cloudManagedAppDistribution + default: + fatalError("Unsupported case \(userRole))!") + } + } +} diff --git a/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift b/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift index b6856196..b43a55c9 100644 --- a/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift +++ b/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift @@ -1,32 +1,41 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd import AppStoreConnect_Swift_SDK +import class Bagbutik.BagbutikService +import struct Bagbutik.JWT import Combine +import CollectionConcurrencyKit import Foundation import Model class AppStoreConnectService { + + private let service: BagbutikService private let provider: APIProvider private let requestor: EndpointRequestor - init(configuration: APIConfiguration) { + init(configuration: APIConfiguration, jwt: JWT) { + service = BagbutikService(jwt: jwt) provider = APIProvider(configuration: configuration) requestor = DefaultEndpointRequestor(provider: provider) } - + + /// Find the opaque internal identifier for an application that related to this bundle ID. + func appResourceIdsForBundleIds(_ bundleIds: [String]) async throws -> [String] { + return try await service.requestAllPages(.listAppsV1(filters: [.bundleId(bundleIds)])).data.map(\.id) + } + func listApps( bundleIds: [String] = [], names: [String] = [], skus: [String] = [], limit: Int? = nil - ) throws -> [Model.App] { + ) async throws -> [Model.App] { let operation = ListAppsOperation( options: .init(bundleIds: bundleIds, names: names, skus: skus, limit: limit) ) - let output = try operation.execute(with: requestor).await() - - return output.map(Model.App.init) + return try await operation.execute(with: service).map { Model.App.init($0) } } func listBundleIds( @@ -35,7 +44,7 @@ class AppStoreConnectService { platforms: [String], seedIds: [String], limit: Int? - ) throws -> [Model.BundleId] { + ) async throws -> [Model.BundleId] { let operation = ListBundleIdsOperation(options: .init( identifiers: identifiers, @@ -46,46 +55,45 @@ class AppStoreConnectService { ) ) - return try operation.execute(with: requestor).await().map(Model.BundleId.init) + return try await operation.execute(with: service).map { Model.BundleId.init($0) } } func listUsers( limitVisibleApps: Int?, limitUsers: Int?, - sort: ListUsers.Sort?, + sort: String?, filterUsername: [String], - filterRole: [UserRole], + filterRole: [Model.UserRole], filterVisibleApps: [AppLookupIdentifier], includeVisibleApps: Bool - ) throws -> [Model.User] { - let appIds = try filterVisibleApps.map { identifier -> String in + ) async throws -> [Model.User] { + let appIds = try await filterVisibleApps.asyncMap { identifier -> String in switch identifier { case .appId(let appid): return appid case .bundleId(let bundleId): - return try ReadAppOperation(options: .init(identifier: .bundleId(bundleId))) - .execute(with: requestor) - .await() + return try await ReadAppOperation(options: .init(identifier: .bundleId(bundleId))) + .execute(with: service) .id } } - return try ListUsersOperation( + return try await ListUsersOperation( options: .init( limitVisibleApps: limitVisibleApps, limitUsers: limitUsers, sort: sort, filterUsername: filterUsername, - filterRole: filterRole, + filterRole: filterRole.map(\.rawValue), filterVisibleApps: appIds, includeVisibleApps: includeVisibleApps ) ) - .execute(with: requestor) - .await() + .execute(with: service) + .map { .init($0) } } - func getUserInfo(with email: String, includeVisibleApps: Bool) throws -> Model.User { + func userInfo(with email: String, includeVisibleApps: Bool) throws -> Model.User { try GetUserInfoOperation( options: .init( email: email, @@ -99,7 +107,7 @@ class AppStoreConnectService { func modifyUserInfo( email: String, - roles: [UserRole], + roles: [Model.UserRole], allAppsVisible: Bool, provisioningAllowed: Bool, bundleIds: [String] @@ -114,7 +122,7 @@ class AppStoreConnectService { userId: userId, allAppsVisible: allAppsVisible, provisioningAllowed: provisioningAllowed, - roles: roles, + roles: roles.map { .init($0) }, appsVisibleIds: bundleIds ) ) @@ -208,7 +216,7 @@ class AppStoreConnectService { bundleId: String, groupName: String, emails: [String] - ) throws { + ) async throws { let testerIds = try emails.map { try GetBetaTesterOperation(options: .init(identifier: .email($0))) .execute(with: requestor) @@ -217,9 +225,8 @@ class AppStoreConnectService { .id } - let app = try ReadAppOperation(options: .init(identifier: .bundleId(bundleId))) - .execute(with: requestor) - .await() + let app = try await ReadAppOperation(options: .init(identifier: .bundleId(bundleId))) + .execute(with: service) let groupId = try GetBetaGroupOperation( options: .init(appId: app.id, bundleId: bundleId, betaGroupName: groupName) @@ -241,16 +248,15 @@ class AppStoreConnectService { email: String, bundleId: String, groupNames: [String] - ) throws { + ) async throws { let testerId = try GetBetaTesterOperation(options: .init(identifier: .email(email))) .execute(with: requestor) .await() .betaTester .id - let app = try ReadAppOperation(options: .init(identifier: .bundleId(bundleId))) - .execute(with: requestor) - .await() + let app = try await ReadAppOperation(options: .init(identifier: .bundleId(bundleId))) + .execute(with: service) let groupIds = try groupNames.map { try GetBetaGroupOperation( @@ -370,9 +376,9 @@ class AppStoreConnectService { func listBetaTestersForGroup( identifier: AppLookupIdentifier, groupName: String - ) throws -> [Model.BetaTester] { + ) async throws -> [Model.BetaTester] { let readAppOperation = ReadAppOperation(options: .init(identifier: identifier)) - let app = try readAppOperation.execute(with: requestor).await() + let app = try await readAppOperation.execute(with: service) let getBetaGroupOperation = GetBetaGroupOperation( options: .init(appId: app.id, bundleId: nil, betaGroupName: groupName) @@ -446,10 +452,9 @@ class AppStoreConnectService { try operation.execute(with: requestor).await() } - func readBetaGroup(bundleId: String, groupName: String) throws -> Model.BetaGroup { - let app = try ReadAppOperation(options: .init(identifier: .bundleId(bundleId))) - .execute(with: requestor) - .await() + func readBetaGroup(bundleId: String, groupName: String) async throws -> Model.BetaGroup { + let app = try await ReadAppOperation(options: .init(identifier: .bundleId(bundleId))) + .execute(with: service) let options = GetBetaGroupOperation.Options(appId: app.id, bundleId: bundleId, betaGroupName: groupName) let betaGroup = try GetBetaGroupOperation(options: options) @@ -615,8 +620,8 @@ class AppStoreConnectService { version: String, buildNumber: String, groupNames: [String] - ) throws { - let (buildId, groupIds) = try getBuildIdAndGroupIdsFrom( + ) async throws { + let (buildId, groupIds) = try await buildIdAndGroupIdsFrom( bundleId: bundleId, version: version, buildNumber: buildNumber, @@ -633,8 +638,8 @@ class AppStoreConnectService { version: String, buildNumber: String, groupNames: [String] - ) throws { - let (buildId, groupIds) = try getBuildIdAndGroupIdsFrom( + ) async throws { + let (buildId, groupIds) = try await buildIdAndGroupIdsFrom( bundleId: bundleId, version: version, buildNumber: buildNumber, @@ -646,12 +651,11 @@ class AppStoreConnectService { .await() } - func readApp(identifier: AppLookupIdentifier) throws -> Model.App { - let sdkApp = try ReadAppOperation(options: .init(identifier: identifier)) - .execute(with: requestor) - .await() + func readApp(identifier: AppLookupIdentifier) async throws -> Model.App { + let app = try await ReadAppOperation(options: .init(identifier: identifier)) + .execute(with: service) - return Model.App(sdkApp) + return Model.App(app) } func listPreReleaseVersions( @@ -729,12 +733,11 @@ class AppStoreConnectService { .map(Device.init) } - func listProfilesByBundleId(_ bundleId: String, limit: Int?) throws -> [Model.Profile] { - let bundleIdResourceIds = try ListBundleIdsOperation( + func listProfilesByBundleId(_ bundleId: String, limit: Int?) async throws -> [Model.Profile] { + let bundleIdResourceIds = try await ListBundleIdsOperation( options: .init(identifiers: [bundleId], names: [], platforms: [], seedIds: [], limit: nil) ) - .execute(with: requestor) - .await() + .execute(with: service) .filter { ($0.attributes?.identifier?.starts(with: bundleId) ?? false) } .map(\.id) @@ -786,12 +789,11 @@ class AppStoreConnectService { profileType: ProfileType, certificateSerialNumbers: [String], deviceUDIDs: [String] - ) throws -> Model.Profile { - let bundleIdResourceId = try ReadBundleIdOperation( + ) async throws -> Model.Profile { + let bundleIdResourceId = try await ReadBundleIdOperation( options: .init(bundleId: bundleId) ) - .execute(with: requestor) - .await() + .execute(with: service) .id let deviceIds = try ListDevicesOperation( @@ -840,39 +842,91 @@ class AppStoreConnectService { func listUserInvitaions( filterEmail: [String], - filterRole: [UserRole], + filterRole: [Model.UserRole], limitVisibleApps: Int?, includeVisibleApps: Bool - ) throws -> [UserInvitation] { - try ListUserInvitationsOperation( + ) async throws -> [Model.UserInvitation] { + try await ListUserInvitationsOperation( options: .init( filterEmail: filterEmail, - filterRole: filterRole, + filterRole: filterRole.map { .init($0) }, includeVisibleApps: includeVisibleApps, limitVisibleApps: limitVisibleApps ) ) - .execute(with: requestor) - .await() + .execute(with: service) + .map { + UserInvitation($0) + } } + /// Find the opaque internal identifier for this invitation; search by email address. + /// + /// This is an App Store Connect internal identifier + func invitationIdentifier(matching email: String) async throws -> String { + let invitations = try await service.request(.listUserInvitationsV1(filters: [.email([email])])) + .data + + guard let invitation = invitations.first(where: { $0.attributes?.email == email }) else { + // FIXME: should throw + fatalError("User with email address '\(email)' not unique or not found") + } + + return invitation.id + } + + func cancel(userInvitationWithId identifier: String) async throws { + let _ = try await service.request(.deleteUserInvitationV1(id: identifier)) + } + + func inviteUserToTeam( + email: String, + firstName: String, + lastName: String, + roles: [Model.UserRole], + allAppsVisible: Bool, + provisioningAllowed: Bool, + appsVisibleIds: [String] = [] + ) async throws -> Model.UserInvitation { + + // appsVisibleIds should be empty when allAppsVisible is true + precondition(allAppsVisible && appsVisibleIds.isEmpty) + + let invitation = try await service.request( + .createUserInvitationV1( + requestBody: .init( + data: .init( + attributes: .init( + allAppsVisible: allAppsVisible, + email: email, + firstName: firstName, + lastName: lastName, + provisioningAllowed: provisioningAllowed, + roles: roles.map { .init($0) } + ) + ) + ) + ) + ).data + + return .init(invitation) + } + + func readBundleIdInformation( bundleId: String - ) throws -> Model.BundleId { - try ReadBundleIdOperation( + ) async throws -> Model.BundleId { + Model.BundleId(try await ReadBundleIdOperation( options: .init(bundleId: bundleId) ) - .execute(with: requestor) - .map(Model.BundleId.init) - .await() + .execute(with: service)) } - func modifyBundleIdInformation(bundleId: String, name: String) throws -> Model.BundleId { - let id = try ReadBundleIdOperation( + func modifyBundleIdInformation(bundleId: String, name: String) async throws -> Model.BundleId { + let id = try await ReadBundleIdOperation( options: .init(bundleId: bundleId) ) - .execute(with: requestor) - .await() + .execute(with: service) .id return try ModifyBundleIdOperation(options: .init(resourceId: id, name: name)) @@ -881,12 +935,11 @@ class AppStoreConnectService { .await() } - func deleteBundleId(bundleId: String) throws { - let id = try ReadBundleIdOperation( + func deleteBundleId(bundleId: String) async throws { + let id = try await ReadBundleIdOperation( options: .init(bundleId: bundleId) ) - .execute(with: requestor) - .await() + .execute(with: service) .id try DeleteBundleIdOperation(options: .init(resourceId: id)) @@ -897,12 +950,11 @@ class AppStoreConnectService { func enableBundleIdCapability( bundleId: String, capabilityType: CapabilityType - ) throws { - let bundleIdResourceId = try ReadBundleIdOperation( + ) async throws { + let bundleIdResourceId = try await ReadBundleIdOperation( options: .init(bundleId: bundleId) ) - .execute(with: requestor) - .await() + .execute(with: service) .id _ = try EnableBundleIdCapabilityOperation( @@ -912,12 +964,11 @@ class AppStoreConnectService { .await() } - func disableBundleIdCapability(bundleId: String, capabilityType: CapabilityType) throws { - let bundleIdResourceId = try ReadBundleIdOperation( + func disableBundleIdCapability(bundleId: String, capabilityType: CapabilityType) async throws { + let bundleIdResourceId = try await ReadBundleIdOperation( options: .init(bundleId: bundleId) ) - .execute(with: requestor) - .await() + .execute(with: service) .id let capability = try ListCapabilitiesOperation( @@ -975,8 +1026,8 @@ class AppStoreConnectService { buildNumber: String, preReleaseVersion: String, limit: Int? - ) throws -> [BuildLocalization] { - let buildId = try getBuildIdFrom( + ) async throws -> [BuildLocalization] { + let buildId = try await buildIdFrom( bundleId: bundleId, buildNumber: buildNumber, preReleaseVersion: preReleaseVersion @@ -995,8 +1046,8 @@ class AppStoreConnectService { buildNumber: String, preReleaseVersion: String, locale: String - ) throws -> BuildLocalization { - let buildId = try getBuildIdFrom( + ) async throws -> BuildLocalization { + let buildId = try await buildIdFrom( bundleId: bundleId, buildNumber: buildNumber, preReleaseVersion: preReleaseVersion @@ -1016,8 +1067,8 @@ class AppStoreConnectService { buildNumber: String, preReleaseVersion: String, locale: String - ) throws { - let buildId = try getBuildIdFrom( + ) async throws { + let buildId = try await buildIdFrom( bundleId: bundleId, buildNumber: buildNumber, preReleaseVersion: preReleaseVersion @@ -1043,8 +1094,8 @@ class AppStoreConnectService { preReleaseVersion: String, locale: String, whatsNew: String - ) throws -> BuildLocalization { - let buildId = try getBuildIdFrom( + ) async throws -> BuildLocalization { + let buildId = try await buildIdFrom( bundleId: bundleId, buildNumber: buildNumber, preReleaseVersion: preReleaseVersion @@ -1065,8 +1116,8 @@ class AppStoreConnectService { preReleaseVersion: String, locale: String, whatsNew: String - ) throws -> BuildLocalization { - let buildId = try getBuildIdFrom( + ) async throws -> BuildLocalization { + let buildId = try await buildIdFrom( bundleId: bundleId, buildNumber: buildNumber, preReleaseVersion: preReleaseVersion @@ -1091,9 +1142,9 @@ class AppStoreConnectService { ) } - func getTestFlightProgram(bundleIds: [String] = []) throws -> TestFlightProgram { + func getTestFlightProgram(bundleIds: [String] = []) async throws -> TestFlightProgram { let appsOperation = ListAppsOperation(options: .init(bundleIds: bundleIds)) - let apps = try appsOperation.execute(with: requestor).await() + let apps = try await appsOperation.execute(with: service) let appIds = apps.map(\.id) // Passing appIds can cause undefined API behaviour for list beta testers so we retrieve all @@ -1154,15 +1205,14 @@ class AppStoreConnectService { extension AppStoreConnectService { - func getBuildIdAndGroupIdsFrom( + func buildIdAndGroupIdsFrom( bundleId: String, version: String, buildNumber: String, groupNames: [String] - ) throws -> (buildId: String, groupIds: [String]) { - let appId = try ReadAppOperation(options: .init(identifier: .bundleId(bundleId))) - .execute(with: requestor) - .await() + ) async throws -> (buildId: String, groupIds: [String]) { + let appId = try await ReadAppOperation(options: .init(identifier: .bundleId(bundleId))) + .execute(with: service) .id let buildId = try ReadBuildOperation( @@ -1190,14 +1240,13 @@ extension AppStoreConnectService { return (buildId, groupIds) } - private func getBuildIdFrom( + private func buildIdFrom( bundleId: String, buildNumber: String, preReleaseVersion: String - ) throws -> String { - let appId = try ReadAppOperation(options: .init(identifier: .bundleId(bundleId))) - .execute(with: requestor) - .await() + ) async throws -> String { + let appId = try await ReadAppOperation(options: .init(identifier: .bundleId(bundleId))) + .execute(with: service) .id return try ReadBuildOperation( diff --git a/Sources/AppStoreConnectCLI/Services/EndpointRequestor.swift b/Sources/AppStoreConnectCLI/Services/EndpointRequestor.swift index aaaf412e..3b05d6eb 100644 --- a/Sources/AppStoreConnectCLI/Services/EndpointRequestor.swift +++ b/Sources/AppStoreConnectCLI/Services/EndpointRequestor.swift @@ -7,11 +7,30 @@ import Foundation protocol EndpointRequestor { func request(_ endpoint: APIEndpoint) -> Future func request(_ endpoint: APIEndpoint) -> Future + + func request(_ endpoint: APIEndpoint) async throws -> T + func request(_ endpoint: APIEndpoint) async throws } struct DefaultEndpointRequestor: EndpointRequestor { let provider: APIProvider + func request(_ endpoint: APIEndpoint) async throws -> T where T : Decodable { + try await withCheckedThrowingContinuation { cont in + provider.request(endpoint) { result in + cont.resume(with: result) + } + } + } + + func request(_ endpoint: APIEndpoint) async throws { + try await withCheckedThrowingContinuation { cont in + provider.request(endpoint) { result in + cont.resume(with: result) + } + } + } + func request(_ endpoint: APIEndpoint) -> Future { Future { [provider] promise in provider.request(endpoint, completion: promise) diff --git a/Sources/AppStoreConnectCLI/Services/Operations/APIOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/APIOperation.swift index 0b026ad8..6a85b09a 100644 --- a/Sources/AppStoreConnectCLI/Services/Operations/APIOperation.swift +++ b/Sources/AppStoreConnectCLI/Services/Operations/APIOperation.swift @@ -11,3 +11,13 @@ protocol APIOperation { func execute(with requestor: EndpointRequestor) throws -> AnyPublisher } + +protocol APIOperationV2 { + associatedtype Options + associatedtype Output + associatedtype Service + + init(options: Options) + + func execute(with service: Service) async throws -> Output +} diff --git a/Sources/AppStoreConnectCLI/Services/Operations/ListAppsOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/ListAppsOperation.swift index 12667294..3ab1a721 100644 --- a/Sources/AppStoreConnectCLI/Services/Operations/ListAppsOperation.swift +++ b/Sources/AppStoreConnectCLI/Services/Operations/ListAppsOperation.swift @@ -1,9 +1,9 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK +import Bagbutik import Combine -struct ListAppsOperation: APIOperation { +struct ListAppsOperation: APIOperationV2 { struct Options { var bundleIds: [String] = [] @@ -17,31 +17,23 @@ struct ListAppsOperation: APIOperation { init(options: Options) { self.options = options } - - typealias App = AppStoreConnect_Swift_SDK.App - - func execute(with requestor: EndpointRequestor) -> AnyPublisher<[App], Error> { - var filters: [ListApps.Filter] = [] + + func execute(with service: BagbutikService) async throws -> [App] { + var filters: [ListAppsV1.Filter] = [] if options.bundleIds.isNotEmpty { filters.append(.bundleId(options.bundleIds)) } if options.names.isNotEmpty { filters.append(.name(options.names)) } if options.skus.isNotEmpty { filters.append(.sku(options.skus)) } - let limits = options.limit.map { [ListApps.Limit.apps($0)] } - + let limits = options.limit.map { [ListAppsV1.Limit.limit($0)] } + guard limits != nil else { - return requestor.requestAllPages { - .apps(filters: filters, next: $0) - } - .map { $0.flatMap(\.data) } - .eraseToAnyPublisher() + return try await service.requestAllPages(.listAppsV1(filters: filters)).data } - return requestor.request(.apps(filters: filters, limits: limits)) - .map(\.data) - .eraseToAnyPublisher() + return try await service + .request(.listAppsV1(filters: filters, limits: limits)) + .data } } - -extension AppsResponse: PaginatedResponse { } diff --git a/Sources/AppStoreConnectCLI/Services/Operations/ListBundleIdsOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/ListBundleIdsOperation.swift index 2727ea92..85835e4d 100644 --- a/Sources/AppStoreConnectCLI/Services/Operations/ListBundleIdsOperation.swift +++ b/Sources/AppStoreConnectCLI/Services/Operations/ListBundleIdsOperation.swift @@ -1,9 +1,9 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK +import Bagbutik import Combine -struct ListBundleIdsOperation: APIOperation { +struct ListBundleIdsOperation: APIOperationV2 { struct Options { let identifiers: [String] let names: [String] @@ -18,12 +18,10 @@ struct ListBundleIdsOperation: APIOperation { self.options = options } - typealias BundleId = AppStoreConnect_Swift_SDK.BundleId + func execute(with service: BagbutikService) async throws -> [BundleId] { + let platforms = options.platforms.compactMap(ListBundleIdsV1.Filter.Platform.init(rawValue:)) - func execute(with requestor: EndpointRequestor) throws -> AnyPublisher<[BundleId], Error> { - let platforms = options.platforms.compactMap(Platform.init(rawValue:)) - - var filters: [BundleIds.Filter] = [] + var filters: [ListBundleIdsV1.Filter] = [] if options.identifiers.isNotEmpty { filters.append(.identifier(options.identifiers)) } if options.names.isNotEmpty { filters.append(.name(options.names)) } @@ -31,19 +29,9 @@ struct ListBundleIdsOperation: APIOperation { if options.seedIds.isNotEmpty { filters.append(.seedId(options.seedIds)) } guard let limit = options.limit else { - return requestor.requestAllPages { - .listBundleIds(filter: filters, next: $0) - } - .map { $0.flatMap(\.data) } - .eraseToAnyPublisher() + return try await service.requestAllPages(.listBundleIdsV1(filters: filters)).data } - return requestor.request( - .listBundleIds(filter: filters, limit: limit) - ) - .map(\.data) - .eraseToAnyPublisher() + return try await service.request(.listBundleIdsV1(filters: filters, limits: [.limit(limit)])).data } } - -extension BundleIdsResponse: PaginatedResponse { } diff --git a/Sources/AppStoreConnectCLI/Services/Operations/ListUserInvitationsOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/ListUserInvitationsOperation.swift index 15ced378..dca8fc7a 100644 --- a/Sources/AppStoreConnectCLI/Services/Operations/ListUserInvitationsOperation.swift +++ b/Sources/AppStoreConnectCLI/Services/Operations/ListUserInvitationsOperation.swift @@ -1,14 +1,14 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK +import Bagbutik import Combine import Foundation -struct ListUserInvitationsOperation: APIOperation { +struct ListUserInvitationsOperation: APIOperationV2 { struct Options { let filterEmail: [String] - let filterRole: [UserRole] + let filterRole: [ListUserInvitationsV1.Filter.Roles] let includeVisibleApps: Bool let limitVisibleApps: Int? } @@ -19,24 +19,15 @@ struct ListUserInvitationsOperation: APIOperation { self.options = options } - func execute(with requestor: EndpointRequestor) throws -> AnyPublisher<[UserInvitation], Error> { - var filters = [ListInvitedUsers.Filter]() + func execute(with service: BagbutikService) async throws -> [UserInvitation] { + var filters = [ListUserInvitationsV1.Filter]() if options.filterEmail.isNotEmpty { filters.append(.email(options.filterEmail)) } - if options.filterRole.isNotEmpty { filters.append(.roles(options.filterRole.map { $0.rawValue })) } - - let limit = options.limitVisibleApps.map { [ListInvitedUsers.Limit.visibleApps($0)] } - - return requestor.requestAllPages { - .invitedUsers( - limit: limit, - filter: filters, - next: $0 - ) - } - .map { $0.flatMap { $0.data } } - .eraseToAnyPublisher() + if options.filterRole.isNotEmpty { filters.append(.roles(options.filterRole)) } + + let limits = options.limitVisibleApps.map { [ListUserInvitationsV1.Limit.visibleApps($0)] } + + return try await service.requestAllPages(.listUserInvitationsV1(filters: filters, limits: limits)).data } } -extension UserInvitationsResponse: PaginatedResponse { } diff --git a/Sources/AppStoreConnectCLI/Services/Operations/ListUsersOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/ListUsersOperation.swift index 0a85f51c..0c2c8d42 100644 --- a/Sources/AppStoreConnectCLI/Services/Operations/ListUsersOperation.swift +++ b/Sources/AppStoreConnectCLI/Services/Operations/ListUsersOperation.swift @@ -1,67 +1,53 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK -import Combine +import Bagbutik import struct Model.User -struct ListUsersOperation: APIOperation { - - typealias Filter = ListUsers.Filter - typealias Limit = ListUsers.Limit - typealias Include = ListUsers.Include +struct ListUsersOperation: APIOperationV2 { + typealias Output = [Bagbutik.User] + + typealias Filter = ListUsersV1.Filter + typealias Limit = ListUsersV1.Limit + typealias Include = ListUsersV1.Include struct Options { let limitVisibleApps: Int? let limitUsers: Int? - let sort: ListUsers.Sort? + let sort: String? let filterUsername: [String] - let filterRole: [UserRole] + let filterRole: [String] let filterVisibleApps: [String] let includeVisibleApps: Bool } - var limit: [Limit]? { - [options.limitUsers.map(Limit.users), options.limitVisibleApps.map(Limit.visibleApps)] + private var limits: [Limit]? { + [options.limitUsers.map(Limit.limit), options.limitVisibleApps.map(Limit.visibleApps)] .compactMap { $0 } .nilIfEmpty() } - var include: [Include]? { - options.includeVisibleApps ? [ListUsers.Include.visibleApps] : nil + private var includes: [Include]? { + options.includeVisibleApps ? [ListUsersV1.Include.visibleApps] : nil } - var filter: [Filter]? { - let roles = options.filterRole.map(\.rawValue).nilIfEmpty().map(Filter.roles) + private var filters: [Filter]? { + let roles = options.filterRole.compactMap(Filter.Roles.init(rawValue:)).nilIfEmpty().map(Filter.roles) let usernames = options.filterUsername.nilIfEmpty().map(Filter.username) let visibleApps = options.filterVisibleApps.nilIfEmpty().map(Filter.visibleApps) return [roles, usernames, visibleApps].compactMap({ $0 }).nilIfEmpty() } - let options: Options + private let options: Options init(options: Options) { self.options = options } - func execute(with requestor: EndpointRequestor) -> AnyPublisher<[User], Error> { - let include = self.include - let limit = self.limit - let sort = options.sort.map { [$0] } - let filter = self.filter + func execute(with service: BagbutikService) async throws -> Output { + let sorts = options.sort.flatMap { ListUsersV1.Sort(rawValue: $0) }.map { [$0] } - return requestor.requestAllPages { - .users( - include: include, - limit: limit, - sort: sort, - filter: filter, - next: $0 - ) - } - .map { $0.flatMap(User.fromAPIResponse) } - .eraseToAnyPublisher() + return try await service.requestAllPages(.listUsersV1(filters: filters, includes: includes, sorts: sorts, limits: limits)).data } } -extension UsersResponse: PaginatedResponse { } diff --git a/Sources/AppStoreConnectCLI/Services/Operations/ReadAppOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/ReadAppOperation.swift index 1c59ee35..508cd43e 100644 --- a/Sources/AppStoreConnectCLI/Services/Operations/ReadAppOperation.swift +++ b/Sources/AppStoreConnectCLI/Services/Operations/ReadAppOperation.swift @@ -1,11 +1,11 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK -import Combine +import Bagbutik import Foundation -struct ReadAppOperation: APIOperation { - +struct ReadAppOperation: APIOperationV2 { + typealias Output = App + struct Options { let identifier: AppLookupIdentifier } @@ -24,37 +24,30 @@ struct ReadAppOperation: APIOperation { } } - typealias App = AppStoreConnect_Swift_SDK.App - private let options: Options init(options: Options) { self.options = options } - func execute(with requestor: EndpointRequestor) -> AnyPublisher { - let result: AnyPublisher - + func execute(with service: BagbutikService) async throws -> Output { + let result: App + switch options.identifier { case .appId(let appId): - result = requestor.request(.app(withId: appId)) - .map(\.data) - .eraseToAnyPublisher() + result = try await service.request(.getAppV1(id: appId)).data + case .bundleId(let bundleId): - let endpoint: APIEndpoint = .apps(filters: [.bundleId([bundleId])]) - - result = requestor.request(endpoint) - .tryMap { (response: AppsResponse) throws -> App in - switch response.data.count { - case 0: - throw Error.notFound(bundleId) - case 1: - return response.data.first! - default: - throw Error.notUnique(bundleId) - } - } - .eraseToAnyPublisher() + let data = try await service.requestAllPages(.listAppsV1(filters: [.bundleId([bundleId])])).data + + switch data.count { + case 0: + throw Error.notFound(bundleId) + case 1: + result = data.first! + default: + throw Error.notUnique(bundleId) + } } return result diff --git a/Sources/AppStoreConnectCLI/Services/Operations/ReadBundleIdOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/ReadBundleIdOperation.swift index 94e979f1..ca3c1385 100644 --- a/Sources/AppStoreConnectCLI/Services/Operations/ReadBundleIdOperation.swift +++ b/Sources/AppStoreConnectCLI/Services/Operations/ReadBundleIdOperation.swift @@ -1,11 +1,12 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK -import Combine +import Bagbutik import Foundation +import Model -struct ReadBundleIdOperation: APIOperation { - +struct ReadBundleIdOperation: APIOperationV2 { + typealias Output = Bagbutik.BundleId + struct Options { let bundleId: String } @@ -30,24 +31,21 @@ struct ReadBundleIdOperation: APIOperation { self.options = options } - func execute(with requestor: EndpointRequestor) -> AnyPublisher { - requestor.request( - .listBundleIds( - filter: [.identifier([options.bundleId])] - ) + func execute(with service: BagbutikService) async throws -> Output { + let bundleIds = try await service.request( + .listBundleIdsV1(filters: [.identifier([options.bundleId])]) ) - .tryMap { - let data = $0.data.filter { $0.attributes?.identifier == self.options.bundleId } - switch data.count { - case 0: - throw Error.couldNotFindBundleId(self.options.bundleId) - case 1: - return data.first! - default: - throw Error.bundleIdNotUnique(self.options.bundleId) - } + .data + .filter { $0.attributes?.identifier == self.options.bundleId } + + switch bundleIds.count { + case 0: + throw Error.couldNotFindBundleId(self.options.bundleId) + case 1: + return bundleIds.first! + default: + throw Error.bundleIdNotUnique(self.options.bundleId) } - .eraseToAnyPublisher() } - + } diff --git a/Sources/Model/UserInvitation.swift b/Sources/Model/UserInvitation.swift new file mode 100644 index 00000000..d1881e43 --- /dev/null +++ b/Sources/Model/UserInvitation.swift @@ -0,0 +1,31 @@ +// Copyright 2022 Itty Bitty Apps Pty Ltd + +import Foundation + +public struct UserInvitation: Codable, Equatable { + public let username: String + public let firstName: String? + public let lastName: String? + public let roles: [UserRole] + public let provisioningAllowed: Bool + public let allAppsVisible: Bool + public let expirationDate: Date + + public init( + username: String, + firstName: String, + lastName: String, + roles: [UserRole], + provisioningAllowed: Bool, + allAppsVisible: Bool, + expirationDate: Date + ) { + self.username = username + self.firstName = firstName + self.lastName = lastName + self.roles = roles + self.provisioningAllowed = provisioningAllowed + self.allAppsVisible = allAppsVisible + self.expirationDate = expirationDate + } +} diff --git a/Sources/Model/UserRole.swift b/Sources/Model/UserRole.swift new file mode 100644 index 00000000..db5a9da6 --- /dev/null +++ b/Sources/Model/UserRole.swift @@ -0,0 +1,30 @@ +// Copyright 2022 Itty Bitty Apps Pty Ltd + +import Foundation +import ArgumentParser + +/// Represents a user's role. +public enum UserRole: String, CaseIterable, Codable, Equatable { + /// Serves as a secondary contact for teams and has many of the same responsibilities as the Account Holder role. Admins have access to all apps. + case admin = "ADMIN" + /// Manages financial information, including reports and tax forms. A user assigned this role can view all apps in Payments and Financial Reports, Sales and Trends, and App Analytics. + case finance = "FINANCE" + /// Responsible for entering into legal agreements with Apple. The person who completes program enrollment is assigned the Account Holder role in both the Apple Developer account and App Store Connect. + case accountHolder = "ACCOUNT_HOLDER" + /// Analyzes sales, downloads, and other analytics for the app. + case sales = "SALES" + /// Manages marketing materials and promotional artwork. A user assigned this role will be contacted by Apple if the app is in consideration to be featured on the App Store. + case marketing = "MARKETING" + /// Manages all aspects of an app, such as pricing, App Store information, and app development and delivery. + case appManager = "APP_MANAGER" + /// Manages development and delivery of an app. + case developer = "DEVELOPER" + /// Downloads reports associated with a role. The Access To Reports role is an additional permission for users with the App Manager, Developer, Marketing, or Sales role. If this permission is added, the user has access to all of your apps. + case accessToReports = "ACCESS_TO_REPORTS" + /// Analyzes and responds to customer reviews on the App Store. If a user has only the Customer Support role, they'll go straight to the Ratings and Reviews section when they click on an app in My Apps. + case customerSupport = "CUSTOMER_SUPPORT" + case imageManager = "IMAGE_MANAGER" + case createApps = "CREATE_APPS" + case cloudManagedDeveloperId = "CLOUD_MANAGED_DEVELOPER_ID" + case cloudManagedAppDistribution = "CLOUD_MANAGED_APP_DISTRIBUTION" +} diff --git a/Sources/appstoreconnect-cli/EntryPoint.swift b/Sources/appstoreconnect-cli/EntryPoint.swift new file mode 100644 index 00000000..28c9d803 --- /dev/null +++ b/Sources/appstoreconnect-cli/EntryPoint.swift @@ -0,0 +1,7 @@ +import AppStoreConnectCLI + +@main struct EntryPoint { + static func main() async { + await AppStoreConnectCLI.main() + } +} diff --git a/Sources/appstoreconnect-cli/main.swift b/Sources/appstoreconnect-cli/main.swift deleted file mode 100644 index bf0dd8f9..00000000 --- a/Sources/appstoreconnect-cli/main.swift +++ /dev/null @@ -1,3 +0,0 @@ -import AppStoreConnectCLI - -AppStoreConnectCLI.main() diff --git a/Tests/appstoreconnect-cliTests/Operations/TestRequestors.swift b/Tests/appstoreconnect-cliTests/Operations/TestRequestors.swift index e0650fa7..97cdf05d 100644 --- a/Tests/appstoreconnect-cliTests/Operations/TestRequestors.swift +++ b/Tests/appstoreconnect-cliTests/Operations/TestRequestors.swift @@ -10,9 +10,17 @@ extension EndpointRequestor { Future { $0(.failure(TestError.somethingBadHappened)) } } + func request(_ endpoint: APIEndpoint) async throws -> T where T : Decodable { + throw TestError.somethingBadHappened + } + func request(_ endpoint: APIEndpoint) -> Future { Future { $0(.failure(TestError.somethingBadHappened)) } } + + func request(_ endpoint: APIEndpoint) async throws { + throw TestError.somethingBadHappened + } } struct OneEndpointTestRequestor: EndpointRequestor { From d8650408479249c55388aa1911e296f1d10d84af Mon Sep 17 00:00:00 2001 From: Oliver Jones Date: Tue, 1 Nov 2022 12:07:23 +1300 Subject: [PATCH 02/12] Make return implicit --- Sources/AppStoreConnectCLI/Arguments/APIKeyID.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AppStoreConnectCLI/Arguments/APIKeyID.swift b/Sources/AppStoreConnectCLI/Arguments/APIKeyID.swift index 7b786b47..06dba264 100644 --- a/Sources/AppStoreConnectCLI/Arguments/APIKeyID.swift +++ b/Sources/AppStoreConnectCLI/Arguments/APIKeyID.swift @@ -31,7 +31,7 @@ struct APIKeyID: EnvironmentLoadableArgument { } func load() throws -> String { - return loadPEM() + try loadPEM() .components(separatedBy: .newlines) .filter { $0.hasSuffix("PRIVATE KEY-----") == false } .joined() From 09eccc5c5493a62452cd9fbabaaca0aa77fd61bf Mon Sep 17 00:00:00 2001 From: Oliver Jones Date: Tue, 1 Nov 2022 12:07:53 +1300 Subject: [PATCH 03/12] Update packages --- Package.resolved | 79 +++++------------------------------------------- 1 file changed, 8 insertions(+), 71 deletions(-) diff --git a/Package.resolved b/Package.resolved index 14be877b..89a62ab2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/MortenGregersen/Bagbutik.git", "state" : { - "revision" : "a0752c08c0f3a20f6648f75be0b7267f96486e67", - "version" : "2.0.0" + "revision" : "e053914690fada30d9169fbe6bb3f0906b0824a8", + "version" : "2.1.2" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/dehesa/CodableCSV.git", "state" : { - "revision" : "2db5f560db5f8c3444f3a00fa145aa699efac3bf", - "version" : "0.5.5" + "revision" : "99ace2cfdfbc19108b529c021005fb57a460e715", + "version" : "0.6.7" } }, { @@ -41,71 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/johnsundell/files.git", "state" : { - "revision" : "22fe84797d499ffca911ccd896b34efaf06a50b9", - "version" : "4.1.1" - } - }, - { - "identity" : "komondor", - "kind" : "remoteSourceControl", - "location" : "https://github.com/shibapm/Komondor.git", - "state" : { - "revision" : "90b087b1e39069684b1ff4bf915c2aae594f2d60", - "version" : "1.1.3" - } - }, - { - "identity" : "packageconfig", - "kind" : "remoteSourceControl", - "location" : "https://github.com/shibapm/PackageConfig.git", - "state" : { - "revision" : "58523193c26fb821ed1720dcd8a21009055c7cdb", - "version" : "1.1.3" - } - }, - { - "identity" : "pathkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kylef/PathKit.git", - "state" : { - "revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574", - "version" : "1.0.1" - } - }, - { - "identity" : "shellout", - "kind" : "remoteSourceControl", - "location" : "https://github.com/JohnSundell/ShellOut.git", - "state" : { - "revision" : "e1577acf2b6e90086d01a6d5e2b8efdaae033568", - "version" : "2.3.0" - } - }, - { - "identity" : "spectre", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kylef/Spectre.git", - "state" : { - "revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7", - "version" : "0.10.1" - } - }, - { - "identity" : "stencil", - "kind" : "remoteSourceControl", - "location" : "https://github.com/stencilproject/Stencil.git", - "state" : { - "revision" : "ccd9402682f4c07dac9561befd207c8156e80e20", - "version" : "0.14.2" - } - }, - { - "identity" : "stencilswiftkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftGen/StencilSwiftKit", - "state" : { - "revision" : "54cbedcdbb4334e03930adcff7343ffaf317bf0f", - "version" : "2.8.0" + "revision" : "d273b5b7025d386feef79ef6bad7de762e106eaf", + "version" : "4.2.0" } }, { @@ -131,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nicklockwood/SwiftFormat", "state" : { - "revision" : "665c3c58923ee8ac36d3e44b17dc185229cce301", - "version" : "0.49.13" + "revision" : "3a5a4b9baa2ad2f6bb528351807eefbf3b9c0786", + "version" : "0.50.3" } }, { From 020774f1a387decbb6343df4241465639c3480fc Mon Sep 17 00:00:00 2001 From: Oliver Jones Date: Tue, 1 Nov 2022 12:10:54 +1300 Subject: [PATCH 04/12] Add BagbutikService initialiser --- .../Commands/CommonParsableCommand.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/AppStoreConnectCLI/Commands/CommonParsableCommand.swift b/Sources/AppStoreConnectCLI/Commands/CommonParsableCommand.swift index c3bc3a39..210dc82a 100755 --- a/Sources/AppStoreConnectCLI/Commands/CommonParsableCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/CommonParsableCommand.swift @@ -3,6 +3,7 @@ import AppStoreConnect_Swift_SDK import ArgumentParser import Foundation +import Bagbutik protocol CommonParsableCommand: AsyncParsableCommand { var common: CommonOptions { get } @@ -24,6 +25,12 @@ extension CommonParsableCommand { } } +extension BagbutikService { + convenience init(authOptions: AuthOptions) throws { + try self.init(jwt: .init(authOptions)) + } +} + struct CommonOptions: ParsableArguments { @OptionGroup() var authOptions: AuthOptions From c23afcd0e5a13e75c73e67191a92fb994acd659a Mon Sep 17 00:00:00 2001 From: Oliver Jones Date: Tue, 1 Nov 2022 12:12:11 +1300 Subject: [PATCH 05/12] Convert List BundleIds command to use Bagbutik --- .../BundleIds/ListBundleIdsCommand.swift | 31 +++++++++++-------- .../Operations/ListBundleIdsOperation.swift | 19 ++++++------ 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/Sources/AppStoreConnectCLI/Commands/BundleIds/ListBundleIdsCommand.swift b/Sources/AppStoreConnectCLI/Commands/BundleIds/ListBundleIdsCommand.swift index 2166aceb..9682bd73 100644 --- a/Sources/AppStoreConnectCLI/Commands/BundleIds/ListBundleIdsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/BundleIds/ListBundleIdsCommand.swift @@ -1,9 +1,11 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK import ArgumentParser +import Model struct ListBundleIdsCommand: CommonParsableCommand { + typealias Platform = ListBundleIdsOperation.Options.Platform + public static var configuration = CommandConfiguration( commandName: "list", abstract: "Find and list bundle IDs that are registered to your team." @@ -26,24 +28,27 @@ struct ListBundleIdsCommand: CommonParsableCommand { @Option( parsing: .upToNextOption, - help: "Filter the results by platform (\(Platform.allCases.description))." + help: ArgumentHelp("Filter the results by platform. One of \(Platform.allValueStrings.formatted(.list(type: .or)))."), + completion: .list(Platform.allValueStrings) ) - var filterPlatform: [String] = [] + var filterPlatform: [Platform] = [] @Option(parsing: .upToNextOption, help: "Filter the results by seed ID") var filterSeedId: [String] = [] func run() async throws { - let service = try makeService() - - let bundleIds = try await service.listBundleIds( - identifiers: filterIdentifier, - names: filterName, - platforms: filterPlatform, - seedIds: filterSeedId, - limit: limit + try await ListBundleIdsOperation( + service: .init(authOptions: common.authOptions), + options: .init( + identifiers: filterIdentifier, + names: filterName, + platforms: filterPlatform, + seedIds: filterSeedId, + limit: limit + ) ) - - bundleIds.render(options: common.outputOptions) + .execute() + .map { Model.BundleId($0) } + .render(options: common.outputOptions) } } diff --git a/Sources/AppStoreConnectCLI/Services/Operations/ListBundleIdsOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/ListBundleIdsOperation.swift index 85835e4d..0fa502aa 100644 --- a/Sources/AppStoreConnectCLI/Services/Operations/ListBundleIdsOperation.swift +++ b/Sources/AppStoreConnectCLI/Services/Operations/ListBundleIdsOperation.swift @@ -1,31 +1,32 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd import Bagbutik -import Combine struct ListBundleIdsOperation: APIOperationV2 { struct Options { + typealias Platform = ListBundleIdsV1.Filter.Platform + let identifiers: [String] let names: [String] - let platforms: [String] + let platforms: [Platform] let seedIds: [String] let limit: Int? } + private let service: BagbutikService private let options: Options - - init(options: Options) { + + init(service: BagbutikService, options: Options) { + self.service = service self.options = options } - - func execute(with service: BagbutikService) async throws -> [BundleId] { - let platforms = options.platforms.compactMap(ListBundleIdsV1.Filter.Platform.init(rawValue:)) - + + func execute() async throws -> [BundleId] { var filters: [ListBundleIdsV1.Filter] = [] if options.identifiers.isNotEmpty { filters.append(.identifier(options.identifiers)) } if options.names.isNotEmpty { filters.append(.name(options.names)) } - if options.platforms.isNotEmpty { filters.append(.platform(platforms)) } + if options.platforms.isNotEmpty { filters.append(.platform(options.platforms)) } if options.seedIds.isNotEmpty { filters.append(.seedId(options.seedIds)) } guard let limit = options.limit else { From fcbc46275b03134d8099e2d5e4dee355b8acb88a Mon Sep 17 00:00:00 2001 From: Oliver Jones Date: Tue, 1 Nov 2022 12:28:15 +1300 Subject: [PATCH 06/12] Update DeleteBundleIdCommand to use Bagbutik --- .../Commands/BundleIds/DeleteBundleIdCommand.swift | 14 +++++++++++--- .../Services/AppStoreConnectService.swift | 12 ------------ 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/Sources/AppStoreConnectCLI/Commands/BundleIds/DeleteBundleIdCommand.swift b/Sources/AppStoreConnectCLI/Commands/BundleIds/DeleteBundleIdCommand.swift index 32c9c3ac..ccf1cb20 100644 --- a/Sources/AppStoreConnectCLI/Commands/BundleIds/DeleteBundleIdCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/BundleIds/DeleteBundleIdCommand.swift @@ -1,7 +1,7 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK import ArgumentParser +import Bagbutik import Combine import Foundation @@ -19,8 +19,16 @@ struct DeleteBundleIdCommand: CommonParsableCommand { var identifier: String func run() async throws { - let service = try makeService() + let service = try BagbutikService(authOptions: common.authOptions) + let bundleId = try await ReadBundleIdOperation( + service: service, + options: .init(bundleId: identifier) + ) + .execute() - try await service.deleteBundleId(bundleId: identifier) + try await DeleteBundleIdOperation( + service: service, + options: .init(resourceId: bundleId.id)) + .execute() } } diff --git a/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift b/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift index b43a55c9..b881e80a 100644 --- a/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift +++ b/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift @@ -935,18 +935,6 @@ class AppStoreConnectService { .await() } - func deleteBundleId(bundleId: String) async throws { - let id = try await ReadBundleIdOperation( - options: .init(bundleId: bundleId) - ) - .execute(with: service) - .id - - try DeleteBundleIdOperation(options: .init(resourceId: id)) - .execute(with: requestor) - .await() - } - func enableBundleIdCapability( bundleId: String, capabilityType: CapabilityType From 1266fe3d9d939573eb3eba0ca3ef176b0beaf2ff Mon Sep 17 00:00:00 2001 From: Oliver Jones Date: Tue, 1 Nov 2022 12:48:28 +1300 Subject: [PATCH 07/12] Update ModifyBundleIdCommand to use Bagbutik --- .../BundleIds/ModifyBundleIdCommand.swift | 24 +++++++++---- .../Operations/ModifyBundleIdOperation.swift | 34 +++++++++++-------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/Sources/AppStoreConnectCLI/Commands/BundleIds/ModifyBundleIdCommand.swift b/Sources/AppStoreConnectCLI/Commands/BundleIds/ModifyBundleIdCommand.swift index aa9e22c7..3c15d5ee 100644 --- a/Sources/AppStoreConnectCLI/Commands/BundleIds/ModifyBundleIdCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/BundleIds/ModifyBundleIdCommand.swift @@ -1,7 +1,7 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK import ArgumentParser +import Bagbutik import Combine import Foundation import struct Model.BundleId @@ -23,11 +23,21 @@ struct ModifyBundleIdCommand: CommonParsableCommand { var name: String func run() async throws { - let service = try makeService() - - let bundleId = try await service - .modifyBundleIdInformation(bundleId: identifier, name: name) - - bundleId.render(options: common.outputOptions) + let service = try BagbutikService(authOptions: common.authOptions) + let bundleId = try await ReadBundleIdOperation( + service: service, + options: .init(bundleId: identifier) + ) + .execute() + + let result = Model.BundleId( + try await ModifyBundleIdOperation( + service: service, + options: .init(resourceId: bundleId.id, name: name) + ) + .execute() + ) + + result.render(options: common.outputOptions) } } diff --git a/Sources/AppStoreConnectCLI/Services/Operations/ModifyBundleIdOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/ModifyBundleIdOperation.swift index f024e9b7..7710199e 100644 --- a/Sources/AppStoreConnectCLI/Services/Operations/ModifyBundleIdOperation.swift +++ b/Sources/AppStoreConnectCLI/Services/Operations/ModifyBundleIdOperation.swift @@ -1,32 +1,38 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK +import Bagbutik import Combine import Foundation -struct ModifyBundleIdOperation: APIOperation { +struct ModifyBundleIdOperation: APIOperationV2 { + typealias Output = Bagbutik.BundleId + struct Options { let resourceId: String let name: String } + private let service: BagbutikService private let options: Options - - init(options: Options) { + + init(service: BagbutikService, options: Options) { + self.service = service self.options = options } - var endpoint: APIEndpoint { - .modifyBundleId( - id: options.resourceId, - name: options.name + func execute() async throws -> Output { + try await service.request( + .updateBundleIdV1( + id: options.resourceId, + requestBody: BundleIdUpdateRequest( + data: .init( + id: options.resourceId, + attributes: .init(name: options.name) + ) + ) + ) ) - } - - func execute(with requestor: EndpointRequestor) -> AnyPublisher { - requestor - .request(endpoint) - .eraseToAnyPublisher() + .data } } From 5eaf4545e9ea3b79da510e379db78c93a3ae032f Mon Sep 17 00:00:00 2001 From: Oliver Jones Date: Tue, 1 Nov 2022 12:50:32 +1300 Subject: [PATCH 08/12] Update ReadBundleIdOperation to be consistent with others --- .../Services/AppStoreConnectService.swift | 27 +++++++------------ .../Operations/ReadBundleIdOperation.swift | 6 +++-- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift b/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift index b881e80a..974fe3ea 100644 --- a/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift +++ b/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift @@ -791,9 +791,10 @@ class AppStoreConnectService { deviceUDIDs: [String] ) async throws -> Model.Profile { let bundleIdResourceId = try await ReadBundleIdOperation( + service: service, options: .init(bundleId: bundleId) ) - .execute(with: service) + .execute() .id let deviceIds = try ListDevicesOperation( @@ -916,23 +917,13 @@ class AppStoreConnectService { func readBundleIdInformation( bundleId: String ) async throws -> Model.BundleId { - Model.BundleId(try await ReadBundleIdOperation( + Model.BundleId( + try await ReadBundleIdOperation( + service: service, options: .init(bundleId: bundleId) ) - .execute(with: service)) - } - - func modifyBundleIdInformation(bundleId: String, name: String) async throws -> Model.BundleId { - let id = try await ReadBundleIdOperation( - options: .init(bundleId: bundleId) + .execute() ) - .execute(with: service) - .id - - return try ModifyBundleIdOperation(options: .init(resourceId: id, name: name)) - .execute(with: requestor) - .map(Model.BundleId.init) - .await() } func enableBundleIdCapability( @@ -940,9 +931,10 @@ class AppStoreConnectService { capabilityType: CapabilityType ) async throws { let bundleIdResourceId = try await ReadBundleIdOperation( + service: service, options: .init(bundleId: bundleId) ) - .execute(with: service) + .execute() .id _ = try EnableBundleIdCapabilityOperation( @@ -954,9 +946,10 @@ class AppStoreConnectService { func disableBundleIdCapability(bundleId: String, capabilityType: CapabilityType) async throws { let bundleIdResourceId = try await ReadBundleIdOperation( + service: service, options: .init(bundleId: bundleId) ) - .execute(with: service) + .execute() .id let capability = try ListCapabilitiesOperation( diff --git a/Sources/AppStoreConnectCLI/Services/Operations/ReadBundleIdOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/ReadBundleIdOperation.swift index ca3c1385..fb2de4f4 100644 --- a/Sources/AppStoreConnectCLI/Services/Operations/ReadBundleIdOperation.swift +++ b/Sources/AppStoreConnectCLI/Services/Operations/ReadBundleIdOperation.swift @@ -25,13 +25,15 @@ struct ReadBundleIdOperation: APIOperationV2 { } } + private let service: BagbutikService private let options: Options - init(options: Options) { + init(service: BagbutikService, options: Options) { + self.service = service self.options = options } - func execute(with service: BagbutikService) async throws -> Output { + func execute() async throws -> Output { let bundleIds = try await service.request( .listBundleIdsV1(filters: [.identifier([options.bundleId])]) ) From 20dd0b31e94f0a95da1cc29918faeae2db8d3500 Mon Sep 17 00:00:00 2001 From: Oliver Jones Date: Tue, 1 Nov 2022 13:20:49 +1300 Subject: [PATCH 09/12] Update ReadBundleIdCommand to use Bagbutik --- .../Commands/BundleIds/ReadBundleIdCommand.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Sources/AppStoreConnectCLI/Commands/BundleIds/ReadBundleIdCommand.swift b/Sources/AppStoreConnectCLI/Commands/BundleIds/ReadBundleIdCommand.swift index 0d476021..c943a6a9 100644 --- a/Sources/AppStoreConnectCLI/Commands/BundleIds/ReadBundleIdCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/BundleIds/ReadBundleIdCommand.swift @@ -1,7 +1,7 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK import ArgumentParser +import Bagbutik import Combine import Foundation import struct Model.BundleId @@ -19,10 +19,14 @@ struct ReadBundleIdCommand: CommonParsableCommand { var identifier: String func run() async throws { - let service = try makeService() - - let bundleId = try await service.readBundleIdInformation(bundleId: identifier) - - bundleId.render(options: common.outputOptions) + let result = Model.BundleId( + try await ReadBundleIdOperation( + service: .init(authOptions: common.authOptions), + options: .init(bundleId: identifier) + ) + .execute() + ) + + result.render(options: common.outputOptions) } } From f7f6b6602ebfac25a9bc510b8675a44c28c9bb6e Mon Sep 17 00:00:00 2001 From: Oliver Jones Date: Tue, 1 Nov 2022 13:48:53 +1300 Subject: [PATCH 10/12] Update RegisterBundleIdCommand to use BagButik --- .../BundleIds/RegisterBundleIdCommand.swift | 35 +++++++++++------- ...ndleIdPlatform+ExpressibleByArgument.swift | 11 +++++- ...ilter.Platform+ExpressibleByArgument.swift | 13 +++++++ .../RegisterBundleIdOperation.swift | 37 +++++++++++++++++++ 4 files changed, 80 insertions(+), 16 deletions(-) create mode 100644 Sources/AppStoreConnectCLI/Model/API/ListBundleIdsV1.Filter.Platform+ExpressibleByArgument.swift create mode 100644 Sources/AppStoreConnectCLI/Services/Operations/RegisterBundleIdOperation.swift diff --git a/Sources/AppStoreConnectCLI/Commands/BundleIds/RegisterBundleIdCommand.swift b/Sources/AppStoreConnectCLI/Commands/BundleIds/RegisterBundleIdCommand.swift index 9d6ecb33..0b16a30e 100644 --- a/Sources/AppStoreConnectCLI/Commands/BundleIds/RegisterBundleIdCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/BundleIds/RegisterBundleIdCommand.swift @@ -1,11 +1,14 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK +import Bagbutik import ArgumentParser import Foundation import struct Model.BundleId struct RegisterBundleIdCommand: CommonParsableCommand { + + typealias Platform = Bagbutik.BundleIdPlatform + public static var configuration = CommandConfiguration( commandName: "register", abstract: "Register a new bundle ID for app development." @@ -20,18 +23,22 @@ struct RegisterBundleIdCommand: CommonParsableCommand { @Argument(help: "The new name for the bundle identifier.") var name: String - @Option(help: "The platform of the bundle identifier \(BundleIdPlatform.allCases).") - var platform: BundleIdPlatform = .universal - - func run() throws { - let service = try makeService() - - let request = APIEndpoint.registerNewBundleId(id: identifier, name: name, platform: platform) - - let bundleId = try service.request(request) - .map(BundleId.init) - .await() - - bundleId.render(options: common.outputOptions) + @Option( + help: "The platform of the bundle identifier. One of \(Platform.allValueStrings.formatted(.list(type: .or))).", + completion: .list(Platform.allValueStrings) + ) + var platform: Platform = .universal + + func run() async throws { + + let result = Model.BundleId( + try await RegisterBundleIdOperation( + service: .init(authOptions: common.authOptions), + options: .init(bundleId: identifier, name: name, platform: platform) + ) + .execute() + ) + + result.render(options: common.outputOptions) } } diff --git a/Sources/AppStoreConnectCLI/Model/API/BundleIdPlatform+ExpressibleByArgument.swift b/Sources/AppStoreConnectCLI/Model/API/BundleIdPlatform+ExpressibleByArgument.swift index bb8f566e..c93f2324 100644 --- a/Sources/AppStoreConnectCLI/Model/API/BundleIdPlatform+ExpressibleByArgument.swift +++ b/Sources/AppStoreConnectCLI/Model/API/BundleIdPlatform+ExpressibleByArgument.swift @@ -2,10 +2,11 @@ import ArgumentParser import AppStoreConnect_Swift_SDK +import Bagbutik import Foundation -extension BundleIdPlatform: CaseIterable, ExpressibleByArgument, CustomStringConvertible { - public typealias AllCases = [BundleIdPlatform] +extension AppStoreConnect_Swift_SDK.BundleIdPlatform: CaseIterable, ExpressibleByArgument, CustomStringConvertible { + public typealias AllCases = [Self] public static var allCases: AllCases { [.iOS, .macOS, .universal] } @@ -18,3 +19,9 @@ extension BundleIdPlatform: CaseIterable, ExpressibleByArgument, CustomStringCon rawValue.lowercased() } } + +extension Bagbutik.BundleIdPlatform: ExpressibleByArgument { + public init?(argument: String) { + self.init(rawValue: argument.uppercased()) + } +} diff --git a/Sources/AppStoreConnectCLI/Model/API/ListBundleIdsV1.Filter.Platform+ExpressibleByArgument.swift b/Sources/AppStoreConnectCLI/Model/API/ListBundleIdsV1.Filter.Platform+ExpressibleByArgument.swift new file mode 100644 index 00000000..23a499ac --- /dev/null +++ b/Sources/AppStoreConnectCLI/Model/API/ListBundleIdsV1.Filter.Platform+ExpressibleByArgument.swift @@ -0,0 +1,13 @@ +// Copyright 2022 Itty Bitty Apps Pty Ltd + +import ArgumentParser +import Bagbutik +import Foundation + +extension ListBundleIdsV1.Filter.Platform: Codable, ExpressibleByArgument { + + public init?(argument: String) { + self.init(rawValue: argument.uppercased()) + } + +} diff --git a/Sources/AppStoreConnectCLI/Services/Operations/RegisterBundleIdOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/RegisterBundleIdOperation.swift new file mode 100644 index 00000000..4e070c5d --- /dev/null +++ b/Sources/AppStoreConnectCLI/Services/Operations/RegisterBundleIdOperation.swift @@ -0,0 +1,37 @@ +// Copyright 2022 Itty Bitty Apps Pty Ltd + +import Bagbutik +import Foundation +import Model + +struct RegisterBundleIdOperation: APIOperationV2 { + typealias Output = Bagbutik.BundleId + + struct Options { + let bundleId: String + let name: String + let platform: BundleIdPlatform + } + + private let service: BagbutikService + private let options: Options + + init(service: BagbutikService, options: Options) { + self.service = service + self.options = options + } + + func execute() async throws -> Output { + let attributes = BundleIdCreateRequest.Data.Attributes( + identifier: options.bundleId, + name: options.name, + platform: options.platform + ) + let request = BundleIdCreateRequest(data: .init(attributes: attributes)) + + return try await service.request( + .createBundleIdV1(requestBody: request) + ).data + } + +} From 00aae6d5551cb500d7ea2e3ce02ce851b36cb30e Mon Sep 17 00:00:00 2001 From: Oliver Jones Date: Tue, 1 Nov 2022 18:02:42 +1300 Subject: [PATCH 11/12] Update Enable/Disable BundleIdCapability --- .../DisableBundleIdCapabilityCommand.swift | 36 ++++++++++-- .../Capability/EnableBundleIdCapability.swift | 31 ---------- .../EnableBundleIdCapabilityCommand.swift | 58 +++++++++++++++++++ ...CapabilityType+ExpressibleByArgument.swift | 39 +------------ .../Model/BundleIdCapability.swift | 31 ++++++++++ .../Services/AppStoreConnectService.swift | 53 ----------------- .../Services/Operations/APIOperation.swift | 4 +- .../DisableBundleIdCapabilityOperation.swift | 24 ++++++++ .../DisableCapabilityOperation.swift | 26 --------- .../EnableBundleIdCapabilityOperation.swift | 29 +++++----- Sources/Model/BundleIdCapability.swift | 14 +++++ 11 files changed, 177 insertions(+), 168 deletions(-) delete mode 100644 Sources/AppStoreConnectCLI/Commands/Capability/EnableBundleIdCapability.swift create mode 100644 Sources/AppStoreConnectCLI/Commands/Capability/EnableBundleIdCapabilityCommand.swift create mode 100644 Sources/AppStoreConnectCLI/Model/BundleIdCapability.swift create mode 100644 Sources/AppStoreConnectCLI/Services/Operations/DisableBundleIdCapabilityOperation.swift delete mode 100644 Sources/AppStoreConnectCLI/Services/Operations/DisableCapabilityOperation.swift create mode 100644 Sources/Model/BundleIdCapability.swift diff --git a/Sources/AppStoreConnectCLI/Commands/Capability/DisableBundleIdCapabilityCommand.swift b/Sources/AppStoreConnectCLI/Commands/Capability/DisableBundleIdCapabilityCommand.swift index 251962fe..212034cf 100644 --- a/Sources/AppStoreConnectCLI/Commands/Capability/DisableBundleIdCapabilityCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/Capability/DisableBundleIdCapabilityCommand.swift @@ -1,6 +1,6 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK +import Bagbutik import ArgumentParser struct DisableBundleIdCapabilityCommand: CommonParsableCommand { @@ -16,12 +16,38 @@ struct DisableBundleIdCapabilityCommand: CommonParsableCommand { @Argument(help: "The reverse-DNS bundle ID identifier to delete. Must be unique. (eg. com.example.app)") var bundleId: String - @Argument(help: ArgumentHelp("Bundle Id capability type.", discussion: "One of \(CapabilityType.allCases)")) - var capabilityType: CapabilityType + @Argument( + help: ArgumentHelp("Bundle Id capability type.", discussion: "List of \(CapabilityType.allValueStrings.formatted(.list(type: .or)))"), + completion: .list(CapabilityType.allValueStrings) + ) + var capabilityType: [CapabilityType] func run() async throws { - let service = try makeService() - try await service.disableBundleIdCapability(bundleId: bundleId, capabilityType: capabilityType) + let service = try BagbutikService(authOptions: common.authOptions) + let bundleIdResourceId = try await ReadBundleIdOperation( + service: service, + options: .init(bundleId: bundleId) + ) + .execute() + .id + + let capabilityIdentifiers = try await ListCapabilitiesOperation( + service: service, + options: .init(bundleIdResourceId: bundleIdResourceId) + ) + .execute() + .filter { capabilityType.contains($0.attributes!.capabilityType!) } + .map { $0.id } + + await withThrowingTaskGroup(of: Void.self) { group in + for id in capabilityIdentifiers { + group.addTask { + try await DisableBundleIdCapabilityOperation(service: service, options: .init(capabilityId: id)).execute() + } + } + } + + // TODO: should list capabilities on bundleId } } diff --git a/Sources/AppStoreConnectCLI/Commands/Capability/EnableBundleIdCapability.swift b/Sources/AppStoreConnectCLI/Commands/Capability/EnableBundleIdCapability.swift deleted file mode 100644 index 96ea5e54..00000000 --- a/Sources/AppStoreConnectCLI/Commands/Capability/EnableBundleIdCapability.swift +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2020 Itty Bitty Apps Pty Ltd - -import AppStoreConnect_Swift_SDK -import ArgumentParser - -struct EnableBundleIdCapabilityCommand: CommonParsableCommand { - - public static var configuration = CommandConfiguration( - commandName: "enable", - abstract: "Enable a capability for a bundle ID." - ) - - @OptionGroup() - var common: CommonOptions - - @Argument(help: "The reverse-DNS bundle ID identifier to delete. Must be unique. (eg. com.example.app)") - var bundleId: String - - @Argument(help: ArgumentHelp("Bundle Id capability type.", discussion: "One of \(CapabilityType.allCases)")) - var capabilityType: CapabilityType - - // TODO: CapabilitySetting - - func run() async throws { - let service = try makeService() - - try await service.enableBundleIdCapability( - bundleId: bundleId, capabilityType: capabilityType - ) - } -} diff --git a/Sources/AppStoreConnectCLI/Commands/Capability/EnableBundleIdCapabilityCommand.swift b/Sources/AppStoreConnectCLI/Commands/Capability/EnableBundleIdCapabilityCommand.swift new file mode 100644 index 00000000..32701877 --- /dev/null +++ b/Sources/AppStoreConnectCLI/Commands/Capability/EnableBundleIdCapabilityCommand.swift @@ -0,0 +1,58 @@ +// Copyright 2020 Itty Bitty Apps Pty Ltd + +import Bagbutik +import ArgumentParser +import struct Model.BundleIdCapability + +struct EnableBundleIdCapabilityCommand: CommonParsableCommand { + + public static var configuration = CommandConfiguration( + commandName: "enable", + abstract: "Enable a capability for a bundle ID." + ) + + @OptionGroup() + var common: CommonOptions + + @Argument(help: "The reverse-DNS bundle ID identifier to delete. Must be unique. (eg. com.example.app)") + var bundleId: String + + @Argument(help: ArgumentHelp("Bundle Id capability type.", discussion: "List of \(CapabilityType.allValueStrings.formatted(.list(type: .or)))")) + var capabilityType: [CapabilityType] + + // TODO: CapabilitySetting + + func run() async throws { + let service = try BagbutikService(authOptions: common.authOptions) + let bundleId = try await ReadBundleIdOperation( + service: service, + options: .init(bundleId: bundleId) + ) + .execute() + + let result = try await withThrowingTaskGroup(of: Model.BundleIdCapability.self) { group in + for type in capabilityType { + group.addTask { + try await Model.BundleIdCapability( + EnableBundleIdCapabilityOperation( + service: service, + options: .init(bundleIdResourceId: bundleId.id, capabilityType: type) + ) + .execute() + ) + } + } + + var values = [Model.BundleIdCapability]() + for try await value in group { + values.append(value) + } + + return values + } + + // TODO: should list capabilities on bundleId + + result.render(options: common.outputOptions) + } +} diff --git a/Sources/AppStoreConnectCLI/Model/API/CapabilityType+ExpressibleByArgument.swift b/Sources/AppStoreConnectCLI/Model/API/CapabilityType+ExpressibleByArgument.swift index 6636d1c9..2cec0dff 100644 --- a/Sources/AppStoreConnectCLI/Model/API/CapabilityType+ExpressibleByArgument.swift +++ b/Sources/AppStoreConnectCLI/Model/API/CapabilityType+ExpressibleByArgument.swift @@ -1,46 +1,11 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK +import Bagbutik import ArgumentParser import Foundation -extension CapabilityType: CaseIterable, ExpressibleByArgument, CustomStringConvertible { - public typealias AllCases = [CapabilityType] - - public static var allCases: AllCases { - [ - .icloud, - .inAppPurchase, - .gameCenter, - .pushNotifications, - .wallet, - .interAppAudio, - .maps, - .associatedDomains, - .personalVpn, - .appGroups, - .healthkit, - .homekit, - .wirelessAccessoryConfiguration, - .applePay, - .dataProtection, - .sirikit, - .networkExtensions, - .multipath, - .hotSpot, - .nfcTagReading, - .classkit, - .autofillCredentialProvider, - .accessWifiInformation, - .appleIdAuth, - ] - } - +extension CapabilityType: ExpressibleByArgument { public init?(argument: String) { self.init(rawValue: argument.uppercased()) } - - public var description: String { - rawValue.lowercased() - } } diff --git a/Sources/AppStoreConnectCLI/Model/BundleIdCapability.swift b/Sources/AppStoreConnectCLI/Model/BundleIdCapability.swift new file mode 100644 index 00000000..ac2f8004 --- /dev/null +++ b/Sources/AppStoreConnectCLI/Model/BundleIdCapability.swift @@ -0,0 +1,31 @@ +// Copyright 2022 Itty Bitty Apps Pty Ltd + +import Bagbutik +import Foundation +import Model +import SwiftyTextTable + +extension Model.BundleIdCapability { + + init(_ attributes: Bagbutik.BundleIdCapability.Attributes?) { + self.init(capabilityType: attributes?.capabilityType?.rawValue) + } + + init(_ apiBundleId: Bagbutik.BundleIdCapability) { + self.init(apiBundleId.attributes) + } +} + +extension Model.BundleIdCapability: ResultRenderable, TableInfoProvider { + static func tableColumns() -> [TextTableColumn] { + return [ + TextTableColumn(header: "Capability Type"), + ] + } + + var tableRow: [CustomStringConvertible] { + return [ + capabilityType ?? "", + ] + } +} diff --git a/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift b/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift index 974fe3ea..b4861d51 100644 --- a/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift +++ b/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift @@ -913,59 +913,6 @@ class AppStoreConnectService { return .init(invitation) } - - func readBundleIdInformation( - bundleId: String - ) async throws -> Model.BundleId { - Model.BundleId( - try await ReadBundleIdOperation( - service: service, - options: .init(bundleId: bundleId) - ) - .execute() - ) - } - - func enableBundleIdCapability( - bundleId: String, - capabilityType: CapabilityType - ) async throws { - let bundleIdResourceId = try await ReadBundleIdOperation( - service: service, - options: .init(bundleId: bundleId) - ) - .execute() - .id - - _ = try EnableBundleIdCapabilityOperation( - options: .init(bundleIdResourceId: bundleIdResourceId, capabilityType: capabilityType) - ) - .execute(with: requestor) - .await() - } - - func disableBundleIdCapability(bundleId: String, capabilityType: CapabilityType) async throws { - let bundleIdResourceId = try await ReadBundleIdOperation( - service: service, - options: .init(bundleId: bundleId) - ) - .execute() - .id - - let capability = try ListCapabilitiesOperation( - options: .init(bundleIdResourceId: bundleIdResourceId) - ) - .execute(with: requestor) - .await() - .first { $0.attributes?.capabilityType == capabilityType } - - guard let id = capability?.id else { return } - - try DisableCapabilityOperation(options: .init(capabilityId: id)) - .execute(with: requestor) - .await() - } - func downloadSales( frequency: [DownloadSalesAndTrendsReports.Filter.Frequency], reportType: [DownloadSalesAndTrendsReports.Filter.ReportType], diff --git a/Sources/AppStoreConnectCLI/Services/Operations/APIOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/APIOperation.swift index 6a85b09a..25433ba4 100644 --- a/Sources/AppStoreConnectCLI/Services/Operations/APIOperation.swift +++ b/Sources/AppStoreConnectCLI/Services/Operations/APIOperation.swift @@ -17,7 +17,7 @@ protocol APIOperationV2 { associatedtype Output associatedtype Service - init(options: Options) + init(service: Service, options: Options) - func execute(with service: Service) async throws -> Output + func execute() async throws -> Output } diff --git a/Sources/AppStoreConnectCLI/Services/Operations/DisableBundleIdCapabilityOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/DisableBundleIdCapabilityOperation.swift new file mode 100644 index 00000000..06639857 --- /dev/null +++ b/Sources/AppStoreConnectCLI/Services/Operations/DisableBundleIdCapabilityOperation.swift @@ -0,0 +1,24 @@ +// Copyright 2020 Itty Bitty Apps Pty Ltd + +import Bagbutik +import Foundation + +struct DisableBundleIdCapabilityOperation: APIOperationV2 { + + struct Options { + let capabilityId: String + } + + private let service: BagbutikService + private let options: Options + + init(service: BagbutikService, options: Options) { + self.service = service + self.options = options + } + + func execute() async throws { + _ = try await service.request(.deleteBundleIdCapabilityV1(id: options.capabilityId)) + } + +} diff --git a/Sources/AppStoreConnectCLI/Services/Operations/DisableCapabilityOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/DisableCapabilityOperation.swift deleted file mode 100644 index 3407c66c..00000000 --- a/Sources/AppStoreConnectCLI/Services/Operations/DisableCapabilityOperation.swift +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2020 Itty Bitty Apps Pty Ltd - -import AppStoreConnect_Swift_SDK -import Combine - -struct DisableCapabilityOperation: APIOperation { - - struct Options { - let capabilityId: String - } - - let option: Options - - init(options: Options) { - self.option = options - } - - func execute(with requestor: EndpointRequestor) -> AnyPublisher { - requestor - .request( - .disableCapability(id: option.capabilityId) - ) - .eraseToAnyPublisher() - } - -} diff --git a/Sources/AppStoreConnectCLI/Services/Operations/EnableBundleIdCapabilityOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/EnableBundleIdCapabilityOperation.swift index aa5d678b..fa62d240 100644 --- a/Sources/AppStoreConnectCLI/Services/Operations/EnableBundleIdCapabilityOperation.swift +++ b/Sources/AppStoreConnectCLI/Services/Operations/EnableBundleIdCapabilityOperation.swift @@ -1,31 +1,32 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK -import Combine +import Bagbutik import Foundation -struct EnableBundleIdCapabilityOperation: APIOperation { +struct EnableBundleIdCapabilityOperation: APIOperationV2 { struct Options { let bundleIdResourceId: String let capabilityType: CapabilityType } - let option: Options + private let service: BagbutikService + private let options: Options - init(options: Options) { - self.option = options + init(service: BagbutikService, options: Options) { + self.service = service + self.options = options } - func execute(with requestor: EndpointRequestor) -> AnyPublisher { - requestor - .request( - .enableCapability( - id: option.bundleIdResourceId, - capabilityType: option.capabilityType - ) + func execute() async throws -> BundleIdCapability { + let body = BundleIdCapabilityCreateRequest( + data: .init( + attributes: .init(capabilityType: options.capabilityType), + relationships: .init(bundleId: .init(data: .init(id: options.bundleIdResourceId))) ) - .eraseToAnyPublisher() + ) + + return try await service.request(.createBundleIdCapabilityV1(requestBody: body)).data } } diff --git a/Sources/Model/BundleIdCapability.swift b/Sources/Model/BundleIdCapability.swift new file mode 100644 index 00000000..640daaec --- /dev/null +++ b/Sources/Model/BundleIdCapability.swift @@ -0,0 +1,14 @@ +// Copyright 2022 Itty Bitty Apps Pty Ltd + +import Foundation + +public struct BundleIdCapability: Codable, Equatable { + public let capabilityType: String? + + // TODO: support Capability Settings + + public init(capabilityType: String?) { + self.capabilityType = capabilityType + + } +} From 8f2780abca53f7712b6bd20979eb745228f32f3c Mon Sep 17 00:00:00 2001 From: Oliver Jones Date: Tue, 1 Nov 2022 18:31:18 +1300 Subject: [PATCH 12/12] Migrate CreateCertificateCommand to Bagbutik --- .../CreateCertificateCommand.swift | 24 ++++--- ...ertificateType+ExpressibleByArgument.swift | 11 ++- .../Model/Certificate.swift | 16 +++++ .../Services/AppStoreConnectService.swift | 11 --- .../CreateCertificateOperation.swift | 35 ++++----- .../CreateCertificateOperationTests.swift | 71 +++++++++---------- .../Operations/Shared.swift | 2 + 7 files changed, 91 insertions(+), 79 deletions(-) diff --git a/Sources/AppStoreConnectCLI/Commands/Certificates/CreateCertificateCommand.swift b/Sources/AppStoreConnectCLI/Commands/Certificates/CreateCertificateCommand.swift index 01970ec7..63416c63 100644 --- a/Sources/AppStoreConnectCLI/Commands/Certificates/CreateCertificateCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/Certificates/CreateCertificateCommand.swift @@ -1,9 +1,9 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK import ArgumentParser -import Combine +import Bagbutik import Foundation +import struct Model.Certificate struct CreateCertificateCommand: CommonParsableCommand { static var configuration = CommandConfiguration( @@ -13,22 +13,24 @@ struct CreateCertificateCommand: CommonParsableCommand { @OptionGroup() var common: CommonOptions - @Argument(help: "The type of certificate to create \(CertificateType.allCases).") + @Argument(help: "The type of certificate to create. One of \(CertificateType.allValueStrings.formatted(.list(type: .or))).") var certificateType: CertificateType @Argument(help: "The Certificate Signing Request (CSR) file path.") var csrFile: String - func run() throws { - let service = try makeService() - + func run() async throws { + let csrContent = try String(contentsOfFile: csrFile, encoding: .utf8) - - let certificate = try service.createCertificate( - certificateType: certificateType, - csrContent: csrContent + + let result = Model.Certificate( + try await CreateCertificateOperation( + service: .init(authOptions: common.authOptions), + options: .init(certificateType: certificateType, csrContent: csrContent) + ) + .execute() ) - certificate.render(options: common.outputOptions) + result.render(options: common.outputOptions) } } diff --git a/Sources/AppStoreConnectCLI/Model/API/CertificateType+ExpressibleByArgument.swift b/Sources/AppStoreConnectCLI/Model/API/CertificateType+ExpressibleByArgument.swift index 2db3781f..4ac60071 100644 --- a/Sources/AppStoreConnectCLI/Model/API/CertificateType+ExpressibleByArgument.swift +++ b/Sources/AppStoreConnectCLI/Model/API/CertificateType+ExpressibleByArgument.swift @@ -2,10 +2,11 @@ import AppStoreConnect_Swift_SDK import ArgumentParser +import Bagbutik import Foundation -extension CertificateType: CaseIterable, ExpressibleByArgument, CustomStringConvertible { - public typealias AllCases = [CertificateType] +extension AppStoreConnect_Swift_SDK.CertificateType: CaseIterable, ExpressibleByArgument, CustomStringConvertible { + public typealias AllCases = [Self] public static var allCases: AllCases { [ .iOSDevelopment, @@ -26,3 +27,9 @@ extension CertificateType: CaseIterable, ExpressibleByArgument, CustomStringConv rawValue.lowercased() } } + +extension Bagbutik.CertificateType: ExpressibleByArgument { + public init?(argument: String) { + self.init(rawValue: argument.uppercased()) + } +} diff --git a/Sources/AppStoreConnectCLI/Model/Certificate.swift b/Sources/AppStoreConnectCLI/Model/Certificate.swift index ea662383..031d429e 100644 --- a/Sources/AppStoreConnectCLI/Model/Certificate.swift +++ b/Sources/AppStoreConnectCLI/Model/Certificate.swift @@ -1,6 +1,7 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd import AppStoreConnect_Swift_SDK +import Bagbutik import Foundation import Model import SwiftyTextTable @@ -20,6 +21,21 @@ extension Model.Certificate { serialNumber: attributes.serialNumber ) } + + init(_ certificate: Bagbutik.Certificate) { + self.init(certificate.attributes) + } + + init(_ attributes: Bagbutik.Certificate.Attributes?) { + self.init( + name: attributes?.name, + type: attributes?.certificateType?.rawValue, + content: attributes?.certificateContent, + platform: attributes?.platform?.rawValue, + expirationDate: attributes?.expirationDate, + serialNumber: attributes?.serialNumber + ) + } } extension Model.Certificate: ResultRenderable, TableInfoProvider { diff --git a/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift b/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift index b4861d51..56351e67 100644 --- a/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift +++ b/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift @@ -160,17 +160,6 @@ class AppStoreConnectService { return Model.Certificate(sdkCertificate) } - func createCertificate( - certificateType: CertificateType, - csrContent: String - ) throws -> Model.Certificate { - try CreateCertificateOperation( - options: .init(certificateType: certificateType, csrContent: csrContent) - ) - .execute(with: requestor) - .await() - } - func revokeCertificates(serials: [String]) throws { let certificatesIds = try serials.map { try ReadCertificateOperation(options: .init(serial: $0)) diff --git a/Sources/AppStoreConnectCLI/Services/Operations/CreateCertificateOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/CreateCertificateOperation.swift index 0f36d998..298886c6 100644 --- a/Sources/AppStoreConnectCLI/Services/Operations/CreateCertificateOperation.swift +++ b/Sources/AppStoreConnectCLI/Services/Operations/CreateCertificateOperation.swift @@ -1,31 +1,34 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd -import AppStoreConnect_Swift_SDK -import Combine +import Bagbutik import Foundation -import struct Model.Certificate -struct CreateCertificateOperation: APIOperation { +struct CreateCertificateOperation: APIOperationV2 { struct Options { let certificateType: CertificateType let csrContent: String } - private let endpoint: APIEndpoint + private let service: BagbutikService + private let options: Options - init(options: Options) { - endpoint = APIEndpoint.create( - certificateWithCertificateType: options.certificateType, - csrContent: options.csrContent - ) + init(service: BagbutikService, options: Options) { + self.service = service + self.options = options } - - func execute(with requestor: EndpointRequestor) -> AnyPublisher { - requestor - .request(endpoint) - .map { Certificate($0.data) } - .eraseToAnyPublisher() + + func execute() async throws -> Certificate { + let body = CertificateCreateRequest( + data: .init( + attributes: .init( + certificateType: options.certificateType, + csrContent: options.csrContent + ) + ) + ) + + return try await service.request(.createCertificateV1(requestBody: body)).data } } diff --git a/Tests/appstoreconnect-cliTests/Operations/CreateCertificateOperationTests.swift b/Tests/appstoreconnect-cliTests/Operations/CreateCertificateOperationTests.swift index d37478e2..b87ca0e3 100644 --- a/Tests/appstoreconnect-cliTests/Operations/CreateCertificateOperationTests.swift +++ b/Tests/appstoreconnect-cliTests/Operations/CreateCertificateOperationTests.swift @@ -1,7 +1,7 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd @testable import AppStoreConnectCLI -import AppStoreConnect_Swift_SDK +import Bagbutik import Combine import Foundation import XCTest @@ -10,44 +10,37 @@ final class CreateCertificateOperationTests: XCTestCase { let options = CreateCertificateOperation.Options( certificateType: .iOSDevelopment, csrContent: "") - - let successRequestor = OneEndpointTestRequestor( - response: { _ in Future({ $0(.success(Certificate.createCertificateResponse)) }) } - ) - - func testExecute_success() { - let operation = CreateCertificateOperation(options: options) - - let result = Result { - try operation.execute(with: successRequestor).await() - } - - switch result { - case .success(let certificate): - XCTAssertEqual(certificate.name, "Mac Installer Distribution: Hello") - XCTAssertEqual(certificate.platform, BundleIdPlatform.macOS.rawValue) - XCTAssertEqual(certificate.content, "MIIFpDCCBIygAwIBAgIIbgb/7NS42MgwDQ") - default: - XCTFail("Error happened when parsing create certificate response") - } + + func testExecute_success() async throws { + let jwt = try JWT(keyId: "", issuerId: "", privateKey: "") + let mockService = BagbutikService(jwt: jwt, fetchData: { _, _ in + return (try Fixture(named: "v1/certificates/created_success").data, URLResponse()) + }) + + let operation = CreateCertificateOperation(service: mockService, options: options) + let certificate = try await operation.execute() + + XCTAssertEqual(certificate.attributes?.name, "Mac Installer Distribution: Hello") + XCTAssertEqual(certificate.attributes?.platform, BundleIdPlatform.macOS) + XCTAssertEqual(certificate.attributes?.certificateContent, "MIIFpDCCBIygAwIBAgIIbgb/7NS42MgwDQ") } - func testExecute_propagatesUpstreamErrors() { - let requestor = FailureTestRequestor() - - let operation = CreateCertificateOperation(options: options) - - let result = Result { - try operation.execute(with: requestor).await() - } - - let expectedError = TestError.somethingBadHappened - - switch result { - case .failure(let error as TestError): - XCTAssertEqual(expectedError, error) - default: - XCTFail("Expected failure with: \(expectedError), got: \(result)") - } - } +// func testExecute_propagatesUpstreamErrors() { +// let requestor = FailureTestRequestor() +// +// let operation = CreateCertificateOperation(options: options) +// +// let result = Result { +// try operation.execute(with: requestor).await() +// } +// +// let expectedError = TestError.somethingBadHappened +// +// switch result { +// case .failure(let error as TestError): +// XCTAssertEqual(expectedError, error) +// default: +// XCTFail("Expected failure with: \(expectedError), got: \(result)") +// } +// } } diff --git a/Tests/appstoreconnect-cliTests/Operations/Shared.swift b/Tests/appstoreconnect-cliTests/Operations/Shared.swift index 1505544d..5d8ee2c6 100644 --- a/Tests/appstoreconnect-cliTests/Operations/Shared.swift +++ b/Tests/appstoreconnect-cliTests/Operations/Shared.swift @@ -15,6 +15,8 @@ enum TestError: Error, Equatable { case somethingBadHappened } + + extension JSONDecoder { // swiftlint:disable force_try