mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-30 07:10:49 +02:00
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>
86 lines
2.3 KiB
Go
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
|
|
}
|