all repos

onasty @ ebcfde16e154958e0949a5abc611dde2e9b22960

a one-time notes service
9 files changed, 245 insertions(+), 2 deletions(-)
feat: add seeder (#116)

* refactor: rename main app cmd dir

* fixup! refactor: rename main app cmd dir

* save the progress

* refactor(seed): migrate to row sql

* chore(taskfile): add seed:run task

* feat(seed): add notes seeder

* chore: fix(ignore) linter warnings

* fixup! refactor: rename main app cmd dir

* chore: add new dsn option for seeder

* fixup! refactor: rename main app cmd dir

* feat(seed): add few more seeds

* refactor(seed): remove unnecessary logs

* fix(seed): sql
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-05-30 15:58:02 +0300
Parent: c685a65
M .env.example

@@ -34,6 +34,7 @@ POSTGRES_PORT=5432

POSTGRES_DATABASE=onasty POSTGRESQL_DSN="postgres://$POSTGRES_USERNAME:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DATABASE?sslmode=disable" MIGRATION_DSN="postgres://$POSTGRES_USERNAME:$POSTGRES_PASSWORD@localhost:$POSTGRES_PORT/$POSTGRES_DATABASE?sslmode=disable" +SEED_DSN=$MIGRATION_DSN REDIS_ADDR="redis:6379" CACHE_USERS_TTL=1h
M .github/workflows/golang.yml

@@ -21,7 +21,10 @@ go-version-file: go.mod

cache-dependency-path: go.mod - name: Build API - run: go build -o .bin/onasty ./cmd/server/ + run: go build -o .bin/onasty ./cmd/api/ + + - name: Build mailer service + run: go build -o .bin/mailer ./mailer/ - name: Unit tests run: go test -v --short ./...
M Dockerfile

@@ -11,7 +11,7 @@

ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 RUN --mount=type=cache,target=/root/.cache/go-build,id=onasty-go-build \ --mount=type=cache,target=/go/pkg/mod,id=onasty-go-mod \ - go build -trimpath -ldflags='-w -s' -o /onasty ./cmd/server + go build -trimpath -ldflags='-w -s' -o /onasty ./cmd/api FROM onasty:runtime COPY --from=builder /onasty /onasty
M Taskfile.yml

@@ -5,6 +5,7 @@ - ".env"

includes: migrate: ./migrations/Taskfile.yml + seed: ./cmd/seed/Taskfile.yml env: DOCKER_BUILDKIT: 1
A cmd/seed/Taskfile.yml

@@ -0,0 +1,6 @@

+version: "3" +tasks: + run: + desc: seeds the database + dir: ./cmd/seed/ + cmd: go run *.go
A cmd/seed/main.go

@@ -0,0 +1,53 @@

+package main + +import ( + "context" + "fmt" + "log/slog" + "os" + + "github.com/olexsmir/onasty/internal/config" + "github.com/olexsmir/onasty/internal/hasher" + "github.com/olexsmir/onasty/internal/logger" + "github.com/olexsmir/onasty/internal/store/psqlutil" +) + +func main() { + if err := run(context.Background()); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} + +func run(ctx context.Context) error { + cfg := config.NewConfig() + cfg.PostgresDSN = os.Getenv("SEED_DSN") + + logger, err := logger.NewCustomLogger(cfg.LogLevel, cfg.LogFormat, cfg.LogShowLine) + if err != nil { + return err + } + slog.SetDefault(logger) + + psql, err := psqlutil.Connect(ctx, cfg.PostgresDSN) + if err != nil { + return err + } + + userHasher := hasher.NewSHA256Hasher(cfg.PasswordSalt) + noteHasher := hasher.NewSHA256Hasher(cfg.NotePasswordSalt) + + if err := seedUsers(ctx, userHasher, psql); err != nil { + return fmt.Errorf("failed to seed users: %w", err) + } + + slog.Info("Users seeded successfully") + + if err := seedNotes(ctx, noteHasher, psql); err != nil { + return fmt.Errorf("failed to seed notes: %w", err) + } + + slog.Info("Notes seeded successfully") + + return nil +}
A cmd/seed/notes.go

@@ -0,0 +1,121 @@

+package main + +import ( + "context" + "log/slog" + "time" + + "github.com/olexsmir/onasty/internal/hasher" + "github.com/olexsmir/onasty/internal/store/psqlutil" +) + +var notesData = []struct { + id string + content string + slug string + burnBeforeExpiration bool + password string + expiresAt time.Time + hasAuthor bool + authorID int +}{ + { //nolint:exhaustruct + content: "that test note one", + slug: "one", + burnBeforeExpiration: false, + }, + { //nolint:exhaustruct + content: "that test note two", + slug: "two", + burnBeforeExpiration: true, + password: "", + expiresAt: time.Now().Add(24 * time.Hour), + }, + { //nolint:exhaustruct + content: "that passworded note", + slug: "passwd", + burnBeforeExpiration: false, + password: "pass", + }, + { //nolint:exhaustruct + content: "that note with author", + slug: "user", + burnBeforeExpiration: false, + hasAuthor: true, + authorID: 0, + }, + { //nolint:exhaustruct + content: "that another authored note", + slug: "user2", + burnBeforeExpiration: false, + hasAuthor: true, + authorID: 0, + }, + { //nolint:exhaustruct + content: "that another authored note", + slug: "user2", + password: "passwd", + burnBeforeExpiration: false, + hasAuthor: true, + authorID: 0, + }, +} + +func seedNotes( + ctx context.Context, + hash hasher.Hasher, + db *psqlutil.DB, +) error { + for i, note := range notesData { + passwd := "" + if note.password != "" { + var err error + passwd, err = hash.Hash(note.password) + if err != nil { + return err + } + } + + err := db.QueryRow( + ctx, ` + insert into notes (content, slug, burn_before_expiration, password, expires_at) + values ($1, $2, $3, $4, $5) + on conflict (slug) do update set + content = excluded.content, + burn_before_expiration = excluded.burn_before_expiration, + password = excluded.password, + expires_at = excluded.expires_at + returning id`, + note.content, + note.slug, + note.burnBeforeExpiration, + passwd, + note.expiresAt, + ).Scan(&notesData[i].id) + if err != nil { + return err + } + + if note.hasAuthor { + slog.Info("setting author", "note", note.id, "author", note.authorID) + if err := setAuthor(ctx, db, notesData[i].id, usersData[note.authorID].id); err != nil { + return err + } + } + } + + return nil +} + +func setAuthor( + ctx context.Context, + db *psqlutil.DB, + noteID string, + authorID string, +) error { + _, err := db.Exec( + ctx, + `insert into notes_authors (note_id, user_id) values ($1, $2)`, + noteID, authorID) + return err +}
A cmd/seed/users.go

@@ -0,0 +1,58 @@

+package main + +import ( + "context" + "time" + + "github.com/jackc/pgx/v5/pgtype" + "github.com/olexsmir/onasty/internal/hasher" + "github.com/olexsmir/onasty/internal/store/psqlutil" +) + +var usersData = []struct { + id string + email string + password string + activated bool +}{ + { //nolint:exhaustruct + email: "admin@onasty.local", + password: "adminadmin", + activated: true, + }, + { //nolint:exhaustruct + email: "users@onasty.local", + activated: false, + password: "qwerty123", + }, +} + +func seedUsers( + ctx context.Context, + hash hasher.Hasher, + db *psqlutil.DB, +) error { + for i, user := range usersData { + passwrd, err := hash.Hash(user.password) + if err != nil { + return err + } + + var id pgtype.UUID + err = db.QueryRow(ctx, ` + insert into users (email, password, activated, created_at, last_login_at) + values ($1, $2, $3, $4, $5) + on conflict (email) do update + set password = excluded.password + returning id + `, user.email, passwrd, user.activated, time.Now(), time.Now()). + Scan(&id) + if err != nil { + return err + } + + usersData[i].id = id.String() + } + + return nil +}