@rei-standard/amsg-server 1.1.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 +50 -0
- package/dist/chunk-M2CNZRRO.cjs +83 -0
- package/dist/chunk-YKLDHUZZ.mjs +83 -0
- package/dist/index-BxrBvKHy.d.ts +1547 -0
- package/dist/index-wQ6O1KrR.d.cts +1547 -0
- package/dist/index.cjs +980 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +980 -0
- package/dist/neon-BKBYTWB7.d.ts +253 -0
- package/dist/neon-CNUoZFv_.d.cts +253 -0
- package/dist/neon-FRQJDC3A.cjs +217 -0
- package/dist/neon-ZBESTDI5.mjs +217 -0
- package/dist/pg-B8JqNFRD.d.cts +248 -0
- package/dist/pg-Bnam-z8h.d.ts +248 -0
- package/dist/pg-PBITGIEU.cjs +210 -0
- package/dist/pg-QKWVA6NG.mjs +210 -0
- package/package.json +51 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
var _chunkM2CNZRROcjs = require('./chunk-M2CNZRRO.cjs');
|
|
7
|
+
|
|
8
|
+
// src/server/adapters/neon.js
|
|
9
|
+
var _serverless = require('@neondatabase/serverless');
|
|
10
|
+
var NeonAdapter = class {
|
|
11
|
+
/** @param {string} connectionString */
|
|
12
|
+
constructor(connectionString) {
|
|
13
|
+
this._connectionString = connectionString;
|
|
14
|
+
this._sql = null;
|
|
15
|
+
}
|
|
16
|
+
/** @private */
|
|
17
|
+
_getSql() {
|
|
18
|
+
if (!this._sql) {
|
|
19
|
+
this._sql = _serverless.neon.call(void 0, this._connectionString);
|
|
20
|
+
}
|
|
21
|
+
return this._sql;
|
|
22
|
+
}
|
|
23
|
+
async initSchema() {
|
|
24
|
+
const sql = this._getSql();
|
|
25
|
+
await sql.query(_chunkM2CNZRROcjs.TABLE_SQL);
|
|
26
|
+
const indexResults = [];
|
|
27
|
+
for (const index of _chunkM2CNZRROcjs.INDEXES) {
|
|
28
|
+
try {
|
|
29
|
+
await sql.query(index.sql);
|
|
30
|
+
indexResults.push({
|
|
31
|
+
name: index.name,
|
|
32
|
+
status: "success",
|
|
33
|
+
description: index.description,
|
|
34
|
+
critical: !!index.critical
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
indexResults.push({
|
|
38
|
+
name: index.name,
|
|
39
|
+
status: "failed",
|
|
40
|
+
description: index.description,
|
|
41
|
+
critical: !!index.critical,
|
|
42
|
+
error: error.message
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const criticalFailures = indexResults.filter((index) => index.critical && index.status === "failed");
|
|
47
|
+
if (criticalFailures.length > 0) {
|
|
48
|
+
const failedNames = criticalFailures.map((index) => index.name).join(", ");
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Critical index creation failed (${failedNames}). Please remove duplicate UUID rows and run initSchema again.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const tableCheck = await sql.query(_chunkM2CNZRROcjs.VERIFY_TABLE_SQL);
|
|
54
|
+
if (tableCheck.length === 0) {
|
|
55
|
+
throw new Error("Table creation verification failed");
|
|
56
|
+
}
|
|
57
|
+
const columns = await sql.query(_chunkM2CNZRROcjs.COLUMNS_SQL);
|
|
58
|
+
return {
|
|
59
|
+
columnsCreated: columns.length,
|
|
60
|
+
indexesCreated: indexResults.filter((r) => r.status === "success").length,
|
|
61
|
+
indexesFailed: indexResults.filter((r) => r.status === "failed").length,
|
|
62
|
+
columns: columns.map((c) => ({ name: c.column_name, type: c.data_type, nullable: c.is_nullable === "YES" })),
|
|
63
|
+
indexes: indexResults
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async dropSchema() {
|
|
67
|
+
const sql = this._getSql();
|
|
68
|
+
await sql.query("DROP TABLE IF EXISTS scheduled_messages CASCADE");
|
|
69
|
+
}
|
|
70
|
+
async createTask(params) {
|
|
71
|
+
const sql = this._getSql();
|
|
72
|
+
const rows = await sql.query(
|
|
73
|
+
`INSERT INTO scheduled_messages
|
|
74
|
+
(user_id, uuid, encrypted_payload, next_send_at, message_type, status, retry_count, created_at, updated_at)
|
|
75
|
+
VALUES ($1, $2, $3, $4, $5, 'pending', 0, NOW(), NOW())
|
|
76
|
+
RETURNING id, uuid, next_send_at, status, created_at`,
|
|
77
|
+
[params.user_id, params.uuid, params.encrypted_payload, params.next_send_at, params.message_type]
|
|
78
|
+
);
|
|
79
|
+
return rows[0] || null;
|
|
80
|
+
}
|
|
81
|
+
async getTaskByUuid(uuid, userId) {
|
|
82
|
+
const sql = this._getSql();
|
|
83
|
+
const rows = await sql.query(
|
|
84
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
85
|
+
FROM scheduled_messages
|
|
86
|
+
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
87
|
+
LIMIT 1`,
|
|
88
|
+
[uuid, userId]
|
|
89
|
+
);
|
|
90
|
+
return rows[0] || null;
|
|
91
|
+
}
|
|
92
|
+
async getTaskByUuidOnly(uuid) {
|
|
93
|
+
const sql = this._getSql();
|
|
94
|
+
const rows = await sql.query(
|
|
95
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
96
|
+
FROM scheduled_messages
|
|
97
|
+
WHERE uuid = $1 AND status = 'pending'
|
|
98
|
+
LIMIT 1`,
|
|
99
|
+
[uuid]
|
|
100
|
+
);
|
|
101
|
+
return rows[0] || null;
|
|
102
|
+
}
|
|
103
|
+
async updateTaskById(taskId, updates) {
|
|
104
|
+
const sql = this._getSql();
|
|
105
|
+
const sets = [];
|
|
106
|
+
const values = [];
|
|
107
|
+
let idx = 1;
|
|
108
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
109
|
+
sets.push(`${key} = $${idx}`);
|
|
110
|
+
values.push(value);
|
|
111
|
+
idx++;
|
|
112
|
+
}
|
|
113
|
+
sets.push("updated_at = NOW()");
|
|
114
|
+
values.push(taskId);
|
|
115
|
+
const rows = await sql.query(
|
|
116
|
+
`UPDATE scheduled_messages SET ${sets.join(", ")} WHERE id = $${idx} RETURNING *`,
|
|
117
|
+
values
|
|
118
|
+
);
|
|
119
|
+
return rows[0] || null;
|
|
120
|
+
}
|
|
121
|
+
async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields) {
|
|
122
|
+
const sql = this._getSql();
|
|
123
|
+
const sets = ["encrypted_payload = $1", "updated_at = NOW()"];
|
|
124
|
+
const values = [encryptedPayload];
|
|
125
|
+
let idx = 2;
|
|
126
|
+
if (extraFields) {
|
|
127
|
+
for (const [key, value] of Object.entries(extraFields)) {
|
|
128
|
+
sets.push(`${key} = $${idx}`);
|
|
129
|
+
values.push(value);
|
|
130
|
+
idx++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
values.push(uuid, userId);
|
|
134
|
+
const rows = await sql.query(
|
|
135
|
+
`UPDATE scheduled_messages SET ${sets.join(", ")}
|
|
136
|
+
WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'
|
|
137
|
+
RETURNING uuid, updated_at`,
|
|
138
|
+
values
|
|
139
|
+
);
|
|
140
|
+
return rows[0] || null;
|
|
141
|
+
}
|
|
142
|
+
async deleteTaskById(taskId) {
|
|
143
|
+
const sql = this._getSql();
|
|
144
|
+
const rows = await sql.query("DELETE FROM scheduled_messages WHERE id = $1 RETURNING id", [taskId]);
|
|
145
|
+
return rows.length > 0;
|
|
146
|
+
}
|
|
147
|
+
async deleteTaskByUuid(uuid, userId) {
|
|
148
|
+
const sql = this._getSql();
|
|
149
|
+
const rows = await sql.query(
|
|
150
|
+
"DELETE FROM scheduled_messages WHERE uuid = $1 AND user_id = $2 RETURNING id",
|
|
151
|
+
[uuid, userId]
|
|
152
|
+
);
|
|
153
|
+
return rows.length > 0;
|
|
154
|
+
}
|
|
155
|
+
async getPendingTasks(limit = 50) {
|
|
156
|
+
const sql = this._getSql();
|
|
157
|
+
return sql.query(
|
|
158
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
159
|
+
FROM scheduled_messages
|
|
160
|
+
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
161
|
+
ORDER BY next_send_at ASC
|
|
162
|
+
LIMIT $1`,
|
|
163
|
+
[limit]
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
async listTasks(userId, opts = {}) {
|
|
167
|
+
const sql = this._getSql();
|
|
168
|
+
const { status = "all", limit = 20, offset = 0 } = opts;
|
|
169
|
+
const conditions = ["user_id = $1"];
|
|
170
|
+
const params = [userId];
|
|
171
|
+
let idx = 2;
|
|
172
|
+
if (status !== "all") {
|
|
173
|
+
conditions.push(`status = $${idx}`);
|
|
174
|
+
params.push(status);
|
|
175
|
+
idx++;
|
|
176
|
+
}
|
|
177
|
+
const where = conditions.join(" AND ");
|
|
178
|
+
const countRows = await sql.query(
|
|
179
|
+
`SELECT COUNT(*) as count FROM scheduled_messages WHERE ${where}`,
|
|
180
|
+
params
|
|
181
|
+
);
|
|
182
|
+
const total = parseInt(countRows[0].count, 10);
|
|
183
|
+
const taskParams = [...params, limit, offset];
|
|
184
|
+
const tasks = await sql.query(
|
|
185
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
|
|
186
|
+
FROM scheduled_messages
|
|
187
|
+
WHERE ${where}
|
|
188
|
+
ORDER BY next_send_at ASC
|
|
189
|
+
LIMIT $${idx} OFFSET $${idx + 1}`,
|
|
190
|
+
taskParams
|
|
191
|
+
);
|
|
192
|
+
return { tasks, total };
|
|
193
|
+
}
|
|
194
|
+
async cleanupOldTasks(days = 7) {
|
|
195
|
+
const sql = this._getSql();
|
|
196
|
+
const safeDays = Math.max(1, Math.floor(Number(days)));
|
|
197
|
+
const rows = await sql.query(
|
|
198
|
+
`DELETE FROM scheduled_messages
|
|
199
|
+
WHERE status IN ('sent', 'failed')
|
|
200
|
+
AND updated_at < NOW() - make_interval(days => $1)
|
|
201
|
+
RETURNING id`,
|
|
202
|
+
[safeDays]
|
|
203
|
+
);
|
|
204
|
+
return rows.length;
|
|
205
|
+
}
|
|
206
|
+
async getTaskStatus(uuid, userId) {
|
|
207
|
+
const sql = this._getSql();
|
|
208
|
+
const rows = await sql.query(
|
|
209
|
+
"SELECT status FROM scheduled_messages WHERE uuid = $1 AND user_id = $2 LIMIT 1",
|
|
210
|
+
[uuid, userId]
|
|
211
|
+
);
|
|
212
|
+
return rows.length > 0 ? rows[0].status : null;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
exports.NeonAdapter = NeonAdapter;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COLUMNS_SQL,
|
|
3
|
+
INDEXES,
|
|
4
|
+
TABLE_SQL,
|
|
5
|
+
VERIFY_TABLE_SQL
|
|
6
|
+
} from "./chunk-YKLDHUZZ.mjs";
|
|
7
|
+
|
|
8
|
+
// src/server/adapters/neon.js
|
|
9
|
+
import { neon } from "@neondatabase/serverless";
|
|
10
|
+
var NeonAdapter = class {
|
|
11
|
+
/** @param {string} connectionString */
|
|
12
|
+
constructor(connectionString) {
|
|
13
|
+
this._connectionString = connectionString;
|
|
14
|
+
this._sql = null;
|
|
15
|
+
}
|
|
16
|
+
/** @private */
|
|
17
|
+
_getSql() {
|
|
18
|
+
if (!this._sql) {
|
|
19
|
+
this._sql = neon(this._connectionString);
|
|
20
|
+
}
|
|
21
|
+
return this._sql;
|
|
22
|
+
}
|
|
23
|
+
async initSchema() {
|
|
24
|
+
const sql = this._getSql();
|
|
25
|
+
await sql.query(TABLE_SQL);
|
|
26
|
+
const indexResults = [];
|
|
27
|
+
for (const index of INDEXES) {
|
|
28
|
+
try {
|
|
29
|
+
await sql.query(index.sql);
|
|
30
|
+
indexResults.push({
|
|
31
|
+
name: index.name,
|
|
32
|
+
status: "success",
|
|
33
|
+
description: index.description,
|
|
34
|
+
critical: !!index.critical
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
indexResults.push({
|
|
38
|
+
name: index.name,
|
|
39
|
+
status: "failed",
|
|
40
|
+
description: index.description,
|
|
41
|
+
critical: !!index.critical,
|
|
42
|
+
error: error.message
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const criticalFailures = indexResults.filter((index) => index.critical && index.status === "failed");
|
|
47
|
+
if (criticalFailures.length > 0) {
|
|
48
|
+
const failedNames = criticalFailures.map((index) => index.name).join(", ");
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Critical index creation failed (${failedNames}). Please remove duplicate UUID rows and run initSchema again.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const tableCheck = await sql.query(VERIFY_TABLE_SQL);
|
|
54
|
+
if (tableCheck.length === 0) {
|
|
55
|
+
throw new Error("Table creation verification failed");
|
|
56
|
+
}
|
|
57
|
+
const columns = await sql.query(COLUMNS_SQL);
|
|
58
|
+
return {
|
|
59
|
+
columnsCreated: columns.length,
|
|
60
|
+
indexesCreated: indexResults.filter((r) => r.status === "success").length,
|
|
61
|
+
indexesFailed: indexResults.filter((r) => r.status === "failed").length,
|
|
62
|
+
columns: columns.map((c) => ({ name: c.column_name, type: c.data_type, nullable: c.is_nullable === "YES" })),
|
|
63
|
+
indexes: indexResults
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async dropSchema() {
|
|
67
|
+
const sql = this._getSql();
|
|
68
|
+
await sql.query("DROP TABLE IF EXISTS scheduled_messages CASCADE");
|
|
69
|
+
}
|
|
70
|
+
async createTask(params) {
|
|
71
|
+
const sql = this._getSql();
|
|
72
|
+
const rows = await sql.query(
|
|
73
|
+
`INSERT INTO scheduled_messages
|
|
74
|
+
(user_id, uuid, encrypted_payload, next_send_at, message_type, status, retry_count, created_at, updated_at)
|
|
75
|
+
VALUES ($1, $2, $3, $4, $5, 'pending', 0, NOW(), NOW())
|
|
76
|
+
RETURNING id, uuid, next_send_at, status, created_at`,
|
|
77
|
+
[params.user_id, params.uuid, params.encrypted_payload, params.next_send_at, params.message_type]
|
|
78
|
+
);
|
|
79
|
+
return rows[0] || null;
|
|
80
|
+
}
|
|
81
|
+
async getTaskByUuid(uuid, userId) {
|
|
82
|
+
const sql = this._getSql();
|
|
83
|
+
const rows = await sql.query(
|
|
84
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
85
|
+
FROM scheduled_messages
|
|
86
|
+
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
87
|
+
LIMIT 1`,
|
|
88
|
+
[uuid, userId]
|
|
89
|
+
);
|
|
90
|
+
return rows[0] || null;
|
|
91
|
+
}
|
|
92
|
+
async getTaskByUuidOnly(uuid) {
|
|
93
|
+
const sql = this._getSql();
|
|
94
|
+
const rows = await sql.query(
|
|
95
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
96
|
+
FROM scheduled_messages
|
|
97
|
+
WHERE uuid = $1 AND status = 'pending'
|
|
98
|
+
LIMIT 1`,
|
|
99
|
+
[uuid]
|
|
100
|
+
);
|
|
101
|
+
return rows[0] || null;
|
|
102
|
+
}
|
|
103
|
+
async updateTaskById(taskId, updates) {
|
|
104
|
+
const sql = this._getSql();
|
|
105
|
+
const sets = [];
|
|
106
|
+
const values = [];
|
|
107
|
+
let idx = 1;
|
|
108
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
109
|
+
sets.push(`${key} = $${idx}`);
|
|
110
|
+
values.push(value);
|
|
111
|
+
idx++;
|
|
112
|
+
}
|
|
113
|
+
sets.push("updated_at = NOW()");
|
|
114
|
+
values.push(taskId);
|
|
115
|
+
const rows = await sql.query(
|
|
116
|
+
`UPDATE scheduled_messages SET ${sets.join(", ")} WHERE id = $${idx} RETURNING *`,
|
|
117
|
+
values
|
|
118
|
+
);
|
|
119
|
+
return rows[0] || null;
|
|
120
|
+
}
|
|
121
|
+
async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields) {
|
|
122
|
+
const sql = this._getSql();
|
|
123
|
+
const sets = ["encrypted_payload = $1", "updated_at = NOW()"];
|
|
124
|
+
const values = [encryptedPayload];
|
|
125
|
+
let idx = 2;
|
|
126
|
+
if (extraFields) {
|
|
127
|
+
for (const [key, value] of Object.entries(extraFields)) {
|
|
128
|
+
sets.push(`${key} = $${idx}`);
|
|
129
|
+
values.push(value);
|
|
130
|
+
idx++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
values.push(uuid, userId);
|
|
134
|
+
const rows = await sql.query(
|
|
135
|
+
`UPDATE scheduled_messages SET ${sets.join(", ")}
|
|
136
|
+
WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'
|
|
137
|
+
RETURNING uuid, updated_at`,
|
|
138
|
+
values
|
|
139
|
+
);
|
|
140
|
+
return rows[0] || null;
|
|
141
|
+
}
|
|
142
|
+
async deleteTaskById(taskId) {
|
|
143
|
+
const sql = this._getSql();
|
|
144
|
+
const rows = await sql.query("DELETE FROM scheduled_messages WHERE id = $1 RETURNING id", [taskId]);
|
|
145
|
+
return rows.length > 0;
|
|
146
|
+
}
|
|
147
|
+
async deleteTaskByUuid(uuid, userId) {
|
|
148
|
+
const sql = this._getSql();
|
|
149
|
+
const rows = await sql.query(
|
|
150
|
+
"DELETE FROM scheduled_messages WHERE uuid = $1 AND user_id = $2 RETURNING id",
|
|
151
|
+
[uuid, userId]
|
|
152
|
+
);
|
|
153
|
+
return rows.length > 0;
|
|
154
|
+
}
|
|
155
|
+
async getPendingTasks(limit = 50) {
|
|
156
|
+
const sql = this._getSql();
|
|
157
|
+
return sql.query(
|
|
158
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
159
|
+
FROM scheduled_messages
|
|
160
|
+
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
161
|
+
ORDER BY next_send_at ASC
|
|
162
|
+
LIMIT $1`,
|
|
163
|
+
[limit]
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
async listTasks(userId, opts = {}) {
|
|
167
|
+
const sql = this._getSql();
|
|
168
|
+
const { status = "all", limit = 20, offset = 0 } = opts;
|
|
169
|
+
const conditions = ["user_id = $1"];
|
|
170
|
+
const params = [userId];
|
|
171
|
+
let idx = 2;
|
|
172
|
+
if (status !== "all") {
|
|
173
|
+
conditions.push(`status = $${idx}`);
|
|
174
|
+
params.push(status);
|
|
175
|
+
idx++;
|
|
176
|
+
}
|
|
177
|
+
const where = conditions.join(" AND ");
|
|
178
|
+
const countRows = await sql.query(
|
|
179
|
+
`SELECT COUNT(*) as count FROM scheduled_messages WHERE ${where}`,
|
|
180
|
+
params
|
|
181
|
+
);
|
|
182
|
+
const total = parseInt(countRows[0].count, 10);
|
|
183
|
+
const taskParams = [...params, limit, offset];
|
|
184
|
+
const tasks = await sql.query(
|
|
185
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
|
|
186
|
+
FROM scheduled_messages
|
|
187
|
+
WHERE ${where}
|
|
188
|
+
ORDER BY next_send_at ASC
|
|
189
|
+
LIMIT $${idx} OFFSET $${idx + 1}`,
|
|
190
|
+
taskParams
|
|
191
|
+
);
|
|
192
|
+
return { tasks, total };
|
|
193
|
+
}
|
|
194
|
+
async cleanupOldTasks(days = 7) {
|
|
195
|
+
const sql = this._getSql();
|
|
196
|
+
const safeDays = Math.max(1, Math.floor(Number(days)));
|
|
197
|
+
const rows = await sql.query(
|
|
198
|
+
`DELETE FROM scheduled_messages
|
|
199
|
+
WHERE status IN ('sent', 'failed')
|
|
200
|
+
AND updated_at < NOW() - make_interval(days => $1)
|
|
201
|
+
RETURNING id`,
|
|
202
|
+
[safeDays]
|
|
203
|
+
);
|
|
204
|
+
return rows.length;
|
|
205
|
+
}
|
|
206
|
+
async getTaskStatus(uuid, userId) {
|
|
207
|
+
const sql = this._getSql();
|
|
208
|
+
const rows = await sql.query(
|
|
209
|
+
"SELECT status FROM scheduled_messages WHERE uuid = $1 AND user_id = $2 LIMIT 1",
|
|
210
|
+
[uuid, userId]
|
|
211
|
+
);
|
|
212
|
+
return rows.length > 0 ? rows[0].status : null;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
export {
|
|
216
|
+
NeonAdapter
|
|
217
|
+
};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './index-wQ6O1KrR.cjs';
|
|
3
|
+
import 'crypto';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* PostgreSQL (pg) Database Adapter
|
|
7
|
+
* ReiStandard SDK v1.1.0
|
|
8
|
+
*
|
|
9
|
+
* Uses the standard 'pg' npm package.
|
|
10
|
+
*
|
|
11
|
+
* @implements {import('./interface.js').DbAdapter}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PgAdapter {
|
|
16
|
+
/** @param {string} connectionString */
|
|
17
|
+
constructor(connectionString) {
|
|
18
|
+
/** @private */
|
|
19
|
+
this._connectionString = connectionString;
|
|
20
|
+
/** @private */
|
|
21
|
+
this._pool = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @private */
|
|
25
|
+
_getPool() {
|
|
26
|
+
if (!this._pool) {
|
|
27
|
+
this._pool = new Pool({ connectionString: this._connectionString });
|
|
28
|
+
}
|
|
29
|
+
return this._pool;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @private */
|
|
33
|
+
async _query(text, params) {
|
|
34
|
+
const pool = this._getPool();
|
|
35
|
+
const result = await pool.query(text, params);
|
|
36
|
+
return result.rows;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async initSchema() {
|
|
40
|
+
await this._query(TABLE_SQL);
|
|
41
|
+
|
|
42
|
+
const indexResults = [];
|
|
43
|
+
for (const index of INDEXES) {
|
|
44
|
+
try {
|
|
45
|
+
await this._query(index.sql);
|
|
46
|
+
indexResults.push({
|
|
47
|
+
name: index.name,
|
|
48
|
+
status: 'success',
|
|
49
|
+
description: index.description,
|
|
50
|
+
critical: !!index.critical
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
indexResults.push({
|
|
54
|
+
name: index.name,
|
|
55
|
+
status: 'failed',
|
|
56
|
+
description: index.description,
|
|
57
|
+
critical: !!index.critical,
|
|
58
|
+
error: error.message
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const criticalFailures = indexResults.filter((index) => index.critical && index.status === 'failed');
|
|
64
|
+
if (criticalFailures.length > 0) {
|
|
65
|
+
const failedNames = criticalFailures.map((index) => index.name).join(', ');
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Critical index creation failed (${failedNames}). ` +
|
|
68
|
+
'Please remove duplicate UUID rows and run initSchema again.'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const tableCheck = await this._query(VERIFY_TABLE_SQL);
|
|
73
|
+
if (tableCheck.length === 0) {
|
|
74
|
+
throw new Error('Table creation verification failed');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const columns = await this._query(COLUMNS_SQL);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
columnsCreated: columns.length,
|
|
81
|
+
indexesCreated: indexResults.filter(r => r.status === 'success').length,
|
|
82
|
+
indexesFailed: indexResults.filter(r => r.status === 'failed').length,
|
|
83
|
+
columns: columns.map(c => ({ name: c.column_name, type: c.data_type, nullable: c.is_nullable === 'YES' })),
|
|
84
|
+
indexes: indexResults
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async dropSchema() {
|
|
89
|
+
await this._query('DROP TABLE IF EXISTS scheduled_messages CASCADE');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async createTask(params) {
|
|
93
|
+
const rows = await this._query(
|
|
94
|
+
`INSERT INTO scheduled_messages
|
|
95
|
+
(user_id, uuid, encrypted_payload, next_send_at, message_type, status, retry_count, created_at, updated_at)
|
|
96
|
+
VALUES ($1, $2, $3, $4, $5, 'pending', 0, NOW(), NOW())
|
|
97
|
+
RETURNING id, uuid, next_send_at, status, created_at`,
|
|
98
|
+
[params.user_id, params.uuid, params.encrypted_payload, params.next_send_at, params.message_type]
|
|
99
|
+
);
|
|
100
|
+
return rows[0] || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getTaskByUuid(uuid, userId) {
|
|
104
|
+
const rows = await this._query(
|
|
105
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
106
|
+
FROM scheduled_messages
|
|
107
|
+
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
108
|
+
LIMIT 1`,
|
|
109
|
+
[uuid, userId]
|
|
110
|
+
);
|
|
111
|
+
return rows[0] || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async getTaskByUuidOnly(uuid) {
|
|
115
|
+
const rows = await this._query(
|
|
116
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
117
|
+
FROM scheduled_messages
|
|
118
|
+
WHERE uuid = $1 AND status = 'pending'
|
|
119
|
+
LIMIT 1`,
|
|
120
|
+
[uuid]
|
|
121
|
+
);
|
|
122
|
+
return rows[0] || null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async updateTaskById(taskId, updates) {
|
|
126
|
+
const sets = [];
|
|
127
|
+
const values = [];
|
|
128
|
+
let idx = 1;
|
|
129
|
+
|
|
130
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
131
|
+
sets.push(`${key} = $${idx}`);
|
|
132
|
+
values.push(value);
|
|
133
|
+
idx++;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
sets.push('updated_at = NOW()');
|
|
137
|
+
values.push(taskId);
|
|
138
|
+
|
|
139
|
+
const rows = await this._query(
|
|
140
|
+
`UPDATE scheduled_messages SET ${sets.join(', ')} WHERE id = $${idx} RETURNING *`,
|
|
141
|
+
values
|
|
142
|
+
);
|
|
143
|
+
return rows[0] || null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields) {
|
|
147
|
+
const sets = ['encrypted_payload = $1', 'updated_at = NOW()'];
|
|
148
|
+
const values = [encryptedPayload];
|
|
149
|
+
let idx = 2;
|
|
150
|
+
|
|
151
|
+
if (extraFields) {
|
|
152
|
+
for (const [key, value] of Object.entries(extraFields)) {
|
|
153
|
+
sets.push(`${key} = $${idx}`);
|
|
154
|
+
values.push(value);
|
|
155
|
+
idx++;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
values.push(uuid, userId);
|
|
160
|
+
const rows = await this._query(
|
|
161
|
+
`UPDATE scheduled_messages SET ${sets.join(', ')}
|
|
162
|
+
WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'
|
|
163
|
+
RETURNING uuid, updated_at`,
|
|
164
|
+
values
|
|
165
|
+
);
|
|
166
|
+
return rows[0] || null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async deleteTaskById(taskId) {
|
|
170
|
+
const rows = await this._query('DELETE FROM scheduled_messages WHERE id = $1 RETURNING id', [taskId]);
|
|
171
|
+
return rows.length > 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async deleteTaskByUuid(uuid, userId) {
|
|
175
|
+
const rows = await this._query(
|
|
176
|
+
'DELETE FROM scheduled_messages WHERE uuid = $1 AND user_id = $2 RETURNING id',
|
|
177
|
+
[uuid, userId]
|
|
178
|
+
);
|
|
179
|
+
return rows.length > 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async getPendingTasks(limit = 50) {
|
|
183
|
+
return this._query(
|
|
184
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
185
|
+
FROM scheduled_messages
|
|
186
|
+
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
187
|
+
ORDER BY next_send_at ASC
|
|
188
|
+
LIMIT $1`,
|
|
189
|
+
[limit]
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async listTasks(userId, opts = {}) {
|
|
194
|
+
const { status = 'all', limit = 20, offset = 0 } = opts;
|
|
195
|
+
|
|
196
|
+
const conditions = ['user_id = $1'];
|
|
197
|
+
const params = [userId];
|
|
198
|
+
let idx = 2;
|
|
199
|
+
|
|
200
|
+
if (status !== 'all') {
|
|
201
|
+
conditions.push(`status = $${idx}`);
|
|
202
|
+
params.push(status);
|
|
203
|
+
idx++;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const where = conditions.join(' AND ');
|
|
207
|
+
|
|
208
|
+
const countRows = await this._query(
|
|
209
|
+
`SELECT COUNT(*) as count FROM scheduled_messages WHERE ${where}`,
|
|
210
|
+
params
|
|
211
|
+
);
|
|
212
|
+
const total = parseInt(countRows[0].count, 10);
|
|
213
|
+
|
|
214
|
+
const taskParams = [...params, limit, offset];
|
|
215
|
+
const tasks = await this._query(
|
|
216
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
|
|
217
|
+
FROM scheduled_messages
|
|
218
|
+
WHERE ${where}
|
|
219
|
+
ORDER BY next_send_at ASC
|
|
220
|
+
LIMIT $${idx} OFFSET $${idx + 1}`,
|
|
221
|
+
taskParams
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return { tasks, total };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async cleanupOldTasks(days = 7) {
|
|
228
|
+
const safeDays = Math.max(1, Math.floor(Number(days)));
|
|
229
|
+
const rows = await this._query(
|
|
230
|
+
`DELETE FROM scheduled_messages
|
|
231
|
+
WHERE status IN ('sent', 'failed')
|
|
232
|
+
AND updated_at < NOW() - make_interval(days => $1)
|
|
233
|
+
RETURNING id`,
|
|
234
|
+
[safeDays]
|
|
235
|
+
);
|
|
236
|
+
return rows.length;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async getTaskStatus(uuid, userId) {
|
|
240
|
+
const rows = await this._query(
|
|
241
|
+
'SELECT status FROM scheduled_messages WHERE uuid = $1 AND user_id = $2 LIMIT 1',
|
|
242
|
+
[uuid, userId]
|
|
243
|
+
);
|
|
244
|
+
return rows.length > 0 ? rows[0].status : null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export { PgAdapter };
|