github.com/filecoin-project/bacalhau@v0.3.23-0.20230228154132-45c989550ace/dashboard/api/pkg/store/postgres.go (about) 1 package store 2 3 import ( 4 "context" 5 "fmt" 6 7 "embed" 8 "time" 9 10 "database/sql" 11 12 sync "github.com/bacalhau-project/golang-mutex-tracer" 13 "github.com/filecoin-project/bacalhau/dashboard/api/pkg/types" 14 "github.com/golang-migrate/migrate/v4" 15 "github.com/golang-migrate/migrate/v4/source/iofs" 16 ) 17 18 type PostgresStore struct { 19 mtx sync.RWMutex 20 connectionString string 21 db *sql.DB 22 } 23 24 func NewPostgresStore( 25 host string, 26 port int, 27 database string, 28 username string, 29 password string, 30 autoMigrate bool, 31 ) (*PostgresStore, error) { 32 connectionString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", username, password, host, port, database) 33 db, err := sql.Open("postgres", connectionString) 34 if err != nil { 35 return nil, err 36 } 37 store := &PostgresStore{ 38 connectionString: connectionString, 39 db: db, 40 } 41 store.mtx.EnableTracerWithOpts(sync.Opts{ 42 Threshold: 10 * time.Millisecond, 43 Id: "PostgresStore.mtx", 44 }) 45 if autoMigrate { 46 err = store.MigrateUp() 47 if err != nil { 48 return nil, fmt.Errorf("there was an error doing the migration: %s", err.Error()) 49 } 50 } 51 return store, nil 52 } 53 54 func (d *PostgresStore) LoadUser( 55 ctx context.Context, 56 username string, 57 ) (*types.User, error) { 58 d.mtx.RLock() 59 defer d.mtx.RUnlock() 60 var id int 61 var created time.Time 62 var hashedPassword string 63 row := d.db.QueryRow("select id, created, hashed_password from useraccount where username = $1 limit 1", username) 64 err := row.Scan(&id, &created, &hashedPassword) 65 if err != nil { 66 if err == sql.ErrNoRows { 67 return nil, fmt.Errorf("user not found: %s %s", username, err.Error()) 68 } else { 69 return nil, err 70 } 71 } 72 return &types.User{ 73 ID: id, 74 Created: created, 75 Username: username, 76 HashedPassword: hashedPassword, 77 }, nil 78 } 79 80 func (d *PostgresStore) LoadUserByID( 81 ctx context.Context, 82 queryID int, 83 ) (*types.User, error) { 84 d.mtx.RLock() 85 defer d.mtx.RUnlock() 86 var username string 87 var created time.Time 88 var hashedPassword string 89 row := d.db.QueryRow("select username, created, hashed_password from useraccount where id = $1 limit 1", queryID) 90 err := row.Scan(&username, &created, &hashedPassword) 91 if err != nil { 92 if err == sql.ErrNoRows { 93 return nil, fmt.Errorf("user not found: %s %s", username, err.Error()) 94 } else { 95 return nil, err 96 } 97 } 98 return &types.User{ 99 ID: queryID, 100 Created: created, 101 Username: username, 102 HashedPassword: hashedPassword, 103 }, nil 104 } 105 106 func (d *PostgresStore) GetJobModeration( 107 ctx context.Context, 108 queryJobID string, 109 ) (*types.JobModeration, error) { 110 d.mtx.Lock() 111 defer d.mtx.Unlock() 112 var id int 113 var jobID string 114 var userAccountID int 115 var created time.Time 116 var status string 117 var notes string 118 row := d.db.QueryRow("select id, job_id, useraccount_id, created, status, notes from job_moderation where job_id = $1 limit 1", queryJobID) 119 err := row.Scan(&id, &jobID, &userAccountID, &created, &status, ¬es) 120 if err != nil { 121 if err == sql.ErrNoRows { 122 return nil, nil 123 } else { 124 return nil, err 125 } 126 } 127 return &types.JobModeration{ 128 ID: id, 129 JobID: jobID, 130 UserAccountID: userAccountID, 131 Created: created, 132 Status: status, 133 Notes: notes, 134 }, nil 135 } 136 137 func (d *PostgresStore) GetAnnotationSummary( 138 ctx context.Context, 139 ) ([]*types.AnnotationSummary, error) { 140 sqlStatement := ` 141 select 142 annotation, 143 count(*) as count 144 from 145 job_annotation 146 group by 147 annotation 148 order by 149 annotation 150 ` 151 152 rows, err := d.db.Query(sqlStatement) 153 if err != nil { 154 return nil, err 155 } 156 defer rows.Close() 157 entries := []*types.AnnotationSummary{} 158 for rows.Next() { 159 var annotation string 160 var count int 161 if err = rows.Scan(&annotation, &count); err != nil { 162 return entries, err 163 } 164 entry := types.AnnotationSummary{ 165 Annotation: annotation, 166 Count: count, 167 } 168 entries = append(entries, &entry) 169 } 170 if err = rows.Err(); err != nil { 171 return entries, err 172 } 173 return entries, nil 174 } 175 176 func (d *PostgresStore) GetJobMonthSummary( 177 ctx context.Context, 178 ) ([]*types.JobMonthSummary, error) { 179 sqlStatement := ` 180 select 181 concat( 182 extract(year from created), 183 '-', 184 extract(month from created) 185 ) as month, 186 count(*) as count 187 from 188 job 189 group by 190 month 191 order by 192 month 193 ` 194 195 rows, err := d.db.Query(sqlStatement) 196 if err != nil { 197 return nil, err 198 } 199 defer rows.Close() 200 entries := []*types.JobMonthSummary{} 201 for rows.Next() { 202 var month string 203 var count int 204 if err = rows.Scan(&month, &count); err != nil { 205 return entries, err 206 } 207 entry := types.JobMonthSummary{ 208 Month: month, 209 Count: count, 210 } 211 entries = append(entries, &entry) 212 } 213 if err = rows.Err(); err != nil { 214 return entries, err 215 } 216 return entries, nil 217 } 218 219 func (d *PostgresStore) GetJobExecutorSummary( 220 ctx context.Context, 221 ) ([]*types.JobExecutorSummary, error) { 222 sqlStatement := ` 223 select 224 executor, 225 count(*) as count 226 from 227 job 228 group by 229 executor 230 order by 231 executor 232 ` 233 234 rows, err := d.db.Query(sqlStatement) 235 if err != nil { 236 return nil, err 237 } 238 defer rows.Close() 239 entries := []*types.JobExecutorSummary{} 240 for rows.Next() { 241 var executor string 242 var count int 243 if err = rows.Scan(&executor, &count); err != nil { 244 return entries, err 245 } 246 entry := types.JobExecutorSummary{ 247 Executor: executor, 248 Count: count, 249 } 250 entries = append(entries, &entry) 251 } 252 if err = rows.Err(); err != nil { 253 return entries, err 254 } 255 return entries, nil 256 } 257 258 func (d *PostgresStore) GetTotalJobsCount( 259 ctx context.Context, 260 ) (*types.Counter, error) { 261 var count int 262 row := d.db.QueryRow("select count(*) as count from job") 263 err := row.Scan(&count) 264 if err != nil { 265 return nil, err 266 } 267 return &types.Counter{ 268 Count: count, 269 }, nil 270 } 271 272 func (d *PostgresStore) GetTotalEventCount( 273 ctx context.Context, 274 ) (*types.Counter, error) { 275 var count int 276 row := d.db.QueryRow("select count(*) as count from job_event") 277 err := row.Scan(&count) 278 if err != nil { 279 return nil, err 280 } 281 return &types.Counter{ 282 Count: count, 283 }, nil 284 } 285 286 func (d *PostgresStore) GetTotalUserCount( 287 ctx context.Context, 288 ) (*types.Counter, error) { 289 var count int 290 row := d.db.QueryRow("select count(distinct clientid) as count from job") 291 err := row.Scan(&count) 292 if err != nil { 293 return nil, err 294 } 295 return &types.Counter{ 296 Count: count, 297 }, nil 298 } 299 300 func (d *PostgresStore) GetTotalExecutorCount( 301 ctx context.Context, 302 ) (*types.Counter, error) { 303 var count int 304 row := d.db.QueryRow("select count(distinct executor) as count from job") 305 err := row.Scan(&count) 306 if err != nil { 307 return nil, err 308 } 309 return &types.Counter{ 310 Count: count, 311 }, nil 312 } 313 314 func (d *PostgresStore) AddUser( 315 ctx context.Context, 316 username string, 317 hashedPassword string, 318 ) error { 319 d.mtx.Lock() 320 defer d.mtx.Unlock() 321 sqlStatement := ` 322 INSERT INTO useraccount (username, hashed_password) 323 VALUES ($1, $2)` 324 _, err := d.db.Exec( 325 sqlStatement, 326 username, 327 hashedPassword, 328 ) 329 if err != nil { 330 return err 331 } 332 return nil 333 } 334 335 func (d *PostgresStore) UpdateUserPassword( 336 ctx context.Context, 337 username string, 338 hashedPassword string, 339 ) error { 340 d.mtx.Lock() 341 defer d.mtx.Unlock() 342 sqlStatement := `UPDATE useraccount SET hashed_password = $1 WHERE username = $2` 343 _, err := d.db.Exec( 344 sqlStatement, 345 hashedPassword, 346 username, 347 ) 348 return err 349 } 350 351 func (d *PostgresStore) CreateJobModeration( 352 ctx context.Context, 353 moderation types.JobModeration, 354 ) error { 355 d.mtx.Lock() 356 defer d.mtx.Unlock() 357 sqlStatement := ` 358 INSERT INTO job_moderation ( 359 job_id, 360 useraccount_id, 361 status, 362 notes 363 ) 364 VALUES ($1, $2, $3, $4)` 365 _, err := d.db.Exec( 366 sqlStatement, 367 moderation.JobID, 368 moderation.UserAccountID, 369 moderation.Status, 370 moderation.Notes, 371 ) 372 if err != nil { 373 return err 374 } 375 return nil 376 } 377 378 //go:embed migrations/*.sql 379 var fs embed.FS 380 381 func (d *PostgresStore) GetMigrations() (*migrate.Migrate, error) { 382 files, err := iofs.New(fs, "migrations") 383 if err != nil { 384 return nil, err 385 } 386 migrations, err := migrate.NewWithSourceInstance( 387 "iofs", 388 files, 389 fmt.Sprintf("%s&&x-migrations-table=dashboard_schema_migrations", d.connectionString), 390 ) 391 if err != nil { 392 return nil, err 393 } 394 return migrations, nil 395 } 396 397 func (d *PostgresStore) MigrateUp() error { 398 migrations, err := d.GetMigrations() 399 if err != nil { 400 return err 401 } 402 err = migrations.Up() 403 if err != migrate.ErrNoChange { 404 return err 405 } 406 return nil 407 } 408 409 func (d *PostgresStore) MigrateDown() error { 410 migrations, err := d.GetMigrations() 411 if err != nil { 412 return err 413 } 414 err = migrations.Down() 415 if err != migrate.ErrNoChange { 416 return err 417 } 418 return nil 419 }