Files
multica/server/pkg/db/generated/dingtalk.sql.go
T
2026-08-19 02:03:12 +08:00

566 lines
20 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: dingtalk.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const deleteDingTalkInstallationForReplacement = `-- name: DeleteDingTalkInstallationForReplacement :one
WITH retired AS (
DELETE FROM channel_installation ci
WHERE ci.id = $1
AND ci.workspace_id = $2
AND ci.agent_id = $3
AND ci.channel_type = 'dingtalk'
RETURNING ci.id
),
cleared_chat_sessions AS (
DELETE FROM channel_chat_session_binding
WHERE installation_id IN (SELECT id FROM retired)
RETURNING chat_session_id
),
cleared_group_routes AS (
DELETE FROM dingtalk_group_route
WHERE installation_id IN (SELECT id FROM retired)
),
cleared_outbound_cards AS (
DELETE FROM channel_outbound_card_message
WHERE chat_session_id IN (SELECT chat_session_id FROM cleared_chat_sessions)
),
cleared_binding_tokens AS (
DELETE FROM channel_binding_token
WHERE installation_id IN (SELECT id FROM retired)
),
cleared_user_bindings AS (
DELETE FROM channel_user_binding
WHERE installation_id IN (SELECT id FROM retired)
),
cleared_inbound_dedup AS (
DELETE FROM channel_inbound_message_dedup
WHERE installation_id IN (SELECT id FROM retired)
),
detached_audit AS (
UPDATE channel_inbound_audit SET installation_id = NULL
WHERE installation_id IN (SELECT id FROM retired)
),
detached_media_intents AS (
UPDATE channel_media_pending_object SET installation_id = NULL
WHERE installation_id IN (SELECT id FROM retired)
)
SELECT retired.id FROM retired
`
type DeleteDingTalkInstallationForReplacementParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
AgentID pgtype.UUID `json:"agent_id"`
}
// Retires an installation when the same agent is connected with a DIFFERENT
// AppKey. A senderStaffId is scoped to one DingTalk organization, so none of the
// old installation's identity, token, session, dedup, or outbound state may
// cross into the new robot. The caller inserts a fresh installation in the same
// transaction, giving the replacement a new installation_id that also fences
// late writes and replies from the old connection.
//
// Chat sessions themselves remain as history, but their channel bindings and
// outbound cards are removed. Audit and media-intent rows remain useful for
// diagnostics / reconciliation, so their installation references are detached.
func (q *Queries) DeleteDingTalkInstallationForReplacement(ctx context.Context, arg DeleteDingTalkInstallationForReplacementParams) (pgtype.UUID, error) {
row := q.db.QueryRow(ctx, deleteDingTalkInstallationForReplacement, arg.InstallationID, arg.WorkspaceID, arg.AgentID)
var id pgtype.UUID
err := row.Scan(&id)
return id, err
}
const deleteDingTalkStaleGroupChatBinding = `-- name: DeleteDingTalkStaleGroupChatBinding :one
WITH cleared AS (
DELETE FROM channel_chat_session_binding b
WHERE b.installation_id = $1
AND b.channel_type = 'dingtalk'
AND b.channel_chat_id = $2::text
AND COALESCE(
NULLIF(b.config ->> 'agent_id', ''),
(
SELECT i.agent_id::text
FROM channel_installation i
WHERE i.id = b.installation_id
),
''
) <> $3::uuid::text
RETURNING b.chat_session_id
), cleared_outbound_cards AS (
DELETE FROM channel_outbound_card_message
WHERE chat_session_id IN (SELECT chat_session_id FROM cleared)
)
SELECT count(*)::bigint AS cleared_count
FROM cleared
`
type DeleteDingTalkStaleGroupChatBindingParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
ConversationID string `json:"conversation_id"`
AgentID pgtype.UUID `json:"agent_id"`
}
// A route reassignment normally removes the group's binding in the same query.
// An inbound message that resolved immediately before the reassignment can,
// however, finish creating its old-agent binding immediately afterwards. Every
// subsequent group message runs this guard before EnsureSession. Legacy
// bindings have no agent stamp; preserve them when the effective route is still
// the installation's original/default agent, but retire them after a genuine
// reassignment so the new route can never inherit the old agent's transcript.
func (q *Queries) DeleteDingTalkStaleGroupChatBinding(ctx context.Context, arg DeleteDingTalkStaleGroupChatBindingParams) (int64, error) {
row := q.db.QueryRow(ctx, deleteDingTalkStaleGroupChatBinding, arg.InstallationID, arg.ConversationID, arg.AgentID)
var cleared_count int64
err := row.Scan(&cleared_count)
return cleared_count, err
}
const dingTalkGroupRouteMatchesAgent = `-- name: DingTalkGroupRouteMatchesAgent :one
SELECT EXISTS (
SELECT 1
FROM dingtalk_group_route r
JOIN agent a ON a.id = r.agent_id
AND a.workspace_id = r.workspace_id
WHERE r.installation_id = $1
AND r.conversation_id = $2::text
AND r.agent_id = $3
AND r.revision = $4
AND a.kind = 'user'
AND a.archived_at IS NULL
) AS matches
`
type DingTalkGroupRouteMatchesAgentParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
ConversationID string `json:"conversation_id"`
AgentID pgtype.UUID `json:"agent_id"`
RouteRevision int64 `json:"route_revision"`
}
func (q *Queries) DingTalkGroupRouteMatchesAgent(ctx context.Context, arg DingTalkGroupRouteMatchesAgentParams) (bool, error) {
row := q.db.QueryRow(ctx, dingTalkGroupRouteMatchesAgent,
arg.InstallationID,
arg.ConversationID,
arg.AgentID,
arg.RouteRevision,
)
var matches bool
err := row.Scan(&matches)
return matches, err
}
const discoverDingTalkGroupRoute = `-- name: DiscoverDingTalkGroupRoute :one
WITH workspace_guard AS MATERIALIZED (
SELECT w.id
FROM workspace w
WHERE w.id = $1
FOR KEY SHARE
), installation AS MATERIALIZED (
SELECT i.id, i.workspace_id, i.agent_id, i.channel_type, i.config, i.status, i.ws_lease_token, i.ws_lease_expires_at, i.installer_user_id, i.installed_at, i.created_at, i.updated_at
FROM channel_installation i
JOIN workspace_guard w ON w.id = i.workspace_id
WHERE i.id = $2
AND i.workspace_id = $1
AND i.channel_type = 'dingtalk'
AND i.status = 'active'
FOR SHARE OF i
), group_route AS (
INSERT INTO dingtalk_group_route (
workspace_id, installation_id, conversation_id,
conversation_title, agent_id
)
SELECT
i.workspace_id, i.id, $3::text,
$4::text, i.agent_id
FROM installation i
ON CONFLICT (installation_id, conversation_id) DO UPDATE SET
conversation_title = CASE
WHEN EXCLUDED.conversation_title = '' THEN dingtalk_group_route.conversation_title
ELSE EXCLUDED.conversation_title
END,
updated_at = CASE
WHEN EXCLUDED.conversation_title <> ''
AND EXCLUDED.conversation_title IS DISTINCT FROM dingtalk_group_route.conversation_title
THEN now()
ELSE dingtalk_group_route.updated_at
END
RETURNING agent_id, workspace_id, revision
)
SELECT r.agent_id,
r.revision,
EXISTS (
SELECT 1 FROM agent a
WHERE a.id = r.agent_id
AND a.workspace_id = r.workspace_id
AND a.kind = 'user'
AND a.archived_at IS NULL
) AS agent_active
FROM group_route r
`
type DiscoverDingTalkGroupRouteParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
InstallationID pgtype.UUID `json:"installation_id"`
ConversationID string `json:"conversation_id"`
ConversationTitle string `json:"conversation_title"`
}
type DiscoverDingTalkGroupRouteRow struct {
AgentID pgtype.UUID `json:"agent_id"`
Revision int64 `json:"revision"`
AgentActive bool `json:"agent_active"`
}
// Persist a group only after the shared inbound router has accepted the @bot
// message and validated the sender's identity/workspace membership. The INSERT
// default is the installation's agent; a later admin reassignment survives
// subsequent inbound events because the conflict update refreshes only the
// human-readable title. The returned active bit revalidates the effective route
// target at runtime while preserving the route across archive/restore. The
// workspace/installation locks serialize discovery with teardown and revoke:
// revoke-first rechecks status after the wait and returns no row; discovery-
// first completes before revoke, matching the existing in-flight semantics.
func (q *Queries) DiscoverDingTalkGroupRoute(ctx context.Context, arg DiscoverDingTalkGroupRouteParams) (DiscoverDingTalkGroupRouteRow, error) {
row := q.db.QueryRow(ctx, discoverDingTalkGroupRoute,
arg.WorkspaceID,
arg.InstallationID,
arg.ConversationID,
arg.ConversationTitle,
)
var i DiscoverDingTalkGroupRouteRow
err := row.Scan(&i.AgentID, &i.Revision, &i.AgentActive)
return i, err
}
const getDingTalkGroupRouteInWorkspace = `-- name: GetDingTalkGroupRouteInWorkspace :one
SELECT r.id, r.workspace_id, r.installation_id, r.conversation_id, r.conversation_title, r.agent_id, r.revision, r.discovered_at, r.updated_at
FROM dingtalk_group_route r
JOIN channel_installation i ON i.id = r.installation_id
WHERE r.id = $1
AND r.workspace_id = $2
AND i.workspace_id = $2
AND i.channel_type = 'dingtalk'
AND i.status = 'active'
`
type GetDingTalkGroupRouteInWorkspaceParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
// Handler diagnosis deliberately uses the same active-installation boundary as
// reassignment. Retained routes stay available for reconnect internally, but a
// revoked installation's route is not a PATCH-visible resource.
func (q *Queries) GetDingTalkGroupRouteInWorkspace(ctx context.Context, arg GetDingTalkGroupRouteInWorkspaceParams) (DingtalkGroupRoute, error) {
row := q.db.QueryRow(ctx, getDingTalkGroupRouteInWorkspace, arg.ID, arg.WorkspaceID)
var i DingtalkGroupRoute
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.InstallationID,
&i.ConversationID,
&i.ConversationTitle,
&i.AgentID,
&i.Revision,
&i.DiscoveredAt,
&i.UpdatedAt,
)
return i, err
}
const getDingTalkInstallationOwnerForUpdate = `-- name: GetDingTalkInstallationOwnerForUpdate :one
SELECT id, COALESCE(config ->> 'app_id', '')::text AS app_id
FROM channel_installation
WHERE workspace_id = $1
AND agent_id = $2
AND channel_type = 'dingtalk'
FOR UPDATE
`
type GetDingTalkInstallationOwnerForUpdateParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
AgentID pgtype.UUID `json:"agent_id"`
}
type GetDingTalkInstallationOwnerForUpdateRow struct {
ID pgtype.UUID `json:"id"`
AppID string `json:"app_id"`
}
// Reads the current robot identity after LockDingTalkInstallationOwner has
// serialized the logical owner slot. app_id is non-null for every DingTalk
// installation; COALESCE treats malformed legacy config as a different robot,
// which safely replaces it instead of preserving unknown identity state.
func (q *Queries) GetDingTalkInstallationOwnerForUpdate(ctx context.Context, arg GetDingTalkInstallationOwnerForUpdateParams) (GetDingTalkInstallationOwnerForUpdateRow, error) {
row := q.db.QueryRow(ctx, getDingTalkInstallationOwnerForUpdate, arg.WorkspaceID, arg.AgentID)
var i GetDingTalkInstallationOwnerForUpdateRow
err := row.Scan(&i.ID, &i.AppID)
return i, err
}
const listDingTalkGroupRoutesByWorkspace = `-- name: ListDingTalkGroupRoutesByWorkspace :many
SELECT r.id, r.workspace_id, r.installation_id, r.conversation_id, r.conversation_title, r.agent_id, r.revision, r.discovered_at, r.updated_at
FROM dingtalk_group_route r
JOIN channel_installation i ON i.id = r.installation_id
WHERE r.workspace_id = $1
AND i.workspace_id = $1
AND i.channel_type = 'dingtalk'
AND i.status = 'active'
ORDER BY r.discovered_at ASC
`
func (q *Queries) ListDingTalkGroupRoutesByWorkspace(ctx context.Context, workspaceID pgtype.UUID) ([]DingtalkGroupRoute, error) {
rows, err := q.db.Query(ctx, listDingTalkGroupRoutesByWorkspace, workspaceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []DingtalkGroupRoute{}
for rows.Next() {
var i DingtalkGroupRoute
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.InstallationID,
&i.ConversationID,
&i.ConversationTitle,
&i.AgentID,
&i.Revision,
&i.DiscoveredAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listDingTalkUserBindingsForMember = `-- name: ListDingTalkUserBindingsForMember :many
SELECT installation_id, channel_user_id
FROM channel_user_binding
WHERE workspace_id = $1
AND multica_user_id = $2
AND channel_type = 'dingtalk'
ORDER BY bound_at DESC, id ASC
`
type ListDingTalkUserBindingsForMemberParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
MulticaUserID pgtype.UUID `json:"multica_user_id"`
}
type ListDingTalkUserBindingsForMemberRow struct {
InstallationID pgtype.UUID `json:"installation_id"`
ChannelUserID string `json:"channel_user_id"`
}
// DingTalk-specific installation identity operations. The underlying channel_*
// tables are shared, but these replacement semantics belong to DingTalk's BYO
// AppKey model and deliberately stay out of the shared channel query surface.
// Returns only the requesting Multica member's DingTalk identities. The
// installation list is member-visible, so returning every member's staff id
// here would expose staff ID values more broadly than necessary.
func (q *Queries) ListDingTalkUserBindingsForMember(ctx context.Context, arg ListDingTalkUserBindingsForMemberParams) ([]ListDingTalkUserBindingsForMemberRow, error) {
rows, err := q.db.Query(ctx, listDingTalkUserBindingsForMember, arg.WorkspaceID, arg.MulticaUserID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListDingTalkUserBindingsForMemberRow{}
for rows.Next() {
var i ListDingTalkUserBindingsForMemberRow
if err := rows.Scan(&i.InstallationID, &i.ChannelUserID); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const lockDingTalkGroupRouteForAppend = `-- name: LockDingTalkGroupRouteForAppend :one
WITH target_agent AS MATERIALIZED (
SELECT a.id
FROM agent a
WHERE a.id = $3
AND a.kind = 'user'
AND a.archived_at IS NULL
FOR SHARE
)
SELECT r.revision
FROM dingtalk_group_route r
WHERE r.installation_id = $1
AND r.conversation_id = $2::text
AND r.agent_id = $3
AND r.revision = $4
AND EXISTS (SELECT 1 FROM target_agent)
FOR SHARE OF r
`
type LockDingTalkGroupRouteForAppendParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
ConversationID string `json:"conversation_id"`
AgentID pgtype.UUID `json:"agent_id"`
RouteRevision int64 `json:"route_revision"`
}
// This is the durable cutover fence. The active target agent and route are
// locked in the same order as ReassignDingTalkGroupRoute, and the lock remains
// held by AppendUserMessage's transaction until the message and dedup mark
// commit. A PATCH that wins first changes revision and produces no row; an
// inbound append that wins first makes PATCH wait until that append commits.
func (q *Queries) LockDingTalkGroupRouteForAppend(ctx context.Context, arg LockDingTalkGroupRouteForAppendParams) (int64, error) {
row := q.db.QueryRow(ctx, lockDingTalkGroupRouteForAppend,
arg.InstallationID,
arg.ConversationID,
arg.AgentID,
arg.RouteRevision,
)
var revision int64
err := row.Scan(&revision)
return revision, err
}
const lockDingTalkInstallationOwner = `-- name: LockDingTalkInstallationOwner :exec
SELECT pg_advisory_xact_lock(
hashtextextended(
($1::uuid)::text || ':' ||
($2::uuid)::text || ':dingtalk',
0
)
)
`
type LockDingTalkInstallationOwnerParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
AgentID pgtype.UUID `json:"agent_id"`
}
// Serializes install / replacement decisions for one logical
// (workspace, agent, channel) slot. A different-AppKey replacement deletes the
// old row and inserts a fresh installation id; the advisory lock closes the
// gap where two concurrent replacements could otherwise miss each other's new
// row and update it in place, carrying identity state across robot boundaries.
func (q *Queries) LockDingTalkInstallationOwner(ctx context.Context, arg LockDingTalkInstallationOwnerParams) error {
_, err := q.db.Exec(ctx, lockDingTalkInstallationOwner, arg.WorkspaceID, arg.AgentID)
return err
}
const reassignDingTalkGroupRoute = `-- name: ReassignDingTalkGroupRoute :one
WITH workspace_guard AS MATERIALIZED (
SELECT w.id
FROM workspace w
WHERE w.id = $1
FOR KEY SHARE
), target_agent AS MATERIALIZED (
SELECT a.id
FROM agent a
JOIN workspace_guard w ON w.id = a.workspace_id
WHERE a.id = $2
AND a.workspace_id = $1
AND a.kind = 'user'
AND a.archived_at IS NULL
FOR SHARE
), active_installation AS MATERIALIZED (
SELECT i.id
FROM channel_installation i
JOIN dingtalk_group_route r ON r.installation_id = i.id
WHERE r.id = $3
AND r.workspace_id = $1
AND i.workspace_id = $1
AND i.channel_type = 'dingtalk'
AND i.status = 'active'
AND EXISTS (SELECT 1 FROM target_agent)
FOR SHARE OF i
), target AS (
SELECT r.id, r.workspace_id, r.installation_id, r.conversation_id, r.conversation_title, r.agent_id, r.revision, r.discovered_at, r.updated_at, r.agent_id AS previous_agent_id
FROM dingtalk_group_route r
JOIN active_installation i ON i.id = r.installation_id
WHERE r.id = $3
AND r.workspace_id = $1
FOR UPDATE OF r
), updated AS (
UPDATE dingtalk_group_route r
SET agent_id = $2,
revision = r.revision + 1,
updated_at = now()
FROM target t
WHERE r.id = t.id
RETURNING r.id, r.workspace_id, r.installation_id, r.conversation_id, r.conversation_title, r.agent_id, r.revision, r.discovered_at, r.updated_at, t.previous_agent_id
), cleared_chat_sessions AS (
DELETE FROM channel_chat_session_binding b
USING updated u
WHERE u.previous_agent_id IS DISTINCT FROM u.agent_id
AND b.installation_id = u.installation_id
AND b.channel_chat_id = u.conversation_id
RETURNING b.chat_session_id
), cleared_outbound_cards AS (
DELETE FROM channel_outbound_card_message
WHERE chat_session_id IN (SELECT chat_session_id FROM cleared_chat_sessions)
)
SELECT id, workspace_id, installation_id, conversation_id,
conversation_title, agent_id, revision, discovered_at, updated_at
FROM updated
`
type ReassignDingTalkGroupRouteParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
AgentID pgtype.UUID `json:"agent_id"`
ID pgtype.UUID `json:"id"`
}
type ReassignDingTalkGroupRouteRow struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
InstallationID pgtype.UUID `json:"installation_id"`
ConversationID string `json:"conversation_id"`
ConversationTitle string `json:"conversation_title"`
AgentID pgtype.UUID `json:"agent_id"`
Revision int64 `json:"revision"`
DiscoveredAt pgtype.Timestamptz `json:"discovered_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
// Reassigning a group must also sever its existing chat-session binding. The
// next message creates a fresh session for the new agent, so transcripts and
// pending outbound updates cannot cross agent boundaries. The old chat session
// remains as history; only its DingTalk route and outbound card state are
// removed. The target agent existence/workspace/archive check is repeated here
// so a concurrent archive or delete cannot create a dangling route. The active
// workspace, active installation, and route locks preserve the teardown order;
// the installation is share-locked before the route is update-locked. Revoke
// takes an exclusive lock on that same installation row, defining the race:
// PATCH-first may finish before revoke, while revoke-first makes PATCH wait,
// re-check status, and return no row without touching the route or binding.
func (q *Queries) ReassignDingTalkGroupRoute(ctx context.Context, arg ReassignDingTalkGroupRouteParams) (ReassignDingTalkGroupRouteRow, error) {
row := q.db.QueryRow(ctx, reassignDingTalkGroupRoute, arg.WorkspaceID, arg.AgentID, arg.ID)
var i ReassignDingTalkGroupRouteRow
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.InstallationID,
&i.ConversationID,
&i.ConversationTitle,
&i.AgentID,
&i.Revision,
&i.DiscoveredAt,
&i.UpdatedAt,
)
return i, err
}