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
        34
         POSTGRES_DATABASE=onasty

      
        35
        35
         POSTGRESQL_DSN="postgres://$POSTGRES_USERNAME:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DATABASE?sslmode=disable"

      
        36
        36
         MIGRATION_DSN="postgres://$POSTGRES_USERNAME:$POSTGRES_PASSWORD@localhost:$POSTGRES_PORT/$POSTGRES_DATABASE?sslmode=disable"

      
        
        37
        +SEED_DSN=$MIGRATION_DSN

      
        37
        38
         

      
        38
        39
         REDIS_ADDR="redis:6379"

      
        39
        40
         CACHE_USERS_TTL=1h

      
M .github/workflows/golang.yml
···
        21
        21
                   cache-dependency-path: go.mod

      
        22
        22
         

      
        23
        23
               - name: Build API

      
        24
        
        -        run: go build -o .bin/onasty ./cmd/server/

      
        
        24
        +        run: go build -o .bin/onasty ./cmd/api/

      
        
        25
        +

      
        
        26
        +      - name: Build mailer service

      
        
        27
        +        run: go build -o .bin/mailer ./mailer/

      
        25
        28
         

      
        26
        29
               - name: Unit tests

      
        27
        30
                 run: go test -v --short ./...

      
M Dockerfile
···
        11
        11
         ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64

      
        12
        12
         RUN --mount=type=cache,target=/root/.cache/go-build,id=onasty-go-build \

      
        13
        13
             --mount=type=cache,target=/go/pkg/mod,id=onasty-go-mod \

      
        14
        
        -    go build -trimpath -ldflags='-w -s' -o /onasty ./cmd/server

      
        
        14
        +    go build -trimpath -ldflags='-w -s' -o /onasty ./cmd/api

      
        15
        15
         

      
        16
        16
         FROM onasty:runtime

      
        17
        17
         COPY --from=builder /onasty /onasty

      
M Taskfile.yml
···
        5
        5
         

      
        6
        6
         includes:

      
        7
        7
           migrate: ./migrations/Taskfile.yml

      
        
        8
        +  seed: ./cmd/seed/Taskfile.yml

      
        8
        9
         

      
        9
        10
         env:

      
        10
        11
           DOCKER_BUILDKIT: 1

      
A cmd/seed/Taskfile.yml
···
        
        1
        +version: "3"

      
        
        2
        +tasks:

      
        
        3
        +  run:

      
        
        4
        +    desc: seeds the database

      
        
        5
        +    dir: ./cmd/seed/

      
        
        6
        +    cmd: go run *.go

      
A cmd/seed/main.go
···
        
        1
        +package main

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"fmt"

      
        
        6
        +	"log/slog"

      
        
        7
        +	"os"

      
        
        8
        +

      
        
        9
        +	"github.com/olexsmir/onasty/internal/config"

      
        
        10
        +	"github.com/olexsmir/onasty/internal/hasher"

      
        
        11
        +	"github.com/olexsmir/onasty/internal/logger"

      
        
        12
        +	"github.com/olexsmir/onasty/internal/store/psqlutil"

      
        
        13
        +)

      
        
        14
        +

      
        
        15
        +func main() {

      
        
        16
        +	if err := run(context.Background()); err != nil {

      
        
        17
        +		fmt.Fprintf(os.Stderr, "error: %v\n", err)

      
        
        18
        +		os.Exit(1)

      
        
        19
        +	}

      
        
        20
        +}

      
        
        21
        +

      
        
        22
        +func run(ctx context.Context) error {

      
        
        23
        +	cfg := config.NewConfig()

      
        
        24
        +	cfg.PostgresDSN = os.Getenv("SEED_DSN")

      
        
        25
        +

      
        
        26
        +	logger, err := logger.NewCustomLogger(cfg.LogLevel, cfg.LogFormat, cfg.LogShowLine)

      
        
        27
        +	if err != nil {

      
        
        28
        +		return err

      
        
        29
        +	}

      
        
        30
        +	slog.SetDefault(logger)

      
        
        31
        +

      
        
        32
        +	psql, err := psqlutil.Connect(ctx, cfg.PostgresDSN)

      
        
        33
        +	if err != nil {

      
        
        34
        +		return err

      
        
        35
        +	}

      
        
        36
        +

      
        
        37
        +	userHasher := hasher.NewSHA256Hasher(cfg.PasswordSalt)

      
        
        38
        +	noteHasher := hasher.NewSHA256Hasher(cfg.NotePasswordSalt)

      
        
        39
        +

      
        
        40
        +	if err := seedUsers(ctx, userHasher, psql); err != nil {

      
        
        41
        +		return fmt.Errorf("failed to seed users: %w", err)

      
        
        42
        +	}

      
        
        43
        +

      
        
        44
        +	slog.Info("Users seeded successfully")

      
        
        45
        +

      
        
        46
        +	if err := seedNotes(ctx, noteHasher, psql); err != nil {

      
        
        47
        +		return fmt.Errorf("failed to seed notes: %w", err)

      
        
        48
        +	}

      
        
        49
        +

      
        
        50
        +	slog.Info("Notes seeded successfully")

      
        
        51
        +

      
        
        52
        +	return nil

      
        
        53
        +}

      
