Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user export subcommand #1097

Merged
merged 9 commits into from
Oct 17, 2023
82 changes: 82 additions & 0 deletions backend/cmd/user/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package user

import (
"encoding/json"
"fmt"
"log"
"os"

"github.com/spf13/cobra"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/persistence"
)

func NewExportCommand() *cobra.Command {
var (
configFile string
outputFile string
)

cmd := &cobra.Command{
Use: "export",
Short: "Export users from database into a Json file",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
cfg, err := config.Load(&configFile)
if err != nil {
log.Fatal(err)
}
persister, err := persistence.New(cfg.Database)
if err != nil {
log.Fatal(err)
}
err = export(persister, outputFile)
if err != nil {
log.Fatal(err)
}
log.Println(fmt.Sprintf("Successfully exported users to %s", outputFile))
},
}

cmd.Flags().StringVar(&configFile, "config", config.DefaultConfigFilePath, "config file")
cmd.Flags().StringVarP(&outputFile, "outputFile", "o", "", "The path of the output file.")
err := cmd.MarkFlagRequired("outputFile")
if err != nil {
log.Println(err)
}
return cmd
}

func export(persister persistence.Persister, outFile string) error {
var entries []ImportOrExportEntry
users, err := persister.GetUserPersister().All()
if err != nil {
return fmt.Errorf("failed to get list of users: %w", err)
}
for _, user := range users {
var emails []ImportOrExportEmail
for _, email := range user.Emails {
emails = append(emails, ImportOrExportEmail{
Address: email.Address,
IsPrimary: email.IsPrimary(),
IsVerified: email.Verified,
})
}
entry := ImportOrExportEntry{
UserID: user.ID.String(),
Emails: emails,
CreatedAt: &user.CreatedAt,
UpdatedAt: &user.UpdatedAt,
}
entries = append(entries, entry)
}
bytes, err := json.Marshal(entries)
if err != nil {
return err
}
err = os.WriteFile(outFile, bytes, 0600)
if err != nil {
return err
}
return nil
}
19 changes: 10 additions & 9 deletions backend/cmd/user/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package user
import (
"errors"
"fmt"
"github.com/gofrs/uuid"
"time"

"github.com/gofrs/uuid"
)

