mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-17 07:09:53 +02:00
- mobile: EMPTY_ISSUE_FALLBACK gains the required properties field (mobile
typecheck was the red CI check).
- server: PATCH /api/properties/{id} rejects config updates that remove
select options still referenced by issues (409 with a per-option usage
census via jsonb ?); renames keep ids and pass. Integration test included.
- core: property value mutations are serialized per workspace via mutation
scope, snapshot the bag from detail OR list caches (board surfaces have no
detail cache — the old path overwrote whole bags with one key), roll back
to the snapshot or invalidate on error, and the last settled mutation does
an authoritative detail+catalog invalidate (usage counts reconcile).
- schemas: unknown-shaped property values (future server types) are dropped
per-entry in a preprocess step instead of failing the whole IssueSchema
and blanking lists through parseWithFallback; test updated to lock the
tolerant behavior.
- realtime: reconnect invalidation covers the property catalog; every
issue_properties:changed event also refreshes catalog usage counts.
- ui: number editor accepts decimals (step=any); settings usage count
pluralizes (issue/issues) with CJK-safe plural keys.
Co-authored-by: multica-agent <github@multica.ai>
362 lines
10 KiB
Go
362 lines
10 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: issue_property.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const countActiveIssueProperties = `-- name: CountActiveIssueProperties :one
|
|
SELECT COUNT(*) FROM issue_property
|
|
WHERE workspace_id = $1 AND archived_at IS NULL
|
|
`
|
|
|
|
func (q *Queries) CountActiveIssueProperties(ctx context.Context, workspaceID pgtype.UUID) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countActiveIssueProperties, workspaceID)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const countIssuesUsingPropertyOptions = `-- name: CountIssuesUsingPropertyOptions :many
|
|
SELECT opt::text AS option_id, COUNT(i.id) AS usage_count
|
|
FROM unnest($1::text[]) AS opt
|
|
LEFT JOIN issue i
|
|
ON i.workspace_id = $2::uuid
|
|
AND (i.properties -> $3::text) ? opt
|
|
GROUP BY opt
|
|
HAVING COUNT(i.id) > 0
|
|
`
|
|
|
|
type CountIssuesUsingPropertyOptionsParams struct {
|
|
OptionIds []string `json:"option_ids"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
PropertyKey string `json:"property_key"`
|
|
}
|
|
|
|
type CountIssuesUsingPropertyOptionsRow struct {
|
|
OptionID string `json:"option_id"`
|
|
UsageCount int64 `json:"usage_count"`
|
|
}
|
|
|
|
// Usage census for specific option ids of one property. jsonb `?` matches
|
|
// both value shapes: array element for multi_select, string equality for
|
|
// select. Only options with at least one referencing issue come back.
|
|
func (q *Queries) CountIssuesUsingPropertyOptions(ctx context.Context, arg CountIssuesUsingPropertyOptionsParams) ([]CountIssuesUsingPropertyOptionsRow, error) {
|
|
rows, err := q.db.Query(ctx, countIssuesUsingPropertyOptions, arg.OptionIds, arg.WorkspaceID, arg.PropertyKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []CountIssuesUsingPropertyOptionsRow{}
|
|
for rows.Next() {
|
|
var i CountIssuesUsingPropertyOptionsRow
|
|
if err := rows.Scan(&i.OptionID, &i.UsageCount); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const createIssueProperty = `-- name: CreateIssueProperty :one
|
|
INSERT INTO issue_property (workspace_id, name, type, description, config, position)
|
|
SELECT $1::uuid,
|
|
$2::text,
|
|
$3::text,
|
|
$4::text,
|
|
$5::jsonb,
|
|
COALESCE((SELECT MAX(position) FROM issue_property WHERE workspace_id = $1::uuid), 0) + 1
|
|
RETURNING id, workspace_id, name, type, description, config, position, archived_at, created_at, updated_at
|
|
`
|
|
|
|
type CreateIssuePropertyParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Description string `json:"description"`
|
|
Config []byte `json:"config"`
|
|
}
|
|
|
|
// New definitions append to the end of the catalog: position = max + 1.
|
|
func (q *Queries) CreateIssueProperty(ctx context.Context, arg CreateIssuePropertyParams) (IssueProperty, error) {
|
|
row := q.db.QueryRow(ctx, createIssueProperty,
|
|
arg.WorkspaceID,
|
|
arg.Name,
|
|
arg.Type,
|
|
arg.Description,
|
|
arg.Config,
|
|
)
|
|
var i IssueProperty
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Type,
|
|
&i.Description,
|
|
&i.Config,
|
|
&i.Position,
|
|
&i.ArchivedAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteIssuePropertyValue = `-- name: DeleteIssuePropertyValue :one
|
|
UPDATE issue
|
|
SET properties = properties - $1::text,
|
|
updated_at = now()
|
|
WHERE id = $2::uuid AND workspace_id = $3::uuid
|
|
RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata, stage, properties
|
|
`
|
|
|
|
type DeleteIssuePropertyValueParams struct {
|
|
Key string `json:"key"`
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) DeleteIssuePropertyValue(ctx context.Context, arg DeleteIssuePropertyValueParams) (Issue, error) {
|
|
row := q.db.QueryRow(ctx, deleteIssuePropertyValue, arg.Key, arg.ID, arg.WorkspaceID)
|
|
var i Issue
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Status,
|
|
&i.Priority,
|
|
&i.AssigneeType,
|
|
&i.AssigneeID,
|
|
&i.CreatorType,
|
|
&i.CreatorID,
|
|
&i.ParentIssueID,
|
|
&i.AcceptanceCriteria,
|
|
&i.ContextRefs,
|
|
&i.Position,
|
|
&i.DueDate,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Number,
|
|
&i.ProjectID,
|
|
&i.OriginType,
|
|
&i.OriginID,
|
|
&i.FirstExecutedAt,
|
|
&i.StartDate,
|
|
&i.Metadata,
|
|
&i.Stage,
|
|
&i.Properties,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getIssueProperty = `-- name: GetIssueProperty :one
|
|
SELECT id, workspace_id, name, type, description, config, position, archived_at, created_at, updated_at FROM issue_property
|
|
WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type GetIssuePropertyParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) GetIssueProperty(ctx context.Context, arg GetIssuePropertyParams) (IssueProperty, error) {
|
|
row := q.db.QueryRow(ctx, getIssueProperty, arg.ID, arg.WorkspaceID)
|
|
var i IssueProperty
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Type,
|
|
&i.Description,
|
|
&i.Config,
|
|
&i.Position,
|
|
&i.ArchivedAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listIssueProperties = `-- name: ListIssueProperties :many
|
|
SELECT p.id, p.workspace_id, p.name, p.type, p.description, p.config, p.position, p.archived_at, p.created_at, p.updated_at,
|
|
(
|
|
SELECT COUNT(*) FROM issue i
|
|
WHERE i.workspace_id = p.workspace_id
|
|
AND i.properties ? p.id::text
|
|
)::bigint AS usage_count
|
|
FROM issue_property p
|
|
WHERE p.workspace_id = $1::uuid
|
|
AND ($2::bool OR p.archived_at IS NULL)
|
|
ORDER BY p.position ASC, LOWER(p.name) ASC
|
|
`
|
|
|
|
type ListIssuePropertiesParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
IncludeArchived bool `json:"include_archived"`
|
|
}
|
|
|
|
type ListIssuePropertiesRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Description string `json:"description"`
|
|
Config []byte `json:"config"`
|
|
Position float64 `json:"position"`
|
|
ArchivedAt pgtype.Timestamptz `json:"archived_at"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
UsageCount int64 `json:"usage_count"`
|
|
}
|
|
|
|
// usage_count = number of issues in the workspace that currently carry a
|
|
// value for this property. `properties ? id` is a seq scan today; fine at
|
|
// the 20-definition / small-workspace scale this feature targets.
|
|
func (q *Queries) ListIssueProperties(ctx context.Context, arg ListIssuePropertiesParams) ([]ListIssuePropertiesRow, error) {
|
|
rows, err := q.db.Query(ctx, listIssueProperties, arg.WorkspaceID, arg.IncludeArchived)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListIssuePropertiesRow{}
|
|
for rows.Next() {
|
|
var i ListIssuePropertiesRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Type,
|
|
&i.Description,
|
|
&i.Config,
|
|
&i.Position,
|
|
&i.ArchivedAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.UsageCount,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const setIssuePropertyValue = `-- name: SetIssuePropertyValue :one
|
|
UPDATE issue
|
|
SET properties = jsonb_set(properties, ARRAY[$1::text], $2::jsonb, true),
|
|
updated_at = now()
|
|
WHERE id = $3::uuid AND workspace_id = $4::uuid
|
|
RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata, stage, properties
|
|
`
|
|
|
|
type SetIssuePropertyValueParams struct {
|
|
Key string `json:"key"`
|
|
Value []byte `json:"value"`
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// Single-key atomic write (mirror of SetIssueMetadataKey): concurrent writers
|
|
// on different property keys never clobber each other.
|
|
func (q *Queries) SetIssuePropertyValue(ctx context.Context, arg SetIssuePropertyValueParams) (Issue, error) {
|
|
row := q.db.QueryRow(ctx, setIssuePropertyValue,
|
|
arg.Key,
|
|
arg.Value,
|
|
arg.ID,
|
|
arg.WorkspaceID,
|
|
)
|
|
var i Issue
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Status,
|
|
&i.Priority,
|
|
&i.AssigneeType,
|
|
&i.AssigneeID,
|
|
&i.CreatorType,
|
|
&i.CreatorID,
|
|
&i.ParentIssueID,
|
|
&i.AcceptanceCriteria,
|
|
&i.ContextRefs,
|
|
&i.Position,
|
|
&i.DueDate,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Number,
|
|
&i.ProjectID,
|
|
&i.OriginType,
|
|
&i.OriginID,
|
|
&i.FirstExecutedAt,
|
|
&i.StartDate,
|
|
&i.Metadata,
|
|
&i.Stage,
|
|
&i.Properties,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateIssueProperty = `-- name: UpdateIssueProperty :one
|
|
UPDATE issue_property SET
|
|
name = COALESCE($3, name),
|
|
description = COALESCE($4, description),
|
|
config = COALESCE($5, config),
|
|
archived_at = CASE WHEN $6::bool THEN $7 ELSE archived_at END,
|
|
updated_at = now()
|
|
WHERE id = $1 AND workspace_id = $2
|
|
RETURNING id, workspace_id, name, type, description, config, position, archived_at, created_at, updated_at
|
|
`
|
|
|
|
type UpdateIssuePropertyParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name pgtype.Text `json:"name"`
|
|
Description pgtype.Text `json:"description"`
|
|
Config []byte `json:"config"`
|
|
ArchivedSet bool `json:"archived_set"`
|
|
ArchivedAt pgtype.Timestamptz `json:"archived_at"`
|
|
}
|
|
|
|
// `type` is deliberately immutable — changing it would silently invalidate
|
|
// existing values. archived_set/archived_at implement tri-state semantics:
|
|
// archived_set=false leaves archived_at untouched.
|
|
func (q *Queries) UpdateIssueProperty(ctx context.Context, arg UpdateIssuePropertyParams) (IssueProperty, error) {
|
|
row := q.db.QueryRow(ctx, updateIssueProperty,
|
|
arg.ID,
|
|
arg.WorkspaceID,
|
|
arg.Name,
|
|
arg.Description,
|
|
arg.Config,
|
|
arg.ArchivedSet,
|
|
arg.ArchivedAt,
|
|
)
|
|
var i IssueProperty
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Type,
|
|
&i.Description,
|
|
&i.Config,
|
|
&i.Position,
|
|
&i.ArchivedAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|