Add canonical ID pipeline and fix UUID consistency for CloudKit sync
- Add local canonicalization pipeline (stadiums, teams, games) that generates deterministic canonical IDs before CloudKit upload - Fix CanonicalSyncService to use deterministic UUIDs from canonical IDs instead of random UUIDs from CloudKit records - Add SyncStadium/SyncTeam/SyncGame types to CloudKitService that preserve canonical ID relationships during sync - Add canonical ID field keys to CKModels for reading from CloudKit records - Bundle canonical JSON files (stadiums_canonical, teams_canonical, games_canonical, stadium_aliases) for consistent bootstrap data - Update BootstrapService to prefer canonical format files over legacy format This ensures all entities use consistent deterministic UUIDs derived from their canonical IDs, preventing duplicate records when syncing CloudKit data with bootstrapped local data. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,27 @@ actor CloudKitService {
|
||||
self.publicDatabase = container.publicCloudDatabase
|
||||
}
|
||||
|
||||
// MARK: - Sync Types (include canonical IDs from CloudKit)
|
||||
|
||||
struct SyncStadium {
|
||||
let stadium: Stadium
|
||||
let canonicalId: String
|
||||
}
|
||||
|
||||
struct SyncTeam {
|
||||
let team: Team
|
||||
let canonicalId: String
|
||||
let stadiumCanonicalId: String
|
||||
}
|
||||
|
||||
struct SyncGame {
|
||||
let game: Game
|
||||
let canonicalId: String
|
||||
let homeTeamCanonicalId: String
|
||||
let awayTeamCanonicalId: String
|
||||
let stadiumCanonicalId: String
|
||||
}
|
||||
|
||||
// MARK: - Availability Check
|
||||
|
||||
func isAvailable() async -> Bool {
|
||||
@@ -189,6 +210,97 @@ actor CloudKitService {
|
||||
return ckGame.game(homeTeamId: homeId, awayTeamId: awayId, stadiumId: stadiumId)
|
||||
}
|
||||
|
||||
// MARK: - Sync Fetch Methods (return canonical IDs directly from CloudKit)
|
||||
|
||||
/// Fetch stadiums with canonical IDs for sync operations
|
||||
func fetchStadiumsForSync() async throws -> [SyncStadium] {
|
||||
let predicate = NSPredicate(value: true)
|
||||
let query = CKQuery(recordType: CKRecordType.stadium, predicate: predicate)
|
||||
|
||||
let (results, _) = try await publicDatabase.records(matching: query)
|
||||
|
||||
return results.compactMap { result -> SyncStadium? in
|
||||
guard case .success(let record) = result.1 else { return nil }
|
||||
let ckStadium = CKStadium(record: record)
|
||||
guard let stadium = ckStadium.stadium,
|
||||
let canonicalId = ckStadium.canonicalId
|
||||
else { return nil }
|
||||
return SyncStadium(stadium: stadium, canonicalId: canonicalId)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch teams with canonical IDs for sync operations
|
||||
func fetchTeamsForSync(for sport: Sport) async throws -> [SyncTeam] {
|
||||
let predicate = NSPredicate(format: "sport == %@", sport.rawValue)
|
||||
let query = CKQuery(recordType: CKRecordType.team, predicate: predicate)
|
||||
|
||||
let (results, _) = try await publicDatabase.records(matching: query)
|
||||
|
||||
return results.compactMap { result -> SyncTeam? in
|
||||
guard case .success(let record) = result.1 else { return nil }
|
||||
let ckTeam = CKTeam(record: record)
|
||||
guard let team = ckTeam.team,
|
||||
let canonicalId = ckTeam.canonicalId,
|
||||
let stadiumCanonicalId = ckTeam.stadiumCanonicalId
|
||||
else { return nil }
|
||||
return SyncTeam(team: team, canonicalId: canonicalId, stadiumCanonicalId: stadiumCanonicalId)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch games with canonical IDs for sync operations
|
||||
func fetchGamesForSync(
|
||||
sports: Set<Sport>,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
) async throws -> [SyncGame] {
|
||||
var allGames: [SyncGame] = []
|
||||
|
||||
for sport in sports {
|
||||
let predicate = NSPredicate(
|
||||
format: "sport == %@ AND dateTime >= %@ AND dateTime <= %@",
|
||||
sport.rawValue,
|
||||
startDate as NSDate,
|
||||
endDate as NSDate
|
||||
)
|
||||
let query = CKQuery(recordType: CKRecordType.game, predicate: predicate)
|
||||
|
||||
let (results, _) = try await publicDatabase.records(matching: query)
|
||||
|
||||
let games = results.compactMap { result -> SyncGame? in
|
||||
guard case .success(let record) = result.1 else { return nil }
|
||||
let ckGame = CKGame(record: record)
|
||||
|
||||
// Extract canonical IDs directly from CloudKit
|
||||
guard let canonicalId = ckGame.canonicalId,
|
||||
let homeTeamCanonicalId = ckGame.homeTeamCanonicalId,
|
||||
let awayTeamCanonicalId = ckGame.awayTeamCanonicalId,
|
||||
let stadiumCanonicalId = ckGame.stadiumCanonicalId
|
||||
else { return nil }
|
||||
|
||||
// For the Game domain object, we still need UUIDs - use placeholder
|
||||
// The sync service will use canonical IDs for relationships
|
||||
let placeholderUUID = UUID()
|
||||
guard let game = ckGame.game(
|
||||
homeTeamId: placeholderUUID,
|
||||
awayTeamId: placeholderUUID,
|
||||
stadiumId: placeholderUUID
|
||||
) else { return nil }
|
||||
|
||||
return SyncGame(
|
||||
game: game,
|
||||
canonicalId: canonicalId,
|
||||
homeTeamCanonicalId: homeTeamCanonicalId,
|
||||
awayTeamCanonicalId: awayTeamCanonicalId,
|
||||
stadiumCanonicalId: stadiumCanonicalId
|
||||
)
|
||||
}
|
||||
|
||||
allGames.append(contentsOf: games)
|
||||
}
|
||||
|
||||
return allGames.sorted { $0.game.dateTime < $1.game.dateTime }
|
||||
}
|
||||
|
||||
// MARK: - League Structure & Team Aliases
|
||||
|
||||
func fetchLeagueStructure(for sport: Sport? = nil) async throws -> [LeagueStructureModel] {
|
||||
|
||||
Reference in New Issue
Block a user