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:
414
front/API.ts
414
front/API.ts
@@ -1,21 +1,26 @@
|
||||
import Artist from './models/Artist';
|
||||
import Artist, { ArtistHandler } from './models/Artist';
|
||||
import Album from './models/Album';
|
||||
import Chapter from './models/Chapter';
|
||||
import Lesson from './models/Lesson';
|
||||
import Genre from './models/Genre';
|
||||
import Genre, { GenreHandler } from './models/Genre';
|
||||
import LessonHistory from './models/LessonHistory';
|
||||
import Song from './models/Song';
|
||||
import SongHistory from './models/SongHistory';
|
||||
import User from './models/User';
|
||||
import Song, { SongHandler } from './models/Song';
|
||||
import { SongHistoryHandler, SongHistoryItem, SongHistoryItemHandler } from './models/SongHistory';
|
||||
import User, { UserHandler } from './models/User';
|
||||
import Constants from 'expo-constants';
|
||||
import store from './state/Store';
|
||||
import { Platform } from 'react-native';
|
||||
import { en } from './i18n/Translations';
|
||||
import UserSettings from './models/UserSettings';
|
||||
import { PartialDeep } from 'type-fest';
|
||||
import SearchHistory from './models/SearchHistory';
|
||||
import UserSettings, { UserSettingsHandler } from './models/UserSettings';
|
||||
import { PartialDeep, RequireExactlyOne } from 'type-fest';
|
||||
import SearchHistory, { SearchHistoryHandler } from './models/SearchHistory';
|
||||
import { Query } from './Queries';
|
||||
import CompetenciesTable from './components/CompetenciesTable';
|
||||
import ResponseHandler from './models/ResponseHandler';
|
||||
import { PlageHandler } from './models/Plage';
|
||||
import { ListHandler } from './models/List';
|
||||
import { AccessTokenResponseHandler } from './models/AccessTokenResponse';
|
||||
import * as yup from 'yup';
|
||||
|
||||
type AuthenticationInput = { username: string; password: string };
|
||||
type RegistrationInput = AuthenticationInput & { email: string };
|
||||
@@ -26,10 +31,14 @@ type FetchParams = {
|
||||
route: string;
|
||||
body?: object;
|
||||
method?: 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'PUT';
|
||||
// If true, No JSON parsing is done, the raw response's content is returned
|
||||
raw?: true;
|
||||
};
|
||||
|
||||
type HandleParams<APIType = unknown, ModelType = APIType> = RequireExactlyOne<{
|
||||
raw: true;
|
||||
emptyResponse: true;
|
||||
handler: ResponseHandler<APIType, ModelType>;
|
||||
}>;
|
||||
|
||||
// This Exception is intended to cover all business logic errors (invalid credentials, couldn't find a song, etc.)
|
||||
// technical errors (network, server, etc.) should be handled as standard Error exceptions
|
||||
// it helps to filter errors in the catch block, APIErrors messages should
|
||||
@@ -46,37 +55,66 @@ export class APIError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
// we will need the same thing for the scorometer API url
|
||||
const baseAPIUrl =
|
||||
process.env.NODE_ENV != 'development' && Platform.OS === 'web'
|
||||
? '/api'
|
||||
: Constants.manifest?.extra?.apiUrl;
|
||||
export class ValidationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export default class API {
|
||||
public static async fetch(params: FetchParams) {
|
||||
public static readonly baseUrl =
|
||||
process.env.NODE_ENV != 'development' && Platform.OS === 'web'
|
||||
? '/api'
|
||||
: Constants.manifest?.extra?.apiUrl;
|
||||
public static async fetch(
|
||||
params: FetchParams,
|
||||
handle: Pick<Required<HandleParams>, 'raw'>
|
||||
): Promise<ArrayBuffer>;
|
||||
public static async fetch(
|
||||
params: FetchParams,
|
||||
handle: Pick<Required<HandleParams>, 'emptyResponse'>
|
||||
): Promise<void>;
|
||||
public static async fetch<A, R>(
|
||||
params: FetchParams,
|
||||
handle: Pick<Required<HandleParams<A, R>>, 'handler'>
|
||||
): Promise<R>;
|
||||
public static async fetch(params: FetchParams): Promise<void>;
|
||||
public static async fetch(params: FetchParams, handle?: HandleParams) {
|
||||
const jwtToken = store.getState().user.accessToken;
|
||||
const header = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const response = await fetch(`${baseAPIUrl}${params.route}`, {
|
||||
const response = await fetch(`${API.baseUrl}${params.route}`, {
|
||||
headers: (jwtToken && { ...header, Authorization: `Bearer ${jwtToken}` }) || header,
|
||||
body: JSON.stringify(params.body),
|
||||
method: params.method ?? 'GET',
|
||||
}).catch(() => {
|
||||
throw new Error('Error while fetching API: ' + baseAPIUrl);
|
||||
throw new Error('Error while fetching API: ' + API.baseUrl);
|
||||
});
|
||||
if (params.raw) {
|
||||
if (!handle || handle.emptyResponse) {
|
||||
return;
|
||||
}
|
||||
if (handle.raw) {
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
const handler = handle.handler;
|
||||
const body = await response.text();
|
||||
try {
|
||||
const jsonResponse = body.length != 0 ? JSON.parse(body) : {};
|
||||
const jsonResponse = JSON.parse(body);
|
||||
if (!response.ok) {
|
||||
throw new APIError(response.statusText ?? body, response.status);
|
||||
}
|
||||
return jsonResponse;
|
||||
const validated = await handler.validator.validate(jsonResponse).catch((e) => {
|
||||
if (e instanceof yup.ValidationError) {
|
||||
console.error(e, 'Got: ' + body);
|
||||
throw new ValidationError(e.message);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
return handler.transformer(handler.validator.cast(validated));
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) throw new Error("Error while parsing Server's response");
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -84,17 +122,21 @@ export default class API {
|
||||
public static async authenticate(
|
||||
authenticationInput: AuthenticationInput
|
||||
): Promise<AccessToken> {
|
||||
return API.fetch({
|
||||
route: '/auth/login',
|
||||
body: authenticationInput,
|
||||
method: 'POST',
|
||||
})
|
||||
return API.fetch(
|
||||
{
|
||||
route: '/auth/login',
|
||||
body: authenticationInput,
|
||||
method: 'POST',
|
||||
},
|
||||
{ handler: AccessTokenResponseHandler }
|
||||
)
|
||||
.then((responseBody) => responseBody.access_token)
|
||||
.catch((e) => {
|
||||
if (!(e instanceof APIError)) throw e;
|
||||
|
||||
/// If validation fails, it means that auth failed.
|
||||
/// We want that 401 error to be thrown, instead of the plain validation vone
|
||||
if (e.status == 401)
|
||||
throw new APIError('invalidCredentials', 401, 'invalidCredentials');
|
||||
if (!(e instanceof APIError)) throw e;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
@@ -118,12 +160,20 @@ export default class API {
|
||||
}
|
||||
|
||||
public static async createAndGetGuestAccount(): Promise<AccessToken> {
|
||||
const response = await API.fetch({
|
||||
route: '/auth/guest',
|
||||
method: 'POST',
|
||||
});
|
||||
if (!response.access_token) throw new APIError('No access token', response.status);
|
||||
return response.access_token;
|
||||
return API.fetch(
|
||||
{
|
||||
route: '/auth/guest',
|
||||
method: 'POST',
|
||||
},
|
||||
{ handler: AccessTokenResponseHandler }
|
||||
)
|
||||
.then(({ access_token }) => access_token)
|
||||
.catch((e) => {
|
||||
if (e.status == 401)
|
||||
throw new APIError('invalidCredentials', 401, 'invalidCredentials');
|
||||
if (!(e instanceof APIError)) throw e;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
public static async transformGuestToUser(registrationInput: RegistrationInput): Promise<void> {
|
||||
@@ -140,50 +190,28 @@ export default class API {
|
||||
public static getUserInfo(): Query<User> {
|
||||
return {
|
||||
key: 'user',
|
||||
exec: async () => {
|
||||
const user = await API.fetch({
|
||||
route: '/auth/me',
|
||||
});
|
||||
|
||||
// this a dummy settings object, we will need to fetch the real one from the API
|
||||
return {
|
||||
id: user.id as number,
|
||||
name: (user.username ?? user.name) as string,
|
||||
email: user.email as string,
|
||||
premium: false,
|
||||
isGuest: user.isGuest as boolean,
|
||||
data: {
|
||||
gamesPlayed: user.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',
|
||||
exec: async () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: '/auth/me',
|
||||
},
|
||||
} as User;
|
||||
},
|
||||
{ handler: UserHandler }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
public static getUserSettings(): Query<UserSettings> {
|
||||
return {
|
||||
key: 'settings',
|
||||
exec: async () => {
|
||||
const settings = await API.fetch({
|
||||
route: '/auth/me/settings',
|
||||
});
|
||||
|
||||
return {
|
||||
notifications: {
|
||||
pushNotif: settings.pushNotification,
|
||||
emailNotif: settings.emailNotification,
|
||||
trainNotif: settings.trainingNotification,
|
||||
newSongNotif: settings.newSongNotification,
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: '/auth/me/settings',
|
||||
},
|
||||
recommendations: settings.recommendations,
|
||||
weeklyReport: settings.weeklyReport,
|
||||
leaderBoard: settings.leaderBoard,
|
||||
showActivity: settings.showActivity,
|
||||
};
|
||||
},
|
||||
{
|
||||
handler: UserSettingsHandler,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -222,28 +250,15 @@ export default class API {
|
||||
public static getAllSongs(): Query<Song[]> {
|
||||
return {
|
||||
key: 'songs',
|
||||
exec: async () => {
|
||||
const songs = await API.fetch({
|
||||
route: '/song',
|
||||
});
|
||||
|
||||
// this is a dummy illustration, we will need to fetch the real one from the API
|
||||
return songs.data.map(
|
||||
// To be fixed with #168
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(song: any) =>
|
||||
({
|
||||
id: song.id as number,
|
||||
name: song.name as string,
|
||||
artistId: song.artistId as number,
|
||||
albumId: song.albumId as number,
|
||||
genreId: song.genreId as number,
|
||||
details: song.difficulties,
|
||||
cover: `${baseAPIUrl}/song/${song.id}/illustration`,
|
||||
metrics: {},
|
||||
} as Song)
|
||||
);
|
||||
},
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: '/song',
|
||||
},
|
||||
{
|
||||
handler: PlageHandler(SongHandler),
|
||||
}
|
||||
).then(({ data }) => data),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -254,22 +269,13 @@ export default class API {
|
||||
public static getSong(songId: number): Query<Song> {
|
||||
return {
|
||||
key: ['song', songId],
|
||||
exec: async () => {
|
||||
const song = await API.fetch({
|
||||
route: `/song/${songId}`,
|
||||
});
|
||||
|
||||
// this is a dummy illustration, we will need to fetch the real one from the API
|
||||
return {
|
||||
id: song.id as number,
|
||||
name: song.name as string,
|
||||
artistId: song.artistId as number,
|
||||
albumId: song.albumId as number,
|
||||
genreId: song.genreId as number,
|
||||
details: song.difficulties,
|
||||
cover: `${baseAPIUrl}/song/${song.id}/illustration`,
|
||||
} as Song;
|
||||
},
|
||||
exec: async () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: `/song/${songId}`,
|
||||
},
|
||||
{ handler: SongHandler }
|
||||
),
|
||||
};
|
||||
}
|
||||
/**
|
||||
@@ -280,10 +286,14 @@ export default class API {
|
||||
return {
|
||||
key: ['midi', songId],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/song/${songId}/midi`,
|
||||
raw: true,
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: `/song/${songId}/midi`,
|
||||
},
|
||||
{
|
||||
raw: true,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -292,7 +302,7 @@ export default class API {
|
||||
* @param songId the id to find the song
|
||||
*/
|
||||
public static getArtistIllustration(artistId: number): string {
|
||||
return `${baseAPIUrl}/artist/${artistId}/illustration`;
|
||||
return `${API.baseUrl}/artist/${artistId}/illustration`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,7 +310,7 @@ export default class API {
|
||||
* @param songId the id to find the song
|
||||
*/
|
||||
public static getGenreIllustration(genreId: number): string {
|
||||
return `${baseAPIUrl}/genre/${genreId}/illustration`;
|
||||
return `${API.baseUrl}/genre/${genreId}/illustration`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,10 +321,12 @@ export default class API {
|
||||
return {
|
||||
key: ['musixml', songId],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/song/${songId}/musicXml`,
|
||||
raw: true,
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: `/song/${songId}/musicXml`,
|
||||
},
|
||||
{ raw: true }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -325,9 +337,12 @@ export default class API {
|
||||
return {
|
||||
key: ['artist', artistId],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/artist/${artistId}`,
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: `/artist/${artistId}`,
|
||||
},
|
||||
{ handler: ArtistHandler }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -356,13 +371,16 @@ export default class API {
|
||||
* Retrieve a song's play history
|
||||
* @param songId the id to find the song
|
||||
*/
|
||||
public static getSongHistory(songId: number): Query<{ best: number; history: SongHistory[] }> {
|
||||
public static getSongHistory(songId: number) {
|
||||
return {
|
||||
key: ['song', 'history', songId],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/song/${songId}/history`,
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: `/song/${songId}/history`,
|
||||
},
|
||||
{ handler: SongHistoryHandler }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -374,9 +392,12 @@ export default class API {
|
||||
return {
|
||||
key: ['search', 'song', query],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/search/songs/${query}`,
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: `/search/songs/${query}`,
|
||||
},
|
||||
{ handler: ListHandler(SongHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -388,9 +409,12 @@ export default class API {
|
||||
return {
|
||||
key: ['search', 'artist', query],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/search/artists/${query}`,
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: `/search/artists/${query}`,
|
||||
},
|
||||
{ handler: ListHandler(ArtistHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -398,31 +422,27 @@ export default class API {
|
||||
* Search Album by name
|
||||
* @param query the string used to find the album
|
||||
*/
|
||||
public static searchAlbum(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
query: string
|
||||
): Query<Album[]> {
|
||||
public static searchAlbum(query: string): Query<Album[]> {
|
||||
return {
|
||||
key: ['search', 'album', query],
|
||||
exec: async () =>
|
||||
[
|
||||
{
|
||||
id: 1,
|
||||
name: 'Super Trooper',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Kingdom Heart 365/2 OST',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'The Legend Of Zelda Ocarina Of Time OST',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Random Access Memories',
|
||||
},
|
||||
] as Album[],
|
||||
exec: async () => [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Super Trooper',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Kingdom Heart 365/2 OST',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'The Legend Of Zelda Ocarina Of Time OST',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Random Access Memories',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -433,9 +453,12 @@ export default class API {
|
||||
return {
|
||||
key: ['search', 'genre', query],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/search/genres/${query}`,
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: `/search/genres/${query}`,
|
||||
},
|
||||
{ handler: ListHandler(GenreHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -447,7 +470,7 @@ export default class API {
|
||||
return {
|
||||
key: ['lesson', lessonId],
|
||||
exec: async () => ({
|
||||
title: 'Song',
|
||||
name: 'Song',
|
||||
description: 'A song',
|
||||
requiredLevel: 1,
|
||||
mainSkill: 'lead-head-change',
|
||||
@@ -466,22 +489,12 @@ export default class API {
|
||||
return {
|
||||
key: ['search', 'history', 'skip', skip, 'take', take],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/history/search?skip=${skip ?? 0}&take=${take ?? 5}`,
|
||||
method: 'GET',
|
||||
}).then((value) =>
|
||||
value.map(
|
||||
// To be fixed with #168
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(e: any) =>
|
||||
({
|
||||
id: e.id,
|
||||
query: e.query,
|
||||
type: e.type,
|
||||
userId: e.userId,
|
||||
timestamp: new Date(e.searchDate),
|
||||
} as SearchHistory)
|
||||
)
|
||||
API.fetch(
|
||||
{
|
||||
route: `/history/search?skip=${skip ?? 0}&take=${take ?? 5}`,
|
||||
method: 'GET',
|
||||
},
|
||||
{ handler: ListHandler(SearchHistoryHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -516,13 +529,16 @@ export default class API {
|
||||
* Retrieve the authenticated user's play history
|
||||
* * @returns an array of songs
|
||||
*/
|
||||
public static getUserPlayHistory(): Query<SongHistory[]> {
|
||||
public static getUserPlayHistory(): Query<SongHistoryItem[]> {
|
||||
return {
|
||||
key: ['history'],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: '/history',
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: '/history',
|
||||
},
|
||||
{ handler: ListHandler(SongHistoryItemHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -543,36 +559,32 @@ export default class API {
|
||||
}
|
||||
|
||||
public static async updateUserEmail(newEmail: string): Promise<User> {
|
||||
const rep = await API.fetch({
|
||||
route: '/auth/me',
|
||||
method: 'PUT',
|
||||
body: {
|
||||
email: newEmail,
|
||||
return API.fetch(
|
||||
{
|
||||
route: '/auth/me',
|
||||
method: 'PUT',
|
||||
body: {
|
||||
email: newEmail,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (rep.error) {
|
||||
throw new Error(rep.error);
|
||||
}
|
||||
return rep;
|
||||
{ handler: UserHandler }
|
||||
);
|
||||
}
|
||||
|
||||
public static async updateUserPassword(
|
||||
oldPassword: string,
|
||||
newPassword: string
|
||||
): Promise<User> {
|
||||
const rep = await API.fetch({
|
||||
route: '/auth/me',
|
||||
method: 'PUT',
|
||||
body: {
|
||||
oldPassword: oldPassword,
|
||||
password: newPassword,
|
||||
return API.fetch(
|
||||
{
|
||||
route: '/auth/me',
|
||||
method: 'PUT',
|
||||
body: {
|
||||
oldPassword: oldPassword,
|
||||
password: newPassword,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (rep.error) {
|
||||
throw new Error(rep.error);
|
||||
}
|
||||
return rep;
|
||||
{ handler: UserHandler }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
13
front/models/AccessTokenResponse.ts
Normal file
13
front/models/AccessTokenResponse.ts
Normal 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,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
front/models/List.ts
Normal file
11
front/models/List.ts
Normal 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)),
|
||||
});
|
||||
@@ -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
front/models/Plage.ts
Normal file
34
front/models/Plage.ts
Normal 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
front/models/ResponseHandler.ts
Normal file
8
front/models/ResponseHandler.ts
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
interface UserData {
|
||||
gamesPlayed: number;
|
||||
xp: number;
|
||||
avatar: string | undefined;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export default UserData;
|
||||
@@ -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;
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"soundfont-player": "^0.12.0",
|
||||
"standardized-audio-context": "^25.3.51",
|
||||
"type-fest": "^3.6.0",
|
||||
"yup": "^0.32.11"
|
||||
"yup": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.3",
|
||||
@@ -87,11 +87,11 @@
|
||||
"@types/react": "~18.0.24",
|
||||
"@types/react-native": "~0.70.6",
|
||||
"@types/react-navigation": "^3.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"babel-loader": "^8.3.0",
|
||||
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
|
||||
"chromatic": "^6.14.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
|
||||
@@ -1232,7 +1232,7 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
version "7.20.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b"
|
||||
integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==
|
||||
@@ -4743,7 +4743,7 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/lodash@^4.14.167", "@types/lodash@^4.14.175":
|
||||
"@types/lodash@^4.14.167":
|
||||
version "4.14.191"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
|
||||
integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
|
||||
@@ -12591,11 +12591,6 @@ locate-path@^6.0.0:
|
||||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash-es@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||
|
||||
lodash.clone@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6"
|
||||
@@ -13705,11 +13700,6 @@ nano-time@1.0.0:
|
||||
dependencies:
|
||||
big-integer "^1.6.16"
|
||||
|
||||
nanoclone@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4"
|
||||
integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==
|
||||
|
||||
nanoid@^3.1.23, nanoid@^3.3.1:
|
||||
version "3.3.4"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
|
||||
@@ -15423,7 +15413,7 @@ prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
property-expr@^2.0.4:
|
||||
property-expr@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4"
|
||||
integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==
|
||||
@@ -17931,6 +17921,11 @@ timsort@^0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||
integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==
|
||||
|
||||
tiny-case@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03"
|
||||
integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==
|
||||
|
||||
tinycolor2@^1.4.2:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.5.2.tgz#7d30b4584d8b7d62b9a94dacc505614a6516a95f"
|
||||
@@ -18165,6 +18160,11 @@ type-fest@^0.8.1:
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
||||
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
||||
|
||||
type-fest@^2.19.0:
|
||||
version "2.19.0"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
|
||||
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
|
||||
|
||||
type-fest@^3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.6.0.tgz#827c36c0e7fcff0cb2d55d091a5c4cf586432b8a"
|
||||
@@ -19366,18 +19366,15 @@ yocto-queue@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
yup@^0.32.11:
|
||||
version "0.32.11"
|
||||
resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5"
|
||||
integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==
|
||||
yup@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/yup/-/yup-1.2.0.tgz#9e51af0c63bdfc9be0fdc6c10aa0710899d8aff6"
|
||||
integrity sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.15.4"
|
||||
"@types/lodash" "^4.14.175"
|
||||
lodash "^4.17.21"
|
||||
lodash-es "^4.17.21"
|
||||
nanoclone "^0.2.1"
|
||||
property-expr "^2.0.4"
|
||||
property-expr "^2.0.5"
|
||||
tiny-case "^1.0.3"
|
||||
toposort "^2.0.2"
|
||||
type-fest "^2.19.0"
|
||||
|
||||
zwitch@^1.0.0:
|
||||
version "1.0.5"
|
||||
|
||||
Reference in New Issue
Block a user