From e8eab3369497a5f7a9ce95db40aec0516c074940 Mon Sep 17 00:00:00 2001 From: Niki Bizjak Date: Thu, 4 Sep 2025 14:57:58 +0200 Subject: [PATCH 1/8] feat: replace zodios with orval --- package.json | 2 -- yarn.lock | 25 +++++++++++-------------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 76d757e..cb1c1da 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,6 @@ "@sentry/react-native": "~6.14.0", "@t3-oss/env-core": "^0.13.8", "@tanstack/react-query": "^5.29.2", - "@zodios/core": "^10.9.6", - "@zodios/react": "^10.4.5", "axios": "^1.6.8", "clsx": "^2.1.0", "exp": "^57.2.1", diff --git a/yarn.lock b/yarn.lock index 554859c..433ef52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3912,7 +3912,7 @@ __metadata: languageName: node linkType: hard -"@zodios/core@npm:^10.3.1, @zodios/core@npm:^10.9.6": +"@zodios/core@npm:^10.3.1": version: 10.9.6 resolution: "@zodios/core@npm:10.9.6" peerDependencies: @@ -3922,17 +3922,6 @@ __metadata: languageName: node linkType: hard -"@zodios/react@npm:^10.4.5": - version: 10.4.5 - resolution: "@zodios/react@npm:10.4.5" - peerDependencies: - "@tanstack/react-query": 4.x - "@zodios/core": ">=10.2.0 <11.0.0" - react: ">=16.8.0" - checksum: 10c0/782f7ad907c6737aa28c0aac53743239eb3a09f0e3921217195a612e80b7d53809e88eca464665748128613861e06a92c5130c1971901e56057f9aa05f6e56e4 - languageName: node - linkType: hard - "JSONStream@npm:^1.3.4, JSONStream@npm:^1.3.5": version: 1.3.5 resolution: "JSONStream@npm:1.3.5" @@ -6772,6 +6761,15 @@ __metadata: languageName: node linkType: hard +"expo-application@npm:~6.1.5": + version: 6.1.5 + resolution: "expo-application@npm:6.1.5" + peerDependencies: + expo: "*" + checksum: 10c0/c4fa0bddfc911af17055334558314d819d403efa5db22b05cffc44c91eef38e9fb57b4a5aae35378523c59847189a7ca09ad9e5370ee5a3b0f23c1c5146c8683 + languageName: node + linkType: hard + "expo-asset@npm:~11.1.5": version: 11.1.5 resolution: "expo-asset@npm:11.1.5" @@ -12571,8 +12569,6 @@ __metadata: "@types/aes-js": "npm:^3.1.4" "@types/react": "npm:~19.0.10" "@types/react-dom": "npm:~18.2.25" - "@zodios/core": "npm:^10.9.6" - "@zodios/react": "npm:^10.4.5" axios: "npm:^1.6.8" clsx: "npm:^2.1.0" eslint: "npm:8" @@ -12580,6 +12576,7 @@ __metadata: eslint-plugin-sonarjs: "npm:^1.0.3" exp: "npm:^57.2.1" expo: "npm:^53.0.13" + expo-application: "npm:~6.1.5" expo-constants: "npm:~17.1.6" expo-font: "npm:~13.3.1" expo-linear-gradient: "npm:~14.1.5" From 97b08789dbc2b150ee743903950799252eb62095 Mon Sep 17 00:00:00 2001 From: Niki Bizjak Date: Thu, 4 Sep 2025 14:59:44 +0200 Subject: [PATCH 2/8] feat: add api authentication plugin & cleanup project --- .prettierignore | 1 - api/api-error-plugin.ts | 22 -------- api/api-toast-plugin.ts | 66 ---------------------- api/api-token-plugin.ts | 27 --------- api/api.ts | 14 ----- api/axios-instance.ts | 35 ++++++++++++ api/example/index.ts | 47 ---------------- app/_layout.tsx | 7 ++- orval.config.ts | 11 ++++ package.json | 13 +++-- scripts/generate-api.ts | 8 +++ scripts/zodios-client-template.hbs | 88 ----------------------------- utils/providers/api-provider.ts | 90 ++++++++++++++++++++++++++++++ 13 files changed, 158 insertions(+), 271 deletions(-) delete mode 100644 .prettierignore delete mode 100644 api/api-error-plugin.ts delete mode 100644 api/api-toast-plugin.ts delete mode 100644 api/api-token-plugin.ts delete mode 100644 api/api.ts create mode 100644 api/axios-instance.ts delete mode 100644 api/example/index.ts create mode 100644 scripts/generate-api.ts delete mode 100644 scripts/zodios-client-template.hbs create mode 100644 utils/providers/api-provider.ts diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index d201650..0000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -zodios-client-template.hbs diff --git a/api/api-error-plugin.ts b/api/api-error-plugin.ts deleted file mode 100644 index 073827a..0000000 --- a/api/api-error-plugin.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ZodiosPlugin } from '@zodios/core'; -import { AxiosError } from 'axios'; - -const SKIP_ERROR_HANDLING_URLS = ['/example/skip-error-handling']; - -const errorErrorPlugin: ZodiosPlugin = { - name: 'errorErrorPlugin', - error: async (api, config, err) => { - if (SKIP_ERROR_HANDLING_URLS.includes(config.url)) { - console.log('Skipping error handling for', config.url); - throw err; - } - - if (err instanceof AxiosError) { - console.error('AxiosError', err); - } - - throw err; - }, -}; - -export default errorErrorPlugin; diff --git a/api/api-toast-plugin.ts b/api/api-toast-plugin.ts deleted file mode 100644 index d3681f0..0000000 --- a/api/api-toast-plugin.ts +++ /dev/null @@ -1,66 +0,0 @@ -import useToastStore from '@utils/stores/toast-store'; -import { ZodiosPlugin } from '@zodios/core'; -import { AxiosError } from 'axios'; -import i18n from 'i18next'; - -const SKIP_ERROR_HANDLING_URLS = ['']; -const SKIP_SUCCESS_HANDLING_URLS = ['']; - -const apiToastPlugin: ZodiosPlugin = { - name: 'apiToastPlugin', - error: async (api, config, err) => { - if (SKIP_ERROR_HANDLING_URLS.includes(config.url)) { - console.log('Skipping error handling for', config.url); - throw err; - } - - if (err instanceof AxiosError) { - useToastStore.getState().setToast({ - type: 'error', - message: - err.response?.data?.message || i18n.t('common:apiErrorDescription'), - }); - } - - throw err; - }, - response: async (api, config, response) => { - if (SKIP_SUCCESS_HANDLING_URLS.includes(config.url)) { - console.log('Skipping success handling for', config.url); - return response; - } - - // Skip handling GET requests - if (config.method?.toUpperCase() === 'GET') { - return response; - } - - const getMessage = () => { - let message = ''; - switch (config.method?.toUpperCase()) { - case 'POST': - message = i18n.t('common:createSuccess'); - break; - case 'PUT': - message = i18n.t('common:updateSuccess'); - break; - case 'DELETE': - message = i18n.t('common:deleteSuccess'); - break; - } - - return message; - }; - - if (response.status >= 200 && response.status < 300) { - useToastStore.getState().setToast({ - type: 'success', - message: getMessage(), - }); - } - - return response; - }, -}; - -export default apiToastPlugin; diff --git a/api/api-token-plugin.ts b/api/api-token-plugin.ts deleted file mode 100644 index 1dbe9bb..0000000 --- a/api/api-token-plugin.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ZodiosPlugin } from '@zodios/core'; - -/** - * Custom plugin for Zodios to inject API key into request headers - */ -const apiTokenPlugin = (): ZodiosPlugin => { - return { - request: async (_, config) => { - /** - * You should implement your own logic to get the auth token - * @example const { data } = await supabase.auth.getSession(); - * @example const authToken = data.session?.access_token; - */ - const authToken = 'fake-token'; - - return { - ...config, - headers: { - ...config.headers, - ...(authToken && { Authorization: `Bearer ${authToken}` }), - }, - }; - }, - }; -}; - -export default apiTokenPlugin; diff --git a/api/api.ts b/api/api.ts deleted file mode 100644 index 692493e..0000000 --- a/api/api.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Zodios } from '@zodios/core'; -import { ZodiosHooks } from '@zodios/react'; -import apiErrorPlugin from './api-error-plugin'; -import exampleApi from './example'; - -const API_URL = process.env.EXPO_PUBLIC_API_URL || ''; - -// Zodios API client -const apiClient = new Zodios(API_URL, [...exampleApi]); - -apiClient.use(apiErrorPlugin); -const api = new ZodiosHooks('exampleApi', apiClient); - -export { api, apiClient }; diff --git a/api/axios-instance.ts b/api/axios-instance.ts new file mode 100644 index 0000000..1c154bf --- /dev/null +++ b/api/axios-instance.ts @@ -0,0 +1,35 @@ +import Axios, { + type AxiosError, + type AxiosRequestConfig, + type AxiosResponse, +} from 'axios'; +import env from '../env'; + +export const AXIOS_INSTANCE = Axios.create({ + baseURL: env.EXPO_PUBLIC_API_URL, +}); + +// add a second `options` argument here if you want to pass extra options to each generated query +export const customAxios = ( + config: AxiosRequestConfig, + options?: AxiosRequestConfig, +): Promise> => { + const source = Axios.CancelToken.source(); + const promise = AXIOS_INSTANCE({ + ...config, + ...options, + cancelToken: source.token, + }).then((data) => data); + + // @ts-expect-error: The cancel method is not typed. + promise.cancel = () => { + source.cancel('Query was cancelled'); + }; + + return promise; +}; + +// In some case with react-query and swr you want to be able to override the return error type so you can also do it here like this +export type ErrorType = AxiosError; + +export type BodyType = BodyData; diff --git a/api/example/index.ts b/api/example/index.ts deleted file mode 100644 index 98a2c91..0000000 --- a/api/example/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { apiBuilder } from '@zodios/core'; -import { z } from 'zod'; - -// Endpoints for Example API - Example Endpoints. -const exampleApi = apiBuilder({ - method: 'get', - path: '/example', - alias: 'getExample', - description: 'Get example', - response: z.object({ - text: z.string(), - }), - parameters: [ - { - type: 'Query', - name: 'name', - description: 'User name', - schema: z.string().optional(), - }, - ], - errors: [{ status: 'default', schema: z.object({ message: z.string() }) }], -}) - .addEndpoint({ - method: 'post', - path: '/example/:exampleId', - description: 'Add example', - alias: 'addExample', - response: z.object({}), - parameters: [ - { - name: 'exampleId', - type: 'Path', - schema: z.string(), - }, - { - name: 'body', - type: 'Body', - schema: z.object({ - name: z.string(), - }), - }, - ], - errors: [{ status: 'default', schema: z.object({ message: z.string() }) }], - }) - .build(); - -export default exampleApi; diff --git a/app/_layout.tsx b/app/_layout.tsx index fbdabd7..94e97ee 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -3,6 +3,7 @@ import * as Sentry from '@sentry/react-native'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import useCustomFonts from '@utils/hooks/use-custom-fonts'; import '@utils/i18n/config'; +import { ApiProvider } from '@utils/providers/api-provider'; import { isRunningInExpoGo } from 'expo'; import { Slot, SplashScreen, useNavigationContainerRef } from 'expo-router'; import { useEffect } from 'react'; @@ -47,8 +48,10 @@ const RootLayout = () => { return ( - - + + + + ); }; diff --git a/orval.config.ts b/orval.config.ts index 3b81f0c..0f3e971 100644 --- a/orval.config.ts +++ b/orval.config.ts @@ -22,4 +22,15 @@ export default defineConfig({ target: './placeholder.yaml', }, }, + apiZod: { + output: { + mode: 'split', + client: 'zod', + target: 'api/generated/types.ts', + }, + input: { + // This will get overridden by /scripts/generate-api.ts + target: './placeholder.yaml', + }, + }, }); diff --git a/package.json b/package.json index cb1c1da..b72c6a4 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,17 @@ "main": "index.js", "scripts": { "start": "infisical run -- expo start -c", - "android": "infisical run -- expo start --android", - "ios": "infisical run -- expo start --ios", + "android": "infisical run -- expo run:android --device", + "android:production": "expo prebuild --clean && eas build --platform android --profile production --local", + "android:preview": "expo prebuild && eas build --platform android --profile preview --local", + "ios": "infisical run -- expo run:ios --device", + "ios:production": "expo prebuild --clean && eas build --platform ios --profile production --local", + "ios:preview": "expo prebuild && eas build --platform ios --profile preview --local", "web": "infisical run -- expo start --web -c", "gen-api": "infisical run --command 'tsx ./scripts/generate-api.ts'", "lint": "eslint .", - "format-check": "prettier --check .", - "format-fix": "prettier --write .", + "format:check": "prettier --check .", + "format:fix": "prettier --write .", "prepare": "husky", "eas-build-pre-install": "./scripts/infisical.sh" }, @@ -23,6 +27,7 @@ "clsx": "^2.1.0", "exp": "^57.2.1", "expo": "^53.0.13", + "expo-application": "~6.1.5", "expo-constants": "~17.1.6", "expo-font": "~13.3.1", "expo-linear-gradient": "~14.1.5", diff --git a/scripts/generate-api.ts b/scripts/generate-api.ts new file mode 100644 index 0000000..8daa92f --- /dev/null +++ b/scripts/generate-api.ts @@ -0,0 +1,8 @@ +import orval from 'orval'; +import env from '../env'; + +const SCHEMA_NAME = '/docs?api-docs.json'; // Usually it's '/openapi.json'; + +orval('orval.config.ts', 'react-native-template', { + input: env.EXPO_PUBLIC_API_URL + SCHEMA_NAME, +}); diff --git a/scripts/zodios-client-template.hbs b/scripts/zodios-client-template.hbs deleted file mode 100644 index 4f519fa..0000000 --- a/scripts/zodios-client-template.hbs +++ /dev/null @@ -1,88 +0,0 @@ -import { makeApi, Zodios } from "@zodios/core"; -import { z } from "zod"; -import apiErrorPlugin from './api-error-plugin'; -import apiTokenPlugin from './api-token-plugin'; -import apiToastPlugin from './api-toast-plugin'; -import { ZodiosHooks } from '@zodios/react'; - - -{{#each types}} -{{{this}}}; -{{/each}} - -{{#each schemas}} -const {{@key}}{{#if (lookup ../emittedType @key)}}: z.ZodType<{{@key}}>{{/if}} = {{{this}}}; -export type {{@key}} = z.infer; -{{/each}} - -{{#ifNotEmptyObj schemas}} -export const schemas = { -{{#each schemas}} - {{@key}}, -{{/each}} -}; -{{/ifNotEmptyObj}} - -const endpoints = makeApi([ -{{#each endpoints}} - { - method: "{{method}}", - path: "{{path}}", - {{#if @root.options.withAlias}} - {{#if alias}} - alias: "{{alias}}", - {{/if}} - {{/if}} - {{#if description}} - description: `{{description}}`, - {{/if}} - {{#if requestFormat}} - requestFormat: "{{requestFormat}}", - {{/if}} - {{#if parameters}} - parameters: [ - {{#each parameters}} - { - name: "{{name}}", - {{#if description}} - description: `{{description}}`, - {{/if}} - {{#if type}} - type: "{{type}}", - {{/if}} - schema: {{{schema}}} - }, - {{/each}} - ], - {{/if}} - response: {{{response}}}, - {{#if errors.length}} - errors: [ - {{#each errors}} - { - {{#ifeq status "default" }} - status: "default", - {{else}} - status: {{status}}, - {{/ifeq}} - {{#if description}} - description: `{{description}}`, - {{/if}} - schema: {{{schema}}} - }, - {{/each}} - ] - {{/if}} - }, -{{/each}} -]); - -const API_URL = process.env.EXPO_PUBLIC_API_URL || ''; - -const apiClient = new Zodios(API_URL, endpoints); -apiClient.use(apiErrorPlugin); -apiClient.use(apiTokenPlugin()); -apiClient.use(apiToastPlugin); -const api = new ZodiosHooks('endpoints', apiClient); - -export { api, apiClient }; \ No newline at end of file diff --git a/utils/providers/api-provider.ts b/utils/providers/api-provider.ts new file mode 100644 index 0000000..8df6bcb --- /dev/null +++ b/utils/providers/api-provider.ts @@ -0,0 +1,90 @@ +import { AXIOS_INSTANCE } from 'api/axios-instance'; +import { AxiosHeaders } from 'axios'; +import * as Application from 'expo-application'; +import { useRouter } from 'expo-router'; +import { PropsWithChildren, useEffect, useMemo } from 'react'; +import { Platform } from 'react-native'; + +export const getApiHeaders = (accessToken?: string | null) => { + const headers = { + 'X-App-Version': Application.nativeApplicationVersion, + 'X-OS': Platform.OS, + }; + + if (accessToken) { + return { + Authorization: `Bearer ${accessToken}`, + ...headers, + }; + } + + return headers; +}; + +/** + * ApiProvider is a context wrapper that injects authentication and versioning + * headers into all outgoing API requests made through Axios. + * + * - Listens for Supabase auth state changes and updates the session token. + * - Automatically attaches the `Authorization` header (with the Supabase + * session access token) to outgoing requests. + * - Adds app metadata headers (`X-App-Version` and `X-OS`) to help the backend + * enforce version control and platform-specific behavior. + * - Intercepts API responses to handle specific status codes (e.g., HTTP 426 + * for forced app updates), and redirects the user to the appropriate screen + * if necessary. + */ +export const ApiProvider = ({ children }: PropsWithChildren) => { + const router = useRouter(); + + /** + * You should implement your own logic to get the auth token + * @example const { data } = await supabase.auth.getSession(); + * @example const accessToken = data.session?.access_token; + */ + const accessToken = 'fake-token'; + + const authorizationHeaders = useMemo( + () => getApiHeaders(accessToken), + [accessToken], + ); + + // Add the authorization token and the app version to the API requests. + useEffect(() => { + const requestInterceptor = AXIOS_INSTANCE.interceptors.request.use( + async (config) => ({ + ...config, + headers: new AxiosHeaders({ + ...config.headers, + ...authorizationHeaders, + }), + }), + ); + + /** + * You should implement your own logic to handle errors here. + * @example + * // If the backend responds with a status code of 426, the app needs to + * // be updated. Here we redirect the user to the update required screen. + * if (error.response?.status === 426) { + * while (router.canGoBack()) { + * router.back(); + * } + * router.replace('/update-required'); + * } + */ + const responseInterceptor = AXIOS_INSTANCE.interceptors.response.use( + (response) => response, + async (error) => { + return Promise.reject(error); + }, + ); + + return () => { + AXIOS_INSTANCE.interceptors.request.eject(requestInterceptor); + AXIOS_INSTANCE.interceptors.response.eject(responseInterceptor); + }; + }, [router, authorizationHeaders]); + + return children; +}; From 0c2b56ff8942808d05c4d2b1c63281c6d3e2393c Mon Sep 17 00:00:00 2001 From: Niki Bizjak Date: Thu, 4 Sep 2025 15:00:00 +0200 Subject: [PATCH 3/8] feat: add example autogenerated orval files --- api/generated/endpoints.msw.ts | 142 +++++++ api/generated/endpoints.ts | 443 +++++++++++++++++++++ api/generated/model/breed.ts | 23 ++ api/generated/model/catFact.ts | 17 + api/generated/model/getBreedsParams.ts | 14 + api/generated/model/getFactsParams.ts | 18 + api/generated/model/getRandomFactParams.ts | 14 + api/generated/model/index.ts | 13 + api/generated/types.ts | 71 ++++ 9 files changed, 755 insertions(+) create mode 100644 api/generated/endpoints.msw.ts create mode 100644 api/generated/endpoints.ts create mode 100644 api/generated/model/breed.ts create mode 100644 api/generated/model/catFact.ts create mode 100644 api/generated/model/getBreedsParams.ts create mode 100644 api/generated/model/getFactsParams.ts create mode 100644 api/generated/model/getRandomFactParams.ts create mode 100644 api/generated/model/index.ts create mode 100644 api/generated/types.ts diff --git a/api/generated/endpoints.msw.ts b/api/generated/endpoints.msw.ts new file mode 100644 index 0000000..e2fc8d3 --- /dev/null +++ b/api/generated/endpoints.msw.ts @@ -0,0 +1,142 @@ +/** + * Generated by orval v7.10.0 🍺 + * Do not edit manually. + * Cat Fact API + * An API for facts about cats + * OpenAPI spec version: 1.0.0 + */ +import { faker } from '@faker-js/faker'; + +import { HttpResponse, delay, http } from 'msw'; + +import type { Breed, CatFact } from './model'; + +export const getGetBreedsResponseMock = (): Breed[] => + Array.from( + { length: faker.number.int({ min: 1, max: 10 }) }, + (_, i) => i + 1, + ).map(() => ({ + breed: faker.helpers.arrayElement([ + faker.string.alpha({ length: { min: 10, max: 20 } }), + undefined, + ]), + country: faker.helpers.arrayElement([ + faker.string.alpha({ length: { min: 10, max: 20 } }), + undefined, + ]), + origin: faker.helpers.arrayElement([ + faker.string.alpha({ length: { min: 10, max: 20 } }), + undefined, + ]), + coat: faker.helpers.arrayElement([ + faker.string.alpha({ length: { min: 10, max: 20 } }), + undefined, + ]), + pattern: faker.helpers.arrayElement([ + faker.string.alpha({ length: { min: 10, max: 20 } }), + undefined, + ]), + })); + +export const getGetRandomFactResponseMock = ( + overrideResponse: Partial = {}, +): CatFact => ({ + fact: faker.helpers.arrayElement([ + faker.string.alpha({ length: { min: 10, max: 20 } }), + undefined, + ]), + length: faker.helpers.arrayElement([ + faker.number.int({ min: undefined, max: undefined }), + undefined, + ]), + ...overrideResponse, +}); + +export const getGetFactsResponseMock = (): CatFact[] => + Array.from( + { length: faker.number.int({ min: 1, max: 10 }) }, + (_, i) => i + 1, + ).map(() => ({ + fact: faker.helpers.arrayElement([ + faker.string.alpha({ length: { min: 10, max: 20 } }), + undefined, + ]), + length: faker.helpers.arrayElement([ + faker.number.int({ min: undefined, max: undefined }), + undefined, + ]), + })); + +export const getGetBreedsMockHandler = ( + overrideResponse?: + | Breed[] + | (( + info: Parameters[1]>[0], + ) => Promise | Breed[]), +) => { + return http.get('*/breeds', async (info) => { + await delay(1000); + + return new HttpResponse( + JSON.stringify( + overrideResponse !== undefined + ? typeof overrideResponse === 'function' + ? await overrideResponse(info) + : overrideResponse + : getGetBreedsResponseMock(), + ), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ); + }); +}; + +export const getGetRandomFactMockHandler = ( + overrideResponse?: + | CatFact + | (( + info: Parameters[1]>[0], + ) => Promise | CatFact), +) => { + return http.get('*/fact', async (info) => { + await delay(1000); + + return new HttpResponse( + JSON.stringify( + overrideResponse !== undefined + ? typeof overrideResponse === 'function' + ? await overrideResponse(info) + : overrideResponse + : getGetRandomFactResponseMock(), + ), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ); + }); +}; + +export const getGetFactsMockHandler = ( + overrideResponse?: + | CatFact[] + | (( + info: Parameters[1]>[0], + ) => Promise | CatFact[]), +) => { + return http.get('*/facts', async (info) => { + await delay(1000); + + return new HttpResponse( + JSON.stringify( + overrideResponse !== undefined + ? typeof overrideResponse === 'function' + ? await overrideResponse(info) + : overrideResponse + : getGetFactsResponseMock(), + ), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ); + }); +}; +export const getCatFactAPIMock = () => [ + getGetBreedsMockHandler(), + getGetRandomFactMockHandler(), + getGetFactsMockHandler(), +]; diff --git a/api/generated/endpoints.ts b/api/generated/endpoints.ts new file mode 100644 index 0000000..1d8c775 --- /dev/null +++ b/api/generated/endpoints.ts @@ -0,0 +1,443 @@ +/** + * Generated by orval v7.10.0 🍺 + * Do not edit manually. + * Cat Fact API + * An API for facts about cats + * OpenAPI spec version: 1.0.0 + */ +import type { + DataTag, + DefinedInitialDataOptions, + DefinedUseQueryResult, + QueryClient, + QueryFunction, + QueryKey, + UndefinedInitialDataOptions, + UseQueryOptions, + UseQueryResult, +} from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; + +import type { + Breed, + CatFact, + GetBreedsParams, + GetFactsParams, + GetRandomFactParams, +} from './model'; + +import type { ErrorType } from '../axios-instance'; +import { customAxios } from '../axios-instance'; + +type SecondParameter unknown> = Parameters[1]; + +/** + * Returns a a list of breeds + * @summary Get a list of breeds + */ +export const getBreeds = ( + params?: GetBreedsParams, + options?: SecondParameter, + signal?: AbortSignal, +) => { + return customAxios( + { url: `/breeds`, method: 'GET', params, signal }, + options, + ); +}; + +export const getGetBreedsQueryKey = (params?: GetBreedsParams) => { + return [`/breeds`, ...(params ? [params] : [])] as const; +}; + +export const getGetBreedsQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + params?: GetBreedsParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetBreedsQueryKey(params); + + const queryFn: QueryFunction>> = ({ + signal, + }) => getBreeds(params, requestOptions, signal); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type GetBreedsQueryResult = NonNullable< + Awaited> +>; +export type GetBreedsQueryError = ErrorType; + +export function useGetBreeds< + TData = Awaited>, + TError = ErrorType, +>( + params: undefined | GetBreedsParams, + options: { + query: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useGetBreeds< + TData = Awaited>, + TError = ErrorType, +>( + params?: GetBreedsParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { queryKey: DataTag }; +export function useGetBreeds< + TData = Awaited>, + TError = ErrorType, +>( + params?: GetBreedsParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { queryKey: DataTag }; +/** + * @summary Get a list of breeds + */ + +export function useGetBreeds< + TData = Awaited>, + TError = ErrorType, +>( + params?: GetBreedsParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { queryKey: DataTag } { + const queryOptions = getGetBreedsQueryOptions(params, options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey; + + return query; +} + +/** + * Returns a random fact + * @summary Get Random Fact + */ +export const getRandomFact = ( + params?: GetRandomFactParams, + options?: SecondParameter, + signal?: AbortSignal, +) => { + return customAxios( + { url: `/fact`, method: 'GET', params, signal }, + options, + ); +}; + +export const getGetRandomFactQueryKey = (params?: GetRandomFactParams) => { + return [`/fact`, ...(params ? [params] : [])] as const; +}; + +export const getGetRandomFactQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + params?: GetRandomFactParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetRandomFactQueryKey(params); + + const queryFn: QueryFunction>> = ({ + signal, + }) => getRandomFact(params, requestOptions, signal); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type GetRandomFactQueryResult = NonNullable< + Awaited> +>; +export type GetRandomFactQueryError = ErrorType; + +export function useGetRandomFact< + TData = Awaited>, + TError = ErrorType, +>( + params: undefined | GetRandomFactParams, + options: { + query: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useGetRandomFact< + TData = Awaited>, + TError = ErrorType, +>( + params?: GetRandomFactParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { queryKey: DataTag }; +export function useGetRandomFact< + TData = Awaited>, + TError = ErrorType, +>( + params?: GetRandomFactParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { queryKey: DataTag }; +/** + * @summary Get Random Fact + */ + +export function useGetRandomFact< + TData = Awaited>, + TError = ErrorType, +>( + params?: GetRandomFactParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { queryKey: DataTag } { + const queryOptions = getGetRandomFactQueryOptions(params, options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey; + + return query; +} + +/** + * Returns a a list of facts + * @summary Get a list of facts + */ +export const getFacts = ( + params?: GetFactsParams, + options?: SecondParameter, + signal?: AbortSignal, +) => { + return customAxios( + { url: `/facts`, method: 'GET', params, signal }, + options, + ); +}; + +export const getGetFactsQueryKey = (params?: GetFactsParams) => { + return [`/facts`, ...(params ? [params] : [])] as const; +}; + +export const getGetFactsQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + params?: GetFactsParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetFactsQueryKey(params); + + const queryFn: QueryFunction>> = ({ + signal, + }) => getFacts(params, requestOptions, signal); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type GetFactsQueryResult = NonNullable< + Awaited> +>; +export type GetFactsQueryError = ErrorType; + +export function useGetFacts< + TData = Awaited>, + TError = ErrorType, +>( + params: undefined | GetFactsParams, + options: { + query: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useGetFacts< + TData = Awaited>, + TError = ErrorType, +>( + params?: GetFactsParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { queryKey: DataTag }; +export function useGetFacts< + TData = Awaited>, + TError = ErrorType, +>( + params?: GetFactsParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { queryKey: DataTag }; +/** + * @summary Get a list of facts + */ + +export function useGetFacts< + TData = Awaited>, + TError = ErrorType, +>( + params?: GetFactsParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { queryKey: DataTag } { + const queryOptions = getGetFactsQueryOptions(params, options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey; + + return query; +} diff --git a/api/generated/model/breed.ts b/api/generated/model/breed.ts new file mode 100644 index 0000000..cd4a140 --- /dev/null +++ b/api/generated/model/breed.ts @@ -0,0 +1,23 @@ +/** + * Generated by orval v7.10.0 🍺 + * Do not edit manually. + * Cat Fact API + * An API for facts about cats + * OpenAPI spec version: 1.0.0 + */ + +/** + * Breed + */ +export interface Breed { + /** Breed */ + breed?: string; + /** Country */ + country?: string; + /** Origin */ + origin?: string; + /** Coat */ + coat?: string; + /** Pattern */ + pattern?: string; +} diff --git a/api/generated/model/catFact.ts b/api/generated/model/catFact.ts new file mode 100644 index 0000000..2da3f3a --- /dev/null +++ b/api/generated/model/catFact.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v7.10.0 🍺 + * Do not edit manually. + * Cat Fact API + * An API for facts about cats + * OpenAPI spec version: 1.0.0 + */ + +/** + * CatFact + */ +export interface CatFact { + /** Fact */ + fact?: string; + /** Length */ + length?: number; +} diff --git a/api/generated/model/getBreedsParams.ts b/api/generated/model/getBreedsParams.ts new file mode 100644 index 0000000..238a417 --- /dev/null +++ b/api/generated/model/getBreedsParams.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.10.0 🍺 + * Do not edit manually. + * Cat Fact API + * An API for facts about cats + * OpenAPI spec version: 1.0.0 + */ + +export type GetBreedsParams = { + /** + * limit the amount of results returned + */ + limit?: number; +}; diff --git a/api/generated/model/getFactsParams.ts b/api/generated/model/getFactsParams.ts new file mode 100644 index 0000000..d5eee93 --- /dev/null +++ b/api/generated/model/getFactsParams.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v7.10.0 🍺 + * Do not edit manually. + * Cat Fact API + * An API for facts about cats + * OpenAPI spec version: 1.0.0 + */ + +export type GetFactsParams = { + /** + * maximum length of returned fact + */ + max_length?: number; + /** + * limit the amount of results returned + */ + limit?: number; +}; diff --git a/api/generated/model/getRandomFactParams.ts b/api/generated/model/getRandomFactParams.ts new file mode 100644 index 0000000..b7d1b7d --- /dev/null +++ b/api/generated/model/getRandomFactParams.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.10.0 🍺 + * Do not edit manually. + * Cat Fact API + * An API for facts about cats + * OpenAPI spec version: 1.0.0 + */ + +export type GetRandomFactParams = { + /** + * maximum length of returned fact + */ + max_length?: number; +}; diff --git a/api/generated/model/index.ts b/api/generated/model/index.ts new file mode 100644 index 0000000..e0281d2 --- /dev/null +++ b/api/generated/model/index.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v7.10.0 🍺 + * Do not edit manually. + * Cat Fact API + * An API for facts about cats + * OpenAPI spec version: 1.0.0 + */ + +export * from './breed'; +export * from './catFact'; +export * from './getBreedsParams'; +export * from './getFactsParams'; +export * from './getRandomFactParams'; diff --git a/api/generated/types.ts b/api/generated/types.ts new file mode 100644 index 0000000..e88d66b --- /dev/null +++ b/api/generated/types.ts @@ -0,0 +1,71 @@ +/** + * Generated by orval v7.10.0 🍺 + * Do not edit manually. + * Cat Fact API + * An API for facts about cats + * OpenAPI spec version: 1.0.0 + */ +import { z as zod } from 'zod'; + +/** + * Returns a a list of breeds + * @summary Get a list of breeds + */ +export const getBreedsQueryParams = zod.object({ + limit: zod + .number() + .optional() + .describe('limit the amount of results returned'), +}); + +export const getBreedsResponseItem = zod + .object({ + breed: zod.string().optional().describe('Breed'), + country: zod.string().optional().describe('Country'), + origin: zod.string().optional().describe('Origin'), + coat: zod.string().optional().describe('Coat'), + pattern: zod.string().optional().describe('Pattern'), + }) + .describe('Breed'); +export const getBreedsResponse = zod.array(getBreedsResponseItem); + +/** + * Returns a random fact + * @summary Get Random Fact + */ +export const getRandomFactQueryParams = zod.object({ + max_length: zod + .number() + .optional() + .describe('maximum length of returned fact'), +}); + +export const getRandomFactResponse = zod + .object({ + fact: zod.string().optional().describe('Fact'), + length: zod.number().optional().describe('Length'), + }) + .describe('CatFact'); + +/** + * Returns a a list of facts + * @summary Get a list of facts + */ +export const getFactsQueryParams = zod.object({ + max_length: zod + .number() + .optional() + .describe('maximum length of returned fact'), + limit: zod + .number() + .optional() + .describe('limit the amount of results returned'), +}); + +export const getFactsResponseItem = zod + .object({ + fact: zod.string().optional().describe('Fact'), + length: zod.number().optional().describe('Length'), + }) + .describe('CatFact'); +export const getFactsResponse = zod.array(getFactsResponseItem); From 71781e7bee032ac83b16e31d38e188cff4aaf2e9 Mon Sep 17 00:00:00 2001 From: Niki Bizjak Date: Thu, 4 Sep 2025 15:02:00 +0200 Subject: [PATCH 4/8] fix: also prettify zod outputs --- orval.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/orval.config.ts b/orval.config.ts index 0f3e971..c5420ed 100644 --- a/orval.config.ts +++ b/orval.config.ts @@ -26,6 +26,7 @@ export default defineConfig({ output: { mode: 'split', client: 'zod', + prettier: true, target: 'api/generated/types.ts', }, input: { From c9edb899aec9ea9e9c711b801ef4daa683d0e407 Mon Sep 17 00:00:00 2001 From: Niki Bizjak Date: Fri, 5 Sep 2025 16:00:28 +0200 Subject: [PATCH 5/8] feat: add example for querying the api --- app/(authenticated)/index.tsx | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/app/(authenticated)/index.tsx b/app/(authenticated)/index.tsx index ba44100..aec17aa 100644 --- a/app/(authenticated)/index.tsx +++ b/app/(authenticated)/index.tsx @@ -5,6 +5,7 @@ import Routes from '@utils/routes'; import { useExampleStore } from '@utils/stores/example-store'; import useToastStore from '@utils/stores/toast-store'; import theme from '@utils/theme'; +import { useGetRandomFact } from 'api/generated/endpoints'; import { router } from 'expo-router'; import { MinusIcon, PlusIcon } from 'lucide-react-native'; import { Text, View } from 'react-native'; @@ -13,6 +14,10 @@ const AuthHomeScreen = () => { const { value, increment, decrement } = useExampleStore(); const { setToast } = useToastStore(); + const { data, isLoading, error, refetch, isFetching } = useGetRandomFact({ + max_length: 140, + }); + return ( { }}> Authenticated Home - {value} - - + + + {value} + + + + Random Cat Fact + {isLoading ? ( + + ) : error ? ( + Unable to load a cat fact. + ) : ( + {data?.data?.fact} + )} + + - From 6b19cdfb6e283372a10f1b93b8ede4ec3d5a208d Mon Sep 17 00:00:00 2001 From: Niki Bizjak Date: Fri, 5 Sep 2025 16:00:39 +0200 Subject: [PATCH 6/8] feat: document orval instead of zodios --- README.md | 91 +++++++++++++++---------------------------------------- 1 file changed, 25 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 5110099..766c317 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Welcome to `react-native-template` 👋, the go-to template for building mobile - [Nativewind Integration](#3-nativewind-integration) - [Full Localization Support](#4-full-localization-support) - [Typed Expo Router Setup](#5-typed-expo-router-setup) - - [Zodius API Client Setup](#6-zodius-api-client-setup) + - [Orval API Client Setup](#6-orval-api-client-setup) - [Custom Utility Hooks](#7-custom-utility-hooks) - [Zustand State Management](#8-zustand-state-management) - [CI/CD Workflow Configuration](#9-cicd-workflow-configuration) @@ -30,7 +30,6 @@ Welcome to `react-native-template` 👋, the go-to template for building mobile - [Loading](#loading) - [FormTextInput](#formtextinput) - [ValidationError](#validationerror) - - [Toaster](#toaster) 7. [Using the Template Effectively](#using-the-template-effectively) - [Recommended Folder Structure](#recommended-folder-structure) - [Development Decision Flow Chart](#development-decision-flow-chart) @@ -186,30 +185,39 @@ export default Routes; // router.push(Routes.artists.artist('1').songs.song('2')); ``` - + -### 6. Zodius API Client Setup 📡 +### 6. Orval API Client Setup 📡 -A pre-configured Zodius API client with Tenstack Query for managing API calls. The `./api` folder includes a fully set up example for GET and POST requests, complete with schemas, definitions, and global error handling through a custom Zodius plugin. +A pre-configured Orval setup generates a typed API client and TanStack Query hooks from your OpenAPI spec. The `api/generated` folder contains endpoints, models, and optional MSW mocks, powered by a custom Axios mutator and React Query. + +```bash +# Generate the client from your OpenAPI schema +yarn gen-api +``` ```typescript -import { Zodios } from '@zodios/core'; -import { ZodiosHooks } from '@zodios/react'; -import apiErrorPlugin from './api-error-plugin'; -import exampleApi from './example'; +// Use generated React Query hooks +import { useGetRandomFact, useGetFacts } from 'api/generated/endpoints'; -const API_URL = process.env.EXPO_PUBLIC_API_URL || ''; +const { data, isLoading, error } = useGetRandomFact({ max_length: 140 }); +``` -// Zodios API client -const apiClient = new Zodios(API_URL, [...exampleApi]); +```typescript +// Imperative request (without a hook) +import { getRandomFact } from 'api/generated/endpoints'; -// Apply global error handling -apiClient.use(apiErrorPlugin); +const { data } = await getRandomFact({ max_length: 140 }); +``` -// Zodios hooks for react -const api = new ZodiosHooks('exampleApi', apiClient); +```typescript +// Global headers and base URL are configured via Axios +// Base URL: env.EXPO_PUBLIC_API_URL (see api/axios-instance.ts) +// Headers: injected by ApiProvider (see utils/providers/api-provider.ts) +import { ApiProvider } from '@utils/providers/api-provider'; -export { api, apiClient }; +// Wrap your app (e.g., in your root layout) +{children}; ``` @@ -623,55 +631,6 @@ import { WithValidationError } from '@components/ValidationError'; The `ValidationError` and `WithValidationError` components help maintain a clean UI by only showing error messages when necessary, enhancing the user experience with clear feedback. - - -## Toaster 🍞 - -The `Toaster` component is a dynamic and interactive toast notification system designed to provide immediate feedback to users. It's connected to a store for global state management and comes with an API plugin for automatic display on API events. - -### Component Features - -- **Gesture Support**: Users can dismiss the toast by dragging it down, thanks to the integrated gesture handler. -- **Animated Visibility**: Uses `react-native-reanimated` for smooth show and hide animations. -- **Safe Area Handling**: Accounts for device safe areas, ensuring the toast is always visible and accessible. -- **Custom Icons**: Displays icons for error, success, or information based on the toast type. - -### How It Works - -The `Toaster` component listens to the toast state from `useToastStore`. When a toast is set, it animates into view. It can be dismissed with a drag gesture or by pressing the 'Dismiss' button. - -### Usage - -The `Toaster` component does not need to be manually managed; it works by setting the toast state through the `useToastStore` actions: - -```javascript -useToastStore.getState().setToast({ - type: 'success', - message: 'Your changes have been saved!', -}); -``` - -### Customizing the Toaster - -While the `Toaster` itself does not require props, you can customize the animations and styles directly within the component's file if needed. - -### API Integration - -`apiToastPlugin` is set up to automatically display toasts in response to API calls, making use of the `ZodiosPlugin` system. It provides feedback for errors and successes, skipping certain URLs or GET requests as configured. - -### Example of Plugin Usage - -Simply add the `apiToastPlugin` to your Zodios API client configuration: - -```javascript -const apiClient = new Zodios(API_URL, [ - /* ...endpoints */ -]); -apiClient.use(apiToastPlugin); -``` - -The `Toaster` provides a smooth, user-friendly notification mechanism that enhances the interactivity of the application, keeping users informed with minimal disruption. - ## More Components Comming Soon... 🎉 Stay tuned for more components and features that will be added to the template in the future. We're committed to providing a comprehensive set of tools and solutions to help you build your mobile applications with ease. From 1221e4ca8ca5561df2261f53bccf72d9e2dac54e Mon Sep 17 00:00:00 2001 From: Niki Bizjak Date: Mon, 8 Sep 2025 08:44:47 +0200 Subject: [PATCH 7/8] fix: remove orval mock --- api/generated/endpoints.msw.ts | 142 --------------------------------- orval.config.ts | 2 +- 2 files changed, 1 insertion(+), 143 deletions(-) delete mode 100644 api/generated/endpoints.msw.ts diff --git a/api/generated/endpoints.msw.ts b/api/generated/endpoints.msw.ts deleted file mode 100644 index e2fc8d3..0000000 --- a/api/generated/endpoints.msw.ts +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * Cat Fact API - * An API for facts about cats - * OpenAPI spec version: 1.0.0 - */ -import { faker } from '@faker-js/faker'; - -import { HttpResponse, delay, http } from 'msw'; - -import type { Breed, CatFact } from './model'; - -export const getGetBreedsResponseMock = (): Breed[] => - Array.from( - { length: faker.number.int({ min: 1, max: 10 }) }, - (_, i) => i + 1, - ).map(() => ({ - breed: faker.helpers.arrayElement([ - faker.string.alpha({ length: { min: 10, max: 20 } }), - undefined, - ]), - country: faker.helpers.arrayElement([ - faker.string.alpha({ length: { min: 10, max: 20 } }), - undefined, - ]), - origin: faker.helpers.arrayElement([ - faker.string.alpha({ length: { min: 10, max: 20 } }), - undefined, - ]), - coat: faker.helpers.arrayElement([ - faker.string.alpha({ length: { min: 10, max: 20 } }), - undefined, - ]), - pattern: faker.helpers.arrayElement([ - faker.string.alpha({ length: { min: 10, max: 20 } }), - undefined, - ]), - })); - -export const getGetRandomFactResponseMock = ( - overrideResponse: Partial = {}, -): CatFact => ({ - fact: faker.helpers.arrayElement([ - faker.string.alpha({ length: { min: 10, max: 20 } }), - undefined, - ]), - length: faker.helpers.arrayElement([ - faker.number.int({ min: undefined, max: undefined }), - undefined, - ]), - ...overrideResponse, -}); - -export const getGetFactsResponseMock = (): CatFact[] => - Array.from( - { length: faker.number.int({ min: 1, max: 10 }) }, - (_, i) => i + 1, - ).map(() => ({ - fact: faker.helpers.arrayElement([ - faker.string.alpha({ length: { min: 10, max: 20 } }), - undefined, - ]), - length: faker.helpers.arrayElement([ - faker.number.int({ min: undefined, max: undefined }), - undefined, - ]), - })); - -export const getGetBreedsMockHandler = ( - overrideResponse?: - | Breed[] - | (( - info: Parameters[1]>[0], - ) => Promise | Breed[]), -) => { - return http.get('*/breeds', async (info) => { - await delay(1000); - - return new HttpResponse( - JSON.stringify( - overrideResponse !== undefined - ? typeof overrideResponse === 'function' - ? await overrideResponse(info) - : overrideResponse - : getGetBreedsResponseMock(), - ), - { status: 200, headers: { 'Content-Type': 'application/json' } }, - ); - }); -}; - -export const getGetRandomFactMockHandler = ( - overrideResponse?: - | CatFact - | (( - info: Parameters[1]>[0], - ) => Promise | CatFact), -) => { - return http.get('*/fact', async (info) => { - await delay(1000); - - return new HttpResponse( - JSON.stringify( - overrideResponse !== undefined - ? typeof overrideResponse === 'function' - ? await overrideResponse(info) - : overrideResponse - : getGetRandomFactResponseMock(), - ), - { status: 200, headers: { 'Content-Type': 'application/json' } }, - ); - }); -}; - -export const getGetFactsMockHandler = ( - overrideResponse?: - | CatFact[] - | (( - info: Parameters[1]>[0], - ) => Promise | CatFact[]), -) => { - return http.get('*/facts', async (info) => { - await delay(1000); - - return new HttpResponse( - JSON.stringify( - overrideResponse !== undefined - ? typeof overrideResponse === 'function' - ? await overrideResponse(info) - : overrideResponse - : getGetFactsResponseMock(), - ), - { status: 200, headers: { 'Content-Type': 'application/json' } }, - ); - }); -}; -export const getCatFactAPIMock = () => [ - getGetBreedsMockHandler(), - getGetRandomFactMockHandler(), - getGetFactsMockHandler(), -]; diff --git a/orval.config.ts b/orval.config.ts index c5420ed..a8750d7 100644 --- a/orval.config.ts +++ b/orval.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ schemas: 'api/generated/model', client: 'react-query', clean: true, - mock: true, + mock: false, prettier: true, override: { mutator: { From 96be48d370c8fb09c4bc4a9c4eb3ac0e88aaf2d0 Mon Sep 17 00:00:00 2001 From: Niki Bizjak Date: Mon, 8 Sep 2025 08:45:34 +0200 Subject: [PATCH 8/8] fix: actions to use the new command format --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 414719c..ec24db4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -46,4 +46,4 @@ jobs: - name: Install Dependencies run: yarn install --frozen-lockfile - name: Run Prettier through format - run: yarn format-check + run: yarn format:check