@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/drizzle-service",
3
- "version": "0.0.67",
3
+ "version": "0.0.68",
4
4
  "description": "Drizzle ORM service over SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -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,