mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-05 09:30:05 +02:00
* feat(runtime): unbind agents on runtime delete instead of destroying them Deleting a runtime archived its agents and then hard-deleted the rows, so the agents and every conversation with them disappeared — while the confirmation dialog said "archive", which a user reasonably reads as recoverable. Retiring a laptop is an ordinary action; losing the agents configured on it is not an ordinary consequence. An agent is now a persistent business object and a runtime is replaceable execution capacity: deleting a runtime unbinds its agents. `runtime_id IS NULL` means unbound — orthogonal to archived — and the agent keeps its instructions, skills, chats, labels, channel installations, autopilots and task history. service.AgentReadiness already refused an agent with no runtime, so the scheduling safety gate needed no change. Two columns become nullable, not one. Without `agent_task_queue.runtime_id`, deleting the runtime still cascades the task history away (and task_message / task_usage / task_token with it), so the agents would survive with no record of anything they did — the same class of loss. A NOT VALID CHECK keeps NULL confined to history: an active task must always have a runtime, so claim / dispatch / delivery-CAS paths can never observe one without. It is written against completed_at rather than a status list so a future non-terminal status fails closed instead of slipping through. Two prerequisites this depends on: - 'deferred' (migration 128) was missing from CancelAgentTasksByRuntimeOrAgent. It went unnoticed because the delete used to cascade those rows away; with the new CHECK it would abort the delete and make the runtime undeletable. - The channel-installation / label / chat-pin / invocation-target / draft-restore cleanups were scoped to "archived agents on this runtime". Archived user agents now survive, so that scope is narrowed to kind='system' — otherwise the fix would produce a subtler loss: agent alive, configuration wiped. Also removes the squad guard that refused (409) when an active squad's leader was an archived agent on the runtime, plus the archived-squad delete that existed only to get past squad.leader_id's RESTRICT FK. The leader is no longer deleted, so nothing needs to be given up to retire a machine. Autopilots are no longer paused either: their assignee survives, and a rebind restores them without the owner having to remember to re-enable. Reason codes: an unbound agent reports agent_runtime_required, not runtime_offline. The copy for runtime_offline tells users to reconnect a machine; an unbound agent has no machine to reconnect, and the fix is to bind a runtime. Chat's bare 409 string gains the same code so the composer can offer that action. API: agents gain runtime_bound. runtime_id stays a string (empty when unbound) so installed clients keep parsing and no gated two-release rollout is needed. The confirmed-delete endpoint is /unbind-agents-and-delete; /archive-agents-and-delete still routes to it, and the compared expected_active_agent_ids set is unchanged — widening it would 409 every older client forever. Co-authored-by: multica-agent <github@multica.ai> * fix: make runtime unbinding recoverable Co-authored-by: multica-agent <github@multica.ai> * fix: address runtime unbind review nits Co-authored-by: multica-agent <github@multica.ai> * fix: resolve runtime unbind review blockers Co-authored-by: multica-agent <github@multica.ai> * fix(migrations): renumber runtime unbind after main merge Co-authored-by: multica-agent <github@multica.ai> * test(daemon): avoid late-request lease flake Co-authored-by: multica-agent <github@multica.ai> * test(autopilots): bind validation fixture runtime Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
728 lines
21 KiB
Go
728 lines
21 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: issue_label.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const attachLabelToAgent = `-- name: AttachLabelToAgent :exec
|
|
INSERT INTO agent_to_label (agent_id, label_id)
|
|
SELECT $1::uuid, $2::uuid
|
|
WHERE EXISTS (
|
|
SELECT 1 FROM agent a
|
|
WHERE a.id = $1::uuid
|
|
AND a.workspace_id = $3::uuid
|
|
)
|
|
AND EXISTS (
|
|
SELECT 1 FROM issue_label l
|
|
WHERE l.id = $2::uuid
|
|
AND l.workspace_id = $3::uuid
|
|
AND l.resource_type = 'agent'
|
|
)
|
|
ON CONFLICT DO NOTHING
|
|
`
|
|
|
|
type AttachLabelToAgentParams struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
LabelID pgtype.UUID `json:"label_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) AttachLabelToAgent(ctx context.Context, arg AttachLabelToAgentParams) error {
|
|
_, err := q.db.Exec(ctx, attachLabelToAgent, arg.AgentID, arg.LabelID, arg.WorkspaceID)
|
|
return err
|
|
}
|
|
|
|
const attachLabelToIssue = `-- name: AttachLabelToIssue :exec
|
|
INSERT INTO issue_to_label (issue_id, label_id)
|
|
SELECT $1::uuid, $2::uuid
|
|
WHERE EXISTS (
|
|
SELECT 1 FROM issue i
|
|
WHERE i.id = $1::uuid
|
|
AND i.workspace_id = $3::uuid
|
|
)
|
|
AND EXISTS (
|
|
SELECT 1 FROM issue_label l
|
|
WHERE l.id = $2::uuid
|
|
AND l.workspace_id = $3::uuid
|
|
AND l.resource_type = 'issue'
|
|
)
|
|
ON CONFLICT DO NOTHING
|
|
`
|
|
|
|
type AttachLabelToIssueParams struct {
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
LabelID pgtype.UUID `json:"label_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// Workspace-guarded INSERT: the WHERE EXISTS clauses ensure both the issue
|
|
// and the label belong to the given workspace. A future caller that forgets
|
|
// handler-level prechecks still cannot attach labels across workspaces.
|
|
func (q *Queries) AttachLabelToIssue(ctx context.Context, arg AttachLabelToIssueParams) error {
|
|
_, err := q.db.Exec(ctx, attachLabelToIssue, arg.IssueID, arg.LabelID, arg.WorkspaceID)
|
|
return err
|
|
}
|
|
|
|
const attachLabelToSkill = `-- name: AttachLabelToSkill :exec
|
|
INSERT INTO skill_to_label (skill_id, label_id)
|
|
SELECT $1::uuid, $2::uuid
|
|
WHERE EXISTS (
|
|
SELECT 1 FROM skill s
|
|
WHERE s.id = $1::uuid
|
|
AND s.workspace_id = $3::uuid
|
|
)
|
|
AND EXISTS (
|
|
SELECT 1 FROM issue_label l
|
|
WHERE l.id = $2::uuid
|
|
AND l.workspace_id = $3::uuid
|
|
AND l.resource_type = 'skill'
|
|
)
|
|
ON CONFLICT DO NOTHING
|
|
`
|
|
|
|
type AttachLabelToSkillParams struct {
|
|
SkillID pgtype.UUID `json:"skill_id"`
|
|
LabelID pgtype.UUID `json:"label_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) AttachLabelToSkill(ctx context.Context, arg AttachLabelToSkillParams) error {
|
|
_, err := q.db.Exec(ctx, attachLabelToSkill, arg.SkillID, arg.LabelID, arg.WorkspaceID)
|
|
return err
|
|
}
|
|
|
|
const createLabel = `-- name: CreateLabel :one
|
|
INSERT INTO issue_label (workspace_id, resource_type, name, description, color)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING id, workspace_id, name, color, created_at, updated_at, resource_type, description
|
|
`
|
|
|
|
type CreateLabelParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ResourceType string `json:"resource_type"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Color string `json:"color"`
|
|
}
|
|
|
|
func (q *Queries) CreateLabel(ctx context.Context, arg CreateLabelParams) (IssueLabel, error) {
|
|
row := q.db.QueryRow(ctx, createLabel,
|
|
arg.WorkspaceID,
|
|
arg.ResourceType,
|
|
arg.Name,
|
|
arg.Description,
|
|
arg.Color,
|
|
)
|
|
var i IssueLabel
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Color,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ResourceType,
|
|
&i.Description,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteAgentLabelAssignmentsByAgent = `-- name: DeleteAgentLabelAssignmentsByAgent :exec
|
|
DELETE FROM agent_to_label WHERE agent_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteAgentLabelAssignmentsByAgent(ctx context.Context, agentID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteAgentLabelAssignmentsByAgent, agentID)
|
|
return err
|
|
}
|
|
|
|
const deleteAgentLabelAssignmentsByLabel = `-- name: DeleteAgentLabelAssignmentsByLabel :exec
|
|
DELETE FROM agent_to_label WHERE label_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteAgentLabelAssignmentsByLabel(ctx context.Context, labelID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteAgentLabelAssignmentsByLabel, labelID)
|
|
return err
|
|
}
|
|
|
|
const deleteAgentLabelAssignmentsBySystemRuntimeAgents = `-- name: DeleteAgentLabelAssignmentsBySystemRuntimeAgents :exec
|
|
|
|
DELETE FROM agent_to_label
|
|
WHERE agent_id IN (SELECT id FROM agent WHERE runtime_id = $1 AND kind = 'system')
|
|
`
|
|
|
|
// The single-entity cleanups above cover one agent/skill at a time. The runtime
|
|
// variant below covers runtime and runtime-profile bulk hard deletes, where the
|
|
// owning agents disappear without passing through a per-entity delete.
|
|
// Workspace-wide cleanup lives in DeleteWorkspace so it is atomic with that
|
|
// workspace's existing multi-table teardown.
|
|
// Runtime teardown hard-deletes the system agents bound to the runtime (user
|
|
// agents are unbound and kept since MUL-5559). Clear only those agents' label
|
|
// links so none survive the agent hard-delete — a surviving unbound agent must
|
|
// keep its labels.
|
|
func (q *Queries) DeleteAgentLabelAssignmentsBySystemRuntimeAgents(ctx context.Context, runtimeID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteAgentLabelAssignmentsBySystemRuntimeAgents, runtimeID)
|
|
return err
|
|
}
|
|
|
|
const deleteIssueLabelAssignmentsByLabel = `-- name: DeleteIssueLabelAssignmentsByLabel :exec
|
|
|
|
DELETE FROM issue_to_label WHERE label_id = $1
|
|
`
|
|
|
|
// The resource-label junctions deliberately have no foreign keys. Keeping
|
|
// their cleanup in the same application transaction as the owner deletion
|
|
// avoids database-level cascades with unreviewed locking and audit behavior.
|
|
func (q *Queries) DeleteIssueLabelAssignmentsByLabel(ctx context.Context, labelID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteIssueLabelAssignmentsByLabel, labelID)
|
|
return err
|
|
}
|
|
|
|
const deleteLabel = `-- name: DeleteLabel :one
|
|
DELETE FROM issue_label
|
|
WHERE id = $1 AND workspace_id = $2
|
|
RETURNING id
|
|
`
|
|
|
|
type DeleteLabelParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// :one RETURNING id so the handler distinguishes pgx.ErrNoRows (→ 404) from
|
|
// infrastructure errors (→ 500), and avoids a TOCTOU precheck.
|
|
func (q *Queries) DeleteLabel(ctx context.Context, arg DeleteLabelParams) (pgtype.UUID, error) {
|
|
row := q.db.QueryRow(ctx, deleteLabel, arg.ID, arg.WorkspaceID)
|
|
var id pgtype.UUID
|
|
err := row.Scan(&id)
|
|
return id, err
|
|
}
|
|
|
|
const deleteSkillLabelAssignmentsByLabel = `-- name: DeleteSkillLabelAssignmentsByLabel :exec
|
|
DELETE FROM skill_to_label WHERE label_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteSkillLabelAssignmentsByLabel(ctx context.Context, labelID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteSkillLabelAssignmentsByLabel, labelID)
|
|
return err
|
|
}
|
|
|
|
const deleteSkillLabelAssignmentsBySkill = `-- name: DeleteSkillLabelAssignmentsBySkill :exec
|
|
DELETE FROM skill_to_label WHERE skill_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteSkillLabelAssignmentsBySkill(ctx context.Context, skillID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteSkillLabelAssignmentsBySkill, skillID)
|
|
return err
|
|
}
|
|
|
|
const detachLabelFromAgent = `-- name: DetachLabelFromAgent :exec
|
|
DELETE FROM agent_to_label
|
|
WHERE agent_id = $1::uuid
|
|
AND label_id = $2::uuid
|
|
AND EXISTS (
|
|
SELECT 1 FROM agent a
|
|
WHERE a.id = $1::uuid
|
|
AND a.workspace_id = $3::uuid
|
|
)
|
|
`
|
|
|
|
type DetachLabelFromAgentParams struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
LabelID pgtype.UUID `json:"label_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) DetachLabelFromAgent(ctx context.Context, arg DetachLabelFromAgentParams) error {
|
|
_, err := q.db.Exec(ctx, detachLabelFromAgent, arg.AgentID, arg.LabelID, arg.WorkspaceID)
|
|
return err
|
|
}
|
|
|
|
const detachLabelFromIssue = `-- name: DetachLabelFromIssue :exec
|
|
DELETE FROM issue_to_label
|
|
WHERE issue_id = $1::uuid
|
|
AND label_id = $2::uuid
|
|
AND EXISTS (
|
|
SELECT 1 FROM issue i
|
|
WHERE i.id = $1::uuid
|
|
AND i.workspace_id = $3::uuid
|
|
)
|
|
`
|
|
|
|
type DetachLabelFromIssueParams struct {
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
LabelID pgtype.UUID `json:"label_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// Workspace-guarded DELETE: only deletes if the issue is in the given
|
|
// workspace. Mirror of the attach query.
|
|
func (q *Queries) DetachLabelFromIssue(ctx context.Context, arg DetachLabelFromIssueParams) error {
|
|
_, err := q.db.Exec(ctx, detachLabelFromIssue, arg.IssueID, arg.LabelID, arg.WorkspaceID)
|
|
return err
|
|
}
|
|
|
|
const detachLabelFromSkill = `-- name: DetachLabelFromSkill :exec
|
|
DELETE FROM skill_to_label
|
|
WHERE skill_id = $1::uuid
|
|
AND label_id = $2::uuid
|
|
AND EXISTS (
|
|
SELECT 1 FROM skill s
|
|
WHERE s.id = $1::uuid
|
|
AND s.workspace_id = $3::uuid
|
|
)
|
|
`
|
|
|
|
type DetachLabelFromSkillParams struct {
|
|
SkillID pgtype.UUID `json:"skill_id"`
|
|
LabelID pgtype.UUID `json:"label_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) DetachLabelFromSkill(ctx context.Context, arg DetachLabelFromSkillParams) error {
|
|
_, err := q.db.Exec(ctx, detachLabelFromSkill, arg.SkillID, arg.LabelID, arg.WorkspaceID)
|
|
return err
|
|
}
|
|
|
|
const getLabel = `-- name: GetLabel :one
|
|
SELECT id, workspace_id, name, color, created_at, updated_at, resource_type, description FROM issue_label
|
|
WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type GetLabelParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) GetLabel(ctx context.Context, arg GetLabelParams) (IssueLabel, error) {
|
|
row := q.db.QueryRow(ctx, getLabel, arg.ID, arg.WorkspaceID)
|
|
var i IssueLabel
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Color,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ResourceType,
|
|
&i.Description,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listLabels = `-- name: ListLabels :many
|
|
SELECT l.id, l.workspace_id, l.name, l.color, l.created_at, l.updated_at, l.resource_type, l.description,
|
|
CASE l.resource_type
|
|
WHEN 'issue' THEN (SELECT COUNT(*) FROM issue_to_label x WHERE x.label_id = l.id)
|
|
WHEN 'agent' THEN (SELECT COUNT(*) FROM agent_to_label x WHERE x.label_id = l.id)
|
|
WHEN 'skill' THEN (SELECT COUNT(*) FROM skill_to_label x WHERE x.label_id = l.id)
|
|
ELSE 0
|
|
END::bigint AS usage_count
|
|
FROM issue_label l
|
|
WHERE l.workspace_id = $1::uuid
|
|
AND l.resource_type = $2::text
|
|
ORDER BY LOWER(name) ASC
|
|
`
|
|
|
|
type ListLabelsParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ResourceType string `json:"resource_type"`
|
|
}
|
|
|
|
type ListLabelsRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Color string `json:"color"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
ResourceType string `json:"resource_type"`
|
|
Description string `json:"description"`
|
|
UsageCount int64 `json:"usage_count"`
|
|
}
|
|
|
|
func (q *Queries) ListLabels(ctx context.Context, arg ListLabelsParams) ([]ListLabelsRow, error) {
|
|
rows, err := q.db.Query(ctx, listLabels, arg.WorkspaceID, arg.ResourceType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListLabelsRow{}
|
|
for rows.Next() {
|
|
var i ListLabelsRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Color,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ResourceType,
|
|
&i.Description,
|
|
&i.UsageCount,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listLabelsByAgent = `-- name: ListLabelsByAgent :many
|
|
SELECT l.id, l.workspace_id, l.name, l.color, l.created_at, l.updated_at, l.resource_type, l.description
|
|
FROM issue_label l
|
|
JOIN agent_to_label atl ON atl.label_id = l.id
|
|
WHERE atl.agent_id = $1::uuid
|
|
AND l.workspace_id = $2::uuid
|
|
AND l.resource_type = 'agent'
|
|
ORDER BY LOWER(l.name) ASC
|
|
`
|
|
|
|
type ListLabelsByAgentParams struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) ListLabelsByAgent(ctx context.Context, arg ListLabelsByAgentParams) ([]IssueLabel, error) {
|
|
rows, err := q.db.Query(ctx, listLabelsByAgent, arg.AgentID, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []IssueLabel{}
|
|
for rows.Next() {
|
|
var i IssueLabel
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Color,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ResourceType,
|
|
&i.Description,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listLabelsByIssue = `-- name: ListLabelsByIssue :many
|
|
SELECT l.id, l.workspace_id, l.name, l.color, l.created_at, l.updated_at, l.resource_type, l.description
|
|
FROM issue_label l
|
|
JOIN issue_to_label il ON il.label_id = l.id
|
|
WHERE il.issue_id = $1::uuid
|
|
AND l.workspace_id = $2::uuid
|
|
AND l.resource_type = 'issue'
|
|
ORDER BY LOWER(l.name) ASC
|
|
`
|
|
|
|
type ListLabelsByIssueParams struct {
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// Workspace filter at the SQL layer (mirrors GetProjectInWorkspace). Any caller
|
|
// that passes the wrong workspace gets an empty list rather than leaking labels.
|
|
func (q *Queries) ListLabelsByIssue(ctx context.Context, arg ListLabelsByIssueParams) ([]IssueLabel, error) {
|
|
rows, err := q.db.Query(ctx, listLabelsByIssue, arg.IssueID, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []IssueLabel{}
|
|
for rows.Next() {
|
|
var i IssueLabel
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Color,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ResourceType,
|
|
&i.Description,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listLabelsBySkill = `-- name: ListLabelsBySkill :many
|
|
SELECT l.id, l.workspace_id, l.name, l.color, l.created_at, l.updated_at, l.resource_type, l.description
|
|
FROM issue_label l
|
|
JOIN skill_to_label stl ON stl.label_id = l.id
|
|
WHERE stl.skill_id = $1::uuid
|
|
AND l.workspace_id = $2::uuid
|
|
AND l.resource_type = 'skill'
|
|
ORDER BY LOWER(l.name) ASC
|
|
`
|
|
|
|
type ListLabelsBySkillParams struct {
|
|
SkillID pgtype.UUID `json:"skill_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) ListLabelsBySkill(ctx context.Context, arg ListLabelsBySkillParams) ([]IssueLabel, error) {
|
|
rows, err := q.db.Query(ctx, listLabelsBySkill, arg.SkillID, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []IssueLabel{}
|
|
for rows.Next() {
|
|
var i IssueLabel
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Color,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ResourceType,
|
|
&i.Description,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listLabelsForAgents = `-- name: ListLabelsForAgents :many
|
|
SELECT atl.agent_id, l.id, l.workspace_id, l.name, l.color, l.created_at, l.updated_at, l.resource_type, l.description
|
|
FROM issue_label l
|
|
JOIN agent_to_label atl ON atl.label_id = l.id
|
|
WHERE atl.agent_id = ANY($1::uuid[])
|
|
AND l.workspace_id = $2::uuid
|
|
AND l.resource_type = 'agent'
|
|
ORDER BY atl.agent_id, LOWER(l.name) ASC
|
|
`
|
|
|
|
type ListLabelsForAgentsParams struct {
|
|
AgentIds []pgtype.UUID `json:"agent_ids"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
type ListLabelsForAgentsRow struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Color string `json:"color"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
ResourceType string `json:"resource_type"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
func (q *Queries) ListLabelsForAgents(ctx context.Context, arg ListLabelsForAgentsParams) ([]ListLabelsForAgentsRow, error) {
|
|
rows, err := q.db.Query(ctx, listLabelsForAgents, arg.AgentIds, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListLabelsForAgentsRow{}
|
|
for rows.Next() {
|
|
var i ListLabelsForAgentsRow
|
|
if err := rows.Scan(
|
|
&i.AgentID,
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Color,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ResourceType,
|
|
&i.Description,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listLabelsForIssues = `-- name: ListLabelsForIssues :many
|
|
SELECT il.issue_id, l.id, l.workspace_id, l.name, l.color, l.created_at, l.updated_at, l.resource_type, l.description
|
|
FROM issue_label l
|
|
JOIN issue_to_label il ON il.label_id = l.id
|
|
WHERE il.issue_id = ANY($1::uuid[])
|
|
AND l.workspace_id = $2::uuid
|
|
AND l.resource_type = 'issue'
|
|
ORDER BY il.issue_id, LOWER(l.name) ASC
|
|
`
|
|
|
|
type ListLabelsForIssuesParams struct {
|
|
IssueIds []pgtype.UUID `json:"issue_ids"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
type ListLabelsForIssuesRow struct {
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Color string `json:"color"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
ResourceType string `json:"resource_type"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// Bulk variant: fetch labels for many issues in one round-trip so the issue
|
|
// list endpoints can fold labels into each row without N+1 queries from the
|
|
// client. Workspace-guarded the same way as ListLabelsByIssue.
|
|
func (q *Queries) ListLabelsForIssues(ctx context.Context, arg ListLabelsForIssuesParams) ([]ListLabelsForIssuesRow, error) {
|
|
rows, err := q.db.Query(ctx, listLabelsForIssues, arg.IssueIds, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListLabelsForIssuesRow{}
|
|
for rows.Next() {
|
|
var i ListLabelsForIssuesRow
|
|
if err := rows.Scan(
|
|
&i.IssueID,
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Color,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ResourceType,
|
|
&i.Description,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listLabelsForSkills = `-- name: ListLabelsForSkills :many
|
|
SELECT stl.skill_id, l.id, l.workspace_id, l.name, l.color, l.created_at, l.updated_at, l.resource_type, l.description
|
|
FROM issue_label l
|
|
JOIN skill_to_label stl ON stl.label_id = l.id
|
|
WHERE stl.skill_id = ANY($1::uuid[])
|
|
AND l.workspace_id = $2::uuid
|
|
AND l.resource_type = 'skill'
|
|
ORDER BY stl.skill_id, LOWER(l.name) ASC
|
|
`
|
|
|
|
type ListLabelsForSkillsParams struct {
|
|
SkillIds []pgtype.UUID `json:"skill_ids"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
type ListLabelsForSkillsRow struct {
|
|
SkillID pgtype.UUID `json:"skill_id"`
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Color string `json:"color"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
ResourceType string `json:"resource_type"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
func (q *Queries) ListLabelsForSkills(ctx context.Context, arg ListLabelsForSkillsParams) ([]ListLabelsForSkillsRow, error) {
|
|
rows, err := q.db.Query(ctx, listLabelsForSkills, arg.SkillIds, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListLabelsForSkillsRow{}
|
|
for rows.Next() {
|
|
var i ListLabelsForSkillsRow
|
|
if err := rows.Scan(
|
|
&i.SkillID,
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Color,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ResourceType,
|
|
&i.Description,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const updateLabel = `-- name: UpdateLabel :one
|
|
UPDATE issue_label SET
|
|
name = COALESCE($3, name),
|
|
description = COALESCE($4, description),
|
|
color = COALESCE($5, color),
|
|
updated_at = now()
|
|
WHERE id = $1 AND workspace_id = $2
|
|
RETURNING id, workspace_id, name, color, created_at, updated_at, resource_type, description
|
|
`
|
|
|
|
type UpdateLabelParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name pgtype.Text `json:"name"`
|
|
Description pgtype.Text `json:"description"`
|
|
Color pgtype.Text `json:"color"`
|
|
}
|
|
|
|
func (q *Queries) UpdateLabel(ctx context.Context, arg UpdateLabelParams) (IssueLabel, error) {
|
|
row := q.db.QueryRow(ctx, updateLabel,
|
|
arg.ID,
|
|
arg.WorkspaceID,
|
|
arg.Name,
|
|
arg.Description,
|
|
arg.Color,
|
|
)
|
|
var i IssueLabel
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Color,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ResourceType,
|
|
&i.Description,
|
|
)
|
|
return i, err
|
|
}
|