Files
inventory-plus-plus/domains/accounts/accounts.go
T

1484 lines
31 KiB
Go

package accounts
// to generate StoreWithContext
//go:generate concurry -s Store
import (
"context"
"errors"
"fmt"
"slices"
"strings"
"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
}
// 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)
}