@remit/data-ports 0.0.52 → 0.0.53

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/data-ports",
3
- "version": "0.0.52",
3
+ "version": "0.0.53",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -59,6 +59,29 @@ export function calendarCollectionRepositoryConformance(
59
59
  assert.equal(all.length, 1);
60
60
  });
61
61
 
62
+ test("createExclusive refuses a segment the account already uses", async () => {
63
+ const accountConfigId = harness.makeId();
64
+
65
+ const first = await repo.createExclusive({
66
+ accountConfigId,
67
+ urlSegment: "work",
68
+ displayName: "Work",
69
+ });
70
+ const second = await repo.createExclusive({
71
+ accountConfigId,
72
+ urlSegment: "work",
73
+ displayName: "Work again",
74
+ });
75
+
76
+ assert.ok(first);
77
+ assert.equal(second, null);
78
+ assert.equal(
79
+ (await repo.get(accountConfigId, first.calendarId)).displayName,
80
+ "Work",
81
+ "the loser never overwrote the calendar the winner made",
82
+ );
83
+ });
84
+
62
85
  test("get throws a not-found error for a missing collection", async () => {
63
86
  await assert.rejects(
64
87
  repo.get(harness.makeId(), harness.makeId()),
@@ -1,5 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { after, before, describe, test } from "node:test";
3
+ import { CalendarEventStatus, CalendarTransparency } from "@remit/domain-enums";
3
4
  import type { ICalendarEventIndexRepository } from "../interfaces/calendar-event-index.js";
4
5
  import type { CalendarOccurrenceInput } from "../types.js";
5
6
  import type { RepositoryConformanceHarness } from "./harness.js";
@@ -8,11 +9,16 @@ const occurrence = (
8
9
  recurrenceId: string,
9
10
  startAt: string,
10
11
  endAt: string,
12
+ overrides: Partial<CalendarOccurrenceInput> = {},
11
13
  ): CalendarOccurrenceInput => ({
12
14
  recurrenceId,
13
15
  startAt,
14
16
  endAt,
15
17
  allDay: false,
18
+ summary: "Stand-up",
19
+ status: CalendarEventStatus.Confirmed,
20
+ transparency: CalendarTransparency.Opaque,
21
+ ...overrides,
16
22
  });
17
23
 
18
24
  export function calendarEventIndexRepositoryConformance(
@@ -139,6 +145,46 @@ export function calendarEventIndexRepositoryConformance(
139
145
  assert.equal(rows.length, 1);
140
146
  });
141
147
 
148
+ test("an occurrence keeps its own summary, status and transparency", async () => {
149
+ const calendarId = harness.makeId();
150
+ const calendarObjectId = harness.makeId();
151
+
152
+ await repo.replaceForObject(calendarId, calendarObjectId, [
153
+ occurrence(
154
+ "2026-08-26T07:00:00Z",
155
+ "2026-08-26T07:00:00Z",
156
+ "2026-08-26T08:00:00Z",
157
+ ),
158
+ occurrence(
159
+ "2026-09-02T07:00:00Z",
160
+ "2026-09-02T07:00:00Z",
161
+ "2026-09-02T08:00:00Z",
162
+ {
163
+ summary: "Stand-up (moved)",
164
+ status: CalendarEventStatus.Cancelled,
165
+ transparency: CalendarTransparency.Transparent,
166
+ },
167
+ ),
168
+ ]);
169
+
170
+ const rows = await repo.listForObject(calendarId, calendarObjectId);
171
+ assert.deepEqual(
172
+ rows.map((row) => [row.summary, row.status, row.transparency]),
173
+ [
174
+ [
175
+ "Stand-up",
176
+ CalendarEventStatus.Confirmed,
177
+ CalendarTransparency.Opaque,
178
+ ],
179
+ [
180
+ "Stand-up (moved)",
181
+ CalendarEventStatus.Cancelled,
182
+ CalendarTransparency.Transparent,
183
+ ],
184
+ ],
185
+ );
186
+ });
187
+
142
188
  test("deleteForObject removes every occurrence of one resource", async () => {
143
189
  const calendarId = harness.makeId();
144
190
  const calendarObjectId = harness.makeId();
@@ -107,6 +107,49 @@ export function calendarObjectRepositoryConformance(
107
107
  );
108
108
  });
109
109
 
110
+ test("find answers with null where get throws", async () => {
111
+ const calendarId = harness.makeId();
112
+ const object = await repo.put(objectInput(calendarId));
113
+
114
+ assert.equal(
115
+ (await repo.find(calendarId, object.calendarObjectId))?.icalData,
116
+ ICAL_DATA,
117
+ );
118
+ assert.equal(await repo.find(calendarId, harness.makeId()), null);
119
+ assert.equal(
120
+ await repo.find(harness.makeId(), object.calendarObjectId),
121
+ null,
122
+ "a resource is only found through the collection that holds it",
123
+ );
124
+ });
125
+
126
+ test("listIncompleteExpansions returns only the series whose index stops short", async () => {
127
+ const calendarId = harness.makeId();
128
+ await repo.put(objectInput(calendarId, { resourceName: "complete.ics" }));
129
+ const incomplete = await repo.put(
130
+ objectInput(calendarId, {
131
+ resourceName: "incomplete.ics",
132
+ expandedThrough: "2026-01-01T00:00:00Z",
133
+ }),
134
+ );
135
+
136
+ assert.deepEqual(
137
+ (
138
+ await repo.listIncompleteExpansions(
139
+ calendarId,
140
+ "2026-06-01T00:00:00Z",
141
+ )
142
+ ).map((object) => object.calendarObjectId),
143
+ [incomplete.calendarObjectId],
144
+ "a resource with no expandedThrough is complete and never appears",
145
+ );
146
+ assert.deepEqual(
147
+ await repo.listIncompleteExpansions(calendarId, "2025-06-01T00:00:00Z"),
148
+ [],
149
+ "and one whose index already reaches the window does not either",
150
+ );
151
+ });
152
+
110
153
  test("findByResourceName returns null for a name the collection does not hold", async () => {
111
154
  assert.equal(
112
155
  await repo.findByResourceName(harness.makeId(), "absent.ics"),
@@ -12,6 +12,18 @@ export interface ICalendarCollectionRepository {
12
12
  * request without a prior read.
13
13
  */
14
14
  create(input: CreateCalendarCollectionInput): Promise<CalendarCollectionItem>;
15
+ /**
16
+ * Writes the collection, or answers `null` when its `urlSegment` is already
17
+ * taken in this account config.
18
+ *
19
+ * The distinction from `create` is who asked. Provisioning wants the row
20
+ * whoever got there first wrote; a person naming a calendar wants to be told
21
+ * the name is taken, and handing them somebody else's calendar instead is a
22
+ * silent merge of two things they meant to keep apart.
23
+ */
24
+ createExclusive(
25
+ input: CreateCalendarCollectionInput,
26
+ ): Promise<CalendarCollectionItem | null>;
15
27
  get(
16
28
  accountConfigId: string,
17
29
  calendarId: string,
@@ -6,6 +6,16 @@ export interface ICalendarObjectRepository {
6
6
  calendarId: string,
7
7
  calendarObjectId: string,
8
8
  ): Promise<CalendarObjectItem>;
9
+ /**
10
+ * The resource under an id, or `null` when the collection does not hold it.
11
+ * `get` throws instead, which is right for a caller that already knows the
12
+ * resource exists; a request naming one is asking whether it does, and that
13
+ * is an answer rather than a fault.
14
+ */
15
+ find(
16
+ calendarId: string,
17
+ calendarObjectId: string,
18
+ ): Promise<CalendarObjectItem | null>;
9
19
  delete(calendarId: string, calendarObjectId: string): Promise<void>;
10
20
  findByResourceName(
11
21
  calendarId: string,
@@ -21,6 +31,17 @@ export interface ICalendarObjectRepository {
21
31
  icalUid: string,
22
32
  ): Promise<CalendarObjectItem | null>;
23
33
  listByCalendar(calendarId: string): Promise<CalendarObjectItem[]>;
34
+ /**
35
+ * The resources whose occurrence rows stop before `instant` — the series a
36
+ * range read has to expand for itself because the index does not reach that
37
+ * far. Narrow by construction: a resource that fits inside the expansion
38
+ * horizon carries no `expandedThrough` and never appears here, so a view of
39
+ * an ordinary calendar reads no iCalendar text at all.
40
+ */
41
+ listIncompleteExpansions(
42
+ calendarId: string,
43
+ instant: string,
44
+ ): Promise<CalendarObjectItem[]>;
24
45
  /**
25
46
  * Everything written to the collection after `syncSequence`, in change
26
47
  * order — the read behind a WebDAV-Sync report. Exclusive of the token