@remit/drizzle-service 0.0.95 → 0.0.97
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
|
@@ -35,6 +35,16 @@ const filterAtDedupe = sqliteTable("filter", {
|
|
|
35
35
|
updatedAt: integer("updated_at").notNull(),
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
const organizeJobAtDedupe = sqliteTable("organize_job_request", {
|
|
39
|
+
organizeJobId: text("organize_job_id").primaryKey(),
|
|
40
|
+
accountConfigId: text("account_config_id").notNull(),
|
|
41
|
+
userId: text("user_id").notNull(),
|
|
42
|
+
actionMailboxId: text("action_mailbox_id").notNull(),
|
|
43
|
+
ttl: integer("ttl").notNull(),
|
|
44
|
+
createdAt: integer("created_at").notNull(),
|
|
45
|
+
updatedAt: integer("updated_at").notNull(),
|
|
46
|
+
});
|
|
47
|
+
|
|
38
48
|
type Handle = { sqlite: Database.Database; db: Db<Record<string, unknown>> };
|
|
39
49
|
|
|
40
50
|
const atPredecessor = (): Handle => {
|
|
@@ -229,7 +239,7 @@ describe("(account_id, full_path) becomes unique", () => {
|
|
|
229
239
|
createdAt: 0,
|
|
230
240
|
updatedAt: 0,
|
|
231
241
|
});
|
|
232
|
-
await handle.db.insert(
|
|
242
|
+
await handle.db.insert(organizeJobAtDedupe).values({
|
|
233
243
|
organizeJobId: "job-1",
|
|
234
244
|
accountConfigId: CONFIG,
|
|
235
245
|
userId: "user",
|
|
@@ -272,7 +282,7 @@ describe("(account_id, full_path) becomes unique", () => {
|
|
|
272
282
|
assert.equal(child?.parent, "with-mail");
|
|
273
283
|
const [filter] = await handle.db.select().from(filterAtDedupe);
|
|
274
284
|
assert.equal(filter?.actionMailboxId, "with-mail");
|
|
275
|
-
const [job] = await handle.db.select().from(
|
|
285
|
+
const [job] = await handle.db.select().from(organizeJobAtDedupe);
|
|
276
286
|
assert.equal(job?.actionMailboxId, "with-mail");
|
|
277
287
|
const [copied] = await handle.db
|
|
278
288
|
.select({ original: entities.messages.originalMailboxId })
|
|
@@ -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),
|
|
@@ -22,6 +22,7 @@ function rowToItem(
|
|
|
22
22
|
accountConfigId: row.accountConfigId,
|
|
23
23
|
userId: row.userId,
|
|
24
24
|
state: row.state as OrganizeJobRequestItem["state"],
|
|
25
|
+
filterId: row.filterId,
|
|
25
26
|
anchorMessageId: row.anchorMessageId,
|
|
26
27
|
matchOperator: row.matchOperator as OrganizeJobRequestItem["matchOperator"],
|
|
27
28
|
literalClauses:
|
|
@@ -53,6 +54,7 @@ export class OrganizeJobRequestRepo implements IOrganizeJobRequestRepository {
|
|
|
53
54
|
accountConfigId: input.accountConfigId,
|
|
54
55
|
userId: input.userId,
|
|
55
56
|
state: input.state ?? "Pending",
|
|
57
|
+
filterId: input.filterId,
|
|
56
58
|
anchorMessageId: input.anchorMessageId,
|
|
57
59
|
matchOperator: input.matchOperator,
|
|
58
60
|
literalClauses: input.literalClauses,
|
|
@@ -664,6 +664,7 @@ export class DrizzleThreadMessageRepository
|
|
|
664
664
|
limit?: number;
|
|
665
665
|
continuationToken?: string;
|
|
666
666
|
excludeDeleted?: boolean;
|
|
667
|
+
mailboxIds?: readonly string[];
|
|
667
668
|
},
|
|
668
669
|
): Promise<ResultList<ThreadMessageItem>> {
|
|
669
670
|
const order = options?.order ?? "desc";
|
|
@@ -681,6 +682,9 @@ export class DrizzleThreadMessageRepository
|
|
|
681
682
|
options?.excludeDeleted
|
|
682
683
|
? eq(threadMessageTable.isDeleted, false)
|
|
683
684
|
: undefined,
|
|
685
|
+
options?.mailboxIds
|
|
686
|
+
? inArray(threadMessageTable.mailboxId, [...options.mailboxIds])
|
|
687
|
+
: undefined,
|
|
684
688
|
buildFieldTermCondition(terms, options?.operator ?? "and"),
|
|
685
689
|
sentDateCursorCond(order, cursor),
|
|
686
690
|
),
|