Front: Api models validation (#245)

* Front: Model: Write Validators

* Front: Plage response validator

* Front: API: Typing 'fetch' return

* Front: Basic Models: Response Handlers

* Front: API: Validate authentication response

* Front: Validate Search History

* Front: Validate Responses of User updates

* Front: On Validation Error, more verbose console error
This commit is contained in:
Arthur Jamet
2023-07-05 09:22:55 +01:00
committed by GitHub
parent 350a4870cd
commit 10d1342294
20 changed files with 575 additions and 287 deletions
+13
View File
@@ -0,0 +1,13 @@
import * as yup from 'yup';
import ResponseHandler from './ResponseHandler';
const AccessTokenResponseValidator = yup.object({
access_token: yup.string().required(),
});
type AccessTokenResponse = yup.InferType<typeof AccessTokenResponseValidator>;
export const AccessTokenResponseHandler: ResponseHandler<AccessTokenResponse> = {
validator: AccessTokenResponseValidator,
transformer: (value) => value,
};
+9 -1
View File
@@ -1,4 +1,12 @@
import Model from './Model';
import Model, { ModelValidator } from './Model';
import * as yup from 'yup';
export const AlbumValidator = yup
.object({
name: yup.string().required(),
artistId: yup.number().required(),
})
.concat(ModelValidator);
interface Album extends Model {
name: string;
+14 -2
View File
@@ -1,8 +1,20 @@
import Model from './Model';
import Model, { ModelValidator } from './Model';
import * as yup from 'yup';
import ResponseHandler from './ResponseHandler';
export const ArtistValidator = yup
.object({
name: yup.string().required(),
})
.concat(ModelValidator);
export const ArtistHandler: ResponseHandler<Artist> = {
validator: ArtistValidator,
transformer: (value) => value,
};
interface Artist extends Model {
name: string;
picture?: string;
}
export default Artist;
+14 -1
View File
@@ -1,4 +1,17 @@
import Model from './Model';
import Model, { ModelValidator } from './Model';
import * as yup from 'yup';
import ResponseHandler from './ResponseHandler';
export const GenreValidator = yup
.object({
name: yup.string().required(),
})
.concat(ModelValidator);
export const GenreHandler: ResponseHandler<Genre> = {
validator: GenreValidator,
transformer: (value) => value,
};
interface Genre extends Model {
name: string;
+13 -20
View File
@@ -1,26 +1,19 @@
import Skill from './Skill';
import Model from './Model';
import { SkillValidator } from './Skill';
import { ModelValidator } from './Model';
import * as yup from 'yup';
export const LessonValidator = yup
.object({
name: yup.string().required(),
description: yup.string().required(),
requiredLevel: yup.number().required(),
mainSkill: SkillValidator.required(),
})
.concat(ModelValidator);
/**
* A Lesson is an exercice that the user can try to practice a skill
*/
interface Lesson extends Model {
/**
* The title of the lesson
*/
title: string;
/**
* Short description of the lesson
*/
description: string;
/**
* The minimum level required for the user to access this lesson
*/
requiredLevel: number;
/**
* The main skill learnt in this lesson
*/
mainSkill: Skill;
}
type Lesson = yup.InferType<typeof LessonValidator>;
export default Lesson;
+11
View File
@@ -0,0 +1,11 @@
import * as yup from 'yup';
import ResponseHandler from './ResponseHandler';
const ListValidator = <T>(itemType: yup.Schema<T>) => yup.array(itemType).required();
export const ListHandler = <A, R>(
itemHandler: ResponseHandler<A, R>
): ResponseHandler<A[], R[]> => ({
validator: ListValidator(itemHandler.validator),
transformer: (plage) => plage.map((item) => itemHandler.transformer(item)),
});
+7 -3
View File
@@ -1,5 +1,9 @@
interface Model {
id: number;
}
import * as yup from 'yup';
export const ModelValidator = yup.object({
id: yup.number().required(),
});
type Model = yup.InferType<typeof ModelValidator>;
export default Model;
+34
View File
@@ -0,0 +1,34 @@
import * as yup from 'yup';
import ResponseHandler from './ResponseHandler';
// Ty https://github.com/Arthi-chaud/Meelo/blob/master/front/src/models/pagination.ts
export const PlageValidator = <T>(itemType: yup.Schema<T>) =>
yup.object({
data: yup.array(itemType).required(),
metadata: yup.object({
/**
* Current route
*/
this: yup.string().required(),
/**
* route to use for the next items
*/
next: yup.string().required().nullable(),
/**
* route to use for the previous items
*/
previous: yup.string().required().nullable(),
}),
});
type Plage<T> = yup.InferType<ReturnType<typeof PlageValidator<T>>>;
export const PlageHandler = <A, R>(
itemHandler: ResponseHandler<A, R>
): ResponseHandler<Plage<A>, Plage<R>> => ({
validator: PlageValidator(itemHandler.validator),
transformer: (plage) => ({
...plage,
data: plage.data.map((item) => itemHandler.transformer(item)),
}),
});
+8
View File
@@ -0,0 +1,8 @@
import * as yup from 'yup';
type ResponseHandler<APIType, ModelType = APIType> = {
validator: yup.Schema<APIType>;
transformer: (value: APIType) => ModelType;
};
export default ResponseHandler;
+26 -1
View File
@@ -1,4 +1,29 @@
import Model from './Model';
import Model, { ModelValidator } from './Model';
import * as yup from 'yup';
import ResponseHandler from './ResponseHandler';
export const SearchType = ['song', 'artist', 'album'] as const;
export type SearchType = (typeof SearchType)[number];
const SearchHistoryValidator = yup
.object({
query: yup.string().required(),
type: yup.mixed<SearchType>().oneOf(SearchType).required(),
userId: yup.number().required(),
searchDate: yup.date().required(),
})
.concat(ModelValidator);
export const SearchHistoryHandler: ResponseHandler<
yup.InferType<typeof SearchHistoryValidator>,
SearchHistory
> = {
validator: SearchHistoryValidator,
transformer: (value) => ({
...value,
timestamp: value.searchDate,
}),
};
interface SearchHistory extends Model {
query: string;
+19 -13
View File
@@ -1,15 +1,21 @@
type Skill =
| 'rhythm'
| 'two-hands'
| 'combos'
| 'arpeggio'
| 'distance'
| 'left-hand'
| 'right-hand'
| 'lead-head-change'
| 'chord-complexity'
| 'chord-timing'
| 'pedal'
| 'precision';
import * as yup from 'yup';
const Skills = [
'rhythm',
'two-hands',
'combos',
'arpeggio',
'distance',
'left-hand',
'right-hand',
'lead-head-change',
'chord-complexity',
'chord-timing',
'pedal',
'precision',
] as const;
type Skill = (typeof Skills)[number];
export const SkillValidator = yup.mixed<Skill>().oneOf(Skills);
export default Skill;
+31 -2
View File
@@ -1,6 +1,35 @@
import Model from './Model';
import SongDetails from './SongDetails';
import Model, { ModelValidator } from './Model';
import SongDetails, { SongDetailsHandler, SongDetailsValidator } from './SongDetails';
import Artist from './Artist';
import * as yup from 'yup';
import ResponseHandler from './ResponseHandler';
import API from '../API';
export const SongValidator = yup
.object({
name: yup.string().required(),
midiPath: yup.string().required(),
musicXmlPath: yup.string().required(),
artistId: yup.number().required(),
albumId: yup.number().required().nullable(),
genreId: yup.number().required().nullable(),
difficulties: SongDetailsValidator.required(),
illustrationPath: yup.string().required(),
})
.concat(ModelValidator);
export const SongHandler: ResponseHandler<yup.InferType<typeof SongValidator>, Song> = {
validator: SongValidator,
transformer: (song) => ({
id: song.id,
name: song.name,
artistId: song.artistId,
albumId: song.albumId,
genreId: song.genreId,
details: SongDetailsHandler.transformer(song.difficulties),
cover: `${API.baseUrl}/song/${song.id}/illustration`,
}),
};
interface Song extends Model {
id: number;
+29 -2
View File
@@ -1,7 +1,34 @@
import * as yup from 'yup';
import ResponseHandler from './ResponseHandler';
export const SongDetailsValidator = yup.object({
length: yup.number().required(),
rhythm: yup.number().required(),
arpeggio: yup.number().required(),
distance: yup.number().required(),
lefthand: yup.number().required(),
twohands: yup.number().required(),
notecombo: yup.number().required(),
precision: yup.number().required(),
righthand: yup.number().required(),
pedalpoint: yup.number().required(),
chordtiming: yup.number().required(),
leadhandchange: yup.number().required(),
chordcomplexity: yup.number().required(),
});
export const SongDetailsHandler: ResponseHandler<
yup.InferType<typeof SongDetailsValidator>,
SongDetails
> = {
validator: SongDetailsValidator,
transformer: (value) => value,
};
interface SongDetails {
length: number;
rhythm: number;
arppegio: number;
arpeggio: number;
distance: number;
lefthand: number;
righthand: number;
@@ -10,7 +37,7 @@ interface SongDetails {
precision: number;
pedalpoint: number;
chordtiming: number;
leadheadchange: number;
leadhandchange: number;
chordcomplexity: number;
}
+39 -3
View File
@@ -1,8 +1,44 @@
interface SongHistory {
import * as yup from 'yup';
import ResponseHandler from './ResponseHandler';
export const SongHistoryItemValidator = yup.object({
songID: yup.number().required(),
userID: yup.number().required(),
score: yup.number().required(),
difficulties: yup.mixed().required(),
});
export const SongHistoryItemHandler: ResponseHandler<
yup.InferType<typeof SongHistoryItemValidator>,
SongHistoryItem
> = {
validator: SongHistoryItemValidator,
transformer: (value) => ({
...value,
difficulties: value.difficulties,
}),
};
export const SongHistoryValidator = yup.object({
best: yup.number().required().nullable(),
history: yup.array(SongHistoryItemValidator).required(),
});
export type SongHistory = yup.InferType<typeof SongHistoryValidator>;
export const SongHistoryHandler: ResponseHandler<SongHistory> = {
validator: SongHistoryValidator,
transformer: (value) => ({
...value,
history: value.history.map((item) => SongHistoryItemHandler.transformer(item)),
}),
};
export type SongHistoryItem = {
songID: number;
userID: number;
score: number;
difficulties: JSON;
}
difficulties: object;
};
export default SongHistory;
+35 -4
View File
@@ -1,6 +1,31 @@
import UserData from './UserData';
import Model from './Model';
import UserSettings from './UserSettings';
import Model, { ModelValidator } from './Model';
import * as yup from 'yup';
import ResponseHandler from './ResponseHandler';
export const UserValidator = yup
.object({
username: yup.string().required(),
password: yup.string().required(),
email: yup.string().required(),
isGuest: yup.boolean().required(),
partyPlayed: yup.number().required(),
})
.concat(ModelValidator);
export const UserHandler: ResponseHandler<yup.InferType<typeof UserValidator>, User> = {
validator: UserValidator,
transformer: (value) => ({
...value,
name: value.username,
premium: false,
data: {
gamesPlayed: value.partyPlayed as number,
xp: 0,
createdAt: new Date('2023-04-09T00:00:00.000Z'),
avatar: 'https://imgs.search.brave.com/RnQpFhmAFvuQsN_xTw7V-CN61VeHDBg2tkEXnKRYHAE/rs:fit:768:512:1/g:ce/aHR0cHM6Ly96b29h/c3Ryby5jb20vd3At/Y29udGVudC91cGxv/YWRzLzIwMjEvMDIv/Q2FzdG9yLTc2OHg1/MTIuanBn',
},
}),
};
interface User extends Model {
name: string;
@@ -8,7 +33,13 @@ interface User extends Model {
isGuest: boolean;
premium: boolean;
data: UserData;
settings: UserSettings;
}
interface UserData {
gamesPlayed: number;
xp: number;
avatar: string | undefined;
createdAt: Date;
}
export default User;
-8
View File
@@ -1,8 +0,0 @@
interface UserData {
gamesPlayed: number;
xp: number;
avatar: string | undefined;
createdAt: Date;
}
export default UserData;
+37
View File
@@ -1,3 +1,40 @@
import * as yup from 'yup';
import { ModelValidator } from './Model';
import ResponseHandler from './ResponseHandler';
export const UserSettingsValidator = yup
.object({
userId: yup.number().required(),
pushNotification: yup.boolean().required(),
emailNotification: yup.boolean().required(),
trainingNotification: yup.boolean().required(),
newSongNotification: yup.boolean().required(),
recommendations: yup.boolean().required(),
weeklyReport: yup.boolean().required(),
leaderBoard: yup.boolean().required(),
showActivity: yup.boolean().required(),
})
.concat(ModelValidator);
export const UserSettingsHandler: ResponseHandler<
yup.InferType<typeof UserSettingsValidator>,
UserSettings
> = {
validator: UserSettingsValidator,
transformer: (settings) => ({
notifications: {
pushNotif: settings.pushNotification,
emailNotif: settings.emailNotification,
trainNotif: settings.trainingNotification,
newSongNotif: settings.newSongNotification,
},
recommendations: settings.recommendations,
weeklyReport: settings.weeklyReport,
leaderBoard: settings.leaderBoard,
showActivity: settings.showActivity,
}),
};
interface UserSettings {
notifications: {
pushNotif: boolean;