CRUD API Service¶
Универсальный RTK Query клиент для работы с любой DotORM-моделью.
Концепция¶
Backend автоматически генерирует CRUD-эндпоинты для каждой модели. Frontend использует один crudApi для работы со всеми моделями — имя модели передаётся как параметр.
graph LR
C[Component] -->|useSearchQuery| RTK[RTK Query]
RTK -->|POST /products/search| BE[Backend CRUD Auto]
BE -->|DotORM| DB[(PostgreSQL)]
style RTK fill:#764abc,color:white
style BE fill:#009688,color:white
Типы¶
frontend/src/services/api/crudTypes.ts
// Параметры поиска
interface GetListParams {
model: string; // имя модели ("products", "users")
filter?: Array<[string, string, any]>; // [field, operator, value]
fields?: string[];
order?: string;
sort?: 'asc' | 'desc';
start?: number;
end?: number;
limit?: number;
}
// Результат поиска
interface GetListResult<T = FaraRecord> {
data: T[];
total: number;
}
// Параметры чтения
interface ReadParams {
model: string;
id: number;
fields?: string[];
}
// Параметры создания
interface CreateParams {
model: string;
data: Record<string, any>;
}
// Параметры редактирования
interface EditParams {
model: string;
id: number;
data: Record<string, any>;
}
// Параметры удаления
interface DeleteListParams {
model: string;
ids: number[];
}
Хуки¶
RTK Query автоматически генерирует хуки из endpoints:
useSearchQuery¶
import { crudApi } from '@services/api/crudApi';
function PartnerList() {
const { data, isLoading, error, refetch } = crudApi.useSearchQuery({
model: 'partners',
filter: [
['active', '=', true],
['name', 'ilike', '%john%'],
],
fields: ['id', 'name', 'email', 'phone'],
order: 'name',
sort: 'asc',
limit: 50,
});
if (isLoading) return <Loader />;
if (error) return <Alert color="red">Ошибка загрузки</Alert>;
return (
<DataTable
records={data?.data ?? []}
columns={[
{ accessor: 'name' },
{ accessor: 'email' },
{ accessor: 'phone' },
]}
/>
);
}
useReadQuery¶
function ProductDetail({ id }: { id: number }) {
const { data } = crudApi.useReadQuery({
model: 'products',
id,
fields: ['id', 'name', 'price', 'description', 'category_id'],
});
return <ProductForm initialValues={data?.data} />;
}
useCreateMutation¶
function CreatePartnerForm() {
const [create, { isLoading }] = crudApi.useCreateMutation();
const handleSubmit = async (values: PartnerForm) => {
const result = await create({
model: 'partners',
data: values,
}).unwrap();
notifications.show({
title: 'Создано',
message: `Партнёр #${result.data.id} создан`,
});
};
return <Form onSubmit={handleSubmit} loading={isLoading} />;
}
useEditMutation¶
function EditProductForm({ id }: { id: number }) {
const [edit, { isLoading }] = crudApi.useEditMutation();
const handleSubmit = async (values: Partial<Product>) => {
await edit({
model: 'products',
id,
data: values,
}).unwrap();
};
return <Form onSubmit={handleSubmit} loading={isLoading} />;
}
useDeleteListMutation¶
function DeleteButton({ ids }: { ids: number[] }) {
const [deleteList] = crudApi.useDeleteListMutation();
return (
<Button
color="red"
onClick={() => deleteList({ model: 'products', ids })}
>
Удалить ({ids.length})
</Button>
);
}
Кэширование и инвалидация¶
RTK Query автоматически кэширует ответы и инвалидирует при мутациях:
// crudApi endpoints
search: build.query({
// ...
providesTags: (result, error, arg) =>
result
? [{ type: arg.model, id: 'LIST' }]
: [],
}),
create: build.mutation({
// ...
invalidatesTags: (result, error, arg) => [
{ type: arg.model, id: 'LIST' }, // (1)!
],
}),
- После создания записи RTK Query автоматически перезапросит все
search-запросы для этой модели.