forked from farhoodlabs/paperclip
Merge upstream/master into dev (13 commits — includes #5922, #5938, blocked inbox, recovery actions)
This commit is contained in:
@@ -42,6 +42,7 @@ import {
|
||||
heartbeatService,
|
||||
ISSUE_LIST_DEFAULT_LIMIT,
|
||||
issueApprovalService,
|
||||
issueRecoveryActionService,
|
||||
issueService,
|
||||
logActivity,
|
||||
syncInstructionsBundleConfigFromFilePath,
|
||||
@@ -1742,16 +1743,18 @@ export function agentRoutes(
|
||||
}
|
||||
|
||||
const issuesSvc = issueService(db);
|
||||
const recoveryActionsSvc = issueRecoveryActionService(db);
|
||||
const rows = await issuesSvc.list(req.actor.companyId, {
|
||||
assigneeAgentId: req.actor.agentId,
|
||||
status: "todo,in_progress,blocked",
|
||||
includeRoutineExecutions: true,
|
||||
limit: ISSUE_LIST_DEFAULT_LIMIT,
|
||||
});
|
||||
const dependencyReadiness = await issuesSvc.listDependencyReadiness(
|
||||
req.actor.companyId,
|
||||
rows.map((issue) => issue.id),
|
||||
);
|
||||
const issueIds = rows.map((issue) => issue.id);
|
||||
const [dependencyReadiness, recoveryActionByIssue] = await Promise.all([
|
||||
issuesSvc.listDependencyReadiness(req.actor.companyId, issueIds),
|
||||
recoveryActionsSvc.listActiveForIssues(req.actor.companyId, issueIds),
|
||||
]);
|
||||
|
||||
res.json(
|
||||
rows.map((issue) => ({
|
||||
@@ -1765,6 +1768,7 @@ export function agentRoutes(
|
||||
parentId: issue.parentId,
|
||||
updatedAt: issue.updatedAt,
|
||||
activeRun: issue.activeRun,
|
||||
activeRecoveryAction: recoveryActionByIssue.get(issue.id) ?? null,
|
||||
dependencyReady: dependencyReadiness.get(issue.id)?.isDependencyReady ?? true,
|
||||
unresolvedBlockerCount: dependencyReadiness.get(issue.id)?.unresolvedBlockerCount ?? 0,
|
||||
unresolvedBlockerIssueIds: dependencyReadiness.get(issue.id)?.unresolvedBlockerIssueIds ?? [],
|
||||
|
||||
+279
-11
@@ -2,9 +2,16 @@ import { randomUUID } from "node:crypto";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import multer from "multer";
|
||||
import { z } from "zod";
|
||||
import { and, desc, eq, inArray } from "drizzle-orm";
|
||||
import { and, desc, eq, inArray, notInArray } from "drizzle-orm";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { activityLog, executionWorkspaces, issueExecutionDecisions, projectWorkspaces } from "@paperclipai/db";
|
||||
import {
|
||||
activityLog,
|
||||
executionWorkspaces,
|
||||
issueExecutionDecisions,
|
||||
issueRelations,
|
||||
issues as issueRows,
|
||||
projectWorkspaces,
|
||||
} from "@paperclipai/db";
|
||||
import {
|
||||
addIssueCommentSchema,
|
||||
acceptIssueThreadInteractionSchema,
|
||||
@@ -18,6 +25,7 @@ import {
|
||||
createChildIssueSchema,
|
||||
createIssueSchema,
|
||||
resolveCreateIssueStatusDefault,
|
||||
resolveIssueRecoveryActionSchema,
|
||||
feedbackTargetTypeSchema,
|
||||
feedbackTraceStatusSchema,
|
||||
feedbackVoteValueSchema,
|
||||
@@ -37,6 +45,7 @@ import {
|
||||
type CompanySearchQuery,
|
||||
type CompanySearchResponse,
|
||||
type ExecutionWorkspace,
|
||||
type IssueRelationIssueSummary,
|
||||
type SuccessfulRunHandoffState,
|
||||
} from "@paperclipai/shared";
|
||||
import { trackAgentTaskCompleted } from "@paperclipai/shared/telemetry";
|
||||
@@ -53,6 +62,7 @@ import {
|
||||
goalService,
|
||||
heartbeatService,
|
||||
issueApprovalService,
|
||||
issueRecoveryActionService,
|
||||
issueThreadInteractionService,
|
||||
ISSUE_LIST_DEFAULT_LIMIT,
|
||||
ISSUE_LIST_MAX_LIMIT,
|
||||
@@ -405,6 +415,47 @@ async function listSuccessfulRunHandoffStates(
|
||||
return states;
|
||||
}
|
||||
|
||||
type RecoveryActionsLister = {
|
||||
listActiveForIssues: (
|
||||
companyId: string,
|
||||
sourceIssueIds: string[],
|
||||
) => Promise<Map<string, NonNullable<IssueRelationIssueSummary["activeRecoveryAction"]>>>;
|
||||
};
|
||||
|
||||
async function relationRecoveryActionMap(
|
||||
recoveryActionsSvc: RecoveryActionsLister,
|
||||
companyId: string,
|
||||
relations: { blockedBy: IssueRelationIssueSummary[]; blocks: IssueRelationIssueSummary[] },
|
||||
): Promise<Map<string, NonNullable<IssueRelationIssueSummary["activeRecoveryAction"]>>> {
|
||||
const candidates: IssueRelationIssueSummary[] = [];
|
||||
const visit = (summary: IssueRelationIssueSummary) => {
|
||||
candidates.push(summary);
|
||||
for (const terminal of summary.terminalBlockers ?? []) {
|
||||
visit(terminal);
|
||||
}
|
||||
};
|
||||
for (const blocker of relations.blockedBy) visit(blocker);
|
||||
for (const blocking of relations.blocks) visit(blocking);
|
||||
if (candidates.length === 0) return new Map();
|
||||
const ids = [...new Set(candidates.map((summary) => summary.id))];
|
||||
return recoveryActionsSvc.listActiveForIssues(companyId, ids);
|
||||
}
|
||||
|
||||
function withRecoveryActionsOnRelationSummaries(
|
||||
relations: { blockedBy: IssueRelationIssueSummary[]; blocks: IssueRelationIssueSummary[] },
|
||||
recoveryActionByIssueId: Map<string, NonNullable<IssueRelationIssueSummary["activeRecoveryAction"]>>,
|
||||
) {
|
||||
const augment = (summary: IssueRelationIssueSummary): IssueRelationIssueSummary => ({
|
||||
...summary,
|
||||
activeRecoveryAction: recoveryActionByIssueId.get(summary.id) ?? summary.activeRecoveryAction ?? null,
|
||||
terminalBlockers: summary.terminalBlockers?.map(augment),
|
||||
});
|
||||
return {
|
||||
blockedBy: relations.blockedBy.map(augment),
|
||||
blocks: relations.blocks.map(augment),
|
||||
};
|
||||
}
|
||||
|
||||
const ACTIVE_REVIEW_APPROVAL_STATUSES = new Set(["pending", "revision_requested"]);
|
||||
|
||||
const INVALID_AGENT_IN_REVIEW_DISPOSITION_MESSAGE =
|
||||
@@ -787,6 +838,7 @@ export function issueRoutes(
|
||||
const projectsSvc = projectService(db);
|
||||
const goalsSvc = goalService(db);
|
||||
const issueApprovalsSvc = issueApprovalService(db);
|
||||
const recoveryActionsSvc = issueRecoveryActionService(db);
|
||||
const executionWorkspacesSvc = executionWorkspaceServiceDirect(db);
|
||||
const workProductsSvc = workProductService(db);
|
||||
const documentsSvc = documentService(db);
|
||||
@@ -1392,6 +1444,7 @@ export function issueRoutes(
|
||||
const parsedOffset = rawOffset !== undefined && /^\d+$/.test(rawOffset)
|
||||
? Number.parseInt(rawOffset, 10)
|
||||
: null;
|
||||
const attention = req.query.attention as string | undefined;
|
||||
|
||||
if (assigneeUserFilterRaw === "me" && (!assigneeUserId || req.actor.type !== "board")) {
|
||||
res.status(403).json({ error: "assigneeUserId=me requires board authentication" });
|
||||
@@ -1409,6 +1462,10 @@ export function issueRoutes(
|
||||
res.status(403).json({ error: "unreadForUserId=me requires board authentication" });
|
||||
return;
|
||||
}
|
||||
if (attention !== undefined && attention !== "blocked") {
|
||||
res.status(400).json({ error: "attention must be 'blocked' when provided" });
|
||||
return;
|
||||
}
|
||||
if (rawLimit !== undefined && (parsedLimit === null || !Number.isInteger(parsedLimit) || parsedLimit <= 0)) {
|
||||
res.status(400).json({ error: `limit must be a positive integer up to ${ISSUE_LIST_MAX_LIMIT}` });
|
||||
return;
|
||||
@@ -1420,6 +1477,7 @@ export function issueRoutes(
|
||||
const offset = parsedOffset ?? 0;
|
||||
|
||||
const result = await svc.list(companyId, {
|
||||
attention: attention === "blocked" ? "blocked" : undefined,
|
||||
status: req.query.status as string | undefined,
|
||||
assigneeAgentId: req.query.assigneeAgentId as string | undefined,
|
||||
participantAgentId: req.query.participantAgentId as string | undefined,
|
||||
@@ -1443,21 +1501,65 @@ export function issueRoutes(
|
||||
includePluginOperations:
|
||||
req.query.includePluginOperations === "true" || req.query.includePluginOperations === "1",
|
||||
includeBlockedBy: req.query.includeBlockedBy === "true" || req.query.includeBlockedBy === "1",
|
||||
includeBlockedInboxAttention:
|
||||
req.query.includeBlockedInboxAttention === "true" || req.query.includeBlockedInboxAttention === "1",
|
||||
q: req.query.q as string | undefined,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
const handoffStates = await listSuccessfulRunHandoffStates(
|
||||
db,
|
||||
companyId,
|
||||
result.map((issue) => issue.id),
|
||||
);
|
||||
const issueIds = result.map((issue) => issue.id);
|
||||
const [handoffStates, recoveryActionByIssue] = await Promise.all([
|
||||
listSuccessfulRunHandoffStates(db, companyId, issueIds),
|
||||
recoveryActionsSvc.listActiveForIssues(companyId, issueIds),
|
||||
]);
|
||||
res.json(result.map((issue) => ({
|
||||
...issue,
|
||||
successfulRunHandoff: handoffStates.get(issue.id) ?? null,
|
||||
activeRecoveryAction: recoveryActionByIssue.get(issue.id) ?? null,
|
||||
})));
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/issues/count", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const attention = req.query.attention as string | undefined;
|
||||
if (attention !== "blocked") {
|
||||
res.status(400).json({ error: "issues/count currently requires attention=blocked" });
|
||||
return;
|
||||
}
|
||||
if (req.query.limit !== undefined || req.query.offset !== undefined) {
|
||||
res.status(400).json({ error: "issues/count does not accept limit or offset" });
|
||||
return;
|
||||
}
|
||||
|
||||
const count = await svc.count(companyId, {
|
||||
attention: "blocked",
|
||||
status: req.query.status as string | undefined,
|
||||
assigneeAgentId: req.query.assigneeAgentId as string | undefined,
|
||||
participantAgentId: req.query.participantAgentId as string | undefined,
|
||||
assigneeUserId: req.query.assigneeUserId as string | undefined,
|
||||
projectId: req.query.projectId as string | undefined,
|
||||
workspaceId: req.query.workspaceId as string | undefined,
|
||||
executionWorkspaceId: req.query.executionWorkspaceId as string | undefined,
|
||||
parentId: req.query.parentId as string | undefined,
|
||||
descendantOf: req.query.descendantOf as string | undefined,
|
||||
labelId: req.query.labelId as string | undefined,
|
||||
originKind: req.query.originKind as string | undefined,
|
||||
originKindPrefix: req.query.originKindPrefix as string | undefined,
|
||||
originId: req.query.originId as string | undefined,
|
||||
includeRoutineExecutions:
|
||||
req.query.includeRoutineExecutions === "true" || req.query.includeRoutineExecutions === "1",
|
||||
excludeRoutineExecutions:
|
||||
req.query.excludeRoutineExecutions === "true" || req.query.excludeRoutineExecutions === "1",
|
||||
includePluginOperations:
|
||||
req.query.includePluginOperations === "true" || req.query.includePluginOperations === "1",
|
||||
includeBlockedBy: true,
|
||||
includeBlockedInboxAttention: true,
|
||||
q: req.query.q as string | undefined,
|
||||
});
|
||||
res.json({ count });
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/labels", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
@@ -1541,6 +1643,7 @@ export function issueRoutes(
|
||||
attachments,
|
||||
continuationSummary,
|
||||
currentExecutionWorkspace,
|
||||
activeRecoveryAction,
|
||||
] =
|
||||
await Promise.all([
|
||||
resolveIssueProjectAndGoal(issue),
|
||||
@@ -1554,7 +1657,17 @@ export function issueRoutes(
|
||||
svc.listAttachments(issue.id),
|
||||
documentsSvc.getIssueDocumentByKey(issue.id, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY),
|
||||
currentExecutionWorkspacePromise,
|
||||
recoveryActionsSvc.getActiveForIssue(issue.companyId, issue.id),
|
||||
]);
|
||||
const recoveryActionsByRelationIssue = await relationRecoveryActionMap(
|
||||
recoveryActionsSvc,
|
||||
issue.companyId,
|
||||
relations,
|
||||
);
|
||||
const relationsWithRecoveryActions = withRecoveryActionsOnRelationSummaries(
|
||||
relations,
|
||||
recoveryActionsByRelationIssue,
|
||||
);
|
||||
|
||||
res.json({
|
||||
issue: {
|
||||
@@ -1567,12 +1680,13 @@ export function issueRoutes(
|
||||
...(blockerAttention ? { blockerAttention } : {}),
|
||||
productivityReview,
|
||||
scheduledRetry,
|
||||
activeRecoveryAction,
|
||||
priority: issue.priority,
|
||||
projectId: issue.projectId,
|
||||
goalId: goal?.id ?? issue.goalId,
|
||||
parentId: issue.parentId,
|
||||
blockedBy: relations.blockedBy,
|
||||
blocks: relations.blocks,
|
||||
blockedBy: relationsWithRecoveryActions.blockedBy,
|
||||
blocks: relationsWithRecoveryActions.blocks,
|
||||
assigneeAgentId: issue.assigneeAgentId,
|
||||
assigneeUserId: issue.assigneeUserId,
|
||||
originKind: issue.originKind,
|
||||
@@ -1649,6 +1763,7 @@ export function issueRoutes(
|
||||
referenceSummary,
|
||||
successfulRunHandoffStates,
|
||||
scheduledRetry,
|
||||
activeRecoveryAction,
|
||||
] = await Promise.all([
|
||||
resolveIssueProjectAndGoal(issue),
|
||||
svc.getAncestors(issue.id),
|
||||
@@ -1660,7 +1775,17 @@ export function issueRoutes(
|
||||
issueReferencesSvc.listIssueReferenceSummary(issue.id),
|
||||
listSuccessfulRunHandoffStates(db, issue.companyId, [issue.id]),
|
||||
svc.getCurrentScheduledRetry(issue.id),
|
||||
recoveryActionsSvc.getActiveForIssue(issue.companyId, issue.id),
|
||||
]);
|
||||
const recoveryActionsByRelationIssue = await relationRecoveryActionMap(
|
||||
recoveryActionsSvc,
|
||||
issue.companyId,
|
||||
relations,
|
||||
);
|
||||
const relationsWithRecoveryActions = withRecoveryActionsOnRelationSummaries(
|
||||
relations,
|
||||
recoveryActionsByRelationIssue,
|
||||
);
|
||||
const mentionedProjects = mentionedProjectIds.length > 0
|
||||
? await projectsSvc.listByIds(issue.companyId, mentionedProjectIds)
|
||||
: [];
|
||||
@@ -1676,8 +1801,9 @@ export function issueRoutes(
|
||||
productivityReview,
|
||||
successfulRunHandoff: successfulRunHandoffStates.get(issue.id) ?? null,
|
||||
scheduledRetry,
|
||||
blockedBy: relations.blockedBy,
|
||||
blocks: relations.blocks,
|
||||
activeRecoveryAction,
|
||||
blockedBy: relationsWithRecoveryActions.blockedBy,
|
||||
blocks: relationsWithRecoveryActions.blocks,
|
||||
relatedWork: referenceSummary,
|
||||
referencedIssueIdentifiers: referenceSummary.outbound.map((item) => item.issue.identifier ?? item.issue.id),
|
||||
...documentPayload,
|
||||
@@ -1689,6 +1815,148 @@ export function issueRoutes(
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/issues/:id/recovery-actions", async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const issue = await svc.getById(id);
|
||||
if (!issue) {
|
||||
res.status(404).json({ error: "Issue not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, issue.companyId);
|
||||
const active = await recoveryActionsSvc.getActiveForIssue(issue.companyId, issue.id);
|
||||
res.json({
|
||||
active,
|
||||
actions: active ? [active] : [],
|
||||
});
|
||||
});
|
||||
|
||||
router.post("/issues/:id/recovery-actions/resolve", validate(resolveIssueRecoveryActionSchema), async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const existing = await svc.getById(id);
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: "Issue not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, existing.companyId);
|
||||
if (!(await assertAgentIssueMutationAllowed(req, res, existing))) return;
|
||||
|
||||
const { actionId, outcome, sourceIssueStatus, resolutionNote } = req.body;
|
||||
if (outcome === "false_positive" || outcome === "cancelled") {
|
||||
assertBoard(req);
|
||||
}
|
||||
|
||||
const actor = getActorInfo(req);
|
||||
const updateFields = sourceIssueStatus ? { status: sourceIssueStatus } : {};
|
||||
await assertAgentInReviewReviewPath({
|
||||
existing,
|
||||
updateFields,
|
||||
actorType: req.actor.type,
|
||||
});
|
||||
|
||||
const actionStatus = outcome === "cancelled" ? "cancelled" : "resolved";
|
||||
const result = await db.transaction(async (tx) => {
|
||||
let issue = existing;
|
||||
if (outcome === "blocked") {
|
||||
const unresolvedBlockers = await tx
|
||||
.select({ id: issueRows.id })
|
||||
.from(issueRelations)
|
||||
.innerJoin(issueRows, eq(issueRelations.issueId, issueRows.id))
|
||||
.where(
|
||||
and(
|
||||
eq(issueRelations.companyId, existing.companyId),
|
||||
eq(issueRelations.relatedIssueId, existing.id),
|
||||
eq(issueRelations.type, "blocks"),
|
||||
notInArray(issueRows.status, ["done", "cancelled"]),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
if (unresolvedBlockers.length === 0) {
|
||||
throw unprocessable("Blocked recovery resolution requires an unresolved first-class blocker on the source issue");
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceIssueStatus) {
|
||||
const updatedIssue = await svc.update(
|
||||
id,
|
||||
{
|
||||
status: sourceIssueStatus,
|
||||
actorAgentId: actor.agentId ?? null,
|
||||
actorUserId: actor.actorType === "user" ? actor.actorId : null,
|
||||
},
|
||||
tx,
|
||||
);
|
||||
if (!updatedIssue) throw notFound("Issue not found");
|
||||
issue = updatedIssue;
|
||||
}
|
||||
|
||||
const recoveryAction = await recoveryActionsSvc.resolveActiveForIssue(
|
||||
{
|
||||
companyId: existing.companyId,
|
||||
sourceIssueId: existing.id,
|
||||
actionId: actionId ?? null,
|
||||
status: actionStatus,
|
||||
outcome,
|
||||
resolutionNote: resolutionNote ?? null,
|
||||
},
|
||||
tx,
|
||||
);
|
||||
if (!recoveryAction) throw notFound("Active recovery action not found");
|
||||
|
||||
return { issue, recoveryAction };
|
||||
});
|
||||
|
||||
await routinesSvc.syncRunStatusForIssue(result.issue.id);
|
||||
|
||||
if (sourceIssueStatus && existing.status !== result.issue.status) {
|
||||
await logActivity(db, {
|
||||
companyId: result.issue.companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "issue.updated",
|
||||
entityType: "issue",
|
||||
entityId: result.issue.id,
|
||||
details: {
|
||||
identifier: result.issue.identifier,
|
||||
status: result.issue.status,
|
||||
source: "recovery_action_resolution",
|
||||
recoveryActionId: result.recoveryAction.id,
|
||||
_previous: {
|
||||
status: existing.status,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await logActivity(db, {
|
||||
companyId: result.issue.companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "issue.recovery_action_resolved",
|
||||
entityType: "issue",
|
||||
entityId: result.issue.id,
|
||||
details: {
|
||||
identifier: result.issue.identifier,
|
||||
recoveryActionId: result.recoveryAction.id,
|
||||
recoveryActionStatus: result.recoveryAction.status,
|
||||
outcome: result.recoveryAction.outcome,
|
||||
sourceIssueStatus: sourceIssueStatus ?? null,
|
||||
resolutionNote: result.recoveryAction.resolutionNote,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
issue: {
|
||||
...result.issue,
|
||||
activeRecoveryAction: null,
|
||||
},
|
||||
recoveryAction: result.recoveryAction,
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/issues/:id/work-products", async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const issue = await svc.getById(id);
|
||||
|
||||
@@ -199,9 +199,9 @@ function listBundledPluginExamples(): AvailablePluginExample[] {
|
||||
*
|
||||
* Lookup order:
|
||||
* - UUID-like IDs: getById first, then getByKey.
|
||||
* - Scoped package keys (e.g. "@scope/name"): getByKey only, never getById.
|
||||
* - Other non-UUID IDs: try getById first (test/memory registries may allow this),
|
||||
* then fallback to getByKey. Any UUID parse error from getById is ignored.
|
||||
* - All non-UUID values: getByKey only, never getById. The persisted plugin
|
||||
* ID column is a PostgreSQL UUID, so probing it with keys such as
|
||||
* "acme.plugin" raises a database cast error before a key lookup can happen.
|
||||
*
|
||||
* @param registry - The plugin registry service instance
|
||||
* @param pluginId - Either a database UUID or plugin key (manifest id)
|
||||
@@ -212,27 +212,13 @@ async function resolvePlugin(
|
||||
pluginId: string,
|
||||
) {
|
||||
const isUuid = UUID_REGEX.test(pluginId);
|
||||
const isScopedPackageKey = pluginId.startsWith("@") || pluginId.includes("/");
|
||||
|
||||
// Scoped package IDs are valid plugin keys but invalid UUIDs.
|
||||
// Skip getById() entirely to avoid Postgres uuid parse errors.
|
||||
if (isScopedPackageKey && !isUuid) {
|
||||
if (!isUuid) {
|
||||
return registry.getByKey(pluginId);
|
||||
}
|
||||
|
||||
try {
|
||||
const byId = await registry.getById(pluginId);
|
||||
if (byId) return byId;
|
||||
} catch (error) {
|
||||
const maybeCode =
|
||||
typeof error === "object" && error !== null && "code" in error
|
||||
? (error as { code?: unknown }).code
|
||||
: undefined;
|
||||
// Ignore invalid UUID cast errors and continue with key lookup.
|
||||
if (maybeCode !== "22P02") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const byId = await registry.getById(pluginId);
|
||||
if (byId) return byId;
|
||||
|
||||
return registry.getByKey(pluginId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user