Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions migration/db/migrations/20261019100500000_unified_history.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
-- +micrate Up
-- SQL in section 'Up' is executed when this migration is applied

-- Unified change history for tracked models (see `History::Tracked`).
-- Replaces the previous `history` table (changed fields only, never read)
-- and `group_history`, whose rows are migrated with the group id as the
-- correlation id.
DROP TABLE IF EXISTS "history";

CREATE TABLE IF NOT EXISTS "history" (
id UUID PRIMARY KEY DEFAULT uuidv7(),
resource_type TEXT NOT NULL,
resource_id TEXT NOT NULL,
correlation_id TEXT,
action TEXT NOT NULL,
changed_fields TEXT[] NOT NULL DEFAULT '{}',
-- {"field": [previous, current]}
diff JSONB NOT NULL DEFAULT '{}'::jsonb,
-- the full record, for deletes and approvals
snapshot JSONB,
-- the acting user, kept without a foreign key so the trail survives user deletion
user_id TEXT,
user_email TEXT,
-- request details: app id, name, build, user agent, ip etc
context JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS history_resource_index
ON "history" USING BTREE (resource_type, resource_id, created_at DESC);
CREATE INDEX IF NOT EXISTS history_correlation_id_index
ON "history" USING BTREE (correlation_id, created_at DESC)
WHERE correlation_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS history_user_id_index
ON "history" USING BTREE (user_id)
WHERE user_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS history_created_at_index
ON "history" USING BTREE (created_at);

INSERT INTO "history" (
resource_type, resource_id, correlation_id, action, changed_fields, user_id, user_email, created_at
)
SELECT
CASE resource_type
WHEN 'group' THEN 'groups'
WHEN 'group_user' THEN 'group_users'
WHEN 'group_zone' THEN 'group_zones'
WHEN 'group_invitation' THEN 'group_invitations'
ELSE resource_type
END,
resource_id,
group_id::TEXT,
action,
changed_fields,
user_id,
email,
created_at
FROM "group_history";

DROP TABLE IF EXISTS "group_history";

-- +micrate Down
-- SQL section 'Down' is executed when this migration is rolled back

CREATE TABLE IF NOT EXISTS "group_history"(
id UUID PRIMARY KEY DEFAULT uuidv7(),
group_id UUID,
user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL,
email TEXT NOT NULL,
action TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT NOT NULL,
changed_fields TEXT[] NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL
);

CREATE INDEX IF NOT EXISTS group_history_group_id_index
ON "group_history" USING BTREE (group_id);
CREATE INDEX IF NOT EXISTS group_history_user_id_index
ON "group_history" USING BTREE (user_id);
CREATE INDEX IF NOT EXISTS group_history_created_at_index
ON "group_history" USING BTREE (created_at);

INSERT INTO "group_history" (
group_id, user_id, email, action, resource_type, resource_id, changed_fields, created_at
)
SELECT
h.correlation_id::UUID,
u.id,
h.user_email,
h.action,
CASE h.resource_type
WHEN 'groups' THEN 'group'
WHEN 'group_users' THEN 'group_user'
WHEN 'group_zones' THEN 'group_zone'
WHEN 'group_invitations' THEN 'group_invitation'
END,
h.resource_id,
h.changed_fields,
h.created_at
FROM "history" h
LEFT JOIN "user" u ON u.id = h.user_id
WHERE h.resource_type IN ('groups', 'group_users', 'group_zones', 'group_invitations')
AND h.user_email IS NOT NULL;

DROP TABLE IF EXISTS "history";

CREATE TABLE IF NOT EXISTS "history" (
id TEXT NOT NULL PRIMARY KEY,
type TEXT NOT NULL,
resource_id TEXT NOT NULL,
action TEXT NOT NULL,
changed_fields TEXT[] NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);

CREATE INDEX IF NOT EXISTS history_type_index ON "history" USING BTREE (type);
CREATE INDEX IF NOT EXISTS history_resource_id_index ON "history" USING BTREE (resource_id);
4 changes: 2 additions & 2 deletions shard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ dependencies:
# Data validation library
active-model:
github: spider-gazelle/active-model
version: ~> 4.4
version: ~> 4.7

# Data validation library
CrystalEmail:
Expand Down Expand Up @@ -41,7 +41,7 @@ dependencies:
# ORM for Postgres built on active-model
pg-orm:
github: spider-gazelle/pg-orm
version: ">= 2.4.1"
version: ">= 2.5.1"

# PlaceOS calendar for Tenant model
place_calendar:
Expand Down
22 changes: 19 additions & 3 deletions spec/event_metadata_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ module PlaceOS::Model

histories = History.all.to_a
histories.size.should eq 1
histories.first.type.should eq "event"
histories.first.resource_type.should eq "event_metadatas"
histories.first.resource_id.should eq event.event_id
histories.first.action.should eq "updated"
histories.first.changed_fields.should contain "setup_time"
histories.first.action.should eq "update"
histories.first.changed_fields.should eq ["setup_time"]
histories.first.diff["setup_time"].should eq JSON.parse("[0, 300]")
end

it "records multiple changed fields" do
Expand Down Expand Up @@ -106,6 +107,8 @@ module PlaceOS::Model
histories.size.should eq 1
histories.first.changed_fields.should contain "ext_data.catering"
histories.first.changed_fields.should contain "ext_data.notes"
histories.first.changed_fields.should_not contain "ext_data"
histories.first.diff["ext_data"][1].should eq JSON.parse(%({"catering": true, "notes": "test"}))
end

it "does not record history when no fields changed" do
Expand All @@ -121,6 +124,19 @@ module PlaceOS::Model

History.all.to_a.size.should eq 0
end

it "records deletes with a snapshot" do
tenant = get_tenant
event = Generator.event_metadata(tenant.id, 5.minutes.from_now, 10.minutes.from_now)
event.save!
event.destroy

histories = History.all.to_a
histories.size.should eq 1
histories.first.action.should eq "delete"
histories.first.resource_id.should eq event.event_id
histories.first.snapshot.not_nil!["event_id"].should eq event.event_id
end
end
end
end
33 changes: 8 additions & 25 deletions spec/generator.cr
Original file line number Diff line number Diff line change
Expand Up @@ -963,16 +963,20 @@ module PlaceOS::Model
end

def self.history(
type : String = "zone",
resource_type : String = "zone",
resource_id : String = "zone-#{RANDOM.hex(4)}",
action : String = "update",
changed_fields : Array(String) = ["name", "description"],
correlation_id : String? = nil,
created_at : Time = Time.utc,
)
History.new(
type: type,
resource_type: resource_type,
resource_id: resource_id,
action: action,
changed_fields: changed_fields
correlation_id: correlation_id,
changed_fields: ["name"],
diff: JSON.parse(%({"name": ["old", "new"]})),
created_at: created_at,
)
end

Expand Down Expand Up @@ -1066,27 +1070,6 @@ module PlaceOS::Model
)
end

