You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: add server name validation and clean invalid data before migration 009 (#591)
## Summary
This PR adds two important improvements to ensure data integrity:
1. **Adds server name format validation at the API layer** to match the
database constraint from migration 008
2. **Includes migration 008 to clean up invalid data** before applying
strict constraints in migration 009
## Changes
### Server Name Validation
- Enforces validation at the API layer matching the database constraint
- **Namespace pattern**: `[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]`
- Must start and end with alphanumeric characters
- Can contain dots and hyphens in the middle
- **Name pattern**: `[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]`
- Must start and end with alphanumeric characters
- Can contain dots, underscores, and hyphens in the middle
- Provides clear, user-friendly error messages indicating which part is
invalid
- Uses DRY approach with composed regex patterns
### Migration 008: Data Cleanup
- Removes servers with invalid names or empty versions (5 servers
affected in production)
- Fixes invalid status values by setting them to 'active' (1 server
affected)
- Removes duplicate name+version combinations
- Includes comprehensive safety checks to prevent accidental data loss
- Must run before migration 009 which adds strict constraints
## Testing
- All validator tests pass
- Migration includes safety checks that match production data
- CI verification included
---------
Co-authored-by: Claude <[email protected]>
-- SAFETY CHECK: Ensure we're deleting exactly the expected data
122
+
-- We only reach this point if we found the known bad server
123
+
-- In production (2025-09-30), we expect exactly 5 deletions and 1 status update
124
+
IF total_to_delete !=5 THEN
125
+
RAISE EXCEPTION 'Safety check failed: Expected to delete exactly 5 servers but would delete %. Check the log above for details. Aborting to prevent data loss.', total_to_delete;
126
+
END IF;
127
+
128
+
IF invalid_status_count !=1 THEN
129
+
RAISE EXCEPTION 'Safety check failed: Expected to update exactly 1 server status but would update %. Check the log above for details. Aborting to prevent data corruption.', invalid_status_count;
130
+
END IF;
131
+
END $$;
132
+
133
+
-- Delete servers with invalid names or empty versions
134
+
-- These cannot be reasonably fixed and would violate primary key constraints
135
+
DELETEFROM servers
136
+
WHERE value->>'name' NOT SIMILAR TO '[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]/[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]'
137
+
OR value->>'version' IS NULL
138
+
OR value->>'version'='';
139
+
140
+
-- Fix invalid status values by setting them to 'active'
141
+
-- These can be reasonably defaulted to a valid value
142
+
UPDATE servers
143
+
SET value = jsonb_set(value, '{status}', '"active"')
144
+
WHERE value->>'status'IS NOT NULL
145
+
AND value->>'status'!=''
146
+
AND value->>'status' NOT IN ('active', 'deprecated', 'deleted');
147
+
148
+
-- Remove duplicate name+version combinations
149
+
-- Keep the one with the most recent publishedAt date
150
+
DELETEFROM servers s1
151
+
WHERE EXISTS (
152
+
SELECT1FROM servers s2
153
+
WHEREs2.value->>'name'=s1.value->>'name'
154
+
ANDs2.value->>'version'=s1.value->>'version'
155
+
AND (s2.value->>'publishedAt')::timestamp> (s1.value->>'publishedAt')::timestamp
156
+
);
157
+
158
+
-- Verify the operations completed as expected
159
+
DO $$
160
+
DECLARE
161
+
remaining_count INTEGER;
162
+
actual_deleted INTEGER;
163
+
actual_updated INTEGER;
164
+
still_invalid_names INTEGER;
165
+
still_empty_versions INTEGER;
166
+
still_invalid_status INTEGER;
167
+
BEGIN
168
+
SELECTCOUNT(*) INTO remaining_count FROM servers;
169
+
170
+
-- Check if any invalid data remains
171
+
SELECTCOUNT(*) INTO still_invalid_names
172
+
FROM servers
173
+
WHERE value->>'name' NOT SIMILAR TO '[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]/[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]';
174
+
175
+
SELECTCOUNT(*) INTO still_empty_versions
176
+
FROM servers
177
+
WHERE value->>'version' IS NULLOR value->>'version'='';
178
+
179
+
SELECTCOUNT(*) INTO still_invalid_status
180
+
FROM servers
181
+
WHERE value->>'status'IS NOT NULL
182
+
AND value->>'status'!=''
183
+
AND value->>'status' NOT IN ('active', 'deprecated', 'deleted');
return"", fmt.Errorf("server name must be in format 'dns-namespace/name' with non-empty namespace and name parts")
409
422
}
410
423
424
+
// Validate name format using regex
425
+
if!serverNameRegex.MatchString(name) {
426
+
namespace:=parts[0]
427
+
serverName:=parts[1]
428
+
429
+
// Check which part is invalid for a better error message
430
+
if!namespaceRegex.MatchString(namespace) {
431
+
return"", fmt.Errorf("%w: namespace '%s' is invalid. Namespace must start and end with alphanumeric characters, and may contain dots and hyphens in the middle", ErrInvalidServerNameFormat, namespace)
432
+
}
433
+
if!namePartRegex.MatchString(serverName) {
434
+
return"", fmt.Errorf("%w: name '%s' is invalid. Name must start and end with alphanumeric characters, and may contain dots, underscores, and hyphens in the middle", ErrInvalidServerNameFormat, serverName)
435
+
}
436
+
// Fallback in case both somehow pass individually but not together
437
+
return"", fmt.Errorf("%w: invalid format for '%s'", ErrInvalidServerNameFormat, name)
0 commit comments