@remit/drizzle-service 0.0.96 → 0.0.98

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.96",
3
+ "version": "0.0.98",
4
4
  "description": "Drizzle ORM service over SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -242,6 +242,7 @@ describe("the calendar tables under the shipped migrations", () => {
242
242
  summary: "Design review",
243
243
  dtStart: "2026-09-01T10:00:00+02:00",
244
244
  dtEnd: "2026-09-01T11:00:00+02:00",
245
+ endsAtUtc: "2026-09-01T09:00:00Z",
245
246
  allDay: false,
246
247
  location: "",
247
248
  organizer: "organizer@example.test",
@@ -256,3 +257,89 @@ describe("the calendar tables under the shipped migrations", () => {
256
257
  assert.equal(fetched.organizer, "organizer@example.test");
257
258
  });
258
259
  });
260
+
261
+ describe("the ends_at_utc backfill of existing suggestions", () => {
262
+ const TAG = "0039_calendar_suggestion_ends_at_utc";
263
+ const VTIMEZONE = [
264
+ "BEGIN:VTIMEZONE",
265
+ "TZID:Europe/Amsterdam",
266
+ "BEGIN:DAYLIGHT",
267
+ "DTSTART:19700329T020000",
268
+ "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU",
269
+ "END:DAYLIGHT",
270
+ "END:VTIMEZONE",
271
+ ];
272
+ const invite = (...eventLines: string[]): string =>
273
+ [
274
+ "BEGIN:VCALENDAR",
275
+ ...VTIMEZONE,
276
+ "BEGIN:VEVENT",
277
+ ...eventLines,
278
+ "END:VEVENT",
279
+ "END:VCALENDAR",
280
+ "",
281
+ ].join("\r\n");
282
+
283
+ const backfilled = (
284
+ rows: Record<string, { dtEnd: string; icalData: string }>,
285
+ ): Record<string, string> => {
286
+ const sqlite = new Database(":memory:");
287
+ for (const entry of migrationJournal()) {
288
+ if (entry.tag === TAG) break;
289
+ applyMigration(sqlite, entry.tag);
290
+ }
291
+ const columns = sqlite
292
+ .prepare("PRAGMA table_info(calendar_suggestion)")
293
+ .all() as { name: string; type: string }[];
294
+ for (const [id, row] of Object.entries(rows)) {
295
+ const values: Record<string, string | number> = {};
296
+ for (const column of columns) {
297
+ values[column.name] = column.type === "integer" ? 0 : "";
298
+ }
299
+ values.suggestion_id = id;
300
+ values.dt_end = row.dtEnd;
301
+ values.ical_data = row.icalData;
302
+ const names = Object.keys(values);
303
+ sqlite
304
+ .prepare(
305
+ `INSERT INTO calendar_suggestion (${names.join(",")}) VALUES (${names.map(() => "?").join(",")})`,
306
+ )
307
+ .run(...Object.values(values));
308
+ }
309
+ applyMigration(sqlite, TAG);
310
+ const result = Object.fromEntries(
311
+ (
312
+ sqlite
313
+ .prepare("SELECT suggestion_id, ends_at_utc FROM calendar_suggestion")
314
+ .all() as { suggestion_id: string; ends_at_utc: string }[]
315
+ ).map((row) => [row.suggestion_id, row.ends_at_utc]),
316
+ );
317
+ sqlite.close();
318
+ return result;
319
+ };
320
+
321
+ test("converts a past non-recurring invite carrying a VTIMEZONE to its UTC end", () => {
322
+ const result = backfilled({
323
+ plain: {
324
+ dtEnd: "2024-03-01T11:00:00+02:00",
325
+ icalData: invite(
326
+ "UID:a@example.test",
327
+ "DTSTART;TZID=Europe/Amsterdam:20240301T100000",
328
+ "DTEND;TZID=Europe/Amsterdam:20240301T110000",
329
+ ),
330
+ },
331
+ weekly: {
332
+ dtEnd: "2024-03-01T11:00:00+02:00",
333
+ icalData: invite(
334
+ "UID:b@example.test",
335
+ "DTSTART;TZID=Europe/Amsterdam:20240301T100000",
336
+ "DTEND;TZID=Europe/Amsterdam:20240301T110000",
337
+ "RRULE:FREQ=WEEKLY",
338
+ ),
339
+ },
340
+ });
341
+
342
+ assert.equal(result.plain, "2024-03-01T09:00:00Z");
343
+ assert.equal(result.weekly, "9999-12-31T23:59:59Z");
344
+ });
345
+ });
@@ -7,7 +7,7 @@ import type {
7
7
  } from "@remit/data-ports";
8
8
  import { deriveCalendarSuggestionId } from "@remit/data-ports/id";
9
9
  import { CalendarSuggestionState } from "@remit/domain-enums";
10
- import { and, asc, desc, eq, inArray, lt, or } from "drizzle-orm";
10
+ import { and, asc, desc, eq, gte, inArray, lt, or } from "drizzle-orm";
11
11
  import type { Db } from "../db.js";
12
12
  import { NotFoundError } from "../error.js";
