2024-02-27 14:14:30 +01:00
|
|
|
package sqlite
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"database/sql"
|
|
|
|
"fmt"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"gitlab.com/wpetit/goweb/logger"
|
|
|
|
)
|
|
|
|
|
|
|
|
type TenantRepository struct {
|
|
|
|
repository
|
|
|
|
}
|
|
|
|
|
2024-02-27 17:01:24 +01:00
|
|
|
// Query implements datastore.TenantRepository.
|
|
|
|
func (r *TenantRepository) Query(ctx context.Context, opts ...datastore.TenantQueryOptionFunc) ([]*datastore.Tenant, int, error) {
|
|
|
|
options := &datastore.TenantQueryOptions{}
|
|
|
|
for _, fn := range opts {
|
|
|
|
fn(options)
|
|
|
|
}
|
|
|
|
|
|
|
|
tenants := make([]*datastore.Tenant, 0)
|
|
|
|
count := 0
|
|
|
|
|
|
|
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
|
|
|
query := `SELECT id, label, created_at, updated_at FROM tenants`
|
|
|
|
|
|
|
|
limit := 10
|
|
|
|
if options.Limit != nil {
|
|
|
|
limit = *options.Limit
|
|
|
|
}
|
|
|
|
|
|
|
|
offset := 0
|
|
|
|
if options.Offset != nil {
|
|
|
|
offset = *options.Offset
|
|
|
|
}
|
|
|
|
|
|
|
|
filters := ""
|
|
|
|
paramIndex := 3
|
|
|
|
args := []any{offset, limit}
|
|
|
|
|
|
|
|
if options.IDs != nil && len(options.IDs) > 0 {
|
|
|
|
filter, newArgs, newParamIndex := inFilter("id", paramIndex, options.IDs)
|
|
|
|
filters += filter
|
|
|
|
paramIndex = newParamIndex
|
|
|
|
args = append(args, newArgs...)
|
|
|
|
}
|
|
|
|
|
|
|
|
if filters != "" {
|
|
|
|
filters = ` WHERE ` + filters
|
|
|
|
}
|
|
|
|
|
|
|
|
query += filters + ` LIMIT $2 OFFSET $1`
|
|
|
|
|
|
|
|
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
|
|
|
|
|
|
|
|
rows, err := tx.QueryContext(ctx, query, args...)
|
|
|
|
if err != nil {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
if err := rows.Close(); err != nil {
|
|
|
|
err = errors.WithStack(err)
|
|
|
|
logger.Error(ctx, "could not close rows", logger.CapturedE(err))
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
tenant := &datastore.Tenant{}
|
|
|
|
|
|
|
|
if err := rows.Scan(&tenant.ID, &tenant.Label, &tenant.CreatedAt, &tenant.UpdatedAt); err != nil {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
tenants = append(tenants, tenant)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
row := tx.QueryRowContext(ctx, `SELECT count(id) FROM tenants `+filters, args...)
|
|
|
|
if err := row.Scan(&count); err != nil {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return tenants, count, nil
|
|
|
|
}
|
|
|
|
|
2024-02-27 14:14:30 +01:00
|
|
|
// Create implements datastore.TenantRepository.
|
|
|
|
func (r *TenantRepository) Create(ctx context.Context, label string) (*datastore.Tenant, error) {
|
|
|
|
var tenant datastore.Tenant
|
|
|
|
|
|
|
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
|
|
|
now := time.Now().UTC()
|
|
|
|
|
|
|
|
query := `
|
|
|
|
INSERT INTO tenants (id, label, created_at, updated_at)
|
|
|
|
VALUES($1, $2, $3, $3)
|
|
|
|
RETURNING "id", "label", "created_at", "updated_at"
|
|
|
|
`
|
|
|
|
|
|
|
|
tenantID := datastore.NewTenantID()
|
|
|
|
|
|
|
|
row := tx.QueryRowContext(
|
|
|
|
ctx, query,
|
|
|
|
tenantID, label, now,
|
|
|
|
)
|
|
|
|
|
|
|
|
if err := row.Scan(&tenant.ID, &tenant.Label, &tenant.CreatedAt, &tenant.UpdatedAt); err != nil {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &tenant, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete implements datastore.TenantRepository.
|
|
|
|
func (r *TenantRepository) Delete(ctx context.Context, id datastore.TenantID) error {
|
|
|
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
|
|
|
if exists, err := r.tenantExists(ctx, tx, id); !exists {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
query := `DELETE FROM tenants WHERE id = $1`
|
|
|
|
_, err := tx.ExecContext(ctx, query, id)
|
|
|
|
if err != nil {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
query = `DELETE FROM agents WHERE tenant_id = $1`
|
|
|
|
_, err = tx.ExecContext(ctx, query, id)
|
|
|
|
if err != nil {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
query = `DELETE FROM specs WHERE tenant_id = $1`
|
|
|
|
_, err = tx.ExecContext(ctx, query, id)
|
|
|
|
if err != nil {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get implements datastore.TenantRepository.
|
|
|
|
func (r *TenantRepository) Get(ctx context.Context, id datastore.TenantID) (*datastore.Tenant, error) {
|
|
|
|
var tenant datastore.Tenant
|
|
|
|
|
|
|
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
|
|
|
query := `
|
|
|
|
SELECT "id", "label", "created_at", "updated_at"
|
|
|
|
FROM tenants
|
|
|
|
WHERE id = $1
|
|
|
|
`
|
|
|
|
|
|
|
|
row := tx.QueryRowContext(ctx, query, id)
|
|
|
|
|
|
|
|
if err := row.Scan(&tenant.ID, &tenant.Label, &tenant.CreatedAt, &tenant.UpdatedAt); err != nil {
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
return errors.WithStack(datastore.ErrNotFound)
|
|
|
|
}
|
|
|
|
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &tenant, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update implements datastore.TenantRepository.
|
|
|
|
func (r *TenantRepository) Update(ctx context.Context, id datastore.TenantID, updates ...datastore.TenantUpdateOptionFunc) (*datastore.Tenant, error) {
|
|
|
|
options := &datastore.TenantUpdateOptions{}
|
|
|
|
for _, fn := range updates {
|
|
|
|
fn(options)
|
|
|
|
}
|
|
|
|
|
|
|
|
var tenant datastore.Tenant
|
|
|
|
|
|
|
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
|
|
|
if exists, err := r.tenantExists(ctx, tx, id); !exists {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
query := `
|
|
|
|
UPDATE tenants SET updated_at = $1
|
|
|
|
`
|
|
|
|
|
|
|
|
args := []any{id}
|
|
|
|
index := 2
|
|
|
|
|
|
|
|
if options.Label != nil {
|
|
|
|
query += fmt.Sprintf(`, label = $%d`, index)
|
|
|
|
args = append(args, *options.Label)
|
|
|
|
index++
|
|
|
|
}
|
|
|
|
|
|
|
|
updated := options.Label != nil
|
|
|
|
if updated {
|
|
|
|
now := time.Now().UTC()
|
|
|
|
query += fmt.Sprintf(`, updated_at = $%d`, index)
|
|
|
|
args = append(args, now)
|
|
|
|
index++
|
|
|
|
}
|
|
|
|
|
|
|
|
query += `
|
|
|
|
WHERE id = $1
|
|
|
|
RETURNING "id", "label", "created_at", "updated_at"
|
|
|
|
`
|
|
|
|
|
|
|
|
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
|
|
|
|
|
|
|
|
row := tx.QueryRowContext(ctx, query, args...)
|
|
|
|
|
|
|
|
if err := row.Scan(&tenant.ID, &tenant.Label, &tenant.CreatedAt, &tenant.UpdatedAt); err != nil {
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
return errors.WithStack(datastore.ErrNotFound)
|
|
|
|
}
|
|
|
|
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &tenant, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *TenantRepository) tenantExists(ctx context.Context, tx *sql.Tx, tenantID datastore.TenantID) (bool, error) {
|
|
|
|
row := tx.QueryRowContext(ctx, `SELECT count(id) FROM tenants WHERE id = $1`, tenantID)
|
|
|
|
|
|
|
|
var count int
|
|
|
|
|
|
|
|
if err := row.Scan(&count); err != nil {
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
return false, errors.WithStack(datastore.ErrNotFound)
|
|
|
|
}
|
|
|
|
|
|
|
|
return false, errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if count == 0 {
|
|
|
|
return false, errors.WithStack(datastore.ErrNotFound)
|
|
|
|
}
|
|
|
|
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewTenantRepository(db *sql.DB, sqliteBusyRetryMaxAttempts int) *TenantRepository {
|
|
|
|
return &TenantRepository{
|
|
|
|
repository: repository{db, sqliteBusyRetryMaxAttempts},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ datastore.TenantRepository = &TenantRepository{}
|