@astral/mobx-query 1.8.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/MobxQuery/MobxQuery.d.ts +44 -0
  2. package/MobxQuery/MobxQuery.js +48 -0
  3. package/README.md +1048 -287
  4. package/Sets/InfiniteQuerySet/InfiniteQuerySet.d.ts +60 -0
  5. package/Sets/InfiniteQuerySet/InfiniteQuerySet.js +71 -0
  6. package/Sets/InfiniteQuerySet/index.d.ts +1 -0
  7. package/Sets/InfiniteQuerySet/index.js +1 -0
  8. package/Sets/MutationSet/MutationSet.d.ts +32 -0
  9. package/Sets/MutationSet/MutationSet.js +31 -0
  10. package/Sets/MutationSet/index.d.ts +1 -0
  11. package/Sets/MutationSet/index.js +1 -0
  12. package/Sets/QuerySet/QuerySet.d.ts +64 -0
  13. package/Sets/QuerySet/QuerySet.js +72 -0
  14. package/Sets/QuerySet/index.d.ts +1 -0
  15. package/Sets/QuerySet/index.js +1 -0
  16. package/Sets/index.d.ts +3 -0
  17. package/Sets/index.js +3 -0
  18. package/Sets/utils/generateQuerySetBaseKey/generateQuerySetBaseKey.d.ts +4 -0
  19. package/Sets/utils/generateQuerySetBaseKey/generateQuerySetBaseKey.js +5 -0
  20. package/Sets/utils/generateQuerySetBaseKey/index.d.ts +1 -0
  21. package/Sets/utils/generateQuerySetBaseKey/index.js +1 -0
  22. package/Sets/utils/generateQuerySetKeys/generateQuerySetKeys.d.ts +9 -0
  23. package/Sets/utils/generateQuerySetKeys/generateQuerySetKeys.js +20 -0
  24. package/Sets/utils/generateQuerySetKeys/index.d.ts +1 -0
  25. package/Sets/utils/generateQuerySetKeys/index.js +1 -0
  26. package/Sets/utils/index.d.ts +2 -0
  27. package/Sets/utils/index.js +2 -0
  28. package/index.d.ts +2 -1
  29. package/index.js +1 -0
  30. package/node/MobxQuery/MobxQuery.d.ts +44 -0
  31. package/node/MobxQuery/MobxQuery.js +48 -0
  32. package/node/Sets/InfiniteQuerySet/InfiniteQuerySet.d.ts +60 -0
  33. package/node/Sets/InfiniteQuerySet/InfiniteQuerySet.js +75 -0
  34. package/node/Sets/InfiniteQuerySet/index.d.ts +1 -0
  35. package/node/Sets/InfiniteQuerySet/index.js +17 -0
  36. package/node/Sets/MutationSet/MutationSet.d.ts +32 -0
  37. package/node/Sets/MutationSet/MutationSet.js +35 -0
  38. package/node/Sets/MutationSet/index.d.ts +1 -0
  39. package/node/Sets/MutationSet/index.js +17 -0
  40. package/node/Sets/QuerySet/QuerySet.d.ts +64 -0
  41. package/node/Sets/QuerySet/QuerySet.js +76 -0
  42. package/node/Sets/QuerySet/index.d.ts +1 -0
  43. package/node/Sets/QuerySet/index.js +17 -0
  44. package/node/Sets/index.d.ts +3 -0
  45. package/node/Sets/index.js +19 -0
  46. package/node/Sets/utils/generateQuerySetBaseKey/generateQuerySetBaseKey.d.ts +4 -0
  47. package/node/Sets/utils/generateQuerySetBaseKey/generateQuerySetBaseKey.js +12 -0
  48. package/node/Sets/utils/generateQuerySetBaseKey/index.d.ts +1 -0
  49. package/node/Sets/utils/generateQuerySetBaseKey/index.js +17 -0
  50. package/node/Sets/utils/generateQuerySetKeys/generateQuerySetKeys.d.ts +9 -0
  51. package/node/Sets/utils/generateQuerySetKeys/generateQuerySetKeys.js +24 -0
  52. package/node/Sets/utils/generateQuerySetKeys/index.d.ts +1 -0
  53. package/node/Sets/utils/generateQuerySetKeys/index.js +17 -0
  54. package/node/Sets/utils/index.d.ts +2 -0
  55. package/node/Sets/utils/index.js +18 -0
  56. package/node/index.d.ts +2 -1
  57. package/node/index.js +3 -1
  58. package/package.json +5 -2
package/README.md CHANGED
@@ -3,414 +3,704 @@
3
3
  Библиотека для кеширования запросов.
4
4
 
5
5
  Особенности:
