@uploadista/server 0.0.17 → 0.0.18-beta.10
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/README.md +83 -0
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +759 -9
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +759 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/docs/HEALTH_CHECKS.md +256 -0
- package/package.json +14 -12
- package/src/core/health-check-service.ts +367 -0
- package/src/core/http-handlers/dlq-http-handlers.ts +219 -0
- package/src/core/http-handlers/flow-http-handlers.ts +61 -6
- package/src/core/http-handlers/health-http-handlers.ts +150 -0
- package/src/core/http-handlers/http-handlers.ts +50 -0
- package/src/core/http-handlers/upload-http-handlers.ts +56 -3
- package/src/core/routes.ts +171 -0
- package/src/core/server.ts +45 -4
- package/src/core/types.ts +114 -0
- package/src/index.ts +2 -0
- package/src/permissions/errors.ts +105 -0
- package/src/permissions/index.ts +9 -0
- package/src/permissions/matcher.ts +139 -0
- package/src/permissions/types.ts +151 -0
- package/src/plugins-typing.ts +1 -1
- package/src/service.ts +101 -3
- package/src/usage-hooks/index.ts +8 -0
- package/src/usage-hooks/service.ts +162 -0
- package/src/usage-hooks/types.ts +221 -0
- package/tests/core/health-check-service.test.ts +570 -0
- package/tests/core/http-handlers/health-handlers.test.ts +351 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { DeadLetterQueueService } from "@uploadista/core/flow";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { PERMISSIONS } from "../../permissions/types";
|
|
4
|
+
import { AuthContextService } from "../../service";
|
|
5
|
+
import type {
|
|
6
|
+
DlqCleanupRequest,
|
|
7
|
+
DlqCleanupResponse,
|
|
8
|
+
DlqDeleteRequest,
|
|
9
|
+
DlqDeleteResponse,
|
|
10
|
+
DlqGetRequest,
|
|
11
|
+
DlqGetResponse,
|
|
12
|
+
DlqListRequest,
|
|
13
|
+
DlqListResponse,
|
|
14
|
+
DlqResolveRequest,
|
|
15
|
+
DlqResolveResponse,
|
|
16
|
+
DlqRetryAllRequest,
|
|
17
|
+
DlqRetryAllResponse,
|
|
18
|
+
DlqRetryRequest,
|
|
19
|
+
DlqRetryResponse,
|
|
20
|
+
DlqStatsRequest,
|
|
21
|
+
DlqStatsResponse,
|
|
22
|
+
} from "../routes";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Handle GET /api/dlq - List DLQ items
|
|
26
|
+
*/
|
|
27
|
+
export const handleDlqList = (req: DlqListRequest) =>
|
|
28
|
+
Effect.gen(function* () {
|
|
29
|
+
const authService = yield* AuthContextService;
|
|
30
|
+
|
|
31
|
+
// Check permission for reading DLQ
|
|
32
|
+
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_READ);
|
|
33
|
+
|
|
34
|
+
const dlq = yield* DeadLetterQueueService;
|
|
35
|
+
const result = yield* dlq.list(req.options);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
type: "dlq-list",
|
|
39
|
+
status: 200,
|
|
40
|
+
headers: { "Content-Type": "application/json" },
|
|
41
|
+
body: result,
|
|
42
|
+
} satisfies DlqListResponse;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Handle GET /api/dlq/:itemId - Get a specific DLQ item
|
|
47
|
+
*/
|
|
48
|
+
export const handleDlqGet = (req: DlqGetRequest) =>
|
|
49
|
+
Effect.gen(function* () {
|
|
50
|
+
const authService = yield* AuthContextService;
|
|
51
|
+
|
|
52
|
+
// Check permission for reading DLQ
|
|
53
|
+
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_READ);
|
|
54
|
+
|
|
55
|
+
const dlq = yield* DeadLetterQueueService;
|
|
56
|
+
const item = yield* dlq.get(req.itemId);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
type: "dlq-get",
|
|
60
|
+
status: 200,
|
|
61
|
+
headers: { "Content-Type": "application/json" },
|
|
62
|
+
body: item,
|
|
63
|
+
} satisfies DlqGetResponse;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Handle POST /api/dlq/:itemId/retry - Retry a specific DLQ item
|
|
68
|
+
*/
|
|
69
|
+
export const handleDlqRetry = (req: DlqRetryRequest) =>
|
|
70
|
+
Effect.gen(function* () {
|
|
71
|
+
const authService = yield* AuthContextService;
|
|
72
|
+
|
|
73
|
+
// Check permission for writing to DLQ
|
|
74
|
+
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_WRITE);
|
|
75
|
+
|
|
76
|
+
const dlq = yield* DeadLetterQueueService;
|
|
77
|
+
|
|
78
|
+
// Mark item as retrying
|
|
79
|
+
yield* dlq.markRetrying(req.itemId);
|
|
80
|
+
|
|
81
|
+
// TODO: Implement actual retry logic by re-executing the flow
|
|
82
|
+
// This would require access to FlowServer and the original job context
|
|
83
|
+
// For now, we just mark it as retrying and return success
|
|
84
|
+
// The actual retry would be handled by a background scheduler
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
type: "dlq-retry",
|
|
88
|
+
status: 200,
|
|
89
|
+
headers: { "Content-Type": "application/json" },
|
|
90
|
+
body: { success: true },
|
|
91
|
+
} satisfies DlqRetryResponse;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Handle POST /api/dlq/retry-all - Retry all matching DLQ items
|
|
96
|
+
*/
|
|
97
|
+
export const handleDlqRetryAll = (req: DlqRetryAllRequest) =>
|
|
98
|
+
Effect.gen(function* () {
|
|
99
|
+
const authService = yield* AuthContextService;
|
|
100
|
+
|
|
101
|
+
// Check permission for writing to DLQ
|
|
102
|
+
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_WRITE);
|
|
103
|
+
|
|
104
|
+
const dlq = yield* DeadLetterQueueService;
|
|
105
|
+
|
|
106
|
+
// List items matching the filter
|
|
107
|
+
const { items } = yield* dlq.list({
|
|
108
|
+
status: req.options?.status,
|
|
109
|
+
flowId: req.options?.flowId,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let succeeded = 0;
|
|
113
|
+
let failed = 0;
|
|
114
|
+
|
|
115
|
+
// Mark each item for retry
|
|
116
|
+
for (const item of items) {
|
|
117
|
+
const result = yield* Effect.either(dlq.markRetrying(item.id));
|
|
118
|
+
if (result._tag === "Right") {
|
|
119
|
+
succeeded++;
|
|
120
|
+
} else {
|
|
121
|
+
failed++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
type: "dlq-retry-all",
|
|
127
|
+
status: 200,
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
body: {
|
|
130
|
+
retried: items.length,
|
|
131
|
+
succeeded,
|
|
132
|
+
failed,
|
|
133
|
+
},
|
|
134
|
+
} satisfies DlqRetryAllResponse;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handle DELETE /api/dlq/:itemId - Delete a DLQ item
|
|
139
|
+
*/
|
|
140
|
+
export const handleDlqDelete = (req: DlqDeleteRequest) =>
|
|
141
|
+
Effect.gen(function* () {
|
|
142
|
+
const authService = yield* AuthContextService;
|
|
143
|
+
|
|
144
|
+
// Check permission for writing to DLQ
|
|
145
|
+
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_WRITE);
|
|
146
|
+
|
|
147
|
+
const dlq = yield* DeadLetterQueueService;
|
|
148
|
+
yield* dlq.delete(req.itemId);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
type: "dlq-delete",
|
|
152
|
+
status: 200,
|
|
153
|
+
headers: { "Content-Type": "application/json" },
|
|
154
|
+
body: { success: true },
|
|
155
|
+
} satisfies DlqDeleteResponse;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Handle POST /api/dlq/:itemId/resolve - Manually resolve a DLQ item
|
|
160
|
+
*/
|
|
161
|
+
export const handleDlqResolve = (req: DlqResolveRequest) =>
|
|
162
|
+
Effect.gen(function* () {
|
|
163
|
+
const authService = yield* AuthContextService;
|
|
164
|
+
|
|
165
|
+
// Check permission for writing to DLQ
|
|
166
|
+
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_WRITE);
|
|
167
|
+
|
|
168
|
+
const dlq = yield* DeadLetterQueueService;
|
|
169
|
+
const item = yield* dlq.markResolved(req.itemId);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
type: "dlq-resolve",
|
|
173
|
+
status: 200,
|
|
174
|
+
headers: { "Content-Type": "application/json" },
|
|
175
|
+
body: item,
|
|
176
|
+
} satisfies DlqResolveResponse;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Handle POST /api/dlq/cleanup - Cleanup old DLQ items
|
|
181
|
+
*/
|
|
182
|
+
export const handleDlqCleanup = (req: DlqCleanupRequest) =>
|
|
183
|
+
Effect.gen(function* () {
|
|
184
|
+
const authService = yield* AuthContextService;
|
|
185
|
+
|
|
186
|
+
// Check permission for writing to DLQ
|
|
187
|
+
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_WRITE);
|
|
188
|
+
|
|
189
|
+
const dlq = yield* DeadLetterQueueService;
|
|
190
|
+
const result = yield* dlq.cleanup(req.options);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
type: "dlq-cleanup",
|
|
194
|
+
status: 200,
|
|
195
|
+
headers: { "Content-Type": "application/json" },
|
|
196
|
+
body: result,
|
|
197
|
+
} satisfies DlqCleanupResponse;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Handle GET /api/dlq/stats - Get DLQ statistics
|
|
202
|
+
*/
|
|
203
|
+
export const handleDlqStats = (_req: DlqStatsRequest) =>
|
|
204
|
+
Effect.gen(function* () {
|
|
205
|
+
const authService = yield* AuthContextService;
|
|
206
|
+
|
|
207
|
+
// Check permission for reading DLQ
|
|
208
|
+
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_READ);
|
|
209
|
+
|
|
210
|
+
const dlq = yield* DeadLetterQueueService;
|
|
211
|
+
const stats = yield* dlq.getStats();
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
type: "dlq-stats",
|
|
215
|
+
status: 200,
|
|
216
|
+
headers: { "Content-Type": "application/json" },
|
|
217
|
+
body: stats,
|
|
218
|
+
} satisfies DlqStatsResponse;
|
|
219
|
+
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { FlowServer } from "@uploadista/core/flow";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
3
|
import { AuthCacheService } from "../../cache";
|
|
4
|
+
import { PERMISSIONS } from "../../permissions/types";
|
|
5
|
+
import { QuotaExceededError } from "../../permissions/errors";
|
|
4
6
|
import { AuthContextService } from "../../service";
|
|
7
|
+
import { UsageHookService } from "../../usage-hooks/service";
|
|
5
8
|
import type {
|
|
6
9
|
CancelFlowRequest,
|
|
7
10
|
CancelFlowResponse,
|
|
@@ -20,10 +23,12 @@ import type {
|
|
|
20
23
|
export const handleGetFlow = ({ flowId }: GetFlowRequest) => {
|
|
21
24
|
return Effect.gen(function* () {
|
|
22
25
|
const flowServer = yield* FlowServer;
|
|
23
|
-
// Access auth context if available
|
|
24
26
|
const authService = yield* AuthContextService;
|
|
25
27
|
const clientId = yield* authService.getClientId();
|
|
26
28
|
|
|
29
|
+
// Check permission for reading flow status
|
|
30
|
+
yield* authService.requirePermission(PERMISSIONS.FLOW.STATUS);
|
|
31
|
+
|
|
27
32
|
if (clientId) {
|
|
28
33
|
yield* Effect.logInfo(
|
|
29
34
|
`[Flow] Getting flow data: ${flowId}, client: ${clientId}`,
|
|
@@ -46,11 +51,14 @@ export const handleRunFlow = <TRequirements>({
|
|
|
46
51
|
}: RunFlowRequest) => {
|
|
47
52
|
return Effect.gen(function* () {
|
|
48
53
|
const flowServer = yield* FlowServer;
|
|
49
|
-
// Access auth context if available
|
|
50
54
|
const authService = yield* AuthContextService;
|
|
51
55
|
const authCache = yield* AuthCacheService;
|
|
56
|
+
const usageHookService = yield* UsageHookService;
|
|
52
57
|
const clientId = yield* authService.getClientId();
|
|
53
58
|
|
|
59
|
+
// Check permission for executing flows
|
|
60
|
+
yield* authService.requirePermission(PERMISSIONS.FLOW.EXECUTE);
|
|
61
|
+
|
|
54
62
|
if (clientId) {
|
|
55
63
|
yield* Effect.logInfo(
|
|
56
64
|
`[Flow] Executing flow: ${flowId}, storage: ${storageId}, client: ${clientId}`,
|
|
@@ -65,7 +73,28 @@ export const handleRunFlow = <TRequirements>({
|
|
|
65
73
|
);
|
|
66
74
|
}
|
|
67
75
|
|
|
76
|
+
// Execute onFlowStart hook for quota checking
|
|
77
|
+
if (clientId) {
|
|
78
|
+
const hookResult = yield* usageHookService.onFlowStart({
|
|
79
|
+
clientId,
|
|
80
|
+
operation: "flow",
|
|
81
|
+
metadata: {
|
|
82
|
+
flowId,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (hookResult.action === "abort") {
|
|
87
|
+
return yield* Effect.fail(
|
|
88
|
+
new QuotaExceededError(
|
|
89
|
+
hookResult.reason,
|
|
90
|
+
hookResult.code ?? "SUBSCRIPTION_REQUIRED",
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
68
96
|
// Run flow returns immediately with jobId
|
|
97
|
+
const startTime = Date.now();
|
|
69
98
|
yield* Effect.logInfo(`[Flow] Calling flowServer.runFlow...`);
|
|
70
99
|
const result = yield* flowServer
|
|
71
100
|
.runFlow<TRequirements>({
|
|
@@ -91,6 +120,10 @@ export const handleRunFlow = <TRequirements>({
|
|
|
91
120
|
|
|
92
121
|
yield* Effect.logInfo(`[Flow] Flow started with jobId: ${result.id}`);
|
|
93
122
|
|
|
123
|
+
// Note: onFlowComplete hook is called when the flow actually completes,
|
|
124
|
+
// which happens asynchronously. The hook should be invoked from the
|
|
125
|
+
// flow execution pipeline, not here where we just start the flow.
|
|
126
|
+
|
|
94
127
|
return {
|
|
95
128
|
status: 200,
|
|
96
129
|
body: result,
|
|
@@ -101,11 +134,13 @@ export const handleRunFlow = <TRequirements>({
|
|
|
101
134
|
export const handleJobStatus = ({ jobId }: GetJobStatusRequest) => {
|
|
102
135
|
return Effect.gen(function* () {
|
|
103
136
|
const flowServer = yield* FlowServer;
|
|
104
|
-
// Access auth context if available
|
|
105
137
|
const authService = yield* AuthContextService;
|
|
106
138
|
const authCache = yield* AuthCacheService;
|
|
107
139
|
const clientId = yield* authService.getClientId();
|
|
108
140
|
|
|
141
|
+
// Check permission for checking flow status
|
|
142
|
+
yield* authService.requirePermission(PERMISSIONS.FLOW.STATUS);
|
|
143
|
+
|
|
109
144
|
if (!jobId) {
|
|
110
145
|
throw new Error("No job id");
|
|
111
146
|
}
|
|
@@ -142,10 +177,12 @@ export const handleResumeFlow = <TRequirements>({
|
|
|
142
177
|
}: ResumeFlowRequest) => {
|
|
143
178
|
return Effect.gen(function* () {
|
|
144
179
|
const flowServer = yield* FlowServer;
|
|
145
|
-
// Try to get auth from current request or cached auth
|
|
146
180
|
const authService = yield* AuthContextService;
|
|
147
181
|
const authCache = yield* AuthCacheService;
|
|
148
182
|
|
|
183
|
+
// Check permission for executing flows (resume is part of execution)
|
|
184
|
+
yield* authService.requirePermission(PERMISSIONS.FLOW.EXECUTE);
|
|
185
|
+
|
|
149
186
|
// Try current auth first, fallback to cached auth
|
|
150
187
|
let clientId = yield* authService.getClientId();
|
|
151
188
|
if (!clientId) {
|
|
@@ -186,13 +223,16 @@ export const handleResumeFlow = <TRequirements>({
|
|
|
186
223
|
} as ResumeFlowResponse;
|
|
187
224
|
});
|
|
188
225
|
};
|
|
226
|
+
|
|
189
227
|
export const handlePauseFlow = ({ jobId }: PauseFlowRequest) => {
|
|
190
228
|
return Effect.gen(function* () {
|
|
191
229
|
const flowServer = yield* FlowServer;
|
|
192
|
-
// Try to get auth from current request or cached auth
|
|
193
230
|
const authService = yield* AuthContextService;
|
|
194
231
|
const authCache = yield* AuthCacheService;
|
|
195
232
|
|
|
233
|
+
// Check permission for cancelling flows (pause is related to cancel)
|
|
234
|
+
yield* authService.requirePermission(PERMISSIONS.FLOW.CANCEL);
|
|
235
|
+
|
|
196
236
|
// Try current auth first, fallback to cached auth
|
|
197
237
|
let clientId = yield* authService.getClientId();
|
|
198
238
|
if (!clientId) {
|
|
@@ -224,9 +264,12 @@ export const handlePauseFlow = ({ jobId }: PauseFlowRequest) => {
|
|
|
224
264
|
export const handleCancelFlow = ({ jobId }: CancelFlowRequest) => {
|
|
225
265
|
return Effect.gen(function* () {
|
|
226
266
|
const flowServer = yield* FlowServer;
|
|
227
|
-
// Try to get auth from current request or cached auth
|
|
228
267
|
const authService = yield* AuthContextService;
|
|
229
268
|
const authCache = yield* AuthCacheService;
|
|
269
|
+
const usageHookService = yield* UsageHookService;
|
|
270
|
+
|
|
271
|
+
// Check permission for cancelling flows
|
|
272
|
+
yield* authService.requirePermission(PERMISSIONS.FLOW.CANCEL);
|
|
230
273
|
|
|
231
274
|
if (!jobId) {
|
|
232
275
|
throw new Error("No job id");
|
|
@@ -253,6 +296,18 @@ export const handleCancelFlow = ({ jobId }: CancelFlowRequest) => {
|
|
|
253
296
|
yield* Effect.logInfo(
|
|
254
297
|
`[Flow] Flow cancelled, cleared auth cache: ${jobId}`,
|
|
255
298
|
);
|
|
299
|
+
|
|
300
|
+
// Execute onFlowComplete hook for cancelled flow
|
|
301
|
+
yield* Effect.forkDaemon(
|
|
302
|
+
usageHookService.onFlowComplete({
|
|
303
|
+
clientId,
|
|
304
|
+
operation: "flow",
|
|
305
|
+
metadata: {
|
|
306
|
+
jobId,
|
|
307
|
+
status: "cancelled",
|
|
308
|
+
},
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
256
311
|
}
|
|
257
312
|
|
|
258
313
|
return {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check HTTP Handlers for Uploadista SDK.
|
|
3
|
+
*
|
|
4
|
+
* This module provides HTTP handlers for health check endpoints:
|
|
5
|
+
* - `/health` (liveness) - Simple alive check, no dependencies
|
|
6
|
+
* - `/ready` (readiness) - Full dependency check for accepting traffic
|
|
7
|
+
* - `/health/components` - Detailed component status for debugging
|
|
8
|
+
*
|
|
9
|
+
* @module core/http-handlers/health-http-handlers
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
formatHealthAsText,
|
|
14
|
+
getHealthResponseFormat,
|
|
15
|
+
type HealthCheckConfig,
|
|
16
|
+
} from "@uploadista/core/types";
|
|
17
|
+
import { Effect } from "effect";
|
|
18
|
+
import { PERMISSIONS } from "../../permissions/types";
|
|
19
|
+
import { AuthContextService } from "../../service";
|
|
20
|
+
import {
|
|
21
|
+
createLivenessResponse,
|
|
22
|
+
performComponentsCheck,
|
|
23
|
+
performReadinessCheck,
|
|
24
|
+
} from "../health-check-service";
|
|
25
|
+
import type {
|
|
26
|
+
HealthComponentsRequest,
|
|
27
|
+
HealthComponentsResponse,
|
|
28
|
+
HealthReadyRequest,
|
|
29
|
+
HealthReadyResponse,
|
|
30
|
+
HealthRequest,
|
|
31
|
+
HealthResponse,
|
|
32
|
+
} from "../routes";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Handle GET /health - Liveness probe
|
|
36
|
+
*
|
|
37
|
+
* Returns immediately with 200 OK if the server is alive.
|
|
38
|
+
* Does not check any dependencies.
|
|
39
|
+
*/
|
|
40
|
+
export const handleHealthLiveness = (
|
|
41
|
+
req: HealthRequest,
|
|
42
|
+
config?: HealthCheckConfig,
|
|
43
|
+
) =>
|
|
44
|
+
Effect.sync(() => {
|
|
45
|
+
const response = createLivenessResponse(config);
|
|
46
|
+
const format = getHealthResponseFormat(req.acceptHeader);
|
|
47
|
+
|
|
48
|
+
if (format === "text") {
|
|
49
|
+
return {
|
|
50
|
+
type: "health",
|
|
51
|
+
status: 200,
|
|
52
|
+
headers: { "Content-Type": "text/plain" },
|
|
53
|
+
body: formatHealthAsText(response.status),
|
|
54
|
+
} as HealthResponse;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
type: "health",
|
|
59
|
+
status: 200,
|
|
60
|
+
headers: { "Content-Type": "application/json" },
|
|
61
|
+
body: response,
|
|
62
|
+
} satisfies HealthResponse;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Handle GET /ready - Readiness probe
|
|
67
|
+
*
|
|
68
|
+
* Checks all critical dependencies (storage, KV store) and returns:
|
|
69
|
+
* - 200 OK if all dependencies are healthy
|
|
70
|
+
* - 503 Service Unavailable if any critical dependency is unavailable
|
|
71
|
+
*
|
|
72
|
+
* Requires `engine:readiness` permission.
|
|
73
|
+
*/
|
|
74
|
+
export const handleHealthReadiness = (
|
|
75
|
+
req: HealthReadyRequest,
|
|
76
|
+
config?: HealthCheckConfig,
|
|
77
|
+
) =>
|
|
78
|
+
Effect.gen(function* () {
|
|
79
|
+
const authService = yield* AuthContextService;
|
|
80
|
+
|
|
81
|
+
// Check permission for readiness endpoint
|
|
82
|
+
yield* authService.requirePermission(PERMISSIONS.ENGINE.READINESS);
|
|
83
|
+
|
|
84
|
+
const response = yield* performReadinessCheck(config);
|
|
85
|
+
const format = getHealthResponseFormat(req.acceptHeader);
|
|
86
|
+
|
|
87
|
+
// Determine HTTP status based on health status
|
|
88
|
+
const httpStatus = response.status === "unhealthy" ? 503 : 200;
|
|
89
|
+
|
|
90
|
+
if (format === "text") {
|
|
91
|
+
return {
|
|
92
|
+
type: "health-ready",
|
|
93
|
+
status: httpStatus,
|
|
94
|
+
headers: { "Content-Type": "text/plain" },
|
|
95
|
+
body: formatHealthAsText(response.status),
|
|
96
|
+
} as HealthReadyResponse;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
type: "health-ready",
|
|
101
|
+
status: httpStatus,
|
|
102
|
+
headers: { "Content-Type": "application/json" },
|
|
103
|
+
body: response,
|
|
104
|
+
} satisfies HealthReadyResponse;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Handle GET /health/components - Detailed component status
|
|
109
|
+
*
|
|
110
|
+
* Returns detailed health information for each component including:
|
|
111
|
+
* - Storage backend
|
|
112
|
+
* - KV store
|
|
113
|
+
* - Event broadcaster
|
|
114
|
+
* - Circuit breaker (if enabled)
|
|
115
|
+
* - Dead letter queue (if enabled)
|
|
116
|
+
*
|
|
117
|
+
* Always returns 200 OK for debugging purposes (even if components are degraded).
|
|
118
|
+
*
|
|
119
|
+
* Requires `engine:readiness` permission.
|
|
120
|
+
*/
|
|
121
|
+
export const handleHealthComponents = (
|
|
122
|
+
req: HealthComponentsRequest,
|
|
123
|
+
config?: HealthCheckConfig,
|
|
124
|
+
) =>
|
|
125
|
+
Effect.gen(function* () {
|
|
126
|
+
const authService = yield* AuthContextService;
|
|
127
|
+
|
|
128
|
+
// Check permission for components endpoint
|
|
129
|
+
yield* authService.requirePermission(PERMISSIONS.ENGINE.READINESS);
|
|
130
|
+
|
|
131
|
+
const response = yield* performComponentsCheck(config);
|
|
132
|
+
const format = getHealthResponseFormat(req.acceptHeader);
|
|
133
|
+
|
|
134
|
+
if (format === "text") {
|
|
135
|
+
// For text format, just return the overall status
|
|
136
|
+
return {
|
|
137
|
+
type: "health-components",
|
|
138
|
+
status: 200,
|
|
139
|
+
headers: { "Content-Type": "text/plain" },
|
|
140
|
+
body: formatHealthAsText(response.status),
|
|
141
|
+
} as HealthComponentsResponse;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
type: "health-components",
|
|
146
|
+
status: 200,
|
|
147
|
+
headers: { "Content-Type": "application/json" },
|
|
148
|
+
body: response,
|
|
149
|
+
} satisfies HealthComponentsResponse;
|
|
150
|
+
});
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import type { HealthCheckConfig } from "@uploadista/core/types";
|
|
1
2
|
import { Effect } from "effect";
|
|
2
3
|
import type { UploadistaRequest, UploadistaResponse } from "../routes";
|
|
4
|
+
import {
|
|
5
|
+
handleDlqCleanup,
|
|
6
|
+
handleDlqDelete,
|
|
7
|
+
handleDlqGet,
|
|
8
|
+
handleDlqList,
|
|
9
|
+
handleDlqResolve,
|
|
10
|
+
handleDlqRetry,
|
|
11
|
+
handleDlqRetryAll,
|
|
12
|
+
handleDlqStats,
|
|
13
|
+
} from "./dlq-http-handlers";
|
|
3
14
|
import {
|
|
4
15
|
handleCancelFlow,
|
|
5
16
|
handleGetFlow,
|
|
@@ -8,6 +19,11 @@ import {
|
|
|
8
19
|
handleResumeFlow,
|
|
9
20
|
handleRunFlow,
|
|
10
21
|
} from "./flow-http-handlers";
|
|
22
|
+
import {
|
|
23
|
+
handleHealthComponents,
|
|
24
|
+
handleHealthLiveness,
|
|
25
|
+
handleHealthReadiness,
|
|
26
|
+
} from "./health-http-handlers";
|
|
11
27
|
import {
|
|
12
28
|
handleCreateUpload,
|
|
13
29
|
handleGetCapabilities,
|
|
@@ -19,6 +35,7 @@ export type { UploadistaRequest, UploadistaResponse } from "../routes";
|
|
|
19
35
|
|
|
20
36
|
export const handleUploadistaRequest = <TRequirements>(
|
|
21
37
|
req: UploadistaRequest,
|
|
38
|
+
options?: { healthCheckConfig?: HealthCheckConfig },
|
|
22
39
|
) => {
|
|
23
40
|
return Effect.gen(function* () {
|
|
24
41
|
switch (req.type) {
|
|
@@ -44,6 +61,39 @@ export const handleUploadistaRequest = <TRequirements>(
|
|
|
44
61
|
return (yield* handlePauseFlow(req)) as UploadistaResponse;
|
|
45
62
|
case "cancel-flow":
|
|
46
63
|
return (yield* handleCancelFlow(req)) as UploadistaResponse;
|
|
64
|
+
// DLQ Admin routes
|
|
65
|
+
case "dlq-list":
|
|
66
|
+
return (yield* handleDlqList(req)) as UploadistaResponse;
|
|
67
|
+
case "dlq-get":
|
|
68
|
+
return (yield* handleDlqGet(req)) as UploadistaResponse;
|
|
69
|
+
case "dlq-retry":
|
|
70
|
+
return (yield* handleDlqRetry(req)) as UploadistaResponse;
|
|
71
|
+
case "dlq-retry-all":
|
|
72
|
+
return (yield* handleDlqRetryAll(req)) as UploadistaResponse;
|
|
73
|
+
case "dlq-delete":
|
|
74
|
+
return (yield* handleDlqDelete(req)) as UploadistaResponse;
|
|
75
|
+
case "dlq-resolve":
|
|
76
|
+
return (yield* handleDlqResolve(req)) as UploadistaResponse;
|
|
77
|
+
case "dlq-cleanup":
|
|
78
|
+
return (yield* handleDlqCleanup(req)) as UploadistaResponse;
|
|
79
|
+
case "dlq-stats":
|
|
80
|
+
return (yield* handleDlqStats(req)) as UploadistaResponse;
|
|
81
|
+
// Health check routes
|
|
82
|
+
case "health":
|
|
83
|
+
return (yield* handleHealthLiveness(
|
|
84
|
+
req,
|
|
85
|
+
options?.healthCheckConfig,
|
|
86
|
+
)) as UploadistaResponse;
|
|
87
|
+
case "health-ready":
|
|
88
|
+
return (yield* handleHealthReadiness(
|
|
89
|
+
req,
|
|
90
|
+
options?.healthCheckConfig,
|
|
91
|
+
)) as UploadistaResponse;
|
|
92
|
+
case "health-components":
|
|
93
|
+
return (yield* handleHealthComponents(
|
|
94
|
+
req,
|
|
95
|
+
options?.healthCheckConfig,
|
|
96
|
+
)) as UploadistaResponse;
|
|
47
97
|
case "not-found":
|
|
48
98
|
return {
|
|
49
99
|
status: 404,
|