diff --git a/cmd/api/main.go b/cmd/api/main.go index 38e6ac4..f5d6a01 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "database/sql" "flag" "log" "log/slog" @@ -14,6 +13,7 @@ import ( "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/requests" "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/transport" + "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" ) @@ -47,8 +47,8 @@ type application struct { var version = "0.1.0" -func openDB(cfg config) (*sql.DB, error) { - db, err := sql.Open("sqlite3", cfg.db.dsn) +func openDB(cfg config) (*sqlx.DB, error) { + db, err := sqlx.Open("sqlite3", cfg.db.dsn) if err != nil { return nil, err } diff --git a/db/migrations/000001_create_foundry_state.up.sql b/db/migrations/000001_create_foundry_state.up.sql index 1d6daf0..ff4bc9f 100644 --- a/db/migrations/000001_create_foundry_state.up.sql +++ b/db/migrations/000001_create_foundry_state.up.sql @@ -690,18 +690,18 @@ CREATE TABLE IF NOT EXISTS package_warnings_data ( ); CREATE TABLE IF NOT EXISTS package_warnings_data_warning ( - id TEXT PRIMARY KEY, + id INTEGER PRIMARY KEY, - warning VARCHAR(128) NOT NULL, + value VARCHAR(128) NOT NULL, package_warnings_data_id TEXT, FOREIGN KEY (package_warnings_data_id) REFERENCES package_warnings_data(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS package_warnings_data_error ( - id TEXT PRIMARY KEY, + id INTEGER PRIMARY KEY, - error VARCHAR(128) NOT NULL, + value VARCHAR(128) NOT NULL, package_warnings_data_id TEXT, FOREIGN KEY (package_warnings_data_id) REFERENCES package_warnings_data(id) ON DELETE CASCADE @@ -1175,7 +1175,7 @@ CREATE TABLE IF NOT EXISTS files_storage ( ); CREATE TABLE IF NOT EXISTS setup_language ( - id TEXT PRIMARY KEY, + id INTEGER PRIMARY KEY, label VARCHAR(128) NOT NULL, @@ -1189,7 +1189,7 @@ CREATE TABLE IF NOT EXISTS setup_language_module ( label VARCHAR(128) NOT NULL, path VARCHAR(128) NOT NULL, - setup_language_id TEXT, + setup_language_id INTEGER, FOREIGN KEY (setup_language_id) REFERENCES setup_language(id) ON DELETE CASCADE ); diff --git a/go.mod b/go.mod index 99b16b5..e984511 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( github.com/in-toto/in-toto-golang v0.9.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/julienschmidt/httprouter v1.3.0 // indirect github.com/klauspost/compress v1.18.3 // indirect diff --git a/go.sum b/go.sum index 5258397..edfd418 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8= cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= @@ -152,6 +153,7 @@ github.com/go-openapi/swag/yamlutils v0.25.3 h1:LKTJjCn/W1ZfMec0XDL4Vxh8kyAnv1or github.com/go-openapi/swag/yamlutils v0.25.3/go.mod h1:Y7QN6Wc5DOBXK14/xeo1cQlq0EA0wvLoSv13gDQoCao= github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw= github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= @@ -195,6 +197,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= @@ -207,10 +211,12 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.39 h1:sIwSjlJGOaRJjw44/HXaeTblZMjseqr6OOio1tz/+JI= github.com/mattn/go-sqlite3 v1.14.39/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= diff --git a/internal/foundry/models/db/foundry_state.go b/internal/foundry/models/db/foundry_state.go index a345805..04a074e 100644 --- a/internal/foundry/models/db/foundry_state.go +++ b/internal/foundry/models/db/foundry_state.go @@ -5,6 +5,8 @@ import ( "database/sql" "errors" "time" + + "github.com/jmoiron/sqlx" ) type StateType int @@ -44,7 +46,7 @@ type Options struct { } type FoundryStateModel struct { - DB *sql.DB + DB *sqlx.DB } func (m FoundryStateModel) Insert(state *FoundryState) error { diff --git a/internal/foundry/models/db/models.go b/internal/foundry/models/db/models.go index de557d2..20effae 100644 --- a/internal/foundry/models/db/models.go +++ b/internal/foundry/models/db/models.go @@ -1,6 +1,6 @@ package db -import "database/sql" +import "github.com/jmoiron/sqlx" // var ( // ErrRecordNotFound = errors.New("record not found") @@ -21,7 +21,7 @@ type Models struct { // } // } -func NewModels(db *sql.DB) *Models { +func NewModels(db *sqlx.DB) *Models { return &Models{ FoundryState: FoundryStateModel{DB: db}, } diff --git a/internal/foundry/temp_models/db/files.go b/internal/foundry/temp_models/db/files.go index c03c862..44b7d5e 100644 --- a/internal/foundry/temp_models/db/files.go +++ b/internal/foundry/temp_models/db/files.go @@ -1,7 +1,99 @@ package db +import ( + "context" + "fmt" + "sync" + + "github.com/jmoiron/sqlx" +) + type Files struct { ID uint - Storages []string + Storages []FilesStorage +} + +func (f *Files) Insert(tx *sqlx.Tx, data *InsertId[uint]) error { + query := fmt.Sprintf(` + INSERT INTO files (%s) + VALUES ($1) + RETURNING id`, data.fieldName) + + args := []any{data.id} + + err := tx.QueryRowx(query, args...).Scan(&f.ID) + if err != nil { + return err + } + + syncDB := SyncDbOperations{ + wg: sync.WaitGroup{}, + errChan: make(chan error), + } + + InsertSliceParallel[uint](&syncDB, tx, f.Storages, &InsertId[uint]{id: f.ID}) + + return syncDB.Wait() +} + +func (f *Files) InsertCtx(ctx context.Context, tx *sqlx.Tx, data *InsertId[uint]) error { + query := fmt.Sprintf(` + INSERT INTO files (%s) + VALUES ($1) + RETURNING id`, data.fieldName) + + args := []any{data.id} + + err := tx.QueryRowxContext(ctx, query, args...).Scan(&f.ID) + if err != nil { + return err + } + + syncDB := SyncDbOperations{ + wg: sync.WaitGroup{}, + errChan: make(chan error), + } + + InsertSliceParallel(&syncDB, tx, f.Storages, &InsertId[uint]{id: f.ID}) + + return syncDB.Wait() +} + +type FilesStorage struct { + ID uint + + Storage string +} + +func (f FilesStorage) Insert(tx *sqlx.Tx, data *InsertId[uint]) error { + query := ` + INSERT INTO files (files_id, storage) + VALUES ($1, $2) + RETURNING id` + + args := []any{data.id, f.Storage} + + err := tx.QueryRowx(query, args...).Scan(&f.ID) + if err != nil { + return err + } + + return nil +} + +func (f FilesStorage) InsertCtx(ctx context.Context, tx *sqlx.Tx, data *InsertId[uint]) error { + const query = ` + INSERT INTO featured_content (files_id, storage) + VALUES ($1, $2) + RETURNING id` + + args := []any{data.id, f.Storage} + + err := tx.QueryRowxContext(ctx, query, args...).Scan(&f.ID) + if err != nil { + return err + } + + return nil } diff --git a/internal/foundry/temp_models/db/foundry_data.go b/internal/foundry/temp_models/db/foundry_data.go index 1e35cc3..a91430b 100644 --- a/internal/foundry/temp_models/db/foundry_data.go +++ b/internal/foundry/temp_models/db/foundry_data.go @@ -1,6 +1,9 @@ package db -import "time" +import ( + "database/sql" + "time" +) type FoundryDataType string @@ -17,3 +20,7 @@ type FoundryData struct { Game *Game CreatedAt time.Time } + +type FoundryDataModel struct { + DB *sql.DB +} diff --git a/internal/foundry/temp_models/db/game.go b/internal/foundry/temp_models/db/game.go index d9761f8..3375d60 100644 --- a/internal/foundry/temp_models/db/game.go +++ b/internal/foundry/temp_models/db/game.go @@ -11,13 +11,13 @@ type Game struct { Files Files Options GameOptions Release Release - World World - System System + World *World + System *System CoreUpdate CoreUpdate SystemUpdate SystemUpdate ActiveUsers []string - Modules []Module - PackageWarnings []PackageWarning + Modules []*Module + PackageWarnings []*PackageWarning Packs []Pack Messages []Message Combats []Combat diff --git a/internal/foundry/temp_models/db/language.go b/internal/foundry/temp_models/db/language.go index 39c1d29..af4d97a 100644 --- a/internal/foundry/temp_models/db/language.go +++ b/internal/foundry/temp_models/db/language.go @@ -1,5 +1,11 @@ package db +import ( + "context" + + "github.com/jmoiron/sqlx" +) + type Language struct { ID uint @@ -15,9 +21,77 @@ type SetupLanguage struct { Modules []SetupLanguageModule } +func (l SetupLanguage) Insert(tx *sqlx.Tx, data *InsertId[uint]) error { + const query = ` + INSERT INTO setup_language (setup_id, label) + VALUES ($1, $2) + RETURNING id` + + args := []any{data.id, l.Label} + + err := tx.QueryRowx(query, args...).Scan(&l.ID) + if err != nil { + return err + } + + InsertSlice(tx, l.Modules, &InsertId[string]{id: l.ID}) + + return nil +} + +func (l SetupLanguage) InsertCtx(ctx context.Context, tx *sqlx.Tx, data *InsertId[uint]) error { + const query = ` + INSERT INTO setup_language (setup_id, label) + VALUES ($1, $2) + RETURNING id` + + args := []any{data.id, l.Label} + + err := tx.QueryRowxContext(ctx, query, args...).Scan(&l.ID) + if err != nil { + return err + } + + InsertSlice(tx, l.Modules, &InsertId[string]{id: l.ID}) + + return nil +} + type SetupLanguageModule struct { ID string Label string Path string } + +func (l SetupLanguageModule) Insert(tx *sqlx.Tx, data *InsertId[string]) error { + const query = ` + INSERT INTO setup_language_module (setup_language_id, label, path) + VALUES ($1, $2, $3) + RETURNING id` + + args := []any{data.id, l.Label, l.Path} + + err := tx.QueryRowx(query, args...).Scan(&l.ID) + if err != nil { + return err + } + + return nil +} + +func (l SetupLanguageModule) InsertCtx(ctx context.Context, tx *sqlx.Tx, data *InsertId[string]) error { + const query = ` + INSERT INTO setup_language_module (setup_language_id, label, path) + VALUES ($1, $2, $3) + RETURNING id` + + args := []any{data.id, l.Label, l.Path} + + err := tx.QueryRowxContext(ctx, query, args...).Scan(&l.ID) + if err != nil { + return err + } + + return nil +} diff --git a/internal/foundry/temp_models/db/options.go b/internal/foundry/temp_models/db/options.go index 350d8c0..ffc67b2 100644 --- a/internal/foundry/temp_models/db/options.go +++ b/internal/foundry/temp_models/db/options.go @@ -1,5 +1,11 @@ package db +import ( + "context" + + "github.com/jmoiron/sqlx" +) + type GameOptions struct { ID uint @@ -8,23 +14,95 @@ type GameOptions struct { Port int } +func (g *GameOptions) Insert(tx *sqlx.Tx, gameId *InsertId[uint]) error { + query := ` + INSERT INTO featured_content (game_id, language, update_channel, port) + VALUES ($1, $2, $3, $4) + RETURNING id` + + args := []any{gameId.id, g.Language, g.UpdateChannel, g.Port} + + err := tx.QueryRowx(query, args...).Scan(&g.ID) + if err != nil { + return err + } + + return nil +} + +func (g *GameOptions) InsertCtx(ctx context.Context, tx *sqlx.Tx, gameId *InsertId[uint]) error { + query := ` + INSERT INTO featured_content (game_id, language, update_channel, port) + VALUES ($1, $2, $3, $4) + RETURNING id` + + args := []any{gameId.id, g.Language, g.UpdateChannel, g.Port} + + err := tx.QueryRowxContext(ctx, query, args...).Scan(&g.ID) + if err != nil { + return err + } + + return nil +} + type SetupOptions struct { ID uint - CompressSocket bool - CompressStatic bool CSSTheme string DataPath string - Fullscreen bool Hostname string - HotReload bool Language string LocalHostname string + UpdateChannel string Port int + CompressSocket bool + CompressStatic bool + Fullscreen bool + HotReload bool ProxySSL bool Telemetry bool - UpdateChannel string Upnp bool DeleteNEDB bool NoBackups bool } + +func (s *SetupOptions) Insert(tx *sqlx.Tx, setupId *InsertId[uint]) error { + query := ` + INSERT INTO featured_content (setup_id, compress_socket, compress_static, css_theme, data_path, + fullscreen, hostname, hot_reload, language, local_hostname, port, + proxy_ssl, telemetry, update_channel, upnp, delete_nedb, no_backups) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) + RETURNING id` + + args := []any{setupId.id, s.CompressSocket, s.CompressStatic, s.CSSTheme, s.DataPath, + s.Fullscreen, s.Hostname, s.HotReload, s.Language, s.LocalHostname, s.Port, + s.ProxySSL, s.Telemetry, s.UpdateChannel, s.Upnp, s.DeleteNEDB, s.NoBackups} + + err := tx.QueryRowx(query, args...).Scan(&s.ID) + if err != nil { + return err + } + + return nil +} + +func (s *SetupOptions) InsertCtx(ctx context.Context, tx *sqlx.Tx, setupId *InsertId[uint]) error { + query := ` + INSERT INTO featured_content (setup_id, compress_socket, compress_static, css_theme, data_path, + fullscreen, hostname, hot_reload, language, local_hostname, port, + proxy_ssl, telemetry, update_channel, upnp, delete_nedb, no_backups) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) + RETURNING id` + + args := []any{setupId.id, s.CompressSocket, s.CompressStatic, s.CSSTheme, s.DataPath, + s.Fullscreen, s.Hostname, s.HotReload, s.Language, s.LocalHostname, s.Port, + s.ProxySSL, s.Telemetry, s.UpdateChannel, s.Upnp, s.DeleteNEDB, s.NoBackups} + + err := tx.QueryRowxContext(ctx, query, args...).Scan(&s.ID) + if err != nil { + return err + } + + return nil +} diff --git a/internal/foundry/temp_models/db/package_warnings.go b/internal/foundry/temp_models/db/package_warnings.go index 512056b..416b2f0 100644 --- a/internal/foundry/temp_models/db/package_warnings.go +++ b/internal/foundry/temp_models/db/package_warnings.go @@ -1,5 +1,12 @@ package db +import ( + "context" + "fmt" + + "github.com/jmoiron/sqlx" +) + type PackageWarningsData struct { ID string @@ -10,9 +17,89 @@ type PackageWarningsData struct { Manifest string } +func (p *PackageWarningsData) InsertSliceType(tx *sqlx.Tx, data *InsertId[string], sliceType string) error { + query := fmt.Sprintf(` + INSERT INTO package_warnings_data (%s, id, type, reinstallable, manifest) + VALUES ($1, $2, $3, $4, $5)`, data.fieldName) + + args := []any{data.id, p.ID, p.Type, p.Reinstallable, p.Manifest} + + _, err := tx.Exec(query, args...) + if err != nil { + return err + } + + return nil +} + +func (p *PackageWarningsData) Insert(tx *sqlx.Tx, data *InsertId[string]) error { + query := fmt.Sprintf(` + INSERT INTO package_warnings_data (%s, id, type, reinstallable, manifest) + VALUES ($1, $2, $3, $4, $5)`, data.fieldName) + + args := []any{data.id, p.ID, p.Type, p.Reinstallable, p.Manifest} + + _, err := tx.Exec(query, args...) + if err != nil { + return err + } + + return nil +} + +func (p *PackageWarningsData) InsertCtx(ctx context.Context, tx *sqlx.Tx, data *InsertId[string]) error { + query := fmt.Sprintf(` + INSERT INTO package_warnings_data (%s, id, type, reinstallable, manifest) + VALUES ($1, $2, $3, $4, $5) + RETURNING id`, data.fieldName) + + args := []any{data.id, p.ID, p.Type, p.Reinstallable, p.Manifest} + + _, err := tx.ExecContext(ctx, query, args...) + if err != nil { + return err + } + + return nil +} + type PackageWarning struct { ID string Key string Value PackageWarningsData } + +func (p *PackageWarning) Insert(tx *sqlx.Tx, data *InsertId[uint]) error { + query := fmt.Sprintf(` + INSERT INTO package_warnings (%s, id) + VALUES ($1, $2)`, data.fieldName) + + args := []any{data.id, p.ID} + + _, err := tx.Execx(query, args...) + if err != nil { + return err + } + + InsertWithCtx(tx, &p.Value, &InsertId[string]{id: p.ID}) + + return nil +} + +func (p *PackageWarning) InsertCtx(ctx context.Context, tx *sqlx.Tx, data *InsertId[uint]) error { + query := fmt.Sprintf(` + INSERT INTO package_warnings (%s, id) + VALUES ($1, $2)`, data.fieldName) + + args := []any{data.id, p.ID} + + _, err := tx.Exec(query, args...) + if err != nil { + return err + } + + InsertWithCtx(tx, &p.Value, &InsertId[string]{id: p.ID}) + + return nil +} diff --git a/internal/foundry/temp_models/db/relationships.go b/internal/foundry/temp_models/db/relationships.go index 1dbe6d7..d35340a 100644 --- a/internal/foundry/temp_models/db/relationships.go +++ b/internal/foundry/temp_models/db/relationships.go @@ -3,7 +3,10 @@ package db type Relationships struct { ID uint - Data []RelationshipsData + Systems []RelationshipsData + Requires []RelationshipsData + Recommends []RelationshipsData + Conflicts []RelationshipsData } type RelationshipsData struct { diff --git a/internal/foundry/temp_models/db/release.go b/internal/foundry/temp_models/db/release.go index 7f0033c..b5d418b 100644 --- a/internal/foundry/temp_models/db/release.go +++ b/internal/foundry/temp_models/db/release.go @@ -1,14 +1,53 @@ package db +import ( + "context" + "fmt" + + "github.com/jmoiron/sqlx" +) + type Release struct { ID uint Generation int - Channel string - Suffix string Build int NodeVersion int MaxGeneration int MaxStableGeneration int Time int64 + Channel string + Suffix string +} + +func (r *Release) Insert(tx *sqlx.Tx, data *InsertId[uint]) error { + query := fmt.Sprintf(` + INSERT INTO release_ (%s, generation, channel, suffix, build, node_version, max_generation, max_stable_generation, time) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING id`, data.fieldName) + + args := []any{data.id, r.Generation, r.Channel, r.Suffix, r.Build, r.NodeVersion, r.MaxGeneration, r.MaxStableGeneration, r.Time} + + err := tx.QueryRowx(query, args...).Scan(&r.ID) + if err != nil { + return err + } + + return nil +} + +func (r *Release) InsertCtx(ctx context.Context, tx *sqlx.Tx, data *InsertId[uint]) error { + query := fmt.Sprintf(` + INSERT INTO release_ (%s, generation, channel, suffix, build, node_version, max_generation, max_stable_generation, time) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING id`, data.fieldName) + + args := []any{data.id, r.Generation, r.Channel, r.Suffix, r.Build, r.NodeVersion, r.MaxGeneration, r.MaxStableGeneration, r.Time} + + err := tx.QueryRowxContext(ctx, query, args...).Scan(&r.ID) + if err != nil { + return err + } + + return nil } diff --git a/internal/foundry/temp_models/db/setup.go b/internal/foundry/temp_models/db/setup.go index b7c17c8..e9356bd 100644 --- a/internal/foundry/temp_models/db/setup.go +++ b/internal/foundry/temp_models/db/setup.go @@ -1,21 +1,72 @@ package db +import ( + "context" + "sync" + "time" + + "github.com/jmoiron/sqlx" +) + type Setup struct { - ID uint + ID uint + CreatedAt time.Time IsAdmin bool IsSetup bool - CoreUpdate CoreUpdate - FeaturedContent FeaturedContent - Files Files - Options SetupOptions - Release Release - Languages []SetupLanguage - Modules []Module - News []News - PackageWarnings []PackageWarning - Systems []System - Worlds []World + CoreUpdate CoreUpdate //+ + FeaturedContent FeaturedContent //+ + Files Files //+ + Options *SetupOptions //+ + Release Release //+ + Languages []SetupLanguage //+ + Modules []*Module + News []News //+ + PackageWarnings []*PackageWarning + Systems []*System + Worlds []*World +} + +func (s *Setup) Insert(db *sqlx.DB) error { + tx := db.MustBegin() + defer tx.Rollback() + + const query = ` + INSERT INTO setup (is_admin, is_setup) + VALUES ($1, $2) + RETURNING id, created_at` + + args := []any{s.IsAdmin, s.IsSetup} + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + err := tx.QueryRowContext(ctx, query, args...).Scan(&s.ID, &s.CreatedAt) + if err != nil { + return err + } + id := InsertId[uint]{id: s.ID, fieldName: "setup_id"} + + dataSync := SyncDbOperations{ + wg: sync.WaitGroup{}, + errChan: make(chan error), + } + defer close(dataSync.errChan) + + go InsertSliceParallel(&dataSync, tx, s.News, &id) + go InsertSliceParallel(&dataSync, tx, s.Languages, &id) + + InsertWithCtx(tx, &s.FeaturedContent, &id) + InsertWithCtx(tx, &s.Files, &id) + InsertWithCtx(tx, &s.Release, &id) + InsertWithCtx(tx, &s.CoreUpdate, &id) + + err = dataSync.Wait() + if err != nil { + return err + } + + return tx.Commit() } type FeaturedContent struct { @@ -27,6 +78,38 @@ type FeaturedContent struct { Image string } +func (f *FeaturedContent) Insert(tx *sqlx.Tx, setup *InsertId[uint]) error { + const query = ` + INSERT INTO featured_content (setup_id, title, caption, url, image) + VALUES ($1, $2, $3, $4, $5) + RETURNING id` + + args := []any{setup.id, f.Title, f.Caption, f.URL, f.Image} + + err := tx.QueryRowx(query, args...).Scan(&f.ID) + if err != nil { + return err + } + + return nil +} + +func (f *FeaturedContent) InsertCtx(ctx context.Context, tx *sqlx.Tx, setup *InsertId[uint]) error { + const query = ` + INSERT INTO featured_content (setup_id, title, caption, url, image) + VALUES ($1, $2, $3, $4, $5) + RETURNING id` + + args := []any{setup.id, f.Title, f.Caption, f.URL, f.Image} + + err := tx.QueryRowxContext(ctx, query, args...).Scan(&f.ID) + if err != nil { + return err + } + + return nil +} + type News struct { ID uint @@ -35,3 +118,35 @@ type News struct { URL string Image string } + +func (n News) Insert(tx *sqlx.Tx, setup *InsertId[uint]) error { + const insertNews = ` + INSERT INTO news (setup_id, title, caption, url, image) + VALUES ($1, $2, $3, $4, $5) + RETURNING id` + + args := []any{setup.id, n.Title, n.Caption, n.URL, n.Image} + + err := tx.QueryRow(insertNews, args...).Scan(&n.ID) + if err != nil { + return err + } + + return nil +} + +func (n News) InsertCtx(ctx context.Context, tx *sqlx.Tx, setup *InsertId[uint]) error { + const insertNews = ` + INSERT INTO news (setup_id, title, caption, url, image) + VALUES ($1, $2, $3, $4, $5) + RETURNING id` + + args := []any{setup.id, n.Title, n.Caption, n.URL, n.Image} + + err := tx.QueryRowContext(ctx, insertNews, args...).Scan(&n.ID) + if err != nil { + return err + } + + return nil +} diff --git a/internal/foundry/temp_models/db/update.go b/internal/foundry/temp_models/db/update.go index a610c23..d4c8fcc 100644 --- a/internal/foundry/temp_models/db/update.go +++ b/internal/foundry/temp_models/db/update.go @@ -1,5 +1,12 @@ package db +import ( + "context" + "fmt" + + "github.com/jmoiron/sqlx" +) + type CoreUpdate struct { ID uint @@ -12,6 +19,38 @@ type CoreUpdate struct { WillDisableModules bool } +func (c *CoreUpdate) Insert(tx *sqlx.Tx, data *InsertId[uint]) error { + query := fmt.Sprintf(` + INSERT INTO core_update (%s, has_update, can_update, could_reach_website, slow_response, version, channel, will_disable_modules) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING id`, data.fieldName) + + args := []any{data.id, c.HasUpdate, c.CanUpdate, c.CouldReachWebsite, c.SlowResponse, c.Version, c.Channel, c.WillDisableModules} + + err := tx.QueryRowx(query, args...).Scan(&c.ID) + if err != nil { + return err + } + + return nil +} + +func (c *CoreUpdate) InsertCtx(ctx context.Context, tx *sqlx.Tx, data *InsertId[uint]) error { + query := fmt.Sprintf(` + INSERT INTO core_update (%s, has_update, can_update, could_reach_website, slow_response, version, channel, will_disable_modules) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING id`, data.fieldName) + + args := []any{data.id, c.HasUpdate, c.CanUpdate, c.CouldReachWebsite, c.SlowResponse, c.Version, c.Channel, c.WillDisableModules} + + err := tx.QueryRowxContext(ctx, query, args...).Scan(&c.ID) + if err != nil { + return err + } + + return nil +} + type SystemUpdate struct { ID uint diff --git a/internal/foundry/temp_models/db/utils.go b/internal/foundry/temp_models/db/utils.go new file mode 100644 index 0000000..44c2819 --- /dev/null +++ b/internal/foundry/temp_models/db/utils.go @@ -0,0 +1,116 @@ +package db + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/jmoiron/sqlx" +) + +type AllowedIds interface { + ~uint | ~string +} + +type InsertId[T AllowedIds] struct { + id T + fieldName string + tableName string +} + +type SyncDbOperations struct { + errChan chan error + wg sync.WaitGroup +} + +func (s *SyncDbOperations) Wait() error { + return WaitSync(&s.wg, s.errChan) +} + +func WaitSync(wg *sync.WaitGroup, errChan chan error) error { + wgDone := make(chan struct{}) + + go func() { + wg.Wait() + close(wgDone) + }() + + select { + case <-wgDone: + return nil + case err := <-errChan: + close(wgDone) + return err + } +} + +type Insertable[T AllowedIds] interface { + Insert(tx *sqlx.Tx, relId *InsertId[T]) error + InsertCtx(ctx context.Context, tx *sqlx.Tx, relId *InsertId[T]) error +} + +func InsertWithCtx[T AllowedIds, I Insertable[T]](tx *sqlx.Tx, data I, relId *InsertId[T]) error { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + return data.InsertCtx(ctx, tx, relId) +} + +func InsertSlice[T AllowedIds, I Insertable[T]](tx *sqlx.Tx, data []I, relId *InsertId[T]) error { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + for i := range data { + err := data[i].InsertCtx(ctx, tx, relId) + if err != nil { + return err + } + } + return nil +} + +func InsertSliceParallel[T AllowedIds, I Insertable[T]](syncDb *SyncDbOperations, tx *sqlx.Tx, data []I, relId *InsertId[T]) { + syncDb.wg.Add(1) + + wg := sync.WaitGroup{} + errChan := make(chan error) + defer close(errChan) + + for i := range data { + go func() { + wg.Add(1) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + errChan <- data[i].InsertCtx(ctx, tx, relId) + + wg.Done() + }() + } + + err := WaitSync(&wg, errChan) + if err != nil { + syncDb.errChan <- err + } + syncDb.wg.Done() +} + +func InsertSimpleSliceToTable[T AllowedIds, I Insertable[T]](tx *sqlx.Tx, data []I, relId *InsertId[T]) error { + query := fmt.Sprintf(` + INSERT INTO %s (%s, value) + VALUES (:id, :value)`, relId.tableName, relId.fieldName) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + for i := range data { + _, err := tx.ExecContext(ctx, query, relId.id, data[i]) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/foundry/temp_models/json/files.go b/internal/foundry/temp_models/json/files.go index 5480609..888ed5b 100644 --- a/internal/foundry/temp_models/json/files.go +++ b/internal/foundry/temp_models/json/files.go @@ -12,7 +12,10 @@ func (f *Files) ToDB(dest *db.Files) bool { return false } - copy(dest.Storages, f.Storages) + dest.Storages = make([]db.FilesStorage, len(f.Storages)) + for i := range f.Storages { + dest.Storages[i].Storage = f.Storages[i] + } return true } diff --git a/internal/foundry/temp_models/json/module.go b/internal/foundry/temp_models/json/module.go index 8c929da..6ca62aa 100644 --- a/internal/foundry/temp_models/json/module.go +++ b/internal/foundry/temp_models/json/module.go @@ -44,50 +44,54 @@ type Module struct { // ModulesFlags ModulesFlags `json:"flags,omitempty"` } -func (m *Module) ToDB(dest *db.Module) bool { +func (m *Module) ToDB(dest **db.Module) bool { if dest == nil { return false } - dest.ID = m.ID - dest.Title = m.Title - dest.Description = m.Description - dest.URL = m.URL - dest.License = m.License - dest.Readme = m.Readme - dest.Bugs = m.Bugs - dest.Changelog = m.Changelog - dest.Version = m.Version - dest.Socket = m.Socket - dest.Manifest = m.Manifest - dest.Download = m.Download - dest.Protected = m.Protected - dest.Exclusive = m.Exclusive - dest.PersistentStorage = m.PersistentStorage - dest.CoreTranslation = m.CoreTranslation - dest.Library = m.Library - dest.Availability = m.Availability - dest.Locked = m.Locked - dest.Owned = m.Owned - dest.HasStorage = m.HasStorage - dest.Active = m.Active + module := &db.Module{ + ID: m.ID, + Title: m.Title, + Description: m.Description, + URL: m.URL, + License: m.License, + Readme: m.Readme, + Bugs: m.Bugs, + Changelog: m.Changelog, + Version: m.Version, + Socket: m.Socket, + Manifest: m.Manifest, + Download: m.Download, + Protected: m.Protected, + Exclusive: m.Exclusive, + PersistentStorage: m.PersistentStorage, + CoreTranslation: m.CoreTranslation, + Library: m.Library, + Availability: m.Availability, + Locked: m.Locked, + Owned: m.Owned, + HasStorage: m.HasStorage, + Active: m.Active, + } - copy(dest.Scripts, m.Scripts) - copy(dest.Esmodules, m.Esmodules) - copy(dest.Tags, m.Tags) + copy(module.Scripts, m.Scripts) + copy(module.Esmodules, m.Esmodules) + copy(module.Tags, m.Tags) - m.Compatibility.ToDB(&dest.Compatibility) - m.Relationships.ToDB(&dest.Relationships) - m.DocumentTypes.ToDB(&dest.DocumentTypes) + m.Compatibility.ToDB(&module.Compatibility) + m.Relationships.ToDB(&module.Relationships) + m.DocumentTypes.ToDB(&module.DocumentTypes) wg := sync.WaitGroup{} - go CopySliceToDBParallel(&wg, &dest.Authors, m.Authors) - go CopySliceToDBParallel(&wg, &dest.Media, m.Media) - go CopySliceToDBParallel(&wg, &dest.Styles, m.Styles) - go CopySliceToDBParallel(&wg, &dest.Languages, m.Languages) - go CopySliceToDBParallel(&wg, &dest.Packs, m.Packs) - go CopySliceToDBParallel(&wg, &dest.PackFolders, m.PackFolders) + go CopySliceToDBParallel(&wg, &module.Authors, m.Authors) + go CopySliceToDBParallel(&wg, &module.Media, m.Media) + go CopySliceToDBParallel(&wg, &module.Styles, m.Styles) + go CopySliceToDBParallel(&wg, &module.Languages, m.Languages) + go CopySliceToDBParallel(&wg, &module.Packs, m.Packs) + go CopySliceToDBParallel(&wg, &module.PackFolders, m.PackFolders) wg.Wait() + *dest = module + return true } diff --git a/internal/foundry/temp_models/json/system.go b/internal/foundry/temp_models/json/system.go index 0b8986c..83884be 100644 --- a/internal/foundry/temp_models/json/system.go +++ b/internal/foundry/temp_models/json/system.go @@ -43,49 +43,53 @@ type System struct { // SystemFlags SystemFlags `json:"flags"` } -func (s *System) ToDB(dest *db.System) bool { +func (s *System) ToDB(dest **db.System) bool { if dest == nil { return false } - dest.ID = s.ID - dest.Title = s.Title - dest.Description = s.Description - dest.URL = s.URL - dest.License = s.License - dest.Bugs = s.Bugs - dest.Changelog = s.Changelog - dest.Version = s.Version - dest.Socket = s.Socket - dest.Manifest = s.Manifest - dest.Download = s.Download - dest.Protected = s.Protected - dest.Exclusive = s.Exclusive - dest.PersistentStorage = s.PersistentStorage - dest.Background = s.Background - dest.PrimaryTokenAttribute = s.PrimaryTokenAttribute - dest.Availability = s.Availability - dest.Locked = s.Locked - dest.Owned = s.Owned - dest.HasStorage = s.HasStorage + system := &db.System{ + ID: s.ID, + Title: s.Title, + Description: s.Description, + URL: s.URL, + License: s.License, + Bugs: s.Bugs, + Changelog: s.Changelog, + Version: s.Version, + Socket: s.Socket, + Manifest: s.Manifest, + Download: s.Download, + Protected: s.Protected, + Exclusive: s.Exclusive, + PersistentStorage: s.PersistentStorage, + Background: s.Background, + PrimaryTokenAttribute: s.PrimaryTokenAttribute, + Availability: s.Availability, + Locked: s.Locked, + Owned: s.Owned, + HasStorage: s.HasStorage, + } - copy(dest.Scripts, s.Scripts) - copy(dest.Esmodules, s.Esmodules) - copy(dest.Tags, s.Tags) + copy(system.Scripts, s.Scripts) + copy(system.Esmodules, s.Esmodules) + copy(system.Tags, s.Tags) - s.Compatibility.ToDB(&dest.Compatibility) - s.Relationships.ToDB(&dest.Relationships) - s.DocumentTypes.ToDB(&dest.DocumentTypes) - s.Grid.ToDB(&dest.Grid) + s.Compatibility.ToDB(&system.Compatibility) + s.Relationships.ToDB(&system.Relationships) + s.DocumentTypes.ToDB(&system.DocumentTypes) + s.Grid.ToDB(&system.Grid) wg := sync.WaitGroup{} - go CopySliceToDBParallel(&wg, &dest.Authors, s.Authors) - go CopySliceToDBParallel(&wg, &dest.Media, s.Media) - go CopySliceToDBParallel(&wg, &dest.Styles, s.Styles) - go CopySliceToDBParallel(&wg, &dest.Languages, s.Languages) - go CopySliceToDBParallel(&wg, &dest.Packs, s.Packs) - go CopySliceToDBParallel(&wg, &dest.PackFolders, s.PackFolders) + go CopySliceToDBParallel(&wg, &system.Authors, s.Authors) + go CopySliceToDBParallel(&wg, &system.Media, s.Media) + go CopySliceToDBParallel(&wg, &system.Styles, s.Styles) + go CopySliceToDBParallel(&wg, &system.Languages, s.Languages) + go CopySliceToDBParallel(&wg, &system.Packs, s.Packs) + go CopySliceToDBParallel(&wg, &system.PackFolders, s.PackFolders) wg.Wait() + *dest = system + return true } diff --git a/internal/foundry/temp_models/json/utils.go b/internal/foundry/temp_models/json/utils.go index dd83616..d4f26bc 100644 --- a/internal/foundry/temp_models/json/utils.go +++ b/internal/foundry/temp_models/json/utils.go @@ -28,8 +28,8 @@ func HotbarToDB(dest *[]db.UserHotbar, src map[int]string) { } } -func PackageWarningsToDB(dest *[]db.PackageWarning, src map[string]PackageWarningsData) { - *dest = make([]db.PackageWarning, len(src)) +func PackageWarningsToDB(dest *[]*db.PackageWarning, src map[string]PackageWarningsData) { + *dest = make([]*db.PackageWarning, len(src)) i := 0 for k, v := range src { diff --git a/internal/foundry/temp_models/json/world.go b/internal/foundry/temp_models/json/world.go index 48233fd..0166076 100644 --- a/internal/foundry/temp_models/json/world.go +++ b/internal/foundry/temp_models/json/world.go @@ -43,48 +43,52 @@ type World struct { // Flags any `json:"flags"` } -func (w *World) ToDB(dest *db.World) bool { +func (w *World) ToDB(dest **db.World) bool { if dest == nil { return false } - dest.ID = w.ID - dest.Title = w.Title - dest.Description = w.Description - dest.Version = w.Version - dest.Socket = w.Socket - dest.Protected = w.Protected - dest.Exclusive = w.Exclusive - dest.PersistentStorage = w.PersistentStorage - dest.System = w.System - dest.Background = w.Background - dest.JoinTheme = w.JoinTheme - dest.CoreVersion = w.CoreVersion - dest.SystemVersion = w.SystemVersion - dest.LastPlayed = w.LastPlayed - dest.Playtime = w.Playtime - dest.NextSession = w.NextSession - dest.Availability = w.Availability - dest.Locked = w.Locked - dest.Owned = w.Owned - dest.HasStorage = w.HasStorage + world := &db.World{ + ID: w.ID, + Title: w.Title, + Description: w.Description, + Version: w.Version, + Socket: w.Socket, + Protected: w.Protected, + Exclusive: w.Exclusive, + PersistentStorage: w.PersistentStorage, + System: w.System, + Background: w.Background, + JoinTheme: w.JoinTheme, + CoreVersion: w.CoreVersion, + SystemVersion: w.SystemVersion, + LastPlayed: w.LastPlayed, + Playtime: w.Playtime, + NextSession: w.NextSession, + Availability: w.Availability, + Locked: w.Locked, + Owned: w.Owned, + HasStorage: w.HasStorage, + } - copy(dest.Scripts, w.Scripts) - copy(dest.Esmodules, w.Esmodules) - copy(dest.Tags, w.Tags) + copy(world.Scripts, w.Scripts) + copy(world.Esmodules, w.Esmodules) + copy(world.Tags, w.Tags) - w.Compatibility.ToDB(&dest.Compatibility) - w.Relationships.ToDB(&dest.Relationships) + w.Compatibility.ToDB(&world.Compatibility) + w.Relationships.ToDB(&world.Relationships) wg := sync.WaitGroup{} - go CopySliceToDBParallel(&wg, &dest.Authors, w.Authors) - go CopySliceToDBParallel(&wg, &dest.Media, w.Media) - go CopySliceToDBParallel(&wg, &dest.Styles, w.Styles) - go CopySliceToDBParallel(&wg, &dest.Languages, w.Languages) - go CopySliceToDBParallel(&wg, &dest.Packs, w.Packs) - go CopySliceToDBParallel(&wg, &dest.PackFolders, w.PackFolders) + go CopySliceToDBParallel(&wg, &world.Authors, w.Authors) + go CopySliceToDBParallel(&wg, &world.Media, w.Media) + go CopySliceToDBParallel(&wg, &world.Styles, w.Styles) + go CopySliceToDBParallel(&wg, &world.Languages, w.Languages) + go CopySliceToDBParallel(&wg, &world.Packs, w.Packs) + go CopySliceToDBParallel(&wg, &world.PackFolders, w.PackFolders) wg.Wait() + *dest = world + return true } diff --git a/internal/foundry/transport/transport.go b/internal/foundry/transport/transport.go index d7a7ef7..6a8b09d 100644 --- a/internal/foundry/transport/transport.go +++ b/internal/foundry/transport/transport.go @@ -1,7 +1,6 @@ package transport import ( - "database/sql" "log/slog" "sync" "time" @@ -10,6 +9,7 @@ import ( "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/requests" "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/types" "github.com/gorilla/websocket" + "github.com/jmoiron/sqlx" ) type FoundryTransport struct { @@ -32,7 +32,7 @@ type FoundryTransport struct { Logger *slog.Logger } -func NewFoundryTransport(dbConn *sql.DB, logger *slog.Logger, httpConfig *requests.FoundryHttpRequest) *FoundryTransport { +func NewFoundryTransport(dbConn *sqlx.DB, logger *slog.Logger, httpConfig *requests.FoundryHttpRequest) *FoundryTransport { return &FoundryTransport{ CurrWsId: 0, ExchangeChan: types.ExchangeChannels{