caldav-adapter 8.3.5 → 8.3.6

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,7 +1,7 @@
1
1
  {
2
2
  "name": "caldav-adapter",
3
3
  "description": "CalDAV server for Node.js and Koa. Modernized and maintained for Forward Email.",
4
- "version": "8.3.5",
4
+ "version": "8.3.6",
5
5
  "author": "Sanders DeNardi and Forward Email LLC",
6
6
  "contributors": [
7
7
  "Sanders DeNardi <sedenardi@gmail.com> (http://www.sandersdenardi.com/)",
@@ -12,21 +12,6 @@ module.exports = function (options) {
12
12
 
13
13
  return async function (ctx, events, calendar, children) {
14
14
  const eventActions = _.map(events, async (event) => {
15
- //
16
- // For deleted events without href, we cannot reliably construct the
17
- // correct URL that the client originally used. Returning an incorrect
18
- // URL causes Apple Calendar to fail with "Couldn't get a calendar item
19
- // to remove" errors because the URL doesn't match its local cache.
20
- //
21
- // Skip these events in sync-collection responses. The client will
22
- // eventually clean them up through other sync mechanisms.
23
- //
24
- // See: https://www.rfc-editor.org/rfc/rfc6578.html (sync-collection)
25
- //
26
- if (event.deleted_at && !event.href) {
27
- return null;
28
- }
29
-
30
15
  const misses = [];
31
16
  const propActions = _.map(children, async (child) => {
32
17
  return tags.getResponse({
@@ -63,6 +48,8 @@ module.exports = function (options) {
63
48
  // Priority order:
64
49
  // 1. event.href - the original resource path (if stored by the data layer)
65
50
  // 2. Constructed from event.eventId - fallback for backwards compatibility
51
+ // This fallback produces the same URLs that were returned to clients
52
+ // before the href field was added, so it matches what clients cached.
66
53
  //
67
54
  // See: https://www.rfc-editor.org/rfc/rfc6578.html (sync-collection)
68
55
  //
@@ -80,7 +67,6 @@ module.exports = function (options) {
80
67
  return resp;
81
68
  });
82
69
  const responses = await Promise.all(eventActions);
83
- // Filter out null responses (deleted events without href)
84
- return { responses: _.compact(responses) };
70
+ return { responses };
85
71
  };
86
72
  };
@@ -95,17 +95,15 @@ test('event-response uses event.href when available instead of constructing from
95
95
  t.is(responseObj['D:href'], '/cal/user/calendar/original@event.ics');
96
96
  });
97
97
 
98
- test('event-response returns 404 status for deleted events with href', async (t) => {
98
+ test('event-response returns 404 status for deleted events', async (t) => {
99
99
  const options = createMockOptions();
100
100
  const eventResponse = eventResponseFactory(options);
101
101
  const ctx = createMockCtx('/cal/user/calendar');
102
102
  const calendar = createMockCalendar();
103
103
 
104
- // Deleted event WITH href - should be included in response
105
104
  const events = [
106
105
  {
107
106
  eventId: 'deleted-event',
108
- href: '/cal/user/calendar/deleted-event.ics',
109
107
  deleted_at: new Date(),
110
108
  ical: 'BEGIN:VCALENDAR\nEND:VCALENDAR'
111
109
  }
@@ -127,13 +125,15 @@ test('event-response returns 404 status for deleted events with href', async (t)
127
125
  t.true(responseObj['D:status'].includes('404'));
128
126
  });
129
127
 
130
- test('event-response skips deleted events without href', async (t) => {
128
+ test('event-response returns 404 for deleted events without href using fallback URL', async (t) => {
131
129
  const options = createMockOptions();
132
130
  const eventResponse = eventResponseFactory(options);
133
131
  const ctx = createMockCtx('/cal/user/calendar');
134
132
  const calendar = createMockCalendar();
135
133
 
136
- // Deleted event WITHOUT href - should be skipped to prevent Apple sync errors
134
+ // Deleted event WITHOUT href - should use fallback URL construction
135
+ // This is critical for backwards compatibility with events created
136
+ // before the href field was added
137
137
  const events = [
138
138
  {
139
139
  eventId: 'deleted-event-no-href',
@@ -147,8 +147,13 @@ test('event-response skips deleted events without href', async (t) => {
147
147
  const result = await eventResponse(ctx, events, calendar, children);
148
148
 
149
149
  t.truthy(result.responses);
150
- // Should be empty - deleted events without href are skipped
151
- t.is(result.responses.length, 0);
150
+ // Should NOT be skipped - fallback URL construction works for backwards compatibility
151
+ t.is(result.responses.length, 1);
152
+
153
+ const responseObj = result.responses[0];
154
+ t.is(responseObj['D:href'], '/cal/user/calendar/deleted-event-no-href.ics');
155
+ t.truthy(responseObj['D:status']);
156
+ t.true(responseObj['D:status'].includes('404'));
152
157
  });
153
158
 
154
159
  test('event-response uses href for deleted events when available (critical for sync)', async (t) => {
@@ -225,7 +230,6 @@ test('event-response handles multiple events correctly', async (t) => {
225
230
  },
226
231
  {
227
232
  eventId: 'event3',
228
- href: '/cal/user/calendar/event3.ics',
229
233
  deleted_at: new Date(),
230
234
  ical: 'BEGIN:VCALENDAR\nEND:VCALENDAR'
231
235
  }
@@ -244,7 +248,7 @@ test('event-response handles multiple events correctly', async (t) => {
244
248
  // Event 2: uses href
245
249
  t.is(result.responses[1]['D:href'], '/cal/user/calendar/custom-path.ics');
246
250
 
247
- // Event 3: deleted with href
251
+ // Event 3: deleted, uses eventId (no href) - fallback URL construction
248
252
  t.is(result.responses[2]['D:href'], '/cal/user/calendar/event3.ics');
249
253
  t.true(result.responses[2]['D:status'].includes('404'));
250
254
  });
@@ -279,8 +283,8 @@ test('event-response handles multiple events with mixed deleted states', async (
279
283
  const result = await eventResponse(ctx, events, calendar, children);
280
284
 
281
285
  t.truthy(result.responses);
282
- // Only 2 responses - deleted without href is skipped
283
- t.is(result.responses.length, 2);
286
+ // All 3 responses - deleted without href is NO LONGER skipped
287
+ t.is(result.responses.length, 3);
284
288
 
285
289
  // Active event
286
290
  t.is(result.responses[0]['D:href'], '/cal/user/calendar/active-event.ics');
@@ -291,6 +295,10 @@ test('event-response handles multiple events with mixed deleted states', async (
291
295
  '/cal/user/calendar/deleted-with-href.ics'
292
296
  );
293
297
  t.true(result.responses[1]['D:status'].includes('404'));
298
+
299
+ // Deleted event without href - uses fallback URL construction
300
+ t.is(result.responses[2]['D:href'], '/cal/user/calendar/deleted-no-href.ics');
301
+ t.true(result.responses[2]['D:status'].includes('404'));
294
302
  });
295
303
 
296
304
  test('event-response handles email-like eventId with @ symbol', async (t) => {