// ImportEmail The import format for a user's email
type ImportEmail struct {
// ImportOrExportEmail The import/export format for a user's email
type ImportOrExportEmail struct {
// Address Valid email address
Address string `json:"address" yaml:"address"`
// IsPrimary indicates if this is the primary email of the users. In the Emails array there has to be exactly one primary email.
Expand All @@ -18,10 +19,10 @@ type ImportEmail struct {
}

// Emails Array of email addresses
type Emails []ImportEmail
type Emails []ImportOrExportEmail

// ImportEntry represents a user to be imported to the Hanko database
type ImportEntry struct {
// ImportOrExportEntry represents a user to be imported/export to the Hanko database
type ImportOrExportEntry struct {
// UserID optional uuid.v4. If not provided a new one will be generated for the user
UserID string `json:"user_id" yaml:"user_id"`
// Emails List of emails
Expand All @@ -32,10 +33,10 @@ type ImportEntry struct {
UpdatedAt *time.Time `json:"updated_at" yaml:"updated_at"`
}

// ImportList a list of ImportEntries
type ImportList []ImportEntry
// ImportOrExportList a list of ImportEntries
type ImportOrExportList []ImportOrExportEntry

func (entry *ImportEntry) validate() error {
func (entry *ImportOrExportEntry) validate() error {
if len(entry.Emails) == 0 {
return errors.New(fmt.Sprintf("Entry with id: %v has got no Emails.", entry.UserID))
}
Expand Down
17 changes: 9 additions & 8 deletions backend/cmd/user/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package user

import (
"fmt"
"github.com/stretchr/testify/assert"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

const validUUID = "62418053-a2cd-47a8-9b61-4426380d263a"
Expand All @@ -27,7 +28,7 @@ func TestImportEntry_validate(t *testing.T) {
fields: fields{
UserID: "",
Emails: Emails{
ImportEmail{
ImportOrExportEmail{
Address: "[email protected]",
IsPrimary: true,
IsVerified: false,
Expand All @@ -43,7 +44,7 @@ func TestImportEntry_validate(t *testing.T) {
fields: fields{
UserID: validUUID,
Emails: Emails{
ImportEmail{
ImportOrExportEmail{
Address: "[email protected]",
IsPrimary: true,
IsVerified: false,
Expand All @@ -59,7 +60,7 @@ func TestImportEntry_validate(t *testing.T) {
fields: fields{
UserID: invalidUUID,
Emails: Emails{
ImportEmail{
ImportOrExportEmail{
Address: "[email protected]",
IsPrimary: true,
IsVerified: false,
Expand All @@ -85,7 +86,7 @@ func TestImportEntry_validate(t *testing.T) {
fields: fields{
UserID: "",
Emails: Emails{
ImportEmail{
ImportOrExportEmail{
Address: "[email protected]",
IsPrimary: false,
IsVerified: false,
Expand All @@ -101,12 +102,12 @@ func TestImportEntry_validate(t *testing.T) {
fields: fields{
UserID: "",
Emails: Emails{
ImportEmail{
ImportOrExportEmail{
Address: "[email protected]",
IsPrimary: true,
IsVerified: false,
},
ImportEmail{
ImportOrExportEmail{
Address: "[email protected]",
IsPrimary: true,
IsVerified: false,
Expand All @@ -120,7 +121,7 @@ func TestImportEntry_validate(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
entry := &ImportEntry{
entry := &ImportOrExportEntry{
UserID: tt.fields.UserID,
Emails: tt.fields.Emails,
CreatedAt: tt.fields.CreatedAt,
Expand Down
13 changes: 7 additions & 6 deletions backend/cmd/user/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package user

import (
"encoding/json"
"github.com/brianvoe/gofakeit/v6"
"github.com/gofrs/uuid"
"github.com/spf13/cobra"
"log"
"os"
"time"

"github.com/brianvoe/gofakeit/v6"
"github.com/gofrs/uuid"
"github.com/spf13/cobra"
)

var outputFile string
Expand Down Expand Up @@ -36,18 +37,18 @@ func NewGenerateCommand() *cobra.Command {
}

func generate() error {
var entries []ImportEntry
var entries []ImportOrExportEntry
for i := 0; i < count; i++ {
now := time.Now().UTC()
id, _ := uuid.NewV4()
emails := []ImportEmail{
emails := []ImportOrExportEmail{
{
Address: gofakeit.Email(),
IsPrimary: true,
IsVerified: true,
},
}
entry := ImportEntry{
entry := ImportOrExportEntry{
UserID: id.String(),
Emails: emails,
CreatedAt: &now,
Expand Down
21 changes: 11 additions & 10 deletions backend/cmd/user/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"
"github.com/spf13/cobra"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/persistence/models"
"io"
"log"
"net/http"
"os"
"strings"
"time"

"github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"
"github.com/spf13/cobra"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/persistence/models"
)

func NewImportCommand() *cobra.Command {
Expand Down Expand Up @@ -103,7 +104,7 @@ func NewImportCommand() *cobra.Command {

// loadAndValidate reads json from an io.Reader so we read every entry separate and validate it. We go through the whole
// array to print out every validation error in the input data.
func loadAndValidate(input io.Reader) ([]ImportEntry, error) {
func loadAndValidate(input io.Reader) ([]ImportOrExportEntry, error) {
dec := json.NewDecoder(input)

// read the open bracket
Expand All @@ -112,14 +113,14 @@ func loadAndValidate(input io.Reader) ([]ImportEntry, error) {
return nil, err
}

users := []ImportEntry{}
users := []ImportOrExportEntry{}

numErrors := 0
index := 0
// while the array contains values
for dec.More() {
index = index + 1
var userEntry ImportEntry
var userEntry ImportOrExportEntry
// decode one ImportEntry
err := dec.Decode(&userEntry)
if err != nil {
Expand Down Expand Up @@ -152,7 +153,7 @@ func loadAndValidate(input io.Reader) ([]ImportEntry, error) {
}

// commits the list of ImportEntries to the database. Wrapped in a transaction so if something fails no new users are added.
func addToDatabase(entries []ImportEntry, persister persistence.Persister) error {
func addToDatabase(entries []ImportOrExportEntry, persister persistence.Persister) error {
tx := persister.GetConnection()
err := tx.Transaction(func(tx *pop.Connection) error {
for i, v := range entries {
Expand Down
Loading