surface-cli 0.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.
Files changed (60) hide show
  1. package/README.md +307 -0
  2. package/dist/cli.js +521 -0
  3. package/dist/cli.js.map +1 -0
  4. package/dist/config.js +156 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/contracts/account.js +9 -0
  7. package/dist/contracts/account.js.map +1 -0
  8. package/dist/contracts/mail.js +2 -0
  9. package/dist/contracts/mail.js.map +1 -0
  10. package/dist/e2e/gmail-v1.js +247 -0
  11. package/dist/e2e/gmail-v1.js.map +1 -0
  12. package/dist/e2e/outlook-v1.js +179 -0
  13. package/dist/e2e/outlook-v1.js.map +1 -0
  14. package/dist/lib/errors.js +47 -0
  15. package/dist/lib/errors.js.map +1 -0
  16. package/dist/lib/json.js +4 -0
  17. package/dist/lib/json.js.map +1 -0
  18. package/dist/lib/public-mail.js +29 -0
  19. package/dist/lib/public-mail.js.map +1 -0
  20. package/dist/lib/remote-auth.js +407 -0
  21. package/dist/lib/remote-auth.js.map +1 -0
  22. package/dist/lib/time.js +4 -0
  23. package/dist/lib/time.js.map +1 -0
  24. package/dist/lib/write-safety.js +34 -0
  25. package/dist/lib/write-safety.js.map +1 -0
  26. package/dist/paths.js +29 -0
  27. package/dist/paths.js.map +1 -0
  28. package/dist/providers/gmail/adapter.js +1102 -0
  29. package/dist/providers/gmail/adapter.js.map +1 -0
  30. package/dist/providers/gmail/api.js +99 -0
  31. package/dist/providers/gmail/api.js.map +1 -0
  32. package/dist/providers/gmail/normalize.js +336 -0
  33. package/dist/providers/gmail/normalize.js.map +1 -0
  34. package/dist/providers/gmail/oauth.js +328 -0
  35. package/dist/providers/gmail/oauth.js.map +1 -0
  36. package/dist/providers/index.js +12 -0
  37. package/dist/providers/index.js.map +1 -0
  38. package/dist/providers/outlook/adapter.js +1443 -0
  39. package/dist/providers/outlook/adapter.js.map +1 -0
  40. package/dist/providers/outlook/extract.js +416 -0
  41. package/dist/providers/outlook/extract.js.map +1 -0
  42. package/dist/providers/outlook/normalize.js +126 -0
  43. package/dist/providers/outlook/normalize.js.map +1 -0
  44. package/dist/providers/outlook/session.js +178 -0
  45. package/dist/providers/outlook/session.js.map +1 -0
  46. package/dist/providers/shared/html.js +88 -0
  47. package/dist/providers/shared/html.js.map +1 -0
  48. package/dist/providers/shared/types.js +2 -0
  49. package/dist/providers/shared/types.js.map +1 -0
  50. package/dist/providers/types.js +2 -0
  51. package/dist/providers/types.js.map +1 -0
  52. package/dist/refs.js +18 -0
  53. package/dist/refs.js.map +1 -0
  54. package/dist/runtime.js +23 -0
  55. package/dist/runtime.js.map +1 -0
  56. package/dist/state/database.js +731 -0
  57. package/dist/state/database.js.map +1 -0
  58. package/dist/summarizer.js +217 -0
  59. package/dist/summarizer.js.map +1 -0
  60. package/package.json +55 -0
