Skip to content

Commit

Permalink
Merge pull request #6 from Piitschy/dev
Browse files Browse the repository at this point in the history
More README, tests, and npm-support
  • Loading branch information
Piitschy authored Jul 6, 2024
2 parents 43e0f2b + 62b9169 commit 7b800a7
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 23 deletions.
File renamed without changes.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
*.dll
*.so
*.dylib
*/node_modules/*
node_modules/*
node_modules
package-lock.json

# Test binary, built with `go test -c`
*.test
Expand All @@ -19,3 +23,5 @@

# Go workspace file
go.work

dist/
50 changes: 50 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com

# The lines below are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/need to use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj

version: 2

before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...

builds:
- env:
- CGO_ENABLED=0
binary: drcts
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm64

archives:
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
format: zip

changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
# Directus Data Model CLI
Directus Data Model CLI (drcts) to migrate schemas from one instance to an other.
[Directus](https://directus.io) Data Model CLI (drcts) to migrate schemas from one instance to an other.


![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/Piitschy/drcts)
![GitHub Release](https://img.shields.io/github/v/release/Piitschy/drcts)
![GitHub Release Date](https://img.shields.io/github/release-date/Piitschy/drcts)
![GitHub branch check runs](https://img.shields.io/github/check-runs/Piitschy/drcts/main)


It is a Go implementation of the [Directus Migration](https://docs.directus.io/guides/migration/node.html), allowing you to:
- migrate schemas from one instance to another
- save schemas to files
- restore schemas from files
- save schema differences

It is planned to be extended with additional features in the future:
- selective migration
- creation of custom collection presets
- data migration

For complete backup and restore functionality, see [Postgres Migration](https://github.com/Piitschy/pgrd).

## Installation

```bash
Expand All @@ -15,13 +29,39 @@ go install github.com/Piitschy/drcts@latest

## Usage

### Authenticate

You can authenticate with the Directus API by providing the URL with a token or by login with a email and password.
The token is the primary way to authenticate with the Directus API, so if set, the email and password will be ignored.

- `--bu` or `--base-url` - Base URL of the Directus instance.
- `--bt` or `--base-token` - Base token of the Directus instance.
- `--be` or `--base-email` - Base email of the Directus instance.
- `--bp` or `--base-password` - Base password of the Directus instance.

Same for the target instance:
- `--tu` or `--target-url` - Target URL of the Directus instance.
- `--tt` or `--target-token` - Target token of the Directus instance.
- `--te` or `--target-email` - Target email of the Directus instance.
- `--tp` or `--target-password` - Target password of the Directus instance.

In the following examples, we will use the `--bu` and `--bt` flags to authenticate with the Directus API.
You can also use the `--be` and `--bp` flags to authenticate with the Directus API by providing an email and password.

> You can get the token from the Directus instance by going to the account settings and creating a new token.
> I recommend creating a new role and account with only the necessary permissions for the migration.
>
> IMPORTANT: Only collections that are readable by the directus user can be migrated. On the target system, the user must have the appropriate permissions to create collections!
### Migrate

```bash
drcts --bu <base-url> --bt <base-token> --tu <target-url> --tt <target-token> migrate
```
or just run `drcts migrate` and follow the instructions.

> Don't forget to set `-y` flag in scripts to skip the confirmation prompt.
### Export

To export the schema of a Directus instance to a file, run:
Expand Down
10 changes: 10 additions & 0 deletions cmd/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/Piitschy/drcts/internal/dialogs"
"github.com/Piitschy/drcts/internal/directus"
"github.com/urfave/cli/v2"
)

Expand All @@ -18,8 +19,17 @@ func Migrate(cCtx *cli.Context) error {

verbose(cCtx, "Getting snapshot from base instance...")
s, err := dBase.GetSnapshot()
if err != nil {
return err
}
verbose(cCtx, "Getting diff from target instance...")
diff, err := dTarget.GetDiff(s, force)
if err == directus.DiffErr400 && !force {
return fmt.Errorf("%s Try to use --force to ignore this error", err.Error())
}
if err != nil {
return err
}

if diff == nil {
verbose(cCtx, "No changes detected")
Expand Down
85 changes: 82 additions & 3 deletions internal/dialogs/directus-instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dialogs

import (
"fmt"
"net/mail"
"strings"

"github.com/Piitschy/drcts/internal/directus"
Expand Down Expand Up @@ -30,8 +31,7 @@ func DirectusInstance(cCtx *cli.Context, prefix string) (*directus.Directus, err
fmt.Printf("Invalid URL %v\n", err)
continue
}
err = d.TestConnection()
if err != nil {
if err = d.TestConnection(); err != nil {
fmt.Printf("Connection failed %v\n", err)
continue
}
Expand All @@ -43,7 +43,30 @@ func DirectusInstance(cCtx *cli.Context, prefix string) (*directus.Directus, err
}

token := cCtx.String(prefix + "-token")
if token == "" {
email := cCtx.String(prefix + "-email")
password := cCtx.String(prefix + "-password")
method := ""

if token != "" {
method = "Token"
} else if email != "" || password != "" {
method = "Email/Password"
}

if email == "" && password == "" && token == "" {
methodPrompt := promptui.Select{
Label: "Choose authentication method",
Items: []string{"Token", "Email/Password"},
}
var err error
_, method, err = methodPrompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return nil, err
}
}

if token == "" && method == "Token" {
prompt := promptui.Prompt{
Label: "Token of the " + prefix + " Directus instance",
Mask: '*',
Expand All @@ -57,11 +80,45 @@ func DirectusInstance(cCtx *cli.Context, prefix string) (*directus.Directus, err
token = result
}

if email == "" && method == "Email/Password" {
prompt := promptui.Prompt{
Label: "Email of the " + prefix + " Directus instance",
Validate: validateEmail,
}
result, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return nil, err
}
email = result
}

if password == "" && method == "Email/Password" {
prompt := promptui.Prompt{
Label: "Password of the " + prefix + " Directus instance",
Mask: '*',
Validate: validatePassword,
}
result, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return nil, err
}
password = result
}

d, err := directus.NewDirectus(url, token)
if err != nil {
fmt.Printf("Failed to create Directus instance %v\n", err)
return nil, err
}
if method == "Email/Password" {
err = d.Login(email, password)
if err != nil {
fmt.Printf("Login failed %v\n", err)
return nil, err
}
}
err = d.TestConnection()
if err != nil {
fmt.Printf("Connection failed %v\n", err)
Expand All @@ -86,3 +143,25 @@ func validateToken(input string) error {
}
return nil
}

func validateEmail(input string) error {
if len(input) == 0 {
return fmt.Errorf("Email cannot be empty")
}

if !strings.Contains(input, "@") {
return fmt.Errorf("Email must contain an @")
}
_, err := mail.ParseAddress(input)
if err != nil {
return fmt.Errorf("Invalid email address")
}
return nil
}

func validatePassword(input string) error {
if len(input) == 0 {
return fmt.Errorf("Password cannot be empty")
}
return nil
}
6 changes: 6 additions & 0 deletions internal/directus/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"net/http"
)

var DiffErr400 = fmt.Errorf("Failed with status code 400. Maybe the Directus versions does'nt match.")

type Diff struct {
Hash string `json:"hash"`
Diff Difference `json:"diff"`
Expand Down Expand Up @@ -69,6 +71,10 @@ func (d *Directus) GetRawDiff(s *Snapshot, format string, force bool) ([]byte, e
return nil, nil
}

if res.StatusCode == 400 {
return nil, DiffErr400
}

if res.StatusCode != 200 {
return nil, fmt.Errorf("Failed to get diff with status code %d", res.StatusCode)
}
Expand Down
24 changes: 24 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ var app = &cli.App{
Usage: "Token of the base Directus instance",
EnvVars: []string{"DRCTS_BASE_TOKEN"},
},
&cli.StringFlag{
Name: "base-email",
Aliases: []string{"be"},
Usage: "(Alternative to token) Email of the base Directus instance",
EnvVars: []string{"DRCTS_BASE_EMAIL"},
},
&cli.StringFlag{
Name: "base-password",
Aliases: []string{"bp"},
Usage: "(Alternative to token) Password of the base Directus instance",
EnvVars: []string{"DRCTS_BASE_PASSWORD"},
},
&cli.StringFlag{
Name: "target-url",
Aliases: []string{"tu"},
Expand All @@ -43,6 +55,18 @@ var app = &cli.App{
Usage: "Token of the target Directus instance",
EnvVars: []string{"DRCTS_TARGET_TOKEN"},
},
&cli.StringFlag{
Name: "target-email",
Aliases: []string{"te"},
Usage: "(Alternative to token) Email of the target Directus instance",
EnvVars: []string{"DRCTS_TARGET_EMAIL"},
},
&cli.StringFlag{
Name: "target-password",
Aliases: []string{"tp"},
Usage: "(Alternative to token) Password of the target Directus instance",
EnvVars: []string{"DRCTS_TARGET_PASSWORD"},
},
},
Commands: []*cli.Command{
{
Expand Down
Loading

0 comments on commit 7b800a7

Please sign in to comment.