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

u-wave / core / 11085094286

28 Sep 2024 03:39PM UTC coverage: 79.715% (-0.4%) from 80.131%
11085094286

Pull #637

github

web-flow
Merge 11ccf3b06 into 14c162f19
Pull Request #637: Switch to a relational database, closes #549

751 of 918 branches covered (81.81%)

Branch coverage included in aggregate %.

1891 of 2530 new or added lines in 50 files covered. (74.74%)

13 existing lines in 7 files now uncovered.

9191 of 11554 relevant lines covered (79.55%)

68.11 hits per line

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

79.33
/src/controllers/playlists.js
1
import { HTTPError, PlaylistNotFoundError, PlaylistItemNotFoundError } 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<import('../schema').PlaylistItem, 'id' | 'artist' | 'title' | 'start' | 'end' | 'createdAt'>} playlistItem
1✔
18
 * @param {Pick<import('../schema').Media, 'id' | 'sourceType' | 'sourceID' | 'sourceData' | 'artist' | 'title' | 'duration' | 'thumbnail'>} media
1✔
19
 */
1✔
20
export function legacyPlaylistItem(playlistItem, media) {
1✔
NEW
21
  return {
×
NEW
22
    _id: playlistItem.id,
×
NEW
23
    artist: playlistItem.artist,
×
NEW
24
    title: playlistItem.title,
×
NEW
25
    start: playlistItem.start,
×
NEW
26
    end: playlistItem.end,
×
NEW
27
    media: {
×
NEW
28
      _id: media.id,
×
NEW
29
      sourceType: media.sourceType,
×
NEW
30
      sourceID: media.sourceID,
×
NEW
31
      sourceData: media.sourceData,
×
NEW
32
      artist: media.artist,
×
NEW
33
      title: media.title,
×
NEW
34
      duration: media.duration,
×
NEW
35
      thumbnail: media.thumbnail,
×
NEW
36
    },
×
NEW
37
    createdAt: playlistItem.createdAt,
×
NEW
38
  };
×
NEW
39
}
×
40

1✔
41
/**
1✔
42
 * @typedef {object} GetPlaylistsQuery
1✔
43
 * @prop {MediaID} [contains]
1✔
44
 */
1✔
45

1✔
46
/**
1✔
47
 * @type {import('../types.js').AuthenticatedController<{}, GetPlaylistsQuery>}
1✔
48
 */
1✔
49
async function getPlaylists(req) {
1✔
50
  const { user } = req;
1✔
51
  const uw = req.uwave;
1✔
52
  const { contains } = req.query;
1✔
53

1✔
54
  let playlists;
1✔
55
  if (contains) {
1!
NEW
56
    playlists = await uw.playlists.getPlaylistsContainingMedia(contains, { author: user.id });
×
57
  } else {
1✔
58
    playlists = await uw.playlists.getUserPlaylists(user);
1✔
59
  }
1✔
60

1✔
61
  return toListResponse(
1✔
62
    playlists.map(serializePlaylist),
1✔
63
    { url: req.fullUrl },
1✔
64
  );
1✔
65
}
1✔
66

1✔
67
/**
1✔
68
 * @typedef {object} GetPlaylistParams
1✔
69
 * @prop {PlaylistID} id
1✔
70
 */
1✔
71

1✔
72
/**
1✔
73
 * @type {import('../types.js').AuthenticatedController<GetPlaylistParams>}
1✔
74
 */
1✔
75
async function getPlaylist(req) {
2✔
76
  const { user } = req;
2✔
77
  const { playlists } = req.uwave;
2✔
78
  const { id } = req.params;
2✔
79

2✔
80
  const playlist = await playlists.getUserPlaylist(user, id);
2✔
81

2✔
82
  if (!playlist) {
2!
83
    throw new PlaylistNotFoundError({ id });
×
84
  }
×
85

2✔
86
  return toItemResponse(
2✔
87
    serializePlaylist(playlist),
2✔
88
    { url: req.fullUrl },
2✔
89
  );
2✔
90
}
2✔
91

1✔
92
/**
1✔
93
 * @typedef {object} CreatePlaylistBody
1✔
94
 * @prop {string} name
1✔
95
 */
1✔
96

1✔
97
/**
1✔
98
 * @type {import('../types.js').AuthenticatedController<{}, {}, CreatePlaylistBody>}
1✔
99
 */
