@bobfrankston/mailx-store 0.1.18 → 0.1.19
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/db.d.ts +1 -0
- package/db.js +55 -10
- package/package.json +5 -5
package/db.d.ts
CHANGED
|
@@ -82,6 +82,7 @@ export declare class MailxDB {
|
|
|
82
82
|
dirty?: boolean;
|
|
83
83
|
recurringEventId?: string;
|
|
84
84
|
htmlLink?: string;
|
|
85
|
+
isHoliday?: boolean;
|
|
85
86
|
}): string;
|
|
86
87
|
getCalendarEvents(accountId: string, fromMs: number, toMs: number): any[];
|
|
87
88
|
/** Lookup by uuid only — used by patch/delete paths that don't have an
|
package/db.js
CHANGED
|
@@ -406,6 +406,11 @@ export class MailxDB {
|
|
|
406
406
|
// Filters like "hide recurring events" check this column.
|
|
407
407
|
this.addColumnIfMissing("calendar_events", "recurring_event_id", "TEXT");
|
|
408
408
|
this.addColumnIfMissing("calendar_events", "html_link", "TEXT");
|
|
409
|
+
// Holidays come from a separate Google calendar
|
|
410
|
+
// (en.usa#holiday@group.v.calendar.google.com) but share the
|
|
411
|
+
// calendar_events table; the flag distinguishes them so the UI can
|
|
412
|
+
// style them differently and the alarm scheduler can skip them.
|
|
413
|
+
this.addColumnIfMissing("calendar_events", "is_holiday", "INTEGER NOT NULL DEFAULT 0");
|
|
409
414
|
// Backfill UUIDs for any pre-existing rows that were inserted before
|
|
410
415
|
// this column landed. One UPDATE + an id roundtrip per row — cheap
|
|
411
416
|
// at our row counts, runs once per DB upgrade.
|
|
@@ -732,8 +737,8 @@ export class MailxDB {
|
|
|
732
737
|
INSERT INTO calendar_events
|
|
733
738
|
(uuid, account_id, provider_id, calendar_id, title, start_ms, end_ms,
|
|
734
739
|
all_day, location, notes, etag, last_synced, dirty, deleted, updated_at,
|
|
735
|
-
recurring_event_id, html_link)
|
|
736
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?)
|
|
740
|
+
recurring_event_id, html_link, is_holiday)
|
|
741
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)
|
|
737
742
|
ON CONFLICT(uuid) DO UPDATE SET
|
|
738
743
|
account_id=excluded.account_id, provider_id=excluded.provider_id,
|
|
739
744
|
calendar_id=excluded.calendar_id, title=excluded.title,
|
|
@@ -743,8 +748,9 @@ export class MailxDB {
|
|
|
743
748
|
last_synced=excluded.last_synced, dirty=excluded.dirty,
|
|
744
749
|
updated_at=excluded.updated_at,
|
|
745
750
|
recurring_event_id=excluded.recurring_event_id,
|
|
746
|
-
html_link=excluded.html_link
|
|
747
|
-
|
|
751
|
+
html_link=excluded.html_link,
|
|
752
|
+
is_holiday=excluded.is_holiday
|
|
753
|
+
`).run(uuid, ev.accountId, ev.providerId || null, ev.calendarId || "primary", ev.title, ev.startMs, ev.endMs, ev.allDay ? 1 : 0, ev.location || "", ev.notes || "", ev.etag || null, ev.dirty ? 0 : Date.now(), ev.dirty ? 1 : 0, Date.now(), ev.recurringEventId || null, ev.htmlLink || null, ev.isHoliday ? 1 : 0);
|
|
748
754
|
return uuid;
|
|
749
755
|
}
|
|
750
756
|
getCalendarEvents(accountId, fromMs, toMs) {
|
|
@@ -779,6 +785,7 @@ export class MailxDB {
|
|
|
779
785
|
etag: r.etag, lastSynced: r.last_synced, dirty: !!r.dirty, deleted: !!r.deleted,
|
|
780
786
|
recurringEventId: r.recurring_event_id || null,
|
|
781
787
|
htmlLink: r.html_link || null,
|
|
788
|
+
isHoliday: !!r.is_holiday,
|
|
782
789
|
};
|
|
783
790
|
}
|
|
784
791
|
/** Find a calendar event by its Google Calendar event id (provider_id).
|
|
@@ -1045,10 +1052,31 @@ export class MailxDB {
|
|
|
1045
1052
|
}
|
|
1046
1053
|
}
|
|
1047
1054
|
deleteFolder(folderId) {
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
1055
|
+
// Repoint the legacy `messages.folder_id` for any message that
|
|
1056
|
+
// lives in this folder AND at least one other. Without this the
|
|
1057
|
+
// final `DELETE FROM folders` fails with a FOREIGN KEY constraint:
|
|
1058
|
+
// the message survives the GC (still belongs to its other folder)
|
|
1059
|
+
// but its primary folder_id still points at the row we're about
|
|
1060
|
+
// to delete. Pick the smallest remaining membership as the new
|
|
1061
|
+
// primary; arbitrary but stable.
|
|
1062
|
+
this.db.prepare(`
|
|
1063
|
+
UPDATE messages
|
|
1064
|
+
SET folder_id = (
|
|
1065
|
+
SELECT MIN(folder_id) FROM message_folders
|
|
1066
|
+
WHERE message_row_id = messages.id
|
|
1067
|
+
AND folder_id != ?
|
|
1068
|
+
)
|
|
1069
|
+
WHERE folder_id = ?
|
|
1070
|
+
AND EXISTS (
|
|
1071
|
+
SELECT 1 FROM message_folders
|
|
1072
|
+
WHERE message_row_id = messages.id
|
|
1073
|
+
AND folder_id != ?
|
|
1074
|
+
)
|
|
1075
|
+
`).run(folderId, folderId, folderId);
|
|
1076
|
+
// Drop memberships in this folder, then GC any messages rows that
|
|
1077
|
+
// are now orphaned (no remaining memberships). A multi-folder
|
|
1078
|
+
// message (e.g. Gmail label = INBOX + Important) keeps its body
|
|
1079
|
+
// intact when only one of its folders is deleted.
|
|
1052
1080
|
const orphans = this.gcMembershipsAndCollectOrphans(folderId);
|
|
1053
1081
|
if (orphans.count > 0)
|
|
1054
1082
|
this.audit({ kind: "delete-bulk", folderId, count: orphans.count, reason: "folder deleted", source: "deleteFolder" });
|
|
@@ -1234,6 +1262,23 @@ export class MailxDB {
|
|
|
1234
1262
|
if (msg.providerId && !existing.provider_id) {
|
|
1235
1263
|
this.db.prepare("UPDATE messages SET provider_id = ? WHERE id = ?").run(msg.providerId, existing.id);
|
|
1236
1264
|
}
|
|
1265
|
+
// Local-first flag preservation: a row whose flags the user
|
|
1266
|
+
// changed (mark read, flag/star, etc.) gets a pending
|
|
1267
|
+
// `sync_action` queued. Until that action drains to the server,
|
|
1268
|
+
// the server's snapshot is STALE for flags — pulling its
|
|
1269
|
+
// "unread" answer back and overwriting the local "\Seen" would
|
|
1270
|
+
// bounce the read row back to unread, which is what Bob 2026-05-12
|
|
1271
|
+
// reported: "you keep reverting messages to show up as unread
|
|
1272
|
+
// even after it is marked read." Keep the local flags whenever a
|
|
1273
|
+
// pending flag-push exists; the regular processSyncActions cycle
|
|
1274
|
+
// will reconcile in the user's direction.
|
|
1275
|
+
const pendingFlags = this.db.prepare(`SELECT 1 FROM sync_actions
|
|
1276
|
+
WHERE account_id = ? AND folder_id = ? AND uid = ?
|
|
1277
|
+
AND action = 'flags'
|
|
1278
|
+
LIMIT 1`).get(msg.accountId, msg.folderId, msg.uid);
|
|
1279
|
+
const flagsToWrite = pendingFlags
|
|
1280
|
+
? (this.db.prepare("SELECT flags_json FROM messages WHERE id = ?").get(existing.id)?.flags_json ?? JSON.stringify(msg.flags))
|
|
1281
|
+
: JSON.stringify(msg.flags);
|
|
1237
1282
|
// Only overwrite body_path / preview when the caller actually has a
|
|
1238
1283
|
// body. Metadata-only syncs (Gmail API storeApiMessages, IMAP
|
|
1239
1284
|
// header-only fetches) pass bodyPath: "" and would otherwise wipe
|
|
@@ -1243,13 +1288,13 @@ export class MailxDB {
|
|
|
1243
1288
|
this.db.prepare(`
|
|
1244
1289
|
UPDATE messages SET flags_json = ?, preview = ?, body_path = ?, cached_at = ?
|
|
1245
1290
|
WHERE id = ?
|
|
1246
|
-
`).run(
|
|
1291
|
+
`).run(flagsToWrite, msg.preview, msg.bodyPath, Date.now(), existing.id);
|
|
1247
1292
|
}
|
|
1248
1293
|
else {
|
|
1249
1294
|
this.db.prepare(`
|
|
1250
1295
|
UPDATE messages SET flags_json = ?, cached_at = ?
|
|
1251
1296
|
WHERE id = ?
|
|
1252
|
-
`).run(
|
|
1297
|
+
`).run(flagsToWrite, Date.now(), existing.id);
|
|
1253
1298
|
}
|
|
1254
1299
|
// Refresh membership last_seen_at — server confirmed this UID
|
|
1255
1300
|
// is still in this folder. No-op if migration already populated
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-store",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@bobfrankston/mailx-types": "^0.1.
|
|
13
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
12
|
+
"@bobfrankston/mailx-types": "^0.1.11",
|
|
13
|
+
"@bobfrankston/mailx-settings": "^0.1.15"
|
|
14
14
|
},
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
},
|
|
26
26
|
".transformedSnapshot": {
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@bobfrankston/mailx-types": "^0.1.
|
|
29
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
28
|
+
"@bobfrankston/mailx-types": "^0.1.11",
|
|
29
|
+
"@bobfrankston/mailx-settings": "^0.1.15"
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
}
|