mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-04 07:34:25 +02:00
Squashed history of PR #4892 (pr-4784-fix). Makes spaces the primary navigation and working surface, on the "associations bind at creation time only" model. Model - Issue <-> space is the only enforced ownership (per-space numbering). Parent/child and project<->space associations only seed defaults at creation; cross-space/child and project-association validations are removed. - Moving an issue renumbers it and records the old identifier in issue_identifier_alias; API/CLI lookups and GitHub branch/PR auto-linking fall back to the alias, so old identifiers resolve forever. - Membership drives only the sidebar and personal defaults — never access. Anyone can configure any space's member set wholesale (PUT /api/spaces/{id}/members); saving an empty set archives the space behind a confirm. - Per-user space order (workspace_space_member.sort_order, fractional): drag-sorted sidebar, "my first space" is the personal issue-creation default; the workspace default space backs headless creation (agents/CLI/Slack) and system placement. Surfaces - Sidebar: joined-spaces section (drag reorder, row -> space page, per-group persisted collapse), Workspace group with a More menu, Settings demoted to a footer icon. - /space/:key/{issues,projects,autopilots,settings} — space surfaces reuse shared page components; a routed /space/new create page (replacing the earlier create-space modal), reserved key "NEW" so it can never collide with a real space's /space/:key detail page. - Issue detail moves to /issue/:id (identifier-first, Linear-style; old /issues/:id redirects); create dialogs lead with a required space pill. - Agent runtime brief now carries Space context (id/key/name) through the daemon claim -> TaskContextForEnv -> prompt pipeline, with a "## Space Context" section and --space on issue create/update in both brief renderers, matching Project's existing treatment. - zh-Hans: Space translated to 空间 across locales and conventions.zh.mdx. Fixes along the way - Cache membership judgment gains the space dimension + space_changed WS flag. - Silent skip on default-space lookup during invite acceptance is now a hard failure (no space-less members). - Backfilled space_id into ~28 raw-SQL Go test fixtures across internal/handler and cmd/server that predated migration 132's NOT NULL cutover. - Fixed a resolve-loop bug in the IssueDetail identifier wrapper (mount/ unmount cycle on resolution failure) and gave it its own loading skeleton instead of a blank screen while resolving. - reserved-slugs generator's stale hardcoded doc-comment example synced back to /create-space. Verification - go build ./..., go vet ./..., go test ./... all clean. - pnpm typecheck (core/views/web/desktop) clean. - packages/views: 162 files / 1656 tests passing. - pnpm generate:reserved-slugs produces no diff. Follow-ups tracked in docs/follow-ups/space-rollout.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
737 lines
20 KiB
Go
737 lines
20 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: space.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const addWorkspaceSpaceMember = `-- name: AddWorkspaceSpaceMember :exec
|
|
INSERT INTO workspace_space_member (workspace_id, space_id, user_id, role, sort_order)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
ON CONFLICT (space_id, user_id) DO UPDATE SET role = EXCLUDED.role
|
|
`
|
|
|
|
type AddWorkspaceSpaceMemberParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
SpaceID pgtype.UUID `json:"space_id"`
|
|
UserID pgtype.UUID `json:"user_id"`
|
|
Role string `json:"role"`
|
|
SortOrder float64 `json:"sort_order"`
|
|
}
|
|
|
|
func (q *Queries) AddWorkspaceSpaceMember(ctx context.Context, arg AddWorkspaceSpaceMemberParams) error {
|
|
_, err := q.db.Exec(ctx, addWorkspaceSpaceMember,
|
|
arg.WorkspaceID,
|
|
arg.SpaceID,
|
|
arg.UserID,
|
|
arg.Role,
|
|
arg.SortOrder,
|
|
)
|
|
return err
|
|
}
|
|
|
|
const archiveWorkspaceSpace = `-- name: ArchiveWorkspaceSpace :one
|
|
UPDATE workspace_space SET
|
|
archived_at = now(),
|
|
archived_by = $3,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
AND workspace_id = $2
|
|
AND archived_at IS NULL
|
|
RETURNING id, workspace_id, name, key, description, icon, issue_counter, archived_at, archived_by, created_by, created_at, updated_at
|
|
`
|
|
|
|
type ArchiveWorkspaceSpaceParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ArchivedBy pgtype.UUID `json:"archived_by"`
|
|
}
|
|
|
|
// Callers must first lock the workspace's active Spaces (see
|
|
// ListActiveWorkspaceSpacesForUpdate) and confirm more than one remains —
|
|
// this query no longer guards that itself.
|
|
func (q *Queries) ArchiveWorkspaceSpace(ctx context.Context, arg ArchiveWorkspaceSpaceParams) (WorkspaceSpace, error) {
|
|
row := q.db.QueryRow(ctx, archiveWorkspaceSpace, arg.ID, arg.WorkspaceID, arg.ArchivedBy)
|
|
var i WorkspaceSpace
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Key,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.IssueCounter,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createWorkspaceSpace = `-- name: CreateWorkspaceSpace :one
|
|
INSERT INTO workspace_space (
|
|
workspace_id, name, key, description, icon, created_by
|
|
) VALUES (
|
|
$1, $2, $3, COALESCE($4::text, ''), $5::text, $6
|
|
) RETURNING id, workspace_id, name, key, description, icon, issue_counter, archived_at, archived_by, created_by, created_at, updated_at
|
|
`
|
|
|
|
type CreateWorkspaceSpaceParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Key string `json:"key"`
|
|
Description pgtype.Text `json:"description"`
|
|
Icon pgtype.Text `json:"icon"`
|
|
CreatedBy pgtype.UUID `json:"created_by"`
|
|
}
|
|
|
|
func (q *Queries) CreateWorkspaceSpace(ctx context.Context, arg CreateWorkspaceSpaceParams) (WorkspaceSpace, error) {
|
|
row := q.db.QueryRow(ctx, createWorkspaceSpace,
|
|
arg.WorkspaceID,
|
|
arg.Name,
|
|
arg.Key,
|
|
arg.Description,
|
|
arg.Icon,
|
|
arg.CreatedBy,
|
|
)
|
|
var i WorkspaceSpace
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Key,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.IssueCounter,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getDefaultWorkspaceSpace = `-- name: GetDefaultWorkspaceSpace :one
|
|
SELECT id, workspace_id, name, key, description, icon, issue_counter, archived_at, archived_by, created_by, created_at, updated_at FROM workspace_space
|
|
WHERE workspace_id = $1 AND archived_at IS NULL
|
|
ORDER BY created_at ASC, id ASC
|
|
LIMIT 1
|
|
`
|
|
|
|
// Fallback Space when none is specified: the workspace's earliest-created
|
|
// active Space. No is_default flag — deterministic from created_at, and
|
|
// always resolvable because ArchiveSpace refuses to archive a workspace's
|
|
// last active Space.
|
|
func (q *Queries) GetDefaultWorkspaceSpace(ctx context.Context, workspaceID pgtype.UUID) (WorkspaceSpace, error) {
|
|
row := q.db.QueryRow(ctx, getDefaultWorkspaceSpace, workspaceID)
|
|
var i WorkspaceSpace
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Key,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.IssueCounter,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getWorkspaceSpace = `-- name: GetWorkspaceSpace :one
|
|
SELECT id, workspace_id, name, key, description, icon, issue_counter, archived_at, archived_by, created_by, created_at, updated_at FROM workspace_space
|
|
WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type GetWorkspaceSpaceParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) GetWorkspaceSpace(ctx context.Context, arg GetWorkspaceSpaceParams) (WorkspaceSpace, error) {
|
|
row := q.db.QueryRow(ctx, getWorkspaceSpace, arg.ID, arg.WorkspaceID)
|
|
var i WorkspaceSpace
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Key,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.IssueCounter,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getWorkspaceSpaceByKey = `-- name: GetWorkspaceSpaceByKey :one
|
|
SELECT id, workspace_id, name, key, description, icon, issue_counter, archived_at, archived_by, created_by, created_at, updated_at FROM workspace_space
|
|
WHERE workspace_id = $1
|
|
AND lower(key) = lower($2)
|
|
LIMIT 1
|
|
`
|
|
|
|
type GetWorkspaceSpaceByKeyParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Lower string `json:"lower"`
|
|
}
|
|
|
|
func (q *Queries) GetWorkspaceSpaceByKey(ctx context.Context, arg GetWorkspaceSpaceByKeyParams) (WorkspaceSpace, error) {
|
|
row := q.db.QueryRow(ctx, getWorkspaceSpaceByKey, arg.WorkspaceID, arg.Lower)
|
|
var i WorkspaceSpace
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Key,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.IssueCounter,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getWorkspaceSpaceMember = `-- name: GetWorkspaceSpaceMember :one
|
|
SELECT workspace_id, space_id, user_id, role, created_at, sort_order FROM workspace_space_member
|
|
WHERE space_id = $1
|
|
AND user_id = $2
|
|
`
|
|
|
|
type GetWorkspaceSpaceMemberParams struct {
|
|
SpaceID pgtype.UUID `json:"space_id"`
|
|
UserID pgtype.UUID `json:"user_id"`
|
|
}
|
|
|
|
func (q *Queries) GetWorkspaceSpaceMember(ctx context.Context, arg GetWorkspaceSpaceMemberParams) (WorkspaceSpaceMember, error) {
|
|
row := q.db.QueryRow(ctx, getWorkspaceSpaceMember, arg.SpaceID, arg.UserID)
|
|
var i WorkspaceSpaceMember
|
|
err := row.Scan(
|
|
&i.WorkspaceID,
|
|
&i.SpaceID,
|
|
&i.UserID,
|
|
&i.Role,
|
|
&i.CreatedAt,
|
|
&i.SortOrder,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const incrementSpaceIssueCounter = `-- name: IncrementSpaceIssueCounter :one
|
|
UPDATE workspace_space
|
|
SET issue_counter = issue_counter + 1,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
AND workspace_id = $2
|
|
AND archived_at IS NULL
|
|
RETURNING issue_counter
|
|
`
|
|
|
|
type IncrementSpaceIssueCounterParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) IncrementSpaceIssueCounter(ctx context.Context, arg IncrementSpaceIssueCounterParams) (int32, error) {
|
|
row := q.db.QueryRow(ctx, incrementSpaceIssueCounter, arg.ID, arg.WorkspaceID)
|
|
var issue_counter int32
|
|
err := row.Scan(&issue_counter)
|
|
return issue_counter, err
|
|
}
|
|
|
|
const listActiveWorkspaceSpaces = `-- name: ListActiveWorkspaceSpaces :many
|
|
SELECT id, workspace_id, name, key, description, icon, issue_counter, archived_at, archived_by, created_by, created_at, updated_at FROM workspace_space
|
|
WHERE workspace_id = $1
|
|
AND archived_at IS NULL
|
|
ORDER BY name ASC, created_at ASC
|
|
`
|
|
|
|
func (q *Queries) ListActiveWorkspaceSpaces(ctx context.Context, workspaceID pgtype.UUID) ([]WorkspaceSpace, error) {
|
|
rows, err := q.db.Query(ctx, listActiveWorkspaceSpaces, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []WorkspaceSpace{}
|
|
for rows.Next() {
|
|
var i WorkspaceSpace
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Key,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.IssueCounter,
|
|
&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 listActiveWorkspaceSpacesForUpdate = `-- name: ListActiveWorkspaceSpacesForUpdate :many
|
|
SELECT id, workspace_id, name, key, description, icon, issue_counter, archived_at, archived_by, created_by, created_at, updated_at FROM workspace_space
|
|
WHERE workspace_id = $1
|
|
AND archived_at IS NULL
|
|
FOR UPDATE
|
|
`
|
|
|
|
// Locks every active Space row for this workspace so a concurrent archive on
|
|
// another Space in the same workspace serializes behind this one. Used by
|
|
// ArchiveSpace to count active Spaces before archiving without racing another
|
|
// archive down to zero.
|
|
func (q *Queries) ListActiveWorkspaceSpacesForUpdate(ctx context.Context, workspaceID pgtype.UUID) ([]WorkspaceSpace, error) {
|
|
rows, err := q.db.Query(ctx, listActiveWorkspaceSpacesForUpdate, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []WorkspaceSpace{}
|
|
for rows.Next() {
|
|
var i WorkspaceSpace
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Key,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.IssueCounter,
|
|
&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 listWorkspaceSpaceMembers = `-- name: ListWorkspaceSpaceMembers :many
|
|
SELECT workspace_id, space_id, user_id, role, created_at, sort_order FROM workspace_space_member
|
|
WHERE workspace_id = $1
|
|
AND space_id = $2
|
|
ORDER BY role ASC, created_at ASC
|
|
`
|
|
|
|
type ListWorkspaceSpaceMembersParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
SpaceID pgtype.UUID `json:"space_id"`
|
|
}
|
|
|
|
func (q *Queries) ListWorkspaceSpaceMembers(ctx context.Context, arg ListWorkspaceSpaceMembersParams) ([]WorkspaceSpaceMember, error) {
|
|
rows, err := q.db.Query(ctx, listWorkspaceSpaceMembers, arg.WorkspaceID, arg.SpaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []WorkspaceSpaceMember{}
|
|
for rows.Next() {
|
|
var i WorkspaceSpaceMember
|
|
if err := rows.Scan(
|
|
&i.WorkspaceID,
|
|
&i.SpaceID,
|
|
&i.UserID,
|
|
&i.Role,
|
|
&i.CreatedAt,
|
|
&i.SortOrder,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listWorkspaceSpaceMembersWithUser = `-- name: ListWorkspaceSpaceMembersWithUser :many
|
|
SELECT m.workspace_id, m.space_id, m.user_id, m.role, m.sort_order, m.created_at,
|
|
u.name AS user_name, u.email AS user_email, u.avatar_url AS user_avatar_url
|
|
FROM workspace_space_member m
|
|
JOIN "user" u ON u.id = m.user_id
|
|
WHERE m.workspace_id = $1
|
|
AND m.space_id = $2
|
|
ORDER BY m.role ASC, m.created_at ASC
|
|
`
|
|
|
|
type ListWorkspaceSpaceMembersWithUserParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
SpaceID pgtype.UUID `json:"space_id"`
|
|
}
|
|
|
|
type ListWorkspaceSpaceMembersWithUserRow struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
SpaceID pgtype.UUID `json:"space_id"`
|
|
UserID pgtype.UUID `json:"user_id"`
|
|
Role string `json:"role"`
|
|
SortOrder float64 `json:"sort_order"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UserName string `json:"user_name"`
|
|
UserEmail string `json:"user_email"`
|
|
UserAvatarUrl pgtype.Text `json:"user_avatar_url"`
|
|
}
|
|
|
|
func (q *Queries) ListWorkspaceSpaceMembersWithUser(ctx context.Context, arg ListWorkspaceSpaceMembersWithUserParams) ([]ListWorkspaceSpaceMembersWithUserRow, error) {
|
|
rows, err := q.db.Query(ctx, listWorkspaceSpaceMembersWithUser, arg.WorkspaceID, arg.SpaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListWorkspaceSpaceMembersWithUserRow{}
|
|
for rows.Next() {
|
|
var i ListWorkspaceSpaceMembersWithUserRow
|
|
if err := rows.Scan(
|
|
&i.WorkspaceID,
|
|
&i.SpaceID,
|
|
&i.UserID,
|
|
&i.Role,
|
|
&i.SortOrder,
|
|
&i.CreatedAt,
|
|
&i.UserName,
|
|
&i.UserEmail,
|
|
&i.UserAvatarUrl,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listWorkspaceSpaces = `-- name: ListWorkspaceSpaces :many
|
|
SELECT id, workspace_id, name, key, description, icon, issue_counter, archived_at, archived_by, created_by, created_at, updated_at FROM workspace_space
|
|
WHERE workspace_id = $1
|
|
ORDER BY archived_at NULLS FIRST, name ASC, created_at ASC
|
|
`
|
|
|
|
func (q *Queries) ListWorkspaceSpaces(ctx context.Context, workspaceID pgtype.UUID) ([]WorkspaceSpace, error) {
|
|
rows, err := q.db.Query(ctx, listWorkspaceSpaces, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []WorkspaceSpace{}
|
|
for rows.Next() {
|
|
var i WorkspaceSpace
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Key,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.IssueCounter,
|
|
&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 listWorkspaceSpacesByIDs = `-- name: ListWorkspaceSpacesByIDs :many
|
|
SELECT id, workspace_id, name, key, description, icon, issue_counter, archived_at, archived_by, created_by, created_at, updated_at FROM workspace_space
|
|
WHERE workspace_id = $1
|
|
AND id = ANY($2::uuid[])
|
|
`
|
|
|
|
type ListWorkspaceSpacesByIDsParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
SpaceIds []pgtype.UUID `json:"space_ids"`
|
|
}
|
|
|
|
func (q *Queries) ListWorkspaceSpacesByIDs(ctx context.Context, arg ListWorkspaceSpacesByIDsParams) ([]WorkspaceSpace, error) {
|
|
rows, err := q.db.Query(ctx, listWorkspaceSpacesByIDs, arg.WorkspaceID, arg.SpaceIds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []WorkspaceSpace{}
|
|
for rows.Next() {
|
|
var i WorkspaceSpace
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Key,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.IssueCounter,
|
|
&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 listWorkspaceSpacesForUser = `-- name: ListWorkspaceSpacesForUser :many
|
|
SELECT wt.id, wt.workspace_id, wt.name, wt.key, wt.description, wt.icon, wt.issue_counter, wt.archived_at, wt.archived_by, wt.created_by, wt.created_at, wt.updated_at,
|
|
(m.user_id IS NOT NULL)::boolean AS is_member,
|
|
COALESCE(m.sort_order, 0)::double precision AS member_sort_order
|
|
FROM workspace_space wt
|
|
LEFT JOIN workspace_space_member m
|
|
ON m.space_id = wt.id AND m.user_id = $2
|
|
WHERE wt.workspace_id = $1
|
|
ORDER BY wt.archived_at NULLS FIRST, wt.name ASC, wt.created_at ASC
|
|
`
|
|
|
|
type ListWorkspaceSpacesForUserParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
UserID pgtype.UUID `json:"user_id"`
|
|
}
|
|
|
|
type ListWorkspaceSpacesForUserRow struct {
|
|
WorkspaceSpace WorkspaceSpace `json:"workspace_space"`
|
|
IsMember bool `json:"is_member"`
|
|
MemberSortOrder float64 `json:"member_sort_order"`
|
|
}
|
|
|
|
// Space list enriched with the requesting user's membership (drives the
|
|
// sidebar Spaces section: only joined spaces, ordered by member sort_order).
|
|
func (q *Queries) ListWorkspaceSpacesForUser(ctx context.Context, arg ListWorkspaceSpacesForUserParams) ([]ListWorkspaceSpacesForUserRow, error) {
|
|
rows, err := q.db.Query(ctx, listWorkspaceSpacesForUser, arg.WorkspaceID, arg.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListWorkspaceSpacesForUserRow{}
|
|
for rows.Next() {
|
|
var i ListWorkspaceSpacesForUserRow
|
|
if err := rows.Scan(
|
|
&i.WorkspaceSpace.ID,
|
|
&i.WorkspaceSpace.WorkspaceID,
|
|
&i.WorkspaceSpace.Name,
|
|
&i.WorkspaceSpace.Key,
|
|
&i.WorkspaceSpace.Description,
|
|
&i.WorkspaceSpace.Icon,
|
|
&i.WorkspaceSpace.IssueCounter,
|
|
&i.WorkspaceSpace.ArchivedAt,
|
|
&i.WorkspaceSpace.ArchivedBy,
|
|
&i.WorkspaceSpace.CreatedBy,
|
|
&i.WorkspaceSpace.CreatedAt,
|
|
&i.WorkspaceSpace.UpdatedAt,
|
|
&i.IsMember,
|
|
&i.MemberSortOrder,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const lockWorkspaceSpaceForKeyUpdate = `-- name: LockWorkspaceSpaceForKeyUpdate :one
|
|
SELECT id, workspace_id, name, key, description, icon, issue_counter, archived_at, archived_by, created_by, created_at, updated_at FROM workspace_space
|
|
WHERE id = $1 AND workspace_id = $2
|
|
FOR UPDATE
|
|
`
|
|
|
|
type LockWorkspaceSpaceForKeyUpdateParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) LockWorkspaceSpaceForKeyUpdate(ctx context.Context, arg LockWorkspaceSpaceForKeyUpdateParams) (WorkspaceSpace, error) {
|
|
row := q.db.QueryRow(ctx, lockWorkspaceSpaceForKeyUpdate, arg.ID, arg.WorkspaceID)
|
|
var i WorkspaceSpace
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Key,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.IssueCounter,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const nextSpaceMemberSortOrder = `-- name: NextSpaceMemberSortOrder :one
|
|
SELECT (COALESCE(MAX(sort_order), 0) + 1)::double precision FROM workspace_space_member
|
|
WHERE workspace_id = $1
|
|
AND user_id = $2
|
|
`
|
|
|
|
type NextSpaceMemberSortOrderParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
UserID pgtype.UUID `json:"user_id"`
|
|
}
|
|
|
|
// Next slot at the end of this user's space list (per-user ordering).
|
|
func (q *Queries) NextSpaceMemberSortOrder(ctx context.Context, arg NextSpaceMemberSortOrderParams) (float64, error) {
|
|
row := q.db.QueryRow(ctx, nextSpaceMemberSortOrder, arg.WorkspaceID, arg.UserID)
|
|
var column_1 float64
|
|
err := row.Scan(&column_1)
|
|
return column_1, err
|
|
}
|
|
|
|
const removeWorkspaceSpaceMember = `-- name: RemoveWorkspaceSpaceMember :execrows
|
|
DELETE FROM workspace_space_member
|
|
WHERE workspace_id = $1
|
|
AND space_id = $2
|
|
AND user_id = $3
|
|
`
|
|
|
|
type RemoveWorkspaceSpaceMemberParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
SpaceID pgtype.UUID `json:"space_id"`
|
|
UserID pgtype.UUID `json:"user_id"`
|
|
}
|
|
|
|
func (q *Queries) RemoveWorkspaceSpaceMember(ctx context.Context, arg RemoveWorkspaceSpaceMemberParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, removeWorkspaceSpaceMember, arg.WorkspaceID, arg.SpaceID, arg.UserID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const updateSpaceMemberSortOrder = `-- name: UpdateSpaceMemberSortOrder :one
|
|
UPDATE workspace_space_member
|
|
SET sort_order = $4
|
|
WHERE workspace_id = $1
|
|
AND space_id = $2
|
|
AND user_id = $3
|
|
RETURNING workspace_id, space_id, user_id, role, created_at, sort_order
|
|
`
|
|
|
|
type UpdateSpaceMemberSortOrderParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
SpaceID pgtype.UUID `json:"space_id"`
|
|
UserID pgtype.UUID `json:"user_id"`
|
|
SortOrder float64 `json:"sort_order"`
|
|
}
|
|
|
|
func (q *Queries) UpdateSpaceMemberSortOrder(ctx context.Context, arg UpdateSpaceMemberSortOrderParams) (WorkspaceSpaceMember, error) {
|
|
row := q.db.QueryRow(ctx, updateSpaceMemberSortOrder,
|
|
arg.WorkspaceID,
|
|
arg.SpaceID,
|
|
arg.UserID,
|
|
arg.SortOrder,
|
|
)
|
|
var i WorkspaceSpaceMember
|
|
err := row.Scan(
|
|
&i.WorkspaceID,
|
|
&i.SpaceID,
|
|
&i.UserID,
|
|
&i.Role,
|
|
&i.CreatedAt,
|
|
&i.SortOrder,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateWorkspaceSpace = `-- name: UpdateWorkspaceSpace :one
|
|
UPDATE workspace_space SET
|
|
name = COALESCE($3, name),
|
|
key = COALESCE($4, key),
|
|
description = COALESCE($5, description),
|
|
icon = COALESCE($6, icon),
|
|
updated_at = now()
|
|
WHERE id = $1 AND workspace_id = $2
|
|
RETURNING id, workspace_id, name, key, description, icon, issue_counter, archived_at, archived_by, created_by, created_at, updated_at
|
|
`
|
|
|
|
type UpdateWorkspaceSpaceParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name pgtype.Text `json:"name"`
|
|
Key pgtype.Text `json:"key"`
|
|
Description pgtype.Text `json:"description"`
|
|
Icon pgtype.Text `json:"icon"`
|
|
}
|
|
|
|
func (q *Queries) UpdateWorkspaceSpace(ctx context.Context, arg UpdateWorkspaceSpaceParams) (WorkspaceSpace, error) {
|
|
row := q.db.QueryRow(ctx, updateWorkspaceSpace,
|
|
arg.ID,
|
|
arg.WorkspaceID,
|
|
arg.Name,
|
|
arg.Key,
|
|
arg.Description,
|
|
arg.Icon,
|
|
)
|
|
var i WorkspaceSpace
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Key,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.IssueCounter,
|
|
&i.ArchivedAt,
|
|
&i.ArchivedBy,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|