• 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

10.2
/src/migrations/003-populate-sql.cjs
1
'use strict';
1✔
2

1✔
3
const { randomUUID } = require('node:crypto');
1✔
4
const mongoose = require('mongoose');
1✔
5
const { sql } = require('kysely');
1✔
6

1✔
7
/** @param {unknown} value */
1✔
NEW
8
function jsonb(value) {
×
NEW
9
  return sql`jsonb(${JSON.stringify(value)})`;
×
NEW
10
}
×
11

1✔
12
/**
1✔
13
 * @param {import('umzug').MigrationParams<import('../Uwave').default>} params
1✔
14
 */
1✔
15
async function up({ context: uw }) {
92✔
16
  const { db } = uw;
92✔
17

92✔
18
  if (uw.options.mongo == null) {
92✔
19
    return;
92✔
20
  }
92✔
NEW
21

×
NEW
22
  const mongo = await mongoose.connect(uw.options.mongo).catch(() => null);
×
NEW
23
  if (mongo == null) {
×
NEW
24
    return;
×
NEW
25
  }
×
NEW
26

×
NEW
27
  const models = {
×
NEW
28
    AclRole: mongo.model('AclRole', await import('../models/AclRole.js').then((m) => m.default)),
×
NEW
29
    Authentication: mongo.model('Authentication', await import('../models/Authentication.js').then((m) => m.default)),
×
NEW
30
    Config: mongo.model('Config', await import('../models/Config.js').then((m) => m.default)),
×
NEW
31
    HistoryEntry: mongo.model('History', await import('../models/History.js').then((m) => m.default)),
×
NEW
32
    Media: mongo.model('Media', await import('../models/Media.js').then((m) => m.default)),
×
NEW
33
    Migration: mongo.model('Migration', await import('../models/Migration.js').then((m) => m.default)),
×
NEW
34
    Playlist: mongo.model('Playlist', await import('../models/Playlist.js').then((m) => m.default)),
×
NEW
35
    PlaylistItem: mongo.model('PlaylistItem', await import('../models/PlaylistItem.js').then((m) => m.default)),
×
NEW
36
    User: mongo.model('User', await import('../models/User.js').then((m) => m.default)),
×
NEW
37
  };
×
NEW
38

×
NEW
39
  // For now redis is still required.
×
NEW
40
  const motd = await uw.redis.get('motd');
×
NEW
41

×
NEW
42
  /** @type {Map<string, string>} */
×
NEW
43
  const idMap = new Map();
×
NEW
44

×
NEW
45
  await db.transaction().execute(async (tx) => {
×
NEW
46
    for await (const config of models.Config.find().lean()) {
×
NEW
47
      const { _id: name, ...value } = config;
×
NEW
48
      await tx.insertInto('configuration')
×
NEW
49
        .values({ name, value: jsonb(value) })
×
NEW
50
        .execute();
×
NEW
51
    }
×
NEW
52

×
NEW
53
    if (motd != null && motd !== '') {
×
NEW
54
      await tx.insertInto('configuration')
×
NEW
55
        .values({ name: 'u-wave:motd', value: jsonb(motd) })
×
NEW
56
        .execute();
×
NEW
57
    }
×
NEW
58

×
NEW
59
    for await (const media of models.Media.find().lean()) {
×
NEW
60
      const id = randomUUID();
×
NEW
61
      await tx.insertInto('media')
×
NEW
62
        .values({
×
NEW
63
          id,
×
NEW
64
          sourceType: media.sourceType,
×
NEW
65
          sourceID: media.sourceID,
×
NEW
66
          sourceData: jsonb(media.sourceData),
×
NEW
67
          artist: media.artist,
×
NEW
68
          title: media.title,
×
NEW
69
          duration: media.duration,
×
NEW
70
          thumbnail: media.thumbnail,
×
NEW
71
          createdAt: media.createdAt.toISOString(),
×
NEW
72
          updatedAt: media.updatedAt.toISOString(),
×
NEW
73
        })
×
NEW
74
        .onConflict((conflict) => conflict.columns(['sourceType', 'sourceID']).doUpdateSet({
×
NEW
75
          updatedAt: (eb) => eb.ref('excluded.updatedAt'),
×
NEW
76
        }))
×
NEW
77
        .execute();
×
NEW
78

×
NEW
79
      idMap.set(media._id.toString(), id);
×
NEW
80
    }
×
NEW
81

×
NEW
82
    const roles = await models.AclRole.find().lean();
×
NEW
83
    /** @type {Record<string, string[]>} */
×
NEW
84
    const roleMap = Object.create(null);
×
NEW
85
    for (const role of roles) {
×
NEW
86
      if (role._id.includes('.') || role._id === '*') {
×
NEW
87
        continue;
×
NEW
88
      }
×
NEW
89

×
NEW
90
      roleMap[role._id] = role.roles ?? [];
×
NEW
91
    }
×
NEW
92
    const permissionRows = Object.entries(roleMap).map(([role, permissions]) => ({
×
NEW
93
      id: role,
×
NEW
94
      permissions: jsonb(
×
NEW
95
        permissions.flatMap((perm) => perm.includes('.') || perm === '*' ? [perm] : roleMap[perm]),
×
NEW
96
      ),
×
NEW
97
    }));
×
NEW
98

×
NEW
99
    if (permissionRows.length > 0) {
×
NEW
100
      await tx.insertInto('roles')
×
NEW
101
        .values(permissionRows)
×
NEW
102
        .execute();
×
NEW
103
    }
×
NEW
104

×
NEW
105
    for await (const user of models.User.find().lean()) {
×
NEW
106
      const userID = randomUUID();
×
NEW
107
      idMap.set(user._id.toString(), userID);
×
NEW
108

×
NEW
109
      await tx.insertInto('users')
×
NEW
110
        .values({
×
NEW
111
          id: userID,
×
NEW
112
          username: user.username,
×
NEW
113
          slug: user.slug,
×
NEW
114
          createdAt: user.createdAt.toISOString(),
×
NEW
115
          updatedAt: user.updatedAt.toISOString(),
×
NEW
116
        })
×
NEW
117
        .execute();
×
NEW
118

×
NEW
119
      if (user.roles.length > 0) {
×
NEW
120
        await tx.insertInto('userRoles')
×
NEW
121
          .values(user.roles.map((role) => ({ userID, role })))
×
NEW
122
          .execute();
×
NEW
123
      }
×
NEW
124

×
NEW
125
      for await (const playlist of models.Playlist.where('author', user._id).lean()) {
×
NEW
126
        const playlistID = randomUUID();
×
NEW
127
        idMap.set(playlist._id.toString(), playlistID);
×
NEW
128

×
NEW
129
        await tx.insertInto('playlists')
×
NEW
130
          .values({
×
NEW
131
            id: playlistID,
×
NEW
132
            name: playlist.name,
×
NEW
133
            userID,
×
NEW
134
            createdAt: playlist.createdAt.toISOString(),
×
NEW
135
            updatedAt: playlist.updatedAt.toISOString(),
×
NEW
136
          })
×
NEW
137
          .execute();
×
NEW
138

×
NEW
139
        const items = [];
×
NEW
140
        for (const itemMongoID of playlist.media) {
×
NEW
141
          const itemID = randomUUID();
×
NEW
142
          idMap.set(itemMongoID.toString(), itemID);
×
NEW
143

×
NEW
144
          const item = await models.PlaylistItem.findById(itemMongoID).lean();
×
NEW
145
          await tx.insertInto('playlistItems')
×
NEW
146
            .values({
×
NEW
147
              id: itemID,
×
NEW
148
              playlistID,
×
NEW
149
              mediaID: idMap.get(item.media.toString()),
×
NEW
150
              artist: item.artist,
×
NEW
151
              title: item.title,
×
NEW
152
              start: item.start,
×
NEW
153
              end: item.end,
×
NEW
154
              createdAt: item.createdAt.toISOString(),
×
NEW
155
              updatedAt: item.updatedAt.toISOString(),
×
NEW
156
            })
×
NEW
157
            .execute();
×
NEW
158

×
NEW
159
          items.push(itemID);
×
NEW
160
        }
×
NEW
161

×
NEW
162
        await tx.updateTable('playlists')
×
NEW
163
          .where('id', '=', playlistID)
×
NEW
164
          .set({ items: jsonb(items) })
×
NEW
165
          .execute();
×
NEW
166
      }
×
NEW
167
    }
×
NEW
168

×
NEW
169
    for await (const entry of models.Authentication.find().lean()) {
×
NEW
170
      const userID = idMap.get(entry.user.toString());
×
NEW
171
      if (userID == null) {
×
NEW
172
        throw new Error('Migration failure: unknown user ID');
×
NEW
173
      }
×
NEW
174

×
NEW
175
      if (entry.email != null) {
×
NEW
176
        await tx.updateTable('users')
×
NEW
177
          .where('id', '=', userID)
×
NEW
178
          .set({ email: entry.email })
×
NEW
179
          .execute();
×
NEW
180
      }
×
NEW
181

×
NEW
182
      if (entry.hash != null) {
×
NEW
183
        await tx.updateTable('users')
×
NEW
184
          .where('id', '=', userID)
×
NEW
185
          .set({ password: entry.hash })
×
NEW
186
          .execute();
×
NEW
187
      }
×
NEW
188
    }
×
NEW
189

×
NEW
190
    for await (const entry of models.HistoryEntry.find().lean()) {
×
NEW
191
      const entryID = randomUUID();
×
NEW
192
      idMap.set(entry._id.toString(), entryID);
×
NEW
193
      const userID = idMap.get(entry.user.toString());
×
NEW
194
      const mediaID = idMap.get(entry.media.media.toString());
×
NEW
195
      await tx.insertInto('historyEntries')
×
NEW
196
        .values({
×
NEW
197
          id: entryID,
×
NEW
198
          mediaID,
×
NEW
199
          userID,
×
NEW
200
          artist: entry.media.artist,
×
NEW
201
          title: entry.media.title,
×
NEW
202
          start: entry.media.start,
×
NEW
203
          end: entry.media.end,
×
NEW
204
          sourceData: jsonb(entry.media.sourceData),
×
NEW
205
          createdAt: entry.playedAt.toISOString(),
×
NEW
206
        })
×
NEW
207
        .execute();
×
NEW
208

×
NEW
209
      const feedback = new Map();
×
NEW
210
      for (const id of entry.upvotes) {
×
NEW
211
        feedback.set(id.toString(), {
×
NEW
212
          historyEntryID: entryID,
×
NEW
213
          userID: idMap.get(id.toString()),
×
NEW
214
          vote: 1,
×
NEW
215
        });
×
NEW
216
      }
×
NEW
217
      for (const id of entry.downvotes) {
×
NEW
218
        feedback.set(id.toString(), {
×
NEW
219
          historyEntryID: entryID,
×
NEW
220
          userID: idMap.get(id.toString()),
×
NEW
221
          vote: -1,
×
NEW
222
        });
×
NEW
223
      }
×
NEW
224
      for (const id of entry.favorites) {
×
NEW
225
        const entry = feedback.get(id.toString());
×
NEW
226
        if (entry != null) {
×
NEW
227
          entry.favorite = 1;
×
NEW
228
        } else {
×
NEW
229
          feedback.set(id.toString(), {
×
NEW
230
            historyEntryID: entryID,
×
NEW
231
            userID: idMap.get(id.toString()),
×
NEW
232
            favorite: 1,
×
NEW
233
          });
×
NEW
234
        }
×
NEW
235
      }
×
NEW
236

×
NEW
237
      if (feedback.size > 0) {
×
NEW
238
        await tx.insertInto('feedback')
×
NEW
239
          .values(Array.from(feedback.values()))
×
NEW
240
          .execute();
×
NEW
241
      }
×
NEW
242
    }
×
NEW
243
  })
×
NEW
244
    .finally(() => mongo.disconnect());
×
245
}
92✔
246

1✔
247
/**
1✔
248
 * @param {import('umzug').MigrationParams<import('../Uwave').default>} params
1✔
249
 */
1✔
NEW
250
async function down() {}
×
251

1✔
252
module.exports = { up, down };
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