package accounts // to generate StoreWithContext //go:generate concurry -s Store import ( "context" "errors" "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" "ruben/inventory2/consts" "ruben/inventory2/logging" ) type ( Store struct { log *logging.Logger db *pgxpool.Pool } Account struct { AccountIDs OAuthUser Email string } OAuthUser struct { UserID string } AccountShop struct { AccountShopIDs Name string } Listing struct { AccountShopListingIDs SKU string Name string Description string Count int64 } ) func NewStore(logger *logging.Logger, db *pgxpool.Pool) *Store { return &Store{ log: logger, db: db, } } func (db *Store) WithContext(ctx context.Context) *StoreWithContext { return NewStoreWithContext(ctx, db) } func (db *Store) CreateAccount(ctx context.Context, userID, email string) (Account, error) { rows, err := db.db.Query( ctx, ` INSERT INTO accounts ( user_id, email ) VALUES ( @user_id, @email ) ON CONFLICT DO NOTHING RETURNING account_id `, pgx.NamedArgs{ "user_id": userID, "email": email, }, ) if err != nil { return Account{}, fmt.Errorf("failed to perform query: %w", err) } acctID, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[int64]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return Account{}, fmt.Errorf("account already exists: %w", consts.ErrConflict) } return Account{}, fmt.Errorf("failed to scan row: %w", err) } return Account{ OAuthUser: OAuthUser{ UserID: userID, }, AccountIDs: AccountIDs{ AccountID: acctID, }, Email: email, }, nil } func (db *Store) GetAccount(ctx context.Context, id int64) (Account, error) { rows, err := db.db.Query( ctx, "SELECT email, user_id FROM accounts WHERE account_id = @account_id", pgx.NamedArgs{ "account_id": id, }, ) if err != nil { return Account{}, fmt.Errorf("failed to perform query: %w", err) } type Row struct { Email string User_id string } r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return Account{}, consts.ErrNotFound } return Account{}, fmt.Errorf("failed to scan row: %w", err) } return Account{ AccountIDs: AccountIDs{ AccountID: id, }, OAuthUser: OAuthUser{ r.User_id, }, Email: r.Email, }, nil } func (db *Store) GetAccountByUserID(ctx context.Context, userID string) (Account, error) { rows, err := db.db.Query( ctx, `SELECT email, account_id FROM accounts WHERE user_id = @user_id`, pgx.NamedArgs{ "user_id": userID, }, ) if err != nil { return Account{}, fmt.Errorf("failed to perform query: %w", err) } type Row struct { Email string Account_ID int64 } r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return Account{}, consts.ErrNotFound } return Account{}, fmt.Errorf("failed to scan row: %w", err) } return Account{ AccountIDs: AccountIDs{ AccountID: r.Account_ID, }, OAuthUser: OAuthUser{ UserID: userID, }, Email: r.Email, }, nil } func (db *Store) GetAccountByEmail(ctx context.Context, email string) (Account, error) { rows, err := db.db.Query( ctx, "SELECT account_id, user_id FROM accounts WHERE email = @email", pgx.NamedArgs{ "email": email, }, ) if err != nil { return Account{}, fmt.Errorf("failed to perform query: %w", err) } type Row struct { Account_id int64 User_id string } r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return Account{}, consts.ErrNotFound } return Account{}, fmt.Errorf("failed to scan row: %w", err) } return Account{ AccountIDs: AccountIDs{ AccountID: r.Account_id, }, OAuthUser: OAuthUser{ UserID: r.User_id, }, Email: email, }, nil } func (db *Store) GetUserAndAccountByAccessToken(ctx context.Context, accessToken string) (OAuthUser, *Account, error) { rows, err := db.db.Query( ctx, ` SELECT u.user_id, a.account_id, a.email FROM oauth_users u LEFT JOIN oauth_tokens t ON u.user_id = id_token_subject LEFT JOIN accounts a USING (user_id) WHERE access_token = @access_token `, pgx.NamedArgs{ "access_token": accessToken, }, ) if err != nil { return OAuthUser{}, nil, fmt.Errorf("failed to perform query: %w", err) } type Row struct { User_ID string Account_ID *int64 Email *string } r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return OAuthUser{}, nil, consts.ErrNotFound } return OAuthUser{}, nil, fmt.Errorf("failed to scan row: %w", err) } user := OAuthUser{UserID: r.User_ID} var acct *Account if r.Account_ID != nil { acct = &Account{ AccountIDs: AccountIDs{ AccountID: *r.Account_ID, }, OAuthUser: user, Email: *r.Email, } } return user, acct, nil } func (db *Store) GetShops(ctx context.Context, acctID int64) ([]AccountShop, error) { var shops []AccountShop for _, v := range getDevShops(acctID) { shops = append(shops, v) } return shops, nil } func (db *Store) GetMockShops(ctx context.Context, acctID int64) ([]AccountShop, error) { infos := getAllMockShopSchemaInfos() lists := make([][]AccountShop, len(infos)) numShops := 0 for i, in := range infos { rows, err := db.db.Query( ctx, fmt.Sprintf( ` SELECT shop_id, name FROM %s WHERE account_id = @account_id `, in.shopTable, ), pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return nil, fmt.Errorf("failed to perform query: %w", err) } vs, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct { Shop_id string Name string }]) if err != nil { return nil, fmt.Errorf("failed to scan rows: %w", err) } list := make([]AccountShop, len(vs)) for i, v := range vs { list[i] = AccountShop{ AccountShopIDs: NewAccountIDs(acctID). ShopID(in.platform, v.Shop_id), Name: v.Name, } } lists[i] = list numShops += len(list) } shops := make([]AccountShop, 0, numShops) for _, list := range lists { shops = append(shops, list...) } return shops, nil } func (db *Store) GetListingsForShop(ctx context.Context, acctID int64, platform Platform, shopID string) ([]Listing, error) { var vs []Listing for _, v := range getDevListings(acctID) { if v.Platform != platform { continue } if v.ShopID != shopID { continue } vs = append(vs, v) } return vs, nil } func (db *Store) GetMockListingsForShop(ctx context.Context, acctID int64, platform Platform, shopID string) ([]Listing, error) { in, ok := getMockShopSchemaInfo(platform) if !ok { return nil, consts.ErrNotFound } rows, err := db.db.Query( ctx, fmt.Sprintf( ` SELECT listing_id, sku, name, description, "count" FROM %s WHERE account_id = @account_id AND shop_id = @shop_id `, in.listingsTable, ), pgx.NamedArgs{ "account_id": acctID, "shop_id": shopID, }, ) if err != nil { return nil, fmt.Errorf("failed to perform query: %w", err) } vs, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct { Listing_id string SKU string Name string Description string Count int64 }]) if err != nil { return nil, fmt.Errorf("failed to scan rows: %w", err) } listings := make([]Listing, len(vs)) for i, v := range vs { listings[i] = Listing{ AccountShopListingIDs: NewAccountIDs(acctID). ShopID(in.platform, shopID). ListingID(v.Listing_id), SKU: v.SKU, Name: v.Name, Description: v.Description, Count: v.Count, } } return listings, nil } func (db *Store) GetMockListing(ctx context.Context, acctID int64, platform Platform, shopID, listingID string) (*Listing, error) { in, ok := getMockShopSchemaInfo(platform) if !ok { return nil, fmt.Errorf("%w: unrecognized platform: %s", consts.ErrNotFound, platform) } rows, err := db.db.Query( ctx, fmt.Sprintf( ` SELECT name, count, sku, description FROM %s WHERE account_id = @account_id AND shop_id = @shop_id AND listing_id = @listing_id `, in.listingsTable, ), pgx.NamedArgs{ "account_id": acctID, "shop_id": shopID, "listing_id": listingID, }, ) if err != nil { return nil, fmt.Errorf("failed to perform query: %w", err) } v, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[struct { Name string Count int64 Sku string Description string }]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, consts.ErrNotFound } return nil, fmt.Errorf("failed to scan row: %w", err) } return &Listing{ AccountShopListingIDs: AccountShopListingIDs{ AccountShopIDs: AccountShopIDs{ AccountIDs: AccountIDs{ AccountID: acctID, }, Platform: platform, ShopID: shopID, }, ListingID: listingID, }, SKU: v.Sku, Name: v.Name, Description: v.Name, Count: v.Count, }, nil } func (db *Store) DeleteMockSyncGroup(ctx context.Context, acctID, syncGroupID int64) error { tag, err := db.db.Exec( ctx, ` DELETE FROM mock.sync_groups WHERE account_id = @account_id AND sync_group_id = @sync_group_id `, pgx.NamedArgs{ "account_id": acctID, "sync_group_id": syncGroupID, }, ) if err != nil { return fmt.Errorf("failed to perform query: %w", err) } if tag.RowsAffected() == 0 { return consts.ErrNotFound } return nil } func (db *Store) StartEditingMockSyncGroup(ctx context.Context, acctID, syncGroupID int64) (prevSyncGroupID int64, prevSyncGroupExists bool, err error) { rows, err := db.db.Query( ctx, ` WITH deleted_row AS ( DELETE FROM mock.sync_group_editing WHERE account_id = @account_id AND sync_group_id <> @sync_group_id RETURNING account_id, sync_group_id ), new_row AS ( INSERT INTO mock.sync_group_editing ( account_id, sync_group_id ) SELECT x.account_id, x.sync_group_id FROM ( VALUES ( @account_id, @sync_group_id ) ) AS x(account_id, sync_group_id) LEFT JOIN deleted_row ON TRUE ON CONFLICT DO NOTHING ) SELECT sync_group_id FROM deleted_row `, pgx.NamedArgs{ "account_id": acctID, "sync_group_id": syncGroupID, }, ) if err != nil { return 0, false, fmt.Errorf("failed to perform query: %w", err) } v, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[pgtype.Int8]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return 0, false, nil } return 0, false, fmt.Errorf("failed to scan rows: %w", err) } return v.Int64, v.Valid, nil } func (db *Store) CancelEditingOfMockSyncGroupForAccount(ctx context.Context, acctID int64) (prevSyncGroupID int64, prevSyncGroupExists bool, err error) { rows, err := db.db.Query( ctx, ` DELETE FROM mock.sync_group_editing WHERE account_id = @account_id RETURNING sync_group_id `, pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return 0, false, fmt.Errorf("failed to perform query: %w", err) } prevSyncGroupID, err = pgx.CollectExactlyOneRow(rows, pgx.RowTo[int64]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return 0, false, nil } return 0, false, fmt.Errorf("failed to scan rows: %w", err) } return prevSyncGroupID, true, nil } func (db *Store) MockSyncGroupIsBeingEdited(ctx context.Context, acctID, syncGroupID int64) (bool, error) { rows, err := db.db.Query( ctx, ` SELECT true FROM mock.sync_group_editing WHERE account_id = @account_id AND sync_group_id = @sync_group_id `, pgx.NamedArgs{ "account_id": acctID, "sync_group_id": syncGroupID, }, ) if err != nil { return false, fmt.Errorf("failed to perform query: %w", err) } if _, err = pgx.CollectExactlyOneRow(rows, pgx.RowTo[pgtype.Bool]); err != nil { if errors.Is(err, pgx.ErrNoRows) { return false, nil } return false, fmt.Errorf("failed to scan rows: %w", err) } return true, nil } // additional context func (db *Store) GetAccountPointerByUserID(ctx context.Context, userID string) (*Account, error) { acct, err := db.GetAccountByUserID(ctx, userID) if err == nil { return &acct, nil } if errors.Is(err, consts.ErrNotFound) { return nil, nil } return nil, err }