@lucaapp/service-utils 4.10.0 → 4.12.0

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/dist/index.d.ts CHANGED
@@ -16,4 +16,6 @@ export * from './lib/random';
16
16
  export * from './lib/crypto';
17
17
  export * from './lib/phone';
18
18
  export * from './lib/http';
19
+ export * from './lib/jobs';
20
+ export * from './lib/validation';
19
21
  export * from './types';
package/dist/index.js CHANGED
@@ -32,4 +32,6 @@ __exportStar(require("./lib/random"), exports);
32
32
  __exportStar(require("./lib/crypto"), exports);
33
33
  __exportStar(require("./lib/phone"), exports);
34
34
  __exportStar(require("./lib/http"), exports);
35
+ __exportStar(require("./lib/jobs"), exports);
36
+ __exportStar(require("./lib/validation"), exports);
35
37
  __exportStar(require("./types"), exports);
@@ -0,0 +1,2 @@
1
+ export * from './jobTracker';
2
+ export * from './schemas';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./jobTracker"), exports);
18
+ __exportStar(require("./schemas"), exports);
@@ -0,0 +1,30 @@
1
+ import { Logger } from 'pino';
2
+ import { JsonSchema } from './schemas';
3
+ export interface JobStatus {
4
+ jobId: string;
5
+ jobName: string;
6
+ method: string;
7
+ status: 'running' | 'completed' | 'failed';
8
+ result?: JsonSchema;
9
+ error?: string;
10
+ startedAt: string;
11
+ completedAt?: string;
12
+ duration?: number;
13
+ }
14
+ export interface RedisClient {
15
+ setex(key: string | Buffer, seconds: number, value: string | Buffer): Promise<string | void>;
16
+ get(key: string | Buffer): Promise<string | null>;
17
+ scan(cursor: number, command: string, pattern: string, countCommand: string, count: number): Promise<[string, string[]]>;
18
+ }
19
+ export declare const createJobTracker: (logger: Logger, redis: RedisClient) => {
20
+ createJobExecution: (jobName: string, method: string) => Promise<string>;
21
+ updateJobStatus: (jobId: string, result: JsonSchema, error?: Error) => Promise<void>;
22
+ getJobStatus: (jobId: string) => Promise<JobStatus | null>;
23
+ getAllRunningJobs: () => Promise<JobStatus[]>;
24
+ getLatestCompletions: () => Promise<Record<string, {
25
+ completedAt: string;
26
+ status: "completed" | "failed";
27
+ error?: string;
28
+ }>>;
29
+ };
30
+ export type JobTracker = ReturnType<typeof createJobTracker>;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createJobTracker = void 0;
4
+ const uuid_1 = require("uuid");
5
+ const JOB_STATUS_PREFIX = 'job:status:';
6
+ const JOB_TTL = 24 * 60 * 60;
7
+ const createJobTracker = (logger, redis) => {
8
+ const createJobExecution = async (jobName, method) => {
9
+ const jobId = (0, uuid_1.v4)();
10
+ const jobStatus = {
11
+ jobId,
12
+ jobName,
13
+ method,
14
+ status: 'running',
15
+ startedAt: new Date().toISOString(),
16
+ };
17
+ const key = `${JOB_STATUS_PREFIX}${jobId}`;
18
+ await redis.setex(key, JOB_TTL, JSON.stringify(jobStatus));
19
+ logger.info(`Created job execution: ${jobId} for job: ${jobName}, method: ${method}`);
20
+ return jobId;
21
+ };
22
+ const updateJobStatus = async (jobId, result, error) => {
23
+ const key = `${JOB_STATUS_PREFIX}${jobId}`;
24
+ const data = await redis.get(key);
25
+ if (!data) {
26
+ logger.warn(`Job status not found for jobId: ${jobId}`);
27
+ return;
28
+ }
29
+ const jobStatus = JSON.parse(data);
30
+ const completedAt = new Date();
31
+ const startedAt = new Date(jobStatus.startedAt);
32
+ const duration = completedAt.getTime() - startedAt.getTime();
33
+ if (error) {
34
+ jobStatus.status = 'failed';
35
+ jobStatus.error = error.message;
36
+ }
37
+ else {
38
+ jobStatus.status = 'completed';
39
+ jobStatus.result = result;
40
+ }
41
+ jobStatus.completedAt = completedAt.toISOString();
42
+ jobStatus.duration = duration;
43
+ await redis.setex(key, JOB_TTL, JSON.stringify(jobStatus));
44
+ logger.info(`Updated job status: ${jobId} - status: ${jobStatus.status}, duration: ${duration}ms`);
45
+ };
46
+ const getJobStatus = async (jobId) => {
47
+ const key = `${JOB_STATUS_PREFIX}${jobId}`;
48
+ const data = await redis.get(key);
49
+ if (!data) {
50
+ logger.warn(`Job status not found for jobId: ${jobId}`);
51
+ return null;
52
+ }
53
+ return JSON.parse(data);
54
+ };
55
+ const getAllRunningJobs = async () => {
56
+ const pattern = `${JOB_STATUS_PREFIX}*`;
57
+ const allJobs = [];
58
+ let cursor = 0;
59
+ do {
60
+ const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
61
+ cursor = Number.parseInt(nextCursor, 10);
62
+ for (const key of keys) {
63
+ const data = await redis.get(key);
64
+ if (data) {
65
+ const jobStatus = JSON.parse(data);
66
+ if (jobStatus.status === 'running') {
67
+ allJobs.push(jobStatus);
68
+ }
69
+ }
70
+ }
71
+ } while (cursor !== 0);
72
+ logger.info(`Found ${allJobs.length} running jobs`);
73
+ return allJobs;
74
+ };
75
+ const getLatestCompletions = async () => {
76
+ const pattern = `${JOB_STATUS_PREFIX}*`;
77
+ const completions = new Map();
78
+ let cursor = 0;
79
+ do {
80
+ const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
81
+ cursor = Number.parseInt(nextCursor, 10);
82
+ for (const key of keys) {
83
+ const data = await redis.get(key);
84
+ if (data) {
85
+ const jobStatus = JSON.parse(data);
86
+ if (jobStatus.completedAt &&
87
+ (jobStatus.status === 'completed' || jobStatus.status === 'failed')) {
88
+ const jobKey = `${jobStatus.jobName}/${jobStatus.method}`;
89
+ const existing = completions.get(jobKey);
90
+ if (!existing ||
91
+ new Date(jobStatus.completedAt) > new Date(existing.completedAt)) {
92
+ completions.set(jobKey, {
93
+ completedAt: jobStatus.completedAt,
94
+ status: jobStatus.status,
95
+ error: jobStatus.error,
96
+ });
97
+ }
98
+ }
99
+ }
100
+ }
101
+ } while (cursor !== 0);
102
+ logger.info(`Found ${completions.size} job completions`);
103
+ return Object.fromEntries(completions);
104
+ };
105
+ return {
106
+ createJobExecution,
107
+ updateJobStatus,
108
+ getJobStatus,
109
+ getAllRunningJobs,
110
+ getLatestCompletions,
111
+ };
112
+ };
113
+ exports.createJobTracker = createJobTracker;
@@ -0,0 +1,258 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Schema for job execution parameters
4
+ */
5
+ export declare const parametersSchema: z.ZodObject<{
6
+ job: z.ZodString;
7
+ method: z.ZodString;
8
+ }, "strip", z.ZodTypeAny, {
9
+ method: string;
10
+ job: string;
11
+ }, {
12
+ method: string;
13
+ job: string;
14
+ }>;
15
+ /**
16
+ * Schema for job ID parameters
17
+ */
18
+ export declare const jobIdParametersSchema: z.ZodObject<{
19
+ jobId: z.ZodString;
20
+ }, "strip", z.ZodTypeAny, {
21
+ jobId: string;
22
+ }, {
23
+ jobId: string;
24
+ }>;
25
+ /**
26
+ * Schema for JSON request/response bodies
27
+ * Allows any object structure to pass through
28
+ */
29
+ export declare const jsonSchema: z.ZodObject<{}, "passthrough", z.ZodTypeAny, z.objectOutputType<{}, z.ZodTypeAny, "passthrough">, z.objectInputType<{}, z.ZodTypeAny, "passthrough">>;
30
+ export type JsonSchema = z.infer<typeof jsonSchema>;
31
+ /**
32
+ * Schema for running job information
33
+ */
34
+ export declare const runningJobSchema: z.ZodObject<{
35
+ jobId: z.ZodString;
36
+ jobName: z.ZodString;
37
+ method: z.ZodString;
38
+ status: z.ZodEnum<["running", "completed", "failed"]>;
39
+ startedAt: z.ZodString;
40
+ completedAt: z.ZodOptional<z.ZodString>;
41
+ duration: z.ZodOptional<z.ZodNumber>;
42
+ error: z.ZodOptional<z.ZodString>;
43
+ }, "strip", z.ZodTypeAny, {
44
+ status: "running" | "completed" | "failed";
45
+ method: string;
46
+ jobId: string;
47
+ jobName: string;
48
+ startedAt: string;
49
+ completedAt?: string | undefined;
50
+ duration?: number | undefined;
51
+ error?: string | undefined;
52
+ }, {
53
+ status: "running" | "completed" | "failed";
54
+ method: string;
55
+ jobId: string;
56
+ jobName: string;
57
+ startedAt: string;
58
+ completedAt?: string | undefined;
59
+ duration?: number | undefined;
60
+ error?: string | undefined;
61
+ }>;
62
+ /**
63
+ * Schema for job completion information
64
+ */
65
+ export declare const jobCompletionSchema: z.ZodObject<{
66
+ completedAt: z.ZodString;
67
+ status: z.ZodEnum<["completed", "failed"]>;
68
+ error: z.ZodOptional<z.ZodString>;
69
+ }, "strip", z.ZodTypeAny, {
70
+ status: "completed" | "failed";
71
+ completedAt: string;
72
+ error?: string | undefined;
73
+ }, {
74
+ status: "completed" | "failed";
75
+ completedAt: string;
76
+ error?: string | undefined;
77
+ }>;
78
+ /**
79
+ * Schema for job metadata (path and methods)
80
+ */
81
+ export declare const jobMetadataSchema: z.ZodObject<{
82
+ path: z.ZodString;
83
+ methods: z.ZodArray<z.ZodString, "many">;
84
+ lastExecution: z.ZodOptional<z.ZodNullable<z.ZodObject<{
85
+ startedAt: z.ZodString;
86
+ completedAt: z.ZodString;
87
+ duration: z.ZodNumber;
88
+ error: z.ZodNullable<z.ZodString>;
89
+ }, "strip", z.ZodTypeAny, {
90
+ error: string | null;
91
+ startedAt: string;
92
+ completedAt: string;
93
+ duration: number;
94
+ }, {
95
+ error: string | null;
96
+ startedAt: string;
97
+ completedAt: string;
98
+ duration: number;
99
+ }>>>;
100
+ }, "strip", z.ZodTypeAny, {
101
+ path: string;
102
+ methods: string[];
103
+ lastExecution?: {
104
+ error: string | null;
105
+ startedAt: string;
106
+ completedAt: string;
107
+ duration: number;
108
+ } | null | undefined;
109
+ }, {
110
+ path: string;
111
+ methods: string[];
112
+ lastExecution?: {
113
+ error: string | null;
114
+ startedAt: string;
115
+ completedAt: string;
116
+ duration: number;
117
+ } | null | undefined;
118
+ }>;
119
+ /**
120
+ * Schema for the complete jobs response
121
+ */
122
+ export declare const jobsSchema: z.ZodObject<{
123
+ jobs: z.ZodArray<z.ZodObject<{
124
+ path: z.ZodString;
125
+ methods: z.ZodArray<z.ZodString, "many">;
126
+ lastExecution: z.ZodOptional<z.ZodNullable<z.ZodObject<{
127
+ startedAt: z.ZodString;
128
+ completedAt: z.ZodString;
129
+ duration: z.ZodNumber;
130
+ error: z.ZodNullable<z.ZodString>;
131
+ }, "strip", z.ZodTypeAny, {
132
+ error: string | null;
133
+ startedAt: string;
134
+ completedAt: string;
135
+ duration: number;
136
+ }, {
137
+ error: string | null;
138
+ startedAt: string;
139
+ completedAt: string;
140
+ duration: number;
141
+ }>>>;
142
+ }, "strip", z.ZodTypeAny, {
143
+ path: string;
144
+ methods: string[];
145
+ lastExecution?: {
146
+ error: string | null;
147
+ startedAt: string;
148
+ completedAt: string;
149
+ duration: number;
150
+ } | null | undefined;
151
+ }, {
152
+ path: string;
153
+ methods: string[];
154
+ lastExecution?: {
155
+ error: string | null;
156
+ startedAt: string;
157
+ completedAt: string;
158
+ duration: number;
159
+ } | null | undefined;
160
+ }>, "many">;
161
+ runningJobs: z.ZodArray<z.ZodObject<{
162
+ jobId: z.ZodString;
163
+ jobName: z.ZodString;
164
+ method: z.ZodString;
165
+ status: z.ZodEnum<["running", "completed", "failed"]>;
166
+ startedAt: z.ZodString;
167
+ completedAt: z.ZodOptional<z.ZodString>;
168
+ duration: z.ZodOptional<z.ZodNumber>;
169
+ error: z.ZodOptional<z.ZodString>;
170
+ }, "strip", z.ZodTypeAny, {
171
+ status: "running" | "completed" | "failed";
172
+ method: string;
173
+ jobId: string;
174
+ jobName: string;
175
+ startedAt: string;
176
+ completedAt?: string | undefined;
177
+ duration?: number | undefined;
178
+ error?: string | undefined;
179
+ }, {
180
+ status: "running" | "completed" | "failed";
181
+ method: string;
182
+ jobId: string;
183
+ jobName: string;
184
+ startedAt: string;
185
+ completedAt?: string | undefined;
186
+ duration?: number | undefined;
187
+ error?: string | undefined;
188
+ }>, "many">;
189
+ jobCompletions: z.ZodRecord<z.ZodString, z.ZodObject<{
190
+ completedAt: z.ZodString;
191
+ status: z.ZodEnum<["completed", "failed"]>;
192
+ error: z.ZodOptional<z.ZodString>;
193
+ }, "strip", z.ZodTypeAny, {
194
+ status: "completed" | "failed";
195
+ completedAt: string;
196
+ error?: string | undefined;
197
+ }, {
198
+ status: "completed" | "failed";
199
+ completedAt: string;
200
+ error?: string | undefined;
201
+ }>>;
202
+ }, "strip", z.ZodTypeAny, {
203
+ jobs: {
204
+ path: string;
205
+ methods: string[];
206
+ lastExecution?: {
207
+ error: string | null;
208
+ startedAt: string;
209
+ completedAt: string;
210
+ duration: number;
211
+ } | null | undefined;
212
+ }[];
213
+ runningJobs: {
214
+ status: "running" | "completed" | "failed";
215
+ method: string;
216
+ jobId: string;
217
+ jobName: string;
218
+ startedAt: string;
219
+ completedAt?: string | undefined;
220
+ duration?: number | undefined;
221
+ error?: string | undefined;
222
+ }[];
223
+ jobCompletions: Record<string, {
224
+ status: "completed" | "failed";
225
+ completedAt: string;
226
+ error?: string | undefined;
227
+ }>;
228
+ }, {
229
+ jobs: {
230
+ path: string;
231
+ methods: string[];
232
+ lastExecution?: {
233
+ error: string | null;
234
+ startedAt: string;
235
+ completedAt: string;
236
+ duration: number;
237
+ } | null | undefined;
238
+ }[];
239
+ runningJobs: {
240
+ status: "running" | "completed" | "failed";
241
+ method: string;
242
+ jobId: string;
243
+ jobName: string;
244
+ startedAt: string;
245
+ completedAt?: string | undefined;
246
+ duration?: number | undefined;
247
+ error?: string | undefined;
248
+ }[];
249
+ jobCompletions: Record<string, {
250
+ status: "completed" | "failed";
251
+ completedAt: string;
252
+ error?: string | undefined;
253
+ }>;
254
+ }>;
255
+ export type JobsSchema = z.infer<typeof jobsSchema>;
256
+ export type RunningJobSchema = z.infer<typeof runningJobSchema>;
257
+ export type JobCompletionSchema = z.infer<typeof jobCompletionSchema>;
258
+ export type JobMetadataSchema = z.infer<typeof jobMetadataSchema>;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.jobsSchema = exports.jobMetadataSchema = exports.jobCompletionSchema = exports.runningJobSchema = exports.jsonSchema = exports.jobIdParametersSchema = exports.parametersSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ /**
6
+ * Schema for job execution parameters
7
+ */
8
+ exports.parametersSchema = zod_1.z.object({
9
+ job: zod_1.z.string(),
10
+ method: zod_1.z.string(),
11
+ });
12
+ /**
13
+ * Schema for job ID parameters
14
+ */
15
+ exports.jobIdParametersSchema = zod_1.z.object({
16
+ jobId: zod_1.z.string(),
17
+ });
18
+ /**
19
+ * Schema for JSON request/response bodies
20
+ * Allows any object structure to pass through
21
+ */
22
+ exports.jsonSchema = zod_1.z.object({}).passthrough();
23
+ /**
24
+ * Schema for running job information
25
+ */
26
+ exports.runningJobSchema = zod_1.z.object({
27
+ jobId: zod_1.z.string(),
28
+ jobName: zod_1.z.string(),
29
+ method: zod_1.z.string(),
30
+ status: zod_1.z.enum(['running', 'completed', 'failed']),
31
+ startedAt: zod_1.z.string(),
32
+ completedAt: zod_1.z.string().optional(),
33
+ duration: zod_1.z.number().optional(),
34
+ error: zod_1.z.string().optional(),
35
+ });
36
+ /**
37
+ * Schema for job completion information
38
+ */
39
+ exports.jobCompletionSchema = zod_1.z.object({
40
+ completedAt: zod_1.z.string(),
41
+ status: zod_1.z.enum(['completed', 'failed']),
42
+ error: zod_1.z.string().optional(),
43
+ });
44
+ /**
45
+ * Schema for job metadata (path and methods)
46
+ */
47
+ exports.jobMetadataSchema = zod_1.z.object({
48
+ path: zod_1.z.string(),
49
+ methods: zod_1.z.array(zod_1.z.string()),
50
+ lastExecution: zod_1.z
51
+ .object({
52
+ startedAt: zod_1.z.string(),
53
+ completedAt: zod_1.z.string(),
54
+ duration: zod_1.z.number(),
55
+ error: zod_1.z.string().nullable(),
56
+ })
57
+ .nullable()
58
+ .optional(),
59
+ });
60
+ /**
61
+ * Schema for the complete jobs response
62
+ */
63
+ exports.jobsSchema = zod_1.z.object({
64
+ jobs: zod_1.z.array(exports.jobMetadataSchema),
65
+ runningJobs: zod_1.z.array(exports.runningJobSchema),
66
+ jobCompletions: zod_1.z.record(exports.jobCompletionSchema),
67
+ });
@@ -34,6 +34,7 @@ const api_1 = require("../api");
34
34
  const moment_1 = __importDefault(require("moment"));
