diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift index 04ca5b26..392073a0 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift @@ -182,11 +182,8 @@ public struct BushelCloudKitService: Sendable, RecordManaging, CloudKitRecordCol /// /// This is the protocol-conforming version that doesn't track create vs update. /// For detailed tracking, use the overload with `classification` parameter. - public func executeBatchOperations( - _ operations: [RecordOperation], - recordType: String - ) async throws { - // Create empty classification (no tracking) + public func executeBatchOperations(_ operations: [RecordOperation]) async throws { + guard let recordType = operations.first?.recordType else { return } let classification = OperationClassification(proposedRecords: [], existingRecords: []) _ = try await executeBatchOperations( operations, recordType: recordType, classification: classification diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/MockCloudKitServiceTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/MockCloudKitServiceTests.swift index e00bbff9..94f3138d 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/MockCloudKitServiceTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/MockCloudKitServiceTests.swift @@ -59,7 +59,7 @@ internal struct MockCloudKitServiceTests { fields: record.toCloudKitFields() ) - try await service.executeBatchOperations([operation], recordType: "RestoreImage") + try await service.executeBatchOperations([operation]) let storedRecords = await service.getStoredRecords(ofType: "RestoreImage") #expect(storedRecords.count == 1) @@ -79,7 +79,7 @@ internal struct MockCloudKitServiceTests { recordName: recordName, fields: initialRecord.toCloudKitFields() ) - try await service.executeBatchOperations([createOp], recordType: "RestoreImage") + try await service.executeBatchOperations([createOp]) // Replace with updated record let updatedRecord = RestoreImageRecord( @@ -103,7 +103,7 @@ internal struct MockCloudKitServiceTests { recordName: recordName, fields: updatedRecord.toCloudKitFields() ) - try await service.executeBatchOperations([replaceOp], recordType: "RestoreImage") + try await service.executeBatchOperations([replaceOp]) // Verify only one record exists with updated data let storedRecords = await service.getStoredRecords(ofType: "RestoreImage") @@ -130,7 +130,7 @@ internal struct MockCloudKitServiceTests { recordName: recordName, fields: record.toCloudKitFields() ) - try await service.executeBatchOperations([createOp], recordType: "RestoreImage") + try await service.executeBatchOperations([createOp]) // Delete record let deleteOp = RecordOperation( @@ -138,7 +138,7 @@ internal struct MockCloudKitServiceTests { recordType: "RestoreImage", recordName: recordName ) - try await service.executeBatchOperations([deleteOp], recordType: "RestoreImage") + try await service.executeBatchOperations([deleteOp]) // Verify record is gone let storedRecords = await service.getStoredRecords(ofType: "RestoreImage") @@ -170,11 +170,8 @@ internal struct MockCloudKitServiceTests { ), ] - try await service.executeBatchOperations( - Array(operations[0...1]), - recordType: "RestoreImage" - ) - try await service.executeBatchOperations([operations[2]], recordType: "XcodeVersion") + try await service.executeBatchOperations(Array(operations[0...1])) + try await service.executeBatchOperations([operations[2]]) let restoreImages = await service.getStoredRecords(ofType: "RestoreImage") let xcodeVersions = await service.getStoredRecords(ofType: "XcodeVersion") @@ -213,7 +210,7 @@ internal struct MockCloudKitServiceTests { ) do { - try await service.executeBatchOperations([operation], recordType: "RestoreImage") + try await service.executeBatchOperations([operation]) Issue.record("Expected error to be thrown") } catch is MockCloudKitError { // Success - error was thrown as expected @@ -244,8 +241,8 @@ internal struct MockCloudKitServiceTests { ) ] - try await service.executeBatchOperations(batch1, recordType: "RestoreImage") - try await service.executeBatchOperations(batch2, recordType: "XcodeVersion") + try await service.executeBatchOperations(batch1) + try await service.executeBatchOperations(batch2) let history = await service.getOperationHistory() #expect(history.count == 2) @@ -264,7 +261,7 @@ internal struct MockCloudKitServiceTests { recordName: "test", fields: TestFixtures.sonoma1421.toCloudKitFields() ) - try await service.executeBatchOperations([operation], recordType: "RestoreImage") + try await service.executeBatchOperations([operation]) // Clear storage await service.clearStorage() diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/CloudKitErrorHandlingTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/CloudKitErrorHandlingTests.swift index 92c88015..968e0732 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/CloudKitErrorHandlingTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/CloudKitErrorHandlingTests.swift @@ -51,7 +51,7 @@ internal struct CloudKitErrorHandlingTests { ) do { - try await service.executeBatchOperations([operation], recordType: "RestoreImage") + try await service.executeBatchOperations([operation]) Issue.record("Expected quota exceeded error to be thrown") } catch let error as MockCloudKitError { if case .quotaExceeded = error { @@ -78,7 +78,7 @@ internal struct CloudKitErrorHandlingTests { ) do { - try await service.executeBatchOperations([operation], recordType: "XcodeVersion") + try await service.executeBatchOperations([operation]) Issue.record("Expected reference validation error to be thrown") } catch let error as MockCloudKitError { if case .validatingReferenceError = error { @@ -105,7 +105,7 @@ internal struct CloudKitErrorHandlingTests { ) do { - try await service.executeBatchOperations([operation], recordType: "RestoreImage") + try await service.executeBatchOperations([operation]) Issue.record("Expected conflict error to be thrown") } catch let error as MockCloudKitError { if case .conflict = error { diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockCloudKitService.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockCloudKitService.swift index b633748c..7329f425 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockCloudKitService.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockCloudKitService.swift @@ -82,18 +82,16 @@ internal actor MockCloudKitService: RecordManaging { return storedRecords[recordType] ?? [] } - internal func executeBatchOperations( - _ operations: [RecordOperation], - recordType: String - ) async throws { + internal func executeBatchOperations(_ operations: [RecordOperation]) async throws { operationHistory.append(operations) if shouldFailModify { throw modifyError ?? MockCloudKitError.networkError } - // Process operations + // Each operation carries its own record type for operation in operations { + let recordType = operation.recordType switch operation.operationType { case .create, .forceReplace: handleCreateOrReplace(operation, recordType: recordType) diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/CurrentUserCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/CurrentUserCommand.swift index b86c3bf5..6f02a7a4 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/CurrentUserCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/CurrentUserCommand.swift @@ -72,27 +72,12 @@ public struct CurrentUserCommand: MistDemoCommand, OutputFormatting { // Create CloudKit client let client = try MistKitClientFactory.create(for: config.base) - // Fetch current user information let userInfo = try await client.fetchCaller() - - // Filter fields if requested - let filteredUser = filterUserFields(userInfo, fields: config.fields) - - // Format and output result - try await outputResult(filteredUser, format: config.output) + try await outputResult(userInfo, format: config.output) } catch { throw CurrentUserError.operationFailed(error.localizedDescription) } } - - /// Filter user fields based on requested fields - /// Since UserInfo constructor is internal, we work with the original object - /// and filter during output instead - private func filterUserFields(_ userInfo: UserInfo, fields _: [String]?) -> UserInfo { - // Since we can't create new UserInfo instances, return the original - // Field filtering will be handled in the output methods - userInfo - } } // CurrentUserError is now defined in Errors/CurrentUserError.swift diff --git a/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper+SetupHelpers.swift b/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper+SetupHelpers.swift index eecd938b..13d49e28 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper+SetupHelpers.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper+SetupHelpers.swift @@ -32,7 +32,6 @@ import MistKit extension AuthenticationHelper { internal static func setupServerToServer( - apiToken _: String, keyID: String, privateKey: String?, privateKeyFile: String?, diff --git a/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper.swift b/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper.swift index c26ed74f..a3459f9c 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper.swift @@ -54,7 +54,6 @@ internal enum AuthenticationHelper { ) async throws -> AuthenticationResult { if let keyID { return try await setupServerToServer( - apiToken: apiToken, keyID: keyID, privateKey: privateKey, privateKeyFile: privateKeyFile, diff --git a/Sources/MistKit/OpenAPI/Operations/InputPaths/Operations.discoverAllUserIdentities.Input.Path.swift b/Sources/MistKit/OpenAPI/Operations/InputPaths/Operations.discoverAllUserIdentities.Input.Path.swift deleted file mode 100644 index b8a1b0f0..00000000 --- a/Sources/MistKit/OpenAPI/Operations/InputPaths/Operations.discoverAllUserIdentities.Input.Path.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// Operations.discoverAllUserIdentities.Input.Path.swift -// MistKit -// -// Created by Leo Dion. -// Copyright © 2026 BrightDigit. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -internal import Foundation - -@available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) -extension Operations.discoverAllUserIdentities.Input.Path { - /// Initialize from MistKit configuration components. - internal init( - containerIdentifier: String, - environment: Environment, - database: Database - ) { - self.init( - version: "1", - container: containerIdentifier, - environment: .init(from: environment), - database: .init(from: database) - ) - } -} diff --git a/Sources/MistKit/Protocols/RecordManaging+Generic.swift b/Sources/MistKit/Protocols/RecordManaging+Generic.swift index f3ca288b..f2cb21d8 100644 --- a/Sources/MistKit/Protocols/RecordManaging+Generic.swift +++ b/Sources/MistKit/Protocols/RecordManaging+Generic.swift @@ -64,7 +64,7 @@ extension RecordManaging { let batches = operations.chunked(into: 200) for batch in batches { - try await executeBatchOperations(batch, recordType: T.cloudKitRecordType) + try await executeBatchOperations(batch) } } diff --git a/Sources/MistKit/Protocols/RecordManaging+RecordCollection.swift b/Sources/MistKit/Protocols/RecordManaging+RecordCollection.swift index 6c7da8f7..900747cd 100644 --- a/Sources/MistKit/Protocols/RecordManaging+RecordCollection.swift +++ b/Sources/MistKit/Protocols/RecordManaging+RecordCollection.swift @@ -81,7 +81,7 @@ extension RecordManaging where Self: CloudKitRecordCollection { } // Execute batch operation for this record type - try await executeBatchOperations(operations, recordType: typeName) + try await executeBatchOperations(operations) } } @@ -168,7 +168,7 @@ extension RecordManaging where Self: CloudKitRecordCollection { } // Execute batch delete operations - try await executeBatchOperations(operations, recordType: typeName) + try await executeBatchOperations(operations) deletedByType[typeName] = records.count totalDeleted += records.count diff --git a/Sources/MistKit/Protocols/RecordManaging.swift b/Sources/MistKit/Protocols/RecordManaging.swift index 588853ac..3620a7f6 100644 --- a/Sources/MistKit/Protocols/RecordManaging.swift +++ b/Sources/MistKit/Protocols/RecordManaging.swift @@ -49,13 +49,12 @@ public protocol RecordManaging { /// Execute a batch of record operations /// /// Handles batching operations to respect CloudKit's 200 operations/request limit. - /// Provides detailed progress reporting and error tracking. + /// Each `RecordOperation` carries its own record type, so no separate + /// `recordType` parameter is required. /// - /// - Parameters: - /// - operations: Array of record operations to execute - /// - recordType: The record type being operated on (for logging) + /// - Parameter operations: Array of record operations to execute /// - Throws: CloudKit errors if the batch operations fail - func executeBatchOperations(_ operations: [RecordOperation], recordType _: String) async throws + func executeBatchOperations(_ operations: [RecordOperation]) async throws /// Query all records of a specific type, automatically paginating /// diff --git a/Sources/MistKit/Service/Extensions/CloudKitService+RecordManaging.swift b/Sources/MistKit/Service/Extensions/CloudKitService+RecordManaging.swift index e0f29bdb..db479660 100644 --- a/Sources/MistKit/Service/Extensions/CloudKitService+RecordManaging.swift +++ b/Sources/MistKit/Service/Extensions/CloudKitService+RecordManaging.swift @@ -60,10 +60,7 @@ extension CloudKitService: RecordManaging { } /// Execute a batch of record operations via modify - public func executeBatchOperations( - _ operations: [RecordOperation], - recordType _: String - ) async throws { + public func executeBatchOperations(_ operations: [RecordOperation]) async throws { _ = try await self.modifyRecords( operations, database: .public(.prefers(.serverToServer)) diff --git a/Sources/MistKit/Service/Extensions/CloudKitService+UserOperations.swift b/Sources/MistKit/Service/Extensions/CloudKitService+UserOperations.swift index d119473e..f973b96b 100644 --- a/Sources/MistKit/Service/Extensions/CloudKitService+UserOperations.swift +++ b/Sources/MistKit/Service/Extensions/CloudKitService+UserOperations.swift @@ -78,40 +78,6 @@ extension CloudKitService { try await fetchCaller() } - /// Discover all user identities in the caller's CloudKit address book. - /// - /// Hits CloudKit's GET `users/discover` endpoint. Routed against the public - /// database with web-auth credentials. - /// - /// > Important: Marked `unavailable` until #28 is resolved — see issue for - /// > the live-testing investigation log. - @available( - *, unavailable, - message: "Not yet ready: GET /users/discover returns HTTP 500 in live testing. See #28." - ) - public func discoverAllUserIdentities() async throws(CloudKitError) -> [UserIdentity] { - do { - let client = try self.client(for: .public(.requires(.webAuth))) - let response = try await client.discoverAllUserIdentities( - .init( - path: Operations.discoverAllUserIdentities.Input.Path( - containerIdentifier: containerIdentifier, - environment: environment, - database: .public(.requires(.webAuth)) - ) - ) - ) - - let discoverData: Components.Schemas.DiscoverResponse = - try await responseProcessor.processDiscoverAllUserIdentitiesResponse( - response - ) - return discoverData.users?.map(UserIdentity.init(from:)) ?? [] - } catch { - throw mapToCloudKitError(error, context: "discoverAllUserIdentities") - } - } - /// Look up user identities by email address. /// /// Hits CloudKit's POST `users/lookup/email` endpoint. Each requested email diff --git a/Sources/MistKit/Service/ResponseProcessing/CloudKitResponseProcessor+Changes.swift b/Sources/MistKit/Service/ResponseProcessing/CloudKitResponseProcessor+Changes.swift index 5c25e960..cd35f2e2 100644 --- a/Sources/MistKit/Service/ResponseProcessing/CloudKitResponseProcessor+Changes.swift +++ b/Sources/MistKit/Service/ResponseProcessing/CloudKitResponseProcessor+Changes.swift @@ -74,32 +74,6 @@ extension CloudKitResponseProcessor { } } - /// Process discoverAllUserIdentities response. - /// - /// Marked unavailable in lockstep with `CloudKitService.discoverAllUserIdentities()`. - /// The body throws `CloudKitError.unsupportedOperationType` so any stray - /// caller (for example via `@testable import` under Swift 6.1, where the - /// `@available(*, unavailable)` cascade does not apply) gets a recoverable - /// error rather than a crash. When #28 is resolved, restore the - /// protocol-generic implementation and re-add the `CloudKitResponseType` - /// conformance for `Operations.discoverAllUserIdentities.Output`. - /// - /// The `@available(*, unavailable)` attribute is gated to Swift 6.2+ because - /// Swift 6.1 rejects calls to an unavailable function from within another - /// unavailable function; 6.2 relaxed that rule. Once Swift 6.1 is dropped - /// from the support matrix, delete the `#if swift(>=6.2)`/`#endif` lines so - /// the attribute always applies. - #if swift(>=6.2) - @available(*, unavailable, message: "Pending #28: discoverAllUserIdentities is not yet ready.") - #endif - internal func processDiscoverAllUserIdentitiesResponse( - _: Operations.discoverAllUserIdentities.Output - ) async throws(CloudKitError) -> Components.Schemas.DiscoverResponse { - throw CloudKitError.unsupportedOperationType( - "discoverAllUserIdentities is not yet ready (pending #28)" - ) - } - /// Process lookupUsersByEmail response internal func processLookupUsersByEmailResponse( _ response: Operations.lookupUsersByEmail.Output diff --git a/Tests/MistKitTests/Mocks/ResponseConfig.swift b/Tests/MistKitTests/Mocks/ResponseConfig.swift index 29d878f7..66746228 100644 --- a/Tests/MistKitTests/Mocks/ResponseConfig.swift +++ b/Tests/MistKitTests/Mocks/ResponseConfig.swift @@ -139,8 +139,8 @@ extension ResponseConfig { .networkError(URLError(.networkConnectionLost)) } - /// Creates a successful query response - internal static func successfulQuery(records _: [String: Any] = [:]) -> ResponseConfig { + /// Creates a successful query response with an empty records body + internal static func successfulQuery() -> ResponseConfig { let responseJSON = """ { "records": [] diff --git a/Tests/MistKitTests/Mocks/ResponseProvider.swift b/Tests/MistKitTests/Mocks/ResponseProvider.swift index f3d251cc..db8e7ac9 100644 --- a/Tests/MistKitTests/Mocks/ResponseProvider.swift +++ b/Tests/MistKitTests/Mocks/ResponseProvider.swift @@ -68,8 +68,8 @@ internal actor ResponseProvider { } /// Response provider for successful query operations - internal static func successfulQuery(records: [String: Any] = [:]) -> ResponseProvider { - ResponseProvider(defaultResponse: .successfulQuery(records: records)) + internal static func successfulQuery() -> ResponseProvider { + ResponseProvider(defaultResponse: .successfulQuery()) } /// Response provider that simulates a request timeout. diff --git a/Tests/MistKitTests/Protocols/MockRecordManagingService.swift b/Tests/MistKitTests/Protocols/MockRecordManagingService.swift index 205218b4..32888dc0 100644 --- a/Tests/MistKitTests/Protocols/MockRecordManagingService.swift +++ b/Tests/MistKitTests/Protocols/MockRecordManagingService.swift @@ -49,9 +49,7 @@ internal actor MockRecordManagingService: RecordManaging { return recordsToReturn } - internal func executeBatchOperations(_ operations: [RecordOperation], recordType _: String) - async throws - { + internal func executeBatchOperations(_ operations: [RecordOperation]) async throws { executeCallCount += 1 batchSizes.append(operations.count) lastExecutedOperations.append(contentsOf: operations) diff --git a/Tests/MistKitTests/Service/CloudKitServiceQuery/CloudKitServiceTests.Query+Helpers.swift b/Tests/MistKitTests/Service/CloudKitServiceQuery/CloudKitServiceTests.Query+Helpers.swift index 28ca19b9..28db6866 100644 --- a/Tests/MistKitTests/Service/CloudKitServiceQuery/CloudKitServiceTests.Query+Helpers.swift +++ b/Tests/MistKitTests/Service/CloudKitServiceQuery/CloudKitServiceTests.Query+Helpers.swift @@ -50,11 +50,9 @@ extension CloudKitServiceTests.Query { /// Create service for successful operations @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) - internal static func makeSuccessfulService( - records: [String: Any] = [:] - ) throws -> CloudKitService { + internal static func makeSuccessfulService() throws -> CloudKitService { let transport = MockTransport( - responseProvider: .successfulQuery(records: records) + responseProvider: .successfulQuery() ) return try CloudKitService( containerIdentifier: TestConstants.serviceContainerIdentifier,