package client import ( "context" "database/sql" "encoding/json" "errors" "fmt" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger" _ "github.com/mattn/go-sqlite3" ) var ErrNotFound = errors.New("not found") const clientRows = `"client"."id", "client"."secret", "client"."redirect_uris", "client"."trusted_peers", "client"."name"` type ClientDB interface { GetClientByID(ctx context.Context, id string) (*model.Client, error) GetAllClients(ctx context.Context) ([]*model.Client, error) AddClient(ctx context.Context, client *model.Client) error DeleteClient(ctx context.Context, id string) error } type sqlClientDB struct { db *sql.DB } func strArrayToSlice(rawVal string) []string { var res []string if err := json.Unmarshal([]byte(rawVal), &res); err != nil { return nil } return res } func sliceToStrArray(rawVal []string) string { res, err := json.Marshal(rawVal) if err != nil { return "[]" } return string(res) } type scannable interface { Scan(dest ...any) error } func clientFromRow(row scannable) (*model.Client, error) { var res model.Client redirectURIsStr := "" trustedPeersStr := "" if err := row.Scan(&res.ID, &res.Secret, &redirectURIsStr, &trustedPeersStr, &res.Name); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return nil, fmt.Errorf("invalid format for client: %w", err) } res.ClientConfig.RedirectURIs = strArrayToSlice(redirectURIsStr) res.ClientConfig.TrustedPeers = strArrayToSlice(trustedPeersStr) return &res, nil } func (db *sqlClientDB) GetClientByID(ctx context.Context, id string) (*model.Client, error) { logger.L.Debugf("Getting client app with ID %s from DB", id) query := fmt.Sprintf(`SELECT %s FROM "client" WHERE "id" = ?`, clientRows) row := db.db.QueryRowContext(ctx, query, id) return clientFromRow(row) } func (db *sqlClientDB) GetAllClients(ctx context.Context) ([]*model.Client, error) { rows, err := db.db.QueryContext(ctx, fmt.Sprintf(`SELECT %s FROM "client"`, clientRows)) if err != nil { return nil, fmt.Errorf("failed to query clients from DB: %w", err) } var res []*model.Client for rows.Next() { clt, err := clientFromRow(rows) if err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } res = append(res, clt) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("failed to read all rows %w", err) } return res, nil } func (db *sqlClientDB) AddClient(ctx context.Context, client *model.Client) error { logger.L.Debugf("Creating client %s", client.Name) query := `INSERT INTO "client" ("id", "secret", "redirect_uris", "trusted_peers", "name") VALUES ($1, $2, $3, $4, $5)` tx, err := db.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to start transaction: %w", err) } defer func() { _ = tx.Rollback() }() if affectedRows, err := tx.ExecContext(ctx, query, client.ID, client.Secret, sliceToStrArray(client.RedirectURIs()), sliceToStrArray(client.TrustedPeers), client.Name); err != nil { return fmt.Errorf("failed to insert in DB: %w", err) } else if nbAffected, err := affectedRows.RowsAffected(); err != nil { return fmt.Errorf("failed to check number of affected rows: %w", err) } else if nbAffected != 1 { return fmt.Errorf("unexpected number of affected rows: %d", nbAffected) } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil } func (db *sqlClientDB) DeleteClient(ctx context.Context, id string) error { tx, err := db.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to start transaction: %w", err) } if _, err := tx.ExecContext(ctx, `DELETE FROM "client" WHERE "id" = ?`, id); err != nil { return fmt.Errorf("failed to exec query: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil } func New(db *sql.DB) *sqlClientDB { return &sqlClientDB{db: db} }