Files
multica/server/internal/handler/skill_create.go
Naiyuan Qing 31ec36146f refactor(skills): split createSkillWithFiles for tx composition + add workspace find-or-create query
Two soft blockers cleared so create-from-template (next commit) can
fold N skill creates and the agent + binding writes into one outer
transaction:

1. createSkillWithFiles used to Begin/Commit its own tx. Caller
   composition was impossible — N invocations meant N separate
   transactions and no atomicity over the whole materialise step.
   Pull the body into createSkillWithFilesInTx(ctx, qtx, input); the
   original function becomes a thin wrapper that manages its own tx
   for standalone callers. Existing call sites: zero behaviour change.

2. Add GetSkillByWorkspaceAndName sqlc query — workspace skill lookup
   by name, anchored to UNIQUE(workspace_id, name) from migration
   008. Lets the template materialiser implement find-or-create:
   reuse the workspace's existing skill row when a template
   references the same name, rather than crashing on the unique
   constraint or polluting the workspace with `<name>-2` clones.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:45:04 +08:00

86 lines
2.3 KiB
Go

package handler
import (
"context"
"encoding/json"
"github.com/jackc/pgx/v5/pgtype"
db "github.com/multica-ai/multica/server/pkg/db/generated"
)
type skillCreateInput struct {
WorkspaceID pgtype.UUID
CreatorID pgtype.UUID
Name string
Description string
Content string
Config any
Files []CreateSkillFileRequest
}
// createSkillWithFilesInTx writes a skill plus its supporting files using the
// provided sqlc Queries handle, which must already be bound to an open
// transaction. Callers compose skill creation with other writes (e.g. agent
// template materialization) inside one outer transaction. For standalone
// skill creation, prefer createSkillWithFiles, which manages its own tx.
func createSkillWithFilesInTx(ctx context.Context, qtx *db.Queries, input skillCreateInput) (SkillWithFilesResponse, error) {
config, err := json.Marshal(input.Config)
if err != nil {
return SkillWithFilesResponse{}, err
}
if input.Config == nil {
config = []byte("{}")
}
skill, err := qtx.CreateSkill(ctx, db.CreateSkillParams{
WorkspaceID: input.WorkspaceID,
Name: sanitizeNullBytes(input.Name),
Description: sanitizeNullBytes(input.Description),
Content: sanitizeNullBytes(input.Content),
Config: config,
CreatedBy: input.CreatorID,
})
if err != nil {
return SkillWithFilesResponse{}, err
}
fileResps := make([]SkillFileResponse, 0, len(input.Files))
for _, f := range input.Files {
sf, err := qtx.UpsertSkillFile(ctx, db.UpsertSkillFileParams{
SkillID: skill.ID,
Path: sanitizeNullBytes(f.Path),
Content: sanitizeNullBytes(f.Content),
})
if err != nil {
return SkillWithFilesResponse{}, err
}
fileResps = append(fileResps, skillFileToResponse(sf))
}
return SkillWithFilesResponse{
SkillResponse: skillToResponse(skill),
Files: fileResps,
}, nil
}
func (h *Handler) createSkillWithFiles(ctx context.Context, input skillCreateInput) (SkillWithFilesResponse, error) {
tx, err := h.TxStarter.Begin(ctx)
if err != nil {
return SkillWithFilesResponse{}, err
}
defer tx.Rollback(ctx)
qtx := h.Queries.WithTx(tx)
result, err := createSkillWithFilesInTx(ctx, qtx, input)
if err != nil {
return SkillWithFilesResponse{}, err
}
if err := tx.Commit(ctx); err != nil {
return SkillWithFilesResponse{}, err
}
return result, nil
}