|
| 1 | +# CloudKit Quick Reference for MistKit Development |
| 2 | + |
| 3 | +## REST API Endpoints (webservices.md) |
| 4 | + |
| 5 | +### Base URL Structure |
| 6 | +``` |
| 7 | +https://api.apple-cloudkit.com/database/{version}/{container}/{environment}/{database}/{operation} |
| 8 | +``` |
| 9 | + |
| 10 | +### Authentication |
| 11 | + |
| 12 | +**API Token (User-based)** |
| 13 | +``` |
| 14 | +Headers: |
| 15 | + X-Apple-CloudKit-Request-KeyID: [api-token] |
| 16 | + X-Apple-CloudKit-Request-ISO8601Date: [timestamp] |
| 17 | + X-Apple-CloudKit-Request-SignatureV1: [signature] |
| 18 | +``` |
| 19 | + |
| 20 | +**Server-to-Server Key** |
| 21 | +``` |
| 22 | +Headers: |
| 23 | + X-Apple-CloudKit-Request-KeyID: [key-id] |
| 24 | + X-Apple-CloudKit-Request-ISO8601Date: [timestamp] |
| 25 | + X-Apple-CloudKit-Request-SignatureV1: [signature] |
| 26 | +Body: Included in signature |
| 27 | +``` |
| 28 | + |
| 29 | +### Common Endpoints |
| 30 | + |
| 31 | +| Operation | Path | Method | |
| 32 | +|-----------|------|--------| |
| 33 | +| Query Records | `/records/query` | POST | |
| 34 | +| Modify Records | `/records/modify` | POST | |
| 35 | +| Lookup Records | `/records/lookup` | POST | |
| 36 | +| Record Changes | `/records/changes` | POST | |
| 37 | +| List Zones | `/zones/list` | POST | |
| 38 | +| Modify Zones | `/zones/modify` | POST | |
| 39 | +| Current User | `/users/current` | GET | |
| 40 | +| Upload Asset | `/assets/upload` | POST | |
| 41 | + |
| 42 | +### Request Format (POST endpoints) |
| 43 | +```json |
| 44 | +{ |
| 45 | + "operations": [ |
| 46 | + { |
| 47 | + "operationType": "create", |
| 48 | + "record": { |
| 49 | + "recordType": "Article", |
| 50 | + "fields": { |
| 51 | + "title": { |
| 52 | + "value": "Hello World" |
| 53 | + } |
| 54 | + } |
| 55 | + } |
| 56 | + } |
| 57 | + ] |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +### Response Format |
| 62 | +```json |
| 63 | +{ |
| 64 | + "records": [ |
| 65 | + { |
| 66 | + "recordName": "unique-id", |
| 67 | + "recordType": "Article", |
| 68 | + "fields": { |
| 69 | + "title": { |
| 70 | + "value": "Hello World" |
| 71 | + } |
| 72 | + }, |
| 73 | + "created": { |
| 74 | + "timestamp": 1234567890, |
| 75 | + "userRecordName": "_user-id" |
| 76 | + }, |
| 77 | + "modified": { |
| 78 | + "timestamp": 1234567890, |
| 79 | + "userRecordName": "_user-id" |
| 80 | + }, |
| 81 | + "recordChangeTag": "etag-value" |
| 82 | + } |
| 83 | + ] |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +### Error Response |
| 88 | +```json |
| 89 | +{ |
| 90 | + "serverErrorCode": "INVALID_ARGUMENTS", |
| 91 | + "reason": "Detailed error message", |
| 92 | + "uuid": "request-id" |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +--- |
| 97 | + |
| 98 | +## CloudKit Field Types (webservices.md) |
| 99 | + |
| 100 | +| Type | Description | Example | |
| 101 | +|------|-------------|---------| |
| 102 | +| `STRING` | Text string | `{"value": "Hello"}` | |
| 103 | +| `INT64` | Integer | `{"value": 42}` | |
| 104 | +| `DOUBLE` | Floating point | `{"value": 3.14}` | |
| 105 | +| `BYTES` | Binary data | `{"value": "base64..."}` | |
| 106 | +| `DATE` | Timestamp | `{"value": 1234567890000}` | |
| 107 | +| `LOCATION` | Coordinates | `{"value": {"latitude": 37.7, "longitude": -122.4}}` | |
| 108 | +| `REFERENCE` | Record ref | `{"value": {"recordName": "id", "action": "NONE"}}` | |
| 109 | +| `ASSET` | File reference | `{"value": {"fileChecksum": "...", "size": 1024, "downloadURL": "..."}}` | |
| 110 | +| `STRING_LIST` | Array of strings | `{"value": ["a", "b"]}` | |
| 111 | +| `INT64_LIST` | Array of ints | `{"value": [1, 2, 3]}` | |
| 112 | +| `DOUBLE_LIST` | Array of doubles | `{"value": [1.1, 2.2]}` | |
| 113 | +| `DATE_LIST` | Array of dates | `{"value": [123, 456]}` | |
| 114 | +| `LOCATION_LIST` | Array of locations | `{"value": [{"latitude": ...}, ...]}` | |
| 115 | +| `REFERENCE_LIST` | Array of refs | `{"value": [{"recordName": "id1"}, ...]}` | |
| 116 | + |
| 117 | +--- |
| 118 | + |
| 119 | +## Query Filters (cloudkitjs.md adapted) |
| 120 | + |
| 121 | +### Filter Comparators |
| 122 | +- `EQUALS`, `NOT_EQUALS` |
| 123 | +- `LESS_THAN`, `LESS_THAN_OR_EQUALS` |
| 124 | +- `GREATER_THAN`, `GREATER_THAN_OR_EQUALS` |
| 125 | +- `IN`, `NOT_IN` |
| 126 | +- `BEGINS_WITH`, `NOT_BEGINS_WITH` |
| 127 | +- `CONTAINS_ALL_TOKENS` |
| 128 | +- `LIST_CONTAINS`, `NOT_LIST_CONTAINS` |
| 129 | + |
| 130 | +### Query Structure |
| 131 | +```json |
| 132 | +{ |
| 133 | + "query": { |
| 134 | + "recordType": "Article", |
| 135 | + "filterBy": [ |
| 136 | + { |
| 137 | + "comparator": "EQUALS", |
| 138 | + "fieldName": "status", |
| 139 | + "fieldValue": {"value": "published"} |
| 140 | + } |
| 141 | + ], |
| 142 | + "sortBy": [ |
| 143 | + { |
| 144 | + "fieldName": "createdAt", |
| 145 | + "ascending": false |
| 146 | + } |
| 147 | + ] |
| 148 | + }, |
| 149 | + "zoneID": { |
| 150 | + "zoneName": "_defaultZone" |
| 151 | + }, |
| 152 | + "resultsLimit": 100 |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +--- |
| 157 | + |
| 158 | +## Swift Testing Patterns (testing-enablinganddisabling.md) |
| 159 | + |
| 160 | +### Basic Test |
| 161 | +```swift |
| 162 | +@Test("Description of what is tested") |
| 163 | +func testFeature() async throws { |
| 164 | + let result = await someAsyncOperation() |
| 165 | + #expect(result == expectedValue) |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +### Parameterized Test |
| 170 | +```swift |
| 171 | +@Test("Validate multiple inputs", arguments: [1, 2, 3, 4, 5]) |
| 172 | +func testWithParameter(value: Int) { |
| 173 | + #expect(value > 0) |
| 174 | +} |
| 175 | +``` |
| 176 | + |
| 177 | +### Conditional Tests |
| 178 | +```swift |
| 179 | +@Test("Only run on macOS", .enabled(if: Platform.current == .macOS)) |
| 180 | +func macOSTest() { } |
| 181 | + |
| 182 | +@Test("Skip when feature disabled", .disabled("Feature not ready")) |
| 183 | +func disabledTest() { } |
| 184 | +``` |
| 185 | + |
| 186 | +### Async Expectations |
| 187 | +```swift |
| 188 | +@Test func testAsync() async throws { |
| 189 | + let result = try await apiCall() |
| 190 | + #expect(result.status == .success) |
| 191 | + #expect(result.data != nil) |
| 192 | +} |
| 193 | +``` |
| 194 | + |
| 195 | +### Required Values (halts on nil) |
| 196 | +```swift |
| 197 | +@Test func testRequired() throws { |
| 198 | + let value = try #require(optionalValue) // Stops if nil |
| 199 | + #expect(value.count > 0) |
| 200 | +} |
| 201 | +``` |
| 202 | + |
| 203 | +### Known Issues |
| 204 | +```swift |
| 205 | +@Test("Test with known bug", .bug(id: "12345")) |
| 206 | +func testWithBug() { |
| 207 | + // Test that tracks a known issue |
| 208 | +} |
| 209 | +``` |
| 210 | + |
| 211 | +### Test Suites |
| 212 | +```swift |
| 213 | +@Suite("Feature X Tests") |
| 214 | +struct FeatureXTests { |
| 215 | + @Test func testA() { } |
| 216 | + @Test func testB() { } |
| 217 | +} |
| 218 | +``` |
| 219 | + |
| 220 | +--- |
| 221 | + |
| 222 | +## MistKit Type Mapping |
| 223 | + |
| 224 | +### CloudKit → Swift |
| 225 | + |
| 226 | +| CloudKit Type | Swift Type | |
| 227 | +|---------------|------------| |
| 228 | +| `STRING` | `String` | |
| 229 | +| `INT64` | `Int` | |
| 230 | +| `DOUBLE` | `Double` | |
| 231 | +| `BYTES` | `Data` | |
| 232 | +| `DATE` | `Date` (milliseconds since epoch) | |
| 233 | +| `LOCATION` | `CLLocationCoordinate2D` or custom struct | |
| 234 | +| `REFERENCE` | Custom `CKReference` struct | |
| 235 | +| `ASSET` | Custom `CKAsset` struct with URL | |
| 236 | +| `*_LIST` | `[T]` arrays | |
| 237 | + |
| 238 | +### Swift API Design Patterns |
| 239 | + |
| 240 | +**Container Access** |
| 241 | +```swift |
| 242 | +let container = CloudKitService.container(identifier: "...") |
| 243 | +let database = container.publicDatabase |
| 244 | +``` |
| 245 | + |
| 246 | +**Record Operations** |
| 247 | +```swift |
| 248 | +// Create |
| 249 | +let record = CKRecord(type: "Article") |
| 250 | +record["title"] = "Hello" |
| 251 | +try await database.save(record) |
| 252 | + |
| 253 | +// Query |
| 254 | +let query = CKQuery(recordType: "Article", predicate: ...) |
| 255 | +let results = try await database.perform(query) |
| 256 | + |
| 257 | +// Modify |
| 258 | +record["title"] = "Updated" |
| 259 | +try await database.save(record) |
| 260 | +``` |
| 261 | + |
| 262 | +**Async Sequences for Pagination** |
| 263 | +```swift |
| 264 | +for try await record in database.records(matching: query) { |
| 265 | + process(record) |
| 266 | +} |
| 267 | +``` |
| 268 | + |
| 269 | +--- |
| 270 | + |
| 271 | +## Common Error Codes (webservices.md) |
| 272 | + |
| 273 | +| Code | Meaning | Action | |
| 274 | +|------|---------|--------| |
| 275 | +| `AUTHENTICATION_REQUIRED` | Not authenticated | Obtain web auth token | |
| 276 | +| `INVALID_ARGUMENTS` | Bad request data | Check request format | |
| 277 | +| `NOT_FOUND` | Record doesn't exist | Handle gracefully | |
| 278 | +| `CONFLICT` | Record changed | Resolve conflict | |
| 279 | +| `ATOMIC_ERROR` | Batch partially failed | Check individual results | |
| 280 | +| `ZONE_NOT_FOUND` | Zone doesn't exist | Create zone first | |
| 281 | +| `THROTTLED` | Rate limited | Implement backoff | |
| 282 | +| `INTERNAL_ERROR` | Server error | Retry with backoff | |
| 283 | + |
| 284 | +--- |
| 285 | + |
| 286 | +## Authentication Flow |
| 287 | + |
| 288 | +### User Authentication (API Token) |
| 289 | +1. Call `/tokens/create` with API token |
| 290 | +2. Receive `webAuthToken` |
| 291 | +3. Include in subsequent requests |
| 292 | +4. Token expires after 1 hour |
| 293 | +5. Refresh before expiry |
| 294 | + |
| 295 | +### Server-to-Server |
| 296 | +1. Generate key pair |
| 297 | +2. Upload public key to CloudKit Dashboard |
| 298 | +3. Sign requests with private key |
| 299 | +4. Include signature in headers |
| 300 | + |
| 301 | +--- |
| 302 | + |
| 303 | +## Development Checklist |
| 304 | + |
| 305 | +### Before implementing an endpoint: |
| 306 | +- [ ] Check `webservices.md` for exact endpoint path and parameters |
| 307 | +- [ ] Review `cloudkitjs.md` for operation semantics |
| 308 | +- [ ] Design Swift types matching CloudKit structures |
| 309 | +- [ ] Plan async/await API surface |
| 310 | +- [ ] Consider error handling paths |
| 311 | + |
| 312 | +### Before writing tests: |
| 313 | +- [ ] Review `testing-enablinganddisabling.md` for patterns |
| 314 | +- [ ] Use `@Test` macro, not XCTest |
| 315 | +- [ ] Use `#expect()` and `#require()` for assertions |
| 316 | +- [ ] Test async code with `async throws` |
| 317 | +- [ ] Consider parameterized tests for multiple cases |
| 318 | + |
| 319 | +### Code review: |
| 320 | +- [ ] All types are `Sendable` |
| 321 | +- [ ] All network calls use `async/await` |
| 322 | +- [ ] Errors conform to `LocalizedError` |
| 323 | +- [ ] Public APIs have tests |
| 324 | +- [ ] Swift Testing patterns used correctly |
0 commit comments