diff --git a/OPENAPI_DOC.yml b/OPENAPI_DOC.yml index f3c0bc1..a8af15e 100644 --- a/OPENAPI_DOC.yml +++ b/OPENAPI_DOC.yml @@ -4368,6 +4368,129 @@ paths: application/json: schema: $ref: '#/components/schemas/Application__CommonError' + /api/staff/v1/events/history: + get: + summary: returns history records for events in the specified period + tags: + - Events + operationId: Events_history + parameters: + - name: period_start + in: query + description: event period start as a unix epoch + example: "1661725146" + required: true + schema: + type: integer + format: Int64 + - name: period_end + in: query + description: event period end as a unix epoch + example: "1661743123" + required: true + schema: + type: integer + format: Int64 + - name: calendars + in: query + description: a comma seperated list of calendar ids, recommend using `system_id` + for resource calendars + example: user@org.com,room2@resource.org.com + schema: + type: string + nullable: true + - name: zone_ids + in: query + description: a comma seperated list of zone ids + example: zone-123,zone-456 + schema: + type: string + nullable: true + - name: system_ids + in: query + description: a comma seperated list of event spaces + example: sys-1234,sys-5678 + schema: + type: string + nullable: true + - name: ical_uid + in: query + description: the ical uid of the event you are looking for + example: sqvitruh3ho3mrq896tplad4v8 + schema: + type: string + nullable: true + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PlaceOS__Model__History' + 429: + description: Too Many Requests + content: + application/json: + schema: + $ref: '#/components/schemas/Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/Application__CommonError' + 511: + description: Network Authentication Required + content: + application/json: + schema: + $ref: '#/components/schemas/Application__CommonError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/Application__ContentError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/Application__ValidationError' + 500: + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Application__CommonError' + 405: + description: Method Not Allowed + content: + application/json: + schema: + $ref: '#/components/schemas/Application__CommonError' /api/staff/v1/events/{id}: get: summary: returns the event requested. @@ -12281,6 +12404,34 @@ components: - private - all_day - attachments + PlaceOS__Model__History: + type: object + properties: + created_at: + type: integer + format: Int64 + nullable: true + updated_at: + type: integer + format: Int64 + nullable: true + type: + type: string + nullable: true + resource_id: + type: string + nullable: true + action: + type: string + nullable: true + changed_fields: + type: array + items: + type: string + nullable: true + id: + type: string + nullable: true _PlaceCalendar__Event__Attendee___PlaceOS__Model__Attendee_: anyOf: - type: object diff --git a/shard.lock b/shard.lock index 0071e1d..212bcce 100644 --- a/shard.lock +++ b/shard.lock @@ -59,7 +59,7 @@ shards: eventbus: git: https://github.com/spider-gazelle/eventbus.git - version: 1.0.0+git.commit.fab6102d472ce324db4df74c8df9f4a5fa22dafd + version: 1.0.1+git.commit.a707ac06aba16ba6070ff4972acfd839f1f05e15 exception_page: git: https://github.com/crystal-loot/exception_page.git @@ -131,11 +131,11 @@ shards: pg-orm: git: https://github.com/spider-gazelle/pg-orm.git - version: 2.0.5 + version: 2.1.0 place_calendar: git: https://github.com/placeos/calendar.git - version: 4.28.0 + version: 4.28.1 placeos: git: https://github.com/placeos/crystal-client.git @@ -147,7 +147,7 @@ shards: placeos-models: git: https://github.com/placeos/models.git - version: 9.79.0 + version: 9.81.0 promise: git: https://github.com/spider-gazelle/promise.git diff --git a/spec/controllers/events_spec.cr b/spec/controllers/events_spec.cr index e23ceac..70ad7d4 100644 --- a/spec/controllers/events_spec.cr +++ b/spec/controllers/events_spec.cr @@ -1213,4 +1213,127 @@ describe Events, tags: ["event"] do request_body[0].should eq(created_event_id) end end + + describe "#history" do + before_each do + History.clear + end + + it "returns history for events in the specified period" do + tenant = get_tenant + event_start = 10.minutes.from_now.to_unix + event_end = 30.minutes.from_now.to_unix + + # Create event metadata + event = EventMetadatasHelper.create_event( + tenant.id, + event_start: event_start, + event_end: event_end, + system_id: "sys-test-123" + ) + + # Create history records for the event + History.create!( + type: "event", + resource_id: event.event_id, + action: "created", + changed_fields: [] of String + ) + History.create!( + type: "event", + resource_id: event.event_id, + action: "updated", + changed_fields: ["event_start", "event_end"] + ) + + # Query history + response = client.get( + "#{EVENTS_BASE}/history?period_start=#{event_start - 60}&period_end=#{event_end + 60}", + headers: headers + ) + response.status_code.should eq(200) + + histories = Array(History).from_json(response.body) + histories.size.should eq(2) + histories.map(&.action).should contain("created") + histories.map(&.action).should contain("updated") + end + + it "filters history by system_ids" do + tenant = get_tenant + event_start = 10.minutes.from_now.to_unix + event_end = 30.minutes.from_now.to_unix + + # Create events in different systems + event1 = EventMetadatasHelper.create_event( + tenant.id, + event_start: event_start, + event_end: event_end, + system_id: "sys-test-aaa" + ) + event2 = EventMetadatasHelper.create_event( + tenant.id, + event_start: event_start, + event_end: event_end, + system_id: "sys-test-bbb" + ) + + # Create history for both events + History.create!(type: "event", resource_id: event1.event_id, action: "created", changed_fields: [] of String) + History.create!(type: "event", resource_id: event2.event_id, action: "created", changed_fields: [] of String) + + # Query history filtered by system_id + response = client.get( + "#{EVENTS_BASE}/history?period_start=#{event_start - 60}&period_end=#{event_end + 60}&system_ids=sys-test-aaa", + headers: headers + ) + response.status_code.should eq(200) + + histories = Array(History).from_json(response.body) + histories.size.should eq(1) + histories.first.resource_id.should eq(event1.event_id) + end + + it "returns empty array when no events in period" do + tenant = get_tenant + past_start = 2.hours.ago.to_unix + past_end = 1.hour.ago.to_unix + + response = client.get( + "#{EVENTS_BASE}/history?period_start=#{past_start}&period_end=#{past_end}", + headers: headers + ) + response.status_code.should eq(200) + + histories = Array(History).from_json(response.body) + histories.size.should eq(0) + end + + it "returns history matching by ical_uid" do + tenant = get_tenant + event_start = 10.minutes.from_now.to_unix + event_end = 30.minutes.from_now.to_unix + + event = EventMetadatasHelper.create_event( + tenant.id, + event_start: event_start, + event_end: event_end, + ical_uid: "test-ical-uid-12345" + ) + + # Create history using ical_uid as resource_id (as might happen with some calendar providers) + History.create!(type: "event", resource_id: event.ical_uid, action: "updated", changed_fields: ["ext_data.notes"]) + + response = client.get( + "#{EVENTS_BASE}/history?period_start=#{event_start - 60}&period_end=#{event_end + 60}", + headers: headers + ) + response.status_code.should eq(200) + + histories = Array(History).from_json(response.body) + histories.size.should eq(1) + histories.first.resource_id.should eq(event.ical_uid) + histories.first.changed_fields.should eq(["ext_data.notes"]) + end + end end diff --git a/src/config.cr b/src/config.cr index 9345a82..a4bb57f 100644 --- a/src/config.cr +++ b/src/config.cr @@ -16,6 +16,7 @@ alias EventMetadata = PlaceOS::Model::EventMetadata alias Booking = PlaceOS::Model::Booking alias Survey = PlaceOS::Model::Survey alias OutlookManifest = PlaceOS::Model::OutlookManifest +alias History = PlaceOS::Model::History # Server required after application controllers require "action-controller/server" diff --git a/src/controllers/events.cr b/src/controllers/events.cr index ccedaf4..c57c733 100644 --- a/src/controllers/events.cr +++ b/src/controllers/events.cr @@ -320,6 +320,55 @@ class Events < Application } end + # returns history records for events in the specified period + @[AC::Route::GET("/history")] + def history( + @[AC::Param::Info(name: "period_start", description: "event period start as a unix epoch", example: "1661725146")] + starting : Int64, + @[AC::Param::Info(name: "period_end", description: "event period end as a unix epoch", example: "1661743123")] + ending : Int64, + @[AC::Param::Info(description: "a comma seperated list of calendar ids, recommend using `system_id` for resource calendars", example: "user@org.com,room2@resource.org.com")] + calendars : String? = nil, + @[AC::Param::Info(description: "a comma seperated list of zone ids", example: "zone-123,zone-456")] + zone_ids : String? = nil, + @[AC::Param::Info(description: "a comma seperated list of event spaces", example: "sys-1234,sys-5678")] + system_ids : String? = nil, + @[AC::Param::Info(name: "ical_uid", description: "the ical uid of the event you are looking for", example: "sqvitruh3ho3mrq896tplad4v8")] + icaluid : String? = nil, + ) : Array(History) + # Query EventMetadata for events in the time period + query = EventMetadata + .by_tenant(tenant.id) + .is_ending_after(starting) + .is_starting_before(ending) + + # Filter by system_ids if provided + sys_ids = (system_ids || "").split(',').compact_map(&.strip.presence).uniq! + if sys_ids.size > 0 + query = query.where({:system_id => sys_ids}) + end + + # Filter by calendars and zone_ids if provided + calendar_ids = matching_calendar_ids(calendars, zone_ids, nil, allow_default: false) + if calendar_ids.size > 0 + query = query.where({:resource_calendar => calendar_ids.keys}) + end + + # Filter by ical_uid if provided + if icaluid + query = query.where({:ical_uid => icaluid}) + end + + metadatas = query.to_a + return [] of History if metadatas.empty? + + # Collect event IDs from metadata + event_ids = metadatas.flat_map { |meta| [meta.event_id, meta.ical_uid] }.uniq! + + # Query history for these event IDs + History.where({:type => "event", :resource_id => event_ids}).to_a + end + protected def can_create?(user_email : String, host_email : String, attendees : Array(String)) : Bool # if the current user is not then host then they should be an attendee return true if user_email == host_email @@ -1293,6 +1342,20 @@ class Events < Application end notify_destroyed(system, event_id, meta.try &.ical_uid, event, meta, reason: :deleted) end + + # Record history for calendar event change + record_event_history(event_id, change.to_s.downcase) + end + + private def record_event_history(event_id : String, action : String, changed_fields : Array(String) = [] of String) + History.create!( + type: "event", + resource_id: event_id, + action: action, + changed_fields: changed_fields + ) + rescue ex + Log.error(exception: ex) { "failed to record event history for #{event_id}" } end # returns the event requested.