ImportWorkspace imports a workspace from an exported data structure. It creates a new workspace with regenerated IDs, remapping all references. If newName is non-empty, it overrides the workspace name and slug.
(data *models.WorkspaceExport, newName string, ownerID string)
| 208 | // It creates a new workspace with regenerated IDs, remapping all references. |
| 209 | // If newName is non-empty, it overrides the workspace name and slug. |
| 210 | func (s *Store) ImportWorkspace(data *models.WorkspaceExport, newName string, ownerID string) (*models.Workspace, error) { |
| 211 | if data.Version != 1 { |
| 212 | return nil, fmt.Errorf("unsupported export version: %d", data.Version) |
| 213 | } |
| 214 | |
| 215 | // Determine workspace name/slug |
| 216 | wsName := data.Workspace.Name |
| 217 | wsSlug := data.Workspace.Slug |
| 218 | if newName != "" { |
| 219 | wsName = newName |
| 220 | wsSlug = newName |
| 221 | } |
| 222 | |
| 223 | ws, err := s.CreateWorkspace(models.WorkspaceCreate{ |
| 224 | Name: wsName, |
| 225 | Slug: wsSlug, |
| 226 | Description: data.Workspace.Description, |
| 227 | Settings: data.Workspace.Settings, |
| 228 | OwnerID: ownerID, |
| 229 | }) |
| 230 | if err != nil { |
| 231 | return nil, fmt.Errorf("create workspace: %w", err) |
| 232 | } |
| 233 | |
| 234 | // Run all data inserts in a single transaction for atomicity |
| 235 | tx, err := s.db.Begin() |
| 236 | if err != nil { |
| 237 | return nil, fmt.Errorf("begin transaction: %w", err) |
| 238 | } |
| 239 | defer tx.Rollback() |
| 240 | |
| 241 | // ID mapping: old ID -> new ID |
| 242 | collMap := make(map[string]string) |
| 243 | itemMap := make(map[string]string) |
| 244 | |
| 245 | // Import collections |
| 246 | for _, c := range data.Collections { |
| 247 | newCollID := newID() |
| 248 | collMap[c.ID] = newCollID |
| 249 | |
| 250 | // Coerce empty-string / malformed settings to a valid JSON object |
| 251 | // before insert. IDEA-1484 (PR #562) hardened collections.settings |
| 252 | // to NOT NULL DEFAULT '{}', but this INSERT explicitly supplies |
| 253 | // the settings column — so the DEFAULT clause does NOT fire when |
| 254 | // c.Settings is "". Without this coercion, Postgres rejects `""` |
| 255 | // at JSONB type-validation and SQLite silently stores invalid |
| 256 | // JSON. Legacy bundles and plain-JSON workspace imports |
| 257 | // (handlers_workspaces.go, handlers_import_bundle.go, |
| 258 | // cmd/pad/main.go's migrate command) can still carry "" or |
| 259 | // malformed settings, so normalization belongs at the import |
| 260 | // boundary rather than at the schema level. IDEA-1488 extends |
| 261 | // this to log-and-coerce on non-empty malformed JSON. |
| 262 | settings := coerceJSONForImport(c.Settings, "{}", "collections.settings", c.ID, ws.ID, true) |
| 263 | |
| 264 | _, err := tx.Exec(s.q(` |
| 265 | INSERT INTO collections (id, workspace_id, name, slug, icon, description, schema, settings, prefix, sort_order, is_default, is_system, created_at, updated_at) |
| 266 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`), |
| 267 | newCollID, ws.ID, c.Name, c.Slug, c.Icon, c.Description, c.Schema, settings, c.Prefix, c.SortOrder, s.dialect.BoolToInt(c.IsDefault), s.dialect.BoolToInt(c.IsSystem), |