@remit/drizzle-service 0.0.37 → 0.0.38

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/drizzle-service",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "description": "Drizzle ORM service over SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -122,6 +122,26 @@ describe("MessageFlagPushRepo (relational counterpart to MessageFlagPushService)
122
122
  assert.equal(found?.state, "pending");
123
123
  });
124
124
 
125
+ test("put resets createdAt too — a stale createdAt would misread as a stuck deferred marker later", async () => {
126
+ const input = seedInput();
127
+ const first = await repo.put(input);
128
+
129
+ await new Promise((resolve) => setTimeout(resolve, 5));
130
+
131
+ const second = await repo.put(
132
+ seedInput({
133
+ messageId: input.messageId,
134
+ flagName: input.flagName,
135
+ operation: "remove",
136
+ }),
137
+ );
138
+
139
+ assert.ok(
140
+ second.createdAt > first.createdAt,
141
+ "a replacing put must start a fresh lifecycle, not inherit the row it replaces",
142
+ );
143
+ });
144
+
125
145
  test("put is idempotent — a later flip of the SAME field replaces the marker", async () => {
126
146
  const messageId = randomId();
127
147
  const flagName = "\\Seen";
@@ -64,9 +64,14 @@ export class MessageFlagPushRepo {
64
64
  input: PutMessageFlagPushInput,
65
65
  ): Promise<MessageFlagPushItem> => {
66
66
  const now = Date.now();
67
- // A fresh put ALWAYS resets state to `pending` (the field's default)
68
- // a new flip decision starts a new lifecycle regardless of whatever
69
- // state the row it replaces was in.
67
+ // A fresh put ALWAYS resets state to `pending` AND createdAt to now a
68
+ // new flip decision starts a genuinely new lifecycle regardless of
69
+ // whatever state (or age) the row it replaces was in. The conflict path
70
+ // must reset createdAt explicitly: onConflictDoUpdate only touches the
71
+ // columns named in `set`, so an omission here would let a replacing put
72
+ // silently inherit the original row's createdAt (its age then misreads
73
+ // as a stuck deferred marker elsewhere — see flag-push.ts's defer-max
74
+ // check, which measures a marker's age off exactly this field).
70
75
  const [row] = await this.db
71
76
  .insert(messageFlagPushTable)
72
77
  .values({
@@ -83,6 +88,7 @@ export class MessageFlagPushRepo {
83
88
  mailboxId: input.mailboxId,
84
89
  operation: input.operation,
85
90
  state: DEFAULT_STATE,
91
+ createdAt: now,
86
92
  updatedAt: now,
87
93
  },
88
94
  })
@@ -94,6 +94,9 @@ function toMessageItem(row: typeof messageTable.$inferSelect): MessageItem {
94
94
  ...(row.placementDecidedAt !== null
95
95
  ? { placementDecidedAt: row.placementDecidedAt }
96
96
  : {}),
97
+ ...(row.spamReport !== null
98
+ ? { spamReport: row.spamReport as MessageItem["spamReport"] }
99
+ : {}),
97
100
  };
98
101
  }
99
102
 
@@ -193,6 +196,7 @@ export class DrizzleMessageRepository implements IMessageRepository {
193
196
  placementVerdict: input.placementVerdict ?? null,
194
197
  filterMove: input.filterMove ?? null,
195
198
  placementDecidedAt: input.placementDecidedAt ?? null,
199
+ spamReport: input.spamReport ?? null,
196
200
  createdAt: now,
197
201
  updatedAt: now,
198
202
  };
@@ -388,6 +392,9 @@ export class DrizzleMessageRepository implements IMessageRepository {
388
392
  ...(input.messageIdHeader !== undefined
389
393
  ? { messageIdHeader: input.messageIdHeader }
390
394
  : {}),
395
+ ...(input.spamReport !== undefined
396
+ ? { spamReport: input.spamReport }
397
+ : {}),
391
398
  updatedAt: now,
392
399
  };
393
400
 
@@ -432,6 +439,28 @@ export class DrizzleMessageRepository implements IMessageRepository {
432
439
  return this.get(messageId);
433
440
  }
434
441
 
442
+ async clearSpamReport(
443
+ messageId: string,
444
+ ): ReturnType<IMessageRepository["clearSpamReport"]> {
445
+ const now = Date.now();
446
+ await this.db
447
+ .update(messageTable)
448
+ .set({ spamReport: null, updatedAt: now })
449
+ .where(eq(messageTable.messageId, messageId));
450
+ return this.get(messageId);
451
+ }
452
+
453
+ async clearOriginalMailboxId(
454
+ messageId: string,
455
+ ): ReturnType<IMessageRepository["clearOriginalMailboxId"]> {
456
+ const now = Date.now();
457
+ await this.db
458
+ .update(messageTable)
459
+ .set({ originalMailboxId: null, originalUid: null, updatedAt: now })
460
+ .where(eq(messageTable.messageId, messageId));
461
+ return this.get(messageId);
462
+ }
463
+
435
464
  async delete(messageId: string): Promise<void> {
436
465
  await this.deleteMany([messageId]);
437
466
  }