1✔
100
async function createPlaylist(req) {
11✔
101
  const { user } = req;
11✔
102
  const { name } = req.body;
11✔
103
  const { playlists } = req.uwave;
11✔
104

11✔
105
  const { playlist, active } = await playlists.createPlaylist(user, {
11✔
106
    name,
11✔
107
  });
11✔
108

11✔
109
  return toItemResponse(
11✔
110
    serializePlaylist(playlist),
11✔
111
    {
11✔
112
      url: req.fullUrl,
11✔
113
      meta: { active },
11✔
114
    },
11✔
115
  );
11✔
116
}
11✔
117

1✔
118
/**
1✔
119
 * @typedef {object} DeletePlaylistParams
1✔
120
 * @prop {PlaylistID} id
1✔
121
 */
1✔
122

1✔
123
/**
1✔
124
 * @type {import('../types.js').AuthenticatedController<DeletePlaylistParams>}
1✔
125
 */
1✔
126
async function deletePlaylist(req) {
×
127
  const { user } = req;
×
128
  const { id } = req.params;
×
129
  const { playlists } = req.uwave;
×
130

×
NEW
131
  const playlist = await playlists.getUserPlaylist(user, id);
×
132
  if (!playlist) {
×
133
    throw new PlaylistNotFoundError({ id });
×
134
  }
×
135

×
136
  await playlists.deletePlaylist(playlist);
×
137

×
138
  return toItemResponse({}, { url: req.fullUrl });
×
139
}
×
140

1✔
141
const patchableKeys = ['name', 'description'];
1✔
142

1✔
143
/**
1✔
144
 * @typedef {object} UpdatePlaylistParams
1✔
145
 * @prop {PlaylistID} id
1✔
146
 * @typedef {Record<string, string>} UpdatePlaylistBody
1✔
147
 */
1✔
148

1✔
149
/**
1✔
150
 * @type {import('../types.js').AuthenticatedController<
1✔
151
 *     UpdatePlaylistParams, {}, UpdatePlaylistBody>}
1✔
152
 */
1✔
153
async function updatePlaylist(req) {
3✔
154
  const { user } = req;
3✔
155
  const { id } = req.params;
3✔
156
  const patch = req.body;
3✔
157
  const { playlists } = req.uwave;
3✔
158

3✔
159
  const patches = Object.keys(patch);
3✔
160
  patches.forEach((patchKey) => {
3✔
161
    if (!patchableKeys.includes(patchKey)) {
3✔
162
      throw new HTTPError(400, `Key "${patchKey}" cannot be updated.`);
1✔
163
    }
1✔
164
  });
3✔
165

3✔
166
  const playlist = await playlists.getUserPlaylist(user, id);
3✔
167
  if (!playlist) {
3!
168
    throw new PlaylistNotFoundError({ id });
×
169
  }
×
170

1✔
171
  const updatedPlaylist = await playlists.updatePlaylist(playlist, patch);
1✔
172

1✔
173
  return toItemResponse(
1✔
174
    serializePlaylist(updatedPlaylist),
1✔
175
    { url: req.fullUrl },
1✔
176
  );
1✔
177
}
3✔
178

1✔
179
/**
1✔
180
 * @typedef {object} RenamePlaylistParams
1✔
181
 * @prop {PlaylistID} id
1✔
182
 * @typedef {object} RenamePlaylistBody
1✔
183
 * @prop {string} name
1✔
184
 */
1✔
185

1✔
186
/**
1✔
187
 * @type {import('../types.js').AuthenticatedController<
1✔
188
 *     RenamePlaylistParams, {}, RenamePlaylistBody>}
1✔
189
 */
1✔
190
async function renamePlaylist(req) {
2✔
191
  const { user } = req;
2✔
192
  const { id } = req.params;
2✔
193
  const { name } = req.body;
2✔
194
  const { playlists } = req.uwave;
2✔
195

2✔
196
  const playlist = await playlists.getUserPlaylist(user, id);
2✔
197
  if (!playlist) {
2!
198
    throw new PlaylistNotFoundError({ id });
×
199
  }
×
200

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

1✔
203
  return toItemResponse(
1✔
204
    serializePlaylist(updatedPlaylist),
1✔
205
    { url: req.fullUrl },
1✔
206
  );
1✔
207
}
2✔
208