def self.group_history(
group_id : UUID? = nil,
user : User? = nil,
action : String = "update",
resource_type : String = "group",
resource_id : String? = nil,
changed_fields : Array(String) = ["name"],
)
actor_email = user.try(&.email.to_s) || Faker::Internet.email
actor_id = user.try(&.id)
GroupHistory.new(
group_id: group_id,
user_id: actor_id,
email: actor_email,
action: action,
resource_type: resource_type,
resource_id: resource_id || "res-#{RANDOM.hex(4)}",
changed_fields: changed_fields,
)
end

def self.group_playlist(
group : Group? = nil,
playlist : Playlist? = nil,
Expand Down
2 changes: 1 addition & 1 deletion spec/group_ad_groups_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ require "./helper"
module PlaceOS::Model
describe "Group AD group mappings" do
Spec.before_each do
GroupHistory.clear
History.clear
GroupInvitation.clear
GroupZone.clear
GroupUser.clear
Expand Down
6 changes: 3 additions & 3 deletions spec/group_features_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ end
module PlaceOS::Model
describe "Group features" do
Spec.before_each do
GroupHistory.clear
History.clear
GroupInvitation.clear
GroupZone.clear
GroupUser.clear
Expand Down Expand Up @@ -97,11 +97,11 @@ module PlaceOS::Model
actor = Generator.user(authority: authority).save!
group = Generator.group(authority: authority, subsystems: ["signage"]).save!

group.acting_user = actor
group.history_metadata = History::Metadata.new.with_user(actor)
group.features = feature_json(%({"signage": {"ai": true}}))
group.save!

entry = GroupHistory.where(resource_type: "group", resource_id: group.id.to_s, action: "update").to_a.first
entry = History.where(resource_type: "groups", resource_id: group.id.to_s, action: "update").to_a.first
entry.changed_fields.should contain("features")
end

Expand Down
70 changes: 0 additions & 70 deletions spec/group_history_spec.cr

This file was deleted.

2 changes: 1 addition & 1 deletion spec/group_invitation_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ require "./helper"
module PlaceOS::Model
describe GroupInvitation do
Spec.before_each do
GroupHistory.clear
History.clear
GroupInvitation.clear
GroupZone.clear
GroupUser.clear
Expand Down
2 changes: 1 addition & 1 deletion spec/group_permissions_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ module PlaceOS::Model
# - effective_permissions / zone_accessible? / accessible_zone_ids
describe Group do
Spec.before_each do
GroupHistory.clear
History.clear
GroupInvitation.clear
GroupZone.clear
GroupUser.clear
Expand Down
2 changes: 1 addition & 1 deletion spec/group_playlist_item_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ require "./helper"
module PlaceOS::Model
describe GroupPlaylistItem do
Spec.before_each do
GroupHistory.clear
History.clear
GroupInvitation.clear
GroupZone.clear
GroupUser.clear
Expand Down
2 changes: 1 addition & 1 deletion spec/group_playlist_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ require "./helper"
module PlaceOS::Model
describe GroupPlaylist do
Spec.before_each do
GroupHistory.clear
History.clear
GroupInvitation.clear
GroupZone.clear
GroupUser.clear
Expand Down
2 changes: 1 addition & 1 deletion spec/group_signage_template_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ require "./helper"
module PlaceOS::Model
describe GroupSignageTemplate do
Spec.before_each do
GroupHistory.clear
History.clear
GroupInvitation.clear
GroupZone.clear
GroupUser.clear
Expand Down
Loading
Loading