• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

u-wave / core / 11980840475

22 Nov 2024 10:04PM UTC coverage: 78.492% (-1.7%) from 80.158%
11980840475

Pull #637

github

goto-bus-stop
ci: add node 22
Pull Request #637: Switch to a relational database

757 of 912 branches covered (83.0%)

Branch coverage included in aggregate %.

2001 of 2791 new or added lines in 52 files covered. (71.69%)

9 existing lines in 7 files now uncovered.

8666 of 11093 relevant lines covered (78.12%)

70.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.55
/src/controllers/playlists.js
1
import { HTTPError, PlaylistNotFoundError } from '../errors/index.js';
1✔
2
import { serializePlaylist, serializePlaylistItem } from '../utils/serialize.js';
1✔
3
import getOffsetPagination from '../utils/getOffsetPagination.js';
1✔
4
import toItemResponse from '../utils/toItemResponse.js';
1✔
5
import toListResponse from '../utils/toListResponse.js';
1✔
6
import toPaginatedResponse from '../utils/toPaginatedResponse.js';
1✔
7

1✔
8
/**
1✔
9
 * @typedef {import('../schema').PlaylistID} PlaylistID
1✔
10
 * @typedef {import('../schema').PlaylistItemID} PlaylistItemID
1✔
11
 * @typedef {import('../schema').MediaID} MediaID
1✔
12
 */
1✔
13

1✔
14
/**
1✔
15
 * TODO move to a serializer?
1✔
16
 *
1✔
17
 * @param {Pick<
1✔
18
 *   import('../schema').PlaylistItem,
1✔
19
 *   'id' | 'artist' | 'title' | 'start' | 'end' | 'createdAt'
1✔
20
 * >} playlistItem
1✔
21
 * @param {Pick<
1✔
22
 *   import('../schema').Media,
1✔
23
 *   'id' | 'sourceType' | 'sourceID' | 'sourceData' | 'artist' | 'title' | 'duration' | 'thumbnail'
1✔
24
 * >} media
1✔
25
 */
1✔
26
export function legacyPlaylistItem(playlistItem, media) {
1✔
NEW
27
  return {
×
NEW
28
    _id: playlistItem.id,
×
NEW
29
    artist: playlistItem.artist,
×
NEW
30
    title: playlistItem.title,
×
NEW
31
    start: playlistItem.start,
×
NEW
32
    end: playlistItem.end,
×
NEW
33
    media: {
×
NEW
34
      _id: media.id,
×
NEW
35
      sourceType: media.sourceType,
×
NEW
36
      sourceID: media.sourceID,
×
NEW
37
      sourceData: media.sourceData,
×
NEW
38
      artist: media.artist,
×
NEW
39
      title: media.title,
×
NEW
40
      duration: media.duration,
×
NEW
41
      thumbnail: media.thumbnail,
×
NEW
42
    },
×
NEW
43
    createdAt: playlistItem.createdAt,
×
NEW
44
  };
×
NEW
45
}
×
46

1✔
47
/**
1✔
48
 * @typedef {object} GetPlaylistsQuery
1✔
49
 * @prop {MediaID} [contains]
1✔
50
 */
1✔
51

1✔
52
/**
1✔
53
 * @type {import('../types.js').AuthenticatedController<{}, GetPlaylistsQuery>}
1✔
54
 */
1✔
55
async function getPlaylists(req) {
1✔
56
  const { user } = req;
1✔
57
  const uw = req.uwave;
1✔
58
  const { contains } = req.query;
1✔
59

1✔
60
  let playlists;
1✔
61
  if (contains) {
1!
NEW
62
    playlists = await uw.playlists.getPlaylistsContainingMedia(contains, { author: user.id });
×
63
  } else {
1✔
64
    playlists = await uw.playlists.getUserPlaylists(user);
1✔
65
  }
1✔
66

1✔
67
  return toListResponse(
1✔
68
    playlists.map(serializePlaylist),
1✔
69
    { url: req.fullUrl },
1✔
70
  );
1✔
71
}
1✔
72

1✔
73
/**
1✔
74
 * @typedef {object} GetPlaylistParams
1✔
75
 * @prop {PlaylistID} id
1✔
76
 */
1✔
77

1✔
78
/**
1✔
79
 * @type {import('../types.js').AuthenticatedController<GetPlaylistParams>}
1✔
80
 */
