@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
|
@@ -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
|
|
151
|
+
options: {
|
|
152
|
+
endsAfter: string;
|
|
153
|
+
limit?: number;
|
|
154
|
+
continuationToken?: string;
|
|
155
|
+
},
|
|
151
156
|
): Promise<ResultList<CalendarSuggestionItem>> {
|
|
152
|
-
const limit = options
|
|
153
|
-
const cursor = options
|
|
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(
|
|
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(
|
|
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(
|
|
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 +
|
|
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
|
|