Skip to content

Commit 3105a66

Browse files
feat(preferences): add scope columns migration and constants (#1361)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent dc3f312 commit 3105a66

File tree

5 files changed

+52
-2
lines changed

5 files changed

+52
-2
lines changed

core/preference/preference.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ const (
4444
// user default traits
4545
UserFirstName = "first_name"
4646
UserNewsletter = "newsletter"
47+
48+
// Zero values for global/unscoped preferences
49+
// Used instead of NULL for PostgreSQL 14 compatibility
50+
ScopeTypeGlobal = "app/platform"
51+
ScopeIDGlobal = "00000000-0000-0000-0000-000000000000"
4752
)
4853

4954
type Trait struct {
@@ -84,6 +89,8 @@ type Preference struct {
8489
Value string `json:"value"`
8590
ResourceID string `json:"resource_id"`
8691
ResourceType string `json:"resource_type"`
92+
ScopeType string `json:"scope_type"`
93+
ScopeID string `json:"scope_id"`
8794
CreatedAt time.Time `json:"created_at"`
8895
UpdatedAt time.Time `json:"updated_at"`
8996
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Revert scope columns addition
2+
ALTER TABLE preferences DROP CONSTRAINT IF EXISTS uq_preferences_resource_scope;
3+
4+
ALTER TABLE preferences ADD CONSTRAINT resource_type_name_unique
5+
UNIQUE (resource_type, resource_id, name);
6+
7+
ALTER TABLE preferences DROP COLUMN IF EXISTS scope_id;
8+
ALTER TABLE preferences DROP COLUMN IF EXISTS scope_type;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-- Add scope columns for org-scoped user preferences
2+
-- scope_type references namespaces table (like resource_type)
3+
-- scope_id is text (like resource_id) to support different ID formats
4+
-- Using zero values instead of NULL for global preferences:
5+
-- - scope_type: 'app/platform' (global/unscoped)
6+
-- - scope_id: '00000000-0000-0000-0000-000000000000' (zero UUID)
7+
ALTER TABLE preferences ADD COLUMN scope_type text NOT NULL DEFAULT 'app/platform' REFERENCES namespaces (name) ON DELETE CASCADE;
8+
ALTER TABLE preferences ADD COLUMN scope_id text NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000';
9+
10+
-- Update unique constraint to include scope columns
11+
ALTER TABLE preferences DROP CONSTRAINT resource_type_name_unique;
12+
13+
ALTER TABLE preferences ADD CONSTRAINT uq_preferences_resource_scope
14+
UNIQUE (resource_type, resource_id, scope_type, scope_id, name);

internal/store/postgres/preference.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ type Preference struct {
1313
Value string `db:"value"`
1414
ResourceType string `db:"resource_type"`
1515
ResourceID string `db:"resource_id"`
16+
ScopeType string `db:"scope_type"`
17+
ScopeID string `db:"scope_id"`
1618
CreatedAt time.Time `db:"created_at"`
1719
UpdatedAt time.Time `db:"updated_at"`
1820
}
1921

2022
func (from Preference) transformToPreference() preference.Preference {
21-
return preference.Preference{
23+
pref := preference.Preference{
2224
ID: from.ID.String(),
2325
Name: from.Name,
2426
Value: from.Value,
@@ -27,4 +29,12 @@ func (from Preference) transformToPreference() preference.Preference {
2729
CreatedAt: from.CreatedAt,
2830
UpdatedAt: from.UpdatedAt,
2931
}
32+
// Convert zero values back to empty strings for API layer
33+
if from.ScopeType != preference.ScopeTypeGlobal {
34+
pref.ScopeType = from.ScopeType
35+
}
36+
if from.ScopeID != preference.ScopeIDGlobal {
37+
pref.ScopeID = from.ScopeID
38+
}
39+
return pref
3040
}

internal/store/postgres/preference_repository.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,26 @@ func (s *PreferenceRepository) Set(ctx context.Context, pref preference.Preferen
2929
if pref.ID == "" {
3030
pref.ID = uuid.New().String()
3131
}
32+
// Use zero values for scope if not provided (global/unscoped preference)
33+
scopeType := pref.ScopeType
34+
if scopeType == "" {
35+
scopeType = preference.ScopeTypeGlobal
36+
}
37+
scopeID := pref.ScopeID
38+
if scopeID == "" {
39+
scopeID = preference.ScopeIDGlobal
40+
}
3241
query, params, err := dialect.Insert(TABLE_PREFERENCES).Rows(
3342
goqu.Record{
3443
"id": pref.ID,
3544
"name": pref.Name,
3645
"value": pref.Value,
3746
"resource_type": pref.ResourceType,
3847
"resource_id": pref.ResourceID,
48+
"scope_type": scopeType,
49+
"scope_id": scopeID,
3950
"updated_at": goqu.L("NOW()"),
40-
}).OnConflict(goqu.DoUpdate("resource_type, resource_id, name",
51+
}).OnConflict(goqu.DoUpdate("resource_type, resource_id, scope_type, scope_id, name",
4152
goqu.Record{
4253
"value": pref.Value,
4354
"updated_at": time.Now().UTC(),

0 commit comments

Comments
 (0)