Skip to content

Commit 0429413

Browse files
authored
Merge pull request #119 from jchen042/feat/IMP-329-aura-cli-import
feat(import): Aura cli for data import
2 parents 2763ed6 + 7f4bb5e commit 0429413

File tree

12 files changed

+1268
-11
lines changed

12 files changed

+1268
-11
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: Added
2+
body: Aura cli for data import
3+
time: 2025-09-12T14:57:19.93387+01:00

neo4j-cli/aura/aura.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package aura
22

33
import (
44
"github.com/neo4j/cli/neo4j-cli/aura/internal/subcommands/graphanalytics"
5+
_import "github.com/neo4j/cli/neo4j-cli/aura/internal/subcommands/import"
56
"github.com/spf13/cobra"
67

78
"github.com/neo4j/cli/common/clicfg"
@@ -28,6 +29,7 @@ func NewCmd(cfg *clicfg.Config) *cobra.Command {
2829
cmd.AddCommand(graphanalytics.NewCmd(cfg))
2930
if cfg.Aura.AuraBetaEnabled() {
3031
cmd.AddCommand(dataapi.NewCmd(cfg))
32+
cmd.AddCommand(_import.NewCmd(cfg))
3133
}
3234

3335
return cmd

neo4j-cli/aura/internal/api/response.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ func NewSingleValueResponseData(data map[string]any) ResponseData {
300300
}
301301
}
302302

303-
func NewResponseData(data []map[string]any) ResponseData {
303+
func NewListResponseData(data []map[string]any) ResponseData {
304304
return ListResponseData{
305305
Data: data,
306306
}

neo4j-cli/aura/internal/output/output.go

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package output
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"reflect"
7+
"strings"
68

79
"github.com/jedib0t/go-pretty/v6/table"
810
"github.com/spf13/cobra"
@@ -38,6 +40,27 @@ func PrintBody(cmd *cobra.Command, cfg *clicfg.Config, body []byte, fields []str
3840
PrintBodyMap(cmd, cfg, values, fields)
3941
}
4042

43+
func getNestedField(v map[string]any, subFields []string) string {
44+
if len(subFields) == 1 {
45+
value := v[subFields[0]]
46+
if value == nil {
47+
return ""
48+
}
49+
if reflect.TypeOf(value).Kind() == reflect.Slice {
50+
marshaledSlice, _ := json.MarshalIndent(value, "", " ")
51+
return string(marshaledSlice)
52+
}
53+
return fmt.Sprintf("%+v", value)
54+
}
55+
switch val := v[subFields[0]].(type) {
56+
case map[string]any:
57+
return getNestedField(val, subFields[1:])
58+
default:
59+
//The field is no longer nested, so we can't proceed in the next level
60+
return ""
61+
}
62+
}
63+
4164
func printTable(cmd *cobra.Command, responseData api.ResponseData, fields []string) {
4265
t := table.NewWriter()
4366

@@ -50,16 +73,8 @@ func printTable(cmd *cobra.Command, responseData api.ResponseData, fields []stri
5073
for _, v := range responseData.AsArray() {
5174
row := table.Row{}
5275
for _, f := range fields {
53-
formattedValue := v[f]
54-
55-
if v[f] == nil {
56-
formattedValue = ""
57-
}
58-
59-
if reflect.TypeOf(formattedValue).Kind() == reflect.Slice {
60-
marshaledSlice, _ := json.MarshalIndent(formattedValue, "", " ")
61-
formattedValue = string(marshaledSlice)
62-
}
76+
subfields := strings.Split(f, ":")
77+
formattedValue := getNestedField(v, subfields)
6378

6479
row = append(row, formattedValue)
6580
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package _import
2+
3+
import (
4+
"fmt"
5+
"github.com/neo4j/cli/common/clicfg"
6+
"github.com/neo4j/cli/common/clierr"
7+
"github.com/neo4j/cli/neo4j-cli/aura/internal/subcommands/import/job"
8+
"github.com/spf13/cobra"
9+
"strings"
10+
)
11+
12+
func NewCmd(cfg *clicfg.Config) *cobra.Command {
13+
var cmd = &cobra.Command{
14+
Use: "import",
15+
Short: "Allows you to import your data into Aura instances and manage your import job",
16+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
17+
outputValue := cmd.Flags().Lookup("output").Value.String()
18+
if outputValue != "" {
19+
validOutputValue := false
20+
for _, v := range clicfg.ValidOutputValues {
21+
if v == outputValue {
22+
validOutputValue = true
23+
break
24+
}
25+
}
26+
if !validOutputValue {
27+
return clierr.NewUsageError("invalid output value specified: %s", outputValue)
28+
}
29+
}
30+
31+
cfg.Aura.BindOutput(cmd.Flags().Lookup("output"))
32+
return nil
33+
},
34+
}
35+
36+
cmd.AddCommand(job.NewCmd(cfg))
37+
cmd.PersistentFlags().String("output", "", fmt.Sprintf("Format to print console output in, from a choice of [%s]", strings.Join(clicfg.ValidOutputValues[:], ", ")))
38+
39+
return cmd
40+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package job
2+
3+
import (
4+
"fmt"
5+
"github.com/neo4j/cli/common/clicfg"
6+
"github.com/neo4j/cli/neo4j-cli/aura/internal/api"
7+
"github.com/neo4j/cli/neo4j-cli/aura/internal/output"
8+
"github.com/spf13/cobra"
9+
"log"
10+
"net/http"
11+
)
12+
13+
func NewCancelCommand(cfg *clicfg.Config) *cobra.Command {
14+
var (
15+
organizationId string
16+
projectId string
17+
jobId string
18+
)
19+
20+
const (
21+
organizationIdFlag = "organization-id"
22+
projectIdFlag = "project-id"
23+
)
24+
25+
cmd := &cobra.Command{
26+
Use: "cancel <id>",
27+
Short: "Cancel a job by id",
28+
Args: cobra.ExactArgs(1),
29+
RunE: func(cmd *cobra.Command, args []string) error {
30+
jobId = args[0]
31+
path := fmt.Sprintf("/organizations/%s/projects/%s/import/jobs/%s/cancellation", organizationId, projectId, jobId)
32+
resBody, statusCode, err := api.MakeRequest(cfg, path, &api.RequestConfig{
33+
Method: http.MethodPost,
34+
Version: api.AuraApiVersion2,
35+
})
36+
if err != nil || statusCode != http.StatusOK {
37+
return err
38+
}
39+
output.PrintBody(cmd, cfg, resBody, []string{"id"})
40+
41+
return nil
42+
},
43+
}
44+
cmd.Flags().StringVar(&organizationId, organizationIdFlag, "", "(required) Organization ID")
45+
cmd.Flags().StringVar(&projectId, projectIdFlag, "", "(required) Project/tenant ID")
46+
err := cmd.MarkFlagRequired(organizationIdFlag)
47+
if err != nil {
48+
log.Fatal(err)
49+
}
50+
err = cmd.MarkFlagRequired(projectIdFlag)
51+
if err != nil {
52+
log.Fatal(err)
53+
}
54+
55+
return cmd
56+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package job_test
2+
3+
import (
4+
"fmt"
5+
"github.com/neo4j/cli/neo4j-cli/aura/internal/test/testutils"
6+
"net/http"
7+
"testing"
8+
)
9+
10+
func TestCancelImportJob(t *testing.T) {
11+
organizationId := "f607bebe-0cc0-4166-b60c-b4eed69ee7ee"
12+
projectId := "f607bebe-0cc0-4166-b60c-b4eed69ee7ee"
13+
jobId := "87d485b4-73fc-4a7f-bb03-720f4672947e"
14+
15+
helper := testutils.NewAuraTestHelper(t)
16+
defer helper.Close()
17+
18+
mockHandler := helper.NewRequestHandlerMock(fmt.Sprintf("/v2beta1/organizations/%s/projects/%s/import/jobs/%s/cancellation", organizationId, projectId, jobId), http.StatusOK, fmt.Sprintf(`
19+
{
20+
"data": {"id": "%s"}
21+
}
22+
`, jobId))
23+
24+
helper.SetConfigValue("aura.beta-enabled", true)
25+
26+
helper.ExecuteCommand(fmt.Sprintf("import job cancel --organization-id=%s --project-id=%s %s", organizationId, projectId, jobId))
27+
28+
mockHandler.AssertCalledTimes(1)
29+
mockHandler.AssertCalledWithMethod(http.MethodPost)
30+
31+
helper.AssertErr("")
32+
helper.AssertOutJson(fmt.Sprintf(`
33+
{
34+
"data": {"id": "%s"}
35+
}
36+
`, jobId))
37+
}
38+
39+
func TestCancelImportJobError(t *testing.T) {
40+
organizationId := "f607bebe-0cc0-4166-b60c-b4eed69ee7ee"
41+
projectId := "f607bebe-0cc0-4166-b60c-b4eed69ee7ee"
42+
jobId := "87d485b4-73fc-4a7f-bb03-720f4672947e"
43+
44+
testCases := map[string]struct {
45+
executeCommand string
46+
statusCode int
47+
expectedCalledTimes int
48+
expectedError string
49+
returnBody string
50+
}{
51+
"correct command with error response 1": {
52+
executeCommand: fmt.Sprintf("import job cancel --organization-id=%s --project-id=%s %s", organizationId, projectId, jobId),
53+
statusCode: http.StatusBadRequest,
54+
expectedCalledTimes: 1,
55+
expectedError: "Error: [The job 87d485b4-73fc-4a7f-bb03-720f4672947e has requested to cancel]",
56+
returnBody: `{
57+
"errors": [
58+
{
59+
"message": "The job 87d485b4-73fc-4a7f-bb03-720f4672947e has requested to cancel",
60+
"reason": "Requested"
61+
}
62+
]
63+
}`,
64+
},
65+
"correct command with error response 2": {
66+
executeCommand: fmt.Sprintf("import job cancel --organization-id=%s --project-id=%s %s", organizationId, projectId, jobId),
67+
statusCode: http.StatusMethodNotAllowed,
68+
expectedCalledTimes: 1,
69+
expectedError: "Error: [string]",
70+
returnBody: `{
71+
"errors": [
72+
{
73+
"message": "string",
74+
"reason": "string",
75+
"field": "string"
76+
}
77+
]
78+
}`,
79+
},
80+
"incorrect command with missing organization id": {
81+
executeCommand: fmt.Sprintf("import job cancel --project-id=%s %s", projectId, jobId),
82+
statusCode: http.StatusBadRequest,
83+
expectedCalledTimes: 0,
84+
expectedError: "Error: required flag(s) \"organization-id\" not set",
85+
returnBody: ``,
86+
},
87+
"incorrect command with missing project id": {
88+
executeCommand: fmt.Sprintf("import job cancel --organization-id=%s %s", organizationId, jobId),
89+
statusCode: http.StatusBadRequest,
90+
expectedCalledTimes: 0,
91+
expectedError: "Error: required flag(s) \"project-id\" not set",
92+
returnBody: ``,
93+
},
94+
"incorrect command with missing job id": {
95+
executeCommand: fmt.Sprintf("import job cancel --organization-id=%s --project-id=%s", organizationId, projectId),
96+
statusCode: http.StatusBadRequest,
97+
expectedCalledTimes: 0,
98+
expectedError: "Error: accepts 1 arg(s), received 0",
99+
returnBody: ``,
100+
},
101+
}
102+
103+
for name, testCase := range testCases {
104+
t.Run(name, func(t *testing.T) {
105+
helper := testutils.NewAuraTestHelper(t)
106+
defer helper.Close()
107+
108+
mockHandler := helper.NewRequestHandlerMock(fmt.Sprintf("/v2beta1/organizations/%s/projects/%s/import/jobs/%s/cancellation", organizationId, projectId, jobId), testCase.statusCode, testCase.returnBody)
109+
110+
helper.SetConfigValue("aura.beta-enabled", true)
111+
112+
helper.ExecuteCommand(testCase.executeCommand)
113+
114+
mockHandler.AssertCalledTimes(testCase.expectedCalledTimes)
115+
if testCase.expectedCalledTimes > 0 {
116+
mockHandler.AssertCalledWithMethod(http.MethodPost)
117+
}
118+
119+
helper.AssertErr(testCase.expectedError)
120+
})
121+
}
122+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package job
2+
3+
import (
4+
"fmt"
5+
"github.com/neo4j/cli/common/clicfg"
6+
"github.com/neo4j/cli/neo4j-cli/aura/internal/api"
7+
"github.com/neo4j/cli/neo4j-cli/aura/internal/output"
8+
"github.com/spf13/cobra"
9+
"log"
10+
"net/http"
11+
)
12+
13+
func NewCreateCmd(cfg *clicfg.Config) *cobra.Command {
14+
var (
15+
organizationId string
16+
projectId string
17+
importModelId string
18+
auraDbId string
19+
user string
20+
password string
21+
)
22+
23+
const (
24+
organizationIdFlag = "organization-id"
25+
projectIdFlag = "project-id"
26+
importModelIdFlag = "import-model-id"
27+
dbIdFlag = "db-id"
28+
userFlag = "user"
29+
passwordFlag = "password"
30+
)
31+
cmd := &cobra.Command{
32+
Use: "create",
33+
Short: "Allows you to create a new import job",
34+
RunE: func(cmd *cobra.Command, args []string) error {
35+
path := fmt.Sprintf("/organizations/%s/projects/%s/import/jobs", organizationId, projectId)
36+
37+
responseBody, statusCode, err := api.MakeRequest(cfg, path, &api.RequestConfig{
38+
Method: http.MethodPost,
39+
Version: api.AuraApiVersion2,
40+
PostBody: map[string]any{
41+
"importModelId": importModelId,
42+
"auraCredentials": map[string]any{
43+
"dbId": auraDbId,
44+
"user": user,
45+
"password": password,
46+
},
47+
},
48+
})
49+
if err != nil || statusCode != 201 {
50+
return err
51+
}
52+
output.PrintBody(cmd, cfg, responseBody, []string{"id"})
53+
return nil
54+
},
55+
}
56+
57+
cmd.Flags().StringVar(&organizationId, organizationIdFlag, "", "(required) Sets the organization ID the job belongs to")
58+
cmd.Flags().StringVar(&projectId, projectIdFlag, "", "(required) Project/Tenant ID")
59+
cmd.Flags().StringVar(&importModelId, importModelIdFlag, "", "(required) The model ID can be found in the URL as such console-preview.neo4j.io/tools/import/model/<model ID>.")
60+
cmd.Flags().StringVar(&auraDbId, dbIdFlag, "", "(required) Aura database ID to import data into. Currently, it's the same as Aura instance ID. In the future, instance ID and database ID are different")
61+
cmd.Flags().StringVar(&user, userFlag, "", "Username to use for authentication")
62+
cmd.Flags().StringVar(&password, passwordFlag, "", "Password to use for authentication")
63+
err := cmd.MarkFlagRequired(organizationIdFlag)
64+
if err != nil {
65+
log.Fatal(err)
66+
}
67+
err = cmd.MarkFlagRequired(projectIdFlag)
68+
if err != nil {
69+
log.Fatal(err)
70+
}
71+
err = cmd.MarkFlagRequired(importModelIdFlag)
72+
if err != nil {
73+
log.Fatal(err)
74+
}
75+
err = cmd.MarkFlagRequired(dbIdFlag)
76+
if err != nil {
77+
log.Fatal(err)
78+
}
79+
80+
return cmd
81+
}

0 commit comments

Comments
 (0)