From 7e6c97cfa44e80d6ca35703348274ba914846e09 Mon Sep 17 00:00:00 2001 From: Tommaso Morganti Date: Sun, 23 Jul 2023 13:04:35 +0200 Subject: [PATCH 1/2] zod poc --- package.json | 3 +- src/api/HttpClient.ts | 21 +++++-- src/api/collections/articles.ts | 58 +++--------------- src/api/collections/auth.ts | 7 ++- src/api/collections/event.ts | 103 ++------------------------------ src/api/collections/groups.ts | 40 +++---------- src/api/schemas/articles.ts | 54 +++++++++++++++++ src/api/schemas/auth.ts | 26 ++++++++ src/api/schemas/event.ts | 79 ++++++++++++++++++++++++ src/api/schemas/generals.ts | 7 +++ src/api/schemas/groups.ts | 32 ++++++++++ src/api/schemas/index.ts | 5 ++ src/contexts/login.ts | 21 ------- yarn.lock | 5 ++ 14 files changed, 251 insertions(+), 210 deletions(-) create mode 100644 src/api/schemas/articles.ts create mode 100644 src/api/schemas/auth.ts create mode 100644 src/api/schemas/event.ts create mode 100644 src/api/schemas/generals.ts create mode 100644 src/api/schemas/groups.ts create mode 100644 src/api/schemas/index.ts diff --git a/package.json b/package.json index b4d71ccc..dfa9568d 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,8 @@ "react-navigation-shared-element": "^3.1.3", "rn-slick-bottom-tabs": "^1.1.1", "styled-components": "^5.3.6", - "yarn": "^1.22.19" + "yarn": "^1.22.19", + "zod": "^3.21.4" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/src/api/HttpClient.ts b/src/api/HttpClient.ts index 2055441b..0fd15b27 100644 --- a/src/api/HttpClient.ts +++ b/src/api/HttpClient.ts @@ -6,10 +6,11 @@ import axios, { AxiosResponse, InternalAxiosRequestConfig, } from "axios" -import { PolimiToken, PoliNetworkToken, Tokens } from "contexts/login" +import { PolimiToken, PoliNetworkToken, Tokens } from "./schemas" import AsyncStorage from "@react-native-async-storage/async-storage" import { wait } from "utils/functions" import { Alert } from "react-native" +import { z } from "zod" /*Docs used to make this: Singleton: @@ -76,9 +77,14 @@ declare module "axios" { waitingTime?: number //seconds readonly retryCount?: number authType?: AuthType + zodSchema?: z.ZodSchema } } +type InferrableAxiosRequestConfig = AxiosRequestConfig & { + zodSchema?: z.ZodSchema +} + /** * Singleton object which manages requests and retries * to PoliNetwork Server and Polimi Server. @@ -186,6 +192,9 @@ export class HttpClient extends EventEmitter { * does (or will do) something before `.then` is called * */ private _handleResponse = (res: AxiosResponse): AxiosResponse => { + if (res.config.zodSchema) { + res.config.zodSchema.parse(res.data) + } return res } @@ -287,7 +296,9 @@ export class HttpClient extends EventEmitter { throw error } - callPolimi(options: AxiosRequestConfig): CancellableApiRequest { + callPolimi( + options: InferrableAxiosRequestConfig + ): CancellableApiRequest { const controller = new AbortController() const request = this.polimiInstance.request({ ...options, @@ -300,7 +311,7 @@ export class HttpClient extends EventEmitter { } callPoliNetwork( - options: AxiosRequestConfig + options: InferrableAxiosRequestConfig ): CancellableApiRequest { const controller = new AbortController() const request = this.poliNetworkInstance.request({ @@ -313,7 +324,9 @@ export class HttpClient extends EventEmitter { return request } - callGeneral(options: AxiosRequestConfig): CancellableApiRequest { + callGeneral( + options: InferrableAxiosRequestConfig + ): CancellableApiRequest { const controller = new AbortController() const request = this.generalInstance.request({ ...options, diff --git a/src/api/collections/articles.ts b/src/api/collections/articles.ts index c330aa4f..b628bd47 100644 --- a/src/api/collections/articles.ts +++ b/src/api/collections/articles.ts @@ -2,54 +2,7 @@ import { mapAxiosRequest } from "api/mapAxiosRequest" import { ApiCollection } from "api/useApiCall" import { HttpClient, RequestOptions } from "../HttpClient" - -export interface Tags { - tags: Tag[] -} - -export interface Tag { - name: string - image: string - blurhash: string -} - -export interface Articles { - articles: Article[] - start: string | null - end: string | null - tag: string | null - author_id: number | null - title: string | null -} -interface ArticleAuthor { - name?: string - link?: string - image?: string -} - -export interface Article { - id: number - tag_id: string - latitude?: number - longitude?: number - publish_time: string - target_time?: string - hidden_until?: string - content: { - it: ArticlesParams - en: ArticlesParams - } - image?: string - blurhash?: string - author?: ArticleAuthor -} - -interface ArticlesParams { - content: string - title: string - subtitle: string - url: string -} +import { articlesSchema, tagsSchema } from "api/schemas" const client = HttpClient.getInstance() @@ -86,7 +39,7 @@ export const articles = { params: { tag: string; limit: number; offset: number }, options?: RequestOptions ) { - const request = client.callPoliNetwork({ + const request = client.callPoliNetwork({ url: "/v1/articles", method: "GET", params: { @@ -96,6 +49,7 @@ export const articles = { tag: params.tag, sort: "date", }, + zodSchema: articlesSchema, ...options, }) return mapAxiosRequest(request, res => res.articles) @@ -109,10 +63,11 @@ export const articles = { * @param options see {@link RequestOptions} */ getLastArticleByTag(params: { tag: string }, options?: RequestOptions) { - const request = client.callPoliNetwork({ + const request = client.callPoliNetwork({ url: "/v1/articles", method: "GET", params: { tag: params.tag, limit: 1, sort: "date", platform: 1 }, + zodSchema: articlesSchema, ...options, }) return mapAxiosRequest(request, res => res.articles[0]) @@ -122,9 +77,10 @@ export const articles = { * Retrieves Tags (news categories) from PoliNetwork server. */ getTags(_params?: Record, options?: RequestOptions) { - const request = client.callPoliNetwork({ + const request = client.callPoliNetwork({ url: "/v1/tags", method: "GET", + zodSchema: tagsSchema, ...options, }) diff --git a/src/api/collections/auth.ts b/src/api/collections/auth.ts index 35fd7876..ee1aba96 100644 --- a/src/api/collections/auth.ts +++ b/src/api/collections/auth.ts @@ -1,7 +1,7 @@ import { mapAxiosRequest } from "api/mapAxiosRequest" import { ApiCollection } from "api/useApiCall" -import { PolimiToken } from "contexts/login" import { HttpClient, RequestOptions } from "../HttpClient" +import { polimiTokenSchema } from "api/schemas/auth" const client = HttpClient.getInstance() @@ -15,13 +15,14 @@ export const auth = { * @returns polimi accessToken and refreshToken */ getPolimiToken(params: { authcode: string }, options?: RequestOptions) { - const request = client.callPolimi({ + const request = client.callPolimi({ url: `/rest/jaf/oauth/token/get/${params.authcode}`, method: "GET", - ...options, headers: { accept: "application/json", }, + zodSchema: polimiTokenSchema, + ...options, }) return mapAxiosRequest(request, res => res) }, diff --git a/src/api/collections/event.ts b/src/api/collections/event.ts index 83ace1d4..71828032 100644 --- a/src/api/collections/event.ts +++ b/src/api/collections/event.ts @@ -1,101 +1,7 @@ import { mapAxiosRequest } from "api/mapAxiosRequest" import { ApiCollection } from "api/useApiCall" import { AuthType, HttpClient, RequestOptions } from "../HttpClient" - -/* eslint-disable @typescript-eslint/naming-convention */ -export interface Event { - event_id: number - date_start: string - date_end: string - show_agenda: boolean - matricola?: string - title: { - it: string - en: string - } - event_type: { - typeId: number - type_dn: { - it: string - en: string - } - } - event_subtype?: string - calendar: { - calendar_id: number - calendar_dn: { - it: string - en: string - } - } - room?: { - room_id: number - acronym_dn: string - classroom_id: number - room_dn: string - } - //used by Timetable to store color - lectureColor?: string -} - -export interface RemoteLink { - url: string - link_description: { - it: string - en: string - } -} - -export interface Lecture { - event_id: number - room_id: number - date_start: string - date_end: string - class_code: number - teaching_description: { - it: string - en: string - } - lecturer: string - lecture_type: string - event_type: { - typeId: number - type_dn: { - it: string - en: string - } - } - calendar: { - calendar_id: number - calendar_dn: { - it: string - en: string - } - } - room: { - room_id: number - campus: string - address: string - floor: string - building: string - type: string - acronym_dn: string - coordinates: { - latitude: number - longitude: number - } - csi_id: string - csi_parent_id: string - classroom_id: number - room_dn: string - } - remote_links: RemoteLink[] -} - -export interface LectureDetails { - lecture: Lecture - personal_event: Event -} +import { eventSchema, lectureDetailsSchema } from "api/schemas" const client = HttpClient.getInstance() @@ -105,11 +11,13 @@ export const events = { options?: RequestOptions ) { const url = "/agenda/api/me/" + params.matricola + "/events" - const request = client.callPolimi({ + const request = client.callPolimi({ url, method: "GET", authType: AuthType.POLIMI, + // eslint-disable-next-line @typescript-eslint/naming-convention params: { start_date: params.startDate, n_events: params.nEvents }, + zodSchema: eventSchema.array(), ...options, }) return mapAxiosRequest(request, response => response) @@ -120,10 +28,11 @@ export const events = { options?: RequestOptions ) { const url = `/agenda/api/me/${params.matricola}/lectures/${params.eventId}` - const request = client.callPolimi({ + const request = client.callPolimi({ url, method: "GET", authType: AuthType.POLIMI, + zodSchema: lectureDetailsSchema, ...options, }) return mapAxiosRequest(request, response => response.lecture) diff --git a/src/api/collections/groups.ts b/src/api/collections/groups.ts index c933d363..c6ff0811 100644 --- a/src/api/collections/groups.ts +++ b/src/api/collections/groups.ts @@ -1,36 +1,8 @@ import { mapAxiosRequest } from "api/mapAxiosRequest" import { ApiCollection } from "api/useApiCall" import { HttpClient, RequestOptions } from "../HttpClient" - -/* eslint-disable @typescript-eslint/naming-convention */ - -export interface GroupOptions { - name?: string - year?: string - degree?: string - type?: string - platform?: string - language?: string - office?: string -} - -export interface Group { - class: string | null - office?: string - id: string - degree?: string - school?: string - id_link: string - language?: string - type?: string - year: string | null //probably I should use | null everywhere? - platform: string - permanentId?: number - LastUpdateInviteLinkTime?: string - linkfunzionante?: string - LinkType?: string - members?: string -} +import { groupSchema } from "api/schemas" +import { z } from "zod" const client = HttpClient.getInstance() @@ -44,12 +16,14 @@ export const groups = { */ getFromGithub(_params?: Record, options?: RequestOptions) { - const request = client.callGeneral<{ - index_data: Group[] - }>({ + const request = client.callGeneral({ url: "https://raw.githubusercontent.com/PoliNetworkOrg/polinetworkWebsiteData/main/groupsGenerated.json", method: "GET", ...options, + zodSchema: z.object({ + // eslint-disable-next-line @typescript-eslint/naming-convention + index_data: groupSchema.array(), + }), }) return mapAxiosRequest(request, response => response.index_data) }, diff --git a/src/api/schemas/articles.ts b/src/api/schemas/articles.ts new file mode 100644 index 00000000..7c3d7411 --- /dev/null +++ b/src/api/schemas/articles.ts @@ -0,0 +1,54 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { z } from "zod" + +export const tagSchema = z.object({ + name: z.string(), + image: z.string(), + blurhash: z.string(), +}) + +export const tagsSchema = z.object({ + tags: z.array(tagSchema), +}) +export type Tags = z.infer + +const articleAuthorSchema = z.object({ + name: z.string().nullable(), + link: z.string().nullable(), + image: z.string().nullable(), +}) + +const articlesParamsSchema = z.object({ + content: z.string(), + title: z.string(), + subtitle: z.string(), + url: z.string(), +}) + +export const articleSchema = z.object({ + id: z.number(), + tag_id: z.string(), + latitude: z.number().nullable(), + longitude: z.number().nullable(), + publish_time: z.string(), + target_time: z.string().nullable(), + hidden_until: z.string().nullable(), + content: z.object({ + it: articlesParamsSchema, + en: articlesParamsSchema, + }), + image: z.string().nullable(), + blurhash: z.string().nullable(), + author: articleAuthorSchema.nullable(), +}) +export type Article = z.infer + +export const articlesSchema = z.object({ + articles: z.array(articleSchema), + start: z.string().nullable(), + end: z.string().nullable(), + tag: z.string().nullable(), + author_id: z.number().nullable(), + title: z.string().nullable(), +}) +export type Articles = z.infer diff --git a/src/api/schemas/auth.ts b/src/api/schemas/auth.ts new file mode 100644 index 00000000..f58774fa --- /dev/null +++ b/src/api/schemas/auth.ts @@ -0,0 +1,26 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { z } from "zod" + +export const poliNetworkTokenSchema = z.object({ + access_token: z.string(), + expires_in: z.number(), + ext_expires_in: z.number(), + id_token: z.string(), + refresh_token: z.string(), + scope: z.string(), + token_type: z.string(), +}) +export type PoliNetworkToken = z.infer + +export const polimiTokenSchema = z.object({ + accessToken: z.string(), + expiresIn: z.number(), + refreshToken: z.string(), +}) +export type PolimiToken = z.infer + +export const tokensSchema = z.object({ + polimiToken: polimiTokenSchema, + poliNetworkToken: poliNetworkTokenSchema, +}) +export type Tokens = z.infer diff --git a/src/api/schemas/event.ts b/src/api/schemas/event.ts new file mode 100644 index 00000000..e60ff4da --- /dev/null +++ b/src/api/schemas/event.ts @@ -0,0 +1,79 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { z } from "zod" +import { translatedStringSchema } from "./generals" + +const eventTypeSchema = z.object({ + typeId: z.number(), + type_dn: translatedStringSchema, +}) + +const calendarTypeSchema = z.object({ + calendar_id: z.number(), + calendar_dn: translatedStringSchema, +}) + +export const eventSchema = z.object({ + event_id: z.number(), + date_start: z.string(), + date_end: z.string(), + show_agenda: z.boolean(), + matricola: z.string().optional(), + title: translatedStringSchema, + event_type: eventTypeSchema, + event_subtype: z.string().optional(), + calendar: calendarTypeSchema, + room: z + .object({ + room_id: z.number(), + acronym_dn: z.string(), + classroom_id: z.number(), + room_dn: z.string(), + }) + .optional(), + lectureColor: z.string().optional(), +}) +export type Event = z.infer + +export const remoteLinkSchema = z.object({ + url: z.string(), + link_description: translatedStringSchema, +}) +export type RemoteLink = z.infer + +export const lectureSchema = z.object({ + event_id: z.number(), + room_id: z.number(), + date_start: z.string(), + date_end: z.string(), + class_code: z.number(), + teaching_description: translatedStringSchema, + lecturer: z.string(), + lecture_type: z.string(), + event_type: eventTypeSchema, + calendar: calendarTypeSchema, + room: z.object({ + room_id: z.number(), + campus: z.string(), + address: z.string(), + floor: z.string(), + building: z.string(), + type: z.string(), + acronym_dn: z.string(), + coordinates: z.object({ + latitude: z.number(), + longitude: z.number(), + }), + csi_id: z.string(), + csi_parent_id: z.string(), + classroom_id: z.number(), + room_dn: z.string(), + }), + remote_links: z.array(remoteLinkSchema), +}) +export type Lecture = z.infer + +export const lectureDetailsSchema = z.object({ + lecture: lectureSchema, + personal_event: eventSchema, +}) +export type LectureDetails = z.infer diff --git a/src/api/schemas/generals.ts b/src/api/schemas/generals.ts new file mode 100644 index 00000000..2a57afc8 --- /dev/null +++ b/src/api/schemas/generals.ts @@ -0,0 +1,7 @@ +import { z } from "zod" + +export const translatedStringSchema = z.object({ + it: z.string(), + en: z.string(), +}) +export type TranslatedString = z.infer diff --git a/src/api/schemas/groups.ts b/src/api/schemas/groups.ts new file mode 100644 index 00000000..3c780c73 --- /dev/null +++ b/src/api/schemas/groups.ts @@ -0,0 +1,32 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { z } from "zod" + +export const groupOptionsSchema = z.object({ + name: z.string().optional(), + year: z.string().optional(), + degree: z.string().optional(), + type: z.string().optional(), + platform: z.string().optional(), + language: z.string().optional(), + office: z.string().optional(), +}) +export type GroupOptions = z.infer + +export const groupSchema = z.object({ + class: z.string().nullable(), + office: z.string().optional(), + id: z.string(), + degree: z.string().optional(), + school: z.string().optional(), + id_link: z.string(), + language: z.string().optional(), + type: z.string().optional(), + year: z.string().nullable(), + platform: z.string(), + permanentId: z.number().optional(), + LastUpdateInviteLinkTime: z.string().optional(), + linkfunzionante: z.string().optional(), + LinkType: z.string().optional(), + members: z.string().optional(), +}) +export type Group = z.infer diff --git a/src/api/schemas/index.ts b/src/api/schemas/index.ts new file mode 100644 index 00000000..2b43b23f --- /dev/null +++ b/src/api/schemas/index.ts @@ -0,0 +1,5 @@ +export * from "./articles" +export * from "./auth" +export * from "./event" +export * from "./generals" +export * from "./groups" diff --git a/src/contexts/login.ts b/src/contexts/login.ts index 029df325..b1fee293 100644 --- a/src/contexts/login.ts +++ b/src/contexts/login.ts @@ -3,27 +3,6 @@ import React from "react" /* eslint-disable @typescript-eslint/naming-convention */ -export interface PoliNetworkToken { - access_token: string - expires_in: number - ext_expires_in: number - id_token: string - refresh_token: string - scope: string - token_type: string -} - -export interface PolimiToken { - accessToken: string - expiresIn: number - refreshToken: string -} - -export interface Tokens { - polimiToken: PolimiToken - poliNetworkToken: PoliNetworkToken -} - export type LoginState = | { loggedIn: false diff --git a/yarn.lock b/yarn.lock index f42197dc..93e1e630 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11238,3 +11238,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@^3.21.4: + version "3.21.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" + integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== From 84491e5427626998542266e3ebf7dec2265da2d9 Mon Sep 17 00:00:00 2001 From: Tommaso Morganti Date: Sun, 15 Oct 2023 13:18:36 +0200 Subject: [PATCH 2/2] token schema parse --- src/api/HttpClient.ts | 5 ++--- src/pages/Login.tsx | 11 ++++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/api/HttpClient.ts b/src/api/HttpClient.ts index 92a24073..c235a506 100644 --- a/src/api/HttpClient.ts +++ b/src/api/HttpClient.ts @@ -6,7 +6,7 @@ import axios, { AxiosResponse, InternalAxiosRequestConfig, } from "axios" -import { PolimiToken, PoliNetworkToken, Tokens } from "./schemas" +import { PolimiToken, PoliNetworkToken, Tokens, tokensSchema } from "./schemas" import AsyncStorage from "@react-native-async-storage/async-storage" import { wait } from "utils/functions" import { Alert } from "react-native" @@ -432,8 +432,7 @@ export class HttpClient extends EventEmitter { async loadTokens() { const tokens = await AsyncStorage.getItem("api:tokens") if (tokens) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const parsedTokens: Tokens = JSON.parse(tokens) + const parsedTokens: Tokens = tokensSchema.parse(JSON.parse(tokens)) console.log("Loaded tokens from local storage") this.polimiToken = parsedTokens.polimiToken this.poliNetworkToken = parsedTokens.poliNetworkToken diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index ef90ccbf..cf5b10c6 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -6,7 +6,11 @@ import WebView from "react-native-webview" import { usePalette } from "utils/colors" import { api } from "api" import { HttpClient } from "api/HttpClient" -import { PolimiToken, PoliNetworkToken } from "api/schemas" +import { + PolimiToken, + PoliNetworkToken, + poliNetworkTokenSchema, +} from "api/schemas" import { NavBar } from "components/NavBar" // TODO: HANDLE ERRORS, this will break as soon as something goes wrong @@ -132,8 +136,9 @@ export const Login: RootStackScreen<"Login"> = () => { if (url.startsWith(polinetworkTargetUrl)) { try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - setPoliNetworkToken(JSON.parse(data)) + setPoliNetworkToken( + poliNetworkTokenSchema.parse(JSON.parse(data)) + ) setCurrentURL(magicTokenUrl) } catch (e) { console.log("error while parsing!!")