feat(ui): api layer refactor

*migrate from `openapi-typescript-codegen` to `openapi-typescript` and `openapi-fetch`*

`openapi-typescript-codegen` is not very actively maintained - it's been over a year since the last update.
`openapi-typescript` and `openapi-fetch` are part of the actively maintained repo. key differences:

- provides a `fetch` client instead of `axios`, which means we need to be a bit more verbose with typing thunks
- fetch client is created at runtime and has a very nice typescript DX
- generates a single file with all types in it, from which we then extract individual types. i don't like how verbose this is, but i do like how it is more explicit.
- removed npm api generation scripts - now we have a single `typegen` script

overall i have more confidence in this new library.

*use nanostores for api base and token*

very simple reactive store for api base url and token. this was suggested in the `openapi-fetch` docs and i quite like the strategy.

*organise rtk-query api*

split out each endpoint (models, images, boards, boardImages) into their own api extensions. tidy!
This commit is contained in:
psychedelicious
2023-06-23 19:14:13 +10:00
parent 8137a99981
commit e386b5dc53
267 changed files with 6257 additions and 7008 deletions

View File

@ -0,0 +1,66 @@
import { OffsetPaginatedResults_ImageDTO_ } from 'services/api/types';
import { api } from '..';
import { paths } from '../schema';
type ListBoardImagesArg =
paths['/api/v1/board_images/{board_id}']['get']['parameters']['path'] &
paths['/api/v1/board_images/{board_id}']['get']['parameters']['query'];
type AddImageToBoardArg =
paths['/api/v1/board_images/']['post']['requestBody']['content']['application/json'];
type RemoveImageFromBoardArg =
paths['/api/v1/board_images/']['delete']['requestBody']['content']['application/json'];
export const boardImagesApi = api.injectEndpoints({
endpoints: (build) => ({
/**
* Board Images Queries
*/
listBoardImages: build.query<
OffsetPaginatedResults_ImageDTO_,
ListBoardImagesArg
>({
query: ({ board_id, offset, limit }) => ({
url: `board_images/${board_id}`,
method: 'DELETE',
body: { offset, limit },
}),
}),
/**
* Board Images Mutations
*/
addImageToBoard: build.mutation<void, AddImageToBoardArg>({
query: ({ board_id, image_name }) => ({
url: `board_images/`,
method: 'POST',
body: { board_id, image_name },
}),
invalidatesTags: (result, error, arg) => [
{ type: 'Board', id: arg.board_id },
{ type: 'Image', id: arg.image_name },
],
}),
removeImageFromBoard: build.mutation<void, RemoveImageFromBoardArg>({
query: ({ board_id, image_name }) => ({
url: `board_images/`,
method: 'DELETE',
body: { board_id, image_name },
}),
invalidatesTags: (result, error, arg) => [
{ type: 'Board', id: arg.board_id },
{ type: 'Image', id: arg.image_name },
],
}),
}),
});
export const {
useAddImageToBoardMutation,
useRemoveImageFromBoardMutation,
useListBoardImagesQuery,
} = boardImagesApi;

View File