1✔
81
async function getPlaylist(req) {
2✔
82
  const { user } = req;
2✔
83
  const { playlists } = req.uwave;
2✔
84
  const { id } = req.params;
2✔
85

2✔
86
  const playlist = await playlists.getUserPlaylist(user, id);
2✔
87

2✔
88
  if (!playlist) {
2!
89
    throw new PlaylistNotFoundError({ id });
×
90
  }
×
91

2✔
92
  return toItemResponse(
2✔
93
    serializePlaylist(playlist),
2✔
94
    { url: req.fullUrl },
2✔
95
  );
2✔
96
}
2✔
97

1✔
98
/**
1✔
99
 * @typedef {object} CreatePlaylistBody
1✔
100
 * @prop {string} name
1✔
101
 */
1✔
102

1✔
103
/**
1✔
104
 * @type {import('../types.js').AuthenticatedController<{}, {}, CreatePlaylistBody>}
1✔
105
 */
1✔
106
async function createPlaylist(req) {
11✔
107
  const { user } = req;
11✔
108
  const { name } = req.body;
11✔
109
  const { playlists } = req.uwave;
11✔
110

11✔
111
  const { playlist, active } = await playlists.createPlaylist(user, {
11✔
112
    name,
11✔
113
  });
11✔
114

11✔
115
  return toItemResponse(
11✔
116
    serializePlaylist(playlist),
11✔
117
    {
11✔
118
      url: req.fullUrl,
11✔
119
      meta: { active },
11✔
120
    },
11✔
121
  );
11✔
122
}
11✔
123

1✔
124
/**
1✔
125
 * @typedef {object} DeletePlaylistParams
1✔
126
 * @prop {PlaylistID} id
1✔
127
 */
1✔
128

1✔
129
/**
1✔
130
 * @type {import('../types.js').AuthenticatedController<DeletePlaylistParams>}
1✔
131
 */
1✔
132
async function deletePlaylist(req) {
×
133
  const { user } = req;
×
134
  const { id } = req.params;
×
135
  const { playlists } = req.uwave;
×
136

×
NEW
137
  const playlist = await playlists.getUserPlaylist(user, id);
×
138
  if (!playlist) {
×
139
    throw new PlaylistNotFoundError({ id });
×
140
  }
×
141

×
142
  await playlists.deletePlaylist(playlist);
×
143

×
144
  return toItemResponse({}, { url: req.fullUrl });
×
145
}
×
146

1✔
147
const patchableKeys = ['name', 'description'];
1✔
148

1✔
149
/**
1✔
150
 * @typedef {object} UpdatePlaylistParams
1✔
151
 * @prop {PlaylistID} id
1✔
152
 * @typedef {Record<string, string>} UpdatePlaylistBody
1✔
153
 */
1✔
154

1✔
155
/**
1✔
156
 * @type {import('../types.js').AuthenticatedController<
1✔
157
 *     UpdatePlaylistParams, {}, UpdatePlaylistBody>}
1✔
158
 */
1✔
159
async function updatePlaylist(req) {
3✔
160
  const { user } = req;
3✔
161
  const { id } = req.params;
3✔
162
  const patch = req.body;
3✔
163
  const { playlists } = req.uwave;
3✔
164

3✔
165
  const patches = Object.keys(patch);
3✔
166
  patches.forEach((patchKey) => {
3✔
167
    if (!patchableKeys.includes(patchKey)) {
3✔
168
      throw new HTTPError(400, `Key "${patchKey}" cannot be updated.`);
1✔
169
    }
1✔
170
  });
3✔
171

3✔
172
  const playlist = await playlists.getUserPlaylist(user, id);
3✔
173
  if (!playlist) {
3!
174
    throw new PlaylistNotFoundError({ id });
×
175
  }
×
176

1✔
177
  const updatedPlaylist = await playlists.updatePlaylist(playlist, patch);
1✔
178

1✔
179
  return toItemResponse(
1✔
180
    serializePlaylist(updatedPlaylist),
1✔
181
    { url: req.fullUrl },
1✔
182
  );
1✔
183
}
3✔
184

1✔
185
/**
1✔
186
 * @typedef {object} RenamePlaylistParams
1✔
187
 * @prop {PlaylistID} id
1✔
188
 * @typedef {object} RenamePlaylistBody
1✔
189
 * @prop {string} name
1✔
190
 */
1✔
191

1✔
192
/**
1✔
193
 * @type {import('../types.js').AuthenticatedController<
1✔
194
 *     RenamePlaylistParams, {}, RenamePlaylistBody>}
1✔
195
 */
