@lucaapp/service-utils 5.12.0 → 5.13.1
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.
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { RequestHandler } from 'express';
|
|
2
|
+
import type { Logger } from 'pino';
|
|
3
|
+
import type { Sequelize } from 'sequelize';
|
|
4
|
+
import type { QueueOptions } from 'pg-boss';
|
|
5
|
+
/** pg-boss queue policy values. Accepted at runtime but missing from typed surface. */
|
|
6
|
+
export type QueuePolicy = 'standard' | 'singleton' | 'short' | 'stately';
|
|
7
|
+
/** Queue options extended with the runtime-accepted `policy` field. */
|
|
8
|
+
export type BossQueueOptions = QueueOptions & {
|
|
9
|
+
policy?: QueuePolicy;
|
|
10
|
+
};
|
|
11
|
+
import type { Api } from '../api/api';
|
|
12
|
+
import type { Middleware } from '../api/types/middleware';
|
|
13
|
+
import type { PgBossContext } from './index';
|
|
14
|
+
import type { QueueDefinition } from './types';
|
|
15
|
+
/**
|
|
16
|
+
* Default queue options applied to every queue created via `bootstrapPgBoss`
|
|
17
|
+
* unless the caller overrides per-queue. Tuned for the operational profile
|
|
18
|
+
* we care about: singleton execution to prevent overlapping runs, exponential
|
|
19
|
+
* retry, and a small retry budget that's appropriate for short jobs.
|
|
20
|
+
*
|
|
21
|
+
* Override per-queue (e.g. `retryLimit: 5` for slow reconcile jobs) by
|
|
22
|
+
* passing `options` on the queue definition.
|
|
23
|
+
*
|
|
24
|
+
* `policy` is accepted at runtime by pg-boss but missing from the public
|
|
25
|
+
* `QueueOptions` type — cast via `unknown` to keep the type clean for callers.
|
|
26
|
+
*/
|
|
27
|
+
export declare const QUEUE_DEFAULTS: BossQueueOptions;
|
|
28
|
+
export interface PgBossBootstrapOptions {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
sequelize: Sequelize;
|
|
31
|
+
/** PG schema. Defaults to `'pgboss'` (each service's own DB isolates it). */
|
|
32
|
+
schema?: string;
|
|
33
|
+
mattermostWebhookUrl?: string | null;
|
|
34
|
+
serviceName?: string;
|
|
35
|
+
/** Queue definitions created via `boss.createQueue` after start. */
|
|
36
|
+
queueDefinitions?: QueueDefinition[];
|
|
37
|
+
/** Optional callback invoked after start so callers can register workers. */
|
|
38
|
+
registerWorkers?: (context: PgBossContext) => void;
|
|
39
|
+
logger: Logger;
|
|
40
|
+
}
|
|
41
|
+
export interface PgBossBootstrap {
|
|
42
|
+
context: PgBossContext;
|
|
43
|
+
start: () => Promise<void>;
|
|
44
|
+
stop: () => Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One-call bootstrap for a service's pg-boss instance:
|
|
48
|
+
* `createPgBoss` → `startBoss` → `boss.createQueue` per definition →
|
|
49
|
+
* caller-provided `registerWorkers` callback.
|
|
50
|
+
*
|
|
51
|
+
* Returns `null` when `enabled` is false so callers can skip wiring without
|
|
52
|
+
* branching. Returns `{ context, start, stop }` otherwise — caller awaits
|
|
53
|
+
* `start()` after the database connection is up and registers `stop` as a
|
|
54
|
+
* shutdown handler.
|
|
55
|
+
*/
|
|
56
|
+
export declare const bootstrapPgBoss: (opts: PgBossBootstrapOptions) => PgBossBootstrap | null;
|
|
57
|
+
/**
|
|
58
|
+
* Mount the pg-boss admin API on a parent `Api`. Wraps the
|
|
59
|
+
* `Api.child` + `PgBossService` + `mountPgBossApiRoutes` boilerplate.
|
|
60
|
+
*
|
|
61
|
+
* `middlewares` MUST contain the auth middleware that gates support
|
|
62
|
+
* access (`mountPgBossApiRoutes` rejects an empty list at mount time).
|
|
63
|
+
*/
|
|
64
|
+
export declare const mountPgBossDashboard: (parentApi: Api, context: PgBossContext, middlewares: Middleware<any, any, any, any, any, any>[]) => void;
|
|
65
|
+
/**
|
|
66
|
+
* Wrap an Express request handler so the request is enqueued via pg-boss
|
|
67
|
+
* when a context is provided. On enqueue failure (or when pg-boss is
|
|
68
|
+
* disabled) the original handler runs synchronously.
|
|
69
|
+
*
|
|
70
|
+
* Useful for HTTP-triggered jobs that must keep working when pg-boss is
|
|
71
|
+
* off or temporarily unavailable.
|
|
72
|
+
*/
|
|
73
|
+
export declare const withPgBossFallback: (context: PgBossContext | undefined, queueName: string, handler: RequestHandler, buildPayload?: (request: Parameters<RequestHandler>[0]) => object) => RequestHandler;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withPgBossFallback = exports.mountPgBossDashboard = exports.bootstrapPgBoss = exports.QUEUE_DEFAULTS = void 0;
|
|
4
|
+
const index_1 = require("./index");
|
|
5
|
+
const service_1 = require("./service");
|
|
6
|
+
const routes_1 = require("./controller/routes");
|
|
7
|
+
/**
|
|
8
|
+
* Default queue options applied to every queue created via `bootstrapPgBoss`
|
|
9
|
+
* unless the caller overrides per-queue. Tuned for the operational profile
|
|
10
|
+
* we care about: singleton execution to prevent overlapping runs, exponential
|
|
11
|
+
* retry, and a small retry budget that's appropriate for short jobs.
|
|
12
|
+
*
|
|
13
|
+
* Override per-queue (e.g. `retryLimit: 5` for slow reconcile jobs) by
|
|
14
|
+
* passing `options` on the queue definition.
|
|
15
|
+
*
|
|
16
|
+
* `policy` is accepted at runtime by pg-boss but missing from the public
|
|
17
|
+
* `QueueOptions` type — cast via `unknown` to keep the type clean for callers.
|
|
18
|
+
*/
|
|
19
|
+
exports.QUEUE_DEFAULTS = {
|
|
20
|
+
policy: 'singleton',
|
|
21
|
+
retryLimit: 3,
|
|
22
|
+
retryBackoff: true,
|
|
23
|
+
};
|
|
24
|
+
const DEFAULT_SCHEMA = 'pgboss';
|
|
25
|
+
/**
|
|
26
|
+
* One-call bootstrap for a service's pg-boss instance:
|
|
27
|
+
* `createPgBoss` → `startBoss` → `boss.createQueue` per definition →
|
|
28
|
+
* caller-provided `registerWorkers` callback.
|
|
29
|
+
*
|
|
30
|
+
* Returns `null` when `enabled` is false so callers can skip wiring without
|
|
31
|
+
* branching. Returns `{ context, start, stop }` otherwise — caller awaits
|
|
32
|
+
* `start()` after the database connection is up and registers `stop` as a
|
|
33
|
+
* shutdown handler.
|
|
34
|
+
*/
|
|
35
|
+
const bootstrapPgBoss = (opts) => {
|
|
36
|
+
if (!opts.enabled)
|
|
37
|
+
return null;
|
|
38
|
+
const context = (0, index_1.createPgBoss)({
|
|
39
|
+
enabled: true,
|
|
40
|
+
sequelize: opts.sequelize,
|
|
41
|
+
schema: opts.schema ?? DEFAULT_SCHEMA,
|
|
42
|
+
mattermostWebhookUrl: opts.mattermostWebhookUrl ?? null,
|
|
43
|
+
}, opts.logger, opts.serviceName ?? opts.schema ?? DEFAULT_SCHEMA);
|
|
44
|
+
const start = async () => {
|
|
45
|
+
await (0, index_1.startBoss)(context);
|
|
46
|
+
if (opts.queueDefinitions) {
|
|
47
|
+
for (const queue of opts.queueDefinitions) {
|
|
48
|
+
await context.boss.createQueue(queue.name, {
|
|
49
|
+
...exports.QUEUE_DEFAULTS,
|
|
50
|
+
...queue.options,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
opts.registerWorkers?.(context);
|
|
55
|
+
};
|
|
56
|
+
const stop = () => (0, index_1.stopBoss)(context);
|
|
57
|
+
return { context, start, stop };
|
|
58
|
+
};
|
|
59
|
+
exports.bootstrapPgBoss = bootstrapPgBoss;
|
|
60
|
+
/**
|
|
61
|
+
* Mount the pg-boss admin API on a parent `Api`. Wraps the
|
|
62
|
+
* `Api.child` + `PgBossService` + `mountPgBossApiRoutes` boilerplate.
|
|
63
|
+
*
|
|
64
|
+
* `middlewares` MUST contain the auth middleware that gates support
|
|
65
|
+
* access (`mountPgBossApiRoutes` rejects an empty list at mount time).
|
|
66
|
+
*/
|
|
67
|
+
const mountPgBossDashboard = (parentApi, context,
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
middlewares) => {
|
|
70
|
+
const childApi = parentApi.child({ tags: ['PgBoss'] });
|
|
71
|
+
const service = new service_1.PgBossService(context.boss, context.logger, context.config.schema);
|
|
72
|
+
(0, routes_1.mountPgBossApiRoutes)(childApi, service, middlewares);
|
|
73
|
+
};
|
|
74
|
+
exports.mountPgBossDashboard = mountPgBossDashboard;
|
|
75
|
+
/**
|
|
76
|
+
* Wrap an Express request handler so the request is enqueued via pg-boss
|
|
77
|
+
* when a context is provided. On enqueue failure (or when pg-boss is
|
|
78
|
+
* disabled) the original handler runs synchronously.
|
|
79
|
+
*
|
|
80
|
+
* Useful for HTTP-triggered jobs that must keep working when pg-boss is
|
|
81
|
+
* off or temporarily unavailable.
|
|
82
|
+
*/
|
|
83
|
+
const withPgBossFallback = (context, queueName, handler, buildPayload = () => ({})) => {
|
|
84
|
+
return async (request, response, next) => {
|
|
85
|
+
if (context) {
|
|
86
|
+
try {
|
|
87
|
+
const jobId = await context.boss.send(queueName, buildPayload(request));
|
|
88
|
+
response.json({ queued: true, jobId });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
context.logger.error({ error, queueName }, 'pg-boss enqueue failed, falling back to sync handler');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
await handler(request, response, next);
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
exports.withPgBossFallback = withPgBossFallback;
|
|
@@ -3,9 +3,12 @@ import type { Db, Job, SendOptions } from 'pg-boss';
|
|
|
3
3
|
import type { Logger } from 'pino';
|
|
4
4
|
import type { Sequelize, Transaction } from 'sequelize';
|
|
5
5
|
/**
|
|
6
|
-
* Create a pg-boss database adapter that runs SQL
|
|
7
|
-
*
|
|
8
|
-
* fingerprint pinning), retries, and disconnect handling
|
|
6
|
+
* Create a pg-boss database adapter that runs SQL via Sequelize's raw pg
|
|
7
|
+
* client. Reuses Sequelize's connection pool, dialect options (TLS,
|
|
8
|
+
* fingerprint pinning), retries, and disconnect handling, but bypasses
|
|
9
|
+
* Sequelize's SQL parser — pg-boss's DDL is multi-statement and uses
|
|
10
|
+
* `GENERATED ALWAYS AS IDENTITY` syntax that breaks under prepared-statement
|
|
11
|
+
* mode.
|
|
9
12
|
*
|
|
10
13
|
* Used as pg-boss's `db` option so pg-boss does not open its own pool.
|
|
11
14
|
*/
|
|
@@ -3,23 +3,30 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.withDlqAlerting = exports.enqueueInTransaction = exports.enqueueJob = exports.createSequelizeDbAdapter = void 0;
|
|
4
4
|
const metrics_1 = require("./metrics");
|
|
5
5
|
/**
|
|
6
|
-
* Create a pg-boss database adapter that runs SQL
|
|
7
|
-
*
|
|
8
|
-
* fingerprint pinning), retries, and disconnect handling
|
|
6
|
+
* Create a pg-boss database adapter that runs SQL via Sequelize's raw pg
|
|
7
|
+
* client. Reuses Sequelize's connection pool, dialect options (TLS,
|
|
8
|
+
* fingerprint pinning), retries, and disconnect handling, but bypasses
|
|
9
|
+
* Sequelize's SQL parser — pg-boss's DDL is multi-statement and uses
|
|
10
|
+
* `GENERATED ALWAYS AS IDENTITY` syntax that breaks under prepared-statement
|
|
11
|
+
* mode.
|
|
9
12
|
*
|
|
10
13
|
* Used as pg-boss's `db` option so pg-boss does not open its own pool.
|
|
11
14
|
*/
|
|
12
15
|
const createSequelizeDbAdapter = (sequelize) => ({
|
|
13
16
|
executeSql: async (text, values) => {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
: [],
|
|
22
|
-
|
|
17
|
+
const connectionManager = sequelize.connectionManager;
|
|
18
|
+
const connection = await connectionManager.getConnection({ type: 'write' });
|
|
19
|
+
try {
|
|
20
|
+
const result = values && values.length > 0
|
|
21
|
+
? await connection.query(text, values)
|
|
22
|
+
: await connection.query(text);
|
|
23
|
+
return {
|
|
24
|
+
rows: Array.isArray(result.rows) ? result.rows : [],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
await connectionManager.releaseConnection(connection);
|
|
29
|
+
}
|
|
23
30
|
},
|
|
24
31
|
});
|
|
25
32
|
exports.createSequelizeDbAdapter = createSequelizeDbAdapter;
|
|
@@ -11,6 +11,8 @@ export { PgBossService } from './service';
|
|
|
11
11
|
export { registerPgBossMetrics } from './metrics';
|
|
12
12
|
export { enqueueJob, enqueueInTransaction, withDlqAlerting } from './helpers';
|
|
13
13
|
export { sendDlqAlert } from './metrics';
|
|
14
|
+
export { bootstrapPgBoss, mountPgBossDashboard, withPgBossFallback, QUEUE_DEFAULTS, } from './bootstrap';
|
|
15
|
+
export type { PgBossBootstrapOptions, PgBossBootstrap } from './bootstrap';
|
|
14
16
|
export interface PgBossContext {
|
|
15
17
|
boss: PgBoss;
|
|
16
18
|
logger: Logger;
|
package/dist/lib/pgBoss/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.defaultRetention = exports.stopBoss = exports.startBoss = exports.createPgBoss = exports.sendDlqAlert = exports.withDlqAlerting = exports.enqueueInTransaction = exports.enqueueJob = exports.registerPgBossMetrics = exports.PgBossService = exports.mountPgBossApiRoutes = exports.buildPgBossDefaults = exports.PG_BOSS_DEFAULTS = void 0;
|
|
3
|
+
exports.defaultRetention = exports.stopBoss = exports.startBoss = exports.createPgBoss = exports.QUEUE_DEFAULTS = exports.withPgBossFallback = exports.mountPgBossDashboard = exports.bootstrapPgBoss = exports.sendDlqAlert = exports.withDlqAlerting = exports.enqueueInTransaction = exports.enqueueJob = exports.registerPgBossMetrics = exports.PgBossService = exports.mountPgBossApiRoutes = exports.buildPgBossDefaults = exports.PG_BOSS_DEFAULTS = void 0;
|
|
4
4
|
const pg_boss_1 = require("pg-boss");
|
|
5
5
|
const config_1 = require("./config");
|
|
6
6
|
const metrics_1 = require("./metrics");
|
|
@@ -20,6 +20,11 @@ Object.defineProperty(exports, "enqueueInTransaction", { enumerable: true, get:
|
|
|
20
20
|
Object.defineProperty(exports, "withDlqAlerting", { enumerable: true, get: function () { return helpers_2.withDlqAlerting; } });
|
|
21
21
|
var metrics_3 = require("./metrics");
|
|
22
22
|
Object.defineProperty(exports, "sendDlqAlert", { enumerable: true, get: function () { return metrics_3.sendDlqAlert; } });
|
|
23
|
+
var bootstrap_1 = require("./bootstrap");
|
|
24
|
+
Object.defineProperty(exports, "bootstrapPgBoss", { enumerable: true, get: function () { return bootstrap_1.bootstrapPgBoss; } });
|
|
25
|
+
Object.defineProperty(exports, "mountPgBossDashboard", { enumerable: true, get: function () { return bootstrap_1.mountPgBossDashboard; } });
|
|
26
|
+
Object.defineProperty(exports, "withPgBossFallback", { enumerable: true, get: function () { return bootstrap_1.withPgBossFallback; } });
|
|
27
|
+
Object.defineProperty(exports, "QUEUE_DEFAULTS", { enumerable: true, get: function () { return bootstrap_1.QUEUE_DEFAULTS; } });
|
|
23
28
|
const RESTART_DELAY_MIN_MS = 5_000;
|
|
24
29
|
const RESTART_DELAY_MAX_MS = 60_000;
|
|
25
30
|
const RESTART_MAX_ATTEMPTS = 10;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lucaapp/service-utils",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.13.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"files": [
|
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
"follow-redirects": "^1.16.0",
|
|
100
100
|
"axios": "1.16.0",
|
|
101
101
|
"path-to-regexp": "0.1.13",
|
|
102
|
-
"ip-address": "10.1.1"
|
|
102
|
+
"ip-address": "10.1.1",
|
|
103
|
+
"fast-xml-builder": "^1.1.7"
|
|
103
104
|
}
|
|
104
105
|
}
|