@hotmeshio/hotmesh 0.13.0 → 0.14.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/README.md +18 -22
- package/build/modules/enums.d.ts +60 -5
- package/build/modules/enums.js +62 -7
- package/build/modules/errors.d.ts +15 -3
- package/build/modules/errors.js +17 -2
- package/build/package.json +6 -1
- package/build/services/activities/activity/context.d.ts +22 -0
- package/build/services/activities/activity/context.js +76 -0
- package/build/services/activities/activity/index.d.ts +116 -0
- package/build/services/activities/activity/index.js +299 -0
- package/build/services/activities/activity/mapping.d.ts +12 -0
- package/build/services/activities/activity/mapping.js +63 -0
- package/build/services/activities/activity/process.d.ts +28 -0
- package/build/services/activities/activity/process.js +100 -0
- package/build/services/activities/activity/protocol.d.ts +39 -0
- package/build/services/activities/activity/protocol.js +151 -0
- package/build/services/activities/activity/state.d.ts +40 -0
- package/build/services/activities/activity/state.js +143 -0
- package/build/services/activities/activity/transition.d.ts +23 -0
- package/build/services/activities/activity/transition.js +71 -0
- package/build/services/activities/activity/verify.d.ts +22 -0
- package/build/services/activities/activity/verify.js +85 -0
- package/build/services/activities/await.d.ts +1 -4
- package/build/services/activities/await.js +2 -36
- package/build/services/activities/cycle.d.ts +1 -11
- package/build/services/activities/cycle.js +3 -46
- package/build/services/activities/hook.d.ts +2 -11
- package/build/services/activities/hook.js +30 -50
- package/build/services/activities/interrupt.d.ts +2 -4
- package/build/services/activities/interrupt.js +4 -38
- package/build/services/activities/signal.d.ts +1 -11
- package/build/services/activities/signal.js +3 -48
- package/build/services/activities/trigger.d.ts +1 -3
- package/build/services/activities/trigger.js +0 -3
- package/build/services/activities/worker.d.ts +3 -6
- package/build/services/activities/worker.js +4 -40
- package/build/services/connector/factory.d.ts +6 -0
- package/build/services/connector/factory.js +24 -0
- package/build/services/durable/activity.d.ts +1 -1
- package/build/services/durable/activity.js +2 -2
- package/build/services/durable/client.d.ts +24 -29
- package/build/services/durable/client.js +24 -29
- package/build/services/durable/connection.d.ts +13 -7
- package/build/services/durable/connection.js +13 -7
- package/build/services/durable/handle.d.ts +58 -40
- package/build/services/durable/handle.js +60 -40
- package/build/services/durable/index.d.ts +148 -286
- package/build/services/durable/index.js +157 -292
- package/build/services/durable/interceptor.d.ts +43 -33
- package/build/services/durable/interceptor.js +59 -39
- package/build/services/durable/schemas/factory.d.ts +1 -1
- package/build/services/durable/schemas/factory.js +168 -38
- package/build/services/durable/telemetry.d.ts +80 -0
- package/build/services/durable/telemetry.js +137 -0
- package/build/services/durable/worker.d.ts +100 -21
- package/build/services/durable/worker.js +304 -63
- package/build/services/durable/workflow/all.d.ts +1 -1
- package/build/services/durable/workflow/all.js +1 -1
- package/build/services/durable/workflow/cancellationScope.d.ts +104 -0
- package/build/services/durable/workflow/cancellationScope.js +139 -0
- package/build/services/durable/workflow/common.d.ts +5 -4
- package/build/services/durable/workflow/common.js +6 -1
- package/build/services/durable/workflow/{waitFor.d.ts → condition.d.ts} +9 -8
- package/build/services/durable/workflow/{waitFor.js → condition.js} +44 -11
- package/build/services/durable/workflow/continueAsNew.d.ts +65 -0
- package/build/services/durable/workflow/continueAsNew.js +92 -0
- package/build/services/durable/workflow/didRun.d.ts +1 -1
- package/build/services/durable/workflow/didRun.js +3 -3
- package/build/services/durable/workflow/enrich.d.ts +5 -0
- package/build/services/durable/workflow/enrich.js +5 -0
- package/build/services/durable/workflow/entityMethods.d.ts +7 -0
- package/build/services/durable/workflow/entityMethods.js +7 -0
- package/build/services/durable/workflow/execHook.js +3 -3
- package/build/services/durable/workflow/execHookBatch.js +2 -2
- package/build/services/durable/workflow/{execChild.d.ts → executeChild.d.ts} +4 -40
- package/build/services/durable/workflow/{execChild.js → executeChild.js} +36 -45
- package/build/services/durable/workflow/hook.d.ts +1 -1
- package/build/services/durable/workflow/hook.js +4 -3
- package/build/services/durable/workflow/index.d.ts +45 -50
- package/build/services/durable/workflow/index.js +46 -51
- package/build/services/durable/workflow/interruption.d.ts +7 -6
- package/build/services/durable/workflow/interruption.js +11 -7
- package/build/services/durable/workflow/patched.d.ts +72 -0
- package/build/services/durable/workflow/patched.js +110 -0
- package/build/services/durable/workflow/proxyActivities.d.ts +7 -7
- package/build/services/durable/workflow/proxyActivities.js +50 -15
- package/build/services/durable/workflow/searchMethods.d.ts +7 -0
- package/build/services/durable/workflow/searchMethods.js +7 -0
- package/build/services/durable/workflow/signal.d.ts +4 -4
- package/build/services/durable/workflow/signal.js +4 -4
- package/build/services/durable/workflow/{sleepFor.d.ts → sleep.d.ts} +7 -7
- package/build/services/durable/workflow/{sleepFor.js → sleep.js} +39 -10
- package/build/services/durable/workflow/terminate.d.ts +55 -0
- package/build/services/durable/workflow/{interrupt.js → terminate.js} +21 -21
- package/build/services/durable/workflow/trace.js +2 -2
- package/build/services/durable/workflow/uuid4.d.ts +14 -0
- package/build/services/durable/workflow/uuid4.js +39 -0
- package/build/services/durable/workflow/{context.d.ts → workflowInfo.d.ts} +5 -5
- package/build/services/durable/workflow/{context.js → workflowInfo.js} +7 -7
- package/build/services/engine/compiler.d.ts +19 -0
- package/build/services/engine/compiler.js +20 -0
- package/build/services/engine/completion.d.ts +46 -0
- package/build/services/engine/completion.js +145 -0
- package/build/services/engine/dispatch.d.ts +24 -0
- package/build/services/engine/dispatch.js +98 -0
- package/build/services/engine/index.d.ts +49 -81
- package/build/services/engine/index.js +175 -573
- package/build/services/engine/init.d.ts +42 -0
- package/build/services/engine/init.js +74 -0
- package/build/services/engine/pubsub.d.ts +50 -0
- package/build/services/engine/pubsub.js +118 -0
- package/build/services/engine/reporting.d.ts +20 -0
- package/build/services/engine/reporting.js +38 -0
- package/build/services/engine/schema.d.ts +23 -0
- package/build/services/engine/schema.js +62 -0
- package/build/services/engine/signal.d.ts +57 -0
- package/build/services/engine/signal.js +117 -0
- package/build/services/engine/state.d.ts +35 -0
- package/build/services/engine/state.js +61 -0
- package/build/services/engine/version.d.ts +31 -0
- package/build/services/engine/version.js +73 -0
- package/build/services/hotmesh/deployment.d.ts +21 -0
- package/build/services/hotmesh/deployment.js +25 -0
- package/build/services/hotmesh/index.d.ts +141 -532
- package/build/services/hotmesh/index.js +222 -673
- package/build/services/hotmesh/init.d.ts +42 -0
- package/build/services/hotmesh/init.js +93 -0
- package/build/services/hotmesh/jobs.d.ts +67 -0
- package/build/services/hotmesh/jobs.js +99 -0
- package/build/services/hotmesh/pubsub.d.ts +38 -0
- package/build/services/hotmesh/pubsub.js +54 -0
- package/build/services/hotmesh/quorum.d.ts +30 -0
- package/build/services/hotmesh/quorum.js +62 -0
- package/build/services/hotmesh/validation.d.ts +6 -0
- package/build/services/hotmesh/validation.js +28 -0
- package/build/services/quorum/index.js +1 -0
- package/build/services/router/consumption/index.d.ts +11 -5
- package/build/services/router/consumption/index.js +24 -17
- package/build/services/router/error-handling/index.d.ts +2 -2
- package/build/services/router/error-handling/index.js +14 -14
- package/build/services/router/index.d.ts +1 -1
- package/build/services/router/index.js +2 -2
- package/build/services/serializer/index.d.ts +22 -0
- package/build/services/serializer/index.js +39 -1
- package/build/services/store/index.d.ts +1 -0
- package/build/services/store/providers/postgres/exporter-sql.d.ts +2 -2
- package/build/services/store/providers/postgres/exporter-sql.js +4 -4
- package/build/services/store/providers/postgres/kvtables.js +7 -6
- package/build/services/store/providers/postgres/kvtypes/hash/basic.js +67 -52
- package/build/services/store/providers/postgres/kvtypes/hash/jsonb.js +87 -72
- package/build/services/store/providers/postgres/kvtypes/hash/udata.js +106 -79
- package/build/services/store/providers/postgres/kvtypes/hash/utils.d.ts +16 -0
- package/build/services/store/providers/postgres/kvtypes/hash/utils.js +29 -16
- package/build/services/store/providers/postgres/postgres.d.ts +1 -0
- package/build/services/store/providers/postgres/postgres.js +14 -4
- package/build/services/stream/factory.d.ts +3 -1
- package/build/services/stream/factory.js +2 -2
- package/build/services/stream/index.d.ts +1 -0
- package/build/services/stream/providers/nats/nats.d.ts +1 -0
- package/build/services/stream/providers/nats/nats.js +1 -0
- package/build/services/stream/providers/postgres/credentials.d.ts +56 -0
- package/build/services/stream/providers/postgres/credentials.js +129 -0
- package/build/services/stream/providers/postgres/kvtables.js +18 -0
- package/build/services/stream/providers/postgres/messages.js +7 -7
- package/build/services/stream/providers/postgres/notifications.js +16 -2
- package/build/services/stream/providers/postgres/postgres.d.ts +7 -0
- package/build/services/stream/providers/postgres/postgres.js +35 -4
- package/build/services/stream/providers/postgres/procedures.d.ts +21 -0
- package/build/services/stream/providers/postgres/procedures.js +213 -0
- package/build/services/stream/providers/postgres/secured.d.ts +34 -0
- package/build/services/stream/providers/postgres/secured.js +146 -0
- package/build/services/stream/providers/postgres/stats.d.ts +1 -0
- package/build/services/stream/providers/postgres/stats.js +1 -0
- package/build/services/stream/registry.d.ts +1 -1
- package/build/services/stream/registry.js +5 -2
- package/build/services/telemetry/index.d.ts +10 -1
- package/build/services/telemetry/index.js +40 -7
- package/build/services/worker/credentials.d.ts +51 -0
- package/build/services/worker/credentials.js +87 -0
- package/build/services/worker/index.d.ts +2 -2
- package/build/services/worker/index.js +7 -6
- package/build/types/codec.d.ts +84 -0
- package/build/types/codec.js +2 -0
- package/build/types/durable.d.ts +104 -28
- package/build/types/error.d.ts +10 -1
- package/build/types/hotmesh.d.ts +67 -4
- package/build/types/index.d.ts +2 -1
- package/build/types/provider.d.ts +2 -2
- package/build/types/quorum.d.ts +35 -1
- package/build/types/stream.d.ts +12 -6
- package/package.json +6 -1
- package/build/services/activities/activity.d.ts +0 -192
- package/build/services/activities/activity.js +0 -786
- package/build/services/durable/workflow/interrupt.d.ts +0 -55
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker role credential lifecycle management.
|
|
3
|
+
*
|
|
4
|
+
* These are the low-level functions that create, rotate, and revoke
|
|
5
|
+
* Postgres roles for worker routers. Each role is scoped to specific
|
|
6
|
+
* stream_name(s) via the `app.allowed_streams` session variable.
|
|
7
|
+
*
|
|
8
|
+
* **Prefer the high-level API**: Call `HotMesh.provisionWorkerRole()`,
|
|
9
|
+
* `HotMesh.rotateWorkerPassword()`, etc. — those methods handle
|
|
10
|
+
* connection creation and cleanup automatically.
|
|
11
|
+
*/
|
|
12
|
+
import { PostgresClientType } from '../../../../types/postgres';
|
|
13
|
+
export interface WorkerCredential {
|
|
14
|
+
roleName: string;
|
|
15
|
+
password: string;
|
|
16
|
+
}
|
|
17
|
+
export interface WorkerCredentialInfo {
|
|
18
|
+
id: number;
|
|
19
|
+
roleName: string;
|
|
20
|
+
streamNames: string[];
|
|
21
|
+
createdAt: Date;
|
|
22
|
+
revokedAt: Date | null;
|
|
23
|
+
lastRotatedAt: Date;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Provision a new Postgres role scoped to specific stream names.
|
|
27
|
+
*
|
|
28
|
+
* The role:
|
|
29
|
+
* - Can LOGIN with the returned password
|
|
30
|
+
* - Has USAGE on the schema
|
|
31
|
+
* - Has EXECUTE on the 5 worker stored procedures
|
|
32
|
+
* - Has `app.allowed_streams` set to the comma-separated stream names
|
|
33
|
+
* - Has NO direct table access
|
|
34
|
+
*
|
|
35
|
+
* @param adminClient - A Postgres client connected as the admin/owner
|
|
36
|
+
* @param schema - The appId schema name (e.g., 'durable')
|
|
37
|
+
* @param streamNames - Stream names this role can access (e.g., ['payment-activity'])
|
|
38
|
+
* @param password - Optional password (generated if not provided)
|
|
39
|
+
* @returns The created role name and password
|
|
40
|
+
*/
|
|
41
|
+
export declare function provisionWorkerRole(adminClient: PostgresClientType, schema: string, streamNames: string[], password?: string): Promise<WorkerCredential>;
|
|
42
|
+
/**
|
|
43
|
+
* Rotate the password for an existing worker role.
|
|
44
|
+
*/
|
|
45
|
+
export declare function rotateWorkerPassword(adminClient: PostgresClientType, schema: string, roleName: string, newPassword?: string): Promise<{
|
|
46
|
+
password: string;
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* Revoke a worker role by disabling login.
|
|
50
|
+
* The role is not dropped — this preserves the audit trail.
|
|
51
|
+
*/
|
|
52
|
+
export declare function revokeWorkerRole(adminClient: PostgresClientType, schema: string, roleName: string): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* List all worker roles for a schema.
|
|
55
|
+
*/
|
|
56
|
+
export declare function listWorkerRoles(adminClient: PostgresClientType, schema: string): Promise<WorkerCredentialInfo[]>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Worker role credential lifecycle management.
|
|
4
|
+
*
|
|
5
|
+
* These are the low-level functions that create, rotate, and revoke
|
|
6
|
+
* Postgres roles for worker routers. Each role is scoped to specific
|
|
7
|
+
* stream_name(s) via the `app.allowed_streams` session variable.
|
|
8
|
+
*
|
|
9
|
+
* **Prefer the high-level API**: Call `HotMesh.provisionWorkerRole()`,
|
|
10
|
+
* `HotMesh.rotateWorkerPassword()`, etc. — those methods handle
|
|
11
|
+
* connection creation and cleanup automatically.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.listWorkerRoles = exports.revokeWorkerRole = exports.rotateWorkerPassword = exports.provisionWorkerRole = void 0;
|
|
15
|
+
const crypto_1 = require("crypto");
|
|
16
|
+
/**
|
|
17
|
+
* Sanitize a stream name for use in a Postgres role name.
|
|
18
|
+
* Replaces non-alphanumeric characters with underscores.
|
|
19
|
+
*/
|
|
20
|
+
function sanitizeForRoleName(name) {
|
|
21
|
+
return name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Provision a new Postgres role scoped to specific stream names.
|
|
25
|
+
*
|
|
26
|
+
* The role:
|
|
27
|
+
* - Can LOGIN with the returned password
|
|
28
|
+
* - Has USAGE on the schema
|
|
29
|
+
* - Has EXECUTE on the 5 worker stored procedures
|
|
30
|
+
* - Has `app.allowed_streams` set to the comma-separated stream names
|
|
31
|
+
* - Has NO direct table access
|
|
32
|
+
*
|
|
33
|
+
* @param adminClient - A Postgres client connected as the admin/owner
|
|
34
|
+
* @param schema - The appId schema name (e.g., 'durable')
|
|
35
|
+
* @param streamNames - Stream names this role can access (e.g., ['payment-activity'])
|
|
36
|
+
* @param password - Optional password (generated if not provided)
|
|
37
|
+
* @returns The created role name and password
|
|
38
|
+
*/
|
|
39
|
+
async function provisionWorkerRole(adminClient, schema, streamNames, password) {
|
|
40
|
+
const safeSchema = schema.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
41
|
+
const streamSuffix = streamNames.map(sanitizeForRoleName).join('__');
|
|
42
|
+
const roleName = `hmsh_wrk_${safeSchema}_${streamSuffix}`.substring(0, 63);
|
|
43
|
+
const rolePassword = password || (0, crypto_1.randomUUID)();
|
|
44
|
+
const allowedStreams = streamNames.join(',');
|
|
45
|
+
// Create the role (idempotent — drop if exists first for credential reset)
|
|
46
|
+
await adminClient.query(`
|
|
47
|
+
DO $$
|
|
48
|
+
BEGIN
|
|
49
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${roleName}') THEN
|
|
50
|
+
EXECUTE format('ALTER ROLE %I LOGIN PASSWORD %L', '${roleName}', '${rolePassword}');
|
|
51
|
+
ELSE
|
|
52
|
+
EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', '${roleName}', '${rolePassword}');
|
|
53
|
+
END IF;
|
|
54
|
+
END
|
|
55
|
+
$$;
|
|
56
|
+
`);
|
|
57
|
+
// Grant schema usage (required to call functions in the schema)
|
|
58
|
+
await adminClient.query(`GRANT USAGE ON SCHEMA ${safeSchema} TO ${roleName};`);
|
|
59
|
+
// Grant EXECUTE on the 5 worker procedures
|
|
60
|
+
const procedures = [
|
|
61
|
+
`${safeSchema}.worker_dequeue(TEXT, INT, TEXT, INT)`,
|
|
62
|
+
`${safeSchema}.worker_ack(TEXT, BIGINT[])`,
|
|
63
|
+
`${safeSchema}.worker_dead_letter(TEXT, BIGINT[])`,
|
|
64
|
+
`${safeSchema}.worker_respond(TEXT, TEXT, INT, NUMERIC, INT, TIMESTAMPTZ, INT)`,
|
|
65
|
+
`${safeSchema}.worker_listen(TEXT)`,
|
|
66
|
+
`${safeSchema}.worker_unlisten(TEXT)`,
|
|
67
|
+
];
|
|
68
|
+
for (const proc of procedures) {
|
|
69
|
+
await adminClient.query(`GRANT EXECUTE ON FUNCTION ${proc} TO ${roleName};`);
|
|
70
|
+
}
|
|
71
|
+
// Set the allowed streams as a session default for this role
|
|
72
|
+
await adminClient.query(`ALTER ROLE ${roleName} SET app.allowed_streams = '${allowedStreams}';`);
|
|
73
|
+
// Explicitly revoke direct table access (defense in depth)
|
|
74
|
+
await adminClient.query(`
|
|
75
|
+
REVOKE ALL ON ALL TABLES IN SCHEMA ${safeSchema} FROM ${roleName};
|
|
76
|
+
`);
|
|
77
|
+
// Record in worker_credentials table
|
|
78
|
+
await adminClient.query(`INSERT INTO ${safeSchema}.worker_credentials (role_name, stream_names)
|
|
79
|
+
VALUES ($1, $2)
|
|
80
|
+
ON CONFLICT (role_name) DO UPDATE
|
|
81
|
+
SET stream_names = $2,
|
|
82
|
+
revoked_at = NULL,
|
|
83
|
+
last_rotated_at = NOW()`, [roleName, streamNames]);
|
|
84
|
+
return { roleName, password: rolePassword };
|
|
85
|
+
}
|
|
86
|
+
exports.provisionWorkerRole = provisionWorkerRole;
|
|
87
|
+
/**
|
|
88
|
+
* Rotate the password for an existing worker role.
|
|
89
|
+
*/
|
|
90
|
+
async function rotateWorkerPassword(adminClient, schema, roleName, newPassword) {
|
|
91
|
+
const safeSchema = schema.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
92
|
+
const password = newPassword || (0, crypto_1.randomUUID)();
|
|
93
|
+
await adminClient.query(`ALTER ROLE ${roleName} PASSWORD '${password}';`);
|
|
94
|
+
await adminClient.query(`UPDATE ${safeSchema}.worker_credentials
|
|
95
|
+
SET last_rotated_at = NOW()
|
|
96
|
+
WHERE role_name = $1`, [roleName]);
|
|
97
|
+
return { password };
|
|
98
|
+
}
|
|
99
|
+
exports.rotateWorkerPassword = rotateWorkerPassword;
|
|
100
|
+
/**
|
|
101
|
+
* Revoke a worker role by disabling login.
|
|
102
|
+
* The role is not dropped — this preserves the audit trail.
|
|
103
|
+
*/
|
|
104
|
+
async function revokeWorkerRole(adminClient, schema, roleName) {
|
|
105
|
+
const safeSchema = schema.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
106
|
+
await adminClient.query(`ALTER ROLE ${roleName} NOLOGIN;`);
|
|
107
|
+
await adminClient.query(`UPDATE ${safeSchema}.worker_credentials
|
|
108
|
+
SET revoked_at = NOW()
|
|
109
|
+
WHERE role_name = $1`, [roleName]);
|
|
110
|
+
}
|
|
111
|
+
exports.revokeWorkerRole = revokeWorkerRole;
|
|
112
|
+
/**
|
|
113
|
+
* List all worker roles for a schema.
|
|
114
|
+
*/
|
|
115
|
+
async function listWorkerRoles(adminClient, schema) {
|
|
116
|
+
const safeSchema = schema.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
117
|
+
const result = await adminClient.query(`SELECT id, role_name, stream_names, created_at, revoked_at, last_rotated_at
|
|
118
|
+
FROM ${safeSchema}.worker_credentials
|
|
119
|
+
ORDER BY created_at`);
|
|
120
|
+
return result.rows.map((row) => ({
|
|
121
|
+
id: row.id,
|
|
122
|
+
roleName: row.role_name,
|
|
123
|
+
streamNames: row.stream_names,
|
|
124
|
+
createdAt: row.created_at,
|
|
125
|
+
revokedAt: row.revoked_at,
|
|
126
|
+
lastRotatedAt: row.last_rotated_at,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
exports.listWorkerRoles = listWorkerRoles;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getNotificationChannelName = exports.deploySchema = void 0;
|
|
4
4
|
const enums_1 = require("../../../../modules/enums");
|
|
5
5
|
const utils_1 = require("../../../../modules/utils");
|
|
6
|
+
const procedures_1 = require("./procedures");
|
|
6
7
|
async function deploySchema(streamClient, appId, logger) {
|
|
7
8
|
const isPool = streamClient?.totalCount !== undefined &&
|
|
8
9
|
streamClient?.idleCount !== undefined;
|
|
@@ -279,6 +280,23 @@ async function createTables(client, schemaName) {
|
|
|
279
280
|
ON ${workerTable} (jid, msg_type, created_at)
|
|
280
281
|
WHERE jid != '';
|
|
281
282
|
`);
|
|
283
|
+
// ---- ROLES table (used by scout for distributed leader election) ----
|
|
284
|
+
const rolesTable = `${schemaName}.roles`;
|
|
285
|
+
await client.query(`
|
|
286
|
+
CREATE TABLE IF NOT EXISTS ${rolesTable} (
|
|
287
|
+
key TEXT PRIMARY KEY,
|
|
288
|
+
value TEXT,
|
|
289
|
+
expiry TIMESTAMP WITH TIME ZONE
|
|
290
|
+
);
|
|
291
|
+
`);
|
|
292
|
+
// ---- WORKER ACCESS CONTROL ----
|
|
293
|
+
// SECURITY DEFINER stored procedures for scoped worker access
|
|
294
|
+
const procedureStatements = (0, procedures_1.getCreateProceduresSQL)(schemaName);
|
|
295
|
+
for (const sql of procedureStatements) {
|
|
296
|
+
await client.query(sql);
|
|
297
|
+
}
|
|
298
|
+
// Worker credentials registry
|
|
299
|
+
await client.query((0, procedures_1.getCreateWorkerCredentialsTableSQL)(schemaName));
|
|
282
300
|
}
|
|
283
301
|
async function createNotificationTriggers(client, schemaName) {
|
|
284
302
|
const engineTable = `${schemaName}.engine_streams`;
|
|
@@ -57,13 +57,13 @@ function buildPublishSQL(tableName, streamName, isEngine, messages, options) {
|
|
|
57
57
|
const msgType = data.type || '';
|
|
58
58
|
const topic = data.metadata?.topic || '';
|
|
59
59
|
// Determine if this message has explicit retry config
|
|
60
|
-
const hasExplicitConfig = (retryConfig && 'max_retry_attempts' in retryConfig) || options?.
|
|
60
|
+
const hasExplicitConfig = (retryConfig && 'max_retry_attempts' in retryConfig) || options?.retry;
|
|
61
61
|
let normalizedPolicy = null;
|
|
62
62
|
if (retryConfig && 'max_retry_attempts' in retryConfig) {
|
|
63
63
|
normalizedPolicy = retryConfig;
|
|
64
64
|
}
|
|
65
|
-
else if (options?.
|
|
66
|
-
normalizedPolicy = (0, utils_1.normalizeRetryPolicy)(options.
|
|
65
|
+
else if (options?.retry) {
|
|
66
|
+
normalizedPolicy = (0, utils_1.normalizeRetryPolicy)(options.retry, {
|
|
67
67
|
maximumAttempts: 3,
|
|
68
68
|
backoffCoefficient: 10,
|
|
69
69
|
maximumInterval: 120,
|
|
@@ -72,7 +72,7 @@ function buildPublishSQL(tableName, streamName, isEngine, messages, options) {
|
|
|
72
72
|
return {
|
|
73
73
|
message: JSON.stringify(data),
|
|
74
74
|
hasExplicitConfig,
|
|
75
|
-
|
|
75
|
+
retry: normalizedPolicy,
|
|
76
76
|
visibilityDelayMs: visibilityDelayMs || 0,
|
|
77
77
|
retryAttempt: retryAttempt || 0,
|
|
78
78
|
workflowName,
|
|
@@ -123,7 +123,7 @@ function buildPublishSQL(tableName, streamName, isEngine, messages, options) {
|
|
|
123
123
|
if (pm.hasExplicitConfig) {
|
|
124
124
|
const paramOffset = params.length + 1;
|
|
125
125
|
valuesClauses.push(`($1, $${paramOffset}, $${paramOffset + 1}, $${paramOffset + 2}, $${paramOffset + 3}, ${visibleAtClause}, $${paramOffset + 4})`);
|
|
126
|
-
params.push(pm.message, pm.
|
|
126
|
+
params.push(pm.message, pm.retry.max_retry_attempts, pm.retry.backoff_coefficient, pm.retry.maximum_interval_seconds, pm.retryAttempt);
|
|
127
127
|
}
|
|
128
128
|
else {
|
|
129
129
|
const paramOffset = params.length + 1;
|
|
@@ -167,7 +167,7 @@ function buildPublishSQL(tableName, streamName, isEngine, messages, options) {
|
|
|
167
167
|
if (pm.hasExplicitConfig) {
|
|
168
168
|
const paramOffset = params.length + 1;
|
|
169
169
|
valuesClauses.push(`($1, $${paramOffset}, $${paramOffset + 1}, $${paramOffset + 2}, $${paramOffset + 3}, $${paramOffset + 4}, $${paramOffset + 5}, $${paramOffset + 6}, $${paramOffset + 7}, $${paramOffset + 8}, $${paramOffset + 9}, ${visibleAtClause}, $${paramOffset + 10})`);
|
|
170
|
-
params.push(pm.workflowName, pm.jid, pm.aid, pm.dad, pm.msgType, pm.topic, pm.message, pm.
|
|
170
|
+
params.push(pm.workflowName, pm.jid, pm.aid, pm.dad, pm.msgType, pm.topic, pm.message, pm.retry.max_retry_attempts, pm.retry.backoff_coefficient, pm.retry.maximum_interval_seconds, pm.retryAttempt);
|
|
171
171
|
}
|
|
172
172
|
else {
|
|
173
173
|
const paramOffset = params.length + 1;
|
|
@@ -243,7 +243,7 @@ async function fetchMessages(client, tableName, streamName, isEngine, consumerNa
|
|
|
243
243
|
return {
|
|
244
244
|
id: row.id.toString(),
|
|
245
245
|
data,
|
|
246
|
-
|
|
246
|
+
retry: (row.max_retry_attempts !== null && !hasDefaultRetryPolicy) ? {
|
|
247
247
|
maximumAttempts: row.max_retry_attempts,
|
|
248
248
|
backoffCoefficient: parseFloat(row.backoff_coefficient),
|
|
249
249
|
maximumInterval: row.maximum_interval_seconds,
|
|
@@ -192,7 +192,14 @@ class NotificationManager {
|
|
|
192
192
|
// Set up LISTEN for this channel (only once per channel)
|
|
193
193
|
try {
|
|
194
194
|
const listenStart = Date.now();
|
|
195
|
-
|
|
195
|
+
// In secured mode, use stored procedure for validated LISTEN
|
|
196
|
+
if (serviceAny.securedMode && serviceAny.safeName) {
|
|
197
|
+
const schema = serviceAny.safeName(serviceAny.appId);
|
|
198
|
+
await this.client.query(`SELECT ${schema}.worker_listen($1)`, [resolvedStreamName]);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
await this.client.query(`LISTEN "${channelName}"`);
|
|
202
|
+
}
|
|
196
203
|
this.logger.debug('postgres-stream-listen-start', {
|
|
197
204
|
streamName,
|
|
198
205
|
groupName,
|
|
@@ -257,7 +264,14 @@ class NotificationManager {
|
|
|
257
264
|
clientNotificationConsumers.delete(consumerKey);
|
|
258
265
|
const channelName = (0, kvtables_1.getNotificationChannelName)(resolvedStreamName, isEngine);
|
|
259
266
|
try {
|
|
260
|
-
|
|
267
|
+
// In secured mode, use stored procedure for validated UNLISTEN
|
|
268
|
+
if (serviceAny.securedMode && serviceAny.safeName) {
|
|
269
|
+
const schema = serviceAny.safeName(serviceAny.appId);
|
|
270
|
+
await this.client.query(`SELECT ${schema}.worker_unlisten($1)`, [resolvedStreamName]);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
await this.client.query(`UNLISTEN "${channelName}"`);
|
|
274
|
+
}
|
|
261
275
|
this.logger.debug('postgres-stream-unlisten', {
|
|
262
276
|
streamName,
|
|
263
277
|
groupName,
|
|
@@ -24,6 +24,12 @@ declare class PostgresStreamService extends StreamService<PostgresClientType & P
|
|
|
24
24
|
namespace: string;
|
|
25
25
|
appId: string;
|
|
26
26
|
logger: ILogger;
|
|
27
|
+
/**
|
|
28
|
+
* When true, all worker stream operations use SECURITY DEFINER
|
|
29
|
+
* stored procedures instead of raw SQL. Enabled when the worker
|
|
30
|
+
* connects with scoped `workerCredentials`.
|
|
31
|
+
*/
|
|
32
|
+
securedMode: boolean;
|
|
27
33
|
private scoutManager;
|
|
28
34
|
private notificationManager;
|
|
29
35
|
constructor(streamClient: PostgresClientType & ProviderClient, storeClient: ProviderClient, config?: StreamConfig);
|
|
@@ -104,6 +110,7 @@ declare class PostgresStreamService extends StreamService<PostgresClientType & P
|
|
|
104
110
|
supportsTrimming: boolean;
|
|
105
111
|
supportsRetry: boolean;
|
|
106
112
|
supportsNotifications: boolean;
|
|
113
|
+
supportsParallelProcessing: boolean;
|
|
107
114
|
maxMessageSize: number;
|
|
108
115
|
maxBatchSize: number;
|
|
109
116
|
};
|
|
@@ -29,6 +29,7 @@ const index_1 = require("../../index");
|
|
|
29
29
|
const kvtables_1 = require("./kvtables");
|
|
30
30
|
const Stats = __importStar(require("./stats"));
|
|
31
31
|
const Messages = __importStar(require("./messages"));
|
|
32
|
+
const Secured = __importStar(require("./secured"));
|
|
32
33
|
const scout_1 = require("./scout");
|
|
33
34
|
const notifications_1 = require("./notifications");
|
|
34
35
|
const Lifecycle = __importStar(require("./lifecycle"));
|
|
@@ -42,21 +43,35 @@ const Lifecycle = __importStar(require("./lifecycle"));
|
|
|
42
43
|
class PostgresStreamService extends index_1.StreamService {
|
|
43
44
|
constructor(streamClient, storeClient, config = {}) {
|
|
44
45
|
super(streamClient, storeClient, config);
|
|
46
|
+
/**
|
|
47
|
+
* When true, all worker stream operations use SECURITY DEFINER
|
|
48
|
+
* stored procedures instead of raw SQL. Enabled when the worker
|
|
49
|
+
* connects with scoped `workerCredentials`.
|
|
50
|
+
*/
|
|
51
|
+
this.securedMode = false;
|
|
52
|
+
if (config?.securedWorker) {
|
|
53
|
+
this.securedMode = true;
|
|
54
|
+
}
|
|
45
55
|
}
|
|
46
56
|
async init(namespace, appId, logger) {
|
|
47
57
|
this.namespace = namespace;
|
|
48
58
|
this.appId = appId;
|
|
49
59
|
this.logger = logger;
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
60
|
+
// Secured workers skip schema deployment and scout initialization —
|
|
61
|
+
// they use SECURITY DEFINER stored procedures for all stream ops
|
|
62
|
+
// and never need direct table access.
|
|
63
|
+
if (!this.securedMode) {
|
|
64
|
+
await (0, kvtables_1.deploySchema)(this.streamClient, this.appId, this.logger);
|
|
65
|
+
}
|
|
66
|
+
// Initialize scout manager (skipped in secured mode — roles table inaccessible)
|
|
67
|
+
this.scoutManager = this.securedMode ? null : new scout_1.ScoutManager(this.streamClient, this.appId, this.getEngineTableName.bind(this), this.mintKey.bind(this), this.logger);
|
|
53
68
|
// Initialize notification manager
|
|
54
69
|
this.notificationManager = new notifications_1.NotificationManager(this.streamClient, this.getEngineTableName.bind(this), () => (0, notifications_1.getFallbackInterval)(this.config), this.logger);
|
|
55
70
|
// Set up notification handler if supported
|
|
56
71
|
if (this.streamClient.on && this.isNotificationsEnabled()) {
|
|
57
72
|
this.notificationManager.setupClientNotificationHandler(this);
|
|
58
73
|
this.notificationManager.startClientFallbackPoller(this.checkForMissedMessages.bind(this));
|
|
59
|
-
this.scoutManager
|
|
74
|
+
this.scoutManager?.startRouterScoutPoller();
|
|
60
75
|
}
|
|
61
76
|
}
|
|
62
77
|
isNotificationsEnabled() {
|
|
@@ -152,6 +167,10 @@ class PostgresStreamService extends index_1.StreamService {
|
|
|
152
167
|
* added to the transaction for atomic execution.
|
|
153
168
|
*/
|
|
154
169
|
async publishMessages(streamName, messages, options) {
|
|
170
|
+
if (this.securedMode) {
|
|
171
|
+
const target = this.resolveStreamTarget(streamName);
|
|
172
|
+
return Secured.publishMessagesSecured(this.streamClient, this.safeName(this.appId), target.streamName, messages, this.logger);
|
|
173
|
+
}
|
|
155
174
|
const target = this.resolveStreamTarget(streamName);
|
|
156
175
|
return Messages.publishMessages(this.streamClient, target.tableName, target.streamName, target.isEngine, messages, options, this.logger);
|
|
157
176
|
}
|
|
@@ -213,14 +232,26 @@ class PostgresStreamService extends index_1.StreamService {
|
|
|
213
232
|
await this.notificationManager.stopNotificationConsumer(this, streamName, groupName);
|
|
214
233
|
}
|
|
215
234
|
async fetchMessages(streamName, groupName, consumerName, options) {
|
|
235
|
+
if (this.securedMode) {
|
|
236
|
+
const target = this.resolveStreamTarget(streamName);
|
|
237
|
+
return Secured.fetchMessagesSecured(this.streamClient, this.safeName(this.appId), target.streamName, consumerName, options || {}, this.logger);
|
|
238
|
+
}
|
|
216
239
|
const target = this.resolveStreamTarget(streamName);
|
|
217
240
|
return Messages.fetchMessages(this.streamClient, target.tableName, target.streamName, target.isEngine, consumerName, options || {}, this.logger);
|
|
218
241
|
}
|
|
219
242
|
async ackAndDelete(streamName, groupName, messageIds) {
|
|
243
|
+
if (this.securedMode) {
|
|
244
|
+
const target = this.resolveStreamTarget(streamName);
|
|
245
|
+
return Secured.ackAndDeleteSecured(this.streamClient, this.safeName(this.appId), target.streamName, messageIds, this.logger);
|
|
246
|
+
}
|
|
220
247
|
const target = this.resolveStreamTarget(streamName);
|
|
221
248
|
return Messages.ackAndDelete(this.streamClient, target.tableName, target.streamName, messageIds, this.logger);
|
|
222
249
|
}
|
|
223
250
|
async deadLetterMessages(streamName, groupName, messageIds) {
|
|
251
|
+
if (this.securedMode) {
|
|
252
|
+
const target = this.resolveStreamTarget(streamName);
|
|
253
|
+
return Secured.deadLetterMessagesSecured(this.streamClient, this.safeName(this.appId), target.streamName, messageIds, this.logger);
|
|
254
|
+
}
|
|
224
255
|
const target = this.resolveStreamTarget(streamName);
|
|
225
256
|
return Messages.deadLetterMessages(this.streamClient, target.tableName, target.streamName, messageIds, this.logger);
|
|
226
257
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SECURITY DEFINER stored procedures for worker router access control.
|
|
3
|
+
*
|
|
4
|
+
* Each procedure runs as the schema owner (admin) and validates the
|
|
5
|
+
* caller's `app.allowed_streams` session variable before executing.
|
|
6
|
+
* Worker roles are granted EXECUTE on these procedures only — they
|
|
7
|
+
* have zero direct table access.
|
|
8
|
+
*
|
|
9
|
+
* ## Procedures
|
|
10
|
+
*
|
|
11
|
+
* | Procedure | Purpose |
|
|
12
|
+
* |-----------|---------|
|
|
13
|
+
* | `worker_dequeue` | Fetch and reserve messages from `worker_streams` |
|
|
14
|
+
* | `worker_ack` | Soft-delete (ack) messages in `worker_streams` |
|
|
15
|
+
* | `worker_dead_letter` | Dead-letter messages in `worker_streams` |
|
|
16
|
+
* | `worker_respond` | Publish a response into `engine_streams` |
|
|
17
|
+
* | `worker_listen` | Subscribe to NOTIFY channel for a stream |
|
|
18
|
+
* | `worker_unlisten` | Unsubscribe from NOTIFY channel |
|
|
19
|
+
*/
|
|
20
|
+
export declare function getCreateProceduresSQL(schemaName: string): string[];
|
|
21
|
+
export declare function getCreateWorkerCredentialsTableSQL(schemaName: string): string;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SECURITY DEFINER stored procedures for worker router access control.
|
|
4
|
+
*
|
|
5
|
+
* Each procedure runs as the schema owner (admin) and validates the
|
|
6
|
+
* caller's `app.allowed_streams` session variable before executing.
|
|
7
|
+
* Worker roles are granted EXECUTE on these procedures only — they
|
|
8
|
+
* have zero direct table access.
|
|
9
|
+
*
|
|
10
|
+
* ## Procedures
|
|
11
|
+
*
|
|
12
|
+
* | Procedure | Purpose |
|
|
13
|
+
* |-----------|---------|
|
|
14
|
+
* | `worker_dequeue` | Fetch and reserve messages from `worker_streams` |
|
|
15
|
+
* | `worker_ack` | Soft-delete (ack) messages in `worker_streams` |
|
|
16
|
+
* | `worker_dead_letter` | Dead-letter messages in `worker_streams` |
|
|
17
|
+
* | `worker_respond` | Publish a response into `engine_streams` |
|
|
18
|
+
* | `worker_listen` | Subscribe to NOTIFY channel for a stream |
|
|
19
|
+
* | `worker_unlisten` | Unsubscribe from NOTIFY channel |
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.getCreateWorkerCredentialsTableSQL = exports.getCreateProceduresSQL = void 0;
|
|
23
|
+
const STREAM_ACCESS_CHECK = `
|
|
24
|
+
DECLARE
|
|
25
|
+
allowed_streams TEXT[];
|
|
26
|
+
BEGIN
|
|
27
|
+
allowed_streams := string_to_array(current_setting('app.allowed_streams', true), ',');
|
|
28
|
+
IF allowed_streams IS NULL OR NOT (p_stream_name = ANY(allowed_streams)) THEN
|
|
29
|
+
RAISE EXCEPTION 'access denied: stream_name "%" not in allowed streams', p_stream_name
|
|
30
|
+
USING ERRCODE = '42501';
|
|
31
|
+
END IF;
|
|
32
|
+
`;
|
|
33
|
+
function getCreateProceduresSQL(schemaName) {
|
|
34
|
+
const engineTable = `${schemaName}.engine_streams`;
|
|
35
|
+
const workerTable = `${schemaName}.worker_streams`;
|
|
36
|
+
return [
|
|
37
|
+
// -- worker_dequeue --
|
|
38
|
+
`CREATE OR REPLACE FUNCTION ${schemaName}.worker_dequeue(
|
|
39
|
+
p_stream_name TEXT,
|
|
40
|
+
p_batch_size INT,
|
|
41
|
+
p_consumer_id TEXT,
|
|
42
|
+
p_reservation_timeout_sec INT DEFAULT 30
|
|
43
|
+
)
|
|
44
|
+
RETURNS TABLE (
|
|
45
|
+
id BIGINT,
|
|
46
|
+
message TEXT,
|
|
47
|
+
workflow_name TEXT,
|
|
48
|
+
max_retry_attempts INT,
|
|
49
|
+
backoff_coefficient NUMERIC,
|
|
50
|
+
maximum_interval_seconds INT,
|
|
51
|
+
retry_attempt INT
|
|
52
|
+
)
|
|
53
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
54
|
+
SET search_path = ${schemaName}, pg_temp
|
|
55
|
+
AS $$
|
|
56
|
+
${STREAM_ACCESS_CHECK}
|
|
57
|
+
RETURN QUERY
|
|
58
|
+
UPDATE ${workerTable} ws
|
|
59
|
+
SET reserved_at = NOW(), reserved_by = p_consumer_id
|
|
60
|
+
WHERE ws.id IN (
|
|
61
|
+
SELECT ws2.id FROM ${workerTable} ws2
|
|
62
|
+
WHERE ws2.stream_name = p_stream_name
|
|
63
|
+
AND (ws2.reserved_at IS NULL OR ws2.reserved_at < NOW() - (p_reservation_timeout_sec || ' seconds')::INTERVAL)
|
|
64
|
+
AND ws2.expired_at IS NULL
|
|
65
|
+
AND ws2.visible_at <= NOW()
|
|
66
|
+
ORDER BY ws2.id
|
|
67
|
+
LIMIT p_batch_size
|
|
68
|
+
FOR UPDATE SKIP LOCKED
|
|
69
|
+
)
|
|
70
|
+
RETURNING ws.id, ws.message, ws.workflow_name, ws.max_retry_attempts,
|
|
71
|
+
ws.backoff_coefficient, ws.maximum_interval_seconds, ws.retry_attempt;
|
|
72
|
+
END;
|
|
73
|
+
$$;`,
|
|
74
|
+
// -- worker_ack --
|
|
75
|
+
`CREATE OR REPLACE FUNCTION ${schemaName}.worker_ack(
|
|
76
|
+
p_stream_name TEXT,
|
|
77
|
+
p_message_ids BIGINT[]
|
|
78
|
+
)
|
|
79
|
+
RETURNS INT
|
|
80
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
81
|
+
SET search_path = ${schemaName}, pg_temp
|
|
82
|
+
AS $$
|
|
83
|
+
DECLARE
|
|
84
|
+
affected INT;
|
|
85
|
+
${STREAM_ACCESS_CHECK}
|
|
86
|
+
UPDATE ${workerTable}
|
|
87
|
+
SET expired_at = NOW()
|
|
88
|
+
WHERE stream_name = p_stream_name AND id = ANY(p_message_ids);
|
|
89
|
+
GET DIAGNOSTICS affected = ROW_COUNT;
|
|
90
|
+
RETURN affected;
|
|
91
|
+
END;
|
|
92
|
+
$$;`,
|
|
93
|
+
// -- worker_dead_letter --
|
|
94
|
+
`CREATE OR REPLACE FUNCTION ${schemaName}.worker_dead_letter(
|
|
95
|
+
p_stream_name TEXT,
|
|
96
|
+
p_message_ids BIGINT[]
|
|
97
|
+
)
|
|
98
|
+
RETURNS INT
|
|
99
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
100
|
+
SET search_path = ${schemaName}, pg_temp
|
|
101
|
+
AS $$
|
|
102
|
+
DECLARE
|
|
103
|
+
affected INT;
|
|
104
|
+
${STREAM_ACCESS_CHECK}
|
|
105
|
+
UPDATE ${workerTable}
|
|
106
|
+
SET dead_lettered_at = NOW(), expired_at = NOW()
|
|
107
|
+
WHERE stream_name = p_stream_name AND id = ANY(p_message_ids);
|
|
108
|
+
GET DIAGNOSTICS affected = ROW_COUNT;
|
|
109
|
+
RETURN affected;
|
|
110
|
+
END;
|
|
111
|
+
$$;`,
|
|
112
|
+
// -- worker_respond --
|
|
113
|
+
// Inserts into engine_streams. The engine stream_name is the appId
|
|
114
|
+
// (hardcoded from the schema name, not caller-controlled).
|
|
115
|
+
// NOTE: Does NOT use STREAM_ACCESS_CHECK because p_stream_name is
|
|
116
|
+
// not the target — the engine stream is always hardcoded. Instead,
|
|
117
|
+
// we verify the caller has at least one allowed stream (i.e., is a
|
|
118
|
+
// legitimate provisioned worker).
|
|
119
|
+
`CREATE OR REPLACE FUNCTION ${schemaName}.worker_respond(
|
|
120
|
+
p_stream_name TEXT,
|
|
121
|
+
p_message TEXT,
|
|
122
|
+
p_max_retry_attempts INT DEFAULT NULL,
|
|
123
|
+
p_backoff_coefficient NUMERIC DEFAULT NULL,
|
|
124
|
+
p_maximum_interval_seconds INT DEFAULT NULL,
|
|
125
|
+
p_visible_at TIMESTAMPTZ DEFAULT NOW(),
|
|
126
|
+
p_retry_attempt INT DEFAULT 0
|
|
127
|
+
)
|
|
128
|
+
RETURNS BIGINT
|
|
129
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
130
|
+
SET search_path = ${schemaName}, pg_temp
|
|
131
|
+
AS $$
|
|
132
|
+
DECLARE
|
|
133
|
+
new_id BIGINT;
|
|
134
|
+
engine_stream_name TEXT;
|
|
135
|
+
allowed_streams TEXT[];
|
|
136
|
+
BEGIN
|
|
137
|
+
allowed_streams := string_to_array(current_setting('app.allowed_streams', true), ',');
|
|
138
|
+
IF allowed_streams IS NULL OR array_length(allowed_streams, 1) IS NULL THEN
|
|
139
|
+
RAISE EXCEPTION 'access denied: caller has no allowed streams'
|
|
140
|
+
USING ERRCODE = '42501';
|
|
141
|
+
END IF;
|
|
142
|
+
-- Engine stream_name is the schema/appId name
|
|
143
|
+
engine_stream_name := '${schemaName}';
|
|
144
|
+
|
|
145
|
+
IF p_max_retry_attempts IS NOT NULL THEN
|
|
146
|
+
INSERT INTO ${engineTable}
|
|
147
|
+
(stream_name, message, max_retry_attempts, backoff_coefficient, maximum_interval_seconds, visible_at, retry_attempt)
|
|
148
|
+
VALUES
|
|
149
|
+
(engine_stream_name, p_message, p_max_retry_attempts, p_backoff_coefficient, p_maximum_interval_seconds, p_visible_at, p_retry_attempt)
|
|
150
|
+
RETURNING id INTO new_id;
|
|
151
|
+
ELSE
|
|
152
|
+
INSERT INTO ${engineTable}
|
|
153
|
+
(stream_name, message, visible_at, retry_attempt)
|
|
154
|
+
VALUES
|
|
155
|
+
(engine_stream_name, p_message, p_visible_at, p_retry_attempt)
|
|
156
|
+
RETURNING id INTO new_id;
|
|
157
|
+
END IF;
|
|
158
|
+
|
|
159
|
+
RETURN new_id;
|
|
160
|
+
END;
|
|
161
|
+
$$;`,
|
|
162
|
+
// -- worker_listen --
|
|
163
|
+
`CREATE OR REPLACE FUNCTION ${schemaName}.worker_listen(
|
|
164
|
+
p_stream_name TEXT
|
|
165
|
+
)
|
|
166
|
+
RETURNS VOID
|
|
167
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
168
|
+
SET search_path = ${schemaName}, pg_temp
|
|
169
|
+
AS $$
|
|
170
|
+
DECLARE
|
|
171
|
+
channel_name TEXT;
|
|
172
|
+
${STREAM_ACCESS_CHECK}
|
|
173
|
+
channel_name := 'wrk_' || p_stream_name;
|
|
174
|
+
IF length(channel_name) > 63 THEN
|
|
175
|
+
channel_name := left(channel_name, 63);
|
|
176
|
+
END IF;
|
|
177
|
+
EXECUTE format('LISTEN %I', channel_name);
|
|
178
|
+
END;
|
|
179
|
+
$$;`,
|
|
180
|
+
// -- worker_unlisten --
|
|
181
|
+
`CREATE OR REPLACE FUNCTION ${schemaName}.worker_unlisten(
|
|
182
|
+
p_stream_name TEXT
|
|
183
|
+
)
|
|
184
|
+
RETURNS VOID
|
|
185
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
186
|
+
SET search_path = ${schemaName}, pg_temp
|
|
187
|
+
AS $$
|
|
188
|
+
DECLARE
|
|
189
|
+
channel_name TEXT;
|
|
190
|
+
${STREAM_ACCESS_CHECK}
|
|
191
|
+
channel_name := 'wrk_' || p_stream_name;
|
|
192
|
+
IF length(channel_name) > 63 THEN
|
|
193
|
+
channel_name := left(channel_name, 63);
|
|
194
|
+
END IF;
|
|
195
|
+
EXECUTE format('UNLISTEN %I', channel_name);
|
|
196
|
+
END;
|
|
197
|
+
$$;`,
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
exports.getCreateProceduresSQL = getCreateProceduresSQL;
|
|
201
|
+
function getCreateWorkerCredentialsTableSQL(schemaName) {
|
|
202
|
+
return `
|
|
203
|
+
CREATE TABLE IF NOT EXISTS ${schemaName}.worker_credentials (
|
|
204
|
+
id SERIAL PRIMARY KEY,
|
|
205
|
+
role_name TEXT NOT NULL UNIQUE,
|
|
206
|
+
stream_names TEXT[] NOT NULL,
|
|
207
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
208
|
+
revoked_at TIMESTAMPTZ,
|
|
209
|
+
last_rotated_at TIMESTAMPTZ DEFAULT NOW()
|
|
210
|
+
);
|
|
211
|
+
`;
|
|
212
|
+
}
|
|
213
|
+
exports.getCreateWorkerCredentialsTableSQL = getCreateWorkerCredentialsTableSQL;
|