13
13
  import { decodeToken, resultList } from "../pagination.js";
@@ -31,6 +31,7 @@ function rowToCalendarSuggestion(
31
31
  summary: row.summary,
32
32
  dtStart: row.dtStart,
33
33
  dtEnd: row.dtEnd,
34
+ endsAtUtc: row.endsAtUtc,
34
35
  allDay: row.allDay,
35
36
  location: row.location,
36
37
  organizer: row.organizer,
@@ -147,10 +148,14 @@ export class CalendarSuggestionRepo implements ICalendarSuggestionRepository {
147
148
  async listByState(
148
149
  accountConfigId: string,
149
150
  state: CalendarSuggestionItem["state"],
150
- options?: { limit?: number; continuationToken?: string },
151
+ options: {
152
+ endsAfter: string;
153
+ limit?: number;
154
+ continuationToken?: string;
155
+ },
151
156
  ): Promise<ResultList<CalendarSuggestionItem>> {
152
- const limit = options?.limit ?? 100;
153
- const cursor = options?.continuationToken
157
+ const limit = options.limit ?? 100;
158
+ const cursor = options.continuationToken
154
159
  ? decodeToken(options.continuationToken)
155
160
  : undefined;
156
161
  const after = cursor
@@ -167,6 +172,7 @@ export class CalendarSuggestionRepo implements ICalendarSuggestionRepository {
167
172
  and(
168
173
  eq(calendarSuggestionTable.accountConfigId, accountConfigId),
169
174
  eq(calendarSuggestionTable.state, state),
175
+ gte(calendarSuggestionTable.endsAtUtc, options.endsAfter),
170
176
  after
171
177
  ? or(
172
178
  lt(calendarSuggestionTable.createdAt, after.createdAt),
@@ -43,6 +43,7 @@ describe("MailboxLockRepo", () => {
43
43
  eventName,
44
44
  accountId,
45
45
  lockId,
46
+ 3600,
46
47
  );
47
48
  assert.equal(acquired, true, "should acquire lock");
48
49
 
@@ -71,6 +72,7 @@ describe("MailboxLockRepo", () => {
71
72
  eventName,
72
73
  accountId,
73
74
  lockId1,
75
+ 3600,
74
76
  );
75
77
  assert.equal(first, true, "first acquire should succeed");
76
78
 
@@ -79,6 +81,7 @@ describe("MailboxLockRepo", () => {
79
81
  eventName,
80
82
  accountId,
81
83
  lockId2,
84
+ 3600,
82
85
  );
83
86
  assert.equal(second, false, "second acquire should fail");
84
87
 
@@ -104,6 +107,7 @@ describe("MailboxLockRepo", () => {
104
107
  eventName,
105
108
  accountId,
106
109
  freshLockId,
110
+ 3600,
107
111
  );
108
112
  assert.equal(acquired, true, "should steal the expired lock");
109
113
 
@@ -129,13 +133,20 @@ describe("MailboxLockRepo", () => {
129
133
  const contenderLockId = randomId();
130
134
  const eventName = "SYNC_MESSAGES";
131
135
 
132
- await repo.tryAcquireLock(mailboxId, eventName, accountId, liveLockId);
136
+ await repo.tryAcquireLock(
137
+ mailboxId,
138
+ eventName,
139
+ accountId,
140
+ liveLockId,
141
+ 3600,
142
+ );
133
143
 
134
144
  const acquired = await repo.tryAcquireLock(
135
145
  mailboxId,
136
146
  eventName,
137
147
  accountId,
138
148
  contenderLockId,
149
+ 3600,
139
150
  );
140
151
  assert.equal(acquired, false, "should not steal a live lock");
141
152
 
@@ -166,8 +177,8 @@ describe("MailboxLockRepo", () => {
166
177
  });
167
178
 
168
179
  const results = await Promise.all([
169
- repo.tryAcquireLock(mailboxId, eventName, accountId, contenderA),
170
- repo.tryAcquireLock(mailboxId, eventName, accountId, contenderB),
180
+ repo.tryAcquireLock(mailboxId, eventName, accountId, contenderA, 3600),
181
+ repo.tryAcquireLock(mailboxId, eventName, accountId, contenderB, 3600),
171
182
  ]);
172
183
 
173
184
  assert.equal(
@@ -187,7 +198,7 @@ describe("MailboxLockRepo", () => {
187
198
  const lockId = randomId();
188
199
  const eventName = "SYNC_MESSAGES";
189
200
 
190
- await repo.tryAcquireLock(mailboxId, eventName, accountId, lockId);
201
+ await repo.tryAcquireLock(mailboxId, eventName, accountId, lockId, 3600);
191
202
  await repo.releaseLock(accountId, mailboxId, eventName, lockId);
192
203
 
193
204
  const lock = await repo.get(accountId, mailboxId, eventName);
@@ -201,7 +212,7 @@ describe("MailboxLockRepo", () => {
201
212
  const lockId2 = randomId();
202
213
  const eventName = "SYNC_MESSAGES";
203
214
 
204
- await repo.tryAcquireLock(mailboxId, eventName, accountId, lockId1);
215
+ await repo.tryAcquireLock(mailboxId, eventName, accountId, lockId1, 3600);
205
216
  await repo.releaseLock(accountId, mailboxId, eventName, lockId2); // wrong lockId
206
217
 
207
218
  const lock = await repo.get(accountId, mailboxId, eventName);
@@ -241,7 +252,7 @@ describe("MailboxLockRepo", () => {
241
252
  const lockId = randomId();
242
253
  const eventName = "SYNC_MESSAGES";
243
254
 
244
- await repo.tryAcquireLock(mailboxId, eventName, accountId, lockId);
255
+ await repo.tryAcquireLock(mailboxId, eventName, accountId, lockId, 3600);
245
256
 
246
257
  const result = await repo.withMailboxLock(
247
258
  mailboxId,
@@ -287,12 +298,14 @@ describe("MailboxLockRepo", () => {
287
298
  "SYNC_MESSAGES",
288
299
  accountId,
289
300
  lockId1,
301
+ 3600,
290
302
  );
291
303
  const a2 = await repo.tryAcquireLock(
292
304
  mailboxId,
293
305
  "SYNC_FLAGS",
294
306
  accountId,
295
307
  lockId2,
308
+ 3600,
296
309
  );
297
310
 
298
311
  assert.equal(a1, true);
@@ -311,8 +324,8 @@ describe("MailboxLockRepo", () => {
311
324
  const lockId2 = randomId();
312
325
  const eventName = "SYNC_MESSAGES";
313
326
 
314
- await repo.tryAcquireLock(mailboxId1, eventName, accountId1, lockId1);
315
- await repo.tryAcquireLock(mailboxId2, eventName, accountId2, lockId2);
327
+ await repo.tryAcquireLock(mailboxId1, eventName, accountId1, lockId1, 3600);
328
+ await repo.tryAcquireLock(mailboxId2, eventName, accountId2, lockId2, 3600);
316
329
 
317
330
  const locks = await repo.listByAccount(accountId1);
318
331
  assert.equal(locks.filter((l) => l.lockId === lockId1).length, 1);
@@ -329,8 +342,8 @@ describe("MailboxLockRepo", () => {
329
342
  const mb1 = randomId();
330
343
  const mb2 = randomId();
331
344
 
332
- await repo.tryAcquireLock(mb1, "SYNC_MESSAGES", accountId, lockId1);
333
- await repo.tryAcquireLock(mb2, "SYNC_FLAGS", accountId, lockId2);
345
+ await repo.tryAcquireLock(mb1, "SYNC_MESSAGES", accountId, lockId1, 3600);
346
+ await repo.tryAcquireLock(mb2, "SYNC_FLAGS", accountId, lockId2, 3600);
334
347
 
335
348
  await repo.deleteByAccount(accountId);
336
349
 
@@ -343,7 +356,13 @@ describe("MailboxLockRepo", () => {
343
356
  const other = randomId();
344
357
  const mailboxId = randomId();
345
358
  const lockId = randomId();
346
- await repo.tryAcquireLock(mailboxId, "SYNC_MESSAGES", accountId, lockId);
359
+ await repo.tryAcquireLock(
360
+ mailboxId,
361
+ "SYNC_MESSAGES",
362
+ accountId,
363
+ lockId,
364
+ 3600,
365
+ );
347
366
 
348
367
  assert.ok(await repo.get(accountId, mailboxId, "SYNC_MESSAGES"));
349
368
  assert.equal(await repo.get(other, mailboxId, "SYNC_MESSAGES"), null);
@@ -356,7 +375,13 @@ describe("MailboxLockRepo", () => {
356
375
  const other = randomId();
357
376
  const mailboxId = randomId();
358
377
  const lockId = randomId();
359
- await repo.tryAcquireLock(mailboxId, "SYNC_MESSAGES", accountId, lockId);
378
+ await repo.tryAcquireLock(
379
+ mailboxId,
380
+ "SYNC_MESSAGES",
381
+ accountId,
382
+ lockId,
383
+ 3600,
384
+ );
360
385
 
361
386
  // Foreign account with the (leaked) lockId still cannot release it.
362
387
  await repo.releaseLock(other, mailboxId, "SYNC_MESSAGES", lockId);
@@ -32,10 +32,11 @@ export class MailboxLockRepo implements IMailboxLockRepository {
32
32
  eventName: string,
33
33
  accountId: string,
34
34
  lockId: string,
35
+ ttlSeconds: number,
35
36
  ): Promise<boolean> {
36
37
  const now = Date.now();
37
38
  const nowSeconds = Math.floor(now / 1000);
38
- const ttl = nowSeconds + TTL_SECONDS;
39
+ const ttl = nowSeconds + ttlSeconds;
39
40
 
40
41
  const rows = await this.db
41
42
  .insert(mailboxLockTable)
@@ -90,6 +91,7 @@ export class MailboxLockRepo implements IMailboxLockRepository {
90
91
  eventName,
91
92
  accountId,
92
93
  lockId,
94
+ TTL_SECONDS,
93
95
  );
94
96
  if (!acquired) return { executed: false };
95
97