package accounts // to generate StoreWithContext //go:generate concurry -s Store import ( "context" "errors" "fmt" "ruben/inventory2/internal/consts" "ruben/inventory2/internal/logging" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) 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 } // ids AccountIDs struct { AccountID int64 } AccountShopIDs struct { AccountIDs Platform Platform ShopID string } AccountShopListingIDs struct { AccountShopIDs ListingID string } MockShop struct { AccountShopIDs Name string } ) 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) 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) CreateMockShop(ctx context.Context, acctID int64, platform Platform, name string) (shopID uuid.UUID, err error) { shopTableName, err := getMockShopTableName(platform) if err != nil { return uuid.Nil, err } tx, err := db.db.Begin(ctx) if err != nil { return uuid.Nil, fmt.Errorf("failed to being transaction: %w", err) } defer tx.Rollback(ctx) // TODO: are we really going to just copy the id between accounts and mock.accounts? // upsert mock account rows, err := tx.Query( ctx, ` WITH existing_account(account_id, user_id) AS ( SELECT account_id, user_id FROM accounts WHERE account_id = @account_id ), existing_mock_account AS ( SELECT user_id FROM accounts WHERE account_id = @account_id ), inserted_mock_account AS ( INSERT INTO mock.accounts ( account_id, user_id ) SELECT account_id, user_id FROM existing_account ON CONFLICT DO NOTHING RETURNING user_id ) SELECT COALESCE(ia.user_id, ea.user_id) FROM inserted_mock_account ia FULL OUTER JOIN existing_mock_account ea USING (user_id) `, pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return uuid.Nil, fmt.Errorf("test query failed: %w", err) } userID, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return uuid.Nil, fmt.Errorf("%w: account not found", consts.ErrNotFound) } return uuid.Nil, fmt.Errorf("failed to scan rows: %w", err) } // create the mock shop shopID = uuid.New() _, err = tx.Exec( ctx, fmt.Sprintf( ` INSERT INTO %s ( user_id, account_id, shop_id, name ) VALUES ( @user_id, @account_id, @shop_id, @name ) `, shopTableName, ), pgx.NamedArgs{ "account_id": acctID, "user_id": userID, "shop_id": shopID, "name": name, }, ) if err != nil { return uuid.Nil, fmt.Errorf("failed to perform query to create mock shop: %w", err) } if err := tx.Commit(ctx); err != nil { return uuid.Nil, fmt.Errorf("failed to commit transaction: %w", err) } return shopID, nil } func (db *Store) ListMockShopsForPlatform(ctx context.Context, acctID int64, platform Platform) ([]MockShop, error) { shopTableName, err := getMockShopTableName(platform) if err != nil { return nil, err } rows, err := db.db.Query( ctx, fmt.Sprintf( ` SELECT shop_id, name FROM %s WHERE account_id = @account_id `, shopTableName, ), 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 uuid.UUID Name string }]) if err != nil { return nil, fmt.Errorf("failed to scan rows: %w", err) } shops := make([]MockShop, len(vs)) for i, v := range vs { shops[i] = MockShop{ AccountShopIDs: AccountShopIDs{ AccountIDs: AccountIDs{ AccountID: acctID, }, Platform: platform, ShopID: v.Shop_id.String(), }, Name: v.Name, } } return shops, nil } func (db *Store) GetMockShop(ctx context.Context, acctID int64, platform Platform, shopID string) (*MockShop, error) { shopTableName, err := getMockShopTableName(platform) if err != nil { return nil, err } rows, err := db.db.Query( ctx, fmt.Sprintf( ` SELECT name FROM %s WHERE account_id = @account_id AND shop_id = @shop_id `, shopTableName, ), pgx.NamedArgs{ "account_id": acctID, "shop_id": shopID, }, ) if err != nil { return nil, fmt.Errorf("failed to perform query: %w", err) } name, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, consts.ErrNotFound } return nil, fmt.Errorf("failed to scan rows: %w", err) } return &MockShop{ AccountShopIDs: AccountShopIDs{ AccountIDs: AccountIDs{ AccountID: acctID, }, Platform: platform, ShopID: shopID, }, Name: name, }, nil } func getMockShopTableName(platform Platform) (string, error) { switch platform { case Amazon: return "mock.shop_amazon", nil case BigCartel: return "mock.shop_big_cartel", nil case Ebay: return "mock.shop_ebay", nil case Ecwid: return "mock.shop_ecwid", nil case Etsy: return "mock.shop_etsy", nil case Shopify: return "mock.shop_shopify", nil case SquareOnline: return "mock.shop_square_online", nil case Squarespace: return "mock.shop_squarespace", nil case Tiktok: return "mock.shop_tiktok", nil case WalmartMarketplace: return "mock.shop_walmart_marketplace", nil case Wix: return "mock.shop_wix", nil case WooCommerce: return "mock.shop_woo_commerce", nil case Zoho: return "mock.shop_zoho", nil default: return "", fmt.Errorf("%w: unrecognized platform", consts.ErrBadRequest) } } // 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 }