Enable auto default sync channels on agent activate, bind-code/phone confirm flows, publish ALTER, and align admin/yuheng docs with the production bind path.
230 lines
5.7 KiB
Go
230 lines
5.7 KiB
Go
package meta
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"aijianzhan/platform/internal/blueprint"
|
|
|
|
"github.com/lib/pq"
|
|
)
|
|
|
|
type PostgresStore struct {
|
|
DB *sql.DB
|
|
}
|
|
|
|
func NewPostgresStore(db *sql.DB) *PostgresStore {
|
|
return &PostgresStore{DB: db}
|
|
}
|
|
|
|
func (s *PostgresStore) GetBySlug(ctx context.Context, tenantID int64, slug string) (*AppRecord, error) {
|
|
const q = `
|
|
SELECT app_id, tenant_id, slug, name, schema_name, COALESCE(database_name,''), engine, status,
|
|
blueprint_json, ddl_json, endpoints_json, error_msg, created_at, updated_at
|
|
FROM platform_meta.tenant_apps
|
|
WHERE tenant_id = $1 AND slug = $2
|
|
LIMIT 1`
|
|
row := s.DB.QueryRowContext(ctx, q, tenantID, slug)
|
|
rec, err := scanApp(row)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, fmt.Errorf("app not found")
|
|
}
|
|
return rec, err
|
|
}
|
|
|
|
func (s *PostgresStore) FindPublishedBySlug(ctx context.Context, slug string) (*AppRecord, error) {
|
|
const q = `
|
|
SELECT app_id, tenant_id, slug, name, schema_name, COALESCE(database_name,''), engine, status,
|
|
blueprint_json, ddl_json, endpoints_json, error_msg, created_at, updated_at
|
|
FROM platform_meta.tenant_apps
|
|
WHERE slug = $1 AND status = $2
|
|
ORDER BY updated_at DESC
|
|
LIMIT 1`
|
|
row := s.DB.QueryRowContext(ctx, q, slug, string(StatusPublished))
|
|
rec, err := scanApp(row)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, fmt.Errorf("app not found")
|
|
}
|
|
return rec, err
|
|
}
|
|
|
|
func (s *PostgresStore) ListByTenant(ctx context.Context, tenantID int64) ([]AppSummary, error) {
|
|
const q = `
|
|
SELECT app_id, tenant_id, slug, name, schema_name, COALESCE(database_name,''), engine, status,
|
|
blueprint_json, ddl_json, endpoints_json, error_msg, created_at, updated_at
|
|
FROM platform_meta.tenant_apps
|
|
WHERE tenant_id = $1
|
|
ORDER BY updated_at DESC`
|
|
rows, err := s.DB.QueryContext(ctx, q, tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := make([]AppSummary, 0)
|
|
for rows.Next() {
|
|
rec, err := scanApp(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, summarizeApp(rec))
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *PostgresStore) ListPublishedApps(ctx context.Context) ([]*AppRecord, error) {
|
|
const q = `
|
|
SELECT app_id, tenant_id, slug, name, schema_name, COALESCE(database_name,''), engine, status,
|
|
blueprint_json, ddl_json, endpoints_json, error_msg, created_at, updated_at
|
|
FROM platform_meta.tenant_apps
|
|
WHERE status = $1
|
|
ORDER BY tenant_id, slug`
|
|
rows, err := s.DB.QueryContext(ctx, q, string(StatusPublished))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := make([]*AppRecord, 0)
|
|
for rows.Next() {
|
|
rec, err := scanApp(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, rec)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *PostgresStore) Save(ctx context.Context, app *AppRecord) error {
|
|
if app == nil {
|
|
return fmt.Errorf("app is nil")
|
|
}
|
|
if app.Blueprint == nil {
|
|
return fmt.Errorf("blueprint is nil")
|
|
}
|
|
bpRaw, err := json.Marshal(app.Blueprint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ddlRaw, err := json.Marshal(app.DDL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
epRaw, err := json.Marshal(app.Endpoints)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if app.CreatedAt.IsZero() {
|
|
app.CreatedAt = time.Now().UTC()
|
|
}
|
|
app.UpdatedAt = time.Now().UTC()
|
|
|
|
const q = `
|
|
INSERT INTO platform_meta.tenant_apps (
|
|
app_id, tenant_id, slug, name, schema_name, database_name, engine, status,
|
|
blueprint_json, ddl_json, endpoints_json, error_msg, created_at, updated_at
|
|
) VALUES (
|
|
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14
|
|
)
|
|
ON CONFLICT (tenant_id, slug) DO UPDATE SET
|
|
name = EXCLUDED.name,
|
|
schema_name = EXCLUDED.schema_name,
|
|
database_name = EXCLUDED.database_name,
|
|
engine = EXCLUDED.engine,
|
|
status = EXCLUDED.status,
|
|
blueprint_json = EXCLUDED.blueprint_json,
|
|
ddl_json = EXCLUDED.ddl_json,
|
|
endpoints_json = EXCLUDED.endpoints_json,
|
|
error_msg = EXCLUDED.error_msg,
|
|
updated_at = EXCLUDED.updated_at,
|
|
app_id = platform_meta.tenant_apps.app_id,
|
|
created_at = platform_meta.tenant_apps.created_at
|
|
RETURNING app_id, created_at, updated_at`
|
|
|
|
err = s.DB.QueryRowContext(ctx, q,
|
|
app.AppID,
|
|
app.TenantID,
|
|
app.Slug,
|
|
app.Name,
|
|
app.SchemaName,
|
|
app.DatabaseName,
|
|
app.Engine,
|
|
string(app.Status),
|
|
bpRaw,
|
|
ddlRaw,
|
|
epRaw,
|
|
app.Error,
|
|
app.CreatedAt,
|
|
app.UpdatedAt,
|
|
).Scan(&app.AppID, &app.CreatedAt, &app.UpdatedAt)
|
|
if err != nil {
|
|
return fmt.Errorf("meta save: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *PostgresStore) ResolveResource(ctx context.Context, tenantID int64, slug, resource string) (*ResourceRef, error) {
|
|
app, err := s.GetBySlug(ctx, tenantID, slug)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resolveResource(app, resource)
|
|
}
|
|
|
|
type scannable interface {
|
|
Scan(dest ...any) error
|
|
}
|
|
|
|
func scanApp(row scannable) (*AppRecord, error) {
|
|
var (
|
|
rec AppRecord
|
|
status string
|
|
bpRaw, ddlRaw, epRaw []byte
|
|
errMsg sql.NullString
|
|
)
|
|
err := row.Scan(
|
|
&rec.AppID,
|
|
&rec.TenantID,
|
|
&rec.Slug,
|
|
&rec.Name,
|
|
&rec.SchemaName,
|
|
&rec.DatabaseName,
|
|
&rec.Engine,
|
|
&status,
|
|
&bpRaw,
|
|
&ddlRaw,
|
|
&epRaw,
|
|
&errMsg,
|
|
&rec.CreatedAt,
|
|
&rec.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rec.Status = AppStatus(status)
|
|
if errMsg.Valid {
|
|
rec.Error = errMsg.String
|
|
}
|
|
var bp blueprint.Blueprint
|
|
if err := json.Unmarshal(bpRaw, &bp); err != nil {
|
|
return nil, fmt.Errorf("blueprint json: %w", err)
|
|
}
|
|
rec.Blueprint = &bp
|
|
if len(ddlRaw) > 0 {
|
|
_ = json.Unmarshal(ddlRaw, &rec.DDL)
|
|
}
|
|
if len(epRaw) > 0 {
|
|
_ = json.Unmarshal(epRaw, &rec.Endpoints)
|
|
}
|
|
return &rec, nil
|
|
}
|
|
|
|
// IsUniqueViolation 便于上层识别冲突(预留)。
|
|
func IsUniqueViolation(err error) bool {
|
|
var pqErr *pq.Error
|
|
return errors.As(err, &pqErr) && pqErr.Code == "23505"
|
|
}
|