@tellescope/sdk 1.246.2 → 1.247.0
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/lib/cjs/sdk.d.ts +1 -0
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/tests/api_tests/mdb_sort.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/mdb_sort.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/mdb_sort.test.js +370 -0
- package/lib/cjs/tests/api_tests/mdb_sort.test.js.map +1 -0
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +136 -124
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/sdk.d.ts +1 -0
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/tests/api_tests/mdb_sort.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/mdb_sort.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/mdb_sort.test.js +366 -0
- package/lib/esm/tests/api_tests/mdb_sort.test.js.map +1 -0
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +136 -124
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/sdk.ts +1 -0
- package/src/tests/api_tests/calendar_events_bulk_update.test.ts +418 -0
- package/src/tests/api_tests/mdb_sort.test.ts +259 -0
- package/src/tests/api_tests/organization_settings_duplicates.test.ts +201 -0
- package/src/tests/tests.ts +6 -0
- package/test_generated.pdf +0 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session } from "../../sdk"
|
|
4
|
+
import {
|
|
5
|
+
async_test,
|
|
6
|
+
assert,
|
|
7
|
+
handleAnyError,
|
|
8
|
+
log_header,
|
|
9
|
+
} from "@tellescope/testing"
|
|
10
|
+
import { setup_tests } from "../setup"
|
|
11
|
+
|
|
12
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
13
|
+
|
|
14
|
+
export const calendar_events_bulk_update_tests = async ({ sdk }: { sdk: Session }) => {
|
|
15
|
+
log_header("Calendar Events Bulk Update Tests")
|
|
16
|
+
|
|
17
|
+
// Track all created resources for cleanup
|
|
18
|
+
const createdEventIds: string[] = []
|
|
19
|
+
const createdEnduserIds: string[] = []
|
|
20
|
+
|
|
21
|
+
const createEvent = async (overrides: Record<string, any> = {}) => {
|
|
22
|
+
const event = await sdk.api.calendar_events.createOne({
|
|
23
|
+
title: "Bulk Update Test Event",
|
|
24
|
+
durationInMinutes: 30,
|
|
25
|
+
startTimeInMS: Date.now() + Math.random() * 100000,
|
|
26
|
+
...overrides,
|
|
27
|
+
})
|
|
28
|
+
createdEventIds.push(event.id)
|
|
29
|
+
return event
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const createEnduser = async () => {
|
|
33
|
+
const enduser = await sdk.api.endusers.createOne({})
|
|
34
|
+
createdEnduserIds.push(enduser.id)
|
|
35
|
+
return enduser
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// ============================================================
|
|
40
|
+
// SECTION 1: Validation / Error Cases
|
|
41
|
+
// ============================================================
|
|
42
|
+
log_header("Bulk Update - Validation")
|
|
43
|
+
|
|
44
|
+
await async_test(
|
|
45
|
+
"bulk_update errors when neither recurringEventId nor ids provided",
|
|
46
|
+
() => sdk.api.calendar_events.bulk_update({ action: 'cancel' } as any),
|
|
47
|
+
{ shouldError: true, onError: (e: any) => e.message.includes("Either recurringEventId or ids is required") }
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const validationEvent = await createEvent()
|
|
51
|
+
|
|
52
|
+
await async_test(
|
|
53
|
+
"bulk_update errors when both recurringEventId and ids provided",
|
|
54
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
55
|
+
recurringEventId: validationEvent.id,
|
|
56
|
+
ids: [validationEvent.id],
|
|
57
|
+
action: 'cancel',
|
|
58
|
+
}),
|
|
59
|
+
{ shouldError: true, onError: (e: any) => e.message.includes("Provide either recurringEventId or ids, not both") }
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const validationEnduser = await createEnduser()
|
|
63
|
+
|
|
64
|
+
await async_test(
|
|
65
|
+
"bulk_update errors when attendee-level action used with ids",
|
|
66
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
67
|
+
ids: [validationEvent.id],
|
|
68
|
+
action: 'cancel_for_attendee',
|
|
69
|
+
enduserId: validationEnduser.id,
|
|
70
|
+
}),
|
|
71
|
+
{ shouldError: true, onError: (e: any) => e.message.includes("only supported with recurringEventId") }
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
await async_test(
|
|
75
|
+
"bulk_update errors when attendee action missing enduserId",
|
|
76
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
77
|
+
recurringEventId: validationEvent.id,
|
|
78
|
+
action: 'cancel_for_attendee',
|
|
79
|
+
}),
|
|
80
|
+
{ shouldError: true, onError: (e: any) => e.message.includes("enduserId is required") }
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
// ============================================================
|
|
84
|
+
// SECTION 2: ID-based bulk operations (new feature)
|
|
85
|
+
// ============================================================
|
|
86
|
+
log_header("Bulk Update - ID-based Cancel")
|
|
87
|
+
|
|
88
|
+
const [ev1, ev2, ev3] = await Promise.all([createEvent(), createEvent(), createEvent()])
|
|
89
|
+
|
|
90
|
+
await async_test(
|
|
91
|
+
"bulk cancel by IDs cancels selected events",
|
|
92
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
93
|
+
ids: [ev1.id, ev2.id],
|
|
94
|
+
action: 'cancel',
|
|
95
|
+
cancelReason: 'Testing bulk cancel',
|
|
96
|
+
}),
|
|
97
|
+
{
|
|
98
|
+
onResult: (r: any) => (
|
|
99
|
+
r.updated.length === 2
|
|
100
|
+
&& r.updated.every((e: any) => !!e.cancelledAt)
|
|
101
|
+
&& r.updated.every((e: any) => e.cancelReason === 'Testing bulk cancel')
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
// Verify third event is unchanged
|
|
107
|
+
const ev3After = await sdk.api.calendar_events.getOne(ev3.id)
|
|
108
|
+
assert(!ev3After.cancelledAt, 'Third event should not be cancelled', 'Third event remains uncancelled')
|
|
109
|
+
|
|
110
|
+
// --- Uncancel ---
|
|
111
|
+
log_header("Bulk Update - ID-based Uncancel")
|
|
112
|
+
|
|
113
|
+
await async_test(
|
|
114
|
+
"bulk uncancel by IDs restores cancelled events",
|
|
115
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
116
|
+
ids: [ev1.id, ev2.id],
|
|
117
|
+
action: 'uncancel',
|
|
118
|
+
}),
|
|
119
|
+
{
|
|
120
|
+
onResult: (r: any) => (
|
|
121
|
+
r.updated.length === 2
|
|
122
|
+
&& r.updated.every((e: any) => !e.cancelledAt)
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
// --- Confirm ---
|
|
128
|
+
log_header("Bulk Update - ID-based Confirm")
|
|
129
|
+
|
|
130
|
+
await async_test(
|
|
131
|
+
"bulk confirm by IDs sets confirmedAt",
|
|
132
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
133
|
+
ids: [ev1.id, ev2.id],
|
|
134
|
+
action: 'confirm',
|
|
135
|
+
}),
|
|
136
|
+
{
|
|
137
|
+
onResult: (r: any) => (
|
|
138
|
+
r.updated.length === 2
|
|
139
|
+
&& r.updated.every((e: any) => !!e.confirmedAt)
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
// --- Idempotency: confirm already-confirmed ---
|
|
145
|
+
await async_test(
|
|
146
|
+
"bulk confirm on already-confirmed events returns empty updated",
|
|
147
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
148
|
+
ids: [ev1.id, ev2.id],
|
|
149
|
+
action: 'confirm',
|
|
150
|
+
}),
|
|
151
|
+
{ onResult: (r: any) => r.updated.length === 0 }
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
// --- No Show ---
|
|
155
|
+
log_header("Bulk Update - ID-based No Show")
|
|
156
|
+
|
|
157
|
+
await async_test(
|
|
158
|
+
"bulk no_show by IDs sets noShowedAt",
|
|
159
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
160
|
+
ids: [ev1.id, ev2.id],
|
|
161
|
+
action: 'no_show',
|
|
162
|
+
}),
|
|
163
|
+
{
|
|
164
|
+
onResult: (r: any) => (
|
|
165
|
+
r.updated.length === 2
|
|
166
|
+
&& r.updated.every((e: any) => !!e.noShowedAt)
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
// --- Un-No-Show ---
|
|
172
|
+
log_header("Bulk Update - ID-based Un-No-Show")
|
|
173
|
+
|
|
174
|
+
await async_test(
|
|
175
|
+
"bulk un_no_show by IDs clears noShowedAt",
|
|
176
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
177
|
+
ids: [ev1.id, ev2.id],
|
|
178
|
+
action: 'un_no_show',
|
|
179
|
+
}),
|
|
180
|
+
{
|
|
181
|
+
onResult: (r: any) => (
|
|
182
|
+
r.updated.length === 2
|
|
183
|
+
&& r.updated.every((e: any) => !e.noShowedAt)
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
// --- Delete by IDs ---
|
|
189
|
+
log_header("Bulk Update - ID-based Delete")
|
|
190
|
+
|
|
191
|
+
const [delEv1, delEv2] = await Promise.all([createEvent(), createEvent()])
|
|
192
|
+
|
|
193
|
+
await async_test(
|
|
194
|
+
"bulk delete by IDs removes events",
|
|
195
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
196
|
+
ids: [delEv1.id, delEv2.id],
|
|
197
|
+
action: 'delete',
|
|
198
|
+
}),
|
|
199
|
+
{ onResult: (r: any) => r.deleted.length === 2 }
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// Verify deleted events are gone
|
|
203
|
+
await async_test(
|
|
204
|
+
"deleted events are no longer accessible",
|
|
205
|
+
() => sdk.api.calendar_events.getOne(delEv1.id),
|
|
206
|
+
{ shouldError: true, onError: (e: any) => e.message.includes("Could not find") }
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
// Remove from cleanup tracking since already deleted
|
|
210
|
+
createdEventIds.splice(createdEventIds.indexOf(delEv1.id), 1)
|
|
211
|
+
createdEventIds.splice(createdEventIds.indexOf(delEv2.id), 1)
|
|
212
|
+
|
|
213
|
+
// ============================================================
|
|
214
|
+
// SECTION 3: Recurring series operations
|
|
215
|
+
// ============================================================
|
|
216
|
+
log_header("Bulk Update - Recurring Series")
|
|
217
|
+
|
|
218
|
+
const now = Date.now()
|
|
219
|
+
const DAY = 24 * 60 * 60 * 1000
|
|
220
|
+
const enduser = await createEnduser()
|
|
221
|
+
|
|
222
|
+
// Create a "recurring series": root event + child events with copiedFrom
|
|
223
|
+
const rootEvent = await createEvent({
|
|
224
|
+
title: "Recurring Root",
|
|
225
|
+
startTimeInMS: now - 2 * DAY, // 2 days ago
|
|
226
|
+
attendees: [{ id: enduser.id, type: 'enduser' }],
|
|
227
|
+
})
|
|
228
|
+
const childEvent1 = await createEvent({
|
|
229
|
+
title: "Recurring Child 1",
|
|
230
|
+
startTimeInMS: now - 1 * DAY, // 1 day ago
|
|
231
|
+
copiedFrom: rootEvent.id,
|
|
232
|
+
attendees: [{ id: enduser.id, type: 'enduser' }],
|
|
233
|
+
})
|
|
234
|
+
const childEvent2 = await createEvent({
|
|
235
|
+
title: "Recurring Child 2",
|
|
236
|
+
startTimeInMS: now + 1 * DAY, // tomorrow
|
|
237
|
+
copiedFrom: rootEvent.id,
|
|
238
|
+
attendees: [{ id: enduser.id, type: 'enduser' }],
|
|
239
|
+
})
|
|
240
|
+
const childEvent3 = await createEvent({
|
|
241
|
+
title: "Recurring Child 3",
|
|
242
|
+
startTimeInMS: now + 2 * DAY, // 2 days from now
|
|
243
|
+
copiedFrom: rootEvent.id,
|
|
244
|
+
attendees: [{ id: enduser.id, type: 'enduser' }],
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// --- Cancel with scope 'this_and_future' ---
|
|
248
|
+
log_header("Bulk Update - Recurring Cancel this_and_future")
|
|
249
|
+
|
|
250
|
+
await async_test(
|
|
251
|
+
"recurring cancel this_and_future cancels anchor and future events",
|
|
252
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
253
|
+
recurringEventId: childEvent2.id,
|
|
254
|
+
action: 'cancel',
|
|
255
|
+
scope: 'this_and_future',
|
|
256
|
+
}),
|
|
257
|
+
{
|
|
258
|
+
onResult: (r: any) => (
|
|
259
|
+
r.updated.length === 2
|
|
260
|
+
&& r.updated.every((e: any) => !!e.cancelledAt)
|
|
261
|
+
&& r.updated.some((e: any) => e.id === childEvent2.id)
|
|
262
|
+
&& r.updated.some((e: any) => e.id === childEvent3.id)
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
// Verify earlier events were NOT cancelled
|
|
268
|
+
const rootAfter = await sdk.api.calendar_events.getOne(rootEvent.id)
|
|
269
|
+
assert(!rootAfter.cancelledAt, 'Root should not be cancelled', 'Root event not cancelled by this_and_future')
|
|
270
|
+
const child1After = await sdk.api.calendar_events.getOne(childEvent1.id)
|
|
271
|
+
assert(!child1After.cancelledAt, 'Child 1 should not be cancelled', 'Child 1 not cancelled by this_and_future')
|
|
272
|
+
|
|
273
|
+
// --- Uncancel with scope 'all' to reset ---
|
|
274
|
+
await sdk.api.calendar_events.bulk_update({
|
|
275
|
+
recurringEventId: rootEvent.id,
|
|
276
|
+
action: 'uncancel',
|
|
277
|
+
scope: 'all',
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
// --- Cancel with scope 'all' ---
|
|
281
|
+
log_header("Bulk Update - Recurring Cancel All")
|
|
282
|
+
|
|
283
|
+
await async_test(
|
|
284
|
+
"recurring cancel all cancels entire series",
|
|
285
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
286
|
+
recurringEventId: childEvent1.id,
|
|
287
|
+
action: 'cancel',
|
|
288
|
+
scope: 'all',
|
|
289
|
+
}),
|
|
290
|
+
{
|
|
291
|
+
onResult: (r: any) => (
|
|
292
|
+
r.updated.length === 4
|
|
293
|
+
&& r.updated.every((e: any) => !!e.cancelledAt)
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
// Reset: uncancel all
|
|
299
|
+
await sdk.api.calendar_events.bulk_update({
|
|
300
|
+
recurringEventId: rootEvent.id,
|
|
301
|
+
action: 'uncancel',
|
|
302
|
+
scope: 'all',
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
// --- Cancel for attendee ---
|
|
306
|
+
log_header("Bulk Update - Recurring Cancel for Attendee")
|
|
307
|
+
|
|
308
|
+
await async_test(
|
|
309
|
+
"cancel_for_attendee adds enduser to cancelledGroupAttendees across series",
|
|
310
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
311
|
+
recurringEventId: rootEvent.id,
|
|
312
|
+
action: 'cancel_for_attendee',
|
|
313
|
+
scope: 'all',
|
|
314
|
+
enduserId: enduser.id,
|
|
315
|
+
}),
|
|
316
|
+
{
|
|
317
|
+
onResult: (r: any) => (
|
|
318
|
+
r.updated.length === 4
|
|
319
|
+
&& r.updated.every((e: any) =>
|
|
320
|
+
e.cancelledGroupAttendees?.some((c: any) => c.id === enduser.id)
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
// --- Uncancel for attendee ---
|
|
327
|
+
log_header("Bulk Update - Recurring Uncancel for Attendee")
|
|
328
|
+
|
|
329
|
+
await async_test(
|
|
330
|
+
"uncancel_for_attendee removes enduser from cancelledGroupAttendees",
|
|
331
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
332
|
+
recurringEventId: rootEvent.id,
|
|
333
|
+
action: 'uncancel_for_attendee',
|
|
334
|
+
scope: 'all',
|
|
335
|
+
enduserId: enduser.id,
|
|
336
|
+
}),
|
|
337
|
+
{
|
|
338
|
+
onResult: (r: any) => (
|
|
339
|
+
r.updated.length === 4
|
|
340
|
+
&& r.updated.every((e: any) =>
|
|
341
|
+
!e.cancelledGroupAttendees?.some((c: any) => c.id === enduser.id)
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
// --- Remove attendee ---
|
|
348
|
+
log_header("Bulk Update - Recurring Remove Attendee")
|
|
349
|
+
|
|
350
|
+
await async_test(
|
|
351
|
+
"remove_attendee removes enduser from attendees across series",
|
|
352
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
353
|
+
recurringEventId: rootEvent.id,
|
|
354
|
+
action: 'remove_attendee',
|
|
355
|
+
scope: 'all',
|
|
356
|
+
enduserId: enduser.id,
|
|
357
|
+
}),
|
|
358
|
+
{
|
|
359
|
+
onResult: (r: any) => (
|
|
360
|
+
r.updated.length === 4
|
|
361
|
+
&& r.updated.every((e: any) =>
|
|
362
|
+
!e.attendees?.some((a: any) => a.id === enduser.id)
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
// --- Recurring delete ---
|
|
369
|
+
log_header("Bulk Update - Recurring Delete")
|
|
370
|
+
|
|
371
|
+
await async_test(
|
|
372
|
+
"recurring delete removes all events in series",
|
|
373
|
+
() => sdk.api.calendar_events.bulk_update({
|
|
374
|
+
recurringEventId: rootEvent.id,
|
|
375
|
+
action: 'delete',
|
|
376
|
+
scope: 'all',
|
|
377
|
+
}),
|
|
378
|
+
{ onResult: (r: any) => r.deleted.length === 4 }
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
// Remove from cleanup tracking since already deleted
|
|
382
|
+
for (const id of [rootEvent.id, childEvent1.id, childEvent2.id, childEvent3.id]) {
|
|
383
|
+
const idx = createdEventIds.indexOf(id)
|
|
384
|
+
if (idx !== -1) createdEventIds.splice(idx, 1)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
} finally {
|
|
388
|
+
// Cleanup remaining resources
|
|
389
|
+
for (const id of createdEventIds) {
|
|
390
|
+
try { await sdk.api.calendar_events.deleteOne(id) } catch (_) {}
|
|
391
|
+
}
|
|
392
|
+
for (const id of createdEnduserIds) {
|
|
393
|
+
try { await sdk.api.endusers.deleteOne(id) } catch (_) {}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Allow running this test file independently
|
|
399
|
+
if (require.main === module) {
|
|
400
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
401
|
+
const sdk = new Session({ host })
|
|
402
|
+
const sdkNonAdmin = new Session({ host })
|
|
403
|
+
|
|
404
|
+
const runTests = async () => {
|
|
405
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
406
|
+
await calendar_events_bulk_update_tests({ sdk })
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
runTests()
|
|
410
|
+
.then(() => {
|
|
411
|
+
console.log("✅ Calendar events bulk update test suite completed successfully")
|
|
412
|
+
process.exit(0)
|
|
413
|
+
})
|
|
414
|
+
.catch((error) => {
|
|
415
|
+
console.error("❌ Calendar events bulk update test suite failed:", error)
|
|
416
|
+
process.exit(1)
|
|
417
|
+
})
|
|
418
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session } from "../../sdk"
|
|
4
|
+
import {
|
|
5
|
+
assert,
|
|
6
|
+
async_test,
|
|
7
|
+
log_header,
|
|
8
|
+
} from "@tellescope/testing"
|
|
9
|
+
import { setup_tests } from "../setup"
|
|
10
|
+
|
|
11
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
12
|
+
|
|
13
|
+
// Main test function that can be called independently
|
|
14
|
+
export const mdb_sort_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
15
|
+
log_header("mdbSort Custom Sorting Support")
|
|
16
|
+
|
|
17
|
+
// Create test endusers with known field values for sorting
|
|
18
|
+
const testEndusers = await Promise.all([
|
|
19
|
+
sdk.api.endusers.createOne({ fname: 'Alice', lname: 'Smith', email: 'alice-mdbsort@tellescope.com' }),
|
|
20
|
+
sdk.api.endusers.createOne({ fname: 'Bob', lname: 'Jones', email: 'bob-mdbsort@tellescope.com' }),
|
|
21
|
+
sdk.api.endusers.createOne({ fname: 'Charlie', lname: 'Adams', email: 'charlie-mdbsort@tellescope.com' }),
|
|
22
|
+
sdk.api.endusers.createOne({ fname: 'Alice', lname: 'Zeta', email: 'alice2-mdbsort@tellescope.com' }), // Same fname for multi-field test
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
const enduserIds = testEndusers.map(e => e.id)
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Test 1: Sort by fname ascending (alphabetical order)
|
|
29
|
+
await async_test(
|
|
30
|
+
'mdbSort-fname-ascending',
|
|
31
|
+
async () => {
|
|
32
|
+
const results = await sdk.api.endusers.getSome({
|
|
33
|
+
filter: { id: { _in: enduserIds } },
|
|
34
|
+
mdbSort: { fname: 1 },
|
|
35
|
+
})
|
|
36
|
+
assert(results.length === 4, 'Expected 4 endusers', `Got ${results.length}`)
|
|
37
|
+
|
|
38
|
+
// Verify alphabetical order: Alice, Alice, Bob, Charlie
|
|
39
|
+
assert(results[0].fname === 'Alice', 'First should be Alice')
|
|
40
|
+
assert(results[1].fname === 'Alice', 'Second should be Alice')
|
|
41
|
+
assert(results[2].fname === 'Bob', 'Third should be Bob')
|
|
42
|
+
assert(results[3].fname === 'Charlie', 'Fourth should be Charlie')
|
|
43
|
+
|
|
44
|
+
return results
|
|
45
|
+
},
|
|
46
|
+
{ onResult: () => true },
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// Test 2: Sort by fname descending (reverse alphabetical order)
|
|
50
|
+
await async_test(
|
|
51
|
+
'mdbSort-fname-descending',
|
|
52
|
+
async () => {
|
|
53
|
+
const results = await sdk.api.endusers.getSome({
|
|
54
|
+
filter: { id: { _in: enduserIds } },
|
|
55
|
+
mdbSort: { fname: -1 },
|
|
56
|
+
})
|
|
57
|
+
assert(results.length === 4, 'Expected 4 endusers')
|
|
58
|
+
|
|
59
|
+
// Verify reverse alphabetical order: Charlie, Bob, Alice, Alice
|
|
60
|
+
assert(results[0].fname === 'Charlie', 'First should be Charlie')
|
|
61
|
+
assert(results[1].fname === 'Bob', 'Second should be Bob')
|
|
62
|
+
assert(results[2].fname === 'Alice', 'Third should be Alice')
|
|
63
|
+
assert(results[3].fname === 'Alice', 'Fourth should be Alice')
|
|
64
|
+
|
|
65
|
+
return results
|
|
66
|
+
},
|
|
67
|
+
{ onResult: () => true },
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
// Test 3: Multi-field sort (fname ascending, then lname ascending for ties)
|
|
71
|
+
await async_test(
|
|
72
|
+
'mdbSort-multi-field',
|
|
73
|
+
async () => {
|
|
74
|
+
const results = await sdk.api.endusers.getSome({
|
|
75
|
+
filter: { id: { _in: enduserIds } },
|
|
76
|
+
mdbSort: { fname: 1, lname: 1 },
|
|
77
|
+
})
|
|
78
|
+
assert(results.length === 4, 'Expected 4 endusers')
|
|
79
|
+
|
|
80
|
+
// Verify multi-field sort:
|
|
81
|
+
// Alice Smith (fname: Alice, lname: Smith)
|
|
82
|
+
// Alice Zeta (fname: Alice, lname: Zeta)
|
|
83
|
+
// Bob Jones (fname: Bob)
|
|
84
|
+
// Charlie Adams (fname: Charlie)
|
|
85
|
+
assert(results[0].fname === 'Alice' && results[0].lname === 'Smith', 'First should be Alice Smith')
|
|
86
|
+
assert(results[1].fname === 'Alice' && results[1].lname === 'Zeta', 'Second should be Alice Zeta')
|
|
87
|
+
assert(results[2].fname === 'Bob', 'Third should be Bob')
|
|
88
|
+
assert(results[3].fname === 'Charlie', 'Fourth should be Charlie')
|
|
89
|
+
|
|
90
|
+
return results
|
|
91
|
+
},
|
|
92
|
+
{ onResult: () => true },
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
// Test 4: mdbSort combined with mdbFilter
|
|
96
|
+
await async_test(
|
|
97
|
+
'mdbSort-with-mdbFilter',
|
|
98
|
+
async () => {
|
|
99
|
+
const results = await sdk.api.endusers.getSome({
|
|
100
|
+
mdbFilter: {
|
|
101
|
+
email: { $in: ['alice-mdbsort@tellescope.com', 'alice2-mdbsort@tellescope.com'] },
|
|
102
|
+
fname: 'Alice', // Only get Alice endusers
|
|
103
|
+
},
|
|
104
|
+
mdbSort: { lname: 1 }, // Sort by last name
|
|
105
|
+
})
|
|
106
|
+
assert(results.length === 2, 'Expected 2 Alice endusers', `Got ${results.length}`)
|
|
107
|
+
|
|
108
|
+
// Verify both are Alice and sorted by lname: Smith, then Zeta
|
|
109
|
+
assert(results[0].fname === 'Alice' && results[0].lname === 'Smith', 'First Alice should be Smith')
|
|
110
|
+
assert(results[1].fname === 'Alice' && results[1].lname === 'Zeta', 'Second Alice should be Zeta')
|
|
111
|
+
|
|
112
|
+
return results
|
|
113
|
+
},
|
|
114
|
+
{ onResult: () => true },
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
// Test 5: mdbSort keyset pagination via mdbFilter $or
|
|
118
|
+
// Note: filter is ignored when mdbFilter is present, so both id scoping and keyset cursor
|
|
119
|
+
// must be expressed in mdbFilter.
|
|
120
|
+
await async_test(
|
|
121
|
+
'mdbSort-keyset-pagination',
|
|
122
|
+
async () => {
|
|
123
|
+
const testEmails = [
|
|
124
|
+
'alice-mdbsort@tellescope.com',
|
|
125
|
+
'alice2-mdbsort@tellescope.com',
|
|
126
|
+
'bob-mdbsort@tellescope.com',
|
|
127
|
+
'charlie-mdbsort@tellescope.com',
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
// Page 1: first 2 results sorted by fname ascending → both Alices
|
|
131
|
+
const page1 = await sdk.api.endusers.getSome({
|
|
132
|
+
mdbFilter: { email: { $in: testEmails } },
|
|
133
|
+
mdbSort: { fname: 1 },
|
|
134
|
+
limit: 2,
|
|
135
|
+
})
|
|
136
|
+
assert(page1.length === 2, 'Expected 2 endusers on page 1')
|
|
137
|
+
assert(page1[0].fname === 'Alice', 'Page 1 first should be Alice')
|
|
138
|
+
assert(page1[1].fname === 'Alice', 'Page 1 second should be Alice')
|
|
139
|
+
|
|
140
|
+
// Page 2: keyset cursor — fname > last seen fname ('Alice')
|
|
141
|
+
const lastFname = page1[page1.length - 1].fname
|
|
142
|
+
const page2 = await sdk.api.endusers.getSome({
|
|
143
|
+
mdbFilter: {
|
|
144
|
+
email: { $in: testEmails },
|
|
145
|
+
fname: { $gt: lastFname },
|
|
146
|
+
},
|
|
147
|
+
mdbSort: { fname: 1 },
|
|
148
|
+
limit: 2,
|
|
149
|
+
})
|
|
150
|
+
assert(page2.length === 2, 'Expected 2 endusers on page 2')
|
|
151
|
+
assert(page2[0].fname === 'Bob', 'Page 2 first should be Bob')
|
|
152
|
+
assert(page2[1].fname === 'Charlie', 'Page 2 second should be Charlie')
|
|
153
|
+
|
|
154
|
+
return [...page1, ...page2]
|
|
155
|
+
},
|
|
156
|
+
{ onResult: () => true },
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
// Test 6: mdbSort with projection (ensure both work together)
|
|
160
|
+
await async_test(
|
|
161
|
+
'mdbSort-with-projection',
|
|
162
|
+
async () => {
|
|
163
|
+
const results = await sdk.api.endusers.getSome({
|
|
164
|
+
filter: { id: { _in: enduserIds } },
|
|
165
|
+
mdbSort: { fname: -1 },
|
|
166
|
+
projection: { fname: 1, lname: 1 },
|
|
167
|
+
})
|
|
168
|
+
assert(results.length === 4, 'Expected 4 endusers')
|
|
169
|
+
|
|
170
|
+
// Verify sort order (descending)
|
|
171
|
+
assert(results[0].fname === 'Charlie', 'First should be Charlie')
|
|
172
|
+
|
|
173
|
+
// Verify projection (only fname, lname, plus id and createdAt)
|
|
174
|
+
assert(results[0].fname !== undefined, 'fname should be present')
|
|
175
|
+
assert(results[0].lname !== undefined, 'lname should be present')
|
|
176
|
+
assert((results[0] as any).email === undefined, 'email should NOT be present')
|
|
177
|
+
|
|
178
|
+
return results
|
|
179
|
+
},
|
|
180
|
+
{ onResult: () => true },
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
// Test 7: Non-admin access with mdbSort (RBA still applies)
|
|
184
|
+
await async_test(
|
|
185
|
+
'non-admin-mdbSort',
|
|
186
|
+
async () => {
|
|
187
|
+
const results = await sdkNonAdmin.api.endusers.getSome({
|
|
188
|
+
filter: { id: { _in: enduserIds } },
|
|
189
|
+
mdbSort: { fname: 1 },
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
// Non-admin should still get results (RBA should apply)
|
|
193
|
+
assert(Array.isArray(results), 'Non-admin should receive an array response')
|
|
194
|
+
|
|
195
|
+
// Verify sort order
|
|
196
|
+
if (results.length >= 2) {
|
|
197
|
+
const fnames = results.map(e => e.fname)
|
|
198
|
+
// Should be sorted alphabetically
|
|
199
|
+
assert(fnames[0]! <= fnames[1]!, 'Non-admin results should be sorted')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return results
|
|
203
|
+
},
|
|
204
|
+
{ onResult: () => true },
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
// Test 8: mdbSort fallback behavior (no mdbSort uses sortBy default)
|
|
208
|
+
await async_test(
|
|
209
|
+
'no-mdbSort-fallback',
|
|
210
|
+
async () => {
|
|
211
|
+
// Without mdbSort, should fall back to default sorting by _id
|
|
212
|
+
const results = await sdk.api.endusers.getSome({
|
|
213
|
+
filter: { id: { _in: enduserIds } },
|
|
214
|
+
sortBy: 'updatedAt',
|
|
215
|
+
sort: 'oldFirst',
|
|
216
|
+
})
|
|
217
|
+
assert(results.length === 4, 'Expected 4 endusers')
|
|
218
|
+
|
|
219
|
+
// Verify traditional sortBy still works when mdbSort not provided
|
|
220
|
+
assert(results[0].id !== undefined, 'Should return valid endusers')
|
|
221
|
+
|
|
222
|
+
return results
|
|
223
|
+
},
|
|
224
|
+
{ onResult: () => true },
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
} finally {
|
|
228
|
+
// Cleanup: Delete test resources
|
|
229
|
+
try {
|
|
230
|
+
for (const enduserId of enduserIds) {
|
|
231
|
+
await sdk.api.endusers.deleteOne(enduserId)
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error('Cleanup error:', error)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Allow running this test file independently
|
|
240
|
+
if (require.main === module) {
|
|
241
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
242
|
+
const sdk = new Session({ host })
|
|
243
|
+
const sdkNonAdmin = new Session({ host })
|
|
244
|
+
|
|
245
|
+
const runTests = async () => {
|
|
246
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
247
|
+
await mdb_sort_tests({ sdk, sdkNonAdmin })
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
runTests()
|
|
251
|
+
.then(() => {
|
|
252
|
+
console.log("✅ mdbSort test suite completed successfully")
|
|
253
|
+
process.exit(0)
|
|
254
|
+
})
|
|
255
|
+
.catch((error) => {
|
|
256
|
+
console.error("❌ mdbSort test suite failed:", error)
|
|
257
|
+
process.exit(1)
|
|
258
|
+
})
|
|
259
|
+
}
|