feat(server): add brand kit management and project binding
Add a brand kit domain with memory and postgres stores, a BrandKitService for CRUD and resolution, and REST endpoints to list, upsert, and delete brand kits. Projects can bind a brand kit whose resolved context and reference images feed into the agent's long-term memory and design prompts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"img_infinite_canvas/internal/domain/brandkit"
|
||||
)
|
||||
|
||||
type BrandKitStore struct {
|
||||
mu sync.RWMutex
|
||||
kits map[string]brandkit.Kit
|
||||
}
|
||||
|
||||
func NewBrandKitStore() *BrandKitStore {
|
||||
return &BrandKitStore{kits: make(map[string]brandkit.Kit)}
|
||||
}
|
||||
|
||||
func (s *BrandKitStore) List(ctx context.Context, userID string) ([]brandkit.Kit, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
items := make([]brandkit.Kit, 0)
|
||||
for _, kit := range s.kits {
|
||||
if kit.UserID == userID {
|
||||
items = append(items, brandkit.Clone(kit))
|
||||
}
|
||||
}
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if items[i].UpdatedAt.Equal(items[j].UpdatedAt) {
|
||||
return items[i].ID < items[j].ID
|
||||
}
|
||||
return items[i].UpdatedAt.After(items[j].UpdatedAt)
|
||||
})
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *BrandKitStore) Get(ctx context.Context, userID string, id string) (brandkit.Kit, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return brandkit.Kit{}, err
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
kit, ok := s.kits[id]
|
||||
if !ok || kit.UserID != userID {
|
||||
return brandkit.Kit{}, brandkit.ErrNotFound
|
||||
}
|
||||
return brandkit.Clone(kit), nil
|
||||
}
|
||||
|
||||
func (s *BrandKitStore) GetDefault(ctx context.Context, userID string) (brandkit.Kit, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return brandkit.Kit{}, err
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, kit := range s.kits {
|
||||
if kit.UserID == userID && kit.IsDefault {
|
||||
return brandkit.Clone(kit), nil
|
||||
}
|
||||
}
|
||||
return brandkit.Kit{}, brandkit.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *BrandKitStore) Upsert(ctx context.Context, kit brandkit.Kit) (brandkit.Kit, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return brandkit.Kit{}, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if existing, ok := s.kits[kit.ID]; ok && existing.UserID != kit.UserID {
|
||||
return brandkit.Kit{}, brandkit.ErrConflict
|
||||
}
|
||||
if kit.IsDefault {
|
||||
for id, candidate := range s.kits {
|
||||
if candidate.UserID == kit.UserID && candidate.IsDefault && id != kit.ID {
|
||||
candidate.IsDefault = false
|
||||
s.kits[id] = candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
s.kits[kit.ID] = brandkit.Clone(kit)
|
||||
return brandkit.Clone(kit), nil
|
||||
}
|
||||
|
||||
func (s *BrandKitStore) Delete(ctx context.Context, userID string, id string) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
kit, ok := s.kits[id]
|
||||
if !ok || kit.UserID != userID {
|
||||
return brandkit.ErrNotFound
|
||||
}
|
||||
delete(s.kits, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BrandKitStore) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -38,13 +38,14 @@ func (r *ProjectRepository) List(ctx context.Context) ([]design.ProjectSummary,
|
||||
continue
|
||||
}
|
||||
items = append(items, design.ProjectSummary{
|
||||
ID: project.ID,
|
||||
UserID: project.UserID,
|
||||
Title: project.Title,
|
||||
Brief: project.Brief,
|
||||
Status: project.Status,
|
||||
Thumbnail: project.Thumbnail,
|
||||
UpdatedAt: project.UpdatedAt,
|
||||
ID: project.ID,
|
||||
UserID: project.UserID,
|
||||
Title: project.Title,
|
||||
Brief: project.Brief,
|
||||
Status: project.Status,
|
||||
Thumbnail: project.Thumbnail,
|
||||
BrandKitID: project.BrandKitID,
|
||||
UpdatedAt: project.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
@@ -207,6 +208,7 @@ func repositoryUserID(ctx context.Context, userID string) string {
|
||||
|
||||
func cloneProject(project design.Project) design.Project {
|
||||
project.UserID = repositoryUserID(context.Background(), project.UserID)
|
||||
project.BrandImages = append([]string(nil), project.BrandImages...)
|
||||
project.Nodes = append([]design.Node(nil), project.Nodes...)
|
||||
project.Connections = append([]design.Connection(nil), project.Connections...)
|
||||
project.Messages = append([]design.Message(nil), project.Messages...)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package piagent
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
)
|
||||
|
||||
func TestBrandKitContextReachesPlannerAndDirectImagePrompts(t *testing.T) {
|
||||
project := design.Project{
|
||||
Title: "Campaign",
|
||||
Brief: "Launch visual",
|
||||
BrandKitID: "brand-kit-alpha",
|
||||
BrandContext: "Primary color: Signal Green #28C76F. Typography: Avenir Next.",
|
||||
}
|
||||
planner := projectBriefContext(project)
|
||||
direct := directImagePrompt(design.AgentRequest{Project: project, Prompt: "Create a product hero"})
|
||||
fallback := fallbackImagePrompt(design.AgentRequest{
|
||||
Project: project,
|
||||
Prompt: "Create a product hero",
|
||||
TaskPlan: design.AgentTaskPlan{ImageTasks: []design.AgentImageTask{
|
||||
{Title: "Hero", Brief: "Centered product"},
|
||||
}},
|
||||
}, 0)
|
||||
for name, value := range map[string]string{"planner": planner, "direct": direct, "fallback": fallback} {
|
||||
if !strings.Contains(value, "Signal Green #28C76F") || !strings.Contains(value, "Avenir Next") {
|
||||
t.Fatalf("%s prompt omitted Brand Kit context: %s", name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrandKitImagesFollowUserReferences(t *testing.T) {
|
||||
req := design.AgentRequest{
|
||||
Project: design.Project{BrandImages: []string{
|
||||
"data:image/png;base64,YnJhbmQtbG9nbw==",
|
||||
"data:image/webp;base64,YnJhbmQtcmVmZXJlbmNl",
|
||||
}},
|
||||
Messages: []design.AgentChatMessage{{Contents: []design.AgentContent{{Type: "image", ImageURL: "https://example.com/user-reference.png"}}}},
|
||||
}
|
||||
images := referenceImageURLs(req)
|
||||
if len(images) != 3 {
|
||||
t.Fatalf("expected user and Brand Kit references, got %#v", images)
|
||||
}
|
||||
if images[0] != "https://example.com/user-reference.png" || images[1] != req.Project.BrandImages[0] || images[2] != req.Project.BrandImages[1] {
|
||||
t.Fatalf("expected user references before Brand Kit images, got %#v", images)
|
||||
}
|
||||
}
|
||||
@@ -843,6 +843,11 @@ func projectBriefContext(project design.Project) string {
|
||||
builder.WriteString(project.Brief)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
if strings.TrimSpace(project.BrandContext) != "" {
|
||||
builder.WriteString("Brand Kit context:\n")
|
||||
builder.WriteString(strings.TrimSpace(project.BrandContext))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("Canvas nodes: %d", len(project.Nodes)))
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ func directImagePlan(req design.AgentRequest) design.AgentPlan {
|
||||
|
||||
func directImagePrompts(req design.AgentRequest) []design.AgentImagePrompt {
|
||||
count := plannedGenerationCount(req)
|
||||
prompt := strings.TrimSpace(req.Prompt)
|
||||
prompt := directImagePrompt(req)
|
||||
if prompt == "" {
|
||||
prompt = fallbackDirectImagePrompt(req)
|
||||
}
|
||||
@@ -257,7 +257,7 @@ func directImageTasksSharePrompt(req design.AgentRequest) bool {
|
||||
}
|
||||
|
||||
func directImageBatchPrompt(req design.AgentRequest) design.AgentImagePrompt {
|
||||
prompt := strings.TrimSpace(req.Prompt)
|
||||
prompt := directImagePrompt(req)
|
||||
if prompt == "" {
|
||||
prompt = fallbackDirectImagePrompt(req)
|
||||
}
|
||||
@@ -614,6 +614,12 @@ func referenceImageURLs(req design.AgentRequest) []string {
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, image := range req.Project.BrandImages {
|
||||
add(image)
|
||||
if len(urls) >= 16 {
|
||||
return urls[:16]
|
||||
}
|
||||
}
|
||||
if len(urls) > 16 {
|
||||
return urls[:16]
|
||||
}
|
||||
@@ -662,9 +668,9 @@ func fallbackImagePrompt(req design.AgentRequest, index int) string {
|
||||
task := req.TaskPlan.ImageTasks[index]
|
||||
if brief := strings.TrimSpace(task.Brief); brief != "" {
|
||||
if title := strings.TrimSpace(task.Title); title != "" && !strings.Contains(brief, title) {
|
||||
return title + ": " + brief
|
||||
return imagePromptWithBrandKit(title+": "+brief, req)
|
||||
}
|
||||
return brief
|
||||
return imagePromptWithBrandKit(brief, req)
|
||||
}
|
||||
}
|
||||
return fallbackDirectImagePrompt(req)
|
||||
@@ -1064,6 +1070,9 @@ func fallbackDirectImagePrompt(req design.AgentRequest) string {
|
||||
if brief := strings.TrimSpace(req.Project.Brief); brief != "" {
|
||||
lines = append(lines, "Project brief: "+truncateRunes(brief, 180))
|
||||
}
|
||||
if brandContext := strings.TrimSpace(req.Project.BrandContext); brandContext != "" {
|
||||
lines = append(lines, "Binding Brand Kit: "+truncateRunes(brandContext, 520))
|
||||
}
|
||||
if memory := compactFallbackMemory(req.Memory); memory != "" {
|
||||
lines = append(lines, "Relevant memory: "+memory)
|
||||
}
|
||||
@@ -1080,6 +1089,19 @@ func fallbackDirectImagePrompt(req design.AgentRequest) string {
|
||||
return compactImageGenerationPrompt(strings.Join(lines, "\n"), imageGenerationFallbackMaxRunes)
|
||||
}
|
||||
|
||||
func directImagePrompt(req design.AgentRequest) string {
|
||||
return imagePromptWithBrandKit(req.Prompt, req)
|
||||
}
|
||||
|
||||
func imagePromptWithBrandKit(prompt string, req design.AgentRequest) string {
|
||||
prompt = strings.TrimSpace(prompt)
|
||||
brandContext := strings.TrimSpace(req.Project.BrandContext)
|
||||
if brandContext == "" {
|
||||
return prompt
|
||||
}
|
||||
return strings.TrimSpace(prompt + "\n\nBinding Brand Kit requirements:\n" + brandContext)
|
||||
}
|
||||
|
||||
func compactFallbackMemory(memory design.AgentMemory) string {
|
||||
if memory.IsZero() {
|
||||
return ""
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"img_infinite_canvas/internal/domain/brandkit"
|
||||
sqlcdb "img_infinite_canvas/internal/infrastructure/postgres/db"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type BrandKitStore struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewBrandKitStore(ctx context.Context, dataSource string) (*BrandKitStore, error) {
|
||||
pool, err := pgxpool.New(ctx, dataSource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := pool.Exec(ctx, schemaSQL); err != nil {
|
||||
pool.Close()
|
||||
return nil, err
|
||||
}
|
||||
return &BrandKitStore{pool: pool}, nil
|
||||
}
|
||||
|
||||
func (s *BrandKitStore) List(ctx context.Context, userID string) ([]brandkit.Kit, error) {
|
||||
rows, err := sqlcdb.New(s.pool).ListBrandKits(ctx, toPGUUID(userID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]brandkit.Kit, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, mapBrandKit(row))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *BrandKitStore) Get(ctx context.Context, userID string, id string) (brandkit.Kit, error) {
|
||||
row, err := sqlcdb.New(s.pool).GetBrandKit(ctx, sqlcdb.GetBrandKitParams{ID: id, UserID: toPGUUID(userID)})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return brandkit.Kit{}, brandkit.ErrNotFound
|
||||
}
|
||||
return brandkit.Kit{}, err
|
||||
}
|
||||
return mapBrandKit(row), nil
|
||||
}
|
||||
|
||||
func (s *BrandKitStore) GetDefault(ctx context.Context, userID string) (brandkit.Kit, error) {
|
||||
row, err := sqlcdb.New(s.pool).GetDefaultBrandKit(ctx, toPGUUID(userID))
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return brandkit.Kit{}, brandkit.ErrNotFound
|
||||
}
|
||||
return brandkit.Kit{}, err
|
||||
}
|
||||
return mapBrandKit(row), nil
|
||||
}
|
||||
|
||||
func (s *BrandKitStore) Upsert(ctx context.Context, kit brandkit.Kit) (brandkit.Kit, error) {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return brandkit.Kit{}, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
q := sqlcdb.New(s.pool).WithTx(tx)
|
||||
if kit.IsDefault {
|
||||
if err := q.ClearDefaultBrandKits(ctx, sqlcdb.ClearDefaultBrandKitsParams{UserID: toPGUUID(kit.UserID), ID: kit.ID}); err != nil {
|
||||
return brandkit.Kit{}, err
|
||||
}
|
||||
}
|
||||
row, err := q.UpsertBrandKit(ctx, sqlcdb.UpsertBrandKitParams{
|
||||
ID: kit.ID,
|
||||
UserID: toPGUUID(kit.UserID),
|
||||
Name: kit.Name,
|
||||
Document: append([]byte(nil), kit.Document...),
|
||||
IsDefault: kit.IsDefault,
|
||||
CreatedAt: pgtype.Timestamptz{Time: kit.CreatedAt, Valid: true},
|
||||
UpdatedAt: pgtype.Timestamptz{Time: kit.UpdatedAt, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return brandkit.Kit{}, brandkit.ErrNotFound
|
||||
}
|
||||
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
||||
return brandkit.Kit{}, brandkit.ErrConflict
|
||||
}
|
||||
return brandkit.Kit{}, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return brandkit.Kit{}, err
|
||||
}
|
||||
return mapBrandKit(row), nil
|
||||
}
|
||||
|
||||
func (s *BrandKitStore) Delete(ctx context.Context, userID string, id string) error {
|
||||
_, err := sqlcdb.New(s.pool).DeleteBrandKit(ctx, sqlcdb.DeleteBrandKitParams{ID: id, UserID: toPGUUID(userID)})
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return brandkit.ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *BrandKitStore) Close() error {
|
||||
if s != nil && s.pool != nil {
|
||||
s.pool.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapBrandKit(row sqlcdb.BrandKit) brandkit.Kit {
|
||||
return brandkit.Kit{
|
||||
ID: row.ID,
|
||||
UserID: fromPGUUID(row.UserID),
|
||||
Name: row.Name,
|
||||
Document: append([]byte(nil), row.Document...),
|
||||
IsDefault: row.IsDefault,
|
||||
CreatedAt: row.CreatedAt.Time,
|
||||
UpdatedAt: row.UpdatedAt.Time,
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,29 @@ type AgentMessage struct {
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type AuthVerificationCode struct {
|
||||
ID string `json:"id"`
|
||||
Region string `json:"region"`
|
||||
Channel string `json:"channel"`
|
||||
Target string `json:"target"`
|
||||
Purpose string `json:"purpose"`
|
||||
CodeHash string `json:"code_hash"`
|
||||
Attempts int32 `json:"attempts"`
|
||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||
ConsumedAt pgtype.Timestamptz `json:"consumed_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type BrandKit struct {
|
||||
ID string `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Document []byte `json:"document"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CanvasConnection struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
@@ -81,6 +104,14 @@ type CanvasSnapshot struct {
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type MockupBlob struct {
|
||||
DataID string `json:"data_id"`
|
||||
Width int32 `json:"width"`
|
||||
Height int32 `json:"height"`
|
||||
Data []byte `json:"data"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type OutboxEvent struct {
|
||||
ID string `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
@@ -105,17 +136,61 @@ type Project struct {
|
||||
Version string `json:"version"`
|
||||
SnapshotID string `json:"snapshot_id"`
|
||||
LastThreadID string `json:"last_thread_id"`
|
||||
BrandKitID pgtype.Text `json:"brand_kit_id"`
|
||||
ViewportX float64 `json:"viewport_x"`
|
||||
ViewportY float64 `json:"viewport_y"`
|
||||
ViewportK float64 `json:"viewport_k"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
type ProjectShareMember struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
IdentifierType string `json:"identifier_type"`
|
||||
IdentifierHash []byte `json:"identifier_hash"`
|
||||
IdentifierCiphertext []byte `json:"identifier_ciphertext"`
|
||||
Permission string `json:"permission"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ProjectSharePolicy struct {
|
||||
ProjectID string `json:"project_id"`
|
||||
OwnerID pgtype.UUID `json:"owner_id"`
|
||||
TokenHash []byte `json:"token_hash"`
|
||||
TokenCiphertext []byte `json:"token_ciphertext"`
|
||||
LinkPermission string `json:"link_permission"`
|
||||
PolicyVersion int64 `json:"policy_version"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ProjectShareVisit struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
VisitorHash []byte `json:"visitor_hash"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
VisitCount int64 `json:"visit_count"`
|
||||
FirstSeenAt pgtype.Timestamptz `json:"first_seen_at"`
|
||||
LastSeenAt pgtype.Timestamptz `json:"last_seen_at"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
Region string `json:"region"`
|
||||
PhoneCountryCode string `json:"phone_country_code"`
|
||||
Phone string `json:"phone"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
WechatOpenID string `json:"wechat_open_id"`
|
||||
WechatUnionID string `json:"wechat_union_id"`
|
||||
GoogleSubject string `json:"google_subject"`
|
||||
PhoneVerifiedAt pgtype.Timestamptz `json:"phone_verified_at"`
|
||||
EmailVerifiedAt pgtype.Timestamptz `json:"email_verified_at"`
|
||||
LastLoginAt pgtype.Timestamptz `json:"last_login_at"`
|
||||
}
|
||||
|
||||
@@ -11,15 +11,20 @@ import (
|
||||
)
|
||||
|
||||
type Querier interface {
|
||||
ClearDefaultBrandKits(ctx context.Context, arg ClearDefaultBrandKitsParams) error
|
||||
CreateCanvasSnapshot(ctx context.Context, arg CreateCanvasSnapshotParams) error
|
||||
CreateConnection(ctx context.Context, arg CreateConnectionParams) error
|
||||
CreateMessage(ctx context.Context, arg CreateMessageParams) error
|
||||
DeleteBrandKit(ctx context.Context, arg DeleteBrandKitParams) (string, error)
|
||||
DeleteConnectionsByProject(ctx context.Context, arg DeleteConnectionsByProjectParams) error
|
||||
DeleteMessagesByProject(ctx context.Context, arg DeleteMessagesByProjectParams) error
|
||||
DeleteNodesByProject(ctx context.Context, arg DeleteNodesByProjectParams) error
|
||||
DeleteProject(ctx context.Context, arg DeleteProjectParams) error
|
||||
GetBrandKit(ctx context.Context, arg GetBrandKitParams) (BrandKit, error)
|
||||
GetCanvasSnapshot(ctx context.Context, arg GetCanvasSnapshotParams) (CanvasSnapshot, error)
|
||||
GetDefaultBrandKit(ctx context.Context, userID pgtype.UUID) (BrandKit, error)
|
||||
GetProject(ctx context.Context, arg GetProjectParams) (Project, error)
|
||||
ListBrandKits(ctx context.Context, userID pgtype.UUID) ([]BrandKit, error)
|
||||
ListConnectionsByProject(ctx context.Context, arg ListConnectionsByProjectParams) ([]CanvasConnection, error)
|
||||
ListMessagesByProject(ctx context.Context, arg ListMessagesByProjectParams) ([]AgentMessage, error)
|
||||
ListNodesByProject(ctx context.Context, arg ListNodesByProjectParams) ([]CanvasNode, error)
|
||||
@@ -27,6 +32,7 @@ type Querier interface {
|
||||
ListProjectsPage(ctx context.Context, arg ListProjectsPageParams) ([]ListProjectsPageRow, error)
|
||||
LockProject(ctx context.Context, arg LockProjectParams) (LockProjectRow, error)
|
||||
UpdateProjectCanvasSnapshot(ctx context.Context, arg UpdateProjectCanvasSnapshotParams) error
|
||||
UpsertBrandKit(ctx context.Context, arg UpsertBrandKitParams) (BrandKit, error)
|
||||
UpsertCanvasNode(ctx context.Context, arg UpsertCanvasNodeParams) error
|
||||
UpsertProject(ctx context.Context, arg UpsertProjectParams) error
|
||||
}
|
||||
|
||||
@@ -11,6 +11,22 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const clearDefaultBrandKits = `-- name: ClearDefaultBrandKits :exec
|
||||
UPDATE brand_kits
|
||||
SET is_default = FALSE
|
||||
WHERE user_id = $1 AND is_default = TRUE AND id <> $2
|
||||
`
|
||||
|
||||
type ClearDefaultBrandKitsParams struct {
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) ClearDefaultBrandKits(ctx context.Context, arg ClearDefaultBrandKitsParams) error {
|
||||
_, err := q.db.Exec(ctx, clearDefaultBrandKits, arg.UserID, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const createCanvasSnapshot = `-- name: CreateCanvasSnapshot :exec
|
||||
INSERT INTO canvas_snapshots (id, project_id, user_id, version, canvas, viewport_json, nodes_json, connections_json, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
@@ -140,6 +156,24 @@ func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) er
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteBrandKit = `-- name: DeleteBrandKit :one
|
||||
DELETE FROM brand_kits
|
||||
WHERE id = $1 AND user_id = $2
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type DeleteBrandKitParams struct {
|
||||
ID string `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteBrandKit(ctx context.Context, arg DeleteBrandKitParams) (string, error) {
|
||||
row := q.db.QueryRow(ctx, deleteBrandKit, arg.ID, arg.UserID)
|
||||
var id string
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const deleteConnectionsByProject = `-- name: DeleteConnectionsByProject :exec
|
||||
DELETE FROM canvas_connections
|
||||
WHERE project_id = $1 AND user_id = $2
|
||||
@@ -200,6 +234,32 @@ func (q *Queries) DeleteProject(ctx context.Context, arg DeleteProjectParams) er
|
||||
return err
|
||||
}
|
||||
|
||||
const getBrandKit = `-- name: GetBrandKit :one
|
||||
SELECT id, user_id, name, document, is_default, created_at, updated_at
|
||||
FROM brand_kits
|
||||
WHERE id = $1 AND user_id = $2
|
||||
`
|
||||
|
||||
type GetBrandKitParams struct {
|
||||
ID string `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetBrandKit(ctx context.Context, arg GetBrandKitParams) (BrandKit, error) {
|
||||
row := q.db.QueryRow(ctx, getBrandKit, arg.ID, arg.UserID)
|
||||
var i BrandKit
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Name,
|
||||
&i.Document,
|
||||
&i.IsDefault,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getCanvasSnapshot = `-- name: GetCanvasSnapshot :one
|
||||
SELECT id, project_id, user_id, version, canvas, viewport_json, nodes_json, connections_json, created_at
|
||||
FROM canvas_snapshots
|
||||
@@ -229,8 +289,30 @@ func (q *Queries) GetCanvasSnapshot(ctx context.Context, arg GetCanvasSnapshotPa
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getDefaultBrandKit = `-- name: GetDefaultBrandKit :one
|
||||
SELECT id, user_id, name, document, is_default, created_at, updated_at
|
||||
FROM brand_kits
|
||||
WHERE user_id = $1 AND is_default = TRUE
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetDefaultBrandKit(ctx context.Context, userID pgtype.UUID) (BrandKit, error) {
|
||||
row := q.db.QueryRow(ctx, getDefaultBrandKit, userID)
|
||||
var i BrandKit
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Name,
|
||||
&i.Document,
|
||||
&i.IsDefault,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getProject = `-- name: GetProject :one
|
||||
SELECT id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, viewport_x, viewport_y, viewport_k, updated_at
|
||||
SELECT id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, brand_kit_id, viewport_x, viewport_y, viewport_k, updated_at
|
||||
FROM projects
|
||||
WHERE id = $1 AND user_id = $2
|
||||
`
|
||||
@@ -254,6 +336,7 @@ func (q *Queries) GetProject(ctx context.Context, arg GetProjectParams) (Project
|
||||
&i.Version,
|
||||
&i.SnapshotID,
|
||||
&i.LastThreadID,
|
||||
&i.BrandKitID,
|
||||
&i.ViewportX,
|
||||
&i.ViewportY,
|
||||
&i.ViewportK,
|
||||
@@ -262,6 +345,41 @@ func (q *Queries) GetProject(ctx context.Context, arg GetProjectParams) (Project
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listBrandKits = `-- name: ListBrandKits :many
|
||||
SELECT id, user_id, name, document, is_default, created_at, updated_at
|
||||
FROM brand_kits
|
||||
WHERE user_id = $1
|
||||
ORDER BY updated_at DESC, id ASC
|
||||
`
|
||||
|
||||
func (q *Queries) ListBrandKits(ctx context.Context, userID pgtype.UUID) ([]BrandKit, error) {
|
||||
rows, err := q.db.Query(ctx, listBrandKits, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []BrandKit
|
||||
for rows.Next() {
|
||||
var i BrandKit
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Name,
|
||||
&i.Document,
|
||||
&i.IsDefault,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listConnectionsByProject = `-- name: ListConnectionsByProject :many
|
||||
SELECT id, project_id, user_id, from_node_id, to_node_id
|
||||
FROM canvas_connections
|
||||
@@ -414,20 +532,21 @@ func (q *Queries) ListNodesByProject(ctx context.Context, arg ListNodesByProject
|
||||
}
|
||||
|
||||
const listProjects = `-- name: ListProjects :many
|
||||
SELECT id, user_id, title, brief, status, thumbnail, updated_at
|
||||
SELECT id, user_id, title, brief, status, thumbnail, brand_kit_id, updated_at
|
||||
FROM projects
|
||||
WHERE user_id = $1
|
||||
ORDER BY updated_at DESC
|
||||
`
|
||||
|
||||
type ListProjectsRow struct {
|
||||
ID string `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Title string `json:"title"`
|
||||
Brief string `json:"brief"`
|
||||
Status string `json:"status"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Title string `json:"title"`
|
||||
Brief string `json:"brief"`
|
||||
Status string `json:"status"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
BrandKitID pgtype.Text `json:"brand_kit_id"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListProjects(ctx context.Context, userID pgtype.UUID) ([]ListProjectsRow, error) {
|
||||
@@ -446,6 +565,7 @@ func (q *Queries) ListProjects(ctx context.Context, userID pgtype.UUID) ([]ListP
|
||||
&i.Brief,
|
||||
&i.Status,
|
||||
&i.Thumbnail,
|
||||
&i.BrandKitID,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
@@ -459,7 +579,7 @@ func (q *Queries) ListProjects(ctx context.Context, userID pgtype.UUID) ([]ListP
|
||||
}
|
||||
|
||||
const listProjectsPage = `-- name: ListProjectsPage :many
|
||||
SELECT id, user_id, title, brief, status, thumbnail, updated_at
|
||||
SELECT id, user_id, title, brief, status, thumbnail, brand_kit_id, updated_at
|
||||
FROM projects
|
||||
WHERE user_id = $1
|
||||
ORDER BY updated_at DESC
|
||||
@@ -473,13 +593,14 @@ type ListProjectsPageParams struct {
|
||||
}
|
||||
|
||||
type ListProjectsPageRow struct {
|
||||
ID string `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Title string `json:"title"`
|
||||
Brief string `json:"brief"`
|
||||
Status string `json:"status"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Title string `json:"title"`
|
||||
Brief string `json:"brief"`
|
||||
Status string `json:"status"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
BrandKitID pgtype.Text `json:"brand_kit_id"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListProjectsPage(ctx context.Context, arg ListProjectsPageParams) ([]ListProjectsPageRow, error) {
|
||||
@@ -498,6 +619,7 @@ func (q *Queries) ListProjectsPage(ctx context.Context, arg ListProjectsPagePara
|
||||
&i.Brief,
|
||||
&i.Status,
|
||||
&i.Thumbnail,
|
||||
&i.BrandKitID,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
@@ -577,6 +699,51 @@ func (q *Queries) UpdateProjectCanvasSnapshot(ctx context.Context, arg UpdatePro
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertBrandKit = `-- name: UpsertBrandKit :one
|
||||
INSERT INTO brand_kits (id, user_id, name, document, is_default, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
document = EXCLUDED.document,
|
||||
is_default = EXCLUDED.is_default,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
WHERE brand_kits.user_id = EXCLUDED.user_id
|
||||
RETURNING id, user_id, name, document, is_default, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpsertBrandKitParams struct {
|
||||
ID string `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Document []byte `json:"document"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertBrandKit(ctx context.Context, arg UpsertBrandKitParams) (BrandKit, error) {
|
||||
row := q.db.QueryRow(ctx, upsertBrandKit,
|
||||
arg.ID,
|
||||
arg.UserID,
|
||||
arg.Name,
|
||||
arg.Document,
|
||||
arg.IsDefault,
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
var i BrandKit
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Name,
|
||||
&i.Document,
|
||||
&i.IsDefault,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const upsertCanvasNode = `-- name: UpsertCanvasNode :exec
|
||||
INSERT INTO canvas_nodes (id, project_id, user_id, node_type, title, x, y, width, height, content, tone, status, sort_order, parent_id, layer_role, font_size, font_family, font_weight, color, text_align, line_height, letter_spacing, text_decoration, text_transform, opacity, fill_color, stroke_color, stroke_width, stroke_style, flip_x, flip_y, rotation)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32)
|
||||
@@ -688,8 +855,8 @@ func (q *Queries) UpsertCanvasNode(ctx context.Context, arg UpsertCanvasNodePara
|
||||
}
|
||||
|
||||
const upsertProject = `-- name: UpsertProject :exec
|
||||
INSERT INTO projects (id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, viewport_x, viewport_y, viewport_k, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
INSERT INTO projects (id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, brand_kit_id, viewport_x, viewport_y, viewport_k, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
user_id = EXCLUDED.user_id,
|
||||
title = EXCLUDED.title,
|
||||
@@ -700,6 +867,7 @@ ON CONFLICT (id) DO UPDATE SET
|
||||
version = EXCLUDED.version,
|
||||
snapshot_id = EXCLUDED.snapshot_id,
|
||||
last_thread_id = EXCLUDED.last_thread_id,
|
||||
brand_kit_id = EXCLUDED.brand_kit_id,
|
||||
viewport_x = EXCLUDED.viewport_x,
|
||||
viewport_y = EXCLUDED.viewport_y,
|
||||
viewport_k = EXCLUDED.viewport_k,
|
||||
@@ -717,6 +885,7 @@ type UpsertProjectParams struct {
|
||||
Version string `json:"version"`
|
||||
SnapshotID string `json:"snapshot_id"`
|
||||
LastThreadID string `json:"last_thread_id"`
|
||||
BrandKitID pgtype.Text `json:"brand_kit_id"`
|
||||
ViewportX float64 `json:"viewport_x"`
|
||||
ViewportY float64 `json:"viewport_y"`
|
||||
ViewportK float64 `json:"viewport_k"`
|
||||
@@ -735,6 +904,7 @@ func (q *Queries) UpsertProject(ctx context.Context, arg UpsertProjectParams) er
|
||||
arg.Version,
|
||||
arg.SnapshotID,
|
||||
arg.LastThreadID,
|
||||
arg.BrandKitID,
|
||||
arg.ViewportX,
|
||||
arg.ViewportY,
|
||||
arg.ViewportK,
|
||||
|
||||
@@ -50,13 +50,14 @@ func (r *ProjectRepository) List(ctx context.Context) ([]design.ProjectSummary,
|
||||
items := make([]design.ProjectSummary, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, design.ProjectSummary{
|
||||
ID: row.ID,
|
||||
UserID: fromPGUUID(row.UserID),
|
||||
Title: row.Title,
|
||||
Brief: row.Brief,
|
||||
Status: design.ProjectStatus(row.Status),
|
||||
Thumbnail: row.Thumbnail,
|
||||
UpdatedAt: row.UpdatedAt.Time,
|
||||
ID: row.ID,
|
||||
UserID: fromPGUUID(row.UserID),
|
||||
Title: row.Title,
|
||||
Brief: row.Brief,
|
||||
Status: design.ProjectStatus(row.Status),
|
||||
Thumbnail: row.Thumbnail,
|
||||
BrandKitID: fromPGText(row.BrandKitID),
|
||||
UpdatedAt: row.UpdatedAt.Time,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
@@ -81,13 +82,14 @@ func (r *ProjectRepository) ListPage(ctx context.Context, limit int64, offset in
|
||||
items := make([]design.ProjectSummary, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, design.ProjectSummary{
|
||||
ID: row.ID,
|
||||
UserID: fromPGUUID(row.UserID),
|
||||
Title: row.Title,
|
||||
Brief: row.Brief,
|
||||
Status: design.ProjectStatus(row.Status),
|
||||
Thumbnail: row.Thumbnail,
|
||||
UpdatedAt: row.UpdatedAt.Time,
|
||||
ID: row.ID,
|
||||
UserID: fromPGUUID(row.UserID),
|
||||
Title: row.Title,
|
||||
Brief: row.Brief,
|
||||
Status: design.ProjectStatus(row.Status),
|
||||
Thumbnail: row.Thumbnail,
|
||||
BrandKitID: fromPGText(row.BrandKitID),
|
||||
UpdatedAt: row.UpdatedAt.Time,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
@@ -129,6 +131,7 @@ func (r *ProjectRepository) Get(ctx context.Context, id string) (design.Project,
|
||||
SnapshotID: row.SnapshotID,
|
||||
Canvas: row.Canvas,
|
||||
LastThreadID: row.LastThreadID,
|
||||
BrandKitID: fromPGText(row.BrandKitID),
|
||||
UpdatedAt: row.UpdatedAt.Time,
|
||||
Viewport: design.Viewport{
|
||||
X: row.ViewportX,
|
||||
@@ -165,6 +168,7 @@ func (r *ProjectRepository) Save(ctx context.Context, project design.Project) er
|
||||
Version: project.Version,
|
||||
SnapshotID: project.SnapshotID,
|
||||
LastThreadID: project.LastThreadID,
|
||||
BrandKitID: toPGText(project.BrandKitID),
|
||||
ViewportX: project.Viewport.X,
|
||||
ViewportY: project.Viewport.Y,
|
||||
ViewportK: project.Viewport.K,
|
||||
@@ -463,6 +467,18 @@ func fromPGUUID(value pgtype.UUID) string {
|
||||
return uuid.UUID(value.Bytes).String()
|
||||
}
|
||||
|
||||
func toPGText(value string) pgtype.Text {
|
||||
value = strings.TrimSpace(value)
|
||||
return pgtype.Text{String: value, Valid: value != ""}
|
||||
}
|
||||
|
||||
func fromPGText(value pgtype.Text) string {
|
||||
if !value.Valid {
|
||||
return ""
|
||||
}
|
||||
return value.String
|
||||
}
|
||||
|
||||
func insertProjectOutboxEvent(ctx context.Context, tx pgx.Tx, project design.Project, eventType string) error {
|
||||
eventID := uuid.NewString()
|
||||
createdAt := project.UpdatedAt
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
-- name: ListProjects :many
|
||||
SELECT id, user_id, title, brief, status, thumbnail, updated_at
|
||||
SELECT id, user_id, title, brief, status, thumbnail, brand_kit_id, updated_at
|
||||
FROM projects
|
||||
WHERE user_id = $1
|
||||
ORDER BY updated_at DESC;
|
||||
|
||||
-- name: ListProjectsPage :many
|
||||
SELECT id, user_id, title, brief, status, thumbnail, updated_at
|
||||
SELECT id, user_id, title, brief, status, thumbnail, brand_kit_id, updated_at
|
||||
FROM projects
|
||||
WHERE user_id = $1
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT $2 OFFSET $3;
|
||||
|
||||
-- name: GetProject :one
|
||||
SELECT id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, viewport_x, viewport_y, viewport_k, updated_at
|
||||
SELECT id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, brand_kit_id, viewport_x, viewport_y, viewport_k, updated_at
|
||||
FROM projects
|
||||
WHERE id = $1 AND user_id = $2;
|
||||
|
||||
@@ -27,8 +27,8 @@ DELETE FROM projects
|
||||
WHERE id = $1 AND user_id = $2;
|
||||
|
||||
-- name: UpsertProject :exec
|
||||
INSERT INTO projects (id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, viewport_x, viewport_y, viewport_k, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
INSERT INTO projects (id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, brand_kit_id, viewport_x, viewport_y, viewport_k, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
user_id = EXCLUDED.user_id,
|
||||
title = EXCLUDED.title,
|
||||
@@ -39,11 +39,50 @@ ON CONFLICT (id) DO UPDATE SET
|
||||
version = EXCLUDED.version,
|
||||
snapshot_id = EXCLUDED.snapshot_id,
|
||||
last_thread_id = EXCLUDED.last_thread_id,
|
||||
brand_kit_id = EXCLUDED.brand_kit_id,
|
||||
viewport_x = EXCLUDED.viewport_x,
|
||||
viewport_y = EXCLUDED.viewport_y,
|
||||
viewport_k = EXCLUDED.viewport_k,
|
||||
updated_at = EXCLUDED.updated_at;
|
||||
|
||||
-- name: ListBrandKits :many
|
||||
SELECT id, user_id, name, document, is_default, created_at, updated_at
|
||||
FROM brand_kits
|
||||
WHERE user_id = $1
|
||||
ORDER BY updated_at DESC, id ASC;
|
||||
|
||||
-- name: GetBrandKit :one
|
||||
SELECT id, user_id, name, document, is_default, created_at, updated_at
|
||||
FROM brand_kits
|
||||
WHERE id = $1 AND user_id = $2;
|
||||
|
||||
-- name: GetDefaultBrandKit :one
|
||||
SELECT id, user_id, name, document, is_default, created_at, updated_at
|
||||
FROM brand_kits
|
||||
WHERE user_id = $1 AND is_default = TRUE
|
||||
LIMIT 1;
|
||||
|
||||
-- name: ClearDefaultBrandKits :exec
|
||||
UPDATE brand_kits
|
||||
SET is_default = FALSE
|
||||
WHERE user_id = $1 AND is_default = TRUE AND id <> $2;
|
||||
|
||||
-- name: UpsertBrandKit :one
|
||||
INSERT INTO brand_kits (id, user_id, name, document, is_default, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
document = EXCLUDED.document,
|
||||
is_default = EXCLUDED.is_default,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
WHERE brand_kits.user_id = EXCLUDED.user_id
|
||||
RETURNING id, user_id, name, document, is_default, created_at, updated_at;
|
||||
|
||||
-- name: DeleteBrandKit :one
|
||||
DELETE FROM brand_kits
|
||||
WHERE id = $1 AND user_id = $2
|
||||
RETURNING id;
|
||||
|
||||
-- name: CreateCanvasSnapshot :exec
|
||||
INSERT INTO canvas_snapshots (id, project_id, user_id, version, canvas, viewport_json, nodes_json, connections_json, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
|
||||
@@ -43,6 +43,19 @@ CREATE TABLE IF NOT EXISTS auth_verification_codes (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS auth_codes_target_idx ON auth_verification_codes(region, channel, target, purpose, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS brand_kits (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
document JSONB NOT NULL CHECK (jsonb_typeof(document) = 'object'),
|
||||
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS brand_kits_user_updated_idx ON brand_kits(user_id, updated_at DESC);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS brand_kits_user_default_idx ON brand_kits(user_id) WHERE is_default;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000001'::uuid,
|
||||
@@ -54,6 +67,7 @@ CREATE TABLE IF NOT EXISTS projects (
|
||||
version TEXT NOT NULL DEFAULT '',
|
||||
snapshot_id TEXT NOT NULL DEFAULT '',
|
||||
last_thread_id TEXT NOT NULL DEFAULT '',
|
||||
brand_kit_id TEXT REFERENCES brand_kits(id) ON DELETE SET NULL,
|
||||
viewport_x DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
viewport_y DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
viewport_k DOUBLE PRECISION NOT NULL DEFAULT 1,
|
||||
@@ -70,7 +84,22 @@ ALTER TABLE projects ADD COLUMN IF NOT EXISTS canvas TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE projects ADD COLUMN IF NOT EXISTS version TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE projects ADD COLUMN IF NOT EXISTS snapshot_id TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE projects ADD COLUMN IF NOT EXISTS last_thread_id TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE projects ADD COLUMN IF NOT EXISTS brand_kit_id TEXT;
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'projects_brand_kit_id_fkey'
|
||||
AND conrelid = 'projects'::regclass
|
||||
) THEN
|
||||
ALTER TABLE projects
|
||||
ADD CONSTRAINT projects_brand_kit_id_fkey
|
||||
FOREIGN KEY (brand_kit_id) REFERENCES brand_kits(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
CREATE INDEX IF NOT EXISTS projects_user_updated_idx ON projects(user_id, updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS projects_brand_kit_idx ON projects(user_id, brand_kit_id) WHERE brand_kit_id IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS project_share_policies (
|
||||
project_id TEXT PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE,
|
||||
|
||||
Reference in New Issue
Block a user