@c4t4/heyamigo 0.9.0 → 0.9.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.
- package/dist/channels/adapter.js +24 -0
- package/dist/channels/baileys.js +158 -0
- package/dist/channels/index.js +16 -0
- package/dist/config.js +4 -0
- package/dist/db/identity-sync.js +147 -0
- package/dist/db/schema.js +45 -1
- package/dist/gateway/outgoing.js +127 -177
- package/dist/index.js +13 -2
- package/dist/queue/outbound-postsend.js +77 -0
- package/dist/queue/outbound.js +185 -0
- package/dist/queue/sender-worker.js +199 -0
- package/migrations/0001_phase1_outbound.sql +23 -0
- package/migrations/meta/0001_snapshot.json +377 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Sender worker. Drains the outbound queue and pushes each row to the
|
|
2
|
+
// matching channel adapter. One per process (no concurrency) so
|
|
3
|
+
// per-address ordering is preserved naturally and rate-limiting lives
|
|
4
|
+
// in one place.
|
|
5
|
+
import { hostname } from 'os';
|
|
6
|
+
import { resolve } from 'path';
|
|
7
|
+
import { eq } from 'drizzle-orm';
|
|
8
|
+
import { config } from '../config.js';
|
|
9
|
+
import { getDb } from '../db/index.js';
|
|
10
|
+
import { parseAddress } from '../db/address.js';
|
|
11
|
+
import { workers } from '../db/schema.js';
|
|
12
|
+
import { getChannelAdapter, PermanentChannelError, TransientChannelError, } from '../channels/index.js';
|
|
13
|
+
import { logger } from '../logger.js';
|
|
14
|
+
import { claimNextOutbound, markOutboundDone, markOutboundFailed, markOutboundRetryOrDlq, } from './outbound.js';
|
|
15
|
+
import { afterSend } from './outbound-postsend.js';
|
|
16
|
+
const HEARTBEAT_INTERVAL_MS = 5_000;
|
|
17
|
+
const IDLE_POLL_INTERVAL_MS = 500; // when queue empty
|
|
18
|
+
const BUSY_POLL_INTERVAL_MS = 50; // immediately fetch next after a successful send
|
|
19
|
+
let workerId = null;
|
|
20
|
+
let stopping = false;
|
|
21
|
+
let heartbeatTimer = null;
|
|
22
|
+
function newWorkerId() {
|
|
23
|
+
return `${hostname()}-${process.pid}-sender-0`;
|
|
24
|
+
}
|
|
25
|
+
function registerWorker(id) {
|
|
26
|
+
const db = getDb();
|
|
27
|
+
const now = Math.floor(Date.now() / 1000);
|
|
28
|
+
db.insert(workers)
|
|
29
|
+
.values({
|
|
30
|
+
id,
|
|
31
|
+
kind: 'sender',
|
|
32
|
+
status: 'idle',
|
|
33
|
+
currentJob: null,
|
|
34
|
+
lastSeen: now,
|
|
35
|
+
startedAt: now,
|
|
36
|
+
})
|
|
37
|
+
.onConflictDoUpdate({
|
|
38
|
+
target: workers.id,
|
|
39
|
+
set: { status: 'idle', currentJob: null, lastSeen: now, startedAt: now },
|
|
40
|
+
})
|
|
41
|
+
.run();
|
|
42
|
+
}
|
|
43
|
+
function setWorkerStatus(id, status, currentJob = null) {
|
|
44
|
+
const db = getDb();
|
|
45
|
+
db.update(workers)
|
|
46
|
+
.set({
|
|
47
|
+
status,
|
|
48
|
+
currentJob,
|
|
49
|
+
lastSeen: Math.floor(Date.now() / 1000),
|
|
50
|
+
})
|
|
51
|
+
.where(eq(workers.id, id))
|
|
52
|
+
.run();
|
|
53
|
+
}
|
|
54
|
+
function heartbeat(id) {
|
|
55
|
+
const db = getDb();
|
|
56
|
+
db.update(workers)
|
|
57
|
+
.set({ lastSeen: Math.floor(Date.now() / 1000) })
|
|
58
|
+
.where(eq(workers.id, id))
|
|
59
|
+
.run();
|
|
60
|
+
}
|
|
61
|
+
// Translate an outbound row into the channel-agnostic message shape.
|
|
62
|
+
// Media paths are stored relative to the project root in the row;
|
|
63
|
+
// resolved to absolute here so the adapter can readFileSync directly.
|
|
64
|
+
function rowToMessage(row) {
|
|
65
|
+
return {
|
|
66
|
+
kind: row.kind,
|
|
67
|
+
text: row.text ?? undefined,
|
|
68
|
+
mediaPath: row.mediaPath ? resolve(process.cwd(), row.mediaPath) : undefined,
|
|
69
|
+
mediaMime: row.mediaMime ?? undefined,
|
|
70
|
+
quoteMsgId: row.quoteMsgId ?? undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Enforce media-size cap. mediaBytes is stored on the row by the
|
|
74
|
+
// producer; if missing, we trust the channel to enforce its own limit.
|
|
75
|
+
function tooLarge(row) {
|
|
76
|
+
const cap = config.reply.maxOutboundMediaBytes ?? null;
|
|
77
|
+
if (cap === null)
|
|
78
|
+
return null;
|
|
79
|
+
if (row.mediaBytes !== null && row.mediaBytes > cap) {
|
|
80
|
+
return `media too large: ${row.mediaBytes} > ${cap} bytes`;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
async function processOne(id, row) {
|
|
85
|
+
setWorkerStatus(id, 'busy', `outbound:${row.id}`);
|
|
86
|
+
// Size cap → permanent fail; no point retrying same payload.
|
|
87
|
+
const sizeError = tooLarge(row);
|
|
88
|
+
if (sizeError) {
|
|
89
|
+
markOutboundFailed(row.id, id, sizeError);
|
|
90
|
+
logger.warn({ id: row.id, address: row.address }, sizeError);
|
|
91
|
+
setWorkerStatus(id, 'idle');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let address;
|
|
95
|
+
try {
|
|
96
|
+
address = parseAddress(row.address);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
markOutboundFailed(row.id, id, `bad address: ${row.address}`);
|
|
100
|
+
logger.warn({ id: row.id, err }, 'outbound row has unparseable address');
|
|
101
|
+
setWorkerStatus(id, 'idle');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// system:* addresses are bot-internal; not real channels. Drop them
|
|
105
|
+
// with a friendly log so future cron-emitted system messages don't
|
|
106
|
+
// accidentally try to "send" anywhere.
|
|
107
|
+
if (address.channel === 'system') {
|
|
108
|
+
markOutboundFailed(row.id, id, 'system addresses are not sendable');
|
|
109
|
+
logger.warn({ id: row.id, address: row.address }, 'system address routed to sender');
|
|
110
|
+
setWorkerStatus(id, 'idle');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
let adapter;
|
|
114
|
+
try {
|
|
115
|
+
adapter = getChannelAdapter(address.channel);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
markOutboundFailed(row.id, id, err.message);
|
|
119
|
+
logger.error({ id: row.id, channel: address.channel }, 'no adapter for channel');
|
|
120
|
+
setWorkerStatus(id, 'idle');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const result = await adapter.send(address.externalId, rowToMessage(row));
|
|
125
|
+
const ok = markOutboundDone(row.id, id);
|
|
126
|
+
if (!ok) {
|
|
127
|
+
// Lost the claim — orchestrator reclaimed it as stuck, or
|
|
128
|
+
// status changed under us. The send already happened though
|
|
129
|
+
// (channel returned a msg_id), so we just log and move on. The
|
|
130
|
+
// reclaimed copy may re-send → that's why idempotency_key
|
|
131
|
+
// matters at the producer side.
|
|
132
|
+
logger.warn({ id: row.id, msgId: result.msgId }, 'outbound sent but markDone failed (claim lost?)');
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
await afterSend(row, result.msgId).catch((err) => {
|
|
136
|
+
logger.error({ err, id: row.id }, 'afterSend hook failed');
|
|
137
|
+
});
|
|
138
|
+
logger.info({ id: row.id, address: row.address, kind: row.kind, msgId: result.msgId }, 'outbound sent');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (err instanceof PermanentChannelError) {
|
|
143
|
+
markOutboundFailed(row.id, id, err.message);
|
|
144
|
+
logger.error({ id: row.id, err: err.message }, 'outbound permanent failure');
|
|
145
|
+
}
|
|
146
|
+
else if (err instanceof TransientChannelError) {
|
|
147
|
+
const result = markOutboundRetryOrDlq(row.id, id, err.message);
|
|
148
|
+
if (result.deadLettered) {
|
|
149
|
+
logger.error({ id: row.id }, 'outbound dead-lettered after max attempts');
|
|
150
|
+
}
|
|
151
|
+
else if (result.retried) {
|
|
152
|
+
logger.warn({ id: row.id, err: err.message }, 'outbound transient fail, will retry');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Unexpected throw — treat as transient (safer to retry than to
|
|
157
|
+
// give up on something we don't understand).
|
|
158
|
+
const result = markOutboundRetryOrDlq(row.id, id, `unexpected error: ${err.message}`);
|
|
159
|
+
logger.error({ id: row.id, err, retried: result.retried, dlq: result.deadLettered }, 'outbound unexpected error');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
setWorkerStatus(id, 'idle');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function loop(id) {
|
|
167
|
+
while (!stopping) {
|
|
168
|
+
let processed = false;
|
|
169
|
+
try {
|
|
170
|
+
const row = claimNextOutbound(id);
|
|
171
|
+
if (row) {
|
|
172
|
+
await processOne(id, row);
|
|
173
|
+
processed = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
logger.error({ err }, 'sender worker loop error');
|
|
178
|
+
}
|
|
179
|
+
const delay = processed ? BUSY_POLL_INTERVAL_MS : IDLE_POLL_INTERVAL_MS;
|
|
180
|
+
await new Promise((res) => setTimeout(res, delay));
|
|
181
|
+
}
|
|
182
|
+
setWorkerStatus(id, 'dead');
|
|
183
|
+
}
|
|
184
|
+
export function startSenderWorker() {
|
|
185
|
+
if (workerId) {
|
|
186
|
+
logger.warn('sender worker already started; ignoring');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
workerId = newWorkerId();
|
|
190
|
+
registerWorker(workerId);
|
|
191
|
+
heartbeatTimer = setInterval(() => workerId && heartbeat(workerId), HEARTBEAT_INTERVAL_MS);
|
|
192
|
+
void loop(workerId).catch((err) => logger.fatal({ err }, 'sender worker loop crashed'));
|
|
193
|
+
logger.info({ workerId }, 'sender worker started');
|
|
194
|
+
}
|
|
195
|
+
export function stopSenderWorker() {
|
|
196
|
+
stopping = true;
|
|
197
|
+
if (heartbeatTimer)
|
|
198
|
+
clearInterval(heartbeatTimer);
|
|
199
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
CREATE TABLE `outbound` (
|
|
2
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
3
|
+
`address` text NOT NULL,
|
|
4
|
+
`kind` text NOT NULL,
|
|
5
|
+
`text` text,
|
|
6
|
+
`media_path` text,
|
|
7
|
+
`media_mime` text,
|
|
8
|
+
`media_bytes` integer,
|
|
9
|
+
`quote_msg_id` text,
|
|
10
|
+
`idempotency_key` text,
|
|
11
|
+
`status` text NOT NULL,
|
|
12
|
+
`attempts` integer DEFAULT 0 NOT NULL,
|
|
13
|
+
`next_attempt_at` integer,
|
|
14
|
+
`last_error` text,
|
|
15
|
+
`claimed_by` text,
|
|
16
|
+
`claimed_at` integer,
|
|
17
|
+
`created_at` integer NOT NULL,
|
|
18
|
+
`updated_at` integer NOT NULL
|
|
19
|
+
);
|
|
20
|
+
--> statement-breakpoint
|
|
21
|
+
CREATE INDEX `outbound_by_status_next` ON `outbound` (`status`,`next_attempt_at`);--> statement-breakpoint
|
|
22
|
+
CREATE INDEX `outbound_by_address` ON `outbound` (`address`);--> statement-breakpoint
|
|
23
|
+
CREATE UNIQUE INDEX `outbound_idempotency_key_uq` ON `outbound` (`idempotency_key`) WHERE "outbound"."idempotency_key" IS NOT NULL;
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "6",
|
|
3
|
+
"dialect": "sqlite",
|
|
4
|
+
"id": "56b9231e-919c-4b8d-9a77-68735c33cbb0",
|
|
5
|
+
"prevId": "16f29db1-6a21-41fb-b7e2-933ea7384dad",
|
|
6
|
+
"tables": {
|
|
7
|
+
"control": {
|
|
8
|
+
"name": "control",
|
|
9
|
+
"columns": {
|
|
10
|
+
"key": {
|
|
11
|
+
"name": "key",
|
|
12
|
+
"type": "text",
|
|
13
|
+
"primaryKey": true,
|
|
14
|
+
"notNull": true,
|
|
15
|
+
"autoincrement": false
|
|
16
|
+
},
|
|
17
|
+
"value": {
|
|
18
|
+
"name": "value",
|
|
19
|
+
"type": "text",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": false,
|
|
22
|
+
"autoincrement": false
|
|
23
|
+
},
|
|
24
|
+
"requested_by": {
|
|
25
|
+
"name": "requested_by",
|
|
26
|
+
"type": "text",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": false,
|
|
29
|
+
"autoincrement": false
|
|
30
|
+
},
|
|
31
|
+
"requested_at": {
|
|
32
|
+
"name": "requested_at",
|
|
33
|
+
"type": "integer",
|
|
34
|
+
"primaryKey": false,
|
|
35
|
+
"notNull": true,
|
|
36
|
+
"autoincrement": false
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"indexes": {},
|
|
40
|
+
"foreignKeys": {},
|
|
41
|
+
"compositePrimaryKeys": {},
|
|
42
|
+
"uniqueConstraints": {},
|
|
43
|
+
"checkConstraints": {}
|
|
44
|
+
},
|
|
45
|
+
"identities": {
|
|
46
|
+
"name": "identities",
|
|
47
|
+
"columns": {
|
|
48
|
+
"person_id": {
|
|
49
|
+
"name": "person_id",
|
|
50
|
+
"type": "text",
|
|
51
|
+
"primaryKey": false,
|
|
52
|
+
"notNull": true,
|
|
53
|
+
"autoincrement": false
|
|
54
|
+
},
|
|
55
|
+
"address": {
|
|
56
|
+
"name": "address",
|
|
57
|
+
"type": "text",
|
|
58
|
+
"primaryKey": false,
|
|
59
|
+
"notNull": true,
|
|
60
|
+
"autoincrement": false
|
|
61
|
+
},
|
|
62
|
+
"added_at": {
|
|
63
|
+
"name": "added_at",
|
|
64
|
+
"type": "integer",
|
|
65
|
+
"primaryKey": false,
|
|
66
|
+
"notNull": true,
|
|
67
|
+
"autoincrement": false
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"indexes": {
|
|
71
|
+
"identities_address_unique": {
|
|
72
|
+
"name": "identities_address_unique",
|
|
73
|
+
"columns": [
|
|
74
|
+
"address"
|
|
75
|
+
],
|
|
76
|
+
"isUnique": true
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"foreignKeys": {
|
|
80
|
+
"identities_person_id_persons_id_fk": {
|
|
81
|
+
"name": "identities_person_id_persons_id_fk",
|
|
82
|
+
"tableFrom": "identities",
|
|
83
|
+
"tableTo": "persons",
|
|
84
|
+
"columnsFrom": [
|
|
85
|
+
"person_id"
|
|
86
|
+
],
|
|
87
|
+
"columnsTo": [
|
|
88
|
+
"id"
|
|
89
|
+
],
|
|
90
|
+
"onDelete": "no action",
|
|
91
|
+
"onUpdate": "no action"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"compositePrimaryKeys": {
|
|
95
|
+
"identities_person_id_address_pk": {
|
|
96
|
+
"columns": [
|
|
97
|
+
"person_id",
|
|
98
|
+
"address"
|
|
99
|
+
],
|
|
100
|
+
"name": "identities_person_id_address_pk"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"uniqueConstraints": {},
|
|
104
|
+
"checkConstraints": {}
|
|
105
|
+
},
|
|
106
|
+
"outbound": {
|
|
107
|
+
"name": "outbound",
|
|
108
|
+
"columns": {
|
|
109
|
+
"id": {
|
|
110
|
+
"name": "id",
|
|
111
|
+
"type": "integer",
|
|
112
|
+
"primaryKey": true,
|
|
113
|
+
"notNull": true,
|
|
114
|
+
"autoincrement": true
|
|
115
|
+
},
|
|
116
|
+
"address": {
|
|
117
|
+
"name": "address",
|
|
118
|
+
"type": "text",
|
|
119
|
+
"primaryKey": false,
|
|
120
|
+
"notNull": true,
|
|
121
|
+
"autoincrement": false
|
|
122
|
+
},
|
|
123
|
+
"kind": {
|
|
124
|
+
"name": "kind",
|
|
125
|
+
"type": "text",
|
|
126
|
+
"primaryKey": false,
|
|
127
|
+
"notNull": true,
|
|
128
|
+
"autoincrement": false
|
|
129
|
+
},
|
|
130
|
+
"text": {
|
|
131
|
+
"name": "text",
|
|
132
|
+
"type": "text",
|
|
133
|
+
"primaryKey": false,
|
|
134
|
+
"notNull": false,
|
|
135
|
+
"autoincrement": false
|
|
136
|
+
},
|
|
137
|
+
"media_path": {
|
|
138
|
+
"name": "media_path",
|
|
139
|
+
"type": "text",
|
|
140
|
+
"primaryKey": false,
|
|
141
|
+
"notNull": false,
|
|
142
|
+
"autoincrement": false
|
|
143
|
+
},
|
|
144
|
+
"media_mime": {
|
|
145
|
+
"name": "media_mime",
|
|
146
|
+
"type": "text",
|
|
147
|
+
"primaryKey": false,
|
|
148
|
+
"notNull": false,
|
|
149
|
+
"autoincrement": false
|
|
150
|
+
},
|
|
151
|
+
"media_bytes": {
|
|
152
|
+
"name": "media_bytes",
|
|
153
|
+
"type": "integer",
|
|
154
|
+
"primaryKey": false,
|
|
155
|
+
"notNull": false,
|
|
156
|
+
"autoincrement": false
|
|
157
|
+
},
|
|
158
|
+
"quote_msg_id": {
|
|
159
|
+
"name": "quote_msg_id",
|
|
160
|
+
"type": "text",
|
|
161
|
+
"primaryKey": false,
|
|
162
|
+
"notNull": false,
|
|
163
|
+
"autoincrement": false
|
|
164
|
+
},
|
|
165
|
+
"idempotency_key": {
|
|
166
|
+
"name": "idempotency_key",
|
|
167
|
+
"type": "text",
|
|
168
|
+
"primaryKey": false,
|
|
169
|
+
"notNull": false,
|
|
170
|
+
"autoincrement": false
|
|
171
|
+
},
|
|
172
|
+
"status": {
|
|
173
|
+
"name": "status",
|
|
174
|
+
"type": "text",
|
|
175
|
+
"primaryKey": false,
|
|
176
|
+
"notNull": true,
|
|
177
|
+
"autoincrement": false
|
|
178
|
+
},
|
|
179
|
+
"attempts": {
|
|
180
|
+
"name": "attempts",
|
|
181
|
+
"type": "integer",
|
|
182
|
+
"primaryKey": false,
|
|
183
|
+
"notNull": true,
|
|
184
|
+
"autoincrement": false,
|
|
185
|
+
"default": 0
|
|
186
|
+
},
|
|
187
|
+
"next_attempt_at": {
|
|
188
|
+
"name": "next_attempt_at",
|
|
189
|
+
"type": "integer",
|
|
190
|
+
"primaryKey": false,
|
|
191
|
+
"notNull": false,
|
|
192
|
+
"autoincrement": false
|
|
193
|
+
},
|
|
194
|
+
"last_error": {
|
|
195
|
+
"name": "last_error",
|
|
196
|
+
"type": "text",
|
|
197
|
+
"primaryKey": false,
|
|
198
|
+
"notNull": false,
|
|
199
|
+
"autoincrement": false
|
|
200
|
+
},
|
|
201
|
+
"claimed_by": {
|
|
202
|
+
"name": "claimed_by",
|
|
203
|
+
"type": "text",
|
|
204
|
+
"primaryKey": false,
|
|
205
|
+
"notNull": false,
|
|
206
|
+
"autoincrement": false
|
|
207
|
+
},
|
|
208
|
+
"claimed_at": {
|
|
209
|
+
"name": "claimed_at",
|
|
210
|
+
"type": "integer",
|
|
211
|
+
"primaryKey": false,
|
|
212
|
+
"notNull": false,
|
|
213
|
+
"autoincrement": false
|
|
214
|
+
},
|
|
215
|
+
"created_at": {
|
|
216
|
+
"name": "created_at",
|
|
217
|
+
"type": "integer",
|
|
218
|
+
"primaryKey": false,
|
|
219
|
+
"notNull": true,
|
|
220
|
+
"autoincrement": false
|
|
221
|
+
},
|
|
222
|
+
"updated_at": {
|
|
223
|
+
"name": "updated_at",
|
|
224
|
+
"type": "integer",
|
|
225
|
+
"primaryKey": false,
|
|
226
|
+
"notNull": true,
|
|
227
|
+
"autoincrement": false
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
"indexes": {
|
|
231
|
+
"outbound_by_status_next": {
|
|
232
|
+
"name": "outbound_by_status_next",
|
|
233
|
+
"columns": [
|
|
234
|
+
"status",
|
|
235
|
+
"next_attempt_at"
|
|
236
|
+
],
|
|
237
|
+
"isUnique": false
|
|
238
|
+
},
|
|
239
|
+
"outbound_by_address": {
|
|
240
|
+
"name": "outbound_by_address",
|
|
241
|
+
"columns": [
|
|
242
|
+
"address"
|
|
243
|
+
],
|
|
244
|
+
"isUnique": false
|
|
245
|
+
},
|
|
246
|
+
"outbound_idempotency_key_uq": {
|
|
247
|
+
"name": "outbound_idempotency_key_uq",
|
|
248
|
+
"columns": [
|
|
249
|
+
"idempotency_key"
|
|
250
|
+
],
|
|
251
|
+
"isUnique": true,
|
|
252
|
+
"where": "\"outbound\".\"idempotency_key\" IS NOT NULL"
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
"foreignKeys": {},
|
|
256
|
+
"compositePrimaryKeys": {},
|
|
257
|
+
"uniqueConstraints": {},
|
|
258
|
+
"checkConstraints": {}
|
|
259
|
+
},
|
|
260
|
+
"persons": {
|
|
261
|
+
"name": "persons",
|
|
262
|
+
"columns": {
|
|
263
|
+
"id": {
|
|
264
|
+
"name": "id",
|
|
265
|
+
"type": "text",
|
|
266
|
+
"primaryKey": true,
|
|
267
|
+
"notNull": true,
|
|
268
|
+
"autoincrement": false
|
|
269
|
+
},
|
|
270
|
+
"display_name": {
|
|
271
|
+
"name": "display_name",
|
|
272
|
+
"type": "text",
|
|
273
|
+
"primaryKey": false,
|
|
274
|
+
"notNull": false,
|
|
275
|
+
"autoincrement": false
|
|
276
|
+
},
|
|
277
|
+
"timezone": {
|
|
278
|
+
"name": "timezone",
|
|
279
|
+
"type": "text",
|
|
280
|
+
"primaryKey": false,
|
|
281
|
+
"notNull": false,
|
|
282
|
+
"autoincrement": false
|
|
283
|
+
},
|
|
284
|
+
"created_at": {
|
|
285
|
+
"name": "created_at",
|
|
286
|
+
"type": "integer",
|
|
287
|
+
"primaryKey": false,
|
|
288
|
+
"notNull": true,
|
|
289
|
+
"autoincrement": false
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
"indexes": {},
|
|
293
|
+
"foreignKeys": {},
|
|
294
|
+
"compositePrimaryKeys": {},
|
|
295
|
+
"uniqueConstraints": {},
|
|
296
|
+
"checkConstraints": {}
|
|
297
|
+
},
|
|
298
|
+
"workers": {
|
|
299
|
+
"name": "workers",
|
|
300
|
+
"columns": {
|
|
301
|
+
"id": {
|
|
302
|
+
"name": "id",
|
|
303
|
+
"type": "text",
|
|
304
|
+
"primaryKey": true,
|
|
305
|
+
"notNull": true,
|
|
306
|
+
"autoincrement": false
|
|
307
|
+
},
|
|
308
|
+
"kind": {
|
|
309
|
+
"name": "kind",
|
|
310
|
+
"type": "text",
|
|
311
|
+
"primaryKey": false,
|
|
312
|
+
"notNull": true,
|
|
313
|
+
"autoincrement": false
|
|
314
|
+
},
|
|
315
|
+
"status": {
|
|
316
|
+
"name": "status",
|
|
317
|
+
"type": "text",
|
|
318
|
+
"primaryKey": false,
|
|
319
|
+
"notNull": true,
|
|
320
|
+
"autoincrement": false
|
|
321
|
+
},
|
|
322
|
+
"current_job": {
|
|
323
|
+
"name": "current_job",
|
|
324
|
+
"type": "text",
|
|
325
|
+
"primaryKey": false,
|
|
326
|
+
"notNull": false,
|
|
327
|
+
"autoincrement": false
|
|
328
|
+
},
|
|
329
|
+
"last_seen": {
|
|
330
|
+
"name": "last_seen",
|
|
331
|
+
"type": "integer",
|
|
332
|
+
"primaryKey": false,
|
|
333
|
+
"notNull": true,
|
|
334
|
+
"autoincrement": false
|
|
335
|
+
},
|
|
336
|
+
"started_at": {
|
|
337
|
+
"name": "started_at",
|
|
338
|
+
"type": "integer",
|
|
339
|
+
"primaryKey": false,
|
|
340
|
+
"notNull": true,
|
|
341
|
+
"autoincrement": false
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
"indexes": {
|
|
345
|
+
"workers_by_kind_status": {
|
|
346
|
+
"name": "workers_by_kind_status",
|
|
347
|
+
"columns": [
|
|
348
|
+
"kind",
|
|
349
|
+
"status"
|
|
350
|
+
],
|
|
351
|
+
"isUnique": false
|
|
352
|
+
},
|
|
353
|
+
"workers_by_last_seen": {
|
|
354
|
+
"name": "workers_by_last_seen",
|
|
355
|
+
"columns": [
|
|
356
|
+
"last_seen"
|
|
357
|
+
],
|
|
358
|
+
"isUnique": false
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
"foreignKeys": {},
|
|
362
|
+
"compositePrimaryKeys": {},
|
|
363
|
+
"uniqueConstraints": {},
|
|
364
|
+
"checkConstraints": {}
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
"views": {},
|
|
368
|
+
"enums": {},
|
|
369
|
+
"_meta": {
|
|
370
|
+
"schemas": {},
|
|
371
|
+
"tables": {},
|
|
372
|
+
"columns": {}
|
|
373
|
+
},
|
|
374
|
+
"internal": {
|
|
375
|
+
"indexes": {}
|
|
376
|
+
}
|
|
377
|
+
}
|