1✔
196
async function renamePlaylist(req) {
2✔
197
  const { user } = req;
2✔
198
  const { id } = req.params;
2✔
199
  const { name } = req.body;
2✔
200
  const { playlists } = req.uwave;
2✔
201

2✔
202
  const playlist = await playlists.getUserPlaylist(user, id);
2✔
203
  if (!playlist) {
2!
204
    throw new PlaylistNotFoundError({ id });
×
205
  }
×
206

1✔
207
  const updatedPlaylist = await playlists.updatePlaylist(playlist, { name });
1✔
208

1✔
209
  return toItemResponse(
1✔
210
    serializePlaylist(updatedPlaylist),
1✔
211
    { url: req.fullUrl },
1✔
212
  );
1✔
213
}
2✔
214

1✔
215
/**
1✔
216
 * @typedef {object} ActivatePlaylistParams
1✔
217
 * @prop {PlaylistID} id
1✔
218
 */
1✔
219

1✔
220
/**
1✔
221
 * @type {import('../types.js').AuthenticatedController<ActivatePlaylistParams>}
1✔
222
 */
1✔
223
async function activatePlaylist(req) {
1✔
224
  const { user } = req;
1✔
225
  const { db, playlists } = req.uwave;
1✔
226
  const { id } = req.params;
1✔
227

1✔
228
  const playlist = await playlists.getUserPlaylist(user, id);
1✔
229
  if (!playlist) {
1!
230
    throw new PlaylistNotFoundError({ id });
×
231
  }
×
232

1✔
233
  await db.updateTable('users')
1✔
234
    .where('id', '=', user.id)
1✔
235
    .set({ activePlaylistID: playlist.id })
1✔
236
    .execute();
1✔
237

1✔
238
  return toItemResponse({});
1✔
239
}
1✔
240

1✔
241
/**
1✔
242
 * @typedef {object} GetPlaylistItemsParams
1✔
243
 * @prop {PlaylistID} id
1✔
244
 * @typedef {import('../types.js').PaginationQuery & { filter?: string }} GetPlaylistItemsQuery
1✔
245
 */
1✔
246

1✔
247
/**
1✔
248
 * @type {import('../types.js').AuthenticatedController<
1✔
249
 *     GetPlaylistItemsParams, GetPlaylistItemsQuery>}
1✔
250
 */
1✔
251
async function getPlaylistItems(req) {
12✔
252
  const { user } = req;
12✔
253
  const { playlists } = req.uwave;
12✔
254
  const { id } = req.params;
12✔
255
  const filter = req.query.filter ?? undefined;
12✔
256
  const pagination = getOffsetPagination(req.query);
12✔
257

12✔
258
  const playlist = await playlists.getUserPlaylist(user, id);
12✔
259
  if (!playlist) {
12!
260
    throw new PlaylistNotFoundError({ id });
×
261
  }
×
262

11✔
263
  const items = await playlists.getPlaylistItems(playlist, filter, pagination);
11✔
264

11✔
265
  return toPaginatedResponse(items, {
11✔
266
    baseUrl: req.fullUrl,
11✔
267
    included: {
11✔
268
      media: ['media'],
11✔
269
    },
11✔
270
  });
11✔
271
}
12✔
272

1✔
273
/**
1✔
274
 * @typedef {import('../plugins/playlists.js').PlaylistItemDesc} PlaylistItemDesc
1✔
275
 * @typedef {object} AddPlaylistItemsParams
1✔
276
 * @prop {PlaylistID} id
1✔
277
 * @typedef {object} AtPosition
1✔
278
 * @prop {'start'|'end'} at
1✔
279
 * @prop {undefined} after
1✔
280
 * @typedef {object} AfterPosition
1✔
281
 * @prop {undefined} at
1✔
282
 * @prop {PlaylistItemID|-1} after
1✔
283
 * @typedef {{ items: PlaylistItemDesc[] } & (AtPosition | AfterPosition)} AddPlaylistItemsBody
1✔
284
 */
1✔
285

1✔
286
/**
1✔
287
 * @type {import('../types.js').AuthenticatedController<
1✔
288
 *     AddPlaylistItemsParams, {}, AddPlaylistItemsBody>}
1✔
289
 */
