// Copyright 2023 wanderer // SPDX-License-Identifier: AGPL-3.0-only package db import ( "context" "fmt" "os" "git.dotya.ml/mirre-mt/pcmt/ent" "git.dotya.ml/mirre-mt/pcmt/ent/migrate" moduser "git.dotya.ml/mirre-mt/pcmt/modules/user" "git.dotya.ml/mirre-mt/pcmt/slogging" "golang.org/x/exp/slog" ) // CtxKey serves as a key to context values for this package. type CtxKey struct{} // migrateRecreateColIdx deletes and re-creates db schema. func migrateRecreateColIdx(ctx context.Context, client *ent.Client) error { //nolint: unused slogger := ctx.Value(CtxKey{}).(*slogging.Slogger) log := *slogger log.Logger = log.Logger.With( slog.Group("pcmt extra", slog.String("module", "modules/db")), ) // deleting data is very drastic and should be done by the user. // if setup is already found, simply bail. // _, err := client.Setup.Delete().Exec(ctx) // if err != nil { // log.Errorf("failed to delete setup table: %v", err) // return err // } // // _, err = client.User.Delete().Exec(ctx) // if err != nil { // log.Errorf("failed to delete user table: %v", err) // return err // } err := client.Schema. Create( ctx, migrate.WithDropIndex(true), migrate.WithDropColumn(true), ) if err != nil { log.Errorf("failed to recreate schema resources: %v", err) return err } return err } // IsSetUp checks if the database has previously been set up. func IsSetUp(ctx context.Context, client *ent.Client) (bool, error) { slogger := ctx.Value(CtxKey{}).(*slogging.Slogger) log := *slogger log.Logger = log.Logger.With( slog.Group("pcmt extra", slog.String("module", "modules/db")), ) is, err := client.Setup. Query(). Only(ctx) if is != nil { log.Debug("apparently the db has already been set up") } else { log.Debug("apparently the db was not yet set up") } switch { case ent.IsNotFound(err) && is == nil: return false, nil case ent.IsNotSingular(err): return true, err case err != nil: return false, err case err == nil && is != nil: return true, nil } return false, nil } // PrintMigration prints the upcoming migration to stdout. func PrintMigration(ctx context.Context, client *ent.Client) error { slogger := ctx.Value(CtxKey{}).(*slogging.Slogger) log := *slogger log.Logger = log.With( slog.Group("pcmt extra", slog.String("module", "modules/db")), ) log.Info("printing the upcoming migration to stdout") if err := client.Schema.WriteTo(ctx, os.Stdout); err != nil { log.Errorf("failed to print schema changes: %v", err) return err } return nil } // SetUp attempts to automatically migrate DB schema and creates the set-up // record indicating that the DB has been set up. Optionally and only if the DB // has not been set up prior, it creates and admin user with the initPasswd and // the default username and email (see ../user/const.go). func SetUp(ctx context.Context, client *ent.Client, createAdmin bool, initPasswd string) error { // TODO: https://entgo.io/blog/2022/05/09/versioned-migrations-sum-file/ if err := client.Schema.Create(ctx); err != nil { return fmt.Errorf("failed to create schema resources: %v", err) } isSetup, err := IsSetUp(ctx, client) switch { case err == nil && isSetup: return nil case err != nil && isSetup: return err case err == nil && !isSetup: // run the setup in a transaction. tx, err := client.Tx(ctx) if err != nil { return err } txClient := tx.Client() usrCtx := context.WithValue(context.Background(), moduser.CtxKey{}, ctx.Value(CtxKey{}).(*slogging.Slogger), ) // use the "doSetUp" below, but give it the transactional client; no // code changes to "doSetUp". if err := doSetUp(usrCtx, txClient, createAdmin, initPasswd); err != nil { return rollback(tx, err) } return tx.Commit() } // the remaining scenario (not set up and err) simply returns the err. return err } func doSetUp(ctx context.Context, client *ent.Client, createAdmin bool, initPasswd string) error { if _, err := client.Setup. Create(). Save(ctx); err != nil { return err } if createAdmin { if err := moduser.CreateFirst(ctx, client, moduser.AdminUname, moduser.AdminEmail, initPasswd); err != nil { return err } } return nil } // rollback calls to tx.Rollback and wraps the given error with the rollback // error if occurred. func rollback(tx *ent.Tx, err error) error { if rerr := tx.Rollback(); rerr != nil { err = fmt.Errorf("%w: %v", err, rerr) } return err }