A cmd/seed/notes.go
···
        
        1
        +package main

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"log/slog"

      
        
        6
        +	"time"

      
        
        7
        +

      
        
        8
        +	"github.com/olexsmir/onasty/internal/hasher"

      
        
        9
        +	"github.com/olexsmir/onasty/internal/store/psqlutil"

      
        
        10
        +)

      
        
        11
        +

      
        
        12
        +var notesData = []struct {

      
        
        13
        +	id                   string

      
        
        14
        +	content              string

      
        
        15
        +	slug                 string

      
        
        16
        +	burnBeforeExpiration bool

      
        
        17
        +	password             string

      
        
        18
        +	expiresAt            time.Time

      
        
        19
        +	hasAuthor            bool

      
        
        20
        +	authorID             int

      
        
        21
        +}{

      
        
        22
        +	{ //nolint:exhaustruct

      
        
        23
        +		content:              "that test note one",

      
        
        24
        +		slug:                 "one",

      
        
        25
        +		burnBeforeExpiration: false,

      
        
        26
        +	},

      
        
        27
        +	{ //nolint:exhaustruct

      
        
        28
        +		content:              "that test note two",

      
        
        29
        +		slug:                 "two",

      
        
        30
        +		burnBeforeExpiration: true,

      
        
        31
        +		password:             "",

      
        
        32
        +		expiresAt:            time.Now().Add(24 * time.Hour),

      
        
        33
        +	},

      
        
        34
        +	{ //nolint:exhaustruct

      
        
        35
        +		content:              "that passworded note",

      
        
        36
        +		slug:                 "passwd",

      
        
        37
        +		burnBeforeExpiration: false,

      
        
        38
        +		password:             "pass",

      
        
        39
        +	},

      
        
        40
        +	{ //nolint:exhaustruct

      
        
        41
        +		content:              "that note with author",

      
        
        42
        +		slug:                 "user",

      
        
        43
        +		burnBeforeExpiration: false,

      
        
        44
        +		hasAuthor:            true,

      
        
        45
        +		authorID:             0,

      
        
        46
        +	},

      
        
        47
        +	{ //nolint:exhaustruct

      
        
        48
        +		content:              "that another authored note",

      
        
        49
        +		slug:                 "user2",

      
        
        50
        +		burnBeforeExpiration: false,

      
        
        51
        +		hasAuthor:            true,

      
        
        52
        +		authorID:             0,

      
        
        53
        +	},

      
        
        54
        +	{ //nolint:exhaustruct

      
        
        55
        +		content:              "that another authored note",

      
        
        56
        +		slug:                 "user2",

      
        
        57
        +		password:             "passwd",

      
        
        58
        +		burnBeforeExpiration: false,

      
        
        59
        +		hasAuthor:            true,

      
        
        60
        +		authorID:             0,

      
        
        61
        +	},

      
        
        62
        +}

      
        
        63
        +

      
        
        64
        +func seedNotes(

      
        
        65
        +	ctx context.Context,

      
        
        66
        +	hash hasher.Hasher,

      
        
        67
        +	db *psqlutil.DB,

      
        
        68
        +) error {

      
        
        69
        +	for i, note := range notesData {

      
        
        70
        +		passwd := ""

      
        
        71
        +		if note.password != "" {

      
        
        72
        +			var err error

      
        
        73
        +			passwd, err = hash.Hash(note.password)

      
        
        74
        +			if err != nil {

      
        
        75
        +				return err

      
        
        76
        +			}

      
        
        77
        +		}

      
        
        78
        +

      
        
        79
        +		err := db.QueryRow(

      
        
        80
        +			ctx, `

      
        
        81
        +		insert into notes (content, slug, burn_before_expiration, password, expires_at)

      
        
        82
        +		values ($1, $2, $3, $4, $5)

      
        
        83
        +		on conflict (slug) do update set

      
        
        84
        +			content = excluded.content,

      
        
        85
        +			burn_before_expiration = excluded.burn_before_expiration,

      
        
        86
        +			password = excluded.password,

      
        
        87
        +			expires_at = excluded.expires_at

      
        
        88
        +		returning id`,

      
        
        89
        +			note.content,

      
        
        90
        +			note.slug,

      
        
        91
        +			note.burnBeforeExpiration,

      
        
        92
        +			passwd,

      
        
        93
        +			note.expiresAt,

      
        
        94
        +		).Scan(&notesData[i].id)

      
        
        95
        +		if err != nil {

      
        
        96
        +			return err

      
        
        97
        +		}

      
        
        98
        +

      
        
        99
        +		if note.hasAuthor {

      
        
        100
        +			slog.Info("setting author", "note", note.id, "author", note.authorID)

      
        
        101
        +			if err := setAuthor(ctx, db, notesData[i].id, usersData[note.authorID].id); err != nil {

      
        
        102
        +				return err

      
        
        103
        +			}

      
        
        104
        +		}

      
        
        105
        +	}

      
        
        106
        +

      
        
        107
        +	return nil

      
        
        108
        +}

      
        
        109
        +

      
        
        110
        +func setAuthor(

      
        
        111
        +	ctx context.Context,

      
        
        112
        +	db *psqlutil.DB,

      
        
        113
        +	noteID string,

      
        
        114
        +	authorID string,

      
        
        115
        +) error {

      
        
        116
        +	_, err := db.Exec(

      
        
        117
        +		ctx,

      
        
        118
        +		`insert into notes_authors (note_id, user_id) values ($1, $2)`,

      
        
        119
        +		noteID, authorID)

      
        
        120
        +	return err

      
        
        121
        +}

      
