axiodb 16.2.1 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/cli/VERSION +1 -1
- package/cli/cmd/db_export.go +66 -0
- package/cli/cmd/db_import.go +84 -0
- package/cli/cmd/root.go +2 -0
- package/cli/internal/config/config.go +9 -0
- package/cli/pkg/httpclient/client.go +160 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -341,7 +341,7 @@ The rule: the `-e AXIODB_TLS_CERT_PATH=...` value must always match the *right-h
|
|
|
341
341
|
|
|
342
342
|
## 💻 AxioDB CLI — Command Line Interface
|
|
343
343
|
|
|
344
|
-
A Go-based CLI tool for interacting with AxioDB servers via the TCP protocol. Supports all database operations with both single-command and interactive REPL modes.
|
|
344
|
+
A Go-based CLI tool for interacting with AxioDB servers via the TCP protocol. Supports all database operations with both single-command and interactive REPL modes. Export and import databases via the HTTP API.
|
|
345
345
|
|
|
346
346
|
### Quick Install
|
|
347
347
|
|
|
@@ -399,14 +399,27 @@ axiodb -c axiodb://127.0.0.1:27019 -u admin -p admin connect
|
|
|
399
399
|
axiodb -c axiodb://127.0.0.1:27019 --tls --tls-cert ./cert.pem connect
|
|
400
400
|
```
|
|
401
401
|
|
|
402
|
+
**Export database (via HTTP API):**
|
|
403
|
+
```bash
|
|
404
|
+
axiodb export mydb --http-host localhost --http-port 27018 -u admin -p secret
|
|
405
|
+
# Saves mydb.tar.gz in current directory
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**Import database (via HTTP API):**
|
|
409
|
+
```bash
|
|
410
|
+
axiodb import ./backups/mydb.tar.gz --http-host localhost --http-port 27018 -u admin -p secret
|
|
411
|
+
# Tab completes .tar.gz file paths
|
|
412
|
+
```
|
|
413
|
+
|
|
402
414
|
### Features
|
|
403
415
|
|
|
404
|
-
- **All
|
|
416
|
+
- **All 23 commands:** database, collection, document CRUD, aggregation, indexing, export, import
|
|
405
417
|
- **Interactive REPL:** MongoDB shell syntax (`use`, `show dbs`, `db.coll.find()`)
|
|
418
|
+
- **Export & Import:** backup/restore databases via HTTP API, tab-completes file paths
|
|
406
419
|
- **TLS support:** `--tls`, `--tls-cert`, `--tls-skip-verify`
|
|
407
|
-
- **Auth support:** `-u` / `-p` flags
|
|
420
|
+
- **Auth support:** `-u` / `-p` flags (TCP and HTTP)
|
|
408
421
|
- **JSON output:** `--output json` for scripting
|
|
409
|
-
- **Tab autocomplete** in REPL mode
|
|
422
|
+
- **Tab autocomplete** in REPL mode and file path completion
|
|
410
423
|
|
|
411
424
|
👉 **[Full CLI Documentation](https://github.com/nexoral/AxioDB/tree/main/cli)**
|
|
412
425
|
|
package/cli/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
17.0.0
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
|
|
7
|
+
"github.com/nexoral/axiodb-cli/internal/config"
|
|
8
|
+
"github.com/spf13/cobra"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
var exportCmd = &cobra.Command{
|
|
12
|
+
Use: "export <dbname>",
|
|
13
|
+
Short: "Export a database to a .tar.gz file via HTTP",
|
|
14
|
+
Long: "Export an AxioDB database to a .tar.gz archive. Uses the HTTP API (port 27018), not TCP.",
|
|
15
|
+
Args: cobra.ExactArgs(1),
|
|
16
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
17
|
+
dbName := args[0]
|
|
18
|
+
cfg, err := config.FromFlags(cmd)
|
|
19
|
+
if err != nil {
|
|
20
|
+
return err
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if cfg.Username == "" || cfg.Password == "" {
|
|
24
|
+
return fmt.Errorf("username and password required for HTTP auth (-u, -p)")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
client := cfg.NewHTTPClient()
|
|
28
|
+
|
|
29
|
+
fmt.Fprintf(os.Stderr, "Logging in to %s:%d...\n", cfg.HTTPHost, cfg.HTTPPort)
|
|
30
|
+
if err := client.Login(cfg.Username, cfg.Password); err != nil {
|
|
31
|
+
return err
|
|
32
|
+
}
|
|
33
|
+
defer client.Logout()
|
|
34
|
+
|
|
35
|
+
fmt.Fprintf(os.Stderr, "Exporting %s...\n", dbName)
|
|
36
|
+
filename, size, err := client.Export(dbName)
|
|
37
|
+
if err != nil {
|
|
38
|
+
return err
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fmt.Fprintf(os.Stderr, "Exported %s (%s)\n", filename, formatBytes(size))
|
|
42
|
+
return nil
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func formatBytes(bytes int64) string {
|
|
47
|
+
const (
|
|
48
|
+
KB = 1024
|
|
49
|
+
MB = 1024 * KB
|
|
50
|
+
GB = 1024 * MB
|
|
51
|
+
)
|
|
52
|
+
switch {
|
|
53
|
+
case bytes >= GB:
|
|
54
|
+
return fmt.Sprintf("%.1f GB", float64(bytes)/float64(GB))
|
|
55
|
+
case bytes >= MB:
|
|
56
|
+
return fmt.Sprintf("%.1f MB", float64(bytes)/float64(MB))
|
|
57
|
+
case bytes >= KB:
|
|
58
|
+
return fmt.Sprintf("%.1f KB", float64(bytes)/float64(KB))
|
|
59
|
+
default:
|
|
60
|
+
return fmt.Sprintf("%d B", bytes)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func init() {
|
|
65
|
+
rootCmd.AddCommand(exportCmd)
|
|
66
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"github.com/nexoral/axiodb-cli/internal/config"
|
|
10
|
+
"github.com/spf13/cobra"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
var importCmd = &cobra.Command{
|
|
14
|
+
Use: "import <file>",
|
|
15
|
+
Short: "Import a database from a .tar.gz file via HTTP",
|
|
16
|
+
Long: "Import an AxioDB database from a .tar.gz archive. Uses the HTTP API (port 27018), not TCP.",
|
|
17
|
+
Args: cobra.ExactArgs(1),
|
|
18
|
+
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
19
|
+
dir := "."
|
|
20
|
+
prefix := toComplete
|
|
21
|
+
|
|
22
|
+
if idx := strings.LastIndex(toComplete, "/"); idx >= 0 {
|
|
23
|
+
dir = toComplete[:idx+1]
|
|
24
|
+
prefix = toComplete[idx+1:]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
entries, err := os.ReadDir(dir)
|
|
28
|
+
if err != nil {
|
|
29
|
+
return nil, cobra.ShellCompDirectiveDefault
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var completions []string
|
|
33
|
+
for _, entry := range entries {
|
|
34
|
+
if entry.IsDir() {
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
name := entry.Name()
|
|
38
|
+
if strings.HasPrefix(name, prefix) && strings.HasSuffix(name, ".tar.gz") {
|
|
39
|
+
completions = append(completions, filepath.Join(dir, name))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return completions, cobra.ShellCompDirectiveNoFileComp
|
|
43
|
+
},
|
|
44
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
45
|
+
filePath := args[0]
|
|
46
|
+
cfg, err := config.FromFlags(cmd)
|
|
47
|
+
if err != nil {
|
|
48
|
+
return err
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if cfg.Username == "" || cfg.Password == "" {
|
|
52
|
+
return fmt.Errorf("username and password required for HTTP auth (-u, -p)")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
info, err := os.Stat(filePath)
|
|
56
|
+
if err != nil {
|
|
57
|
+
return fmt.Errorf("file not found: %s", filePath)
|
|
58
|
+
}
|
|
59
|
+
if info.IsDir() {
|
|
60
|
+
return fmt.Errorf("not a file: %s", filePath)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
client := cfg.NewHTTPClient()
|
|
64
|
+
|
|
65
|
+
fmt.Fprintf(os.Stderr, "Logging in to %s:%d...\n", cfg.HTTPHost, cfg.HTTPPort)
|
|
66
|
+
if err := client.Login(cfg.Username, cfg.Password); err != nil {
|
|
67
|
+
return err
|
|
68
|
+
}
|
|
69
|
+
defer client.Logout()
|
|
70
|
+
|
|
71
|
+
fmt.Fprintf(os.Stderr, "Importing %s...\n", filepath.Base(filePath))
|
|
72
|
+
dbName, err := client.Import(filePath)
|
|
73
|
+
if err != nil {
|
|
74
|
+
return err
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fmt.Fprintf(os.Stderr, "Imported database: %s\n", dbName)
|
|
78
|
+
return nil
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func init() {
|
|
83
|
+
rootCmd.AddCommand(importCmd)
|
|
84
|
+
}
|
package/cli/cmd/root.go
CHANGED
|
@@ -33,4 +33,6 @@ func init() {
|
|
|
33
33
|
rootCmd.PersistentFlags().Int("timeout", 30, "Request timeout in seconds")
|
|
34
34
|
rootCmd.PersistentFlags().String("db", "", "Database name")
|
|
35
35
|
rootCmd.PersistentFlags().String("collection", "", "Collection name")
|
|
36
|
+
rootCmd.PersistentFlags().String("http-host", "localhost", "HTTP server host (for export/import)")
|
|
37
|
+
rootCmd.PersistentFlags().Int("http-port", 27018, "HTTP server port (for export/import)")
|
|
36
38
|
}
|
|
@@ -6,6 +6,7 @@ import (
|
|
|
6
6
|
"strings"
|
|
7
7
|
"time"
|
|
8
8
|
|
|
9
|
+
"github.com/nexoral/axiodb-cli/pkg/httpclient"
|
|
9
10
|
"github.com/nexoral/axiodb-cli/pkg/protocol"
|
|
10
11
|
"github.com/spf13/cobra"
|
|
11
12
|
)
|
|
@@ -23,6 +24,8 @@ type Config struct {
|
|
|
23
24
|
Timeout int
|
|
24
25
|
DB string
|
|
25
26
|
Collection string
|
|
27
|
+
HTTPHost string
|
|
28
|
+
HTTPPort int
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
func FromFlags(cmd *cobra.Command) (*Config, error) {
|
|
@@ -40,6 +43,8 @@ func FromFlags(cmd *cobra.Command) (*Config, error) {
|
|
|
40
43
|
cfg.Timeout, _ = cmd.Flags().GetInt("timeout")
|
|
41
44
|
cfg.DB, _ = cmd.Flags().GetString("db")
|
|
42
45
|
cfg.Collection, _ = cmd.Flags().GetString("collection")
|
|
46
|
+
cfg.HTTPHost, _ = cmd.Flags().GetString("http-host")
|
|
47
|
+
cfg.HTTPPort, _ = cmd.Flags().GetInt("http-port")
|
|
43
48
|
|
|
44
49
|
if cfg.ConnString != "" {
|
|
45
50
|
host, port, err := parseConnectionString(cfg.ConnString)
|
|
@@ -109,3 +114,7 @@ func GetCollectionContext(cmd *cobra.Command) string {
|
|
|
109
114
|
coll, _ := cmd.Flags().GetString("collection")
|
|
110
115
|
return coll
|
|
111
116
|
}
|
|
117
|
+
|
|
118
|
+
func (c *Config) NewHTTPClient() *httpclient.HTTPClient {
|
|
119
|
+
return httpclient.New(c.HTTPHost, c.HTTPPort, time.Duration(c.Timeout)*time.Second)
|
|
120
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
package httpclient
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"mime/multipart"
|
|
9
|
+
"net/http"
|
|
10
|
+
"net/http/cookiejar"
|
|
11
|
+
"os"
|
|
12
|
+
"path/filepath"
|
|
13
|
+
"strings"
|
|
14
|
+
"time"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
type HTTPClient struct {
|
|
18
|
+
baseURL string
|
|
19
|
+
httpClient *http.Client
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func New(host string, port int, timeout time.Duration) *HTTPClient {
|
|
23
|
+
jar, _ := cookiejar.New(nil)
|
|
24
|
+
return &HTTPClient{
|
|
25
|
+
baseURL: fmt.Sprintf("http://%s:%d", host, port),
|
|
26
|
+
httpClient: &http.Client{
|
|
27
|
+
Timeout: timeout,
|
|
28
|
+
Jar: jar,
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func (c *HTTPClient) Login(username, password string) error {
|
|
34
|
+
body, _ := json.Marshal(map[string]string{
|
|
35
|
+
"username": username,
|
|
36
|
+
"password": password,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
resp, err := c.httpClient.Post(c.baseURL+"/api/auth/login", "application/json", bytes.NewReader(body))
|
|
40
|
+
if err != nil {
|
|
41
|
+
return fmt.Errorf("login request failed: %w", err)
|
|
42
|
+
}
|
|
43
|
+
defer resp.Body.Close()
|
|
44
|
+
|
|
45
|
+
if resp.StatusCode == http.StatusOK {
|
|
46
|
+
return nil
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var errResp struct {
|
|
50
|
+
Message string `json:"message"`
|
|
51
|
+
}
|
|
52
|
+
json.NewDecoder(resp.Body).Decode(&errResp)
|
|
53
|
+
msg := errResp.Message
|
|
54
|
+
if msg == "" {
|
|
55
|
+
msg = resp.Status
|
|
56
|
+
}
|
|
57
|
+
return fmt.Errorf("login failed: %s", msg)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func (c *HTTPClient) Logout() error {
|
|
61
|
+
resp, err := c.httpClient.Post(c.baseURL+"/api/auth/logout", "application/json", nil)
|
|
62
|
+
if err != nil {
|
|
63
|
+
return err
|
|
64
|
+
}
|
|
65
|
+
defer resp.Body.Close()
|
|
66
|
+
return nil
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func (c *HTTPClient) Export(dbName string) (string, int64, error) {
|
|
70
|
+
url := fmt.Sprintf("%s/api/db/export-database/?dbName=%s", c.baseURL, dbName)
|
|
71
|
+
resp, err := c.httpClient.Get(url)
|
|
72
|
+
if err != nil {
|
|
73
|
+
return "", 0, fmt.Errorf("export request failed: %w", err)
|
|
74
|
+
}
|
|
75
|
+
defer resp.Body.Close()
|
|
76
|
+
|
|
77
|
+
if resp.StatusCode != http.StatusOK {
|
|
78
|
+
var errResp struct {
|
|
79
|
+
Message string `json:"message"`
|
|
80
|
+
}
|
|
81
|
+
json.NewDecoder(resp.Body).Decode(&errResp)
|
|
82
|
+
msg := errResp.Message
|
|
83
|
+
if msg == "" {
|
|
84
|
+
msg = resp.Status
|
|
85
|
+
}
|
|
86
|
+
return "", 0, fmt.Errorf("export failed: %s", msg)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
filename := dbName + ".tar.gz"
|
|
90
|
+
if cd := resp.Header.Get("Content-Disposition"); cd != "" {
|
|
91
|
+
if idx := strings.Index(cd, "filename="); idx >= 0 {
|
|
92
|
+
name := cd[idx+9:]
|
|
93
|
+
name = strings.Trim(name, "\"")
|
|
94
|
+
if name != "" {
|
|
95
|
+
filename = filepath.Base(name)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
out, err := os.Create(filename)
|
|
101
|
+
if err != nil {
|
|
102
|
+
return "", 0, fmt.Errorf("create file: %w", err)
|
|
103
|
+
}
|
|
104
|
+
defer out.Close()
|
|
105
|
+
|
|
106
|
+
written, err := io.Copy(out, resp.Body)
|
|
107
|
+
if err != nil {
|
|
108
|
+
os.Remove(filename)
|
|
109
|
+
return "", 0, fmt.Errorf("write file: %w", err)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return filename, written, nil
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func (c *HTTPClient) Import(filePath string) (string, error) {
|
|
116
|
+
file, err := os.Open(filePath)
|
|
117
|
+
if err != nil {
|
|
118
|
+
return "", fmt.Errorf("open file: %w", err)
|
|
119
|
+
}
|
|
120
|
+
defer file.Close()
|
|
121
|
+
|
|
122
|
+
var body bytes.Buffer
|
|
123
|
+
writer := multipart.NewWriter(&body)
|
|
124
|
+
part, err := writer.CreateFormFile("file", filepath.Base(filePath))
|
|
125
|
+
if err != nil {
|
|
126
|
+
return "", fmt.Errorf("create form file: %w", err)
|
|
127
|
+
}
|
|
128
|
+
if _, err := io.Copy(part, file); err != nil {
|
|
129
|
+
return "", fmt.Errorf("write form data: %w", err)
|
|
130
|
+
}
|
|
131
|
+
writer.Close()
|
|
132
|
+
|
|
133
|
+
resp, err := c.httpClient.Post(
|
|
134
|
+
c.baseURL+"/api/db/import-database/",
|
|
135
|
+
writer.FormDataContentType(),
|
|
136
|
+
&body,
|
|
137
|
+
)
|
|
138
|
+
if err != nil {
|
|
139
|
+
return "", fmt.Errorf("import request failed: %w", err)
|
|
140
|
+
}
|
|
141
|
+
defer resp.Body.Close()
|
|
142
|
+
|
|
143
|
+
var result struct {
|
|
144
|
+
Message string `json:"message"`
|
|
145
|
+
Database string `json:"database"`
|
|
146
|
+
}
|
|
147
|
+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
148
|
+
return "", fmt.Errorf("parse response: %w", err)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if resp.StatusCode != http.StatusOK {
|
|
152
|
+
msg := result.Message
|
|
153
|
+
if msg == "" {
|
|
154
|
+
msg = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
|
155
|
+
}
|
|
156
|
+
return "", fmt.Errorf("import failed: %s", msg)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result.Database, nil
|
|
160
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "axiodb",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "17.0.0",
|
|
4
4
|
"description": "The Pure JavaScript Alternative to SQLite. Embedded NoSQL database for Node.js with MongoDB-style queries, zero native dependencies, built-in InMemoryCache, and web GUI. Perfect for desktop apps, CLI tools, and embedded systems. No compilation, no platform issues—pure JavaScript from npm install to production.",
|
|
5
5
|
"main": "./lib/config/DB.js",
|
|
6
6
|
"types": "./lib/config/DB.d.ts",
|