1✔
209
/**
1✔
210
 * @typedef {object} ActivatePlaylistParams
1✔
211
 * @prop {PlaylistID} id
1✔
212
 */
1✔
213

1✔
214
/**
1✔
215
 * @type {import('../types.js').AuthenticatedController<ActivatePlaylistParams>}
1✔
216
 */
1✔
217
async function activatePlaylist(req) {
1✔
218
  const { user } = req;
1✔
219
  const { db, playlists } = req.uwave;
1✔
220
  const { id } = req.params;
1✔
221

1✔
222
  const playlist = await playlists.getUserPlaylist(user, id);
1✔
223
  if (!playlist) {
1!
224
    throw new PlaylistNotFoundError({ id });
×
225
  }
×
226

1✔
227
  await db.updateTable('users')
1✔
228
    .where('id', '=', user.id)
1✔
229
    .set({ activePlaylistID: playlist.id })
1✔
230
    .execute();
1✔
231

1✔
232
  return toItemResponse({});
1✔
233
}
1✔
234

1✔
235
/**
1✔
236
 * @typedef {object} GetPlaylistItemsParams
1✔
237
 * @prop {PlaylistID} id
1✔
238
 * @typedef {import('../types.js').PaginationQuery & { filter?: string }} GetPlaylistItemsQuery
1✔
239
 */
1✔
240

1✔
241
/**
1✔
242
 * @type {import('../types.js').AuthenticatedController<
1✔
243
 *     GetPlaylistItemsParams, GetPlaylistItemsQuery>}
1✔
244
 */
1✔
245
async function getPlaylistItems(req) {
12✔
246
  const { user } = req;
12✔
247
  const { playlists } = req.uwave;
12✔
248
  const { id } = req.params;
12✔
249
  const filter = req.query.filter ?? undefined;
12✔
250
  const pagination = getOffsetPagination(req.query);
12✔
251

12✔
252
  const playlist = await playlists.getUserPlaylist(user, id);
12✔
253
  if (!playlist) {
12!
254
    throw new PlaylistNotFoundError({ id });
×
255
  }
×
256

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

11✔
259
  return toPaginatedResponse(items, {
11✔
260
    baseUrl: req.fullUrl,
11✔
261
    included: {
11✔
262
      media: ['media'],
11✔
263
    },
11✔
264
  });
11✔
265
}
12✔
266

1✔
267
/**
1✔
268
 * @typedef {import('../plugins/playlists.js').PlaylistItemDesc} PlaylistItemDesc
1✔
269
 * @typedef {object} AddPlaylistItemsParams
1✔
270
 * @prop {PlaylistID} id
1✔
271
 * @typedef {object} AtPosition
1✔
272
 * @prop {'start'|'end'} at
1✔
273
 * @prop {undefined} after
1✔
274
 * @typedef {object} AfterPosition
1✔
275
 * @prop {undefined} at
1✔
276
 * @prop {PlaylistItemID|-1} after
1✔
277
 * @typedef {{ items: PlaylistItemDesc[] } & (AtPosition | AfterPosition)} AddPlaylistItemsBody
1✔
278
 */
1✔
279

1✔
280
/**
1✔
281
 * @type {import('../types.js').AuthenticatedController<
1✔
282
 *     AddPlaylistItemsParams, {}, AddPlaylistItemsBody>}
1✔
283
 */
1✔
284
async function addPlaylistItems(req) {
8✔
285
  const { user } = req;
8✔
286
  const { playlists } = req.uwave;
8✔
287
  const { id } = req.params;
8✔
288
  const { at, after, items } = req.body;
8✔
289

8✔
290
  const playlist = await playlists.getUserPlaylist(user, id);
8✔
291
  if (!playlist) {
8!
292
    throw new PlaylistNotFoundError({ id });
×
293
  }
×
294

7✔
295
  let options;
7✔
296
  if (at === 'start' || at === 'end') {
8✔
297
    options = { at };
3✔
298
  } else if (after === -1) {
8✔
299
    options = { at: /** @type {const} */ ('end') };
1✔
300
  } else if (after == null) {
4✔
301
    options = { at: /** @type {const} */ ('start') };
2✔
302
  } else {
3✔
303
    options = { after };
1✔
304
  }
1✔
305

7✔
306
  const {
7✔
307
    added,
7✔
308
    afterID: actualAfterID,
7✔
309
    playlistSize,
7✔
310
  } = await playlists.addPlaylistItems(playlist, items, options);
7✔
311

7✔
312
  return toListResponse(added.map(serializePlaylistItem), {
7✔
313
    included: {
7✔
314
      media: ['media'],
7✔
315
    },
7✔
316
    meta: {
7✔
317
      afterID: actualAfterID ? actualAfterID.toString() : null,
8✔
318
      playlistSize,
8✔
319
    },
8✔
320
  });
8✔
321
}
8✔
322

