From 8ba954674e0ae25ab715a11fc031dc62d9d1d30d Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Tue, 27 Sep 2022 20:26:44 -0800 Subject: [PATCH] feat: versioned migrations (#26) * enable atlas migrations * use embedded atlas migrations * chores * bad migration example * tidy * fix linter issues * reset migration state * sort slice before testing * move temp write logic to migrations package --- .gitignore | 2 +- Taskfile.yml | 26 +++++++++-- backend/app/api/main.go | 37 +++++++++++++++- backend/app/migrations/main.go | 42 ++++++++++++++++++ backend/ent/generate.go | 2 +- backend/ent/migrate/migrate.go | 32 ++++++++++++++ backend/go.mod | 12 ++--- backend/go.sum | 31 ++++++++----- backend/internal/config/conf_database.go | 4 +- backend/internal/migrations/migrations.go | 44 +++++++++++++++++++ .../migrations/20220928001319_init.sql | 40 +++++++++++++++++ .../internal/migrations/migrations/atlas.sum | 2 + backend/pkgs/set/set_test.go | 7 ++- docs/docs/quick-start.md | 4 +- 14 files changed, 255 insertions(+), 30 deletions(-) create mode 100644 backend/app/migrations/main.go create mode 100644 backend/internal/migrations/migrations.go create mode 100644 backend/internal/migrations/migrations/20220928001319_init.sql create mode 100644 backend/internal/migrations/migrations/atlas.sum diff --git a/.gitignore b/.gitignore index b81b742..2aab999 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Project Specific -backend/homebox-data/* +backend/.data/* config.yml homebox.db .idea diff --git a/Taskfile.yml b/Taskfile.yml index 04aa8e1..88146c3 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,14 +1,16 @@ version: "3" +env: + HBOX_STORAGE_SQLITE_URL: .data/homebox.db?_fk=1 + tasks: generate: desc: | Generates collateral files from the backend project including swagger docs and typescripts type for the frontend + deps: + - db:generate cmds: - - | - cd backend && ent generate ./ent/schema \ - --template=ent/schema/templates/has_id.tmpl - cd backend/app/api/ && swag fmt - cd backend/app/api/ && swag init --dir=./,../../internal,../../pkgs - | @@ -72,3 +74,21 @@ tasks: desc: Run frontend development server cmds: - cd frontend && pnpm dev + + db:generate: + desc: Run Entgo.io Code Generation + cmds: + - | + cd backend && go generate ./... \ + --template=ent/schema/templates/has_id.tmpl + sources: + - "./backend/ent/schema/**/*" + generates: + - "./backend/ent/" + + db:migration: + desc: Runs the database diff engine to generate a SQL migration files + deps: + - db:generate + cmds: + - cd backend && go run app/migrations/main.go {{ .CLI_ARGS }} diff --git a/backend/app/api/main.go b/backend/app/api/main.go index bd833b3..23d3813 100644 --- a/backend/app/api/main.go +++ b/backend/app/api/main.go @@ -3,11 +3,15 @@ package main import ( "context" "os" + "path/filepath" "time" + atlas "ariga.io/atlas/sql/migrate" + "entgo.io/ent/dialect/sql/schema" "github.com/hay-kot/homebox/backend/app/api/docs" "github.com/hay-kot/homebox/backend/ent" "github.com/hay-kot/homebox/backend/internal/config" + "github.com/hay-kot/homebox/backend/internal/migrations" "github.com/hay-kot/homebox/backend/internal/repo" "github.com/hay-kot/homebox/backend/internal/services" "github.com/hay-kot/homebox/backend/pkgs/server" @@ -70,9 +74,32 @@ func run(cfg *config.Config) error { Msg("failed opening connection to sqlite") } defer func(c *ent.Client) { - _ = c.Close() + err := c.Close() + if err != nil { + log.Fatal().Err(err).Msg("failed to close database connection") + } }(c) - if err := c.Schema.Create(context.Background()); err != nil { + + temp := filepath.Join(os.TempDir(), "migrations") + + err = migrations.Write(temp) + if err != nil { + return err + } + + dir, err := atlas.NewLocalDir(temp) + if err != nil { + return err + } + + options := []schema.MigrateOption{ + schema.WithDir(dir), + schema.WithDropColumn(true), + schema.WithDropIndex(true), + } + + err = c.Schema.Create(context.Background(), options...) + if err != nil { log.Fatal(). Err(err). Str("driver", "sqlite"). @@ -80,6 +107,12 @@ func run(cfg *config.Config) error { Msg("failed creating schema resources") } + err = os.RemoveAll(temp) + if err != nil { + log.Fatal().Err(err).Msg("failed to remove temporary directory for database migrations") + return err + } + app.db = c app.repos = repo.EntAllRepos(c, cfg.Storage.Data) app.services = services.NewServices(app.repos) diff --git a/backend/app/migrations/main.go b/backend/app/migrations/main.go new file mode 100644 index 0000000..bcbb0dd --- /dev/null +++ b/backend/app/migrations/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "context" + "log" + "os" + + "github.com/hay-kot/homebox/backend/ent/migrate" + + atlas "ariga.io/atlas/sql/migrate" + _ "ariga.io/atlas/sql/sqlite" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql/schema" + _ "github.com/mattn/go-sqlite3" +) + +func main() { + ctx := context.Background() + // Create a local migration directory able to understand Atlas migration file format for replay. + dir, err := atlas.NewLocalDir("internal/migrations/migrations") + if err != nil { + log.Fatalf("failed creating atlas migration directory: %v", err) + } + // Migrate diff options. + opts := []schema.MigrateOption{ + schema.WithDir(dir), // provide migration directory + schema.WithMigrationMode(schema.ModeReplay), // provide migration mode + schema.WithDialect(dialect.SQLite), // Ent dialect to use + schema.WithFormatter(atlas.DefaultFormatter), + schema.WithDropIndex(true), + schema.WithDropColumn(true), + } + if len(os.Args) != 2 { + log.Fatalln("migration name is required. Use: 'go run -mod=mod ent/migrate/main.go '") + } + + // Generate migrations using Atlas support for MySQL (note the Ent dialect option passed above). + err = migrate.NamedDiff(ctx, "sqlite://.data/homebox.migration.db?_fk=1", os.Args[1], opts...) + if err != nil { + log.Fatalf("failed generating migration file: %v", err) + } +} diff --git a/backend/ent/generate.go b/backend/ent/generate.go index 8d3fdfd..eb03ded 100644 --- a/backend/ent/generate.go +++ b/backend/ent/generate.go @@ -1,3 +1,3 @@ package ent -//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema +//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/versioned-migration ./schema diff --git a/backend/ent/migrate/migrate.go b/backend/ent/migrate/migrate.go index 1956a6b..d8d3bcb 100644 --- a/backend/ent/migrate/migrate.go +++ b/backend/ent/migrate/migrate.go @@ -54,6 +54,38 @@ func Create(ctx context.Context, s *Schema, tables []*schema.Table, opts ...sche return migrate.Create(ctx, tables...) } +// Diff compares the state read from a database connection or migration directory with +// the state defined by the Ent schema. Changes will be written to new migration files. +func Diff(ctx context.Context, url string, opts ...schema.MigrateOption) error { + return NamedDiff(ctx, url, "changes", opts...) +} + +// NamedDiff compares the state read from a database connection or migration directory with +// the state defined by the Ent schema. Changes will be written to new named migration files. +func NamedDiff(ctx context.Context, url, name string, opts ...schema.MigrateOption) error { + return schema.Diff(ctx, url, name, Tables, opts...) +} + +// Diff creates a migration file containing the statements to resolve the diff +// between the Ent schema and the connected database. +func (s *Schema) Diff(ctx context.Context, opts ...schema.MigrateOption) error { + migrate, err := schema.NewMigrate(s.drv, opts...) + if err != nil { + return fmt.Errorf("ent/migrate: %w", err) + } + return migrate.Diff(ctx, Tables...) +} + +// NamedDiff creates a named migration file containing the statements to resolve the diff +// between the Ent schema and the connected database. +func (s *Schema) NamedDiff(ctx context.Context, name string, opts ...schema.MigrateOption) error { + migrate, err := schema.NewMigrate(s.drv, opts...) + if err != nil { + return fmt.Errorf("ent/migrate: %w", err) + } + return migrate.NamedDiff(ctx, name, Tables...) +} + // WriteTo writes the schema changes to w instead of running them against the database. // // if err := client.Schema.WriteTo(context.Background(), os.Stdout); err != nil { diff --git a/backend/go.mod b/backend/go.mod index 523e82b..1742423 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,6 +3,7 @@ module github.com/hay-kot/homebox/backend go 1.19 require ( + ariga.io/atlas v0.7.0 entgo.io/ent v0.11.2 github.com/ardanlabs/conf/v2 v2.2.0 github.com/go-chi/chi/v5 v5.0.7 @@ -16,7 +17,6 @@ require ( ) require ( - ariga.io/atlas v0.6.3 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect @@ -26,10 +26,10 @@ require ( github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/spec v0.20.7 // indirect github.com/go-openapi/swag v0.22.3 // indirect - github.com/google/go-cmp v0.5.8 // indirect - github.com/hashicorp/hcl/v2 v2.13.0 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/hashicorp/hcl/v2 v2.14.1 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/kr/pretty v0.2.0 // indirect + github.com/kr/pretty v0.3.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect @@ -38,8 +38,8 @@ require ( github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a // indirect github.com/zclconf/go-cty v1.11.0 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect - golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect - golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect + golang.org/x/net v0.0.0-20220923203811-8be639271d50 // indirect + golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index c1fef24..c1adee0 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,5 +1,5 @@ -ariga.io/atlas v0.6.3 h1:MtT4OxHqkW0XgYRjvqU4bmmv+42U1lvw9u8HzJ8yK9c= -ariga.io/atlas v0.6.3/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE= +ariga.io/atlas v0.7.0 h1:daEFdUsyNm7EHyzcMfjWwq/fVv48fCfad+dIGyobY1k= +ariga.io/atlas v0.7.0/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE= entgo.io/ent v0.11.2 h1:UM2/BUhF2FfsxPHRxLjQbhqJNaDdVlOwNIAMLs2jyto= entgo.io/ent v0.11.2/go.mod h1:YGHEQnmmIUgtD5b1ICD5vg74dS3npkNnmC5K+0J+IHU= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= @@ -34,17 +34,17 @@ github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc= -github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0= +github.com/hashicorp/hcl/v2 v2.14.1 h1:x0BpjfZ+CYdbiz+8yZTQ+gdLO7IXvOut7Da+XJayx34= +github.com/hashicorp/hcl/v2 v2.14.1/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -61,18 +61,24 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -93,15 +99,15 @@ golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY= -golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220923203811-8be639271d50 h1:vKyz8L3zkd+xrMeIaBsQ/MNVPVFSffdaU3ZyYlBGFnI= +golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc= +golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= @@ -113,6 +119,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/backend/internal/config/conf_database.go b/backend/internal/config/conf_database.go index 047d5d2..69a67b9 100644 --- a/backend/internal/config/conf_database.go +++ b/backend/internal/config/conf_database.go @@ -6,6 +6,6 @@ const ( type Storage struct { // Data is the path to the root directory - Data string `yaml:"data" conf:"default:./homebox-data"` - SqliteUrl string `yaml:"sqlite-url" conf:"default:./homebox-data/homebox.db?_fk=1"` + Data string `yaml:"data" conf:"default:./.data"` + SqliteUrl string `yaml:"sqlite-url" conf:"default:./.data/homebox.db?_fk=1"` } diff --git a/backend/internal/migrations/migrations.go b/backend/internal/migrations/migrations.go new file mode 100644 index 0000000..83354aa --- /dev/null +++ b/backend/internal/migrations/migrations.go @@ -0,0 +1,44 @@ +package migrations + +import ( + "embed" + "os" + "path/filepath" +) + +// go:embed all:migrations +var Files embed.FS + +// Write writes the embedded migrations to a temporary directory. +// It returns an error and a cleanup function. The cleanup function +// should be called when the migrations are no longer needed. +func Write(temp string) error { + err := os.MkdirAll(temp, 0755) + + if err != nil { + return err + } + + fsDir, err := Files.ReadDir(".") + if err != nil { + return err + } + + for _, f := range fsDir { + if f.IsDir() { + continue + } + + b, err := Files.ReadFile(filepath.Join("migrations", f.Name())) + if err != nil { + return err + } + + err = os.WriteFile(filepath.Join(temp, f.Name()), b, 0644) + if err != nil { + return err + } + } + + return nil +} diff --git a/backend/internal/migrations/migrations/20220928001319_init.sql b/backend/internal/migrations/migrations/20220928001319_init.sql new file mode 100644 index 0000000..579d559 --- /dev/null +++ b/backend/internal/migrations/migrations/20220928001319_init.sql @@ -0,0 +1,40 @@ +-- create "attachments" table +CREATE TABLE `attachments` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `type` text NOT NULL DEFAULT 'attachment', `document_attachments` uuid NOT NULL, `item_attachments` uuid NOT NULL, PRIMARY KEY (`id`), CONSTRAINT `attachments_documents_attachments` FOREIGN KEY (`document_attachments`) REFERENCES `documents` (`id`) ON DELETE CASCADE, CONSTRAINT `attachments_items_attachments` FOREIGN KEY (`item_attachments`) REFERENCES `items` (`id`) ON DELETE CASCADE); +-- create "auth_tokens" table +CREATE TABLE `auth_tokens` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `token` blob NOT NULL, `expires_at` datetime NOT NULL, `user_auth_tokens` uuid NULL, PRIMARY KEY (`id`), CONSTRAINT `auth_tokens_users_auth_tokens` FOREIGN KEY (`user_auth_tokens`) REFERENCES `users` (`id`) ON DELETE CASCADE); +-- create index "auth_tokens_token_key" to table: "auth_tokens" +CREATE UNIQUE INDEX `auth_tokens_token_key` ON `auth_tokens` (`token`); +-- create index "authtokens_token" to table: "auth_tokens" +CREATE INDEX `authtokens_token` ON `auth_tokens` (`token`); +-- create "documents" table +CREATE TABLE `documents` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `title` text NOT NULL, `path` text NOT NULL, `group_documents` uuid NOT NULL, PRIMARY KEY (`id`), CONSTRAINT `documents_groups_documents` FOREIGN KEY (`group_documents`) REFERENCES `groups` (`id`) ON DELETE CASCADE); +-- create "document_tokens" table +CREATE TABLE `document_tokens` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `token` blob NOT NULL, `uses` integer NOT NULL DEFAULT 1, `expires_at` datetime NOT NULL, `document_document_tokens` uuid NULL, PRIMARY KEY (`id`), CONSTRAINT `document_tokens_documents_document_tokens` FOREIGN KEY (`document_document_tokens`) REFERENCES `documents` (`id`) ON DELETE CASCADE); +-- create index "document_tokens_token_key" to table: "document_tokens" +CREATE UNIQUE INDEX `document_tokens_token_key` ON `document_tokens` (`token`); +-- create index "documenttoken_token" to table: "document_tokens" +CREATE INDEX `documenttoken_token` ON `document_tokens` (`token`); +-- create "groups" table +CREATE TABLE `groups` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `currency` text NOT NULL DEFAULT 'usd', PRIMARY KEY (`id`)); +-- create "items" table +CREATE TABLE `items` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `description` text NULL, `import_ref` text NULL, `notes` text NULL, `quantity` integer NOT NULL DEFAULT 1, `insured` bool NOT NULL DEFAULT false, `serial_number` text NULL, `model_number` text NULL, `manufacturer` text NULL, `lifetime_warranty` bool NOT NULL DEFAULT false, `warranty_expires` datetime NULL, `warranty_details` text NULL, `purchase_time` datetime NULL, `purchase_from` text NULL, `purchase_price` real NOT NULL DEFAULT 0, `sold_time` datetime NULL, `sold_to` text NULL, `sold_price` real NOT NULL DEFAULT 0, `sold_notes` text NULL, `group_items` uuid NOT NULL, `location_items` uuid NULL, PRIMARY KEY (`id`), CONSTRAINT `items_groups_items` FOREIGN KEY (`group_items`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `items_locations_items` FOREIGN KEY (`location_items`) REFERENCES `locations` (`id`) ON DELETE CASCADE); +-- create index "item_name" to table: "items" +CREATE INDEX `item_name` ON `items` (`name`); +-- create index "item_manufacturer" to table: "items" +CREATE INDEX `item_manufacturer` ON `items` (`manufacturer`); +-- create index "item_model_number" to table: "items" +CREATE INDEX `item_model_number` ON `items` (`model_number`); +-- create index "item_serial_number" to table: "items" +CREATE INDEX `item_serial_number` ON `items` (`serial_number`); +-- create "item_fields" table +CREATE TABLE `item_fields` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `description` text NULL, `type` text NOT NULL, `text_value` text NULL, `number_value` integer NULL, `boolean_value` bool NOT NULL DEFAULT false, `time_value` datetime NOT NULL, `item_fields` uuid NULL, PRIMARY KEY (`id`), CONSTRAINT `item_fields_items_fields` FOREIGN KEY (`item_fields`) REFERENCES `items` (`id`) ON DELETE CASCADE); +-- create "labels" table +CREATE TABLE `labels` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `description` text NULL, `color` text NULL, `group_labels` uuid NOT NULL, PRIMARY KEY (`id`), CONSTRAINT `labels_groups_labels` FOREIGN KEY (`group_labels`) REFERENCES `groups` (`id`) ON DELETE CASCADE); +-- create "locations" table +CREATE TABLE `locations` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `description` text NULL, `group_locations` uuid NOT NULL, PRIMARY KEY (`id`), CONSTRAINT `locations_groups_locations` FOREIGN KEY (`group_locations`) REFERENCES `groups` (`id`) ON DELETE CASCADE); +-- create "users" table +CREATE TABLE `users` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `email` text NOT NULL, `password` text NOT NULL, `is_superuser` bool NOT NULL DEFAULT false, `group_users` uuid NOT NULL, PRIMARY KEY (`id`), CONSTRAINT `users_groups_users` FOREIGN KEY (`group_users`) REFERENCES `groups` (`id`) ON DELETE CASCADE); +-- create index "users_email_key" to table: "users" +CREATE UNIQUE INDEX `users_email_key` ON `users` (`email`); +-- create "label_items" table +CREATE TABLE `label_items` (`label_id` uuid NOT NULL, `item_id` uuid NOT NULL, PRIMARY KEY (`label_id`, `item_id`), CONSTRAINT `label_items_label_id` FOREIGN KEY (`label_id`) REFERENCES `labels` (`id`) ON DELETE CASCADE, CONSTRAINT `label_items_item_id` FOREIGN KEY (`item_id`) REFERENCES `items` (`id`) ON DELETE CASCADE); diff --git a/backend/internal/migrations/migrations/atlas.sum b/backend/internal/migrations/migrations/atlas.sum new file mode 100644 index 0000000..9c25df9 --- /dev/null +++ b/backend/internal/migrations/migrations/atlas.sum @@ -0,0 +1,2 @@ +h1:A58dgWs4yGTcWkHBZwIedtCwK1LIWHYxqB5uKQ40f6E= +20220928001319_init.sql h1:KOJZuCHJ5dTHHwVDGgAWyUFahBXqGtmuv4d+rxwpuX0= diff --git a/backend/pkgs/set/set_test.go b/backend/pkgs/set/set_test.go index b571298..c34239c 100644 --- a/backend/pkgs/set/set_test.go +++ b/backend/pkgs/set/set_test.go @@ -2,6 +2,7 @@ package set import ( "reflect" + "sort" "testing" ) @@ -247,7 +248,11 @@ func TestSet_Slice(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := tt.s.Slice(); !reflect.DeepEqual(got, tt.want) { + got := tt.s.Slice() + + sort.Strings(got) + + if !reflect.DeepEqual(got, tt.want) { t.Errorf("Set.Slice() = %v, want %v", got, tt.want) } }) diff --git a/docs/docs/quick-start.md b/docs/docs/quick-start.md index 4a488df..08eff5a 100644 --- a/docs/docs/quick-start.md +++ b/docs/docs/quick-start.md @@ -65,8 +65,8 @@ volumes: --web-port/$HBOX_WEB_PORT (default: 7745) --web-host/$HBOX_WEB_HOST --web-max-upload-size/$HBOX_WEB_MAX_UPLOAD_SIZE (default: 10) - --storage-data/$HBOX_STORAGE_DATA (default: ./homebox-data) - --storage-sqlite-url/$HBOX_STORAGE_SQLITE_URL (default: ./homebox-data/homebox.db?_fk=1) + --storage-data/$HBOX_STORAGE_DATA (default: ./.data) + --storage-sqlite-url/$HBOX_STORAGE_SQLITE_URL (default: ./.data/homebox.db?_fk=1) --log-level/$HBOX_LOG_LEVEL (default: info) --log-format/$HBOX_LOG_FORMAT (default: text) --mailer-host/$HBOX_MAILER_HOST