• 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

62.89
/src/controllers/search.js
1
import lodash from 'lodash';
1✔
2
import { SourceNotFoundError } from '../errors/index.js';
1✔
3
import toListResponse from '../utils/toListResponse.js';
1✔
4

1✔
5
const { isEqual } = lodash;
1✔
6

1✔
7
/** @typedef {import('../schema.js').UserID} UserID */
1✔
8
/** @typedef {import('../schema.js').PlaylistID} PlaylistID */
1✔
9
/** @typedef {import('../schema.js').MediaID} MediaID */
1✔
10
/** @typedef {import('../schema.js').Playlist} Playlist */
1✔
11
/** @typedef {import('../schema.js').Media} Media */
1✔
12
/** @typedef {import('../plugins/playlists.js').PlaylistItemDesc} PlaylistItemDesc */
1✔
13

1✔
14
// TODO should be deprecated once the Web client uses the better single-source route.
1✔
15
/**
1✔
16
 * @type {import('../types.js').AuthenticatedController<never, SearchQuery, never>}
1✔
17
 */
1✔
18
async function searchAll(req) {
×
19
  const { user } = req;
×
20
  const { query } = req.query;
×
21
  const uw = req.uwave;
×
22
  const sourceNames = uw.sources.map((source) => source.type);
×
23
  const searches = uw.sources.map((source) => (
×
24
    source.search(user, query).catch((error) => {
×
25
      req.log.warn(error, { ns: 'uwave:search' });
×
26
      // Default to empty search on failure, for now.
×
27
      return [];
×
28
    })
×
29
  ));
×
30

×
31
  const searchResults = await Promise.all(searches);
×
32

×
33
  const combinedResults = Object.fromEntries(
×
34
    sourceNames.map((name, index) => [name, searchResults[index]]),
×
35
  );
×
36

×
37
  return combinedResults;
×
38
}
×
39

1✔
40
/**
1✔
41
 * @param {import('../Uwave.js').default} uw
1✔
42
 * @param {Map<MediaID, Media['sourceData']>} updates
1✔
43
 */
1✔
44
function updateSourceData(uw, updates) {
1✔
45
  return uw.db.transaction().execute(async (tx) => {
1✔
46
    uw.logger.debug({ ns: 'uwave:search', forMedia: [...updates.keys()] }, 'updating source data');
1✔
47
    for (const [id, sourceData] of updates.entries()) {
1!
NEW
48
      await tx.updateTable('media')
×
NEW
49
        .where('id', '=', id)
×
NEW
50
        .set({ sourceData })
×
NEW
51
        .executeTakeFirst();
×
NEW
52
    }
×
53
  });
1✔
54
}
1✔
55

1✔
56
/**
1✔
57
 * @typedef {object} SearchParams
1✔
58
 * @prop {string} source
1✔
59
 * @typedef {object} SearchQuery
1✔
60
 * @prop {string} query
1✔
61
 * @prop {string} [include]
1✔
62
 */
1✔
63

1✔
64
/**
1✔
65
 * @type {import('../types.js').AuthenticatedController<SearchParams, SearchQuery, never>}
1✔
66
 */
1✔
67
async function search(req) {
2✔
68
  const { user } = req;
2✔
69
  const { source: sourceName } = req.params;
2✔
70
  const { query, include } = req.query;
2✔
71
  const uw = req.uwave;
2✔
72
  const db = uw.db;
2✔
73

2✔
74
  const source = uw.source(sourceName);
2✔
75
  if (!source) {
2✔
76
    throw new SourceNotFoundError({ name: sourceName });
1✔
77
  }
1✔
78

1✔
79
  /** @type {(PlaylistItemDesc & { inPlaylists?: Playlist[] })[]} */
1✔
80
  const searchResults = await source.search(user, query);
1✔
81

1✔
82
  const searchResultsByID = new Map();
1✔
83
  searchResults.forEach((result) => {
1✔
84
    searchResultsByID.set(result.sourceID, result);
1✔
85
  });
1✔
86

1✔
87
  // Track medias whose `sourceData` property no longer matches that from the source.
1✔
88
  // This can happen because the media was actually changed, but also because of new
1✔
89
  // features in the source implementation.
1✔
90
  /** @type {Map<MediaID, Media['sourceData']>} */
1✔
91
  const mediasNeedSourceDataUpdate = new Map();
1✔
92

1✔
93
  const mediasInSearchResults = await db.selectFrom('media')
1✔
94
    .select(['id', 'sourceType', 'sourceID', 'sourceData'])
1✔
95
    .where('sourceType', '=', sourceName)
1✔
96
    .where('sourceID', 'in', Array.from(searchResultsByID.keys()))
1✔
97
    .execute();
1✔
98

1✔
99
  /** @type {Map<string, typeof mediasInSearchResults[0]>} */
1✔
100
  const mediaBySourceID = new Map();
1✔
101
  mediasInSearchResults.forEach((media) => {
1✔
102
    mediaBySourceID.set(media.sourceID, media);
×
103

×
104
    const freshMedia = searchResultsByID.get(media.sourceID);
×
105
    if (freshMedia && !isEqual(media.sourceData, freshMedia.sourceData)) {
×
NEW
106
      mediasNeedSourceDataUpdate.set(media.id, freshMedia.sourceData);
×
107
    }
×
108
  });
1✔
109

1✔
110
  // don't wait for this to complete
1✔
111
  updateSourceData(uw, mediasNeedSourceDataUpdate).catch((error) => {
1✔
112
    uw.logger.error({ ns: 'uwave:search', err: error }, 'sourceData update failed');
×
113
  });
1✔
114

1✔
115
  // Only include related playlists if requested
1✔
116
  if (typeof include === 'string' && include.split(',').includes('playlists')) {
2!
117
    const playlistsByMediaID = await uw.playlists.getPlaylistsContainingAnyMedia(
×
NEW
118
      mediasInSearchResults.map((media) => media.id),
×
NEW
119
      { author: user.id },
×
120
    ).catch((error) => {
×
121
      uw.logger.error({ ns: 'uwave:search', err: error }, 'playlists containing media lookup failed');
×
122
      // just omit the related playlists if we timed out or crashed
×
123
      return new Map();
×
124
    });
×
125

×
126
    searchResults.forEach((result) => {
×
127
      const media = mediaBySourceID.get(String(result.sourceID));
×
128
      if (media) {
×
NEW
129
        result.inPlaylists = playlistsByMediaID.get(media.id);
×
130
      }
×
131
    });
×
132

×
133
    return toListResponse(searchResults, {
×
134
      url: req.fullUrl,
×
135
      included: {
×
136
        playlists: ['inPlaylists'],
×
137
      },
×
138
    });
×
139
  }
×
140

1✔
141
  return toListResponse(searchResults, {
1✔
142
    url: req.fullUrl,
1✔
143
  });
1✔
144
}
2✔
145

1✔
146
export {
1✔
147
  search,
1✔
148
  searchAll,
1✔
149
};
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