Create database CRUD operations for all modules (settings, search, downloads, music)

Co-authored-by: leinelissen <10154841+leinelissen@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-08 15:00:40 +00:00
co-authored by leinelissen
parent 81df4586fd
commit d5041e0f79
4 changed files with 946 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
import { db, sqliteDb } from '@/store/db';
import { downloads } from '@/store/db/schema/downloads';
import { eq } from 'drizzle-orm';
export interface Download {
sourceId: string;
id: string;
hash: string | null;
filename: string | null;
mimetype: string | null;
progress: number | null;
isFailed: boolean;
isComplete: boolean;
metadataJson: string | null;
createdAt: number;
updatedAt: number;
}
export interface DownloadMetadata {
size?: number;
error?: string;
}
/**
* Get all downloads for a source
*/
export async function getAllDownloads(sourceId: string): Promise<Download[]> {
const result = await db
.select()
.from(downloads)
.where(eq(downloads.sourceId, sourceId));
return result as Download[];
}
/**
* Get a single download by id
*/
export async function getDownload(id: string): Promise<Download | undefined> {
const result = await db
.select()
.from(downloads)
.where(eq(downloads.id, id))
.limit(1);
return result[0] as Download | undefined;
}
/**
* Initialize a download
*/
export async function initializeDownload(
sourceId: string,
id: string,
hash?: string,
filename?: string,
mimetype?: string
): Promise<void> {
const now = Date.now();
await db.insert(downloads).values({
sourceId,
id,
hash: hash || null,
filename: filename || null,
mimetype: mimetype || null,
progress: 0,
isFailed: false,
isComplete: false,
metadataJson: null,
createdAt: now,
updatedAt: now,
}).onConflictDoUpdate({
target: downloads.id,
set: {
hash: hash || null,
filename: filename || null,
mimetype: mimetype || null,
progress: 0,
isFailed: false,
isComplete: false,
updatedAt: now,
},
});
sqliteDb.flushPendingReactiveQueries();
}
/**
* Update download progress
*/
export async function updateDownloadProgress(
id: string,
progress: number,
metadata?: DownloadMetadata
): Promise<void> {
const updates: any = {
progress,
updatedAt: Date.now(),
};
if (metadata) {
updates.metadataJson = JSON.stringify(metadata);
}
await db.update(downloads)
.set(updates)
.where(eq(downloads.id, id));
sqliteDb.flushPendingReactiveQueries();
}
/**
* Mark download as complete
*/
export async function completeDownload(
id: string,
hash?: string,
filename?: string
): Promise<void> {
const updates: any = {
isComplete: true,
isFailed: false,
progress: 100,
updatedAt: Date.now(),
};
if (hash) updates.hash = hash;
if (filename) updates.filename = filename;
await db.update(downloads)
.set(updates)
.where(eq(downloads.id, id));
sqliteDb.flushPendingReactiveQueries();
}
/**
* Mark download as failed
*/
export async function failDownload(id: string, error?: string): Promise<void> {
const metadata = error ? JSON.stringify({ error }) : null;
await db.update(downloads)
.set({
isFailed: true,
isComplete: false,
progress: 0,
metadataJson: metadata,
updatedAt: Date.now(),
})
.where(eq(downloads.id, id));
sqliteDb.flushPendingReactiveQueries();
}
/**
* Remove a download
*/
export async function removeDownload(id: string): Promise<void> {
await db.delete(downloads).where(eq(downloads.id, id));
sqliteDb.flushPendingReactiveQueries();
}
/**
* Parse download metadata
*/
export function parseDownloadMetadata(download: Download): DownloadMetadata {
if (!download.metadataJson) {
return {};
}
try {
return JSON.parse(download.metadataJson);
} catch {
return {};
}
}
/**
* Get download with parsed metadata
*/
export interface DownloadWithMetadata extends Download {
size?: number;
error?: string;
}
export function enrichDownload(download: Download): DownloadWithMetadata {
const metadata = parseDownloadMetadata(download);
return {
...download,
...metadata,
};
}
+499
View File
@@ -0,0 +1,499 @@
import { db, sqliteDb } from '@/store/db';
import { albums } from '@/store/db/schema/albums';
import { artists } from '@/store/db/schema/artists';
import { tracks } from '@/store/db/schema/tracks';
import { playlists } from '@/store/db/schema/playlists';
import { albumArtists } from '@/store/db/schema/album-artists';
import { trackArtists } from '@/store/db/schema/track-artists';
import { playlistTracks } from '@/store/db/schema/playlist-tracks';
import { albumSimilar } from '@/store/db/schema/album-similar';
import { eq, and, desc, inArray } from 'drizzle-orm';
import type { Album, AlbumTrack, MusicArtist, Playlist } from './types';
/**
* ALBUMS
*/
export async function getAllAlbums(sourceId: string) {
const result = await db.select().from(albums).where(eq(albums.sourceId, sourceId));
return result.map(album => enrichAlbum(album));
}
export async function getAlbum(id: string) {
const result = await db.select().from(albums).where(eq(albums.id, id)).limit(1);
if (!result[0]) return undefined;
return enrichAlbum(result[0]);
}
export async function getRecentAlbums(sourceId: string, limit: number = 50) {
const result = await db
.select()
.from(albums)
.where(eq(albums.sourceId, sourceId))
.orderBy(desc(albums.dateCreated))
.limit(limit);
return result.map(album => enrichAlbum(album));
}
export async function upsertAlbum(sourceId: string, album: Album) {
const now = Date.now();
const metadata = extractAlbumMetadata(album);
await db.insert(albums).values({
sourceId,
id: album.Id,
name: album.Name,
productionYear: album.ProductionYear || null,
isFolder: album.IsFolder,
albumArtist: album.AlbumArtist || null,
dateCreated: album.DateCreated ? new Date(album.DateCreated).getTime() : now,
lastRefreshed: now,
metadataJson: JSON.stringify(metadata),
createdAt: now,
updatedAt: now,
}).onConflictDoUpdate({
target: albums.id,
set: {
name: album.Name,
productionYear: album.ProductionYear || null,
albumArtist: album.AlbumArtist || null,
lastRefreshed: now,
metadataJson: JSON.stringify(metadata),
updatedAt: now,
},
});
// Update album-artist relations
await updateAlbumArtists(sourceId, album.Id, album.AlbumArtists);
sqliteDb.flushPendingReactiveQueries();
}
export async function upsertAlbums(sourceId: string, albumList: Album[]) {
for (const album of albumList) {
await upsertAlbum(sourceId, album);
}
}
export async function getSimilarAlbums(albumId: string) {
const relations = await db
.select()
.from(albumSimilar)
.where(eq(albumSimilar.albumId, albumId));
if (relations.length === 0) return [];
const similarIds = relations.map(r => r.similarAlbumId);
const result = await db
.select()
.from(albums)
.where(inArray(albums.id, similarIds));
return result.map(album => enrichAlbum(album));
}
export async function setSimilarAlbums(sourceId: string, albumId: string, similarAlbumIds: string[]) {
// Delete existing relations
await db.delete(albumSimilar).where(eq(albumSimilar.albumId, albumId));
// Insert new relations
if (similarAlbumIds.length > 0) {
await db.insert(albumSimilar).values(
similarAlbumIds.map(similarId => ({
sourceId,
albumId,
similarAlbumId: similarId,
}))
);
}
sqliteDb.flushPendingReactiveQueries();
}
/**
* ARTISTS
*/
export async function getAllArtists(sourceId: string) {
const result = await db.select().from(artists).where(eq(artists.sourceId, sourceId));
return result.map(artist => enrichArtist(artist));
}
export async function upsertArtist(sourceId: string, artist: MusicArtist) {
const now = Date.now();
const metadata = extractArtistMetadata(artist);
await db.insert(artists).values({
sourceId,
id: artist.Id,
name: artist.Name,
isFolder: artist.IsFolder,
metadataJson: JSON.stringify(metadata),
createdAt: now,
updatedAt: now,
}).onConflictDoUpdate({
target: artists.id,
set: {
name: artist.Name,
metadataJson: JSON.stringify(metadata),
updatedAt: now,
},
});
sqliteDb.flushPendingReactiveQueries();
}
export async function upsertArtists(sourceId: string, artistList: MusicArtist[]) {
for (const artist of artistList) {
await upsertArtist(sourceId, artist);
}
}
/**
* TRACKS
*/
export async function getTracksByAlbum(albumId: string) {
const result = await db
.select()
.from(tracks)
.where(eq(tracks.albumId, albumId));
return result.map(track => enrichTrack(track));
}
export async function getTracksByPlaylist(playlistId: string) {
const relations = await db
.select()
.from(playlistTracks)
.where(eq(playlistTracks.playlistId, playlistId));
if (relations.length === 0) return [];
const trackIds = relations.map(r => r.trackId);
const result = await db
.select()
.from(tracks)
.where(inArray(tracks.id, trackIds));
// Sort by position in playlist
const trackMap = new Map(result.map(t => [t.id, t]));
return relations
.sort((a, b) => (a.position || 0) - (b.position || 0))
.map(r => trackMap.get(r.trackId))
.filter(Boolean)
.map(track => enrichTrack(track!));
}
export async function getTrack(id: string) {
const result = await db.select().from(tracks).where(eq(tracks.id, id)).limit(1);
if (!result[0]) return undefined;
return enrichTrack(result[0]);
}
export async function upsertTrack(sourceId: string, track: AlbumTrack) {
const now = Date.now();
const metadata = extractTrackMetadata(track);
await db.insert(tracks).values({
sourceId,
id: track.Id,
name: track.Name,
albumId: track.AlbumId || null,
album: track.Album || null,
albumArtist: track.AlbumArtist || null,
productionYear: track.ProductionYear || null,
indexNumber: track.IndexNumber || null,
parentIndexNumber: track.ParentIndexNumber || null,
hasLyrics: track.HasLyrics || false,
runTimeTicks: track.RunTimeTicks || null,
lyrics: track.Lyrics ? JSON.stringify(track.Lyrics) : null,
metadataJson: JSON.stringify(metadata),
createdAt: now,
updatedAt: now,
}).onConflictDoUpdate({
target: tracks.id,
set: {
name: track.Name,
album: track.Album || null,
albumArtist: track.AlbumArtist || null,
hasLyrics: track.HasLyrics || false,
lyrics: track.Lyrics ? JSON.stringify(track.Lyrics) : null,
metadataJson: JSON.stringify(metadata),
updatedAt: now,
},
});
// Update track-artist relations
await updateTrackArtists(sourceId, track.Id, track.ArtistItems);
sqliteDb.flushPendingReactiveQueries();
}
export async function upsertTracks(sourceId: string, trackList: AlbumTrack[]) {
for (const track of trackList) {
await upsertTrack(sourceId, track);
}
}
export async function updateTrackCodec(trackId: string, codec: any) {
const track = await getTrack(trackId);
if (!track) return;
const metadata = track.metadataJson ? JSON.parse(track.metadataJson) : {};
metadata.Codec = codec;
await db.update(tracks)
.set({
metadataJson: JSON.stringify(metadata),
updatedAt: Date.now(),
})
.where(eq(tracks.id, trackId));
sqliteDb.flushPendingReactiveQueries();
}
/**
* PLAYLISTS
*/
export async function getAllPlaylists(sourceId: string) {
const result = await db.select().from(playlists).where(eq(playlists.sourceId, sourceId));
return result.map(playlist => enrichPlaylist(playlist));
}
export async function getPlaylist(id: string) {
const result = await db.select().from(playlists).where(eq(playlists.id, id)).limit(1);
if (!result[0]) return undefined;
return enrichPlaylist(result[0]);
}
export async function upsertPlaylist(sourceId: string, playlist: Playlist) {
const now = Date.now();
const metadata = extractPlaylistMetadata(playlist);
await db.insert(playlists).values({
sourceId,
id: playlist.Id,
name: playlist.Name,
canDelete: playlist.CanDelete,
childCount: playlist.ChildCount || null,
lastRefreshed: now,
metadataJson: JSON.stringify(metadata),
createdAt: now,
updatedAt: now,
}).onConflictDoUpdate({
target: playlists.id,
set: {
name: playlist.Name,
canDelete: playlist.CanDelete,
childCount: playlist.ChildCount || null,
lastRefreshed: now,
metadataJson: JSON.stringify(metadata),
updatedAt: now,
},
});
sqliteDb.flushPendingReactiveQueries();
}
export async function upsertPlaylists(sourceId: string, playlistList: Playlist[]) {
for (const playlist of playlistList) {
await upsertPlaylist(sourceId, playlist);
}
}
export async function setPlaylistTracks(sourceId: string, playlistId: string, trackIds: string[]) {
// Delete existing relations
await db.delete(playlistTracks).where(
and(
eq(playlistTracks.sourceId, sourceId),
eq(playlistTracks.playlistId, playlistId)
)
);
// Insert new relations with positions
if (trackIds.length > 0) {
await db.insert(playlistTracks).values(
trackIds.map((trackId, index) => ({
sourceId,
playlistId,
trackId,
position: index,
}))
);
}
sqliteDb.flushPendingReactiveQueries();
}
/**
* HELPER FUNCTIONS
*/
async function updateAlbumArtists(sourceId: string, albumId: string, albumArtistsList: any[]) {
// Delete existing relations
await db.delete(albumArtists).where(
and(
eq(albumArtists.sourceId, sourceId),
eq(albumArtists.albumId, albumId)
)
);
// Insert new relations
if (albumArtistsList && albumArtistsList.length > 0) {
await db.insert(albumArtists).values(
albumArtistsList.map((artist, index) => ({
sourceId,
albumId,
artistId: artist.Id,
orderIndex: index,
}))
);
}
}
async function updateTrackArtists(sourceId: string, trackId: string, artistItems: any[]) {
// Delete existing relations
await db.delete(trackArtists).where(
and(
eq(trackArtists.sourceId, sourceId),
eq(trackArtists.trackId, trackId)
)
);
// Insert new relations
if (artistItems && artistItems.length > 0) {
await db.insert(trackArtists).values(
artistItems.map((artist, index) => ({
sourceId,
trackId,
artistId: artist.Id,
orderIndex: index,
}))
);
}
}
function extractAlbumMetadata(album: Album) {
return {
ServerId: album.ServerId,
SortName: album.SortName,
RunTimeTicks: album.RunTimeTicks,
Type: album.Type,
UserData: album.UserData,
PrimaryImageAspectRatio: album.PrimaryImageAspectRatio,
Artists: album.Artists,
ArtistItems: album.ArtistItems,
AlbumArtists: album.AlbumArtists,
ImageTags: album.ImageTags,
BackdropImageTags: album.BackdropImageTags,
LocationType: album.LocationType,
Overview: album.Overview,
PrimaryImageItemId: album.PrimaryImageItemId,
};
}
function enrichAlbum(dbAlbum: any): Album {
const metadata = dbAlbum.metadataJson ? JSON.parse(dbAlbum.metadataJson) : {};
return {
Id: dbAlbum.id,
Name: dbAlbum.name,
ProductionYear: dbAlbum.productionYear,
IsFolder: dbAlbum.isFolder,
AlbumArtist: dbAlbum.albumArtist,
DateCreated: dbAlbum.dateCreated ? new Date(dbAlbum.dateCreated).toISOString() : new Date().toISOString(),
lastRefreshed: dbAlbum.lastRefreshed,
...metadata,
};
}
function extractArtistMetadata(artist: MusicArtist) {
return {
ServerId: artist.ServerId,
ChannelId: artist.ChannelId,
RunTimeTicks: artist.RunTimeTicks,
Type: artist.Type,
UserData: artist.UserData,
ImageTags: artist.ImageTags,
BackdropImageTags: artist.BackdropImageTags,
ImageBlurHashes: artist.ImageBlurHashes,
LocationType: artist.LocationType,
Overview: artist.Overview,
};
}
function enrichArtist(dbArtist: any): MusicArtist {
const metadata = dbArtist.metadataJson ? JSON.parse(dbArtist.metadataJson) : {};
return {
Id: dbArtist.id,
Name: dbArtist.name,
IsFolder: dbArtist.isFolder,
...metadata,
};
}
function extractTrackMetadata(track: AlbumTrack) {
return {
ServerId: track.ServerId,
Type: track.Type,
UserData: track.UserData,
Artists: track.Artists,
ArtistItems: track.ArtistItems,
AlbumPrimaryImageTag: track.AlbumPrimaryImageTag,
AlbumArtists: track.AlbumArtists,
ImageTags: track.ImageTags,
BackdropImageTags: track.BackdropImageTags,
LocationType: track.LocationType,
MediaType: track.MediaType,
MediaStreams: track.MediaStreams,
Codec: track.Codec,
};
}
function enrichTrack(dbTrack: any): AlbumTrack {
const metadata = dbTrack.metadataJson ? JSON.parse(dbTrack.metadataJson) : {};
const lyrics = dbTrack.lyrics ? JSON.parse(dbTrack.lyrics) : undefined;
return {
Id: dbTrack.id,
Name: dbTrack.name,
AlbumId: dbTrack.albumId,
Album: dbTrack.album,
AlbumArtist: dbTrack.albumArtist,
ProductionYear: dbTrack.productionYear,
IndexNumber: dbTrack.indexNumber,
ParentIndexNumber: dbTrack.parentIndexNumber,
HasLyrics: dbTrack.hasLyrics,
RunTimeTicks: dbTrack.runTimeTicks,
Lyrics: lyrics,
...metadata,
};
}
function extractPlaylistMetadata(playlist: Playlist) {
return {
ServerId: playlist.ServerId,
SortName: playlist.SortName,
ChannelId: playlist.ChannelId,
RunTimeTicks: playlist.RunTimeTicks,
Type: playlist.Type,
UserData: playlist.UserData,
PrimaryImageAspectRatio: playlist.PrimaryImageAspectRatio,
ImageTags: playlist.ImageTags,
BackdropImageTags: playlist.BackdropImageTags,
LocationType: playlist.LocationType,
MediaType: playlist.MediaType,
};
}
function enrichPlaylist(dbPlaylist: any): Playlist {
const metadata = dbPlaylist.metadataJson ? JSON.parse(dbPlaylist.metadataJson) : {};
return {
Id: dbPlaylist.id,
Name: dbPlaylist.name,
CanDelete: dbPlaylist.canDelete,
ChildCount: dbPlaylist.childCount,
lastRefreshed: dbPlaylist.lastRefreshed,
...metadata,
};
}
+100
View File
@@ -0,0 +1,100 @@
import { db, sqliteDb } from '@/store/db';
import { searchQueries } from '@/store/db/schema/search-queries';
import { desc, eq } from 'drizzle-orm';
type SearchType = 'Audio' | 'MusicAlbum' | 'MusicArtist' | 'Playlist';
export interface SearchQuery {
sourceId: string;
id: string;
query: string;
timestamp: number;
localPlaybackOnly: boolean;
metadataJson: string | null;
createdAt: number;
updatedAt: number;
}
export interface SearchQueryDisplay {
query: string;
filters: SearchType[];
localPlaybackOnly: boolean;
timestamp: number;
}
/**
* Get search history for a source (ordered by timestamp desc, limit 10)
*/
export async function getSearchHistory(sourceId: string): Promise<SearchQuery[]> {
const result = await db
.select()
.from(searchQueries)
.where(eq(searchQueries.sourceId, sourceId))
.orderBy(desc(searchQueries.timestamp))
.limit(10);
return result as SearchQuery[];
}
/**
* Add a search query to history
*/
export async function addSearchQuery(
sourceId: string,
query: string,
filters: SearchType[],
localPlaybackOnly: boolean
): Promise<void> {
const now = Date.now();
const id = `${sourceId}-${query}-${filters.sort().join(',')}-${localPlaybackOnly}`;
const metadata = { filters };
// Delete existing query with same parameters (to move it to top)
await db.delete(searchQueries).where(eq(searchQueries.id, id));
// Insert new query
await db.insert(searchQueries).values({
sourceId,
id,
query,
timestamp: now,
localPlaybackOnly,
metadataJson: JSON.stringify(metadata),
createdAt: now,
updatedAt: now,
});
// Keep only last 10 queries for this source
const allQueries = await getSearchHistory(sourceId);
if (allQueries.length > 10) {
const idsToDelete = allQueries.slice(10).map(q => q.id);
for (const id of idsToDelete) {
await db.delete(searchQueries).where(eq(searchQueries.id, id));
}
}
sqliteDb.flushPendingReactiveQueries();
}
/**
* Clear all search history for a source
*/
export async function clearSearchHistory(sourceId: string): Promise<void> {
await db.delete(searchQueries).where(eq(searchQueries.sourceId, sourceId));
sqliteDb.flushPendingReactiveQueries();
}
/**
* Parse search queries to display format
*/
export function parseSearchQueries(queries: SearchQuery[]): SearchQueryDisplay[] {
return queries.map(q => {
const metadata = q.metadataJson ? JSON.parse(q.metadataJson) : { filters: [] };
return {
query: q.query,
filters: metadata.filters || [],
localPlaybackOnly: q.localPlaybackOnly,
timestamp: q.timestamp,
};
});
}
+154
View File
@@ -0,0 +1,154 @@
import { db, sqliteDb } from '@/store/db';
import { appSettings } from '@/store/db/schema/app-settings';
import { sources } from '@/store/db/schema/sources';
import { eq } from 'drizzle-orm';
import { ColorScheme } from './types';
export interface AppSettings {
id: number;
bitrate: number;
isOnboardingComplete: boolean;
hasReceivedErrorReportingAlert: boolean;
enablePlaybackReporting: boolean;
colorScheme: ColorScheme;
createdAt: number;
updatedAt: number;
}
export interface SourceCredentials {
id: string;
uri: string;
userId: string | null;
accessToken: string | null;
deviceId: string | null;
type: string;
}
/**
* Get app settings (single row, id=1)
*/
export async function getAppSettings(): Promise<AppSettings | undefined> {
const result = await db.select().from(appSettings).where(eq(appSettings.id, 1)).limit(1);
return result[0] as AppSettings | undefined;
}
/**
* Initialize app settings with defaults if not exists
*/
export async function initializeAppSettings(): Promise<AppSettings> {
const existing = await getAppSettings();
if (existing) {
return existing;
}
const now = Date.now();
const defaults: typeof appSettings.$inferInsert = {
id: 1,
bitrate: 140000000,
isOnboardingComplete: false,
hasReceivedErrorReportingAlert: false,
enablePlaybackReporting: true,
colorScheme: ColorScheme.System,
createdAt: now,
updatedAt: now,
};
await db.insert(appSettings).values(defaults);
sqliteDb.flushPendingReactiveQueries();
return defaults as AppSettings;
}
/**
* Update app settings
*/
export async function updateAppSettings(updates: Partial<Omit<AppSettings, 'id' | 'createdAt' | 'updatedAt'>>): Promise<void> {
await db.update(appSettings)
.set({ ...updates, updatedAt: Date.now() })
.where(eq(appSettings.id, 1));
sqliteDb.flushPendingReactiveQueries();
}
/**
* Set bitrate
*/
export async function setBitrate(bitrate: number): Promise<void> {
await updateAppSettings({ bitrate });
}
/**
* Set onboarding status
*/
export async function setOnboardingStatus(isOnboardingComplete: boolean): Promise<void> {
await updateAppSettings({ isOnboardingComplete });
}
/**
* Set error reporting alert received
*/
export async function setReceivedErrorReportingAlert(): Promise<void> {
await updateAppSettings({ hasReceivedErrorReportingAlert: true });
}
/**
* Set enable playback reporting
*/
export async function setEnablePlaybackReporting(enablePlaybackReporting: boolean): Promise<void> {
await updateAppSettings({ enablePlaybackReporting });
}
/**
* Set color scheme
*/
export async function setColorScheme(colorScheme: ColorScheme): Promise<void> {
await updateAppSettings({ colorScheme });
}
/**
* Get active source (credentials)
*/
export async function getActiveSource(): Promise<SourceCredentials | undefined> {
const result = await db.select().from(sources).limit(1);
return result[0] as SourceCredentials | undefined;
}
/**
* Set Jellyfin/Emby credentials
*/
export async function setCredentials(credentials: {
uri: string;
user_id: string;
access_token: string;
device_id: string;
type: 'jellyfin' | 'emby';
}): Promise<void> {
const now = Date.now();
const sourceType = credentials.type === 'jellyfin' ? 'jellyfin.v1' : 'emby.v1';
// Use device_id as the source id for consistency
const sourceId = credentials.device_id;
await db.insert(sources)
.values({
id: sourceId,
uri: credentials.uri,
userId: credentials.user_id,
accessToken: credentials.access_token,
deviceId: credentials.device_id,
type: sourceType,
createdAt: now,
updatedAt: now,
})
.onConflictDoUpdate({
target: sources.id,
set: {
uri: credentials.uri,
userId: credentials.user_id,
accessToken: credentials.access_token,
deviceId: credentials.device_id,
type: sourceType,
updatedAt: now,
},
});
sqliteDb.flushPendingReactiveQueries();
}