diff --git a/.github/workflows/docker-publish-arm.yaml b/.github/workflows/docker-publish-arm.yaml index 0147fa1c..56d93b45 100644 --- a/.github/workflows/docker-publish-arm.yaml +++ b/.github/workflows/docker-publish-arm.yaml @@ -11,18 +11,18 @@ on: - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' env: # Use docker.io for Docker Hub if empty diff --git a/.github/workflows/docker-publish-rootless-arm.yaml b/.github/workflows/docker-publish-rootless-arm.yaml index c40cd55b..2ab20270 100644 --- a/.github/workflows/docker-publish-rootless-arm.yaml +++ b/.github/workflows/docker-publish-rootless-arm.yaml @@ -11,18 +11,18 @@ on: - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' env: # Use docker.io for Docker Hub if empty diff --git a/.github/workflows/docker-publish-rootless.yaml b/.github/workflows/docker-publish-rootless.yaml index f5d022c6..82cd4153 100644 --- a/.github/workflows/docker-publish-rootless.yaml +++ b/.github/workflows/docker-publish-rootless.yaml @@ -11,18 +11,18 @@ on: - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' env: diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml index ab226bdb..2eff34b5 100644 --- a/.github/workflows/docker-publish.yaml +++ b/.github/workflows/docker-publish.yaml @@ -11,18 +11,18 @@ on: - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' env: # Use docker.io for Docker Hub if empty diff --git a/.github/workflows/partial-frontend.yaml b/.github/workflows/partial-frontend.yaml index c793c62a..0786b9eb 100644 --- a/.github/workflows/partial-frontend.yaml +++ b/.github/workflows/partial-frontend.yaml @@ -32,6 +32,20 @@ jobs: integration-tests: name: Integration Tests runs-on: ubuntu-latest + services: + postgres: + image: postgres:17 + env: + POSTGRES_USER: homebox + POSTGRES_PASSWORD: homebox + POSTGRES_DB: homebox + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - name: Checkout uses: actions/checkout@v4 @@ -62,3 +76,53 @@ jobs: - name: Run Integration Tests run: task test:ci + integration-tests-pgsql: + strategy: + matrix: + database_version: [17,16,15] + name: Integration Tests PGSQL ${{ matrix.database_version }} + runs-on: ubuntu-latest + services: + postgres: + image: postgres:${{ matrix.database_version }} + env: + POSTGRES_USER: homebox + POSTGRES_PASSWORD: homebox + POSTGRES_DB: homebox + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Task + uses: arduino/setup-task@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.21" + + - uses: actions/setup-node@v4 + with: + node-version: 18 + + - uses: pnpm/action-setup@v3.0.0 + with: + version: 9.12.2 + + - name: Install dependencies + run: pnpm install + working-directory: frontend + + - name: Run Integration Tests + run: task test:ci:postgresql \ No newline at end of file diff --git a/.github/workflows/pull-requests.yaml b/.github/workflows/pull-requests.yaml index 61a14859..e94977ac 100644 --- a/.github/workflows/pull-requests.yaml +++ b/.github/workflows/pull-requests.yaml @@ -4,10 +4,12 @@ on: pull_request: branches: - main + - vnext paths: - 'backend/**' - 'frontend/**' + - '.github/workflows/**' jobs: backend-tests: diff --git a/.gitignore b/.gitignore index 51a835a4..0159273c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ backend/.data/* config.yml homebox.db +homebox.db-journal +homebox.db-shm +homebox.db-wal .idea .DS_Store test-mailer.json diff --git a/Taskfile.yml b/Taskfile.yml index 4d9c1aa2..c945f260 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -2,7 +2,8 @@ version: "3" env: HBOX_LOG_LEVEL: debug - HBOX_STORAGE_SQLITE_URL: .data/homebox.db?_pragma=busy_timeout=1000&_pragma=journal_mode=WAL&_fk=1 + HBOX_DATABASE_DRIVER: sqlite3 + HBOX_DATABASE_SQLITE_PATH: .data/homebox.db?_pragma=busy_timeout=1000&_pragma=journal_mode=WAL&_fk=1 HBOX_OPTIONS_ALLOW_REGISTRATION: true UNSAFE_DISABLE_PASSWORD_PROJECTION: "yes_i_am_sure" tasks: @@ -60,6 +61,23 @@ tasks: - go run ./app/api/ {{ .CLI_ARGS }} silent: false + go:run:postgresql: + env: + HBOX_DATABASE_DRIVER: postgres + HBOX_DATABASE_USERNAME: homebox + HBOX_DATABASE_PASSWORD: homebox + HBOX_DATABASE_DATABASE: homebox + HBOX_DATABASE_HOST: localhost + HBOX_DATABASE_PORT: 5432 + HBOX_DATABASE_SSL_MODE: disable + desc: Starts the backend api server with postgresql (depends on generate task) + dir: backend + deps: + - generate + cmds: + - go run ./app/api/ {{ .CLI_ARGS }} + silent: false + go:test: desc: Runs all go tests using gotestsum - supports passing gotestsum args dir: backend @@ -114,6 +132,21 @@ tasks: cmds: - cd backend && go run app/tools/migrations/main.go {{ .CLI_ARGS }} + db:migration:postgresql: + env: + HBOX_DATABASE_DRIVER: postgres + HBOX_DATABASE_USERNAME: homebox + HBOX_DATABASE_PASSWORD: homebox + HBOX_DATABASE_DATABASE: homebox + HBOX_DATABASE_HOST: localhost + HBOX_DATABASE_PORT: 5432 + HBOX_DATABASE_SSL_MODE: disable + desc: Runs the database diff engine to generate a SQL migration files for postgresql + deps: + - db:generate + cmds: + - cd backend && go run app/tools/migrations/main.go {{ .CLI_ARGS }} + ui:watch: desc: Starts the vitest test runner in watch mode dir: frontend @@ -143,7 +176,24 @@ tasks: cmds: - cd backend && go build ./app/api - backend/api & - - sleep 5 + - sleep 10 + - cd frontend && pnpm run test:ci + silent: true + + test:ci:postgresql: + env: + HBOX_DATABASE_DRIVER: postgres + HBOX_DATABASE_USERNAME: homebox + HBOX_DATABASE_PASSWORD: homebox + HBOX_DATABASE_DATABASE: homebox + HBOX_DATABASE_HOST: 127.0.0.1 + HBOX_DATABASE_PORT: 5432 + HBOX_DATABASE_SSL_MODE: disable + desc: Runs end-to-end test on a live server with postgresql (only for use in CI) + cmds: + - cd backend && go build ./app/api + - backend/api & + - sleep 10 - cd frontend && pnpm run test:ci silent: true diff --git a/backend/app/api/main.go b/backend/app/api/main.go index 6247fe6a..86efc5dd 100644 --- a/backend/app/api/main.go +++ b/backend/app/api/main.go @@ -4,9 +4,11 @@ import ( "bytes" "context" "fmt" + "github.com/google/uuid" "net/http" "os" "path/filepath" + "strings" "time" atlas "ariga.io/atlas/sql/migrate" @@ -28,6 +30,7 @@ import ( "github.com/sysadminsmedia/homebox/backend/internal/sys/config" "github.com/sysadminsmedia/homebox/backend/internal/web/mid" + _ "github.com/lib/pq" _ "github.com/sysadminsmedia/homebox/backend/pkgs/cgofreesqlite" ) @@ -46,6 +49,19 @@ func build() string { return fmt.Sprintf("%s, commit %s, built at %s", version, short, buildTime) } +func validatePostgresSSLMode(sslMode string) bool { + validModes := map[string]bool{ + "": true, + "disable": true, + "allow": true, + "prefer": true, + "require": true, + "verify-ca": true, + "verify-full": true, + } + return validModes[strings.ToLower(strings.TrimSpace(sslMode))] +} + // @title Homebox API // @version 1.0 // @description Track, Manage, and Organize your Things. @@ -80,13 +96,32 @@ func run(cfg *config.Config) error { log.Fatal().Err(err).Msg("failed to create data directory") } - c, err := ent.Open("sqlite3", cfg.Storage.SqliteURL) + if strings.ToLower(cfg.Database.Driver) == "postgres" { + if !validatePostgresSSLMode(cfg.Database.SslMode) { + log.Fatal().Str("sslmode", cfg.Database.SslMode).Msg("invalid sslmode") + } + } + + // Set up the database URL based on the driver because for some reason a common URL format is not used + databaseURL := "" + switch strings.ToLower(cfg.Database.Driver) { + case "sqlite3": + databaseURL = cfg.Database.SqlitePath + case "postgres": + databaseURL = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", cfg.Database.Host, cfg.Database.Port, cfg.Database.Username, cfg.Database.Password, cfg.Database.Database, cfg.Database.SslMode) + default: + log.Fatal().Str("driver", cfg.Database.Driver).Msg("unsupported database driver") + } + + c, err := ent.Open(strings.ToLower(cfg.Database.Driver), databaseURL) if err != nil { log.Fatal(). Err(err). - Str("driver", "sqlite"). - Str("url", cfg.Storage.SqliteURL). - Msg("failed opening connection to sqlite") + Str("driver", strings.ToLower(cfg.Database.Driver)). + Str("host", cfg.Database.Host). + Str("port", cfg.Database.Port). + Str("database", cfg.Database.Database). + Msg("failed opening connection to {driver} database at {host}:{port}/{database}") } defer func(c *ent.Client) { err := c.Close() @@ -95,9 +130,14 @@ func run(cfg *config.Config) error { } }(c) - temp := filepath.Join(os.TempDir(), "migrations") + // Always create a random temporary directory for migrations + tempUUID, err := uuid.NewUUID() + if err != nil { + return err + } + temp := filepath.Join(os.TempDir(), fmt.Sprintf("homebox-%s", tempUUID.String())) - err = migrations.Write(temp) + err = migrations.Write(temp, cfg.Database.Driver) if err != nil { return err } @@ -117,17 +157,18 @@ func run(cfg *config.Config) error { if err != nil { log.Error(). Err(err). - Str("driver", "sqlite"). - Str("url", cfg.Storage.SqliteURL). + Str("driver", cfg.Database.Driver). + Str("url", databaseURL). Msg("failed creating schema resources") return err } - err = os.RemoveAll(temp) - if err != nil { - log.Error().Err(err).Msg("failed to remove temporary directory for database migrations") - return err - } + defer func() { + err := os.RemoveAll(temp) + if err != nil { + log.Error().Err(err).Msg("failed to remove temporary directory for database migrations") + } + }() collectFuncs := []currencies.CollectorFunc{ currencies.CollectDefaults(), diff --git a/backend/app/tools/migrations/main.go b/backend/app/tools/migrations/main.go index f0bd2c6d..fd952cc8 100644 --- a/backend/app/tools/migrations/main.go +++ b/backend/app/tools/migrations/main.go @@ -3,8 +3,11 @@ package main import ( "context" "fmt" + "github.com/sysadminsmedia/homebox/backend/internal/sys/config" "log" "os" + "path/filepath" + "strings" "github.com/sysadminsmedia/homebox/backend/internal/data/ent/migrate" @@ -12,13 +15,29 @@ import ( _ "ariga.io/atlas/sql/sqlite" "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql/schema" + + _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" ) func main() { + cfg, err := config.New(build(), "Homebox inventory management system") + if err != nil { + panic(err) + } + sqlDialect := "" + switch strings.ToLower(cfg.Database.Driver) { + case "sqlite3": + sqlDialect = dialect.SQLite + case "postgres": + sqlDialect = dialect.Postgres + default: + log.Fatalf("unsupported database driver: %s", cfg.Database.Driver) + } ctx := context.Background() // Create a local migration directory able to understand Atlas migration file format for replay. - dir, err := atlas.NewLocalDir("internal/data/migrations/migrations") + safePath := filepath.Clean(fmt.Sprintf("internal/data/migrations/%s", sqlDialect)) + dir, err := atlas.NewLocalDir(safePath) if err != nil { log.Fatalf("failed creating atlas migration directory: %v", err) } @@ -26,7 +45,7 @@ func main() { 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.WithDialect(sqlDialect), // Ent dialect to use schema.WithFormatter(atlas.DefaultFormatter), schema.WithDropIndex(true), schema.WithDropColumn(true), @@ -35,11 +54,55 @@ func main() { log.Fatalln("migration name is required. Use: 'go run -mod=mod ent/migrate/main.go '") } + if sqlDialect == dialect.Postgres { + if !validatePostgresSSLMode(cfg.Database.SslMode) { + log.Fatalf("invalid sslmode: %s", cfg.Database.SslMode) + } + } + + databaseURL := "" + switch { + case cfg.Database.Driver == "sqlite3": + databaseURL = fmt.Sprintf("sqlite://%s", cfg.Database.SqlitePath) + case cfg.Database.Driver == "postgres": + databaseURL = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Database, cfg.Database.SslMode) + default: + log.Fatalf("unsupported database driver: %s", cfg.Database.Driver) + } + // 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...) + err = migrate.NamedDiff(ctx, databaseURL, os.Args[1], opts...) if err != nil { log.Fatalf("failed generating migration file: %v", err) } fmt.Println("Migration file generated successfully.") } + +var ( + version = "nightly" + commit = "HEAD" + buildTime = "now" +) + +func build() string { + short := commit + if len(short) > 7 { + short = short[:7] + } + + return fmt.Sprintf("%s, commit %s, built at %s", version, short, buildTime) +} + +func validatePostgresSSLMode(sslMode string) bool { + validModes := map[string]bool{ + "": true, + "disable": true, + "allow": true, + "prefer": true, + "require": true, + "verify-ca": true, + "verify-full": true, + } + return validModes[strings.ToLower(strings.TrimSpace(sslMode))] +} diff --git a/backend/go.mod b/backend/go.mod index b2f2d597..fc7c524b 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -13,6 +13,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/schema v1.4.1 github.com/hay-kot/httpkit v0.0.11 + github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.24 github.com/olahol/melody v1.2.1 github.com/pkg/errors v0.9.1 diff --git a/backend/go.sum b/backend/go.sum index f9a83abc..f210751e 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,9 +1,5 @@ ariga.io/atlas v0.19.1 h1:QzBHkakwzEhmPWOzNhw8Yr/Bbicj6Iq5hwEoNI/Jr9A= ariga.io/atlas v0.19.1/go.mod h1:VPlcXdd4w2KqKnH54yEZcry79UAhpaWaxEsmn5JRNoE= -ariga.io/atlas v0.28.0 h1:qmn9tUyJypJkIw+X3ECUwDtkMTiFupgstHbjRN4xGH0= -ariga.io/atlas v0.28.0/go.mod h1:LOOp18LCL9r+VifvVlJqgYJwYl271rrXD9/wIyzJ8sw= -entgo.io/ent v0.12.5 h1:KREM5E4CSoej4zeGa88Ou/gfturAnpUv0mzAjch1sj4= -entgo.io/ent v0.12.5/go.mod h1:Y3JVAjtlIk8xVZYSn3t3mf8xlZIn5SAOXZQxD6kKI+Q= entgo.io/ent v0.14.1 h1:fUERL506Pqr92EPHJqr8EYxbPioflJo6PudkrEA8a/s= entgo.io/ent v0.14.1/go.mod h1:MH6XLG0KXpkcDQhKiHfANZSzR55TJyPL5IGNpI8wpco= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= @@ -58,8 +54,6 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= -github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= @@ -105,6 +99,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -116,10 +112,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= -github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= @@ -129,8 +121,6 @@ github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJm github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olahol/melody v1.2.1 h1:xdwRkzHxf+B0w4TKbGpUSSkV516ZucQZJIWLztOWICQ= github.com/olahol/melody v1.2.1/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= @@ -148,10 +138,6 @@ github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -176,20 +162,12 @@ github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM= github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA= github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= -github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= @@ -197,16 +175,10 @@ golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= @@ -223,8 +195,14 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= @@ -233,8 +211,10 @@ modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= -modernc.org/sqlite v1.33.0 h1:WWkA/T2G17okiLGgKAj4/RMIvgyMT19yQ038160IeYk= -modernc.org/sqlite v1.33.0/go.mod h1:9uQ9hF/pCZoYZK73D/ud5Z7cIRIILSZI8NdIemVMTX8= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= diff --git a/backend/internal/data/migrations/migrations.go b/backend/internal/data/migrations/migrations.go index a2afdc89..ae8852f4 100644 --- a/backend/internal/data/migrations/migrations.go +++ b/backend/internal/data/migrations/migrations.go @@ -3,23 +3,29 @@ package migrations import ( "embed" + "fmt" "os" "path" ) -//go:embed all:migrations +//go:embed all:sqlite3 all:postgres 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 { +func Write(temp string, dialect string) error { + allowedDialects := map[string]bool{"sqlite3": true, "postgres": true} + if !allowedDialects[dialect] { + return fmt.Errorf("unsupported dialect: %s", dialect) + } + err := os.MkdirAll(temp, 0o755) if err != nil { return err } - fsDir, err := Files.ReadDir("migrations") + fsDir, err := Files.ReadDir(dialect) if err != nil { return err } @@ -29,7 +35,7 @@ func Write(temp string) error { continue } - b, err := Files.ReadFile(path.Join("migrations", f.Name())) + b, err := Files.ReadFile(path.Join(dialect, f.Name())) if err != nil { return err } diff --git a/backend/internal/data/migrations/postgres/20241027025146_init.sql b/backend/internal/data/migrations/postgres/20241027025146_init.sql new file mode 100644 index 00000000..a1ebcb70 --- /dev/null +++ b/backend/internal/data/migrations/postgres/20241027025146_init.sql @@ -0,0 +1,58 @@ +-- Create "groups" table +CREATE TABLE "groups" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "currency" character varying NOT NULL DEFAULT 'usd', PRIMARY KEY ("id")); +-- Create "documents" table +CREATE TABLE "documents" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "title" character varying NOT NULL, "path" character varying NOT NULL, "group_documents" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "documents_groups_documents" FOREIGN KEY ("group_documents") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "locations" table +CREATE TABLE "locations" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "group_locations" uuid NOT NULL, "location_children" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "locations_groups_locations" FOREIGN KEY ("group_locations") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "locations_locations_children" FOREIGN KEY ("location_children") REFERENCES "locations" ("id") ON UPDATE NO ACTION ON DELETE SET NULL); +-- Create "items" table +CREATE TABLE "items" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "import_ref" character varying NULL, "notes" character varying NULL, "quantity" bigint NOT NULL DEFAULT 1, "insured" boolean NOT NULL DEFAULT false, "archived" boolean NOT NULL DEFAULT false, "asset_id" bigint NOT NULL DEFAULT 0, "serial_number" character varying NULL, "model_number" character varying NULL, "manufacturer" character varying NULL, "lifetime_warranty" boolean NOT NULL DEFAULT false, "warranty_expires" timestamptz NULL, "warranty_details" character varying NULL, "purchase_time" timestamptz NULL, "purchase_from" character varying NULL, "purchase_price" double precision NOT NULL DEFAULT 0, "sold_time" timestamptz NULL, "sold_to" character varying NULL, "sold_price" double precision NOT NULL DEFAULT 0, "sold_notes" character varying NULL, "group_items" uuid NOT NULL, "item_children" uuid NULL, "location_items" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "items_groups_items" FOREIGN KEY ("group_items") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "items_items_children" FOREIGN KEY ("item_children") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE SET NULL, CONSTRAINT "items_locations_items" FOREIGN KEY ("location_items") REFERENCES "locations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "item_archived" to table: "items" +CREATE INDEX "item_archived" ON "items" ("archived"); +-- Create index "item_asset_id" to table: "items" +CREATE INDEX "item_asset_id" ON "items" ("asset_id"); +-- 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_name" to table: "items" +CREATE INDEX "item_name" ON "items" ("name"); +-- Create index "item_serial_number" to table: "items" +CREATE INDEX "item_serial_number" ON "items" ("serial_number"); +-- Create "attachments" table +CREATE TABLE "attachments" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "type" character varying NOT NULL DEFAULT 'attachment', "primary" boolean NOT NULL DEFAULT false, "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 UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "attachments_items_attachments" FOREIGN KEY ("item_attachments") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "users" table +CREATE TABLE "users" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "email" character varying NOT NULL, "password" character varying NOT NULL, "is_superuser" boolean NOT NULL DEFAULT false, "superuser" boolean NOT NULL DEFAULT false, "role" character varying NOT NULL DEFAULT 'user', "activated_on" timestamptz NULL, "group_users" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "users_groups_users" FOREIGN KEY ("group_users") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "users_email_key" to table: "users" +CREATE UNIQUE INDEX "users_email_key" ON "users" ("email"); +-- Create "auth_tokens" table +CREATE TABLE "auth_tokens" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "token" bytea NOT NULL, "expires_at" timestamptz 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 UPDATE NO ACTION 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 "auth_roles" table +CREATE TABLE "auth_roles" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "role" character varying NOT NULL DEFAULT 'user', "auth_tokens_roles" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "auth_roles_auth_tokens_roles" FOREIGN KEY ("auth_tokens_roles") REFERENCES "auth_tokens" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "auth_roles_auth_tokens_roles_key" to table: "auth_roles" +CREATE UNIQUE INDEX "auth_roles_auth_tokens_roles_key" ON "auth_roles" ("auth_tokens_roles"); +-- Create "group_invitation_tokens" table +CREATE TABLE "group_invitation_tokens" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "token" bytea NOT NULL, "expires_at" timestamptz NOT NULL, "uses" bigint NOT NULL DEFAULT 0, "group_invitation_tokens" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "group_invitation_tokens_groups_invitation_tokens" FOREIGN KEY ("group_invitation_tokens") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "group_invitation_tokens_token_key" to table: "group_invitation_tokens" +CREATE UNIQUE INDEX "group_invitation_tokens_token_key" ON "group_invitation_tokens" ("token"); +-- Create "item_fields" table +CREATE TABLE "item_fields" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "type" character varying NOT NULL, "text_value" character varying NULL, "number_value" bigint NULL, "boolean_value" boolean NOT NULL DEFAULT false, "time_value" timestamptz NOT NULL, "item_fields" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "item_fields_items_fields" FOREIGN KEY ("item_fields") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "labels" table +CREATE TABLE "labels" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "color" character varying NULL, "group_labels" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "labels_groups_labels" FOREIGN KEY ("group_labels") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- 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_item_id" FOREIGN KEY ("item_id") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "label_items_label_id" FOREIGN KEY ("label_id") REFERENCES "labels" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "maintenance_entries" table +CREATE TABLE "maintenance_entries" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "date" timestamptz NULL, "scheduled_date" timestamptz NULL, "name" character varying NOT NULL, "description" character varying NULL, "cost" double precision NOT NULL DEFAULT 0, "item_id" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "maintenance_entries_items_maintenance_entries" FOREIGN KEY ("item_id") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "notifiers" table +CREATE TABLE "notifiers" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "url" character varying NOT NULL, "is_active" boolean NOT NULL DEFAULT true, "group_id" uuid NOT NULL, "user_id" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "notifiers_groups_notifiers" FOREIGN KEY ("group_id") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "notifiers_users_notifiers" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "notifier_group_id" to table: "notifiers" +CREATE INDEX "notifier_group_id" ON "notifiers" ("group_id"); +-- Create index "notifier_group_id_is_active" to table: "notifiers" +CREATE INDEX "notifier_group_id_is_active" ON "notifiers" ("group_id", "is_active"); +-- Create index "notifier_user_id" to table: "notifiers" +CREATE INDEX "notifier_user_id" ON "notifiers" ("user_id"); +-- Create index "notifier_user_id_is_active" to table: "notifiers" +CREATE INDEX "notifier_user_id_is_active" ON "notifiers" ("user_id", "is_active"); diff --git a/backend/internal/data/migrations/postgres/atlas.sum b/backend/internal/data/migrations/postgres/atlas.sum new file mode 100644 index 00000000..c74d1c00 --- /dev/null +++ b/backend/internal/data/migrations/postgres/atlas.sum @@ -0,0 +1,2 @@ +h1:3BgMK0p+hw7ygZFPnd9W6s3IzPNXa87zYGty2LeNjEg= +20241027025146_init.sql h1:PJhm+pjGRtFfgmGu7MwJo8+bVelVfU5LB+LZ/c8nnGE= diff --git a/backend/internal/data/migrations/migrations/20220929052825_init.sql b/backend/internal/data/migrations/sqlite3/20220929052825_init.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20220929052825_init.sql rename to backend/internal/data/migrations/sqlite3/20220929052825_init.sql diff --git a/backend/internal/data/migrations/migrations/20221001210956_group_invitations.sql b/backend/internal/data/migrations/sqlite3/20221001210956_group_invitations.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221001210956_group_invitations.sql rename to backend/internal/data/migrations/sqlite3/20221001210956_group_invitations.sql diff --git a/backend/internal/data/migrations/migrations/20221009173029_add_user_roles.sql b/backend/internal/data/migrations/sqlite3/20221009173029_add_user_roles.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221009173029_add_user_roles.sql rename to backend/internal/data/migrations/sqlite3/20221009173029_add_user_roles.sql diff --git a/backend/internal/data/migrations/migrations/20221020043305_allow_nesting_types.sql b/backend/internal/data/migrations/sqlite3/20221020043305_allow_nesting_types.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221020043305_allow_nesting_types.sql rename to backend/internal/data/migrations/sqlite3/20221020043305_allow_nesting_types.sql diff --git a/backend/internal/data/migrations/migrations/20221101041931_add_archived_field.sql b/backend/internal/data/migrations/sqlite3/20221101041931_add_archived_field.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221101041931_add_archived_field.sql rename to backend/internal/data/migrations/sqlite3/20221101041931_add_archived_field.sql diff --git a/backend/internal/data/migrations/migrations/20221113012312_add_asset_id_field.sql b/backend/internal/data/migrations/sqlite3/20221113012312_add_asset_id_field.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221113012312_add_asset_id_field.sql rename to backend/internal/data/migrations/sqlite3/20221113012312_add_asset_id_field.sql diff --git a/backend/internal/data/migrations/migrations/20221203053132_add_token_roles.sql b/backend/internal/data/migrations/sqlite3/20221203053132_add_token_roles.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221203053132_add_token_roles.sql rename to backend/internal/data/migrations/sqlite3/20221203053132_add_token_roles.sql diff --git a/backend/internal/data/migrations/migrations/20221205230404_drop_document_tokens.sql b/backend/internal/data/migrations/sqlite3/20221205230404_drop_document_tokens.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221205230404_drop_document_tokens.sql rename to backend/internal/data/migrations/sqlite3/20221205230404_drop_document_tokens.sql diff --git a/backend/internal/data/migrations/migrations/20221205234214_add_maintenance_entries.sql b/backend/internal/data/migrations/sqlite3/20221205234214_add_maintenance_entries.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221205234214_add_maintenance_entries.sql rename to backend/internal/data/migrations/sqlite3/20221205234214_add_maintenance_entries.sql diff --git a/backend/internal/data/migrations/migrations/20221205234812_cascade_delete_roles.sql b/backend/internal/data/migrations/sqlite3/20221205234812_cascade_delete_roles.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221205234812_cascade_delete_roles.sql rename to backend/internal/data/migrations/sqlite3/20221205234812_cascade_delete_roles.sql diff --git a/backend/internal/data/migrations/migrations/20230227024134_add_scheduled_date.sql b/backend/internal/data/migrations/sqlite3/20230227024134_add_scheduled_date.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20230227024134_add_scheduled_date.sql rename to backend/internal/data/migrations/sqlite3/20230227024134_add_scheduled_date.sql diff --git a/backend/internal/data/migrations/migrations/20230305065819_add_notifier_types.sql b/backend/internal/data/migrations/sqlite3/20230305065819_add_notifier_types.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20230305065819_add_notifier_types.sql rename to backend/internal/data/migrations/sqlite3/20230305065819_add_notifier_types.sql diff --git a/backend/internal/data/migrations/migrations/20230305071524_add_group_id_to_notifiers.sql b/backend/internal/data/migrations/sqlite3/20230305071524_add_group_id_to_notifiers.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20230305071524_add_group_id_to_notifiers.sql rename to backend/internal/data/migrations/sqlite3/20230305071524_add_group_id_to_notifiers.sql diff --git a/backend/internal/data/migrations/migrations/20231006213457_add_primary_attachment_flag.sql b/backend/internal/data/migrations/sqlite3/20231006213457_add_primary_attachment_flag.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20231006213457_add_primary_attachment_flag.sql rename to backend/internal/data/migrations/sqlite3/20231006213457_add_primary_attachment_flag.sql diff --git a/backend/internal/data/migrations/migrations/atlas.sum b/backend/internal/data/migrations/sqlite3/atlas.sum similarity index 100% rename from backend/internal/data/migrations/migrations/atlas.sum rename to backend/internal/data/migrations/sqlite3/atlas.sum diff --git a/backend/internal/data/repo/repo_group.go b/backend/internal/data/repo/repo_group.go index 2498052e..74d5d171 100644 --- a/backend/internal/data/repo/repo_group.go +++ b/backend/internal/data/repo/repo_group.go @@ -161,16 +161,10 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, gid uuid.UUID, // Get the Totals for the Start and End of the Given Time Period q := ` SELECT - (SELECT Sum(purchase_price) - FROM items - WHERE group_items = ? - AND items.archived = false - AND items.created_at < ?) AS price_at_start, - (SELECT Sum(purchase_price) - FROM items - WHERE group_items = ? - AND items.archived = false - AND items.created_at < ?) AS price_at_end + SUM(CASE WHEN created_at < $1 THEN purchase_price ELSE 0 END) AS price_at_start, + SUM(CASE WHEN created_at < $2 THEN purchase_price ELSE 0 END) AS price_at_end + FROM items + WHERE group_items = $3 AND archived = false ` stats := ValueOverTime{ Start: start, @@ -180,7 +174,7 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, gid uuid.UUID, var maybeStart *float64 var maybeEnd *float64 - row := r.db.Sql().QueryRowContext(ctx, q, gid, sqliteDateFormat(start), gid, sqliteDateFormat(end)) + row := r.db.Sql().QueryRowContext(ctx, q, sqliteDateFormat(start), sqliteDateFormat(end), gid) err := row.Scan(&maybeStart, &maybeEnd) if err != nil { return nil, err @@ -229,20 +223,25 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, gid uuid.UUID, func (r *GroupRepository) StatsGroup(ctx context.Context, gid uuid.UUID) (GroupStatistics, error) { q := ` SELECT - (SELECT COUNT(*) FROM users WHERE group_users = ?) AS total_users, - (SELECT COUNT(*) FROM items WHERE group_items = ? AND items.archived = false) AS total_items, - (SELECT COUNT(*) FROM locations WHERE group_locations = ?) AS total_locations, - (SELECT COUNT(*) FROM labels WHERE group_labels = ?) AS total_labels, - (SELECT SUM(purchase_price*quantity) FROM items WHERE group_items = ? AND items.archived = false) AS total_item_price, - (SELECT COUNT(*) - FROM items - WHERE group_items = ? - AND items.archived = false - AND (items.lifetime_warranty = true OR items.warranty_expires > date()) - ) AS total_with_warranty + COUNT(DISTINCT u.id) as total_users, + COUNT(DISTINCT CASE WHEN i.archived = false THEN i.id END) as total_items, + COUNT(DISTINCT l.id) as total_locations, + COUNT(DISTINCT lb.id) as total_labels, + SUM(CASE WHEN i.archived = false THEN i.purchase_price * i.quantity ELSE 0 END) as total_item_price, + COUNT(DISTINCT CASE + WHEN i.archived = false + AND (i.lifetime_warranty = true OR i.warranty_expires > $1) + THEN i.id + END) as total_with_warranty +FROM groups g + LEFT JOIN users u ON u.group_users = g.id + LEFT JOIN items i ON i.group_items = g.id + LEFT JOIN locations l ON l.group_locations = g.id + LEFT JOIN labels lb ON lb.group_labels = g.id +WHERE g.id = $2; ` var stats GroupStatistics - row := r.db.Sql().QueryRowContext(ctx, q, gid, gid, gid, gid, gid, gid) + row := r.db.Sql().QueryRowContext(ctx, q, sqliteDateFormat(time.Now()), gid) var maybeTotalItemPrice *float64 var maybeTotalWithWarranty *int diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index cffc910f..830205a8 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -92,12 +92,12 @@ type ( // Purchase PurchaseTime types.Date `json:"purchaseTime"` - PurchaseFrom string `json:"purchaseFrom" validate:"max=255"` + PurchaseFrom string `json:"purchaseFrom" validate:"max=255"` PurchasePrice float64 `json:"purchasePrice" extensions:"x-nullable,x-omitempty"` // Sold SoldTime types.Date `json:"soldTime"` - SoldTo string `json:"soldTo" validate:"max=255"` + SoldTo string `json:"soldTo" validate:"max=255"` SoldPrice float64 `json:"soldPrice" extensions:"x-nullable,x-omitempty"` SoldNotes string `json:"soldNotes"` diff --git a/backend/internal/data/repo/repo_locations.go b/backend/internal/data/repo/repo_locations.go index 7ece91b2..d820a166 100644 --- a/backend/internal/data/repo/repo_locations.go +++ b/backend/internal/data/repo/repo_locations.go @@ -121,7 +121,7 @@ func (r *LocationRepository) GetAll(ctx context.Context, gid uuid.UUID, filter L FROM locations WHERE - locations.group_locations = ? {{ FILTER_CHILDREN }} + locations.group_locations = $1 {{ FILTER_CHILDREN }} ORDER BY locations.name ASC ` @@ -278,8 +278,8 @@ func (r *LocationRepository) PathForLoc(ctx context.Context, gid, locID uuid.UUI query := `WITH RECURSIVE location_path AS ( SELECT id, name, location_children FROM locations - WHERE id = ? -- Replace ? with the ID of the item's location - AND group_locations = ? -- Replace ? with the ID of the group + WHERE id = $1 -- Replace ? with the ID of the item's location + AND group_locations = $2 -- Replace ? with the ID of the group UNION ALL @@ -332,7 +332,7 @@ func (r *LocationRepository) Tree(ctx context.Context, gid uuid.UUID, tq TreeQue 'location' AS node_type FROM locations WHERE location_children IS NULL - AND group_locations = ? + AND group_locations = $1 UNION ALL SELECT c.id, @@ -355,10 +355,8 @@ func (r *LocationRepository) Tree(ctx context.Context, gid uuid.UUID, tq TreeQue SELECT * FROM location_tree - {{ WITH_ITEMS_FROM }} - ) tree ORDER BY node_type DESC, -- sort locations before items level, diff --git a/backend/internal/sys/config/conf.go b/backend/internal/sys/config/conf.go index 8b7b23c3..9b531416 100644 --- a/backend/internal/sys/config/conf.go +++ b/backend/internal/sys/config/conf.go @@ -18,14 +18,15 @@ const ( type Config struct { conf.Version - Mode string `yaml:"mode" conf:"default:development"` // development or production - Web WebConfig `yaml:"web"` - Storage Storage `yaml:"storage"` - Log LoggerConf `yaml:"logger"` - Mailer MailerConf `yaml:"mailer"` - Demo bool `yaml:"demo"` - Debug DebugConf `yaml:"debug"` - Options Options `yaml:"options"` + Mode string `yaml:"mode" conf:"default:development"` // development or production + Web WebConfig `yaml:"web"` + Storage Storage `yaml:"storage"` + Database Database `yaml:"database"` + Log LoggerConf `yaml:"logger"` + Mailer MailerConf `yaml:"mailer"` + Demo bool `yaml:"demo"` + Debug DebugConf `yaml:"debug"` + Options Options `yaml:"options"` } type Options struct { diff --git a/backend/internal/sys/config/conf_database.go b/backend/internal/sys/config/conf_database.go index 2c6a7610..06d44bfe 100644 --- a/backend/internal/sys/config/conf_database.go +++ b/backend/internal/sys/config/conf_database.go @@ -6,6 +6,16 @@ const ( type Storage struct { // Data is the path to the root directory - Data string `yaml:"data" conf:"default:./.data"` - SqliteURL string `yaml:"sqlite-url" conf:"default:./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1"` + Data string `yaml:"data" conf:"default:./.data"` +} + +type Database struct { + Driver string `yaml:"driver" conf:"default:sqlite3"` + Username string `yaml:"username"` + Password string `yaml:"password"` + Host string `yaml:"host"` + Port string `yaml:"port"` + Database string `yaml:"database"` + SslMode string `yaml:"ssl_mode"` + SqlitePath string `yaml:"sqlite_path" conf:"default:./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1"` } diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index e018b4b1..a865e8ea 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -26,8 +26,10 @@ module.exports = { "vue/no-setup-props-destructure": 0, "vue/no-multiple-template-root": 0, "vue/no-v-model-argument": 0, + "vue/no-v-html": 0, "@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/ban-ts-comment": 0, + "tailwindcss/no-custom-classname": 0, "@typescript-eslint/no-unused-vars": [ "error", {