mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
refactor(db): port operations, settings, audit, and misc metadata to sqlc (#84)
* refactor(db): port operations, settings, audit, activity, and misc service metadata to sqlc Covers the cross-cutting surface: operations board reads, app settings, notification producers, activity/audit inserts, builds upsert/cancel, git clone credential resolution, invites, environments, registry auth, project/service variables, HA resolver, boot sweeps, and admin user ops. Dynamic filtered lists and Better Auth table probes stay raw by design. Authored By: TDvorak <[email protected]> * refactor(db): port apwhy gateway handlers and proxy data plane to sqlc Final tranche — every static call site now uses generated queries. Raw SQL remains only where the statement is assembled at runtime (filtered lists, sparse updates, admin count closure) or probes Better Auth's auth_users table, which lives outside the local schema. Authored By: TDvorak <[email protected]>
This commit is contained in:
1 parent
66301bb68a
commit
0dc5857b5f
45 files changed
+3272
-689
No files matched your search
@@ -12,6 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -180,13 +181,13 @@ func handleGetProjectActivity(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Access check: approved / owner / member / admin.
|
// Access check: approved / owner / member / admin.
|
||||||
var visible bool
|
userUUID, _ := uuid.Parse(userID)
|
||||||
if err := db.QueryRow(
|
visible, err := sqlcdb.New(db.DB).CheckSecurityProjectAccess(context.Background(), sqlcdb.CheckSecurityProjectAccessParams{
|
||||||
`SELECT p.is_approved OR p.owner_id = $2::uuid OR $3::bool
|
ID: uuid.MustParse(projectID),
|
||||||
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $2::uuid)
|
OwnerID: userUUID,
|
||||||
FROM projects p WHERE p.id = $1`,
|
Column3: contextIsAdmin(c),
|
||||||
projectID, userID, contextIsAdmin(c),
|
})
|
||||||
).Scan(&visible); err != nil || !visible {
|
if err != nil || !visible {
|
||||||
respondError(c, http.StatusNotFound, "NOT_FOUND", "project not found")
|
respondError(c, http.StatusNotFound, "NOT_FOUND", "project not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -237,12 +238,12 @@ func StartAuditRetention(ctx context.Context, db *database.DB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prune := func() {
|
prune := func() {
|
||||||
res, err := db.Exec(`DELETE FROM audit_logs WHERE created_at < NOW() - ($1::int * INTERVAL '1 day')`, days)
|
n, err := sqlcdb.New(db.DB).PruneAuditLogs(context.Background(), int32(days))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("audit retention prune failed: %v", err)
|
log.Printf("audit retention prune failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if n, _ := res.RowsAffected(); n > 0 {
|
if n > 0 {
|
||||||
log.Printf("audit retention: pruned %d rows older than %d days", n, days)
|
log.Printf("audit retention: pruned %d rows older than %d days", n, days)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,34 +266,24 @@ func StartAuditRetention(ctx context.Context, db *database.DB) {
|
|||||||
// backfillAuditMetadata classifies rows written before severity/category/label
|
// backfillAuditMetadata classifies rows written before severity/category/label
|
||||||
// existed so filters and the feed see uniform metadata.
|
// existed so filters and the feed see uniform metadata.
|
||||||
func backfillAuditMetadata(db *database.DB) {
|
func backfillAuditMetadata(db *database.DB) {
|
||||||
cursor := uuid.Nil.String()
|
q := sqlcdb.New(db.DB)
|
||||||
|
cursor := uuid.Nil
|
||||||
for {
|
for {
|
||||||
rows, err := db.Query(
|
rows, err := q.ListAuditBackfillBatch(context.Background(), cursor)
|
||||||
`SELECT id::text, resource, action FROM audit_logs
|
|
||||||
WHERE label = '' AND id > $1::uuid ORDER BY id LIMIT 500`, cursor)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("audit backfill scan failed: %v", err)
|
log.Printf("audit backfill scan failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
type row struct{ id, resource, action string }
|
if len(rows) == 0 {
|
||||||
var batch []row
|
|
||||||
for rows.Next() {
|
|
||||||
var r row
|
|
||||||
if rows.Scan(&r.id, &r.resource, &r.action) == nil {
|
|
||||||
batch = append(batch, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rows.Close()
|
|
||||||
if len(batch) == 0 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, r := range batch {
|
for _, r := range rows {
|
||||||
cursor = r.id
|
id, _ := uuid.Parse(r.ID)
|
||||||
sev, cat, label := classifyAuditEvent(r.resource, r.action)
|
cursor = id
|
||||||
if _, err := db.Exec(
|
sev, cat, label := classifyAuditEvent(r.Resource, r.Action)
|
||||||
`UPDATE audit_logs SET severity = $1, category = $2, label = $3 WHERE id = $4::uuid`,
|
if err := q.SetAuditLogMetadata(context.Background(), sqlcdb.SetAuditLogMetadataParams{
|
||||||
sev, cat, label, r.id,
|
Severity: sev, Category: cat, Label: label, Column4: id,
|
||||||
); err != nil {
|
}); err != nil {
|
||||||
log.Printf("audit backfill update failed: %v", err)
|
log.Printf("audit backfill update failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,15 +52,11 @@ func EnsureAdminAccount(ctx context.Context, db *database.DB, cfg *config.Config
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var userID string
|
if _, err := sqlcdb.New(db.DB).UpsertAdminUser(ctx, sqlcdb.UpsertAdminUserParams{
|
||||||
err = db.QueryRowContext(ctx, `
|
Email: email,
|
||||||
INSERT INTO users (email, password_hash, name, is_admin)
|
PasswordHash: string(hashed),
|
||||||
VALUES ($1, $2, $3, true)
|
Name: name,
|
||||||
ON CONFLICT (email) DO UPDATE
|
}); err != nil {
|
||||||
SET is_admin = true, updated_at = NOW()
|
|
||||||
RETURNING id
|
|
||||||
`, email, string(hashed), name).Scan(&userID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Admin bootstrap failed to upsert user %s: %v", email, err)
|
log.Printf("Admin bootstrap failed to upsert user %s: %v", email, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -185,24 +181,18 @@ func handleAdminOverview(c *gin.Context) {
|
|||||||
func handleAdminListUsers(c *gin.Context) {
|
func handleAdminListUsers(c *gin.Context) {
|
||||||
db := c.MustGet("db").(*database.DB)
|
db := c.MustGet("db").(*database.DB)
|
||||||
|
|
||||||
rows, err := db.QueryContext(c.Request.Context(), `
|
rows, err := sqlcdb.New(db.DB).ListAdminUsers(c.Request.Context())
|
||||||
SELECT id, email, name, COALESCE(avatar_url, ''), is_admin, COALESCE(to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"'), '')
|
|
||||||
FROM users
|
|
||||||
ORDER BY created_at ASC
|
|
||||||
`)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list users"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list users"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
users := make([]adminUserRow, 0)
|
users := make([]adminUserRow, 0, len(rows))
|
||||||
for rows.Next() {
|
for _, r := range rows {
|
||||||
var u adminUserRow
|
users = append(users, adminUserRow{
|
||||||
if err := rows.Scan(&u.ID, &u.Email, &u.Name, &u.AvatarURL, &u.IsAdmin, &u.CreatedAt); err != nil {
|
ID: r.ID.String(), Email: r.Email, Name: r.Name,
|
||||||
continue
|
AvatarURL: r.AvatarUrl, IsAdmin: r.IsAdmin, CreatedAt: r.CreatedAt,
|
||||||
}
|
})
|
||||||
users = append(users, u)
|
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"users": users})
|
c.JSON(http.StatusOK, gin.H{"users": users})
|
||||||
}
|
}
|
||||||
@@ -229,15 +219,16 @@ func handleAdminSetUserAdmin(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := db.ExecContext(c.Request.Context(),
|
targetUUID, _ := uuid.Parse(targetID)
|
||||||
`UPDATE users SET is_admin = $1, updated_at = NOW() WHERE id = $2`,
|
n, err := sqlcdb.New(db.DB).SetUserAdmin(c.Request.Context(), sqlcdb.SetUserAdminParams{
|
||||||
req.IsAdmin, targetID,
|
IsAdmin: req.IsAdmin,
|
||||||
)
|
ID: targetUUID,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if n, _ := result.RowsAffected(); n == 0 {
|
if n == 0 {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -258,15 +249,13 @@ func handleAdminImpersonateUser(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var email, name string
|
userUUID, _ := uuid.Parse(targetID)
|
||||||
var isAdmin bool
|
info, err := sqlcdb.New(db.DB).GetAdminUserInfo(c.Request.Context(), userUUID)
|
||||||
err := db.QueryRowContext(c.Request.Context(),
|
|
||||||
`SELECT email, COALESCE(name, ''), is_admin FROM users WHERE id = $1`, targetID).
|
|
||||||
Scan(&email, &name, &isAdmin)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(c, http.StatusNotFound, "NOT_FOUND", "user not found")
|
respondError(c, http.StatusNotFound, "NOT_FOUND", "user not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
email, name, isAdmin := info.Email, info.Name, info.IsAdmin
|
||||||
if isAdmin {
|
if isAdmin {
|
||||||
respondError(c, http.StatusForbidden, "FORBIDDEN", "admin accounts cannot be impersonated")
|
respondError(c, http.StatusForbidden, "FORBIDDEN", "admin accounts cannot be impersonated")
|
||||||
return
|
return
|
||||||
@@ -286,9 +275,9 @@ func handleAdminImpersonateUser(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LogAuditWithRequest(c, "user", targetID, "impersonate", map[string]interface{}{
|
LogAuditWithRequest(c, "user", targetID, "impersonate", map[string]interface{}{
|
||||||
"actor_id": actorID,
|
"actor_id": actorID,
|
||||||
"target_email": email,
|
"target_email": email,
|
||||||
"expires_at": expiresAt.Format(time.RFC3339),
|
"expires_at": expiresAt.Format(time.RFC3339),
|
||||||
})
|
})
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"token": token,
|
"token": token,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -160,21 +162,22 @@ func handleAPwhyServiceValidate(c *gin.Context) {
|
|||||||
db := c.MustGet("db").(*database.DB)
|
db := c.MustGet("db").(*database.DB)
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
var upstreamURL, healthPath string
|
svcUUID, parseErr := uuid.Parse(serviceID)
|
||||||
var authHeader, authValue sql.NullString
|
var svc sqlcdb.GetAPServiceForValidateRow
|
||||||
var timeoutMs sql.NullInt64
|
var err error
|
||||||
err := db.QueryRowContext(ctx, `
|
if parseErr == nil {
|
||||||
SELECT upstream_url, health_path, upstream_auth_header, upstream_auth_value, request_timeout_ms
|
svc, err = sqlcdb.New(db.DB).GetAPServiceForValidate(ctx, svcUUID)
|
||||||
FROM api_services WHERE id = $1
|
}
|
||||||
`, serviceID).Scan(&upstreamURL, &healthPath, &authHeader, &authValue, &timeoutMs)
|
if parseErr != nil || err != nil {
|
||||||
if err != nil {
|
|
||||||
sendErrorResponse(c, http.StatusNotFound, "SERVICE_NOT_FOUND", "Service not found", nil)
|
sendErrorResponse(c, http.StatusNotFound, "SERVICE_NOT_FOUND", "Service not found", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
upstreamURL, healthPath := svc.UpstreamUrl, svc.HealthPath
|
||||||
|
authHeader, authValue := svc.UpstreamAuthHeader, svc.UpstreamAuthValue
|
||||||
|
|
||||||
timeout := 8000
|
timeout := 8000
|
||||||
if timeoutMs.Valid && timeoutMs.Int64 > 0 {
|
if svc.RequestTimeoutMs.Valid && svc.RequestTimeoutMs.Int32 > 0 {
|
||||||
timeout = int(timeoutMs.Int64)
|
timeout = int(svc.RequestTimeoutMs.Int32)
|
||||||
}
|
}
|
||||||
target := strings.TrimRight(upstreamURL, "/") + "/" + strings.TrimLeft(healthPath, "/")
|
target := strings.TrimRight(upstreamURL, "/") + "/" + strings.TrimLeft(healthPath, "/")
|
||||||
|
|
||||||
@@ -203,17 +206,19 @@ func handleAPwhyServiceValidate(c *gin.Context) {
|
|||||||
validationStatus = "healthy"
|
validationStatus = "healthy"
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _ = db.ExecContext(ctx, `
|
q := sqlcdb.New(db.DB)
|
||||||
UPDATE api_services
|
_ = q.SetAPServiceValidation(ctx, sqlcdb.SetAPServiceValidationParams{
|
||||||
SET last_validation_at = NOW(), last_validation_status = $1, last_validation_message = $2, updated_at = NOW()
|
LastValidationStatus: sql.NullString{String: validationStatus, Valid: true},
|
||||||
WHERE id = $3
|
LastValidationMessage: sql.NullString{String: message, Valid: true},
|
||||||
`, validationStatus, message, serviceID)
|
ID: svcUUID,
|
||||||
|
})
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
_, _ = db.ExecContext(ctx, `
|
_ = q.InsertServiceIncident(ctx, sqlcdb.InsertServiceIncidentParams{
|
||||||
INSERT INTO incident_events (service_id, code, message, severity, http_status, occurred_at)
|
ServiceID: uuid.NullUUID{UUID: svcUUID, Valid: true},
|
||||||
VALUES ($1, 'SERVICE_VALIDATION_FAILED', $2, 'medium', $3, NOW())
|
Message: message,
|
||||||
`, serviceID, message, status)
|
HttpStatus: sql.NullInt32{Int32: int32(status), Valid: true},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
sendSuccessResponse(c, http.StatusOK, map[string]interface{}{
|
sendSuccessResponse(c, http.StatusOK, map[string]interface{}{
|
||||||
@@ -231,36 +236,24 @@ func handleAPwhyServicesList(c *gin.Context) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Query services from database
|
// Query services from database
|
||||||
rows, err := db.QueryContext(ctx, `
|
rows, err := sqlcdb.New(db.DB).ListAPServices(ctx)
|
||||||
SELECT id, name, slug, upstream_url, route_prefix, enabled, created_at, updated_at
|
|
||||||
FROM api_services
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
`)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendErrorResponse(c, http.StatusInternalServerError, "DATABASE_ERROR",
|
sendErrorResponse(c, http.StatusInternalServerError, "DATABASE_ERROR",
|
||||||
"Failed to query services", err.Error())
|
"Failed to query services", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
services := make([]map[string]interface{}, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
var id, name, slug, upstreamURL, routePrefix, createdAt, updatedAt string
|
|
||||||
var enabled int
|
|
||||||
err := rows.Scan(&id, &name, &slug, &upstreamURL, &routePrefix, &enabled, &createdAt, &updatedAt)
|
|
||||||
if err != nil {
|
|
||||||
continue // Skip malformed rows
|
|
||||||
}
|
|
||||||
|
|
||||||
|
services := make([]map[string]interface{}, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
services = append(services, map[string]interface{}{
|
services = append(services, map[string]interface{}{
|
||||||
"id": id,
|
"id": r.ID.String(),
|
||||||
"name": name,
|
"name": r.Name,
|
||||||
"slug": slug,
|
"slug": r.Slug,
|
||||||
"upstreamUrl": upstreamURL,
|
"upstreamUrl": r.UpstreamUrl,
|
||||||
"routePrefix": routePrefix,
|
"routePrefix": r.RoutePrefix,
|
||||||
"enabled": enabled != 0,
|
"enabled": r.Enabled != 0,
|
||||||
"createdAt": createdAt,
|
"createdAt": r.CreatedAt,
|
||||||
"updatedAt": updatedAt,
|
"updatedAt": r.UpdatedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,14 +292,14 @@ func handleAPwhyServicesCreate(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// id defaults to gen_random_uuid(); enabled defaults to 1.
|
// id defaults to gen_random_uuid(); enabled defaults to 1.
|
||||||
var id string
|
svcID, err := sqlcdb.New(db.DB).InsertAPService(ctx, sqlcdb.InsertAPServiceParams{
|
||||||
err := db.QueryRowContext(ctx, `
|
Name: req.Name,
|
||||||
INSERT INTO api_services (
|
Slug: slug,
|
||||||
name, slug, upstream_url, route_prefix, health_path,
|
UpstreamUrl: req.UpstreamURL,
|
||||||
rpm_limit, monthly_quota, created_at, updated_at
|
RoutePrefix: req.RoutePrefix,
|
||||||
) VALUES ($1, $2, $3, $4, '/health', $5, $6, NOW(), NOW())
|
RpmLimit: sql.NullInt32{Int32: int32(rpmLimit), Valid: true},
|
||||||
RETURNING id
|
MonthlyQuota: sql.NullInt32{Int32: int32(monthlyQuota), Valid: true},
|
||||||
`, req.Name, slug, req.UpstreamURL, req.RoutePrefix, rpmLimit, monthlyQuota).Scan(&id)
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isUniqueViolation(err) {
|
if isUniqueViolation(err) {
|
||||||
@@ -320,7 +313,7 @@ func handleAPwhyServicesCreate(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
serviceData := map[string]interface{}{
|
serviceData := map[string]interface{}{
|
||||||
"id": id,
|
"id": svcID.String(),
|
||||||
"name": req.Name,
|
"name": req.Name,
|
||||||
"slug": slug,
|
"slug": slug,
|
||||||
"upstreamUrl": req.UpstreamURL,
|
"upstreamUrl": req.UpstreamURL,
|
||||||
@@ -356,13 +349,15 @@ func handleAPwhyServicesPatch(c *gin.Context) {
|
|||||||
db := c.MustGet("db").(*database.DB)
|
db := c.MustGet("db").(*database.DB)
|
||||||
|
|
||||||
if input.Enabled != nil {
|
if input.Enabled != nil {
|
||||||
enabled := 0
|
enabled := int32(0)
|
||||||
if *input.Enabled {
|
if *input.Enabled {
|
||||||
enabled = 1
|
enabled = 1
|
||||||
}
|
}
|
||||||
_, err := db.ExecContext(context.Background(),
|
svcUUID, _ := uuid.Parse(serviceID)
|
||||||
"UPDATE api_services SET enabled = $1, updated_at = NOW() WHERE id = $2",
|
err := sqlcdb.New(db.DB).SetAPServiceEnabled(context.Background(), sqlcdb.SetAPServiceEnabledParams{
|
||||||
enabled, serviceID)
|
Enabled: enabled,
|
||||||
|
ID: svcUUID,
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
@@ -383,11 +378,7 @@ func handleAPwhyServicesPatch(c *gin.Context) {
|
|||||||
func handleAPwhyKeysList(c *gin.Context) {
|
func handleAPwhyKeysList(c *gin.Context) {
|
||||||
db := c.MustGet("db").(*database.DB)
|
db := c.MustGet("db").(*database.DB)
|
||||||
|
|
||||||
rows, err := db.QueryContext(context.Background(), `
|
rows, err := sqlcdb.New(db.DB).ListAPKeys(context.Background())
|
||||||
SELECT id, name, key_prefix, plan, enabled, rpm_limit, monthly_quota, created_at, updated_at
|
|
||||||
FROM api_keys
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
`)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
@@ -395,34 +386,24 @@ func handleAPwhyKeysList(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
keys := make([]map[string]interface{}, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
var id, name, keyPrefix, plan, createdAt, updatedAt string
|
|
||||||
var enabled int
|
|
||||||
var rpmLimit, monthlyQuota sql.NullInt64
|
|
||||||
|
|
||||||
err := rows.Scan(&id, &name, &keyPrefix, &plan, &enabled, &rpmLimit, &monthlyQuota, &createdAt, &updatedAt)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
|
keys := make([]map[string]interface{}, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
key := map[string]interface{}{
|
key := map[string]interface{}{
|
||||||
"id": id,
|
"id": r.ID.String(),
|
||||||
"name": name,
|
"name": r.Name,
|
||||||
"keyPrefix": keyPrefix,
|
"keyPrefix": r.KeyPrefix,
|
||||||
"plan": plan,
|
"plan": r.Plan,
|
||||||
"enabled": enabled != 0,
|
"enabled": r.Enabled != 0,
|
||||||
"createdAt": createdAt,
|
"createdAt": r.CreatedAt,
|
||||||
"updatedAt": updatedAt,
|
"updatedAt": r.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
if rpmLimit.Valid {
|
if r.RpmLimit.Valid {
|
||||||
key["rpmLimit"] = rpmLimit.Int64
|
key["rpmLimit"] = int64(r.RpmLimit.Int32)
|
||||||
}
|
}
|
||||||
if monthlyQuota.Valid {
|
if r.MonthlyQuota.Valid {
|
||||||
key["monthlyQuota"] = monthlyQuota.Int64
|
key["monthlyQuota"] = int64(r.MonthlyQuota.Int32)
|
||||||
}
|
}
|
||||||
|
|
||||||
keys = append(keys, key)
|
keys = append(keys, key)
|
||||||
@@ -481,14 +462,14 @@ func handleAPwhyKeysCreate(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// id defaults to gen_random_uuid(); enabled defaults to 1.
|
// id defaults to gen_random_uuid(); enabled defaults to 1.
|
||||||
var id string
|
keyID, err := sqlcdb.New(db.DB).InsertAPKey(context.Background(), sqlcdb.InsertAPKeyParams{
|
||||||
err = db.QueryRowContext(context.Background(), `
|
Name: input.Name,
|
||||||
INSERT INTO api_keys (
|
KeyHash: keyHash,
|
||||||
name, key_hash, key_prefix, plan, allowed_service_ids,
|
KeyPrefix: keyPrefix,
|
||||||
rpm_limit, monthly_quota, created_at, updated_at
|
Plan: plan,
|
||||||
) VALUES ($1, $2, $3, $4, '[]', $5, $6, NOW(), NOW())
|
RpmLimit: sql.NullInt32{Int32: int32(rpmLimit), Valid: true},
|
||||||
RETURNING id
|
MonthlyQuota: sql.NullInt32{Int32: int32(monthlyQuota), Valid: true},
|
||||||
`, input.Name, keyHash, keyPrefix, plan, rpmLimit, monthlyQuota).Scan(&id)
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
@@ -501,7 +482,7 @@ func handleAPwhyKeysCreate(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, gin.H{
|
c.JSON(http.StatusCreated, gin.H{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"data": gin.H{
|
"data": gin.H{
|
||||||
"id": id,
|
"id": keyID.String(),
|
||||||
"name": input.Name,
|
"name": input.Name,
|
||||||
"plan": plan,
|
"plan": plan,
|
||||||
"key": apiKey, // Only return the actual key once
|
"key": apiKey, // Only return the actual key once
|
||||||
@@ -532,13 +513,15 @@ func handleAPwhyKeysPatch(c *gin.Context) {
|
|||||||
db := c.MustGet("db").(*database.DB)
|
db := c.MustGet("db").(*database.DB)
|
||||||
|
|
||||||
if input.Enabled != nil {
|
if input.Enabled != nil {
|
||||||
enabled := 0
|
enabled := int32(0)
|
||||||
if *input.Enabled {
|
if *input.Enabled {
|
||||||
enabled = 1
|
enabled = 1
|
||||||
}
|
}
|
||||||
_, err := db.ExecContext(context.Background(),
|
keyUUID, _ := uuid.Parse(keyID)
|
||||||
"UPDATE api_keys SET enabled = $1, updated_at = NOW() WHERE id = $2",
|
err := sqlcdb.New(db.DB).SetAPKeyEnabled(context.Background(), sqlcdb.SetAPKeyEnabledParams{
|
||||||
enabled, keyID)
|
Enabled: enabled,
|
||||||
|
ID: keyUUID,
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
@@ -560,23 +543,27 @@ func handleAPwhyAnalyticsOps(c *gin.Context) {
|
|||||||
db := c.MustGet("db").(*database.DB)
|
db := c.MustGet("db").(*database.DB)
|
||||||
|
|
||||||
// Get counts from database
|
// Get counts from database
|
||||||
var totalServices, totalKeys, totalUsers int
|
q := sqlcdb.New(db.DB)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
if err := db.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM api_services").Scan(&totalServices); err != nil {
|
totalServices, err := q.CountAPServices(ctx)
|
||||||
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"error": "Failed to count services: " + err.Error(),
|
"error": "Failed to count services: " + err.Error(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := db.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM api_keys").Scan(&totalKeys); err != nil {
|
totalKeys, err := q.CountAPKeys(ctx)
|
||||||
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"error": "Failed to count api keys: " + err.Error(),
|
"error": "Failed to count api keys: " + err.Error(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := db.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM users").Scan(&totalUsers); err != nil {
|
totalUsers, err := q.CountUsers(ctx)
|
||||||
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"error": "Failed to count users: " + err.Error(),
|
"error": "Failed to count users: " + err.Error(),
|
||||||
@@ -584,8 +571,8 @@ func handleAPwhyAnalyticsOps(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalRequests int
|
totalRequests, err := q.SumUsageCounters(ctx)
|
||||||
if err := db.QueryRowContext(context.Background(), "SELECT COALESCE(SUM(request_count), 0) FROM usage_counters").Scan(&totalRequests); err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"error": "Failed to aggregate request counters: " + err.Error(),
|
"error": "Failed to aggregate request counters: " + err.Error(),
|
||||||
@@ -593,12 +580,8 @@ func handleAPwhyAnalyticsOps(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestsToday int
|
requestsToday, err := q.SumRequestMetricSince(ctx, "day")
|
||||||
if err := db.QueryRowContext(context.Background(), `
|
if err != nil {
|
||||||
SELECT COALESCE(SUM(value), 0)::int
|
|
||||||
FROM metrics_timeseries
|
|
||||||
WHERE metric = 'request_total' AND occurred_at >= DATE_TRUNC('day', NOW())
|
|
||||||
`).Scan(&requestsToday); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"error": "Failed to aggregate today's requests: " + err.Error(),
|
"error": "Failed to aggregate today's requests: " + err.Error(),
|
||||||
@@ -606,12 +589,8 @@ func handleAPwhyAnalyticsOps(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestsThisMonth int
|
requestsThisMonth, err := q.SumRequestMetricSince(ctx, "month")
|
||||||
if err := db.QueryRowContext(context.Background(), `
|
if err != nil {
|
||||||
SELECT COALESCE(SUM(value), 0)::int
|
|
||||||
FROM metrics_timeseries
|
|
||||||
WHERE metric = 'request_total' AND occurred_at >= DATE_TRUNC('month', NOW())
|
|
||||||
`).Scan(&requestsThisMonth); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"error": "Failed to aggregate monthly requests: " + err.Error(),
|
"error": "Failed to aggregate monthly requests: " + err.Error(),
|
||||||
@@ -636,94 +615,47 @@ func handleAPwhyAnalyticsOps(c *gin.Context) {
|
|||||||
func handleAPwhyAnalyticsTraffic(c *gin.Context) {
|
func handleAPwhyAnalyticsTraffic(c *gin.Context) {
|
||||||
db := c.MustGet("db").(*database.DB)
|
db := c.MustGet("db").(*database.DB)
|
||||||
|
|
||||||
|
q := sqlcdb.New(db.DB)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
topServices := make([]map[string]interface{}, 0)
|
topServices := make([]map[string]interface{}, 0)
|
||||||
serviceRows, err := db.QueryContext(context.Background(), `
|
if serviceRows, err := q.ListTopAPServices(ctx); err == nil {
|
||||||
SELECT s.id, s.name, COALESCE(SUM(u.request_count), 0) AS total_requests
|
for _, r := range serviceRows {
|
||||||
FROM api_services s
|
topServices = append(topServices, map[string]interface{}{
|
||||||
LEFT JOIN usage_counters u ON u.service_id = s.id
|
"service_id": r.ID.String(),
|
||||||
GROUP BY s.id, s.name
|
"name": r.Name,
|
||||||
ORDER BY total_requests DESC, s.name ASC
|
"requests": r.TotalRequests,
|
||||||
LIMIT 10
|
})
|
||||||
`)
|
|
||||||
if err == nil {
|
|
||||||
defer serviceRows.Close()
|
|
||||||
for serviceRows.Next() {
|
|
||||||
var serviceID, serviceName string
|
|
||||||
var totalRequests int
|
|
||||||
if scanErr := serviceRows.Scan(&serviceID, &serviceName, &totalRequests); scanErr == nil {
|
|
||||||
topServices = append(topServices, map[string]interface{}{
|
|
||||||
"service_id": serviceID,
|
|
||||||
"name": serviceName,
|
|
||||||
"requests": totalRequests,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestsByDay := make([]map[string]interface{}, 0)
|
requestsByDay := make([]map[string]interface{}, 0)
|
||||||
trafficRows, err := db.QueryContext(context.Background(), `
|
if trafficRows, err := q.ListRequestsByDay(ctx); err == nil {
|
||||||
SELECT TO_CHAR(DATE_TRUNC('day', occurred_at), 'YYYY-MM-DD') AS day_bucket, COUNT(*) AS total
|
for _, r := range trafficRows {
|
||||||
FROM metrics_timeseries
|
requestsByDay = append(requestsByDay, map[string]interface{}{
|
||||||
WHERE metric = 'request_total' AND occurred_at >= NOW() - INTERVAL '7 days'
|
"day": r.DayBucket,
|
||||||
GROUP BY DATE_TRUNC('day', occurred_at)
|
"requests": r.Total,
|
||||||
ORDER BY DATE_TRUNC('day', occurred_at) ASC
|
})
|
||||||
`)
|
|
||||||
if err == nil {
|
|
||||||
defer trafficRows.Close()
|
|
||||||
for trafficRows.Next() {
|
|
||||||
var day string
|
|
||||||
var count int
|
|
||||||
if scanErr := trafficRows.Scan(&day, &count); scanErr == nil {
|
|
||||||
requestsByDay = append(requestsByDay, map[string]interface{}{
|
|
||||||
"day": day,
|
|
||||||
"requests": count,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statusCodes := make([]map[string]interface{}, 0)
|
statusCodes := make([]map[string]interface{}, 0)
|
||||||
statusRows, err := db.QueryContext(context.Background(), `
|
if statusRows, err := q.ListIncidentStatusCodes(ctx); err == nil {
|
||||||
SELECT COALESCE(http_status, 0) AS status_code, COALESCE(SUM(count), 0) AS total
|
for _, r := range statusRows {
|
||||||
FROM incident_events
|
statusCodes = append(statusCodes, map[string]interface{}{
|
||||||
WHERE occurred_at >= NOW() - INTERVAL '7 days'
|
"status_code": r.StatusCode,
|
||||||
GROUP BY COALESCE(http_status, 0)
|
"count": r.Total,
|
||||||
ORDER BY total DESC, status_code ASC
|
})
|
||||||
`)
|
|
||||||
if err == nil {
|
|
||||||
defer statusRows.Close()
|
|
||||||
for statusRows.Next() {
|
|
||||||
var code int
|
|
||||||
var total int
|
|
||||||
if scanErr := statusRows.Scan(&code, &total); scanErr == nil {
|
|
||||||
statusCodes = append(statusCodes, map[string]interface{}{
|
|
||||||
"status_code": code,
|
|
||||||
"count": total,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clientEvents := make([]map[string]interface{}, 0)
|
clientEvents := make([]map[string]interface{}, 0)
|
||||||
eventRows, err := db.QueryContext(context.Background(), `
|
if eventRows, err := q.ListClientEventPaths(ctx); err == nil {
|
||||||
SELECT COALESCE((labels_json::jsonb ->> 'path'), 'unknown') AS path, COUNT(*) AS total
|
for _, r := range eventRows {
|
||||||
FROM metrics_timeseries
|
clientEvents = append(clientEvents, map[string]interface{}{
|
||||||
WHERE metric = 'client_event' AND occurred_at >= NOW() - INTERVAL '7 days'
|
"path": r.Path,
|
||||||
GROUP BY COALESCE((labels_json::jsonb ->> 'path'), 'unknown')
|
"count": r.Total,
|
||||||
ORDER BY total DESC, path ASC
|
})
|
||||||
LIMIT 20
|
|
||||||
`)
|
|
||||||
if err == nil {
|
|
||||||
defer eventRows.Close()
|
|
||||||
for eventRows.Next() {
|
|
||||||
var path string
|
|
||||||
var total int
|
|
||||||
if scanErr := eventRows.Scan(&path, &total); scanErr == nil {
|
|
||||||
clientEvents = append(clientEvents, map[string]interface{}{
|
|
||||||
"path": path,
|
|
||||||
"count": total,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -11,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/sqlc-dev/pqtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuditLog struct {
|
type AuditLog struct {
|
||||||
@@ -98,16 +102,20 @@ func LogAudit(userID, resource, resourceID, action string, details map[string]in
|
|||||||
}
|
}
|
||||||
|
|
||||||
detailsJSON, _ := json.Marshal(details)
|
detailsJSON, _ := json.Marshal(details)
|
||||||
resourceUUID := parseUUIDOrNil(resourceID)
|
|
||||||
userUUID := parseUUIDOrNil(userID)
|
|
||||||
severity, category, label := classifyAuditEvent(resource, action)
|
severity, category, label := classifyAuditEvent(resource, action)
|
||||||
|
|
||||||
auditID := uuid.New().String()
|
err := sqlcdb.New(db.DB).InsertAuditLog(context.Background(), sqlcdb.InsertAuditLogParams{
|
||||||
_, err := db.Exec(
|
ID: uuid.New(),
|
||||||
`INSERT INTO audit_logs (id, user_id, resource, resource_id, action, details, severity, category, label, created_at)
|
UserID: nullUUIDOrNil(userID),
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
Resource: resource,
|
||||||
auditID, userUUID, resource, resourceUUID, action, string(detailsJSON), severity, category, label, time.Now().UTC(),
|
ResourceID: nullUUIDOrNil(resourceID),
|
||||||
)
|
Action: action,
|
||||||
|
Details: pqtype.NullRawMessage{RawMessage: detailsJSON, Valid: true},
|
||||||
|
Severity: severity,
|
||||||
|
Category: category,
|
||||||
|
Label: label,
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
}
|
}
|
||||||
@@ -131,16 +139,24 @@ func LogAuditWithRequest(c *gin.Context, resource, resourceID, action string, de
|
|||||||
if uid, ok := userID.(string); ok {
|
if uid, ok := userID.(string); ok {
|
||||||
userIDStr = uid
|
userIDStr = uid
|
||||||
}
|
}
|
||||||
userUUID := parseUUIDOrNil(userIDStr)
|
|
||||||
resourceUUID := parseUUIDOrNil(resourceID)
|
|
||||||
severity, category, label := classifyAuditEvent(resource, action)
|
severity, category, label := classifyAuditEvent(resource, action)
|
||||||
|
|
||||||
auditID := uuid.New().String()
|
var inet pqtype.Inet
|
||||||
_, err := db.Exec(
|
_ = inet.Scan(c.ClientIP())
|
||||||
`INSERT INTO audit_logs (id, user_id, resource, resource_id, action, details, ip_address, user_agent, severity, category, label, created_at)
|
err := sqlcdb.New(db.DB).InsertAuditLogWithRequest(context.Background(), sqlcdb.InsertAuditLogWithRequestParams{
|
||||||
VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7, $8, $9, $10, $11, $12)`,
|
ID: uuid.New(),
|
||||||
auditID, userUUID, resource, resourceUUID, action, string(detailsJSON), c.ClientIP(), c.GetHeader("User-Agent"), severity, category, label, time.Now().UTC(),
|
UserID: nullUUIDOrNil(userIDStr),
|
||||||
)
|
Resource: resource,
|
||||||
|
ResourceID: nullUUIDOrNil(resourceID),
|
||||||
|
Action: action,
|
||||||
|
Column6: json.RawMessage(detailsJSON),
|
||||||
|
Column7: inet,
|
||||||
|
UserAgent: sql.NullString{String: c.GetHeader("User-Agent"), Valid: true},
|
||||||
|
Severity: severity,
|
||||||
|
Category: category,
|
||||||
|
Label: label,
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
}
|
}
|
||||||
@@ -255,30 +271,29 @@ func handleGetResourceAuditLogs(c *gin.Context) {
|
|||||||
resource := c.Param("resource")
|
resource := c.Param("resource")
|
||||||
resourceID := c.Param("id")
|
resourceID := c.Param("id")
|
||||||
|
|
||||||
rows, err := db.Query(
|
rows, err := sqlcdb.New(db.DB).ListResourceAuditLogs(context.Background(), sqlcdb.ListResourceAuditLogsParams{
|
||||||
`SELECT id, COALESCE(user_id::text, ''), resource, COALESCE(resource_id::text, ''), action, COALESCE(details::text, '{}'),
|
UserID: nullUUIDOrNil(userID),
|
||||||
COALESCE(ip_address::text, ''), COALESCE(user_agent, ''), created_at
|
Resource: resource,
|
||||||
FROM audit_logs
|
ResourceID: nullUUIDOrNil(resourceID),
|
||||||
WHERE user_id::text = $1 AND resource = $2 AND resource_id::text = $3
|
})
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 100`,
|
|
||||||
userID, resource, resourceID,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch audit logs"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch audit logs"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var logs []AuditLog
|
var logs []AuditLog
|
||||||
for rows.Next() {
|
for _, row := range rows {
|
||||||
var log AuditLog
|
logs = append(logs, AuditLog{
|
||||||
err := rows.Scan(&log.ID, &log.UserID, &log.Resource, &log.ResourceID, &log.Action, &log.Details, &log.IPAddress, &log.UserAgent, &log.CreatedAt)
|
ID: row.ID.String(),
|
||||||
if err != nil {
|
UserID: row.UserID,
|
||||||
continue
|
Resource: row.Resource,
|
||||||
}
|
ResourceID: row.ResourceID,
|
||||||
logs = append(logs, log)
|
Action: row.Action,
|
||||||
|
Details: row.Details,
|
||||||
|
IPAddress: row.IpAddress,
|
||||||
|
UserAgent: row.UserAgent,
|
||||||
|
CreatedAt: row.CreatedAt,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"audit_logs": logs})
|
c.JSON(http.StatusOK, gin.H{"audit_logs": logs})
|
||||||
@@ -302,3 +317,8 @@ func parseUUIDOrNil(raw string) interface{} {
|
|||||||
}
|
}
|
||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nullUUIDOrNil(raw string) uuid.NullUUID {
|
||||||
|
parsed, err := uuid.Parse(strings.TrimSpace(raw))
|
||||||
|
return uuid.NullUUID{UUID: parsed, Valid: err == nil}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"containr/internal/build"
|
"containr/internal/build"
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
"containr/internal/docker"
|
"containr/internal/docker"
|
||||||
"containr/internal/types"
|
"containr/internal/types"
|
||||||
|
|
||||||
@@ -669,39 +670,22 @@ func (h *BuildHandler) upsertBuild(status *BuildStatusResponse) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
_, err := h.db.Exec(
|
err := sqlcdb.New(h.db.DB).UpsertBuild(context.Background(), sqlcdb.UpsertBuildParams{
|
||||||
`INSERT INTO builds
|
ID: status.ID,
|
||||||
(id, project_id, service_id, status, progress, started_at, completed_at, image_name, image_tag, size, error, log, metadata, created_at, updated_at)
|
ProjectID: nsNullIfEmpty(status.ProjectID),
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb, $14, $14)
|
ServiceID: nsNullIfEmpty(status.ServiceID),
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
Status: status.Status,
|
||||||
project_id = EXCLUDED.project_id,
|
Progress: int32(status.Progress),
|
||||||
service_id = EXCLUDED.service_id,
|
StartedAt: status.StartedAt,
|
||||||
status = EXCLUDED.status,
|
CompletedAt: ntPtr(status.CompletedAt),
|
||||||
progress = EXCLUDED.progress,
|
ImageName: status.ImageName,
|
||||||
started_at = EXCLUDED.started_at,
|
ImageTag: status.ImageTag,
|
||||||
completed_at = EXCLUDED.completed_at,
|
Size: status.Size,
|
||||||
image_name = EXCLUDED.image_name,
|
Error: nsNullIfEmpty(status.Error),
|
||||||
image_tag = EXCLUDED.image_tag,
|
Log: status.Log,
|
||||||
size = EXCLUDED.size,
|
Column13: json.RawMessage(jsonOrEmptyObject(metadataRaw)),
|
||||||
error = EXCLUDED.error,
|
CreatedAt: now,
|
||||||
log = EXCLUDED.log,
|
})
|
||||||
metadata = EXCLUDED.metadata,
|
|
||||||
updated_at = EXCLUDED.updated_at`,
|
|
||||||
status.ID,
|
|
||||||
nullIfEmptyString(status.ProjectID),
|
|
||||||
nullIfEmptyString(status.ServiceID),
|
|
||||||
status.Status,
|
|
||||||
status.Progress,
|
|
||||||
status.StartedAt,
|
|
||||||
status.CompletedAt,
|
|
||||||
status.ImageName,
|
|
||||||
status.ImageTag,
|
|
||||||
status.Size,
|
|
||||||
nullIfEmptyString(status.Error),
|
|
||||||
status.Log,
|
|
||||||
jsonOrEmptyObject(metadataRaw),
|
|
||||||
now,
|
|
||||||
)
|
|
||||||
if err != nil && h.isMissingBuildsTable(err) {
|
if err != nil && h.isMissingBuildsTable(err) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -713,18 +697,37 @@ func (h *BuildHandler) getBuildFromDB(buildID string) (BuildStatusResponse, bool
|
|||||||
return BuildStatusResponse{}, false, nil
|
return BuildStatusResponse{}, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
row := h.db.QueryRow(
|
row, err := sqlcdb.New(h.db.DB).GetBuild(context.Background(), buildID)
|
||||||
`SELECT id, project_id, service_id, status, progress, started_at, completed_at, image_name, image_tag, size, error, log, metadata
|
if err != nil {
|
||||||
FROM builds
|
if errors.Is(err, sql.ErrNoRows) || h.isMissingBuildsTable(err) {
|
||||||
WHERE id = $1`,
|
return BuildStatusResponse{}, false, nil
|
||||||
buildID,
|
}
|
||||||
)
|
return BuildStatusResponse{}, false, err
|
||||||
|
|
||||||
build, found, err := scanBuildRow(row.Scan)
|
|
||||||
if err != nil && h.isMissingBuildsTable(err) {
|
|
||||||
return BuildStatusResponse{}, false, nil
|
|
||||||
}
|
}
|
||||||
return build, found, err
|
build := BuildStatusResponse{
|
||||||
|
ID: row.ID,
|
||||||
|
ProjectID: row.ProjectID.String,
|
||||||
|
ServiceID: row.ServiceID.String,
|
||||||
|
Status: row.Status,
|
||||||
|
Progress: int(row.Progress),
|
||||||
|
StartedAt: row.StartedAt.UTC(),
|
||||||
|
ImageName: row.ImageName,
|
||||||
|
ImageTag: row.ImageTag,
|
||||||
|
Size: row.Size,
|
||||||
|
Error: row.Error.String,
|
||||||
|
Log: row.Log,
|
||||||
|
}
|
||||||
|
if t := timePtrNT(row.CompletedAt); t != nil {
|
||||||
|
u := t.UTC()
|
||||||
|
build.CompletedAt = &u
|
||||||
|
}
|
||||||
|
if len(row.Metadata) > 0 {
|
||||||
|
metadata := map[string]string{}
|
||||||
|
if err := json.Unmarshal(row.Metadata, &metadata); err == nil {
|
||||||
|
build.Metadata = metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return build, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *BuildHandler) listBuildsFromDB(projectID, serviceID, status string, page, limit int) ([]BuildStatusResponse, int, error) {
|
func (h *BuildHandler) listBuildsFromDB(projectID, serviceID, status string, page, limit int) ([]BuildStatusResponse, int, error) {
|
||||||
@@ -808,19 +811,11 @@ func (h *BuildHandler) cancelBuildInDB(buildID string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
result, err := h.db.Exec(
|
affected, err := sqlcdb.New(h.db.DB).CancelBuild(context.Background(), sqlcdb.CancelBuildParams{
|
||||||
`UPDATE builds
|
CompletedAt: sql.NullTime{Time: now, Valid: true},
|
||||||
SET status = 'cancelled',
|
Log: h.formatLogLine("Cancellation requested") + "\n",
|
||||||
progress = 100,
|
ID: buildID,
|
||||||
completed_at = $1,
|
})
|
||||||
log = COALESCE(log, '') || $2,
|
|
||||||
updated_at = $1
|
|
||||||
WHERE id = $3
|
|
||||||
AND status NOT IN ('success', 'failed', 'cancelled')`,
|
|
||||||
now,
|
|
||||||
h.formatLogLine("Cancellation requested")+"\n",
|
|
||||||
buildID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if h.isMissingBuildsTable(err) {
|
if h.isMissingBuildsTable(err) {
|
||||||
return false
|
return false
|
||||||
@@ -828,9 +823,7 @@ func (h *BuildHandler) cancelBuildInDB(buildID string) bool {
|
|||||||
log.Printf("failed to cancel build %s in database: %v", buildID, err)
|
log.Printf("failed to cancel build %s in database: %v", buildID, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if affected == 0 {
|
||||||
affected, err := result.RowsAffected()
|
|
||||||
if err != nil || affected == 0 {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -909,12 +902,9 @@ func scanBuildRow(scan func(dest ...interface{}) error) (BuildStatusResponse, bo
|
|||||||
return parsed, true, nil
|
return parsed, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func nullIfEmptyString(value string) interface{} {
|
func nsNullIfEmpty(value string) sql.NullString {
|
||||||
trimmed := strings.TrimSpace(value)
|
trimmed := strings.TrimSpace(value)
|
||||||
if trimmed == "" {
|
return sql.NullString{String: trimmed, Valid: trimmed != ""}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return trimmed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonOrEmptyObject(raw []byte) string {
|
func jsonOrEmptyObject(raw []byte) string {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -57,15 +59,12 @@ func contextIsAdmin(c *gin.Context) bool {
|
|||||||
// public; unapproved ones are visible to the owner, project members, and
|
// public; unapproved ones are visible to the owner, project members, and
|
||||||
// platform admins only.
|
// platform admins only.
|
||||||
func projectReadAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (bool, bool) {
|
func projectReadAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (bool, bool) {
|
||||||
var isApproved bool
|
q := sqlcdb.New(db.DB)
|
||||||
var ownerID string
|
row, err := q.GetProjectReadAccess(context.Background(), projectID)
|
||||||
err := db.QueryRow(
|
|
||||||
`SELECT is_approved, owner_id::text FROM projects WHERE id = $1`,
|
|
||||||
projectID,
|
|
||||||
).Scan(&isApproved, &ownerID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false
|
return false, false
|
||||||
}
|
}
|
||||||
|
isApproved, ownerID := row.IsApproved, row.OwnerID
|
||||||
if isApproved || contextIsAdmin(c) {
|
if isApproved || contextIsAdmin(c) {
|
||||||
return true, true
|
return true, true
|
||||||
}
|
}
|
||||||
@@ -76,11 +75,11 @@ func projectReadAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (bo
|
|||||||
if ownerID == userID {
|
if ownerID == userID {
|
||||||
return true, true
|
return true, true
|
||||||
}
|
}
|
||||||
var member bool
|
userUUID, _ := uuid.Parse(userID)
|
||||||
if err := db.QueryRow(
|
if member, err := q.ProjectMemberExists(context.Background(), sqlcdb.ProjectMemberExistsParams{
|
||||||
`SELECT EXISTS(SELECT 1 FROM project_members WHERE project_id = $1 AND user_id = $2)`,
|
ProjectID: projectID,
|
||||||
projectID, userID,
|
UserID: userUUID,
|
||||||
).Scan(&member); err == nil && member {
|
}); err == nil && member {
|
||||||
return true, true
|
return true, true
|
||||||
}
|
}
|
||||||
return true, false
|
return true, false
|
||||||
@@ -91,11 +90,8 @@ func projectReadAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (bo
|
|||||||
// project members, and platform admins may reach secret-adjacent data
|
// project members, and platform admins may reach secret-adjacent data
|
||||||
// such as variables and runtime logs.
|
// such as variables and runtime logs.
|
||||||
func projectManageAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (bool, bool) {
|
func projectManageAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (bool, bool) {
|
||||||
var ownerID string
|
q := sqlcdb.New(db.DB)
|
||||||
err := db.QueryRow(
|
ownerID, err := q.GetProjectOwnerText(context.Background(), projectID)
|
||||||
`SELECT owner_id::text FROM projects WHERE id = $1`,
|
|
||||||
projectID,
|
|
||||||
).Scan(&ownerID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false
|
return false, false
|
||||||
}
|
}
|
||||||
@@ -109,11 +105,11 @@ func projectManageAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (
|
|||||||
if ownerID == userID {
|
if ownerID == userID {
|
||||||
return true, true
|
return true, true
|
||||||
}
|
}
|
||||||
var member bool
|
userUUID, _ := uuid.Parse(userID)
|
||||||
if err := db.QueryRow(
|
if member, err := q.ProjectMemberExists(context.Background(), sqlcdb.ProjectMemberExistsParams{
|
||||||
`SELECT EXISTS(SELECT 1 FROM project_members WHERE project_id = $1 AND user_id = $2)`,
|
ProjectID: projectID,
|
||||||
projectID, userID,
|
UserID: userUUID,
|
||||||
).Scan(&member); err == nil && member {
|
}); err == nil && member {
|
||||||
return true, true
|
return true, true
|
||||||
}
|
}
|
||||||
return true, false
|
return true, false
|
||||||
@@ -121,8 +117,7 @@ func projectManageAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (
|
|||||||
|
|
||||||
// serviceProjectID resolves the project that owns a service.
|
// serviceProjectID resolves the project that owns a service.
|
||||||
func serviceProjectID(db *database.DB, serviceID uuid.UUID) (uuid.UUID, bool) {
|
func serviceProjectID(db *database.DB, serviceID uuid.UUID) (uuid.UUID, bool) {
|
||||||
var projectID uuid.UUID
|
projectID, err := sqlcdb.New(db.DB).GetServiceProjectID(context.Background(), serviceID)
|
||||||
err := db.QueryRow(`SELECT project_id FROM services WHERE id = $1`, serviceID).Scan(&projectID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return uuid.Nil, false
|
return uuid.Nil, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ func handleDeleteEnvironment(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var projectID uuid.UUID
|
var projectID uuid.UUID
|
||||||
if err := db.QueryRow(`SELECT project_id FROM environments WHERE id = $1`, envID).Scan(&projectID); err != nil {
|
if projectID, err = sqlcdb.New(db.DB).GetEnvironmentProjectID(context.Background(), envID); err != nil {
|
||||||
respondError(c, http.StatusNotFound, "NOT_FOUND", "Environment not found")
|
respondError(c, http.StatusNotFound, "NOT_FOUND", "Environment not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
"containr/internal/gateway"
|
"containr/internal/gateway"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,23 +109,28 @@ func extractGatewayKey(c *gin.Context) string {
|
|||||||
|
|
||||||
// lookupGatewayService resolves the slug to an enabled service row.
|
// lookupGatewayService resolves the slug to an enabled service row.
|
||||||
func lookupGatewayService(ctx context.Context, db *database.DB, slug string) (*gatewayService, error) {
|
func lookupGatewayService(ctx context.Context, db *database.DB, slug string) (*gatewayService, error) {
|
||||||
var s gatewayService
|
r, err := sqlcdb.New(db.DB).GetAPServiceBySlug(ctx, slug)
|
||||||
var enabled int
|
|
||||||
err := db.QueryRowContext(ctx, `
|
|
||||||
SELECT id, upstream_url, route_prefix, upstream_auth_header,
|
|
||||||
upstream_auth_value, enabled, rpm_limit, monthly_quota,
|
|
||||||
request_timeout_ms
|
|
||||||
FROM api_services WHERE slug = $1
|
|
||||||
`, slug).Scan(&s.ID, &s.UpstreamURL, &s.RoutePrefix,
|
|
||||||
&s.UpstreamAuthHeader, &s.UpstreamAuthValue, &enabled,
|
|
||||||
&s.RPMLimit, &s.MonthlyQuota, &s.RequestTimeoutMS)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
s.Enabled = enabled != 0
|
s := gatewayService{
|
||||||
|
ID: r.ID.String(),
|
||||||
|
UpstreamURL: r.UpstreamUrl,
|
||||||
|
RoutePrefix: r.RoutePrefix,
|
||||||
|
UpstreamAuthHeader: r.UpstreamAuthHeader,
|
||||||
|
UpstreamAuthValue: r.UpstreamAuthValue,
|
||||||
|
Enabled: r.Enabled != 0,
|
||||||
|
RPMLimit: nullInt32To64(r.RpmLimit),
|
||||||
|
MonthlyQuota: nullInt32To64(r.MonthlyQuota),
|
||||||
|
RequestTimeoutMS: int(r.RequestTimeoutMs.Int32),
|
||||||
|
}
|
||||||
return &s, nil
|
return &s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nullInt32To64(v sql.NullInt32) sql.NullInt64 {
|
||||||
|
return sql.NullInt64{Int64: int64(v.Int32), Valid: v.Valid}
|
||||||
|
}
|
||||||
|
|
||||||
// authenticateGatewayKey resolves a presented key by its indexed prefix and
|
// authenticateGatewayKey resolves a presented key by its indexed prefix and
|
||||||
// bcrypt-verifies candidates. Returns nil when nothing matches.
|
// bcrypt-verifies candidates. Returns nil when nothing matches.
|
||||||
func authenticateGatewayKey(ctx context.Context, db *database.DB, presented string) (*gatewayKey, error) {
|
func authenticateGatewayKey(ctx context.Context, db *database.DB, presented string) (*gatewayKey, error) {
|
||||||
@@ -131,26 +138,24 @@ func authenticateGatewayKey(ctx context.Context, db *database.DB, presented stri
|
|||||||
if len(prefix) > 8 {
|
if len(prefix) > 8 {
|
||||||
prefix = prefix[:8]
|
prefix = prefix[:8]
|
||||||
}
|
}
|
||||||
rows, err := db.QueryContext(ctx, `
|
rows, err := sqlcdb.New(db.DB).ListAPKeysByPrefix(ctx, prefix)
|
||||||
SELECT id, key_hash, enabled, rpm_limit, monthly_quota, allowed_service_ids
|
|
||||||
FROM api_keys WHERE key_prefix = $1
|
|
||||||
`, prefix)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
for _, r := range rows {
|
||||||
for rows.Next() {
|
k := gatewayKey{
|
||||||
var k gatewayKey
|
ID: r.ID.String(),
|
||||||
var enabled int
|
Hash: r.KeyHash,
|
||||||
if err := rows.Scan(&k.ID, &k.Hash, &enabled, &k.RPMLimit, &k.MonthlyQuota, &k.AllowedServiceIDs); err != nil {
|
Enabled: r.Enabled != 0,
|
||||||
return nil, err
|
RPMLimit: nullInt32To64(r.RpmLimit),
|
||||||
|
MonthlyQuota: nullInt32To64(r.MonthlyQuota),
|
||||||
|
AllowedServiceIDs: r.AllowedServiceIds,
|
||||||
}
|
}
|
||||||
k.Enabled = enabled != 0
|
|
||||||
if bcrypt.CompareHashAndPassword([]byte(k.Hash), []byte(presented)) == nil {
|
if bcrypt.CompareHashAndPassword([]byte(k.Hash), []byte(presented)) == nil {
|
||||||
return &k, nil
|
return &k, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, rows.Err()
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// keyAllowsService applies the allowed_service_ids JSON allowlist. An empty
|
// keyAllowsService applies the allowed_service_ids JSON allowlist. An empty
|
||||||
@@ -168,48 +173,56 @@ func keyAllowsService(k *gatewayKey, serviceID string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// monthlyUsage sums a counter dimension for the current calendar month.
|
// monthlyKeyUsage / monthlyServiceUsage sum a counter dimension for the
|
||||||
func monthlyUsage(ctx context.Context, db *database.DB, column, id, period string) (int, error) {
|
// current calendar month.
|
||||||
var total int
|
func monthlyKeyUsage(ctx context.Context, db *database.DB, id, period string) (int64, error) {
|
||||||
err := db.QueryRowContext(ctx, `
|
keyUUID, _ := uuid.Parse(id)
|
||||||
SELECT COALESCE(SUM(request_count), 0) FROM usage_counters
|
return sqlcdb.New(db.DB).SumUsageByKey(ctx, sqlcdb.SumUsageByKeyParams{
|
||||||
WHERE `+column+` = $1 AND period_month = $2
|
ApiKeyID: keyUUID, PeriodMonth: period,
|
||||||
`, id, period).Scan(&total)
|
})
|
||||||
return total, err
|
}
|
||||||
|
|
||||||
|
func monthlyServiceUsage(ctx context.Context, db *database.DB, id, period string) (int64, error) {
|
||||||
|
svcUUID, _ := uuid.Parse(id)
|
||||||
|
return sqlcdb.New(db.DB).SumUsageByService(ctx, sqlcdb.SumUsageByServiceParams{
|
||||||
|
ServiceID: svcUUID, PeriodMonth: period,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// recordGatewayRequest persists usage, metrics, and failure incidents for a
|
// recordGatewayRequest persists usage, metrics, and failure incidents for a
|
||||||
// proxied call. Runs in the request path but is three small writes.
|
// proxied call. Runs in the request path but is three small writes.
|
||||||
func recordGatewayRequest(ctx context.Context, db *database.DB, svc *gatewayService, k *gatewayKey, status int, period string) {
|
func recordGatewayRequest(ctx context.Context, db *database.DB, svc *gatewayService, k *gatewayKey, status int, period string) {
|
||||||
_, _ = db.ExecContext(ctx, `
|
q := sqlcdb.New(db.DB)
|
||||||
INSERT INTO usage_counters (api_key_id, service_id, period_month, request_count, updated_at)
|
keyUUID, _ := uuid.Parse(k.ID)
|
||||||
VALUES ($1, $2, $3, 1, NOW())
|
svcUUID, _ := uuid.Parse(svc.ID)
|
||||||
ON CONFLICT (api_key_id, service_id, period_month)
|
_ = q.UpsertUsageCounter(ctx, sqlcdb.UpsertUsageCounterParams{
|
||||||
DO UPDATE SET request_count = usage_counters.request_count + 1, updated_at = NOW()
|
ApiKeyID: keyUUID, ServiceID: svcUUID, PeriodMonth: period,
|
||||||
`, k.ID, svc.ID, period)
|
})
|
||||||
_, _ = db.ExecContext(ctx, `
|
_ = q.TouchAPKeyLastUsed(ctx, keyUUID)
|
||||||
UPDATE api_keys SET last_used_at = NOW() WHERE id = $1
|
|
||||||
`, k.ID)
|
|
||||||
labels, _ := json.Marshal(map[string]interface{}{
|
labels, _ := json.Marshal(map[string]interface{}{
|
||||||
"service_id": svc.ID,
|
"service_id": svc.ID,
|
||||||
"key_id": k.ID,
|
"key_id": k.ID,
|
||||||
"status": status,
|
"status": status,
|
||||||
})
|
})
|
||||||
_, _ = db.ExecContext(ctx, `
|
_ = q.InsertMetricPoint(ctx, sql.NullString{String: string(labels), Valid: true})
|
||||||
INSERT INTO metrics_timeseries (metric, value, labels_json, occurred_at)
|
|
||||||
VALUES ('gateway_request', 1, $1, NOW())
|
|
||||||
`, string(labels))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func recordGatewayIncident(ctx context.Context, db *database.DB, svcID string, keyID *string, code, msg, severity string, status int) {
|
func recordGatewayIncident(ctx context.Context, db *database.DB, svcID string, keyID *string, code, msg, severity string, status int) {
|
||||||
var kid interface{}
|
var kid uuid.NullUUID
|
||||||
if keyID != nil {
|
if keyID != nil {
|
||||||
kid = *keyID
|
if u, err := uuid.Parse(*keyID); err == nil {
|
||||||
|
kid = uuid.NullUUID{UUID: u, Valid: true}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_, _ = db.ExecContext(ctx, `
|
svcUUID, _ := uuid.Parse(svcID)
|
||||||
INSERT INTO incident_events (service_id, api_key_id, code, message, severity, http_status, occurred_at)
|
_ = sqlcdb.New(db.DB).InsertGatewayIncident(ctx, sqlcdb.InsertGatewayIncidentParams{
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, NOW())
|
ServiceID: uuid.NullUUID{UUID: svcUUID, Valid: svcUUID != uuid.Nil},
|
||||||
`, svcID, kid, code, msg, severity, status)
|
ApiKeyID: kid,
|
||||||
|
Code: code,
|
||||||
|
Message: msg,
|
||||||
|
Severity: severity,
|
||||||
|
HttpStatus: sql.NullInt32{Int32: int32(status), Valid: true},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGatewayProxy is mounted at /g/:slug and /g/:slug/*path on the root
|
// handleGatewayProxy is mounted at /g/:slug and /g/:slug/*path on the root
|
||||||
@@ -267,13 +280,13 @@ func handleGatewayProxy(c *gin.Context) {
|
|||||||
|
|
||||||
period := now.Format("2006-01")
|
period := now.Format("2006-01")
|
||||||
if key.MonthlyQuota.Valid && key.MonthlyQuota.Int64 > 0 {
|
if key.MonthlyQuota.Valid && key.MonthlyQuota.Int64 > 0 {
|
||||||
if used, err := monthlyUsage(ctx, db, "api_key_id", key.ID, period); err == nil && int64(used) >= key.MonthlyQuota.Int64 {
|
if used, err := monthlyKeyUsage(ctx, db, key.ID, period); err == nil && used >= key.MonthlyQuota.Int64 {
|
||||||
respondError(c, http.StatusTooManyRequests, "QUOTA_EXCEEDED", "Monthly request quota exceeded")
|
respondError(c, http.StatusTooManyRequests, "QUOTA_EXCEEDED", "Monthly request quota exceeded")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if svc.MonthlyQuota.Valid && svc.MonthlyQuota.Int64 > 0 {
|
if svc.MonthlyQuota.Valid && svc.MonthlyQuota.Int64 > 0 {
|
||||||
if used, err := monthlyUsage(ctx, db, "service_id", svc.ID, period); err == nil && int64(used) >= svc.MonthlyQuota.Int64 {
|
if used, err := monthlyServiceUsage(ctx, db, svc.ID, period); err == nil && used >= svc.MonthlyQuota.Int64 {
|
||||||
respondError(c, http.StatusTooManyRequests, "QUOTA_EXCEEDED", "Service monthly quota exceeded")
|
respondError(c, http.StatusTooManyRequests, "QUOTA_EXCEEDED", "Service monthly quota exceeded")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
"containr/internal/source"
|
"containr/internal/source"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// resolveGitCloneSpec turns a service's git_repo (clone URL, ssh remote, or
|
// resolveGitCloneSpec turns a service's git_repo (clone URL, ssh remote, or
|
||||||
@@ -22,26 +26,31 @@ func resolveGitCloneSpec(db *database.DB, ownerID, gitRepo, branch, commit, buil
|
|||||||
}
|
}
|
||||||
|
|
||||||
spec := source.Spec{Branch: branch, Commit: commit, BuildPath: buildPath}
|
spec := source.Spec{Branch: branch, Commit: commit, BuildPath: buildPath}
|
||||||
|
q := sqlcdb.New(db.DB)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
var providerID string
|
ownerUUID, _ := uuid.Parse(ownerID)
|
||||||
|
var providerID uuid.UUID
|
||||||
if !strings.Contains(cloneURL, "://") {
|
if !strings.Contains(cloneURL, "://") {
|
||||||
// Bare owner/repo — the connected repository row knows the host.
|
// Bare owner/repo — the connected repository row knows the host.
|
||||||
err = db.QueryRow(
|
row, err := q.GetGitRepoCloneByFullName(ctx, sqlcdb.GetGitRepoCloneByFullNameParams{
|
||||||
`SELECT clone_url, provider_id FROM git_repositories
|
FullName: cloneURL,
|
||||||
WHERE full_name = $1 AND user_id = $2 LIMIT 1`,
|
UserID: ownerUUID,
|
||||||
cloneURL, ownerID,
|
})
|
||||||
).Scan(&cloneURL, &providerID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return source.Spec{}, fmt.Errorf("repository %s is not connected — connect it under Git settings or store a clone URL", raw)
|
return source.Spec{}, fmt.Errorf("repository %s is not connected — connect it under Git settings or store a clone URL", raw)
|
||||||
}
|
}
|
||||||
|
cloneURL = row.CloneUrl
|
||||||
|
providerID = row.ProviderID
|
||||||
} else {
|
} else {
|
||||||
// URL form — match a connected repo for its provider when possible;
|
// URL form — match a connected repo for its provider when possible;
|
||||||
// an unmatched public URL still clones, just without credentials.
|
// an unmatched public URL still clones, just without credentials.
|
||||||
_ = db.QueryRow(
|
if pid, err := q.GetGitRepoProviderByCloneURL(ctx, sqlcdb.GetGitRepoProviderByCloneURLParams{
|
||||||
`SELECT provider_id FROM git_repositories
|
CloneUrl: cloneURL,
|
||||||
WHERE clone_url = $1 AND user_id = $2 LIMIT 1`,
|
UserID: ownerUUID,
|
||||||
cloneURL, ownerID,
|
}); err == nil {
|
||||||
).Scan(&providerID)
|
providerID = pid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := source.ValidateRemoteURL(cloneURL); err != nil {
|
if err := source.ValidateRemoteURL(cloneURL); err != nil {
|
||||||
@@ -49,17 +58,17 @@ func resolveGitCloneSpec(db *database.DB, ownerID, gitRepo, branch, commit, buil
|
|||||||
}
|
}
|
||||||
spec.CloneURL = cloneURL
|
spec.CloneURL = cloneURL
|
||||||
|
|
||||||
if providerID == "" {
|
if providerID == uuid.Nil {
|
||||||
return spec, nil
|
return spec, nil
|
||||||
}
|
}
|
||||||
var name, apiURL, token string
|
prov, err := q.GetGitProviderForFetch(ctx, sqlcdb.GetGitProviderForFetchParams{
|
||||||
if err := db.QueryRow(
|
ID: providerID,
|
||||||
`SELECT name, api_url, access_token FROM git_providers
|
UserID: ownerUUID,
|
||||||
WHERE id = $1 AND user_id = $2`,
|
})
|
||||||
providerID, ownerID,
|
if err != nil {
|
||||||
).Scan(&name, &apiURL, &token); err != nil {
|
|
||||||
return spec, nil // provider row gone — anonymous clone still works for public repos
|
return spec, nil // provider row gone — anonymous clone still works for public repos
|
||||||
}
|
}
|
||||||
|
name, apiURL, token := prov.Name, prov.ApiUrl, prov.AccessToken
|
||||||
|
|
||||||
switch strings.ToLower(name) {
|
switch strings.ToLower(name) {
|
||||||
case "github_app":
|
case "github_app":
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
"containr/internal/deployment"
|
"containr/internal/deployment"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -88,8 +89,8 @@ func handleAdminGitHubAppManifest(c *gin.Context) {
|
|||||||
|
|
||||||
callbackURL := base + "/git/github-app/callback"
|
callbackURL := base + "/git/github-app/callback"
|
||||||
manifest, err := json.Marshal(map[string]interface{}{
|
manifest, err := json.Marshal(map[string]interface{}{
|
||||||
"name": name,
|
"name": name,
|
||||||
"url": base,
|
"url": base,
|
||||||
"hook_attributes": map[string]interface{}{
|
"hook_attributes": map[string]interface{}{
|
||||||
"url": base + "/api/git/github-app/webhook",
|
"url": base + "/api/git/github-app/webhook",
|
||||||
"active": true,
|
"active": true,
|
||||||
@@ -206,9 +207,8 @@ func handleAdminGitHubAppConvert(c *gin.Context) {
|
|||||||
// GET /admin/git/github-app — provisioning status for the settings UI.
|
// GET /admin/git/github-app — provisioning status for the settings UI.
|
||||||
func handleAdminGetGitHubApp(c *gin.Context) {
|
func handleAdminGetGitHubApp(c *gin.Context) {
|
||||||
db := c.MustGet("db").(*database.DB)
|
db := c.MustGet("db").(*database.DB)
|
||||||
var stored string
|
|
||||||
source := "none"
|
source := "none"
|
||||||
if err := db.QueryRow(`SELECT value FROM app_settings WHERE key = $1`, settingGitHubAppID).Scan(&stored); err == nil && strings.TrimSpace(stored) != "" {
|
if v, err := sqlcdb.New(db.DB).GetAppSetting(c.Request.Context(), settingGitHubAppID); err == nil && strings.TrimSpace(v) != "" {
|
||||||
source = "settings"
|
source = "settings"
|
||||||
} else if strings.TrimSpace(os.Getenv("GITHUB_APP_ID")) != "" {
|
} else if strings.TrimSpace(os.Getenv("GITHUB_APP_ID")) != "" {
|
||||||
source = "env"
|
source = "env"
|
||||||
|
|||||||
@@ -193,8 +193,8 @@ func handleAcceptInvite(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var existing int
|
existing, err := q.CountUsersByEmail(c.Request.Context(), req.Email)
|
||||||
if err := db.QueryRow(`SELECT COUNT(*) FROM users WHERE email = $1`, req.Email).Scan(&existing); err != nil {
|
if err != nil {
|
||||||
respondError(c, http.StatusInternalServerError, "INTERNAL", "database error")
|
respondError(c, http.StatusInternalServerError, "INTERNAL", "database error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -214,17 +214,23 @@ func handleAcceptInvite(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var user User
|
row, err := q.InsertUser(c.Request.Context(), sqlcdb.InsertUserParams{
|
||||||
err = db.QueryRow(`
|
Email: req.Email,
|
||||||
INSERT INTO users (email, password_hash, name)
|
PasswordHash: string(hashedPassword),
|
||||||
VALUES ($1, $2, $3)
|
Name: strings.TrimSpace(req.Name),
|
||||||
RETURNING id, email, name, COALESCE(avatar_url, ''), is_admin, created_at
|
})
|
||||||
`, req.Email, string(hashedPassword), strings.TrimSpace(req.Name)).
|
|
||||||
Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(c, http.StatusConflict, "CONFLICT", "user already exists")
|
respondError(c, http.StatusConflict, "CONFLICT", "user already exists")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
user := User{
|
||||||
|
ID: row.ID.String(),
|
||||||
|
Email: row.Email,
|
||||||
|
Name: row.Name,
|
||||||
|
AvatarURL: row.AvatarUrl,
|
||||||
|
IsAdmin: row.IsAdmin,
|
||||||
|
CreatedAt: row.CreatedAt.Time.String(),
|
||||||
|
}
|
||||||
|
|
||||||
userUUID, _ := uuid.Parse(user.ID)
|
userUUID, _ := uuid.Parse(user.ID)
|
||||||
if err := q.MarkUserInviteUsed(c.Request.Context(), sqlcdb.MarkUserInviteUsedParams{
|
if err := q.MarkUserInviteUsed(c.Request.Context(), sqlcdb.MarkUserInviteUsedParams{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
"containr/internal/docker"
|
"containr/internal/docker"
|
||||||
|
|
||||||
containertypes "github.com/docker/docker/api/types/container"
|
containertypes "github.com/docker/docker/api/types/container"
|
||||||
@@ -60,17 +61,12 @@ func handleGetServiceMetrics(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var projectID uuid.UUID
|
svc, err := sqlcdb.New(db.DB).GetServiceProjectAndStatus(context.Background(), serviceID)
|
||||||
var status string
|
|
||||||
err = db.QueryRow(
|
|
||||||
`SELECT s.project_id, COALESCE(s.status, '')
|
|
||||||
FROM services s WHERE s.id = $1`,
|
|
||||||
serviceID,
|
|
||||||
).Scan(&projectID, &status)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
projectID := svc.ProjectID
|
||||||
if _, allowed := projectReadAccess(c, db, projectID); !allowed {
|
if _, allowed := projectReadAccess(c, db, projectID); !allowed {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -315,10 +315,9 @@ func (r *agentNodeRunner) replicaPayload(spec deployment.RuntimeSpec, agentID st
|
|||||||
// pullCredentials resolves the project owner's registry auth for the image
|
// pullCredentials resolves the project owner's registry auth for the image
|
||||||
// host so private images pull on remote nodes. Empty map → anonymous pull.
|
// host so private images pull on remote nodes. Empty map → anonymous pull.
|
||||||
func (r *agentNodeRunner) pullCredentials(projectID, imageRef string) map[string]interface{} {
|
func (r *agentNodeRunner) pullCredentials(projectID, imageRef string) map[string]interface{} {
|
||||||
var ownerID string
|
pid, _ := uuid.Parse(projectID)
|
||||||
if err := r.db.QueryRow(
|
ownerID, err := sqlcdb.New(r.db.DB).GetProjectOwnerText(context.Background(), pid)
|
||||||
`SELECT owner_id FROM projects WHERE id = $1`, projectID,
|
if err != nil {
|
||||||
).Scan(&ownerID); err != nil {
|
|
||||||
return map[string]interface{}{}
|
return map[string]interface{}{}
|
||||||
}
|
}
|
||||||
auth := registryAuthFor(r.db, ownerID, imageRef)
|
auth := registryAuthFor(r.db, ownerID, imageRef)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
"containr/internal/database/sqlcdb"
|
"containr/internal/database/sqlcdb"
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@@ -29,15 +30,13 @@ func notifyAdmins(db *database.DB, kind, title, body, resourceType, resourceID s
|
|||||||
if db == nil {
|
if db == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rows, err := db.Query(`SELECT id FROM users WHERE is_admin`)
|
adminIDs, err := sqlcdb.New(db.DB).ListAdminUserIDs(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("containr: notify admins lookup failed: %v", err)
|
log.Printf("containr: notify admins lookup failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
for _, id := range adminIDs {
|
||||||
for rows.Next() {
|
{
|
||||||
var id uuid.UUID
|
|
||||||
if err := rows.Scan(&id); err == nil {
|
|
||||||
insertUserNotification(db, id.String(), kind, title, body, resourceType, resourceID)
|
insertUserNotification(db, id.String(), kind, title, body, resourceType, resourceID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,34 +72,32 @@ type agentHealthRow struct {
|
|||||||
func reconcileAgentHealth(db *database.DB, staleAfter time.Duration) {
|
func reconcileAgentHealth(db *database.DB, staleAfter time.Duration) {
|
||||||
cutoff := time.Now().Add(-staleAfter)
|
cutoff := time.Now().Add(-staleAfter)
|
||||||
var agents []agentHealthRow
|
var agents []agentHealthRow
|
||||||
rows, err := db.Query(
|
q := sqlcdb.New(db.DB)
|
||||||
`SELECT id, COALESCE(name, hostname, id::text), COALESCE(status, ''),
|
agentRows, err := q.ListAgentHealthRows(context.Background(), sql.NullTime{Time: cutoff, Valid: true})
|
||||||
COALESCE(last_heartbeat < $1, TRUE), auto_prune
|
|
||||||
FROM node_agents`, cutoff)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("containr: agent health sweep: %v", err)
|
log.Printf("containr: agent health sweep: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for rows.Next() {
|
for _, r := range agentRows {
|
||||||
var a agentHealthRow
|
agents = append(agents, agentHealthRow{
|
||||||
if err := rows.Scan(&a.id, &a.name, &a.status, &a.stale, &a.autoPrune); err == nil {
|
id: r.ID, name: r.Name, status: r.Status, stale: r.Stale, autoPrune: r.AutoPrune,
|
||||||
agents = append(agents, a)
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
rows.Close()
|
|
||||||
|
|
||||||
for _, a := range agents {
|
for _, a := range agents {
|
||||||
switch {
|
switch {
|
||||||
case a.stale && a.status != "offline":
|
case a.stale && a.status != "offline":
|
||||||
if _, err := db.Exec(
|
if err := q.SetAgentStatus(context.Background(), sqlcdb.SetAgentStatusParams{
|
||||||
`UPDATE node_agents SET status = 'offline', updated_at = NOW() WHERE id = $1`, a.id); err != nil {
|
Status: sql.NullString{String: "offline", Valid: true}, ID: a.id,
|
||||||
|
}); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
notifyAdmins(db, "agent", "Node agent offline",
|
notifyAdmins(db, "agent", "Node agent offline",
|
||||||
fmt.Sprintf("Agent %s missed its heartbeat for over %s.", a.name, staleAfter), "agent", a.id)
|
fmt.Sprintf("Agent %s missed its heartbeat for over %s.", a.name, staleAfter), "agent", a.id)
|
||||||
case !a.stale && a.status == "offline":
|
case !a.stale && a.status == "offline":
|
||||||
if _, err := db.Exec(
|
if err := q.SetAgentStatus(context.Background(), sqlcdb.SetAgentStatusParams{
|
||||||
`UPDATE node_agents SET status = 'online', updated_at = NOW() WHERE id = $1`, a.id); err != nil {
|
Status: sql.NullString{String: "online", Valid: true}, ID: a.id,
|
||||||
|
}); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
notifyAdmins(db, "agent", "Node agent back online",
|
notifyAdmins(db, "agent", "Node agent back online",
|
||||||
@@ -186,17 +183,17 @@ func checkForUpgrade(db *database.DB, currentVersion string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastNotified string
|
lastNotified, _ := sqlcdb.New(db.DB).GetAppSetting(context.Background(), "upgrade_notified_tag")
|
||||||
_ = db.QueryRow(`SELECT value FROM app_settings WHERE key = 'upgrade_notified_tag'`).Scan(&lastNotified)
|
|
||||||
if lastNotified == release.TagName {
|
if lastNotified == release.TagName {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
notifyAdmins(db, "upgrade", "Containr update available",
|
notifyAdmins(db, "upgrade", "Containr update available",
|
||||||
fmt.Sprintf("Release %s is available (running %s). %s", release.TagName, currentVersion, release.HTMLURL),
|
fmt.Sprintf("Release %s is available (running %s). %s", release.TagName, currentVersion, release.HTMLURL),
|
||||||
"release", release.TagName)
|
"release", release.TagName)
|
||||||
_, _ = db.Exec(
|
_ = sqlcdb.New(db.DB).UpsertAppSettingValue(context.Background(), sqlcdb.UpsertAppSettingValueParams{
|
||||||
`INSERT INTO app_settings (key, value, updated_at) VALUES ('upgrade_notified_tag', $1, NOW())
|
Key: "upgrade_notified_tag",
|
||||||
ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()`, release.TagName)
|
Value: release.TagName,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// semverNewer reports whether candidate is a higher semver than current.
|
// semverNewer reports whether candidate is a higher semver than current.
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
"containr/internal/deployqueue"
|
"containr/internal/deployqueue"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET /operations — a single cross-cutting view of platform work: active
|
// GET /operations — a single cross-cutting view of platform work: active
|
||||||
@@ -55,91 +58,59 @@ func handleGetOperations(c *gin.Context) {
|
|||||||
db := c.MustGet("db").(*database.DB)
|
db := c.MustGet("db").(*database.DB)
|
||||||
isAdmin := contextIsAdmin(c)
|
isAdmin := contextIsAdmin(c)
|
||||||
|
|
||||||
// Same visibility rule as the deployment list endpoint.
|
userUUID, _ := uuid.Parse(userID)
|
||||||
visibility := `p.is_approved OR p.owner_id = $1 OR $2::bool
|
q := sqlcdb.New(db.DB)
|
||||||
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1)`
|
visibility := sqlcdb.ListActiveOperationDeploymentsParams{OwnerID: userUUID, Column2: isAdmin}
|
||||||
|
|
||||||
active := []operationDeployment{}
|
active := []operationDeployment{}
|
||||||
rows, err := db.Query(
|
if rows, err := q.ListActiveOperationDeployments(context.Background(), visibility); err == nil {
|
||||||
`SELECT d.id, d.service_id, s.name, p.name, d.status,
|
for _, r := range rows {
|
||||||
COALESCE(d.image_name, ''), d.error, d.started_at, d.completed_at, d.created_at
|
active = append(active, operationDeployment{
|
||||||
FROM deployments d
|
ID: r.ID.String(), ServiceID: r.ServiceID.String(), ServiceName: r.ServiceName,
|
||||||
JOIN services s ON s.id = d.service_id
|
ProjectName: r.ProjectName, Status: r.Status.String, Image: r.Image,
|
||||||
JOIN projects p ON p.id = s.project_id
|
Error: strPtrNS(r.Error), StartedAt: timePtrNT(r.StartedAt),
|
||||||
WHERE d.status IN ('queued','pending','building','deploying','rolling_back')
|
CompletedAt: timePtrNT(r.CompletedAt), CreatedAt: r.CreatedAt.Time,
|
||||||
AND (`+visibility+`)
|
})
|
||||||
ORDER BY d.created_at ASC`, userID, isAdmin)
|
|
||||||
if err == nil {
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var d operationDeployment
|
|
||||||
var sid string
|
|
||||||
_ = rows.Scan(&d.ID, &sid, &d.ServiceName, &d.ProjectName, &d.Status, &d.Image, &d.Error, &d.StartedAt, &d.CompletedAt, &d.CreatedAt)
|
|
||||||
d.ServiceID = sid
|
|
||||||
active = append(active, d)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recentFailed := []operationDeployment{}
|
recentFailed := []operationDeployment{}
|
||||||
rows2, err2 := db.Query(
|
if rows, err := q.ListRecentFailedDeployments(context.Background(), sqlcdb.ListRecentFailedDeploymentsParams{
|
||||||
`SELECT d.id, d.service_id, s.name, p.name, d.status,
|
OwnerID: userUUID, Column2: isAdmin,
|
||||||
COALESCE(d.image_name, ''), d.error, d.started_at, d.completed_at, d.created_at
|
}); err == nil {
|
||||||
FROM deployments d
|
for _, r := range rows {
|
||||||
JOIN services s ON s.id = d.service_id
|
recentFailed = append(recentFailed, operationDeployment{
|
||||||
JOIN projects p ON p.id = s.project_id
|
ID: r.ID.String(), ServiceID: r.ServiceID.String(), ServiceName: r.ServiceName,
|
||||||
WHERE d.status IN ('failed','cancelled','rolled_back')
|
ProjectName: r.ProjectName, Status: r.Status.String, Image: r.Image,
|
||||||
AND d.updated_at > NOW() - INTERVAL '24 hours'
|
Error: strPtrNS(r.Error), StartedAt: timePtrNT(r.StartedAt),
|
||||||
AND (`+visibility+`)
|
CompletedAt: timePtrNT(r.CompletedAt), CreatedAt: r.CreatedAt.Time,
|
||||||
ORDER BY d.updated_at DESC
|
})
|
||||||
LIMIT 20`, userID, isAdmin)
|
|
||||||
if err2 == nil {
|
|
||||||
defer rows2.Close()
|
|
||||||
for rows2.Next() {
|
|
||||||
var d operationDeployment
|
|
||||||
var sid string
|
|
||||||
_ = rows2.Scan(&d.ID, &sid, &d.ServiceName, &d.ProjectName, &d.Status, &d.Image, &d.Error, &d.StartedAt, &d.CompletedAt, &d.CreatedAt)
|
|
||||||
d.ServiceID = sid
|
|
||||||
recentFailed = append(recentFailed, d)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cronRuns := []operationCronRun{}
|
cronRuns := []operationCronRun{}
|
||||||
rows3, err3 := db.Query(
|
if rows, err := q.ListRecentCronRuns(context.Background(), sqlcdb.ListRecentCronRunsParams{
|
||||||
`SELECT e.id, j.name, j.schedule, e.status, e.started_at, e.finished_at, e.error
|
OwnerID: userUUID, Column2: isAdmin,
|
||||||
FROM cron_executions e
|
}); err == nil {
|
||||||
JOIN cron_jobs j ON j.id = e.cron_job_id
|
for _, r := range rows {
|
||||||
JOIN projects p ON p.id = j.project_id
|
cronRuns = append(cronRuns, operationCronRun{
|
||||||
WHERE e.started_at > NOW() - INTERVAL '24 hours'
|
ID: r.ID.String(), JobName: r.JobName, Schedule: r.Schedule, Status: r.Status.String,
|
||||||
AND (`+visibility+`)
|
StartedAt: r.StartedAt, FinishedAt: timePtrNT(r.FinishedAt), Error: strPtrNS(r.Error),
|
||||||
ORDER BY e.started_at DESC
|
})
|
||||||
LIMIT 30`, userID, isAdmin)
|
|
||||||
if err3 == nil {
|
|
||||||
defer rows3.Close()
|
|
||||||
for rows3.Next() {
|
|
||||||
var r operationCronRun
|
|
||||||
_ = rows3.Scan(&r.ID, &r.JobName, &r.Schedule, &r.Status, &r.StartedAt, &r.FinishedAt, &r.Error)
|
|
||||||
cronRuns = append(cronRuns, r)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
backups := []operationBackup{}
|
backups := []operationBackup{}
|
||||||
rows4, err4 := db.Query(
|
if rows, err := q.ListRecentOperationBackups(context.Background(), sqlcdb.ListRecentOperationBackupsParams{
|
||||||
`SELECT b.id, b.database_id, ds.name, b.status, b.size, b.created_at, b.completed_at
|
UserID: userID, Column2: isAdmin,
|
||||||
FROM database_backups b
|
}); err == nil {
|
||||||
JOIN database_services ds ON ds.id = b.database_id
|
for _, r := range rows {
|
||||||
WHERE b.created_at > NOW() - INTERVAL '24 hours'
|
backups = append(backups, operationBackup{
|
||||||
AND (ds.user_id = $1 OR $2::bool)
|
ID: r.ID, DatabaseID: r.DatabaseID, DBName: r.DbName, Status: r.Status,
|
||||||
ORDER BY b.created_at DESC
|
Size: r.Size, CreatedAt: r.CreatedAt.Time, Completed: timePtrNT(r.CompletedAt),
|
||||||
LIMIT 20`, userID, isAdmin)
|
})
|
||||||
if err4 == nil {
|
|
||||||
defer rows4.Close()
|
|
||||||
for rows4.Next() {
|
|
||||||
var b operationBackup
|
|
||||||
_ = rows4.Scan(&b.ID, &b.DatabaseID, &b.DBName, &b.Status, &b.Size, &b.CreatedAt, &b.Completed)
|
|
||||||
backups = append(backups, b)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var queue []deployqueue.ServiceQueueState
|
var queue []deployqueue.ServiceQueueState
|
||||||
if queueValue, exists := c.Get("deploy_queue"); exists && queueValue != nil {
|
if queueValue, exists := c.Get("deploy_queue"); exists && queueValue != nil {
|
||||||
queue = queueValue.(*deployqueue.Queue).Snapshot()
|
queue = queueValue.(*deployqueue.Queue).Snapshot()
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
"containr/internal/secrets"
|
"containr/internal/secrets"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -112,7 +114,8 @@ func handleUpdateProjectVariables(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
if _, err = tx.Exec(`DELETE FROM project_variables WHERE project_id = $1`, projectID); err != nil {
|
txq := sqlcdb.New(tx)
|
||||||
|
if err = txq.DeleteProjectVariables(c.Request.Context(), projectID); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear existing variables"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear existing variables"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -127,11 +130,15 @@ func handleUpdateProjectVariables(c *gin.Context) {
|
|||||||
if v.IsSecret {
|
if v.IsSecret {
|
||||||
value = secrets.Encrypt(value)
|
value = secrets.Encrypt(value)
|
||||||
}
|
}
|
||||||
if _, err = tx.Exec(
|
if err = txq.InsertProjectVariable(c.Request.Context(), sqlcdb.InsertProjectVariableParams{
|
||||||
`INSERT INTO project_variables (id, project_id, key, value, is_secret, created_at, updated_at)
|
ID: uuid.New(),
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
ProjectID: projectID,
|
||||||
uuid.New(), projectID, v.Key, value, v.IsSecret, now, now,
|
Key: v.Key,
|
||||||
); err != nil {
|
Value: value,
|
||||||
|
IsSecret: v.IsSecret,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert variable: " + v.Key})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert variable: " + v.Key})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -168,11 +175,10 @@ func handleUpdateProjectVariables(c *gin.Context) {
|
|||||||
|
|
||||||
// resolveSharedVar returns the decrypted value of a project variable.
|
// resolveSharedVar returns the decrypted value of a project variable.
|
||||||
func resolveSharedVar(db *database.DB, projectID uuid.UUID, key string) (string, bool) {
|
func resolveSharedVar(db *database.DB, projectID uuid.UUID, key string) (string, bool) {
|
||||||
var value string
|
value, err := sqlcdb.New(db.DB).GetProjectVariableValue(context.Background(), sqlcdb.GetProjectVariableValueParams{
|
||||||
err := db.QueryRow(
|
ProjectID: projectID,
|
||||||
`SELECT value FROM project_variables WHERE project_id = $1 AND key = $2`,
|
Key: key,
|
||||||
projectID, key,
|
})
|
||||||
).Scan(&value)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
"containr/internal/secrets"
|
"containr/internal/secrets"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
@@ -177,17 +179,17 @@ func handleDeleteRegistry(c *gin.Context) {
|
|||||||
// bare image names (nginx:latest).
|
// bare image names (nginx:latest).
|
||||||
func registryAuthFor(db *database.DB, ownerID, imageRef string) registry.AuthConfig {
|
func registryAuthFor(db *database.DB, ownerID, imageRef string) registry.AuthConfig {
|
||||||
host := registryHost(imageRef)
|
host := registryHost(imageRef)
|
||||||
var username, password string
|
ownerUUID, _ := uuid.Parse(ownerID)
|
||||||
err := db.QueryRow(
|
row, err := sqlcdb.New(db.DB).GetRegistryAuth(context.Background(), sqlcdb.GetRegistryAuthParams{
|
||||||
`SELECT username, password FROM registries WHERE owner_id = $1 AND host = $2`,
|
OwnerID: ownerUUID,
|
||||||
ownerID, host,
|
Host: host,
|
||||||
).Scan(&username, &password)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return registry.AuthConfig{}
|
return registry.AuthConfig{}
|
||||||
}
|
}
|
||||||
return registry.AuthConfig{
|
return registry.AuthConfig{
|
||||||
Username: username,
|
Username: row.Username,
|
||||||
Password: secrets.Decrypt(password),
|
Password: secrets.Decrypt(row.Password),
|
||||||
ServerAddress: host,
|
ServerAddress: host,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"containr/internal/build"
|
"containr/internal/build"
|
||||||
"containr/internal/config"
|
"containr/internal/config"
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
"containr/internal/deployment"
|
"containr/internal/deployment"
|
||||||
"containr/internal/deployqueue"
|
"containr/internal/deployqueue"
|
||||||
"containr/internal/docker"
|
"containr/internal/docker"
|
||||||
@@ -21,6 +22,7 @@ import (
|
|||||||
"containr/internal/secrets"
|
"containr/internal/secrets"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupRoutes(router *gin.Engine, db *database.DB, redis *database.Redis, cfg *config.Config) {
|
func SetupRoutes(router *gin.Engine, db *database.DB, redis *database.Redis, cfg *config.Config) {
|
||||||
@@ -83,19 +85,18 @@ func SetupRoutes(router *gin.Engine, db *database.DB, redis *database.Redis, cfg
|
|||||||
// 127.0.0.1 fallback stays for node-bound checks.
|
// 127.0.0.1 fallback stays for node-bound checks.
|
||||||
docker := dockerClient
|
docker := dockerClient
|
||||||
haManager.WithServiceResolver(func(ctx context.Context, serviceID string) (string, int, error) {
|
haManager.WithServiceResolver(func(ctx context.Context, serviceID string) (string, int, error) {
|
||||||
var name, projectID string
|
sid, err := uuid.Parse(serviceID)
|
||||||
var port int
|
|
||||||
err := db.QueryRowContext(ctx,
|
|
||||||
`SELECT name, project_id::text, COALESCE(port, 0) FROM services WHERE id = $1`,
|
|
||||||
serviceID,
|
|
||||||
).Scan(&name, &projectID, &port)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
if err := docker.ConnectSelfToNetwork(ctx, deployment.ProjectNetworkName(projectID)); err != nil {
|
svc, err := sqlcdb.New(db.DB).GetServiceNameProjectPort(ctx, sid)
|
||||||
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
return name, port, nil
|
if err := docker.ConnectSelfToNetwork(ctx, deployment.ProjectNetworkName(svc.ProjectID)); err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
return svc.Name, int(svc.Port), nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
haAPIManager := NewHAManager(haManager)
|
haAPIManager := NewHAManager(haManager)
|
||||||
@@ -137,31 +138,25 @@ func SetupRoutes(router *gin.Engine, db *database.DB, redis *database.Redis, cfg
|
|||||||
// Boot sweep: in-memory engine state dies on restart, so anything that
|
// Boot sweep: in-memory engine state dies on restart, so anything that
|
||||||
// looked in-flight is actually orphaned — mark it failed rather than
|
// looked in-flight is actually orphaned — mark it failed rather than
|
||||||
// leaving phantom 'building'/'queued' rows.
|
// leaving phantom 'building'/'queued' rows.
|
||||||
_, _ = db.Exec(
|
bootQ := sqlcdb.New(db.DB)
|
||||||
`UPDATE deployments SET status = 'failed', error = 'Interrupted by server restart', completed_at = NOW(), updated_at = NOW()
|
_ = bootQ.FailInterruptedDeployments(context.Background())
|
||||||
WHERE status IN ('queued', 'pending', 'building', 'deploying', 'rolling_back')`,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Boot sweep: encrypt any plaintext secret values left over from before
|
// Boot sweep: encrypt any plaintext secret values left over from before
|
||||||
// encryption-at-rest landed. Transparent and idempotent.
|
// encryption-at-rest landed. Transparent and idempotent.
|
||||||
if encRows, err := db.Query(`SELECT id, value FROM environment_variables WHERE is_secret = TRUE`); err == nil {
|
if encRows, err := bootQ.ListSecretEnvVars(context.Background()); err == nil {
|
||||||
type encRow struct {
|
toEncrypt := 0
|
||||||
id string
|
for _, r := range encRows {
|
||||||
value string
|
if secrets.IsEncrypted(r.Value) {
|
||||||
}
|
continue
|
||||||
var toEncrypt []encRow
|
}
|
||||||
for encRows.Next() {
|
if err := bootQ.UpdateEnvVarValue(context.Background(), sqlcdb.UpdateEnvVarValueParams{
|
||||||
var r encRow
|
Value: secrets.Encrypt(r.Value), ID: r.ID,
|
||||||
if err := encRows.Scan(&r.id, &r.value); err == nil && !secrets.IsEncrypted(r.value) {
|
}); err == nil {
|
||||||
toEncrypt = append(toEncrypt, r)
|
toEncrypt++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
encRows.Close()
|
if toEncrypt > 0 {
|
||||||
for _, r := range toEncrypt {
|
log.Printf("secrets: encrypted %d plaintext secret variable(s)", toEncrypt)
|
||||||
_, _ = db.Exec(`UPDATE environment_variables SET value = $1, updated_at = NOW() WHERE id = $2`, secrets.Encrypt(r.value), r.id)
|
|
||||||
}
|
|
||||||
if len(toEncrypt) > 0 {
|
|
||||||
log.Printf("secrets: encrypted %d plaintext secret variable(s)", len(toEncrypt))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
"containr/internal/deployment"
|
"containr/internal/deployment"
|
||||||
"containr/internal/deployqueue"
|
"containr/internal/deployqueue"
|
||||||
"context"
|
"context"
|
||||||
@@ -70,20 +71,18 @@ type Service struct {
|
|||||||
// loadServiceSleep fills the sleep-mode columns, which live outside the
|
// loadServiceSleep fills the sleep-mode columns, which live outside the
|
||||||
// wide service SELECTs. Missing columns (pre-migration) degrade to off.
|
// wide service SELECTs. Missing columns (pre-migration) degrade to off.
|
||||||
func loadServiceSleep(db *database.DB, s *Service) {
|
func loadServiceSleep(db *database.DB, s *Service) {
|
||||||
var idle int
|
row, err := sqlcdb.New(db.DB).GetServiceSleep(context.Background(), s.ID)
|
||||||
if err := db.QueryRow(
|
if err == nil {
|
||||||
`SELECT COALESCE(sleep_enabled, false), COALESCE(sleep_idle_minutes, 15) FROM services WHERE id = $1`,
|
s.SleepEnabled = row.SleepEnabled
|
||||||
s.ID,
|
s.SleepIdleMinutes = int(row.SleepIdleMinutes)
|
||||||
).Scan(&s.SleepEnabled, &idle); err == nil {
|
|
||||||
s.SleepIdleMinutes = idle
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// serviceNodeID reads the pin column directly — kept out of the wide service
|
// serviceNodeID reads the pin column directly — kept out of the wide service
|
||||||
// SELECTs so pre-migration schemas keep working.
|
// SELECTs so pre-migration schemas keep working.
|
||||||
func serviceNodeID(db *database.DB, serviceID uuid.UUID) string {
|
func serviceNodeID(db *database.DB, serviceID uuid.UUID) string {
|
||||||
var id sql.NullString
|
id, err := sqlcdb.New(db.DB).GetServiceNodeID(context.Background(), serviceID)
|
||||||
if err := db.QueryRow(`SELECT node_id FROM services WHERE id = $1`, serviceID).Scan(&id); err == nil {
|
if err == nil {
|
||||||
return id.String
|
return id.String
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
@@ -91,17 +90,14 @@ func serviceNodeID(db *database.DB, serviceID uuid.UUID) string {
|
|||||||
|
|
||||||
// serviceSpread reads the spread column with the same lazy pattern.
|
// serviceSpread reads the spread column with the same lazy pattern.
|
||||||
func serviceSpread(db *database.DB, serviceID uuid.UUID) bool {
|
func serviceSpread(db *database.DB, serviceID uuid.UUID) bool {
|
||||||
var spread bool
|
spread, err := sqlcdb.New(db.DB).GetServiceSpread(context.Background(), serviceID)
|
||||||
if err := db.QueryRow(`SELECT COALESCE(spread, false) FROM services WHERE id = $1`, serviceID).Scan(&spread); err == nil {
|
return err == nil && spread
|
||||||
return spread
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// servicePlacementTags reads the placement_tags column with the lazy pattern.
|
// servicePlacementTags reads the placement_tags column with the lazy pattern.
|
||||||
func servicePlacementTags(db *database.DB, serviceID uuid.UUID) []string {
|
func servicePlacementTags(db *database.DB, serviceID uuid.UUID) []string {
|
||||||
var raw []byte
|
raw, err := sqlcdb.New(db.DB).GetServicePlacementTags(context.Background(), serviceID)
|
||||||
if err := db.QueryRow(`SELECT placement_tags FROM services WHERE id = $1`, serviceID).Scan(&raw); err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var tags []string
|
var tags []string
|
||||||
@@ -179,8 +175,8 @@ func normalizeTraefikLabels(raw map[string]string) (map[string]string, error) {
|
|||||||
|
|
||||||
// serviceTraefikLabels lazy-reads the traefik_labels column.
|
// serviceTraefikLabels lazy-reads the traefik_labels column.
|
||||||
func serviceTraefikLabels(db *database.DB, serviceID uuid.UUID) map[string]string {
|
func serviceTraefikLabels(db *database.DB, serviceID uuid.UUID) map[string]string {
|
||||||
var raw []byte
|
raw, err := sqlcdb.New(db.DB).GetServiceTraefikLabels(context.Background(), serviceID)
|
||||||
if err := db.QueryRow(`SELECT traefik_labels FROM services WHERE id = $1`, serviceID).Scan(&raw); err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var labels map[string]string
|
var labels map[string]string
|
||||||
@@ -211,7 +207,6 @@ func tagsJSON(tags []string) []byte {
|
|||||||
// loadServiceNode fills the pin + spread + placement tags + the agent's
|
// loadServiceNode fills the pin + spread + placement tags + the agent's
|
||||||
// display name for responses.
|
// display name for responses.
|
||||||
func loadServiceNode(db *database.DB, s *Service) {
|
func loadServiceNode(db *database.DB, s *Service) {
|
||||||
var name sql.NullString
|
|
||||||
s.NodeID = serviceNodeID(db, s.ID)
|
s.NodeID = serviceNodeID(db, s.ID)
|
||||||
s.Spread = serviceSpread(db, s.ID)
|
s.Spread = serviceSpread(db, s.ID)
|
||||||
s.PlacementTags = servicePlacementTags(db, s.ID)
|
s.PlacementTags = servicePlacementTags(db, s.ID)
|
||||||
@@ -219,8 +214,8 @@ func loadServiceNode(db *database.DB, s *Service) {
|
|||||||
if s.NodeID == "" {
|
if s.NodeID == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := db.QueryRow(`SELECT name FROM node_agents WHERE id = $1`, s.NodeID).Scan(&name); err == nil {
|
if name, err := sqlcdb.New(db.DB).GetNodeAgentName(context.Background(), s.NodeID); err == nil {
|
||||||
s.NodeName = name.String
|
s.NodeName = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,8 +230,8 @@ type ServiceVolume struct {
|
|||||||
// loadServiceVolumes reads the JSONB column; absent/invalid data degrades
|
// loadServiceVolumes reads the JSONB column; absent/invalid data degrades
|
||||||
// to no mounts rather than failing the request.
|
// to no mounts rather than failing the request.
|
||||||
func loadServiceVolumes(db *database.DB, serviceID uuid.UUID) []deployment.VolumeMount {
|
func loadServiceVolumes(db *database.DB, serviceID uuid.UUID) []deployment.VolumeMount {
|
||||||
var raw []byte
|
raw, err := sqlcdb.New(db.DB).GetServiceVolumes(context.Background(), serviceID)
|
||||||
if err := db.QueryRow(`SELECT volumes FROM services WHERE id = $1`, serviceID).Scan(&raw); err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var stored []ServiceVolume
|
var stored []ServiceVolume
|
||||||
@@ -703,13 +698,11 @@ func handleCreateService(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getProjectEnvironmentID(db *database.DB, projectID uuid.UUID, environment string) (uuid.UUID, error) {
|
func getProjectEnvironmentID(db *database.DB, projectID uuid.UUID, environment string) (uuid.UUID, error) {
|
||||||
var environmentID uuid.UUID
|
row, err := sqlcdb.New(db.DB).GetProjectEnvironmentByName(context.Background(), sqlcdb.GetProjectEnvironmentByNameParams{
|
||||||
err := db.QueryRow(
|
ProjectID: projectID,
|
||||||
"SELECT id FROM environments WHERE project_id = $1 AND name = $2",
|
Name: environment,
|
||||||
projectID,
|
})
|
||||||
environment,
|
return row.ID, err
|
||||||
).Scan(&environmentID)
|
|
||||||
return environmentID, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func inferServiceSourceType(service Service) string {
|
func inferServiceSourceType(service Service) string {
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
"containr/internal/docker"
|
"containr/internal/docker"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Platform setting keys. Values stored in app_settings take precedence over
|
// Platform setting keys. Values stored in app_settings take precedence over
|
||||||
@@ -70,8 +72,7 @@ func handleGetBranding(c *gin.Context) {
|
|||||||
// then the caller's default.
|
// then the caller's default.
|
||||||
func settingValue(db *database.DB, key, envKey, fallback string) string {
|
func settingValue(db *database.DB, key, envKey, fallback string) string {
|
||||||
if db != nil && db.DB != nil {
|
if db != nil && db.DB != nil {
|
||||||
var value string
|
value, err := sqlcdb.New(db.DB).GetAppSetting(context.Background(), key)
|
||||||
err := db.QueryRow(`SELECT value FROM app_settings WHERE key = $1`, key).Scan(&value)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
@@ -88,18 +89,15 @@ func settingValue(db *database.DB, key, envKey, fallback string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setSetting(db *database.DB, key, value string, secret bool) error {
|
func setSetting(db *database.DB, key, value string, secret bool) error {
|
||||||
_, err := db.Exec(`
|
return sqlcdb.New(db.DB).UpsertAppSetting(context.Background(), sqlcdb.UpsertAppSettingParams{
|
||||||
INSERT INTO app_settings (key, value, is_secret, updated_at)
|
Key: key,
|
||||||
VALUES ($1, $2, $3, NOW())
|
Value: value,
|
||||||
ON CONFLICT (key) DO UPDATE
|
IsSecret: secret,
|
||||||
SET value = EXCLUDED.value, is_secret = EXCLUDED.is_secret, updated_at = NOW()
|
})
|
||||||
`, key, value, secret)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteSetting(db *database.DB, key string) error {
|
func deleteSetting(db *database.DB, key string) error {
|
||||||
_, err := db.Exec(`DELETE FROM app_settings WHERE key = $1`, key)
|
return sqlcdb.New(db.DB).DeleteAppSetting(context.Background(), key)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// signupEnabled controls whether public registration stays open after the
|
// signupEnabled controls whether public registration stays open after the
|
||||||
@@ -110,11 +108,12 @@ func signupEnabled(db *database.DB) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isAdminUser(db *database.DB, userID string) bool {
|
func isAdminUser(db *database.DB, userID string) bool {
|
||||||
var isAdmin bool
|
parsed, err := uuid.Parse(userID)
|
||||||
if err := db.QueryRow(`SELECT is_admin FROM users WHERE id = $1`, userID).Scan(&isAdmin); err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return isAdmin
|
isAdmin, err := sqlcdb.New(db.DB).GetUserIsAdmin(context.Background(), parsed)
|
||||||
|
return err == nil && isAdmin
|
||||||
}
|
}
|
||||||
|
|
||||||
func requireAdmin(c *gin.Context) (*database.DB, bool) {
|
func requireAdmin(c *gin.Context) (*database.DB, bool) {
|
||||||
@@ -242,8 +241,7 @@ func handleUpdateSettings(c *gin.Context) {
|
|||||||
// resolvedTunnelToken prefers the in-app value and falls back to the env.
|
// resolvedTunnelToken prefers the in-app value and falls back to the env.
|
||||||
func resolvedTunnelToken(db *database.DB) (string, string) {
|
func resolvedTunnelToken(db *database.DB) (string, string) {
|
||||||
if db != nil && db.DB != nil {
|
if db != nil && db.DB != nil {
|
||||||
var value string
|
if value, err := sqlcdb.New(db.DB).GetAppSetting(context.Background(), settingCloudflareTunnelToken); err == nil && strings.TrimSpace(value) != "" {
|
||||||
if err := db.QueryRow(`SELECT value FROM app_settings WHERE key = $1`, settingCloudflareTunnelToken).Scan(&value); err == nil && strings.TrimSpace(value) != "" {
|
|
||||||
return strings.TrimSpace(value), "app"
|
return strings.TrimSpace(value), "app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ package api
|
|||||||
// lives in app_settings so it survives restarts.
|
// lives in app_settings so it survives restarts.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"containr/internal/database"
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -25,8 +27,7 @@ func setupComplete(db *database.DB, hasUsers bool) bool {
|
|||||||
if !hasUsers {
|
if !hasUsers {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
var projects int
|
if projects, err := sqlcdb.New(db.DB).CountProjects(context.Background()); err == nil && projects > 0 {
|
||||||
if err := db.QueryRow(`SELECT COUNT(*) FROM projects`).Scan(&projects); err == nil && projects > 0 {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -640,19 +640,20 @@ func deployTemplateGraph(
|
|||||||
createdServiceIDs := []uuid.UUID{}
|
createdServiceIDs := []uuid.UUID{}
|
||||||
createdDatabaseIDs := []string{}
|
createdDatabaseIDs := []string{}
|
||||||
cleanup := func() {
|
cleanup := func() {
|
||||||
|
q := sqlcdb.New(db.DB)
|
||||||
for _, sid := range createdServiceIDs {
|
for _, sid := range createdServiceIDs {
|
||||||
if engine != nil {
|
if engine != nil {
|
||||||
removeServiceRuntime(context.Background(), db, engine, sid)
|
removeServiceRuntime(context.Background(), db, engine, sid)
|
||||||
}
|
}
|
||||||
_, _ = db.Exec(`DELETE FROM environment_variables WHERE service_id = $1`, sid)
|
_ = q.DeleteEnvVarsForService(context.Background(), sid)
|
||||||
_, _ = db.Exec(`DELETE FROM services WHERE id = $1`, sid)
|
_ = q.DeleteServiceByID(context.Background(), sid)
|
||||||
}
|
}
|
||||||
for _, did := range createdDatabaseIDs {
|
for _, did := range createdDatabaseIDs {
|
||||||
if dbHandler != nil && dbHandler.dockerClient != nil {
|
if dbHandler != nil && dbHandler.dockerClient != nil {
|
||||||
_ = dbHandler.dockerClient.RemoveContainer(context.Background(), managedDatabaseContainerName(did), true)
|
_ = dbHandler.dockerClient.RemoveContainer(context.Background(), managedDatabaseContainerName(did), true)
|
||||||
_ = dbHandler.dockerClient.RemoveVolume(context.Background(), managedDatabaseVolumeName(did), true)
|
_ = dbHandler.dockerClient.RemoveVolume(context.Background(), managedDatabaseVolumeName(did), true)
|
||||||
}
|
}
|
||||||
_, _ = db.Exec(`DELETE FROM database_services WHERE id = $1`, did)
|
_ = q.DeleteDatabaseServiceByID(context.Background(), did)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,14 +773,19 @@ func deployTemplateGraph(
|
|||||||
if builder == "" {
|
if builder == "" {
|
||||||
builder = "auto"
|
builder = "auto"
|
||||||
}
|
}
|
||||||
if _, err = tx.ExecContext(ctx,
|
var volumesRaw json.RawMessage
|
||||||
`UPDATE services SET port = $1, healthcheck_path = $2, volumes = COALESCE($3::jsonb, '[]'::jsonb),
|
if s, ok := volumesJSON.(string); ok {
|
||||||
git_repo = NULLIF($4, ''), git_branch = NULLIF($5, ''), builder = $6
|
volumesRaw = json.RawMessage(s)
|
||||||
WHERE id = $7`,
|
}
|
||||||
m.spec.Port, strings.TrimSpace(m.spec.HealthCheck), volumesJSON,
|
if err = sqlcdb.New(tx).UpdateServiceDeployMeta(ctx, sqlcdb.UpdateServiceDeployMetaParams{
|
||||||
strings.TrimSpace(m.spec.Repo), strings.TrimSpace(m.spec.Branch), builder,
|
Port: int32(m.spec.Port),
|
||||||
serviceID,
|
HealthcheckPath: strings.TrimSpace(m.spec.HealthCheck),
|
||||||
); err != nil {
|
Column3: volumesRaw,
|
||||||
|
Column4: strings.TrimSpace(m.spec.Repo),
|
||||||
|
Column5: strings.TrimSpace(m.spec.Branch),
|
||||||
|
Builder: builder,
|
||||||
|
ID: serviceID,
|
||||||
|
}); err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
cleanup()
|
cleanup()
|
||||||
return nil, &gin.H{"error": "Failed to configure service"}
|
return nil, &gin.H{"error": "Failed to configure service"}
|
||||||
@@ -861,15 +867,24 @@ func queueTemplateDeployment(c *gin.Context, db *database.DB, engine *deployment
|
|||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
if _, err := db.Exec(
|
q := sqlcdb.New(db.DB)
|
||||||
`INSERT INTO deployments
|
if err := q.InsertDeployment(context.Background(), sqlcdb.InsertDeploymentParams{
|
||||||
(id, service_id, version, commit_hash, status, image_name, image_tag, created_at, updated_at)
|
ID: dep.ID,
|
||||||
VALUES ($1, $2, $3, NULL, $4, '', '', $5, $6)`,
|
ServiceID: dep.ServiceID,
|
||||||
dep.ID, dep.ServiceID, fmt.Sprintf("v%d", now.Unix()), dep.Status, dep.CreatedAt, dep.UpdatedAt,
|
Version: fmt.Sprintf("v%d", now.Unix()),
|
||||||
); err != nil {
|
Status: sql.NullString{String: dep.Status, Valid: true},
|
||||||
|
ImageName: sql.NullString{String: "", Valid: true},
|
||||||
|
ImageTag: sql.NullString{String: "", Valid: true},
|
||||||
|
CreatedAt: sql.NullTime{Time: dep.CreatedAt, Valid: true},
|
||||||
|
UpdatedAt: sql.NullTime{Time: dep.UpdatedAt, Valid: true},
|
||||||
|
}); err != nil {
|
||||||
return uuid.Nil
|
return uuid.Nil
|
||||||
}
|
}
|
||||||
_, _ = db.Exec(`UPDATE services SET status = 'queued', updated_at = $1 WHERE id = $2`, time.Now(), service.ID)
|
_ = q.SetServiceStatus(context.Background(), sqlcdb.SetServiceStatusParams{
|
||||||
|
Status: sql.NullString{String: "queued", Valid: true},
|
||||||
|
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
||||||
|
ID: service.ID,
|
||||||
|
})
|
||||||
|
|
||||||
getDeployQueue(c).Enqueue(service.ID, deployqueue.Job{
|
getDeployQueue(c).Enqueue(service.ID, deployqueue.Job{
|
||||||
DeploymentID: dep.ID,
|
DeploymentID: dep.ID,
|
||||||
|
|||||||
@@ -411,12 +411,11 @@ func handleCreateFromTemplate(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// sqlc's schema predates the runtime columns; port and healthcheck_path are
|
if err = sqlcdb.New(tx).UpdateServiceRuntimeMeta(ctx, sqlcdb.UpdateServiceRuntimeMetaParams{
|
||||||
// written via raw SQL like every other service mutation path.
|
Port: int32(config.Port),
|
||||||
if _, err = tx.ExecContext(ctx,
|
HealthcheckPath: strings.TrimSpace(config.HealthCheck),
|
||||||
`UPDATE services SET port = $1, healthcheck_path = $2 WHERE id = $3`,
|
ID: serviceID,
|
||||||
config.Port, strings.TrimSpace(config.HealthCheck), serviceID,
|
}); err != nil {
|
||||||
); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create service from template"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create service from template"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"containr/internal/database"
|
"database/sql"
|
||||||
"containr/internal/secrets"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"containr/internal/database"
|
||||||
|
"containr/internal/database/sqlcdb"
|
||||||
|
"containr/internal/secrets"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
@@ -147,7 +150,8 @@ func handleUpdateVariables(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
_, err = tx.Exec("DELETE FROM environment_variables WHERE service_id = $1", serviceID)
|
txq := sqlcdb.New(tx)
|
||||||
|
err = txq.DeleteEnvVarsForService(c.Request.Context(), serviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear existing variables"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear existing variables"})
|
||||||
return
|
return
|
||||||
@@ -164,12 +168,15 @@ func handleUpdateVariables(c *gin.Context) {
|
|||||||
if v.IsSecret {
|
if v.IsSecret {
|
||||||
value = secrets.Encrypt(value)
|
value = secrets.Encrypt(value)
|
||||||
}
|
}
|
||||||
varID := uuid.New()
|
err = txq.InsertEnvVar(c.Request.Context(), sqlcdb.InsertEnvVarParams{
|
||||||
_, err = tx.Exec(
|
ID: uuid.New(),
|
||||||
`INSERT INTO environment_variables (id, service_id, key, value, is_secret, created_at, updated_at)
|
ServiceID: serviceID,
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
Key: v.Key,
|
||||||
varID, serviceID, v.Key, value, v.IsSecret, now, now,
|
Value: value,
|
||||||
)
|
IsSecret: sql.NullBool{Bool: v.IsSecret, Valid: true},
|
||||||
|
CreatedAt: sql.NullTime{Time: now, Valid: true},
|
||||||
|
UpdatedAt: sql.NullTime{Time: now, Valid: true},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert variable: " + v.Key})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert variable: " + v.Key})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,636 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: apwhy.sql
|
||||||
|
|
||||||
|
package sqlcdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const countAPKeys = `-- name: CountAPKeys :one
|
||||||
|
SELECT COUNT(*) FROM api_keys
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountAPKeys(ctx context.Context) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, countAPKeys)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const countAPServices = `-- name: CountAPServices :one
|
||||||
|
SELECT COUNT(*) FROM api_services
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountAPServices(ctx context.Context) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, countAPServices)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAPServiceBySlug = `-- name: GetAPServiceBySlug :one
|
||||||
|
SELECT id, upstream_url, route_prefix, upstream_auth_header,
|
||||||
|
upstream_auth_value, enabled, rpm_limit, monthly_quota,
|
||||||
|
request_timeout_ms
|
||||||
|
FROM api_services WHERE slug = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetAPServiceBySlugRow struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
UpstreamUrl string `json:"upstream_url"`
|
||||||
|
RoutePrefix string `json:"route_prefix"`
|
||||||
|
UpstreamAuthHeader sql.NullString `json:"upstream_auth_header"`
|
||||||
|
UpstreamAuthValue sql.NullString `json:"upstream_auth_value"`
|
||||||
|
Enabled int32 `json:"enabled"`
|
||||||
|
RpmLimit sql.NullInt32 `json:"rpm_limit"`
|
||||||
|
MonthlyQuota sql.NullInt32 `json:"monthly_quota"`
|
||||||
|
RequestTimeoutMs sql.NullInt32 `json:"request_timeout_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAPServiceBySlug(ctx context.Context, slug string) (GetAPServiceBySlugRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getAPServiceBySlug, slug)
|
||||||
|
var i GetAPServiceBySlugRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UpstreamUrl,
|
||||||
|
&i.RoutePrefix,
|
||||||
|
&i.UpstreamAuthHeader,
|
||||||
|
&i.UpstreamAuthValue,
|
||||||
|
&i.Enabled,
|
||||||
|
&i.RpmLimit,
|
||||||
|
&i.MonthlyQuota,
|
||||||
|
&i.RequestTimeoutMs,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAPServiceForValidate = `-- name: GetAPServiceForValidate :one
|
||||||
|
SELECT upstream_url, health_path, upstream_auth_header, upstream_auth_value, request_timeout_ms
|
||||||
|
FROM api_services WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetAPServiceForValidateRow struct {
|
||||||
|
UpstreamUrl string `json:"upstream_url"`
|
||||||
|
HealthPath string `json:"health_path"`
|
||||||
|
UpstreamAuthHeader sql.NullString `json:"upstream_auth_header"`
|
||||||
|
UpstreamAuthValue sql.NullString `json:"upstream_auth_value"`
|
||||||
|
RequestTimeoutMs sql.NullInt32 `json:"request_timeout_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAPServiceForValidate(ctx context.Context, id uuid.UUID) (GetAPServiceForValidateRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getAPServiceForValidate, id)
|
||||||
|
var i GetAPServiceForValidateRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.UpstreamUrl,
|
||||||
|
&i.HealthPath,
|
||||||
|
&i.UpstreamAuthHeader,
|
||||||
|
&i.UpstreamAuthValue,
|
||||||
|
&i.RequestTimeoutMs,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertAPKey = `-- name: InsertAPKey :one
|
||||||
|
INSERT INTO api_keys (
|
||||||
|
name, key_hash, key_prefix, plan, allowed_service_ids,
|
||||||
|
rpm_limit, monthly_quota, created_at, updated_at
|
||||||
|
) VALUES ($1, $2, $3, $4, '[]', $5, $6, NOW(), NOW())
|
||||||
|
RETURNING id
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertAPKeyParams struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
KeyHash string `json:"key_hash"`
|
||||||
|
KeyPrefix string `json:"key_prefix"`
|
||||||
|
Plan string `json:"plan"`
|
||||||
|
RpmLimit sql.NullInt32 `json:"rpm_limit"`
|
||||||
|
MonthlyQuota sql.NullInt32 `json:"monthly_quota"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertAPKey(ctx context.Context, arg InsertAPKeyParams) (uuid.UUID, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, insertAPKey,
|
||||||
|
arg.Name,
|
||||||
|
arg.KeyHash,
|
||||||
|
arg.KeyPrefix,
|
||||||
|
arg.Plan,
|
||||||
|
arg.RpmLimit,
|
||||||
|
arg.MonthlyQuota,
|
||||||
|
)
|
||||||
|
var id uuid.UUID
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertAPService = `-- name: InsertAPService :one
|
||||||
|
INSERT INTO api_services (
|
||||||
|
name, slug, upstream_url, route_prefix, health_path,
|
||||||
|
rpm_limit, monthly_quota, created_at, updated_at
|
||||||
|
) VALUES ($1, $2, $3, $4, '/health', $5, $6, NOW(), NOW())
|
||||||
|
RETURNING id
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertAPServiceParams struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
UpstreamUrl string `json:"upstream_url"`
|
||||||
|
RoutePrefix string `json:"route_prefix"`
|
||||||
|
RpmLimit sql.NullInt32 `json:"rpm_limit"`
|
||||||
|
MonthlyQuota sql.NullInt32 `json:"monthly_quota"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertAPService(ctx context.Context, arg InsertAPServiceParams) (uuid.UUID, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, insertAPService,
|
||||||
|
arg.Name,
|
||||||
|
arg.Slug,
|
||||||
|
arg.UpstreamUrl,
|
||||||
|
arg.RoutePrefix,
|
||||||
|
arg.RpmLimit,
|
||||||
|
arg.MonthlyQuota,
|
||||||
|
)
|
||||||
|
var id uuid.UUID
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertGatewayIncident = `-- name: InsertGatewayIncident :exec
|
||||||
|
INSERT INTO incident_events (service_id, api_key_id, code, message, severity, http_status, occurred_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, NOW())
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertGatewayIncidentParams struct {
|
||||||
|
ServiceID uuid.NullUUID `json:"service_id"`
|
||||||
|
ApiKeyID uuid.NullUUID `json:"api_key_id"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Severity string `json:"severity"`
|
||||||
|
HttpStatus sql.NullInt32 `json:"http_status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertGatewayIncident(ctx context.Context, arg InsertGatewayIncidentParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, insertGatewayIncident,
|
||||||
|
arg.ServiceID,
|
||||||
|
arg.ApiKeyID,
|
||||||
|
arg.Code,
|
||||||
|
arg.Message,
|
||||||
|
arg.Severity,
|
||||||
|
arg.HttpStatus,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertMetricPoint = `-- name: InsertMetricPoint :exec
|
||||||
|
INSERT INTO metrics_timeseries (metric, value, labels_json, occurred_at)
|
||||||
|
VALUES ('gateway_request', 1, $1, NOW())
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) InsertMetricPoint(ctx context.Context, labelsJson sql.NullString) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, insertMetricPoint, labelsJson)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertServiceIncident = `-- name: InsertServiceIncident :exec
|
||||||
|
INSERT INTO incident_events (service_id, code, message, severity, http_status, occurred_at)
|
||||||
|
VALUES ($1, 'SERVICE_VALIDATION_FAILED', $2, 'medium', $3, NOW())
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertServiceIncidentParams struct {
|
||||||
|
ServiceID uuid.NullUUID `json:"service_id"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
HttpStatus sql.NullInt32 `json:"http_status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertServiceIncident(ctx context.Context, arg InsertServiceIncidentParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, insertServiceIncident, arg.ServiceID, arg.Message, arg.HttpStatus)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listAPKeys = `-- name: ListAPKeys :many
|
||||||
|
SELECT id, name, key_prefix, plan, enabled, rpm_limit, monthly_quota,
|
||||||
|
to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS created_at,
|
||||||
|
to_char(updated_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS updated_at
|
||||||
|
FROM api_keys
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListAPKeysRow struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
KeyPrefix string `json:"key_prefix"`
|
||||||
|
Plan string `json:"plan"`
|
||||||
|
Enabled int32 `json:"enabled"`
|
||||||
|
RpmLimit sql.NullInt32 `json:"rpm_limit"`
|
||||||
|
MonthlyQuota sql.NullInt32 `json:"monthly_quota"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListAPKeys(ctx context.Context) ([]ListAPKeysRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listAPKeys)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListAPKeysRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListAPKeysRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.KeyPrefix,
|
||||||
|
&i.Plan,
|
||||||
|
&i.Enabled,
|
||||||
|
&i.RpmLimit,
|
||||||
|
&i.MonthlyQuota,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listAPKeysByPrefix = `-- name: ListAPKeysByPrefix :many
|
||||||
|
SELECT id, key_hash, enabled, rpm_limit, monthly_quota, allowed_service_ids
|
||||||
|
FROM api_keys WHERE key_prefix = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListAPKeysByPrefixRow struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
KeyHash string `json:"key_hash"`
|
||||||
|
Enabled int32 `json:"enabled"`
|
||||||
|
RpmLimit sql.NullInt32 `json:"rpm_limit"`
|
||||||
|
MonthlyQuota sql.NullInt32 `json:"monthly_quota"`
|
||||||
|
AllowedServiceIds string `json:"allowed_service_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListAPKeysByPrefix(ctx context.Context, keyPrefix string) ([]ListAPKeysByPrefixRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listAPKeysByPrefix, keyPrefix)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListAPKeysByPrefixRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListAPKeysByPrefixRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.KeyHash,
|
||||||
|
&i.Enabled,
|
||||||
|
&i.RpmLimit,
|
||||||
|
&i.MonthlyQuota,
|
||||||
|
&i.AllowedServiceIds,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listAPServices = `-- name: ListAPServices :many
|
||||||
|
SELECT id, name, slug, upstream_url, route_prefix, enabled,
|
||||||
|
to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS created_at,
|
||||||
|
to_char(updated_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS updated_at
|
||||||
|
FROM api_services
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListAPServicesRow struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
UpstreamUrl string `json:"upstream_url"`
|
||||||
|
RoutePrefix string `json:"route_prefix"`
|
||||||
|
Enabled int32 `json:"enabled"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListAPServices(ctx context.Context) ([]ListAPServicesRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listAPServices)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListAPServicesRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListAPServicesRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Slug,
|
||||||
|
&i.UpstreamUrl,
|
||||||
|
&i.RoutePrefix,
|
||||||
|
&i.Enabled,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listClientEventPaths = `-- name: ListClientEventPaths :many
|
||||||
|
SELECT COALESCE((labels_json::jsonb ->> 'path'), 'unknown') AS path, COUNT(*) AS total
|
||||||
|
FROM metrics_timeseries
|
||||||
|
WHERE metric = 'client_event' AND occurred_at >= NOW() - INTERVAL '7 days'
|
||||||
|
GROUP BY COALESCE((labels_json::jsonb ->> 'path'), 'unknown')
|
||||||
|
ORDER BY total DESC, path ASC
|
||||||
|
LIMIT 20
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListClientEventPathsRow struct {
|
||||||
|
Path interface{} `json:"path"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListClientEventPaths(ctx context.Context) ([]ListClientEventPathsRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listClientEventPaths)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListClientEventPathsRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListClientEventPathsRow
|
||||||
|
if err := rows.Scan(&i.Path, &i.Total); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listIncidentStatusCodes = `-- name: ListIncidentStatusCodes :many
|
||||||
|
SELECT COALESCE(http_status, 0) AS status_code, COALESCE(SUM(count), 0) AS total
|
||||||
|
FROM incident_events
|
||||||
|
WHERE occurred_at >= NOW() - INTERVAL '7 days'
|
||||||
|
GROUP BY COALESCE(http_status, 0)
|
||||||
|
ORDER BY total DESC, status_code ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListIncidentStatusCodesRow struct {
|
||||||
|
StatusCode int32 `json:"status_code"`
|
||||||
|
Total interface{} `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListIncidentStatusCodes(ctx context.Context) ([]ListIncidentStatusCodesRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listIncidentStatusCodes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListIncidentStatusCodesRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListIncidentStatusCodesRow
|
||||||
|
if err := rows.Scan(&i.StatusCode, &i.Total); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listRequestsByDay = `-- name: ListRequestsByDay :many
|
||||||
|
SELECT TO_CHAR(DATE_TRUNC('day', occurred_at), 'YYYY-MM-DD') AS day_bucket, COUNT(*) AS total
|
||||||
|
FROM metrics_timeseries
|
||||||
|
WHERE metric = 'request_total' AND occurred_at >= NOW() - INTERVAL '7 days'
|
||||||
|
GROUP BY DATE_TRUNC('day', occurred_at)
|
||||||
|
ORDER BY DATE_TRUNC('day', occurred_at) ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListRequestsByDayRow struct {
|
||||||
|
DayBucket string `json:"day_bucket"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListRequestsByDay(ctx context.Context) ([]ListRequestsByDayRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listRequestsByDay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListRequestsByDayRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListRequestsByDayRow
|
||||||
|
if err := rows.Scan(&i.DayBucket, &i.Total); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listTopAPServices = `-- name: ListTopAPServices :many
|
||||||
|
SELECT s.id, s.name, COALESCE(SUM(u.request_count), 0) AS total_requests
|
||||||
|
FROM api_services s
|
||||||
|
LEFT JOIN usage_counters u ON u.service_id = s.id
|
||||||
|
GROUP BY s.id, s.name
|
||||||
|
ORDER BY total_requests DESC, s.name ASC
|
||||||
|
LIMIT 10
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListTopAPServicesRow struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
TotalRequests interface{} `json:"total_requests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListTopAPServices(ctx context.Context) ([]ListTopAPServicesRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listTopAPServices)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListTopAPServicesRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListTopAPServicesRow
|
||||||
|
if err := rows.Scan(&i.ID, &i.Name, &i.TotalRequests); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAPKeyEnabled = `-- name: SetAPKeyEnabled :exec
|
||||||
|
UPDATE api_keys SET enabled = $1, updated_at = NOW() WHERE id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type SetAPKeyEnabledParams struct {
|
||||||
|
Enabled int32 `json:"enabled"`
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SetAPKeyEnabled(ctx context.Context, arg SetAPKeyEnabledParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, setAPKeyEnabled, arg.Enabled, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAPServiceEnabled = `-- name: SetAPServiceEnabled :exec
|
||||||
|
UPDATE api_services SET enabled = $1, updated_at = NOW() WHERE id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type SetAPServiceEnabledParams struct {
|
||||||
|
Enabled int32 `json:"enabled"`
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SetAPServiceEnabled(ctx context.Context, arg SetAPServiceEnabledParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, setAPServiceEnabled, arg.Enabled, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAPServiceValidation = `-- name: SetAPServiceValidation :exec
|
||||||
|
UPDATE api_services
|
||||||
|
SET last_validation_at = NOW(), last_validation_status = $1, last_validation_message = $2, updated_at = NOW()
|
||||||
|
WHERE id = $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type SetAPServiceValidationParams struct {
|
||||||
|
LastValidationStatus sql.NullString `json:"last_validation_status"`
|
||||||
|
LastValidationMessage sql.NullString `json:"last_validation_message"`
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SetAPServiceValidation(ctx context.Context, arg SetAPServiceValidationParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, setAPServiceValidation, arg.LastValidationStatus, arg.LastValidationMessage, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const sumRequestMetricSince = `-- name: SumRequestMetricSince :one
|
||||||
|
SELECT COALESCE(SUM(value), 0)::int
|
||||||
|
FROM metrics_timeseries
|
||||||
|
WHERE metric = 'request_total' AND occurred_at >= DATE_TRUNC($1::text, NOW())
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) SumRequestMetricSince(ctx context.Context, dollar_1 string) (int32, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, sumRequestMetricSince, dollar_1)
|
||||||
|
var column_1 int32
|
||||||
|
err := row.Scan(&column_1)
|
||||||
|
return column_1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const sumUsageByKey = `-- name: SumUsageByKey :one
|
||||||
|
SELECT COALESCE(SUM(request_count), 0)::bigint FROM usage_counters
|
||||||
|
WHERE api_key_id = $1 AND period_month = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type SumUsageByKeyParams struct {
|
||||||
|
ApiKeyID uuid.UUID `json:"api_key_id"`
|
||||||
|
PeriodMonth string `json:"period_month"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SumUsageByKey(ctx context.Context, arg SumUsageByKeyParams) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, sumUsageByKey, arg.ApiKeyID, arg.PeriodMonth)
|
||||||
|
var column_1 int64
|
||||||
|
err := row.Scan(&column_1)
|
||||||
|
return column_1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const sumUsageByService = `-- name: SumUsageByService :one
|
||||||
|
SELECT COALESCE(SUM(request_count), 0)::bigint FROM usage_counters
|
||||||
|
WHERE service_id = $1 AND period_month = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type SumUsageByServiceParams struct {
|
||||||
|
ServiceID uuid.UUID `json:"service_id"`
|
||||||
|
PeriodMonth string `json:"period_month"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SumUsageByService(ctx context.Context, arg SumUsageByServiceParams) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, sumUsageByService, arg.ServiceID, arg.PeriodMonth)
|
||||||
|
var column_1 int64
|
||||||
|
err := row.Scan(&column_1)
|
||||||
|
return column_1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const sumUsageCounters = `-- name: SumUsageCounters :one
|
||||||
|
SELECT COALESCE(SUM(request_count), 0) FROM usage_counters
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) SumUsageCounters(ctx context.Context) (interface{}, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, sumUsageCounters)
|
||||||
|
var coalesce interface{}
|
||||||
|
err := row.Scan(&coalesce)
|
||||||
|
return coalesce, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const touchAPKeyLastUsed = `-- name: TouchAPKeyLastUsed :exec
|
||||||
|
UPDATE api_keys SET last_used_at = NOW() WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) TouchAPKeyLastUsed(ctx context.Context, id uuid.UUID) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, touchAPKeyLastUsed, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertUsageCounter = `-- name: UpsertUsageCounter :exec
|
||||||
|
INSERT INTO usage_counters (api_key_id, service_id, period_month, request_count, updated_at)
|
||||||
|
VALUES ($1, $2, $3, 1, NOW())
|
||||||
|
ON CONFLICT (api_key_id, service_id, period_month)
|
||||||
|
DO UPDATE SET request_count = usage_counters.request_count + 1, updated_at = NOW()
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpsertUsageCounterParams struct {
|
||||||
|
ApiKeyID uuid.UUID `json:"api_key_id"`
|
||||||
|
ServiceID uuid.UUID `json:"service_id"`
|
||||||
|
PeriodMonth string `json:"period_month"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpsertUsageCounter(ctx context.Context, arg UpsertUsageCounterParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, upsertUsageCounter, arg.ApiKeyID, arg.ServiceID, arg.PeriodMonth)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: audit.sql
|
||||||
|
|
||||||
|
package sqlcdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/sqlc-dev/pqtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const insertAuditLog = `-- name: InsertAuditLog :exec
|
||||||
|
INSERT INTO audit_logs (id, user_id, resource, resource_id, action, details, severity, category, label, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertAuditLogParams struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
UserID uuid.NullUUID `json:"user_id"`
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
ResourceID uuid.NullUUID `json:"resource_id"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Details pqtype.NullRawMessage `json:"details"`
|
||||||
|
Severity string `json:"severity"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, insertAuditLog,
|
||||||
|
arg.ID,
|
||||||
|
arg.UserID,
|
||||||
|
arg.Resource,
|
||||||
|
arg.ResourceID,
|
||||||
|
arg.Action,
|
||||||
|
arg.Details,
|
||||||
|
arg.Severity,
|
||||||
|
arg.Category,
|
||||||
|
arg.Label,
|
||||||
|
arg.CreatedAt,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertAuditLogWithRequest = `-- name: InsertAuditLogWithRequest :exec
|
||||||
|
INSERT INTO audit_logs (id, user_id, resource, resource_id, action, details, ip_address, user_agent, severity, category, label, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::inet, $8, $9, $10, $11, $12)
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertAuditLogWithRequestParams struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
UserID uuid.NullUUID `json:"user_id"`
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
ResourceID uuid.NullUUID `json:"resource_id"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Column6 json.RawMessage `json:"column_6"`
|
||||||
|
Column7 pqtype.Inet `json:"column_7"`
|
||||||
|
UserAgent sql.NullString `json:"user_agent"`
|
||||||
|
Severity string `json:"severity"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertAuditLogWithRequest(ctx context.Context, arg InsertAuditLogWithRequestParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, insertAuditLogWithRequest,
|
||||||
|
arg.ID,
|
||||||
|
arg.UserID,
|
||||||
|
arg.Resource,
|
||||||
|
arg.ResourceID,
|
||||||
|
arg.Action,
|
||||||
|
arg.Column6,
|
||||||
|
arg.Column7,
|
||||||
|
arg.UserAgent,
|
||||||
|
arg.Severity,
|
||||||
|
arg.Category,
|
||||||
|
arg.Label,
|
||||||
|
arg.CreatedAt,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listAuditBackfillBatch = `-- name: ListAuditBackfillBatch :many
|
||||||
|
SELECT id::text, resource, action FROM audit_logs
|
||||||
|
WHERE label = '' AND id > $1::uuid ORDER BY id LIMIT 500
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListAuditBackfillBatchRow struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListAuditBackfillBatch(ctx context.Context, dollar_1 uuid.UUID) ([]ListAuditBackfillBatchRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listAuditBackfillBatch, dollar_1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListAuditBackfillBatchRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListAuditBackfillBatchRow
|
||||||
|
if err := rows.Scan(&i.ID, &i.Resource, &i.Action); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listResourceAuditLogs = `-- name: ListResourceAuditLogs :many
|
||||||
|
SELECT id, COALESCE(user_id::text, '')::text AS user_id, resource,
|
||||||
|
COALESCE(resource_id::text, '')::text AS resource_id, action,
|
||||||
|
COALESCE(details::text, '{}')::text AS details,
|
||||||
|
COALESCE(ip_address::text, '')::text AS ip_address,
|
||||||
|
COALESCE(user_agent, '') AS user_agent, created_at
|
||||||
|
FROM audit_logs
|
||||||
|
WHERE user_id::text = $1 AND resource = $2 AND resource_id::text = $3
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 100
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListResourceAuditLogsParams struct {
|
||||||
|
UserID uuid.NullUUID `json:"user_id"`
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
ResourceID uuid.NullUUID `json:"resource_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListResourceAuditLogsRow struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
ResourceID string `json:"resource_id"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Details string `json:"details"`
|
||||||
|
IpAddress string `json:"ip_address"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListResourceAuditLogs(ctx context.Context, arg ListResourceAuditLogsParams) ([]ListResourceAuditLogsRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listResourceAuditLogs, arg.UserID, arg.Resource, arg.ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListResourceAuditLogsRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListResourceAuditLogsRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Resource,
|
||||||
|
&i.ResourceID,
|
||||||
|
&i.Action,
|
||||||
|
&i.Details,
|
||||||
|
&i.IpAddress,
|
||||||
|
&i.UserAgent,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const pruneAuditLogs = `-- name: PruneAuditLogs :execrows
|
||||||
|
DELETE FROM audit_logs WHERE created_at < NOW() - ($1::int * INTERVAL '1 day')
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) PruneAuditLogs(ctx context.Context, dollar_1 int32) (int64, error) {
|
||||||
|
result, err := q.db.ExecContext(ctx, pruneAuditLogs, dollar_1)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAuditLogMetadata = `-- name: SetAuditLogMetadata :exec
|
||||||
|
UPDATE audit_logs SET severity = $1, category = $2, label = $3 WHERE id = $4::uuid
|
||||||
|
`
|
||||||
|
|
||||||
|
type SetAuditLogMetadataParams struct {
|
||||||
|
Severity string `json:"severity"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Column4 uuid.UUID `json:"column_4"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SetAuditLogMetadata(ctx context.Context, arg SetAuditLogMetadataParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, setAuditLogMetadata,
|
||||||
|
arg.Severity,
|
||||||
|
arg.Category,
|
||||||
|
arg.Label,
|
||||||
|
arg.Column4,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -34,6 +34,24 @@ func (q *Queries) CountUsersByEmail(ctx context.Context, email string) (int64, e
|
|||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAdminUserInfo = `-- name: GetAdminUserInfo :one
|
||||||
|
SELECT email, COALESCE(name, '') AS name, is_admin
|
||||||
|
FROM users WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetAdminUserInfoRow struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAdminUserInfo(ctx context.Context, id uuid.UUID) (GetAdminUserInfoRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getAdminUserInfo, id)
|
||||||
|
var i GetAdminUserInfoRow
|
||||||
|
err := row.Scan(&i.Email, &i.Name, &i.IsAdmin)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const getUserByEmailForAuth = `-- name: GetUserByEmailForAuth :one
|
const getUserByEmailForAuth = `-- name: GetUserByEmailForAuth :one
|
||||||
SELECT id, email, password_hash, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
|
SELECT id, email, password_hash, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
|
||||||
FROM users WHERE email = $1
|
FROM users WHERE email = $1
|
||||||
@@ -168,6 +186,69 @@ func (q *Queries) InsertUserAdmin(ctx context.Context, arg InsertUserAdminParams
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const listAdminUsers = `-- name: ListAdminUsers :many
|
||||||
|
SELECT id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin,
|
||||||
|
COALESCE(to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"'), '')::text AS created_at
|
||||||
|
FROM users
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListAdminUsersRow struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
AvatarUrl string `json:"avatar_url"`
|
||||||
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListAdminUsers(ctx context.Context) ([]ListAdminUsersRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listAdminUsers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListAdminUsersRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListAdminUsersRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Email,
|
||||||
|
&i.Name,
|
||||||
|
&i.AvatarUrl,
|
||||||
|
&i.IsAdmin,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const setUserAdmin = `-- name: SetUserAdmin :execrows
|
||||||
|
UPDATE users SET is_admin = $1, updated_at = NOW() WHERE id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type SetUserAdminParams struct {
|
||||||
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SetUserAdmin(ctx context.Context, arg SetUserAdminParams) (int64, error) {
|
||||||
|
result, err := q.db.ExecContext(ctx, setUserAdmin, arg.IsAdmin, arg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
const updateUserProfile = `-- name: UpdateUserProfile :exec
|
const updateUserProfile = `-- name: UpdateUserProfile :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET name = COALESCE($1::varchar, name), avatar_url = COALESCE($2::varchar, avatar_url)
|
SET name = COALESCE($1::varchar, name), avatar_url = COALESCE($2::varchar, avatar_url)
|
||||||
@@ -185,6 +266,27 @@ func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfilePa
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const upsertAdminUser = `-- name: UpsertAdminUser :one
|
||||||
|
INSERT INTO users (email, password_hash, name, is_admin)
|
||||||
|
VALUES ($1, $2, $3, true)
|
||||||
|
ON CONFLICT (email) DO UPDATE
|
||||||
|
SET is_admin = true, updated_at = NOW()
|
||||||
|
RETURNING id
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpsertAdminUserParams struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
PasswordHash string `json:"password_hash"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpsertAdminUser(ctx context.Context, arg UpsertAdminUserParams) (uuid.UUID, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, upsertAdminUser, arg.Email, arg.PasswordHash, arg.Name)
|
||||||
|
var id uuid.UUID
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
const upsertLocalUser = `-- name: UpsertLocalUser :one
|
const upsertLocalUser = `-- name: UpsertLocalUser :one
|
||||||
INSERT INTO users (email, password_hash, name, avatar_url, is_admin)
|
INSERT INTO users (email, password_hash, name, avatar_url, is_admin)
|
||||||
VALUES ($1, $2, $3, NULLIF($4::varchar, ''), $5)
|
VALUES ($1, $2, $3, NULLIF($4::varchar, ''), $5)
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: builds.sql
|
||||||
|
|
||||||
|
package sqlcdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const cancelBuild = `-- name: CancelBuild :execrows
|
||||||
|
UPDATE builds
|
||||||
|
SET status = 'cancelled',
|
||||||
|
progress = 100,
|
||||||
|
completed_at = $1,
|
||||||
|
log = COALESCE(log, '') || $2,
|
||||||
|
updated_at = $1
|
||||||
|
WHERE id = $3
|
||||||
|
AND status NOT IN ('success', 'failed', 'cancelled')
|
||||||
|
`
|
||||||
|
|
||||||
|
type CancelBuildParams struct {
|
||||||
|
CompletedAt sql.NullTime `json:"completed_at"`
|
||||||
|
Log string `json:"log"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CancelBuild(ctx context.Context, arg CancelBuildParams) (int64, error) {
|
||||||
|
result, err := q.db.ExecContext(ctx, cancelBuild, arg.CompletedAt, arg.Log, arg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBuild = `-- name: GetBuild :one
|
||||||
|
SELECT id, project_id, service_id, status, progress, started_at, completed_at, image_name, image_tag, size, error, log, metadata
|
||||||
|
FROM builds
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetBuildRow struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ProjectID sql.NullString `json:"project_id"`
|
||||||
|
ServiceID sql.NullString `json:"service_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Progress int32 `json:"progress"`
|
||||||
|
StartedAt time.Time `json:"started_at"`
|
||||||
|
CompletedAt sql.NullTime `json:"completed_at"`
|
||||||
|
ImageName string `json:"image_name"`
|
||||||
|
ImageTag string `json:"image_tag"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Error sql.NullString `json:"error"`
|
||||||
|
Log string `json:"log"`
|
||||||
|
Metadata json.RawMessage `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetBuild(ctx context.Context, id string) (GetBuildRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getBuild, id)
|
||||||
|
var i GetBuildRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProjectID,
|
||||||
|
&i.ServiceID,
|
||||||
|
&i.Status,
|
||||||
|
&i.Progress,
|
||||||
|
&i.StartedAt,
|
||||||
|
&i.CompletedAt,
|
||||||
|
&i.ImageName,
|
||||||
|
&i.ImageTag,
|
||||||
|
&i.Size,
|
||||||
|
&i.Error,
|
||||||
|
&i.Log,
|
||||||
|
&i.Metadata,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertBuild = `-- name: UpsertBuild :exec
|
||||||
|
INSERT INTO builds
|
||||||
|
(id, project_id, service_id, status, progress, started_at, completed_at, image_name, image_tag, size, error, log, metadata, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb, $14, $14)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
project_id = EXCLUDED.project_id,
|
||||||
|
service_id = EXCLUDED.service_id,
|
||||||
|
status = EXCLUDED.status,
|
||||||
|
progress = EXCLUDED.progress,
|
||||||
|
started_at = EXCLUDED.started_at,
|
||||||
|
completed_at = EXCLUDED.completed_at,
|
||||||
|
image_name = EXCLUDED.image_name,
|
||||||
|
image_tag = EXCLUDED.image_tag,
|
||||||
|
size = EXCLUDED.size,
|
||||||
|
error = EXCLUDED.error,
|
||||||
|
log = EXCLUDED.log,
|
||||||
|
metadata = EXCLUDED.metadata,
|
||||||
|
updated_at = EXCLUDED.updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpsertBuildParams struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ProjectID sql.NullString `json:"project_id"`
|
||||||
|
ServiceID sql.NullString `json:"service_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Progress int32 `json:"progress"`
|
||||||
|
StartedAt time.Time `json:"started_at"`
|
||||||
|
CompletedAt sql.NullTime `json:"completed_at"`
|
||||||
|
ImageName string `json:"image_name"`
|
||||||
|
ImageTag string `json:"image_tag"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Error sql.NullString `json:"error"`
|
||||||
|
Log string `json:"log"`
|
||||||
|
Column13 json.RawMessage `json:"column_13"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpsertBuild(ctx context.Context, arg UpsertBuildParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, upsertBuild,
|
||||||
|
arg.ID,
|
||||||
|
arg.ProjectID,
|
||||||
|
arg.ServiceID,
|
||||||
|
arg.Status,
|
||||||
|
arg.Progress,
|
||||||
|
arg.StartedAt,
|
||||||
|
arg.CompletedAt,
|
||||||
|
arg.ImageName,
|
||||||
|
arg.ImageTag,
|
||||||
|
arg.Size,
|
||||||
|
arg.Error,
|
||||||
|
arg.Log,
|
||||||
|
arg.Column13,
|
||||||
|
arg.CreatedAt,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -157,6 +157,28 @@ func (q *Queries) GetGitProviderTimestamps(ctx context.Context, id uuid.UUID) (G
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getGitRepoCloneByFullName = `-- name: GetGitRepoCloneByFullName :one
|
||||||
|
SELECT clone_url, provider_id FROM git_repositories
|
||||||
|
WHERE full_name = $1 AND user_id = $2 LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetGitRepoCloneByFullNameParams struct {
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetGitRepoCloneByFullNameRow struct {
|
||||||
|
CloneUrl string `json:"clone_url"`
|
||||||
|
ProviderID uuid.UUID `json:"provider_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetGitRepoCloneByFullName(ctx context.Context, arg GetGitRepoCloneByFullNameParams) (GetGitRepoCloneByFullNameRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getGitRepoCloneByFullName, arg.FullName, arg.UserID)
|
||||||
|
var i GetGitRepoCloneByFullNameRow
|
||||||
|
err := row.Scan(&i.CloneUrl, &i.ProviderID)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const getGitRepoForPush = `-- name: GetGitRepoForPush :one
|
const getGitRepoForPush = `-- name: GetGitRepoForPush :one
|
||||||
SELECT clone_url, full_name, user_id FROM git_repositories WHERE id = $1
|
SELECT clone_url, full_name, user_id FROM git_repositories WHERE id = $1
|
||||||
`
|
`
|
||||||
@@ -174,6 +196,23 @@ func (q *Queries) GetGitRepoForPush(ctx context.Context, id uuid.UUID) (GetGitRe
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getGitRepoProviderByCloneURL = `-- name: GetGitRepoProviderByCloneURL :one
|
||||||
|
SELECT provider_id FROM git_repositories
|
||||||
|
WHERE clone_url = $1 AND user_id = $2 LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetGitRepoProviderByCloneURLParams struct {
|
||||||
|
CloneUrl string `json:"clone_url"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetGitRepoProviderByCloneURL(ctx context.Context, arg GetGitRepoProviderByCloneURLParams) (uuid.UUID, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getGitRepoProviderByCloneURL, arg.CloneUrl, arg.UserID)
|
||||||
|
var provider_id uuid.UUID
|
||||||
|
err := row.Scan(&provider_id)
|
||||||
|
return provider_id, err
|
||||||
|
}
|
||||||
|
|
||||||
const getGitRepositoryByFullName = `-- name: GetGitRepositoryByFullName :one
|
const getGitRepositoryByFullName = `-- name: GetGitRepositoryByFullName :one
|
||||||
SELECT id, provider_id, name, full_name, COALESCE(description, '') AS description,
|
SELECT id, provider_id, name, full_name, COALESCE(description, '') AS description,
|
||||||
clone_url, COALESCE(webhook_url, '') AS webhook_url,
|
clone_url, COALESCE(webhook_url, '') AS webhook_url,
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: misc.sql
|
||||||
|
|
||||||
|
package sqlcdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const countProjects = `-- name: CountProjects :one
|
||||||
|
SELECT COUNT(*) FROM projects
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountProjects(ctx context.Context) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, countProjects)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteProjectVariables = `-- name: DeleteProjectVariables :exec
|
||||||
|
DELETE FROM project_variables WHERE project_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteProjectVariables(ctx context.Context, projectID uuid.UUID) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteProjectVariables, projectID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProjectVariableValue = `-- name: GetProjectVariableValue :one
|
||||||
|
SELECT value FROM project_variables WHERE project_id = $1 AND key = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetProjectVariableValueParams struct {
|
||||||
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetProjectVariableValue(ctx context.Context, arg GetProjectVariableValueParams) (string, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getProjectVariableValue, arg.ProjectID, arg.Key)
|
||||||
|
var value string
|
||||||
|
err := row.Scan(&value)
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRegistryAuth = `-- name: GetRegistryAuth :one
|
||||||
|
SELECT username, password FROM registries
|
||||||
|
WHERE owner_id::text = $1 AND host = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetRegistryAuthParams struct {
|
||||||
|
OwnerID uuid.UUID `json:"owner_id"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetRegistryAuthRow struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetRegistryAuth(ctx context.Context, arg GetRegistryAuthParams) (GetRegistryAuthRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getRegistryAuth, arg.OwnerID, arg.Host)
|
||||||
|
var i GetRegistryAuthRow
|
||||||
|
err := row.Scan(&i.Username, &i.Password)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertEnvVar = `-- name: InsertEnvVar :exec
|
||||||
|
INSERT INTO environment_variables (id, service_id, key, value, is_secret, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertEnvVarParams struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
ServiceID uuid.UUID `json:"service_id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
IsSecret sql.NullBool `json:"is_secret"`
|
||||||
|
CreatedAt sql.NullTime `json:"created_at"`
|
||||||
|
UpdatedAt sql.NullTime `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertEnvVar(ctx context.Context, arg InsertEnvVarParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, insertEnvVar,
|
||||||
|
arg.ID,
|
||||||
|
arg.ServiceID,
|
||||||
|
arg.Key,
|
||||||
|
arg.Value,
|
||||||
|
arg.IsSecret,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.UpdatedAt,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertProjectVariable = `-- name: InsertProjectVariable :exec
|
||||||
|
INSERT INTO project_variables (id, project_id, key, value, is_secret, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertProjectVariableParams struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
IsSecret bool `json:"is_secret"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertProjectVariable(ctx context.Context, arg InsertProjectVariableParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, insertProjectVariable,
|
||||||
|
arg.ID,
|
||||||
|
arg.ProjectID,
|
||||||
|
arg.Key,
|
||||||
|
arg.Value,
|
||||||
|
arg.IsSecret,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.UpdatedAt,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateServiceDeployMeta = `-- name: UpdateServiceDeployMeta :exec
|
||||||
|
UPDATE services SET port = $1, healthcheck_path = $2,
|
||||||
|
volumes = COALESCE($3::jsonb, '[]'::jsonb),
|
||||||
|
git_repo = NULLIF($4::text, ''), git_branch = NULLIF($5::text, ''), builder = $6
|
||||||
|
WHERE id = $7
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateServiceDeployMetaParams struct {
|
||||||
|
Port int32 `json:"port"`
|
||||||
|
HealthcheckPath string `json:"healthcheck_path"`
|
||||||
|
Column3 json.RawMessage `json:"column_3"`
|
||||||
|
Column4 string `json:"column_4"`
|
||||||
|
Column5 string `json:"column_5"`
|
||||||
|
Builder string `json:"builder"`
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateServiceDeployMeta(ctx context.Context, arg UpdateServiceDeployMetaParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, updateServiceDeployMeta,
|
||||||
|
arg.Port,
|
||||||
|
arg.HealthcheckPath,
|
||||||
|
arg.Column3,
|
||||||
|
arg.Column4,
|
||||||
|
arg.Column5,
|
||||||
|
arg.Builder,
|
||||||
|
arg.ID,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateServiceRuntimeMeta = `-- name: UpdateServiceRuntimeMeta :exec
|
||||||
|
UPDATE services SET port = $1, healthcheck_path = $2 WHERE id = $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateServiceRuntimeMetaParams struct {
|
||||||
|
Port int32 `json:"port"`
|
||||||
|
HealthcheckPath string `json:"healthcheck_path"`
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateServiceRuntimeMeta(ctx context.Context, arg UpdateServiceRuntimeMetaParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, updateServiceRuntimeMeta, arg.Port, arg.HealthcheckPath, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -49,6 +49,66 @@ type AgentHeartbeat struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ApiKey struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
KeyHash string `json:"key_hash"`
|
||||||
|
KeyPrefix string `json:"key_prefix"`
|
||||||
|
Plan string `json:"plan"`
|
||||||
|
AllowedServiceIds string `json:"allowed_service_ids"`
|
||||||
|
Enabled int32 `json:"enabled"`
|
||||||
|
RpmLimit sql.NullInt32 `json:"rpm_limit"`
|
||||||
|
MonthlyQuota sql.NullInt32 `json:"monthly_quota"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
LastUsedAt sql.NullTime `json:"last_used_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiService struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
UpstreamUrl string `json:"upstream_url"`
|
||||||
|
RoutePrefix string `json:"route_prefix"`
|
||||||
|
HealthPath string `json:"health_path"`
|
||||||
|
UpstreamAuthHeader sql.NullString `json:"upstream_auth_header"`
|
||||||
|
UpstreamAuthValue sql.NullString `json:"upstream_auth_value"`
|
||||||
|
InternalToken sql.NullString `json:"internal_token"`
|
||||||
|
Enabled int32 `json:"enabled"`
|
||||||
|
RpmLimit sql.NullInt32 `json:"rpm_limit"`
|
||||||
|
MonthlyQuota sql.NullInt32 `json:"monthly_quota"`
|
||||||
|
RequestTimeoutMs sql.NullInt32 `json:"request_timeout_ms"`
|
||||||
|
LastValidationAt sql.NullTime `json:"last_validation_at"`
|
||||||
|
LastValidationStatus sql.NullString `json:"last_validation_status"`
|
||||||
|
LastValidationMessage sql.NullString `json:"last_validation_message"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppSetting struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
IsSecret bool `json:"is_secret"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuditLog struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
UserID uuid.NullUUID `json:"user_id"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
ResourceID uuid.NullUUID `json:"resource_id"`
|
||||||
|
Details pqtype.NullRawMessage `json:"details"`
|
||||||
|
IpAddress pqtype.Inet `json:"ip_address"`
|
||||||
|
UserAgent sql.NullString `json:"user_agent"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Severity string `json:"severity"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
|
|
||||||
type BackupTarget struct {
|
type BackupTarget struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
@@ -78,6 +138,24 @@ type Banner struct {
|
|||||||
UpdatedAt sql.NullTime `json:"updated_at"`
|
UpdatedAt sql.NullTime `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Build struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ProjectID sql.NullString `json:"project_id"`
|
||||||
|
ServiceID sql.NullString `json:"service_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Progress int32 `json:"progress"`
|
||||||
|
StartedAt time.Time `json:"started_at"`
|
||||||
|
CompletedAt sql.NullTime `json:"completed_at"`
|
||||||
|
ImageName string `json:"image_name"`
|
||||||
|
ImageTag string `json:"image_tag"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Error sql.NullString `json:"error"`
|
||||||
|
Log string `json:"log"`
|
||||||
|
Metadata json.RawMessage `json:"metadata"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type ComplianceFramework struct {
|
type ComplianceFramework struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -262,6 +340,18 @@ type GitWebhook struct {
|
|||||||
UpdatedAt sql.NullTime `json:"updated_at"`
|
UpdatedAt sql.NullTime `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IncidentEvent struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
ServiceID uuid.NullUUID `json:"service_id"`
|
||||||
|
ApiKeyID uuid.NullUUID `json:"api_key_id"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Severity string `json:"severity"`
|
||||||
|
HttpStatus sql.NullInt32 `json:"http_status"`
|
||||||
|
Count int32 `json:"count"`
|
||||||
|
OccurredAt time.Time `json:"occurred_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type InstanceMetric struct {
|
type InstanceMetric struct {
|
||||||
ServiceID string `json:"service_id"`
|
ServiceID string `json:"service_id"`
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
@@ -287,6 +377,14 @@ type InstanceMetric struct {
|
|||||||
CreatedAt sql.NullTime `json:"created_at"`
|
CreatedAt sql.NullTime `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MetricsTimeseries struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
Metric string `json:"metric"`
|
||||||
|
Value float32 `json:"value"`
|
||||||
|
LabelsJson sql.NullString `json:"labels_json"`
|
||||||
|
OccurredAt time.Time `json:"occurred_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type NodeAgent struct {
|
type NodeAgent struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -408,6 +506,16 @@ type ProjectMember struct {
|
|||||||
CreatedAt sql.NullTime `json:"created_at"`
|
CreatedAt sql.NullTime `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProjectVariable struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
IsSecret bool `json:"is_secret"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OwnerID uuid.UUID `json:"owner_id"`
|
OwnerID uuid.UUID `json:"owner_id"`
|
||||||
@@ -535,6 +643,15 @@ type ServiceTemplate struct {
|
|||||||
UpdatedAt sql.NullTime `json:"updated_at"`
|
UpdatedAt sql.NullTime `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UsageCounter struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
ApiKeyID uuid.UUID `json:"api_key_id"`
|
||||||
|
ServiceID uuid.UUID `json:"service_id"`
|
||||||
|
PeriodMonth string `json:"period_month"`
|
||||||
|
RequestCount int32 `json:"request_count"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: operations.sql
|
||||||
|
|
||||||
|
package sqlcdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const listActiveOperationDeployments = `-- name: ListActiveOperationDeployments :many
|
||||||
|
SELECT d.id, d.service_id, s.name AS service_name, p.name AS project_name, d.status,
|
||||||
|
COALESCE(d.image_name, '') AS image, d.error, d.started_at, d.completed_at, d.created_at
|
||||||
|
FROM deployments d
|
||||||
|
JOIN services s ON s.id = d.service_id
|
||||||
|
JOIN projects p ON p.id = s.project_id
|
||||||
|
WHERE d.status IN ('queued','pending','building','deploying','rolling_back')
|
||||||
|
AND (p.is_approved OR p.owner_id = $1 OR $2::bool
|
||||||
|
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1))
|
||||||
|
ORDER BY d.created_at ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListActiveOperationDeploymentsParams struct {
|
||||||
|
OwnerID uuid.UUID `json:"owner_id"`
|
||||||
|
Column2 bool `json:"column_2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListActiveOperationDeploymentsRow struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
ServiceID uuid.UUID `json:"service_id"`
|
||||||
|
ServiceName string `json:"service_name"`
|
||||||
|
ProjectName string `json:"project_name"`
|
||||||
|
Status sql.NullString `json:"status"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Error sql.NullString `json:"error"`
|
||||||
|
StartedAt sql.NullTime `json:"started_at"`
|
||||||
|
CompletedAt sql.NullTime `json:"completed_at"`
|
||||||
|
CreatedAt sql.NullTime `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListActiveOperationDeployments(ctx context.Context, arg ListActiveOperationDeploymentsParams) ([]ListActiveOperationDeploymentsRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listActiveOperationDeployments, arg.OwnerID, arg.Column2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListActiveOperationDeploymentsRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListActiveOperationDeploymentsRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ServiceID,
|
||||||
|
&i.ServiceName,
|
||||||
|
&i.ProjectName,
|
||||||
|
&i.Status,
|
||||||
|
&i.Image,
|
||||||
|
&i.Error,
|
||||||
|
&i.StartedAt,
|
||||||
|
&i.CompletedAt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listRecentCronRuns = `-- name: ListRecentCronRuns :many
|
||||||
|
SELECT e.id, j.name AS job_name, j.schedule, e.status, e.started_at, e.finished_at, e.error
|
||||||
|
FROM cron_executions e
|
||||||
|
JOIN cron_jobs j ON j.id = e.cron_job_id
|
||||||
|
JOIN projects p ON p.id = j.project_id
|
||||||
|
WHERE e.started_at > NOW() - INTERVAL '24 hours'
|
||||||
|
AND (p.is_approved OR p.owner_id = $1 OR $2::bool
|
||||||
|
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1))
|
||||||
|
ORDER BY e.started_at DESC
|
||||||
|
LIMIT 30
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListRecentCronRunsParams struct {
|
||||||
|
OwnerID uuid.UUID `json:"owner_id"`
|
||||||
|
Column2 bool `json:"column_2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListRecentCronRunsRow struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
JobName string `json:"job_name"`
|
||||||
|
Schedule string `json:"schedule"`
|
||||||
|
Status sql.NullString `json:"status"`
|
||||||
|
StartedAt time.Time `json:"started_at"`
|
||||||
|
FinishedAt sql.NullTime `json:"finished_at"`
|
||||||
|
Error sql.NullString `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListRecentCronRuns(ctx context.Context, arg ListRecentCronRunsParams) ([]ListRecentCronRunsRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listRecentCronRuns, arg.OwnerID, arg.Column2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListRecentCronRunsRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListRecentCronRunsRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.JobName,
|
||||||
|
&i.Schedule,
|
||||||
|
&i.Status,
|
||||||
|
&i.StartedAt,
|
||||||
|
&i.FinishedAt,
|
||||||
|
&i.Error,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listRecentFailedDeployments = `-- name: ListRecentFailedDeployments :many
|
||||||
|
SELECT d.id, d.service_id, s.name AS service_name, p.name AS project_name, d.status,
|
||||||
|
COALESCE(d.image_name, '') AS image, d.error, d.started_at, d.completed_at, d.created_at
|
||||||
|
FROM deployments d
|
||||||
|
JOIN services s ON s.id = d.service_id
|
||||||
|
JOIN projects p ON p.id = s.project_id
|
||||||
|
WHERE d.status IN ('failed','cancelled','rolled_back')
|
||||||
|
AND d.updated_at > NOW() - INTERVAL '24 hours'
|
||||||
|
AND (p.is_approved OR p.owner_id = $1 OR $2::bool
|
||||||
|
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1))
|
||||||
|
ORDER BY d.updated_at DESC
|
||||||
|
LIMIT 20
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListRecentFailedDeploymentsParams struct {
|
||||||
|
OwnerID uuid.UUID `json:"owner_id"`
|
||||||
|
Column2 bool `json:"column_2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListRecentFailedDeploymentsRow struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
ServiceID uuid.UUID `json:"service_id"`
|
||||||
|
ServiceName string `json:"service_name"`
|
||||||
|
ProjectName string `json:"project_name"`
|
||||||
|
Status sql.NullString `json:"status"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Error sql.NullString `json:"error"`
|
||||||
|
StartedAt sql.NullTime `json:"started_at"`
|
||||||
|
CompletedAt sql.NullTime `json:"completed_at"`
|
||||||
|
CreatedAt sql.NullTime `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListRecentFailedDeployments(ctx context.Context, arg ListRecentFailedDeploymentsParams) ([]ListRecentFailedDeploymentsRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listRecentFailedDeployments, arg.OwnerID, arg.Column2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListRecentFailedDeploymentsRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListRecentFailedDeploymentsRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ServiceID,
|
||||||
|
&i.ServiceName,
|
||||||
|
&i.ProjectName,
|
||||||
|
&i.Status,
|
||||||
|
&i.Image,
|
||||||
|
&i.Error,
|
||||||
|
&i.StartedAt,
|
||||||
|
&i.CompletedAt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listRecentOperationBackups = `-- name: ListRecentOperationBackups :many
|
||||||
|
SELECT b.id, b.database_id, ds.name AS db_name, b.status, b.size, b.created_at, b.completed_at
|
||||||
|
FROM database_backups b
|
||||||
|
JOIN database_services ds ON ds.id = b.database_id
|
||||||
|
WHERE b.created_at > NOW() - INTERVAL '24 hours'
|
||||||
|
AND (ds.user_id = $1 OR $2::bool)
|
||||||
|
ORDER BY b.created_at DESC
|
||||||
|
LIMIT 20
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListRecentOperationBackupsParams struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Column2 bool `json:"column_2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListRecentOperationBackupsRow struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
DatabaseID string `json:"database_id"`
|
||||||
|
DbName string `json:"db_name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Size string `json:"size"`
|
||||||
|
CreatedAt sql.NullTime `json:"created_at"`
|
||||||
|
CompletedAt sql.NullTime `json:"completed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListRecentOperationBackups(ctx context.Context, arg ListRecentOperationBackupsParams) ([]ListRecentOperationBackupsRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listRecentOperationBackups, arg.UserID, arg.Column2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListRecentOperationBackupsRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListRecentOperationBackupsRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.DatabaseID,
|
||||||
|
&i.DbName,
|
||||||
|
&i.Status,
|
||||||
|
&i.Size,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.CompletedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ type Querier interface {
|
|||||||
// Portable bucket bucketing (no TimescaleDB): floor epoch seconds into
|
// Portable bucket bucketing (no TimescaleDB): floor epoch seconds into
|
||||||
// bucket-sized buckets, then back to timestamptz.
|
// bucket-sized buckets, then back to timestamptz.
|
||||||
AggregateNodeMetrics(ctx context.Context, arg AggregateNodeMetricsParams) ([]AggregateNodeMetricsRow, error)
|
AggregateNodeMetrics(ctx context.Context, arg AggregateNodeMetricsParams) ([]AggregateNodeMetricsRow, error)
|
||||||
|
CancelBuild(ctx context.Context, arg CancelBuildParams) (int64, error)
|
||||||
CheckSecurityProjectAccess(ctx context.Context, arg CheckSecurityProjectAccessParams) (bool, error)
|
CheckSecurityProjectAccess(ctx context.Context, arg CheckSecurityProjectAccessParams) (bool, error)
|
||||||
ClearServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) error
|
ClearServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) error
|
||||||
ClearServiceNodePins(ctx context.Context, nodeID sql.NullString) (int64, error)
|
ClearServiceNodePins(ctx context.Context, nodeID sql.NullString) (int64, error)
|
||||||
@@ -25,6 +26,8 @@ type Querier interface {
|
|||||||
CompleteCommand(ctx context.Context, arg CompleteCommandParams) (AgentCommand, error)
|
CompleteCommand(ctx context.Context, arg CompleteCommandParams) (AgentCommand, error)
|
||||||
CompleteDeployment(ctx context.Context, arg CompleteDeploymentParams) error
|
CompleteDeployment(ctx context.Context, arg CompleteDeploymentParams) error
|
||||||
ComplianceFrameworkExists(ctx context.Context, id uuid.UUID) (bool, error)
|
ComplianceFrameworkExists(ctx context.Context, id uuid.UUID) (bool, error)
|
||||||
|
CountAPKeys(ctx context.Context) (int64, error)
|
||||||
|
CountAPServices(ctx context.Context) (int64, error)
|
||||||
CountAccessibleDeployments(ctx context.Context, arg CountAccessibleDeploymentsParams) (int64, error)
|
CountAccessibleDeployments(ctx context.Context, arg CountAccessibleDeploymentsParams) (int64, error)
|
||||||
CountActivePreviewsForBranch(ctx context.Context, arg CountActivePreviewsForBranchParams) (int64, error)
|
CountActivePreviewsForBranch(ctx context.Context, arg CountActivePreviewsForBranchParams) (int64, error)
|
||||||
CountDatabaseServicesByUserAndName(ctx context.Context, arg CountDatabaseServicesByUserAndNameParams) (int64, error)
|
CountDatabaseServicesByUserAndName(ctx context.Context, arg CountDatabaseServicesByUserAndNameParams) (int64, error)
|
||||||
@@ -32,6 +35,7 @@ type Querier interface {
|
|||||||
CountDeploymentsForService(ctx context.Context, serviceID uuid.UUID) (int64, error)
|
CountDeploymentsForService(ctx context.Context, serviceID uuid.UUID) (int64, error)
|
||||||
CountEnvironmentServices(ctx context.Context, environmentID uuid.UUID) (int64, error)
|
CountEnvironmentServices(ctx context.Context, environmentID uuid.UUID) (int64, error)
|
||||||
CountGitRepositories(ctx context.Context, arg CountGitRepositoriesParams) (int64, error)
|
CountGitRepositories(ctx context.Context, arg CountGitRepositoriesParams) (int64, error)
|
||||||
|
CountProjects(ctx context.Context) (int64, error)
|
||||||
CountProjectsByUser(ctx context.Context, arg CountProjectsByUserParams) (int64, error)
|
CountProjectsByUser(ctx context.Context, arg CountProjectsByUserParams) (int64, error)
|
||||||
CountServiceDomains(ctx context.Context, serviceID uuid.UUID) (int64, error)
|
CountServiceDomains(ctx context.Context, serviceID uuid.UUID) (int64, error)
|
||||||
CountServicesByName(ctx context.Context, arg CountServicesByNameParams) (int64, error)
|
CountServicesByName(ctx context.Context, arg CountServicesByNameParams) (int64, error)
|
||||||
@@ -63,30 +67,39 @@ type Querier interface {
|
|||||||
DatabaseServiceExistsByIDAndUser(ctx context.Context, arg DatabaseServiceExistsByIDAndUserParams) (bool, error)
|
DatabaseServiceExistsByIDAndUser(ctx context.Context, arg DatabaseServiceExistsByIDAndUserParams) (bool, error)
|
||||||
DeleteAgent(ctx context.Context, id string) error
|
DeleteAgent(ctx context.Context, id string) error
|
||||||
DeleteAllServiceDomains(ctx context.Context, serviceID uuid.UUID) error
|
DeleteAllServiceDomains(ctx context.Context, serviceID uuid.UUID) error
|
||||||
|
DeleteAppSetting(ctx context.Context, key string) error
|
||||||
DeleteBackupTargetByIDAndUser(ctx context.Context, arg DeleteBackupTargetByIDAndUserParams) error
|
DeleteBackupTargetByIDAndUser(ctx context.Context, arg DeleteBackupTargetByIDAndUserParams) error
|
||||||
DeleteBanner(ctx context.Context, id uuid.UUID) error
|
DeleteBanner(ctx context.Context, id uuid.UUID) error
|
||||||
DeleteCronJob(ctx context.Context, id uuid.UUID) error
|
DeleteCronJob(ctx context.Context, id uuid.UUID) error
|
||||||
DeleteDatabaseServiceByID(ctx context.Context, id string) error
|
DeleteDatabaseServiceByID(ctx context.Context, id string) error
|
||||||
DeleteDatabaseServiceByIDAndUser(ctx context.Context, arg DeleteDatabaseServiceByIDAndUserParams) error
|
DeleteDatabaseServiceByIDAndUser(ctx context.Context, arg DeleteDatabaseServiceByIDAndUserParams) error
|
||||||
|
DeleteEnvVarsForService(ctx context.Context, serviceID uuid.UUID) error
|
||||||
DeleteGitProvider(ctx context.Context, arg DeleteGitProviderParams) (int64, error)
|
DeleteGitProvider(ctx context.Context, arg DeleteGitProviderParams) (int64, error)
|
||||||
DeleteNotificationChannel(ctx context.Context, arg DeleteNotificationChannelParams) (int64, error)
|
DeleteNotificationChannel(ctx context.Context, arg DeleteNotificationChannelParams) (int64, error)
|
||||||
DeleteOutboundWebhookByIDAndUser(ctx context.Context, arg DeleteOutboundWebhookByIDAndUserParams) error
|
DeleteOutboundWebhookByIDAndUser(ctx context.Context, arg DeleteOutboundWebhookByIDAndUserParams) error
|
||||||
DeletePreviewEnvironment(ctx context.Context, id uuid.UUID) error
|
DeletePreviewEnvironment(ctx context.Context, id uuid.UUID) error
|
||||||
DeleteProjectByID(ctx context.Context, projectID uuid.UUID) (int64, error)
|
DeleteProjectByID(ctx context.Context, projectID uuid.UUID) (int64, error)
|
||||||
DeleteProjectEnvironment(ctx context.Context, arg DeleteProjectEnvironmentParams) error
|
DeleteProjectEnvironment(ctx context.Context, arg DeleteProjectEnvironmentParams) error
|
||||||
|
DeleteProjectVariables(ctx context.Context, projectID uuid.UUID) error
|
||||||
DeleteServiceByID(ctx context.Context, id uuid.UUID) error
|
DeleteServiceByID(ctx context.Context, id uuid.UUID) error
|
||||||
DeleteServiceContainersOnAgent(ctx context.Context, arg DeleteServiceContainersOnAgentParams) error
|
DeleteServiceContainersOnAgent(ctx context.Context, arg DeleteServiceContainersOnAgentParams) error
|
||||||
DeleteServiceDomain(ctx context.Context, arg DeleteServiceDomainParams) (int64, error)
|
DeleteServiceDomain(ctx context.Context, arg DeleteServiceDomainParams) (int64, error)
|
||||||
DeleteUserInvite(ctx context.Context, id uuid.UUID) error
|
DeleteUserInvite(ctx context.Context, id uuid.UUID) error
|
||||||
DeleteUserTemplate(ctx context.Context, arg DeleteUserTemplateParams) (int64, error)
|
DeleteUserTemplate(ctx context.Context, arg DeleteUserTemplateParams) (int64, error)
|
||||||
FailDeployment(ctx context.Context, arg FailDeploymentParams) error
|
FailDeployment(ctx context.Context, arg FailDeploymentParams) error
|
||||||
|
FailInterruptedDeployments(ctx context.Context) error
|
||||||
FinishCronExecution(ctx context.Context, arg FinishCronExecutionParams) error
|
FinishCronExecution(ctx context.Context, arg FinishCronExecutionParams) error
|
||||||
|
GetAPServiceBySlug(ctx context.Context, slug string) (GetAPServiceBySlugRow, error)
|
||||||
|
GetAPServiceForValidate(ctx context.Context, id uuid.UUID) (GetAPServiceForValidateRow, error)
|
||||||
GetActiveAgentAuthTokenByHash(ctx context.Context, tokenHash string) (AgentAuthToken, error)
|
GetActiveAgentAuthTokenByHash(ctx context.Context, tokenHash string) (AgentAuthToken, error)
|
||||||
|
GetAdminUserInfo(ctx context.Context, id uuid.UUID) (GetAdminUserInfoRow, error)
|
||||||
GetAgent(ctx context.Context, id string) (NodeAgent, error)
|
GetAgent(ctx context.Context, id string) (NodeAgent, error)
|
||||||
GetAgentByHostAndIP(ctx context.Context, arg GetAgentByHostAndIPParams) (NodeAgent, error)
|
GetAgentByHostAndIP(ctx context.Context, arg GetAgentByHostAndIPParams) (NodeAgent, error)
|
||||||
|
GetAppSetting(ctx context.Context, key string) (string, error)
|
||||||
GetBackupTargetByID(ctx context.Context, id string) (BackupTarget, error)
|
GetBackupTargetByID(ctx context.Context, id string) (BackupTarget, error)
|
||||||
GetBackupTargetByIDAndUser(ctx context.Context, arg GetBackupTargetByIDAndUserParams) (BackupTarget, error)
|
GetBackupTargetByIDAndUser(ctx context.Context, arg GetBackupTargetByIDAndUserParams) (BackupTarget, error)
|
||||||
GetBannerByID(ctx context.Context, id uuid.UUID) (Banner, error)
|
GetBannerByID(ctx context.Context, id uuid.UUID) (Banner, error)
|
||||||
|
GetBuild(ctx context.Context, id string) (GetBuildRow, error)
|
||||||
GetCommandForAgent(ctx context.Context, arg GetCommandForAgentParams) (AgentCommand, error)
|
GetCommandForAgent(ctx context.Context, arg GetCommandForAgentParams) (AgentCommand, error)
|
||||||
GetComplianceReportProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
|
GetComplianceReportProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
|
||||||
GetContainer(ctx context.Context, id string) (ContainerInstance, error)
|
GetContainer(ctx context.Context, id string) (ContainerInstance, error)
|
||||||
@@ -103,12 +116,15 @@ type Querier interface {
|
|||||||
GetDeploymentAccess(ctx context.Context, id uuid.UUID) (GetDeploymentAccessRow, error)
|
GetDeploymentAccess(ctx context.Context, id uuid.UUID) (GetDeploymentAccessRow, error)
|
||||||
GetDeploymentForRollback(ctx context.Context, id uuid.UUID) (GetDeploymentForRollbackRow, error)
|
GetDeploymentForRollback(ctx context.Context, id uuid.UUID) (GetDeploymentForRollbackRow, error)
|
||||||
GetDeploymentWithProject(ctx context.Context, id uuid.UUID) (GetDeploymentWithProjectRow, error)
|
GetDeploymentWithProject(ctx context.Context, id uuid.UUID) (GetDeploymentWithProjectRow, error)
|
||||||
|
GetEnvironmentProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
|
||||||
GetGitProviderForFetch(ctx context.Context, arg GetGitProviderForFetchParams) (GetGitProviderForFetchRow, error)
|
GetGitProviderForFetch(ctx context.Context, arg GetGitProviderForFetchParams) (GetGitProviderForFetchRow, error)
|
||||||
GetGitProviderForRepos(ctx context.Context, arg GetGitProviderForReposParams) (GetGitProviderForReposRow, error)
|
GetGitProviderForRepos(ctx context.Context, arg GetGitProviderForReposParams) (GetGitProviderForReposRow, error)
|
||||||
GetGitProviderName(ctx context.Context, arg GetGitProviderNameParams) (string, error)
|
GetGitProviderName(ctx context.Context, arg GetGitProviderNameParams) (string, error)
|
||||||
GetGitProviderNameByID(ctx context.Context, id uuid.UUID) (string, error)
|
GetGitProviderNameByID(ctx context.Context, id uuid.UUID) (string, error)
|
||||||
GetGitProviderTimestamps(ctx context.Context, id uuid.UUID) (GetGitProviderTimestampsRow, error)
|
GetGitProviderTimestamps(ctx context.Context, id uuid.UUID) (GetGitProviderTimestampsRow, error)
|
||||||
|
GetGitRepoCloneByFullName(ctx context.Context, arg GetGitRepoCloneByFullNameParams) (GetGitRepoCloneByFullNameRow, error)
|
||||||
GetGitRepoForPush(ctx context.Context, id uuid.UUID) (GetGitRepoForPushRow, error)
|
GetGitRepoForPush(ctx context.Context, id uuid.UUID) (GetGitRepoForPushRow, error)
|
||||||
|
GetGitRepoProviderByCloneURL(ctx context.Context, arg GetGitRepoProviderByCloneURLParams) (uuid.UUID, error)
|
||||||
GetGitRepositoryByFullName(ctx context.Context, arg GetGitRepositoryByFullNameParams) (GetGitRepositoryByFullNameRow, error)
|
GetGitRepositoryByFullName(ctx context.Context, arg GetGitRepositoryByFullNameParams) (GetGitRepositoryByFullNameRow, error)
|
||||||
GetGitRepositoryProviderID(ctx context.Context, arg GetGitRepositoryProviderIDParams) (uuid.UUID, error)
|
GetGitRepositoryProviderID(ctx context.Context, arg GetGitRepositoryProviderIDParams) (uuid.UUID, error)
|
||||||
GetGitWebhookForPush(ctx context.Context, id uuid.UUID) (GetGitWebhookForPushRow, error)
|
GetGitWebhookForPush(ctx context.Context, id uuid.UUID) (GetGitWebhookForPushRow, error)
|
||||||
@@ -116,6 +132,7 @@ type Querier interface {
|
|||||||
GetLastDeployedImage(ctx context.Context, serviceID uuid.UUID) (string, error)
|
GetLastDeployedImage(ctx context.Context, serviceID uuid.UUID) (string, error)
|
||||||
GetLatestComplianceReport(ctx context.Context, projectID uuid.UUID) (GetLatestComplianceReportRow, error)
|
GetLatestComplianceReport(ctx context.Context, projectID uuid.UUID) (GetLatestComplianceReportRow, error)
|
||||||
GetLatestSecurityScan(ctx context.Context, projectID uuid.UUID) (GetLatestSecurityScanRow, error)
|
GetLatestSecurityScan(ctx context.Context, projectID uuid.UUID) (GetLatestSecurityScanRow, error)
|
||||||
|
GetNodeAgentName(ctx context.Context, id string) (string, error)
|
||||||
GetOutboundWebhookByID(ctx context.Context, id uuid.UUID) (OutboundWebhook, error)
|
GetOutboundWebhookByID(ctx context.Context, id uuid.UUID) (OutboundWebhook, error)
|
||||||
GetOutboundWebhookByIDAndUser(ctx context.Context, arg GetOutboundWebhookByIDAndUserParams) (OutboundWebhook, error)
|
GetOutboundWebhookByIDAndUser(ctx context.Context, arg GetOutboundWebhookByIDAndUserParams) (OutboundWebhook, error)
|
||||||
GetPreviewEnvironment(ctx context.Context, arg GetPreviewEnvironmentParams) (GetPreviewEnvironmentRow, error)
|
GetPreviewEnvironment(ctx context.Context, arg GetPreviewEnvironmentParams) (GetPreviewEnvironmentRow, error)
|
||||||
@@ -130,7 +147,10 @@ type Querier interface {
|
|||||||
GetProjectOwnerByID(ctx context.Context, projectID uuid.UUID) (uuid.UUID, error)
|
GetProjectOwnerByID(ctx context.Context, projectID uuid.UUID) (uuid.UUID, error)
|
||||||
GetProjectOwnerID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
|
GetProjectOwnerID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
|
||||||
GetProjectOwnerText(ctx context.Context, id uuid.UUID) (string, error)
|
GetProjectOwnerText(ctx context.Context, id uuid.UUID) (string, error)
|
||||||
|
GetProjectReadAccess(ctx context.Context, id uuid.UUID) (GetProjectReadAccessRow, error)
|
||||||
GetProjectRoleForUser(ctx context.Context, arg GetProjectRoleForUserParams) (string, error)
|
GetProjectRoleForUser(ctx context.Context, arg GetProjectRoleForUserParams) (string, error)
|
||||||
|
GetProjectVariableValue(ctx context.Context, arg GetProjectVariableValueParams) (string, error)
|
||||||
|
GetRegistryAuth(ctx context.Context, arg GetRegistryAuthParams) (GetRegistryAuthRow, error)
|
||||||
GetSecurityScanProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
|
GetSecurityScanProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
|
||||||
GetServiceAccess(ctx context.Context, id uuid.UUID) (GetServiceAccessRow, error)
|
GetServiceAccess(ctx context.Context, id uuid.UUID) (GetServiceAccessRow, error)
|
||||||
GetServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) (string, error)
|
GetServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) (string, error)
|
||||||
@@ -139,39 +159,67 @@ type Querier interface {
|
|||||||
GetServiceEnvironment(ctx context.Context, id uuid.UUID) (string, error)
|
GetServiceEnvironment(ctx context.Context, id uuid.UUID) (string, error)
|
||||||
GetServiceForDeploy(ctx context.Context, id uuid.UUID) (GetServiceForDeployRow, error)
|
GetServiceForDeploy(ctx context.Context, id uuid.UUID) (GetServiceForDeployRow, error)
|
||||||
GetServiceForDeployWithOwner(ctx context.Context, id uuid.UUID) (GetServiceForDeployWithOwnerRow, error)
|
GetServiceForDeployWithOwner(ctx context.Context, id uuid.UUID) (GetServiceForDeployWithOwnerRow, error)
|
||||||
|
GetServiceNameProjectPort(ctx context.Context, id uuid.UUID) (GetServiceNameProjectPortRow, error)
|
||||||
|
GetServiceNodeID(ctx context.Context, id uuid.UUID) (sql.NullString, error)
|
||||||
GetServicePlacementBrief(ctx context.Context, id uuid.UUID) (GetServicePlacementBriefRow, error)
|
GetServicePlacementBrief(ctx context.Context, id uuid.UUID) (GetServicePlacementBriefRow, error)
|
||||||
|
GetServicePlacementTags(ctx context.Context, id uuid.UUID) (json.RawMessage, error)
|
||||||
GetServicePort(ctx context.Context, id uuid.UUID) (int32, error)
|
GetServicePort(ctx context.Context, id uuid.UUID) (int32, error)
|
||||||
|
GetServiceProjectAndStatus(ctx context.Context, id uuid.UUID) (GetServiceProjectAndStatusRow, error)
|
||||||
|
GetServiceProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
|
||||||
GetServicePublishedPort(ctx context.Context, id uuid.UUID) (int32, error)
|
GetServicePublishedPort(ctx context.Context, id uuid.UUID) (int32, error)
|
||||||
GetServiceRuntimeWithOwner(ctx context.Context, id uuid.UUID) (GetServiceRuntimeWithOwnerRow, error)
|
GetServiceRuntimeWithOwner(ctx context.Context, id uuid.UUID) (GetServiceRuntimeWithOwnerRow, error)
|
||||||
|
GetServiceSleep(ctx context.Context, id uuid.UUID) (GetServiceSleepRow, error)
|
||||||
|
GetServiceSpread(ctx context.Context, id uuid.UUID) (bool, error)
|
||||||
GetServiceStatusText(ctx context.Context, id uuid.UUID) (string, error)
|
GetServiceStatusText(ctx context.Context, id uuid.UUID) (string, error)
|
||||||
GetServiceTemplateByID(ctx context.Context, id string) (ServiceTemplate, error)
|
GetServiceTemplateByID(ctx context.Context, id string) (ServiceTemplate, error)
|
||||||
|
GetServiceTraefikLabels(ctx context.Context, id uuid.UUID) (json.RawMessage, error)
|
||||||
GetServiceTypeBrief(ctx context.Context, arg GetServiceTypeBriefParams) (GetServiceTypeBriefRow, error)
|
GetServiceTypeBrief(ctx context.Context, arg GetServiceTypeBriefParams) (GetServiceTypeBriefRow, error)
|
||||||
GetServiceVariableValue(ctx context.Context, arg GetServiceVariableValueParams) (string, error)
|
GetServiceVariableValue(ctx context.Context, arg GetServiceVariableValueParams) (string, error)
|
||||||
|
GetServiceVolumes(ctx context.Context, id uuid.UUID) (json.RawMessage, error)
|
||||||
GetUserByEmailForAuth(ctx context.Context, email string) (GetUserByEmailForAuthRow, error)
|
GetUserByEmailForAuth(ctx context.Context, email string) (GetUserByEmailForAuthRow, error)
|
||||||
GetUserByID(ctx context.Context, id uuid.UUID) (GetUserByIDRow, error)
|
GetUserByID(ctx context.Context, id uuid.UUID) (GetUserByIDRow, error)
|
||||||
GetUserInviteByTokenHash(ctx context.Context, tokenHash string) (UserInvite, error)
|
GetUserInviteByTokenHash(ctx context.Context, tokenHash string) (UserInvite, error)
|
||||||
|
GetUserIsAdmin(ctx context.Context, id uuid.UUID) (bool, error)
|
||||||
GetUserTokenByHash(ctx context.Context, tokenHash string) (UserToken, error)
|
GetUserTokenByHash(ctx context.Context, tokenHash string) (UserToken, error)
|
||||||
GetVulnerabilityMetrics(ctx context.Context, projectID uuid.UUID) (GetVulnerabilityMetricsRow, error)
|
GetVulnerabilityMetrics(ctx context.Context, projectID uuid.UUID) (GetVulnerabilityMetricsRow, error)
|
||||||
GetVulnerabilityProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
|
GetVulnerabilityProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
|
||||||
|
InsertAPKey(ctx context.Context, arg InsertAPKeyParams) (uuid.UUID, error)
|
||||||
|
InsertAPService(ctx context.Context, arg InsertAPServiceParams) (uuid.UUID, error)
|
||||||
InsertAgentHeartbeat(ctx context.Context, arg InsertAgentHeartbeatParams) error
|
InsertAgentHeartbeat(ctx context.Context, arg InsertAgentHeartbeatParams) error
|
||||||
|
InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) error
|
||||||
|
InsertAuditLogWithRequest(ctx context.Context, arg InsertAuditLogWithRequestParams) error
|
||||||
InsertCronExecution(ctx context.Context, arg InsertCronExecutionParams) error
|
InsertCronExecution(ctx context.Context, arg InsertCronExecutionParams) error
|
||||||
InsertCronJob(ctx context.Context, arg InsertCronJobParams) error
|
InsertCronJob(ctx context.Context, arg InsertCronJobParams) error
|
||||||
InsertDeployment(ctx context.Context, arg InsertDeploymentParams) error
|
InsertDeployment(ctx context.Context, arg InsertDeploymentParams) error
|
||||||
|
InsertEnvVar(ctx context.Context, arg InsertEnvVarParams) error
|
||||||
|
InsertGatewayIncident(ctx context.Context, arg InsertGatewayIncidentParams) error
|
||||||
InsertGitProvider(ctx context.Context, arg InsertGitProviderParams) error
|
InsertGitProvider(ctx context.Context, arg InsertGitProviderParams) error
|
||||||
InsertGitRepository(ctx context.Context, arg InsertGitRepositoryParams) (InsertGitRepositoryRow, error)
|
InsertGitRepository(ctx context.Context, arg InsertGitRepositoryParams) (InsertGitRepositoryRow, error)
|
||||||
|
InsertMetricPoint(ctx context.Context, labelsJson sql.NullString) error
|
||||||
InsertNotificationChannel(ctx context.Context, arg InsertNotificationChannelParams) (NotificationChannel, error)
|
InsertNotificationChannel(ctx context.Context, arg InsertNotificationChannelParams) (NotificationChannel, error)
|
||||||
InsertPendingDeployment(ctx context.Context, arg InsertPendingDeploymentParams) error
|
InsertPendingDeployment(ctx context.Context, arg InsertPendingDeploymentParams) error
|
||||||
InsertProjectEnvironment(ctx context.Context, arg InsertProjectEnvironmentParams) error
|
InsertProjectEnvironment(ctx context.Context, arg InsertProjectEnvironmentParams) error
|
||||||
|
InsertProjectVariable(ctx context.Context, arg InsertProjectVariableParams) error
|
||||||
|
InsertServiceIncident(ctx context.Context, arg InsertServiceIncidentParams) error
|
||||||
InsertUser(ctx context.Context, arg InsertUserParams) (InsertUserRow, error)
|
InsertUser(ctx context.Context, arg InsertUserParams) (InsertUserRow, error)
|
||||||
InsertUserAdmin(ctx context.Context, arg InsertUserAdminParams) (InsertUserAdminRow, error)
|
InsertUserAdmin(ctx context.Context, arg InsertUserAdminParams) (InsertUserAdminRow, error)
|
||||||
|
ListAPKeys(ctx context.Context) ([]ListAPKeysRow, error)
|
||||||
|
ListAPKeysByPrefix(ctx context.Context, keyPrefix string) ([]ListAPKeysByPrefixRow, error)
|
||||||
|
ListAPServices(ctx context.Context) ([]ListAPServicesRow, error)
|
||||||
ListActiveBanners(ctx context.Context) ([]Banner, error)
|
ListActiveBanners(ctx context.Context) ([]Banner, error)
|
||||||
|
ListActiveOperationDeployments(ctx context.Context, arg ListActiveOperationDeploymentsParams) ([]ListActiveOperationDeploymentsRow, error)
|
||||||
|
ListAdminUserIDs(ctx context.Context) ([]uuid.UUID, error)
|
||||||
|
ListAdminUsers(ctx context.Context) ([]ListAdminUsersRow, error)
|
||||||
ListAgentAuthTokens(ctx context.Context) ([]AgentAuthToken, error)
|
ListAgentAuthTokens(ctx context.Context) ([]AgentAuthToken, error)
|
||||||
|
ListAgentHealthRows(ctx context.Context, lastHeartbeat sql.NullTime) ([]ListAgentHealthRowsRow, error)
|
||||||
ListAgentHeartbeatsSince(ctx context.Context, arg ListAgentHeartbeatsSinceParams) ([]AgentHeartbeat, error)
|
ListAgentHeartbeatsSince(ctx context.Context, arg ListAgentHeartbeatsSinceParams) ([]AgentHeartbeat, error)
|
||||||
ListAgents(ctx context.Context) ([]NodeAgent, error)
|
ListAgents(ctx context.Context) ([]NodeAgent, error)
|
||||||
ListAllDatabaseServices(ctx context.Context) ([]DatabaseService, error)
|
ListAllDatabaseServices(ctx context.Context) ([]DatabaseService, error)
|
||||||
ListAllNotificationChannelsByUser(ctx context.Context, userID uuid.UUID) ([]NotificationChannel, error)
|
ListAllNotificationChannelsByUser(ctx context.Context, userID uuid.UUID) ([]NotificationChannel, error)
|
||||||
|
ListAuditBackfillBatch(ctx context.Context, dollar_1 uuid.UUID) ([]ListAuditBackfillBatchRow, error)
|
||||||
ListBackupTargetsByUser(ctx context.Context, userID string) ([]BackupTarget, error)
|
ListBackupTargetsByUser(ctx context.Context, userID string) ([]BackupTarget, error)
|
||||||
ListBanners(ctx context.Context) ([]Banner, error)
|
ListBanners(ctx context.Context) ([]Banner, error)
|
||||||
|
ListClientEventPaths(ctx context.Context) ([]ListClientEventPathsRow, error)
|
||||||
ListCommandsForAgent(ctx context.Context, nodeAgentID string) ([]AgentCommand, error)
|
ListCommandsForAgent(ctx context.Context, nodeAgentID string) ([]AgentCommand, error)
|
||||||
ListComplianceFrameworks(ctx context.Context) ([]ListComplianceFrameworksRow, error)
|
ListComplianceFrameworks(ctx context.Context) ([]ListComplianceFrameworksRow, error)
|
||||||
ListConnectedRepositories(ctx context.Context, arg ListConnectedRepositoriesParams) ([]ListConnectedRepositoriesRow, error)
|
ListConnectedRepositories(ctx context.Context, arg ListConnectedRepositoriesParams) ([]ListConnectedRepositoriesRow, error)
|
||||||
@@ -187,6 +235,7 @@ type Querier interface {
|
|||||||
ListExpiredPreviewsForUser(ctx context.Context, arg ListExpiredPreviewsForUserParams) ([]ListExpiredPreviewsForUserRow, error)
|
ListExpiredPreviewsForUser(ctx context.Context, arg ListExpiredPreviewsForUserParams) ([]ListExpiredPreviewsForUserRow, error)
|
||||||
ListGitProviders(ctx context.Context, userID uuid.UUID) ([]ListGitProvidersRow, error)
|
ListGitProviders(ctx context.Context, userID uuid.UUID) ([]ListGitProvidersRow, error)
|
||||||
ListGitRepositories(ctx context.Context, arg ListGitRepositoriesParams) ([]ListGitRepositoriesRow, error)
|
ListGitRepositories(ctx context.Context, arg ListGitRepositoriesParams) ([]ListGitRepositoriesRow, error)
|
||||||
|
ListIncidentStatusCodes(ctx context.Context) ([]ListIncidentStatusCodesRow, error)
|
||||||
ListInstanceMetrics(ctx context.Context, arg ListInstanceMetricsParams) ([]ListInstanceMetricsRow, error)
|
ListInstanceMetrics(ctx context.Context, arg ListInstanceMetricsParams) ([]ListInstanceMetricsRow, error)
|
||||||
ListNodeAgentsForService(ctx context.Context, serviceID string) ([]string, error)
|
ListNodeAgentsForService(ctx context.Context, serviceID string) ([]string, error)
|
||||||
ListNodeMetrics(ctx context.Context, arg ListNodeMetricsParams) ([]ListNodeMetricsRow, error)
|
ListNodeMetrics(ctx context.Context, arg ListNodeMetricsParams) ([]ListNodeMetricsRow, error)
|
||||||
@@ -203,12 +252,18 @@ type Querier interface {
|
|||||||
// platform admin. Anonymous callers pass user_id = uuid.Nil, is_admin = false.
|
// platform admin. Anonymous callers pass user_id = uuid.Nil, is_admin = false.
|
||||||
ListProjectsWithStatsByUser(ctx context.Context, arg ListProjectsWithStatsByUserParams) ([]ListProjectsWithStatsByUserRow, error)
|
ListProjectsWithStatsByUser(ctx context.Context, arg ListProjectsWithStatsByUserParams) ([]ListProjectsWithStatsByUserRow, error)
|
||||||
ListRecentAccessibleDeployments(ctx context.Context, arg ListRecentAccessibleDeploymentsParams) ([]ListRecentAccessibleDeploymentsRow, error)
|
ListRecentAccessibleDeployments(ctx context.Context, arg ListRecentAccessibleDeploymentsParams) ([]ListRecentAccessibleDeploymentsRow, error)
|
||||||
|
ListRecentCronRuns(ctx context.Context, arg ListRecentCronRunsParams) ([]ListRecentCronRunsRow, error)
|
||||||
|
ListRecentFailedDeployments(ctx context.Context, arg ListRecentFailedDeploymentsParams) ([]ListRecentFailedDeploymentsRow, error)
|
||||||
|
ListRecentOperationBackups(ctx context.Context, arg ListRecentOperationBackupsParams) ([]ListRecentOperationBackupsRow, error)
|
||||||
|
ListRequestsByDay(ctx context.Context) ([]ListRequestsByDayRow, error)
|
||||||
|
ListResourceAuditLogs(ctx context.Context, arg ListResourceAuditLogsParams) ([]ListResourceAuditLogsRow, error)
|
||||||
// Base domains of every online, schedulable agent carrying the required
|
// Base domains of every online, schedulable agent carrying the required
|
||||||
// placement tags — auto-domain candidates for spread services.
|
// placement tags — auto-domain candidates for spread services.
|
||||||
ListSchedulableAgentDomains(ctx context.Context, dollar_1 json.RawMessage) ([]string, error)
|
ListSchedulableAgentDomains(ctx context.Context, dollar_1 json.RawMessage) ([]string, error)
|
||||||
ListSchedulableAgents(ctx context.Context) ([]ListSchedulableAgentsRow, error)
|
ListSchedulableAgents(ctx context.Context) ([]ListSchedulableAgentsRow, error)
|
||||||
// Only agents carrying every required placement tag (jsonb array containment).
|
// Only agents carrying every required placement tag (jsonb array containment).
|
||||||
ListSchedulableAgentsMatching(ctx context.Context, dollar_1 json.RawMessage) ([]ListSchedulableAgentsMatchingRow, error)
|
ListSchedulableAgentsMatching(ctx context.Context, dollar_1 json.RawMessage) ([]ListSchedulableAgentsMatchingRow, error)
|
||||||
|
ListSecretEnvVars(ctx context.Context) ([]ListSecretEnvVarsRow, error)
|
||||||
// Distinct agents holding inventory rows for a service.
|
// Distinct agents holding inventory rows for a service.
|
||||||
ListServiceAgents(ctx context.Context, serviceID string) ([]string, error)
|
ListServiceAgents(ctx context.Context, serviceID string) ([]string, error)
|
||||||
ListServiceContainers(ctx context.Context, serviceID string) ([]ListServiceContainersRow, error)
|
ListServiceContainers(ctx context.Context, serviceID string) ([]ListServiceContainersRow, error)
|
||||||
@@ -221,6 +276,7 @@ type Querier interface {
|
|||||||
ListServicesForPush(ctx context.Context, arg ListServicesForPushParams) ([]ListServicesForPushRow, error)
|
ListServicesForPush(ctx context.Context, arg ListServicesForPushParams) ([]ListServicesForPushRow, error)
|
||||||
ListSleepCandidates(ctx context.Context) ([]ListSleepCandidatesRow, error)
|
ListSleepCandidates(ctx context.Context) ([]ListSleepCandidatesRow, error)
|
||||||
ListSweepablePreviews(ctx context.Context) ([]ListSweepablePreviewsRow, error)
|
ListSweepablePreviews(ctx context.Context) ([]ListSweepablePreviewsRow, error)
|
||||||
|
ListTopAPServices(ctx context.Context) ([]ListTopAPServicesRow, error)
|
||||||
ListUserInvites(ctx context.Context) ([]UserInvite, error)
|
ListUserInvites(ctx context.Context) ([]UserInvite, error)
|
||||||
ListUserTokens(ctx context.Context, userID uuid.UUID) ([]UserToken, error)
|
ListUserTokens(ctx context.Context, userID uuid.UUID) ([]UserToken, error)
|
||||||
ListVulnerabilities(ctx context.Context, projectID uuid.UUID) ([]ListVulnerabilitiesRow, error)
|
ListVulnerabilities(ctx context.Context, projectID uuid.UUID) ([]ListVulnerabilitiesRow, error)
|
||||||
@@ -236,14 +292,21 @@ type Querier interface {
|
|||||||
// Same resource-aware ordering as PickLeastLoadedAgent, restricted to agents
|
// Same resource-aware ordering as PickLeastLoadedAgent, restricted to agents
|
||||||
// carrying every required placement tag.
|
// carrying every required placement tag.
|
||||||
PickLeastLoadedAgentMatching(ctx context.Context, dollar_1 json.RawMessage) (string, error)
|
PickLeastLoadedAgentMatching(ctx context.Context, dollar_1 json.RawMessage) (string, error)
|
||||||
|
ProjectMemberExists(ctx context.Context, arg ProjectMemberExistsParams) (bool, error)
|
||||||
|
PruneAuditLogs(ctx context.Context, dollar_1 int32) (int64, error)
|
||||||
RevokeAgentAuthToken(ctx context.Context, id uuid.UUID) (AgentAuthToken, error)
|
RevokeAgentAuthToken(ctx context.Context, id uuid.UUID) (AgentAuthToken, error)
|
||||||
RevokeUserToken(ctx context.Context, arg RevokeUserTokenParams) (int64, error)
|
RevokeUserToken(ctx context.Context, arg RevokeUserTokenParams) (int64, error)
|
||||||
ScrubCommandPayload(ctx context.Context, arg ScrubCommandPayloadParams) error
|
ScrubCommandPayload(ctx context.Context, arg ScrubCommandPayloadParams) error
|
||||||
ServiceExistsInProject(ctx context.Context, arg ServiceExistsInProjectParams) (bool, error)
|
ServiceExistsInProject(ctx context.Context, arg ServiceExistsInProjectParams) (bool, error)
|
||||||
ServiceWriteAccess(ctx context.Context, arg ServiceWriteAccessParams) (bool, error)
|
ServiceWriteAccess(ctx context.Context, arg ServiceWriteAccessParams) (bool, error)
|
||||||
|
SetAPKeyEnabled(ctx context.Context, arg SetAPKeyEnabledParams) error
|
||||||
|
SetAPServiceEnabled(ctx context.Context, arg SetAPServiceEnabledParams) error
|
||||||
|
SetAPServiceValidation(ctx context.Context, arg SetAPServiceValidationParams) error
|
||||||
SetAgentDefaultDomain(ctx context.Context, arg SetAgentDefaultDomainParams) error
|
SetAgentDefaultDomain(ctx context.Context, arg SetAgentDefaultDomainParams) error
|
||||||
SetAgentSchedulable(ctx context.Context, arg SetAgentSchedulableParams) error
|
SetAgentSchedulable(ctx context.Context, arg SetAgentSchedulableParams) error
|
||||||
|
SetAgentStatus(ctx context.Context, arg SetAgentStatusParams) error
|
||||||
SetAgentTags(ctx context.Context, arg SetAgentTagsParams) error
|
SetAgentTags(ctx context.Context, arg SetAgentTagsParams) error
|
||||||
|
SetAuditLogMetadata(ctx context.Context, arg SetAuditLogMetadataParams) error
|
||||||
SetDatabaseBackupRemoteKeyByID(ctx context.Context, arg SetDatabaseBackupRemoteKeyByIDParams) error
|
SetDatabaseBackupRemoteKeyByID(ctx context.Context, arg SetDatabaseBackupRemoteKeyByIDParams) error
|
||||||
SetDatabaseBackupScheduleByIDAndUser(ctx context.Context, arg SetDatabaseBackupScheduleByIDAndUserParams) error
|
SetDatabaseBackupScheduleByIDAndUser(ctx context.Context, arg SetDatabaseBackupScheduleByIDAndUserParams) error
|
||||||
SetDatabaseBackupStatusByID(ctx context.Context, arg SetDatabaseBackupStatusByIDParams) error
|
SetDatabaseBackupStatusByID(ctx context.Context, arg SetDatabaseBackupStatusByIDParams) error
|
||||||
@@ -260,8 +323,14 @@ type Querier interface {
|
|||||||
SetServicePublishedPort(ctx context.Context, arg SetServicePublishedPortParams) error
|
SetServicePublishedPort(ctx context.Context, arg SetServicePublishedPortParams) error
|
||||||
SetServiceReplicas(ctx context.Context, arg SetServiceReplicasParams) error
|
SetServiceReplicas(ctx context.Context, arg SetServiceReplicasParams) error
|
||||||
SetServiceStatus(ctx context.Context, arg SetServiceStatusParams) error
|
SetServiceStatus(ctx context.Context, arg SetServiceStatusParams) error
|
||||||
|
SetUserAdmin(ctx context.Context, arg SetUserAdminParams) (int64, error)
|
||||||
|
SumRequestMetricSince(ctx context.Context, dollar_1 string) (int32, error)
|
||||||
|
SumUsageByKey(ctx context.Context, arg SumUsageByKeyParams) (int64, error)
|
||||||
|
SumUsageByService(ctx context.Context, arg SumUsageByServiceParams) (int64, error)
|
||||||
|
SumUsageCounters(ctx context.Context) (interface{}, error)
|
||||||
SyncDeploymentProgress(ctx context.Context, arg SyncDeploymentProgressParams) error
|
SyncDeploymentProgress(ctx context.Context, arg SyncDeploymentProgressParams) error
|
||||||
SyncPreviewStatuses(ctx context.Context) error
|
SyncPreviewStatuses(ctx context.Context) error
|
||||||
|
TouchAPKeyLastUsed(ctx context.Context, id uuid.UUID) error
|
||||||
TouchAgentAuthToken(ctx context.Context, tokenHash string) error
|
TouchAgentAuthToken(ctx context.Context, tokenHash string) error
|
||||||
TouchServiceDomainChecked(ctx context.Context, id uuid.UUID) error
|
TouchServiceDomainChecked(ctx context.Context, id uuid.UUID) error
|
||||||
TouchUserToken(ctx context.Context, id uuid.UUID) error
|
TouchUserToken(ctx context.Context, id uuid.UUID) error
|
||||||
@@ -282,14 +351,21 @@ type Querier interface {
|
|||||||
UpdateDatabaseServiceNameAndPlanByIDAndUser(ctx context.Context, arg UpdateDatabaseServiceNameAndPlanByIDAndUserParams) error
|
UpdateDatabaseServiceNameAndPlanByIDAndUser(ctx context.Context, arg UpdateDatabaseServiceNameAndPlanByIDAndUserParams) error
|
||||||
UpdateDatabaseServiceNameByIDAndUser(ctx context.Context, arg UpdateDatabaseServiceNameByIDAndUserParams) error
|
UpdateDatabaseServiceNameByIDAndUser(ctx context.Context, arg UpdateDatabaseServiceNameByIDAndUserParams) error
|
||||||
UpdateDatabaseServicePlanByIDAndUser(ctx context.Context, arg UpdateDatabaseServicePlanByIDAndUserParams) error
|
UpdateDatabaseServicePlanByIDAndUser(ctx context.Context, arg UpdateDatabaseServicePlanByIDAndUserParams) error
|
||||||
|
UpdateEnvVarValue(ctx context.Context, arg UpdateEnvVarValueParams) error
|
||||||
UpdateOutboundWebhook(ctx context.Context, arg UpdateOutboundWebhookParams) error
|
UpdateOutboundWebhook(ctx context.Context, arg UpdateOutboundWebhookParams) error
|
||||||
UpdatePreviewEnvironment(ctx context.Context, arg UpdatePreviewEnvironmentParams) error
|
UpdatePreviewEnvironment(ctx context.Context, arg UpdatePreviewEnvironmentParams) error
|
||||||
UpdateProjectByID(ctx context.Context, arg UpdateProjectByIDParams) (int64, error)
|
UpdateProjectByID(ctx context.Context, arg UpdateProjectByIDParams) (int64, error)
|
||||||
|
UpdateServiceDeployMeta(ctx context.Context, arg UpdateServiceDeployMetaParams) error
|
||||||
UpdateServicePreviewBranch(ctx context.Context, arg UpdateServicePreviewBranchParams) error
|
UpdateServicePreviewBranch(ctx context.Context, arg UpdateServicePreviewBranchParams) error
|
||||||
|
UpdateServiceRuntimeMeta(ctx context.Context, arg UpdateServiceRuntimeMetaParams) error
|
||||||
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) error
|
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) error
|
||||||
UpdateUserTemplate(ctx context.Context, arg UpdateUserTemplateParams) (int64, error)
|
UpdateUserTemplate(ctx context.Context, arg UpdateUserTemplateParams) (int64, error)
|
||||||
UpdateVulnerabilityStatus(ctx context.Context, arg UpdateVulnerabilityStatusParams) error
|
UpdateVulnerabilityStatus(ctx context.Context, arg UpdateVulnerabilityStatusParams) error
|
||||||
UpdateWebhookDeliveryResult(ctx context.Context, arg UpdateWebhookDeliveryResultParams) error
|
UpdateWebhookDeliveryResult(ctx context.Context, arg UpdateWebhookDeliveryResultParams) error
|
||||||
|
UpsertAdminUser(ctx context.Context, arg UpsertAdminUserParams) (uuid.UUID, error)
|
||||||
|
UpsertAppSetting(ctx context.Context, arg UpsertAppSettingParams) error
|
||||||
|
UpsertAppSettingValue(ctx context.Context, arg UpsertAppSettingValueParams) error
|
||||||
|
UpsertBuild(ctx context.Context, arg UpsertBuildParams) error
|
||||||
UpsertEnvironmentVariable(ctx context.Context, arg UpsertEnvironmentVariableParams) error
|
UpsertEnvironmentVariable(ctx context.Context, arg UpsertEnvironmentVariableParams) error
|
||||||
UpsertGitHubAppProvider(ctx context.Context, arg UpsertGitHubAppProviderParams) (UpsertGitHubAppProviderRow, error)
|
UpsertGitHubAppProvider(ctx context.Context, arg UpsertGitHubAppProviderParams) (UpsertGitHubAppProviderRow, error)
|
||||||
UpsertGitRepository(ctx context.Context, arg UpsertGitRepositoryParams) error
|
UpsertGitRepository(ctx context.Context, arg UpsertGitRepositoryParams) error
|
||||||
@@ -300,6 +376,7 @@ type Querier interface {
|
|||||||
UpsertServiceContainer(ctx context.Context, arg UpsertServiceContainerParams) error
|
UpsertServiceContainer(ctx context.Context, arg UpsertServiceContainerParams) error
|
||||||
UpsertServiceMetrics(ctx context.Context, arg UpsertServiceMetricsParams) error
|
UpsertServiceMetrics(ctx context.Context, arg UpsertServiceMetricsParams) error
|
||||||
UpsertServiceTemplate(ctx context.Context, arg UpsertServiceTemplateParams) error
|
UpsertServiceTemplate(ctx context.Context, arg UpsertServiceTemplateParams) error
|
||||||
|
UpsertUsageCounter(ctx context.Context, arg UpsertUsageCounterParams) error
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Querier = (*Queries)(nil)
|
var _ Querier = (*Queries)(nil)
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: services_meta.sql
|
||||||
|
|
||||||
|
package sqlcdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteEnvVarsForService = `-- name: DeleteEnvVarsForService :exec
|
||||||
|
DELETE FROM environment_variables WHERE service_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteEnvVarsForService(ctx context.Context, serviceID uuid.UUID) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteEnvVarsForService, serviceID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const failInterruptedDeployments = `-- name: FailInterruptedDeployments :exec
|
||||||
|
UPDATE deployments SET status = 'failed', error = 'Interrupted by server restart', completed_at = NOW(), updated_at = NOW()
|
||||||
|
WHERE status IN ('queued', 'pending', 'building', 'deploying', 'rolling_back')
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) FailInterruptedDeployments(ctx context.Context) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, failInterruptedDeployments)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEnvironmentProjectID = `-- name: GetEnvironmentProjectID :one
|
||||||
|
SELECT project_id FROM environments WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetEnvironmentProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getEnvironmentProjectID, id)
|
||||||
|
var project_id uuid.UUID
|
||||||
|
err := row.Scan(&project_id)
|
||||||
|
return project_id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNodeAgentName = `-- name: GetNodeAgentName :one
|
||||||
|
SELECT name FROM node_agents WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetNodeAgentName(ctx context.Context, id string) (string, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getNodeAgentName, id)
|
||||||
|
var name string
|
||||||
|
err := row.Scan(&name)
|
||||||
|
return name, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProjectReadAccess = `-- name: GetProjectReadAccess :one
|
||||||
|
SELECT is_approved, owner_id::text FROM projects WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetProjectReadAccessRow struct {
|
||||||
|
IsApproved bool `json:"is_approved"`
|
||||||
|
OwnerID string `json:"owner_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetProjectReadAccess(ctx context.Context, id uuid.UUID) (GetProjectReadAccessRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getProjectReadAccess, id)
|
||||||
|
var i GetProjectReadAccessRow
|
||||||
|
err := row.Scan(&i.IsApproved, &i.OwnerID)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServiceNameProjectPort = `-- name: GetServiceNameProjectPort :one
|
||||||
|
SELECT name, project_id::text, COALESCE(port, 0)::int AS port
|
||||||
|
FROM services WHERE id::text = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetServiceNameProjectPortRow struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
ProjectID string `json:"project_id"`
|
||||||
|
Port int32 `json:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetServiceNameProjectPort(ctx context.Context, id uuid.UUID) (GetServiceNameProjectPortRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getServiceNameProjectPort, id)
|
||||||
|
var i GetServiceNameProjectPortRow
|
||||||
|
err := row.Scan(&i.Name, &i.ProjectID, &i.Port)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServiceNodeID = `-- name: GetServiceNodeID :one
|
||||||
|
SELECT node_id FROM services WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetServiceNodeID(ctx context.Context, id uuid.UUID) (sql.NullString, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getServiceNodeID, id)
|
||||||
|
var node_id sql.NullString
|
||||||
|
err := row.Scan(&node_id)
|
||||||
|
return node_id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServicePlacementTags = `-- name: GetServicePlacementTags :one
|
||||||
|
SELECT placement_tags FROM services WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetServicePlacementTags(ctx context.Context, id uuid.UUID) (json.RawMessage, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getServicePlacementTags, id)
|
||||||
|
var placement_tags json.RawMessage
|
||||||
|
err := row.Scan(&placement_tags)
|
||||||
|
return placement_tags, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServiceProjectAndStatus = `-- name: GetServiceProjectAndStatus :one
|
||||||
|
SELECT s.project_id, COALESCE(s.status, '') AS status
|
||||||
|
FROM services s WHERE s.id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetServiceProjectAndStatusRow struct {
|
||||||
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetServiceProjectAndStatus(ctx context.Context, id uuid.UUID) (GetServiceProjectAndStatusRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getServiceProjectAndStatus, id)
|
||||||
|
var i GetServiceProjectAndStatusRow
|
||||||
|
err := row.Scan(&i.ProjectID, &i.Status)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServiceProjectID = `-- name: GetServiceProjectID :one
|
||||||
|
SELECT project_id FROM services WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetServiceProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getServiceProjectID, id)
|
||||||
|
var project_id uuid.UUID
|
||||||
|
err := row.Scan(&project_id)
|
||||||
|
return project_id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServiceSleep = `-- name: GetServiceSleep :one
|
||||||
|
SELECT COALESCE(sleep_enabled, false) AS sleep_enabled, COALESCE(sleep_idle_minutes, 15) AS sleep_idle_minutes
|
||||||
|
FROM services WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetServiceSleepRow struct {
|
||||||
|
SleepEnabled bool `json:"sleep_enabled"`
|
||||||
|
SleepIdleMinutes int32 `json:"sleep_idle_minutes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetServiceSleep(ctx context.Context, id uuid.UUID) (GetServiceSleepRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getServiceSleep, id)
|
||||||
|
var i GetServiceSleepRow
|
||||||
|
err := row.Scan(&i.SleepEnabled, &i.SleepIdleMinutes)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServiceSpread = `-- name: GetServiceSpread :one
|
||||||
|
SELECT COALESCE(spread, false) FROM services WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetServiceSpread(ctx context.Context, id uuid.UUID) (bool, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getServiceSpread, id)
|
||||||
|
var spread bool
|
||||||
|
err := row.Scan(&spread)
|
||||||
|
return spread, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServiceTraefikLabels = `-- name: GetServiceTraefikLabels :one
|
||||||
|
SELECT traefik_labels FROM services WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetServiceTraefikLabels(ctx context.Context, id uuid.UUID) (json.RawMessage, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getServiceTraefikLabels, id)
|
||||||
|
var traefik_labels json.RawMessage
|
||||||
|
err := row.Scan(&traefik_labels)
|
||||||
|
return traefik_labels, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServiceVolumes = `-- name: GetServiceVolumes :one
|
||||||
|
SELECT volumes FROM services WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetServiceVolumes(ctx context.Context, id uuid.UUID) (json.RawMessage, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getServiceVolumes, id)
|
||||||
|
var volumes json.RawMessage
|
||||||
|
err := row.Scan(&volumes)
|
||||||
|
return volumes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listAgentHealthRows = `-- name: ListAgentHealthRows :many
|
||||||
|
SELECT id, COALESCE(name, hostname, id::text) AS name, COALESCE(status, '') AS status,
|
||||||
|
COALESCE(last_heartbeat < $1, TRUE)::bool AS stale, auto_prune
|
||||||
|
FROM node_agents
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListAgentHealthRowsRow struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Stale bool `json:"stale"`
|
||||||
|
AutoPrune bool `json:"auto_prune"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListAgentHealthRows(ctx context.Context, lastHeartbeat sql.NullTime) ([]ListAgentHealthRowsRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listAgentHealthRows, lastHeartbeat)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListAgentHealthRowsRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListAgentHealthRowsRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Status,
|
||||||
|
&i.Stale,
|
||||||
|
&i.AutoPrune,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listSecretEnvVars = `-- name: ListSecretEnvVars :many
|
||||||
|
SELECT id, value FROM environment_variables WHERE is_secret = TRUE
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListSecretEnvVarsRow struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListSecretEnvVars(ctx context.Context) ([]ListSecretEnvVarsRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listSecretEnvVars)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListSecretEnvVarsRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListSecretEnvVarsRow
|
||||||
|
if err := rows.Scan(&i.ID, &i.Value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectMemberExists = `-- name: ProjectMemberExists :one
|
||||||
|
SELECT EXISTS(SELECT 1 FROM project_members WHERE project_id = $1 AND user_id = $2)
|
||||||
|
`
|
||||||
|
|
||||||
|
type ProjectMemberExistsParams struct {
|
||||||
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ProjectMemberExists(ctx context.Context, arg ProjectMemberExistsParams) (bool, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, projectMemberExists, arg.ProjectID, arg.UserID)
|
||||||
|
var exists bool
|
||||||
|
err := row.Scan(&exists)
|
||||||
|
return exists, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAgentStatus = `-- name: SetAgentStatus :exec
|
||||||
|
UPDATE node_agents SET status = $1, updated_at = NOW() WHERE id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type SetAgentStatusParams struct {
|
||||||
|
Status sql.NullString `json:"status"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SetAgentStatus(ctx context.Context, arg SetAgentStatusParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, setAgentStatus, arg.Status, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateEnvVarValue = `-- name: UpdateEnvVarValue :exec
|
||||||
|
UPDATE environment_variables SET value = $1, updated_at = NOW() WHERE id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateEnvVarValueParams struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateEnvVarValue(ctx context.Context, arg UpdateEnvVarValueParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, updateEnvVarValue, arg.Value, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: settings.sql
|
||||||
|
|
||||||
|
package sqlcdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteAppSetting = `-- name: DeleteAppSetting :exec
|
||||||
|
DELETE FROM app_settings WHERE key = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteAppSetting(ctx context.Context, key string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteAppSetting, key)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAppSetting = `-- name: GetAppSetting :one
|
||||||
|
SELECT value FROM app_settings WHERE key = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAppSetting(ctx context.Context, key string) (string, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getAppSetting, key)
|
||||||
|
var value string
|
||||||
|
err := row.Scan(&value)
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserIsAdmin = `-- name: GetUserIsAdmin :one
|
||||||
|
SELECT is_admin FROM users WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUserIsAdmin(ctx context.Context, id uuid.UUID) (bool, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getUserIsAdmin, id)
|
||||||
|
var is_admin bool
|
||||||
|
err := row.Scan(&is_admin)
|
||||||
|
return is_admin, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listAdminUserIDs = `-- name: ListAdminUserIDs :many
|
||||||
|
SELECT id FROM users WHERE is_admin
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListAdminUserIDs(ctx context.Context) ([]uuid.UUID, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listAdminUserIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []uuid.UUID{}
|
||||||
|
for rows.Next() {
|
||||||
|
var id uuid.UUID
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, id)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertAppSetting = `-- name: UpsertAppSetting :exec
|
||||||
|
INSERT INTO app_settings (key, value, is_secret, updated_at)
|
||||||
|
VALUES ($1, $2, $3, NOW())
|
||||||
|
ON CONFLICT (key) DO UPDATE
|
||||||
|
SET value = EXCLUDED.value, is_secret = EXCLUDED.is_secret, updated_at = NOW()
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpsertAppSettingParams struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
IsSecret bool `json:"is_secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpsertAppSetting(ctx context.Context, arg UpsertAppSettingParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, upsertAppSetting, arg.Key, arg.Value, arg.IsSecret)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertAppSettingValue = `-- name: UpsertAppSettingValue :exec
|
||||||
|
INSERT INTO app_settings (key, value, updated_at) VALUES ($1, $2, NOW())
|
||||||
|
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW()
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpsertAppSettingValueParams struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpsertAppSettingValue(ctx context.Context, arg UpsertAppSettingValueParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, upsertAppSettingValue, arg.Key, arg.Value)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
-- name: GetAPServiceForValidate :one
|
||||||
|
SELECT upstream_url, health_path, upstream_auth_header, upstream_auth_value, request_timeout_ms
|
||||||
|
FROM api_services WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: SetAPServiceValidation :exec
|
||||||
|
UPDATE api_services
|
||||||
|
SET last_validation_at = NOW(), last_validation_status = $1, last_validation_message = $2, updated_at = NOW()
|
||||||
|
WHERE id = $3;
|
||||||
|
|
||||||
|
-- name: InsertServiceIncident :exec
|
||||||
|
INSERT INTO incident_events (service_id, code, message, severity, http_status, occurred_at)
|
||||||
|
VALUES ($1, 'SERVICE_VALIDATION_FAILED', $2, 'medium', $3, NOW());
|
||||||
|
|
||||||
|
-- name: InsertGatewayIncident :exec
|
||||||
|
INSERT INTO incident_events (service_id, api_key_id, code, message, severity, http_status, occurred_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, NOW());
|
||||||
|
|
||||||
|
-- name: ListAPServices :many
|
||||||
|
SELECT id, name, slug, upstream_url, route_prefix, enabled,
|
||||||
|
to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS created_at,
|
||||||
|
to_char(updated_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS updated_at
|
||||||
|
FROM api_services
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
|
||||||
|
-- name: InsertAPService :one
|
||||||
|
INSERT INTO api_services (
|
||||||
|
name, slug, upstream_url, route_prefix, health_path,
|
||||||
|
rpm_limit, monthly_quota, created_at, updated_at
|
||||||
|
) VALUES ($1, $2, $3, $4, '/health', $5, $6, NOW(), NOW())
|
||||||
|
RETURNING id;
|
||||||
|
|
||||||
|
-- name: SetAPServiceEnabled :exec
|
||||||
|
UPDATE api_services SET enabled = $1, updated_at = NOW() WHERE id = $2;
|
||||||
|
|
||||||
|
-- name: ListAPKeys :many
|
||||||
|
SELECT id, name, key_prefix, plan, enabled, rpm_limit, monthly_quota,
|
||||||
|
to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS created_at,
|
||||||
|
to_char(updated_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS updated_at
|
||||||
|
FROM api_keys
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
|
||||||
|
-- name: InsertAPKey :one
|
||||||
|
INSERT INTO api_keys (
|
||||||
|
name, key_hash, key_prefix, plan, allowed_service_ids,
|
||||||
|
rpm_limit, monthly_quota, created_at, updated_at
|
||||||
|
) VALUES ($1, $2, $3, $4, '[]', $5, $6, NOW(), NOW())
|
||||||
|
RETURNING id;
|
||||||
|
|
||||||
|
-- name: SetAPKeyEnabled :exec
|
||||||
|
UPDATE api_keys SET enabled = $1, updated_at = NOW() WHERE id = $2;
|
||||||
|
|
||||||
|
-- name: CountAPServices :one
|
||||||
|
SELECT COUNT(*) FROM api_services;
|
||||||
|
|
||||||
|
-- name: CountAPKeys :one
|
||||||
|
SELECT COUNT(*) FROM api_keys;
|
||||||
|
|
||||||
|
-- name: SumUsageCounters :one
|
||||||
|
SELECT COALESCE(SUM(request_count), 0) FROM usage_counters;
|
||||||
|
|
||||||
|
-- name: SumRequestMetricSince :one
|
||||||
|
SELECT COALESCE(SUM(value), 0)::int
|
||||||
|
FROM metrics_timeseries
|
||||||
|
WHERE metric = 'request_total' AND occurred_at >= DATE_TRUNC($1::text, NOW());
|
||||||
|
|
||||||
|
-- name: ListTopAPServices :many
|
||||||
|
SELECT s.id, s.name, COALESCE(SUM(u.request_count), 0) AS total_requests
|
||||||
|
FROM api_services s
|
||||||
|
LEFT JOIN usage_counters u ON u.service_id = s.id
|
||||||
|
GROUP BY s.id, s.name
|
||||||
|
ORDER BY total_requests DESC, s.name ASC
|
||||||
|
LIMIT 10;
|
||||||
|
|
||||||
|
-- name: ListRequestsByDay :many
|
||||||
|
SELECT TO_CHAR(DATE_TRUNC('day', occurred_at), 'YYYY-MM-DD') AS day_bucket, COUNT(*) AS total
|
||||||
|
FROM metrics_timeseries
|
||||||
|
WHERE metric = 'request_total' AND occurred_at >= NOW() - INTERVAL '7 days'
|
||||||
|
GROUP BY DATE_TRUNC('day', occurred_at)
|
||||||
|
ORDER BY DATE_TRUNC('day', occurred_at) ASC;
|
||||||
|
|
||||||
|
-- name: ListIncidentStatusCodes :many
|
||||||
|
SELECT COALESCE(http_status, 0) AS status_code, COALESCE(SUM(count), 0) AS total
|
||||||
|
FROM incident_events
|
||||||
|
WHERE occurred_at >= NOW() - INTERVAL '7 days'
|
||||||
|
GROUP BY COALESCE(http_status, 0)
|
||||||
|
ORDER BY total DESC, status_code ASC;
|
||||||
|
|
||||||
|
-- name: ListClientEventPaths :many
|
||||||
|
SELECT COALESCE((labels_json::jsonb ->> 'path'), 'unknown') AS path, COUNT(*) AS total
|
||||||
|
FROM metrics_timeseries
|
||||||
|
WHERE metric = 'client_event' AND occurred_at >= NOW() - INTERVAL '7 days'
|
||||||
|
GROUP BY COALESCE((labels_json::jsonb ->> 'path'), 'unknown')
|
||||||
|
ORDER BY total DESC, path ASC
|
||||||
|
LIMIT 20;
|
||||||
|
|
||||||
|
-- name: GetAPServiceBySlug :one
|
||||||
|
SELECT id, upstream_url, route_prefix, upstream_auth_header,
|
||||||
|
upstream_auth_value, enabled, rpm_limit, monthly_quota,
|
||||||
|
request_timeout_ms
|
||||||
|
FROM api_services WHERE slug = $1;
|
||||||
|
|
||||||
|
-- name: ListAPKeysByPrefix :many
|
||||||
|
SELECT id, key_hash, enabled, rpm_limit, monthly_quota, allowed_service_ids
|
||||||
|
FROM api_keys WHERE key_prefix = $1;
|
||||||
|
|
||||||
|
-- name: SumUsageByKey :one
|
||||||
|
SELECT COALESCE(SUM(request_count), 0)::bigint FROM usage_counters
|
||||||
|
WHERE api_key_id = $1 AND period_month = $2;
|
||||||
|
|
||||||
|
-- name: SumUsageByService :one
|
||||||
|
SELECT COALESCE(SUM(request_count), 0)::bigint FROM usage_counters
|
||||||
|
WHERE service_id = $1 AND period_month = $2;
|
||||||
|
|
||||||
|
-- name: UpsertUsageCounter :exec
|
||||||
|
INSERT INTO usage_counters (api_key_id, service_id, period_month, request_count, updated_at)
|
||||||
|
VALUES ($1, $2, $3, 1, NOW())
|
||||||
|
ON CONFLICT (api_key_id, service_id, period_month)
|
||||||
|
DO UPDATE SET request_count = usage_counters.request_count + 1, updated_at = NOW();
|
||||||
|
|
||||||
|
-- name: TouchAPKeyLastUsed :exec
|
||||||
|
UPDATE api_keys SET last_used_at = NOW() WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: InsertMetricPoint :exec
|
||||||
|
INSERT INTO metrics_timeseries (metric, value, labels_json, occurred_at)
|
||||||
|
VALUES ('gateway_request', 1, $1, NOW());
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
-- name: InsertAuditLog :exec
|
||||||
|
INSERT INTO audit_logs (id, user_id, resource, resource_id, action, details, severity, category, label, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
|
||||||
|
|
||||||
|
-- name: InsertAuditLogWithRequest :exec
|
||||||
|
INSERT INTO audit_logs (id, user_id, resource, resource_id, action, details, ip_address, user_agent, severity, category, label, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::inet, $8, $9, $10, $11, $12);
|
||||||
|
|
||||||
|
-- name: PruneAuditLogs :execrows
|
||||||
|
DELETE FROM audit_logs WHERE created_at < NOW() - ($1::int * INTERVAL '1 day');
|
||||||
|
|
||||||
|
-- name: ListAuditBackfillBatch :many
|
||||||
|
SELECT id::text, resource, action FROM audit_logs
|
||||||
|
WHERE label = '' AND id > $1::uuid ORDER BY id LIMIT 500;
|
||||||
|
|
||||||
|
-- name: SetAuditLogMetadata :exec
|
||||||
|
UPDATE audit_logs SET severity = $1, category = $2, label = $3 WHERE id = $4::uuid;
|
||||||
|
|
||||||
|
-- name: ListResourceAuditLogs :many
|
||||||
|
SELECT id, COALESCE(user_id::text, '')::text AS user_id, resource,
|
||||||
|
COALESCE(resource_id::text, '')::text AS resource_id, action,
|
||||||
|
COALESCE(details::text, '{}')::text AS details,
|
||||||
|
COALESCE(ip_address::text, '')::text AS ip_address,
|
||||||
|
COALESCE(user_agent, '') AS user_agent, created_at
|
||||||
|
FROM audit_logs
|
||||||
|
WHERE user_id::text = $1 AND resource = $2 AND resource_id::text = $3
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 100;
|
||||||
@@ -35,3 +35,23 @@ FROM users WHERE id = $1;
|
|||||||
UPDATE users
|
UPDATE users
|
||||||
SET name = COALESCE($1::varchar, name), avatar_url = COALESCE($2::varchar, avatar_url)
|
SET name = COALESCE($1::varchar, name), avatar_url = COALESCE($2::varchar, avatar_url)
|
||||||
WHERE id = $3;
|
WHERE id = $3;
|
||||||
|
|
||||||
|
-- name: UpsertAdminUser :one
|
||||||
|
INSERT INTO users (email, password_hash, name, is_admin)
|
||||||
|
VALUES ($1, $2, $3, true)
|
||||||
|
ON CONFLICT (email) DO UPDATE
|
||||||
|
SET is_admin = true, updated_at = NOW()
|
||||||
|
RETURNING id;
|
||||||
|
|
||||||
|
-- name: ListAdminUsers :many
|
||||||
|
SELECT id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin,
|
||||||
|
COALESCE(to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"'), '')::text AS created_at
|
||||||
|
FROM users
|
||||||
|
ORDER BY created_at ASC;
|
||||||
|
|
||||||
|
-- name: SetUserAdmin :execrows
|
||||||
|
UPDATE users SET is_admin = $1, updated_at = NOW() WHERE id = $2;
|
||||||
|
|
||||||
|
-- name: GetAdminUserInfo :one
|
||||||
|
SELECT email, COALESCE(name, '') AS name, is_admin
|
||||||
|
FROM users WHERE id = $1;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- name: UpsertBuild :exec
|
||||||
|
INSERT INTO builds
|
||||||
|
(id, project_id, service_id, status, progress, started_at, completed_at, image_name, image_tag, size, error, log, metadata, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb, $14, $14)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
project_id = EXCLUDED.project_id,
|
||||||
|
service_id = EXCLUDED.service_id,
|
||||||
|
status = EXCLUDED.status,
|
||||||
|
progress = EXCLUDED.progress,
|
||||||
|
started_at = EXCLUDED.started_at,
|
||||||
|
completed_at = EXCLUDED.completed_at,
|
||||||
|
image_name = EXCLUDED.image_name,
|
||||||
|
image_tag = EXCLUDED.image_tag,
|
||||||
|
size = EXCLUDED.size,
|
||||||
|
error = EXCLUDED.error,
|
||||||
|
log = EXCLUDED.log,
|
||||||
|
metadata = EXCLUDED.metadata,
|
||||||
|
updated_at = EXCLUDED.updated_at;
|
||||||
|
|
||||||
|
-- name: GetBuild :one
|
||||||
|
SELECT id, project_id, service_id, status, progress, started_at, completed_at, image_name, image_tag, size, error, log, metadata
|
||||||
|
FROM builds
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: CancelBuild :execrows
|
||||||
|
UPDATE builds
|
||||||
|
SET status = 'cancelled',
|
||||||
|
progress = 100,
|
||||||
|
completed_at = $1,
|
||||||
|
log = COALESCE(log, '') || $2,
|
||||||
|
updated_at = $1
|
||||||
|
WHERE id = $3
|
||||||
|
AND status NOT IN ('success', 'failed', 'cancelled');
|
||||||
@@ -127,3 +127,11 @@ SELECT s.id, s.project_id, s.name,
|
|||||||
s.created_at, s.updated_at
|
s.created_at, s.updated_at
|
||||||
FROM services s
|
FROM services s
|
||||||
WHERE (s.git_branch = $1 OR $1 = '') AND (s.git_repo = $2 OR s.git_repo = $3);
|
WHERE (s.git_branch = $1 OR $1 = '') AND (s.git_repo = $2 OR s.git_repo = $3);
|
||||||
|
|
||||||
|
-- name: GetGitRepoCloneByFullName :one
|
||||||
|
SELECT clone_url, provider_id FROM git_repositories
|
||||||
|
WHERE full_name = $1 AND user_id = $2 LIMIT 1;
|
||||||
|
|
||||||
|
-- name: GetGitRepoProviderByCloneURL :one
|
||||||
|
SELECT provider_id FROM git_repositories
|
||||||
|
WHERE clone_url = $1 AND user_id = $2 LIMIT 1;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
-- name: CountProjects :one
|
||||||
|
SELECT COUNT(*) FROM projects;
|
||||||
|
|
||||||
|
-- name: GetRegistryAuth :one
|
||||||
|
SELECT username, password FROM registries
|
||||||
|
WHERE owner_id::text = $1 AND host = $2;
|
||||||
|
|
||||||
|
-- name: UpdateServiceRuntimeMeta :exec
|
||||||
|
UPDATE services SET port = $1, healthcheck_path = $2 WHERE id = $3;
|
||||||
|
|
||||||
|
-- name: UpdateServiceDeployMeta :exec
|
||||||
|
UPDATE services SET port = $1, healthcheck_path = $2,
|
||||||
|
volumes = COALESCE($3::jsonb, '[]'::jsonb),
|
||||||
|
git_repo = NULLIF($4::text, ''), git_branch = NULLIF($5::text, ''), builder = $6
|
||||||
|
WHERE id = $7;
|
||||||
|
|
||||||
|
-- name: InsertEnvVar :exec
|
||||||
|
INSERT INTO environment_variables (id, service_id, key, value, is_secret, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7);
|
||||||
|
|
||||||
|
-- name: DeleteProjectVariables :exec
|
||||||
|
DELETE FROM project_variables WHERE project_id = $1;
|
||||||
|
|
||||||
|
-- name: InsertProjectVariable :exec
|
||||||
|
INSERT INTO project_variables (id, project_id, key, value, is_secret, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7);
|
||||||
|
|
||||||
|
-- name: GetProjectVariableValue :one
|
||||||
|
SELECT value FROM project_variables WHERE project_id = $1 AND key = $2;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
-- name: ListActiveOperationDeployments :many
|
||||||
|
SELECT d.id, d.service_id, s.name AS service_name, p.name AS project_name, d.status,
|
||||||
|
COALESCE(d.image_name, '') AS image, d.error, d.started_at, d.completed_at, d.created_at
|
||||||
|
FROM deployments d
|
||||||
|
JOIN services s ON s.id = d.service_id
|
||||||
|
JOIN projects p ON p.id = s.project_id
|
||||||
|
WHERE d.status IN ('queued','pending','building','deploying','rolling_back')
|
||||||
|
AND (p.is_approved OR p.owner_id = $1 OR $2::bool
|
||||||
|
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1))
|
||||||
|
ORDER BY d.created_at ASC;
|
||||||
|
|
||||||
|
-- name: ListRecentFailedDeployments :many
|
||||||
|
SELECT d.id, d.service_id, s.name AS service_name, p.name AS project_name, d.status,
|
||||||
|
COALESCE(d.image_name, '') AS image, d.error, d.started_at, d.completed_at, d.created_at
|
||||||
|
FROM deployments d
|
||||||
|
JOIN services s ON s.id = d.service_id
|
||||||
|
JOIN projects p ON p.id = s.project_id
|
||||||
|
WHERE d.status IN ('failed','cancelled','rolled_back')
|
||||||
|
AND d.updated_at > NOW() - INTERVAL '24 hours'
|
||||||
|
AND (p.is_approved OR p.owner_id = $1 OR $2::bool
|
||||||
|
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1))
|
||||||
|
ORDER BY d.updated_at DESC
|
||||||
|
LIMIT 20;
|
||||||
|
|
||||||
|
-- name: ListRecentCronRuns :many
|
||||||
|
SELECT e.id, j.name AS job_name, j.schedule, e.status, e.started_at, e.finished_at, e.error
|
||||||
|
FROM cron_executions e
|
||||||
|
JOIN cron_jobs j ON j.id = e.cron_job_id
|
||||||
|
JOIN projects p ON p.id = j.project_id
|
||||||
|
WHERE e.started_at > NOW() - INTERVAL '24 hours'
|
||||||
|
AND (p.is_approved OR p.owner_id = $1 OR $2::bool
|
||||||
|
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1))
|
||||||
|
ORDER BY e.started_at DESC
|
||||||
|
LIMIT 30;
|
||||||
|
|
||||||
|
-- name: ListRecentOperationBackups :many
|
||||||
|
SELECT b.id, b.database_id, ds.name AS db_name, b.status, b.size, b.created_at, b.completed_at
|
||||||
|
FROM database_backups b
|
||||||
|
JOIN database_services ds ON ds.id = b.database_id
|
||||||
|
WHERE b.created_at > NOW() - INTERVAL '24 hours'
|
||||||
|
AND (ds.user_id = $1 OR $2::bool)
|
||||||
|
ORDER BY b.created_at DESC
|
||||||
|
LIMIT 20;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
-- name: GetServiceSleep :one
|
||||||
|
SELECT COALESCE(sleep_enabled, false) AS sleep_enabled, COALESCE(sleep_idle_minutes, 15) AS sleep_idle_minutes
|
||||||
|
FROM services WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetServiceNodeID :one
|
||||||
|
SELECT node_id FROM services WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetServiceSpread :one
|
||||||
|
SELECT COALESCE(spread, false) FROM services WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetServicePlacementTags :one
|
||||||
|
SELECT placement_tags FROM services WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetServiceTraefikLabels :one
|
||||||
|
SELECT traefik_labels FROM services WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetServiceVolumes :one
|
||||||
|
SELECT volumes FROM services WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetNodeAgentName :one
|
||||||
|
SELECT name FROM node_agents WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetServiceProjectID :one
|
||||||
|
SELECT project_id FROM services WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetServiceProjectAndStatus :one
|
||||||
|
SELECT s.project_id, COALESCE(s.status, '') AS status
|
||||||
|
FROM services s WHERE s.id = $1;
|
||||||
|
|
||||||
|
-- name: GetEnvironmentProjectID :one
|
||||||
|
SELECT project_id FROM environments WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetProjectReadAccess :one
|
||||||
|
SELECT is_approved, owner_id::text FROM projects WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ProjectMemberExists :one
|
||||||
|
SELECT EXISTS(SELECT 1 FROM project_members WHERE project_id = $1 AND user_id = $2);
|
||||||
|
|
||||||
|
-- name: DeleteEnvVarsForService :exec
|
||||||
|
DELETE FROM environment_variables WHERE service_id = $1;
|
||||||
|
|
||||||
|
-- name: FailInterruptedDeployments :exec
|
||||||
|
UPDATE deployments SET status = 'failed', error = 'Interrupted by server restart', completed_at = NOW(), updated_at = NOW()
|
||||||
|
WHERE status IN ('queued', 'pending', 'building', 'deploying', 'rolling_back');
|
||||||
|
|
||||||
|
-- name: ListSecretEnvVars :many
|
||||||
|
SELECT id, value FROM environment_variables WHERE is_secret = TRUE;
|
||||||
|
|
||||||
|
-- name: UpdateEnvVarValue :exec
|
||||||
|
UPDATE environment_variables SET value = $1, updated_at = NOW() WHERE id = $2;
|
||||||
|
|
||||||
|
-- name: ListAgentHealthRows :many
|
||||||
|
SELECT id, COALESCE(name, hostname, id::text) AS name, COALESCE(status, '') AS status,
|
||||||
|
COALESCE(last_heartbeat < $1, TRUE)::bool AS stale, auto_prune
|
||||||
|
FROM node_agents;
|
||||||
|
|
||||||
|
-- name: SetAgentStatus :exec
|
||||||
|
UPDATE node_agents SET status = $1, updated_at = NOW() WHERE id = $2;
|
||||||
|
|
||||||
|
-- name: GetServiceNameProjectPort :one
|
||||||
|
SELECT name, project_id::text, COALESCE(port, 0)::int AS port
|
||||||
|
FROM services WHERE id::text = $1;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
-- name: GetAppSetting :one
|
||||||
|
SELECT value FROM app_settings WHERE key = $1;
|
||||||
|
|
||||||
|
-- name: UpsertAppSetting :exec
|
||||||
|
INSERT INTO app_settings (key, value, is_secret, updated_at)
|
||||||
|
VALUES ($1, $2, $3, NOW())
|
||||||
|
ON CONFLICT (key) DO UPDATE
|
||||||
|
SET value = EXCLUDED.value, is_secret = EXCLUDED.is_secret, updated_at = NOW();
|
||||||
|
|
||||||
|
-- name: UpsertAppSettingValue :exec
|
||||||
|
INSERT INTO app_settings (key, value, updated_at) VALUES ($1, $2, NOW())
|
||||||
|
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW();
|
||||||
|
|
||||||
|
-- name: DeleteAppSetting :exec
|
||||||
|
DELETE FROM app_settings WHERE key = $1;
|
||||||
|
|
||||||
|
-- name: GetUserIsAdmin :one
|
||||||
|
SELECT is_admin FROM users WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ListAdminUserIDs :many
|
||||||
|
SELECT id FROM users WHERE is_admin;
|
||||||
@@ -144,6 +144,17 @@ CREATE TABLE environment_variables (
|
|||||||
UNIQUE(service_id, key)
|
UNIQUE(service_id, key)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE project_variables (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
|
key VARCHAR(255) NOT NULL,
|
||||||
|
value TEXT NOT NULL DEFAULT '',
|
||||||
|
is_secret BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(project_id, key)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE service_templates (
|
CREATE TABLE service_templates (
|
||||||
id VARCHAR(50) PRIMARY KEY,
|
id VARCHAR(50) PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
@@ -590,3 +601,112 @@ CREATE TABLE vulnerabilities (
|
|||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE app_settings (
|
||||||
|
key VARCHAR(255) PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL DEFAULT '',
|
||||||
|
is_secret BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE audit_logs (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
user_id UUID,
|
||||||
|
action VARCHAR(100) NOT NULL,
|
||||||
|
resource VARCHAR(100) NOT NULL,
|
||||||
|
resource_id UUID,
|
||||||
|
details JSONB DEFAULT '{}',
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
success BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
severity TEXT NOT NULL DEFAULT 'info',
|
||||||
|
category TEXT NOT NULL DEFAULT '',
|
||||||
|
label TEXT NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE builds (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
project_id VARCHAR(255),
|
||||||
|
service_id VARCHAR(255),
|
||||||
|
status VARCHAR(50) NOT NULL DEFAULT 'pending',
|
||||||
|
progress INTEGER NOT NULL DEFAULT 0,
|
||||||
|
started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
completed_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
image_name VARCHAR(500) NOT NULL DEFAULT '',
|
||||||
|
image_tag VARCHAR(200) NOT NULL DEFAULT '',
|
||||||
|
size BIGINT NOT NULL DEFAULT 0,
|
||||||
|
error TEXT,
|
||||||
|
log TEXT NOT NULL DEFAULT '',
|
||||||
|
metadata JSONB NOT NULL DEFAULT '{}',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- APwhy API gateway (api_services/api_keys + usage/metrics/incident planes)
|
||||||
|
CREATE TABLE api_services (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
slug TEXT NOT NULL UNIQUE,
|
||||||
|
upstream_url TEXT NOT NULL,
|
||||||
|
route_prefix TEXT NOT NULL UNIQUE,
|
||||||
|
health_path TEXT NOT NULL DEFAULT '/health',
|
||||||
|
upstream_auth_header TEXT,
|
||||||
|
upstream_auth_value TEXT,
|
||||||
|
internal_token TEXT,
|
||||||
|
enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
|
rpm_limit INTEGER,
|
||||||
|
monthly_quota INTEGER,
|
||||||
|
request_timeout_ms INTEGER DEFAULT 8000,
|
||||||
|
last_validation_at TIMESTAMP WITHOUT TIME ZONE,
|
||||||
|
last_validation_status TEXT,
|
||||||
|
last_validation_message TEXT,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE api_keys (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
key_hash TEXT NOT NULL,
|
||||||
|
key_prefix TEXT NOT NULL,
|
||||||
|
plan TEXT NOT NULL DEFAULT 'free',
|
||||||
|
allowed_service_ids TEXT NOT NULL DEFAULT '[]',
|
||||||
|
enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
|
rpm_limit INTEGER DEFAULT 60,
|
||||||
|
monthly_quota INTEGER DEFAULT 1000,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
last_used_at TIMESTAMP WITHOUT TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE usage_counters (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
api_key_id UUID NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
|
||||||
|
service_id UUID NOT NULL REFERENCES api_services(id) ON DELETE CASCADE,
|
||||||
|
period_month TEXT NOT NULL,
|
||||||
|
request_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (api_key_id, service_id, period_month)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE metrics_timeseries (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
metric TEXT NOT NULL,
|
||||||
|
value REAL NOT NULL,
|
||||||
|
labels_json TEXT,
|
||||||
|
occurred_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE incident_events (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
service_id UUID REFERENCES api_services(id) ON DELETE SET NULL,
|
||||||
|
api_key_id UUID,
|
||||||
|
code TEXT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
severity TEXT NOT NULL DEFAULT 'medium',
|
||||||
|
http_status INTEGER,
|
||||||
|
count INTEGER NOT NULL DEFAULT 1,
|
||||||
|
occurred_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
Reference in new issue
Block a user