package accounts // to generate StoreWithContext //go:generate concurry -s Store import ( "context" "encoding/json" "errors" "fmt" "slices" "strings" "github.com/google/uuid" "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 ShopName string SKU string Name string Description string Count int64 } ListingWithOrderIndex struct { Listing OrderIndex int } ) 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 sh.name as shop_name, li.listing_id, li.sku, li.name, li.description, li."count" FROM %s as sh JOIN %s as li USING (account_id, shop_id) WHERE account_id = @account_id AND shop_id = @shop_id `, in.shopTable, 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 { Shop_name string 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), ShopName: v.Shop_name, 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 sh.name as shop_name, li.name, li.count, li.sku, li.description FROM %s AS sh JOIN %s AS li USING (account_id, shop_id) WHERE account_id = @account_id AND shop_id = @shop_id AND listing_id = @listing_id `, in.shopTable, 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 { Shop_name string 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, }, ShopName: v.Shop_name, 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) { tx, err := db.db.Begin(ctx) if err != nil { return 0, false, fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback(ctx) rows, err := tx.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 && !errors.Is(err, pgx.ErrNoRows) { return 0, false, fmt.Errorf("failed to scan rows: %w", err) } // insert the mock sync group listings into the draft table for _, in := range getAllMockShopSchemaInfos() { _, err := tx.Exec( ctx, fmt.Sprintf( ` WITH existing_listings AS ( SELECT shop_id, listing_id, order_index FROM %s WHERE account_id = @account_id AND sync_group_id = @sync_group_id ), inserted_order_indexes AS ( INSERT INTO mock.sync_group_editing_listing_order_indexes ( account_id, order_index ) SELECT @account_id, order_index FROM existing_listings RETURNING order_index ) INSERT INTO %s ( account_id, shop_id, listing_id, order_index ) SELECT @account_id, shop_id, listing_id, order_index FROM existing_listings JOIN inserted_order_indexes USING (order_index) `, in.syncGroupListingsTable, in.syncGroupEditingListingTable, ), pgx.NamedArgs{ "account_id": acctID, "sync_group_id": syncGroupID, }, ) if err != nil { return 0, false, fmt.Errorf("failed to insert into editing listings table: %w", err) } } if err := tx.Commit(ctx); err != nil { return 0, false, fmt.Errorf("failed to commit transaction: %w", err) } return v.Int64, v.Valid, nil } func (db *Store) SaveMockSyncGroupBeingEdited(ctx context.Context, acctID int64) (syncGroupID int64, err error) { err = db.beginTxn(ctx, func(tx pgx.Tx) error { // delete old sync group listings rows, err := tx.Query( ctx, ` WITH the_sync_group AS ( SELECT sync_group_id FROM mock.sync_group_editing WHERE account_id = @account_id ), deleted_indexes AS ( DELETE FROM mock.sync_group_listing_order_indexes ind USING mock.sync_group_editing AS ed WHERE ed.account_id = @account_id AND ind.sync_group_id = ed.sync_group_id RETURNING ed.sync_group_id ) SELECT sync_group_id FROM deleted_indexes LIMIT 1 `, pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return fmt.Errorf("failed to query editing sync group: %w", err) } if syncGroupID, err = pgx.CollectExactlyOneRow(rows, pgx.RowTo[int64]); err != nil { if errors.Is(err, pgx.ErrNoRows) { return consts.ErrNotFound } return fmt.Errorf("failed to scan row: %w", err) } var listingCount int for _, in := range getAllMockShopSchemaInfos() { // look up edited sync group listings rows, err := tx.Query( ctx, fmt.Sprintf( ` SELECT order_index, shop_id, listing_id FROM %s el WHERE account_id = @account_id `, in.syncGroupEditingListingTable, ), pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return fmt.Errorf("failed to perform query: %w", err) } vs, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct { Order_index int Shop_id pgtype.Text Listing_id pgtype.Text }]) if err != nil { return fmt.Errorf("failed to scan rows: %w", err) } if len(vs) == 0 { continue } // fail if any shop_id's or listing_id's are NULL for _, v := range vs { if !v.Shop_id.Valid { return fmt.Errorf("%w: shop id missing on listing", consts.ErrConflict) } if !v.Listing_id.Valid { return fmt.Errorf("%w: listing id missing on listing", consts.ErrConflict) } } // create new sync group listing order indexes values := make([]string, len(vs)) args := make(pgx.NamedArgs, len(vs)) for i, v := range vs { values[i] = fmt.Sprintf("(@sync_group_id_%[1]d, @order_index_%[1]d)", i) args[fmt.Sprintf("sync_group_id_%d", i)] = syncGroupID args[fmt.Sprintf("order_index_%d", i)] = v.Order_index } listingCount += len(vs) _, err = tx.Exec( ctx, fmt.Sprintf( ` INSERT INTO mock.sync_group_listing_order_indexes ( sync_group_id, order_index ) VALUES %s `, strings.Join(values, ", "), ), args, ) if err != nil { return fmt.Errorf("failed to delete rows for sync group listing order indexes: %w", err) } // create new sync group listings values = make([]string, len(vs)) args = make(pgx.NamedArgs, len(vs)) for i, v := range vs { values[i] = fmt.Sprintf("(@account_id_%[1]d, @sync_group_id_%[1]d, @shop_id_%[1]d, @listing_id_%[1]d, @order_index_%[1]d)", i) args[fmt.Sprintf("account_id_%d", i)] = acctID args[fmt.Sprintf("sync_group_id_%d", i)] = syncGroupID args[fmt.Sprintf("shop_id_%d", i)] = v.Shop_id args[fmt.Sprintf("listing_id_%d", i)] = v.Listing_id args[fmt.Sprintf("order_index_%d", i)] = v.Order_index } _, err = tx.Exec( ctx, fmt.Sprintf( ` INSERT INTO %s ( account_id, sync_group_id, shop_id, listing_id, order_index ) VALUES %s `, in.syncGroupListingsTable, strings.Join(values, ", "), ), args, ) if err != nil { return fmt.Errorf("failed to perform query inserting new sync group listings: %w", err) } } // there must be at least two listings per sync group if listingCount < 2 { return fmt.Errorf("%w: invalid number of listings for sync group: %d: minimum of two required", consts.ErrConflict, listingCount) } // delete the sync group editing record // this will cascade the editing listing records rows, err = tx.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 fmt.Errorf("failed to perform query deleting editing sync group: %w", err) } if syncGroupID, err = pgx.CollectExactlyOneRow(rows, pgx.RowTo[int64]); err != nil { if errors.Is(err, pgx.ErrNoRows) { return consts.ErrNotFound } return fmt.Errorf("failed to scan row: %w", err) } return nil }) return syncGroupID, err } 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) GetMockSyncGroupEditingListings(ctx context.Context, acctID int64) ([]ListingWithOrderIndex, error) { var ( listings []ListingWithOrderIndex allOrderIndexes []int ) //orderIndexPerListing := make(map[Platform]map[string]int) err := db.beginReadonlyTxn(ctx, func(tx pgx.Tx) error { // get the mock sync group editing listings from each table for _, in := range getAllMockShopSchemaInfos() { rows, err := tx.Query( ctx, fmt.Sprintf( ` SELECT eli.shop_id, eli.listing_id, eli.order_index, li.name, li.count, li.sku, li.description, sh.name as shop_name FROM %s AS eli LEFT JOIN %s AS li USING (account_id, shop_id, listing_id) JOIN %s AS sh USING (account_id, shop_id) WHERE account_id = @account_id `, in.syncGroupEditingListingTable, in.listingsTable, in.shopTable, ), pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return fmt.Errorf("failed to perform query to get mock sync group editing listings: %w", err) } vs, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct { Shop_id string Listing_id pgtype.Text Order_index int Name pgtype.Text Count pgtype.Int8 Sku pgtype.Text Description pgtype.Text Shop_name string }]) if err != nil { return fmt.Errorf("failed to scan rows: %w", err) } for _, v := range vs { listings = append(listings, ListingWithOrderIndex{ Listing: Listing{ AccountShopListingIDs: NewAccountIDs(acctID). ShopID(in.platform, v.Shop_id). ListingID(v.Listing_id.String), ShopName: v.Shop_name, SKU: v.Sku.String, Name: v.Name.String, Description: v.Description.String, Count: v.Count.Int64, }, OrderIndex: v.Order_index, }) } } // loading the empty indexes, too rows, err := tx.Query( ctx, ` SELECT order_index FROM mock.sync_group_editing_listing_order_indexes WHERE account_id = @account_id `, pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return fmt.Errorf("failed to perform query: %w", err) } if allOrderIndexes, err = pgx.CollectRows(rows, pgx.RowTo[int]); err != nil { return fmt.Errorf("failed to scan rows: %w", err) } return nil }) slices.SortFunc(listings, func(a, b ListingWithOrderIndex) int { return a.OrderIndex - b.OrderIndex }) indexPerOrderIndex := make(map[int]int) for i, v := range listings { indexPerOrderIndex[v.OrderIndex] = i } for _, i := range allOrderIndexes { if _, ok := indexPerOrderIndex[i]; ok { continue } for j := i - 1; j >= 0; j -= 1 { index, ok := indexPerOrderIndex[j] if !ok { continue } // insert the new empty listing into the list listings = slices.Insert(listings, index+1, ListingWithOrderIndex{ Listing: Listing{ AccountShopListingIDs: NewAccountIDs(acctID). ShopID("", ""). ListingID(""), }, OrderIndex: i, }) indexPerOrderIndex[i] = index + 1 for k, v := range indexPerOrderIndex { if k > i { indexPerOrderIndex[k] = v + 1 } } break } } return listings, err } 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 } func (db *Store) GetMockSyncGroupEditingListing(ctx context.Context, acctID, syncGroupID, orderIndex int64) (*Listing, error) { var listing *Listing err := db.beginReadonlyTxn(ctx, func(tx pgx.Tx) error { // assert the sync group is being edited rows, err := tx.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 fmt.Errorf("failed to perform query: %w", err) } if _, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[bool]); err != nil { if errors.Is(err, pgx.ErrNoRows) { return consts.ErrNotFound } return fmt.Errorf("failed to scan rows: %w", err) } // find the editing listing for _, in := range getAllMockShopSchemaInfos() { rows, err := tx.Query( ctx, fmt.Sprintf( ` SELECT eli.shop_id, eli.listing_id, eli.order_index, li.name, li.count, li.sku, li.description, sh.name as shop_name FROM %s AS eli JOIN %s AS li USING (account_id, shop_id, listing_id) JOIN %s AS sh USING (account_id, shop_id) WHERE account_id = @account_id AND eli.order_index = @order_index `, in.syncGroupEditingListingTable, in.listingsTable, in.shopTable, ), pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, }, ) if err != nil { return fmt.Errorf("failed to perform query to get mock sync group editing listings: %w", err) } v, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[struct { Shop_id string Listing_id string Order_index int Name string Count int64 Sku string Description string Shop_name string }]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { continue } return fmt.Errorf("failed to scan rows: %w", err) } listing = &Listing{ AccountShopListingIDs: NewAccountIDs(acctID). ShopID(in.platform, v.Shop_id). ListingID(v.Listing_id), ShopName: v.Shop_name, SKU: v.Sku, Name: v.Name, Description: v.Description, Count: v.Count, } return nil } return consts.ErrNotFound }) return listing, err } func (db *Store) AddListingToMockSyncGroupBeingEdited(ctx context.Context, acctID int64) (syncGroupID int64, orderIndex int, err error) { err = db.beginTxn(ctx, func(tx pgx.Tx) error { // assert the sync group is being edited rows, err := tx.Query( ctx, ` SELECT sync_group_id FROM mock.sync_group_editing WHERE account_id = @account_id `, pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return fmt.Errorf("failed to perform query: %w", err) } syncGroupID, err = pgx.CollectExactlyOneRow(rows, pgx.RowTo[int64]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return consts.ErrNotFound } return fmt.Errorf("failed to scan rows: %w", err) } rows, err = tx.Query( ctx, ` WITH new_order_index AS ( SELECT COALESCE(MAX(order_index)+1, 0) AS order_index FROM mock.sync_group_editing_listing_order_indexes ) INSERT INTO mock.sync_group_editing_listing_order_indexes ( account_id, order_index ) SELECT @account_id, new_order_index.order_index FROM new_order_index RETURNING order_index `, pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return fmt.Errorf("failed to insert new order index row: %w", err) } orderIndex, err = pgx.CollectExactlyOneRow(rows, pgx.RowTo[int]) if err != nil { return fmt.Errorf("failed to scan rows: %w", err) } return nil }) return syncGroupID, orderIndex, err } func (db *Store) SetShopInListingInMockSyncGroupBeingEdited(ctx context.Context, acctID, syncGroupID int64, orderIndex int, platform Platform, shopID string) error { in, ok := getMockShopSchemaInfo(platform) if !ok { return consts.ErrNotFound } return db.beginTxn(ctx, func(tx pgx.Tx) error { // assert the sync group is being edited rows, err := tx.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 fmt.Errorf("failed to perform query: %w", err) } if _, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[bool]); err != nil { if errors.Is(err, pgx.ErrNoRows) { return consts.ErrNotFound } return fmt.Errorf("failed to scan rows: %w", err) } // delete old listing: this will delete the store-specific listing row implicitly without the lookup tags, err := tx.Exec( ctx, ` DELETE FROM mock.sync_group_editing_listing_order_indexes WHERE account_id = @account_id AND order_index = @order_index `, pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, }, ) if err != nil { return fmt.Errorf("failed to deleted order index record: %w", err) } if tags.RowsAffected() == 0 { return consts.ErrNotFound } // create new listing _, err = tx.Exec( ctx, fmt.Sprintf( ` WITH new_order_index_row(account_id, order_index) AS ( INSERT INTO mock.sync_group_editing_listing_order_indexes ( account_id, order_index ) VALUES ( @account_id, @order_index ) RETURNING account_id, order_index ) INSERT INTO %s AS eli ( account_id, order_index, shop_id ) SELECT account_id, order_index, @shop_id as shop_id FROM new_order_index_row `, in.syncGroupEditingListingTable, ), pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, "shop_id": shopID, }, ) if err != nil { return fmt.Errorf("failed to perform query to insert mock sync group editing listings: %w", err) } return nil }) } func (db *Store) SetListingInListingInMockSyncGroupBeingEdited(ctx context.Context, acctID, syncGroupID int64, orderIndex int, listingID string) error { return db.beginTxn(ctx, func(tx pgx.Tx) error { // assert the sync group is being edited rows, err := tx.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 fmt.Errorf("failed to perform query: %w", err) } if _, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[bool]); err != nil { if errors.Is(err, pgx.ErrNoRows) { return consts.ErrNotFound } return fmt.Errorf("failed to scan rows: %w", err) } // update the editing listing for _, in := range getAllMockShopSchemaInfos() { tag, err := tx.Exec( ctx, fmt.Sprintf( ` UPDATE %s AS eli SET listing_id = @listing_id WHERE account_id = @account_id AND order_index = @order_index `, in.syncGroupEditingListingTable, ), pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, "listing_id": listingID, }, ) if err != nil { return fmt.Errorf("failed to perform query to update mock sync group editing listings: %w", err) } switch n := tag.RowsAffected(); n { case 0: continue case 1: return nil default: return fmt.Errorf("unexpected number of rows affected: %d", n) } return nil } return consts.ErrNotFound }) } func (db *Store) DeleteListingInListingInMockSyncGroupBeingEdited(ctx context.Context, acctID int64, orderIndex int) error { tag, err := db.db.Exec( ctx, ` DELETE FROM mock.sync_group_editing_listing_order_indexes WHERE account_id = @account_id AND order_index = @order_index `, pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, }, ) if err != nil { return fmt.Errorf("failed to perform query: %w", err) } if tag.RowsAffected() == 0 { return consts.ErrNotFound } return nil } // if err := s.accts.SetShopInMockSaleForm(c, acctID, platform, shopID); err != nil { func (db *Store) SetShopInMockSaleForm(ctx context.Context, acctID int64, platform Platform, shopID string) error { _, err := db.db.Exec( ctx, ` INSERT INTO mock.sale_form_shop_selection ( account_id, platform, shop_id ) VALUES ( @account_id, @platform, @shop_id ) ON CONFLICT (account_id) DO UPDATE SET platform = EXCLUDED.platform, shop_id = EXCLUDED.shop_id `, pgx.NamedArgs{ "account_id": acctID, "platform": platform, "shop_id": shopID, }, ) if err != nil { return fmt.Errorf("failed to perform query: %w", err) } return nil } func (db *Store) SetShopInMockRefundForm(ctx context.Context, acctID int64, platform Platform, shopID string) error { _, err := db.db.Exec( ctx, ` INSERT INTO mock.refund_form_shop_selection ( account_id, platform, shop_id ) VALUES ( @account_id, @platform, @shop_id ) ON CONFLICT (account_id) DO UPDATE SET platform = EXCLUDED.platform, shop_id = EXCLUDED.shop_id `, pgx.NamedArgs{ "account_id": acctID, "platform": platform, "shop_id": shopID, }, ) if err != nil { return fmt.Errorf("failed to perform query: %w", err) } return nil } func (db *Store) SetShopInMockCountForm(ctx context.Context, acctID int64, platform Platform, shopID string) error { _, err := db.db.Exec( ctx, ` INSERT INTO mock.count_form_shop_selection ( account_id, platform, shop_id ) VALUES ( @account_id, @platform, @shop_id ) ON CONFLICT (account_id) DO UPDATE SET platform = EXCLUDED.platform, shop_id = EXCLUDED.shop_id `, pgx.NamedArgs{ "account_id": acctID, "platform": platform, "shop_id": shopID, }, ) if err != nil { return fmt.Errorf("failed to perform query: %w", err) } return nil } type MockEventFormValues struct { SaleShopID string SalePlatform Platform RefundShopID string RefundPlatform Platform CountShopID string CountPlatform Platform } func (db *Store) GetMockEventFormValues(ctx context.Context, acctID int64) (*MockEventFormValues, error) { rows, err := db.db.Query( ctx, ` SELECT sf.platform AS sale_platform, sf.shop_id AS sale_shop_id, rf.platform AS refund_platform, rf.shop_id AS refund_shop_id, cf.platform AS count_platform, cf.shop_id AS count_shop_id FROM mock.sale_form_shop_selection AS sf FULL OUTER JOIN mock.refund_form_shop_selection AS rf USING (account_id) FULL OUTER JOIN mock.count_form_shop_selection AS cf USING (account_id) WHERE sf.account_id = @account_id `, pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return nil, fmt.Errorf("failed to perform query: %w", err) } v, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[struct { Sale_shop_id pgtype.Text Sale_platform *Platform Refund_shop_id pgtype.Text Refund_platform *Platform Count_shop_id pgtype.Text Count_platform *Platform }]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return &MockEventFormValues{}, nil } return nil, fmt.Errorf("failed to scan rows: %w", err) } return &MockEventFormValues{ SaleShopID: v.Sale_shop_id.String, SalePlatform: deref(v.Sale_platform), RefundShopID: v.Refund_shop_id.String, RefundPlatform: deref(v.Refund_platform), CountShopID: v.Count_shop_id.String, CountPlatform: deref(v.Count_platform), }, nil } func (db *Store) SaveNewMockSale(ctx context.Context, acctID int64, platform Platform, shopID, listingID string, count int) (uuid.UUID, error) { eventID := uuid.New() _, err := db.db.Exec( ctx, ` INSERT INTO mock.raw_shop_events ( platform, shop_id, event_timestamp, event_id, raw_payload ) VALUES ( @platform, @shop_id, NOW(), @event_id, @raw_payload ) `, pgx.NamedArgs{ "platform": platform, "shop_id": shopID, "event_id": eventID, "raw_payload": json.RawMessage(fmt.Sprintf(`{ "type": "sale", "listingID": %q, "count": %d }`, listingID, count)), }, ) if err != nil { return uuid.Nil, fmt.Errorf("failed to perform query: %w", err) } return eventID, nil } func (db *Store) SaveNewMockRefund(ctx context.Context, acctID int64, platform Platform, shopID, listingID string, count int) (uuid.UUID, error) { eventID := uuid.New() _, err := db.db.Exec( ctx, ` INSERT INTO mock.raw_shop_events ( platform, shop_id, event_timestamp, event_id, raw_payload ) VALUES ( @platform, @shop_id, NOW(), @event_id, @raw_payload ) `, pgx.NamedArgs{ "platform": platform, "shop_id": shopID, "event_id": eventID, "raw_payload": json.RawMessage(fmt.Sprintf(`{ "type": "refund", "listingID": %q, "count": %d }`, listingID, count)), }, ) if err != nil { return uuid.Nil, fmt.Errorf("failed to perform query: %w", err) } return eventID, nil } func (db *Store) SaveNewMockInventoryReset(ctx context.Context, acctID int64, platform Platform, shopID, listingID string, count int) (uuid.UUID, error) { eventID := uuid.New() _, err := db.db.Exec( ctx, ` INSERT INTO mock.raw_shop_events ( platform, shop_id, event_timestamp, event_id, raw_payload ) VALUES ( @platform, @shop_id, NOW(), @event_id, @raw_payload ) `, pgx.NamedArgs{ "platform": platform, "shop_id": shopID, "event_id": eventID, "raw_payload": json.RawMessage(fmt.Sprintf(`{ "type": "inventory-reset", "listingID": %q, "count": %d }`, listingID, count)), }, ) if err != nil { return uuid.Nil, fmt.Errorf("failed to perform query: %w", err) } return eventID, 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 } func (db *Store) beginReadonlyTxn(ctx context.Context, cb func(pgx.Tx) error) error { return pgx.BeginTxFunc(ctx, db.db, pgx.TxOptions{AccessMode: pgx.ReadOnly}, cb) } func (db *Store) beginTxn(ctx context.Context, cb func(pgx.Tx) error) error { return pgx.BeginFunc(ctx, db.db, cb) } func deref[T any](ptr *T) T { if ptr != nil { return *ptr } var zero T return zero }