@ -0,0 +1,99 @@
import { BoardDTO, OffsetPaginatedResults_BoardDTO_ } from 'services/api/types';
import { ApiFullTagDescription, LIST_TAG, api } from '..';
import { paths } from '../schema';
type ListBoardsArg = NonNullable<
paths['/api/v1/boards/']['get']['parameters']['query']
>;
type UpdateBoardArg =
paths['/api/v1/boards/{board_id}']['patch']['parameters']['path'] & {
changes: paths['/api/v1/boards/{board_id}']['patch']['requestBody']['content']['application/json'];
};
export const boardsApi = api.injectEndpoints({
endpoints: (build) => ({
/**
* Boards Queries
*/
listBoards: build.query<OffsetPaginatedResults_BoardDTO_, ListBoardsArg>({
query: (arg) => ({ url: 'boards/', params: arg }),
providesTags: (result, error, arg) => {
// any list of boards
const tags: ApiFullTagDescription[] = [{ id: 'Board', type: LIST_TAG }];
if (result) {
// and individual tags for each board
tags.push(
...result.items.map(({ board_id }) => ({
type: 'Board' as const,
id: board_id,
}))
);
}
return tags;
},
}),
listAllBoards: build.query<Array<BoardDTO>, void>({
query: () => ({
url: 'boards/',
params: { all: true },
}),
providesTags: (result, error, arg) => {
// any list of boards
const tags: ApiFullTagDescription[] = [{ id: 'Board', type: LIST_TAG }];
if (result) {
// and individual tags for each board
tags.push(
...result.map(({ board_id }) => ({
type: 'Board' as const,
id: board_id,
}))
);
}
return tags;
},
}),
/**
* Boards Mutations
*/
createBoard: build.mutation<BoardDTO, string>({
query: (board_name) => ({
url: `boards/`,
method: 'POST',
params: { board_name },
}),
invalidatesTags: [{ id: 'Board', type: LIST_TAG }],
}),
updateBoard: build.mutation<BoardDTO, UpdateBoardArg>({
query: ({ board_id, changes }) => ({
url: `boards/${board_id}`,
method: 'PATCH',
body: changes,
}),
invalidatesTags: (result, error, arg) => [
{ type: 'Board', id: arg.board_id },
],
}),
deleteBoard: build.mutation<void, string>({
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
invalidatesTags: (result, error, arg) => [{ type: 'Board', id: arg }],
}),
}),
});
export const {
useListBoardsQuery,
useListAllBoardsQuery,
useCreateBoardMutation,
useUpdateBoardMutation,
useDeleteBoardMutation,
} = boardsApi;

View File

@ -0,0 +1,22 @@
import { ApiFullTagDescription, api } from '..';
import { ImageDTO } from '../types';
export const imagesApi = api.injectEndpoints({
endpoints: (build) => ({
/**
* Image Queries
*/
getImageDTO: build.query<ImageDTO, string>({
query: (image_name) => ({ url: `images/${image_name}/metadata` }),
providesTags: (result, error, arg) => {
const tags: ApiFullTagDescription[] = [{ type: 'Image', id: arg }];
if (result?.board_id) {
tags.push({ type: 'Board', id: result.board_id });
}
return tags;
},
}),
}),
});
export const { useGetImageDTOQuery } = imagesApi;

View File

@ -0,0 +1,52 @@
import { ModelsList } from 'services/api/types';
import { EntityState, createEntityAdapter } from '@reduxjs/toolkit';
import { keyBy } from 'lodash-es';
import { ApiFullTagDescription, LIST_TAG, api } from '..';
import { paths } from '../schema';
type ModelConfig = ModelsList['models'][number];
type ListModelsArg = NonNullable<
paths['/api/v1/models/']['get']['parameters']['query']
>;
const modelsAdapter = createEntityAdapter<ModelConfig>({
selectId: (model) => getModelId(model),
sortComparer: (a, b) => a.name.localeCompare(b.name),
});
const getModelId = ({ base_model, type, name }: ModelConfig) =>
`${base_model}/${type}/${name}`;
export const modelsApi = api.injectEndpoints({
endpoints: (build) => ({
listModels: build.query<EntityState<ModelConfig>, ListModelsArg>({
query: (arg) => ({ url: 'models/', params: arg }),
providesTags: (result, error, arg) => {
// any list of boards
const tags: ApiFullTagDescription[] = [{ id: 'Model', type: LIST_TAG }];
if (result) {
// and individual tags for each board
tags.push(
...result.ids.map((id) => ({
type: 'Model' as const,
id,
}))
);
}
return tags;
},
transformResponse: (response: ModelsList, meta, arg) => {
return modelsAdapter.setAll(
modelsAdapter.getInitialState(),
keyBy(response.models, getModelId)
);
},
}),
}),
});
export const { useListModelsQuery } = modelsApi;