@lucaapp/service-utils 5.12.0 → 5.13.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.
@@ -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 through the given Sequelize
7
- * instance. Reuses Sequelize's connection pool, dialect options (TLS,
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,28 @@ 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 through the given Sequelize
7
- * instance. Reuses Sequelize's connection pool, dialect options (TLS,
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 [results] = await sequelize.query(text, {
15
- bind: values,
16
- raw: true,
17
- });
18
- return {
19
- rows: Array.isArray(results)
20
- ? results
21
- : [],
22
- };
17
+ const connectionManager = sequelize.connectionManager;
18
+ const connection = await connectionManager.getConnection({ type: 'write' });
19
+ try {
20
+ const result = await connection.query(text, values);
21
+ return {
22
+ rows: Array.isArray(result.rows) ? result.rows : [],
23
+ };
24
+ }
25
+ finally {
26
+ await connectionManager.releaseConnection(connection);
27
+ }
23
28
  },
24
29
  });
25
30
  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;
@@ -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.12.0",
3
+ "version": "5.13.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [