@hotmeshio/long-tail 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/api/escalations/index.d.ts +1 -1
- package/build/api/escalations/index.js +2 -1
- package/build/api/escalations/resolve.d.ts +10 -0
- package/build/api/escalations/resolve.js +52 -0
- package/build/lib/events/system-events.d.ts +19 -0
- package/build/lib/events/system-events.js +62 -0
- package/build/modules/ltconfig.d.ts +8 -0
- package/build/modules/ltconfig.js +10 -0
- package/build/routes/escalations/resolve.js +15 -3
- package/build/services/escalation/bulk.d.ts +2 -1
- package/build/services/escalation/bulk.js +20 -19
- package/build/services/escalation/client.d.ts +22 -0
- package/build/services/escalation/client.js +141 -0
- package/build/services/escalation/crud.d.ts +47 -21
- package/build/services/escalation/crud.js +204 -140
- package/build/services/escalation/index.d.ts +1 -0
- package/build/services/escalation/index.js +3 -0
- package/build/services/escalation/map.d.ts +15 -0
- package/build/services/escalation/map.js +64 -0
- package/build/services/escalation/queries.js +64 -149
- package/build/services/escalation/sql.d.ts +13 -32
- package/build/services/escalation/sql.js +36 -176
- package/build/services/export/post-process.js +23 -4
- package/build/services/interceptor/activities/config.js +5 -1
- package/build/services/interceptor/index.d.ts +3 -0
- package/build/services/interceptor/index.js +7 -21
- package/build/services/mcp/db-server/schemas.d.ts +1 -1
- package/build/services/orchestrator/condition.d.ts +30 -25
- package/build/services/orchestrator/condition.js +30 -26
- package/build/services/yaml-workflow/deployer.js +4 -0
- package/build/services/yaml-workflow/workers/register.js +3 -0
- package/build/start/index.js +2 -1
- package/build/start/workers.js +12 -0
- package/build/system/mcp-servers/admin/schemas.d.ts +1 -1
- package/build/system/mcp-servers/db-query/schemas.d.ts +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/escalation.d.ts +1 -0
- package/docs/api/http/escalations.md +19 -0
- package/docs/api/sdk/escalations.md +33 -0
- package/docs/hitl-guide.md +44 -1
- package/package.json +2 -2
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createEscalation = createEscalation;
|
|
4
4
|
exports.claimEscalation = claimEscalation;
|
|
5
5
|
exports.resolveEscalation = resolveEscalation;
|
|
6
|
+
exports.getEscalationBySignalKey = getEscalationBySignalKey;
|
|
7
|
+
exports.resolveEscalationBySignalKey = resolveEscalationBySignalKey;
|
|
6
8
|
exports.updateEscalationsPriority = updateEscalationsPriority;
|
|
7
9
|
exports.getEscalationRoles = getEscalationRoles;
|
|
8
10
|
exports.releaseEscalation = releaseEscalation;
|
|
@@ -19,32 +21,37 @@ exports.claimByMetadata = claimByMetadata;
|
|
|
19
21
|
exports.resolveByMetadataAtomic = resolveByMetadataAtomic;
|
|
20
22
|
const db_1 = require("../../lib/db");
|
|
21
23
|
const publish_1 = require("../../lib/events/publish");
|
|
22
|
-
const
|
|
24
|
+
const client_1 = require("./client");
|
|
25
|
+
const map_1 = require("./map");
|
|
23
26
|
const sql_1 = require("./sql");
|
|
27
|
+
// All escalation state lives in `public.hmsh_escalations` (HotMesh 0.22.3),
|
|
28
|
+
// reached through `client.escalations.*`. The function signatures and return
|
|
29
|
+
// shapes below are the frozen public surface — only the storage path changed.
|
|
30
|
+
// Generous upper bound for the "all escalations for X" lookups, which the
|
|
31
|
+
// legacy SQL returned without a LIMIT. Escalations per workflow/origin/task are
|
|
32
|
+
// few; this avoids a silent page cap without an unbounded scan.
|
|
33
|
+
const LOOKUP_LIMIT = 1000;
|
|
24
34
|
async function createEscalation(input) {
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
input.
|
|
31
|
-
input.
|
|
32
|
-
input.
|
|
33
|
-
input.
|
|
34
|
-
input.
|
|
35
|
-
input.
|
|
36
|
-
input.
|
|
37
|
-
input.
|
|
38
|
-
input.
|
|
39
|
-
|
|
40
|
-
input.
|
|
41
|
-
input.
|
|
42
|
-
input.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
input.span_id || null,
|
|
46
|
-
]);
|
|
47
|
-
const escalation = rows[0];
|
|
35
|
+
const client = await (0, client_1.escalations)();
|
|
36
|
+
const entry = await client.create({
|
|
37
|
+
type: input.type,
|
|
38
|
+
subtype: input.subtype,
|
|
39
|
+
description: input.description,
|
|
40
|
+
priority: input.priority ?? 2,
|
|
41
|
+
role: input.role,
|
|
42
|
+
taskId: input.task_id,
|
|
43
|
+
originId: input.origin_id,
|
|
44
|
+
parentId: input.parent_id,
|
|
45
|
+
workflowId: input.workflow_id,
|
|
46
|
+
taskQueue: input.task_queue,
|
|
47
|
+
workflowType: input.workflow_type,
|
|
48
|
+
traceId: input.trace_id,
|
|
49
|
+
spanId: input.span_id,
|
|
50
|
+
envelope: (0, map_1.toEnvelopeObject)(input.envelope),
|
|
51
|
+
metadata: input.metadata,
|
|
52
|
+
escalationPayload: (0, map_1.toJsonObject)(input.escalation_payload),
|
|
53
|
+
});
|
|
54
|
+
const escalation = (0, map_1.toEscalationRecord)(entry);
|
|
48
55
|
(0, publish_1.publishEscalationEvent)({
|
|
49
56
|
type: 'escalation.created',
|
|
50
57
|
source: 'service',
|
|
@@ -58,22 +65,16 @@ async function createEscalation(input) {
|
|
|
58
65
|
return escalation;
|
|
59
66
|
}
|
|
60
67
|
/**
|
|
61
|
-
* Atomic claim
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* Conditions:
|
|
65
|
-
* - status = 'pending' (not resolved/cancelled)
|
|
66
|
-
* - Either: unassigned, expired claim, or same user (extension)
|
|
67
|
-
*
|
|
68
|
-
* Uses a CTE to capture the previous state so callers can detect extensions.
|
|
68
|
+
* Atomic claim. Implicit model — status stays 'pending'; "claimed" is
|
|
69
|
+
* assigned_to + assigned_until > NOW(). `isExtension` is true when the same
|
|
70
|
+
* user re-claims (extends expiry). Returns null when the row is not claimable.
|
|
69
71
|
*/
|
|
70
72
|
async function claimEscalation(id, userId, durationMinutes = 30) {
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
73
|
+
const client = await (0, client_1.escalations)();
|
|
74
|
+
const result = await client.claim({ id, assignee: userId, durationMinutes });
|
|
75
|
+
if (!result.ok)
|
|
74
76
|
return null;
|
|
75
|
-
const
|
|
76
|
-
const escalation = row;
|
|
77
|
+
const escalation = (0, map_1.toEscalationRecord)(result.entry);
|
|
77
78
|
(0, publish_1.publishEscalationEvent)({
|
|
78
79
|
type: 'escalation.claimed',
|
|
79
80
|
source: 'service',
|
|
@@ -84,39 +85,68 @@ async function claimEscalation(id, userId, durationMinutes = 30) {
|
|
|
84
85
|
status: 'claimed',
|
|
85
86
|
data: { assigned_to: userId },
|
|
86
87
|
});
|
|
87
|
-
return {
|
|
88
|
-
escalation,
|
|
89
|
-
isExtension: row.prev_assigned_to === userId,
|
|
90
|
-
};
|
|
88
|
+
return { escalation, isExtension: result.isExtension };
|
|
91
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Mark an escalation resolved. Signal delivery is owned by the resolution
|
|
92
|
+
* orchestrator (api/escalations/resolve.ts); service-created rows have no
|
|
93
|
+
* signal_key, so this never delivers a signal itself. Returns null when the
|
|
94
|
+
* row is missing or already terminal.
|
|
95
|
+
*/
|
|
92
96
|
async function resolveEscalation(id, resolverPayload) {
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
}
|
|
97
|
+
const client = await (0, client_1.escalations)();
|
|
98
|
+
const result = await client.resolve({ id, resolverPayload });
|
|
99
|
+
if (!result.ok)
|
|
100
|
+
return null;
|
|
101
|
+
const escalation = (0, map_1.toEscalationRecord)(result.entry);
|
|
102
|
+
(0, publish_1.publishEscalationEvent)({
|
|
103
|
+
type: 'escalation.resolved',
|
|
104
|
+
source: 'service',
|
|
105
|
+
workflowId: escalation.workflow_id || '',
|
|
106
|
+
workflowName: escalation.workflow_type || '',
|
|
107
|
+
taskQueue: escalation.task_queue || '',
|
|
108
|
+
escalationId: escalation.id,
|
|
109
|
+
status: 'resolved',
|
|
110
|
+
data: {},
|
|
111
|
+
});
|
|
108
112
|
return escalation;
|
|
109
113
|
}
|
|
110
114
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
115
|
+
* Look up an efficient (atomic) escalation by its `signal_key` — the signal id
|
|
116
|
+
* passed to `conditionLT(signalId, config)` / `condition(signalId, config)`.
|
|
117
|
+
* Returns null when no row carries that key.
|
|
118
|
+
*/
|
|
119
|
+
async function getEscalationBySignalKey(signalKey) {
|
|
120
|
+
const client = await (0, client_1.escalations)();
|
|
121
|
+
const entry = await client.getBySignalKey(signalKey);
|
|
122
|
+
return entry ? (0, map_1.toEscalationRecord)(entry) : null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolve an efficient (atomic) escalation by its `signal_key` and resume the
|
|
126
|
+
* waiting workflow in place. Convenience for webhook callers that know the
|
|
127
|
+
* deterministic signal id (e.g. `signal-scan-ar-${orderId}`) and want to skip
|
|
128
|
+
* the id lookup. Returns null when the key is unknown or already terminal.
|
|
129
|
+
*
|
|
130
|
+
* Race-free: `signal_key → id` is an immutable mapping, and the state mutation
|
|
131
|
+
* is delegated to `resolveEscalation`, whose `client.resolve` uses FOR UPDATE +
|
|
132
|
+
* `WHERE status = 'pending'` so exactly one concurrent caller commits. No status
|
|
133
|
+
* pre-check (that would be a TOCTOU window) — the atomic resolve is the arbiter.
|
|
134
|
+
*/
|
|
135
|
+
async function resolveEscalationBySignalKey(signalKey, resolverPayload) {
|
|
136
|
+
const escalation = await getEscalationBySignalKey(signalKey);
|
|
137
|
+
if (!escalation)
|
|
138
|
+
return null;
|
|
139
|
+
return resolveEscalation(escalation.id, resolverPayload);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Bulk update priority for a set of escalations. Only pending escalations are
|
|
143
|
+
* updated.
|
|
113
144
|
*/
|
|
114
145
|
async function updateEscalationsPriority(ids, priority) {
|
|
115
146
|
if (ids.length === 0)
|
|
116
147
|
return 0;
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
return rowCount ?? 0;
|
|
148
|
+
const client = await (0, client_1.escalations)();
|
|
149
|
+
return client.updateManyPriority({ ids, priority });
|
|
120
150
|
}
|
|
121
151
|
/**
|
|
122
152
|
* Get the distinct roles for a set of escalation IDs.
|
|
@@ -125,113 +155,145 @@ async function updateEscalationsPriority(ids, priority) {
|
|
|
125
155
|
async function getEscalationRoles(ids) {
|
|
126
156
|
if (ids.length === 0)
|
|
127
157
|
return [];
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
return rows.map(
|
|
158
|
+
const client = await (0, client_1.escalations)();
|
|
159
|
+
const rows = await client.list({ ids, limit: LOOKUP_LIMIT });
|
|
160
|
+
return [...new Set(rows.map(r => r.role).filter((r) => !!r))];
|
|
131
161
|
}
|
|
132
162
|
/**
|
|
133
163
|
* Release a single escalation claim back to the available pool.
|
|
134
164
|
* Only the assigned user (or superadmin via route) may release.
|
|
135
165
|
*/
|
|
136
166
|
async function releaseEscalation(id, userId) {
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return released
|
|
167
|
+
const client = await (0, client_1.escalations)();
|
|
168
|
+
const result = await client.release({ id, assignee: userId });
|
|
169
|
+
if (!result.ok)
|
|
170
|
+
return null;
|
|
171
|
+
const released = (0, map_1.toEscalationRecord)(result.entry);
|
|
172
|
+
(0, publish_1.publishEscalationEvent)({
|
|
173
|
+
type: 'escalation.released',
|
|
174
|
+
source: 'service',
|
|
175
|
+
workflowId: released.workflow_id || '',
|
|
176
|
+
workflowName: released.workflow_type || '',
|
|
177
|
+
taskQueue: released.task_queue || '',
|
|
178
|
+
escalationId: released.id,
|
|
179
|
+
status: 'released',
|
|
180
|
+
data: { released_by: userId },
|
|
181
|
+
});
|
|
182
|
+
return released;
|
|
153
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Sweep expired claims back to the available pool, returning the count cleared.
|
|
186
|
+
* Availability is already query-time in the implicit model, but long-tail's
|
|
187
|
+
* public contract clears `assigned_to` and returns a count, so this runs as a
|
|
188
|
+
* single direct UPDATE on the shared table (the SDK's releaseExpired is a no-op).
|
|
189
|
+
*/
|
|
154
190
|
async function releaseExpiredClaims() {
|
|
191
|
+
await (0, client_1.ensureEscalationCompatView)();
|
|
155
192
|
const pool = (0, db_1.getPool)();
|
|
156
193
|
const { rowCount } = await pool.query(sql_1.RELEASE_EXPIRED_CLAIMS);
|
|
157
|
-
return rowCount
|
|
194
|
+
return rowCount ?? 0;
|
|
158
195
|
}
|
|
159
196
|
/**
|
|
160
197
|
* Reassign an escalation to a different role.
|
|
161
198
|
* Clears the current assignment so it becomes available to the new role.
|
|
162
199
|
*/
|
|
163
200
|
async function escalateToRole(id, targetRole) {
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
return
|
|
201
|
+
const client = await (0, client_1.escalations)();
|
|
202
|
+
const entry = await client.escalateToRole({ id, targetRole });
|
|
203
|
+
return entry ? (0, map_1.toEscalationRecord)(entry) : null;
|
|
167
204
|
}
|
|
168
205
|
async function getEscalation(id) {
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
return
|
|
206
|
+
const client = await (0, client_1.escalations)();
|
|
207
|
+
const entry = await client.get(id);
|
|
208
|
+
return entry ? (0, map_1.toEscalationRecord)(entry) : null;
|
|
172
209
|
}
|
|
173
210
|
async function getEscalationsByTaskId(taskId) {
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
211
|
+
const client = await (0, client_1.escalations)();
|
|
212
|
+
const rows = await client.list({
|
|
213
|
+
taskId,
|
|
214
|
+
sortBy: 'created_at',
|
|
215
|
+
sortOrder: 'desc',
|
|
216
|
+
limit: LOOKUP_LIMIT,
|
|
217
|
+
});
|
|
218
|
+
return (0, map_1.toEscalationRecords)(rows);
|
|
177
219
|
}
|
|
178
220
|
async function getEscalationsByWorkflowId(workflowId) {
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
221
|
+
const client = await (0, client_1.escalations)();
|
|
222
|
+
const rows = await client.list({
|
|
223
|
+
workflowId,
|
|
224
|
+
sortBy: 'created_at',
|
|
225
|
+
sortOrder: 'desc',
|
|
226
|
+
limit: LOOKUP_LIMIT,
|
|
227
|
+
});
|
|
228
|
+
return (0, map_1.toEscalationRecords)(rows);
|
|
182
229
|
}
|
|
183
230
|
async function updateEscalationMetadata(id, patch) {
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
return
|
|
231
|
+
const client = await (0, client_1.escalations)();
|
|
232
|
+
const entry = await client.update({ id, metadata: patch });
|
|
233
|
+
return entry ? (0, map_1.toEscalationRecord)(entry) : null;
|
|
187
234
|
}
|
|
188
235
|
async function enrichEscalationRouting(id, metadataPatch, workflowFields) {
|
|
189
|
-
const
|
|
190
|
-
const
|
|
236
|
+
const client = await (0, client_1.escalations)();
|
|
237
|
+
const entry = await client.update({
|
|
191
238
|
id,
|
|
192
|
-
|
|
193
|
-
workflowFields.workflowType
|
|
194
|
-
workflowFields.workflowId
|
|
195
|
-
workflowFields.taskQueue
|
|
196
|
-
workflowFields.taskId
|
|
197
|
-
|
|
198
|
-
return
|
|
239
|
+
metadata: metadataPatch,
|
|
240
|
+
workflowType: workflowFields.workflowType,
|
|
241
|
+
workflowId: workflowFields.workflowId,
|
|
242
|
+
taskQueue: workflowFields.taskQueue,
|
|
243
|
+
taskId: workflowFields.taskId,
|
|
244
|
+
});
|
|
245
|
+
return entry ? (0, map_1.toEscalationRecord)(entry) : null;
|
|
199
246
|
}
|
|
200
247
|
async function getEscalationsByOriginId(originId) {
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
248
|
+
const client = await (0, client_1.escalations)();
|
|
249
|
+
const rows = await client.list({
|
|
250
|
+
originId,
|
|
251
|
+
sortBy: 'created_at',
|
|
252
|
+
sortOrder: 'desc',
|
|
253
|
+
limit: LOOKUP_LIMIT,
|
|
254
|
+
});
|
|
255
|
+
return (0, map_1.toEscalationRecords)(rows);
|
|
204
256
|
}
|
|
205
257
|
// --- Metadata candidate key lookups -----------------------------------------
|
|
206
258
|
async function findByMetadata(key, value, status, limit = 50, offset = 0) {
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
259
|
+
const client = await (0, client_1.escalations)();
|
|
260
|
+
const metadata = { [key]: value };
|
|
261
|
+
const [rows, total] = await Promise.all([
|
|
262
|
+
client.list({
|
|
263
|
+
metadata,
|
|
264
|
+
status,
|
|
265
|
+
orderBy: [
|
|
266
|
+
{ column: 'priority', direction: 'asc' },
|
|
267
|
+
{ column: 'created_at', direction: 'asc' },
|
|
268
|
+
],
|
|
269
|
+
limit,
|
|
270
|
+
offset,
|
|
271
|
+
}),
|
|
272
|
+
client.count({ metadata, status }),
|
|
273
|
+
]);
|
|
274
|
+
return { escalations: (0, map_1.toEscalationRecords)(rows), total };
|
|
214
275
|
}
|
|
215
276
|
/**
|
|
216
|
-
* Atomic claim by metadata with inline RBAC.
|
|
217
|
-
* The
|
|
218
|
-
*
|
|
219
|
-
*
|
|
277
|
+
* Atomic claim by metadata with inline RBAC and optional metadata merge.
|
|
278
|
+
* The SDK enforces the role filter in SQL — callers without an allowed role
|
|
279
|
+
* match zero rows. Returns `{ escalation, isExtension, candidatesExist }` or
|
|
280
|
+
* null when nothing was claimed.
|
|
220
281
|
*
|
|
221
|
-
* @param allowedRoles — roles the caller can claim (null = no filter / global
|
|
222
|
-
* @returns `{ escalation, isExtension, candidatesExist }` or null
|
|
282
|
+
* @param allowedRoles — roles the caller can claim (null = no filter / global)
|
|
223
283
|
*/
|
|
224
284
|
async function claimByMetadata(key, value, userId, durationMinutes = 30, metadata, allowedRoles) {
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
285
|
+
const client = await (0, client_1.escalations)();
|
|
286
|
+
const result = await client.claimByMetadata({
|
|
287
|
+
key,
|
|
288
|
+
value,
|
|
289
|
+
assignee: userId,
|
|
290
|
+
durationMinutes,
|
|
291
|
+
roles: allowedRoles === null ? undefined : allowedRoles,
|
|
292
|
+
metadata,
|
|
293
|
+
});
|
|
294
|
+
if (!result.ok)
|
|
231
295
|
return null;
|
|
232
|
-
const
|
|
233
|
-
const { candidates_exist, prev_assigned_to, _total, ...rest } = row;
|
|
234
|
-
const escalation = rest;
|
|
296
|
+
const escalation = (0, map_1.toEscalationRecord)(result.entry);
|
|
235
297
|
(0, publish_1.publishEscalationEvent)({
|
|
236
298
|
type: 'escalation.claimed',
|
|
237
299
|
source: 'service',
|
|
@@ -244,19 +306,22 @@ async function claimByMetadata(key, value, userId, durationMinutes = 30, metadat
|
|
|
244
306
|
});
|
|
245
307
|
return {
|
|
246
308
|
escalation,
|
|
247
|
-
isExtension:
|
|
248
|
-
candidatesExist:
|
|
309
|
+
isExtension: result.isExtension,
|
|
310
|
+
candidatesExist: result.candidatesExist,
|
|
249
311
|
};
|
|
250
312
|
}
|
|
251
313
|
/**
|
|
252
|
-
* Atomic resolve by metadata with signal guard.
|
|
314
|
+
* Atomic resolve by metadata with signal guard, in a single CTE.
|
|
253
315
|
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
316
|
+
* Signal-backed rows (those carrying `metadata.signal_id`) are NOT resolved
|
|
317
|
+
* here — long-tail signals the paused workflow and the workflow interceptor
|
|
318
|
+
* resolves durably. If the workflow is gone the signal fails and the row stays
|
|
319
|
+
* pending, which is the contract the route suite pins. This guard is long-tail
|
|
320
|
+
* business logic over the shared table, so it runs as one atomic statement on
|
|
321
|
+
* `hmsh_escalations` rather than through the generic SDK resolve.
|
|
258
322
|
*/
|
|
259
323
|
async function resolveByMetadataAtomic(key, value, userId, resolverPayload, metadata, allowedRoles) {
|
|
324
|
+
await (0, client_1.ensureEscalationCompatView)();
|
|
260
325
|
const pool = (0, db_1.getPool)();
|
|
261
326
|
const filter = JSON.stringify({ [key]: value });
|
|
262
327
|
const payloadJson = JSON.stringify(resolverPayload);
|
|
@@ -267,8 +332,7 @@ async function resolveByMetadataAtomic(key, value, userId, resolverPayload, meta
|
|
|
267
332
|
return { outcome: 'not_found' };
|
|
268
333
|
const row = rows[0];
|
|
269
334
|
if (row.outcome === 'resolved') {
|
|
270
|
-
const
|
|
271
|
-
const escalation = rest;
|
|
335
|
+
const escalation = (0, map_1.toEscalationRecord)(row);
|
|
272
336
|
(0, publish_1.publishEscalationEvent)({
|
|
273
337
|
type: 'escalation.resolved',
|
|
274
338
|
source: 'service',
|
|
@@ -281,7 +345,7 @@ async function resolveByMetadataAtomic(key, value, userId, resolverPayload, meta
|
|
|
281
345
|
});
|
|
282
346
|
return { outcome: 'resolved', escalation };
|
|
283
347
|
}
|
|
284
|
-
// Signal-backed escalation — return the signal info for the caller
|
|
348
|
+
// Signal-backed escalation — return the signal info for the caller to deliver.
|
|
285
349
|
return {
|
|
286
350
|
outcome: 'signal_required',
|
|
287
351
|
signalId: row.signal_id,
|
|
@@ -14,7 +14,10 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.ensureEscalationCompatView = void 0;
|
|
17
18
|
__exportStar(require("./types"), exports);
|
|
18
19
|
__exportStar(require("./crud"), exports);
|
|
19
20
|
__exportStar(require("./bulk"), exports);
|
|
20
21
|
__exportStar(require("./queries"), exports);
|
|
22
|
+
var client_1 = require("./client");
|
|
23
|
+
Object.defineProperty(exports, "ensureEscalationCompatView", { enumerable: true, get: function () { return client_1.ensureEscalationCompatView; } });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Types } from '@hotmeshio/hotmesh';
|
|
2
|
+
import type { LTEscalationRecord } from '../../types';
|
|
3
|
+
type EscalationEntry = Types.EscalationEntry;
|
|
4
|
+
/** SDK row → long-tail public record. */
|
|
5
|
+
export declare function toEscalationRecord(entry: EscalationEntry): LTEscalationRecord;
|
|
6
|
+
export declare function toEscalationRecords(entries: EscalationEntry[]): LTEscalationRecord[];
|
|
7
|
+
/**
|
|
8
|
+
* Parse a TEXT/JSON-string field from the public input shape into the JSONB
|
|
9
|
+
* object the SDK expects. Returns `undefined` (field omitted) for empty/invalid
|
|
10
|
+
* input so the column stays NULL rather than storing garbage.
|
|
11
|
+
*/
|
|
12
|
+
export declare function toJsonObject(value: string | null | undefined): Record<string, unknown> | undefined;
|
|
13
|
+
/** Parse an envelope string, always yielding an object (defaults to `{}`). */
|
|
14
|
+
export declare function toEnvelopeObject(value: string | null | undefined): Record<string, unknown>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toEscalationRecord = toEscalationRecord;
|
|
4
|
+
exports.toEscalationRecords = toEscalationRecords;
|
|
5
|
+
exports.toJsonObject = toJsonObject;
|
|
6
|
+
exports.toEnvelopeObject = toEnvelopeObject;
|
|
7
|
+
/** Serialize a JSONB object column back to the TEXT form the public shape uses. */
|
|
8
|
+
function toJsonText(value) {
|
|
9
|
+
return value == null ? null : JSON.stringify(value);
|
|
10
|
+
}
|
|
11
|
+
/** SDK row → long-tail public record. */
|
|
12
|
+
function toEscalationRecord(entry) {
|
|
13
|
+
return {
|
|
14
|
+
id: entry.id,
|
|
15
|
+
type: entry.type ?? '',
|
|
16
|
+
subtype: entry.subtype ?? '',
|
|
17
|
+
description: entry.description,
|
|
18
|
+
status: entry.status,
|
|
19
|
+
priority: entry.priority,
|
|
20
|
+
task_id: entry.task_id,
|
|
21
|
+
origin_id: entry.origin_id,
|
|
22
|
+
parent_id: entry.parent_id,
|
|
23
|
+
workflow_id: entry.workflow_id,
|
|
24
|
+
task_queue: entry.task_queue,
|
|
25
|
+
workflow_type: entry.workflow_type,
|
|
26
|
+
signal_key: entry.signal_key,
|
|
27
|
+
role: entry.role ?? '',
|
|
28
|
+
assigned_to: entry.assigned_to,
|
|
29
|
+
assigned_until: entry.assigned_until,
|
|
30
|
+
resolved_at: entry.resolved_at,
|
|
31
|
+
claimed_at: entry.claimed_at,
|
|
32
|
+
envelope: entry.envelope == null ? '{}' : JSON.stringify(entry.envelope),
|
|
33
|
+
metadata: entry.metadata,
|
|
34
|
+
escalation_payload: toJsonText(entry.escalation_payload),
|
|
35
|
+
resolver_payload: toJsonText(entry.resolver_payload),
|
|
36
|
+
trace_id: entry.trace_id,
|
|
37
|
+
span_id: entry.span_id,
|
|
38
|
+
created_at: entry.created_at,
|
|
39
|
+
updated_at: entry.updated_at,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function toEscalationRecords(entries) {
|
|
43
|
+
return entries.map(toEscalationRecord);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse a TEXT/JSON-string field from the public input shape into the JSONB
|
|
47
|
+
* object the SDK expects. Returns `undefined` (field omitted) for empty/invalid
|
|
48
|
+
* input so the column stays NULL rather than storing garbage.
|
|
49
|
+
*/
|
|
50
|
+
function toJsonObject(value) {
|
|
51
|
+
if (value == null || value === '')
|
|
52
|
+
return undefined;
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(value);
|
|
55
|
+
return parsed && typeof parsed === 'object' ? parsed : undefined;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Parse an envelope string, always yielding an object (defaults to `{}`). */
|
|
62
|
+
function toEnvelopeObject(value) {
|
|
63
|
+
return toJsonObject(value) ?? {};
|
|
64
|
+
}
|