1✔
290
async function addPlaylistItems(req) {
8✔
291
  const { user } = req;
8✔
292
  const { playlists } = req.uwave;
8✔
293
  const { id } = req.params;
8✔
294
  const { at, after, items } = req.body;
8✔
295

8✔
296
  const playlist = await playlists.getUserPlaylist(user, id);
8✔
297
  if (!playlist) {
8!
298
    throw new PlaylistNotFoundError({ id });
×
299
  }
×
300

7✔
301
  let options;
7✔
302
  if (at === 'start' || at === 'end') {
8✔
303
    options = { at };
3✔
304
  } else if (after === -1) {
8✔
305
    options = { at: /** @type {const} */ ('end') };
1✔
306
  } else if (after == null) {
4✔
307
    options = { at: /** @type {const} */ ('start') };
2✔
308
  } else {
3✔
309
    options = { after };
1✔
310
  }
1✔
311

7✔
312
  const {
7✔
313
    added,
7✔
314
    afterID: actualAfterID,
7✔
315
    playlistSize,
7✔
316
  } = await playlists.addPlaylistItems(playlist, items, options);
7✔
317

7✔
318
  return toListResponse(added.map(serializePlaylistItem), {
7✔
319
    included: {
7✔
320
      media: ['media'],
7✔
321
    },
7✔
322
    meta: {
7✔
323
      afterID: actualAfterID ? actualAfterID.toString() : null,
8✔
324
      playlistSize,
8✔
325
    },
8✔
326
  });
8✔
327
}
8✔
328

1✔
329
/**
1✔
330
 * @typedef {object} RemovePlaylistItemsParams
1✔
331
 * @prop {PlaylistID} id
1✔
332
 * @typedef {object} RemovePlaylistItemsBody
1✔
333
 * @prop {PlaylistItemID[]} items
1✔
334
 */
1✔
335

1✔
336
/**
1✔
337
 * @type {import('../types.js').AuthenticatedController<
1✔
338
 *     RemovePlaylistItemsParams, {}, RemovePlaylistItemsBody>}
1✔
339
 */
1✔
340
async function removePlaylistItems(req) {
4✔
341
  const { user } = req;
4✔
342
  const { playlists } = req.uwave;
4✔
343
  const { id } = req.params;
4✔
344
  const { items } = req.body;
4✔
345

4✔
346
  const playlist = await playlists.getUserPlaylist(user, id);
4✔
347
  if (!playlist) {
4!
348
    throw new PlaylistNotFoundError({ id });
×
349
  }
×
350

3✔
351
  await playlists.removePlaylistItems(playlist, items);
3✔
352

3✔
353
  return toItemResponse({}, {
3✔
354
    meta: {
3✔
355
      playlistSize: playlist.size,
3✔
356
    },
3✔
357
  });
3✔
358
}
4✔
359

1✔
360
/**
1✔
361
 * @typedef {object} MovePlaylistItemsParams
1✔
362
 * @prop {PlaylistID} id
1✔
363
 * @typedef {{ items: PlaylistItemID[] } & (AtPosition | AfterPosition)} MovePlaylistItemsBody
1✔
364
 */
1✔
365

1✔
366
/**
1✔
367
 * @type {import('../types.js').AuthenticatedController<
1✔
368
 *     MovePlaylistItemsParams, {}, MovePlaylistItemsBody>}
1✔
369
 */
1✔
370
async function movePlaylistItems(req) {
7✔
371
  const { user } = req;
7✔
372
  const { playlists } = req.uwave;
7✔
373
  const { id } = req.params;
7✔
374
  const { at, after, items } = req.body;
7✔
375

7✔
376
  const playlist = await playlists.getUserPlaylist(user, id);
7✔
377
  if (!playlist) {
7!
378
    throw new PlaylistNotFoundError({ id });
×
379
  }
×
380

7✔
381
  let options;
7✔
382
  if (at === 'start' || at === 'end') {
7✔
383
    options = { at };
5✔
384
  } else if (after === -1) {
7✔
385
    options = { at: /** @type {const} */ ('end') };
1✔
386
  } else if (after == null) {
1✔
387
    options = { at: /** @type {const} */ ('start') };
1✔
388
  } else {
1!
NEW
389
    options = { after };
×
UNCOV
390
  }
×
391

7✔
392
  const result = await playlists.movePlaylistItems(playlist, items, options);
7✔
393

7✔
394
  return toItemResponse(result, { url: req.fullUrl });
7✔
395
}
7✔
396

1✔
397
/**
1✔
398
 * @typedef {object} ShufflePlaylistItemsParams
1✔
399
 * @prop {PlaylistID} id
1✔
400
 */
1✔
401

1✔
402
/**
1✔
403
 * @type {import('../types.js').AuthenticatedController<ShufflePlaylistItemsParams>}
1✔
404
 */
1✔
405
async function shufflePlaylistItems(req) {
2✔
406
  const { user } = req;
2✔
407
  const { playlists } = req.uwave;
2✔
408
  const { id } = req.params;
2✔
409

2✔
410
  const playlist = await playlists.getUserPlaylist(user, id);
2✔
411
  if (!playlist) {
2!
412
    throw new PlaylistNotFoundError({ id });
×
413
  }
×
414

1✔
415
  await playlists.shufflePlaylist(playlist);
1✔
416

1✔
417
  return toItemResponse({});
1✔
418
}
2✔
419

