@wirecat/cli-core 0.487.0 → 0.489.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/CHANGELOG.md +108 -1
- package/dist/calendar/cli/index.js +48 -13
- package/dist/calendar/index.d.ts +1 -1
- package/dist/calendar/mcp/index.d.ts +390 -0
- package/dist/calendar/mcp/index.js +50 -12
- package/dist/calendar/reads.d.ts +9 -2
- package/dist/calendar/reads.js +60 -5
- package/dist/calendar/recurrence.d.ts +20 -0
- package/dist/calendar/recurrence.js +74 -0
- package/dist/calendar/sync.d.ts +6 -0
- package/dist/calendar/sync.js +123 -41
- package/dist/calendar/testing/memory.js +90 -7
- package/dist/calendar/testing/sync-contract.js +90 -0
- package/dist/calendar/testing/writer-contract.d.ts +2 -6
- package/dist/calendar/testing/writer-contract.js +157 -29
- package/dist/calendar/testing/writer.d.ts +3 -1
- package/dist/calendar/testing/writer.js +11 -0
- package/dist/calendar/writer.d.ts +20 -3
- package/dist/calendar/writer.js +8 -4
- package/dist/calendar/writes.d.ts +21 -12
- package/dist/calendar/writes.js +259 -20
- package/dist/errors/reasons.d.ts +1 -1
- package/dist/errors/reasons.js +5 -0
- package/dist/mail/attachments.d.ts +15 -0
- package/dist/mail/attachments.js +38 -0
- package/dist/mail/locations.d.ts +28 -0
- package/dist/mail/locations.js +54 -0
- package/dist/mail/quick.d.ts +3 -1
- package/dist/mail/quick.js +9 -6
- package/dist/mail/resume.d.ts +6 -0
- package/dist/mail/retry.d.ts +23 -0
- package/dist/mail/retry.js +110 -0
- package/dist/mail/sync.d.ts +2 -3
- package/dist/mail/sync.js +85 -152
- package/dist/mail/testing/fake.d.ts +2 -0
- package/dist/mail/testing/fake.js +1 -1
- package/dist/meetings/event-store.d.ts +80 -2
- package/dist/meetings/event-store.js +65 -1
- package/dist/meetings/index.d.ts +2 -1
- package/dist/meetings/index.js +1 -0
- package/dist/meetings/testing/event-store-contract.d.ts +1 -1
- package/dist/meetings/testing/event-store-contract.js +114 -0
- package/dist/store/drivers/node-sqlite.d.ts +9 -1
- package/dist/store/drivers/node-sqlite.js +30 -8
- package/dist/store/index.d.ts +1 -1
- package/dist/store/sqlite/accounts.js +1 -0
- package/dist/store/sqlite/calendar-events.d.ts +4 -0
- package/dist/store/sqlite/calendar-events.js +89 -0
- package/dist/store/sqlite/calendar.js +126 -94
- package/dist/store/sqlite/email-locations.d.ts +24 -0
- package/dist/store/sqlite/email-locations.js +56 -0
- package/dist/store/sqlite/email-state.d.ts +2 -0
- package/dist/store/sqlite/email-state.js +28 -0
- package/dist/store/sqlite/emails.d.ts +13 -0
- package/dist/store/sqlite/emails.js +9 -29
- package/dist/store/sqlite/manifest.js +1 -0
- package/dist/store/sqlite/migrations.generated.js +7 -0
- package/dist/store/sqlite/schema.d.ts +72 -0
- package/dist/store/sqlite/schema.js +13 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
-
import { calendarText, entryMetadata, joinKeyOf, joinUrlWithoutQuery, } from "../../meetings/index.js";
|
|
2
|
+
import { calendarText, entryInWindow, entryMetadata, joinKeyOf, joinUrlWithoutQuery, } from "../../meetings/index.js";
|
|
3
|
+
import { listEvents } from "./calendar-events.js";
|
|
3
4
|
import { bool, eventOf, fromJson, int, str, toJson } from "./events.js";
|
|
4
5
|
import { driveDocumentOf, driveFileTarget, logFileLink } from "./files.js";
|
|
5
6
|
import { identityOf } from "./identities.js";
|
|
@@ -51,6 +52,8 @@ const entryOf = (row) => ({
|
|
|
51
52
|
etag: str(row.etag),
|
|
52
53
|
seenAt: int(row.seen_at),
|
|
53
54
|
deletedAt: int(row.deleted_at),
|
|
55
|
+
createdAt: Number(row.created_at),
|
|
56
|
+
transparency: str(row.transparency),
|
|
54
57
|
});
|
|
55
58
|
const participantOf = (row) => ({
|
|
56
59
|
entryId: Number(row.entry_id),
|
|
@@ -79,6 +82,7 @@ const reviewOf = (row) => ({
|
|
|
79
82
|
id: Number(row.id),
|
|
80
83
|
linkableType: String(row.linkable_type),
|
|
81
84
|
linkableId: Number(row.linkable_id),
|
|
85
|
+
calendarId: int(row.calendar_id),
|
|
82
86
|
entityType: String(row.entity_type),
|
|
83
87
|
candidates: fromJson(row.candidates),
|
|
84
88
|
reason: String(row.reason),
|
|
@@ -94,6 +98,8 @@ const attachmentOf = (row) => ({
|
|
|
94
98
|
title: str(fromJson(row.metadata)?.title),
|
|
95
99
|
documentId: int(row.to_id),
|
|
96
100
|
});
|
|
101
|
+
const REVIEWS = "SELECT r.*, CASE WHEN r.linkable_type = 'calendar_entry' THEN " +
|
|
102
|
+
"(SELECT calendar_id FROM calendar_entries WHERE id = r.linkable_id) END AS calendar_id FROM link_reviews r";
|
|
97
103
|
const TRACKED = ["title", "starts_at", "ends_at", "location", "status", "recurrence", "recurrence_ends_at"];
|
|
98
104
|
const flag = (value) => (value ? 1 : 0);
|
|
99
105
|
const cursorKey = (calendarId) => `calendar:${calendarId}`;
|
|
@@ -448,6 +454,56 @@ export const eventStoreOver = (context) => {
|
|
|
448
454
|
refreshEvent(Number(row.event_id), now);
|
|
449
455
|
}
|
|
450
456
|
};
|
|
457
|
+
const saveAll = (calendarId, entries, now) => {
|
|
458
|
+
calendarRow(calendarId);
|
|
459
|
+
// A series first, so an occurrence in the same page finds it.
|
|
460
|
+
const ordered = [...entries].sort((a, b) => Number(b.kind === "series") - Number(a.kind === "series"));
|
|
461
|
+
const ids = new Map();
|
|
462
|
+
const saved = ordered.map((input) => {
|
|
463
|
+
const entry = saveEntry(calendarId, input, now);
|
|
464
|
+
ids.set(input, entry.id);
|
|
465
|
+
return entry;
|
|
466
|
+
});
|
|
467
|
+
for (const entry of saved)
|
|
468
|
+
if (entry.kind === "series" && entry.status === "cancelled")
|
|
469
|
+
cancelOccurrences(entry.id, now);
|
|
470
|
+
const current = saved.map(({ id }) => entryOf(entryRow(id)));
|
|
471
|
+
for (const entry of current)
|
|
472
|
+
link(entry, now);
|
|
473
|
+
refreshAround(current.map(({ id }) => id), now);
|
|
474
|
+
return entries.map((input) => entryOf(entryRow(ids.get(input))));
|
|
475
|
+
};
|
|
476
|
+
const removeAll = (calendarId, externalIds, now) => {
|
|
477
|
+
calendarRow(calendarId);
|
|
478
|
+
const removed = database
|
|
479
|
+
.prepare(`SELECT * FROM calendar_entries WHERE calendar_id = ? AND external_id IN (SELECT value FROM json_each(?))
|
|
480
|
+
AND status <> 'cancelled' AND deleted_at IS NULL`)
|
|
481
|
+
.all(calendarId, JSON.stringify(externalIds));
|
|
482
|
+
for (const row of removed) {
|
|
483
|
+
cancel(row, now);
|
|
484
|
+
database.prepare("UPDATE calendar_entries SET seen_at = ? WHERE id = ?").run(now, Number(row.id));
|
|
485
|
+
if (row.kind === "series")
|
|
486
|
+
cancelOccurrences(Number(row.id), now);
|
|
487
|
+
}
|
|
488
|
+
refreshAround(removed.map((row) => Number(row.id)), now);
|
|
489
|
+
return removed.length;
|
|
490
|
+
};
|
|
491
|
+
const putCursor = (calendarId, value, now) => {
|
|
492
|
+
const accountId = Number(calendarRow(calendarId).account_id);
|
|
493
|
+
if (value === null)
|
|
494
|
+
database
|
|
495
|
+
.prepare("DELETE FROM sync_cursors WHERE account_id = ? AND key = ?")
|
|
496
|
+
.run(accountId, cursorKey(calendarId));
|
|
497
|
+
else
|
|
498
|
+
database
|
|
499
|
+
.prepare(`INSERT INTO sync_cursors (account_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?)
|
|
500
|
+
ON CONFLICT (account_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`)
|
|
501
|
+
.run(accountId, cursorKey(calendarId), value, now, now);
|
|
502
|
+
};
|
|
503
|
+
const slotRows = (calendarId, series, window) => database
|
|
504
|
+
.prepare(`SELECT * FROM calendar_entries WHERE calendar_id = ? AND kind = 'occurrence' AND series_external_id = ?
|
|
505
|
+
AND original_starts_at >= ? AND original_starts_at < ? AND deleted_at IS NULL ORDER BY original_starts_at`)
|
|
506
|
+
.all(calendarId, series, window.from, window.to);
|
|
451
507
|
return {
|
|
452
508
|
saveCalendars: async (accountId, calendars, now) => write(database, () => {
|
|
453
509
|
const upsert = database.prepare(`INSERT INTO calendars (account_id, external_id, title, timezone, access_role, primary_calendar, synced, seen_at,
|
|
@@ -468,115 +524,72 @@ export const eventStoreOver = (context) => {
|
|
|
468
524
|
.prepare("UPDATE calendars SET synced = ?, updated_at = ? WHERE id = ?")
|
|
469
525
|
.run(flag(synced), now, calendarId);
|
|
470
526
|
}),
|
|
471
|
-
saveEntries: async (calendarId, entries, now) => write(database, () =>
|
|
472
|
-
|
|
473
|
-
// A series first, so an occurrence in the same page finds it.
|
|
474
|
-
const ordered = [...entries].sort((a, b) => Number(b.kind === "series") - Number(a.kind === "series"));
|
|
475
|
-
const ids = new Map();
|
|
476
|
-
const saved = ordered.map((input) => {
|
|
477
|
-
const entry = saveEntry(calendarId, input, now);
|
|
478
|
-
ids.set(input, entry.id);
|
|
479
|
-
return entry;
|
|
480
|
-
});
|
|
481
|
-
for (const entry of saved)
|
|
482
|
-
if (entry.kind === "series" && entry.status === "cancelled")
|
|
483
|
-
cancelOccurrences(entry.id, now);
|
|
484
|
-
const current = saved.map(({ id }) => entryOf(entryRow(id)));
|
|
485
|
-
for (const entry of current)
|
|
486
|
-
link(entry, now);
|
|
487
|
-
refreshAround(current.map(({ id }) => id), now);
|
|
488
|
-
return entries.map((input) => entryOf(entryRow(ids.get(input))));
|
|
489
|
-
}),
|
|
490
|
-
removeEntries: async (calendarId, externalIds, now) => write(database, () => {
|
|
491
|
-
calendarRow(calendarId);
|
|
492
|
-
const removed = database
|
|
493
|
-
.prepare(`SELECT * FROM calendar_entries WHERE calendar_id = ? AND external_id IN (SELECT value FROM json_each(?))
|
|
494
|
-
AND status <> 'cancelled' AND deleted_at IS NULL`)
|
|
495
|
-
.all(calendarId, JSON.stringify(externalIds));
|
|
496
|
-
for (const row of removed) {
|
|
497
|
-
cancel(row, now);
|
|
498
|
-
database.prepare("UPDATE calendar_entries SET seen_at = ? WHERE id = ?").run(now, Number(row.id));
|
|
499
|
-
if (row.kind === "series")
|
|
500
|
-
cancelOccurrences(Number(row.id), now);
|
|
501
|
-
}
|
|
502
|
-
refreshAround(removed.map((row) => Number(row.id)), now);
|
|
503
|
-
return removed.length;
|
|
504
|
-
}),
|
|
527
|
+
saveEntries: async (calendarId, entries, now) => write(database, () => saveAll(calendarId, entries, now)),
|
|
528
|
+
removeEntries: async (calendarId, externalIds, now) => write(database, () => removeAll(calendarId, externalIds, now)),
|
|
505
529
|
markUnseen: async (calendarId, seenBefore, window, now) => write(database, () => {
|
|
506
530
|
const marked = database
|
|
507
|
-
.prepare(`
|
|
508
|
-
AND (
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
.
|
|
531
|
+
.prepare(`SELECT * FROM calendar_entries WHERE calendar_id = ? AND deleted_at IS NULL AND (seen_at IS NULL OR seen_at < ?)
|
|
532
|
+
AND min(coalesce(starts_at, original_starts_at), coalesce(original_starts_at, starts_at)) < ? AND (kind = 'series'
|
|
533
|
+
OR max(coalesce(ends_at, 0), coalesce(starts_at, 0) + 1, coalesce(original_starts_at, 0)) >= ?)`)
|
|
534
|
+
.all(calendarId, seenBefore, window.to, window.from)
|
|
535
|
+
.filter((row) => entryInWindow(entryOf(row), window));
|
|
536
|
+
const mark = database.prepare("UPDATE calendar_entries SET deleted_at = ?, updated_at = ? WHERE id = ?");
|
|
537
|
+
for (const row of marked)
|
|
538
|
+
mark.run(now, now, Number(row.id));
|
|
512
539
|
for (const eventId of new Set(marked.map((row) => int(row.event_id))))
|
|
513
540
|
if (eventId !== null)
|
|
514
541
|
refreshEvent(eventId, now);
|
|
515
542
|
return marked.length;
|
|
516
543
|
}),
|
|
544
|
+
restoreUnseen: async (calendarId, windowAt, now) => write(database, () => {
|
|
545
|
+
const restored = database
|
|
546
|
+
.prepare("SELECT * FROM calendar_entries WHERE calendar_id = ? AND deleted_at IS NOT NULL")
|
|
547
|
+
.all(calendarId)
|
|
548
|
+
.map(entryOf)
|
|
549
|
+
.filter((entry) => !entryInWindow(entry, windowAt(entry.deletedAt)));
|
|
550
|
+
const restore = database.prepare("UPDATE calendar_entries SET deleted_at = NULL, updated_at = ? WHERE id = ?");
|
|
551
|
+
for (const { id } of restored)
|
|
552
|
+
restore.run(now, id);
|
|
553
|
+
refreshAround(restored.map(({ id }) => id), now);
|
|
554
|
+
return restored.length;
|
|
555
|
+
}),
|
|
517
556
|
entry: async (calendarId, externalId) => read(database, () => {
|
|
518
557
|
const row = database
|
|
519
558
|
.prepare("SELECT * FROM calendar_entries WHERE calendar_id = ? AND external_id = ?")
|
|
520
559
|
.get(calendarId, externalId);
|
|
521
560
|
return row ? entryOf(row) : null;
|
|
522
561
|
}),
|
|
562
|
+
slotsOf: async (calendarId, seriesExternalId, window) => read(database, () => slotRows(calendarId, seriesExternalId, window).map(entryOf)),
|
|
563
|
+
saveSlots: async (calendarId, groups, window, cursor, now) => write(database, () => {
|
|
564
|
+
let [saved, removed] = [0, 0];
|
|
565
|
+
const seen = database.prepare("UPDATE calendar_entries SET seen_at = ? WHERE id = ?");
|
|
566
|
+
for (const { series, slots } of groups) {
|
|
567
|
+
const held = slotRows(calendarId, series, window);
|
|
568
|
+
if (slots === null) {
|
|
569
|
+
for (const row of held)
|
|
570
|
+
seen.run(now, Number(row.id));
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (slots.length)
|
|
574
|
+
saved += saveAll(calendarId, slots, now).length;
|
|
575
|
+
const given = new Set(slots.map(({ externalId }) => externalId));
|
|
576
|
+
const stale = held
|
|
577
|
+
.filter((row) => Number(row.exception) !== 1 && row.status !== "cancelled" && !given.has(String(row.external_id)))
|
|
578
|
+
.map((row) => String(row.external_id));
|
|
579
|
+
if (stale.length)
|
|
580
|
+
removed += removeAll(calendarId, stale, now);
|
|
581
|
+
}
|
|
582
|
+
putCursor(calendarId, cursor, now);
|
|
583
|
+
return { saved, removed };
|
|
584
|
+
}),
|
|
523
585
|
cursor: async (calendarId) => read(database, () => {
|
|
524
586
|
const { account_id } = calendarRow(calendarId);
|
|
525
587
|
return str(database
|
|
526
588
|
.prepare("SELECT value FROM sync_cursors WHERE account_id = ? AND key = ?")
|
|
527
589
|
.get(Number(account_id), cursorKey(calendarId))?.value);
|
|
528
590
|
}),
|
|
529
|
-
setCursor: async (calendarId, value, now) => write(database, () =>
|
|
530
|
-
|
|
531
|
-
if (value === null)
|
|
532
|
-
database
|
|
533
|
-
.prepare("DELETE FROM sync_cursors WHERE account_id = ? AND key = ?")
|
|
534
|
-
.run(accountId, cursorKey(calendarId));
|
|
535
|
-
else
|
|
536
|
-
database
|
|
537
|
-
.prepare(`INSERT INTO sync_cursors (account_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?)
|
|
538
|
-
ON CONFLICT (account_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`)
|
|
539
|
-
.run(accountId, cursorKey(calendarId), value, now, now);
|
|
540
|
-
}),
|
|
541
|
-
events: async (filter = {}) => {
|
|
542
|
-
const clauses = ["e.deleted_at IS NULL"];
|
|
543
|
-
const params = [];
|
|
544
|
-
const entries = (extra) => `EXISTS (SELECT 1 FROM calendar_entries ce JOIN calendars c ON c.id = ce.calendar_id WHERE ce.event_id = e.id AND ${extra})`;
|
|
545
|
-
const dated = (column, date, instant, at) => {
|
|
546
|
-
if (at === undefined)
|
|
547
|
-
return;
|
|
548
|
-
if (date === undefined) {
|
|
549
|
-
clauses.push(instant);
|
|
550
|
-
params.push(at);
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
clauses.push(`(CASE WHEN e.all_day = 1 THEN EXISTS (SELECT 1 FROM calendar_entries ce WHERE ce.event_id = e.id AND ce.${column}) ELSE ${instant} END)`);
|
|
554
|
-
params.push(date, at);
|
|
555
|
-
};
|
|
556
|
-
dated("end_date > ?", filter.fromDate, "coalesce(e.ends_at, e.starts_at + 1) > ?", filter.from);
|
|
557
|
-
dated("start_date < ?", filter.toDate, "e.starts_at < ?", filter.to);
|
|
558
|
-
if (filter.calendarId !== undefined) {
|
|
559
|
-
clauses.push(entries("ce.calendar_id = ?"));
|
|
560
|
-
params.push(filter.calendarId);
|
|
561
|
-
}
|
|
562
|
-
if (filter.accountId !== undefined) {
|
|
563
|
-
clauses.push(entries("c.account_id = ?"));
|
|
564
|
-
params.push(filter.accountId);
|
|
565
|
-
}
|
|
566
|
-
if (filter.personId !== undefined) {
|
|
567
|
-
clauses.push(entries("EXISTS (SELECT 1 FROM calendar_entry_participants p JOIN identity_links il ON il.identity_id = p.identity_id " +
|
|
568
|
-
"WHERE p.entry_id = ce.id AND il.person_id = ?)"));
|
|
569
|
-
params.push(filter.personId);
|
|
570
|
-
}
|
|
571
|
-
const limit = filter.limit ?? 100;
|
|
572
|
-
const offset = filter.offset ?? 0;
|
|
573
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > 1000 || !Number.isInteger(offset) || offset < 0)
|
|
574
|
-
throw new CliError("validation_error", "limit takes 1–1000 and offset 0 or more");
|
|
575
|
-
return database
|
|
576
|
-
.prepare(`SELECT e.* FROM events e WHERE ${clauses.join(" AND ")} ORDER BY ${filter.newest ? "e.starts_at DESC, e.id DESC" : "e.starts_at, e.id"} LIMIT ? OFFSET ?`)
|
|
577
|
-
.all(...params, limit, offset)
|
|
578
|
-
.map(eventOf);
|
|
579
|
-
},
|
|
591
|
+
setCursor: async (calendarId, value, now) => write(database, () => putCursor(calendarId, value, now)),
|
|
592
|
+
events: async (filter = {}) => listEvents(database, filter),
|
|
580
593
|
event: async (id) => read(database, () => {
|
|
581
594
|
const row = database.prepare("SELECT * FROM events WHERE id = ?").get(id);
|
|
582
595
|
if (!row)
|
|
@@ -605,6 +618,25 @@ export const eventStoreOver = (context) => {
|
|
|
605
618
|
.map(attachmentOf),
|
|
606
619
|
};
|
|
607
620
|
}),
|
|
621
|
+
historyOf: async (eventIds, calendarIds, since) => read(database, () => {
|
|
622
|
+
const entries = database
|
|
623
|
+
.prepare(`SELECT * FROM calendar_entries WHERE event_id IN (SELECT value FROM json_each(?))
|
|
624
|
+
AND calendar_id IN (SELECT value FROM json_each(?)) ORDER BY id`)
|
|
625
|
+
.all(JSON.stringify(eventIds), JSON.stringify(calendarIds))
|
|
626
|
+
.map(entryOf);
|
|
627
|
+
const revisions = database
|
|
628
|
+
.prepare(`SELECT * FROM calendar_entry_revisions WHERE entry_id IN (SELECT value FROM json_each(?)) AND created_at >= ?
|
|
629
|
+
ORDER BY created_at, id`)
|
|
630
|
+
.all(JSON.stringify(entries.map(({ id }) => id)), since)
|
|
631
|
+
.map(revisionOf);
|
|
632
|
+
return { entries, revisions };
|
|
633
|
+
}),
|
|
634
|
+
participantsOf: async (eventIds, calendarIds) => read(database, () => database
|
|
635
|
+
.prepare(`SELECT p.*, ce.event_id FROM calendar_entry_participants p JOIN calendar_entries ce ON ce.id = p.entry_id
|
|
636
|
+
WHERE ce.event_id IN (SELECT value FROM json_each(?)) AND ce.calendar_id IN (SELECT value FROM json_each(?))
|
|
637
|
+
ORDER BY ce.event_id, p.entry_id, p.email`)
|
|
638
|
+
.all(JSON.stringify(eventIds), JSON.stringify(calendarIds))
|
|
639
|
+
.map((row) => ({ ...participantOf(row), eventId: Number(row.event_id) }))),
|
|
608
640
|
linkEntry: async (entryId, eventId, now) => write(database, () => {
|
|
609
641
|
const row = database.prepare("SELECT * FROM calendar_entries WHERE id = ?").get(entryId);
|
|
610
642
|
if (!row)
|
|
@@ -636,11 +668,11 @@ export const eventStoreOver = (context) => {
|
|
|
636
668
|
});
|
|
637
669
|
}),
|
|
638
670
|
reviews: async (filter = {}) => database
|
|
639
|
-
.prepare(
|
|
671
|
+
.prepare(`${REVIEWS}${filter.status ? " WHERE r.status = ?" : ""} ORDER BY r.created_at, r.id`)
|
|
640
672
|
.all(...(filter.status ? [filter.status] : []))
|
|
641
673
|
.map(reviewOf),
|
|
642
674
|
resolveReview: async (id, decision, now) => write(database, () => {
|
|
643
|
-
const row = database.prepare(
|
|
675
|
+
const row = database.prepare(`${REVIEWS} WHERE r.id = ?`).get(id);
|
|
644
676
|
if (!row)
|
|
645
677
|
throw new CliError("not_found", `no review ${id} in the local store`);
|
|
646
678
|
const review = reviewOf(row);
|
|
@@ -710,7 +742,7 @@ export const eventStoreOver = (context) => {
|
|
|
710
742
|
database
|
|
711
743
|
.prepare("UPDATE link_reviews SET status = ?, chosen_id = ?, decided_by = 'owner', decided_at = ?, updated_at = ? WHERE id = ?")
|
|
712
744
|
.run(chosen === null ? "apart" : "linked", chosen, now, now, id);
|
|
713
|
-
return reviewOf(database.prepare(
|
|
745
|
+
return reviewOf(database.prepare(`${REVIEWS} WHERE r.id = ?`).get(id));
|
|
714
746
|
}),
|
|
715
747
|
};
|
|
716
748
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CacheDatabase } from "../driver.js";
|
|
2
|
+
/** Where an IMAP server holds an email: `folder` is the mailbox's external id. */
|
|
3
|
+
export interface EmailLocationInput {
|
|
4
|
+
folder: string;
|
|
5
|
+
uidvalidity: string;
|
|
6
|
+
uid: number;
|
|
7
|
+
}
|
|
8
|
+
/** One email of a folder as a sync sees it there. */
|
|
9
|
+
export interface FolderEmail {
|
|
10
|
+
uid: number;
|
|
11
|
+
externalId: string;
|
|
12
|
+
receivedAt: number | null;
|
|
13
|
+
read: boolean | null;
|
|
14
|
+
flagged: boolean | null;
|
|
15
|
+
/** Its locations in every folder, and the folders it is a member of: an email is gone only when all are lost. */
|
|
16
|
+
locations: number;
|
|
17
|
+
folders: number;
|
|
18
|
+
}
|
|
19
|
+
export declare const locate: (database: CacheDatabase, accountId: number, emailId: number, where: EmailLocationInput) => {
|
|
20
|
+
changes: number;
|
|
21
|
+
};
|
|
22
|
+
export declare const folderEmails: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string) => FolderEmail[];
|
|
23
|
+
export declare const setLocations: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string, found: ReadonlyMap<string, number[]>) => number;
|
|
24
|
+
export declare const forgetLocations: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string, uids: number[]) => void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const locate = (database, accountId, emailId, where) => database
|
|
2
|
+
.prepare(`INSERT OR REPLACE INTO email_locations (mailbox_id, uidvalidity, uid, email_id)
|
|
3
|
+
SELECT id, ?, ?, ? FROM mailboxes WHERE account_id = ? AND external_id = ?`)
|
|
4
|
+
.run(where.uidvalidity, where.uid, emailId, accountId, where.folder);
|
|
5
|
+
export const folderEmails = (database, accountId, folder, uidvalidity) => database
|
|
6
|
+
.prepare(`SELECT l.uid, e.external_id, coalesce(e.received_at, e.sent_at) AS at, e.read, e.flagged,
|
|
7
|
+
(SELECT count(*) FROM email_locations x WHERE x.email_id = e.id) AS locations,
|
|
8
|
+
(SELECT count(*) FROM email_mailboxes m JOIN mailboxes b ON b.id = m.mailbox_id
|
|
9
|
+
WHERE m.email_id = e.id AND b.kind IS NULL) AS folders
|
|
10
|
+
FROM email_locations l JOIN emails e ON e.id = l.email_id
|
|
11
|
+
WHERE l.mailbox_id = (SELECT id FROM mailboxes WHERE account_id = ? AND external_id = ?)
|
|
12
|
+
AND l.uidvalidity = ? AND e.deleted_at IS NULL
|
|
13
|
+
ORDER BY l.uid`)
|
|
14
|
+
.all(accountId, folder, uidvalidity)
|
|
15
|
+
.map((row) => ({
|
|
16
|
+
uid: Number(row.uid),
|
|
17
|
+
externalId: String(row.external_id),
|
|
18
|
+
receivedAt: row.at === null ? null : Number(row.at),
|
|
19
|
+
read: row.read === null ? null : Boolean(row.read),
|
|
20
|
+
flagged: row.flagged === null ? null : Boolean(row.flagged),
|
|
21
|
+
locations: Number(row.locations),
|
|
22
|
+
folders: Number(row.folders),
|
|
23
|
+
}));
|
|
24
|
+
export const setLocations = (database, accountId, folder, uidvalidity, found) => {
|
|
25
|
+
database
|
|
26
|
+
.prepare(`DELETE FROM email_locations WHERE uidvalidity <> ?
|
|
27
|
+
AND mailbox_id = (SELECT id FROM mailboxes WHERE account_id = ? AND external_id = ?)`)
|
|
28
|
+
.run(uidvalidity, accountId, folder);
|
|
29
|
+
const lookup = database.prepare("SELECT id FROM emails WHERE account_id = ? AND external_id = ?");
|
|
30
|
+
let count = 0;
|
|
31
|
+
for (const [externalId, uids] of found) {
|
|
32
|
+
const row = lookup.get(accountId, externalId);
|
|
33
|
+
if (!row)
|
|
34
|
+
continue;
|
|
35
|
+
for (const uid of uids)
|
|
36
|
+
locate(database, accountId, Number(row.id), { folder, uidvalidity, uid });
|
|
37
|
+
count++;
|
|
38
|
+
}
|
|
39
|
+
return count;
|
|
40
|
+
};
|
|
41
|
+
export const forgetLocations = (database, accountId, folder, uidvalidity, uids) => {
|
|
42
|
+
const box = database
|
|
43
|
+
.prepare("SELECT id FROM mailboxes WHERE account_id = ? AND external_id = ?")
|
|
44
|
+
.get(accountId, folder);
|
|
45
|
+
if (!box)
|
|
46
|
+
return;
|
|
47
|
+
const emails = database
|
|
48
|
+
.prepare(`DELETE FROM email_locations WHERE mailbox_id = ? AND uidvalidity = ?
|
|
49
|
+
AND uid IN (SELECT value FROM json_each(?)) RETURNING email_id`)
|
|
50
|
+
.all(Number(box.id), uidvalidity, JSON.stringify(uids))
|
|
51
|
+
.map((row) => Number(row.email_id));
|
|
52
|
+
const leave = database.prepare(`DELETE FROM email_mailboxes WHERE email_id = ? AND mailbox_id = ?
|
|
53
|
+
AND NOT EXISTS (SELECT 1 FROM email_locations WHERE email_id = ? AND mailbox_id = ?)`);
|
|
54
|
+
for (const emailId of new Set(emails))
|
|
55
|
+
leave.run(emailId, Number(box.id), emailId, Number(box.id));
|
|
56
|
+
};
|
|
@@ -6,3 +6,5 @@ export declare const setEmailFlags: (database: CacheDatabase, accountId: number,
|
|
|
6
6
|
read?: boolean;
|
|
7
7
|
flagged?: boolean;
|
|
8
8
|
}>, now: number) => number;
|
|
9
|
+
export declare const markEmailsDeleted: (database: CacheDatabase, accountId: number, externalIds: string[], now: number) => number;
|
|
10
|
+
export declare const markEmailsRestored: (database: CacheDatabase, accountId: number, externalIds: string[], now: number) => number;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { dropAttachmentChunks } from "./email-index.js";
|
|
1
2
|
import { flag } from "./events.js";
|
|
3
|
+
import { dropUnusedVectors } from "./vector-index.js";
|
|
2
4
|
export const recountEmailThread = (database, threadId, now) => database
|
|
3
5
|
.prepare(`UPDATE email_threads SET
|
|
4
6
|
emails_count = (SELECT count(*) FROM emails WHERE email_thread_id = ? AND deleted_at IS NULL),
|
|
@@ -23,3 +25,29 @@ export const setEmailFlags = (database, accountId, flags, now) => {
|
|
|
23
25
|
}
|
|
24
26
|
return changed;
|
|
25
27
|
};
|
|
28
|
+
export const markEmailsDeleted = (database, accountId, externalIds, now) => {
|
|
29
|
+
const mark = database.prepare(`UPDATE emails SET deleted_at = ?, body_text = NULL, body_html = NULL, snippet = NULL, updated_at = ?
|
|
30
|
+
WHERE account_id = ? AND external_id = ? AND deleted_at IS NULL
|
|
31
|
+
RETURNING id, email_thread_id`);
|
|
32
|
+
const forget = database.prepare("UPDATE attachments SET text = NULL, normalized_text = NULL WHERE attachable_type = 'email' AND attachable_id = ?");
|
|
33
|
+
const unlocate = database.prepare("DELETE FROM email_locations WHERE email_id = ?");
|
|
34
|
+
const threads = externalIds.flatMap((id) => mark.all(now, now, accountId, id).map((r) => {
|
|
35
|
+
const pieces = dropAttachmentChunks(database, Number(r.id));
|
|
36
|
+
forget.run(Number(r.id));
|
|
37
|
+
unlocate.run(Number(r.id));
|
|
38
|
+
dropUnusedVectors(database, pieces);
|
|
39
|
+
return Number(r.email_thread_id);
|
|
40
|
+
}));
|
|
41
|
+
for (const id of new Set(threads))
|
|
42
|
+
recountEmailThread(database, id, now);
|
|
43
|
+
return threads.length;
|
|
44
|
+
};
|
|
45
|
+
export const markEmailsRestored = (database, accountId, externalIds, now) => {
|
|
46
|
+
const unmark = database.prepare(`UPDATE emails SET deleted_at = NULL, updated_at = ?
|
|
47
|
+
WHERE account_id = ? AND external_id = ? AND deleted_at IS NOT NULL
|
|
48
|
+
RETURNING email_thread_id`);
|
|
49
|
+
const threads = externalIds.flatMap((id) => unmark.all(now, accountId, id).map((r) => Number(r.email_thread_id)));
|
|
50
|
+
for (const id of new Set(threads))
|
|
51
|
+
recountEmailThread(database, id, now);
|
|
52
|
+
return threads.length;
|
|
53
|
+
};
|
|
@@ -3,9 +3,11 @@ import type { Attachment, Metadata } from "../../meetings/index.js";
|
|
|
3
3
|
import type { Stemmer, Stemmers } from "../../search/stem.js";
|
|
4
4
|
import type { CacheDatabase } from "../driver.js";
|
|
5
5
|
import type { ConversationSummary, StoredHit } from "../types.js";
|
|
6
|
+
import { type EmailLocationInput, type FolderEmail } from "./email-locations.js";
|
|
6
7
|
import { type MailRulesStore } from "./mail-rules.js";
|
|
7
8
|
import { type AttachmentInput } from "./meetings.js";
|
|
8
9
|
import type { StoreContext } from "./open.js";
|
|
10
|
+
export type { EmailLocationInput, FolderEmail };
|
|
9
11
|
export interface EmailAddress {
|
|
10
12
|
address: string;
|
|
11
13
|
name: string | null;
|
|
@@ -43,6 +45,8 @@ export interface EmailInput {
|
|
|
43
45
|
mailRuleId?: number | null;
|
|
44
46
|
/** Added to the mailboxes the email is already in; one scan sees one folder, so none is taken away. */
|
|
45
47
|
mailboxes?: MailboxInput[];
|
|
48
|
+
/** Added to the locations the email has; a UID already known in that folder now points here. */
|
|
49
|
+
locations?: EmailLocationInput[];
|
|
46
50
|
/** Replaced when given; the text is word-indexed. */
|
|
47
51
|
attachments?: AttachmentInput[];
|
|
48
52
|
}
|
|
@@ -228,6 +232,15 @@ export interface MailStore {
|
|
|
228
232
|
read?: boolean;
|
|
229
233
|
flagged?: boolean;
|
|
230
234
|
}>, now: number): Promise<number>;
|
|
235
|
+
/** The live emails whose location in `folder` has this UIDVALIDITY, oldest UID first. */
|
|
236
|
+
folderEmails(accountId: number, folder: string, uidvalidity: string): Promise<FolderEmail[]>;
|
|
237
|
+
/**
|
|
238
|
+
* Records where emails the store holds are in `folder`, by Message-ID; one it does not hold is left out. Locations
|
|
239
|
+
* of the folder under another UIDVALIDITY are dropped: its UIDs mean nothing now.
|
|
240
|
+
*/
|
|
241
|
+
setLocations(accountId: number, folder: string, uidvalidity: string, found: ReadonlyMap<string, number[]>): Promise<number>;
|
|
242
|
+
/** Forgets that these UIDs of `folder` hold anything; an email left with no location there leaves the folder too. */
|
|
243
|
+
forgetLocations(accountId: number, folder: string, uidvalidity: string, uids: number[]): Promise<void>;
|
|
231
244
|
/** Marks emails gone at the source and drops their text; saving one again brings it back. Answers how many changed. */
|
|
232
245
|
markDeleted(accountId: number, externalIds: string[], now: number): Promise<number>;
|
|
233
246
|
/**
|
|
@@ -2,7 +2,8 @@ import { formatLocator } from "../../domain/locator.js";
|
|
|
2
2
|
import { formatReference } from "../../domain/references.js";
|
|
3
3
|
import { fold, normalize } from "../normalize.js";
|
|
4
4
|
import { chunkAttachments, drainEmailIndex, dropAttachmentChunks, MAIL_PIECES, queueThreads, threadPieces, titleOf, } from "./email-index.js";
|
|
5
|
-
import {
|
|
5
|
+
import { folderEmails, forgetLocations, locate, setLocations, } from "./email-locations.js";
|
|
6
|
+
import { markEmailsDeleted, markEmailsRestored, recountEmailThread as recount, setEmailFlags } from "./email-state.js";
|
|
6
7
|
import { bool, flag, fromJson, int, page, str, toJson } from "./events.js";
|
|
7
8
|
import { folded } from "./folded.js";
|
|
8
9
|
import { ensurePerson } from "./identities.js";
|
|
@@ -228,6 +229,8 @@ const saveEmail = (database, orm, accountId, threadId, mail, now) => {
|
|
|
228
229
|
}
|
|
229
230
|
for (const box of mail.mailboxes ?? [])
|
|
230
231
|
join(database, emailId, mailboxPk(database, accountId, box, now), now);
|
|
232
|
+
for (const where of mail.locations ?? [])
|
|
233
|
+
locate(database, accountId, emailId, where);
|
|
231
234
|
if (mail.attachments !== undefined) {
|
|
232
235
|
const before = dropAttachmentChunks(database, emailId);
|
|
233
236
|
const positions = mail.attachments.map(({ position }) => position);
|
|
@@ -545,34 +548,11 @@ export const mailStoreOver = ({ database, orm }, stemmerFor = stemmerCache()) =>
|
|
|
545
548
|
return setEmailFlags(database, accountId, flags, now);
|
|
546
549
|
});
|
|
547
550
|
},
|
|
548
|
-
async
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const forget = database.prepare("UPDATE attachments SET text = NULL, normalized_text = NULL WHERE attachable_type = 'email' AND attachable_id = ?");
|
|
554
|
-
const threads = externalIds.flatMap((id) => mark.all(now, now, accountId, id).map((r) => {
|
|
555
|
-
const pieces = dropAttachmentChunks(database, Number(r.id));
|
|
556
|
-
forget.run(Number(r.id));
|
|
557
|
-
dropUnusedVectors(database, pieces);
|
|
558
|
-
return Number(r.email_thread_id);
|
|
559
|
-
}));
|
|
560
|
-
for (const id of new Set(threads))
|
|
561
|
-
recount(database, id, now);
|
|
562
|
-
return threads.length;
|
|
563
|
-
});
|
|
564
|
-
},
|
|
565
|
-
async markRestored(accountId, externalIds, now) {
|
|
566
|
-
return write(database, () => {
|
|
567
|
-
const unmark = database.prepare(`UPDATE emails SET deleted_at = NULL, updated_at = ?
|
|
568
|
-
WHERE account_id = ? AND external_id = ? AND deleted_at IS NOT NULL
|
|
569
|
-
RETURNING email_thread_id`);
|
|
570
|
-
const threads = externalIds.flatMap((id) => unmark.all(now, accountId, id).map((r) => Number(r.email_thread_id)));
|
|
571
|
-
for (const id of new Set(threads))
|
|
572
|
-
recount(database, id, now);
|
|
573
|
-
return threads.length;
|
|
574
|
-
});
|
|
575
|
-
},
|
|
551
|
+
folderEmails: async (accountId, folder, uidvalidity) => folderEmails(database, accountId, folder, uidvalidity),
|
|
552
|
+
setLocations: async (accountId, folder, uidvalidity, found) => write(database, () => setLocations(database, accountId, folder, uidvalidity, found)),
|
|
553
|
+
forgetLocations: async (accountId, folder, uidvalidity, uids) => write(database, () => forgetLocations(database, accountId, folder, uidvalidity, uids)),
|
|
554
|
+
markDeleted: async (accountId, externalIds, now) => write(database, () => markEmailsDeleted(database, accountId, externalIds, now)),
|
|
555
|
+
markRestored: async (accountId, externalIds, now) => write(database, () => markEmailsRestored(database, accountId, externalIds, now)),
|
|
576
556
|
async chunksToEmbed(model, { after, limit }) {
|
|
577
557
|
drainEmailIndex(database, stemmerFor);
|
|
578
558
|
const file = database.prepare("SELECT text FROM attachments WHERE id = ?");
|
|
@@ -52,6 +52,7 @@ export const MANIFEST = [
|
|
|
52
52
|
{ name: "20261029181000_version-37-contact-mention-triggers", version: 37, minCompatible: 34 },
|
|
53
53
|
{ name: "20261029224106_version-38-folded-names", version: 38, minCompatible: 34 },
|
|
54
54
|
{ name: "20261029224107_version-38-folded-triggers", version: 38, minCompatible: 34 },
|
|
55
|
+
{ name: "20261030114008_version-39-email-locations", version: 39, minCompatible: 34 },
|
|
55
56
|
];
|
|
56
57
|
export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
|
|
57
58
|
const migrations = [];
|
|
@@ -830,5 +830,12 @@ export const GENERATED = [
|
|
|
830
830
|
"CREATE TRIGGER email_threads_subject_folded_au AFTER UPDATE OF subject ON email_threads\n WHEN new.subject IS NOT old.subject AND new.subject_folded IS old.subject_folded BEGIN\n UPDATE email_threads SET subject_folded = NULL WHERE id = new.id;\nEND;",
|
|
831
831
|
"CREATE TRIGGER mailboxes_name_folded_au AFTER UPDATE OF name ON mailboxes\n WHEN new.name IS NOT old.name AND new.name_folded IS old.name_folded BEGIN\n UPDATE mailboxes SET name_folded = NULL WHERE id = new.id;\nEND;"
|
|
832
832
|
]
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
"name": "20261030114008_version-39-email-locations",
|
|
836
|
+
"statements": [
|
|
837
|
+
"CREATE TABLE `email_locations` (\n\t`mailbox_id` integer NOT NULL,\n\t`uidvalidity` text NOT NULL,\n\t`uid` integer NOT NULL,\n\t`email_id` integer NOT NULL,\n\tCONSTRAINT `email_locations_pk` PRIMARY KEY(`mailbox_id`, `uidvalidity`, `uid`),\n\tCONSTRAINT `fk_email_locations_mailbox_id_mailboxes_id_fk` FOREIGN KEY (`mailbox_id`) REFERENCES `mailboxes`(`id`),\n\tCONSTRAINT `fk_email_locations_email_id_emails_id_fk` FOREIGN KEY (`email_id`) REFERENCES `emails`(`id`)\n);",
|
|
838
|
+
"CREATE INDEX `email_locations_by_email_id` ON `email_locations` (`email_id`);"
|
|
839
|
+
]
|
|
833
840
|
}
|
|
834
841
|
];
|
|
@@ -6172,6 +6172,78 @@ export declare const emailMailboxes: import("drizzle-orm/sqlite-core").SQLiteTab
|
|
|
6172
6172
|
dialect: 'sqlite';
|
|
6173
6173
|
isAlias: false;
|
|
6174
6174
|
}>;
|
|
6175
|
+
export declare const emailLocations: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
6176
|
+
name: "email_locations";
|
|
6177
|
+
schema: undefined;
|
|
6178
|
+
columns: {
|
|
6179
|
+
mailboxId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
6180
|
+
name: string;
|
|
6181
|
+
tableName: "email_locations";
|
|
6182
|
+
dataType: "number int53";
|
|
6183
|
+
data: number;
|
|
6184
|
+
driverParam: number;
|
|
6185
|
+
notNull: true;
|
|
6186
|
+
hasDefault: false;
|
|
6187
|
+
isPrimaryKey: false;
|
|
6188
|
+
isAutoincrement: false;
|
|
6189
|
+
hasRuntimeDefault: false;
|
|
6190
|
+
enumValues: undefined;
|
|
6191
|
+
baseColumn: never;
|
|
6192
|
+
identity: undefined;
|
|
6193
|
+
generated: undefined;
|
|
6194
|
+
}, {}>;
|
|
6195
|
+
uidvalidity: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
6196
|
+
name: string;
|
|
6197
|
+
tableName: "email_locations";
|
|
6198
|
+
dataType: "string";
|
|
6199
|
+
data: string;
|
|
6200
|
+
driverParam: string;
|
|
6201
|
+
notNull: true;
|
|
6202
|
+
hasDefault: false;
|
|
6203
|
+
isPrimaryKey: false;
|
|
6204
|
+
isAutoincrement: false;
|
|
6205
|
+
hasRuntimeDefault: false;
|
|
6206
|
+
enumValues: [string, ...string[]];
|
|
6207
|
+
baseColumn: never;
|
|
6208
|
+
identity: undefined;
|
|
6209
|
+
generated: undefined;
|
|
6210
|
+
}, {}>;
|
|
6211
|
+
uid: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
6212
|
+
name: string;
|
|
6213
|
+
tableName: "email_locations";
|
|
6214
|
+
dataType: "number int53";
|
|
6215
|
+
data: number;
|
|
6216
|
+
driverParam: number;
|
|
6217
|
+
notNull: true;
|
|
6218
|
+
hasDefault: false;
|
|
6219
|
+
isPrimaryKey: false;
|
|
6220
|
+
isAutoincrement: false;
|
|
6221
|
+
hasRuntimeDefault: false;
|
|
6222
|
+
enumValues: undefined;
|
|
6223
|
+
baseColumn: never;
|
|
6224
|
+
identity: undefined;
|
|
6225
|
+
generated: undefined;
|
|
6226
|
+
}, {}>;
|
|
6227
|
+
emailId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
6228
|
+
name: string;
|
|
6229
|
+
tableName: "email_locations";
|
|
6230
|
+
dataType: "number int53";
|
|
6231
|
+
data: number;
|
|
6232
|
+
driverParam: number;
|
|
6233
|
+
notNull: true;
|
|
6234
|
+
hasDefault: false;
|
|
6235
|
+
isPrimaryKey: false;
|
|
6236
|
+
isAutoincrement: false;
|
|
6237
|
+
hasRuntimeDefault: false;
|
|
6238
|
+
enumValues: undefined;
|
|
6239
|
+
baseColumn: never;
|
|
6240
|
+
identity: undefined;
|
|
6241
|
+
generated: undefined;
|
|
6242
|
+
}, {}>;
|
|
6243
|
+
};
|
|
6244
|
+
dialect: 'sqlite';
|
|
6245
|
+
isAlias: false;
|
|
6246
|
+
}>;
|
|
6175
6247
|
export declare const emailIndexPending: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
6176
6248
|
name: "email_index_pending";
|
|
6177
6249
|
schema: undefined;
|
|
@@ -695,6 +695,19 @@ export const emailMailboxes = sqliteTable("email_mailboxes", {
|
|
|
695
695
|
primaryKey({ columns: [table.emailId, table.mailboxId] }),
|
|
696
696
|
index("email_mailboxes_by_mailbox_id").on(table.mailboxId),
|
|
697
697
|
]);
|
|
698
|
+
export const emailLocations = sqliteTable("email_locations", {
|
|
699
|
+
mailboxId: integer("mailbox_id")
|
|
700
|
+
.notNull()
|
|
701
|
+
.references(() => mailboxes.id),
|
|
702
|
+
uidvalidity: text("uidvalidity").notNull(),
|
|
703
|
+
uid: integer("uid").notNull(),
|
|
704
|
+
emailId: integer("email_id")
|
|
705
|
+
.notNull()
|
|
706
|
+
.references(() => emails.id),
|
|
707
|
+
}, (table) => [
|
|
708
|
+
primaryKey({ columns: [table.mailboxId, table.uidvalidity, table.uid] }),
|
|
709
|
+
index("email_locations_by_email_id").on(table.emailId),
|
|
710
|
+
]);
|
|
698
711
|
export const emailIndexPending = sqliteTable("email_index_pending", {
|
|
699
712
|
id: integer("id").notNull(),
|
|
700
713
|
indexableType: text("indexable_type").notNull(),
|