@craftguild/jscalendar 0.4.1 → 0.5.1
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/README.md +84 -45
- package/dist/__tests__/builders.test.js +19 -6
- package/dist/__tests__/calendar-extra.test.js +27 -27
- package/dist/__tests__/calendar.test.js +13 -20
- package/dist/__tests__/recurrence.test.js +20 -1
- package/dist/__tests__/validation.test.js +9 -5
- package/dist/jscal/base.d.ts +27 -28
- package/dist/jscal/base.js +55 -63
- package/dist/jscal/builders.d.ts +56 -24
- package/dist/jscal/builders.js +70 -33
- package/dist/jscal/event.d.ts +8 -2
- package/dist/jscal/event.js +10 -3
- package/dist/jscal/group.d.ts +10 -4
- package/dist/jscal/group.js +15 -8
- package/dist/jscal/task.d.ts +8 -2
- package/dist/jscal/task.js +10 -3
- package/dist/jscal.d.ts +9 -2
- package/dist/jscal.js +9 -1
- package/dist/patch.d.ts +2 -2
- package/dist/recurrence/expand.d.ts +1 -1
- package/dist/recurrence/expand.js +28 -7
- package/dist/types.d.ts +31 -4
- package/dist/validate/asserts.d.ts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,14 +22,38 @@ pnpm add @craftguild/jscalendar
|
|
|
22
22
|
```ts
|
|
23
23
|
import { JsCal } from "@craftguild/jscalendar";
|
|
24
24
|
|
|
25
|
+
// Create a recurring event and a simple task, then expand occurrences.
|
|
25
26
|
const event = new JsCal.Event({
|
|
26
|
-
title: "
|
|
27
|
-
start:
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
title: "Weekly Sync",
|
|
28
|
+
start: new Date(2026, 0, 1, 0, 0, 0, 0),
|
|
29
|
+
recurrenceRules: [
|
|
30
|
+
JsCal.RecurrenceRule({
|
|
31
|
+
frequency: "weekly",
|
|
32
|
+
byDay: [JsCal.ByDay({ day: "th" })],
|
|
33
|
+
}),
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const task = new JsCal.Task({
|
|
38
|
+
title: "Prepare Notes",
|
|
39
|
+
start: new Date(2026, 0, 1, 0, 0, 0, 0),
|
|
30
40
|
});
|
|
31
41
|
|
|
32
|
-
const
|
|
42
|
+
const from = new Date(2026, 0, 1, 0, 0, 0, 0);
|
|
43
|
+
const to = new Date(2026, 0, 31, 0, 0, 0, 0);
|
|
44
|
+
const generator = JsCal.expandRecurrence([event, task], { from, to });
|
|
45
|
+
|
|
46
|
+
for (const item of generator) {
|
|
47
|
+
// Expanded JSCalendar objects for events and tasks in the range.
|
|
48
|
+
console.log(JSON.stringify(item));
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Sample output (truncated):
|
|
53
|
+
```txt
|
|
54
|
+
{"title":"Weekly Sync","@type":"Event","start":"2026-01-01T00:00:00",...}
|
|
55
|
+
{"title":"Prepare Notes","@type":"Task","start":"2026-01-01T00:00:00",...}
|
|
56
|
+
{"title":"Weekly Sync","@type":"Event","start":"2026-01-08T00:00:00",...}
|
|
33
57
|
```
|
|
34
58
|
|
|
35
59
|
## Object Creation
|
|
@@ -93,14 +117,14 @@ const task = new JsCal.Task({
|
|
|
93
117
|
title: "Write report",
|
|
94
118
|
start: "2026-02-11T09:00:00",
|
|
95
119
|
participants: JsCal.participants([
|
|
96
|
-
JsCal.Participant({ name: "Alice", email: "a@example.com", roles: { attendee: true } }),
|
|
97
|
-
JsCal.Participant({ name: "Bob", roles: { attendee: true } }),
|
|
120
|
+
{ value: JsCal.Participant({ name: "Alice", email: "a@example.com", roles: { attendee: true } }) },
|
|
121
|
+
{ value: JsCal.Participant({ name: "Bob", roles: { attendee: true } }) },
|
|
98
122
|
]),
|
|
99
123
|
locations: JsCal.locations([
|
|
100
|
-
JsCal.Location({ name: "Room A" }),
|
|
124
|
+
{ value: JsCal.Location({ name: "Room A" }) },
|
|
101
125
|
]),
|
|
102
126
|
alerts: JsCal.alerts([
|
|
103
|
-
JsCal.Alert({ trigger: JsCal.OffsetTrigger({ offset: JsCal.duration.minutes(-15) }) }),
|
|
127
|
+
{ value: JsCal.Alert({ trigger: JsCal.OffsetTrigger({ offset: JsCal.duration.minutes(-15) }) }) },
|
|
104
128
|
]),
|
|
105
129
|
});
|
|
106
130
|
```
|
|
@@ -112,7 +136,7 @@ const task = new JsCal.Task({
|
|
|
112
136
|
title: "Imported task",
|
|
113
137
|
start: "2026-02-11T09:00:00",
|
|
114
138
|
participants: {
|
|
115
|
-
p1: { "@type": "Participant", name: "Alice", email: "a@example.com" },
|
|
139
|
+
p1: { "@type": "Participant", name: "Alice", email: "a@example.com", roles: { attendee: true } },
|
|
116
140
|
},
|
|
117
141
|
locations: {
|
|
118
142
|
l1: { "@type": "Location", name: "Room A" },
|
|
@@ -143,8 +167,8 @@ clone of that underlying JSCalendar object for serialization, storage,
|
|
|
143
167
|
or passing across app boundaries.
|
|
144
168
|
|
|
145
169
|
Why `eject()` exists:
|
|
146
|
-
- Class instances are convenient for building and
|
|
147
|
-
helpers like `
|
|
170
|
+
- Class instances are convenient for building and updating objects with
|
|
171
|
+
helpers like `patch`.
|
|
148
172
|
- External APIs, storage layers, and JSON stringify expect plain objects.
|
|
149
173
|
- A deep clone makes it safe to hand off data without accidental mutation
|
|
150
174
|
from the original instance (and vice versa).
|
|
@@ -163,24 +187,59 @@ JSON.stringify(plain);
|
|
|
163
187
|
|
|
164
188
|
// Changes do not affect each other.
|
|
165
189
|
plain.title = "Exported";
|
|
166
|
-
event.
|
|
190
|
+
const updated = event.patch({ title: "Live" });
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Patch Usage
|
|
194
|
+
|
|
195
|
+
Patch helpers return new instances and keep metadata such as
|
|
196
|
+
`updated` and `sequence` consistent. Use `patch` for RFC 8984 PatchObject
|
|
197
|
+
semantics. You can set raw values directly, or use helper methods if you
|
|
198
|
+
prefer validated, type-safe inputs.
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
const patchedEvent = event.patch({ title: "Updated title" });
|
|
202
|
+
const patchedAgain = patchedEvent.patch({ title: "Patched title" });
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
You can also patch nested maps by replacing the full map in one call:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const withParticipants = event.patch({
|
|
209
|
+
participants: {
|
|
210
|
+
p1: { "@type": "Participant", roles: { attendee: true }, email: "a@example.com" },
|
|
211
|
+
},
|
|
212
|
+
});
|
|
167
213
|
```
|
|
168
214
|
|
|
169
|
-
|
|
215
|
+
Two common patterns for nested patches:
|
|
170
216
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
217
|
+
1) Set raw values directly
|
|
218
|
+
```ts
|
|
219
|
+
const withLocations = event.patch({
|
|
220
|
+
locations: {
|
|
221
|
+
l1: { "@type": "Location", name: "Room A" },
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
```
|
|
174
225
|
|
|
226
|
+
2) Use helpers to build or merge map values
|
|
175
227
|
```ts
|
|
176
|
-
event.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
228
|
+
const withLocations = event.patch({
|
|
229
|
+
locations: JsCal.locations([
|
|
230
|
+
{ id: "l1", value: JsCal.Location({ name: "Room A" }) },
|
|
231
|
+
{ value: JsCal.Location({ name: "Room B" }) },
|
|
232
|
+
]),
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
To merge into an existing map, pass the current map as the second argument:
|
|
237
|
+
```ts
|
|
238
|
+
const withLocations = event.patch({
|
|
239
|
+
locations: JsCal.locations(
|
|
240
|
+
[{ value: JsCal.Location({ name: "Room C" }) }],
|
|
241
|
+
event.data.locations,
|
|
242
|
+
),
|
|
184
243
|
});
|
|
185
244
|
```
|
|
186
245
|
|
|
@@ -320,26 +379,6 @@ const icalMany = JsCal.toICal([event, task], { includeXJSCalendar: true });
|
|
|
320
379
|
console.log(icalMany);
|
|
321
380
|
```
|
|
322
381
|
|
|
323
|
-
## Practical Example
|
|
324
|
-
|
|
325
|
-
**Weekly engineering sync (every Wednesday 10:30, 1 hour)**
|
|
326
|
-
|
|
327
|
-
```ts
|
|
328
|
-
const weekly = new JsCal.Event({
|
|
329
|
-
title: "Engineering Sync",
|
|
330
|
-
start: "2026-02-04T10:30:00",
|
|
331
|
-
timeZone: "Asia/Tokyo",
|
|
332
|
-
duration: JsCal.duration.hours(1),
|
|
333
|
-
recurrenceRules: [
|
|
334
|
-
{
|
|
335
|
-
"@type": "RecurrenceRule",
|
|
336
|
-
frequency: "weekly",
|
|
337
|
-
byDay: [{ "@type": "NDay", day: "we" }],
|
|
338
|
-
},
|
|
339
|
-
],
|
|
340
|
-
});
|
|
341
|
-
```
|
|
342
|
-
|
|
343
382
|
## Compliance and Deviations
|
|
344
383
|
|
|
345
384
|
### RFC 8984 Conformance (Implemented)
|
|
@@ -34,17 +34,30 @@ describe("builders", () => {
|
|
|
34
34
|
});
|
|
35
35
|
it("builds id maps with generated ids", () => {
|
|
36
36
|
const participants = JsCal.participants([
|
|
37
|
-
{ name: "Alice", roles: { attendee: true } },
|
|
38
|
-
{ name: "Bob", roles: { attendee: true } },
|
|
37
|
+
{ value: { name: "Alice", roles: { attendee: true } } },
|
|
38
|
+
{ value: { name: "Bob", roles: { attendee: true } } },
|
|
39
39
|
]);
|
|
40
40
|
expect(Object.keys(participants).length).toBe(2);
|
|
41
|
-
const locations = JsCal.locations([{ name: "Room A" }]);
|
|
41
|
+
const locations = JsCal.locations([{ value: { name: "Room A" } }]);
|
|
42
42
|
expect(Object.keys(locations).length).toBe(1);
|
|
43
|
-
const links = JsCal.links([{ href: "https://example.com" }]);
|
|
43
|
+
const links = JsCal.links([{ value: { href: "https://example.com" } }]);
|
|
44
44
|
expect(Object.keys(links).length).toBe(1);
|
|
45
|
-
const related = JsCal.relatedTo([{ relation: { parent: true } }]);
|
|
45
|
+
const related = JsCal.relatedTo([{ value: { relation: { parent: true } } }]);
|
|
46
46
|
expect(Object.keys(related).length).toBe(1);
|
|
47
47
|
});
|
|
48
|
+
it("merges id maps with explicit ids", () => {
|
|
49
|
+
const existing = JsCal.locations([{ id: "loc-1", value: { name: "Room A" } }]);
|
|
50
|
+
const merged = JsCal.locations([
|
|
51
|
+
{ id: "loc-1", value: { name: "Room A Updated" } },
|
|
52
|
+
{ value: { name: "Room B" } },
|
|
53
|
+
], existing);
|
|
54
|
+
expect(existing["loc-1"]?.name).toBe("Room A");
|
|
55
|
+
expect(merged["loc-1"]?.name).toBe("Room A Updated");
|
|
56
|
+
expect(Object.keys(merged).length).toBe(2);
|
|
57
|
+
});
|
|
58
|
+
it("rejects wrapper inputs that mix value and direct fields", () => {
|
|
59
|
+
expect(() => JsCal.locations([{ name: "Room A" }])).toThrowError();
|
|
60
|
+
});
|
|
48
61
|
it("builds a time zone map keyed by tzId", () => {
|
|
49
62
|
const map = buildTimeZoneMap([
|
|
50
63
|
{ tzId: "Asia/Tokyo", standard: [{ "@type": "TimeZoneRule", start: "2026-01-01T00:00:00", offsetFrom: "+09:00", offsetTo: "+09:00" }] },
|
|
@@ -52,7 +65,7 @@ describe("builders", () => {
|
|
|
52
65
|
expect(map["Asia/Tokyo"]?.tzId).toBe("Asia/Tokyo");
|
|
53
66
|
});
|
|
54
67
|
it("buildIdMap uses a custom id function", () => {
|
|
55
|
-
const map = buildIdMap([{ name: "A" }], (item) => item, (_item, index) => `id-${index}`);
|
|
68
|
+
const map = buildIdMap([{ value: { name: "A" } }], (item) => item, (_item, index) => `id-${index}`);
|
|
56
69
|
expect(Object.keys(map)).toEqual(["id-0"]);
|
|
57
70
|
});
|
|
58
71
|
it("throws when @type mismatches", () => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { JsCal, createId, isEvent, isGroup, isTask } from "../jscal.js";
|
|
3
|
-
import { Base } from "../jscal/base.js";
|
|
4
3
|
const fixedNow = () => "2026-02-01T00:00:00Z";
|
|
5
4
|
function makeEvent() {
|
|
6
5
|
return new JsCal.Event({
|
|
@@ -12,8 +11,9 @@ describe("JsCal helpers", () => {
|
|
|
12
11
|
it("creates group and adds entry", () => {
|
|
13
12
|
const group = new JsCal.Group({ entries: [] }, { now: fixedNow });
|
|
14
13
|
const event = makeEvent();
|
|
15
|
-
group.addEntry(event.data);
|
|
16
|
-
expect(group.data.entries.length).toBe(
|
|
14
|
+
const nextGroup = group.addEntry(event.data);
|
|
15
|
+
expect(group.data.entries.length).toBe(0);
|
|
16
|
+
expect(nextGroup.data.entries.length).toBe(1);
|
|
17
17
|
});
|
|
18
18
|
it("accepts JsCal instances and ejected entries in groups", () => {
|
|
19
19
|
const event = makeEvent();
|
|
@@ -31,10 +31,12 @@ describe("JsCal helpers", () => {
|
|
|
31
31
|
});
|
|
32
32
|
it("adds locations and virtual locations", () => {
|
|
33
33
|
const event = makeEvent();
|
|
34
|
-
const locId =
|
|
35
|
-
const vlocId =
|
|
36
|
-
const
|
|
37
|
-
const
|
|
34
|
+
const locId = "loc-1";
|
|
35
|
+
const vlocId = "vloc-1";
|
|
36
|
+
const withLocation = event.addLocation({ name: "Room A" }, locId);
|
|
37
|
+
const withVirtualLocation = withLocation.addVirtualLocation({ name: "Zoom", uri: "https://example.com" }, vlocId);
|
|
38
|
+
const locations = withVirtualLocation.data.locations;
|
|
39
|
+
const virtualLocations = withVirtualLocation.data.virtualLocations;
|
|
38
40
|
expect(locations).toBeDefined();
|
|
39
41
|
expect(virtualLocations).toBeDefined();
|
|
40
42
|
if (!locations || !virtualLocations)
|
|
@@ -44,10 +46,12 @@ describe("JsCal helpers", () => {
|
|
|
44
46
|
});
|
|
45
47
|
it("adds participants and alerts", () => {
|
|
46
48
|
const event = makeEvent();
|
|
47
|
-
const participantId =
|
|
48
|
-
const alertId =
|
|
49
|
-
const
|
|
50
|
-
const
|
|
49
|
+
const participantId = "p1";
|
|
50
|
+
const alertId = "a1";
|
|
51
|
+
const withParticipant = event.addParticipant({ roles: { attendee: true }, email: "a@example.com" }, participantId);
|
|
52
|
+
const withAlert = withParticipant.addAlert({ trigger: { "@type": "AbsoluteTrigger", when: "2026-02-01T01:00:00Z" } }, alertId);
|
|
53
|
+
const participants = withAlert.data.participants;
|
|
54
|
+
const alerts = withAlert.data.alerts;
|
|
51
55
|
expect(participants).toBeDefined();
|
|
52
56
|
expect(alerts).toBeDefined();
|
|
53
57
|
if (!participants || !alerts)
|
|
@@ -63,8 +67,9 @@ describe("JsCal helpers", () => {
|
|
|
63
67
|
});
|
|
64
68
|
it("defaults relativeTo for offset triggers", () => {
|
|
65
69
|
const event = makeEvent();
|
|
66
|
-
const alertId =
|
|
67
|
-
const
|
|
70
|
+
const alertId = "a1";
|
|
71
|
+
const withAlert = event.addAlert({ trigger: { "@type": "OffsetTrigger", offset: "PT15M" } }, alertId);
|
|
72
|
+
const alerts = withAlert.data.alerts;
|
|
68
73
|
expect(alerts).toBeDefined();
|
|
69
74
|
if (!alerts)
|
|
70
75
|
throw new Error("Missing alerts");
|
|
@@ -76,10 +81,10 @@ describe("JsCal helpers", () => {
|
|
|
76
81
|
it("updates sequence for alerts but not for participants", () => {
|
|
77
82
|
const event = makeEvent();
|
|
78
83
|
const initialSequence = event.data.sequence ?? 0;
|
|
79
|
-
event.addParticipant({ roles: { attendee: true }, email: "a@example.com" });
|
|
80
|
-
expect(
|
|
81
|
-
|
|
82
|
-
expect(
|
|
84
|
+
const withParticipant = event.addParticipant({ roles: { attendee: true }, email: "a@example.com" }, "p1");
|
|
85
|
+
expect(withParticipant.data.sequence ?? 0).toBe(initialSequence);
|
|
86
|
+
const withAlert = withParticipant.addAlert({ trigger: { "@type": "AbsoluteTrigger", when: "2026-02-01T01:00:00Z" } }, "a1");
|
|
87
|
+
expect(withAlert.data.sequence ?? 0).toBe(initialSequence + 1);
|
|
83
88
|
});
|
|
84
89
|
it("clones and eject return deep copies", () => {
|
|
85
90
|
const event = makeEvent();
|
|
@@ -90,12 +95,6 @@ describe("JsCal helpers", () => {
|
|
|
90
95
|
json.title = "Changed";
|
|
91
96
|
expect(event.data.title).toBe("Kickoff");
|
|
92
97
|
});
|
|
93
|
-
it("clones Base instances with deep-copied data", () => {
|
|
94
|
-
const base = new Base({ "@type": "Event", uid: "base", updated: "2026-02-01T00:00:00Z", start: "2026-02-01T10:00:00" });
|
|
95
|
-
const cloned = base.clone();
|
|
96
|
-
cloned.data.uid = "changed";
|
|
97
|
-
expect(base.data.uid).toBe("base");
|
|
98
|
-
});
|
|
99
98
|
it("builds participant inputs without @type", () => {
|
|
100
99
|
const participant = JsCal.Participant({ name: "Alice", roles: { attendee: true } });
|
|
101
100
|
const task = new JsCal.Task({
|
|
@@ -106,8 +105,8 @@ describe("JsCal helpers", () => {
|
|
|
106
105
|
});
|
|
107
106
|
it("builds participant maps with stable ids", () => {
|
|
108
107
|
const participants = JsCal.participants([
|
|
109
|
-
{ name: "Alice", roles: { attendee: true } },
|
|
110
|
-
{ name: "Bob", roles: { attendee: true } },
|
|
108
|
+
{ value: { name: "Alice", roles: { attendee: true } } },
|
|
109
|
+
{ value: { name: "Bob", roles: { attendee: true } } },
|
|
111
110
|
]);
|
|
112
111
|
const task = new JsCal.Task({
|
|
113
112
|
start: "2026-02-02T10:00:00",
|
|
@@ -207,8 +206,9 @@ describe("JsCal helpers", () => {
|
|
|
207
206
|
it("supports get/set helpers", () => {
|
|
208
207
|
const event = makeEvent();
|
|
209
208
|
expect(event.get("title")).toBe("Kickoff");
|
|
210
|
-
event.set("title", "Updated");
|
|
211
|
-
expect(event.get("title")).toBe("
|
|
209
|
+
const updated = event.set("title", "Updated");
|
|
210
|
+
expect(event.get("title")).toBe("Kickoff");
|
|
211
|
+
expect(updated.get("title")).toBe("Updated");
|
|
212
212
|
});
|
|
213
213
|
it("exposes type guards", () => {
|
|
214
214
|
const event = makeEvent().eject();
|
|
@@ -56,36 +56,29 @@ describe("JsCal.Event", () => {
|
|
|
56
56
|
expect(negative.data.duration).toBe("PT0S");
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
|
-
describe("Event.
|
|
60
|
-
it("increments sequence on non-participant
|
|
61
|
-
const event = makeEvent();
|
|
62
|
-
event.update({ title: "Updated" }, { now: fixedNow });
|
|
63
|
-
expect(event.data.sequence).toBe(1);
|
|
64
|
-
});
|
|
65
|
-
it("does not increment sequence when only participants change", () => {
|
|
59
|
+
describe("Event.patch", () => {
|
|
60
|
+
it("increments sequence on non-participant patches", () => {
|
|
66
61
|
const event = makeEvent();
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
expect(event.data.sequence
|
|
62
|
+
const patched = event.patch({ title: "Updated" }, { now: fixedNow });
|
|
63
|
+
expect(patched.data.sequence).toBe(1);
|
|
64
|
+
expect(event.data.sequence).toBe(0);
|
|
70
65
|
});
|
|
71
66
|
it("respects touch=false", () => {
|
|
72
67
|
const event = makeEvent();
|
|
73
68
|
const before = event.data.updated;
|
|
74
|
-
event.
|
|
75
|
-
expect(
|
|
69
|
+
const patched = event.patch({ title: "No touch" }, { touch: false, now: () => "2026-02-02T00:00:00Z" });
|
|
70
|
+
expect(patched.data.updated).toBe(before);
|
|
76
71
|
});
|
|
77
|
-
});
|
|
78
|
-
describe("Event.patch", () => {
|
|
79
72
|
it("applies patch and updates metadata", () => {
|
|
80
73
|
const event = makeEvent();
|
|
81
|
-
event.patch({ title: "Patched" }, { now: fixedNow });
|
|
82
|
-
expect(
|
|
83
|
-
expect(
|
|
84
|
-
expect(
|
|
74
|
+
const patched = event.patch({ title: "Patched" }, { now: fixedNow });
|
|
75
|
+
expect(patched.data.title).toBe("Patched");
|
|
76
|
+
expect(patched.data.updated).toBe("2026-02-01T00:00:00Z");
|
|
77
|
+
expect(patched.data.sequence).toBe(1);
|
|
85
78
|
});
|
|
86
79
|
it("does not increment sequence for participants-only patch", () => {
|
|
87
80
|
const event = makeEvent();
|
|
88
|
-
event.patch({
|
|
81
|
+
const patched = event.patch({
|
|
89
82
|
participants: {
|
|
90
83
|
p1: {
|
|
91
84
|
"@type": "Participant",
|
|
@@ -93,7 +86,7 @@ describe("Event.patch", () => {
|
|
|
93
86
|
},
|
|
94
87
|
},
|
|
95
88
|
}, { now: fixedNow });
|
|
96
|
-
expect(
|
|
89
|
+
expect(patched.data.sequence ?? 0).toBe(0);
|
|
97
90
|
});
|
|
98
91
|
});
|
|
99
92
|
describe("createUid", () => {
|
|
@@ -9,6 +9,25 @@ function collect(gen) {
|
|
|
9
9
|
return result;
|
|
10
10
|
}
|
|
11
11
|
describe("recurrence expansion", () => {
|
|
12
|
+
it("sorts occurrences by recurrenceId across items", () => {
|
|
13
|
+
const first = new JsCal.Event({
|
|
14
|
+
title: "First",
|
|
15
|
+
start: "2026-02-01T09:00:00",
|
|
16
|
+
});
|
|
17
|
+
const second = new JsCal.Event({
|
|
18
|
+
title: "Second",
|
|
19
|
+
start: "2026-02-03T09:00:00",
|
|
20
|
+
});
|
|
21
|
+
const occ = collect(JsCal.expandRecurrence([second, first], {
|
|
22
|
+
from: new Date("2026-02-01"),
|
|
23
|
+
to: new Date("2026-02-10"),
|
|
24
|
+
}));
|
|
25
|
+
const starts = occ.map((o) => o.recurrenceId ?? ("start" in o ? o.start : undefined));
|
|
26
|
+
expect(starts).toEqual([
|
|
27
|
+
"2026-02-01T09:00:00",
|
|
28
|
+
"2026-02-03T09:00:00",
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
12
31
|
it("expands weekly byDay", () => {
|
|
13
32
|
const event = new JsCal.Event({
|
|
14
33
|
title: "Weekly",
|
|
@@ -535,7 +554,7 @@ describe("recurrence expansion", () => {
|
|
|
535
554
|
{ "@type": "RecurrenceRule", frequency: "daily", count: 2 },
|
|
536
555
|
],
|
|
537
556
|
recurrenceOverrides: {
|
|
538
|
-
"2026-02-02T10:00:00": {
|
|
557
|
+
"2026-02-02T10:00:00": { due: "2026-02-05T10:00:00" },
|
|
539
558
|
},
|
|
540
559
|
});
|
|
541
560
|
const occ = Array.from(JsCal.expandRecurrence([task], {
|
|
@@ -22,11 +22,10 @@ describe("validation", () => {
|
|
|
22
22
|
descriptionContentType: "text/plain; charset=ascii",
|
|
23
23
|
})).toThrowError("object.descriptionContentType: charset parameter must be utf-8");
|
|
24
24
|
});
|
|
25
|
-
it("allows validation to be disabled for create
|
|
25
|
+
it("allows validation to be disabled for create and patch", () => {
|
|
26
26
|
const event = new JsCal.Event({ start: "2026-02-01T10:00:00Z" }, { validate: false });
|
|
27
27
|
expect(event.get("start")).toBe("2026-02-01T10:00:00Z");
|
|
28
|
-
event.
|
|
29
|
-
event.patch({ "/start": "2026-02-01T10:00:00Z" }, { validate: false });
|
|
28
|
+
event.patch({ start: "2026-02-01T10:00:00Z" }, { validate: false });
|
|
30
29
|
});
|
|
31
30
|
it("throws ValidationError with path and message", () => {
|
|
32
31
|
expect(() => new JsCal.Event({ start: "2026-02-01T10:00:00Z" })).toThrowError(ValidationError);
|
|
@@ -159,10 +158,15 @@ describe("validation", () => {
|
|
|
159
158
|
},
|
|
160
159
|
},
|
|
161
160
|
localizations: {
|
|
162
|
-
en: { title: "Localized", keywords:
|
|
161
|
+
en: { title: "Localized", keywords: { a: true } },
|
|
163
162
|
},
|
|
164
163
|
recurrenceOverrides: {
|
|
165
|
-
"2026-02-02T10:00:00": {
|
|
164
|
+
"2026-02-02T10:00:00": {
|
|
165
|
+
title: null,
|
|
166
|
+
locations: {
|
|
167
|
+
loc1: { "@type": "Location", name: "Room A" },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
166
170
|
},
|
|
167
171
|
timeZones: {
|
|
168
172
|
"Asia/Tokyo": {
|
package/dist/jscal/base.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Alert, Id, JSCalendarObject, Location, Participant,
|
|
1
|
+
import type { Alert, Id, JSCalendarObject, Location, Participant, PatchLike, VirtualLocation } from "../types.js";
|
|
2
2
|
import type { UpdateOptions } from "./types.js";
|
|
3
|
-
export declare class Base<T extends JSCalendarObject
|
|
3
|
+
export declare abstract class Base<T extends JSCalendarObject, TPatch extends PatchLike, TSelf extends Base<T, TPatch, TSelf>> {
|
|
4
4
|
data: T;
|
|
5
5
|
/**
|
|
6
6
|
* Create a new base instance that wraps a JSCalendar object.
|
|
@@ -8,6 +8,12 @@ export declare class Base<T extends JSCalendarObject> {
|
|
|
8
8
|
* @return Result.
|
|
9
9
|
*/
|
|
10
10
|
constructor(data: T);
|
|
11
|
+
/**
|
|
12
|
+
* Wrap updated data in a new instance.
|
|
13
|
+
* @param data Updated JSCalendar data.
|
|
14
|
+
* @return New instance containing the data.
|
|
15
|
+
*/
|
|
16
|
+
protected abstract wrap(data: T): TSelf;
|
|
11
17
|
/**
|
|
12
18
|
* Return a deep-cloned plain object for safe serialization.
|
|
13
19
|
* @return Cloned JSCalendar data.
|
|
@@ -17,7 +23,7 @@ export declare class Base<T extends JSCalendarObject> {
|
|
|
17
23
|
* Clone the current instance with a deep-cloned payload.
|
|
18
24
|
* @return New instance with cloned data.
|
|
19
25
|
*/
|
|
20
|
-
clone():
|
|
26
|
+
clone(): TSelf;
|
|
21
27
|
/**
|
|
22
28
|
* Read a field value from the underlying data.
|
|
23
29
|
* @param key Field key.
|
|
@@ -28,63 +34,56 @@ export declare class Base<T extends JSCalendarObject> {
|
|
|
28
34
|
* Set a field value and update metadata as needed.
|
|
29
35
|
* @param key Field key.
|
|
30
36
|
* @param value Field value.
|
|
31
|
-
* @return
|
|
32
|
-
*/
|
|
33
|
-
set<K extends keyof T>(key: K, value: T[K]): this;
|
|
34
|
-
/**
|
|
35
|
-
* Apply shallow updates and touch updated/sequence metadata.
|
|
36
|
-
* @param values Partial values to merge.
|
|
37
|
-
* @param options Update options.
|
|
38
|
-
* @return Updated instance.
|
|
37
|
+
* @return New instance with the updated field.
|
|
39
38
|
*/
|
|
40
|
-
|
|
39
|
+
set<K extends keyof T>(key: K, value: T[K]): TSelf;
|
|
41
40
|
/**
|
|
42
41
|
* Apply a PatchObject and touch updated/sequence metadata.
|
|
43
42
|
* @param patch Patch to apply.
|
|
44
43
|
* @param options Update options.
|
|
45
|
-
* @return
|
|
44
|
+
* @return New instance with applied patch.
|
|
46
45
|
*/
|
|
47
|
-
patch(patch:
|
|
46
|
+
patch(patch: TPatch, options?: UpdateOptions): TSelf;
|
|
48
47
|
/**
|
|
49
|
-
* Add a physical location and return
|
|
48
|
+
* Add a physical location and return a new instance.
|
|
50
49
|
* @param location Location data (without @type).
|
|
51
50
|
* @param id Optional location ID.
|
|
52
|
-
* @return
|
|
51
|
+
* @return New instance with the added location.
|
|
53
52
|
*/
|
|
54
|
-
addLocation(location: Omit<Location, "@type"> & Partial<Pick<Location, "@type">>, id?: Id):
|
|
53
|
+
addLocation(location: Omit<Location, "@type"> & Partial<Pick<Location, "@type">>, id?: Id): TSelf;
|
|
55
54
|
/**
|
|
56
|
-
* Add a virtual location and return
|
|
55
|
+
* Add a virtual location and return a new instance.
|
|
57
56
|
* @param location Virtual location data (without @type).
|
|
58
57
|
* @param id Optional virtual location ID.
|
|
59
|
-
* @return
|
|
58
|
+
* @return New instance with the added virtual location.
|
|
60
59
|
*/
|
|
61
|
-
addVirtualLocation(location: Omit<VirtualLocation, "@type"> & Partial<Pick<VirtualLocation, "@type">>, id?: Id):
|
|
60
|
+
addVirtualLocation(location: Omit<VirtualLocation, "@type"> & Partial<Pick<VirtualLocation, "@type">>, id?: Id): TSelf;
|
|
62
61
|
/**
|
|
63
|
-
* Add a participant and return
|
|
62
|
+
* Add a participant and return a new instance.
|
|
64
63
|
* @param participant Participant data (without @type).
|
|
65
64
|
* @param id Optional participant ID.
|
|
66
|
-
* @return
|
|
65
|
+
* @return New instance with the added participant.
|
|
67
66
|
*/
|
|
68
|
-
addParticipant(participant: Omit<Participant, "@type"> & Partial<Pick<Participant, "@type">>, id?: Id):
|
|
67
|
+
addParticipant(participant: Omit<Participant, "@type"> & Partial<Pick<Participant, "@type">>, id?: Id): TSelf;
|
|
69
68
|
/**
|
|
70
|
-
* Add an alert and return
|
|
69
|
+
* Add an alert and return a new instance.
|
|
71
70
|
* @param alert Alert data (without @type).
|
|
72
71
|
* @param id Optional alert ID.
|
|
73
|
-
* @return
|
|
72
|
+
* @return New instance with the added alert.
|
|
74
73
|
*/
|
|
75
|
-
addAlert(alert: Omit<Alert, "@type"> & Partial<Pick<Alert, "@type">>, id?: Id):
|
|
74
|
+
addAlert(alert: Omit<Alert, "@type"> & Partial<Pick<Alert, "@type">>, id?: Id): TSelf;
|
|
76
75
|
/**
|
|
77
76
|
* Update updated/sequence metadata for modified keys.
|
|
78
77
|
* @param keys Modified keys.
|
|
79
78
|
* @param options Update options.
|
|
80
79
|
* @return Updated instance.
|
|
81
80
|
*/
|
|
82
|
-
protected touchKeys(keys: string[], options?: UpdateOptions):
|
|
81
|
+
protected touchKeys(data: T, keys: string[], options?: UpdateOptions): T;
|
|
83
82
|
/**
|
|
84
83
|
* Update updated/sequence metadata for PatchObject changes.
|
|
85
84
|
* @param patch Patch applied to the object.
|
|
86
85
|
* @param options Update options.
|
|
87
86
|
* @return Updated instance.
|
|
88
87
|
*/
|
|
89
|
-
protected touchFromPatch(patch:
|
|
88
|
+
protected touchFromPatch(data: T, patch: PatchLike, options?: UpdateOptions): T;
|
|
90
89
|
}
|