Skip to content

Commit 688364f

Browse files
committed
Rename to import
1 parent 0335c97 commit 688364f

File tree

6 files changed

+154
-91
lines changed

6 files changed

+154
-91
lines changed

cmd/dev_server/dev_server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func NewDevServerCmd(client resources.Client, analyticsTrackerFn analytics.Track
7272
cmd.AddCommand(NewRemoveProjectCmd(client))
7373
cmd.AddCommand(NewAddProjectCmd(client))
7474
cmd.AddCommand(NewUpdateProjectCmd(client))
75-
cmd.AddCommand(NewSeedCmd())
75+
cmd.AddCommand(NewImportProjectCmd())
7676

7777
cmd.AddGroup(&cobra.Group{ID: "overrides", Title: "Override commands:"})
7878
cmd.AddCommand(NewAddOverrideCmd(client))
Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ import (
1616
"github.com/launchdarkly/ldcli/internal/dev_server/model"
1717
)
1818

19-
const SeedFileFlag = "file"
19+
const ImportFileFlag = "file"
2020

21-
func NewSeedCmd() *cobra.Command {
21+
func NewImportProjectCmd() *cobra.Command {
2222
cmd := &cobra.Command{
2323
GroupID: "projects",
2424
Args: validators.Validate(),
25-
Long: `Seed the dev server database from a JSON file. Database must be empty.
25+
Long: `Import a project into the dev server database from a JSON file.
2626
2727
The JSON file format matches the output from:
2828
ldcli dev-server get-project --project=<key> \
@@ -33,11 +33,11 @@ Examples:
3333
ldcli dev-server get-project --project=my-project \
3434
--expand=overrides --expand=availableVariations > backup.json
3535
36-
# Later, seed a clean database from backup
37-
ldcli dev-server seed --project=my-project --file=backup.json`,
38-
RunE: seed(),
39-
Short: "seed database from file",
40-
Use: "seed",
36+
# Later, import the project from backup
37+
ldcli dev-server import-project --project=my-project --file=backup.json`,
38+
RunE: importProject(),
39+
Short: "import project from file",
40+
Use: "import-project",
4141
}
4242

4343
cmd.SetUsageTemplate(resourcescmd.SubcommandUsageTemplate())
@@ -47,19 +47,19 @@ Examples:
4747
_ = cmd.Flags().SetAnnotation(cliflags.ProjectFlag, "required", []string{"true"})
4848
_ = viper.BindPFlag(cliflags.ProjectFlag, cmd.Flags().Lookup(cliflags.ProjectFlag))
4949

50-
cmd.Flags().String(SeedFileFlag, "", "Path to JSON file containing project data")
51-
_ = cmd.MarkFlagRequired(SeedFileFlag)
52-
_ = cmd.Flags().SetAnnotation(SeedFileFlag, "required", []string{"true"})
53-
_ = viper.BindPFlag(SeedFileFlag, cmd.Flags().Lookup(SeedFileFlag))
50+
cmd.Flags().String(ImportFileFlag, "", "Path to JSON file containing project data")
51+
_ = cmd.MarkFlagRequired(ImportFileFlag)
52+
_ = cmd.Flags().SetAnnotation(ImportFileFlag, "required", []string{"true"})
53+
_ = viper.BindPFlag(ImportFileFlag, cmd.Flags().Lookup(ImportFileFlag))
5454

5555
return cmd
5656
}
5757

58-
func seed() func(*cobra.Command, []string) error {
58+
func importProject() func(*cobra.Command, []string) error {
5959
return func(cmd *cobra.Command, args []string) error {
6060
ctx := context.Background()
6161
projectKey := viper.GetString(cliflags.ProjectFlag)
62-
filepath := viper.GetString(SeedFileFlag)
62+
filepath := viper.GetString(ImportFileFlag)
6363

6464
// Get database path (same logic as dev_server.go)
6565
dbFilePath, err := xdg.StateFile("ldcli/dev_server.db")
@@ -79,11 +79,11 @@ func seed() func(*cobra.Command, []string) error {
7979
// Import project from file
8080
err = model.ImportProjectFromFile(ctx, projectKey, filepath)
8181
if err != nil {
82-
return fmt.Errorf("unable to seed database: %w", err)
82+
return fmt.Errorf("unable to import project: %w", err)
8383
}
8484

85-
log.Printf("Successfully seeded project '%s' from %s", projectKey, filepath)
86-
fmt.Fprintf(cmd.OutOrStdout(), "Successfully seeded project '%s' from %s\n", projectKey, filepath)
85+
log.Printf("Successfully imported project '%s' from %s", projectKey, filepath)
86+
fmt.Fprintf(cmd.OutOrStdout(), "Successfully imported project '%s' from %s\n", projectKey, filepath)
8787

8888
return nil
8989
}
Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ import (
1717
"github.com/launchdarkly/ldcli/internal/dev_server/model"
1818
)
1919

20-
func TestSeedCommand(t *testing.T) {
21-
t.Run("seeds database successfully from valid JSON file", func(t *testing.T) {
20+
func TestImportProjectCommand(t *testing.T) {
21+
t.Run("imports project successfully from valid JSON file", func(t *testing.T) {
2222
// Create temporary database
23-
tmpDir, err := os.MkdirTemp("", "seed-test-*")
23+
tmpDir, err := os.MkdirTemp("", "import-test-*")
2424
require.NoError(t, err)
2525
defer os.RemoveAll(tmpDir)
2626

@@ -128,9 +128,9 @@ func TestSeedCommand(t *testing.T) {
128128
assert.True(t, overrides[0].Active)
129129
})
130130

131-
t.Run("rejects seeding into non-empty database", func(t *testing.T) {
131+
t.Run("rejects importing when project already exists", func(t *testing.T) {
132132
// Create temporary database
133-
tmpDir, err := os.MkdirTemp("", "seed-test-*")
133+
tmpDir, err := os.MkdirTemp("", "import-test-*")
134134
require.NoError(t, err)
135135
defer os.RemoveAll(tmpDir)
136136

@@ -140,7 +140,7 @@ func TestSeedCommand(t *testing.T) {
140140
require.NoError(t, err)
141141
ctx = model.ContextWithStore(ctx, sqlStore)
142142

143-
// Insert an existing project
143+
// Insert an existing project with the same key
144144
existingProject := model.Project{
145145
Key: "existing-project",
146146
SourceEnvironmentKey: "test",
@@ -151,7 +151,7 @@ func TestSeedCommand(t *testing.T) {
151151
err = sqlStore.InsertProject(ctx, existingProject)
152152
require.NoError(t, err)
153153

154-
// Create seed data file
154+
// Create seed data file for the same project key
155155
seedFile := filepath.Join(tmpDir, "seed.json")
156156
seedData := map[string]interface{}{
157157
"context": map[string]interface{}{
@@ -172,14 +172,72 @@ func TestSeedCommand(t *testing.T) {
172172
err = os.WriteFile(seedFile, data, 0644)
173173
require.NoError(t, err)
174174

175-
// Attempt to seed should fail
176-
err = model.ImportProjectFromFile(ctx, "new-project", seedFile)
175+
// Attempt to import with same project key should fail
176+
err = model.ImportProjectFromFile(ctx, "existing-project", seedFile)
177177
require.Error(t, err)
178-
assert.Contains(t, err.Error(), "database not empty")
178+
assert.Contains(t, err.Error(), "already exists")
179+
})
180+
181+
t.Run("allows importing different project when database has other projects", func(t *testing.T) {
182+
// Create temporary database
183+
tmpDir, err := os.MkdirTemp("", "import-test-*")
184+
require.NoError(t, err)
185+
defer os.RemoveAll(tmpDir)
186+
187+
dbPath := filepath.Join(tmpDir, "test.db")
188+
ctx := context.Background()
189+
sqlStore, err := db.NewSqlite(ctx, dbPath)
190+
require.NoError(t, err)
191+
ctx = model.ContextWithStore(ctx, sqlStore)
192+
193+
// Insert an existing project
194+
existingProject := model.Project{
195+
Key: "project-1",
196+
SourceEnvironmentKey: "test",
197+
Context: ldcontext.NewBuilder("user").Key("existing").Build(),
198+
AllFlagsState: model.FlagsState{},
199+
AvailableVariations: []model.FlagVariation{},
200+
}
201+
err = sqlStore.InsertProject(ctx, existingProject)
202+
require.NoError(t, err)
203+
204+
// Create seed data file for a DIFFERENT project
205+
seedFile := filepath.Join(tmpDir, "seed.json")
206+
seedData := map[string]interface{}{
207+
"context": map[string]interface{}{
208+
"kind": "user",
209+
"key": "test-user",
210+
},
211+
"sourceEnvironmentKey": "production",
212+
"flagsState": map[string]interface{}{
213+
"flag-1": map[string]interface{}{
214+
"value": true,
215+
"version": 1,
216+
},
217+
},
218+
}
219+
220+
data, err := json.Marshal(seedData)
221+
require.NoError(t, err)
222+
err = os.WriteFile(seedFile, data, 0644)
223+
require.NoError(t, err)
224+
225+
// Import a different project should succeed
226+
err = model.ImportProjectFromFile(ctx, "project-2", seedFile)
227+
require.NoError(t, err)
228+
229+
// Verify both projects exist
230+
project1, err := sqlStore.GetDevProject(ctx, "project-1")
231+
require.NoError(t, err)
232+
assert.Equal(t, "project-1", project1.Key)
233+
234+
project2, err := sqlStore.GetDevProject(ctx, "project-2")
235+
require.NoError(t, err)
236+
assert.Equal(t, "project-2", project2.Key)
179237
})
180238

181-
t.Run("validates required fields in seed data", func(t *testing.T) {
182-
tmpDir, err := os.MkdirTemp("", "seed-test-*")
239+
t.Run("validates required fields in import data", func(t *testing.T) {
240+
tmpDir, err := os.MkdirTemp("", "import-test-*")
183241
require.NoError(t, err)
184242
defer os.RemoveAll(tmpDir)
185243

@@ -238,8 +296,8 @@ func TestSeedCommand(t *testing.T) {
238296
}
239297
})
240298

241-
t.Run("handles complex seed data with all fields", func(t *testing.T) {
242-
tmpDir, err := os.MkdirTemp("", "seed-test-*")
299+
t.Run("handles complex import data with all fields", func(t *testing.T) {
300+
tmpDir, err := os.MkdirTemp("", "import-test-*")
243301
require.NoError(t, err)
244302
defer os.RemoveAll(tmpDir)
245303

@@ -357,8 +415,8 @@ func TestSeedCommand(t *testing.T) {
357415
assert.Equal(t, ldvalue.Int(100), overrideMap["number-flag"].Value)
358416
})
359417

360-
t.Run("handles seed data without optional fields", func(t *testing.T) {
361-
tmpDir, err := os.MkdirTemp("", "seed-test-*")
418+
t.Run("handles import data without optional fields", func(t *testing.T) {
419+
tmpDir, err := os.MkdirTemp("", "import-test-*")
362420
require.NoError(t, err)
363421
defer os.RemoveAll(tmpDir)
364422

@@ -412,7 +470,7 @@ func TestSeedCommand(t *testing.T) {
412470
})
413471

414472
t.Run("preserves variation metadata", func(t *testing.T) {
415-
tmpDir, err := os.MkdirTemp("", "seed-test-*")
473+
tmpDir, err := os.MkdirTemp("", "import-test-*")
416474
require.NoError(t, err)
417475
defer os.RemoveAll(tmpDir)
418476

cmd/dev_server/projects.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Examples:
5858
# Get project with basic information
5959
ldcli dev-server get-project --project=my-project
6060
61-
# Get project with all data (for seeding/backup)
61+
# Get project with all data (for import/backup)
6262
ldcli dev-server get-project --project=my-project \
6363
--expand=overrides --expand=availableVariations > backup.json`,
6464
RunE: getProject(client),

internal/dev_server/model/seed.go renamed to internal/dev_server/model/import_project.go

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,50 +10,54 @@ import (
1010
"github.com/pkg/errors"
1111
)
1212

13-
// SeedData represents the JSON structure from the project endpoint
13+
// ImportData represents the JSON structure from the project endpoint
1414
// matching the format from /dev/projects/{projectKey}?expand=overrides&expand=availableVariations
15-
type SeedData struct {
16-
Context ldcontext.Context `json:"context"`
17-
SourceEnvironmentKey string `json:"sourceEnvironmentKey"`
18-
FlagsState FlagsState `json:"flagsState"`
19-
Overrides *FlagsState `json:"overrides,omitempty"`
20-
AvailableVariations *map[string][]SeedVariation `json:"availableVariations,omitempty"`
15+
type ImportData struct {
16+
Context ldcontext.Context `json:"context"`
17+
SourceEnvironmentKey string `json:"sourceEnvironmentKey"`
18+
FlagsState FlagsState `json:"flagsState"`
19+
Overrides *FlagsState `json:"overrides,omitempty"`
20+
AvailableVariations *map[string][]ImportVariation `json:"availableVariations,omitempty"`
2121
}
2222

23-
// SeedVariation represents a variation in the seed data format
24-
type SeedVariation struct {
23+
// ImportVariation represents a variation in the import data format
24+
type ImportVariation struct {
2525
Id string `json:"_id"`
2626
Name *string `json:"name,omitempty"`
2727
Description *string `json:"description,omitempty"`
2828
Value ldvalue.Value `json:"value"`
2929
}
3030

31-
// ImportProjectFromSeed imports a project from seed data into the database.
32-
// Returns an error if the database is not empty (contains any projects).
33-
func ImportProjectFromSeed(ctx context.Context, projectKey string, seedData SeedData) error {
31+
// ImportProject imports a project from import data into the database.
32+
// Returns an error if the project already exists.
33+
func ImportProject(ctx context.Context, projectKey string, importData ImportData) error {
3434
store := StoreFromContext(ctx)
3535

36-
// Validate database is empty
37-
existingKeys, err := store.GetDevProjectKeys(ctx)
36+
// Check if project already exists
37+
existingProject, err := store.GetDevProject(ctx, projectKey)
3838
if err != nil {
39-
return errors.Wrap(err, "unable to check existing projects")
40-
}
41-
if len(existingKeys) > 0 {
42-
return errors.Errorf("database not empty (found %d project(s)), seeding only allowed on clean database", len(existingKeys))
39+
// ErrNotFound is expected - it means the project doesn't exist yet, which is what we want
40+
if _, ok := err.(ErrNotFound); !ok {
41+
return errors.Wrap(err, "unable to check if project exists")
42+
}
43+
// Project doesn't exist, continue with import
44+
} else if existingProject != nil {
45+
// Project exists, cannot import
46+
return errors.Errorf("project '%s' already exists, cannot import", projectKey)
4347
}
4448

45-
// Create project from seed data
49+
// Create project from import data
4650
project := Project{
4751
Key: projectKey,
48-
SourceEnvironmentKey: seedData.SourceEnvironmentKey,
49-
Context: seedData.Context,
50-
AllFlagsState: seedData.FlagsState,
52+
SourceEnvironmentKey: importData.SourceEnvironmentKey,
53+
Context: importData.Context,
54+
AllFlagsState: importData.FlagsState,
5155
AvailableVariations: []FlagVariation{},
5256
}
5357

5458
// Convert available variations if present
55-
if seedData.AvailableVariations != nil {
56-
for flagKey, variations := range *seedData.AvailableVariations {
59+
if importData.AvailableVariations != nil {
60+
for flagKey, variations := range *importData.AvailableVariations {
5761
for _, v := range variations {
5862
project.AvailableVariations = append(project.AvailableVariations, FlagVariation{
5963
FlagKey: flagKey,
@@ -75,8 +79,8 @@ func ImportProjectFromSeed(ctx context.Context, projectKey string, seedData Seed
7579
}
7680

7781
// Import overrides if present
78-
if seedData.Overrides != nil {
79-
for flagKey, flagState := range *seedData.Overrides {
82+
if importData.Overrides != nil {
83+
for flagKey, flagState := range *importData.Overrides {
8084
// Use store directly instead of UpsertOverride to avoid observer notifications
8185
override := Override{
8286
ProjectKey: projectKey,
@@ -104,20 +108,20 @@ func ImportProjectFromFile(ctx context.Context, projectKey, filepath string) err
104108
}
105109

106110
// Parse JSON
107-
var seedData SeedData
108-
err = json.Unmarshal(data, &seedData)
111+
var importData ImportData
112+
err = json.Unmarshal(data, &importData)
109113
if err != nil {
110114
return errors.Wrap(err, "unable to parse JSON")
111115
}
112116

113117
// Validate required fields
114-
if seedData.SourceEnvironmentKey == "" {
115-
return errors.New("sourceEnvironmentKey is required in seed data")
118+
if importData.SourceEnvironmentKey == "" {
119+
return errors.New("sourceEnvironmentKey is required in import data")
116120
}
117-
if seedData.FlagsState == nil {
118-
return errors.New("flagsState is required in seed data")
121+
if importData.FlagsState == nil {
122+
return errors.New("flagsState is required in import data")
119123
}
120124

121125
// Import the project
122-
return ImportProjectFromSeed(ctx, projectKey, seedData)
126+
return ImportProject(ctx, projectKey, importData)
123127
}

0 commit comments

Comments
 (0)