optimize song usage
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {of} from 'rxjs';
|
||||
import {ShowDataService} from './show-data.service';
|
||||
import {ShowSongDataService} from './show-song-data.service';
|
||||
import {ShowSongIndexService} from './show-song-index.service';
|
||||
import {UserSessionService} from '../../../services/user/user-session.service';
|
||||
import {User} from '../../../services/user/user';
|
||||
|
||||
describe('ShowSongIndexService', () => {
|
||||
let service: ShowSongIndexService;
|
||||
let showDataServiceSpy: jasmine.SpyObj<ShowDataService>;
|
||||
let showSongDataServiceSpy: jasmine.SpyObj<ShowSongDataService>;
|
||||
let sessionSpy: jasmine.SpyObj<UserSessionService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
showDataServiceSpy = jasmine.createSpyObj<ShowDataService>('ShowDataService', ['listRaw$', 'update']);
|
||||
showSongDataServiceSpy = jasmine.createSpyObj<ShowSongDataService>('ShowSongDataService', ['list$']);
|
||||
sessionSpy = jasmine.createSpyObj<UserSessionService>('UserSessionService', ['update$'], {
|
||||
user$: of({id: 'admin-1', name: 'Admin', role: 'admin', chordMode: 'onlyFirst', songUsage: {}} as User),
|
||||
});
|
||||
|
||||
showDataServiceSpy.listRaw$.and.returnValue(of([{id: 'show-1'}, {id: 'show-2'}] as never) as unknown as ReturnType<ShowDataService['listRaw$']>);
|
||||
showDataServiceSpy.update.and.resolveTo();
|
||||
showSongDataServiceSpy.list$.and.callFake((showId: string) => {
|
||||
if (showId === 'show-1') {
|
||||
return of([{songId: 'song-1'}, {songId: 'song-2'}, {songId: 'song-1'}] as never) as never;
|
||||
}
|
||||
|
||||
return of([{songId: 'song-3'}] as never) as never;
|
||||
});
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: ShowDataService, useValue: showDataServiceSpy},
|
||||
{provide: ShowSongDataService, useValue: showSongDataServiceSpy},
|
||||
{provide: UserSessionService, useValue: sessionSpy},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(ShowSongIndexService);
|
||||
});
|
||||
|
||||
it('should rebuild the distinct songIds index for all shows', async () => {
|
||||
await expectAsync(service.rebuildShowSongIds()).toBeResolvedTo({
|
||||
showsProcessed: 2,
|
||||
showSongsProcessed: 4,
|
||||
});
|
||||
|
||||
expect(showDataServiceSpy.update).toHaveBeenCalledWith('show-1', {songIds: ['song-1', 'song-2']});
|
||||
expect(showDataServiceSpy.update).toHaveBeenCalledWith('show-2', {songIds: ['song-3']});
|
||||
});
|
||||
|
||||
it('should reject index rebuilds for non-admin users', async () => {
|
||||
Object.defineProperty(sessionSpy, 'user$', {
|
||||
value: of({id: 'user-1', name: 'User', role: 'leader', chordMode: 'onlyFirst', songUsage: {}} as User),
|
||||
});
|
||||
|
||||
await expectAsync(service.rebuildShowSongIds()).toBeRejectedWithError('Admin role required to rebuild show song ids.');
|
||||
});
|
||||
});
|
||||
65
src/app/modules/shows/services/show-song-index.service.ts
Normal file
65
src/app/modules/shows/services/show-song-index.service.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {firstValueFrom} from 'rxjs';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {ShowDataService} from './show-data.service';
|
||||
import {ShowSongDataService} from './show-song-data.service';
|
||||
import {UserSessionService} from '../../../services/user/user-session.service';
|
||||
|
||||
export interface ShowSongIndexMigrationResult {
|
||||
showsProcessed: number;
|
||||
showSongsProcessed: number;
|
||||
}
|
||||
|
||||
export interface MigrationProgress {
|
||||
processed: number;
|
||||
total: number;
|
||||
showId: string;
|
||||
showSongsProcessed: number;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ShowSongIndexService {
|
||||
private session = inject(UserSessionService);
|
||||
private showDataService = inject(ShowDataService);
|
||||
private showSongDataService = inject(ShowSongDataService);
|
||||
|
||||
public async rebuildShowSongIds(onProgress?: (progress: MigrationProgress) => void): Promise<ShowSongIndexMigrationResult> {
|
||||
const currentUser = await firstValueFrom(this.session.user$.pipe(take(1)));
|
||||
if (!currentUser || !this.hasAdminRole(currentUser.role)) {
|
||||
throw new Error('Admin role required to rebuild show song ids.');
|
||||
}
|
||||
|
||||
const shows = await firstValueFrom(this.showDataService.listRaw$());
|
||||
let showSongsProcessed = 0;
|
||||
let processed = 0;
|
||||
|
||||
for (const show of shows) {
|
||||
const showSongs = await firstValueFrom(this.showSongDataService.list$(show.id));
|
||||
const songIds = [...new Set(showSongs.map(showSong => showSong.songId).filter(Boolean))];
|
||||
showSongsProcessed += showSongs.length;
|
||||
await this.showDataService.update(show.id, {songIds});
|
||||
processed += 1;
|
||||
onProgress?.({
|
||||
processed,
|
||||
total: shows.length,
|
||||
showId: show.id,
|
||||
showSongsProcessed,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
showsProcessed: shows.length,
|
||||
showSongsProcessed,
|
||||
};
|
||||
}
|
||||
|
||||
private hasAdminRole(role: string | null | undefined): boolean {
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return role.split(';').includes('admin');
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ describe('ShowSongService', () => {
|
||||
const show = {id: 'show-1', order: ['show-song-1', 'show-song-2']} as unknown as Show;
|
||||
|
||||
beforeEach(async () => {
|
||||
user$ = new BehaviorSubject<User | null>({id: 'user-1', name: 'Benjamin', role: 'editor', chordMode: 'letters', songUsage: {}});
|
||||
user$ = new BehaviorSubject<User | null>({id: 'user-1', name: 'Benjamin', role: 'editor', chordMode: 'onlyFirst', songUsage: {}});
|
||||
showSongDataServiceSpy = jasmine.createSpyObj<ShowSongDataService>('ShowSongDataService', ['add', 'read$', 'list$', 'delete', 'update$']);
|
||||
songDataServiceSpy = jasmine.createSpyObj<SongDataService>('SongDataService', ['read$']);
|
||||
userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['incSongCount', 'decSongCount'], {
|
||||
@@ -66,7 +66,7 @@ describe('ShowSongService', () => {
|
||||
songId: 'song-1',
|
||||
key: 'G',
|
||||
keyOriginal: 'G',
|
||||
chordMode: 'letters',
|
||||
chordMode: 'onlyFirst',
|
||||
addedLive: true,
|
||||
});
|
||||
});
|
||||
@@ -103,7 +103,7 @@ describe('ShowSongService', () => {
|
||||
await service.delete$('show-1', 'show-song-1', 0);
|
||||
|
||||
expect(showSongDataServiceSpy.delete).toHaveBeenCalledWith('show-1', 'show-song-1');
|
||||
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', {order: ['show-song-2']});
|
||||
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', jasmine.objectContaining({order: ['show-song-2']}));
|
||||
expect(userServiceSpy.decSongCount).toHaveBeenCalledWith('song-1');
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {ShowSong} from './show-song';
|
||||
import {SongDataService} from '../../songs/services/song-data.service';
|
||||
import {UserService} from '../../../services/user/user.service';
|
||||
import {ShowService} from './show.service';
|
||||
import {arrayRemove, arrayUnion} from '@angular/fire/firestore';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -27,8 +28,9 @@ export class ShowSongService {
|
||||
chordMode: user.chordMode,
|
||||
addedLive,
|
||||
};
|
||||
await this.userService.incSongCount(songId);
|
||||
return await this.showSongDataService.add(showId, data);
|
||||
const showSongId = await this.showSongDataService.add(showId, data);
|
||||
await Promise.all([this.userService.incSongCount(songId), this.showService.update$(showId, {songIds: arrayUnion(songId) as never})]);
|
||||
return showSongId;
|
||||
}
|
||||
|
||||
public read$ = (showId: string, songId: string): Observable<ShowSong | null> => this.showSongDataService.read$(showId, songId);
|
||||
@@ -38,14 +40,19 @@ export class ShowSongService {
|
||||
public list = (showId: string): Promise<ShowSong[]> => firstValueFrom(this.list$(showId));
|
||||
|
||||
public async delete$(showId: string, showSongId: string, index: number): Promise<void> {
|
||||
const [showSong, show] = await Promise.all([this.read(showId, showSongId), firstValueFrom(this.showService.read$(showId))]);
|
||||
const [showSong, show, showSongs] = await Promise.all([this.read(showId, showSongId), firstValueFrom(this.showService.read$(showId)), this.list(showId)]);
|
||||
if (!show) return;
|
||||
if (!showSong) return;
|
||||
|
||||
const order = [...show.order];
|
||||
order.splice(index, 1);
|
||||
const hasSameSongStillInShow = showSongs.some(song => song.id !== showSongId && song.songId === showSong.songId);
|
||||
|
||||
await Promise.all([this.showSongDataService.delete(showId, showSongId), this.showService.update$(showId, {order}), this.userService.decSongCount(showSong.songId)]);
|
||||
await Promise.all([
|
||||
this.showSongDataService.delete(showId, showSongId),
|
||||
this.showService.update$(showId, hasSameSongStillInShow ? {order} : {order, songIds: arrayRemove(showSong.songId) as never}),
|
||||
this.userService.decSongCount(showSong.songId),
|
||||
]);
|
||||
}
|
||||
|
||||
public update$ = async (showId: string, songId: string, data: Partial<ShowSong>): Promise<void> => await this.showSongDataService.update$(showId, songId, data);
|
||||
|
||||
@@ -16,9 +16,11 @@ describe('ShowService', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
user$ = new BehaviorSubject<unknown>({id: 'user-1'});
|
||||
showDataServiceSpy = jasmine.createSpyObj<ShowDataService>('ShowDataService', ['read$', 'listPublicSince$', 'update', 'add'], {
|
||||
list$: of(shows),
|
||||
});
|
||||
showDataServiceSpy = jasmine.createSpyObj<ShowDataService>(
|
||||
'ShowDataService',
|
||||
['read$', 'listPublicSince$', 'update', 'add'],
|
||||
{list$: of(shows) as unknown as ShowDataService['list$']}
|
||||
);
|
||||
showDataServiceSpy.read$.and.returnValue(of(shows[0]));
|
||||
showDataServiceSpy.listPublicSince$.and.returnValue(of([shows[1]]));
|
||||
showDataServiceSpy.update.and.resolveTo();
|
||||
@@ -97,6 +99,7 @@ describe('ShowService', () => {
|
||||
showType: type,
|
||||
owner: 'user-1',
|
||||
order: [],
|
||||
songIds: [],
|
||||
public: true,
|
||||
});
|
||||
});
|
||||
@@ -111,6 +114,7 @@ describe('ShowService', () => {
|
||||
showType: type,
|
||||
owner: 'user-1',
|
||||
order: [],
|
||||
songIds: [],
|
||||
public: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ export class ShowService {
|
||||
...data,
|
||||
owner: user.id,
|
||||
order: [],
|
||||
songIds: [],
|
||||
public: ShowService.SHOW_TYPE_PUBLIC.indexOf(data.showType) !== -1,
|
||||
};
|
||||
return await this.showDataService.add(calculatedData);
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface Show {
|
||||
showType: string;
|
||||
date: Timestamp;
|
||||
owner: string;
|
||||
songIds?: string[];
|
||||
public: boolean;
|
||||
reported: boolean;
|
||||
published: boolean;
|
||||
|
||||
Reference in New Issue
Block a user