feat(routing): add logical group admin api
This commit is contained in:
@@ -20,19 +20,23 @@ type execQuerier interface {
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
Hosts *HostsRepo
|
||||
Packs *PacksRepo
|
||||
Providers *ProvidersRepo
|
||||
ProviderDrafts *ProviderDraftsRepo
|
||||
ImportBatches *ImportBatchesRepo
|
||||
ImportBatchItems *ImportBatchItemsRepo
|
||||
ImportRuns *ImportRunsRepo
|
||||
ImportRunItems *ImportRunItemsRepo
|
||||
ImportRunEvents *ImportRunItemEventsRepo
|
||||
ManagedResources *ManagedResourcesRepo
|
||||
ProbeResults *ProbeResultsRepo
|
||||
AccessClosures *AccessClosureRecordsRepo
|
||||
ReconcileRuns *ReconcileRunsRepo
|
||||
Hosts *HostsRepo
|
||||
Packs *PacksRepo
|
||||
Providers *ProvidersRepo
|
||||
LogicalGroups *LogicalGroupsRepo
|
||||
LogicalGroupModels *LogicalGroupModelsRepo
|
||||
LogicalGroupRoutes *LogicalGroupRoutesRepo
|
||||
LogicalGroupRouteModels *LogicalGroupRouteModelsRepo
|
||||
ProviderDrafts *ProviderDraftsRepo
|
||||
ImportBatches *ImportBatchesRepo
|
||||
ImportBatchItems *ImportBatchItemsRepo
|
||||
ImportRuns *ImportRunsRepo
|
||||
ImportRunItems *ImportRunItemsRepo
|
||||
ImportRunEvents *ImportRunItemEventsRepo
|
||||
ManagedResources *ManagedResourcesRepo
|
||||
ProbeResults *ProbeResultsRepo
|
||||
AccessClosures *AccessClosureRecordsRepo
|
||||
ReconcileRuns *ReconcileRunsRepo
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
@@ -92,6 +96,22 @@ func (db *DB) Providers() *ProvidersRepo {
|
||||
return db.queries.Providers
|
||||
}
|
||||
|
||||
func (db *DB) LogicalGroups() *LogicalGroupsRepo {
|
||||
return db.queries.LogicalGroups
|
||||
}
|
||||
|
||||
func (db *DB) LogicalGroupModels() *LogicalGroupModelsRepo {
|
||||
return db.queries.LogicalGroupModels
|
||||
}
|
||||
|
||||
func (db *DB) LogicalGroupRoutes() *LogicalGroupRoutesRepo {
|
||||
return db.queries.LogicalGroupRoutes
|
||||
}
|
||||
|
||||
func (db *DB) LogicalGroupRouteModels() *LogicalGroupRouteModelsRepo {
|
||||
return db.queries.LogicalGroupRouteModels
|
||||
}
|
||||
|
||||
func (db *DB) ProviderDrafts() *ProviderDraftsRepo {
|
||||
return db.queries.ProviderDrafts
|
||||
}
|
||||
@@ -161,19 +181,23 @@ func (db *DB) WithTx(ctx context.Context, fn func(*Queries) error) error {
|
||||
|
||||
func newQueries(db execQuerier) *Queries {
|
||||
return &Queries{
|
||||
Hosts: newHostsRepo(db),
|
||||
Packs: newPacksRepo(db),
|
||||
Providers: newProvidersRepo(db),
|
||||
ProviderDrafts: newProviderDraftsRepo(db),
|
||||
ImportBatches: newImportBatchesRepo(db),
|
||||
ImportBatchItems: newImportBatchItemsRepo(db),
|
||||
ImportRuns: newImportRunsRepo(db),
|
||||
ImportRunItems: newImportRunItemsRepo(db),
|
||||
ImportRunEvents: newImportRunItemEventsRepo(db),
|
||||
ManagedResources: newManagedResourcesRepo(db),
|
||||
ProbeResults: newProbeResultsRepo(db),
|
||||
AccessClosures: newAccessClosureRecordsRepo(db),
|
||||
ReconcileRuns: newReconcileRunsRepo(db),
|
||||
Hosts: newHostsRepo(db),
|
||||
Packs: newPacksRepo(db),
|
||||
Providers: newProvidersRepo(db),
|
||||
LogicalGroups: newLogicalGroupsRepo(db),
|
||||
LogicalGroupModels: newLogicalGroupModelsRepo(db),
|
||||
LogicalGroupRoutes: newLogicalGroupRoutesRepo(db),
|
||||
LogicalGroupRouteModels: newLogicalGroupRouteModelsRepo(db),
|
||||
ProviderDrafts: newProviderDraftsRepo(db),
|
||||
ImportBatches: newImportBatchesRepo(db),
|
||||
ImportBatchItems: newImportBatchItemsRepo(db),
|
||||
ImportRuns: newImportRunsRepo(db),
|
||||
ImportRunItems: newImportRunItemsRepo(db),
|
||||
ImportRunEvents: newImportRunItemEventsRepo(db),
|
||||
ManagedResources: newManagedResourcesRepo(db),
|
||||
ProbeResults: newProbeResultsRepo(db),
|
||||
AccessClosures: newAccessClosureRecordsRepo(db),
|
||||
ReconcileRuns: newReconcileRunsRepo(db),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
136
internal/store/sqlite/logical_group_models_repo.go
Normal file
136
internal/store/sqlite/logical_group_models_repo.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LogicalGroupModel struct {
|
||||
ID int64
|
||||
LogicalGroupID string
|
||||
PublicModel string
|
||||
Status string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
type LogicalGroupModelsRepo struct {
|
||||
db execQuerier
|
||||
}
|
||||
|
||||
func newLogicalGroupModelsRepo(db execQuerier) *LogicalGroupModelsRepo {
|
||||
return &LogicalGroupModelsRepo{db: db}
|
||||
}
|
||||
|
||||
func (r *LogicalGroupModelsRepo) Create(ctx context.Context, model LogicalGroupModel) (int64, error) {
|
||||
model, err := normalizeLogicalGroupModel(model)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
result, err := r.db.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO logical_group_models (logical_group_id, public_model, status)
|
||||
VALUES (?, ?, ?)`,
|
||||
model.LogicalGroupID,
|
||||
model.PublicModel,
|
||||
model.Status,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("insert logical group model %q/%q: %w", model.LogicalGroupID, model.PublicModel, err)
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read inserted logical group model id for %q/%q: %w", model.LogicalGroupID, model.PublicModel, err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (r *LogicalGroupModelsRepo) ListByLogicalGroupID(ctx context.Context, logicalGroupID string) ([]LogicalGroupModel, error) {
|
||||
logicalGroupID = strings.TrimSpace(logicalGroupID)
|
||||
if logicalGroupID == "" {
|
||||
return nil, fmt.Errorf("logical_group_id is required")
|
||||
}
|
||||
|
||||
rows, err := r.db.QueryContext(
|
||||
ctx,
|
||||
`SELECT id, logical_group_id, public_model, status, created_at, updated_at
|
||||
FROM logical_group_models
|
||||
WHERE logical_group_id = ?
|
||||
ORDER BY id ASC`,
|
||||
logicalGroupID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list logical group models for %q: %w", logicalGroupID, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
models := make([]LogicalGroupModel, 0)
|
||||
for rows.Next() {
|
||||
var model LogicalGroupModel
|
||||
if err := rows.Scan(
|
||||
&model.ID,
|
||||
&model.LogicalGroupID,
|
||||
&model.PublicModel,
|
||||
&model.Status,
|
||||
&model.CreatedAt,
|
||||
&model.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan logical group model: %w", err)
|
||||
}
|
||||
models = append(models, model)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate logical group models for %q: %w", logicalGroupID, err)
|
||||
}
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func (r *LogicalGroupModelsRepo) DeleteByLogicalGroupIDAndModel(ctx context.Context, logicalGroupID string, publicModel string) error {
|
||||
logicalGroupID = strings.TrimSpace(logicalGroupID)
|
||||
publicModel = strings.TrimSpace(publicModel)
|
||||
if logicalGroupID == "" {
|
||||
return fmt.Errorf("logical_group_id is required")
|
||||
}
|
||||
if publicModel == "" {
|
||||
return fmt.Errorf("public_model is required")
|
||||
}
|
||||
|
||||
result, err := r.db.ExecContext(
|
||||
ctx,
|
||||
`DELETE FROM logical_group_models
|
||||
WHERE logical_group_id = ? AND public_model = ?`,
|
||||
logicalGroupID,
|
||||
publicModel,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete logical group model %q/%q: %w", logicalGroupID, publicModel, err)
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("read deleted logical group model rows for %q/%q: %w", logicalGroupID, publicModel, err)
|
||||
}
|
||||
if affected == 0 {
|
||||
return fmt.Errorf("logical group model %q/%q not found", logicalGroupID, publicModel)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeLogicalGroupModel(model LogicalGroupModel) (LogicalGroupModel, error) {
|
||||
model.LogicalGroupID = strings.TrimSpace(model.LogicalGroupID)
|
||||
model.PublicModel = strings.TrimSpace(model.PublicModel)
|
||||
model.Status = strings.TrimSpace(model.Status)
|
||||
|
||||
switch {
|
||||
case model.LogicalGroupID == "":
|
||||
return LogicalGroupModel{}, fmt.Errorf("logical_group_id is required")
|
||||
case model.PublicModel == "":
|
||||
return LogicalGroupModel{}, fmt.Errorf("public_model is required")
|
||||
}
|
||||
if model.Status == "" {
|
||||
model.Status = "active"
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
113
internal/store/sqlite/logical_group_route_models_repo.go
Normal file
113
internal/store/sqlite/logical_group_route_models_repo.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LogicalGroupRouteModel struct {
|
||||
ID int64
|
||||
RouteID string
|
||||
PublicModel string
|
||||
ShadowModel string
|
||||
Status string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
type LogicalGroupRouteModelsRepo struct {
|
||||
db execQuerier
|
||||
}
|
||||
|
||||
func newLogicalGroupRouteModelsRepo(db execQuerier) *LogicalGroupRouteModelsRepo {
|
||||
return &LogicalGroupRouteModelsRepo{db: db}
|
||||
}
|
||||
|
||||
func (r *LogicalGroupRouteModelsRepo) Create(ctx context.Context, model LogicalGroupRouteModel) (int64, error) {
|
||||
model, err := normalizeLogicalGroupRouteModel(model)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
result, err := r.db.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO logical_group_route_models (route_id, public_model, shadow_model, status)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
model.RouteID,
|
||||
model.PublicModel,
|
||||
model.ShadowModel,
|
||||
model.Status,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("insert logical group route model %q/%q: %w", model.RouteID, model.PublicModel, err)
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read inserted logical group route model id for %q/%q: %w", model.RouteID, model.PublicModel, err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (r *LogicalGroupRouteModelsRepo) ListByRouteID(ctx context.Context, routeID string) ([]LogicalGroupRouteModel, error) {
|
||||
routeID = strings.TrimSpace(routeID)
|
||||
if routeID == "" {
|
||||
return nil, fmt.Errorf("route_id is required")
|
||||
}
|
||||
|
||||
rows, err := r.db.QueryContext(
|
||||
ctx,
|
||||
`SELECT id, route_id, public_model, shadow_model, status, created_at, updated_at
|
||||
FROM logical_group_route_models
|
||||
WHERE route_id = ?
|
||||
ORDER BY id ASC`,
|
||||
routeID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list logical group route models for %q: %w", routeID, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
models := make([]LogicalGroupRouteModel, 0)
|
||||
for rows.Next() {
|
||||
var model LogicalGroupRouteModel
|
||||
if err := rows.Scan(
|
||||
&model.ID,
|
||||
&model.RouteID,
|
||||
&model.PublicModel,
|
||||
&model.ShadowModel,
|
||||
&model.Status,
|
||||
&model.CreatedAt,
|
||||
&model.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan logical group route model: %w", err)
|
||||
}
|
||||
models = append(models, model)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate logical group route models for %q: %w", routeID, err)
|
||||
}
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func normalizeLogicalGroupRouteModel(model LogicalGroupRouteModel) (LogicalGroupRouteModel, error) {
|
||||
model.RouteID = strings.TrimSpace(model.RouteID)
|
||||
model.PublicModel = strings.TrimSpace(model.PublicModel)
|
||||
model.ShadowModel = strings.TrimSpace(model.ShadowModel)
|
||||
model.Status = strings.TrimSpace(model.Status)
|
||||
|
||||
switch {
|
||||
case model.RouteID == "":
|
||||
return LogicalGroupRouteModel{}, fmt.Errorf("route_id is required")
|
||||
case model.PublicModel == "":
|
||||
return LogicalGroupRouteModel{}, fmt.Errorf("public_model is required")
|
||||
}
|
||||
if model.ShadowModel == "" {
|
||||
model.ShadowModel = model.PublicModel
|
||||
}
|
||||
if model.Status == "" {
|
||||
model.Status = "active"
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
238
internal/store/sqlite/logical_group_routes_repo.go
Normal file
238
internal/store/sqlite/logical_group_routes_repo.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LogicalGroupRoute struct {
|
||||
ID int64
|
||||
RouteID string
|
||||
LogicalGroupID string
|
||||
Name string
|
||||
Status string
|
||||
Priority int
|
||||
Weight int
|
||||
ShadowGroupID string
|
||||
ShadowHostID string
|
||||
UpstreamBaseURLHint string
|
||||
CooldownUntil string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
type LogicalGroupRoutesRepo struct {
|
||||
db execQuerier
|
||||
}
|
||||
|
||||
func newLogicalGroupRoutesRepo(db execQuerier) *LogicalGroupRoutesRepo {
|
||||
return &LogicalGroupRoutesRepo{db: db}
|
||||
}
|
||||
|
||||
func (r *LogicalGroupRoutesRepo) Create(ctx context.Context, route LogicalGroupRoute) (int64, error) {
|
||||
route, err := normalizeLogicalGroupRoute(route)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
result, err := r.db.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO logical_group_routes (
|
||||
route_id,
|
||||
logical_group_id,
|
||||
name,
|
||||
status,
|
||||
priority,
|
||||
weight,
|
||||
shadow_group_id,
|
||||
shadow_host_id,
|
||||
upstream_base_url_hint,
|
||||
cooldown_until
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
route.RouteID,
|
||||
route.LogicalGroupID,
|
||||
route.Name,
|
||||
route.Status,
|
||||
route.Priority,
|
||||
route.Weight,
|
||||
route.ShadowGroupID,
|
||||
route.ShadowHostID,
|
||||
route.UpstreamBaseURLHint,
|
||||
route.CooldownUntil,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("insert logical group route %q: %w", route.RouteID, err)
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read inserted logical group route id for %q: %w", route.RouteID, err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (r *LogicalGroupRoutesRepo) GetByRouteID(ctx context.Context, routeID string) (LogicalGroupRoute, error) {
|
||||
routeID = strings.TrimSpace(routeID)
|
||||
if routeID == "" {
|
||||
return LogicalGroupRoute{}, fmt.Errorf("route_id is required")
|
||||
}
|
||||
|
||||
var route LogicalGroupRoute
|
||||
if err := r.db.QueryRowContext(
|
||||
ctx,
|
||||
`SELECT id, route_id, logical_group_id, name, status, priority, weight, shadow_group_id, shadow_host_id, upstream_base_url_hint, cooldown_until, created_at, updated_at
|
||||
FROM logical_group_routes
|
||||
WHERE route_id = ?`,
|
||||
routeID,
|
||||
).Scan(
|
||||
&route.ID,
|
||||
&route.RouteID,
|
||||
&route.LogicalGroupID,
|
||||
&route.Name,
|
||||
&route.Status,
|
||||
&route.Priority,
|
||||
&route.Weight,
|
||||
&route.ShadowGroupID,
|
||||
&route.ShadowHostID,
|
||||
&route.UpstreamBaseURLHint,
|
||||
&route.CooldownUntil,
|
||||
&route.CreatedAt,
|
||||
&route.UpdatedAt,
|
||||
); err != nil {
|
||||
return LogicalGroupRoute{}, err
|
||||
}
|
||||
return route, nil
|
||||
}
|
||||
|
||||
func (r *LogicalGroupRoutesRepo) ListByLogicalGroupID(ctx context.Context, logicalGroupID string) ([]LogicalGroupRoute, error) {
|
||||
logicalGroupID = strings.TrimSpace(logicalGroupID)
|
||||
if logicalGroupID == "" {
|
||||
return nil, fmt.Errorf("logical_group_id is required")
|
||||
}
|
||||
|
||||
rows, err := r.db.QueryContext(
|
||||
ctx,
|
||||
`SELECT id, route_id, logical_group_id, name, status, priority, weight, shadow_group_id, shadow_host_id, upstream_base_url_hint, cooldown_until, created_at, updated_at
|
||||
FROM logical_group_routes
|
||||
WHERE logical_group_id = ?
|
||||
ORDER BY priority ASC, id ASC`,
|
||||
logicalGroupID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list logical group routes for %q: %w", logicalGroupID, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
routes := make([]LogicalGroupRoute, 0)
|
||||
for rows.Next() {
|
||||
var route LogicalGroupRoute
|
||||
if err := rows.Scan(
|
||||
&route.ID,
|
||||
&route.RouteID,
|
||||
&route.LogicalGroupID,
|
||||
&route.Name,
|
||||
&route.Status,
|
||||
&route.Priority,
|
||||
&route.Weight,
|
||||
&route.ShadowGroupID,
|
||||
&route.ShadowHostID,
|
||||
&route.UpstreamBaseURLHint,
|
||||
&route.CooldownUntil,
|
||||
&route.CreatedAt,
|
||||
&route.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan logical group route: %w", err)
|
||||
}
|
||||
routes = append(routes, route)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate logical group routes for %q: %w", logicalGroupID, err)
|
||||
}
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
func (r *LogicalGroupRoutesRepo) UpdateByRouteID(ctx context.Context, route LogicalGroupRoute) error {
|
||||
route, err := normalizeLogicalGroupRoute(route)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := r.db.ExecContext(
|
||||
ctx,
|
||||
`UPDATE logical_group_routes
|
||||
SET logical_group_id = ?, name = ?, status = ?, priority = ?, weight = ?, shadow_group_id = ?, shadow_host_id = ?, upstream_base_url_hint = ?, cooldown_until = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE route_id = ?`,
|
||||
route.LogicalGroupID,
|
||||
route.Name,
|
||||
route.Status,
|
||||
route.Priority,
|
||||
route.Weight,
|
||||
route.ShadowGroupID,
|
||||
route.ShadowHostID,
|
||||
route.UpstreamBaseURLHint,
|
||||
route.CooldownUntil,
|
||||
route.RouteID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update logical group route %q: %w", route.RouteID, err)
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("read updated logical group route rows for %q: %w", route.RouteID, err)
|
||||
}
|
||||
if affected == 0 {
|
||||
return fmt.Errorf("logical group route %q not found", route.RouteID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LogicalGroupRoutesRepo) DeleteByRouteID(ctx context.Context, routeID string) error {
|
||||
routeID = strings.TrimSpace(routeID)
|
||||
if routeID == "" {
|
||||
return fmt.Errorf("route_id is required")
|
||||
}
|
||||
|
||||
result, err := r.db.ExecContext(ctx, `DELETE FROM logical_group_routes WHERE route_id = ?`, routeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete logical group route %q: %w", routeID, err)
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("read deleted logical group route rows for %q: %w", routeID, err)
|
||||
}
|
||||
if affected == 0 {
|
||||
return fmt.Errorf("logical group route %q not found", routeID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeLogicalGroupRoute(route LogicalGroupRoute) (LogicalGroupRoute, error) {
|
||||
route.RouteID = strings.TrimSpace(route.RouteID)
|
||||
route.LogicalGroupID = strings.TrimSpace(route.LogicalGroupID)
|
||||
route.Name = strings.TrimSpace(route.Name)
|
||||
route.Status = strings.TrimSpace(route.Status)
|
||||
route.ShadowGroupID = strings.TrimSpace(route.ShadowGroupID)
|
||||
route.ShadowHostID = strings.TrimSpace(route.ShadowHostID)
|
||||
route.UpstreamBaseURLHint = strings.TrimSpace(route.UpstreamBaseURLHint)
|
||||
route.CooldownUntil = strings.TrimSpace(route.CooldownUntil)
|
||||
|
||||
switch {
|
||||
case route.RouteID == "":
|
||||
return LogicalGroupRoute{}, fmt.Errorf("route_id is required")
|
||||
case route.LogicalGroupID == "":
|
||||
return LogicalGroupRoute{}, fmt.Errorf("logical_group_id is required")
|
||||
case route.Name == "":
|
||||
return LogicalGroupRoute{}, fmt.Errorf("name is required")
|
||||
case route.Status == "":
|
||||
return LogicalGroupRoute{}, fmt.Errorf("status is required")
|
||||
case route.ShadowGroupID == "":
|
||||
return LogicalGroupRoute{}, fmt.Errorf("shadow_group_id is required")
|
||||
case route.ShadowHostID == "":
|
||||
return LogicalGroupRoute{}, fmt.Errorf("shadow_host_id is required")
|
||||
}
|
||||
if route.Weight <= 0 {
|
||||
route.Weight = 100
|
||||
}
|
||||
return route, nil
|
||||
}
|
||||
249
internal/store/sqlite/logical_groups_repo.go
Normal file
249
internal/store/sqlite/logical_groups_repo.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultLogicalGroupRoutePolicy = "priority"
|
||||
defaultLogicalGroupStickyMode = "conversation_preferred"
|
||||
defaultConversationTTLSeconds = 7200
|
||||
defaultUserModelTTLSeconds = 1800
|
||||
defaultFailoverThreshold = 2
|
||||
defaultCooldownSeconds = 600
|
||||
)
|
||||
|
||||
type LogicalGroup struct {
|
||||
ID int64
|
||||
LogicalGroupID string
|
||||
DisplayName string
|
||||
Status string
|
||||
Description string
|
||||
RoutePolicy string
|
||||
StickyMode string
|
||||
ConversationTTLSeconds int
|
||||
UserModelTTLSeconds int
|
||||
FailoverThreshold int
|
||||
CooldownSeconds int
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
type LogicalGroupsRepo struct {
|
||||
db execQuerier
|
||||
}
|
||||
|
||||
func newLogicalGroupsRepo(db execQuerier) *LogicalGroupsRepo {
|
||||
return &LogicalGroupsRepo{db: db}
|
||||
}
|
||||
|
||||
func (r *LogicalGroupsRepo) Create(ctx context.Context, group LogicalGroup) (int64, error) {
|
||||
group, err := normalizeLogicalGroup(group)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
result, err := r.db.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO logical_groups (
|
||||
logical_group_id,
|
||||
display_name,
|
||||
status,
|
||||
description,
|
||||
route_policy,
|
||||
sticky_mode,
|
||||
conversation_ttl_seconds,
|
||||
user_model_ttl_seconds,
|
||||
failover_threshold,
|
||||
cooldown_seconds
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
group.LogicalGroupID,
|
||||
group.DisplayName,
|
||||
group.Status,
|
||||
group.Description,
|
||||
group.RoutePolicy,
|
||||
group.StickyMode,
|
||||
group.ConversationTTLSeconds,
|
||||
group.UserModelTTLSeconds,
|
||||
group.FailoverThreshold,
|
||||
group.CooldownSeconds,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("insert logical group %q: %w", group.LogicalGroupID, err)
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read inserted logical group id for %q: %w", group.LogicalGroupID, err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (r *LogicalGroupsRepo) GetByLogicalGroupID(ctx context.Context, logicalGroupID string) (LogicalGroup, error) {
|
||||
logicalGroupID = strings.TrimSpace(logicalGroupID)
|
||||
if logicalGroupID == "" {
|
||||
return LogicalGroup{}, fmt.Errorf("logical_group_id is required")
|
||||
}
|
||||
|
||||
var group LogicalGroup
|
||||
if err := r.db.QueryRowContext(
|
||||
ctx,
|
||||
`SELECT id, logical_group_id, display_name, status, description, route_policy, sticky_mode, conversation_ttl_seconds, user_model_ttl_seconds, failover_threshold, cooldown_seconds, created_at, updated_at
|
||||
FROM logical_groups
|
||||
WHERE logical_group_id = ?`,
|
||||
logicalGroupID,
|
||||
).Scan(
|
||||
&group.ID,
|
||||
&group.LogicalGroupID,
|
||||
&group.DisplayName,
|
||||
&group.Status,
|
||||
&group.Description,
|
||||
&group.RoutePolicy,
|
||||
&group.StickyMode,
|
||||
&group.ConversationTTLSeconds,
|
||||
&group.UserModelTTLSeconds,
|
||||
&group.FailoverThreshold,
|
||||
&group.CooldownSeconds,
|
||||
&group.CreatedAt,
|
||||
&group.UpdatedAt,
|
||||
); err != nil {
|
||||
return LogicalGroup{}, err
|
||||
}
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (r *LogicalGroupsRepo) List(ctx context.Context) ([]LogicalGroup, error) {
|
||||
rows, err := r.db.QueryContext(
|
||||
ctx,
|
||||
`SELECT id, logical_group_id, display_name, status, description, route_policy, sticky_mode, conversation_ttl_seconds, user_model_ttl_seconds, failover_threshold, cooldown_seconds, created_at, updated_at
|
||||
FROM logical_groups
|
||||
ORDER BY id ASC`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list logical groups: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
groups := make([]LogicalGroup, 0)
|
||||
for rows.Next() {
|
||||
var group LogicalGroup
|
||||
if err := rows.Scan(
|
||||
&group.ID,
|
||||
&group.LogicalGroupID,
|
||||
&group.DisplayName,
|
||||
&group.Status,
|
||||
&group.Description,
|
||||
&group.RoutePolicy,
|
||||
&group.StickyMode,
|
||||
&group.ConversationTTLSeconds,
|
||||
&group.UserModelTTLSeconds,
|
||||
&group.FailoverThreshold,
|
||||
&group.CooldownSeconds,
|
||||
&group.CreatedAt,
|
||||
&group.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan logical group: %w", err)
|
||||
}
|
||||
groups = append(groups, group)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate logical groups: %w", err)
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (r *LogicalGroupsRepo) UpdateByLogicalGroupID(ctx context.Context, group LogicalGroup) error {
|
||||
group, err := normalizeLogicalGroup(group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := r.db.ExecContext(
|
||||
ctx,
|
||||
`UPDATE logical_groups
|
||||
SET display_name = ?, status = ?, description = ?, route_policy = ?, sticky_mode = ?, conversation_ttl_seconds = ?, user_model_ttl_seconds = ?, failover_threshold = ?, cooldown_seconds = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE logical_group_id = ?`,
|
||||
group.DisplayName,
|
||||
group.Status,
|
||||
group.Description,
|
||||
group.RoutePolicy,
|
||||
group.StickyMode,
|
||||
group.ConversationTTLSeconds,
|
||||
group.UserModelTTLSeconds,
|
||||
group.FailoverThreshold,
|
||||
group.CooldownSeconds,
|
||||
group.LogicalGroupID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update logical group %q: %w", group.LogicalGroupID, err)
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("read updated logical group rows for %q: %w", group.LogicalGroupID, err)
|
||||
}
|
||||
if affected == 0 {
|
||||
return fmt.Errorf("logical group %q not found", group.LogicalGroupID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LogicalGroupsRepo) DeleteByLogicalGroupID(ctx context.Context, logicalGroupID string) error {
|
||||
logicalGroupID = strings.TrimSpace(logicalGroupID)
|
||||
if logicalGroupID == "" {
|
||||
return fmt.Errorf("logical_group_id is required")
|
||||
}
|
||||
|
||||
result, err := r.db.ExecContext(ctx, `DELETE FROM logical_groups WHERE logical_group_id = ?`, logicalGroupID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete logical group %q: %w", logicalGroupID, err)
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("read deleted logical group rows for %q: %w", logicalGroupID, err)
|
||||
}
|
||||
if affected == 0 {
|
||||
return fmt.Errorf("logical group %q not found", logicalGroupID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeLogicalGroup(group LogicalGroup) (LogicalGroup, error) {
|
||||
group.LogicalGroupID = strings.TrimSpace(group.LogicalGroupID)
|
||||
group.DisplayName = strings.TrimSpace(group.DisplayName)
|
||||
group.Status = strings.TrimSpace(group.Status)
|
||||
group.Description = strings.TrimSpace(group.Description)
|
||||
group.RoutePolicy = strings.TrimSpace(group.RoutePolicy)
|
||||
group.StickyMode = strings.TrimSpace(group.StickyMode)
|
||||
|
||||
switch {
|
||||
case group.LogicalGroupID == "":
|
||||
return LogicalGroup{}, fmt.Errorf("logical_group_id is required")
|
||||
case group.DisplayName == "":
|
||||
return LogicalGroup{}, fmt.Errorf("display_name is required")
|
||||
case group.Status == "":
|
||||
return LogicalGroup{}, fmt.Errorf("status is required")
|
||||
}
|
||||
|
||||
if group.RoutePolicy == "" {
|
||||
group.RoutePolicy = defaultLogicalGroupRoutePolicy
|
||||
}
|
||||
if group.StickyMode == "" {
|
||||
group.StickyMode = defaultLogicalGroupStickyMode
|
||||
}
|
||||
if group.ConversationTTLSeconds <= 0 {
|
||||
group.ConversationTTLSeconds = defaultConversationTTLSeconds
|
||||
}
|
||||
if group.UserModelTTLSeconds <= 0 {
|
||||
group.UserModelTTLSeconds = defaultUserModelTTLSeconds
|
||||
}
|
||||
if group.FailoverThreshold <= 0 {
|
||||
group.FailoverThreshold = defaultFailoverThreshold
|
||||
}
|
||||
if group.CooldownSeconds <= 0 {
|
||||
group.CooldownSeconds = defaultCooldownSeconds
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}
|
||||
308
internal/store/sqlite/logical_groups_repo_test.go
Normal file
308
internal/store/sqlite/logical_groups_repo_test.go
Normal file
@@ -0,0 +1,308 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogicalGroupsRepoCreateGetUpdateDelete(t *testing.T) {
|
||||
store := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
id, err := store.LogicalGroups().Create(ctx, LogicalGroup{
|
||||
LogicalGroupID: "gpt-shared",
|
||||
DisplayName: "GPT Shared",
|
||||
Status: "active",
|
||||
Description: "shared group",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
if id <= 0 {
|
||||
t.Fatalf("Create() id = %d, want positive", id)
|
||||
}
|
||||
|
||||
group, err := store.LogicalGroups().GetByLogicalGroupID(ctx, "gpt-shared")
|
||||
if err != nil {
|
||||
t.Fatalf("GetByLogicalGroupID() error = %v", err)
|
||||
}
|
||||
if group.RoutePolicy != defaultLogicalGroupRoutePolicy {
|
||||
t.Fatalf("RoutePolicy = %q, want %q", group.RoutePolicy, defaultLogicalGroupRoutePolicy)
|
||||
}
|
||||
if group.StickyMode != defaultLogicalGroupStickyMode {
|
||||
t.Fatalf("StickyMode = %q, want %q", group.StickyMode, defaultLogicalGroupStickyMode)
|
||||
}
|
||||
|
||||
if err := store.LogicalGroups().UpdateByLogicalGroupID(ctx, LogicalGroup{
|
||||
LogicalGroupID: "gpt-shared",
|
||||
DisplayName: "GPT Shared Updated",
|
||||
Status: "paused",
|
||||
Description: "updated",
|
||||
RoutePolicy: "priority",
|
||||
StickyMode: "user_preferred",
|
||||
ConversationTTLSeconds: 3600,
|
||||
UserModelTTLSeconds: 900,
|
||||
FailoverThreshold: 3,
|
||||
CooldownSeconds: 120,
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdateByLogicalGroupID() error = %v", err)
|
||||
}
|
||||
|
||||
updated, err := store.LogicalGroups().GetByLogicalGroupID(ctx, "gpt-shared")
|
||||
if err != nil {
|
||||
t.Fatalf("GetByLogicalGroupID(updated) error = %v", err)
|
||||
}
|
||||
if updated.DisplayName != "GPT Shared Updated" || updated.Status != "paused" {
|
||||
t.Fatalf("updated group = %+v, want updated fields", updated)
|
||||
}
|
||||
|
||||
if err := store.LogicalGroups().DeleteByLogicalGroupID(ctx, "gpt-shared"); err != nil {
|
||||
t.Fatalf("DeleteByLogicalGroupID() error = %v", err)
|
||||
}
|
||||
_, err = store.LogicalGroups().GetByLogicalGroupID(ctx, "gpt-shared")
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
t.Fatalf("GetByLogicalGroupID() after delete error = %v, want sql.ErrNoRows", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogicalGroupsRepoList(t *testing.T) {
|
||||
store := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
for _, group := range []LogicalGroup{
|
||||
{LogicalGroupID: "group-a", DisplayName: "Group A", Status: "active"},
|
||||
{LogicalGroupID: "group-b", DisplayName: "Group B", Status: "active"},
|
||||
} {
|
||||
if _, err := store.LogicalGroups().Create(ctx, group); err != nil {
|
||||
t.Fatalf("Create(%q) error = %v", group.LogicalGroupID, err)
|
||||
}
|
||||
}
|
||||
|
||||
groups, err := store.LogicalGroups().List(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("List() error = %v", err)
|
||||
}
|
||||
if len(groups) != 2 {
|
||||
t.Fatalf("List() len = %d, want 2", len(groups))
|
||||
}
|
||||
if groups[0].LogicalGroupID != "group-a" || groups[1].LogicalGroupID != "group-b" {
|
||||
t.Fatalf("List() = %+v, want insertion order", groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogicalGroupModelsRepoCreateListDelete(t *testing.T) {
|
||||
store := openTestDBWithFK(t)
|
||||
ctx := context.Background()
|
||||
|
||||
if _, err := store.LogicalGroups().Create(ctx, LogicalGroup{
|
||||
LogicalGroupID: "gpt-shared",
|
||||
DisplayName: "GPT Shared",
|
||||
Status: "active",
|
||||
}); err != nil {
|
||||
t.Fatalf("LogicalGroups().Create() error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := store.LogicalGroupModels().Create(ctx, LogicalGroupModel{
|
||||
LogicalGroupID: "gpt-shared",
|
||||
PublicModel: "gpt-5.4",
|
||||
}); err != nil {
|
||||
t.Fatalf("LogicalGroupModels().Create() error = %v", err)
|
||||
}
|
||||
|
||||
models, err := store.LogicalGroupModels().ListByLogicalGroupID(ctx, "gpt-shared")
|
||||
if err != nil {
|
||||
t.Fatalf("ListByLogicalGroupID() error = %v", err)
|
||||
}
|
||||
if len(models) != 1 || models[0].PublicModel != "gpt-5.4" {
|
||||
t.Fatalf("ListByLogicalGroupID() = %+v, want gpt-5.4", models)
|
||||
}
|
||||
|
||||
if err := store.LogicalGroupModels().DeleteByLogicalGroupIDAndModel(ctx, "gpt-shared", "gpt-5.4"); err != nil {
|
||||
t.Fatalf("DeleteByLogicalGroupIDAndModel() error = %v", err)
|
||||
}
|
||||
models, err = store.LogicalGroupModels().ListByLogicalGroupID(ctx, "gpt-shared")
|
||||
if err != nil {
|
||||
t.Fatalf("ListByLogicalGroupID() after delete error = %v", err)
|
||||
}
|
||||
if len(models) != 0 {
|
||||
t.Fatalf("ListByLogicalGroupID() after delete len = %d, want 0", len(models))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogicalGroupRoutesRepoCreateGetListUpdateDelete(t *testing.T) {
|
||||
store := openTestDBWithFK(t)
|
||||
ctx := context.Background()
|
||||
|
||||
if _, err := store.LogicalGroups().Create(ctx, LogicalGroup{
|
||||
LogicalGroupID: "gpt-shared",
|
||||
DisplayName: "GPT Shared",
|
||||
Status: "active",
|
||||
}); err != nil {
|
||||
t.Fatalf("LogicalGroups().Create() error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := store.LogicalGroupRoutes().Create(ctx, LogicalGroupRoute{
|
||||
RouteID: "asxs",
|
||||
LogicalGroupID: "gpt-shared",
|
||||
Name: "ASXS",
|
||||
Status: "active",
|
||||
Priority: 10,
|
||||
ShadowGroupID: "gpt-shared__asxs",
|
||||
ShadowHostID: "remote43",
|
||||
}); err != nil {
|
||||
t.Fatalf("LogicalGroupRoutes().Create() error = %v", err)
|
||||
}
|
||||
|
||||
route, err := store.LogicalGroupRoutes().GetByRouteID(ctx, "asxs")
|
||||
if err != nil {
|
||||
t.Fatalf("GetByRouteID() error = %v", err)
|
||||
}
|
||||
if route.Weight != 100 {
|
||||
t.Fatalf("Weight = %d, want 100", route.Weight)
|
||||
}
|
||||
|
||||
routes, err := store.LogicalGroupRoutes().ListByLogicalGroupID(ctx, "gpt-shared")
|
||||
if err != nil {
|
||||
t.Fatalf("ListByLogicalGroupID() error = %v", err)
|
||||
}
|
||||
if len(routes) != 1 || routes[0].RouteID != "asxs" {
|
||||
t.Fatalf("ListByLogicalGroupID() = %+v, want route asxs", routes)
|
||||
}
|
||||
|
||||
if err := store.LogicalGroupRoutes().UpdateByRouteID(ctx, LogicalGroupRoute{
|
||||
RouteID: "asxs",
|
||||
LogicalGroupID: "gpt-shared",
|
||||
Name: "ASXS Updated",
|
||||
Status: "degraded",
|
||||
Priority: 20,
|
||||
Weight: 80,
|
||||
ShadowGroupID: "gpt-shared__asxs",
|
||||
ShadowHostID: "remote43",
|
||||
UpstreamBaseURLHint: "https://api.asxs.top/v1",
|
||||
CooldownUntil: "2026-05-28T16:00:00Z",
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdateByRouteID() error = %v", err)
|
||||
}
|
||||
|
||||
updated, err := store.LogicalGroupRoutes().GetByRouteID(ctx, "asxs")
|
||||
if err != nil {
|
||||
t.Fatalf("GetByRouteID(updated) error = %v", err)
|
||||
}
|
||||
if updated.Name != "ASXS Updated" || updated.Status != "degraded" || updated.Weight != 80 {
|
||||
t.Fatalf("updated route = %+v, want updated fields", updated)
|
||||
}
|
||||
|
||||
if err := store.LogicalGroupRoutes().DeleteByRouteID(ctx, "asxs"); err != nil {
|
||||
t.Fatalf("DeleteByRouteID() error = %v", err)
|
||||
}
|
||||
_, err = store.LogicalGroupRoutes().GetByRouteID(ctx, "asxs")
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
t.Fatalf("GetByRouteID() after delete error = %v, want sql.ErrNoRows", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogicalGroupRouteModelsRepoCreateList(t *testing.T) {
|
||||
store := openTestDBWithFK(t)
|
||||
ctx := context.Background()
|
||||
|
||||
if _, err := store.LogicalGroups().Create(ctx, LogicalGroup{
|
||||
LogicalGroupID: "gpt-shared",
|
||||
DisplayName: "GPT Shared",
|
||||
Status: "active",
|
||||
}); err != nil {
|
||||
t.Fatalf("LogicalGroups().Create() error = %v", err)
|
||||
}
|
||||
if _, err := store.LogicalGroupRoutes().Create(ctx, LogicalGroupRoute{
|
||||
RouteID: "codex2api",
|
||||
LogicalGroupID: "gpt-shared",
|
||||
Name: "Codex2API",
|
||||
Status: "active",
|
||||
Priority: 20,
|
||||
ShadowGroupID: "gpt-shared__codex2api",
|
||||
ShadowHostID: "remote43",
|
||||
}); err != nil {
|
||||
t.Fatalf("LogicalGroupRoutes().Create() error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := store.LogicalGroupRouteModels().Create(ctx, LogicalGroupRouteModel{
|
||||
RouteID: "codex2api",
|
||||
PublicModel: "gpt-5.4",
|
||||
}); err != nil {
|
||||
t.Fatalf("LogicalGroupRouteModels().Create() error = %v", err)
|
||||
}
|
||||
|
||||
models, err := store.LogicalGroupRouteModels().ListByRouteID(ctx, "codex2api")
|
||||
if err != nil {
|
||||
t.Fatalf("ListByRouteID() error = %v", err)
|
||||
}
|
||||
if len(models) != 1 {
|
||||
t.Fatalf("ListByRouteID() len = %d, want 1", len(models))
|
||||
}
|
||||
if models[0].ShadowModel != "gpt-5.4" {
|
||||
t.Fatalf("ShadowModel = %q, want default public model", models[0].ShadowModel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogicalGroupReposEnforceForeignKeysAndCascadeDelete(t *testing.T) {
|
||||
store := openTestDBWithFK(t)
|
||||
ctx := context.Background()
|
||||
|
||||
if _, err := store.LogicalGroupModels().Create(ctx, LogicalGroupModel{
|
||||
LogicalGroupID: "missing-group",
|
||||
PublicModel: "gpt-5.4",
|
||||
}); err == nil {
|
||||
t.Fatal("LogicalGroupModels().Create() error = nil, want foreign key failure")
|
||||
}
|
||||
|
||||
if _, err := store.LogicalGroups().Create(ctx, LogicalGroup{
|
||||
LogicalGroupID: "gpt-shared",
|
||||
DisplayName: "GPT Shared",
|
||||
Status: "active",
|
||||
}); err != nil {
|
||||
t.Fatalf("LogicalGroups().Create() error = %v", err)
|
||||
}
|
||||
if _, err := store.LogicalGroupModels().Create(ctx, LogicalGroupModel{
|
||||
LogicalGroupID: "gpt-shared",
|
||||
PublicModel: "gpt-5.4",
|
||||
}); err != nil {
|
||||
t.Fatalf("LogicalGroupModels().Create() error = %v", err)
|
||||
}
|
||||
if _, err := store.LogicalGroupRoutes().Create(ctx, LogicalGroupRoute{
|
||||
RouteID: "asxs",
|
||||
LogicalGroupID: "gpt-shared",
|
||||
Name: "ASXS",
|
||||
Status: "active",
|
||||
Priority: 10,
|
||||
ShadowGroupID: "gpt-shared__asxs",
|
||||
ShadowHostID: "remote43",
|
||||
}); err != nil {
|
||||
t.Fatalf("LogicalGroupRoutes().Create() error = %v", err)
|
||||
}
|
||||
if _, err := store.LogicalGroupRouteModels().Create(ctx, LogicalGroupRouteModel{
|
||||
RouteID: "asxs",
|
||||
PublicModel: "gpt-5.4",
|
||||
}); err != nil {
|
||||
t.Fatalf("LogicalGroupRouteModels().Create() error = %v", err)
|
||||
}
|
||||
|
||||
if err := store.LogicalGroups().DeleteByLogicalGroupID(ctx, "gpt-shared"); err != nil {
|
||||
t.Fatalf("DeleteByLogicalGroupID() error = %v", err)
|
||||
}
|
||||
|
||||
models, err := store.LogicalGroupModels().ListByLogicalGroupID(ctx, "gpt-shared")
|
||||
if err != nil {
|
||||
t.Fatalf("ListByLogicalGroupID() after cascade error = %v", err)
|
||||
}
|
||||
if len(models) != 0 {
|
||||
t.Fatalf("models after cascade len = %d, want 0", len(models))
|
||||
}
|
||||
routes, err := store.LogicalGroupRoutes().ListByLogicalGroupID(ctx, "gpt-shared")
|
||||
if err != nil {
|
||||
t.Fatalf("ListByLogicalGroupID(routes) after cascade error = %v", err)
|
||||
}
|
||||
if len(routes) != 0 {
|
||||
t.Fatalf("routes after cascade len = %d, want 0", len(routes))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user