35
35
  const requestTracer_1 = require("../requestTracer");
36
36
  const axios_1 = __importDefault(require("axios"));
37
+ const axios_retry_1 = __importDefault(require("axios-retry"));
37
38
  const zod_1 = require("zod");
38
39
  const validator_1 = __importDefault(require("validator"));
39
40
  const JWT_ALGORITHM = 'ES256';
@@ -188,6 +189,17 @@ class ServiceIdentity {
188
189
  this.identityPrivateKey = identityPrivateKey;
189
190
  this.identityPublicKey = identityPublicKey;
190
191
  this.axiosClient = axios_1.default.create({ proxy: false });
192
+ // Configure axios-retry to handle 429 and respect Retry-After header
193
+ (0, axios_retry_1.default)(this.axiosClient, {
194
+ retries: 3,
195
+ retryDelay: axios_retry_1.default.exponentialDelay,
196
+ shouldResetTimeout: true,
197
+ retryCondition: (error) => {
198
+ // Retry on network errors, 5xx errors, and 429 (rate limit)
199
+ return (axios_retry_1.default.isNetworkOrIdempotentRequestError(error) ||
200
+ error.response?.status === 429);
201
+ },
202
+ });
191
203
  if (!debug) {
192
204
  this.axiosClient.interceptors.response.use(response => response, error => {
193
205
  if (Array.isArray(error?.config?.headers) &&