mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-17 15:19:00 +02:00
Membership rows gain a fractional sort_order (migration 135) — the
user's personal sidebar order, Linear's TeamMembership.sortOrder shape.
System placement is atomic and mandatory: workspace creation, invitation
acceptance, and direct member-add all join the default team (hard
failure instead of the old silent skip, so a system-placed member can
never be team-less). Team creation makes the creator lead and can pull
in members.
The member set is configured wholesale via PUT /api/teams/{id}/members
(anyone can edit anyone — membership only drives the sidebar and
personal defaults, never access): kept rows are untouched, added members
land at the end of their own order, and an empty list is rejected since
zero members means archiving, which the client does explicitly after a
confirm. GET members and PATCH membership (sidebar drag sort) complete
the surface. User-facing wording says identifier; the API keeps 'key',
mirroring Linear's own Team.key.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
464 lines
12 KiB
Go
464 lines
12 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: project.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const addProjectTeam = `-- name: AddProjectTeam :exec
|
|
INSERT INTO project_team (workspace_id, project_id, team_id)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (project_id, team_id) DO NOTHING
|
|
`
|
|
|
|
type AddProjectTeamParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
TeamID pgtype.UUID `json:"team_id"`
|
|
}
|
|
|
|
func (q *Queries) AddProjectTeam(ctx context.Context, arg AddProjectTeamParams) error {
|
|
_, err := q.db.Exec(ctx, addProjectTeam, arg.WorkspaceID, arg.ProjectID, arg.TeamID)
|
|
return err
|
|
}
|
|
|
|
const countIssuesByProject = `-- name: CountIssuesByProject :one
|
|
SELECT count(*) FROM issue
|
|
WHERE project_id = $1
|
|
`
|
|
|
|
func (q *Queries) CountIssuesByProject(ctx context.Context, projectID pgtype.UUID) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countIssuesByProject, projectID)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const createProject = `-- name: CreateProject :one
|
|
INSERT INTO project (
|
|
workspace_id, title, description, icon, status,
|
|
lead_type, lead_id, priority
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, $7, $8
|
|
) RETURNING id, workspace_id, title, description, icon, status, lead_type, lead_id, created_at, updated_at, priority
|
|
`
|
|
|
|
type CreateProjectParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Title string `json:"title"`
|
|
Description pgtype.Text `json:"description"`
|
|
Icon pgtype.Text `json:"icon"`
|
|
Status string `json:"status"`
|
|
LeadType pgtype.Text `json:"lead_type"`
|
|
LeadID pgtype.UUID `json:"lead_id"`
|
|
Priority string `json:"priority"`
|
|
}
|
|
|
|
func (q *Queries) CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error) {
|
|
row := q.db.QueryRow(ctx, createProject,
|
|
arg.WorkspaceID,
|
|
arg.Title,
|
|
arg.Description,
|
|
arg.Icon,
|
|
arg.Status,
|
|
arg.LeadType,
|
|
arg.LeadID,
|
|
arg.Priority,
|
|
)
|
|
var i Project
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.Status,
|
|
&i.LeadType,
|
|
&i.LeadID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Priority,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteProject = `-- name: DeleteProject :exec
|
|
DELETE FROM project WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type DeleteProjectParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// Defense-in-depth: workspace_id is a SQL-layer tenant guard. See DeleteIssue.
|
|
func (q *Queries) DeleteProject(ctx context.Context, arg DeleteProjectParams) error {
|
|
_, err := q.db.Exec(ctx, deleteProject, arg.ID, arg.WorkspaceID)
|
|
return err
|
|
}
|
|
|
|
const getProject = `-- name: GetProject :one
|
|
SELECT id, workspace_id, title, description, icon, status, lead_type, lead_id, created_at, updated_at, priority FROM project
|
|
WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetProject(ctx context.Context, id pgtype.UUID) (Project, error) {
|
|
row := q.db.QueryRow(ctx, getProject, id)
|
|
var i Project
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.Status,
|
|
&i.LeadType,
|
|
&i.LeadID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Priority,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getProjectInWorkspace = `-- name: GetProjectInWorkspace :one
|
|
SELECT id, workspace_id, title, description, icon, status, lead_type, lead_id, created_at, updated_at, priority FROM project
|
|
WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type GetProjectInWorkspaceParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) GetProjectInWorkspace(ctx context.Context, arg GetProjectInWorkspaceParams) (Project, error) {
|
|
row := q.db.QueryRow(ctx, getProjectInWorkspace, arg.ID, arg.WorkspaceID)
|
|
var i Project
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.Status,
|
|
&i.LeadType,
|
|
&i.LeadID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Priority,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getProjectIssueStats = `-- name: GetProjectIssueStats :many
|
|
SELECT project_id,
|
|
count(*)::bigint AS total_count,
|
|
count(*) FILTER (WHERE status IN ('done', 'cancelled'))::bigint AS done_count
|
|
FROM issue
|
|
WHERE project_id = ANY($1::uuid[])
|
|
GROUP BY project_id
|
|
`
|
|
|
|
type GetProjectIssueStatsRow struct {
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
TotalCount int64 `json:"total_count"`
|
|
DoneCount int64 `json:"done_count"`
|
|
}
|
|
|
|
func (q *Queries) GetProjectIssueStats(ctx context.Context, projectIds []pgtype.UUID) ([]GetProjectIssueStatsRow, error) {
|
|
rows, err := q.db.Query(ctx, getProjectIssueStats, projectIds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []GetProjectIssueStatsRow{}
|
|
for rows.Next() {
|
|
var i GetProjectIssueStatsRow
|
|
if err := rows.Scan(&i.ProjectID, &i.TotalCount, &i.DoneCount); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listProjectTeams = `-- name: ListProjectTeams :many
|
|
SELECT wt.id, wt.workspace_id, wt.name, wt.key, wt.description, wt.icon, wt.issue_counter, wt.is_default, wt.archived_at, wt.archived_by, wt.created_by, wt.created_at, wt.updated_at FROM workspace_team wt
|
|
JOIN project_team pt ON pt.team_id = wt.id AND pt.workspace_id = wt.workspace_id
|
|
WHERE pt.workspace_id = $1
|
|
AND pt.project_id = $2
|
|
ORDER BY wt.is_default DESC, wt.name ASC, wt.created_at ASC
|
|
`
|
|
|
|
type ListProjectTeamsParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
}
|
|
|
|
func (q *Queries) ListProjectTeams(ctx context.Context, arg ListProjectTeamsParams) ([]WorkspaceTeam, error) {
|
|
rows, err := q.db.Query(ctx, listProjectTeams, arg.WorkspaceID, arg.ProjectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []WorkspaceTeam{}
|
|
for rows.Next() {
|
|
var i WorkspaceTeam
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Key,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.IssueCounter,
|
|
&i.IsDefault,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.CreatedBy,
|
|
&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 listProjectTeamsByProjects = `-- name: ListProjectTeamsByProjects :many
|
|
SELECT pt.project_id, wt.id, wt.workspace_id, wt.name, wt.key, wt.description,
|
|
wt.icon, wt.issue_counter, wt.is_default, wt.archived_at, wt.archived_by,
|
|
wt.created_by, wt.created_at, wt.updated_at
|
|
FROM project_team pt
|
|
JOIN workspace_team wt ON wt.id = pt.team_id AND wt.workspace_id = pt.workspace_id
|
|
WHERE pt.workspace_id = $1
|
|
AND pt.project_id = ANY($2::uuid[])
|
|
ORDER BY pt.project_id, wt.is_default DESC, wt.name ASC, wt.created_at ASC
|
|
`
|
|
|
|
type ListProjectTeamsByProjectsParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ProjectIds []pgtype.UUID `json:"project_ids"`
|
|
}
|
|
|
|
type ListProjectTeamsByProjectsRow struct {
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Key string `json:"key"`
|
|
Description string `json:"description"`
|
|
Icon pgtype.Text `json:"icon"`
|
|
IssueCounter int32 `json:"issue_counter"`
|
|
IsDefault bool `json:"is_default"`
|
|
ArchivedAt pgtype.Timestamptz `json:"archived_at"`
|
|
ArchivedBy pgtype.UUID `json:"archived_by"`
|
|
CreatedBy pgtype.UUID `json:"created_by"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
}
|
|
|
|
func (q *Queries) ListProjectTeamsByProjects(ctx context.Context, arg ListProjectTeamsByProjectsParams) ([]ListProjectTeamsByProjectsRow, error) {
|
|
rows, err := q.db.Query(ctx, listProjectTeamsByProjects, arg.WorkspaceID, arg.ProjectIds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListProjectTeamsByProjectsRow{}
|
|
for rows.Next() {
|
|
var i ListProjectTeamsByProjectsRow
|
|
if err := rows.Scan(
|
|
&i.ProjectID,
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Key,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.IssueCounter,
|
|
&i.IsDefault,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.CreatedBy,
|
|
&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 listProjects = `-- name: ListProjects :many
|
|
SELECT id, workspace_id, title, description, icon, status, lead_type, lead_id, created_at, updated_at, priority FROM project
|
|
WHERE project.workspace_id = $1
|
|
AND ($2::uuid IS NULL OR EXISTS (
|
|
SELECT 1 FROM project_team pt
|
|
WHERE pt.project_id = project.id
|
|
AND pt.workspace_id = project.workspace_id
|
|
AND pt.team_id = $2::uuid
|
|
))
|
|
AND ($3::text IS NULL OR project.status = $3)
|
|
AND ($4::text IS NULL OR project.priority = $4)
|
|
ORDER BY created_at DESC
|
|
`
|
|
|
|
type ListProjectsParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
TeamID pgtype.UUID `json:"team_id"`
|
|
Status pgtype.Text `json:"status"`
|
|
Priority pgtype.Text `json:"priority"`
|
|
}
|
|
|
|
func (q *Queries) ListProjects(ctx context.Context, arg ListProjectsParams) ([]Project, error) {
|
|
rows, err := q.db.Query(ctx, listProjects,
|
|
arg.WorkspaceID,
|
|
arg.TeamID,
|
|
arg.Status,
|
|
arg.Priority,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Project{}
|
|
for rows.Next() {
|
|
var i Project
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.Status,
|
|
&i.LeadType,
|
|
&i.LeadID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Priority,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const projectHasTeam = `-- name: ProjectHasTeam :one
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM project_team
|
|
WHERE workspace_id = $1
|
|
AND project_id = $2
|
|
AND team_id = $3
|
|
)::boolean
|
|
`
|
|
|
|
type ProjectHasTeamParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
TeamID pgtype.UUID `json:"team_id"`
|
|
}
|
|
|
|
func (q *Queries) ProjectHasTeam(ctx context.Context, arg ProjectHasTeamParams) (bool, error) {
|
|
row := q.db.QueryRow(ctx, projectHasTeam, arg.WorkspaceID, arg.ProjectID, arg.TeamID)
|
|
var column_1 bool
|
|
err := row.Scan(&column_1)
|
|
return column_1, err
|
|
}
|
|
|
|
const replaceProjectTeams = `-- name: ReplaceProjectTeams :exec
|
|
WITH deleted AS (
|
|
DELETE FROM project_team
|
|
WHERE workspace_id = $1
|
|
AND project_id = $2
|
|
AND NOT (team_id = ANY($3::uuid[]))
|
|
)
|
|
INSERT INTO project_team (workspace_id, project_id, team_id)
|
|
SELECT $1, $2, unnest($3::uuid[])
|
|
ON CONFLICT (project_id, team_id) DO NOTHING
|
|
`
|
|
|
|
type ReplaceProjectTeamsParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
TeamIds []pgtype.UUID `json:"team_ids"`
|
|
}
|
|
|
|
func (q *Queries) ReplaceProjectTeams(ctx context.Context, arg ReplaceProjectTeamsParams) error {
|
|
_, err := q.db.Exec(ctx, replaceProjectTeams, arg.WorkspaceID, arg.ProjectID, arg.TeamIds)
|
|
return err
|
|
}
|
|
|
|
const updateProject = `-- name: UpdateProject :one
|
|
UPDATE project SET
|
|
title = COALESCE($2, title),
|
|
description = $3,
|
|
icon = $4,
|
|
status = COALESCE($5, status),
|
|
priority = COALESCE($6, priority),
|
|
lead_type = $7,
|
|
lead_id = $8,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, workspace_id, title, description, icon, status, lead_type, lead_id, created_at, updated_at, priority
|
|
`
|
|
|
|
type UpdateProjectParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Title pgtype.Text `json:"title"`
|
|
Description pgtype.Text `json:"description"`
|
|
Icon pgtype.Text `json:"icon"`
|
|
Status pgtype.Text `json:"status"`
|
|
Priority pgtype.Text `json:"priority"`
|
|
LeadType pgtype.Text `json:"lead_type"`
|
|
LeadID pgtype.UUID `json:"lead_id"`
|
|
}
|
|
|
|
func (q *Queries) UpdateProject(ctx context.Context, arg UpdateProjectParams) (Project, error) {
|
|
row := q.db.QueryRow(ctx, updateProject,
|
|
arg.ID,
|
|
arg.Title,
|
|
arg.Description,
|
|
arg.Icon,
|
|
arg.Status,
|
|
arg.Priority,
|
|
arg.LeadType,
|
|
arg.LeadID,
|
|
)
|
|
var i Project
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.Status,
|
|
&i.LeadType,
|
|
&i.LeadID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Priority,
|
|
)
|
|
return i, err
|
|
}
|