6
- - Ориентирована на специфику frontend приложений
7
- - Для обеспечения реактивности используется [mobx](https://mobx.js.org/)
8
- - По идеологии использования схожа с [@tanstack/react-query](https://www.npmjs.com/package/@tanstack/react-query/)
9
- - TS only, totally no `any`
10
-
11
- ---
6
+ - ⚡️️️️ Реактивный кэш на основе [mobx](https://mobx.js.org/)
7
+ - ️️️️️️⚡️️️️ Вдохновлено [@tanstack/react-query](https://www.npmjs.com/package/@tanstack/react-query/)
8
+ - ⚡️️️️ Декларативный способ описания queries и mutations
9
+ - ⚡️️️️ Реализация архитектурного подхода работы с данными [Astral Architecture Guide](https://guides.frontend.cloud.astral-dev.ru/docs/arch/intro)
10
+ - ⚡️️️️ Фоновая подгрузка данных для работы с WebSocket
11
+ - ⚡️️️️ Возможность тестирования
12
12
 
13
13
  ## Table of contents
14
+
14
15
  - [Installation](#installation)
15
- - [Basic meaning](#basic-meaning)
16
- - [Варианты использования query](#варианты-использования-query)
17
- - [Ручной синхронный](#1-ручной-синхронный)
18
- - [Ручной асинхронный](#2-ручной-асинхронный)
19
- - [Автоматический](#3-автоматический)
20
- - [Инвалидация данных](#инвалидация-данных)
21
- - [Особенности инвалидации](#особенности-инвалидации)
22
- - [Ручная установка данных в кэш](#ручная-установка-данных-в-кэш)
23
- - [InfiniteQuery](#infinitequery)
24
- - [isEndReached](#isendreached)
25
- - [Mutation](#mutation)
26
- - [Sync вариация](#sync-вариация)
27
- - [Async вариация](#async-вариация)
28
- - [Fetch policy](#fetchpolicy)
29
- - [Вспомогательные флаги и поля](#вспомогательные-флаги-и-поля)
30
- - [isLoading](#isloading)
31
- - [isSuccess](#issuccess)
32
- - [isError](#iserror)
33
- - [error](#error)
34
- - [Тестирование](#тестирование)
35
- - [Тестирование при включенном enabledAutoFetch](#тестирование-при-включенном-enabledautofetch)
36
-
37
- # Installation
38
-
39
- ```shell
40
- npm i --save @astral/mobx-query
41
- ```
42
-
43
- ```shell
44
- yarn add @astral/mobx-query
16
+ - [Basic usage](#basic-usage)
17
+ - [Core concepts](#core-concepts)
18
+ - [QuerySet](#queryset)
19
+ - [Создание QuerySet](#создание-queryset)
20
+ - [Использование QuerySet](#использование-queryset)
21
+ - [Синхронный вызов запроса данных](#синхронный-вызов-запроса-данных)
22
+ - [Асинхронный вызов запроса данных](#асинхронный-вызов-запроса-данных)
23
+ - [Автоматический запрос данных](#автоматический-запрос-данных)
24
+ - [InfiniteQuerySet](#infinitequerygroups)
25
+ - [Создание InfiniteQuerySet](#создание-infinitequerygroups)
26
+ - [Использование InfiniteQuerySet](#использование-infinitequerygroups)
27
+ - [Загрузка данных](#загрузка-данных)
28
+ - [isEndReached. Определение конца списка](#isendreached-определение-конца-списка)
29
+ - [Изменение количества запрашиваемых записей](#изменение-количества-запрашиваемых-записей)
30
+ - [MutationSet](#mutationset)
31
+ - [Создание mutations](#создание-и-использование-mutations)
32
+ - [Синхронный вызов mutation](#синхронный-вызов-mutation)
33
+ - [Асинхронный вызов mutation](#асинхронный-вызов-mutation)
34
+ - [Кэширование QuerySet и InfiniteQuerySet](#кэширование-queryset-и-infinitequeryset)
35
+ - [FetchPolicy](#fetchpolicy)
36
+ - [Как работает кэш](#как-работает-кэш)
37
+ - [Инвалидация кэша QuerySet](#инвалидация-кэша-queryset)
38
+ - [Кастомная установка ключей кэширования](#кастомная-установка-ключей-кэширования)
39
+ - [Фоновая загрузка данных QuerySet и InfiniteQuerySet](#фоновая-загрузка-данных-queryset-и-infinitequeryset)
40
+ - [Изменение кэша QuerySet и InfiniteQuerySet](#изменение-кэша-queryset-и-infinitequeryset)
41
+ - [Тестирование QuerySet и InfiniteQuerySet](#тестирование-queryset-и-infinitequeryset)
42
+ - [Core](#core)
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ npm install @astral/mobx-query --save
45
48
  ```
46
49
 
47
- ---
50
+ ```bash
51
+ yarn add @astral/mobx-query
52
+ ```
48
53
 
49
- # Basic meaning
50
- - executor - исполнитель запроса, который будет совершать запрос. Второй аргумент при создании query
51
- - enabledAutoFetch - включает автоматический запрос данных при обращении к полю `data`.
52
- - fetchPolicy - политика, говорящая о том, как следует работать с новыми запросами
53
- - 'cache-first' - политика применяемая по умолчанию, при отсутствии данных в памяти, будет исполнен executor, его ответ запишется в кеш, и при последующих обращениях данные будут взяты из кеша
54
- - 'network-only' - каждый запрос будет приводить к вызову executor, его ответ будет записан в кеш(для использования в cache-first)
54
+ ## Basic usage
55
55
 
56
- # Basic usage
56
+ ```
57
+ ├── api/
58
+ | ├── _fakers/
59
+ | ├── endpoints/
60
+ | ├── fetchers/
61
+ | | ├── docs.ts
62
+ | | └── index.ts
63
+ | ├── services/
64
+ | | ├── CacheService/
65
+ | | | ├── CacheService.ts
66
+ | | | └── index.ts
67
+ | | └── index.ts
68
+ | └── index.ts
69
+ ```
57
70
 
58
- Для начала вам потребуется создать инстанс кеш сервиса
71
+ Инициализация MobxQuery:
72
+ ```api/services/CacheService/CacheService.ts```
59
73
  ```ts
60
74
  import { MobxQuery } from '@astral/mobx-query';
61
75
 
62
- const mobxQuery = new MobxQuery({
63
- onError: (error) => {
64
- console.log(error); // место для вашей обработки ошибок по умолчанию, опционально
65
- },
66
- fetchPolicy: 'cache-first', // 'cache-first' по умолчанию, опционально
67
- enabledAutoFetch: false, // false по умолчанию, опционально
68
- });
76
+ // рекомендуется явно задавать параметры для MobXQuery
77
+ export const createCacheService = () =>
78
+ new MobxQuery({ enableAutoFetch: true, fetchPolicy: 'cache-first' });
79
+
80
+ export const cacheService = createCacheService();
69
81
  ```
70
82
 
71
- # Варианты использования query
72
- ## 1. Ручной синхронный.
83
+ Определение fetcher и cacheGroups для docs:
84
+ ```api/services/Fetcher/docs.ts```
85
+ ```ts
86
+ const docsFetcher = {
87
+ queries: {
88
+ doc: cacheService.createQuerySet((id: string) => ({
89
+ execute: () => docsEndpoint.getDoc(id).then(({ data }) => data),
90
+ })),
91
+ },
92
+ infiniteQueries: {
93
+ docList: cacheService.createInfiniteQuerySet(
94
+ (filters: Omit<DocsEndpointsDTO.DocListFilters, 'offset' | 'count'>) => ({
95
+ execute: ({ offset, count }) =>
96
+ docsEndpoint
97
+ .getDocList({ offset, count, ...filters })
98
+ .then(({ data }) => data.list),
99
+ }),
100
+ ),
101
+ },
102
+ mutations: {
103
+ editDoc: cacheService.createMutationSet(
104
+ (params: DocsEndpointsDTO.EditDocInput) => docsEndpoint.editDoc(params),
105
+ ),
106
+ },
107
+ };
108
+
109
+ export type DocsFetcher = typeof docsFetcher;
110
+ ```
73
111
 
74
- Можно вызывать встроенный метод `sync`, передавая в него колбэк опциональные параметры `onSucess` и `onError`. В onSuccess будут переданы полученные данные от успешного запроса, а в `onError`, соответственно, будет переданы данные ошибки в случае провального запроса.
75
- Если при вызове обработчик `onError` не был передан, вызовется стандартный, переданный при создании MobxQuery инстанса.
112
+ Использование в store:
76
113
 
77
114
  ```ts
78
- const query = mobxQuery.createQuery(
79
- ['some cache key'],
80
- () => Promise.resolve('foo'),
81
- );
115
+ import { type DocsFetcher } from '@example/api';
116
+
117
+ class DocStore {
118
+ constructor(
119
+ private readonly _docID: string,
120
+ private readonly _docsFetcher: DocsFetcher,
121
+ ) {
122
+ makeAutoObservable(this, {}, { autoBind: true });
123
+ }
124
+
125
+ private get docQuery() {
126
+ return this._docsFetcher.queries.doc.create(this.docID);
127
+ }
82
128
 
83
- query.sync({
84
- onSuccess: (data) => {
85
- console.log(data); // место для реагирования на ответ
86
- },
87
- onError: (error) => {
88
- console.log(error); // место для вашей ошибки
129
+ public get docName() {
130
+ if (!this.docQuery.data) {
131
+ return '';
89
132
  }
133
+
134
+ return `Название документа: ${this.docQuery.data.name}`;
135
+ }
136
+
137
+ public get isLoading() {
138
+ return this.docQuery.isLoading;
139
+ }
140
+ }
141
+ ```
142
+
143
+ ```ts
144
+ class DocManagerStore {
145
+ constructor(private readonly _docsFetcher: DocsFetcher) {}
146
+
147
+ public changeOrg = () => {
148
+ // инвалидирует все doc query, которые есть в кэше
149
+ this._docsFetcher.queries.doc.invalidate();
150
+ };
151
+ }
152
+ ```
153
+
154
+ ## Core concepts
155
+
156
+ [`Query`](#query) предназначен для получения данных. Не должен производить изменения.
157
+ [`QuerySet`](#queryset) - набор queries для данных, получаемых по одной и той же сущности с разными параметрами.
158
+ [`InfiniteQuery`](#infinitequery) предназначен для получения бесконечного списка данных.
159
+ [`Mutation`](#mutation) предназначен для изменения данных на сервере.
160
+ [`MutationSet`](#mutationset) - набор mutations для изменения данных. Необходим для консистенстности api с `QuerySet`.
161
+
162
+ `QuerySet`, `InfiniteQuerySet` и `MutationSet` создаются через методы MobxQuery:
163
+ - `createQuerySet`
164
+ - `createInfiniteQuerySet`
165
+ - `createMutationSet`
166
+
167
+ `Query`, `InfiniteQuery` и `Mutation` создаются через методы MobxQuery:
168
+ - `createQuery`
169
+ - `createInfiniteQuery`
170
+ - `createMutation`
171
+
172
+ Рекомендуется использовать именно Set'ы потому что это выскоуровневое api, скрывающее внутри себя сложность работы с кэшем и позволяющее работать с данными более декларативно. Set'ы используют внутри себя `Query`, `InfiniteQuery` и `Mutation`.
173
+
174
+ Методы `createQuery`, `createInfiniteQuery` и `createMutation` необходимо использовать для реализации собственных библиотек для работы с данными.
175
+
176
+ ## QuerySet
177
+
178
+ [`Query`](#query) предназначен для получения данных. Не должен производить изменения.
179
+ `QuerySet` - набор queries для данных, получаемых по одной и той же сущности с разными параметрами.
180
+
181
+ ### Создание QuerySet
182
+
183
+ В примере ниже будет создан doc query, который будет получать данные по id документа. После успешного выполнения запроса данные будут закэшированы.
184
+
185
+ ```ts
186
+ const docsFetcher = {
187
+ queries: {
188
+ doc: mobxQuery.createQuerySet((id: string) => ({
189
+ execute: () => docsEndpoint.getDoc(id).then(({ data }) => data),
190
+ })),
191
+ },
192
+ };
193
+ ```
194
+
195
+ ### Использование QuerySet
196
+
197
+ Для получения объекта query необходимо вызвать метод `create`.
198
+ Параметры `create` полностью идентичны параметрам, указанным при определении query:
199
+ ```ts
200
+ const docsFetcher = {
201
+ queries: {
202
+ doc: mobxQuery.createQuerySet((id: string) => ({
203
+ execute: () => docsEndpoint.getDoc(id).then(({ data }) => data),
204
+ })),
205
+ },
206
+ };
207
+
208
+ // create принимает только один параметр - id: string
209
+ const docQuery = docsFetcher.queries.doc.create('docID');
210
+ ```
211
+
212
+ Метод `create` вернет [объект query](#query-interface), который содержит всю информацию по запросу и методы работы с запросом.
213
+
214
+ ### Синхронный вызов запроса данных
215
+
216
+ Метод `sync` позволяет синхронно запустить запрос на получение данных:
217
+ ```ts
218
+ const docQuery = docsFetcher.queries.doc.create(docID);
219
+
220
+ // есть callbacks на обработку success и error
221
+ docQuery.sync({
222
+ onSuccess: (data) => {
223
+ console.log(data);
224
+ },
225
+ onError: (error) => {
226
+ console.log(error);
227
+ },
90
228
  });
229
+
230
+ // true
231
+ docQuery.isLoading;
91
232
  ```
92
233
 
93
- **[Пример в sandbox](https://codesandbox.io/s/adoring-wing-z8s9ng)**
234
+ ### Асинхронный вызов запроса данных
94
235
 
95
- ## 2. Ручной асинхронный.
236
+ Метод `async` позволяет асинхронно запустить запрос на получение данных:
237
+ ```ts
238
+ const docQuery = docsFetcher.queries.doc.create(docID);
239
+
240
+ // передавать параметры запроса не нужно потому что они уже были переданы при вызове init
241
+ const response = await docQuery.async();
242
+
243
+ docQuery.isSuccess; // true
244
+ docQuery.data; // идентичен response
245
+ ```
246
+
247
+ ### Автоматический запрос данных
96
248
 
97
- Можно вызвать встроенный метод async. Возвращает промис, соответственно в then попадут данные успешного запроса.
98
- `Будьте внимательны, используя метод "async", позаботьтесь о добавлении ".catch", иначе ошибка запроса попадет в глобальный exception`.
249
+ Если MobxQuery был создан с флагом `enableAutoFetch: true`, то данные будут автоматически запрошены при обращении к полю `data`:
99
250
 
100
251
  ```ts
101
- const query = mobxQuery.createQuery(
102
- ['some cache key'],
103
- () => Promise.resolve('foo'),
252
+ const docQuery = docsFetcher.queries.doc.create(docID);
253
+
254
+ docQuery.data; // триггер запроса данных
255
+
256
+ docQuery.isLoading; // true
257
+ ```
258
+
259
+ Если MobxQuery был создан с флагом `enableAutoFetch: false`, то автоматически запрос данных можно включить для текущего query:
260
+ ```ts
261
+ const docQuery = docsFetcher.queries.doc.createWithConfig((
262
+ { enableAutoFetch: true },
263
+ docID
104
264
  );
105
265
 
106
- query
107
- .async()
108
- .then((data) => {
109
- console.log(data); // место для реагирования на ответ
110
- })
111
- .catch((e) => {
112
- console.log(e); // место для вашей ошибки
113
- });
266
+ docQuery.data; // триггер запроса данных
267
+
268
+ docQuery.isLoading; // true
114
269
  ```
115
- **[Пример в sandbox](https://codesandbox.io/s/mobx-query-simple-async-k75tfg)**
116
270
 
117
- ## 3. Автоматический.
271
+ ## InfiniteQuerySet
118
272
 
119
- При создании query, предусмотрен вариант автоматического запроса при обращении к полю `data` из `query`. Требуется активация флага `enabledAutoFetch` при создании query, либо установка стандартного значения, при создании MobxQuery инстанса.
120
- Т.е. благодаря реактивности предоставляемой `mobx`, пока не произойдет считывания поля `data` или же не будут вызваны `sync/async` методы, запрос данных так же не произойдет.
273
+ [`InfiniteQuery`](#infinitequery) предназначен для получения бесконечного списка данных.
274
+ `InfiniteQuerySet` - набор InfiniteQuery для данных, получаемых по одной и той же сущности с разными параметрами.
121
275
 
122
- ```tsx
123
- import { observer } from 'mobx-react-lite';
276
+ ### Создание InfiniteQuerySet
124
277
 
125
- const query = mobxQuery.createQuery(
126
- ['some cache key'],
127
- () => Promise.resolve('foo'),
128
- { enabledAutoFetch: true }
129
- );
278
+ В примере ниже будет создан docList InfiniteQuerySet. После успешного выполнения запроса данные будут закэшированы.
130
279
 
131
- const MyComponent = observer(() => <div>{query.data}</div>) // <div>foo</div>
280
+ ```ts
281
+ const docsFetcher = {
282
+ infiniteQueries: {
283
+ docList: mobxQuery.createInfiniteQuerySet(
284
+ (filters: Omit<DocsEndpointsDTO.DocListFilters, 'offset' | 'count'>) => ({
285
+ execute: ({ offset, count }) =>
286
+ docsEndpoint
287
+ .getDocList({ offset, count, ...filters })
288
+ .then(({ data }) => data.list),
289
+ }),
290
+ ),
291
+ },
292
+ };
132
293
  ```
133
- **[Пример в sandbox](https://codesandbox.io/s/happy-cherry-ytqgry)**
134
294
 
135
- # Инвалидация данных
136
- Существует необходимость инвалидировать данные, типичным примером являются [CRUD операции](https://ru.wikipedia.org/wiki/CRUD).
137
- В контексте нашей библиотеки, инвалидация подразумевает под собой отметку для query, означающую, что данные устарели, и их необходимо обновить.
138
- Для корректной работы инвалидации, при создании query требуется использование `ключа`.
139
- Ключ для создания может быть как примитивом, так и объектом. Главное, чтобы они были подходящими для JSON сериализации.
295
+ ### Использование InfiniteQuerySet
296
+
297
+ Для получения объекта `InfiniteQuery` необходимо вызвать метод `create`.
298
+ Параметры `create` полностью идентичны параметрам, указанным при определении InfiniteQuery:
299
+ ```ts
300
+ const docsFetcher = {
301
+ infiniteQueries: {
302
+ docList: mobxQuery.createInfiniteQuerySet(
303
+ // Omit необходим для того, чтобы не передавать offset и count при вызове запроса из логики. Offset и count будут сформированы и переданы автоматически
304
+ (filters: Omit<DocsEndpointsDTO.DocListFilters, 'offset' | 'count'>) => ({
305
+ execute: ({ offset, count }) =>
306
+ docsEndpoint
307
+ .getDocList({ offset, count, ...filters })
308
+ // fetch должен возвращать array
309
+ .then(({ data }) => data.list),
310
+ }),
311
+ ),
312
+ },
313
+ };
314
+
315
+ // первым параметром create является - filters: Omit<DocsEndpointsDTO.DocListFilters, 'offset' | 'count'>
316
+ const docListQuery = docsFetcher.infiniteQueries.docList.create({ search: 'test' });
317
+ ```
140
318
 
141
- Инстанс MobxQuery содержит специальный метод `invalidate`, принимающий в качестве аргумента `массив ключей`.
319
+ ### Загрузка данных
142
320
 
143
321
  ```ts
144
- const query = mobxQuery.createQuery(
145
- ['some cache key'],
146
- () => Promise.resolve('foo'),
147
- { enabledAutoFetch: true }
148
- );
322
+ import { when } from 'mobx';
323
+
324
+ const docListQuery = docsFetcher.infiniteQueries.docList.create({ search: 'test' });
325
+
326
+ await docListQuery.async(); // запрос на получение первых 30 записей
149
327
 
150
- mobxQuery.invalidate(['some cache key'])
328
+ docListQuery.data.length; // 30 записей
329
+
330
+ docListQuery.fetchMore(); // запрос на получение следующих 30 записей
331
+
332
+ // ждем загрузки
333
+ await when(() => !query.isLoading);
334
+
335
+ docListQuery.data.length; // 60 записей
151
336
  ```
152
337
 
153
- **[Пример в sandbox](https://codesandbox.io/s/mobx-query-invalidate-query-hhcngr)**
338
+ ### `isEndReached`. Определение конца списка
154
339
 
155
- ## Особенности инвалидации
156
- - Как при создании query, так и при инвалидации, нужно использовать массив ключей. Предполагается, что query может быть инвалидирован по нескольким ключам
340
+ Флаг `isEndReached` будет установлен в `true`, если записи для загрузки закончились:
157
341
  ```ts
158
- const query = mobxQuery.createQuery(
159
- ['key one', 'key two'], // ключ - массив строк
160
- () => Promise.resolve('foo'),
161
- { enabledAutoFetch: true }
162
- );
342
+ import { when } from 'mobx';
163
343
 
164
- mobxQuery.invalidate(['key two']); // query будет инвалидирован
165
- mobxQuery.invalidate(['key one']); // query будет инвалидирован
344
+ const docListQuery = docsFetcher.infiniteQueries.docList.create({ search: 'test' });
345
+
346
+ await docListQuery.async(); // запрос на получение первых 30 записей
347
+
348
+ docListQuery.data.length; // 30 записей
349
+
350
+ docListQuery.fetchMore(); // запрос на получение следующих 30 записей
351
+
352
+ // ждем загрузки
353
+ await when(() => !query.isLoading);
354
+
355
+ docListQuery.data.length; // 19 записей. Последняя страница содержит 9 записей, а не 30, как было запрошено, значит больше данных нет
356
+
357
+ docListQuery.isEndReached; // true
166
358
  ```
167
- Но, стоит учитывать, что ключом является цельный элемент массива, а не составляющие элемента
359
+
360
+ Флаг `isEndReached` устанавливается в `true`, когда количество полученных элементов меньше запрошенного количества, что означает отсутствие дополнительных данных на сервере.
361
+
362
+ ### Изменение количества запрашиваемых записей
363
+
364
+ Для изменения конфигурации InfiniteQuery необходимо использовать метод `createWithConfig`:
168
365
  ```ts
169
- const query = mobxQuery.createQuery(
170
- [['key one', 'key two']], // ключ - двумерный массив строк
171
- () => Promise.resolve('foo'),
172
- { enabledAutoFetch: true }
366
+ const docListQuery = docsFetcher.infiniteQueries.docList.createWithConfig(
367
+ { incrementCount: 10, enabledAutoFetch: true },
368
+ { search: 'test' },
173
369
  );
370
+ ```
174
371
 
175
- mobxQuery.invalidate(['key one']); // ключ не совпадает, query НЕ будет инвалидирован
372
+ [Интерфейс `InfiniteQueryConfig`](#интерфейс-infinitequeryconfig).
373
+
374
+ ## MutationSet
375
+
376
+ [`Mutation`](#mutation) предназначен для отправки данных на сервер.
377
+ `MutationSet` - набор mutations, изменяющих одну и ту же сущность. Предоставляет интерфейс, идентичный `QuerySet`, для консистенстности api.
378
+
379
+ ### Создание и использование MutationSet
380
+
381
+ В примере ниже будет создан `editDoc` mutation, который отправляет данные на сервер для редактирования документа:
382
+ ```ts
383
+ const docsFetcher = {
384
+ mutations: {
385
+ editDoc: mobxQuery.createMutationSet((params: DocsEndpointsDTO.EditDocInput) => docsEndpoint.editDoc(params)),
386
+ },
387
+ };
388
+
389
+ const editDocMutation = docsFetcher.mutations.editDoc.create();
390
+
391
+ // params являются параметрами, указанными при определении mutation - DocsEndpointsDTO.EditDocInput
392
+ await editDocMutation.async({ params: { id: 'docID', name: 'test' } });
176
393
  ```
177
394
 
178
- - Инвалидация будет происходить только для query, поле `data` которых считывается в данный момент. Для query, `data` которых будут отрендерены позже, запрос произойдет только в момент использования. Для превентивного обновления данных потребуется последовательное использование `sync/async` методов сразу после `invalidate`.
395
+ Метод `create` вернет [объект mutation](#интерфейс-mutation), который содержит всю информацию по запросу и методы работы с запросом.
179
396
 
180
- ### Массовая инвалидация
181
- Для инвалидации всех query необходимо использовать метод `invalidateQueries`
397
+ ### Синхронный вызов mutation
398
+
399
+ Метод `sync` позволяет синхронно запустить запрос:
182
400
  ```ts
183
- mobxQuery.invalidateQueries();
401
+ const editDocMutation = docsFetcher.mutations.editDoc.create();
402
+
403
+ // есть callbacks на обработку success и error
404
+ editDocMutation.sync({
405
+ params: { id: 'docID', name: 'test' },
406
+ onSuccess: (data) => {
407
+ console.log(data);
408
+ },
409
+ onError: (error) => {
410
+ console.log(error);
411
+ },
412
+ });
413
+
414
+ // true
415
+ editDocMutation.isLoading;
184
416
  ```
185
417
 
186
- # Ручная установка данных в кэш
418
+ ### Асинхронный вызов mutation
187
419
 
188
- Для установки данных, без исполнения executor, используйте метод `forceUpdate`. При вызове все статусные флаги устанавливаются как success состояние
420
+ Метод `async` позволяет асинхронно запустить запрос:
421
+ ```ts
422
+ const editDocMutation = docsFetcher.mutations.editDoc.create();
189
423
 
424
+ await editDocMutation.async({ params: { id: 'docID', name: 'test' } });
425
+ ```
426
+
427
+ ## Кэширование QuerySet и InfiniteQuerySet
428
+
429
+ `QuerySet` и `InfiniteQuerySet` позволяет закэшировать данные, которые были получены ранее.
430
+
431
+ ### FetchPolicy
432
+
433
+ `FetchPolicy` определяет политику получения данных.
434
+
435
+ Существует два типа политики:
436
+ - `cache-first` - если в кэше есть данные, они будут возвращены, если нет, то данные будут получены из сети, после чего ответ будет записан в кэш
437
+ - `network-only` - данные всегда берутся из сети, при этом ответ записывается в кэш
438
+
439
+ #### Глобальная установка fetchPolicy
440
+
441
+ Для глобальной установки конкретной политики fetchPolicy необходимо передать параметр `fetchPolicy` при создании `MobxQuery`:
190
442
  ```ts
191
- query.forceUpdate('foo');
443
+ import { MobxQuery } from 'mobx-query';
444
+
445
+ export const createQuery = () => new MobxQuery({ fetchPolicy: 'cache-first' });
192
446
  ```
193
447
 
194
- # InfiniteQuery
195
- Существует необходимость постепенного запроса массивов данных, в постраничном режиме. Типичный пример, инфинити скролл, когда новая пачка данных запрашивается, в момент когда пользователь докрутил список до конца.
196
- Для удобства, мы создали специальный `query`, который содержит дополнительный метод `fetchMore` и при вызове оного, происходит запрос с увеличенными счетчиками. Данные ответа на этот запрос, будут сконкатенированы с уже имеющимся. В случае, если количество данных меньше, чем длина страницы, будет считаться что мы дошли до конца списка.
197
- В `executor` будет передан объект с `offset` - количество элементов отступа от начала списка, и `count` - количество элементов на одну страницу.
448
+ #### Локальная установка fetchPolicy
198
449
 
199
- Значение `count` и увеличение `offset` регулируется опциональным параметром `incrementCount` при создании `query`. По умолчанию равен `30`.
450
+ Для каждого отдельного query можно установить свою fetchPolicy при инициализации.
200
451
 
452
+ Пример для `Query`:
201
453
  ```ts
202
- import { when } from 'mobx';
454
+ const docQuery = docsFetcher.queries.doc.createWithConfig(({ fetchPolicy: 'network-only' }, 'docID');
455
+ ```
203
456
 
204
- const query = mobxQuery.createInfiniteQuery(
205
- ['some cache key'],
206
- ({ offset, count }) => {
207
- // можно использовать "offset/count" для необходимых преобразований и последующего запроса к api
208
- return Promise.resolve(['foo'])
209
- },
210
- {
211
- incrementCount: 30, // опционально, по умолчанию 30
212
- }
457
+ Пример для `InfiniteQuery`:
458
+ ```ts
459
+ const docListQuery = docsFetcher.infiniteQueries.docList.createWithConfig(
460
+ { incrementCount: 10, fetchPolicy: 'network-only' },
461
+ { search: 'test' },
213
462
  );
463
+ ```
214
464
 
215
- await query.async();
465
+ #### Принцип работы fetchPolicy
216
466
 
217
- console.log(query.data); // ['foo']
467
+ `cache-first`:
468
+ ```ts
469
+ const docQuery = docsFetcher.queries.doc.createWithConfig(({ fetchPolicy: 'cache-first' }, 'docID');
218
470
 
219
- query.fetchMore();
220
- await when(() => !query.isLoading); // ждем фоновой загрузки
471
+ await docQuery.async(); // запрос будет выполнен потому что до этого запроса с такими параметрами не было
221
472
 
222
- console.log(query.data); // ['foo', 'foo']
223
- ```
224
- ## isEndReached
225
- Для определения того, что мы все таки достигли конца списка, присутствует флаг `isEndReached`.
473
+ await docQuery.async(); // запрос не будет выполнен потому что данные уже есть в кэше. Promise будет завершен сразу
474
+ ```
226
475
 
476
+ `network-only`:
227
477
  ```ts
228
- const query = mobxQuery.createInfiniteQuery(
229
- ['some cache key'],
230
- () => Promise.resolve([]),
231
- );
478
+ const docQuery = docsFetcher.queries.doc.createWithConfig(({ fetchPolicy: 'network-only' }, 'docID');
479
+
480
+ await docQuery.async(); // запрос будет выполнен
481
+ await docQuery.async(); // запрос будет выполнен
232
482
 
233
- await query.async();
483
+ const docQueryWithCache = docsFetcher.queries.doc.createWithConfig(({ fetchPolicy: 'cache-first' }, 'docID');
484
+
485
+ await docQueryWithCache.async(); // запрос не будет выполнен потому что прежде данные были получены с политикой `network-only` и записаны в кэш
486
+ ```
234
487
 
235
- console.log(query.isEndReached); // true
236
- ```
488
+ ### Как работает кэш
237
489
 
238
- **[Пример в sandbox](https://codesandbox.io/s/mobx-query-infinityquery-sdcc6g)**
490
+ Все данные, возвращаемые `QuerySet` и `InfiniteQuerySet`, кэшируются в едином хранилище @astral/mobx-query.
239
491
 
240
- # Mutation
241
- Для изменения данных необходимо использовать mutation. Ответы Mutation не кэшируются.
492
+ При первом вызове `create` в хранилище создается запись с ключем, состоящим из:
493
+ - Хэш от функции конфигурации
494
+ - `queryParams`
242
495
 
496
+ Пример:
243
497
  ```ts
244
- const mutation = mobxQuery.createMutation(
245
- (params) => {
246
- console.log(params); // при необходмости, можем использовать опциональные параметры
247
- return Promise.resolve('foo');
248
- },
249
- );
498
+ const docFetcher = {
499
+ queries: {
500
+ doc: mobxQuery.createQuerySet((id: string, filters: { search: string }) => ({
501
+ execute: () => docsEndpoint.getDoc(id, filters),
502
+ })),
503
+ },
504
+ };
505
+
506
+ // ключ кэша будет равен "12fj1d,doc,1,"{"search":"test"}""
507
+ // 12fj1d - это хэш от функции конфигурации
508
+ const docQuery = docFetcher.queries.doc.create('1', { search: 'test' });
509
+ // ключ кэша будет равен "12fj1d,doc,2,"{"search":"test2"}""
510
+ const docQuery = docsFetcher.queries.doc.create('1', { search: 'test2' });
250
511
  ```
251
- **[Пример в sandbox](https://codesandbox.io/s/mobx-query-mutation-p2pnct)**
252
512
 
253
- ## async вариация
513
+ #### Автоматическая чистка кэша
514
+
515
+ Mobx-query кэш организован через `WeekRef`, поэтому не используемые данные автоматически удаляются сборщиком мусора.
516
+
517
+ ### Инвалидация кэша QuerySet и InfiniteQuerySet
518
+
519
+ Если данные в query стали неактуальными, то необходимо вызвать метод `invalidate`:
254
520
  ```ts
255
- mutation
256
- .async('bar') // тут, по нашему примеру, увидим консоль 'bar'
257
- .then((data) => {
258
- console.log(data) // а тут уже 'foo'
259
- });
521
+ // инвалидация всех query с именем `doc`
522
+ docsFetcher.queries.doc.invalidate();
260
523
  ```
261
524
 
262
- ## sync вариация
525
+ После вызова `invalidate` данные в кэше будут помечены как невалидные и при следующем обращении к query будет выполнен запрос на сервер. Если при вызове метода `invalidate` на изменения `.data` подписан любой store, то произойдет моментальный перезапрос активных query:
526
+
263
527
  ```ts
264
- mutation.sync({
265
- params: 'bar',
266
- onSuccess: (data) => {
267
- console.log(data) // а тут уже 'foo'
268
- }
269
- }); // тут, по нашему примеру, увидим консоль 'bar'
528
+ const docQuery = docsFetcher.queries.doc.create('docID');
529
+
530
+ await docQuery.async(); // данные записаны в кэш
531
+
532
+ docsFetcher.queries.doc.invalidate();
533
+
534
+ docQuery.data; // триггер запроса данных потому что они были помечены как невалидные
535
+ docQuery.isLoading; // true
270
536
  ```
271
537
 
272
- # fetchPolicy
538
+ #### Инвалидация всех query по их имени
273
539
 
540
+ Для инвалидации всех query по их имени можно вызвать метод `invalidate`:
274
541
  ```ts
275
- const cacheFirstQuery = mobxQuery.createQuery(
276
- ['cache-first key'],
277
- () => {
278
- console.log('cache-first request');
279
- return Promise.resolve('foo');
280
- },
281
- {
282
- fetchPolicy: 'cache-first',
283
- }
284
- );
542
+ const docQuery1 = docsFetcher.queries.doc.create('1');
543
+ const docQuery2 = docsFetcher.queries.doc.create('2');
285
544
 
286
- const networkOnlyQuery = mobxQuery.createQuery(
287
- ['network-only key'],
288
- () => {
289
- console.log('network-only request');
290
- return Promise.resolve('bar');
291
- },
292
- {
293
- fetchPolicy: 'network-only',
294
- }
295
- );
545
+ docsFetcher.queries.doc.invalidate(); // данные docQuery1 и docQuery2 помечены как невалидные
546
+ ```
296
547
 
297
- await cacheFirstQuery.async(); // увидим консоль 'cache-first request'
298
- await networkOnlyQuery.async(); // увидим консоль 'network-only request'
548
+ #### Инвалидация query по частичному совпадению параметров
299
549
 
300
- await cacheFirstQuery.async(); // вызова executor не произойдет, и консоль не выведется
301
- await networkOnlyQuery.async(); // вновь увидим консоль 'network-only request'
550
+ Если необходимо инвалидировать данные по конкретным параметрам, то их необходимо передать в метод `invalidate`:
551
+ ```ts
552
+ const docsFetcher = {
553
+ queries: {
554
+ doc: mobxQuery.createQuerySet((id: string) => ({
555
+ execute: () => docsEndpoint.getDoc(id).then(({ data }) => data),
556
+ })),
557
+ },
558
+ };
559
+
560
+ docsFetcher.queries.doc.invalidate('1'); // документ с id = 1 будет помечен как невалидный
561
+ ```
302
562
 
303
- const duplicateCacheFirstQuery = mobxQuery.createQuery(
304
- ['cache-first key'], // использован тот же самый ключ, что и для cacheFirstQuery
305
- () => {
306
- console.log('duplicate cache-first request');
307
- return Promise.resolve('foo');
308
- }
309
- );
563
+ Параметры `invalidate` полностью совпадают с параметрами, описанными при определении query:
310
564
 
311
- await duplicateCacheFirstQuery.async(); // вызова executor не произойдет, и консоль не выведется
565
+ ```ts
566
+ const docsFetcher = {
567
+ queries: {
568
+ doc: mobxQuery.createQuerySet((id: string, search: string, filters: Filters) => ({
569
+ execute: () => docsEndpoint.getDoc(id, search, filters),
570
+ })),
571
+ },
572
+ };
573
+
574
+ docsFetcher.queries.doc.invalidate('1', 'test', { sort: 'asc' });
312
575
  ```
313
- **[Пример в sandbox](https://codesandbox.io/s/mobx-query-fetchpolicy-wvh8jl)**
314
-
315
- # Вспомогательные флаги и поля
316
- `Query`, `InfiniteQuery` и `Mutation` имеют одинаковый набор вспомогательных флагов и полей, работающих по единому принципу.
317
576
 
318
- ## isLoading
319
- Boolean флаг, указывающий на процесс выполнения запроса
320
- ## isSuccess
321
- Boolean флаг, указывающий на успешное выполнение запроса
322
- ## isError
323
- Boolean флаг, указывающий на провалившийся запрос
324
- ## isIdle
325
- Boolean флаг, указывающий на простаивание query, первый же вызов запроса переключит его в false
326
- ## error
327
- Поле, содержащее информацию о последней ошибке
577
+ #### Инвалидация query по частичному совпадению параметра-объекта
328
578
 
329
579
  ```ts
330
- const query = mobxQuery.createQuery(
331
- ['some cache key'],
332
- () => Promise.reject('foo'),
333
- );
580
+ const docsFetcher = {
581
+ queries: {
582
+ doc: mobxQuery.createQuerySet((id: string, filters: { sort: string; search: string }) => ({
583
+ execute: () => docsEndpoint.getDoc(id, filters),
584
+ })),
585
+ },
586
+ };
587
+
588
+ const query1 = docsFetcher.queries.doc.create('1', { sort: 'asc', search: 'test' });
589
+ const query2 = docsFetcher.queries.doc.create('1', { sort: 'desc', search: 'test' });
590
+
591
+ docsFetcher.queries.doc.invalidate('1', { sort: 'asc' }); // данные только query1 будут помечены как невалидные
592
+ ```
334
593
 
335
- await query
336
- .async()
337
- .catch((e) => {
338
- console.log(e); // 'foo'
339
- });
594
+ ### Кастомная установка ключей кэширования
595
+
596
+ При определении `QuerySet` и `InfiniteQuerySet` можно указать параметр `keys` и `name`, которые будут использоваться для формирования ключей кэша.
597
+ - `name` - название набора. Будет использоваться вместо хэша от функции конфигурации
598
+ - `keys` - будут использоваться вместо параметров функции конфигурации
340
599
 
341
- console.log(query.isError); // 'true'
342
- console.log(query.error); // 'foo'
600
+ ```ts
601
+ const docsFetcher = {
602
+ queries: {
603
+ doc: mobxQuery.createQuerySet(
604
+ (id: string, search: string) => ({
605
+ keys: [id],
606
+ execute: () => docsEndpoint.getDoc(id, search),
607
+ }),
608
+ { name: 'doc' }
609
+ ),
610
+ },
611
+ };
612
+
613
+ // ключ кэша будет равен "doc,1"
614
+ const query = docsFetcher.queries.doc.create('1', 'test');
615
+
616
+ docsFetcher.queries.doc.invalidate('1'); // данные query будут помечены как невалидные
617
+ docsFetcher.queries.doc.invalidate('1', 'test'); // второй параметр будет проигнорирован
343
618
  ```
344
619
 
345
- ## Режим фонового обновления
346
- `Query` и `InfiniteQuery` имеют режим фонового обновления. Предполагается, что будет хорошо подходить для обновления данных через websocket.
620
+ ## Фоновая загрузка данных QuerySet и InfiniteQuerySet
347
621
 
348
- В этом режиме, основные статусные флаги `isSuccess`, `isLoading`, `isError`, `error` будут изменяться до первого успешного запроса. Последующие запросы уже будут изменять статусные флаги под полем `background`
622
+ Для фонового обновления данных возможно использовать флаг `isBackground` для query и infiniteQuery.
623
+ Данный метод хорошо подходит для инвалидации данных по событиям из WebSocket.
349
624
 
625
+ Пример для `QuerySet`:
350
626
  ```ts
351
- const query = mobxQuery.createQuery(
352
- ['some cache key'],
353
- () => Promise.resolve('foo'),
354
- { isBackground: true }
355
- );
627
+ const docQuery = docsFetcher.queries.doc.createWithConfig(({ isBackground: true }, 'docID');
628
+
629
+ docQuery.sync(); // запрос будет выполнен в фоновом режиме
356
630
 
357
- await query.async();
358
- console.log(query.isLoading); // переключался в true на момент запроса
359
- console.log(query.isSuccess); // true
631
+ docQuery.isLoading; // true. Первый запрос изменит статусные флаги
360
632
 
361
- query.invalidate();
362
- await query.async();
363
- console.log(query.isLoading); // не изменялся
364
- console.log(query.isSuccess); // остался неизменным - true
633
+ await when(() => docQuery.isSuccess);
365
634
 
366
- console.log(query.background.isLoading); // переключался в true на момент обновления
367
- console.log(query.background.isSuccess); // true
635
+ docsFetcher.queries.doc.invalidate();
636
+
637
+ docQuery.isLoading; // false. При этом запрос данных уже выполняется в фоновом режиме, если где-то есть подписчик на изменения `.data`
638
+ docQuery.background.isLoading; // true
639
+ ```
640
+
641
+ ## Изменение кэша QuerySet и InfiniteQuerySet
642
+
643
+ Для изменения данных в кэше необходимо использовать `forceUpdate`, содержащийся в query:
644
+ ```ts
645
+ const docQuery = docsFetcher.queries.doc.create('docID');
646
+
647
+ // в кэше будет записано "{ name: 'test' }", а статустные флаги перейдут в состояние 'success'
648
+ docQuery.forceUpdate({ name: 'test' });
368
649
  ```
369
650
 
370
- # Тестирование
651
+ ## Тестирование Fetcher на сонове QuerySet и InfiniteQuerySet.
652
+
653
+ Для мокинга QuerySet и InfiniteQuerySet необходимо использовать mock следующего вида:
654
+ ```ts
655
+ type Fetcher = {
656
+ queries: QuerySet<any[], any>;
657
+ infiniteQueries: InfiniteQuerySet<any[], any>;
658
+ mutations: MutationSet<any[], any>;
659
+ };
660
+
661
+ const mockFetcher = <TFetcher extends Fetcher>(config: DeepPartial<TFetcher>) => config as TFetcher;
662
+ ```
371
663
 
372
- ## Тестирование при включенном ```enabledAutoFetch```
664
+ **Не используйте 'vitest-mock-extended' для мокинга Fetcher.**
665
+ Причина: `vitest-mock-extended` оборачивает объект в Proxy, что нарушает работу Mobx.
373
666
 
374
- ### Исходный код
667
+ ### Тестирование с включенным ```enabledAutoFetch```
375
668
 
376
669
  ```MobxQuery``` инициализируется с параметром: ```enabledAutoFetch```:
377
670
  ```ts
378
- const createMobxQuery = () => new MobxQuery<ApiDataError>({
671
+ const createMobxQuery = () => new MobxQuery({
379
672
  enabledAutoFetch: true,
380
673
  });
381
674
  ```
382
675
 
383
- ```BookRepository``` - фасад для работы с данными, который использует MobxQuery:
676
+ ```booksFetcher.ts```
384
677
  ```ts
385
- export class BookRepository {
386
- constructor(private readonly mobxQuery: MobxQuery) {}
387
-
388
- public getBookListQuery = (params: BookRepositoryDTO.BookListInputDTO) =>
389
- this.mobxQuery.createQuery<BookRepositoryDTO.BookListDTO>(
390
- ['book-list', params],
391
- () =>
392
- apiHttpClient.get('/books', {
393
- params,
394
- }),
395
- );
396
- }
678
+ export const booksFetcher = {
679
+ queries: {
680
+ bookList: mobxQuery.createQuerySet((params: BooksEndpointsDTO.BookListInput) => ({
681
+ execute: () => booksEndpoint.getBookList(params),
682
+ })),
683
+ },
684
+ };
685
+
686
+ export type BooksFetcher = typeof booksFetcher;
397
687
  ```
398
688
 
399
- ```BooksListStore``` - использует BookRepository для получения данных:
689
+ ```BooksListStore``` - использует `BooksFetcher` для получения данных:
400
690
  ```ts
401
691
  class BooksListStore {
402
692
  public sort?: SortData;
403
693
 
404
- constructor(private readonly bookRepository: BookRepository) {
694
+ constructor(private readonly _booksFetcher: BooksFetcher) {
405
695
  makeAutoObservable(this);
406
696
  }
407
697
 
408
698
  private get listQuery() {
409
- return this.bookRepository.getBookListQuery(this.sort);
699
+ return this._booksFetcher.queries.bookList.create(this.sort);
410
700
  }
411
701
 
412
702
  public get list(): ListItem[] {
413
- const data = this.listQuery.data?.data || [];
703
+ const data = this.listQuery.data || [];
414
704
 
415
705
  return data.map(({ id, name, price }) => ({
416
706
  id,
@@ -421,34 +711,36 @@ class BooksListStore {
421
711
  }
422
712
  ```
423
713
 
424
- ### Тест
714
+ Тест ```BooksListStore```:
425
715
 
426
716
  ```ts
427
717
  import { when } from 'mobx';
718
+ import { mockCacheGroups } from '@astral/mobx-query-vitest-mock';
428
719
 
429
720
  describe('BooksListStore', () => {
430
721
  it('Список книг форматируется для отображения', async () => {
431
722
  // Для каждого теста необходимо инициализировать свой instance MobxQuery,
432
- // иначе будет проблема состояния гонки при выполнении нескольких тестов
723
+ // в противном случае каждый тест будет модифицировать кэш
433
724
  const mobxQuery = createMobxQuery();
434
725
 
435
726
  const fakeBookList = makeFakeBookList(2, { price: 1000 });
436
727
  const fakeBookListItem = fakeBookList.data[0];
437
728
 
438
- const bookRepositoryMock = mock<BookRepository>({
439
- // Подменяем реализацию метода для того, чтобы получить ожидаемый результат
440
- getBookListQuery: () =>
441
- // Создаем моковый Query, соответствующий интерфейсу BookRepository
442
- mobxQuery.createQuery(['id'], async () => fakeBookList),
729
+ const booksFetcherMock = mockFetcher<BooksFetcher>({
730
+ queries: {
731
+ bookList: () => mobxQuery.createQuerySet(() => ({
732
+ execute: async () => fakeBookList,
733
+ })),
734
+ },
443
735
  });
444
736
 
445
- const sut = new GoodsListStore(bookRepositoryMock);
737
+ const sut = new BooksListStore(booksFetcherMock);
446
738
 
447
739
  // Ждем автоматической загрузки данных
448
740
  // Загрузка данных начнется автоматически при обращении к sut.list за счет параметра enabledAutoFetch
449
741
  await when(() => Boolean(sut.list?.length));
450
742
 
451
- expect(sut.list[0]).toMatchObject({
743
+ expect(sut.list[0]).toEqual({
452
744
  id: fakeBookListItem.id,
453
745
  name: fakeBookListItem.name,
454
746
  price: '1 000 руб.',
@@ -457,3 +749,472 @@ describe('BooksListStore', () => {
457
749
  });
458
750
  ```
459
751
 
752
+ ## Core
753
+
754
+ Концепции, описанные ниже являются ядром библиотеки и используются внутри `QuerySet`, `InfiniteQuerySet` и `MutationSet`.
755
+
756
+ ### Query
757
+
758
+ `Query` позволяет получать данные из API и кешировать их. Query не должны производить изменения.
759
+
760
+ #### Создание Query
761
+
762
+ В примере ниже будет создан doc query, который будет получать данные по id документа. После успешного выполнения запроса данные будут закэшированы.
763
+
764
+ ```ts
765
+ const createDocQuery = (id: string) =>
766
+ // первый параметр - ключи, по которому в кэше будет храниться ответ запроса
767
+ cacheService.createQuery(['doc', id],
768
+ (params: { id: string }) => docsEndpoint.getDoc(params.id)
769
+ );
770
+ ```
771
+
772
+ #### Использование Query
773
+
774
+ ```ts
775
+ const createDocQuery = (id: string) =>
776
+ // первый параметр - ключи, по которому в кэше будет храниться ответ запроса
777
+ cacheService.createQuery(['doc', id],
778
+ (params: { id: string }) => docsEndpoint.getDoc(params.id)
779
+ );
780
+
781
+ const docQuery = createDocQuery('docID');
782
+ ```
783
+
784
+ `docQuery` является объектом, который содержит всю информацию по запросу и методы работы с запросом:
785
+
786
+ #### Interface Query
787
+
788
+ ```ts
789
+ export type Query<TResult = unknown, TError = unknown, TIsBackground = boolean> = {
790
+ /**
791
+ * Текущие данные запроса
792
+ */
793
+ data: TResult | undefined;
794
+
795
+ /**
796
+ * Флаг загрузки
797
+ */
798
+ isLoading: boolean;
799
+
800
+ /**
801
+ * Флаг успешного выполнения запроса
802
+ */
803
+ isSuccess: boolean;
804
+
805
+ /**
806
+ * Флаг наличия ошибки
807
+ */
808
+ isError: boolean;
809
+
810
+ /**
811
+ * Текущая ошибка
812
+ */
813
+ error: TError | null;
814
+
815
+ /**
816
+ * Флаг, обозначающий простаивание, т.е. запроса еще не было
817
+ */
818
+ isIdle: boolean;
819
+
820
+ /**
821
+ * Синхронизирует данные с сервером
822
+ * @param onSuccess - Callback успешного выполнения
823
+ * @param onError - Callback ошибки
824
+ */
825
+ sync: ({ onSuccess, onError }: SyncParams<TResult, TError>) => void;
826
+
827
+ /**
828
+ * Асинхронный метод получения данных
829
+ */
830
+ async: () => Promise<TResult>;
831
+
832
+ /**
833
+ * Метод инвалидации текущего query
834
+ */
835
+ invalidate: () => void;
836
+
837
+ // Статусы, изменяющиеся после первого успешного запроса в режиме фоновой загрузки isBackground: true
838
+ background: {
839
+ /**
840
+ * Флаг обозначающий загрузку данных в фоновом режиме
841
+ */
842
+ isLoading: boolean;
843
+
844
+ /**
845
+ * Флаг обозначающий, что последний запрос был зафейлен в фоновом режиме
846
+ */
847
+ isError: boolean;
848
+
849
+ /**
850
+ * Данные о последней ошибке в фоновом режиме
851
+ */
852
+ error?: TError;
853
+
854
+ /**
855
+ * Флаг, обозначающий успешность завершения последнего запроса в фоновом режиме
856
+ */
857
+ isSuccess: boolean;
858
+ };
859
+ };
860
+ ```
861
+
862
+ #### Синхронный вызов запроса данных
863
+
864
+ Метод `sync` позволяет синхронно запустить запрос на получение данных:
865
+ ```ts
866
+ const createDocQuery = (id: string) =>
867
+ // первый параметр - ключи, по которому в кэше будет храниться ответ запроса
868
+ cacheService.createQuery(['doc', id],
869
+ (params: { id: string }) => docsEndpoint.getDoc(params.id)
870
+ );
871
+
872
+ const docQuery = createDocQuery('docID');
873
+
874
+ // есть callbacks на обработку success и error
875
+ docQuery.sync({
876
+ onSuccess: (data) => {
877
+ console.log(data);
878
+ },
879
+ onError: (error) => {
880
+ console.log(error);
881
+ },
882
+ });
883
+
884
+ // true
885
+ docQuery.isLoading;
886
+ ```
887
+
888
+ #### Асинхронный вызов запроса данных
889
+
890
+ Метод `async` позволяет асинхронно запустить запрос на получение данных:
891
+ ```ts
892
+ const createDocQuery = (id: string) =>
893
+ // первый параметр - ключи, по которому в кэше будет храниться ответ запроса
894
+ cacheService.createQuery(['doc', id],
895
+ (params: { id: string }) => docsEndpoint.getDoc(params.id)
896
+ );
897
+
898
+ const docQuery = createDocQuery('docID');
899
+
900
+ // передавать параметры запроса не нужно потому что они уже были переданы при вызове init
901
+ const response = await docQuery.async();
902
+
903
+ docQuery.isSuccess; // true
904
+ docQuery.data; // идентичен response
905
+ ```
906
+
907
+ #### Автоматический запрос данных
908
+
909
+ Если MobxQuery был создан с флагом `enableAutoFetch: true`, то данные будут автоматически запрошены при обращении к полю `data`:
910
+ ```ts
911
+ const cacheService = new MobxQuery({ enableAutoFetch: true });
912
+
913
+ const createDocQuery = (id: string) =>
914
+ // первый параметр - ключи, по которому в кэше будет храниться ответ запроса
915
+ cacheService.createQuery(['doc', id],
916
+ (params: { id: string }) => docsEndpoint.getDoc(params.id)
917
+ );
918
+
919
+ docQuery.data; // триггер запроса данных
920
+
921
+ docQuery.isLoading; // true
922
+ ```
923
+
924
+ Если MobxQuery был создан с флагом `enableAutoFetch: false`, то автоматически запрос данных можно включить для текущего query:
925
+ ```ts
926
+ const createDocQuery = (id: string) =>
927
+ cacheService.createQuery(['doc', id],
928
+ (params: { id: string }) => docsEndpoint.getDoc(params.id),
929
+ { enableAutoFetch: true }
930
+ );
931
+
932
+ docQuery.data; // триггер запроса данных
933
+
934
+ docQuery.isLoading; // true
935
+ ```
936
+
937
+ ### InfiniteQuery
938
+
939
+ `InfiniteQuery` предназначен для получения бесконечного списка данных.
940
+
941
+ ### Создание InfiniteQuery
942
+
943
+ В примере ниже будет создан docList InfiniteQuery. После успешного выполнения запроса данные будут закэшированы.
944
+
945
+ ```ts
946
+ const createDocListInfiniteQuery = (filters: Omit<DocsEndpointsDTO.DocListFilters, 'offset' | 'count'>) =>
947
+ cacheService.createInfiniteQuery(['docList', filters],
948
+ (params: { offset: number; count: number }) => docsEndpoint.getDocList({ offset: params.offset, count: params.count, ...filters })
949
+ );
950
+ ```
951
+
952
+ #### Использование InfiniteQuery
953
+
954
+ ```ts
955
+ const createDocListInfiniteQuery = (filters: Omit<DocsEndpointsDTO.DocListFilters, 'offset' | 'count'>) =>
956
+ cacheService.createInfiniteQuery(['docList', filters],
957
+ (params: { offset: number; count: number }) => docsEndpoint.getDocList({ offset: params.offset, count: params.count, ...filters })
958
+ );
959
+
960
+ // первым параметром create является - filters: Omit<DocsEndpointsDTO.DocListFilters, 'offset' | 'count'>
961
+ const docListQuery = createDocListInfiniteQuery({ search: 'test' });
962
+ ```
963
+
964
+ #### Загрузка данных
965
+
966
+ ```ts
967
+ import { when } from 'mobx';
968
+
969
+ const createDocListInfiniteQuery = (filters: Omit<DocsEndpointsDTO.DocListFilters, 'offset' | 'count'>) =>
970
+ cacheService.createInfiniteQuery(['docList', filters],
971
+ (params: { offset: number; count: number }) => docsEndpoint.getDocList({ offset: params.offset, count: params.count, ...filters })
972
+ );
973
+
974
+ const docListQuery = createDocListInfiniteQuery({ search: 'test' });
975
+
976
+ await docListQuery.async(); // запрос на получение первых 30 записей
977
+
978
+ docListQuery.data.length; // 30 записей
979
+
980
+ docListQuery.fetchMore(); // запрос на получение следующих 30 записей
981
+
982
+ // ждем загрузки
983
+ await when(() => !query.isLoading);
984
+
985
+ docListQuery.data.length; // 60 записей
986
+ ```
987
+
988
+ #### `isEndReached`. Определение конца списка
989
+
990
+ Флаг `isEndReached` будет установлен в `true`, если записи для загрузки закончились:
991
+ ```ts
992
+ import { when } from 'mobx';
993
+
994
+ const createDocListInfiniteQuery = (filters: Omit<DocsEndpointsDTO.DocListFilters, 'offset' | 'count'>) =>
995
+ cacheService.createInfiniteQuery(['docList', filters],
996
+ (params: { offset: number; count: number }) => docsEndpoint.getDocList({ offset: params.offset, count: params.count, ...filters })
997
+ );
998
+
999
+ const docListQuery = createDocListInfiniteQuery({ search: 'test' });
1000
+
1001
+ await docListQuery.async(); // запрос на получение первых 30 записей
1002
+
1003
+ docListQuery.data.length; // 30 записей
1004
+
1005
+ docListQuery.fetchMore(); // запрос на получение следующих 30 записей
1006
+
1007
+ // ждем загрузки
1008
+ await when(() => !query.isLoading);
1009
+
1010
+ docListQuery.data.length; // 19 записей. Последняя страница содержит 9 записей, а не 30, как было запрошено, значит больше данных нет
1011
+
1012
+ docListQuery.isEndReached; // true
1013
+ ```
1014
+
1015
+ Флаг `isEndReached` устанавливается в `true`, когда количество полученных элементов меньше запрошенного количества, что означает отсутствие дополнительных данных на сервере.
1016
+
1017
+ #### Изменение количества запрашиваемых записей
1018
+
1019
+ Для изменения конфигурации InfiniteQuery необходимо использовать третий параметр `config`:
1020
+ ```ts
1021
+ const createDocListInfiniteQuery = (filters: Omit<DocsEndpointsDTO.DocListFilters, 'offset' | 'count'>) =>
1022
+ cacheService.createInfiniteQuery(['docList', filters],
1023
+ (params: { offset: number; count: number }) => docsEndpoint.getDocList({ offset: params.offset, count: params.count, ...filters }),
1024
+ { incrementCount: 10 }
1025
+ );
1026
+ ```
1027
+
1028
+ #### Интерфейс InfiniteQueryConfig
1029
+ ```ts
1030
+ export type InfiniteQueryConfig = {
1031
+ /**
1032
+ * Количество записей, которое будет загружено при первом и следующих запросах
1033
+ * @default 30
1034
+ */
1035
+ incrementCount?: number;
1036
+ /**
1037
+ * Обработчик ошибки
1038
+ */
1039
+ onError?: (error: unknown) => void;
1040
+ /**
1041
+ * Флаг, отвечающий за автоматический запрос данных при обращении к полю data
1042
+ */
1043
+ enabledAutoFetch?: boolean;
1044
+ /**
1045
+ * Политика кэширования данных.
1046
+ */
1047
+ fetchPolicy?: FetchPolicy;
1048
+ /**
1049
+ * Режим фонового обновления
1050
+ * @default false
1051
+ */
1052
+ isBackground?: boolean;
1053
+ };
1054
+ ```
1055
+
1056
+ ### Mutation
1057
+
1058
+ `Mutation` предназначен для отправки данных на сервер с целью произведения изменений.
1059
+
1060
+ ### Создание и использование mutations
1061
+
1062
+ В примере ниже будет создан `editDoc` mutation, который отправляет данные на сервер для редактирования документа:
1063
+ ```ts
1064
+ const createEditDocMutation = (params: DocsEndpointsDTO.EditDocInput) =>
1065
+ cacheService.createMutation((params: DocsEndpointsDTO.EditDocInput) => docsEndpoint.editDoc(params));
1066
+
1067
+ const editDocMutation = createEditDocMutation();
1068
+
1069
+ // params являются параметрами, указанными при определении mutation - DocsEndpointsDTO.EditDocInput
1070
+ await editDocMutation.async({ params: { id: 'docID', name: 'test' } });
1071
+ ```
1072
+
1073
+ #### Интерфейс Mutation
1074
+
1075
+ ```ts
1076
+ type Mutation<TResult, TError = unknown, TExecutorParams = void> = {
1077
+ /**
1078
+ * Синхронный метод выполнения мутации
1079
+ */
1080
+ sync: (options: {
1081
+ onSuccess?: (res: TResult) => void;
1082
+ onError?: (e: TError) => void;
1083
+ params?: TExecutorParams;
1084
+ }) => void;
1085
+
1086
+ /**
1087
+ * Асинхронный метод выполнения мутации
1088
+ */
1089
+ async: (params: TExecutorParams) => Promise<TResult>;
1090
+
1091
+ /**
1092
+ * Флаг загрузки
1093
+ */
1094
+ isLoading: boolean;
1095
+
1096
+ /**
1097
+ * Флаг успешного выполнения запроса
1098
+ */
1099
+ isSuccess: boolean;
1100
+
1101
+ /**
1102
+ * Флаг наличия ошибки
1103
+ */
1104
+ isError: boolean;
1105
+
1106
+ /**
1107
+ * Текущая ошибка
1108
+ */
1109
+ error: TError | null;
1110
+
1111
+ /**
1112
+ * Флаг, обозначающий простаивание, т.е. запроса еще не было
1113
+ */
1114
+ isIdle: boolean;
1115
+
1116
+ // Статусы, изменяющиеся после первого успешного запроса в режиме фоновой загрузки isBackground: true
1117
+ background: {
1118
+ /**
1119
+ * Флаг обозначающий загрузку данных в фоновом режиме
1120
+ */
1121
+ isLoading: boolean;
1122
+
1123
+ /**
1124
+ * Флаг обозначающий, что последний запрос был зафейлен в фоновом режиме
1125
+ */
1126
+ isError: boolean;
1127
+
1128
+ /**
1129
+ * Данные о последней ошибке в фоновом режиме
1130
+ */
1131
+ error?: TError;
1132
+
1133
+ /**
1134
+ * Флаг, обозначающий успешность завершения последнего запроса в фоновом режиме
1135
+ */
1136
+ isSuccess: boolean;
1137
+ };
1138
+ };
1139
+ ```
1140
+
1141
+ #### Синхронный вызов mutation
1142
+
1143
+ Метод `sync` позволяет синхронно запустить запрос:
1144
+ ```ts
1145
+ const createEditDocMutation = (params: DocsEndpointsDTO.EditDocInput) =>
1146
+ cacheService.createMutation((params: DocsEndpointsDTO.EditDocInput) => docsEndpoint.editDoc(params));
1147
+
1148
+ const editDocMutation = createEditDocMutation();
1149
+
1150
+ // есть callbacks на обработку success и error
1151
+ editDocMutation.sync({
1152
+ params: { id: 'docID', name: 'test' },
1153
+ onSuccess: (data) => {
1154
+ console.log(data);
1155
+ },
1156
+ onError: (error) => {
1157
+ console.log(error);
1158
+ },
1159
+ });
1160
+
1161
+ // true
1162
+ editDocMutation.isLoading;
1163
+ ```
1164
+
1165
+ ##### Асинхронный вызов mutation
1166
+
1167
+ Метод `async` позволяет асинхронно запустить запрос:
1168
+ ```ts
1169
+ const createEditDocMutation = (params: DocsEndpointsDTO.EditDocInput) =>
1170
+ cacheService.createMutation((params: DocsEndpointsDTO.EditDocInput) => docsEndpoint.editDoc(params));
1171
+
1172
+ const editDocMutation = createEditDocMutation();
1173
+
1174
+ await editDocMutation.async({ params: { id: 'docID', name: 'test' } });
1175
+ ```
1176
+
1177
+ ### Особенности инвалидации Queries
1178
+
1179
+ Как при создании query, так и при инвалидации, нужно использовать массив ключей. Предполагается, что query может быть инвалидирован по нескольким ключам.
1180
+
1181
+ ```ts
1182
+ const query = mobxQuery.createQuery(
1183
+ ['key one', 'key two'], // ключ - массив строк
1184
+ () => Promise.resolve('foo'),
1185
+ { enabledAutoFetch: true }
1186
+ );
1187
+
1188
+ mobxQuery.invalidate(['key two']); // query будет инвалидирован
1189
+ mobxQuery.invalidate(['key one']); // query будет инвалидирован
1190
+ ```
1191
+
1192
+ Но, стоит учитывать, что ключом является цельный элемент массива, а не составляющие элемента.
1193
+ ```ts
1194
+ const query = mobxQuery.createQuery(
1195
+ [['key one', 'key two']], // ключ - двумерный массив строк
1196
+ () => Promise.resolve('foo'),
1197
+ { enabledAutoFetch: true }
1198
+ );
1199
+
1200
+ mobxQuery.invalidate(['key one']); // ключ не совпадает, query НЕ будет инвалидирован
1201
+ ```
1202
+
1203
+ Инвалидация будет происходить только для query, поле data которых считывается в данный момент. Для query, data которых будут отрендерены позже, запрос произойдет только в момент использования. Для превентивного обновления данных потребуется последовательное использование sync/async методов сразу после invalidate.
1204
+
1205
+ ### Массовая инвалидация
1206
+
1207
+ Для инвалидации всех query необходимо использовать метод `invalidateQueries`:
1208
+ ```ts
1209
+ mobxQuery.invalidateQueries();
1210
+ ```
1211
+
1212
+ ### Изменение кэша
1213
+
1214
+ Для изменения данных в кэше необходимо использовать `forceUpdate`, содержащийся в query:
1215
+ ```ts
1216
+ const query = mobxQuery.createQuery(['key'], () => Promise.resolve('1'));
1217
+
1218
+ // в кэше будет записано '2', а статустные флаги перейдут в состояние 'success'
1219
+ query.forceUpdate('2');
1220
+ ```