A cmd/seed/users.go
···
        
        1
        +package main

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"time"

      
        
        6
        +

      
        
        7
        +	"github.com/jackc/pgx/v5/pgtype"

      
        
        8
        +	"github.com/olexsmir/onasty/internal/hasher"

      
        
        9
        +	"github.com/olexsmir/onasty/internal/store/psqlutil"

      
        
        10
        +)

      
        
        11
        +

      
        
        12
        +var usersData = []struct {

      
        
        13
        +	id        string

      
        
        14
        +	email     string

      
        
        15
        +	password  string

      
        
        16
        +	activated bool

      
        
        17
        +}{

      
        
        18
        +	{ //nolint:exhaustruct

      
        
        19
        +		email:     "admin@onasty.local",

      
        
        20
        +		password:  "adminadmin",

      
        
        21
        +		activated: true,

      
        
        22
        +	},

      
        
        23
        +	{ //nolint:exhaustruct

      
        
        24
        +		email:     "users@onasty.local",

      
        
        25
        +		activated: false,

      
        
        26
        +		password:  "qwerty123",

      
        
        27
        +	},

      
        
        28
        +}

      
        
        29
        +

      
        
        30
        +func seedUsers(

      
        
        31
        +	ctx context.Context,

      
        
        32
        +	hash hasher.Hasher,

      
        
        33
        +	db *psqlutil.DB,

      
        
        34
        +) error {

      
        
        35
        +	for i, user := range usersData {

      
        
        36
        +		passwrd, err := hash.Hash(user.password)

      
        
        37
        +		if err != nil {

      
        
        38
        +			return err

      
        
        39
        +		}

      
        
        40
        +

      
        
        41
        +		var id pgtype.UUID

      
        
        42
        +		err = db.QueryRow(ctx, `

      
        
        43
        +			insert into users (email, password, activated, created_at, last_login_at)

      
        
        44
        +			values ($1, $2, $3, $4, $5)

      
        
        45
        +				on conflict (email) do update

      
        
        46
        +				set password = excluded.password

      
        
        47
        +			returning id

      
        
        48
        +		`, user.email, passwrd, user.activated, time.Now(), time.Now()).

      
        
        49
        +			Scan(&id)

      
        
        50
        +		if err != nil {

      
        
        51
        +			return err

      
        
        52
        +		}

      
        
        53
        +

      
        
        54
        +		usersData[i].id = id.String()

      
        
        55
        +	}

      
        
        56
        +

      
        
        57
        +	return nil

      
        
        58
        +}