@remit/drizzle-service 0.0.67 → 0.0.68
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
|
@@ -72,6 +72,37 @@ export class CalendarCollectionRepo implements ICalendarCollectionRepository {
|
|
|
72
72
|
return rowToCalendar(row);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* One statement, so the loser of a race is told the segment is taken rather
|
|
77
|
+
* than handed the winner's calendar. A read followed by an insert would let
|
|
78
|
+
* both callers see the segment free and one of them silently write into the
|
|
79
|
+
* other's collection.
|
|
80
|
+
*/
|
|
81
|
+
async createExclusive(
|
|
82
|
+
input: CreateCalendarCollectionInput,
|
|
83
|
+
): Promise<CalendarCollectionItem | null> {
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const urlSegment = normalizeCalendarUrlSegment(input.urlSegment);
|
|
86
|
+
const [row] = await this.db
|
|
87
|
+
.insert(calendarTable)
|
|
88
|
+
.values({
|
|
89
|
+
calendarId: deriveCalendarId(input.accountConfigId, urlSegment),
|
|
90
|
+
accountConfigId: input.accountConfigId,
|
|
91
|
+
urlSegment,
|
|
92
|
+
displayName: input.displayName,
|
|
93
|
+
color: input.color ?? "Cal1",
|
|
94
|
+
componentSet: input.componentSet ?? "VeventOnly",
|
|
95
|
+
source: input.source ?? "UserCreated",
|
|
96
|
+
timezone: input.timezone ?? "",
|
|
97
|
+
syncSequence: 0,
|
|
98
|
+
createdAt: now,
|
|
99
|
+
updatedAt: now,
|
|
100
|
+
})
|
|
101
|
+
.onConflictDoNothing({ target: calendarTable.calendarId })
|
|
102
|
+
.returning();
|
|
103
|
+
return row ? rowToCalendar(row) : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
75
106
|
async get(
|
|
76
107
|
accountConfigId: string,
|
|
77
108
|
calendarId: string,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { after, before, describe, test } from "node:test";
|
|
3
|
+
import { provisionDefaultCalendar } from "@remit/calendar-service";
|
|
4
|
+
import type { ICalendarUnitOfWork } from "@remit/data-ports";
|
|
5
|
+
import type { Db } from "../db.js";
|
|
6
|
+
import {
|
|
7
|
+
calendarEventIndexTable,
|
|
8
|
+
calendarObjectTable,
|
|
9
|
+
calendarTable,
|
|
10
|
+
} from "../schema.js";
|
|
11
|
+
import { createSqliteTestDb } from "../test-db-sqlite.js";
|
|
12
|
+
import { CalendarCollectionRepo } from "./calendar-collection.js";
|
|
13
|
+
import { DrizzleCalendarUnitOfWork } from "./calendar-unit-of-work.js";
|
|
14
|
+
|
|
15
|
+
const ACCOUNT_CONFIG_ID = "account-config-default";
|
|
16
|
+
|
|
17
|
+
describe("provisioning the default calendar", () => {
|
|
18
|
+
let db: Db<Record<string, unknown>>;
|
|
19
|
+
let close: () => Promise<void>;
|
|
20
|
+
let unitOfWork: ICalendarUnitOfWork;
|
|
21
|
+
|
|
22
|
+
before(async () => {
|
|
23
|
+
const created = await createSqliteTestDb({
|
|
24
|
+
calendars: calendarTable,
|
|
25
|
+
calendarObjects: calendarObjectTable,
|
|
26
|
+
calendarEventIndexes: calendarEventIndexTable,
|
|
27
|
+
});
|
|
28
|
+
db = created.db as unknown as Db<Record<string, unknown>>;
|
|
29
|
+
close = created.close;
|
|
30
|
+
unitOfWork = new DrizzleCalendarUnitOfWork(db);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
after(() => close());
|
|
34
|
+
|
|
35
|
+
test("leaves one calendar behind when several first reads arrive together", async () => {
|
|
36
|
+
const provisioned = await Promise.all(
|
|
37
|
+
Array.from({ length: 8 }, () =>
|
|
38
|
+
provisionDefaultCalendar(unitOfWork, ACCOUNT_CONFIG_ID),
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const ids = new Set(provisioned.map((collection) => collection.calendarId));
|
|
43
|
+
assert.equal(ids.size, 1, "every caller was handed the same calendar");
|
|
44
|
+
|
|
45
|
+
const rows = await new CalendarCollectionRepo(db).listByAccountConfig(
|
|
46
|
+
ACCOUNT_CONFIG_ID,
|
|
47
|
+
);
|
|
48
|
+
assert.equal(rows.length, 1);
|
|
49
|
+
assert.equal(rows[0]?.urlSegment, "default");
|
|
50
|
+
assert.equal(
|
|
51
|
+
rows[0]?.syncSequence,
|
|
52
|
+
0,
|
|
53
|
+
"a losing provision did not reset a collection the winner may already have written to",
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("a later provision returns the stored collection rather than resetting it", async () => {
|
|
58
|
+
const repo = new CalendarCollectionRepo(db);
|
|
59
|
+
const [before] = await repo.listByAccountConfig(ACCOUNT_CONFIG_ID);
|
|
60
|
+
assert.ok(before);
|
|
61
|
+
await repo.bumpSyncSequence(ACCOUNT_CONFIG_ID, before.calendarId);
|
|
62
|
+
|
|
63
|
+
const again = await provisionDefaultCalendar(
|
|
64
|
+
unitOfWork,
|
|
65
|
+
ACCOUNT_CONFIG_ID,
|
|
66
|
+
"A different name",
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
assert.equal(again.calendarId, before.calendarId);
|
|
70
|
+
assert.equal(again.displayName, before.displayName);
|
|
71
|
+
assert.equal(again.syncSequence, 1);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -19,6 +19,9 @@ function rowToOccurrence(
|
|
|
19
19
|
startAt: row.startAt,
|
|
20
20
|
endAt: row.endAt,
|
|
21
21
|
allDay: row.allDay,
|
|
22
|
+
summary: row.summary,
|
|
23
|
+
status: row.status,
|
|
24
|
+
transparency: row.transparency,
|
|
22
25
|
createdAt: row.createdAt,
|
|
23
26
|
updatedAt: row.updatedAt,
|
|
24
27
|
};
|
|
@@ -44,6 +47,9 @@ export class CalendarEventIndexRepo implements ICalendarEventIndexRepository {
|
|
|
44
47
|
startAt: occurrence.startAt,
|
|
45
48
|
endAt: occurrence.endAt,
|
|
46
49
|
allDay: occurrence.allDay,
|
|
50
|
+
summary: occurrence.summary,
|
|
51
|
+
status: occurrence.status,
|
|
52
|
+
transparency: occurrence.transparency,
|
|
47
53
|
createdAt: now,
|
|
48
54
|
updatedAt: now,
|
|
49
55
|
})),
|
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
PutCalendarObjectInput,
|
|
5
5
|
} from "@remit/data-ports";
|
|
6
6
|
import { deriveCalendarObjectId } from "@remit/data-ports/id";
|
|
7
|
-
import { and, asc, eq, gt } from "drizzle-orm";
|
|
7
|
+
import { and, asc, eq, gt, lt, ne } from "drizzle-orm";
|
|
8
8
|
import type { Db } from "../db.js";
|
|
9
9
|
import { NotFoundError } from "../error.js";
|
|
10
10
|
import { calendarObjectTable } from "../schema.js";
|
|
@@ -82,6 +82,22 @@ export class CalendarObjectRepo implements ICalendarObjectRepository {
|
|
|
82
82
|
return rowToCalendarObject(row);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
async find(
|
|
86
|
+
calendarId: string,
|
|
87
|
+
calendarObjectId: string,
|
|
88
|
+
): Promise<CalendarObjectItem | null> {
|
|
89
|
+
const [row] = await this.db
|
|
90
|
+
.select()
|
|
91
|
+
.from(calendarObjectTable)
|
|
92
|
+
.where(
|
|
93
|
+
and(
|
|
94
|
+
eq(calendarObjectTable.calendarId, calendarId),
|
|
95
|
+
eq(calendarObjectTable.calendarObjectId, calendarObjectId),
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
return row ? rowToCalendarObject(row) : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
85
101
|
async delete(calendarId: string, calendarObjectId: string): Promise<void> {
|
|
86
102
|
await this.db
|
|
87
103
|
.delete(calendarObjectTable)
|
|
@@ -139,6 +155,24 @@ export class CalendarObjectRepo implements ICalendarObjectRepository {
|
|
|
139
155
|
return rows.map(rowToCalendarObject);
|
|
140
156
|
}
|
|
141
157
|
|
|
158
|
+
async listIncompleteExpansions(
|
|
159
|
+
calendarId: string,
|
|
160
|
+
instant: string,
|
|
161
|
+
): Promise<CalendarObjectItem[]> {
|
|
162
|
+
const rows = await this.db
|
|
163
|
+
.select()
|
|
164
|
+
.from(calendarObjectTable)
|
|
165
|
+
.where(
|
|
166
|
+
and(
|
|
167
|
+
eq(calendarObjectTable.calendarId, calendarId),
|
|
168
|
+
ne(calendarObjectTable.expandedThrough, ""),
|
|
169
|
+
lt(calendarObjectTable.expandedThrough, instant),
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
.orderBy(asc(calendarObjectTable.calendarObjectId));
|
|
173
|
+
return rows.map(rowToCalendarObject);
|
|
174
|
+
}
|
|
175
|
+
|
|
142
176
|
async listChangedSince(
|
|
143
177
|
calendarId: string,
|
|
144
178
|
syncSequence: number,
|