@autofleet/sheilta 2.2.8 → 2.4.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/lib/chunk-CTAAG5j7.js +1 -0
- package/lib/index.cjs +115 -1
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +422 -6
- package/lib/index.d.ts +419 -5
- package/lib/index.js +58 -1
- package/lib/index.js.map +1 -1
- package/package.json +6 -2
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { Op, literal } from "sequelize";
|
|
1
|
+
import { Model, ModelStatic, Op, Sequelize, WhereOptions, literal } from "sequelize";
|
|
2
2
|
import { LoggerInstanceManager } from "@autofleet/logger";
|
|
3
|
-
import {
|
|
3
|
+
import { RedisClientType, createClient } from "redis";
|
|
4
|
+
import { Handler, Router } from "express";
|
|
5
|
+
import { z, z as z$1 } from "zod/v3";
|
|
6
|
+
import { EventEmitter } from "node:events";
|
|
7
|
+
import rabbit from "@autofleet/rabbit";
|
|
4
8
|
|
|
5
9
|
//#region src/operators/index.d.ts
|
|
6
10
|
|
|
@@ -180,7 +184,7 @@ interface QueryHandlerOptions {
|
|
|
180
184
|
count: number;
|
|
181
185
|
}, queryValues: QueryValues) => any;
|
|
182
186
|
}
|
|
183
|
-
type Asyncify<T extends (...a: any[]) => any> = (...a: Parameters<T>) => Promise<Awaited<ReturnType<T>>>;
|
|
187
|
+
type Asyncify$1<T extends (...a: any[]) => any> = (...a: Parameters<T>) => Promise<Awaited<ReturnType<T>>>;
|
|
184
188
|
declare const queryHandler: ({
|
|
185
189
|
model,
|
|
186
190
|
logger,
|
|
@@ -190,7 +194,417 @@ declare const queryHandler: ({
|
|
|
190
194
|
additionalScopes,
|
|
191
195
|
modifyQueryValues,
|
|
192
196
|
onRowsRetrieved
|
|
193
|
-
}: QueryHandlerOptions) => Asyncify<Handler>;
|
|
197
|
+
}: QueryHandlerOptions) => Asyncify$1<Handler>;
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/bulker/src/types.d.ts
|
|
200
|
+
type Id = string | number;
|
|
201
|
+
interface JobMetadata {
|
|
202
|
+
status: string;
|
|
203
|
+
total: number;
|
|
204
|
+
action: string;
|
|
205
|
+
}
|
|
206
|
+
interface ConsumeOptions {
|
|
207
|
+
enableRabbitTrace?: boolean;
|
|
208
|
+
[key: string]: any;
|
|
209
|
+
}
|
|
210
|
+
interface JobStatus {
|
|
211
|
+
jobId: string;
|
|
212
|
+
status: string;
|
|
213
|
+
action: string;
|
|
214
|
+
total: number;
|
|
215
|
+
queued: number;
|
|
216
|
+
processed: number;
|
|
217
|
+
succeeded: number;
|
|
218
|
+
failed: number;
|
|
219
|
+
errors: any[];
|
|
220
|
+
createdAt?: string;
|
|
221
|
+
updatedAt?: string;
|
|
222
|
+
duration: {
|
|
223
|
+
startTime: string | null;
|
|
224
|
+
endTime: string | null;
|
|
225
|
+
durationMs: number | null;
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
type MessageAck = () => Promise<void>;
|
|
229
|
+
type MessageNack = (err?: Error, requeue?: boolean) => Promise<void>;
|
|
230
|
+
type BulkPerIdHandler<T = any> = (args: {
|
|
231
|
+
jobId?: string;
|
|
232
|
+
id: Id;
|
|
233
|
+
payload: T;
|
|
234
|
+
}, ack: MessageAck, nack: MessageNack) => Promise<void>;
|
|
235
|
+
interface AdditionalIdsHookData<T = any> {
|
|
236
|
+
rawPayload: {
|
|
237
|
+
query: Record<string, any>;
|
|
238
|
+
include?: any[];
|
|
239
|
+
searchTerm: string;
|
|
240
|
+
};
|
|
241
|
+
payload: T;
|
|
242
|
+
}
|
|
243
|
+
interface BulkRouteOptions<T = any> {
|
|
244
|
+
/** e.g. "change-state" - the action identifier sent in request body */
|
|
245
|
+
action: string;
|
|
246
|
+
/** Sequelize model from which to scan IDs */
|
|
247
|
+
model: Model<any, any> | ModelStatic<any> | any;
|
|
248
|
+
modelScopes: string[];
|
|
249
|
+
/** Per-ID handler that performs the operation */
|
|
250
|
+
consumer: BulkPerIdHandler<T>;
|
|
251
|
+
consumerOptions?: ConsumeOptions;
|
|
252
|
+
rabbitQueueName?: string;
|
|
253
|
+
payloadSchema?: z.ZodType<T>;
|
|
254
|
+
/** Identity Scopes to be provided and are used and validated from the query.query object */
|
|
255
|
+
identityScopes?: string[];
|
|
256
|
+
queryFunction?: (reqBody: any) => Promise<Record<string, any>[]>;
|
|
257
|
+
idField?: string;
|
|
258
|
+
pageSize?: number;
|
|
259
|
+
workerConcurrency?: number;
|
|
260
|
+
jobAttempts?: number;
|
|
261
|
+
jobBackoffMs?: number;
|
|
262
|
+
removeOnComplete?: boolean | number;
|
|
263
|
+
removeOnFail?: boolean | number;
|
|
264
|
+
/** Inject tenant/RBAC filters (AND-ed with user query) */
|
|
265
|
+
contextWhere?: (payload: T) => WhereOptions;
|
|
266
|
+
/**
|
|
267
|
+
* Hook to provide additional IDs that will be OR-ed with the main query.
|
|
268
|
+
* This is useful when you need to manually fetch additional IDs based on the raw request body.
|
|
269
|
+
* The returned IDs will be included in the where clause using an OR statement.
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* additionalIdsHook: async (data) => {
|
|
273
|
+
* const { rawPayload, payload } = reqBody;
|
|
274
|
+
* if (!rawPayload.searchTerm) return [];
|
|
275
|
+
* const filters = rawPayload?.query?.fleetId ? { fleetId: query.query.fleetId } : {};
|
|
276
|
+
* const labelIds = await searchDriversByLabelsValue(rawPayload.searchTerm, filters);
|
|
277
|
+
* const vendorIds = await searchDriversByVendorName(rawPayload.searchTerm, filters);
|
|
278
|
+
* return [...labelIds, ...vendorIds];
|
|
279
|
+
* }
|
|
280
|
+
*/
|
|
281
|
+
additionalIdsHook?: (data: AdditionalIdsHookData) => Promise<Id[]>;
|
|
282
|
+
}
|
|
283
|
+
interface BulkRouterEvents$1 {
|
|
284
|
+
"job:created": (data: {
|
|
285
|
+
jobId: string;
|
|
286
|
+
action: string;
|
|
287
|
+
total: number;
|
|
288
|
+
}) => void;
|
|
289
|
+
"job:started": (data: {
|
|
290
|
+
jobId: string;
|
|
291
|
+
action: string;
|
|
292
|
+
}) => void;
|
|
293
|
+
"job:queued": (data: {
|
|
294
|
+
jobId: string;
|
|
295
|
+
action: string;
|
|
296
|
+
queued: number;
|
|
297
|
+
total: number;
|
|
298
|
+
}) => void;
|
|
299
|
+
"job:completed": (data: {
|
|
300
|
+
jobId: string;
|
|
301
|
+
action: string;
|
|
302
|
+
processed: number;
|
|
303
|
+
failed: number;
|
|
304
|
+
duration: number;
|
|
305
|
+
}) => void;
|
|
306
|
+
"job:canceled": (data: {
|
|
307
|
+
jobId: string;
|
|
308
|
+
action: string;
|
|
309
|
+
}) => void;
|
|
310
|
+
"job:failed": (data: {
|
|
311
|
+
jobId: string;
|
|
312
|
+
action: string;
|
|
313
|
+
error: string;
|
|
314
|
+
}) => void;
|
|
315
|
+
"item:processing": (data: {
|
|
316
|
+
jobId: string;
|
|
317
|
+
action: string;
|
|
318
|
+
id: string | number;
|
|
319
|
+
}) => void;
|
|
320
|
+
"item:processed": (data: {
|
|
321
|
+
jobId: string;
|
|
322
|
+
action: string;
|
|
323
|
+
id: string | number;
|
|
324
|
+
}) => void;
|
|
325
|
+
"item:failed": (data: {
|
|
326
|
+
jobId: string;
|
|
327
|
+
action: string;
|
|
328
|
+
id: string | number;
|
|
329
|
+
error: string;
|
|
330
|
+
}) => void;
|
|
331
|
+
"item:retrying": (data: {
|
|
332
|
+
jobId: string;
|
|
333
|
+
action: string;
|
|
334
|
+
id: string | number;
|
|
335
|
+
}) => void;
|
|
336
|
+
"worker:started": (data: {
|
|
337
|
+
action: string;
|
|
338
|
+
queueName: string;
|
|
339
|
+
}) => void;
|
|
340
|
+
"worker:error": (data: {
|
|
341
|
+
action: string;
|
|
342
|
+
error: string;
|
|
343
|
+
}) => void;
|
|
344
|
+
"scan:page": (data: {
|
|
345
|
+
jobId: string;
|
|
346
|
+
action: string;
|
|
347
|
+
pageNumber: number;
|
|
348
|
+
itemsInPage: number;
|
|
349
|
+
}) => void;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Interface for event emitters with the emitEvent helper method
|
|
353
|
+
*/
|
|
354
|
+
interface IBulkEventEmitter {
|
|
355
|
+
emitEvent<K extends keyof BulkRouterEvents$1>(event: K, ...args: Parameters<BulkRouterEvents$1[K]>): void;
|
|
356
|
+
}
|
|
357
|
+
//#endregion
|
|
358
|
+
//#region src/bulker/src/BulkRoute.d.ts
|
|
359
|
+
type Asyncify<T extends (...a: any[]) => any> = (...a: Parameters<T>) => Promise<Awaited<ReturnType<T>>>;
|
|
360
|
+
declare class BulkRoute<T = any> {
|
|
361
|
+
private readonly bulker;
|
|
362
|
+
private readonly opts;
|
|
363
|
+
readonly action: string;
|
|
364
|
+
private readonly model;
|
|
365
|
+
private readonly consumer;
|
|
366
|
+
private readonly idField;
|
|
367
|
+
private readonly pageSize;
|
|
368
|
+
private readonly rabbitQueueName;
|
|
369
|
+
private readonly rabbit;
|
|
370
|
+
private readonly consumerOptions;
|
|
371
|
+
private readonly queryFunction?;
|
|
372
|
+
private readonly payloadSchema?;
|
|
373
|
+
private readonly identityScopeSchema;
|
|
374
|
+
private readonly modelScopes;
|
|
375
|
+
constructor(bulker: Bulker, opts: BulkRouteOptions<T>);
|
|
376
|
+
bulkHandler: Asyncify<Handler>;
|
|
377
|
+
/** Get user jobs - public method for BulkRouter to use */
|
|
378
|
+
getUserJobs(userId: string, limit?: number): Promise<(JobStatus | null)[]>;
|
|
379
|
+
private rabbitScanAndEnqueue;
|
|
380
|
+
startRabbitWorker(): Promise<void>;
|
|
381
|
+
}
|
|
382
|
+
//#endregion
|
|
383
|
+
//#region src/bulker/src/BulkRouter.d.ts
|
|
384
|
+
declare class BulkRouter {
|
|
385
|
+
private readonly bulker;
|
|
386
|
+
private readonly router;
|
|
387
|
+
private routes;
|
|
388
|
+
private actionsToRouteMap;
|
|
389
|
+
private staticRoute;
|
|
390
|
+
constructor(bulker: Bulker, router?: Router, staticRoute?: string);
|
|
391
|
+
private registerStaticRoutes;
|
|
392
|
+
addAction<T = any>(actionName: string, opts: Omit<BulkRouteOptions<T>, "action">): BulkRoute<T>;
|
|
393
|
+
private bulkHandler;
|
|
394
|
+
private getJobHandler;
|
|
395
|
+
private cancelJobHandler;
|
|
396
|
+
private getMyJobsHandler;
|
|
397
|
+
/** Get the configured router instance */
|
|
398
|
+
getRouter(): Router;
|
|
399
|
+
}
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/bulker/src/events.d.ts
|
|
402
|
+
interface BulkRouterEvents {
|
|
403
|
+
"job:created": (data: {
|
|
404
|
+
jobId: string;
|
|
405
|
+
action: string;
|
|
406
|
+
total: number;
|
|
407
|
+
}) => void;
|
|
408
|
+
"job:started": (data: {
|
|
409
|
+
jobId: string;
|
|
410
|
+
action: string;
|
|
411
|
+
}) => void;
|
|
412
|
+
"job:queued": (data: {
|
|
413
|
+
jobId: string;
|
|
414
|
+
action: string;
|
|
415
|
+
queued: number;
|
|
416
|
+
total: number;
|
|
417
|
+
}) => void;
|
|
418
|
+
"job:completed": (data: {
|
|
419
|
+
jobId: string;
|
|
420
|
+
action: string;
|
|
421
|
+
processed: number;
|
|
422
|
+
failed: number;
|
|
423
|
+
duration: number;
|
|
424
|
+
}) => void;
|
|
425
|
+
"job:canceled": (data: {
|
|
426
|
+
jobId: string;
|
|
427
|
+
action: string;
|
|
428
|
+
}) => void;
|
|
429
|
+
"job:failed": (data: {
|
|
430
|
+
jobId: string;
|
|
431
|
+
action: string;
|
|
432
|
+
error: string;
|
|
433
|
+
}) => void;
|
|
434
|
+
"item:processing": (data: {
|
|
435
|
+
jobId: string;
|
|
436
|
+
action: string;
|
|
437
|
+
id: string | number;
|
|
438
|
+
}) => void;
|
|
439
|
+
"item:processed": (data: {
|
|
440
|
+
jobId: string;
|
|
441
|
+
action: string;
|
|
442
|
+
id: string | number;
|
|
443
|
+
}) => void;
|
|
444
|
+
"item:failed": (data: {
|
|
445
|
+
jobId: string;
|
|
446
|
+
action: string;
|
|
447
|
+
id: string | number;
|
|
448
|
+
error: string;
|
|
449
|
+
}) => void;
|
|
450
|
+
"item:retrying": (data: {
|
|
451
|
+
jobId: string;
|
|
452
|
+
action: string;
|
|
453
|
+
id: string | number;
|
|
454
|
+
}) => void;
|
|
455
|
+
"worker:started": (data: {
|
|
456
|
+
action: string;
|
|
457
|
+
queueName: string;
|
|
458
|
+
}) => void;
|
|
459
|
+
"worker:error": (data: {
|
|
460
|
+
action: string;
|
|
461
|
+
error: string;
|
|
462
|
+
}) => void;
|
|
463
|
+
"scan:page": (data: {
|
|
464
|
+
jobId: string;
|
|
465
|
+
action: string;
|
|
466
|
+
pageNumber: number;
|
|
467
|
+
itemsInPage: number;
|
|
468
|
+
}) => void;
|
|
469
|
+
}
|
|
470
|
+
interface BulkEventEmitter extends EventEmitter {
|
|
471
|
+
on<K extends keyof BulkRouterEvents>(event: K, listener: BulkRouterEvents[K]): this;
|
|
472
|
+
once<K extends keyof BulkRouterEvents>(event: K, listener: BulkRouterEvents[K]): this;
|
|
473
|
+
off<K extends keyof BulkRouterEvents>(event: K, listener: BulkRouterEvents[K]): this;
|
|
474
|
+
removeListener<K extends keyof BulkRouterEvents>(event: K, listener: BulkRouterEvents[K]): this;
|
|
475
|
+
emit<K extends keyof BulkRouterEvents>(event: K, ...args: Parameters<BulkRouterEvents[K]>): boolean;
|
|
476
|
+
}
|
|
477
|
+
interface EmitsBulkEvents {
|
|
478
|
+
emitEvent<K extends keyof BulkRouterEvents>(event: K, ...args: Parameters<BulkRouterEvents[K]>): void;
|
|
479
|
+
}
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/bulker/src/JobManager.d.ts
|
|
482
|
+
/**
|
|
483
|
+
* Manages all job-related operations in Redis
|
|
484
|
+
* Centralizes job creation, status updates, cancellation, and user job tracking
|
|
485
|
+
*/
|
|
486
|
+
declare class JobManager {
|
|
487
|
+
private eventEmitter?;
|
|
488
|
+
private readonly redis;
|
|
489
|
+
private readonly defaults;
|
|
490
|
+
constructor(redis: RedisClientType, defaults?: {
|
|
491
|
+
maxJobsPerUser?: number;
|
|
492
|
+
jobTtlSeconds?: number;
|
|
493
|
+
errorLogLimit?: number;
|
|
494
|
+
});
|
|
495
|
+
/** Set event emitter for emitting job events */
|
|
496
|
+
setEventEmitter(emitter: EmitsBulkEvents): void;
|
|
497
|
+
/** Generate Redis key for job */
|
|
498
|
+
static jobKey(jobId: string): string;
|
|
499
|
+
/** Generate Redis key for user jobs list */
|
|
500
|
+
static userJobsKey(userId: string): string;
|
|
501
|
+
/**
|
|
502
|
+
* Initialize a new job in Redis
|
|
503
|
+
*/
|
|
504
|
+
initJob(jobId: string, meta: JobMetadata): Promise<void>;
|
|
505
|
+
/**
|
|
506
|
+
* Get job status from Redis
|
|
507
|
+
*/
|
|
508
|
+
getJob(jobId: string): Promise<JobStatus | null>;
|
|
509
|
+
/**
|
|
510
|
+
* Set a single field on a job
|
|
511
|
+
*/
|
|
512
|
+
setJobField(jobId: string, field: string, value: string): Promise<boolean>;
|
|
513
|
+
/**
|
|
514
|
+
* Set multiple fields on a job
|
|
515
|
+
*/
|
|
516
|
+
setJobFields(jobId: string, fields: Record<string, string>): Promise<boolean>;
|
|
517
|
+
/**
|
|
518
|
+
* Increment a counter field on a job
|
|
519
|
+
*/
|
|
520
|
+
incrJobField(jobId: string, field: "queued" | "processed" | "succeeded" | "failed", by?: number): Promise<number>;
|
|
521
|
+
/**
|
|
522
|
+
* Get a single field from a job
|
|
523
|
+
*/
|
|
524
|
+
getJobField(jobId: string, field: string): Promise<string | undefined>;
|
|
525
|
+
/**
|
|
526
|
+
* Cancel a job
|
|
527
|
+
*/
|
|
528
|
+
cancelJob(jobId: string): Promise<boolean>;
|
|
529
|
+
/**
|
|
530
|
+
* Mark job as completed (for jobs with no items to process)
|
|
531
|
+
*/
|
|
532
|
+
completeEmptyJob(jobId: string): Promise<void>;
|
|
533
|
+
/**
|
|
534
|
+
* Handle successful message processing (ack)
|
|
535
|
+
* Uses Redis Lua script for atomic operation
|
|
536
|
+
*/
|
|
537
|
+
ack(jobId: string): Promise<void>;
|
|
538
|
+
/**
|
|
539
|
+
* Handle failed message processing (nack)
|
|
540
|
+
* Uses Redis Lua script for atomic operation
|
|
541
|
+
*/
|
|
542
|
+
nack(jobId: string, errorMsg: string, data: any): Promise<void>;
|
|
543
|
+
/**
|
|
544
|
+
* Add a job to user's job list
|
|
545
|
+
*/
|
|
546
|
+
addUserJob(userId: string, jobId: string): Promise<void>;
|
|
547
|
+
/**
|
|
548
|
+
* Remove a job from user's job list
|
|
549
|
+
*/
|
|
550
|
+
removeUserJob(userId: string, jobId: string): Promise<void>;
|
|
551
|
+
/**
|
|
552
|
+
* Get all jobs for a user
|
|
553
|
+
*/
|
|
554
|
+
getUserJobs(userId: string, limit?: number): Promise<(JobStatus | null)[]>;
|
|
555
|
+
}
|
|
556
|
+
//#endregion
|
|
557
|
+
//#region src/bulker/src/Errors.d.ts
|
|
558
|
+
declare class BulkerError extends Error {
|
|
559
|
+
readonly retryable: boolean;
|
|
560
|
+
constructor(message: string, retryable?: boolean);
|
|
561
|
+
static isBulkerError(err: unknown): err is BulkerError;
|
|
562
|
+
static wrap(err: unknown, message?: string, retryable?: boolean): BulkerError;
|
|
563
|
+
static retryable(message: string): BulkerError;
|
|
564
|
+
static nonRetryable(message: string): BulkerError;
|
|
565
|
+
}
|
|
566
|
+
declare namespace index_d_exports {
|
|
567
|
+
export { BulkEventEmitter, BulkRoute, BulkRouteOptions, BulkRouterEvents$1 as BulkRouterEvents, Bulker, BulkerError, BulkerInit, IBulkEventEmitter, JobManager, JobMetadata, JobStatus, z$1 as z };
|
|
568
|
+
}
|
|
569
|
+
interface BulkerInit {
|
|
570
|
+
sequelize: Sequelize;
|
|
571
|
+
logger: LoggerInstanceManager;
|
|
572
|
+
rabbit: rabbit;
|
|
573
|
+
redis: RedisClientType | ReturnType<typeof createClient> | {
|
|
574
|
+
host: string;
|
|
575
|
+
port?: number;
|
|
576
|
+
password?: string;
|
|
577
|
+
db?: number;
|
|
578
|
+
};
|
|
579
|
+
getUserId?: () => string | null;
|
|
580
|
+
emitEvents?: boolean;
|
|
581
|
+
defaults?: {
|
|
582
|
+
pageSize?: number;
|
|
583
|
+
maxJobsPerUser?: number;
|
|
584
|
+
idField?: string;
|
|
585
|
+
workerConcurrency?: number;
|
|
586
|
+
jobTtlSeconds?: number;
|
|
587
|
+
errorLogLimit?: number;
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
declare class Bulker extends EventEmitter implements BulkEventEmitter {
|
|
591
|
+
readonly sequelize: Sequelize;
|
|
592
|
+
readonly logger: BulkerInit["logger"];
|
|
593
|
+
readonly redis: RedisClientType;
|
|
594
|
+
readonly rabbit: rabbit;
|
|
595
|
+
readonly defaults: Required<NonNullable<BulkerInit["defaults"]>>;
|
|
596
|
+
readonly jobManager: JobManager;
|
|
597
|
+
private readonly bulkRouters;
|
|
598
|
+
getUserId: BulkerInit["getUserId"];
|
|
599
|
+
readonly eventsEnabled: boolean;
|
|
600
|
+
constructor(init: BulkerInit);
|
|
601
|
+
createBulkRouter(router?: Router, staticRoute?: string): BulkRouter;
|
|
602
|
+
/**
|
|
603
|
+
* Emit event only if events are enabled.
|
|
604
|
+
* Centralizes the eventsEnabled check to avoid repetition.
|
|
605
|
+
*/
|
|
606
|
+
emitEvent<K extends keyof BulkRouterEvents>(event: K, ...args: Parameters<BulkRouterEvents[K]>): void;
|
|
607
|
+
}
|
|
194
608
|
//#endregion
|
|
195
|
-
export { type LiteralAttribute, type MiddlewareValidationOption, formatOperators, generateFilterReplacements, queryFormatMiddleware, queryHandler, queryValidationMiddleware, validatePayload };
|
|
609
|
+
export { index_d_exports as Bulker, type LiteralAttribute, type MiddlewareValidationOption, formatOperators, generateFilterReplacements, queryFormatMiddleware, queryHandler, queryValidationMiddleware, validatePayload };
|
|
196
610
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
CHANGED
|
@@ -1,2 +1,59 @@
|
|
|
1
|
-
import{Op as e,literal as t}from"sequelize";import n from"@autofleet/logger";import{BadRequest as r,UnexpectedError as i,handleError as a}from"@autofleet/errors";import o from"joi";import{customFields as s}from"@autofleet/common-types";import{randomInt as c}from"node:crypto";const l=[`eq`,`ne`,`gte`,`gt`,`lte`,`lt`,`not`,`in`,`notIn`,`is`,`like`,`iLike`,`notLike`,`between`,`and`,`or`,`overlap`,`contains`],u={$eq:`=`,$ne:`!=`,$gte:`>=`,$gt:`>`,$lte:`<=`,$lt:`<`,$not:`NOT`,$in:`IN`,$notIn:`NOT IN`,$is:`IS`,$like:`LIKE`,$iLike:`ILIKE`,$notLike:`NOT LIKE`,$and:`AND`,$or:`OR`},d=(t={Op:e})=>{let{Op:n}=t;return Object.fromEntries(l.map(e=>[`${`$`+e}`,n[e]]))},f=e=>`\$${e}\$`,p=(e,t)=>e.includes(`.`)&&t.includes(e.split(`.`,1)[0]),m=(e,t)=>{let n=e;return e.includes(`-`)&&([,n]=n.split(`-`,2)),p(n,t)&&([n]=n.split(`.`,1)),n},h=e=>e.includes(`-`),g=e=>{throw new r([Error(e)])},_=e=>e.split(`.`,2)[1],v=(e=5)=>Array.from({length:e},()=>`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`.charAt(c(52))).join(``),y=(e,t)=>Object.fromEntries(t.map(t=>[t,e[t]])),b={length:`length`};function x(e){return e.replace(/(?!^)[A-Z]/g,e=>`_${e.toLowerCase()}`)}function S(e){switch(e.action){case b.length:return[t(`jsonb_array_length(${x(e.columnName)})`),e.alias];default:return e.action,[]}}function C(e){return e.map(e=>S(e))}function w(e){return e.map(e=>{let n=x(e.columnName),r=`json_build_object(${e.keys.map(e=>`'${e}', ${n} -> '${e}'`).join(`, `)})`,i=e.alias||e.columnName;return[t(r),i]})}function T({select:e=[],computed:t=[]}={}){let n=w(e),r=C(t);return[...n,...r]}const E=`DESC`,D=`customFields.`,{CUSTOM_FIELDS_FILTER_SCOPE:O,CUSTOM_FIELDS_SORT_SCOPE:k}=s,ee=e=>[`string`,`number`].includes(typeof e)||Array.isArray(e)?e:Object.entries(e).map(([e,t])=>({operator:u[e],value:t})),A=(e,t={})=>{let{literalAttributes:n=[],DBFormatter:r=void 0}=t,[i,a]=e.reduce((e,t)=>{let[i,a=`ASC`]=Array.isArray(t)?t:[t],o=n?.find(e=>e.attribute===i);if(o){let t=r?r(`"${o.attribute}" ${a}`):`${o.attribute} ${a}`;e[1].push(o.literal),e[0].push([t])}else e[0].push(t);return e},[[],[]]);return[i,a]},j=e=>{let t={};return Object.entries(e).forEach(([e,n])=>{let r=v();if(t[r]=e.split(D,2)[1],Array.isArray(n))n.forEach(e=>{let n=v();t[n]=typeof e==`string`?e:e.value});else if(typeof n==`string`||typeof n==`number`){let e=v();t[e]=n}else if(n?.operator){let e=v();t[e]=n.value}}),t},M=e=>{let t={};return e.forEach(e=>{if(e.startsWith(D)){let n=v();t[n]=e.split(D,2)[1]}else if(e.substring(1).startsWith(D)){let n=v();t[n]=e.substring(1).split(D,2)[1]}}),t},N=(e,t)=>({...M(e),...j(t)}),P=({order:e,associationModels:t=[],replacementsMap:n={}})=>{let r=[],i=new Map;return e.forEach(e=>{if([e,e.substring(1)].some(e=>e.startsWith(D))){i.has(k)||i.set(k,{});let t=e.split(D,2)[1];i.get(k)[t]=h(e)?E:`ASC`;return}let n=[m(e,t)],a=h(e);p(a?e.split(`-`,2)[1]:e,t)&&n.push(_(e)),a&&n.push(E),r.push(n)}),{formattedOrders:r,replacementsMap:n,orderScopes:Array.from(i.entries()).map(([e,t])=>t?{method:[e,{replacementsMap:n,scopeValue:t}]}:e)}},te=e=>e||1,ne=e=>e||20,F=(e,t={})=>{let n=e.map(e=>{let n=t[typeof e==`string`?e:e.association||e.model];return{...typeof e!=`string`&&e,association:n,required:typeof e==`string`||e.required!==!1,...typeof e!=`string`&&e.include&&{include:F(e.include,n?.target?.associations)}}});return n=n.map(({model:e,...t})=>t),n},re=(e,t,n,r=[])=>{let i={},a={},o=new Map;return Object.entries(e).forEach(([e,n])=>{if(e.startsWith(D)){o.has(O)||o.set(O,{});let t=e.split(D,2)[1];o.get(O)[t]=ee(n);return}if(r.includes(e)){a[e]=n;return}let s=p(e,t)?f(e):e;i[s]=n}),{formattedQuery:i,externalQueryValues:a,formattedScopes:Array.from(o.entries()).map(([e,t])=>t?{method:[e,{replacementsMap:n,scopeValue:t}]}:e)}},ie=(e,t,n)=>({$and:e.split(` `).map(e=>({$or:t.filter(e=>n[e].type.key===`STRING`).map(t=>({[t]:{$iLike:`%${e}%`}}))}))});var ae=({order:e=[],page:t=1,perPage:n=20,include:r=[],query:i={},attributes:a=null,searchTerm:o=null,jsonAttributes:s={}},c,l)=>{let u=N(e,i),d=Object.keys(c?.associations||{}),{formattedOrders:f,orderScopes:p}=P({order:[...e,`id`],associationModels:d,replacementsMap:u}),[m,h]=A(f,l),g=T(s),_=[...h,...a??[],...g],v=a?.length?_:{include:_},y=F(r,c?.associations),b=te(t),x=ne(n),S=re(i,d,u,l?.additionalAllowedAttributes),{formattedScopes:C,externalQueryValues:w}=S,{formattedQuery:E}=S;if(o&&!l?.skipSearchTermFormat){let e=ie(o,a?.length?a:Object.keys(c.rawAttributes||{}),c.rawAttributes);E=!E||Object.keys(E).length===0?e:{$and:[E,e]}}return{query:E,order:m,page:b,perPage:x,include:y,scopes:[...C,...p],...v&&{attributes:v},...Object.keys(w).length>0&&{externalQueryValues:w}}};const oe=e=>l.includes(e.split(`$`,2)[1]),I=(e,t=[],n=[],r=[])=>{let i=e.startsWith(`$`)&&e.endsWith(`$`)?e.slice(1,-1):e;return[...t,...n].includes(i.includes(`.`)?i.split(`.`,1)[0]:i)||r.includes(i)},L=(e,t,n,r={})=>{let i=h(e);i&&!e.startsWith(`-`)&&g(`- must be only at the beginning of the word`);let a=i?e.split(`-`,2)[1]:e,o=p(a,n),s=m(e,n),c=r?.literalAttributes?.map(e=>e.attribute)?.includes(a);!o&&s.includes(`.`)&&([s]=s.split(`.`,1)),t.includes(s)||o||c||g(`${e} is invalid. isLiteralAttribute: ${c}`)},R=(e,t)=>{t.includes(e)||g(`${e} is invalid`)},z=(e,t,n=[],r={})=>{e.forEach(e=>L(e,t,n,r))},B=(e,t)=>{e.forEach(e=>R(e,t))},V=(e,t)=>{B([...e.select?.map(e=>e.columnName)??[],...e.computed?.map(e=>e.columnName)??[]],t)},H=(e,t)=>{let n=Array.isArray(e)?e:Object.keys(e);if(!n?.length)return;let r=n.find(e=>!t?.enrichmentAttributes?.includes(e));r&&g(`enrichment attribute ${r} is invalid`)},U=(e,t,n=[],r=[])=>{Object.entries(e).forEach(([e,i])=>{Array.isArray(i)?i[0]&&typeof i[0]==`object`&&i.map(e=>U(e,t,n,r)):oe(e)||I(e,t,n,r)?i&&typeof i==`object`&&U(i,t,[],r):g(`invalid key: ${e}`)})},W=({page:e,perPage:t})=>{e<1&&g(`Page must be greater than 0`),(t>100||t<1)&&g(`PerPage must be between 1 to 100`)},G=(e,t)=>{let n=Object.keys(t);e.forEach(e=>{I(e.model,n);let r=t[e.model]?.target;r||g(`model not found in associations`);let{rawAttributes:i}=r,a=Object.keys(i);e.where&&U(e.where,a),e.order&&z(e.order,a),e.attributes&&B(e.attributes,a),[null,void 0,!0,!1].includes(e.required)||g(`include.required must be a boolean`)})},K=({query:e={},order:t=[],attributes:n=[],include:r=[],page:i=1,perPage:a=20,enrichments:o=[],group:s=[],jsonAttributes:c={}},l,u={})=>{let d=Object.keys(l.rawAttributes),f=Object.keys(l?.associations||{});return!n||n.length===0?n=d:B(n,d),z(t,d,f,u),U(e,d,f,u.additionalAllowedAttributes),H(o,u),V(c,d),Array.isArray(s)||g(`group must be an array`),r.length&&typeof r==`object`?G(r,l?.associations):r&&typeof r!=`object`&&g(`include must be an array`),W({page:i,perPage:a}),!0},{object:q,string:J,number:Y,any:se,array:X,alternatives:ce}=o.types(),le=n(),ue=q.keys({query:q,attributes:X.items(J),order:X.items(J),page:Y,perPage:Y,include:X.items(se),searchTerm:J,group:X.items(J),enrichments:ce.try(X.items(J),q.pattern(J,{exclude:X.items(J)})),jsonAttributes:o.object({select:o.array().items(o.object({columnName:o.string().required(),keys:o.array().items(o.string().required()).required(),alias:o.string().optional()})).default([]),computed:o.array().items(o.object({columnName:o.string().required(),action:o.string().valid(...Object.values(b)).required(),alias:o.string().required()})).default([])}).default({})}),Z=(e,t,n={})=>{let{query:i,attributes:a,order:o,page:s,perPage:c,include:l,group:u,enrichments:d,jsonAttributes:f}=t,p=ue.validate(t);if(p.error)throw new r([p.error]);K({query:i,attributes:a,order:o,page:s,perPage:c,include:l,enrichments:d,group:u,jsonAttributes:f},e,n)},Q=(e,t={},n=`body`)=>(r,i,o)=>{try{Z(e,r[n],t),o()}catch(e){let{query:o,attributes:s,order:c}=r[n];a(e,i,{logger:t.logger??le,message:`error in query middleware`,payload:{error:e,query:o,attributes:s,order:c}})}},$=(e,t,n={})=>{let{order:r,page:i,perPage:a,include:o,query:s,attributes:c,searchTerm:l,jsonAttributes:u}=t,{query:d,externalQueryValues:f,order:p,page:m,perPage:h,include:g,scopes:_,attributes:v}=ae({query:s,order:r,page:i,perPage:a,include:o,attributes:c,searchTerm:l,jsonAttributes:u},e,n);t.query=d,t.externalQueryValues=f,t.order=p,t.attributes=v,t.page=m,t.perPage=h,t.include=g,t.scopes=_,n.includeRawPayload&&(t.rawPayload={order:r,page:i,perPage:a,include:o,query:s,attributes:c,searchTerm:l})},de=(e,t={},n=`body`)=>(r,i,a)=>{$(e,r[n],t),a()},fe=({model:e,logger:t,validationOptions:n,formatOptions:r,modelName:o=e.constructor?.name,additionalScopes:s=[],modifyQueryValues:c,onRowsRetrieved:l})=>async(u,d)=>{try{Z(e,u.body,{...n,logger:t})}catch(e){a(e,d,{logger:t,message:`error in query endpoint`,payload:y(u.body,[`query`,`order`,`attributes`])});return}try{$(e,u.body,r);let n=Object.assign(y(u.body,[`query`,`externalQueryValues`,`order`,`attributes`,`page`,`perPage`,`include`,`scopes`,`enrichments`]),{distinct:!0});t.info(`querying ${o}`,{queryValues:n});let i=c?.(n)??n,{scopes:a=[],query:f,perPage:p,page:m,enrichments:h,externalQueryValues:g,..._}=i,v=await e.scope([...s,...a]).findAndCountAll({where:f,limit:p,offset:(m-1)*p,..._});if(!v.rows.length||!l){d.json(v);return}let b=await l(v,i);d.json(b)}catch(e){a(new i(e),d,{logger:t,message:`Error while querying ${o}`,payload:{query:u.body}})}};export{d as formatOperators,j as generateFilterReplacements,de as queryFormatMiddleware,fe as queryHandler,Q as queryValidationMiddleware,K as validatePayload};
|
|
1
|
+
import{__export as e}from"./chunk-CTAAG5j7.js";import{Op as t,literal as n}from"sequelize";import r from"@autofleet/logger";import{BadRequest as i,UnexpectedError as a,handleError as o}from"@autofleet/errors";import s from"joi";import{customFields as c}from"@autofleet/common-types";import{randomInt as l,randomUUID as u}from"node:crypto";import{createClient as d}from"redis";import{Router as f}from"express";import{z as p,z as m}from"zod/v3";import{EventEmitter as h}from"node:events";const g=[`eq`,`ne`,`gte`,`gt`,`lte`,`lt`,`not`,`in`,`notIn`,`is`,`like`,`iLike`,`notLike`,`between`,`and`,`or`,`overlap`,`contains`],_={$eq:`=`,$ne:`!=`,$gte:`>=`,$gt:`>`,$lte:`<=`,$lt:`<`,$not:`NOT`,$in:`IN`,$notIn:`NOT IN`,$is:`IS`,$like:`LIKE`,$iLike:`ILIKE`,$notLike:`NOT LIKE`,$and:`AND`,$or:`OR`},v=(e={Op:t})=>{let{Op:n}=e;return Object.fromEntries(g.map(e=>[`${`$`+e}`,n[e]]))},y=e=>`\$${e}\$`,b=(e,t)=>e.includes(`.`)&&t.includes(e.split(`.`,1)[0]),x=(e,t)=>{let n=e;return e.includes(`-`)&&([,n]=n.split(`-`,2)),b(n,t)&&([n]=n.split(`.`,1)),n},S=e=>e.includes(`-`),C=e=>{throw new i([Error(e)])},w=e=>e.split(`.`,2)[1],T=(e=5)=>Array.from({length:e},()=>`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`.charAt(l(52))).join(``),E=(e,t)=>Object.fromEntries(t.map(t=>[t,e[t]])),D={length:`length`};function O(e){return e.replace(/(?!^)[A-Z]/g,e=>`_${e.toLowerCase()}`)}function ee(e){switch(e.action){case D.length:return[n(`jsonb_array_length(${O(e.columnName)})`),e.alias];default:return e.action,[]}}function k(e){return e.map(e=>ee(e))}function te(e){return e.map(e=>{let t=O(e.columnName),r=`json_build_object(${e.keys.map(e=>`'${e}', ${t} -> '${e}'`).join(`, `)})`,i=e.alias||e.columnName;return[n(r),i]})}function ne({select:e=[],computed:t=[]}={}){let n=te(e),r=k(t);return[...n,...r]}const A=`DESC`,j=`customFields.`,{CUSTOM_FIELDS_FILTER_SCOPE:M,CUSTOM_FIELDS_SORT_SCOPE:N}=c,re=e=>[`string`,`number`].includes(typeof e)||Array.isArray(e)?e:Object.entries(e).map(([e,t])=>({operator:_[e],value:t})),ie=(e,t={})=>{let{literalAttributes:n=[],DBFormatter:r=void 0}=t,[i,a]=e.reduce((e,t)=>{let[i,a=`ASC`]=Array.isArray(t)?t:[t],o=n?.find(e=>e.attribute===i);if(o){let t=r?r(`"${o.attribute}" ${a}`):`${o.attribute} ${a}`;e[1].push(o.literal),e[0].push([t])}else e[0].push(t);return e},[[],[]]);return[i,a]},P=e=>{let t={};return Object.entries(e).forEach(([e,n])=>{let r=T();if(t[r]=e.split(j,2)[1],Array.isArray(n))n.forEach(e=>{let n=T();t[n]=typeof e==`string`?e:e.value});else if(typeof n==`string`||typeof n==`number`){let e=T();t[e]=n}else if(n?.operator){let e=T();t[e]=n.value}}),t},ae=e=>{let t={};return e.forEach(e=>{if(e.startsWith(j)){let n=T();t[n]=e.split(j,2)[1]}else if(e.substring(1).startsWith(j)){let n=T();t[n]=e.substring(1).split(j,2)[1]}}),t},oe=(e,t)=>({...ae(e),...P(t)}),F=({order:e,associationModels:t=[],replacementsMap:n={}})=>{let r=[],i=new Map;return e.forEach(e=>{if([e,e.substring(1)].some(e=>e.startsWith(j))){i.has(N)||i.set(N,{});let t=e.split(j,2)[1];i.get(N)[t]=S(e)?A:`ASC`;return}let n=[x(e,t)],a=S(e);b(a?e.split(`-`,2)[1]:e,t)&&n.push(w(e)),a&&n.push(A),r.push(n)}),{formattedOrders:r,replacementsMap:n,orderScopes:Array.from(i.entries()).map(([e,t])=>t?{method:[e,{replacementsMap:n,scopeValue:t}]}:e)}},se=e=>e||1,ce=e=>e||20,I=(e,t={})=>{let n=e.map(e=>{let n=t[typeof e==`string`?e:e.association||e.model];return{...typeof e!=`string`&&e,association:n,required:typeof e==`string`||e.required!==!1,...typeof e!=`string`&&e.include&&{include:I(e.include,n?.target?.associations)}}});return n=n.map(({model:e,...t})=>t),n},le=(e,t,n,r=[])=>{let i={},a={},o=new Map;return Object.entries(e).forEach(([e,n])=>{if(e.startsWith(j)){o.has(M)||o.set(M,{});let t=e.split(j,2)[1];o.get(M)[t]=re(n);return}if(r.includes(e)){a[e]=n;return}let s=b(e,t)?y(e):e;i[s]=n}),{formattedQuery:i,externalQueryValues:a,formattedScopes:Array.from(o.entries()).map(([e,t])=>t?{method:[e,{replacementsMap:n,scopeValue:t}]}:e)}},ue=(e,t,n)=>({$and:e.split(` `).map(e=>({$or:t.filter(e=>n[e].type.key===`STRING`).map(t=>({[t]:{$iLike:`%${e}%`}}))}))});var de=({order:e=[],page:t=1,perPage:n=20,include:r=[],query:i={},attributes:a=null,searchTerm:o=null,jsonAttributes:s={}},c,l)=>{let u=oe(e,i),d=Object.keys(c?.associations||{}),{formattedOrders:f,orderScopes:p}=F({order:[...e,`id`],associationModels:d,replacementsMap:u}),[m,h]=ie(f,l),g=ne(s),_=[...h,...a??[],...g],v=a?.length?_:{include:_},y=I(r,c?.associations),b=se(t),x=ce(n),S=le(i,d,u,l?.additionalAllowedAttributes),{formattedScopes:C,externalQueryValues:w}=S,{formattedQuery:T}=S;if(o&&!l?.skipSearchTermFormat){let e=ue(o,a?.length?a:Object.keys(c.rawAttributes||{}),c.rawAttributes);T=!T||Object.keys(T).length===0?e:{$and:[T,e]}}return{query:T,order:m,page:b,perPage:x,include:y,scopes:[...C,...p],...v&&{attributes:v},...Object.keys(w).length>0&&{externalQueryValues:w}}};const fe=e=>g.includes(e.split(`$`,2)[1]),L=(e,t=[],n=[],r=[])=>{let i=e.startsWith(`$`)&&e.endsWith(`$`)?e.slice(1,-1):e;return[...t,...n].includes(i.includes(`.`)?i.split(`.`,1)[0]:i)||r.includes(i)},pe=(e,t,n,r={})=>{let i=S(e);i&&!e.startsWith(`-`)&&C(`- must be only at the beginning of the word`);let a=i?e.split(`-`,2)[1]:e,o=b(a,n),s=x(e,n),c=r?.literalAttributes?.map(e=>e.attribute)?.includes(a);!o&&s.includes(`.`)&&([s]=s.split(`.`,1)),t.includes(s)||o||c||C(`${e} is invalid. isLiteralAttribute: ${c}`)},R=(e,t)=>{t.includes(e)||C(`${e} is invalid`)},z=(e,t,n=[],r={})=>{e.forEach(e=>pe(e,t,n,r))},B=(e,t)=>{e.forEach(e=>R(e,t))},me=(e,t)=>{B([...e.select?.map(e=>e.columnName)??[],...e.computed?.map(e=>e.columnName)??[]],t)},he=(e,t)=>{let n=Array.isArray(e)?e:Object.keys(e);if(!n?.length)return;let r=n.find(e=>!t?.enrichmentAttributes?.includes(e));r&&C(`enrichment attribute ${r} is invalid`)},V=(e,t,n=[],r=[])=>{Object.entries(e).forEach(([e,i])=>{Array.isArray(i)?i[0]&&typeof i[0]==`object`&&i.map(e=>V(e,t,n,r)):fe(e)||L(e,t,n,r)?i&&typeof i==`object`&&V(i,t,[],r):C(`invalid key: ${e}`)})},ge=({page:e,perPage:t})=>{e<1&&C(`Page must be greater than 0`),(t>100||t<1)&&C(`PerPage must be between 1 to 100`)},_e=(e,t)=>{let n=Object.keys(t);e.forEach(e=>{L(e.model,n);let r=t[e.model]?.target;r||C(`model not found in associations`);let{rawAttributes:i}=r,a=Object.keys(i);e.where&&V(e.where,a),e.order&&z(e.order,a),e.attributes&&B(e.attributes,a),[null,void 0,!0,!1].includes(e.required)||C(`include.required must be a boolean`)})},H=({query:e={},order:t=[],attributes:n=[],include:r=[],page:i=1,perPage:a=20,enrichments:o=[],group:s=[],jsonAttributes:c={}},l,u={})=>{let d=Object.keys(l.rawAttributes),f=Object.keys(l?.associations||{});return!n||n.length===0?n=d:B(n,d),z(t,d,f,u),V(e,d,f,u.additionalAllowedAttributes),he(o,u),me(c,d),Array.isArray(s)||C(`group must be an array`),r.length&&typeof r==`object`?_e(r,l?.associations):r&&typeof r!=`object`&&C(`include must be an array`),ge({page:i,perPage:a}),!0},{object:U,string:W,number:G,any:ve,array:K,alternatives:q}=s.types(),ye=r(),be=U.keys({query:U,attributes:K.items(W),order:K.items(W),page:G,perPage:G,include:K.items(ve),searchTerm:W,group:K.items(W),enrichments:q.try(K.items(W),U.pattern(W,{exclude:K.items(W)})),jsonAttributes:s.object({select:s.array().items(s.object({columnName:s.string().required(),keys:s.array().items(s.string().required()).required(),alias:s.string().optional()})).default([]),computed:s.array().items(s.object({columnName:s.string().required(),action:s.string().valid(...Object.values(D)).required(),alias:s.string().required()})).default([])}).default({})}),J=(e,t,n={})=>{let{query:r,attributes:a,order:o,page:s,perPage:c,include:l,group:u,enrichments:d,jsonAttributes:f}=t,p=be.validate(t);if(p.error)throw new i([p.error]);H({query:r,attributes:a,order:o,page:s,perPage:c,include:l,enrichments:d,group:u,jsonAttributes:f},e,n)},xe=(e,t={},n=`body`)=>(r,i,a)=>{try{J(e,r[n],t),a()}catch(e){let{query:a,attributes:s,order:c}=r[n];o(e,i,{logger:t.logger??ye,message:`error in query middleware`,payload:{error:e,query:a,attributes:s,order:c}})}},Y=(e,t,n={})=>{let{order:r,page:i,perPage:a,include:o,query:s,attributes:c,searchTerm:l,jsonAttributes:u}=t,{query:d,externalQueryValues:f,order:p,page:m,perPage:h,include:g,scopes:_,attributes:v}=de({query:s,order:r,page:i,perPage:a,include:o,attributes:c,searchTerm:l,jsonAttributes:u},e,n);t.query=d,t.externalQueryValues=f,t.order=p,t.attributes=v,t.page=m,t.perPage=h,t.include=g,t.scopes=_,n.includeRawPayload&&(t.rawPayload={order:r,page:i,perPage:a,include:o,query:s,attributes:c,searchTerm:l})},Se=(e,t={},n=`body`)=>(r,i,a)=>{Y(e,r[n],t),a()},Ce=({model:e,logger:t,validationOptions:n,formatOptions:r,modelName:i=e.constructor?.name,additionalScopes:s=[],modifyQueryValues:c,onRowsRetrieved:l})=>async(u,d)=>{try{J(e,u.body,{...n,logger:t})}catch(e){o(e,d,{logger:t,message:`error in query endpoint`,payload:E(u.body,[`query`,`order`,`attributes`])});return}try{Y(e,u.body,r);let n=Object.assign(E(u.body,[`query`,`externalQueryValues`,`order`,`attributes`,`page`,`perPage`,`include`,`scopes`,`enrichments`]),{distinct:!0});t.info(`querying ${i}`,{queryValues:n});let a=c?.(n)??n,{scopes:o=[],query:f,perPage:p,page:m,enrichments:h,externalQueryValues:g,..._}=a,v=await e.scope([...s,...o]).findAndCountAll({where:f,limit:p,offset:(m-1)*p,..._});if(!v.rows.length||!l){d.json(v);return}let y=await l(v,a);d.json(y)}catch(e){o(new a(e),d,{logger:t,message:`Error while querying ${i}`,payload:{query:u.body}})}};var X=class e extends Error{constructor(e,t=!1){super(e),this.name=`BulkerError`,this.retryable=t,Object.setPrototypeOf(this,new.target.prototype)}static isBulkerError(t){return t instanceof e}static wrap(t,n,r=!1){return t instanceof e?new e(n??t.message,r||t.retryable):new e(n?`${n}: ${t instanceof Error?t.message:String(t)}`:String(t),r)}static retryable(t){return new e(t,!0)}static nonRetryable(t){return new e(t,!1)}};const we=e=>e.length===0?`No keys provided`:e.length===1?`Key "${e[0]}" is required`:`Exactly one of [${e.join(`, `)}] must be provided`;function Te(e,t){return p.object(e).refine(e=>t.filter(t=>e[t]!==void 0&&e[t]!==null).length===1,{message:we(t)})}const Z=p.string().uuid();function Ee(e){return Te(Object.fromEntries(e.map(e=>[e,p.union([Z,p.array(Z)]).optional()])),e)}const De=[`businessModelId`,`fleetId`,`demandSourceId`,`contextId`,`userId`,`businessAccountId`,`activeBusinessModelId`];var Q=class{constructor(e,t){if(this.bulker=e,this.opts=t,this.rabbitQueueName=null,this.rabbit=null,this.bulkHandler=async(e,t,n)=>{try{let{query:n,payload:r,preview:i}=e.body??{},a=r;if(r&&this.payloadSchema)try{a=this.payloadSchema.parse(r)}catch(e){if(e instanceof p.ZodError)return t.status(400).json({error:`invalid_payload`,details:e.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`).join(`, `)});throw e}try{J(this.model,n,{logger:this.bulker.logger})}catch(e){return t.status(400).json({error:`invalid_query`,details:e.message||e})}let o=this.identityScopeSchema.safeParse(n?.query||{});if(!o.success)return t.status(400).json({error:`invalid_identity_scope`,details:o.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`).join(`, `)});Y(this.model,n,{includeRawPayload:!0});let{query:s,...c}=Object.assign(E(n,[`query`,`externalQueryValues`,`order`,`include`,`scopes`,`enrichments`]),{distinct:!0}),l=[];if(this.opts.additionalIdsHook){let{rawPayload:e}=n;try{l=await this.opts.additionalIdsHook({rawPayload:e,payload:a}),this.bulker.logger.info(`additionalIdsHook returned ${l.length} IDs for action ${this.action}`)}catch(e){return this.bulker.logger.error(`Error in additionalIdsHook for action ${this.action}: ${e.message||e}`,{err:e}),t.status(500).json({error:`additional_ids_hook_error`,details:e.message||e})}}let d=[];s&&typeof s==`object`&&Object.keys(s).length>0&&d.push(s),Array.isArray(l)&&l.length>0&&d.push({id:{$in:l}});let f;if(d.length===0)return t.status(400).json({error:`no_query`,details:`No valid query provided to select records`});if(d.length===1){let[e]=d;f=e}else f={$or:d};this.bulker.logger.info(`Constructed final where clause for action ${this.action}`,{where:f});let m={where:f,...c},h=await this.model.scope(this.modelScopes).count({...m,col:this.idField});if(i)return t.json({estimatedCount:h});let g=u();return await this.bulker.jobManager.initJob(g,{status:`queued`,total:h,action:this.action}),this.bulker.emitEvent(`job:created`,{jobId:g,action:this.action,total:h}),setImmediate(()=>{this.rabbitScanAndEnqueue(g,m,a).catch(e=>this.bulker.logger.error(e))}),t.status(202).json({jobId:g,estimatedCount:h})}catch(e){return this.bulker.logger.error(`Error in bulkHandler for action ${this.action}: ${e.message||e}`,{err:e}),n(e)}},this.action=t.action,this.model=t.model,this.modelScopes=t.modelScopes??[],this.consumer=t.consumer,this.rabbit=e.rabbit,this.rabbitQueueName=t.rabbitQueueName??`bulk-${this.action}-queue`,this.consumerOptions={enableRabbitTrace:!0,...this.opts.consumerOptions},this.queryFunction=t.queryFunction,this.payloadSchema=t.payloadSchema,!/^[a-zA-Z0-9-_]+$/.test(this.action))throw Error(`BulkRoute action must be alphanumeric`);if(!this.model||typeof this.model.findAll!=`function`||typeof this.model.count!=`function`)throw Error(`BulkRoute model must be a valid Sequelize model`);if(typeof this.consumer!=`function`)throw Error(`BulkRoute consumer must be a function`);if(this.queryFunction&&typeof this.queryFunction!=`function`)throw Error(`BulkRoute queryFunction must be a function`);if(this.payloadSchema&&!(this.payloadSchema instanceof p.ZodType))throw Error(`BulkRoute payloadSchema must be a Zod schema`);if(this.modelScopes&&!Array.isArray(this.modelScopes))throw Error(`BulkRoute scopes must be an array of strings`);if(this.opts.additionalIdsHook&&typeof this.opts.additionalIdsHook!=`function`)throw Error(`BulkRoute additionalIdsHook must be a function`);this.idField=t.idField??e.defaults.idField,this.pageSize=t.pageSize??e.defaults.pageSize;let n=this.model.rawAttributes||{},r=(t.identityScopes&&t.identityScopes.length>0?t.identityScopes:De).filter(e=>!!n[e]);r.length===0&&this.bulker.logger.warn(`BulkRoute for action ${this.action} has no valid identityScopes configured - all records will be accessible`),this.bulker.logger.info(`BulkRoute for action ${this.action} using idField ${this.idField}, pageSize ${this.pageSize}, identityScopes: ${r.join(`, `)}`),this.identityScopeSchema=Ee(r),this.startRabbitWorker().catch(e=>{this.bulker.logger.error(`Failed to start RabbitMQ worker for queue ${this.rabbitQueueName}: ${e.message||e}`)})}async getUserJobs(e,t=20){return this.bulker.jobManager.getUserJobs(e,t)}async rabbitScanAndEnqueue(e,n,r){if(!this.rabbit)throw Error(`RabbitMQ not configured in Bulker`);let i=Date.now().toString();if(this.bulker.getUserId){let t=this.bulker.getUserId();t&&await this.bulker.jobManager.addUserJob(t,e)}await this.bulker.jobManager.setJobFields(e,{status:`running`,startTime:i}),this.bulker.emitEvent(`job:started`,{jobId:e,action:this.action});let a=null,o=0,s=0;for(;;){let i=await this.bulker.jobManager.getJobField(e,`status`);if(!i||i===`canceled`){this.bulker.logger.info(`Job ${e} was canceled, stopping scan and enqueue`);break}let c=a?{[t.and]:[n.where,{[this.idField]:{[t.gt]:a}}]}:n.where,l=await this.model.scope(this.modelScopes).findAll({where:c,...E(n,[`include`,`order`,`scopes`]),attributes:[this.idField],order:[[this.idField,`ASC`]],limit:this.pageSize,raw:!0,subQuery:!1});if(l.length===0)break;let u=l.map(t=>({jobId:e,id:t[this.idField],payload:r}));this.bulker.logger.info(`Enqueuing ${u.length} messages to RabbitMQ queue ${this.rabbitQueueName}`);let d=(await Promise.allSettled(u.map(e=>this.rabbit.sendToQueue(this.rabbitQueueName,e)))).filter(e=>e.status===`rejected`);if(d.length>0)throw this.bulker.logger.error(`Failed to enqueue ${d.length} messages to RabbitMQ`,{rejected:d}),Error(`Failed to enqueue ${d.length} messages to RabbitMQ`);o+=l.length,await this.bulker.jobManager.incrJobField(e,`queued`,l.length),a=l[l.length-1][this.idField],s+=1,this.bulker.emitEvent(`scan:page`,{jobId:e,action:this.action,pageNumber:s,itemsInPage:l.length});let f=await this.bulker.jobManager.getJob(e);f&&this.bulker.emitEvent(`job:queued`,{jobId:e,action:this.action,queued:o,total:f.total})}o===0&&await this.bulker.jobManager.completeEmptyJob(e)}async startRabbitWorker(){return this.bulker.logger.info(`Starting RabbitMQ consumer for queue ${this.rabbitQueueName}`),this.bulker.emitEvent(`worker:started`,{action:this.action,queueName:this.rabbitQueueName}),this.rabbit?.consume(this.rabbitQueueName,async(e,t,n)=>{if(!e)return;let{jobId:r,id:i,payload:a}=e.content,o=!1,s=async()=>{o||(o=!0,await Promise.all([this.bulker.jobManager.ack(r),t()]),this.bulker.emitEvent(`item:processed`,{jobId:r,action:this.action,id:i}))},c=async(e,t)=>{if(o)return;o=!0;let s=t;s===void 0&&(s=X.isBulkerError(e)?e.retryable:!1),await Promise.all([this.bulker.jobManager.nack(r,e?e.message||String(e):`nacked`,{id:i,payload:a}),n(void 0,{skipRetry:!s})]),s?this.bulker.emitEvent(`item:retrying`,{jobId:r,action:this.action,id:i}):this.bulker.emitEvent(`item:failed`,{jobId:r,action:this.action,id:i,error:e?e.message||String(e):`nacked`})},l=await this.bulker.jobManager.getJobField(r,`status`);if(!l||l===`canceled`){await this.bulker.jobManager.incrJobField(r,`processed`,1);return}try{this.bulker.logger.info(`Processing job ${r} action ${this.action} id ${i}`),this.bulker.emitEvent(`item:processing`,{jobId:r,action:this.action,id:i}),await this.consumer({jobId:r,id:i,payload:a},s,c),await s()}catch(e){this.bulker.logger.error(`Error processing job ${r} action ${this.action} id ${i}: ${e.message||e}`,{err:e}),this.bulker.emitEvent(`worker:error`,{action:this.action,error:e.message||String(e)}),await c(e,!1)}},this.consumerOptions)}},Oe=class{constructor(e,t,n=`/bulk-actions`){this.routes=[],this.actionsToRouteMap=new Map,this.bulkHandler=async(e,t,n)=>{try{let{action:r}=e.body??{};if(!r)return t.status(400).json({error:`missing_action`,message:`Request body must include an "action" field`});let i=this.actionsToRouteMap.get(r);return i?i.bulkHandler(e,t,n):t.status(404).json({error:`unknown_action`,message:`Action "${r}" is not registered`,availableActions:Array.from(this.actionsToRouteMap.keys())})}catch(e){return this.bulker.logger.error(`Error in BulkRouter bulkHandler: ${e.message||e}`,{err:e}),n(e),null}},this.getJobHandler=async(e,t,n)=>{try{if(!e.params.id)return t.status(400).json({error:`missing_id`});let n=await this.bulker.jobManager.getJob(e.params.id);return n?t.json(n):t.status(404).json({error:`not_found`})}catch(e){return n(e)}},this.cancelJobHandler=async(e,t,n)=>{try{return e.params.id?await this.bulker.jobManager.cancelJob(e.params.id)?(this.bulker.logger.info(`Job ${e.params.id} cancel requested`),t.json({ok:!0})):t.status(404).json({error:`not_found`}):t.status(400).json({error:`missing_id`})}catch(e){return n(e)}},this.getMyJobsHandler=async(e,t,n)=>{try{if(!this.bulker.getUserId)return t.status(400).json({error:`user_id_function_not_configured`,message:`Bulker instance does not have a getUserId function configured`});let e=this.bulker.getUserId();if(!e)return t.json([]);let n=(await this.bulker.jobManager.getUserJobs(e)).filter(e=>e!==null);return n.sort((e,t)=>{let n=e.createdAt?new Date(e.createdAt).getTime():0;return(t.createdAt?new Date(t.createdAt).getTime():0)-n}),t.json(n)}catch(e){return n(e)}},this.bulker=e,this.router=t??f(),this.staticRoute=n,this.registerStaticRoutes()}registerStaticRoutes(){this.router.post(this.staticRoute,this.bulkHandler),this.router.get(`/jobs/:id`,this.getJobHandler),this.router.get(`/jobs`,this.getMyJobsHandler),this.router.post(`/jobs/:id/cancel`,this.cancelJobHandler)}addAction(e,t){let n=new Q(this.bulker,{action:e,...t});if(this.bulker.logger.info(`Registering action handler: ${e}`),this.actionsToRouteMap.has(e))throw Error(`Action "${e}" is already registered`);return this.actionsToRouteMap.set(e,n),this.routes.push(n),n}getRouter(){return this.router}},$=class e{constructor(e,t){this.redis=e,this.defaults={maxJobsPerUser:t?.maxJobsPerUser??-1,jobTtlSeconds:t?.jobTtlSeconds??168*3600,errorLogLimit:t?.errorLogLimit??10}}setEventEmitter(e){this.eventEmitter=e}static jobKey(e){return`bulker:job:${e}`}static userJobsKey(e){return`bulker:user:${e}:jobs`}async initJob(t,n){let r=Date.now().toString(),i=e.jobKey(t),a=this.redis.multi();a.hSet(i,{status:n.status,total:String(n.total),queued:`0`,processed:`0`,succeeded:`0`,failed:`0`,action:n.action,errors:JSON.stringify([]),createdAt:r,updatedAt:r,startTime:r,endTime:``}),a.expire(i,this.defaults.jobTtlSeconds),await a.exec()}async getJob(t){let n=e.jobKey(t),r=await this.redis.hGetAll(n);if(!r||Object.keys(r).length===0)return null;let i=r.startTime?Number(r.startTime):null,a=r.endTime&&r.endTime!==``?Number(r.endTime):null,o=Date.now(),s={startTime:i?new Date(i).toISOString():null,endTime:a?new Date(a).toISOString():null,durationMs:i?(a||o)-i:null};return{jobId:t,status:r.status,action:r.action,total:Number(r.total??0),queued:Number(r.queued??0),processed:Number(r.processed??0),succeeded:Number(r.succeeded??0),failed:Number(r.failed??0),errors:r.errors?JSON.parse(r.errors):[],createdAt:r.createdAt?new Date(Number(r.createdAt)).toISOString():void 0,updatedAt:r.updatedAt?new Date(Number(r.updatedAt)).toISOString():void 0,duration:s}}async setJobField(t,n,r){let i=e.jobKey(t);if(!await this.redis.exists(i))return!1;let a=this.redis.multi();return a.hSet(i,n,r),a.hSet(i,`updatedAt`,Date.now().toString()),await a.exec(),!0}async setJobFields(t,n){let r=e.jobKey(t);if(!await this.redis.exists(r))return!1;let i=this.redis.multi();return i.hSet(r,n),i.hSet(r,`updatedAt`,Date.now().toString()),await i.exec(),!0}async incrJobField(t,n,r=1){let i=e.jobKey(t),a=this.redis.multi();a.hIncrBy(i,n,r),a.hSet(i,`updatedAt`,Date.now().toString());let o=await a.exec();return o?.[0]&&o[0][0]===null?o[0][1]:0}async getJobField(t,n){let r=e.jobKey(t);return this.redis.hGet(r,n)}async cancelJob(e){return this.setJobField(e,`status`,`canceled`)}async completeEmptyJob(e){let t=Date.now().toString();await this.setJobFields(e,{status:`completed`,endTime:t})}async ack(t){let n=e.jobKey(t);await this.redis.eval(`
|
|
2
|
+
local jobKey = KEYS[1]
|
|
3
|
+
local updatedAt = ARGV[1]
|
|
4
|
+
local total = tonumber(redis.call('HGET', jobKey, 'total'))
|
|
5
|
+
if not total then
|
|
6
|
+
return 0
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
local processed = redis.call('HINCRBY', jobKey, 'processed', 1)
|
|
10
|
+
redis.call('HINCRBY', jobKey, 'succeeded', 1)
|
|
11
|
+
redis.call('HSET', jobKey, 'updatedAt', updatedAt)
|
|
12
|
+
|
|
13
|
+
if processed >= total then
|
|
14
|
+
redis.call('HSET', jobKey, 'status', 'completed')
|
|
15
|
+
redis.call('HSET', jobKey, 'endTime', updatedAt)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
return processed
|
|
19
|
+
`,{keys:[n],arguments:[Date.now().toString()]});let r=await this.getJob(t);if(r&&r.status===`completed`){let e=r.duration.durationMs||0;this.eventEmitter?.emitEvent(`job:completed`,{jobId:t,action:r.action,processed:r.processed,failed:r.failed,duration:e})}}async nack(t,n,r){let i=e.jobKey(t);await this.redis.eval(`
|
|
20
|
+
local jobKey = KEYS[1]
|
|
21
|
+
local errorMsg = ARGV[1]
|
|
22
|
+
local errorData = ARGV[2]
|
|
23
|
+
local errorLogLimit = tonumber(ARGV[3])
|
|
24
|
+
local updatedAt = ARGV[4]
|
|
25
|
+
|
|
26
|
+
local total = tonumber(redis.call('HGET', jobKey, 'total'))
|
|
27
|
+
if not total then
|
|
28
|
+
return 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
-- Add error
|
|
32
|
+
local errorsJson = redis.call('HGET', jobKey, 'errors') or '[]'
|
|
33
|
+
local errors = cjson.decode(errorsJson)
|
|
34
|
+
|
|
35
|
+
if #errors >= errorLogLimit then
|
|
36
|
+
-- Keep only the last (errorLogLimit - 1) entries
|
|
37
|
+
local newErrors = {}
|
|
38
|
+
for i = #errors - errorLogLimit + 2, #errors do
|
|
39
|
+
table.insert(newErrors, errors[i])
|
|
40
|
+
end
|
|
41
|
+
errors = newErrors
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
table.insert(errors, { message = errorMsg, data = cjson.decode(errorData) })
|
|
45
|
+
redis.call('HSET', jobKey, 'errors', cjson.encode(errors))
|
|
46
|
+
|
|
47
|
+
-- Update counters
|
|
48
|
+
redis.call('HINCRBY', jobKey, 'failed', 1)
|
|
49
|
+
local processed = redis.call('HINCRBY', jobKey, 'processed', 1)
|
|
50
|
+
redis.call('HSET', jobKey, 'updatedAt', updatedAt)
|
|
51
|
+
|
|
52
|
+
if processed >= total then
|
|
53
|
+
redis.call('HSET', jobKey, 'status', 'completed')
|
|
54
|
+
redis.call('HSET', jobKey, 'endTime', updatedAt)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
return processed
|
|
58
|
+
`,{keys:[i],arguments:[n,JSON.stringify(r),String(this.defaults.errorLogLimit),Date.now().toString()]});let a=await this.getJob(t);if(a&&a.status===`completed`){let e=a.duration.durationMs||0;this.eventEmitter?.emitEvent(`job:completed`,{jobId:t,action:a.action,processed:a.processed,failed:a.failed,duration:e})}}async addUserJob(t,n){let r=e.userJobsKey(t),i=this.redis.multi();i.lPush(r,n),this.defaults.maxJobsPerUser>0&&i.lTrim(r,0,this.defaults.maxJobsPerUser-1),i.expire(r,3600),await i.exec()}async removeUserJob(t,n){let r=e.userJobsKey(t);await this.redis.lRem(r,0,n)}async getUserJobs(t,n=20){let r=e.userJobsKey(t),i=await this.redis.lRange(r,0,n-1);return await Promise.all(i.map(e=>this.getJob(e)))}},ke=e({BulkRoute:()=>Q,Bulker:()=>je,BulkerError:()=>X,JobManager:()=>$,z:()=>m});const Ae={emitEvent:()=>{}};var je=class extends h{constructor(e){if(super(),this.bulkRouters=[],this.sequelize=e.sequelize,this.logger=e.logger,this.rabbit=e.rabbit,this.eventsEnabled=e.emitEvents??!1,e.redis&&typeof e.redis.connect==`function`)this.redis=e.redis,this.redis.isOpen||this.redis.connect().catch(e=>{this.logger.error(`Error connecting to Redis:`,e)});else{let t=e.redis;this.redis=d({socket:{host:t.host,port:t.port},password:t.password,database:t.db}),this.redis.connect().catch(e=>{this.logger.error(`Error connecting to Redis:`,e)})}this.getUserId=e.getUserId??(()=>null),this.defaults={maxJobsPerUser:e.defaults?.maxJobsPerUser??-1,pageSize:e.defaults?.pageSize??1e3,idField:e.defaults?.idField??`id`,workerConcurrency:e.defaults?.workerConcurrency??16,jobTtlSeconds:e.defaults?.jobTtlSeconds??168*3600,errorLogLimit:e.defaults?.errorLogLimit??10},this.jobManager=new $(this.redis,{maxJobsPerUser:this.defaults.maxJobsPerUser,jobTtlSeconds:this.defaults.jobTtlSeconds,errorLogLimit:this.defaults.errorLogLimit}),this.jobManager.setEventEmitter(this.eventsEnabled?{emitEvent:this.emitEvent.bind(this)}:Ae)}createBulkRouter(e,t){let n=new Oe(this,e,t);return this.bulkRouters.push(n),n}emitEvent(e,...t){this.eventsEnabled&&this.emit(e,...t)}};export{ke as Bulker,v as formatOperators,P as generateFilterReplacements,Se as queryFormatMiddleware,Ce as queryHandler,xe as queryValidationMiddleware,H as validatePayload};
|
|
2
59
|
//# sourceMappingURL=index.js.map
|