From 153812d79ff97908aa3bf882f6acd8c02b1118e1 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 10 Sep 2023 16:49:16 +0200 Subject: [PATCH] add database seed from file --- .gitignore | 1 + README.md | 13 ++- go.mod | 1 + scripts/seeddb/main.go | 183 +++++++++++++++++++++-------------------- seed.example.yaml | 43 ++++++++++ 5 files changed, 150 insertions(+), 91 deletions(-) create mode 100644 seed.example.yaml diff --git a/.gitignore b/.gitignore index 8e5f23c..418b353 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ vite.config.js.timestamp-* vite.config.ts.timestamp-* target tmp +seed.yaml diff --git a/README.md b/README.md index c0ac541..d01b981 100644 --- a/README.md +++ b/README.md @@ -26,19 +26,24 @@ Requirements: - Redis 6.0 or later - Node.js (latest version) - MinIO **if using avatars, flags, or data exports** (_not_ required otherwise) +- [Air](https://github.com/cosmtrek/air) for live reloading the backend ### Setup -1. Create a PostgreSQL user and database (the user should own the database). +1. Create a PostgreSQL user and database (the user should own the database). For example: `create user pronouns with password 'password'; create database pronouns with owner pronouns;` 2. Copy `.env.example` in the repository root to a new file named `.env` and fill out the required options. 3. Run `go run -v . database migrate` to initialize the database, then optionally `go run -v . database seed` to insert a test user. -4. Run `go run -v . web` to run the backend. -5. Copy `frontend/.env.example` into `frontend/.env` and tweak as necessary. -6. cd into the `frontend` directory and run `pnpm dev` to run the frontend. +4. Run `pnpm dev`. Alternatively, if you don't want the backend to live reload, run `go run -v . web`, + then change to the `frontend/` directory and run `pnpm dev`. See [`docs/production.md`](/docs/production.md#configuration) for more information about keys in the backend and frontend `.env` files. +### Seeding + +To seed the database with some data, create a `seed.yaml` file, then use `go run -v . database seed`. +For the file format, refer to the `Seed` struct in `scripts/seeddb`. + ## License Copyright (C) 2022 Sam diff --git a/go.mod b/go.mod index 862413f..360bd54 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( go.uber.org/zap v1.24.0 golang.org/x/oauth2 v0.7.0 google.golang.org/api v0.118.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( diff --git a/scripts/seeddb/main.go b/scripts/seeddb/main.go index d18b354..c2e9f26 100644 --- a/scripts/seeddb/main.go +++ b/scripts/seeddb/main.go @@ -1,15 +1,42 @@ package seeddb import ( - "fmt" + "log" "os" "codeberg.org/pronounscc/pronouns.cc/backend/db" "github.com/jackc/pgx/v5/pgxpool" "github.com/joho/godotenv" "github.com/urfave/cli/v2" + "gopkg.in/yaml.v3" ) +type Seed struct { + Users []SeedUser `yaml:"users"` +} + +type SeedUser struct { + Username string `yaml:"username"` + DisplayName *string `yaml:"displayName"` + Bio *string `yaml:"bio"` + Links []string `yaml:"links"` + Names []db.FieldEntry `yaml:"names"` + Pronouns []db.PronounEntry `yaml:"pronouns"` + Fields []db.Field `yaml:"fields"` + Members []SeedMember `yaml:"members"` +} + +type SeedMember struct { + Name string `yaml:"name"` + DisplayName *string `yaml:"displayName"` + Bio string `yaml:"bio"` + Links []string `yaml:"links"` + Names []db.FieldEntry `yaml:"names"` + Pronouns []db.PronounEntry `yaml:"pronouns"` + Fields []db.Field `yaml:"fields"` + Members []SeedMember `yaml:"members"` +} + var Command = &cli.Command{ Name: "seed", Usage: "Seed the database with test data", @@ -19,7 +46,7 @@ var Command = &cli.Command{ func run(c *cli.Context) error { err := godotenv.Load() if err != nil { - fmt.Println("error loading .env file:", err) + log.Println("error loading .env file:", err) return err } @@ -27,112 +54,94 @@ func run(c *cli.Context) error { pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL")) if err != nil { - fmt.Println("error opening database:", err) + log.Println("error opening database:", err) return err } defer pool.Close() - fmt.Println("opened database") + log.Println("opened database") pg := &db.DB{Pool: pool} + // read seed file + seedFile, err := os.ReadFile("seed.yaml") + if err != nil { + log.Println("error opening seed.yaml:", err) + return err + } + + var seed Seed + err = yaml.Unmarshal(seedFile, &seed) + if err != nil { + log.Println("error reading seed.yaml:", err) + return err + } + tx, err := pg.Begin(ctx) if err != nil { - fmt.Println("error beginning transaction:", err) + log.Println("error beginning transaction:", err) return err } + defer tx.Rollback(ctx) - u, err := pg.CreateUser(ctx, tx, "test") - if err != nil { - fmt.Println("error creating user:", err) - return err - } + for i, su := range seed.Users { + u, err := pg.CreateUser(ctx, tx, su.Username) + if err != nil { + log.Printf("error creating user #%v/%s: %v", i+1, su.Username, err) + return err + } - _, err = pg.UpdateUser(ctx, tx, u.ID, ptr("testing"), ptr("This is a bio!"), nil, ptr(false), &[]string{"https://pronouns.cc"}, nil, nil, nil) - if err != nil { - fmt.Println("error setting user info:", err) - return err - } + _, err = pg.UpdateUser(ctx, tx, u.ID, su.DisplayName, su.Bio, nil, nil, &su.Links, nil, nil, nil) + if err != nil { + log.Printf("updating user %s: %v", su.Username, err) + return err + } - err = pg.SetUserNamesPronouns(ctx, tx, u.ID, []db.FieldEntry{ - {Value: "testing 1", Status: "favourite"}, - {Value: "testing 2", Status: "okay"}, - }, []db.PronounEntry{ - {Pronouns: "it/it/its/its/itself", DisplayText: ptr("it/its"), Status: "favourite"}, - {Pronouns: "they/them/their/theirs/themself", Status: "okay"}, - }) - if err != nil { - fmt.Println("error setting pronouns:", err) - return err - } + err = pg.SetUserNamesPronouns(ctx, tx, u.ID, db.NotNull(su.Names), db.NotNull(su.Pronouns)) + if err != nil { + log.Printf("setting names/pronouns for user %s: %v", su.Username, err) + return err + } - err = pg.SetUserFields(ctx, tx, u.ID, []db.Field{ - { - Name: "Field 1", - Entries: []db.FieldEntry{ - {Value: "Favourite 1", Status: "favourite"}, - {Value: "Okay 1", Status: "okay"}, - {Value: "Jokingly 1", Status: "jokingly"}, - {Value: "Friends only 1", Status: "friends_only"}, - {Value: "Avoid 1", Status: "avoid"}, - }, - }, - { - Name: "Field 2", - Entries: []db.FieldEntry{ - {Value: "Favourite 2", Status: "favourite"}, - {Value: "Okay 2", Status: "okay"}, - {Value: "Jokingly 2", Status: "jokingly"}, - {Value: "Friends only 2", Status: "friends_only"}, - {Value: "Avoid 2", Status: "avoid"}, - }, - }, - { - Name: "Field 3", - Entries: []db.FieldEntry{ - {Value: "Favourite 3", Status: "favourite"}, - {Value: "Okay 3", Status: "okay"}, - {Value: "Jokingly 3", Status: "jokingly"}, - {Value: "Friends only 3", Status: "friends_only"}, - {Value: "Avoid 3", Status: "avoid"}, - }, - }, - { - Name: "Field 4", - Entries: []db.FieldEntry{ - {Value: "Favourite 4", Status: "favourite"}, - {Value: "Okay 4", Status: "okay"}, - {Value: "Jokingly 4", Status: "jokingly"}, - {Value: "Friends only 4", Status: "friends_only"}, - {Value: "Avoid 4", Status: "avoid"}, - }, - }, - { - Name: "Field 5", - Entries: []db.FieldEntry{ - {Value: "Favourite 5", Status: "favourite"}, - {Value: "Okay 5", Status: "okay"}, - {Value: "Jokingly 5", Status: "jokingly"}, - {Value: "Friends only 5", Status: "friends_only"}, - {Value: "Avoid 5", Status: "avoid"}, - }, - }, - }) - if err != nil { - fmt.Println("error setting fields:", err) - return err + err = pg.SetUserFields(ctx, tx, u.ID, db.NotNull(su.Fields)) + if err != nil { + log.Printf("setting fields for user %s: %v", su.Username, err) + return err + } + + log.Printf("creating members for user %s", su.Username) + + for _, sm := range su.Members { + m, err := pg.CreateMember(ctx, tx, u.ID, sm.Name, sm.DisplayName, sm.Bio, db.NotNull(sm.Links)) + if err != nil { + log.Printf("creating member %s: %v", sm.Name, err) + return err + } + + err = pg.SetMemberNamesPronouns(ctx, tx, m.ID, db.NotNull(sm.Names), db.NotNull(sm.Pronouns)) + if err != nil { + log.Printf("setting names/pronouns for member %s: %v", sm.Name, err) + return err + } + + err = pg.SetMemberFields(ctx, tx, m.ID, db.NotNull(sm.Fields)) + if err != nil { + log.Printf("setting fields for member %s: %v", sm.Name, err) + return err + } + + log.Printf("created member %s", sm.Name) + } + + log.Printf("created user %s", su.Username) } err = tx.Commit(ctx) if err != nil { - fmt.Println("error committing transaction:", err) + log.Println("error committing transaction:", err) return err } - fmt.Println("Created testing user with ID", u.ID, "and name", u.Username) + log.Printf("seeded database with %d users", len(seed.Users)) return nil } - -func ptr[T any](v T) *T { - return &v -} diff --git a/seed.example.yaml b/seed.example.yaml new file mode 100644 index 0000000..d4a862c --- /dev/null +++ b/seed.example.yaml @@ -0,0 +1,43 @@ +users: +- username: test1 + displayName: "Test 1 :3" + bio: | + *Hiiiiii* + I'm a user! + links: [https://pronouns.cc, https://codeberg.org/pronounscc/pronouns.cc] + names: + - value: Test + status: favourite + - value: Tester + status: okay + - value: Testington + status: avoid + pronouns: + - pronouns: they/them/their/theirs/themself + status: favourite + - pronouns: it/it/its/its/itself + displayText: it/its + status: okay + fields: + - name: Field 1 + entries: + - value: Entry 1 + status: favourite + - value: Entry 2 + status: favourite + - value: Entry 3 + status: friends_only + - name: Field 2 + entries: + - value: Entry 4 + status: avoid + - value: Entry 4 + status: okay + members: + - name: member 1 + - name: member 2 + - name: member 3 + - name: member 4 + - name: member 5 + - name: member 6 + - name: member 7