@@ -0,0 +1,731 @@
1
+ import Database from "better-sqlite3";
2
+ import { makeAccountId } from "../refs.js";
3
+ import { nowIsoUtc } from "../lib/time.js";
4
+ export class SurfaceDatabase {
5
+ connection;
6
+ constructor(path) {
7
+ this.connection = new Database(path);
8
+ this.connection.pragma("journal_mode = WAL");
9
+ this.connection.pragma("foreign_keys = ON");
10
+ this.migrate();
11
+ }
12
+ close() {
13
+ this.connection.close();
14
+ }
15
+ migrate() {
16
+ this.connection.exec(`
17
+ CREATE TABLE IF NOT EXISTS accounts (
18
+ account_id TEXT PRIMARY KEY,
19
+ name TEXT NOT NULL UNIQUE,
20
+ provider TEXT NOT NULL,
21
+ transport TEXT NOT NULL,
22
+ email TEXT NOT NULL,
23
+ created_at TEXT NOT NULL,
24
+ updated_at TEXT NOT NULL
25
+ );
26
+
27
+ CREATE TABLE IF NOT EXISTS threads (
28
+ thread_ref TEXT PRIMARY KEY,
29
+ account_id TEXT NOT NULL,
30
+ subject TEXT,
31
+ mailbox TEXT,
32
+ participants_json TEXT NOT NULL DEFAULT '[]',
33
+ labels_json TEXT NOT NULL DEFAULT '[]',
34
+ received_at TEXT,
35
+ message_count INTEGER NOT NULL DEFAULT 0,
36
+ unread_count INTEGER NOT NULL DEFAULT 0,
37
+ has_attachments INTEGER NOT NULL DEFAULT 0,
38
+ first_seen_at TEXT NOT NULL,
39
+ last_synced_at TEXT NOT NULL,
40
+ FOREIGN KEY(account_id) REFERENCES accounts(account_id) ON DELETE CASCADE
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS messages (
44
+ message_ref TEXT PRIMARY KEY,
45
+ account_id TEXT NOT NULL,
46
+ thread_ref TEXT NOT NULL,
47
+ subject TEXT,
48
+ from_name TEXT,
49
+ from_email TEXT,
50
+ to_json TEXT NOT NULL DEFAULT '[]',
51
+ cc_json TEXT NOT NULL DEFAULT '[]',
52
+ sent_at TEXT,
53
+ received_at TEXT,
54
+ unread INTEGER NOT NULL DEFAULT 0,
55
+ snippet TEXT NOT NULL DEFAULT '',
56
+ body_cache_path TEXT,
57
+ body_cached INTEGER NOT NULL DEFAULT 0,
58
+ body_truncated INTEGER NOT NULL DEFAULT 0,
59
+ body_cached_bytes INTEGER NOT NULL DEFAULT 0,
60
+ invite_json TEXT,
61
+ first_seen_at TEXT NOT NULL,
62
+ last_synced_at TEXT NOT NULL,
63
+ FOREIGN KEY(account_id) REFERENCES accounts(account_id) ON DELETE CASCADE,
64
+ FOREIGN KEY(thread_ref) REFERENCES threads(thread_ref) ON DELETE CASCADE
65
+ );
66
+
67
+ CREATE TABLE IF NOT EXISTS thread_messages (
68
+ thread_ref TEXT NOT NULL,
69
+ message_ref TEXT NOT NULL,
70
+ position INTEGER NOT NULL,
71
+ PRIMARY KEY(thread_ref, message_ref),
72
+ FOREIGN KEY(thread_ref) REFERENCES threads(thread_ref) ON DELETE CASCADE,
73
+ FOREIGN KEY(message_ref) REFERENCES messages(message_ref) ON DELETE CASCADE
74
+ );
75
+
76
+ CREATE TABLE IF NOT EXISTS attachments (
77
+ attachment_id TEXT PRIMARY KEY,
78
+ message_ref TEXT NOT NULL,
79
+ filename TEXT NOT NULL,
80
+ mime_type TEXT NOT NULL,
81
+ size_bytes INTEGER,
82
+ inline INTEGER NOT NULL DEFAULT 0,
83
+ saved_to TEXT,
84
+ FOREIGN KEY(message_ref) REFERENCES messages(message_ref) ON DELETE CASCADE
85
+ );
86
+
87
+ CREATE TABLE IF NOT EXISTS provider_locators (
88
+ entity_kind TEXT NOT NULL,
89
+ entity_ref TEXT NOT NULL,
90
+ account_id TEXT NOT NULL,
91
+ provider_key TEXT NOT NULL,
92
+ locator_json TEXT NOT NULL,
93
+ PRIMARY KEY(entity_kind, entity_ref),
94
+ UNIQUE(entity_kind, account_id, provider_key)
95
+ );
96
+
97
+ CREATE TABLE IF NOT EXISTS summaries (
98
+ thread_ref TEXT PRIMARY KEY,
99
+ backend TEXT NOT NULL,
100
+ model TEXT NOT NULL,
101
+ brief TEXT NOT NULL,
102
+ needs_action INTEGER NOT NULL DEFAULT 0,
103
+ importance TEXT NOT NULL,
104
+ generated_at TEXT NOT NULL,
105
+ FOREIGN KEY(thread_ref) REFERENCES threads(thread_ref) ON DELETE CASCADE
106
+ );
107
+ `);
108
+ this.ensureColumn("threads", "participants_json", "TEXT NOT NULL DEFAULT '[]'");
109
+ this.ensureColumn("messages", "invite_json", "TEXT");
110
+ this.ensureProviderLocatorSchema();
111
+ }
112
+ tableColumns(tableName) {
113
+ return this.connection
114
+ .prepare(`PRAGMA table_info(${tableName})`)
115
+ .all().map((column) => column.name);
116
+ }
117
+ ensureColumn(tableName, columnName, definition) {
118
+ if (this.tableColumns(tableName).includes(columnName)) {
119
+ return;
120
+ }
121
+ this.connection.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`);
122
+ }
123
+ ensureProviderLocatorSchema() {
124
+ const columns = this.tableColumns("provider_locators");
125
+ if (columns.includes("account_id") && columns.includes("provider_key")) {
126
+ return;
127
+ }
128
+ this.connection.exec(`
129
+ ALTER TABLE provider_locators RENAME TO provider_locators_legacy;
130
+
131
+ CREATE TABLE provider_locators (
132
+ entity_kind TEXT NOT NULL,
133
+ entity_ref TEXT NOT NULL,
134
+ account_id TEXT NOT NULL,
135
+ provider_key TEXT NOT NULL,
136
+ locator_json TEXT NOT NULL,
137
+ PRIMARY KEY(entity_kind, entity_ref),
138
+ UNIQUE(entity_kind, account_id, provider_key)
139
+ );
140
+ `);
141
+ if (columns.length > 0) {
142
+ this.connection.exec(`
143
+ INSERT OR IGNORE INTO provider_locators (
144
+ entity_kind,
145
+ entity_ref,
146
+ account_id,
147
+ provider_key,
148
+ locator_json
149
+ )
150
+ SELECT
151
+ entity_kind,
152
+ entity_ref,
153
+ '',
154
+ entity_ref,
155
+ locator_json
156
+ FROM provider_locators_legacy;
157
+ `);
158
+ }
159
+ this.connection.exec("DROP TABLE IF EXISTS provider_locators_legacy;");
160
+ }
161
+ upsertAccount(input) {
162
+ const existing = this.findAccountByName(input.name);
163
+ const timestamp = nowIsoUtc();
164
+ if (existing) {
165
+ this.connection
166
+ .prepare(`
167
+ UPDATE accounts
168
+ SET provider = @provider,
169
+ transport = @transport,
170
+ email = @email,
171
+ updated_at = @updated_at
172
+ WHERE name = @name
173
+ `)
174
+ .run({
175
+ name: input.name,
176
+ provider: input.provider,
177
+ transport: input.transport,
178
+ email: input.email,
179
+ updated_at: timestamp,
180
+ });
181
+ return this.findAccountByName(input.name);
182
+ }
183
+ const account = {
184
+ account_id: makeAccountId(),
185
+ name: input.name,
186
+ provider: input.provider,
187
+ transport: input.transport,
188
+ email: input.email,
189
+ created_at: timestamp,
190
+ updated_at: timestamp,
191
+ };
192
+ this.connection
193
+ .prepare(`
194
+ INSERT INTO accounts (
195
+ account_id,
196
+ name,
197
+ provider,
198
+ transport,
199
+ email,
200
+ created_at,
201
+ updated_at
202
+ ) VALUES (
203
+ @account_id,
204
+ @name,
205
+ @provider,
206
+ @transport,
207
+ @email,
208
+ @created_at,
209
+ @updated_at
210
+ )
211
+ `)
212
+ .run(account);
213
+ return account;
214
+ }
215
+ listAccounts() {
216
+ return this.connection
217
+ .prepare(`
218
+ SELECT account_id, name, provider, transport, email, created_at, updated_at
219
+ FROM accounts
220
+ ORDER BY name ASC
221
+ `)
222
+ .all();
223
+ }
224
+ findAccountByName(name) {
225
+ return this.connection
226
+ .prepare(`
227
+ SELECT account_id, name, provider, transport, email, created_at, updated_at
228
+ FROM accounts
229
+ WHERE name = ?
230
+ LIMIT 1
231
+ `)
232
+ .get(name);
233
+ }
234
+ findAccountById(accountId) {
235
+ return this.connection
236
+ .prepare(`
237
+ SELECT account_id, name, provider, transport, email, created_at, updated_at
238
+ FROM accounts
239
+ WHERE account_id = ?
240
+ LIMIT 1
241
+ `)
242
+ .get(accountId);
243
+ }
244
+ removeAccountByName(name) {
245
+ const existing = this.findAccountByName(name);
246
+ if (!existing) {
247
+ return undefined;
248
+ }
249
+ this.connection.prepare("DELETE FROM accounts WHERE name = ?").run(name);
250
+ return existing;
251
+ }
252
+ transaction(work) {
253
+ return this.connection.transaction(work)();
254
+ }
255
+ findEntityRefByProviderKey(entityKind, accountId, providerKey) {
256
+ const row = this.connection
257
+ .prepare(`
258
+ SELECT entity_ref
259
+ FROM provider_locators
260
+ WHERE entity_kind = ?
261
+ AND account_id = ?
262
+ AND provider_key = ?
263
+ LIMIT 1
264
+ `)
265
+ .get(entityKind, accountId, providerKey);
266
+ return row?.entity_ref;
267
+ }
268
+ upsertProviderLocator(input) {
269
+ this.connection
270
+ .prepare(`
271
+ INSERT INTO provider_locators (
272
+ entity_kind,
273
+ entity_ref,
274
+ account_id,
275
+ provider_key,
276
+ locator_json
277
+ ) VALUES (
278
+ @entity_kind,
279
+ @entity_ref,
280
+ @account_id,
281
+ @provider_key,
282
+ @locator_json
283
+ )
284
+ ON CONFLICT(entity_kind, entity_ref) DO UPDATE SET
285
+ account_id = excluded.account_id,
286
+ provider_key = excluded.provider_key,
287
+ locator_json = excluded.locator_json
288
+ `)
289
+ .run(input);
290
+ }
291
+ findProviderLocator(entityKind, entityRef) {
292
+ return this.connection
293
+ .prepare(`
294
+ SELECT entity_kind, entity_ref, account_id, provider_key, locator_json
295
+ FROM provider_locators
296
+ WHERE entity_kind = ?
297
+ AND entity_ref = ?
298
+ LIMIT 1
299
+ `)
300
+ .get(entityKind, entityRef);
301
+ }
302
+ upsertThread(input) {
303
+ const timestamp = nowIsoUtc();
304
+ const existing = this.connection
305
+ .prepare("SELECT thread_ref FROM threads WHERE thread_ref = ? LIMIT 1")
306
+ .get(input.thread_ref);
307
+ if (!existing) {
308
+ this.connection
309
+ .prepare(`
310
+ INSERT INTO threads (
311
+ thread_ref,
312
+ account_id,
313
+ subject,
314
+ mailbox,
315
+ participants_json,
316
+ labels_json,
317
+ received_at,
318
+ message_count,
319
+ unread_count,
320
+ has_attachments,
321
+ first_seen_at,
322
+ last_synced_at
323
+ ) VALUES (
324
+ @thread_ref,
325
+ @account_id,
326
+ @subject,
327
+ @mailbox,
328
+ @participants_json,
329
+ @labels_json,
330
+ @received_at,
331
+ @message_count,
332
+ @unread_count,
333
+ @has_attachments,
334
+ @first_seen_at,
335
+ @last_synced_at
336
+ )
337
+ `)
338
+ .run({
339
+ ...input,
340
+ participants_json: JSON.stringify(input.participants),
341
+ labels_json: JSON.stringify(input.labels),
342
+ has_attachments: input.has_attachments ? 1 : 0,
343
+ first_seen_at: timestamp,
344
+ last_synced_at: timestamp,
345
+ });
346
+ return;
347
+ }
348
+ this.connection
349
+ .prepare(`
350
+ UPDATE threads
351
+ SET subject = @subject,
352
+ mailbox = @mailbox,
353
+ participants_json = @participants_json,
354
+ labels_json = @labels_json,
355
+ received_at = @received_at,
356
+ message_count = @message_count,
357
+ unread_count = @unread_count,
358
+ has_attachments = @has_attachments,
359
+ last_synced_at = @last_synced_at
360
+ WHERE thread_ref = @thread_ref
361
+ `)
362
+ .run({
363
+ ...input,
364
+ participants_json: JSON.stringify(input.participants),
365
+ labels_json: JSON.stringify(input.labels),
366
+ has_attachments: input.has_attachments ? 1 : 0,
367
+ last_synced_at: timestamp,
368
+ });
369
+ }
370
+ upsertMessage(input) {
371
+ const timestamp = nowIsoUtc();
372
+ const existing = this.connection
373
+ .prepare("SELECT message_ref FROM messages WHERE message_ref = ? LIMIT 1")
374
+ .get(input.message_ref);
375
+ const payload = {
376
+ ...input,
377
+ unread: input.unread ? 1 : 0,
378
+ body_cached: input.body_cached ? 1 : 0,
379
+ body_truncated: input.body_truncated ? 1 : 0,
380
+ };
381
+ if (!existing) {
382
+ this.connection
383
+ .prepare(`
384
+ INSERT INTO messages (
385
+ message_ref,
386
+ account_id,
387
+ thread_ref,
388
+ subject,
389
+ from_name,
390
+ from_email,
391
+ to_json,
392
+ cc_json,
393
+ sent_at,
394
+ received_at,
395
+ unread,
396
+ snippet,
397
+ body_cache_path,
398
+ body_cached,
399
+ body_truncated,
400
+ body_cached_bytes,
401
+ invite_json,
402
+ first_seen_at,
403
+ last_synced_at
404
+ ) VALUES (
405
+ @message_ref,
406
+ @account_id,
407
+ @thread_ref,
408
+ @subject,
409
+ @from_name,
410
+ @from_email,
411
+ @to_json,
412
+ @cc_json,
413
+ @sent_at,
414
+ @received_at,
415
+ @unread,
416
+ @snippet,
417
+ @body_cache_path,
418
+ @body_cached,
419
+ @body_truncated,
420
+ @body_cached_bytes,
421
+ @invite_json,
422
+ @first_seen_at,
423
+ @last_synced_at
424
+ )
425
+ `)
426
+ .run({
427
+ ...payload,
428
+ first_seen_at: timestamp,
429
+ last_synced_at: timestamp,
430
+ });
431
+ return;
432
+ }
433
+ this.connection
434
+ .prepare(`
435
+ UPDATE messages
436
+ SET thread_ref = @thread_ref,
437
+ subject = @subject,
438
+ from_name = @from_name,
439
+ from_email = @from_email,
440
+ to_json = @to_json,
441
+ cc_json = @cc_json,
442
+ sent_at = @sent_at,
443
+ received_at = @received_at,
444
+ unread = @unread,
445
+ snippet = @snippet,
446
+ body_cache_path = @body_cache_path,
447
+ body_cached = @body_cached,
448
+ body_truncated = @body_truncated,
449
+ body_cached_bytes = @body_cached_bytes,
450
+ invite_json = @invite_json,
451
+ last_synced_at = @last_synced_at
452
+ WHERE message_ref = @message_ref
453
+ `)
454
+ .run({
455
+ ...payload,
456
+ last_synced_at: timestamp,
457
+ });
458
+ }
459
+ replaceThreadMessages(threadRef, messageRefs) {
460
+ this.connection.prepare("DELETE FROM thread_messages WHERE thread_ref = ?").run(threadRef);
461
+ const insert = this.connection.prepare(`
462
+ INSERT INTO thread_messages (
463
+ thread_ref,
464
+ message_ref,
465
+ position
466
+ ) VALUES (?, ?, ?)
467
+ `);
468
+ for (const [index, messageRef] of messageRefs.entries()) {
469
+ insert.run(threadRef, messageRef, index);
470
+ }
471
+ }
472
+ replaceAttachments(messageRef, attachments) {
473
+ const existingSavedTo = new Map(this.listAttachmentsForMessage(messageRef).map((attachment) => [attachment.attachment_id, attachment.saved_to]));
474
+ this.connection.prepare("DELETE FROM attachments WHERE message_ref = ?").run(messageRef);
475
+ const insert = this.connection.prepare(`
476
+ INSERT INTO attachments (
477
+ attachment_id,
478
+ message_ref,
479
+ filename,
480
+ mime_type,
481
+ size_bytes,
482
+ inline,
483
+ saved_to
484
+ ) VALUES (
485
+ @attachment_id,
486
+ @message_ref,
487
+ @filename,
488
+ @mime_type,
489
+ @size_bytes,
490
+ @inline,
491
+ @saved_to
492
+ )
493
+ `);
494
+ for (const attachment of attachments) {
495
+ insert.run({
496
+ ...attachment,
497
+ message_ref: messageRef,
498
+ inline: attachment.inline ? 1 : 0,
499
+ saved_to: attachment.saved_to ?? existingSavedTo.get(attachment.attachment_id) ?? null,
500
+ });
501
+ }
502
+ }
503
+ updateAttachmentSavedTo(attachmentId, savedTo) {
504
+ this.connection
505
+ .prepare(`
506
+ UPDATE attachments
507
+ SET saved_to = ?
508
+ WHERE attachment_id = ?
509
+ `)
510
+ .run(savedTo, attachmentId);
511
+ }
512
+ upsertSummary(threadRef, summary) {
513
+ this.connection
514
+ .prepare(`
515
+ INSERT INTO summaries (
516
+ thread_ref,
517
+ backend,
518
+ model,
519
+ brief,
520
+ needs_action,
521
+ importance,
522
+ generated_at
523
+ ) VALUES (
524
+ @thread_ref,
525
+ @backend,
526
+ @model,
527
+ @brief,
528
+ @needs_action,
529
+ @importance,
530
+ @generated_at
531
+ )
532
+ ON CONFLICT(thread_ref) DO UPDATE SET
533
+ backend = excluded.backend,
534
+ model = excluded.model,
535
+ brief = excluded.brief,
536
+ needs_action = excluded.needs_action,
537
+ importance = excluded.importance,
538
+ generated_at = excluded.generated_at
539
+ `)
540
+ .run({
541
+ thread_ref: threadRef,
542
+ backend: summary.backend,
543
+ model: summary.model,
544
+ brief: summary.brief,
545
+ needs_action: summary.needs_action ? 1 : 0,
546
+ importance: summary.importance,
547
+ generated_at: nowIsoUtc(),
548
+ });
549
+ }
550
+ findSummary(threadRef) {
551
+ const row = this.connection
552
+ .prepare(`
553
+ SELECT backend, model, brief, needs_action, importance
554
+ FROM summaries
555
+ WHERE thread_ref = ?
556
+ LIMIT 1
557
+ `)
558
+ .get(threadRef);
559
+ if (!row) {
560
+ return null;
561
+ }
562
+ return {
563
+ backend: row.backend,
564
+ model: row.model,
565
+ brief: row.brief,
566
+ needs_action: Boolean(row.needs_action),
567
+ importance: row.importance,
568
+ };
569
+ }
570
+ listAttachmentsForMessage(messageRef) {
571
+ return this.connection
572
+ .prepare(`
573
+ SELECT attachment_id, filename, mime_type, size_bytes, inline, saved_to
574
+ FROM attachments
575
+ WHERE message_ref = ?
576
+ ORDER BY filename ASC
577
+ `)
578
+ .all(messageRef);
579
+ }
580
+ getStoredMessage(messageRef) {
581
+ return this.connection
582
+ .prepare(`
583
+ SELECT
584
+ message_ref,
585
+ account_id,
586
+ thread_ref,
587
+ subject,
588
+ from_name,
589
+ from_email,
590
+ to_json,
591
+ cc_json,
592
+ sent_at,
593
+ received_at,
594
+ unread,
595
+ snippet,
596
+ body_cache_path,
597
+ body_cached,
598
+ body_truncated,
599
+ body_cached_bytes,
600
+ invite_json
601
+ FROM messages
602
+ WHERE message_ref = ?
603
+ LIMIT 1
604
+ `)
605
+ .get(messageRef);
606
+ }
607
+ updateInviteStatusForThread(threadRef, responseStatus) {
608
+ const rows = this.connection
609
+ .prepare(`
610
+ SELECT message_ref, invite_json
611
+ FROM messages
612
+ WHERE thread_ref = ?
613
+ AND invite_json IS NOT NULL
614
+ `)
615
+ .all(threadRef);
616
+ if (rows.length === 0) {
617
+ return;
618
+ }
619
+ const update = this.connection.prepare(`
620
+ UPDATE messages
621
+ SET invite_json = @invite_json,
622
+ last_synced_at = @last_synced_at
623
+ WHERE message_ref = @message_ref
624
+ `);
625
+ const lastSyncedAt = nowIsoUtc();
626
+ for (const row of rows) {
627
+ const invite = JSON.parse(row.invite_json);
628
+ update.run({
629
+ message_ref: row.message_ref,
630
+ invite_json: JSON.stringify({
631
+ ...invite,
632
+ response_status: responseStatus,
633
+ }),
634
+ last_synced_at: lastSyncedAt,
635
+ });
636
+ }
637
+ }
638
+ listMessageRefsForThread(threadRef) {
639
+ return this.connection
640
+ .prepare(`
641
+ SELECT message_ref
642
+ FROM thread_messages
643
+ WHERE thread_ref = ?
644
+ ORDER BY position ASC
645
+ `)
646
+ .all(threadRef).map((row) => row.message_ref);
647
+ }
648
+ markThreadArchived(threadRef) {
649
+ this.connection
650
+ .prepare(`
651
+ UPDATE threads
652
+ SET mailbox = 'archive',
653
+ labels_json = '["archive"]',
654
+ last_synced_at = @last_synced_at
655
+ WHERE thread_ref = @thread_ref
656
+ `)
657
+ .run({
658
+ thread_ref: threadRef,
659
+ last_synced_at: nowIsoUtc(),
660
+ });
661
+ }
662
+ updateMessagesUnreadState(messageRefs, unread) {
663
+ if (messageRefs.length === 0) {
664
+ return;
665
+ }
666
+ const update = this.connection.prepare(`
667
+ UPDATE messages
668
+ SET unread = @unread,
669
+ last_synced_at = @last_synced_at
670
+ WHERE message_ref = @message_ref
671
+ `);
672
+ const lastSyncedAt = nowIsoUtc();
673
+ for (const messageRef of messageRefs) {
674
+ update.run({
675
+ message_ref: messageRef,
676
+ unread: unread ? 1 : 0,
677
+ last_synced_at: lastSyncedAt,
678
+ });
679
+ }
680
+ }
681
+ recomputeThreadUnreadCounts(threadRefs) {
682
+ if (threadRefs.length === 0) {
683
+ return;
684
+ }
685
+ const selectUnreadCount = this.connection.prepare(`
686
+ SELECT COUNT(*) AS unread_count
687
+ FROM messages
688
+ WHERE thread_ref = ?
689
+ AND unread = 1
690
+ `);
691
+ const updateThread = this.connection.prepare(`
692
+ UPDATE threads
693
+ SET unread_count = @unread_count,
694
+ labels_json = @labels_json,
695
+ last_synced_at = @last_synced_at
696
+ WHERE thread_ref = @thread_ref
697
+ `);
698
+ const lastSyncedAt = nowIsoUtc();
699
+ for (const threadRef of new Set(threadRefs)) {
700
+ const row = selectUnreadCount.get(threadRef);
701
+ const unreadCount = row?.unread_count ?? 0;
702
+ updateThread.run({
703
+ thread_ref: threadRef,
704
+ unread_count: unreadCount,
705
+ labels_json: JSON.stringify(unreadCount > 0 ? ["inbox", "unread"] : ["inbox"]),
706
+ last_synced_at: lastSyncedAt,
707
+ });
708
+ }
709
+ }
710
+ findMessageByRef(messageRef) {
711
+ return this.connection
712
+ .prepare(`
713
+ SELECT message_ref, thread_ref, account_id
714
+ FROM messages
715
+ WHERE message_ref = ?
716
+ LIMIT 1
717
+ `)
718
+ .get(messageRef);
719
+ }
720
+ findAttachmentById(attachmentId) {
721
+ return this.connection
722
+ .prepare(`
723
+ SELECT attachment_id, message_ref
724
+ FROM attachments
725
+ WHERE attachment_id = ?
726
+ LIMIT 1
727
+ `)
728
+ .get(attachmentId);
729
+ }
730
+ }
731
+ //# sourceMappingURL=database.js.map