diff --git a/Examples/BushelCloud/.gitrepo b/Examples/BushelCloud/.gitrepo index e4c3c566..2141daf1 100644 --- a/Examples/BushelCloud/.gitrepo +++ b/Examples/BushelCloud/.gitrepo @@ -7,6 +7,6 @@ remote = git@github.com:brightdigit/BushelCloud.git branch = mistkit commit = f13f8695a0ca4b041db1e775c239d6cc8b258fb2 - parent = 1ec3d15919adf827cd144f948fc31e822c60ab99 + parent = 95d49423328d8db2778fbf440deea16e651305c8 method = merge cmdver = 0.4.9 diff --git a/Examples/BushelCloud/README.md b/Examples/BushelCloud/README.md index f74476d8..e8029b6e 100644 --- a/Examples/BushelCloud/README.md +++ b/Examples/BushelCloud/README.md @@ -190,6 +190,9 @@ export CLOUDKIT_PRIVATE_KEY_PATH="./path/to/private-key.pem" # Optional: Enable VirtualBuddy TSS signing status export VIRTUALBUDDY_API_KEY="YOUR_VIRTUALBUDDY_API_KEY" +# Optional: Enable VirtualBuddy TSS signing status +export VIRTUALBUDDY_API_KEY="YOUR_VIRTUALBUDDY_API_KEY" + # Sync with verbose logging to learn how MistKit works .build/debug/bushel-cloud sync --verbose @@ -490,6 +493,9 @@ export CLOUDKIT_PRIVATE_KEY_PATH="$HOME/.cloudkit/bushel-private-key.pem" # Optional: VirtualBuddy TSS signing status (get from https://tss.virtualbuddy.app/) export VIRTUALBUDDY_API_KEY="YOUR_VIRTUALBUDDY_API_KEY" +# Optional: VirtualBuddy TSS signing status (get from https://tss.virtualbuddy.app/) +export VIRTUALBUDDY_API_KEY="YOUR_VIRTUALBUDDY_API_KEY" + # Then simply run bushel-cloud sync ``` diff --git a/Examples/CelestraCloud/.gitrepo b/Examples/CelestraCloud/.gitrepo index 17b8b563..90ee4e15 100644 --- a/Examples/CelestraCloud/.gitrepo +++ b/Examples/CelestraCloud/.gitrepo @@ -7,6 +7,6 @@ remote = git@github.com:brightdigit/CelestraCloud.git branch = mistkit commit = 319bc6208763c31706b9e10c3608d9f7cc5c2ef5 - parent = 10cf4510ab393ad1b4277832fcd8441e32fe0b65 + parent = 95d49423328d8db2778fbf440deea16e651305c8 method = merge cmdver = 0.4.9 diff --git a/Examples/CelestraCloud/Package.swift b/Examples/CelestraCloud/Package.swift index 8e2223e5..f65143e5 100644 --- a/Examples/CelestraCloud/Package.swift +++ b/Examples/CelestraCloud/Package.swift @@ -60,20 +60,20 @@ let swiftSettings: [SwiftSetting] = [ .enableExperimentalFeature("WarnUnsafeReflection"), // Enhanced compiler checking - .unsafeFlags([ - // Enable concurrency warnings - "-warn-concurrency", - // Enable actor data race checks - "-enable-actor-data-race-checks", - // Complete strict concurrency checking - "-strict-concurrency=complete", - // Enable testing support - "-enable-testing", - // Warn about functions with >100 lines - "-Xfrontend", "-warn-long-function-bodies=100", - // Warn about slow type checking expressions - "-Xfrontend", "-warn-long-expression-type-checking=100" - ]) + // .unsafeFlags([ + // // Enable concurrency warnings + // "-warn-concurrency", + // // Enable actor data race checks + // "-enable-actor-data-race-checks", + // // Complete strict concurrency checking + // "-strict-concurrency=complete", + // // Enable testing support + // "-enable-testing", + // // Warn about functions with >100 lines + // "-Xfrontend", "-warn-long-function-bodies=100", + // // Warn about slow type checking expressions + // "-Xfrontend", "-warn-long-expression-type-checking=100" + // ]) ] let package = Package( diff --git a/Examples/CelestraCloud/Scripts/setup-cloudkit-schema.sh b/Examples/CelestraCloud/Scripts/setup-cloudkit-schema.sh index cedc9197..243830c3 100755 --- a/Examples/CelestraCloud/Scripts/setup-cloudkit-schema.sh +++ b/Examples/CelestraCloud/Scripts/setup-cloudkit-schema.sh @@ -5,6 +5,35 @@ set -eo pipefail +# Parse command line arguments +DRY_RUN=false +while [[ $# -gt 0 ]]; do + case $1 in + --dry-run) + DRY_RUN=true + shift + ;; + --help|-h) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --dry-run Validate schema without importing" + echo " --help, -h Show this help message" + echo "" + echo "Environment variables:" + echo " CLOUDKIT_CONTAINER_ID CloudKit container ID (default: iCloud.com.brightdigit.Bushel)" + echo " CLOUDKIT_TEAM_ID Apple Developer Team ID (10-character)" + echo " CLOUDKIT_ENVIRONMENT Environment (development or production, default: development)" + exit 0 + ;; + *) + echo "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' @@ -14,6 +43,9 @@ NC='\033[0m' # No Color echo "========================================" echo "CloudKit Schema Setup for Celestra" echo "========================================" +if [ "$DRY_RUN" = true ]; then + echo "(DRY RUN MODE - No changes will be made)" +fi echo "" # Check if cktool is available @@ -108,6 +140,15 @@ fi echo "" +# Skip import if dry-run +if [ "$DRY_RUN" = true ]; then + echo "" + echo -e "${GREEN}✓✓✓ Dry run complete! ✓✓✓${NC}" + echo "" + echo "Schema validation passed. Run without --dry-run to import." + exit 0 +fi + # Confirm before import echo -e "${YELLOW}Warning: This will import the schema into your CloudKit container.${NC}" echo "This operation will create/modify record types in the $ENVIRONMENT environment." @@ -140,6 +181,7 @@ if xcrun cktool import-schema \ echo " a. Go to: https://icloud.developer.apple.com/dashboard/" echo " b. Navigate to: API Access → Server-to-Server Keys" echo " c. Create a new key and download the private key .pem file" + echo " d. Store it securely (e.g., ~/.cloudkit/bushel-private-key.pem)" echo "" echo " 2. Configure your .env file with CloudKit credentials" echo " 3. Run 'swift run celestra add-feed ' to add an RSS feed" diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCloudKitService.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCloudKitService.swift index bcd9cf73..a8ec79d5 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCloudKitService.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCloudKitService.swift @@ -110,16 +110,14 @@ public struct ArticleCloudKitService: Sendable { _ guids: [String], feedRecordName: String? ) async throws(CloudKitError) -> [Article] { - // CloudKit Web Services has issues with combining .in() with other filters. - // Current approach: Use .in() ONLY for GUID filtering (single filter, no combinations). - // Feed filtering is done in-memory (line 135-136) to avoid the .in() + filter issue. - // - // Known limitation: Cannot efficiently query by both GUID and feedRecordName in one query. - // This is acceptable because GUID queries are typically small batches (<150 items). - // - // Alternative considered: Multiple single-GUID queries would be significantly slower - // and hit rate limits faster. The in-memory filter is the pragmatic solution. - let filters: [QueryFilter] = [.in("guid", guids.map { FieldValue.string($0) })] + // Query articles by GUID using the IN filter. + // Now that issue #192 is fixed, we can combine .in() with other filters. + // If feedRecordName is specified, we filter at query time for efficiency. + var filters: [QueryFilter] = [.in("guid", guids.map { FieldValue.string($0) })] + if let feedName = feedRecordName { + filters.append(.equals("feedRecordName", FieldValue.string(feedName))) + } + let records = try await recordOperator.queryRecords( recordType: "Article", filters: filters, @@ -127,7 +125,7 @@ public struct ArticleCloudKitService: Sendable { limit: 200, desiredKeys: nil ) - let articles = records.compactMap { record in + return records.compactMap { record in do { return try Article(from: record) } catch { @@ -137,12 +135,6 @@ public struct ArticleCloudKitService: Sendable { return nil } } - - // Filter by feedRecordName in-memory if specified - if let feedName = feedRecordName { - return articles.filter { $0.feedRecordName == feedName } - } - return articles } // MARK: - Create Operations diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleSyncService.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleSyncService.swift index 0c0085c8..2d46c2e3 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleSyncService.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleSyncService.swift @@ -72,14 +72,11 @@ public struct ArticleSyncService: Sendable { feedRecordName: String ) async throws(CloudKitError) -> ArticleSyncResult { // 1. Query existing articles by GUID - // TEMPORARY: Skip GUID query due to CloudKit Web Services .in() operator issue - // TODO: Fix query or implement alternative deduplication strategy - let existingArticles: [Article] = [] - // let guids = items.map(\.guid) - // let existingArticles = try await articleService.queryArticlesByGUIDs( - // guids, - // feedRecordName: feedRecordName - // ) + let guids = items.map(\.guid) + let existingArticles = try await articleService.queryArticlesByGUIDs( + guids, + feedRecordName: feedRecordName + ) // 2. Categorize into new vs modified (pure function) let categorization = categorizer.categorize( diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift index 2ec566de..4ec2091c 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift @@ -114,14 +114,12 @@ extension ArticleCloudKitService { let mock = MockCloudKitRecordOperator() let service = CelestraCloudKit.ArticleCloudKitService(recordOperator: mock) - // Mock returns 2 articles: one matching feed, one not + // Now that issue #192 is fixed, feedRecordName filter is applied at query time. + // Mock returns only the matching article (CloudKit would filter server-side). let matchingFields = createArticleRecordFields(guid: "guid-1") - let nonMatchingFields = createArticleRecordFields(guid: "guid-2") - .merging(["feedRecordName": .string("other-feed")]) { _, new in new } mock.queryRecordsResult = .success([ - createMockRecordInfo(recordName: "article-1", fields: matchingFields), - createMockRecordInfo(recordName: "article-2", fields: nonMatchingFields) + createMockRecordInfo(recordName: "article-1", fields: matchingFields) ]) let result = try await service.queryArticlesByGUIDs( @@ -129,12 +127,12 @@ extension ArticleCloudKitService { feedRecordName: "feed-123" ) - // Verify CloudKit query behavior + // Verify CloudKit query combines filters #expect(mock.queryCalls.count == 1) - // Should have 1 filter (GUID only), feedRecordName filtered in-memory - #expect(mock.queryCalls[0].filters?.count == 1) + // Should have 2 filters: IN for GUID + EQUALS for feedRecordName + #expect(mock.queryCalls[0].filters?.count == 2) - // Verify in-memory filtering works + // Verify filtering at query time works correctly #expect(result.count == 1) // Only matching article returned #expect(result[0].guid == "guid-1") #expect(result[0].feedRecordName == "feed-123") diff --git a/Package.swift b/Package.swift index 464e8021..1c718ca4 100644 --- a/Package.swift +++ b/Package.swift @@ -62,12 +62,12 @@ let swiftSettings: [SwiftSetting] = [ .enableExperimentalFeature("WarnUnsafeReflection"), // Enhanced compiler checking - // .unsafeFlags([ - // // Warn about functions with >100 lines - // "-Xfrontend", "-warn-long-function-bodies=100", - // // Warn about slow type checking expressions - // "-Xfrontend", "-warn-long-expression-type-checking=100" - // ]) + .unsafeFlags([ + // Warn about functions with >100 lines + "-Xfrontend", "-warn-long-function-bodies=100", + // Warn about slow type checking expressions + "-Xfrontend", "-warn-long-expression-type-checking=100" + ]) ] let package = Package(