1✔
323
/**
1✔
324
 * @typedef {object} RemovePlaylistItemsParams
1✔
325
 * @prop {PlaylistID} id
1✔
326
 * @typedef {object} RemovePlaylistItemsBody
1✔
327
 * @prop {PlaylistItemID[]} items
1✔
328
 */
1✔
329

1✔
330
/**
1✔
331
 * @type {import('../types.js').AuthenticatedController<
1✔
332
 *     RemovePlaylistItemsParams, {}, RemovePlaylistItemsBody>}
1✔
333
 */
1✔
334
async function removePlaylistItems(req) {
4✔
335
  const { user } = req;
4✔
336
  const { playlists } = req.uwave;
4✔
337
  const { id } = req.params;
4✔
338
  const { items } = req.body;
4✔
339

4✔
340
  const playlist = await playlists.getUserPlaylist(user, id);
4✔
341
  if (!playlist) {
4!
342
    throw new PlaylistNotFoundError({ id });
×
343
  }
×
344

3✔
345
  await playlists.removePlaylistItems(playlist, items);
3✔
346

3✔
347
  return toItemResponse({}, {
3✔
348
    meta: {
3✔
349
      playlistSize: playlist.size,
3✔
350
    },
3✔
351
  });
3✔
352
}
4✔
353

1✔
354
/**
1✔
355
 * @typedef {object} MovePlaylistItemsParams
1✔
356
 * @prop {PlaylistID} id
1✔
357
 * @typedef {{ items: PlaylistItemID[] } & (AtPosition | AfterPosition)} MovePlaylistItemsBody
1✔
358
 */
1✔
359

1✔
360
/**
1✔
361
 * @type {import('../types.js').AuthenticatedController<
1✔
362
 *     MovePlaylistItemsParams, {}, MovePlaylistItemsBody>}
1✔
363
 */
1✔
364
async function movePlaylistItems(req) {
7✔
365
  const { user } = req;
7✔
366
  const { playlists } = req.uwave;
7✔
367
  const { id } = req.params;
7✔
368
  const { at, after, items } = req.body;
7✔
369

7✔
370
  const playlist = await playlists.getUserPlaylist(user, id);
7✔
371
  if (!playlist) {
7!
372
    throw new PlaylistNotFoundError({ id });
×
373
  }
×
374

7✔
375
  let options;
7✔
376
  if (at === 'start' || at === 'end') {
7✔
377
    options = { at };
5✔
378
  } else if (after === -1) {
7✔
379
    options = { at: /** @type {const} */ ('end') };
1✔
380
  } else if (after == null) {
1✔
381
    options = { at: /** @type {const} */ ('start') };
1✔
382
  } else {
1!
NEW
383
    options = { after };
×
UNCOV
384
  }
×
385

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

7✔
388
  return toItemResponse(result, { url: req.fullUrl });
7✔
389
}
7✔
390

1✔
391
/**
1✔
392
 * @typedef {object} ShufflePlaylistItemsParams
1✔
393
 * @prop {PlaylistID} id
1✔
394
 */
1✔
395

1✔
396
/**
1✔
397
 * @type {import('../types.js').AuthenticatedController<ShufflePlaylistItemsParams>}
1✔
398
 */
1✔
399
async function shufflePlaylistItems(req) {
2✔
400
  const { user } = req;
2✔
401
  const { playlists } = req.uwave;
2✔
402
  const { id } = req.params;
2✔
403

2✔
404
  const playlist = await playlists.getUserPlaylist(user, id);
2✔
405
  if (!playlist) {
2!
406
    throw new PlaylistNotFoundError({ id });
×
407
  }
×
408

1✔
409
  await playlists.shufflePlaylist(playlist);
1✔
410

1✔
411
  return toItemResponse({});
1✔
412
}
2✔
413

