diff --git a/Sources/App/Features/Event Day/Migrations/EventDay+Migration+V1.swift b/Sources/App/Features/Event Day/Migrations/EventDay+Migration+V1.swift index b936c113..43a1835a 100644 --- a/Sources/App/Features/Event Day/Migrations/EventDay+Migration+V1.swift +++ b/Sources/App/Features/Event Day/Migrations/EventDay+Migration+V1.swift @@ -1,3 +1,4 @@ +import Foundation import Fluent final class EventDayMigrationV1: AsyncMigration { @@ -10,14 +11,26 @@ final class EventDayMigrationV1: AsyncMigration { .field("end_time", .string, .required) .field("name", .string, .required) .create() - + + // We use this local-only model (instead of the 'real' Slot) to solve a migration step problem, + // whilst also allowing us to drop event and date from Slot + final class MigrationSlot: Model { + static let schema = Schema.slot + + @ID(key: .id) var id: UUID? + @Field(key: "date") var date: Date? + @Parent(key: "event_id") var event: Event + + init() {} + } + // Custom migratory code to automatically seed the `event_days` table with previous years information let events = try await Event.query(on: database).all() - let slots = try await Slot.query(on: database).with(\.$event).all() - + let slots = try await MigrationSlot.query(on: database).with(\.$event).all() + for event in events { print("[Migrator] Processing event: \(event.name)") - let eventSlots = slots.filter { $0.event?.id == event.id } + let eventSlots = slots.filter { $0.event.id == event.id } let uniqueDays = Set(eventSlots.compactMap { $0.date?.withoutTime }).sorted() print("[Migrator] Found \(eventSlots.count) slots over \(uniqueDays.count) days") diff --git a/Sources/App/Features/Slots/Controllers/SlotRouteController.swift b/Sources/App/Features/Slots/Controllers/SlotRouteController.swift index 5a775fb3..5aab31d1 100644 --- a/Sources/App/Features/Slots/Controllers/SlotRouteController.swift +++ b/Sources/App/Features/Slots/Controllers/SlotRouteController.swift @@ -87,9 +87,7 @@ struct SlotRouteController: RouteCollection { let mutableSlot = slot ?? Slot() mutableSlot.startDate = input.startTime - mutableSlot.date = inputDate mutableSlot.duration = duration - mutableSlot.$event.id = try event.requireID() mutableSlot.$day.id = try eventDay.requireID() if let activity { diff --git a/Sources/App/Features/Slots/Migrations/Slot+Migration+v3.swift b/Sources/App/Features/Slots/Migrations/Slot+Migration+v3.swift index abf3b81d..827408bb 100644 --- a/Sources/App/Features/Slots/Migrations/Slot+Migration+v3.swift +++ b/Sources/App/Features/Slots/Migrations/Slot+Migration+v3.swift @@ -6,10 +6,21 @@ struct SlotMigrationV3: AsyncMigration { try await database.schema(Schema.slot) .field("day_id", .uuid, .references(Schema.eventDay, "id")) .update() - - let slots = try await Slot.query(on: database).all() + + // Define a local-only model to access the old structure + final class MigrationSlot: Model { + static let schema = Schema.slot + + @ID(key: .id) var id: UUID? + @Field(key: "date") var date: Date? + @OptionalParent(key: "day_id") var day: EventDay? + + init() {} + } + + let slots = try await MigrationSlot.query(on: database).all() let days = try await EventDay.query(on: database).all() - + for slot in slots { if let day = days.first(where: { $0.date.withoutTime == slot.date?.withoutTime }) { slot.$day.id = try day.requireID() @@ -19,7 +30,7 @@ struct SlotMigrationV3: AsyncMigration { } } } - + func revert(on database: Database) async throws { try await database.schema(Schema.slot) .deleteField("day_id") diff --git a/Sources/App/Features/Slots/Migrations/Slot+Migration+v4.swift b/Sources/App/Features/Slots/Migrations/Slot+Migration+v4.swift index c73f1749..25ff6917 100644 --- a/Sources/App/Features/Slots/Migrations/Slot+Migration+v4.swift +++ b/Sources/App/Features/Slots/Migrations/Slot+Migration+v4.swift @@ -3,13 +3,38 @@ import Fluent struct SlotMigrationV4: AsyncMigration { func prepare(on database: Database) async throws { - fatalError("Read comment from Aug 2024 below") + + final class MigrationSlot: Model { + static let schema = Schema.slot + + @ID(key: .id) var id: UUID? + @OptionalField(key: "duration") var duration: Double? + @OptionalParent(key: "presentation_id") var presentation: Presentation? + @OptionalParent(key: "activity_id") var activity: Activity? + } + + final class MigrationPresentation: Model { + static let schema = Schema.presentation + + @ID(key: .id) var id: UUID? + @OptionalField(key: "duration") var duration: Double? + @OptionalField(key: "slot_id") var slotID: UUID? + } + + final class MigrationActivity: Model { + static let schema = Schema.activity + + @ID(key: .id) var id: UUID? + @OptionalField(key: "duration") var duration: Double? + @OptionalField(key: "slot_id") var slotID: UUID? + } + + // fatalError("Read comment from Aug 2024 below") // Aug 2024: This migrator has been partially commented out as the field it relies on has been removed (as it was no longer needed). // In order to migrate past this point, you need to go to an earlier commit, migrate, and then come to a more recent commit before finishing // the migration. // Or, alternatively, just take a backup of production and apply that locally so you're up to date without playing git games. - - /* + try await database.schema(Schema.slot) .field("presentation_id", .uuid, .references(Schema.presentation, "id")) .field("activity_id", .uuid, .references(Schema.activity, "id")) @@ -25,36 +50,35 @@ struct SlotMigrationV4: AsyncMigration { // Data Migrator - let slots = try await Slot.query(on: database).all() - let presentations = try await Presentation.query(on: database).with(\.$slot).all() - let activities = try await Activity.query(on: database).with(\.$slot).all() - + let slots = try await MigrationSlot.query(on: database).all() + let presentations = try await MigrationPresentation.query(on: database).all() + let activities = try await MigrationActivity.query(on: database).all() + for presentation in presentations { - if let slot = slots.first(where: { $0.id == presentation.slot?.id }) { + if let slot = slots.first(where: { $0.id == presentation.slotID }) { let slotDuration = slot.duration slot.$presentation.id = try presentation.requireID() - slot.duration = 0 // easy to set to nil in future cleanup + slot.duration = 0 try await slot.update(on: database) - - presentation.$slot.id = nil + + presentation.slotID = nil presentation.duration = slotDuration ?? 0 try await presentation.update(on: database) } } - + for activity in activities { - if let slot = slots.first(where: { $0.id == activity.slot?.id }) { + if let slot = slots.first(where: { $0.id == activity.slotID }) { let slotDuration = slot.duration slot.$activity.id = try activity.requireID() - slot.duration = 0 // easy to set to nil in future cleanup + slot.duration = 0 try await slot.update(on: database) - - activity.$slot.id = nil + + activity.slotID = nil activity.duration = slotDuration ?? 0 try await activity.update(on: database) } } - */ } func revert(on database: Database) async throws { diff --git a/Sources/App/Features/Slots/Migrations/Slot+Migration+v6.swift b/Sources/App/Features/Slots/Migrations/Slot+Migration+v6.swift new file mode 100644 index 00000000..a95461f4 --- /dev/null +++ b/Sources/App/Features/Slots/Migrations/Slot+Migration+v6.swift @@ -0,0 +1,19 @@ +import Foundation +import Fluent + +struct SlotMigrationV6: AsyncMigration { + func prepare(on db: Database) async throws { + try await db.schema(Schema.slot) + .deleteField("date") + .deleteField("event_id") + .update() + } + + func revert(on db: Database) async throws { + try await db.schema(Schema.slot) + .field("date", .datetime) + .field("event_id", .uuid, .references(Schema.event, "id")) + .update() + + } +} diff --git a/Sources/App/Features/Slots/Models/Slot.swift b/Sources/App/Features/Slots/Models/Slot.swift index d4a47e61..1d392bdc 100644 --- a/Sources/App/Features/Slots/Models/Slot.swift +++ b/Sources/App/Features/Slots/Models/Slot.swift @@ -12,19 +12,9 @@ final class Slot: Codable, Model, Content, @unchecked Sendable { @Field(key: "start_date") var startDate: String - - // DO NOT USE (June 2024) - // TODO: This will be removed in a future PR as part of a cleanup - @Field(key: "date") - var date: Date? @Field(key: "duration") var duration: Double? - - // DO NOT USE (June 2024) - // TODO: This will be removed in a future PR as part of a cleanup - it needs to be done this way for safe migrations. - @OptionalParent(key: "event_id") - var event: Event? @OptionalParent(key: "day_id") var day: EventDay? @@ -40,23 +30,26 @@ final class Slot: Codable, Model, Content, @unchecked Sendable { init( id: IDValue?, startDate: String, - date: Date, duration: Double? ) { self.id = id self.startDate = startDate - self.date = date self.duration = duration } } extension Array where Element == Slot { var schedule: [[Slot]] { - let dates = Set(compactMap { $0.date?.withoutTime }).sorted(by: (<)) + let dates = Set(compactMap { $0.day?.date.withoutTime }).sorted() var slots: [[Slot]] = [] for date in dates { - slots.append(filter { Calendar.current.compare(date, to: $0.date ?? Date(), toGranularity: .day) == .orderedSame }) + slots.append( + filter { + guard let slotDate = $0.day?.date else { return false } + return Calendar.current.isDate(slotDate, inSameDayAs: date) + } + ) } return slots diff --git a/Sources/App/Features/Slots/Transformers/SlotTransformer.swift b/Sources/App/Features/Slots/Transformers/SlotTransformer.swift index 16088021..2e068395 100644 --- a/Sources/App/Features/Slots/Transformers/SlotTransformer.swift +++ b/Sources/App/Features/Slots/Transformers/SlotTransformer.swift @@ -28,7 +28,7 @@ enum SlotTransformer: Transformer { return .init( id: id, startTime: entity.startDate, - date: entity.date, + date: entity.day?.date, duration: entity.presentation?.duration ?? entity.activity?.duration ?? entity.duration ?? 0, presentation: presentation, activity: activity diff --git a/Sources/App/Migrations.swift b/Sources/App/Migrations.swift index 71c130b3..9768767e 100644 --- a/Sources/App/Migrations.swift +++ b/Sources/App/Migrations.swift @@ -83,6 +83,7 @@ class Migrations { app.migrations.add(SpeakerMigrationV2()) // Add more social link options app.migrations.add(SlotMigrationV5()) // Remove unneeded slot_id params app.migrations.add(PresentationMigrationV6()) // Add video visibility + app.migrations.add(SlotMigrationV6()) // Remove legacy date and event_id fields do { guard let url = Environment.get("DATABASE_URL") else { diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 132ee8f2..1c64a130 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -86,12 +86,15 @@ func routes(_ app: Application) throws { .all() let slots = try await Slot .query(on: request.db) - .sort(\.$date) .sort(\.$startDate) .with(\.$day) .with(\.$presentation) .with(\.$activity) .all() + .sorted { + guard let d1 = $0.day?.date, let d2 = $1.day?.date else { return false } + return d1 < d2 + } let activities = try await Activity .query(on: request.db) .sort(\.$event.$id, .descending) // This moves 'Reusable' events to the top of the filtered view