From a2a2f451b53d31c564a59809f46cfd84fabcbada Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Thu, 3 Sep 2026 15:13:46 -0400 Subject: [PATCH] Add RecordName value type for record identifiers (#465). Co-authored-by: Cursor --- AGENTS.md | 1 + .../Commands/ExportCommand.swift | 2 +- .../CloudKit/BushelCloudKitService.swift | 2 +- .../BushelCloudKit/CloudKit/SyncEngine.swift | 42 +++++---- .../DataSourceMetadata+CloudKit.swift | 4 + .../RestoreImageRecord+CloudKit.swift | 4 + .../SwiftVersionRecord+CloudKit.swift | 4 + .../XcodeVersionRecord+CloudKit.swift | 13 ++- .../CloudKit/MockCloudKitServiceTests.swift | 10 +-- .../Mocks/MockCloudKitService.swift | 2 +- .../Utilities/FieldValue+Assertions.swift | 2 +- .../Utilities/MockRecordInfo.swift | 2 +- .../Extensions/Article+MistKit.swift | 2 +- .../Extensions/Feed+MistKit.swift | 2 +- .../Services/ArticleOperationBuilder.swift | 4 +- .../Services/CloudKitService+Celestra.swift | 4 +- .../Services/FeedCloudKitService.swift | 4 +- .../ArticleConversion+FromCloudKit.swift | 2 +- .../Extensions/FeedConversion+RoundTrip.swift | 2 +- .../Models/BatchOperationResultTests.swift | 2 +- .../ArticleCloudKitService+Mutations.swift | 2 +- .../ArticleCloudKitService+Query.swift | 2 +- .../Services/ArticleSyncService.swift | 2 +- .../Services/FeedCloudKitService+CRUD.swift | 2 +- .../Services/FeedCloudKitService+Query.swift | 2 +- .../MistDemoKit/Commands/CreateCommand.swift | 2 +- .../MistDemoKit/Commands/DeleteCommand.swift | 2 +- .../Commands/DemoErrorsRunner.swift | 10 +-- .../Commands/DemoInFilterCommand.swift | 8 +- .../Commands/LookupAllRecordsCommand.swift | 2 +- .../MistDemoKit/Commands/LookupCommand.swift | 4 +- .../MistDemoKit/Commands/ModifyCommand.swift | 2 +- .../Commands/RereferenceAssetCommand.swift | 4 +- .../MistDemoKit/Commands/UpdateCommand.swift | 2 +- .../Commands/UploadAssetCommand.swift | 8 +- .../Configuration/ModifyOperationInput.swift | 6 +- .../ChangeTrackingVerification.swift | 4 +- .../Phases/ChangesRequestOptionsPhase.swift | 2 +- .../Integration/Phases/CleanupPhase.swift | 2 +- .../Phases/CreateRecordsPhase.swift | 4 +- .../Phases/CustomZoneQueryPhase.swift | 8 +- .../Phases/IncrementalSyncPhase.swift | 2 +- .../Integration/Phases/InitialSyncPhase.swift | 2 +- .../Phases/LookupRecordsPhase.swift | 2 +- .../Phases/ModifyRecordsPhase.swift | 2 +- .../Phases/ModifyRequestOptionsPhase.swift | 2 +- .../Phases/NotificationRoundtripPhase.swift | 10 +-- .../Phases/QueryRecordsPhase.swift | 2 +- .../Phases/QueryRequestOptionsPhase.swift | 2 +- .../Phases/RereferenceAssetPhase.swift | 6 +- .../Phases/ShareCreateAndAcceptPhase.swift | 8 +- .../Phases/SharedZoneRoundtripPhase.swift | 14 +-- .../Output/Formatters/CSVFormatter.swift | 2 +- .../Output/Formatters/TableFormatter.swift | 2 +- .../Output/Formatters/YAMLFormatter.swift | 2 +- .../Protocols/OutputFormatting+Records.swift | 6 +- .../CloudKitService+WebBackend+Reads.swift | 2 +- .../Server/CloudKitService+WebBackend.swift | 12 +-- .../Utilities/FieldValueFormatter.swift | 2 +- .../MockBackend+ServiceOperations.swift | 2 +- .../Server/MockBackend+ShareOperations.swift | 4 +- ReleaseNotes.md | 1 + .../CloudKitService+AssetOperations.swift | 6 +- .../CloudKitService+AssetRereference.swift | 12 +-- .../CloudKitService+Classification.swift | 4 +- .../CloudKitService+CreateShare.swift | 8 +- .../CloudKitService+LookupAllRecords.swift | 2 +- .../CloudKitService+LookupOperations.swift | 4 +- ...oudKitService+RecordWriteConvenience.swift | 6 +- .../AssetUploading/AssetUploadReceipt.swift | 4 +- .../AssetUploading/AssetUploadToken.swift | 6 +- .../FieldValues/FieldValue+Components.swift | 2 +- .../Models/FieldValues/Reference.swift | 4 +- .../Notifications/CourierNotification.swift | 4 +- .../Models/OperationClassification.swift | 16 ++-- Sources/MistKit/Models/RecordInfo.swift | 6 +- Sources/MistKit/Models/RecordName.swift | 85 +++++++++++++++++++ Sources/MistKit/Models/RecordOperation.swift | 10 +-- Sources/MistKit/Models/RecordTarget.swift | 2 +- .../MistKit/Models/Sharing/CreatedShare.swift | 4 +- .../Models/Sharing/ShareRecordInfo.swift | 6 +- .../Models/Sharing/ShareTargetReference.swift | 6 +- ...Components.Schemas.FieldValueRequest.swift | 2 +- .../Components.Schemas.ListValuePayload.swift | 2 +- .../Components.Schemas.RecordOperation.swift | 2 +- .../RecordManagement/CloudKitRecord.swift | 2 +- ...Tests.BatchChunking+LookupAllRecords.swift | 12 +-- ...dKitServiceTests.Upload+SuccessCases.swift | 2 +- .../Models/BatchSyncResultTests.swift | 2 +- .../MistKitTests/Models/RecordNameTests.swift | 75 ++++++++++++++++ .../RecordManagement/AltTestRecord.swift | 2 +- .../RecordManagingTests+Sync.swift | 8 +- .../RecordManagement/TestRecord.swift | 2 +- 93 files changed, 388 insertions(+), 197 deletions(-) create mode 100644 Sources/MistKit/Models/RecordName.swift create mode 100644 Tests/MistKitTests/Models/RecordNameTests.swift diff --git a/AGENTS.md b/AGENTS.md index 3e98fb9d0..63cebaf04 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -257,6 +257,7 @@ The `users/lookup/email` and `users/lookup/id` primitives (`lookupUsersByEmail` In MistDemo, integration runs targeting these endpoints use `PhaseContext.userContextService` (a public+web-auth `CloudKitService`) which is built from `CLOUDKIT_API_TOKEN` + `CLOUDKIT_WEB_AUTH_TOKEN` regardless of the primary `--database` selection. The `DatabaseConfiguration` / `AuthenticationCredentials` types in `Examples/MistDemo/Sources/MistDemoKit/Configuration/` enforce valid database+auth combinations at construction time. **Result Types (Sources/MistKit/Models/ and Sources/MistKit/Models/Zones/):** +- `RecordName` — string-backed struct for a record's identity within a zone (UUID or custom string). Encodes as a JSON string. Not a `typealias` (that would not distinguish record IDs from owner/zone names) and not an enum (the set of names is open). Distinct from `UserRecordName`. - `QueryResult` — `records: [RecordInfo]`, `continuationMarker: String?` - `RecordChangesResult` — `records: [RecordInfo]`, `syncToken: String?`, `moreComing: Bool` - `ZoneChangesResult` — `zones: [ZoneInfo]`, `syncToken: String?`, `moreComing: Bool` *(deprecated `zones/changes`; `syncToken` rides the wire as `metaSyncToken` — see #430 above)* diff --git a/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ExportCommand.swift b/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ExportCommand.swift index 985f2a985..afc39e065 100644 --- a/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ExportCommand.swift +++ b/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ExportCommand.swift @@ -48,7 +48,7 @@ internal enum ExportCommand { let fields: [String: String] init(from recordInfo: RecordInfo) { - self.recordName = recordInfo.recordName + self.recordName = recordInfo.recordName.rawValue self.recordType = recordInfo.recordType self.fields = recordInfo.fields.mapValues { fieldValue in String(describing: fieldValue) diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift index ca5874900..f3a1df173 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift @@ -100,7 +100,7 @@ public struct BushelCloudKitService: Sendable, RecordManaging, CloudKitRecordCol desiredKeys: [], database: .public(.prefers(.serverToServer)) ) - let recordNames = Set(records.map(\.recordName)) + let recordNames = Set(records.map(\.recordName.rawValue)) Self.logger.debug("Found \(recordNames.count) existing \(recordType) records") return recordNames diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/SyncEngine.swift b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/SyncEngine.swift index 03eca0348..6d1260d5d 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/SyncEngine.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/SyncEngine.swift @@ -231,16 +231,16 @@ public struct SyncEngine: Sendable { // Classify operations for each type let swiftClassification = OperationClassification( - proposedRecordNames: fetchResult.swiftVersions.map(\.recordName), - existingRecordNames: swiftNames + proposedRecordNames: fetchResult.swiftVersions.map { RecordName($0.recordName) }, + existingRecordNames: Set(swiftNames.map(RecordName.init(rawValue:))) ) let restoreClassification = OperationClassification( - proposedRecordNames: fetchResult.restoreImages.map(\.recordName), - existingRecordNames: restoreNames + proposedRecordNames: fetchResult.restoreImages.map { RecordName($0.recordName) }, + existingRecordNames: Set(restoreNames.map(RecordName.init(rawValue:))) ) let xcodeClassification = OperationClassification( - proposedRecordNames: fetchResult.xcodeVersions.map(\.recordName), - existingRecordNames: xcodeNames + proposedRecordNames: fetchResult.xcodeVersions.map { RecordName($0.recordName) }, + existingRecordNames: Set(xcodeNames.map(RecordName.init(rawValue:))) ) Self.logger.debug( @@ -252,15 +252,24 @@ public struct SyncEngine: Sendable { // XcodeVersion last (references the other two) let swiftResult = try await syncRecords( fetchResult.swiftVersions, - classification: swiftClassification + classification: swiftClassification, + recordType: SwiftVersionRecord.cloudKitRecordType, + name: \.recordName, + fields: { $0.toCloudKitFields() } ) let restoreResult = try await syncRecords( fetchResult.restoreImages, - classification: restoreClassification + classification: restoreClassification, + recordType: RestoreImageRecord.cloudKitRecordType, + name: \.recordName, + fields: { $0.toCloudKitFields() } ) let xcodeResult = try await syncRecords( fetchResult.xcodeVersions, - classification: xcodeClassification + classification: xcodeClassification, + recordType: XcodeVersionRecord.cloudKitRecordType, + name: \.recordName, + fields: { $0.toCloudKitFields() } ) print("\n" + String(repeating: "=", count: 60)) @@ -305,9 +314,12 @@ public struct SyncEngine: Sendable { /// - records: Records to sync /// - classification: Classification of operations as creates vs updates /// - Returns: Sync result for this record type - private func syncRecords( + private func syncRecords( _ records: [T], - classification: OperationClassification + classification: OperationClassification, + recordType: String, + name: KeyPath, + fields: (T) -> [String: FieldValue] ) async throws -> TypeSyncResult { guard !records.isEmpty else { return TypeSyncResult(created: 0, updated: 0, failed: 0, failedRecordNames: []) @@ -316,15 +328,15 @@ public struct SyncEngine: Sendable { let operations = records.map { record in RecordOperation( operationType: .forceReplace, - recordType: T.cloudKitRecordType, - recordName: record.recordName, - fields: record.toCloudKitFields() + recordType: recordType, + recordName: RecordName(record[keyPath: name]), + fields: fields(record) ) } return try await cloudKitService.executeBatchOperations( operations, - recordType: T.cloudKitRecordType, + recordType: recordType, classification: classification ) } diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/DataSourceMetadata+CloudKit.swift b/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/DataSourceMetadata+CloudKit.swift index 7b87e7d95..855e0da12 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/DataSourceMetadata+CloudKit.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/DataSourceMetadata+CloudKit.swift @@ -35,6 +35,10 @@ public import MistKit // MARK: - CloudKitRecord Conformance extension DataSourceMetadata: CloudKitRecord { + @_implements(CloudKitRecord, recordName) + public var cloudKitRecordName: RecordName { + RecordName(rawValue: recordName) + } public static var cloudKitRecordType: String { "DataSourceMetadata" } public static func from(recordInfo: RecordInfo) -> Self? { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/RestoreImageRecord+CloudKit.swift b/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/RestoreImageRecord+CloudKit.swift index dace29b31..4efbae54b 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/RestoreImageRecord+CloudKit.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/RestoreImageRecord+CloudKit.swift @@ -35,6 +35,10 @@ public import MistKit // MARK: - CloudKitRecord Conformance extension RestoreImageRecord: @retroactive CloudKitRecord { + @_implements(CloudKitRecord, recordName) + public var cloudKitRecordName: RecordName { + RecordName(rawValue: recordName) + } public static var cloudKitRecordType: String { "RestoreImage" } public static func from(recordInfo: RecordInfo) -> Self? { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/SwiftVersionRecord+CloudKit.swift b/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/SwiftVersionRecord+CloudKit.swift index 0fe147d10..4c128c590 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/SwiftVersionRecord+CloudKit.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/SwiftVersionRecord+CloudKit.swift @@ -35,6 +35,10 @@ public import MistKit // MARK: - CloudKitRecord Conformance extension SwiftVersionRecord: @retroactive CloudKitRecord { + @_implements(CloudKitRecord, recordName) + public var cloudKitRecordName: RecordName { + RecordName(rawValue: recordName) + } public static var cloudKitRecordType: String { "SwiftVersion" } public static func from(recordInfo: RecordInfo) -> Self? { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/XcodeVersionRecord+CloudKit.swift b/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/XcodeVersionRecord+CloudKit.swift index 8b76bb814..b23522ee6 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/XcodeVersionRecord+CloudKit.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/Extensions/XcodeVersionRecord+CloudKit.swift @@ -35,6 +35,10 @@ public import MistKit // MARK: - CloudKitRecord Conformance extension XcodeVersionRecord: @retroactive CloudKitRecord { + @_implements(CloudKitRecord, recordName) + public var cloudKitRecordName: RecordName { + RecordName(rawValue: recordName) + } public static var cloudKitRecordType: String { "XcodeVersion" } public static func from(recordInfo: RecordInfo) -> Self? { @@ -52,8 +56,9 @@ extension XcodeVersionRecord: @retroactive CloudKitRecord { downloadURL: recordInfo.fields["downloadURL"]?.urlValue, fileSize: recordInfo.fields["fileSize"]?.intValue, isPrerelease: recordInfo.fields["isPrerelease"]?.boolValue ?? false, - minimumMacOS: recordInfo.fields["minimumMacOS"]?.referenceValue?.recordName, - includedSwiftVersion: recordInfo.fields["includedSwiftVersion"]?.referenceValue?.recordName, + minimumMacOS: recordInfo.fields["minimumMacOS"]?.referenceValue?.recordName.rawValue, + includedSwiftVersion: recordInfo.fields["includedSwiftVersion"]?.referenceValue?.recordName + .rawValue, sdkVersions: recordInfo.fields["sdkVersions"]?.stringValue, notes: recordInfo.fields["notes"]?.stringValue ) @@ -93,7 +98,7 @@ extension XcodeVersionRecord: @retroactive CloudKitRecord { if let minimumMacOS { fields["minimumMacOS"] = .reference( Reference( - recordName: minimumMacOS, + recordName: RecordName(minimumMacOS), action: nil ) ) @@ -102,7 +107,7 @@ extension XcodeVersionRecord: @retroactive CloudKitRecord { if let includedSwiftVersion { fields["includedSwiftVersion"] = .reference( Reference( - recordName: includedSwiftVersion, + recordName: RecordName(includedSwiftVersion), action: nil ) ) diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/MockCloudKitServiceTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/MockCloudKitServiceTests.swift index 63d678fd6..83a970658 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/MockCloudKitServiceTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/MockCloudKitServiceTests.swift @@ -55,7 +55,7 @@ internal struct MockCloudKitServiceTests { let operation = RecordOperation( operationType: .create, recordType: "RestoreImage", - recordName: "RestoreImage-\(record.buildNumber)", + recordName: RecordName("RestoreImage-\(record.buildNumber)"), fields: record.toCloudKitFields() ) @@ -76,7 +76,7 @@ internal struct MockCloudKitServiceTests { let createOp = RecordOperation( operationType: .create, recordType: "RestoreImage", - recordName: recordName, + recordName: RecordName(recordName), fields: initialRecord.toCloudKitFields() ) try await service.executeBatchOperations([createOp]) @@ -100,7 +100,7 @@ internal struct MockCloudKitServiceTests { let replaceOp = RecordOperation( operationType: .forceReplace, recordType: "RestoreImage", - recordName: recordName, + recordName: RecordName(recordName), fields: updatedRecord.toCloudKitFields() ) try await service.executeBatchOperations([replaceOp]) @@ -127,7 +127,7 @@ internal struct MockCloudKitServiceTests { let createOp = RecordOperation( operationType: .create, recordType: "RestoreImage", - recordName: recordName, + recordName: RecordName(recordName), fields: record.toCloudKitFields() ) try await service.executeBatchOperations([createOp]) @@ -136,7 +136,7 @@ internal struct MockCloudKitServiceTests { let deleteOp = RecordOperation( operationType: .delete, recordType: "RestoreImage", - recordName: recordName + recordName: RecordName(recordName) ) try await service.executeBatchOperations([deleteOp]) diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockCloudKitService.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockCloudKitService.swift index c4c3cc4bd..a599f784b 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockCloudKitService.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockCloudKitService.swift @@ -166,7 +166,7 @@ internal actor MockCloudKitService: RecordManaging { private func createRecordInfo(from operation: RecordOperation) -> RecordInfo { RecordInfo( - recordName: operation.recordName ?? UUID().uuidString, + recordName: operation.recordName ?? RecordName(UUID().uuidString), recordType: operation.recordType, recordChangeTag: UUID().uuidString, fields: operation.fields diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/FieldValue+Assertions.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/FieldValue+Assertions.swift index b985a0d14..2efb5b4f6 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/FieldValue+Assertions.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/FieldValue+Assertions.swift @@ -77,7 +77,7 @@ extension FieldValue { Issue.record("Expected .reference, got \(self)") return } - #expect(ref.recordName == expectedRecordName) + #expect(ref.recordName.rawValue == expectedRecordName) } /// Asserts that this FieldValue is a date (does not validate the exact value) diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/MockRecordInfo.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/MockRecordInfo.swift index 1a0808c34..19e44d9dc 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/MockRecordInfo.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/MockRecordInfo.swift @@ -44,7 +44,7 @@ public enum MockRecordInfo: Sendable { fields: [String: FieldValue] ) -> RecordInfo { RecordInfo( - recordName: recordName, + recordName: RecordName(recordName), recordType: recordType, recordChangeTag: nil, fields: fields diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Extensions/Article+MistKit.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Extensions/Article+MistKit.swift index bd526ac41..ac7279073 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Extensions/Article+MistKit.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Extensions/Article+MistKit.swift @@ -58,7 +58,7 @@ extension Article: CloudKitConvertible { let tags = record.stringArray(forKey: "tags") self.init( - recordName: record.recordName, + recordName: record.recordName.rawValue, recordChangeTag: record.recordChangeTag, feedRecordName: feedRecordName, guid: guid, diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Extensions/Feed+MistKit.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Extensions/Feed+MistKit.swift index 09ba2c95a..d7e33ed45 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Extensions/Feed+MistKit.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Extensions/Feed+MistKit.swift @@ -64,7 +64,7 @@ extension Feed: CloudKitConvertible { let tags = record.stringArray(forKey: "tags") self.init( - recordName: record.recordName, + recordName: record.recordName.rawValue, recordChangeTag: record.recordChangeTag, feedURL: feedURL, title: title, diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleOperationBuilder.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleOperationBuilder.swift index fcc13c070..07c28af0e 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleOperationBuilder.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleOperationBuilder.swift @@ -45,7 +45,7 @@ public struct ArticleOperationBuilder: Sendable { articles.map { article in RecordOperation.create( recordType: "Article", - recordName: UUID().uuidString, + recordName: RecordName(UUID().uuidString), fields: article.toFieldsDict() ) } @@ -68,7 +68,7 @@ public struct ArticleOperationBuilder: Sendable { return RecordOperation.update( recordType: "Article", - recordName: recordName, + recordName: RecordName(recordName), fields: article.toFieldsDict(), recordChangeTag: article.recordChangeTag ) diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CloudKitService+Celestra.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CloudKitService+Celestra.swift index 13f647046..f4cdb02e7 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CloudKitService+Celestra.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CloudKitService+Celestra.swift @@ -42,7 +42,7 @@ extension CloudKitService { let operation = RecordOperation.create( recordType: "Feed", - recordName: UUID().uuidString, + recordName: RecordName(UUID().uuidString), fields: feed.toFieldsDict() ) let results = try await self.modifyRecords([operation]) @@ -58,7 +58,7 @@ extension CloudKitService { let operation = RecordOperation.update( recordType: "Feed", - recordName: recordName, + recordName: RecordName(recordName), fields: feed.toFieldsDict(), recordChangeTag: feed.recordChangeTag ) diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/FeedCloudKitService.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/FeedCloudKitService.swift index 7f62b556e..c211b83cf 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/FeedCloudKitService.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/FeedCloudKitService.swift @@ -53,7 +53,7 @@ public struct FeedCloudKitService: Sendable { let operation = RecordOperation.create( recordType: "Feed", - recordName: UUID().uuidString, + recordName: RecordName(UUID().uuidString), fields: feed.toFieldsDict() ) let results = try await recordOperator.modifyRecords([operation]) @@ -74,7 +74,7 @@ public struct FeedCloudKitService: Sendable { let operation = RecordOperation.update( recordType: "Feed", - recordName: recordName, + recordName: RecordName(recordName), fields: feed.toFieldsDict(), recordChangeTag: feed.recordChangeTag ) diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+FromCloudKit.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+FromCloudKit.swift index b628db729..6d078d89a 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+FromCloudKit.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+FromCloudKit.swift @@ -161,7 +161,7 @@ extension ArticleConversion { // Create a record let record = RecordInfo( - recordName: originalArticle.recordName ?? "roundtrip-article", + recordName: RecordName(originalArticle.recordName ?? "roundtrip-article"), recordType: "Article", recordChangeTag: originalArticle.recordChangeTag, fields: fields diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+RoundTrip.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+RoundTrip.swift index 0a0e92b58..6c7394369 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+RoundTrip.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+RoundTrip.swift @@ -73,7 +73,7 @@ extension FeedConversion { // Create a record let record = RecordInfo( - recordName: originalFeed.recordName ?? "round-trip", + recordName: RecordName(originalFeed.recordName ?? "round-trip"), recordType: "Feed", recordChangeTag: originalFeed.recordChangeTag, fields: fields diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Models/BatchOperationResultTests.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Models/BatchOperationResultTests.swift index e8176827a..a0d0058f5 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Models/BatchOperationResultTests.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Models/BatchOperationResultTests.swift @@ -193,7 +193,7 @@ internal struct BatchOperationResultTests { private func createTestRecords(count: Int) -> [RecordInfo] { (0.. RecordInfo { RecordInfo( - recordName: recordName, + recordName: RecordName(recordName), recordType: "Article", recordChangeTag: "tag-123", fields: fields diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift index 130a1c16e..d440acc30 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift @@ -44,7 +44,7 @@ extension ArticleCloudKitService { fields: [String: FieldValue] = [:] ) -> RecordInfo { RecordInfo( - recordName: recordName, + recordName: RecordName(recordName), recordType: "Article", recordChangeTag: "tag-123", fields: fields diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleSyncService.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleSyncService.swift index 16df48dc5..f624b5964 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleSyncService.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleSyncService.swift @@ -64,7 +64,7 @@ extension ArticleSyncService { fields: [String: FieldValue] = [:] ) -> RecordInfo { RecordInfo( - recordName: recordName, + recordName: RecordName(recordName), recordType: "Article", recordChangeTag: "tag-123", fields: fields diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+CRUD.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+CRUD.swift index 081f90d7b..c875ae612 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+CRUD.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+CRUD.swift @@ -44,7 +44,7 @@ extension FeedCloudKitService { fields: [String: FieldValue] = [:] ) -> RecordInfo { RecordInfo( - recordName: recordName, + recordName: RecordName(recordName), recordType: "Feed", recordChangeTag: "tag-123", fields: fields diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+Query.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+Query.swift index 997415115..7efbe40cd 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+Query.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+Query.swift @@ -44,7 +44,7 @@ extension FeedCloudKitService { fields: [String: FieldValue] = [:] ) -> RecordInfo { RecordInfo( - recordName: recordName, + recordName: RecordName(recordName), recordType: "Feed", recordChangeTag: "tag-123", fields: fields diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/CreateCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/CreateCommand.swift index 425ebbffd..a07c95d73 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/CreateCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/CreateCommand.swift @@ -83,7 +83,7 @@ public struct CreateCommand: MistDemoCommand, OutputFormatting { // NOTE: Zone support requires enhancements to CloudKitService.createRecord method let recordInfo = try await client.createRecord( recordType: config.recordType, - recordName: recordName, + recordName: RecordName(recordName), fields: cloudKitFields, // Zone: config.zone - to be added when CloudKitService supports it database: config.base.database diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteCommand.swift index af125cf9c..8a09aef07 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteCommand.swift @@ -93,7 +93,7 @@ public struct DeleteCommand: MistDemoCommand, OutputFormatting { try await client.deleteRecord( recordType: config.recordType, - recordName: config.recordName, + recordName: RecordName(config.recordName), recordChangeTag: effectiveChangeTag, database: config.base.database ) diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner.swift index ae9b8219d..77b625942 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner.swift @@ -144,11 +144,11 @@ internal struct DemoErrorsRunner { do { let created = try await service.createRecord( recordType: Self.conflictRecordType, - recordName: recordName, + recordName: RecordName(recordName), fields: ["title": .string("original")], database: config.database ) - createdRecordName = created.recordName + createdRecordName = created.recordName.rawValue staleTag = created.recordChangeTag } catch { print("❌ Setup create failed: \(error)") @@ -159,7 +159,7 @@ internal struct DemoErrorsRunner { do { _ = try await service.updateRecord( recordType: Self.conflictRecordType, - recordName: recordName, + recordName: RecordName(recordName), fields: ["title": .string("first-update")], recordChangeTag: staleTag, database: config.database @@ -173,7 +173,7 @@ internal struct DemoErrorsRunner { do { _ = try await service.updateRecord( recordType: Self.conflictRecordType, - recordName: recordName, + recordName: RecordName(recordName), fields: ["title": .string("second-update-stale")], recordChangeTag: staleTag, database: config.database @@ -201,7 +201,7 @@ internal struct DemoErrorsRunner { do { try await service.deleteRecord( recordType: Self.conflictRecordType, - recordName: createdRecordName, + recordName: RecordName(createdRecordName), database: config.database ) print(" ✅ Deleted.") diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoInFilterCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoInFilterCommand.swift index 16d57afb9..f4c3aa751 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoInFilterCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoInFilterCommand.swift @@ -109,7 +109,7 @@ public struct DemoInFilterCommand: MistDemoCommand { ], database: config.database ) - createdNames.append(record.recordName) + createdNames.append(record.recordName.rawValue) print(" Created \(record.recordName) (index=\(idx))") } return createdNames @@ -127,7 +127,7 @@ public struct DemoInFilterCommand: MistDemoCommand { database: config.database ).records let visible = allRecords.filter { - createdNames.contains($0.recordName) + createdNames.contains($0.recordName.rawValue) } print(" Visible: \(visible.count)") if visible.count < 3 { @@ -145,7 +145,7 @@ public struct DemoInFilterCommand: MistDemoCommand { ).records let matching = results.filter { - createdNames.contains($0.recordName) + createdNames.contains($0.recordName.rawValue) } print("Matching demo records: \(matching.count) (expected 2)") @@ -166,7 +166,7 @@ public struct DemoInFilterCommand: MistDemoCommand { let operation = RecordOperation( operationType: .forceDelete, recordType: recordType, - recordName: name + recordName: RecordName(name) ) _ = try await client.modifyRecords( [operation], diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupAllRecordsCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupAllRecordsCommand.swift index 8b45bb9ad..8554dcb2a 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupAllRecordsCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupAllRecordsCommand.swift @@ -85,7 +85,7 @@ public struct LookupAllRecordsCommand: MistDemoCommand, OutputFormatting { FileHandle.standardError.write(Data(note.utf8)) let results = try await client.lookupAllRecords( - recordNames: config.recordNames, + recordNames: config.recordNames.map(RecordName.init(rawValue:)), desiredKeys: config.fields, database: config.base.database, batchSize: config.batchSize diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupCommand.swift index b291301bd..53a8c7986 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupCommand.swift @@ -77,7 +77,7 @@ public struct LookupCommand: MistDemoCommand, OutputFormatting { let client = try MistKitClientFactory.create(for: config.base) let results = try await client.lookupRecords( - recordNames: config.recordNames, + recordNames: config.recordNames.map(RecordName.init(rawValue:)), desiredKeys: config.fields, database: config.base.database ) @@ -88,7 +88,7 @@ public struct LookupCommand: MistDemoCommand, OutputFormatting { } // Report missing names to stderr so a JSON/CSV/etc. stdout stream stays parseable - let foundNames = Set(records.map(\.recordName)) + let foundNames = Set(records.map(\.recordName.rawValue)) let missing = config.recordNames.filter { !foundNames.contains($0) } if !missing.isEmpty { let line = diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyCommand.swift index 28211fb4b..13a778cd5 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyCommand.swift @@ -75,7 +75,7 @@ public struct ModifyCommand: MistDemoCommand, OutputFormatting { ModifyResultRow( operation: "applied", recordType: record.recordType, - recordName: record.recordName, + recordName: record.recordName.rawValue, recordChangeTag: record.recordChangeTag ) } diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/RereferenceAssetCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/RereferenceAssetCommand.swift index 286558222..0365bd352 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/RereferenceAssetCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/RereferenceAssetCommand.swift @@ -91,9 +91,9 @@ public struct RereferenceAssetCommand: MistDemoCommand, OutputFormatting { do { let service = try MistKitClientFactory.create(for: config.base) let record = try await service.rereferenceAsset( - fromRecord: sourceRecord, + fromRecord: RecordName(sourceRecord), field: assetField, - toRecord: targetRecord, + toRecord: RecordName(targetRecord), field: config.targetAssetField, database: config.base.database ) diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/UpdateCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/UpdateCommand.swift index 69fd6e0af..3edf1b4e5 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/UpdateCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/UpdateCommand.swift @@ -106,7 +106,7 @@ public struct UpdateCommand: MistDemoCommand, OutputFormatting { let recordInfo = try await client.updateRecord( recordType: config.recordType, - recordName: config.recordName, + recordName: RecordName(config.recordName), fields: cloudKitFields, recordChangeTag: effectiveChangeTag, database: config.base.database diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/UploadAssetCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/UploadAssetCommand.swift index 8d87a9aa7..5b8dcb5b0 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/UploadAssetCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/UploadAssetCommand.swift @@ -137,7 +137,7 @@ public struct UploadAssetCommand: MistDemoCommand, OutputFormatting { data: data, recordType: config.recordType, fieldName: config.fieldName, - recordName: config.recordName, + recordName: config.recordName.map(RecordName.init(rawValue:)), database: config.base.database ) print("\n✅ Asset uploaded!") @@ -186,7 +186,7 @@ public struct UploadAssetCommand: MistDemoCommand, OutputFormatting { let newRecordName = UUID().uuidString.lowercased() return try await service.createRecord( recordType: config.recordType, - recordName: newRecordName, + recordName: RecordName(newRecordName), fields: fields, database: config.base.database ) @@ -199,7 +199,7 @@ public struct UploadAssetCommand: MistDemoCommand, OutputFormatting { service: CloudKitService ) async throws -> RecordInfo { let existingRecords = try await service.lookupRecords( - recordNames: [recordName], + recordNames: [RecordName(recordName)], database: config.base.database ) guard let firstResult = existingRecords.first, @@ -211,7 +211,7 @@ public struct UploadAssetCommand: MistDemoCommand, OutputFormatting { } return try await service.updateRecord( recordType: config.recordType, - recordName: recordName, + recordName: RecordName(recordName), fields: fields, recordChangeTag: existingRecord.recordChangeTag, database: config.base.database diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/ModifyOperationInput.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ModifyOperationInput.swift index 86376338d..0847677b7 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/ModifyOperationInput.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ModifyOperationInput.swift @@ -80,7 +80,7 @@ public struct ModifyOperationInput: Codable, Sendable { case .create: return RecordOperation.create( recordType: recordType, - recordName: recordName, + recordName: recordName.map(RecordName.init(rawValue:)), fields: cloudKitFields ) case .update: @@ -92,7 +92,7 @@ public struct ModifyOperationInput: Codable, Sendable { } return RecordOperation.update( recordType: recordType, - recordName: recordName, + recordName: RecordName(recordName), fields: cloudKitFields, recordChangeTag: recordChangeTag ) @@ -105,7 +105,7 @@ public struct ModifyOperationInput: Codable, Sendable { } return RecordOperation.delete( recordType: recordType, - recordName: recordName, + recordName: RecordName(recordName), recordChangeTag: recordChangeTag ) } diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/ChangeTrackingVerification.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/ChangeTrackingVerification.swift index 4bd4550e5..ae44bacfb 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/ChangeTrackingVerification.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/ChangeTrackingVerification.swift @@ -57,7 +57,7 @@ internal enum ChangeTrackingVerification { RecordOperation( operationType: .forceUpdate, recordType: MistDemoConfig.recordType, - recordName: recordName, + recordName: RecordName(recordName), fields: [ "title": .string("Zone changes \(index + 1)"), "index": .int64(index + 1), @@ -120,7 +120,7 @@ internal enum ChangeTrackingVerification { in changes: [ZoneRecordChanges] ) -> Set { Set( - changes.flatMap(\.records).map(\.recordName) + changes.flatMap(\.records).map(\.recordName.rawValue) ) } diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ChangesRequestOptionsPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ChangesRequestOptionsPhase.swift index 18188e257..543adeb75 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ChangesRequestOptionsPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ChangesRequestOptionsPhase.swift @@ -88,7 +88,7 @@ internal struct ChangesRequestOptionsPhase: IntegrationPhase { RecordOperation( operationType: .forceUpdate, recordType: MistDemoConfig.recordType, - recordName: "mistkit-test-\(UUID().uuidString.lowercased())", + recordName: RecordName("mistkit-test-\(UUID().uuidString.lowercased())"), fields: [ "title": .string("Changes options \(index)"), "index": .int64(index), diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CleanupPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CleanupPhase.swift index 915fef5d9..4e63b96ff 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CleanupPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CleanupPhase.swift @@ -54,7 +54,7 @@ internal struct CleanupPhase: IntegrationPhase, CleanupPhaseMarker { RecordOperation( operationType: .forceDelete, recordType: MistDemoConfig.recordType, - recordName: recordName + recordName: RecordName(recordName) ) } diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CreateRecordsPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CreateRecordsPhase.swift index 5d4804a48..a10a1bc8d 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CreateRecordsPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CreateRecordsPhase.swift @@ -60,7 +60,7 @@ internal struct CreateRecordsPhase: IntegrationPhase { let recordName = "mistkit-test-\(UUID().uuidString.lowercased())" let record = try await context.service.createRecord( recordType: MistDemoConfig.recordType, - recordName: recordName, + recordName: RecordName(recordName), fields: [ "title": .string("Test Record \(recordIndex)"), "index": .int64(recordIndex), @@ -69,7 +69,7 @@ internal struct CreateRecordsPhase: IntegrationPhase { ], database: context.database ) - createdRecordNames.append(record.recordName) + createdRecordNames.append(record.recordName.rawValue) if context.verbose { print(" ✅ Created: \(record.recordName)") } diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CustomZoneQueryPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CustomZoneQueryPhase.swift index f73a93606..a4ca3fe3f 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CustomZoneQueryPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CustomZoneQueryPhase.swift @@ -80,7 +80,7 @@ internal struct CustomZoneQueryPhase: IntegrationPhase { RecordOperation( operationType: .forceUpdate, recordType: MistDemoConfig.recordType, - recordName: recordName1, + recordName: RecordName(recordName1), fields: [ "title": .string("Zone query 1"), "index": .int64(1), @@ -89,7 +89,7 @@ internal struct CustomZoneQueryPhase: IntegrationPhase { RecordOperation( operationType: .forceUpdate, recordType: MistDemoConfig.recordType, - recordName: recordName2, + recordName: RecordName(recordName2), fields: [ "title": .string("Zone query 2"), "index": .int64(2), @@ -144,7 +144,7 @@ internal struct CustomZoneQueryPhase: IntegrationPhase { zoneID: .defaultZone, database: context.database ) - let defaultNames = Set(defaultResult.records.map(\.recordName)) + let defaultNames = Set(defaultResult.records.map(\.recordName.rawValue)) let leaked = expectedNames.intersection(defaultNames) guard leaked.isEmpty else { try await cleanup(zoneName: zoneName, context: context) @@ -230,7 +230,7 @@ internal struct CustomZoneQueryPhase: IntegrationPhase { zoneID: zoneID, database: context.database ) - lastFound = Set(result.records.map(\.recordName)) + lastFound = Set(result.records.map(\.recordName.rawValue)) if expectedNames.isSubset(of: lastFound) { return lastFound } diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/IncrementalSyncPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/IncrementalSyncPhase.swift index a7c3fda44..bbdb8100a 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/IncrementalSyncPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/IncrementalSyncPhase.swift @@ -69,7 +69,7 @@ internal struct IncrementalSyncPhase: IntegrationPhase { } let changedRecords = incrementalResult.records.filter { - input.recordNames.contains($0.recordName) + input.recordNames.contains($0.recordName.rawValue) } print(" Found \(changedRecords.count) of our modified records") diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/InitialSyncPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/InitialSyncPhase.swift index 86753e92c..07abf2e04 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/InitialSyncPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/InitialSyncPhase.swift @@ -57,7 +57,7 @@ internal struct InitialSyncPhase: IntegrationPhase { print(" More coming: \(initialResult.moreComing)") } - let ourRecords = initialResult.records.filter { input.names.contains($0.recordName) } + let ourRecords = initialResult.records.filter { input.names.contains($0.recordName.rawValue) } print(" Found \(ourRecords.count) of our test records") if ourRecords.count != input.names.count && context.verbose { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupRecordsPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupRecordsPhase.swift index 135bc5317..ad91c655c 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupRecordsPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupRecordsPhase.swift @@ -75,7 +75,7 @@ internal struct LookupRecordsPhase: IntegrationPhase { } let results = try await context.service.lookupRecords( - recordNames: lookupNames, + recordNames: lookupNames.map(RecordName.init(rawValue:)), database: context.database ) let records = results.compactMap { result in diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyRecordsPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyRecordsPhase.swift index 5cc8277f4..3fe930937 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyRecordsPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyRecordsPhase.swift @@ -49,7 +49,7 @@ internal struct ModifyRecordsPhase: IntegrationPhase { RecordOperation( operationType: .forceReplace, recordType: MistDemoConfig.recordType, - recordName: recordName, + recordName: RecordName(recordName), fields: [ "title": .string("Updated Record \(offset + 1)") ] diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyRequestOptionsPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyRequestOptionsPhase.swift index 291b7a121..5a9ef6578 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyRequestOptionsPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyRequestOptionsPhase.swift @@ -61,7 +61,7 @@ internal struct ModifyRequestOptionsPhase: IntegrationPhase { let operation = RecordOperation( operationType: .forceUpdate, recordType: MistDemoConfig.recordType, - recordName: recordName, + recordName: RecordName(recordName), fields: ["title": .string("Request-options update")] ) diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/NotificationRoundtripPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/NotificationRoundtripPhase.swift index ff832883a..7bf636b76 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/NotificationRoundtripPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/NotificationRoundtripPhase.swift @@ -82,13 +82,13 @@ internal struct NotificationRoundtripPhase: IntegrationPhase { // 2. Mint + register a courier token, then trigger a matching change. let courierURL = try await mintCourierToken(context: context) let record = try await trigger(suffix: suffix, context: context) - createdRecordName = record.recordName + createdRecordName = record.recordName.rawValue // 3. Await our subscription's push (bounded, soft — see helper). await awaitPush( courierURL: courierURL, subscriptionID: subscriptionID, - expectedRecordName: record.recordName + expectedRecordName: record.recordName.rawValue ) } catch { await cleanup( @@ -141,7 +141,7 @@ internal struct NotificationRoundtripPhase: IntegrationPhase { private func trigger(suffix: String, context: PhaseContext) async throws -> RecordInfo { let record = try await context.service.createRecord( recordType: MistDemoConfig.recordType, - recordName: "mistkit-notif-rec-\(suffix)", + recordName: RecordName("mistkit-notif-rec-\(suffix)"), fields: ["title": .string("notification probe \(suffix)")], database: context.database ) @@ -180,7 +180,7 @@ internal struct NotificationRoundtripPhase: IntegrationPhase { " ✅ Received push for '\(subscriptionID)' — " + "record \(notification.recordName ?? "?"), reason \(reason)" ) - if notification.recordName != expectedRecordName { + if notification.recordName?.rawValue != expectedRecordName { print( " ⚠️ Notification record '\(notification.recordName ?? "nil")' " + "≠ created '\(expectedRecordName)'." @@ -206,7 +206,7 @@ internal struct NotificationRoundtripPhase: IntegrationPhase { if let recordName { try? await context.service.deleteRecord( recordType: MistDemoConfig.recordType, - recordName: recordName, + recordName: RecordName(rawValue: recordName), database: context.database ) } diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/QueryRecordsPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/QueryRecordsPhase.swift index 3a312ff45..46ba83560 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/QueryRecordsPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/QueryRecordsPhase.swift @@ -50,7 +50,7 @@ internal struct QueryRecordsPhase: IntegrationPhase { ) print("✅ Queried \(records.count) record(s) of type '\(MistDemoConfig.recordType)'") if context.verbose { - let ours = records.filter { input.names.contains($0.recordName) } + let ours = records.filter { input.names.contains($0.recordName.rawValue) } print(" Found \(ours.count) of our \(input.names.count) test records") } } catch { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/QueryRequestOptionsPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/QueryRequestOptionsPhase.swift index 1f521c2b0..be57ecccb 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/QueryRequestOptionsPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/QueryRequestOptionsPhase.swift @@ -63,7 +63,7 @@ internal struct QueryRequestOptionsPhase: IntegrationPhase { ) print("✅ Queried \(result.records.count) record(s) with request options") - let ours = result.records.filter { input.names.contains($0.recordName) } + let ours = result.records.filter { input.names.contains($0.recordName.rawValue) } guard let sample = ours.first else { print(" ⚠️ None of our test records came back — skipping desiredKeys assertion") return NoState() diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/RereferenceAssetPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/RereferenceAssetPhase.swift index 258dd0cb4..d10c71f05 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/RereferenceAssetPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/RereferenceAssetPhase.swift @@ -93,7 +93,7 @@ internal struct RereferenceAssetPhase: IntegrationPhase { let targetRecordName = "mistkit-test-\(UUID().uuidString.lowercased())" _ = try await context.service.createRecord( recordType: MistDemoConfig.recordType, - recordName: targetRecordName, + recordName: RecordName(targetRecordName), fields: [ "title": .string("Rereference Target"), "index": .int64(0), @@ -107,9 +107,9 @@ internal struct RereferenceAssetPhase: IntegrationPhase { } let updated = try await context.service.rereferenceAsset( - fromRecord: sourceRecordName, + fromRecord: RecordName(sourceRecordName), field: "image", - toRecord: targetRecordName, + toRecord: RecordName(targetRecordName), field: "image", database: context.database ) diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ShareCreateAndAcceptPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ShareCreateAndAcceptPhase.swift index 6c66156d9..3df6771c6 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ShareCreateAndAcceptPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ShareCreateAndAcceptPhase.swift @@ -98,7 +98,7 @@ internal struct ShareCreateAndAcceptPhase: IntegrationPhase { do { let created = try await context.service.createShare( rootRecordType: MistDemoConfig.recordType, - rootRecordName: rootRecordName, + rootRecordName: RecordName(rootRecordName), rootFields: [ "title": .string("Share roundtrip"), "index": .int64(0), @@ -164,7 +164,7 @@ internal struct ShareCreateAndAcceptPhase: IntegrationPhase { sharer: context.service, database: context.database, zoneID: zoneID, - rootRecordName: rootRecordName, + rootRecordName: RecordName(rootRecordName), shareRecordName: nil, verbose: context.verbose ) @@ -178,8 +178,8 @@ internal struct ShareCreateAndAcceptPhase: IntegrationPhase { sharer: CloudKitService, database: MistKit.Database, zoneID: ZoneID, - rootRecordName: String, - shareRecordName: String?, + rootRecordName: RecordName, + shareRecordName: RecordName?, verbose: Bool ) async throws { var ops = [ diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/SharedZoneRoundtripPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/SharedZoneRoundtripPhase.swift index 2149151c3..c4a762463 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/SharedZoneRoundtripPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/SharedZoneRoundtripPhase.swift @@ -79,11 +79,11 @@ internal struct SharedZoneRoundtripPhase: IntegrationPhase { print(" Sharee: \(shareeIdentity.userRecordName)") } - var shareRecordName: String? + var shareRecordName: RecordName? do { let created = try await context.service.createShare( rootRecordType: MistDemoConfig.recordType, - rootRecordName: rootRecordName, + rootRecordName: RecordName(rootRecordName), rootFields: [ "title": .string("Shared zone root"), "index": .int64(0), @@ -181,7 +181,7 @@ internal struct SharedZoneRoundtripPhase: IntegrationPhase { operation: "fetchRecordZoneChanges (shared)" ) let changeNames = ChangeTrackingVerification.recordNames(in: changeResult.changes) - if !changeNames.contains(sharedRootName) { + if !changeNames.contains(sharedRootName.rawValue) { throw IntegrationTestError.verificationFailed( "shared changes/zone missing root \(sharedRootName); found \(changeNames.sorted())" ) @@ -232,7 +232,7 @@ internal struct SharedZoneRoundtripPhase: IntegrationPhase { sharer: context.service, database: context.database, zoneID: privateZoneID, - rootRecordName: rootRecordName, + rootRecordName: RecordName(rootRecordName), shareRecordName: shareRecordName, extraRecordNames: [], verbose: context.verbose, @@ -248,8 +248,8 @@ internal struct SharedZoneRoundtripPhase: IntegrationPhase { sharer: CloudKitService, database: MistKit.Database, zoneID: ZoneID, - rootRecordName: String, - shareRecordName: String?, + rootRecordName: RecordName, + shareRecordName: RecordName?, extraRecordNames: [String], verbose: Bool, skipCleanup: Bool @@ -271,7 +271,7 @@ internal struct SharedZoneRoundtripPhase: IntegrationPhase { RecordOperation( operationType: .forceDelete, recordType: MistDemoConfig.recordType, - recordName: name + recordName: RecordName(name) ) ) } diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/CSVFormatter.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/CSVFormatter.swift index 3dd997c00..456576fde 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/CSVFormatter.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/CSVFormatter.swift @@ -64,7 +64,7 @@ public struct CSVFormatter: OutputFormatter { output += "Field,Value\n" // Basic fields - output += "recordName,\(escaper.escape(record.recordName))\n" + output += "recordName,\(escaper.escape(record.recordName.rawValue))\n" output += "recordType,\(escaper.escape(record.recordType ?? ""))\n" // Custom fields diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/TableFormatter.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/TableFormatter.swift index 1d842c44c..38f528f0d 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/TableFormatter.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/TableFormatter.swift @@ -60,7 +60,7 @@ public struct TableFormatter: OutputFormatter { let escaper = TableEscaper() var output = "" - output += "Record Name: \(escaper.escape(record.recordName))\n" + output += "Record Name: \(escaper.escape(record.recordName.rawValue))\n" output += "Record Type: \(escaper.escape(record.recordType ?? ""))\n" if !record.fields.isEmpty { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/YAMLFormatter.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/YAMLFormatter.swift index 01924cea2..4293977bd 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/YAMLFormatter.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/YAMLFormatter.swift @@ -60,7 +60,7 @@ public struct YAMLFormatter: OutputFormatter { private func formatRecord(_ record: RecordInfo, escaper: YAMLEscaper) -> String { var output = "" - output += "recordName: \(escaper.escape(record.recordName))\n" + output += "recordName: \(escaper.escape(record.recordName.rawValue))\n" output += "recordType: \(escaper.escape(record.recordType ?? ""))\n" if !record.fields.isEmpty { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Records.swift b/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Records.swift index d2bf470f7..3a6a8790d 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Records.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Records.swift @@ -108,7 +108,7 @@ extension OutputFormatting { for fieldName in sortedFieldNames { switch fieldName { case MistDemoConstants.FieldNames.recordName: - values.append(csvEscaper.escape(record.recordName)) + values.append(csvEscaper.escape(record.recordName.rawValue)) case MistDemoConstants.FieldNames.recordType: values.append(csvEscaper.escape(record.recordType ?? "")) case MistDemoConstants.FieldNames.recordChangeTag: @@ -137,7 +137,7 @@ extension OutputFormatting { if records.count == 1 { let record = records[0] print("record:") - let name = yamlEscaper.escape(record.recordName) + let name = yamlEscaper.escape(record.recordName.rawValue) print(" \(recordNameKey): \(name)") let rtype = yamlEscaper.escape(record.recordType ?? "") print(" \(recordTypeKey): \(rtype)") @@ -155,7 +155,7 @@ extension OutputFormatting { } else { print("records:") for record in records { - let name = yamlEscaper.escape(record.recordName) + let name = yamlEscaper.escape(record.recordName.rawValue) print(" - \(recordNameKey): \(name)") let rtype = yamlEscaper.escape(record.recordType ?? "") print(" \(recordTypeKey): \(rtype)") diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Reads.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Reads.swift index 9c36b638d..56d1536a0 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Reads.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Reads.swift @@ -39,7 +39,7 @@ extension CloudKitService { database: MistKit.Database ) async throws -> [RecordInfo] { let results = try await lookupRecords( - recordNames: recordNames, + recordNames: recordNames.map(RecordName.init(rawValue:)), desiredKeys: nil, database: database ) diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend.swift index acffe839b..366484829 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend.swift @@ -61,7 +61,7 @@ extension CloudKitService: WebBackend { ) async throws -> RecordInfo { try await createRecord( recordType: recordType, - recordName: recordName, + recordName: recordName.map(RecordName.init(rawValue:)), fields: fields, zoneID: zone?.zoneID, database: database @@ -78,7 +78,7 @@ extension CloudKitService: WebBackend { ) async throws -> RecordInfo { try await updateRecord( recordType: recordType, - recordName: recordName, + recordName: RecordName(recordName), fields: fields, recordChangeTag: recordChangeTag, zoneID: zone?.zoneID, @@ -95,7 +95,7 @@ extension CloudKitService: WebBackend { ) async throws { try await deleteRecord( recordType: recordType, - recordName: recordName, + recordName: RecordName(recordName), recordChangeTag: recordChangeTag, zoneID: zone?.zoneID, database: database @@ -176,9 +176,9 @@ extension CloudKitService: WebBackend { database: MistKit.Database ) async throws -> RecordInfo { try await rereferenceAsset( - fromRecord: sourceRecordName, + fromRecord: RecordName(sourceRecordName), field: assetField, - toRecord: targetRecordName, + toRecord: RecordName(targetRecordName), field: targetAssetField, database: database ) @@ -196,7 +196,7 @@ extension CloudKitService: WebBackend { data: data, recordType: recordType, fieldName: fieldName, - recordName: recordName, + recordName: recordName.map(RecordName.init(rawValue:)), zoneID: zone?.zoneID, database: database ) diff --git a/Examples/MistDemo/Sources/MistDemoKit/Utilities/FieldValueFormatter.swift b/Examples/MistDemo/Sources/MistDemoKit/Utilities/FieldValueFormatter.swift index 8d9d16458..25c0b58bd 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Utilities/FieldValueFormatter.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Utilities/FieldValueFormatter.swift @@ -51,7 +51,7 @@ internal enum FieldValueFormatter { case .location(let location): return "(\(location.latitude), \(location.longitude))" case .reference(let reference): - return reference.recordName + return reference.recordName.rawValue case .asset(let asset): return asset.downloadURL ?? "no URL" case .list(let values): diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+ServiceOperations.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+ServiceOperations.swift index 0d1d30048..a1201dba9 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+ServiceOperations.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+ServiceOperations.swift @@ -151,7 +151,7 @@ receipt: "stub-receipt", downloadURL: nil ), - recordName: assignedName, + recordName: RecordName(assignedName), fieldName: fieldName ) } diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+ShareOperations.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+ShareOperations.swift index d3ac709ae..498bb41f3 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+ShareOperations.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+ShareOperations.swift @@ -47,7 +47,7 @@ return shortGUIDs.map { guid in ShareRecordInfo( shortGUID: ShortGUIDDictionary(value: guid), - rootRecordName: "stub-root-\(guid)", + rootRecordName: RecordName("stub-root-\(guid)"), participantPermission: .readWrite, participantStatus: .accepted ) @@ -68,7 +68,7 @@ return shortGUIDs.map { guid in ShareRecordInfo( shortGUID: ShortGUIDDictionary(value: guid), - rootRecordName: "stub-root-\(guid)", + rootRecordName: RecordName("stub-root-\(guid)"), participantPermission: .readWrite, participantStatus: .accepted ) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 3dae4c762..15f828611 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -12,6 +12,7 @@ * Add records/resolve and records/accept share operations (#41, #42) by @leogdion in https://github.com/brightdigit/MistKit/pull/428 ### API Cleanup +* Add `RecordName` value type for record identifiers (#465) * Remove deprecated API, model server error codes, refactor FieldValue conversion, add cloud toolchain (#424, #421, #378, #358, #295) by @leogdion in https://github.com/brightdigit/MistKit/pull/424 ### MistDemo & Tooling diff --git a/Sources/MistKit/CloudKitService/CloudKitService+AssetOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+AssetOperations.swift index 34bb054b1..e78e3c308 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+AssetOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+AssetOperations.swift @@ -77,7 +77,7 @@ extension CloudKitService { data: Data, recordType: String, fieldName: String, - recordName: String? = nil, + recordName: RecordName? = nil, zoneID: ZoneID? = nil, using uploader: AssetUploader? = nil, database: Database @@ -127,7 +127,7 @@ extension CloudKitService { public func requestAssetUploadURL( recordType: String, fieldName: String, - recordName: String? = nil, + recordName: RecordName? = nil, zoneID: ZoneID? = nil, database: Database ) async throws(CloudKitError) -> AssetUploadToken { @@ -135,7 +135,7 @@ extension CloudKitService { let tokenRequest = Operations.uploadAssets.Input.Body .jsonPayload.tokensPayloadPayload( - recordName: recordName, + recordName: recordName?.rawValue, recordType: recordType, fieldName: fieldName ) diff --git a/Sources/MistKit/CloudKitService/CloudKitService+AssetRereference.swift b/Sources/MistKit/CloudKitService/CloudKitService+AssetRereference.swift index e5fd83f26..2811cb97b 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+AssetRereference.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+AssetRereference.swift @@ -48,14 +48,14 @@ extension CloudKitService { /// top-level ``CloudKitError/badRequest(reason:)``, so there are no /// per-item failures to inspect. public func rereferenceAssets( - _ fields: [(recordName: String, fieldName: String)], + _ fields: [(recordName: RecordName, fieldName: String)], zoneID: ZoneID? = nil, database: Database ) async throws(CloudKitError) -> [Asset] { do { let assetRequests = fields.map { field in Operations.rereferenceAssets.Input.Body.jsonPayload.assetsPayloadPayload( - recordName: field.recordName, + recordName: field.recordName.rawValue, fieldName: field.fieldName ) } @@ -111,9 +111,9 @@ extension CloudKitService { /// record is not found or carries no `recordType`; or a record failure if the /// target lookup or update failed. public func rereferenceAsset( - fromRecord sourceRecordName: String, + fromRecord sourceRecordName: RecordName, field assetField: String, - toRecord targetRecordName: String, + toRecord targetRecordName: RecordName, field targetField: String? = nil, zoneID: ZoneID? = nil, database: Database @@ -168,9 +168,9 @@ extension CloudKitService { /// `assets/rereference` returned no descriptor for the source field; or a /// record failure if the target update failed. public func rereferenceAsset( - fromRecord sourceRecordName: String, + fromRecord sourceRecordName: RecordName, field assetField: String, - toRecord targetRecordName: String, + toRecord targetRecordName: RecordName, recordType: String, recordChangeTag: String?, field targetField: String? = nil, diff --git a/Sources/MistKit/CloudKitService/CloudKitService+Classification.swift b/Sources/MistKit/CloudKitService/CloudKitService+Classification.swift index 3ddda5165..f6044d748 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+Classification.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+Classification.swift @@ -49,7 +49,7 @@ extension CloudKitService { /// Used as the first step of the pre-fetch + classify pattern for tracking /// creates vs updates in batch modify operations. Internally this calls /// `queryRecords(_:limit:database:)` and projects the results down to a - /// `Set` of record names. + /// `Set` of record names. /// /// - Important: This issues a single `queryRecords` call. CloudKit caps a /// single response at 200 records, so for larger record types you must @@ -71,7 +71,7 @@ extension CloudKitService { limit: Int? = nil, zoneID: ZoneID? = nil, database: Database - ) async throws(CloudKitError) -> Set { + ) async throws(CloudKitError) -> Set { let result: QueryResult = try await queryRecords( Query(recordType: recordType), limit: limit ?? Self.maxRecordsPerRequest, diff --git a/Sources/MistKit/CloudKitService/CloudKitService+CreateShare.swift b/Sources/MistKit/CloudKitService/CloudKitService+CreateShare.swift index 341edcf55..dcd8920dc 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+CreateShare.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+CreateShare.swift @@ -62,7 +62,7 @@ extension CloudKitService { /// - Throws: ``CloudKitError``. public func createShare( rootRecordType: String, - rootRecordName: String? = nil, + rootRecordName: RecordName? = nil, rootFields: [String: FieldValue] = [:], zoneID: ZoneID, publicPermission: SharePermission = .none, @@ -73,7 +73,7 @@ extension CloudKitService { // Pre-assign the root name so the share step can target it even if the // caller omitted `rootRecordName`. let resolvedRootName = - rootRecordName ?? "mistkit-share-root-\(UUID().uuidString.lowercased())" + rootRecordName ?? RecordName(rawValue: "mistkit-share-root-\(UUID().uuidString.lowercased())") let (rootRecord, rootChangeTag) = try await createShareRoot( recordType: rootRecordType, recordName: resolvedRootName, @@ -100,7 +100,7 @@ extension CloudKitService { private func createShareRoot( recordType: String, - recordName: String, + recordName: RecordName, fields: [String: FieldValue], zoneID: ZoneID, database: Database @@ -178,7 +178,7 @@ extension CloudKitService { shareURL: CreatedShare.shareURL(forShortGUID: share.shortGUID), share: share, rootRecord: rootRecord, - shareRecordName: shareRecordName + shareRecordName: RecordName(rawValue: shareRecordName) ) } } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+LookupAllRecords.swift b/Sources/MistKit/CloudKitService/CloudKitService+LookupAllRecords.swift index 755e2be06..02a205a78 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+LookupAllRecords.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+LookupAllRecords.swift @@ -49,7 +49,7 @@ extension CloudKitService { /// `NOT_FOUND`) for one CloudKit could not return. /// - Throws: `CloudKitError` if any batch fails. public func lookupAllRecords( - recordNames: [String], + recordNames: [RecordName], desiredKeys: [String]? = nil, database: Database, batchSize: Int = CloudKitService.maxRecordsPerRequest diff --git a/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift index c8a6d0cf4..d59dee8e8 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift @@ -57,7 +57,7 @@ extension CloudKitService { /// - Returns: A ``RecordResult`` per requested record — `.success` for a found /// record, `.failure` (e.g. `NOT_FOUND`) for one CloudKit could not return. public func lookupRecords( - recordNames: [String], + recordNames: [RecordName], desiredKeys: [String]? = nil, database: Database ) async throws(CloudKitError) -> [RecordResult] { @@ -74,7 +74,7 @@ extension CloudKitService { .init( records: recordNames.map { recordName in .init( - recordName: recordName, + recordName: recordName.rawValue, desiredKeys: desiredKeys ) } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+RecordWriteConvenience.swift b/Sources/MistKit/CloudKitService/CloudKitService+RecordWriteConvenience.swift index 298120d4b..286f5042e 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+RecordWriteConvenience.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+RecordWriteConvenience.swift @@ -51,7 +51,7 @@ extension CloudKitService { /// ``` public func createRecord( recordType: String, - recordName: String? = nil, + recordName: RecordName? = nil, fields: [String: FieldValue], zoneID: ZoneID? = nil, database: Database @@ -95,7 +95,7 @@ extension CloudKitService { /// ``` public func updateRecord( recordType: String, - recordName: String, + recordName: RecordName, fields: [String: FieldValue], recordChangeTag: String? = nil, zoneID: ZoneID? = nil, @@ -128,7 +128,7 @@ extension CloudKitService { /// - Throws: CloudKitError if the operation fails public func deleteRecord( recordType: String, - recordName: String, + recordName: RecordName, recordChangeTag: String? = nil, zoneID: ZoneID? = nil, database: Database diff --git a/Sources/MistKit/Models/AssetUploading/AssetUploadReceipt.swift b/Sources/MistKit/Models/AssetUploading/AssetUploadReceipt.swift index 43bd5ff4f..4dcdd221a 100644 --- a/Sources/MistKit/Models/AssetUploading/AssetUploadReceipt.swift +++ b/Sources/MistKit/Models/AssetUploading/AssetUploadReceipt.swift @@ -38,13 +38,13 @@ public struct AssetUploadReceipt: Codable, Sendable { public let asset: Asset /// The record name this asset is associated with - public let recordName: String + public let recordName: RecordName /// The field name this asset should be assigned to public let fieldName: String /// Initialize an asset upload receipt - public init(asset: Asset, recordName: String, fieldName: String) { + public init(asset: Asset, recordName: RecordName, fieldName: String) { self.asset = asset self.recordName = recordName self.fieldName = fieldName diff --git a/Sources/MistKit/Models/AssetUploading/AssetUploadToken.swift b/Sources/MistKit/Models/AssetUploading/AssetUploadToken.swift index 9fb23edae..da17ea2b4 100644 --- a/Sources/MistKit/Models/AssetUploading/AssetUploadToken.swift +++ b/Sources/MistKit/Models/AssetUploading/AssetUploadToken.swift @@ -38,12 +38,12 @@ public struct AssetUploadToken: Sendable, Equatable { /// The upload URL for the CDN endpoint public let url: URL? /// The record name this token is associated with - public let recordName: String? + public let recordName: RecordName? /// The field name this token should be assigned to public let fieldName: String? /// Initialize an asset upload token - public init(url: URL?, recordName: String?, fieldName: String?) { + public init(url: URL?, recordName: RecordName?, fieldName: String?) { self.url = url self.recordName = recordName self.fieldName = fieldName @@ -51,7 +51,7 @@ public struct AssetUploadToken: Sendable, Equatable { internal init(from token: Components.Schemas.AssetUploadResponse.tokensPayloadPayload) { self.url = token.url.flatMap { URL(string: $0) } - self.recordName = token.recordName + self.recordName = token.recordName.map(RecordName.init(rawValue:)) self.fieldName = token.fieldName } } diff --git a/Sources/MistKit/Models/FieldValues/FieldValue+Components.swift b/Sources/MistKit/Models/FieldValues/FieldValue+Components.swift index ad318b3a2..6ed3df067 100644 --- a/Sources/MistKit/Models/FieldValues/FieldValue+Components.swift +++ b/Sources/MistKit/Models/FieldValues/FieldValue+Components.swift @@ -95,7 +95,7 @@ extension FieldValue { internal init( referenceValue: Components.Schemas.ReferenceValue ) { - let recordName = referenceValue.recordName + let recordName = RecordName(rawValue: referenceValue.recordName) let action: Reference.Action? switch referenceValue.action { case .DELETE_SELF: diff --git a/Sources/MistKit/Models/FieldValues/Reference.swift b/Sources/MistKit/Models/FieldValues/Reference.swift index 648ee9248..3a6195e4f 100644 --- a/Sources/MistKit/Models/FieldValues/Reference.swift +++ b/Sources/MistKit/Models/FieldValues/Reference.swift @@ -36,12 +36,12 @@ public struct Reference: Codable, Equatable, Sendable { } /// The record name being referenced - public let recordName: String + public let recordName: RecordName /// The action to take (DELETE_SELF, NONE, or nil) public let action: Action? /// Initialize a reference value - public init(recordName: String, action: Action? = nil) { + public init(recordName: RecordName, action: Action? = nil) { self.recordName = recordName self.action = action } diff --git a/Sources/MistKit/Models/Notifications/CourierNotification.swift b/Sources/MistKit/Models/Notifications/CourierNotification.swift index 510e3fbc6..f0b60a9c4 100644 --- a/Sources/MistKit/Models/Notifications/CourierNotification.swift +++ b/Sources/MistKit/Models/Notifications/CourierNotification.swift @@ -61,7 +61,7 @@ public struct CourierNotification: Sendable { /// `ck.qry.sid` — the subscription that fired. public let subscriptionID: String? /// `ck.qry.rid` — the record that changed. - public let recordName: String? + public let recordName: RecordName? /// `ck.qry.zid` — the zone the record lives in. public let zoneID: String? /// `ck.qry.fo` — why the subscription fired. @@ -79,7 +79,7 @@ public struct CourierNotification: Sendable { self.notificationID = cloudKit?.nid self.containerIdentifier = cloudKit?.cid self.subscriptionID = query?.sid - self.recordName = query?.rid + self.recordName = query?.rid.map(RecordName.init(rawValue:)) self.zoneID = query?.zid self.reason = query?.firesOn.flatMap(Reason.init(rawValue:)) self.databaseScope = query?.dbs diff --git a/Sources/MistKit/Models/OperationClassification.swift b/Sources/MistKit/Models/OperationClassification.swift index 17154f2e6..231b0fbbe 100644 --- a/Sources/MistKit/Models/OperationClassification.swift +++ b/Sources/MistKit/Models/OperationClassification.swift @@ -56,10 +56,10 @@ internal import Foundation /// ``` public struct OperationClassification: Sendable, Equatable { /// Record names that are expected to be created (not present in CloudKit). - public let creates: Set + public let creates: Set /// Record names that are expected to be updated (already present in CloudKit). - public let updates: Set + public let updates: Set /// Build a classification by comparing proposed record names against existing ones. /// @@ -72,11 +72,11 @@ public struct OperationClassification: Sendable, Equatable { /// - existingRecordNames: Record names already present in CloudKit /// (typically obtained via `fetchExistingRecordNames(recordType:)`). public init( - proposedRecordNames: [String], - existingRecordNames: Set + proposedRecordNames: [RecordName], + existingRecordNames: Set ) { - var creates = Set() - var updates = Set() + var creates = Set() + var updates = Set() for recordName in proposedRecordNames { if existingRecordNames.contains(recordName) { @@ -101,7 +101,7 @@ public struct OperationClassification: Sendable, Equatable { /// - existingRecordNames: Record names already present in CloudKit. public init( operations: [RecordOperation], - existingRecordNames: Set + existingRecordNames: Set ) { let proposedNames = operations.compactMap(\.recordName) self.init( @@ -113,7 +113,7 @@ public struct OperationClassification: Sendable, Equatable { /// Direct initializer for tests and manual construction. /// /// Prefer the comparison-based initializers in production code. - internal init(creates: Set, updates: Set) { + internal init(creates: Set, updates: Set) { self.creates = creates self.updates = updates } diff --git a/Sources/MistKit/Models/RecordInfo.swift b/Sources/MistKit/Models/RecordInfo.swift index 2c44cda5a..ebe0b796f 100644 --- a/Sources/MistKit/Models/RecordInfo.swift +++ b/Sources/MistKit/Models/RecordInfo.swift @@ -42,7 +42,7 @@ internal import MistKitOpenAPI /// conversion failure (logged, asserted in DEBUG, and thrown). public struct RecordInfo: Codable, Sendable { /// The record name - public let recordName: String + public let recordName: RecordName /// The record type, or `nil` for tombstones and other typeless responses public let recordType: String? /// The record change tag for optimistic locking @@ -61,7 +61,7 @@ public struct RecordInfo: Codable, Sendable { guard let recordName = record.recordName else { try ConversionError.recordMissingRecordName.reportAndThrow() } - self.recordName = recordName + self.recordName = RecordName(rawValue: recordName) self.recordType = record.recordType self.recordChangeTag = record.recordChangeTag if let created = record.created { @@ -102,7 +102,7 @@ public struct RecordInfo: Codable, Sendable { /// - modified: Optional timestamp when the record was last modified /// - deleted: Whether the record has been deleted public init( - recordName: String, + recordName: RecordName, recordType: String?, recordChangeTag: String? = nil, fields: [String: FieldValue], diff --git a/Sources/MistKit/Models/RecordName.swift b/Sources/MistKit/Models/RecordName.swift new file mode 100644 index 000000000..7cff05cde --- /dev/null +++ b/Sources/MistKit/Models/RecordName.swift @@ -0,0 +1,85 @@ +// +// RecordName.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. +// + +/// A CloudKit record's identity within a zone. +/// +/// Typical `recordName` values are opaque strings unique in that zone. CloudKit +/// auto-generated names are UUID strings; callers may also supply a custom +/// string. Both share the same JSON wire type (a single string). +/// +/// This is a string-backed struct rather than a `typealias` or enum: +/// - The set of names is open (UUID or caller-chosen), so an enum is the wrong +/// model. +/// - `typealias RecordName = String` would not distinguish a record's identity +/// from owner names, zone names, or other strings. +/// +/// ``UserRecordName`` stays a separate enum (`recordName` vs `nonDiscoverable`) +/// for user-identity lookup; do not merge the two types. +public struct RecordName: RawRepresentable, Hashable, Sendable, Codable, + ExpressibleByStringLiteral, CustomStringConvertible, Comparable +{ + /// The record name string as it appears on the CloudKit wire. + public let rawValue: String + + /// Creates a record name from its wire string. + /// - Parameter rawValue: The CloudKit record name (UUID or custom string). + public init(rawValue: String) { + self.rawValue = rawValue + } + + /// Creates a record name from a string value (for interpolations and stored `String`s). + public init(_ rawValue: String) { + self.init(rawValue: rawValue) + } + + /// Creates a record name from a string literal so `"foo"` still type-checks + /// at call sites that take ``RecordName``. + public init(stringLiteral value: String) { + self.init(rawValue: value) + } + + /// Decodes from a single JSON string (not a keyed object). + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + self.init(rawValue: try container.decode(String.self)) + } + + /// Encodes as a single JSON string (not a keyed object). + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } + + /// The record name string, suitable for logs and display. + public var description: String { rawValue } + + public static func < (lhs: RecordName, rhs: RecordName) -> Bool { + lhs.rawValue < rhs.rawValue + } +} diff --git a/Sources/MistKit/Models/RecordOperation.swift b/Sources/MistKit/Models/RecordOperation.swift index 61c3528a1..7be9ff1a9 100644 --- a/Sources/MistKit/Models/RecordOperation.swift +++ b/Sources/MistKit/Models/RecordOperation.swift @@ -54,7 +54,7 @@ public struct RecordOperation: Sendable { /// The record type (e.g., "RestoreImage", "XcodeVersion") public let recordType: String /// The unique record name (optional for creates - CloudKit will generate one if not provided) - public let recordName: String? + public let recordName: RecordName? /// The record fields as FieldValue types public let fields: [String: FieldValue] /// Optional record change tag for optimistic locking @@ -75,7 +75,7 @@ public struct RecordOperation: Sendable { public init( operationType: OperationType, recordType: String, - recordName: String?, + recordName: RecordName?, fields: [String: FieldValue] = [:], recordChangeTag: String? = nil, createShortGUID: Bool? = nil, @@ -97,7 +97,7 @@ public struct RecordOperation: Sendable { /// Convenience initializer for creating a new record public static func create( recordType: String, - recordName: String? = nil, + recordName: RecordName? = nil, fields: [String: FieldValue] ) -> RecordOperation { RecordOperation( @@ -111,7 +111,7 @@ public struct RecordOperation: Sendable { /// Convenience initializer for updating an existing record public static func update( recordType: String, - recordName: String, + recordName: RecordName, fields: [String: FieldValue], recordChangeTag: String? ) -> RecordOperation { @@ -127,7 +127,7 @@ public struct RecordOperation: Sendable { /// Convenience initializer for deleting a record public static func delete( recordType: String, - recordName: String, + recordName: RecordName, recordChangeTag: String? = nil ) -> RecordOperation { RecordOperation( diff --git a/Sources/MistKit/Models/RecordTarget.swift b/Sources/MistKit/Models/RecordTarget.swift index bc45cb9fd..500097189 100644 --- a/Sources/MistKit/Models/RecordTarget.swift +++ b/Sources/MistKit/Models/RecordTarget.swift @@ -45,7 +45,7 @@ extension OperationFailure where Target == RecordTarget { /// /// A named alias for ``OperationFailure/identifier`` scoped to the /// record target, matching CloudKit's `recordName` wire field. - public var recordName: String { identifier } + public var recordName: RecordName { RecordName(rawValue: identifier) } internal init(from schema: Components.Schemas.RecordOperationFailure) { self.init(identifier: schema.value2.recordName, common: schema.value1) diff --git a/Sources/MistKit/Models/Sharing/CreatedShare.swift b/Sources/MistKit/Models/Sharing/CreatedShare.swift index 5d06b7252..149dc357f 100644 --- a/Sources/MistKit/Models/Sharing/CreatedShare.swift +++ b/Sources/MistKit/Models/Sharing/CreatedShare.swift @@ -52,7 +52,7 @@ public struct CreatedShare: Sendable { /// The root record that was shared. public let rootRecord: RecordInfo /// The record name of the created `cloudkit.share` record. - public let shareRecordName: String + public let shareRecordName: RecordName /// Initialize a created-share result. /// - Parameters: @@ -66,7 +66,7 @@ public struct CreatedShare: Sendable { shareURL: URL, share: ShareInfo, rootRecord: RecordInfo, - shareRecordName: String + shareRecordName: RecordName ) { self.shortGUID = shortGUID self.shareURL = shareURL diff --git a/Sources/MistKit/Models/Sharing/ShareRecordInfo.swift b/Sources/MistKit/Models/Sharing/ShareRecordInfo.swift index cf9ab420b..fc485bcf2 100644 --- a/Sources/MistKit/Models/Sharing/ShareRecordInfo.swift +++ b/Sources/MistKit/Models/Sharing/ShareRecordInfo.swift @@ -53,7 +53,7 @@ public struct ShareRecordInfo: Codable, Sendable { /// The zone holding the shared record. public let zoneID: ZoneID? /// The name of the root record that was shared. - public let rootRecordName: String? + public let rootRecordName: RecordName? /// The shared root record, when ``ShortGUIDDictionary/shouldFetchRootRecord`` /// asked for it. public let rootRecord: RecordInfo? @@ -90,7 +90,7 @@ public struct ShareRecordInfo: Codable, Sendable { } else { self.zoneID = nil } - self.rootRecordName = schema.rootRecordName + self.rootRecordName = schema.rootRecordName.map(RecordName.init(rawValue:)) if let rootRecord = schema.rootRecord { self.rootRecord = try RecordInfo(from: rootRecord) } else { @@ -139,7 +139,7 @@ public struct ShareRecordInfo: Codable, Sendable { databaseScope: ShareDatabaseScope? = nil, environment: Environment? = nil, zoneID: ZoneID? = nil, - rootRecordName: String? = nil, + rootRecordName: RecordName? = nil, rootRecord: RecordInfo? = nil, share: RecordInfo? = nil, shareInfo: ShareInfo? = nil, diff --git a/Sources/MistKit/Models/Sharing/ShareTargetReference.swift b/Sources/MistKit/Models/Sharing/ShareTargetReference.swift index 7fa94096c..37e0f3027 100644 --- a/Sources/MistKit/Models/Sharing/ShareTargetReference.swift +++ b/Sources/MistKit/Models/Sharing/ShareTargetReference.swift @@ -33,7 +33,7 @@ internal import MistKitOpenAPI /// record (CloudKit's `forRecord` key). public struct ShareTargetReference: Codable, Sendable, Equatable, Hashable { /// The record name of the shared root record. - public let recordName: String + public let recordName: RecordName /// Optional change tag for optimistic concurrency when creating the share. public let recordChangeTag: String? @@ -41,7 +41,7 @@ public struct ShareTargetReference: Codable, Sendable, Equatable, Hashable { /// - Parameters: /// - recordName: The record name of the shared root record. /// - recordChangeTag: Optional change tag for the root record. - public init(recordName: String, recordChangeTag: String? = nil) { + public init(recordName: RecordName, recordChangeTag: String? = nil) { self.recordName = recordName self.recordChangeTag = recordChangeTag } @@ -50,7 +50,7 @@ public struct ShareTargetReference: Codable, Sendable, Equatable, Hashable { extension Components.Schemas.ShareTargetReference { internal init(from reference: ShareTargetReference) { self.init( - recordName: reference.recordName, + recordName: reference.recordName.rawValue, recordChangeTag: reference.recordChangeTag ) } diff --git a/Sources/MistKit/OpenAPI/Components/Components.Schemas.FieldValueRequest.swift b/Sources/MistKit/OpenAPI/Components/Components.Schemas.FieldValueRequest.swift index 342d7edd4..b2dd1b60d 100644 --- a/Sources/MistKit/OpenAPI/Components/Components.Schemas.FieldValueRequest.swift +++ b/Sources/MistKit/OpenAPI/Components/Components.Schemas.FieldValueRequest.swift @@ -105,7 +105,7 @@ extension Components.Schemas.FieldValueRequest { action = nil } let referenceValue = Components.Schemas.ReferenceValue( - recordName: reference.recordName, + recordName: reference.recordName.rawValue, action: action ) self.init(value: .ReferenceValue(referenceValue)) diff --git a/Sources/MistKit/OpenAPI/Components/Components.Schemas.ListValuePayload.swift b/Sources/MistKit/OpenAPI/Components/Components.Schemas.ListValuePayload.swift index 1fca78713..74d9b6dcf 100644 --- a/Sources/MistKit/OpenAPI/Components/Components.Schemas.ListValuePayload.swift +++ b/Sources/MistKit/OpenAPI/Components/Components.Schemas.ListValuePayload.swift @@ -103,7 +103,7 @@ extension Components.Schemas.ListValuePayload { action = nil } return Components.Schemas.ReferenceValue( - recordName: reference.recordName, + recordName: reference.recordName.rawValue, action: action ) } diff --git a/Sources/MistKit/OpenAPI/Components/Components.Schemas.RecordOperation.swift b/Sources/MistKit/OpenAPI/Components/Components.Schemas.RecordOperation.swift index 24b2643e3..9a8bf1fda 100644 --- a/Sources/MistKit/OpenAPI/Components/Components.Schemas.RecordOperation.swift +++ b/Sources/MistKit/OpenAPI/Components/Components.Schemas.RecordOperation.swift @@ -61,7 +61,7 @@ extension Components.Schemas.RecordOperation { self.init( operationType: apiOperationType, record: .init( - recordName: recordOperation.recordName, + recordName: recordOperation.recordName?.rawValue, recordType: recordOperation.recordType, recordChangeTag: recordOperation.recordChangeTag, fields: .init(additionalProperties: apiFields), diff --git a/Sources/MistKit/RecordManagement/CloudKitRecord.swift b/Sources/MistKit/RecordManagement/CloudKitRecord.swift index 06d02f3c9..579403059 100644 --- a/Sources/MistKit/RecordManagement/CloudKitRecord.swift +++ b/Sources/MistKit/RecordManagement/CloudKitRecord.swift @@ -74,7 +74,7 @@ public protocol CloudKitRecord: Codable, Sendable { /// /// This is typically computed from the model's primary key or unique identifier. /// For example: "RestoreImage-23C71" or "XcodeVersion-15.2" - var recordName: String { get } + var recordName: RecordName { get } /// Parse a CloudKit record into a model instance /// diff --git a/Tests/MistKitTests/CloudKitService/BatchChunking/CloudKitServiceTests.BatchChunking+LookupAllRecords.swift b/Tests/MistKitTests/CloudKitService/BatchChunking/CloudKitServiceTests.BatchChunking+LookupAllRecords.swift index 44d6edae2..ce61577ea 100644 --- a/Tests/MistKitTests/CloudKitService/BatchChunking/CloudKitServiceTests.BatchChunking+LookupAllRecords.swift +++ b/Tests/MistKitTests/CloudKitService/BatchChunking/CloudKitServiceTests.BatchChunking+LookupAllRecords.swift @@ -57,7 +57,7 @@ extension CloudKitServiceTests.BatchChunking { let names = (0..<5).map { "rec-\($0)" } let results = try await service.lookupAllRecords( - recordNames: names, + recordNames: names.map(RecordName.init(rawValue:)), database: .private ) @@ -83,7 +83,7 @@ extension CloudKitServiceTests.BatchChunking { let names = (0..<450).map { "rec-\($0)" } let results = try await service.lookupAllRecords( - recordNames: names, + recordNames: names.map(RecordName.init(rawValue:)), database: .private ) @@ -110,7 +110,7 @@ extension CloudKitServiceTests.BatchChunking { let names = (0..<5).map { "rec-\($0)" } _ = try await service.lookupAllRecords( - recordNames: names, + recordNames: names.map(RecordName.init(rawValue:)), database: .private, batchSize: 2 ) @@ -132,7 +132,7 @@ extension CloudKitServiceTests.BatchChunking { let names = (0..<3).map { "rec-\($0)" } _ = try await service.lookupAllRecords( - recordNames: names, + recordNames: names.map(RecordName.init(rawValue:)), database: .private, batchSize: 0 ) @@ -154,7 +154,7 @@ extension CloudKitServiceTests.BatchChunking { let names = (0..<250).map { "rec-\($0)" } _ = try await service.lookupAllRecords( - recordNames: names, + recordNames: names.map(RecordName.init(rawValue:)), database: .private, batchSize: 9_999 ) @@ -206,7 +206,7 @@ extension CloudKitServiceTests.BatchChunking { await #expect(throws: CloudKitError.self) { _ = try await service.lookupAllRecords( - recordNames: names, + recordNames: names.map(RecordName.init(rawValue:)), database: .private ) } diff --git a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+SuccessCases.swift index 933d0b51e..d9f0c3389 100644 --- a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+SuccessCases.swift @@ -52,7 +52,7 @@ extension CloudKitServiceTests.Upload { database: .public(.prefers(.serverToServer)) ) - #expect(result.recordName.isEmpty == false, "Result should have a record name") + #expect(result.recordName.rawValue.isEmpty == false, "Result should have a record name") #expect(result.fieldName == "file", "Result should have the field name from mock response") #expect(result.asset.receipt != nil, "Asset should have a receipt from CloudKit") } diff --git a/Tests/MistKitTests/Models/BatchSyncResultTests.swift b/Tests/MistKitTests/Models/BatchSyncResultTests.swift index 42aeb2ff6..6202a8286 100644 --- a/Tests/MistKitTests/Models/BatchSyncResultTests.swift +++ b/Tests/MistKitTests/Models/BatchSyncResultTests.swift @@ -41,7 +41,7 @@ internal struct BatchSyncResultTests { type: String = "Article" ) -> RecordInfo { RecordInfo( - recordName: name, + recordName: RecordName(name), recordType: type, recordChangeTag: nil, fields: [:] diff --git a/Tests/MistKitTests/Models/RecordNameTests.swift b/Tests/MistKitTests/Models/RecordNameTests.swift new file mode 100644 index 000000000..4f5a6fe61 --- /dev/null +++ b/Tests/MistKitTests/Models/RecordNameTests.swift @@ -0,0 +1,75 @@ +// +// RecordNameTests.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 +internal import MistKitOpenAPI +internal import Testing + +@testable import MistKit + +@Suite("RecordName") +internal struct RecordNameTests { + @Test("encodes and decodes as a single JSON string") + internal func codableRoundTripIsASingleJSONString() throws { + let name: RecordName = "custom-note-1" + let encoded = try JSONEncoder().encode(name) + #expect(String(data: encoded, encoding: .utf8) == "\"custom-note-1\"") + + let decoded = try JSONDecoder().decode(RecordName.self, from: encoded) + #expect(decoded == name) + #expect(decoded.rawValue == "custom-note-1") + } + + @Test("accepts a string literal at the call site") + internal func stringLiteralInitializesRecordName() { + let name: RecordName = "550e8400-e29b-41d4-a716-446655440000" + #expect(name == RecordName(rawValue: "550e8400-e29b-41d4-a716-446655440000")) + #expect(name.description == "550e8400-e29b-41d4-a716-446655440000") + } + + @Test("Reference conversion still maps the record name to the OpenAPI string") + internal func referenceConversionUsesTheWireString() { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("FieldValue is not available on this operating system.") + return + } + let reference = Reference(recordName: "parent-record-123", action: .deleteSelf) + let request = Components.Schemas.FieldValueRequest(from: .reference(reference)) + + guard case .ReferenceValue(let value) = request.value else { + Issue.record("Expected ReferenceValue") + return + } + #expect(value.recordName == "parent-record-123") + #expect(value.action == .DELETE_SELF) + + let fieldValue = FieldValue(referenceValue: value) + #expect(fieldValue == .reference(reference)) + } +} diff --git a/Tests/MistKitTests/RecordManagement/AltTestRecord.swift b/Tests/MistKitTests/RecordManagement/AltTestRecord.swift index d38fdf734..9f79fc17e 100644 --- a/Tests/MistKitTests/RecordManagement/AltTestRecord.swift +++ b/Tests/MistKitTests/RecordManagement/AltTestRecord.swift @@ -35,7 +35,7 @@ internal import Foundation internal struct AltTestRecord: CloudKitRecord { internal static var cloudKitRecordType: String { "AltTestRecord" } - internal var recordName: String + internal var recordName: RecordName internal var title: String internal static func from(recordInfo: RecordInfo) -> AltTestRecord? { diff --git a/Tests/MistKitTests/RecordManagement/RecordManagingTests+Sync.swift b/Tests/MistKitTests/RecordManagement/RecordManagingTests+Sync.swift index 15c087fa4..b020f342e 100644 --- a/Tests/MistKitTests/RecordManagement/RecordManagingTests+Sync.swift +++ b/Tests/MistKitTests/RecordManagement/RecordManagingTests+Sync.swift @@ -41,7 +41,7 @@ extension RecordManagingTests { let records: [TestRecord] = (0..<50).map { index in TestRecord( - recordName: "test-\(index)", + recordName: RecordName("test-\(index)"), name: "Record \(index)", count: index, isActive: true, @@ -70,7 +70,7 @@ extension RecordManagingTests { // Create 450 records to test batching (should be split into 200, 200, 50) let records: [TestRecord] = (0..<450).map { index in TestRecord( - recordName: "test-\(index)", + recordName: RecordName("test-\(index)"), name: "Record \(index)", count: index, isActive: true, @@ -143,7 +143,7 @@ extension RecordManagingTests { let records: [TestRecord] = (0..<200).map { index in TestRecord( - recordName: "test-\(index)", + recordName: RecordName("test-\(index)"), name: "Record \(index)", count: index, isActive: true, @@ -167,7 +167,7 @@ extension RecordManagingTests { let records: [TestRecord] = (0..<201).map { index in TestRecord( - recordName: "test-\(index)", + recordName: RecordName("test-\(index)"), name: "Record \(index)", count: index, isActive: true, diff --git a/Tests/MistKitTests/RecordManagement/TestRecord.swift b/Tests/MistKitTests/RecordManagement/TestRecord.swift index 7892f9679..9864e98b7 100644 --- a/Tests/MistKitTests/RecordManagement/TestRecord.swift +++ b/Tests/MistKitTests/RecordManagement/TestRecord.swift @@ -35,7 +35,7 @@ internal import Foundation internal struct TestRecord: CloudKitRecord { internal static var cloudKitRecordType: String { "TestRecord" } - internal var recordName: String + internal var recordName: RecordName internal var name: String internal var count: Int internal var isActive: Bool