1✔
414
/**
1✔
415
 * @typedef {object} GetPlaylistItemParams
1✔
416
 * @prop {PlaylistID} id
1✔
417
 * @prop {PlaylistItemID} itemID
1✔
418
 */
1✔
419

1✔
420
/**
1✔
421
 * @type {import('../types.js').AuthenticatedController<GetPlaylistItemParams>}
1✔
422
 */
1✔
423
async function getPlaylistItem(req) {
×
424
  const { user } = req;
×
425
  const { playlists } = req.uwave;
×
426
  const { id, itemID } = req.params;
×
427

×
NEW
428
  const playlist = await playlists.getUserPlaylist(user, id);
×
429
  if (!playlist) {
×
430
    throw new PlaylistNotFoundError({ id });
×
431
  }
×
432

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

×
NEW
435
  return toItemResponse(legacyPlaylistItem(playlistItem, media), { url: req.fullUrl });
×
436
}
×
437

1✔
438
/**
1✔
439
 * @typedef {object} UpdatePlaylistItemParams
1✔
440
 * @prop {PlaylistID} id
1✔
441
 * @prop {PlaylistItemID} itemID
1✔
442
 * @typedef {object} UpdatePlaylistItemBody
1✔
443
 * @prop {string} [artist]
1✔
444
 * @prop {string} [title]
1✔
445
 * @prop {number} [start]
1✔
446
 * @prop {number} [end]
1✔
447
 */
1✔
448

1✔
449
/**
1✔
450
 * @type {import('../types.js').AuthenticatedController<
1✔
451
 *     UpdatePlaylistItemParams, {}, UpdatePlaylistItemBody>}
1✔
452
 */
1✔
453
async function updatePlaylistItem(req) {
×
454
  const { user } = req;
×
455
  const { playlists } = req.uwave;
×
456
  const { id, itemID } = req.params;
×
457
  const {
×
458
    artist, title, start, end,
×
459
  } = req.body;
×
460

×
461
  const patch = {
×
462
    artist,
×
463
    title,
×
464
    start,
×
465
    end,
×
466
  };
×
467

×
NEW
468
  const playlist = await playlists.getUserPlaylist(user, id);
×
469
  if (!playlist) {
×
470
    throw new PlaylistNotFoundError({ id });
×
471
  }
×
472

×
NEW
473
  const { playlistItem, media } = await playlists.getPlaylistItem(playlist, itemID);
×
NEW
474
  const updatedItem = await playlists.updatePlaylistItem(playlistItem, patch);
×
475

×
NEW
476
  return toItemResponse(legacyPlaylistItem(updatedItem, media), { url: req.fullUrl });
×
477
}
×
478

1✔
479
/**
1✔
480
 * @typedef {object} RemovePlaylistItemParams
1✔
481
 * @prop {PlaylistID} id
1✔
482
 * @prop {PlaylistItemID} itemID
1✔
483
 */
1✔
484

1✔
485
/**
1✔
486
 * @type {import('../types.js').AuthenticatedController<RemovePlaylistItemParams>}
1✔
487
 */
1✔
488
async function removePlaylistItem(req) {
×
489
  const { user } = req;
×
490
  const { playlists } = req.uwave;
×
491
  const { id, itemID } = req.params;
×
492

×
NEW
493
  const playlist = await playlists.getUserPlaylist(user, id);
×
494
  if (!playlist) {
×
495
    throw new PlaylistNotFoundError({ id });
×
496
  }
×
497

×
NEW
498
  await playlists.removePlaylistItems(playlist, [itemID]);
×
499

×
NEW
500
  return toItemResponse({}, { url: req.fullUrl });
×
501
}
×
502

1✔
503
export {
1✔
504
  getPlaylists,
1✔
505
  getPlaylist,
1✔
506
  createPlaylist,
1✔
507
  deletePlaylist,
1✔
508
  updatePlaylist,
1✔
509
  renamePlaylist,
1✔
510
  activatePlaylist,
1✔
511
  getPlaylistItems,
1✔
512
  addPlaylistItems,
1✔
513
  removePlaylistItems,
1✔
514
  movePlaylistItems,
1✔
515
  shufflePlaylistItems,
1✔
516
  getPlaylistItem,
1✔
517
  updatePlaylistItem,
1✔
518
  removePlaylistItem,
1✔
519
};
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