1✔
420
/**
1✔
421
 * @typedef {object} GetPlaylistItemParams
1✔
422
 * @prop {PlaylistID} id
1✔
423
 * @prop {PlaylistItemID} itemID
1✔
424
 */
1✔
425

1✔
426
/**
1✔
427
 * @type {import('../types.js').AuthenticatedController<GetPlaylistItemParams>}
1✔
428
 */
1✔
429
async function getPlaylistItem(req) {
×
430
  const { user } = req;
×
431
  const { playlists } = req.uwave;
×
432
  const { id, itemID } = req.params;
×
433

×
NEW
434
  const playlist = await playlists.getUserPlaylist(user, id);
×
435
  if (!playlist) {
×
436
    throw new PlaylistNotFoundError({ id });
×
437
  }
×
438

×
NEW
439
  const { playlistItem, media } = await playlists.getPlaylistItem(playlist, itemID);
×
440

×
NEW
441
  return toItemResponse(legacyPlaylistItem(playlistItem, media), { url: req.fullUrl });
×
442
}
×
443

1✔
444
/**
1✔
445
 * @typedef {object} UpdatePlaylistItemParams
1✔
446
 * @prop {PlaylistID} id
1✔
447
 * @prop {PlaylistItemID} itemID
1✔
448
 * @typedef {object} UpdatePlaylistItemBody
1✔
449
 * @prop {string} [artist]
1✔
450
 * @prop {string} [title]
1✔
451
 * @prop {number} [start]
1✔
452
 * @prop {number} [end]
1✔
453
 */
1✔
454

1✔
455
/**
1✔
456
 * @type {import('../types.js').AuthenticatedController<
1✔
457
 *     UpdatePlaylistItemParams, {}, UpdatePlaylistItemBody>}
1✔
458
 */
1✔
459
async function updatePlaylistItem(req) {
×
460
  const { user } = req;
×
461
  const { playlists } = req.uwave;
×
462
  const { id, itemID } = req.params;
×
463
  const {
×
464
    artist, title, start, end,
×
465
  } = req.body;
×
466

×
467
  const patch = {
×
468
    artist,
×
469
    title,
×
470
    start,
×
471
    end,
×
472
  };
×
473

×
NEW
474
  const playlist = await playlists.getUserPlaylist(user, id);
×
475
  if (!playlist) {
×
476
    throw new PlaylistNotFoundError({ id });
×
477
  }
×
478

×
NEW
479
  const { playlistItem, media } = await playlists.getPlaylistItem(playlist, itemID);
×
NEW
480
  const updatedItem = await playlists.updatePlaylistItem(playlistItem, patch);
×
481

×
NEW
482
  return toItemResponse(legacyPlaylistItem(updatedItem, media), { url: req.fullUrl });
×
483
}
×
484

1✔
485
/**
1✔
486
 * @typedef {object} RemovePlaylistItemParams
1✔
487
 * @prop {PlaylistID} id
1✔
488
 * @prop {PlaylistItemID} itemID
1✔
489
 */
1✔
490

1✔
491
/**
1✔
492
 * @type {import('../types.js').AuthenticatedController<RemovePlaylistItemParams>}
1✔
493
 */
1✔
494
async function removePlaylistItem(req) {
×
495
  const { user } = req;
×
496
  const { playlists } = req.uwave;
×
497
  const { id, itemID } = req.params;
×
498

×
NEW
499
  const playlist = await playlists.getUserPlaylist(user, id);
×
500
  if (!playlist) {
×
501
    throw new PlaylistNotFoundError({ id });
×
502
  }
×
503

×
NEW
504
  await playlists.removePlaylistItems(playlist, [itemID]);
×
505

×
NEW
506
  return toItemResponse({}, { url: req.fullUrl });
×
507
}
×
508

1✔
509
export {
1✔
510
  getPlaylists,
1✔
511
  getPlaylist,
1✔
512
  createPlaylist,
1✔
513
  deletePlaylist,
1✔
514
  updatePlaylist,
1✔
515
  renamePlaylist,
1✔
516
  activatePlaylist,
1✔
517
  getPlaylistItems,
1✔
518
  addPlaylistItems,
1✔
519
  removePlaylistItems,
1✔
520
  movePlaylistItems,
1✔
521
  shufflePlaylistItems,
1✔
522
  getPlaylistItem,
1✔
523
  updatePlaylistItem,
1✔
524
  removePlaylistItem,
1✔
525
};
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc