Skip to content

Commit

Permalink
Orval implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
andresgnlez committed May 7, 2024
1 parent 5537f38 commit 96db1c2
Show file tree
Hide file tree
Showing 19 changed files with 3,984 additions and 87 deletions.
1 change: 1 addition & 0 deletions .husky/post-merge
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cd ./client && yarn types && yarn check-types && git add src/types/generated/
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cd ./client && yarn types && yarn check-types && git add src/types/generated/
2 changes: 1 addition & 1 deletion client/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ module.exports = {
'no-console': ['warn'],
'no-debugger': ['warn'],
},
ignorePatterns: ['*.md'],
ignorePatterns: ['*.md', 'src/types/generated/*'],
};
13 changes: 8 additions & 5 deletions client/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@ COPY --chown=$USER:$USER .yarn ./.yarn
COPY --chown=$USER:$USER package.json yarn.lock .yarnrc.yml ./
RUN yarn install --immutable

# NextJS project folders

# ? NextJS project folders and files
COPY --chown=$USER:$USER src ./src
COPY --chown=$USER:$USER public ./public

# NextJS required files
COPY --chown=$USER:$USER next.config.js local.d.ts \
postcss.config.cjs tailwind.config.ts entrypoint.sh \
tsconfig.json .browserlistrc .eslintrc.cjs .prettierrc.cjs ./
postcss.config.cjs tailwind.config.ts entrypoint.sh \
tsconfig.json .browserlistrc .eslintrc.cjs .prettierrc.cjs \
orval.config.ts ./

# ? generates types based on API's schemas
RUN yarn types

RUN yarn build

Expand Down
31 changes: 31 additions & 0 deletions client/orval.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Config } from '@orval/core';

export default {
landgriffon: {
output: {
mode: 'tags',
client: 'react-query',
target: './src/types/generated/api.ts',
mock: false,
clean: true,
prettier: true,
override: {
mutator: {
path: './src/services/orval.ts',
name: 'API',
},
query: {
useQuery: true,
useMutation: true,
signal: true,
},
},
},
input: {
target: '../api/swagger-spec.json',
filters: {
tags: ['User'],
},
},
},
} satisfies Config;
6 changes: 5 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
"start": "next start",
"lint": "next lint",
"check-types": "tsc",
"types": "orval --config ./orval.config.ts",
"prettier": "prettier --config .prettierrc.json --check --write \"./src/**/*.{tsx,jsx,ts,js}\"",
"cypress": "cypress open",
"cypress:headless": "cypress run --browser chrome",
"test:e2e": "./node_modules/.bin/cypress run --headless --browser chrome",
"test": "start-server-and-test 'yarn build && yarn start' http://localhost:3000/auth/signin 'nyc --reporter nyc-report-lcov-absolute yarn cypress:headless'"
"test": "start-server-and-test 'yarn build && yarn start' http://localhost:3000/auth/signin 'nyc --reporter nyc-report-lcov-absolute yarn cypress:headless'",
"prepare": "cd .. && husky"
},
"dependencies": {
"@date-fns/utc": "1.1.1",
Expand Down Expand Up @@ -116,10 +118,12 @@
"eslint-config-next": "14.2.2",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-prettier": "5.1.3",
"husky": "9.0.11",
"istanbul-reports": "3.0.0",
"jiti": "1.21.0",
"nyc": "15.1.0",
"nyc-report-lcov-absolute": "1.0.0",
"orval": "6.27.1",
"prettier": "^3.1.1",
"prettier-plugin-tailwindcss": "^0.5.10",
"start-server-and-test": "1.14.0",
Expand Down
12 changes: 10 additions & 2 deletions client/src/containers/admin/data-upload-error/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { useCallback, useState } from 'react';
import { format } from 'date-fns';

import { useUpdateTask, useTaskErrors } from 'hooks/tasks';
import { useProfile } from 'hooks/profile';
import UploadIcon from 'components/icons/upload-icon';
import Disclaimer from 'components/disclaimer';
import Button from 'components/button';
import { triggerCsvDownload } from 'utils/csv-download';
import { useUsersControllerUserMetadata } from '@/types/generated/user';

import type { Task } from 'types';
import type { DisclaimerProps } from 'components/disclaimer/component';
Expand All @@ -25,7 +25,15 @@ type DataUploadErrorProps = {
const DataUploadError: React.FC<DataUploadErrorProps> = ({ task }) => {
const [open, setOpen] = useState(true);
const updateTask = useUpdateTask();
const { data: profile, isLoading: profileIsLoading } = useProfile();

const { data: profile, isLoading: profileIsLoading } = useUsersControllerUserMetadata({
query: {
select: (data) => ({
id: data?.data?.id,
...data?.data?.attributes,
}),
},
});

const handleDismiss = useCallback(() => {
updateTask.mutate({ id: task.id, data: { dismissedBy: profile?.id } });
Expand Down
9 changes: 7 additions & 2 deletions client/src/containers/update-profile-form/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { yupResolver } from '@hookform/resolvers/yup';
import { useForm } from 'react-hook-form';
import toast from 'react-hot-toast';

import { useProfile, useUpdateProfile } from 'hooks/profile';
import { useUpdateProfile } from 'hooks/profile';
import { Label, Input } from 'components/forms';
import { Button } from 'components/button';
import { useUsersControllerUserMetadata } from '@/types/generated/user';

import type { ProfilePayload, ErrorResponse } from 'types';

Expand All @@ -25,7 +26,11 @@ const UserDataForm: React.FC = () => {
resolver: yupResolver(schemaValidation),
});

const user = useProfile();
const user = useUsersControllerUserMetadata({
query: {
select: (data) => data?.data?.attributes,
},
});
const updateProfile = useUpdateProfile();

const handleEditUserData = useCallback(
Expand Down
3 changes: 1 addition & 2 deletions client/src/containers/user-avatar/component.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Avatar from 'components/avatar/component';
import StringAvatar from 'components/string-avatar/component';

import type { User } from 'types';
import { User } from '@/types/generated/api.schemas';

type UserAvatarProps = {
user: User;
Expand Down
8 changes: 6 additions & 2 deletions client/src/containers/user-dropdown/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { FloatingPortal } from '@floating-ui/react';

import Loading from 'components/loading';
import UserAvatar from 'containers/user-avatar';
import { useProfile } from 'hooks/profile';
import getUserFullName from 'utils/user-full-name';
import { useUsersControllerUserMetadata } from '@/types/generated/user';

const MENU_ITEM_CLASSNAME =
'block w-full py-2 px-4 text-sm text-left text-gray-900 h-9 hover:bg-navy-50 focus-visible:outline-navy-50';
Expand All @@ -20,7 +20,11 @@ const UserDropdown: React.FC = () => {
middleware: [offset({ crossAxis: 20, mainAxis: 10 }), shift()],
});

const { data: user, status } = useProfile();
const { data: user, status } = useUsersControllerUserMetadata({
query: {
select: (data) => data?.data?.attributes,
},
});

const handleSignOut = useCallback(() => signOut({ callbackUrl: '/auth/signin' }), []);

Expand Down
22 changes: 13 additions & 9 deletions client/src/hooks/permissions/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { RoleName } from './enums';

import { useProfile } from 'hooks/profile';

import type { Permission } from './enums';
import { useUsersControllerUserMetadata } from '@/types/generated/user';
import { RoleName, Permission } from '@/types/generated/api.schemas';

export function usePermissions() {
const { data, isLoading } = useProfile();
const { data, isLoading } = useUsersControllerUserMetadata({
query: {
select: (data) => ({
id: data?.data?.id,
...data?.data?.attributes,
}),
},
});

const roles: RoleName[] = [];
const permissions: Permission[] = [];
const permissions: Permission['action'][] = [];

data?.roles?.forEach((role) => {
roles.push(role.name);
Expand All @@ -27,15 +31,15 @@ export function usePermissions() {
* Function to determine if a user is allowed to perform an action.
* For CREATE actions add param needsCreatorPermission=false, so it will not check the 'creatorId'
*/
const hasPermission = (permissionName: Permission, creatorId?: string) => {
const hasPermission = (permissionName: Permission['action'], creatorId?: string) => {
// The user has permission
let permission = permissions?.includes(permissionName);
// The user is creator of the entity and has permission (for delete and update actions)
if (!!creatorId) {
permission = permission && creatorId === data?.id;
}
// Admin always has permission
return !isLoading && (hasRole(RoleName.ADMIN) || permission);
return !isLoading && (hasRole(RoleName.admin) || permission);
};

return { roles, hasRole, permissions, hasPermission, isLoading };
Expand Down
21 changes: 1 addition & 20 deletions client/src/hooks/profile/index.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,11 @@
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
import { useSession } from 'next-auth/react';
import { useQueryClient, useMutation } from '@tanstack/react-query';

import { apiRawServiceWithoutAuth, apiService } from 'services/api';
import { authService } from 'services/authentication';

import type { User, PasswordPayload, ErrorResponse, ProfilePayload } from 'types';
import type { AxiosPromise } from 'axios';

export function useProfile() {
const { data: session } = useSession();

return useQuery(['profile', session.accessToken], () =>
apiService
.request<{
data: User & {
id: string;
type: 'users';
};
}>({
method: 'GET',
url: '/users/me',
})
.then(({ data }) => data?.data),
);
}

export function useUpdateProfile() {
const queryClient = useQueryClient();

Expand Down
11 changes: 9 additions & 2 deletions client/src/pages/profile/users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import Search from 'components/search';
import Table from 'components/table';
import { DEFAULT_PAGE_SIZES } from 'components/table/pagination/constants';
import UserAvatar from 'containers/user-avatar';
import { useProfile } from 'hooks/profile';
import EditUser from 'containers/edit-user';
import getUserFullName from 'utils/user-full-name';
import { usePermissions } from 'hooks/permissions';
import { RoleName } from 'hooks/permissions/enums';
import Modal from 'components/modal';
import UserForm from 'containers/edit-user/user-form';
import getQueryClient from '@/lib/react-query';
import { useUsersControllerUserMetadata } from '@/types/generated/user';

import type { User } from 'types';
import type { TableProps } from 'components/table/component';
Expand All @@ -40,7 +40,14 @@ const AdminUsersPage: React.FC = () => {
return sorting.map((sort) => (sort.desc ? `-${sort.id}` : sort.id)).join(',') || null;
}, [sorting]);

const { data: user, isFetching: isFetchingUser } = useProfile();
const { data: user, isFetching: isFetchingUser } = useUsersControllerUserMetadata({
query: {
select: (data) => ({
id: data?.data?.id,
...data?.data?.attributes,
}),
},
});

const searchTerm = useMemo(
() => (typeof query.search === 'string' ? query.search : null),
Expand Down
8 changes: 4 additions & 4 deletions client/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import toast from 'react-hot-toast';
import { env } from '@/env.mjs';

import type { ApiError, ErrorResponse } from 'types';
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';

/**
* API service require to be authenticated.
Expand All @@ -20,14 +20,14 @@ const defaultConfig: AxiosRequestConfig = {
headers: { 'Content-Type': 'application/json' },
};

const authorizedRequest = async (config) => {
export const authorizedRequest = async (config: InternalAxiosRequestConfig) => {
const session = await getSession();
config.headers['Authorization'] = `Bearer ${session?.accessToken}`;

return config;
};

const onResponseError = async (error: unknown) => {
export const onResponseError = async (error: unknown) => {
if (axios.isAxiosError<ApiError>(error)) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
if (error.response.status === 401) {
Expand All @@ -41,7 +41,7 @@ const onResponseError = async (error: unknown) => {
// This endpoint by default will deserialize the data
export const apiService = axios.create(defaultConfig);

const responseDeserializer = (response: AxiosResponse) => ({
export const responseDeserializer = (response: AxiosResponse) => ({
...response,
data: {
...response.data,
Expand Down
34 changes: 34 additions & 0 deletions client/src/services/orval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Axios, { AxiosRequestConfig, AxiosError } from 'axios';

import { authorizedRequest, onResponseError } from './api';

export const apiService = Axios.create({
baseURL: `${process.env.NEXT_PUBLIC_API_URL}`,
headers: { 'Content-Type': 'application/json' },
});

apiService.interceptors.request.use(authorizedRequest, onResponseError);

// add a second `options` argument here if you want to pass extra options to each generated query
export const API = <T>(config: AxiosRequestConfig, options?: AxiosRequestConfig): Promise<T> => {
const source = Axios.CancelToken.source();
const promise = apiService({
...config,
...options,
cancelToken: source.token,
}).then(({ data }) => data);

// @ts-expect-error cancel is not part of the promise
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<Error> = AxiosError<Error>;

export type BodyType<BodyData> = BodyData;

export default API;
Loading

0 comments on commit 96db1c2

Please sign in to comment.