mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
The uniqueness check on workspace invitations only filtered by status='pending', not by expires_at. Combined with the partial unique index idx_invitation_unique_pending (also keyed only on status), a past-due pending row permanently blocked re-inviting the same email. Now, before creating a new invitation, the handler flips any past-due pending row for the same (workspace_id, invitee_email) to 'expired', freeing the unique slot. Also tightens GetPendingInvitationByEmail to require expires_at > now(), matching the existing list queries. Closes multica-ai/multica#2055.
312 lines
9.3 KiB
Go
312 lines
9.3 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.30.0
|
|
// source: invitation.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const acceptInvitation = `-- name: AcceptInvitation :one
|
|
UPDATE workspace_invitation
|
|
SET status = 'accepted', updated_at = now()
|
|
WHERE id = $1 AND status = 'pending'
|
|
RETURNING id, workspace_id, inviter_id, invitee_email, invitee_user_id, role, status, created_at, updated_at, expires_at
|
|
`
|
|
|
|
func (q *Queries) AcceptInvitation(ctx context.Context, id pgtype.UUID) (WorkspaceInvitation, error) {
|
|
row := q.db.QueryRow(ctx, acceptInvitation, id)
|
|
var i WorkspaceInvitation
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InviterID,
|
|
&i.InviteeEmail,
|
|
&i.InviteeUserID,
|
|
&i.Role,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ExpiresAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createInvitation = `-- name: CreateInvitation :one
|
|
INSERT INTO workspace_invitation (workspace_id, inviter_id, invitee_email, invitee_user_id, role)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING id, workspace_id, inviter_id, invitee_email, invitee_user_id, role, status, created_at, updated_at, expires_at
|
|
`
|
|
|
|
type CreateInvitationParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
InviterID pgtype.UUID `json:"inviter_id"`
|
|
InviteeEmail string `json:"invitee_email"`
|
|
InviteeUserID pgtype.UUID `json:"invitee_user_id"`
|
|
Role string `json:"role"`
|
|
}
|
|
|
|
func (q *Queries) CreateInvitation(ctx context.Context, arg CreateInvitationParams) (WorkspaceInvitation, error) {
|
|
row := q.db.QueryRow(ctx, createInvitation,
|
|
arg.WorkspaceID,
|
|
arg.InviterID,
|
|
arg.InviteeEmail,
|
|
arg.InviteeUserID,
|
|
arg.Role,
|
|
)
|
|
var i WorkspaceInvitation
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InviterID,
|
|
&i.InviteeEmail,
|
|
&i.InviteeUserID,
|
|
&i.Role,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ExpiresAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const declineInvitation = `-- name: DeclineInvitation :one
|
|
UPDATE workspace_invitation
|
|
SET status = 'declined', updated_at = now()
|
|
WHERE id = $1 AND status = 'pending'
|
|
RETURNING id, workspace_id, inviter_id, invitee_email, invitee_user_id, role, status, created_at, updated_at, expires_at
|
|
`
|
|
|
|
func (q *Queries) DeclineInvitation(ctx context.Context, id pgtype.UUID) (WorkspaceInvitation, error) {
|
|
row := q.db.QueryRow(ctx, declineInvitation, id)
|
|
var i WorkspaceInvitation
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InviterID,
|
|
&i.InviteeEmail,
|
|
&i.InviteeUserID,
|
|
&i.Role,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ExpiresAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const expireStalePendingInvitations = `-- name: ExpireStalePendingInvitations :exec
|
|
UPDATE workspace_invitation
|
|
SET status = 'expired', updated_at = now()
|
|
WHERE workspace_id = $1
|
|
AND invitee_email = $2
|
|
AND status = 'pending'
|
|
AND expires_at <= now()
|
|
`
|
|
|
|
type ExpireStalePendingInvitationsParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
InviteeEmail string `json:"invitee_email"`
|
|
}
|
|
|
|
// Mark any past-due pending invitations for (workspace_id, invitee_email) as expired,
|
|
// so the next CreateInvitation does not collide with the partial unique index
|
|
// idx_invitation_unique_pending (which is WHERE status = 'pending' and cannot
|
|
// itself reference now() in its predicate).
|
|
func (q *Queries) ExpireStalePendingInvitations(ctx context.Context, arg ExpireStalePendingInvitationsParams) error {
|
|
_, err := q.db.Exec(ctx, expireStalePendingInvitations, arg.WorkspaceID, arg.InviteeEmail)
|
|
return err
|
|
}
|
|
|
|
const getInvitation = `-- name: GetInvitation :one
|
|
SELECT id, workspace_id, inviter_id, invitee_email, invitee_user_id, role, status, created_at, updated_at, expires_at FROM workspace_invitation
|
|
WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetInvitation(ctx context.Context, id pgtype.UUID) (WorkspaceInvitation, error) {
|
|
row := q.db.QueryRow(ctx, getInvitation, id)
|
|
var i WorkspaceInvitation
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InviterID,
|
|
&i.InviteeEmail,
|
|
&i.InviteeUserID,
|
|
&i.Role,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ExpiresAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getPendingInvitationByEmail = `-- name: GetPendingInvitationByEmail :one
|
|
SELECT id, workspace_id, inviter_id, invitee_email, invitee_user_id, role, status, created_at, updated_at, expires_at FROM workspace_invitation
|
|
WHERE workspace_id = $1 AND invitee_email = $2 AND status = 'pending' AND expires_at > now()
|
|
`
|
|
|
|
type GetPendingInvitationByEmailParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
InviteeEmail string `json:"invitee_email"`
|
|
}
|
|
|
|
func (q *Queries) GetPendingInvitationByEmail(ctx context.Context, arg GetPendingInvitationByEmailParams) (WorkspaceInvitation, error) {
|
|
row := q.db.QueryRow(ctx, getPendingInvitationByEmail, arg.WorkspaceID, arg.InviteeEmail)
|
|
var i WorkspaceInvitation
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InviterID,
|
|
&i.InviteeEmail,
|
|
&i.InviteeUserID,
|
|
&i.Role,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ExpiresAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listPendingInvitationsByWorkspace = `-- name: ListPendingInvitationsByWorkspace :many
|
|
SELECT wi.id, wi.workspace_id, wi.inviter_id, wi.invitee_email, wi.invitee_user_id, wi.role, wi.status, wi.created_at, wi.updated_at, wi.expires_at,
|
|
u.name AS inviter_name,
|
|
u.email AS inviter_email
|
|
FROM workspace_invitation wi
|
|
JOIN "user" u ON u.id = wi.inviter_id
|
|
WHERE wi.workspace_id = $1 AND wi.status = 'pending' AND wi.expires_at > now()
|
|
ORDER BY wi.created_at DESC
|
|
`
|
|
|
|
type ListPendingInvitationsByWorkspaceRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
InviterID pgtype.UUID `json:"inviter_id"`
|
|
InviteeEmail string `json:"invitee_email"`
|
|
InviteeUserID pgtype.UUID `json:"invitee_user_id"`
|
|
Role string `json:"role"`
|
|
Status string `json:"status"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
|
InviterName string `json:"inviter_name"`
|
|
InviterEmail string `json:"inviter_email"`
|
|
}
|
|
|
|
func (q *Queries) ListPendingInvitationsByWorkspace(ctx context.Context, workspaceID pgtype.UUID) ([]ListPendingInvitationsByWorkspaceRow, error) {
|
|
rows, err := q.db.Query(ctx, listPendingInvitationsByWorkspace, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListPendingInvitationsByWorkspaceRow{}
|
|
for rows.Next() {
|
|
var i ListPendingInvitationsByWorkspaceRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InviterID,
|
|
&i.InviteeEmail,
|
|
&i.InviteeUserID,
|
|
&i.Role,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ExpiresAt,
|
|
&i.InviterName,
|
|
&i.InviterEmail,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listPendingInvitationsForUser = `-- name: ListPendingInvitationsForUser :many
|
|
SELECT wi.id, wi.workspace_id, wi.inviter_id, wi.invitee_email, wi.invitee_user_id, wi.role, wi.status, wi.created_at, wi.updated_at, wi.expires_at,
|
|
w.name AS workspace_name,
|
|
u.name AS inviter_name,
|
|
u.email AS inviter_email
|
|
FROM workspace_invitation wi
|
|
JOIN workspace w ON w.id = wi.workspace_id
|
|
JOIN "user" u ON u.id = wi.inviter_id
|
|
WHERE wi.status = 'pending'
|
|
AND (wi.invitee_user_id = $1 OR wi.invitee_email = $2)
|
|
AND wi.expires_at > now()
|
|
ORDER BY wi.created_at DESC
|
|
`
|
|
|
|
type ListPendingInvitationsForUserParams struct {
|
|
InviteeUserID pgtype.UUID `json:"invitee_user_id"`
|
|
InviteeEmail string `json:"invitee_email"`
|
|
}
|
|
|
|
type ListPendingInvitationsForUserRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
InviterID pgtype.UUID `json:"inviter_id"`
|
|
InviteeEmail string `json:"invitee_email"`
|
|
InviteeUserID pgtype.UUID `json:"invitee_user_id"`
|
|
Role string `json:"role"`
|
|
Status string `json:"status"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
|
WorkspaceName string `json:"workspace_name"`
|
|
InviterName string `json:"inviter_name"`
|
|
InviterEmail string `json:"inviter_email"`
|
|
}
|
|
|
|
func (q *Queries) ListPendingInvitationsForUser(ctx context.Context, arg ListPendingInvitationsForUserParams) ([]ListPendingInvitationsForUserRow, error) {
|
|
rows, err := q.db.Query(ctx, listPendingInvitationsForUser, arg.InviteeUserID, arg.InviteeEmail)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListPendingInvitationsForUserRow{}
|
|
for rows.Next() {
|
|
var i ListPendingInvitationsForUserRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InviterID,
|
|
&i.InviteeEmail,
|
|
&i.InviteeUserID,
|
|
&i.Role,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ExpiresAt,
|
|
&i.WorkspaceName,
|
|
&i.InviterName,
|
|
&i.InviterEmail,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const revokeInvitation = `-- name: RevokeInvitation :exec
|
|
DELETE FROM workspace_invitation
|
|
WHERE id = $1 AND status = 'pending'
|
|
`
|
|
|
|
func (q *Queries) RevokeInvitation(ctx context.Context, id pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, revokeInvitation, id)
|
|
return err
|
|
}
|