or3-provider-sqlite 0.0.1 → 0.0.2
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 +6 -3
- package/dist/module.json +1 -1
- package/dist/runtime/server/admin/adapters/sync-sqlite.js +12 -34
- package/dist/runtime/server/admin/stores/sqlite-store.js +64 -9
- package/dist/runtime/server/auth/sqlite-auth-workspace-store.d.ts +16 -1
- package/dist/runtime/server/auth/sqlite-auth-workspace-store.js +114 -3
- package/dist/runtime/server/db/migrate.js +45 -1
- package/dist/runtime/server/db/migrations/006_sync_snapshots.d.ts +10 -0
- package/dist/runtime/server/db/migrations/006_sync_snapshots.js +106 -0
- package/dist/runtime/server/db/migrations/007_device_cursor_ownership.d.ts +7 -0
- package/dist/runtime/server/db/migrations/007_device_cursor_ownership.js +5 -0
- package/dist/runtime/server/db/migrations/008_upload_intents.d.ts +4 -0
- package/dist/runtime/server/db/migrations/008_upload_intents.js +26 -0
- package/dist/runtime/server/db/schema.d.ts +40 -0
- package/dist/runtime/server/plugins/register.js +7 -0
- package/dist/runtime/server/sync/history-gc-policy.d.ts +13 -0
- package/dist/runtime/server/sync/history-gc-policy.js +8 -0
- package/dist/runtime/server/sync/sqlite-sync-gateway-adapter.d.ts +14 -2
- package/dist/runtime/server/sync/sqlite-sync-gateway-adapter.js +901 -67
- package/dist/runtime/server/webhooks/sqlite-webhook-store.d.ts +9 -0
- package/dist/runtime/server/webhooks/sqlite-webhook-store.js +521 -0
- package/package.json +5 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import type { WebhookStore } from '~~/server/utils/webhooks/store/types';
|
|
3
|
+
type SqliteDatabase = InstanceType<typeof Database>;
|
|
4
|
+
export interface SqliteWebhookStoreOptions {
|
|
5
|
+
database?: SqliteDatabase;
|
|
6
|
+
path?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function createSqliteWebhookStore(options?: SqliteWebhookStoreOptions): WebhookStore;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
function envFlag(value) {
|
|
4
|
+
if (!value) return false;
|
|
5
|
+
const normalized = value.trim().toLowerCase();
|
|
6
|
+
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
7
|
+
}
|
|
8
|
+
function resolveDatabasePath() {
|
|
9
|
+
const isTestEnv = process.env.NODE_ENV === "test" || envFlag(process.env.VITEST);
|
|
10
|
+
const allowInMemory = envFlag(process.env.OR3_SQLITE_ALLOW_IN_MEMORY);
|
|
11
|
+
const strictMode = envFlag(process.env.OR3_SQLITE_STRICT);
|
|
12
|
+
const configuredPath = process.env.OR3_SQLITE_DB_PATH;
|
|
13
|
+
const path = configuredPath ?? ":memory:";
|
|
14
|
+
if (!configuredPath && !isTestEnv && !allowInMemory) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"OR3_SQLITE_DB_PATH is required in non-test environments. Set OR3_SQLITE_ALLOW_IN_MEMORY=true only if you intentionally want ephemeral storage."
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
if (strictMode && path === ":memory:") {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"OR3_SQLITE_STRICT=true forbids in-memory SQLite. Set OR3_SQLITE_DB_PATH to a persistent file path."
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
if (!isTestEnv && path === ":memory:" && allowInMemory) {
|
|
25
|
+
console.warn(
|
|
26
|
+
"[webhooks:sqlite] OR3_SQLITE_ALLOW_IN_MEMORY=true enabled. Data will be lost on process restart."
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return path;
|
|
30
|
+
}
|
|
31
|
+
function parseStringArray(raw) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (!Array.isArray(parsed)) return [];
|
|
35
|
+
return parsed.filter((value) => typeof value === "string");
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function toWebhookRegistration(row) {
|
|
41
|
+
return {
|
|
42
|
+
id: row.id,
|
|
43
|
+
scope: row.scope,
|
|
44
|
+
user_id: row.user_id,
|
|
45
|
+
workspace_id: row.workspace_id,
|
|
46
|
+
url: row.url,
|
|
47
|
+
label: row.label,
|
|
48
|
+
events: parseStringArray(row.events),
|
|
49
|
+
custom_hooks: parseStringArray(row.custom_hooks),
|
|
50
|
+
signing_secret_enc: row.signing_secret_enc,
|
|
51
|
+
enabled: row.enabled === 1,
|
|
52
|
+
health: row.health,
|
|
53
|
+
created_at: row.created_at,
|
|
54
|
+
updated_at: row.updated_at
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function toDeliveryLog(row) {
|
|
58
|
+
return {
|
|
59
|
+
id: row.id,
|
|
60
|
+
webhook_id: row.webhook_id,
|
|
61
|
+
event_id: row.event_id,
|
|
62
|
+
event_type: row.event_type,
|
|
63
|
+
attempt: row.attempt,
|
|
64
|
+
status: row.status,
|
|
65
|
+
claimed_by: row.claimed_by,
|
|
66
|
+
claimed_at: row.claimed_at,
|
|
67
|
+
http_status: row.http_status,
|
|
68
|
+
error_message: row.error_message,
|
|
69
|
+
request_payload: row.request_payload,
|
|
70
|
+
response_body: row.response_body,
|
|
71
|
+
duration_ms: row.duration_ms,
|
|
72
|
+
next_retry_at: row.next_retry_at,
|
|
73
|
+
created_at: row.created_at
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
class SqliteWebhookStore {
|
|
77
|
+
constructor(db) {
|
|
78
|
+
this.db = db;
|
|
79
|
+
this.initialize();
|
|
80
|
+
}
|
|
81
|
+
initialize() {
|
|
82
|
+
this.db.pragma("journal_mode = WAL");
|
|
83
|
+
this.db.pragma("synchronous = NORMAL");
|
|
84
|
+
this.db.pragma("foreign_keys = ON");
|
|
85
|
+
this.db.exec(`
|
|
86
|
+
CREATE TABLE IF NOT EXISTS webhook_registrations (
|
|
87
|
+
id TEXT PRIMARY KEY,
|
|
88
|
+
scope TEXT NOT NULL CHECK (scope IN ('user', 'admin')),
|
|
89
|
+
user_id TEXT,
|
|
90
|
+
workspace_id TEXT,
|
|
91
|
+
url TEXT NOT NULL,
|
|
92
|
+
label TEXT NOT NULL DEFAULT '',
|
|
93
|
+
events TEXT NOT NULL,
|
|
94
|
+
custom_hooks TEXT NOT NULL,
|
|
95
|
+
signing_secret_enc TEXT NOT NULL,
|
|
96
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
97
|
+
health TEXT NOT NULL DEFAULT 'unknown' CHECK (health IN ('healthy', 'failing', 'unknown')),
|
|
98
|
+
created_at INTEGER NOT NULL,
|
|
99
|
+
updated_at INTEGER NOT NULL
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE TABLE IF NOT EXISTS webhook_delivery_logs (
|
|
103
|
+
id TEXT PRIMARY KEY,
|
|
104
|
+
webhook_id TEXT NOT NULL,
|
|
105
|
+
event_id TEXT NOT NULL,
|
|
106
|
+
event_type TEXT NOT NULL,
|
|
107
|
+
attempt INTEGER NOT NULL,
|
|
108
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'in_flight', 'success', 'failed', 'cancelled')),
|
|
109
|
+
claimed_by TEXT,
|
|
110
|
+
claimed_at INTEGER,
|
|
111
|
+
http_status INTEGER,
|
|
112
|
+
error_message TEXT,
|
|
113
|
+
request_payload TEXT NOT NULL,
|
|
114
|
+
response_body TEXT,
|
|
115
|
+
duration_ms INTEGER,
|
|
116
|
+
next_retry_at INTEGER,
|
|
117
|
+
created_at INTEGER NOT NULL,
|
|
118
|
+
FOREIGN KEY (webhook_id) REFERENCES webhook_registrations(id) ON DELETE CASCADE
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
CREATE TABLE IF NOT EXISTS webhook_custom_hooks (
|
|
122
|
+
webhook_id TEXT NOT NULL,
|
|
123
|
+
hook_name TEXT NOT NULL,
|
|
124
|
+
PRIMARY KEY (webhook_id, hook_name),
|
|
125
|
+
FOREIGN KEY (webhook_id) REFERENCES webhook_registrations(id) ON DELETE CASCADE
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
CREATE INDEX IF NOT EXISTS idx_webhook_registrations_scope_user_workspace
|
|
129
|
+
ON webhook_registrations (scope, user_id, workspace_id);
|
|
130
|
+
CREATE INDEX IF NOT EXISTS idx_webhook_delivery_logs_webhook_created
|
|
131
|
+
ON webhook_delivery_logs (webhook_id, created_at);
|
|
132
|
+
CREATE INDEX IF NOT EXISTS idx_webhook_delivery_logs_status_retry
|
|
133
|
+
ON webhook_delivery_logs (status, next_retry_at);
|
|
134
|
+
CREATE INDEX IF NOT EXISTS idx_webhook_delivery_logs_status_claimed
|
|
135
|
+
ON webhook_delivery_logs (status, claimed_at);
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_webhook_custom_hooks_hook_name
|
|
137
|
+
ON webhook_custom_hooks (hook_name);
|
|
138
|
+
`);
|
|
139
|
+
this.db.exec(`
|
|
140
|
+
INSERT OR IGNORE INTO webhook_custom_hooks (webhook_id, hook_name)
|
|
141
|
+
SELECT wr.id, json_each.value
|
|
142
|
+
FROM webhook_registrations AS wr,
|
|
143
|
+
json_each(
|
|
144
|
+
CASE
|
|
145
|
+
WHEN json_valid(wr.custom_hooks) THEN wr.custom_hooks
|
|
146
|
+
ELSE '[]'
|
|
147
|
+
END
|
|
148
|
+
)
|
|
149
|
+
WHERE typeof(json_each.value) = 'text'
|
|
150
|
+
`);
|
|
151
|
+
}
|
|
152
|
+
async createWebhook(webhook) {
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
const row = {
|
|
155
|
+
...webhook,
|
|
156
|
+
id: randomUUID(),
|
|
157
|
+
health: "unknown",
|
|
158
|
+
created_at: now,
|
|
159
|
+
updated_at: now
|
|
160
|
+
};
|
|
161
|
+
const create = this.db.transaction(() => {
|
|
162
|
+
this.db.prepare(
|
|
163
|
+
`
|
|
164
|
+
INSERT INTO webhook_registrations (
|
|
165
|
+
id, scope, user_id, workspace_id, url, label, events, custom_hooks,
|
|
166
|
+
signing_secret_enc, enabled, health, created_at, updated_at
|
|
167
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
168
|
+
`
|
|
169
|
+
).run(
|
|
170
|
+
row.id,
|
|
171
|
+
row.scope,
|
|
172
|
+
row.user_id,
|
|
173
|
+
row.workspace_id,
|
|
174
|
+
row.url,
|
|
175
|
+
row.label,
|
|
176
|
+
JSON.stringify(row.events),
|
|
177
|
+
JSON.stringify(row.custom_hooks),
|
|
178
|
+
row.signing_secret_enc,
|
|
179
|
+
row.enabled ? 1 : 0,
|
|
180
|
+
row.health,
|
|
181
|
+
row.created_at,
|
|
182
|
+
row.updated_at
|
|
183
|
+
);
|
|
184
|
+
const insertCustomHook = this.db.prepare(
|
|
185
|
+
`
|
|
186
|
+
INSERT OR IGNORE INTO webhook_custom_hooks (webhook_id, hook_name)
|
|
187
|
+
VALUES (?, ?)
|
|
188
|
+
`
|
|
189
|
+
);
|
|
190
|
+
for (const hookName of row.custom_hooks) {
|
|
191
|
+
insertCustomHook.run(row.id, hookName);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
create();
|
|
195
|
+
return row;
|
|
196
|
+
}
|
|
197
|
+
async updateWebhook(webhookId, patch) {
|
|
198
|
+
const current = await this.getWebhook(webhookId);
|
|
199
|
+
if (!current) {
|
|
200
|
+
throw new Error(`Webhook not found: ${webhookId}`);
|
|
201
|
+
}
|
|
202
|
+
const urlChanged = typeof patch.url === "string" && patch.url !== current.url;
|
|
203
|
+
const next = {
|
|
204
|
+
...current,
|
|
205
|
+
...patch,
|
|
206
|
+
events: patch.events ?? current.events,
|
|
207
|
+
custom_hooks: patch.custom_hooks ?? current.custom_hooks,
|
|
208
|
+
health: urlChanged ? "unknown" : current.health,
|
|
209
|
+
updated_at: Date.now()
|
|
210
|
+
};
|
|
211
|
+
const update = this.db.transaction(() => {
|
|
212
|
+
this.db.prepare(
|
|
213
|
+
`
|
|
214
|
+
UPDATE webhook_registrations
|
|
215
|
+
SET url = ?, label = ?, events = ?, custom_hooks = ?, enabled = ?, workspace_id = ?, health = ?, updated_at = ?
|
|
216
|
+
WHERE id = ?
|
|
217
|
+
`
|
|
218
|
+
).run(
|
|
219
|
+
next.url,
|
|
220
|
+
next.label,
|
|
221
|
+
JSON.stringify(next.events),
|
|
222
|
+
JSON.stringify(next.custom_hooks),
|
|
223
|
+
next.enabled ? 1 : 0,
|
|
224
|
+
next.workspace_id,
|
|
225
|
+
next.health,
|
|
226
|
+
next.updated_at,
|
|
227
|
+
webhookId
|
|
228
|
+
);
|
|
229
|
+
if ("custom_hooks" in patch) {
|
|
230
|
+
this.db.prepare("DELETE FROM webhook_custom_hooks WHERE webhook_id = ?").run(webhookId);
|
|
231
|
+
const insertCustomHook = this.db.prepare(
|
|
232
|
+
`
|
|
233
|
+
INSERT OR IGNORE INTO webhook_custom_hooks (webhook_id, hook_name)
|
|
234
|
+
VALUES (?, ?)
|
|
235
|
+
`
|
|
236
|
+
);
|
|
237
|
+
for (const hookName of next.custom_hooks) {
|
|
238
|
+
insertCustomHook.run(webhookId, hookName);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
update();
|
|
243
|
+
return next;
|
|
244
|
+
}
|
|
245
|
+
async deleteWebhook(webhookId) {
|
|
246
|
+
this.db.prepare("DELETE FROM webhook_registrations WHERE id = ?").run(webhookId);
|
|
247
|
+
}
|
|
248
|
+
async getWebhook(webhookId) {
|
|
249
|
+
const row = this.db.prepare("SELECT * FROM webhook_registrations WHERE id = ?").get(webhookId);
|
|
250
|
+
return row ? toWebhookRegistration(row) : null;
|
|
251
|
+
}
|
|
252
|
+
async listWebhooks(userId, workspaceId) {
|
|
253
|
+
const rows = this.db.prepare(
|
|
254
|
+
`
|
|
255
|
+
SELECT * FROM webhook_registrations
|
|
256
|
+
WHERE scope = 'user' AND user_id = ? AND workspace_id = ?
|
|
257
|
+
ORDER BY created_at DESC
|
|
258
|
+
`
|
|
259
|
+
).all(userId, workspaceId);
|
|
260
|
+
return rows.map(toWebhookRegistration);
|
|
261
|
+
}
|
|
262
|
+
async listAdminWebhooks() {
|
|
263
|
+
const rows = this.db.prepare(
|
|
264
|
+
`
|
|
265
|
+
SELECT * FROM webhook_registrations
|
|
266
|
+
WHERE scope = 'admin'
|
|
267
|
+
ORDER BY created_at DESC
|
|
268
|
+
`
|
|
269
|
+
).all();
|
|
270
|
+
return rows.map(toWebhookRegistration);
|
|
271
|
+
}
|
|
272
|
+
async listWebhooksByEvent(eventType, scope, workspaceId) {
|
|
273
|
+
const eventQuery = `
|
|
274
|
+
EXISTS (
|
|
275
|
+
SELECT 1
|
|
276
|
+
FROM json_each(
|
|
277
|
+
CASE
|
|
278
|
+
WHEN json_valid(events) THEN events
|
|
279
|
+
ELSE '[]'
|
|
280
|
+
END
|
|
281
|
+
)
|
|
282
|
+
WHERE json_each.value = ?
|
|
283
|
+
)
|
|
284
|
+
`;
|
|
285
|
+
let rows;
|
|
286
|
+
if (!workspaceId) {
|
|
287
|
+
if (scope === "user") {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
rows = this.db.prepare(
|
|
291
|
+
`
|
|
292
|
+
SELECT * FROM webhook_registrations
|
|
293
|
+
WHERE scope = ? AND enabled = 1 AND ${eventQuery}
|
|
294
|
+
AND workspace_id IS NULL
|
|
295
|
+
`
|
|
296
|
+
).all(scope, eventType);
|
|
297
|
+
} else if (scope === "admin") {
|
|
298
|
+
rows = this.db.prepare(
|
|
299
|
+
`
|
|
300
|
+
SELECT * FROM webhook_registrations
|
|
301
|
+
WHERE scope = ? AND enabled = 1 AND ${eventQuery}
|
|
302
|
+
AND (workspace_id IS NULL OR workspace_id = ?)
|
|
303
|
+
`
|
|
304
|
+
).all(scope, eventType, workspaceId);
|
|
305
|
+
} else {
|
|
306
|
+
rows = this.db.prepare(
|
|
307
|
+
`
|
|
308
|
+
SELECT * FROM webhook_registrations
|
|
309
|
+
WHERE scope = ? AND enabled = 1 AND ${eventQuery}
|
|
310
|
+
AND workspace_id = ?
|
|
311
|
+
`
|
|
312
|
+
).all(scope, eventType, workspaceId);
|
|
313
|
+
}
|
|
314
|
+
return rows.map(toWebhookRegistration);
|
|
315
|
+
}
|
|
316
|
+
async listWebhooksByCustomHook(hookName) {
|
|
317
|
+
const rows = this.db.prepare(
|
|
318
|
+
`
|
|
319
|
+
SELECT wr.* FROM webhook_registrations wr
|
|
320
|
+
INNER JOIN webhook_custom_hooks wch ON wch.webhook_id = wr.id
|
|
321
|
+
WHERE wr.scope = 'admin' AND wr.enabled = 1 AND wch.hook_name = ?
|
|
322
|
+
`
|
|
323
|
+
).all(hookName);
|
|
324
|
+
return rows.map(toWebhookRegistration);
|
|
325
|
+
}
|
|
326
|
+
async listActiveCustomHookNames() {
|
|
327
|
+
const rows = this.db.prepare(
|
|
328
|
+
`
|
|
329
|
+
SELECT DISTINCT wch.hook_name
|
|
330
|
+
FROM webhook_custom_hooks wch
|
|
331
|
+
INNER JOIN webhook_registrations wr ON wr.id = wch.webhook_id
|
|
332
|
+
WHERE wr.scope = 'admin' AND wr.enabled = 1
|
|
333
|
+
ORDER BY wch.hook_name ASC
|
|
334
|
+
`
|
|
335
|
+
).all();
|
|
336
|
+
return rows.map((row) => row.hook_name);
|
|
337
|
+
}
|
|
338
|
+
async updateWebhookHealth(webhookId, health) {
|
|
339
|
+
this.db.prepare(
|
|
340
|
+
`
|
|
341
|
+
UPDATE webhook_registrations
|
|
342
|
+
SET health = ?, updated_at = ?
|
|
343
|
+
WHERE id = ?
|
|
344
|
+
`
|
|
345
|
+
).run(health, Date.now(), webhookId);
|
|
346
|
+
}
|
|
347
|
+
async disableAllWebhooks(userId, workspaceId) {
|
|
348
|
+
const result = this.db.prepare(
|
|
349
|
+
`
|
|
350
|
+
UPDATE webhook_registrations
|
|
351
|
+
SET enabled = 0, updated_at = ?
|
|
352
|
+
WHERE scope = 'user' AND user_id = ? AND workspace_id = ? AND enabled = 1
|
|
353
|
+
`
|
|
354
|
+
).run(Date.now(), userId, workspaceId);
|
|
355
|
+
return result.changes;
|
|
356
|
+
}
|
|
357
|
+
async createDeliveryLog(log) {
|
|
358
|
+
const row = {
|
|
359
|
+
...log,
|
|
360
|
+
id: randomUUID()
|
|
361
|
+
};
|
|
362
|
+
this.db.prepare(
|
|
363
|
+
`
|
|
364
|
+
INSERT INTO webhook_delivery_logs (
|
|
365
|
+
id, webhook_id, event_id, event_type, attempt, status, claimed_by, claimed_at,
|
|
366
|
+
http_status, error_message, request_payload, response_body, duration_ms,
|
|
367
|
+
next_retry_at, created_at
|
|
368
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
369
|
+
`
|
|
370
|
+
).run(
|
|
371
|
+
row.id,
|
|
372
|
+
row.webhook_id,
|
|
373
|
+
row.event_id,
|
|
374
|
+
row.event_type,
|
|
375
|
+
row.attempt,
|
|
376
|
+
row.status,
|
|
377
|
+
row.claimed_by,
|
|
378
|
+
row.claimed_at,
|
|
379
|
+
row.http_status,
|
|
380
|
+
row.error_message,
|
|
381
|
+
row.request_payload,
|
|
382
|
+
row.response_body,
|
|
383
|
+
row.duration_ms,
|
|
384
|
+
row.next_retry_at,
|
|
385
|
+
row.created_at
|
|
386
|
+
);
|
|
387
|
+
return row;
|
|
388
|
+
}
|
|
389
|
+
async updateDeliveryLog(logId, patch) {
|
|
390
|
+
const current = this.db.prepare("SELECT * FROM webhook_delivery_logs WHERE id = ?").get(logId);
|
|
391
|
+
if (!current) {
|
|
392
|
+
throw new Error(`Webhook delivery log not found: ${logId}`);
|
|
393
|
+
}
|
|
394
|
+
const status = patch.status ?? current.status;
|
|
395
|
+
const clearClaim = status !== "in_flight";
|
|
396
|
+
const attempt = "attempt" in patch ? patch.attempt : current.attempt;
|
|
397
|
+
const httpStatus = "http_status" in patch ? patch.http_status : current.http_status;
|
|
398
|
+
const errorMessage = "error_message" in patch ? patch.error_message : current.error_message;
|
|
399
|
+
const responseBody = "response_body" in patch ? patch.response_body : current.response_body;
|
|
400
|
+
const durationMs = "duration_ms" in patch ? patch.duration_ms : current.duration_ms;
|
|
401
|
+
const nextRetryAt = "next_retry_at" in patch ? patch.next_retry_at : current.next_retry_at;
|
|
402
|
+
this.db.prepare(
|
|
403
|
+
`
|
|
404
|
+
UPDATE webhook_delivery_logs
|
|
405
|
+
SET status = ?, attempt = ?, http_status = ?, error_message = ?, response_body = ?,
|
|
406
|
+
duration_ms = ?, next_retry_at = ?, claimed_by = ?, claimed_at = ?
|
|
407
|
+
WHERE id = ?
|
|
408
|
+
`
|
|
409
|
+
).run(
|
|
410
|
+
status,
|
|
411
|
+
attempt,
|
|
412
|
+
httpStatus,
|
|
413
|
+
errorMessage,
|
|
414
|
+
responseBody,
|
|
415
|
+
durationMs,
|
|
416
|
+
nextRetryAt,
|
|
417
|
+
clearClaim ? null : current.claimed_by,
|
|
418
|
+
clearClaim ? null : current.claimed_at,
|
|
419
|
+
logId
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
async getDeliveryLogs(webhookId, since) {
|
|
423
|
+
const rows = this.db.prepare(
|
|
424
|
+
`
|
|
425
|
+
SELECT * FROM webhook_delivery_logs
|
|
426
|
+
WHERE webhook_id = ? AND created_at >= ?
|
|
427
|
+
ORDER BY created_at DESC
|
|
428
|
+
`
|
|
429
|
+
).all(webhookId, since);
|
|
430
|
+
return rows.map(toDeliveryLog);
|
|
431
|
+
}
|
|
432
|
+
async getRecentTerminalDeliveries(webhookId, limit) {
|
|
433
|
+
const safeLimit = Math.max(1, Math.floor(limit));
|
|
434
|
+
const rows = this.db.prepare(
|
|
435
|
+
`
|
|
436
|
+
SELECT * FROM webhook_delivery_logs
|
|
437
|
+
WHERE webhook_id = ? AND status IN ('success', 'failed')
|
|
438
|
+
ORDER BY created_at DESC
|
|
439
|
+
LIMIT ?
|
|
440
|
+
`
|
|
441
|
+
).all(webhookId, safeLimit);
|
|
442
|
+
return rows.map(toDeliveryLog);
|
|
443
|
+
}
|
|
444
|
+
async claimPendingDeliveries(workerId, limit) {
|
|
445
|
+
const safeLimit = Math.max(0, Math.floor(limit));
|
|
446
|
+
if (safeLimit === 0) {
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
449
|
+
const now = Date.now();
|
|
450
|
+
const claim = this.db.transaction(() => {
|
|
451
|
+
const rows = this.db.prepare(
|
|
452
|
+
`
|
|
453
|
+
SELECT * FROM webhook_delivery_logs
|
|
454
|
+
WHERE status = 'pending'
|
|
455
|
+
AND (next_retry_at IS NULL OR next_retry_at <= ?)
|
|
456
|
+
ORDER BY COALESCE(next_retry_at, created_at) ASC, created_at ASC
|
|
457
|
+
LIMIT ?
|
|
458
|
+
`
|
|
459
|
+
).all(now, safeLimit);
|
|
460
|
+
if (rows.length === 0) {
|
|
461
|
+
return [];
|
|
462
|
+
}
|
|
463
|
+
const update = this.db.prepare(
|
|
464
|
+
`
|
|
465
|
+
UPDATE webhook_delivery_logs
|
|
466
|
+
SET status = 'in_flight', claimed_by = ?, claimed_at = ?
|
|
467
|
+
WHERE id = ? AND status = 'pending'
|
|
468
|
+
`
|
|
469
|
+
);
|
|
470
|
+
const claimedRows = [];
|
|
471
|
+
for (const row of rows) {
|
|
472
|
+
const result = update.run(workerId, now, row.id);
|
|
473
|
+
if (result.changes > 0) {
|
|
474
|
+
claimedRows.push(
|
|
475
|
+
toDeliveryLog({
|
|
476
|
+
...row,
|
|
477
|
+
status: "in_flight",
|
|
478
|
+
claimed_by: workerId,
|
|
479
|
+
claimed_at: now
|
|
480
|
+
})
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return claimedRows;
|
|
485
|
+
});
|
|
486
|
+
return claim();
|
|
487
|
+
}
|
|
488
|
+
async resetStaleInFlightDeliveries(olderThanMs) {
|
|
489
|
+
const cutoff = Date.now() - Math.max(0, Math.floor(olderThanMs));
|
|
490
|
+
const result = this.db.prepare(
|
|
491
|
+
`
|
|
492
|
+
UPDATE webhook_delivery_logs
|
|
493
|
+
SET status = 'pending', claimed_by = NULL, claimed_at = NULL
|
|
494
|
+
WHERE status = 'in_flight' AND claimed_at IS NOT NULL AND claimed_at < ?
|
|
495
|
+
`
|
|
496
|
+
).run(cutoff);
|
|
497
|
+
return result.changes;
|
|
498
|
+
}
|
|
499
|
+
async cancelDeliveriesByWebhook(webhookId) {
|
|
500
|
+
const result = this.db.prepare(
|
|
501
|
+
`
|
|
502
|
+
UPDATE webhook_delivery_logs
|
|
503
|
+
SET status = 'cancelled', claimed_by = NULL, claimed_at = NULL, next_retry_at = NULL
|
|
504
|
+
WHERE webhook_id = ? AND status IN ('pending', 'in_flight')
|
|
505
|
+
`
|
|
506
|
+
).run(webhookId);
|
|
507
|
+
return result.changes;
|
|
508
|
+
}
|
|
509
|
+
async deleteDeliveryLogsByWebhook(webhookId) {
|
|
510
|
+
const result = this.db.prepare("DELETE FROM webhook_delivery_logs WHERE webhook_id = ?").run(webhookId);
|
|
511
|
+
return result.changes;
|
|
512
|
+
}
|
|
513
|
+
async purgeExpiredLogs(beforeTimestamp) {
|
|
514
|
+
const result = this.db.prepare("DELETE FROM webhook_delivery_logs WHERE created_at < ?").run(beforeTimestamp);
|
|
515
|
+
return result.changes;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
export function createSqliteWebhookStore(options = {}) {
|
|
519
|
+
const db = options.database ?? new Database(options.path ?? resolveDatabasePath());
|
|
520
|
+
return new SqliteWebhookStore(db);
|
|
521
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "or3-provider-sqlite",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "SQLite sync and workspace store provider for OR3 Chat — lightweight self-hosted backend via Kysely.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"./nuxt": {
|
|
14
14
|
"types": "./dist/types.d.mts",
|
|
15
15
|
"import": "./dist/module.mjs"
|
|
16
|
+
},
|
|
17
|
+
"./webhooks/sqlite-store": {
|
|
18
|
+
"types": "./dist/runtime/server/webhooks/sqlite-webhook-store.d.ts",
|
|
19
|
+
"import": "./dist/runtime/server/webhooks/sqlite-webhook-store.js"
|
|
16
20
|
}
|
|
17
21
|
},
|
|
18
22
|
"main": "./dist/module.mjs",
|