@reforgium/statum 2.1.2 → 3.0.0-rc.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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @reforgium/statum
1
+ # @reforgium/statum
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/%40reforgium%2Fstatum.svg)](https://www.npmjs.com/package/@reforgium/statum)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -25,13 +25,29 @@ Designed for **application state**, not abstract reducers.
25
25
 
26
26
  ---
27
27
 
28
- ## Installation
29
-
30
- ```bash
31
- npm install @reforgium/statum
32
- ```
33
-
34
- ---
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install @reforgium/statum
32
+ ```
33
+
34
+ ## Configuration
35
+
36
+ ```ts
37
+ import { provideStatum } from '@reforgium/statum';
38
+
39
+ providers: [
40
+ provideStatum({
41
+ pagedQuery: {
42
+ defaultMethod: 'GET',
43
+ defaultHasCache: true,
44
+ defaultCacheSize: 10,
45
+ },
46
+ }),
47
+ ];
48
+ ```
49
+
50
+ ---
35
51
 
36
52
  ## Package Structure
37
53
 
@@ -136,7 +152,7 @@ const user = await userStore.get(
136
152
  );
137
153
  ```
138
154
 
139
- Retry + trace example:
155
+ Retry + trace example:
140
156
 
141
157
  ```ts
142
158
  import { ResourceStore } from '@reforgium/statum';
@@ -150,11 +166,22 @@ const store = new ResourceStore<{ id: number; name: string }>(
150
166
  }
151
167
  );
152
168
 
153
- await store.get(
154
- { params: { id: '42' } },
155
- { retry: { attempts: 1 } } // per-call override
156
- );
157
- ```
169
+ await store.get(
170
+ { params: { id: '42' } },
171
+ { retry: { attempts: 1 } } // per-call override
172
+ );
173
+ ```
174
+
175
+ Profiles example:
176
+
177
+ ```ts
178
+ import { createResourceProfile, ResourceStore } from '@reforgium/statum';
179
+
180
+ const usersStore = new ResourceStore<{ id: number; name: string }>(
181
+ { GET: '/users' },
182
+ createResourceProfile('table', { baseUrl: '/api' })
183
+ );
184
+ ```
158
185
 
159
186
  ---
160
187
 
@@ -179,9 +206,9 @@ entities.removeOne(1);
179
206
 
180
207
  ---
181
208
 
182
- ## PaginatedDataStore
183
-
184
- Lightweight store for server-side pagination with sorting, filtering, and page cache.
209
+ ## PagedQueryStore
210
+
211
+ Lightweight store for server-side pagination with filtering, dynamic query params, and page cache.
185
212
 
186
213
  ### When to use
187
214
 
@@ -200,46 +227,54 @@ Lightweight store for server-side pagination with sorting, filtering, and page c
200
227
  | page | `number` | Current page (0-based) |
201
228
  | pageSize | `number` | Page size |
202
229
  | totalElements | `number` | Total items on server |
203
- | filters | `Partial<F>` | Active filters |
204
- | sort | `string \| undefined` | Sort as `"field,asc"` / `"field,desc"` |
230
+ | filters | `Partial<F>` | Active filters |
231
+ | query | `Record<string, unknown>` | Active query params |
205
232
 
206
233
  ### Methods
207
234
 
208
- | Method | Description |
209
- |----------------|---------------------------------------------------|
210
- | refresh | Repeat request with current params |
211
- | updatePage | Change page (can use cache) |
212
- | updatePageSize | Change page size (can use cache) |
213
- | updateFilters | Merge filters, reset cache, go to page 0 |
214
- | setFilters | Replace filters, reset cache + sort, go to page 0 |
215
- | updateQuery | Map table events to `page`/`sort` |
216
- | updateRoute | Change endpoint, reset meta/cache |
217
- | setRouteParams | Fill route url with params |
218
- | updateConfig | Patch config and apply presets |
219
- | copy | Copy config/meta from another store |
220
- | destroy | Manual destroying of caches and abort requests |
235
+ | Method | Description |
236
+ |----------------|---------------------------------------------------------------------------|
237
+ | fetch | Clean first-page request: `fetch({ filters, query, routeParams })` |
238
+ | refresh | Repeat request, optional merge overrides: `refresh({ filters, query })` |
239
+ | updatePage | Change page: `updatePage(page, { ignoreCache })` |
240
+ | updatePageSize | Change page size and reset cache: `updatePageSize(size)` |
241
+ | updateByOffset | Table-event mapper: `updateByOffset({ page/first/rows }, { query })` |
242
+ | setRouteParams | Update route params: `setRouteParams(params, { reset, abort })` |
243
+ | updateConfig | Patch config: `updateConfig(config)` |
244
+ | copy | Copy config/meta: `copy(store)` |
245
+ | destroy | Manual destroying of caches and abort requests |
246
+
247
+ ### Cache behavior
248
+
249
+ | Method | Cache read | Cache reset | Notes |
250
+ |----------------|------------|-------------|-------|
251
+ | fetch | no | yes | Always starts clean from page `0` |
252
+ | refresh | no | no | Uses active page/filters/query, merges overrides |
253
+ | updatePage | yes | no | Can bypass with `ignoreCache: true` |
254
+ | updatePageSize | no | yes | Prevents mixed caches for different page sizes |
255
+ | updateByOffset | yes | no | Internally maps to `page + size` |
221
256
 
222
257
  Example:
223
258
 
224
259
  ```ts
225
- import { PaginatedDataStore } from '@reforgium/statum';
260
+ import { PagedQueryStore } from '@reforgium/statum';
226
261
 
227
262
  type User = { id: number; name: string };
228
263
 
229
- const store = new PaginatedDataStore<User, { search?: string }>('api/users', {
230
- method: 'GET',
231
- debounceTime: 200,
232
- });
233
-
234
- store.updateFilters({ search: 'neo' });
235
- store.updatePage(1);
264
+ const store = new PagedQueryStore<User, { search?: string }>('api/users', {
265
+ method: 'GET',
266
+ debounceTime: 200,
267
+ });
268
+
269
+ store.fetch({ filters: { search: 'neo' }, query: { tenant: 'kg' } });
270
+ store.updateByOffset({ first: 20, rows: 20 });
236
271
  ```
237
272
 
238
273
  ---
239
274
 
240
275
  ## DictStore
241
276
 
242
- Helper for dictionaries/options (select/autocomplete) on top of PaginatedDataStore.
277
+ Helper for dictionaries/options (select/autocomplete) on top of PagedQueryStore.
243
278
 
244
279
  ### When to use
245
280
 
@@ -309,15 +344,15 @@ dict.search('kir'); // local search over cache
309
344
 
310
345
  ## Composition Patterns
311
346
 
312
- ### PaginatedDataStore + EntityStore
347
+ ### PagedQueryStore + EntityStore
313
348
 
314
349
  ```ts
315
350
  import { effect } from '@angular/core';
316
- import { EntityStore, PaginatedDataStore } from '@reforgium/statum';
351
+ import { EntityStore, PagedQueryStore } from '@reforgium/statum';
317
352
 
318
353
  type User = { id: number; name: string };
319
354
 
320
- const pages = new PaginatedDataStore<User, { search?: string }>('/api/users');
355
+ const pages = new PagedQueryStore<User, { search?: string }>('/api/users');
321
356
  const entities = new EntityStore<User, 'id'>({ idKey: 'id' });
322
357
 
323
358
  effect(() => {
@@ -325,7 +360,7 @@ effect(() => {
325
360
  });
326
361
  ```
327
362
 
328
- This keeps page-loading logic in `PaginatedDataStore` and normalized lookup/update logic in `EntityStore`.
363
+ This keeps page-loading logic in `PagedQueryStore` and normalized lookup/update logic in `EntityStore`.
329
364
 
330
365
  ---
331
366
 
@@ -374,11 +409,12 @@ const body = serializer.serialize({ name: ' Vasya ', active: null });
374
409
 
375
410
  ---
376
411
 
377
- ## Source Structure
378
-
379
- - Cache: `src/cache`
380
- - Stores: `src/stores`
381
- - Serializer: `src/serializer`
412
+ ## Source Structure
413
+
414
+ - Cache: `src/cache`
415
+ - Stores: `src/stores`
416
+ - Serializer: `src/serializer`
417
+ - Migration: `MIGRATION.md`
382
418
 
383
419
  ---
384
420
 
@@ -393,3 +429,5 @@ const body = serializer.serialize({ name: ' Vasya ', active: null });
393
429
  ## License
394
430
 
395
431
  MIT
432
+
433
+
@@ -1,6 +1,6 @@
1
1
  import { formatDate, isNullable, isDatePeriod, parseToDate, makeQuery, isNumber, isObject, parseToDatePeriod, parseQueryArray, fillUrlWithParams, deepEqual, concatArray, debounceSignal } from '@reforgium/internal';
2
2
  import { HttpClient } from '@angular/common/http';
3
- import { InjectionToken, inject, signal, computed, DestroyRef, effect, untracked } from '@angular/core';
3
+ import { InjectionToken, makeEnvironmentProviders, inject, signal, computed, DestroyRef, effect, untracked } from '@angular/core';
4
4
  import { Subject, filter, timer, merge, map } from 'rxjs';
5
5
  import { debounce, tap, throttle, finalize } from 'rxjs/operators';
6
6
 
@@ -599,6 +599,7 @@ const storageStrategy = (strategy, options = {}) => {
599
599
 
600
600
  // noinspection ES6PreferShortImport
601
601
  const STATUM_CONFIG = new InjectionToken('RE_STATUM_CONFIG');
602
+ const provideStatum = (config) => makeEnvironmentProviders([{ provide: STATUM_CONFIG, useValue: config }]);
602
603
 
603
604
  /**
604
605
  * Error thrown when requested data is missing in the cache.
@@ -1219,39 +1220,64 @@ class ResourceStore {
1219
1220
  }
1220
1221
  }
1221
1222
 
1223
+ const RESOURCE_PROFILES = {
1224
+ table: {
1225
+ ttlMs: 15_000,
1226
+ delay: 120,
1227
+ delayMode: 'debounce',
1228
+ maxEntries: 500,
1229
+ },
1230
+ lookup: {
1231
+ ttlMs: 60_000,
1232
+ delay: 200,
1233
+ delayMode: 'debounce',
1234
+ maxEntries: 300,
1235
+ },
1236
+ mutation: {
1237
+ ttlMs: 0,
1238
+ delay: 0,
1239
+ delayMode: 'throttle',
1240
+ maxEntries: 50,
1241
+ },
1242
+ };
1243
+ const createResourceProfile = (profile, overrides = {}) => ({
1244
+ ...RESOURCE_PROFILES[profile],
1245
+ ...overrides,
1246
+ });
1247
+
1222
1248
  // noinspection ES6PreferShortImport
1223
1249
  /**
1224
1250
  * Store for paginated data (tables/lists) with per-page cache and unified requests.
1225
1251
  *
1226
1252
  * Provides:
1227
1253
  * - reactive signals: `items`, `loading`, `cached`;
1228
- * - methods to control page/size/filters/sorting;
1254
+ * - methods to control page/size/query;
1229
1255
  * - optional LRU cache by pages;
1230
- * - configurable transport (GET/POST/PATCH/…).
1256
+ * - configurable transport (GET/POST/PATCH/…).
1231
1257
  *
1232
1258
  * Example:
1233
1259
  * ```ts
1234
- * const ds = new PaginatedDataStore<User>('/users', { method: 'GET', hasCache: true });
1260
+ * const ds = new PagedQueryStore<User>('/users', { method: 'GET', hasCache: true });
1235
1261
  * await ds.updatePage(0); // load the first page
1236
1262
  * effect(() => console.log(ds.items(), ds.loading()));
1237
1263
  * ```
1238
1264
  */
1239
- class PaginatedDataStore {
1265
+ class PagedQueryStore {
1240
1266
  route;
1241
1267
  config;
1242
- defaultConfig = inject(STATUM_CONFIG, { optional: true })?.paginatedData || {};
1268
+ defaultConfig = inject(STATUM_CONFIG, { optional: true })?.pagedQuery || {};
1243
1269
  #transport;
1244
1270
  #cache;
1245
1271
  /** Current page data (reactive). */
1246
1272
  items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
1247
- /** Merged cache of pages (flat list) handy for search/export. */
1273
+ /** Merged cache of pages (flat list) — handy for search/export. */
1248
1274
  cached = signal([], ...(ngDevMode ? [{ debugName: "cached" }] : []));
1249
1275
  /** Loading flag of the current operation. */
1250
1276
  loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
1251
1277
  /** Current filters (applied to requests). */
1252
1278
  filters = {};
1253
- /** Current sorting (`field,asc|desc`). */
1254
- sort;
1279
+ /** Additional query params sent to transport requests. */
1280
+ query = {};
1255
1281
  /** Current page index (0-based). */
1256
1282
  page = 0;
1257
1283
  /** Default page size. */
@@ -1272,17 +1298,32 @@ class PaginatedDataStore {
1272
1298
  this.initTransport();
1273
1299
  inject(DestroyRef).onDestroy(() => this.destroy());
1274
1300
  }
1275
- /** Force reload current data (with the same page/filters/sort). */
1276
- refresh() {
1277
- return this.#fetchItems({});
1301
+ /**
1302
+ * Fetch data with explicit filters and query params from the first page.
1303
+ * Replaces legacy `setFilters`.
1304
+ */
1305
+ fetch = ({ filters = {}, query = {}, routeParams = {} } = {}) => {
1306
+ this.#cache.clear();
1307
+ this.cached.set([]);
1308
+ return this.#fetchItems({ page: 0, filters, query, routeParams });
1309
+ };
1310
+ /**
1311
+ * Force reload current data.
1312
+ * Optional args merge into active filters/query.
1313
+ * Route params are intentionally not changed from `refresh`.
1314
+ */
1315
+ refresh({ filters, query } = {}) {
1316
+ const nextFilters = filters == null ? this.filters : { ...this.filters, ...filters };
1317
+ const nextQuery = query == null ? this.query : { ...this.query, ...query };
1318
+ return this.#fetchItems({ filters: nextFilters, query: nextQuery });
1278
1319
  }
1279
1320
  /**
1280
1321
  * Switch page with a request.
1281
- * If cache is enabled and the page is present in LRU returns it from cache.
1322
+ * If cache is enabled and the page is present in LRU — returns it from cache.
1282
1323
  * @param page page index (0-based)
1283
1324
  * @param ignoreCache ignore cache and fetch from network
1284
1325
  */
1285
- updatePage = (page = this.page, ignoreCache = false) => {
1326
+ updatePage = (page = this.page, { ignoreCache = false } = {}) => {
1286
1327
  if (this.config.hasCache && this.#cache.has(page) && !ignoreCache) {
1287
1328
  const cached = this.#cache.get(page);
1288
1329
  this.items.set(cached);
@@ -1298,54 +1339,23 @@ class PaginatedDataStore {
1298
1339
  * @param size new size (rows per page)
1299
1340
  */
1300
1341
  updatePageSize = (size = this.pageSize) => {
1301
- return this.#fetchItems({ page: 0, size });
1302
- };
1303
- /**
1304
- * Update filters (goes to the first page) and fetch.
1305
- * The previous cache is cleared.
1306
- */
1307
- updateFilters = (filters) => {
1308
1342
  this.#cache.clear();
1309
1343
  this.cached.set([]);
1310
- return this.#fetchItems({ page: 0, filters: { ...this.filters, ...filters } });
1344
+ return this.#fetchItems({ page: 0, size });
1311
1345
  };
1312
1346
  /**
1313
- * Update the state from table events (PrimeNG, etc.) and fetch.
1314
- * Supports `page/first/rows/sortField/sortOrder`.
1347
+ * Helper for offset-based table events (PrimeNG, etc.) with `page/first/rows`.
1315
1348
  */
1316
- updateQuery = ({ page: pageNum, first = 0, rows = 0, sortOrder, sortField }) => {
1349
+ updateByOffset = ({ page: pageNum, first = 0, rows = 0 } = {}, { query = this.query } = {}) => {
1317
1350
  const page = (pageNum ?? (first && rows && Math.floor(first / rows))) || 0;
1318
- const sort = sortField ? `${sortField},${sortOrder === 1 ? 'asc' : 'desc'}` : '';
1319
- return this.#fetchItems({ page, sort });
1320
- };
1321
- /**
1322
- * Set filters from scratch (goes to the first page and resets sorting) and fetch.
1323
- * Useful for quick presets.
1324
- */
1325
- setFilters = (filters = {}) => {
1326
- this.#cache.clear();
1327
- this.cached.set([]);
1328
- return this.#fetchItems({ page: 0, filters, sort: undefined });
1329
- };
1330
- /**
1331
- * Change the resource route (resets page, cache, and presets) without fetching.
1332
- * Useful when one store should work with different endpoints.
1333
- */
1334
- updateRoute = (route) => {
1335
- this.route = route;
1336
- this.page = 0;
1337
- this.pageSize = 20;
1338
- this.totalElements = 0;
1339
- this.#cache.clear();
1340
- this.cached.set([]);
1341
- this.initTransport();
1351
+ const size = rows || this.pageSize;
1352
+ return this.#fetchItems({ page, size, query });
1342
1353
  };
1343
1354
  /**
1344
1355
  * Set route parameters (path variables) for the resource URL.
1345
- * Does not trigger loading automatically call `refresh()` or `updatePage()` after.
1356
+ * Does not trigger loading automatically - call `refresh()` or `updatePage()` after.
1346
1357
  *
1347
1358
  * @param params Dictionary of route parameters (e.g., `{ id: '123' }`)
1348
- * @param opts Options object
1349
1359
  * @param opts.reset If `true`, resets page to 0, clears cache, total elements count, and items
1350
1360
  * @param opts.abort If `true`, aborts all active transport requests and sets loading to false
1351
1361
  *
@@ -1358,20 +1368,20 @@ class PaginatedDataStore {
1358
1368
  * store.setRouteParams({ userId: '42' }, { reset: false, abort: true });
1359
1369
  * ```
1360
1370
  */
1361
- setRouteParams = (params = {}, opts = {}) => {
1371
+ setRouteParams = (params = {}, { reset = false, abort = false } = {}) => {
1362
1372
  const isChanged = !deepEqual(this.routeParams, params);
1363
1373
  if (!isChanged) {
1364
1374
  return;
1365
1375
  }
1366
1376
  this.routeParams = params;
1367
- if (opts.reset) {
1377
+ if (reset) {
1368
1378
  this.page = 0;
1369
1379
  this.totalElements = 0;
1370
1380
  this.#cache.clear();
1371
1381
  this.cached.set([]);
1372
1382
  this.items.set([]);
1373
1383
  }
1374
- if (opts?.abort) {
1384
+ if (abort) {
1375
1385
  try {
1376
1386
  this.#transport?.abortAll?.('Route params changed');
1377
1387
  }
@@ -1392,14 +1402,14 @@ class PaginatedDataStore {
1392
1402
  /**
1393
1403
  * Copy configuration and presets from another store of the same type.
1394
1404
  */
1395
- copy(helper) {
1396
- this.applyConfig(helper.config);
1405
+ copy(store) {
1406
+ this.applyConfig(store.config);
1397
1407
  this.applyPresetMeta();
1398
1408
  }
1399
1409
  /** Clean up resources and cancel active requests. Automatically called onDestroy. */
1400
1410
  destroy() {
1401
1411
  try {
1402
- this.#transport?.abortAll?.('PaginatedDataStore destroyed');
1412
+ this.#transport?.abortAll?.('PagedQueryStore destroyed');
1403
1413
  }
1404
1414
  catch (e) {
1405
1415
  if (!isAbort(e)) {
@@ -1407,15 +1417,16 @@ class PaginatedDataStore {
1407
1417
  }
1408
1418
  }
1409
1419
  }
1410
- #fetchItems = async ({ page = this.page, size = this.pageSize, sort = this.sort, filters = this.filters, }) => {
1411
- const query = this.parseQuery({ page, size, sort, filters });
1420
+ #fetchItems = async ({ page = this.page, size = this.pageSize, filters = this.filters, query = this.query, routeParams = this.routeParams, }) => {
1421
+ const builtQuery = this.parseQuery({ page, size, filters, query });
1412
1422
  this.loading.set(true);
1423
+ this.routeParams = routeParams;
1413
1424
  this.#transport.abortAll();
1414
1425
  try {
1415
- const response = await this.runTransport(filters, query);
1426
+ const response = await this.runTransport(filters, builtQuery, routeParams);
1416
1427
  this.page = page;
1417
- this.sort = sort;
1418
1428
  this.filters = filters;
1429
+ this.query = query;
1419
1430
  const parsed = await this.parseResponseData(response);
1420
1431
  this.pageSize = parsed.pageable?.pageSize || size;
1421
1432
  this.totalElements = parsed.totalElements;
@@ -1437,12 +1448,11 @@ class PaginatedDataStore {
1437
1448
  this.#transport = new ResourceStore({ [this.config.method || 'GET']: this.route }, {
1438
1449
  delay: this.config.debounceTime,
1439
1450
  delayMode: 'debounce',
1440
- presetQueries: { page: this.page, size: this.pageSize, sort: this.sort },
1451
+ presetQueries: { page: this.page, size: this.pageSize },
1441
1452
  presetPayload: this.filters,
1442
1453
  });
1443
1454
  }
1444
- async runTransport(payload, query) {
1445
- const params = this.routeParams;
1455
+ async runTransport(payload, query, params) {
1446
1456
  if (this.config.method === 'GET') {
1447
1457
  return this.#transport.get({ query, params }, { promote: false, dedupe: true });
1448
1458
  }
@@ -1450,10 +1460,16 @@ class PaginatedDataStore {
1450
1460
  // @ts-ignore
1451
1461
  return this.#transport[method]({ query, payload, params }, { promote: false, dedupe: true });
1452
1462
  }
1453
- parseQuery({ page = 0, size, sort, filters }) {
1463
+ parseQuery({ page = 0, size, filters, query = {} }) {
1454
1464
  const method = this.config.method || 'GET';
1455
- const requestPayload = { page, size, ...(method === 'GET' ? filters : {}) };
1456
- const rawQueries = this.config.parseRequest?.({ ...requestPayload, sort }) || requestPayload;
1465
+ const requestPayload = {
1466
+ page,
1467
+ size,
1468
+ ...filters,
1469
+ ...query,
1470
+ };
1471
+ const fallbackQuery = method === 'GET' ? requestPayload : { page, size, ...query };
1472
+ const rawQueries = this.config.parseRequest?.(requestPayload) || fallbackQuery;
1457
1473
  const queries = {};
1458
1474
  Object.entries(rawQueries).forEach(([key, value]) => {
1459
1475
  if (Array.isArray(value)) {
@@ -1463,7 +1479,6 @@ class PaginatedDataStore {
1463
1479
  queries[key] = value;
1464
1480
  }
1465
1481
  });
1466
- sort && (queries['sort'] = sort);
1467
1482
  return queries;
1468
1483
  }
1469
1484
  parseResponseData = async (data) => {
@@ -1509,7 +1524,6 @@ class PaginatedDataStore {
1509
1524
  this.filters = this.config.presetFilters || {};
1510
1525
  this.pageSize = this.config.presetQuery?.pageSize || 20;
1511
1526
  this.page = this.config.presetQuery?.page || 0;
1512
- this.sort = this.config.presetQuery?.sort;
1513
1527
  }
1514
1528
  }
1515
1529
 
@@ -1521,7 +1535,7 @@ var _a;
1521
1535
  * - reactive `items` (current list) and `options` (label/value),
1522
1536
  * - local cache filtering (`fixed: true`) or server-side search by name (`fixed: false`),
1523
1537
  * - preload and restore cache from `storage` (`localStorage/session/LRU`),
1524
- * - smooth integration with `PaginatedDataStore` for server fetching.
1538
+ * - smooth integration with `PagedQueryStore` for server fetching.
1525
1539
  *
1526
1540
  * Example:
1527
1541
  * ```ts
@@ -1561,7 +1575,7 @@ class DictStore {
1561
1575
  cachedItems = signal([], ...(ngDevMode ? [{ debugName: "cachedItems" }] : []));
1562
1576
  /**
1563
1577
  * Current list of dictionary items.
1564
- * Source local cache (fixed=true) or data from `PaginatedDataStore`.
1578
+ * Source — local cache (fixed=true) or data from `PagedQueryStore`.
1565
1579
  */
1566
1580
  items = computed(() => {
1567
1581
  const cached = this.cachedItems();
@@ -1591,7 +1605,7 @@ class DictStore {
1591
1605
  this.storageKey = storageKey;
1592
1606
  const searchDebounce = debounceTime ?? 300;
1593
1607
  this.debouncedSearchText = debounceSignal(this.searchText, searchDebounce);
1594
- this.#helper = new PaginatedDataStore(this.apiUrl, {
1608
+ this.#helper = new PagedQueryStore(this.apiUrl, {
1595
1609
  method: method,
1596
1610
  hasCache: false,
1597
1611
  presetFilters: { name: '', ...presetFilters },
@@ -1624,12 +1638,12 @@ class DictStore {
1624
1638
  if (!this.fixed) {
1625
1639
  const query = this.debouncedSearchText().trim();
1626
1640
  untracked(() => {
1627
- this._lastPromise = this.#helper.updateFilters({ name: query, ...rest });
1641
+ this._lastPromise = this.#helper.fetch({ filters: { name: query, ...rest } });
1628
1642
  });
1629
1643
  }
1630
1644
  else if (!this.cachedItems().length) {
1631
1645
  untracked(() => {
1632
- this._lastPromise = this.#helper.updateFilters({ name: '', ...rest });
1646
+ this._lastPromise = this.#helper.fetch({ filters: { name: '', ...rest } });
1633
1647
  });
1634
1648
  }
1635
1649
  }));
@@ -1640,7 +1654,7 @@ class DictStore {
1640
1654
  }
1641
1655
  /**
1642
1656
  * Set a search query and filters.
1643
- * With `fixed: false` initiates server search; with `fixed: true` local filtering.
1657
+ * With `fixed: false` initiates server search; with `fixed: true` — local filtering.
1644
1658
  */
1645
1659
  search = (name = '', filters = {}) => {
1646
1660
  this._armed.set(true);
@@ -2017,5 +2031,5 @@ class EntityStore {
2017
2031
  * Generated bundle index. Do not edit.
2018
2032
  */
2019
2033
 
2020
- export { AbortError, CacheMissError, DictLocalStore, DictStore, EntityStore, LruCache, PaginatedDataStore, ResourceStore, STATUM_CONFIG, Serializer, SerializerFieldError, createBodySerializer, createQuerySerializer, createStrictSerializer, isAbort, storageStrategy };
2034
+ export { AbortError, CacheMissError, DictLocalStore, DictStore, EntityStore, LruCache, PagedQueryStore, RESOURCE_PROFILES, ResourceStore, STATUM_CONFIG, Serializer, SerializerFieldError, createBodySerializer, createQuerySerializer, createResourceProfile, createStrictSerializer, isAbort, provideStatum, storageStrategy };
2021
2035
  //# sourceMappingURL=reforgium-statum.mjs.map
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.1.2",
2
+ "version": "3.0.0-rc.0",
3
3
  "name": "@reforgium/statum",
4
4
  "description": "reforgium State modules",
5
5
  "author": "rtommievich",
@@ -32,6 +32,7 @@
32
32
  "types/*.d.ts",
33
33
  "package.json",
34
34
  "README.md",
35
+ "MIGRATION.md",
35
36
  "CHANGELOG.md",
36
37
  "LICENSE*"
37
38
  ],
@@ -1,6 +1,6 @@
1
- import { AnyType, AnyDict, RestMethods, PageableRequest, Query, PageableResponse } from '@reforgium/internal';
1
+ import { AnyType, AnyDict, RestMethods, PageableRequest, PageableResponse } from '@reforgium/internal';
2
2
  import * as _angular_core from '@angular/core';
3
- import { Signal, WritableSignal, InjectionToken } from '@angular/core';
3
+ import { Signal, WritableSignal, EnvironmentProviders, InjectionToken } from '@angular/core';
4
4
  import * as _reforgium_statum from '@reforgium/statum';
5
5
 
6
6
  /**
@@ -534,6 +534,29 @@ declare class ResourceStore<Data> {
534
534
  private promoteCurrent;
535
535
  }
536
536
 
537
+ declare const RESOURCE_PROFILES: {
538
+ readonly table: {
539
+ readonly ttlMs: 15000;
540
+ readonly delay: 120;
541
+ readonly delayMode: "debounce";
542
+ readonly maxEntries: 500;
543
+ };
544
+ readonly lookup: {
545
+ readonly ttlMs: 60000;
546
+ readonly delay: 200;
547
+ readonly delayMode: "debounce";
548
+ readonly maxEntries: 300;
549
+ };
550
+ readonly mutation: {
551
+ readonly ttlMs: 0;
552
+ readonly delay: 0;
553
+ readonly delayMode: "throttle";
554
+ readonly maxEntries: 50;
555
+ };
556
+ };
557
+ type ResourceProfileName = keyof typeof RESOURCE_PROFILES;
558
+ declare const createResourceProfile: (profile: ResourceProfileName, overrides?: ResourceStoreOptions) => ResourceStoreOptions;
559
+
537
560
  /**
538
561
  * Error thrown when requested data is missing in the cache.
539
562
  *
@@ -576,7 +599,7 @@ declare function isAbort(e: unknown): e is AbortError;
576
599
  * Controls request method, page cache, debounce delay, and
577
600
  * request/response transformations.
578
601
  */
579
- type PaginatedDataStoreConfig<ItemsType extends object, FilterType = unknown> = {
602
+ type PagedQueryStoreConfig<ItemsType extends object, FilterType = unknown> = {
580
603
  /** Transport HTTP method: `GET`/`POST`/`PATCH`/`PUT`/`DELETE`. Defaults to global config or `POST`. */
581
604
  method?: RestMethods;
582
605
  /** Enable LRU cache for pages. */
@@ -585,19 +608,19 @@ type PaginatedDataStoreConfig<ItemsType extends object, FilterType = unknown> =
585
608
  cacheSize?: number;
586
609
  /** Debounce delay before request (ms). Useful for frequent filter changes. */
587
610
  debounceTime?: number;
588
- /** Initial pagination/sort params. `page` is required. */
611
+ /** Initial pagination params. `page` is required. */
589
612
  presetQuery?: {
590
613
  page: number;
591
614
  pageSize?: number;
592
- sort?: string;
593
615
  };
594
616
  /** Initial filters. Will be sent with the first request. */
595
617
  presetFilters?: Partial<FilterType>;
596
618
  /**
597
619
  * Custom transformation of request data into a query object.
598
- * Useful for mapping `page/pageSize/sort` API-specific keys.
620
+ * Useful for mapping `page/pageSize` and selected filter fields в†’ API-specific query keys.
621
+ * Returned object can contain nullable values; they are filtered before the transport call.
599
622
  */
600
- parseRequest?: (data: PageableRequest) => Query;
623
+ parseRequest?: (data: PageableRequest & Partial<FilterType> & AnyDict) => AnyDict;
601
624
  /**
602
625
  * Custom parser of API response into unified `PageableResponse<ItemsType>`.
603
626
  * Use if the server returns an array or a "non-standard" structure.
@@ -605,60 +628,77 @@ type PaginatedDataStoreConfig<ItemsType extends object, FilterType = unknown> =
605
628
  parseResponse?: (data: AnyType) => PageableResponse<ItemsType> | Promise<PageableResponse<ItemsType>>;
606
629
  };
607
630
  /**
608
- * Global defaults for `PaginatedDataStore`.
631
+ * Global defaults for `PagedQueryStore`.
609
632
  * Can be provided via DI to avoid duplicating config in each store.
610
633
  */
611
- type PaginatedDataStoreProviderConfig = {
634
+ type PagedQueryStoreProviderConfig = {
612
635
  /** Default method for all stores (if not overridden locally). */
613
636
  defaultMethod?: RestMethods;
614
- /** Default base `page/pageSize/sort`. */
615
- defaultQuery?: PaginatedDataStoreConfig<object>['presetQuery'];
616
- /** Global `parseRequest` maps pagination/sort into a query object. */
617
- defaultParseRequest?: PaginatedDataStoreConfig<object>['parseRequest'];
637
+ /** Default base `page/pageSize`. */
638
+ defaultQuery?: PagedQueryStoreConfig<object>['presetQuery'];
639
+ /** Global `parseRequest` — maps pagination into a query object. */
640
+ defaultParseRequest?: PagedQueryStoreConfig<object>['parseRequest'];
618
641
  /** Whether to enable page cache by default. */
619
642
  defaultHasCache?: boolean;
620
643
  /** Default LRU page cache size. */
621
644
  defaultCacheSize?: number;
622
645
  };
623
646
 
624
- type PaginationType = {
647
+ type OffsetPaginationType = {
625
648
  page?: number;
626
649
  first?: number | null;
627
650
  rows?: number | null;
628
- sortField?: string | string[] | null;
629
- sortOrder?: number | null;
651
+ };
652
+ type FetchInput<FilterType> = {
653
+ filters?: Partial<FilterType>;
654
+ query?: AnyDict;
655
+ routeParams?: AnyDict;
656
+ };
657
+ type RefreshInput<FilterType> = {
658
+ filters?: Partial<FilterType>;
659
+ query?: AnyDict;
660
+ };
661
+ type UpdatePageOptions = {
662
+ ignoreCache?: boolean;
663
+ };
664
+ type UpdateByOffsetOptions = {
665
+ query?: AnyDict;
666
+ };
667
+ type SetRouteParamsOptions = {
668
+ reset?: boolean;
669
+ abort?: boolean;
630
670
  };
631
671
  /**
632
672
  * Store for paginated data (tables/lists) with per-page cache and unified requests.
633
673
  *
634
674
  * Provides:
635
675
  * - reactive signals: `items`, `loading`, `cached`;
636
- * - methods to control page/size/filters/sorting;
676
+ * - methods to control page/size/query;
637
677
  * - optional LRU cache by pages;
638
- * - configurable transport (GET/POST/PATCH/…).
678
+ * - configurable transport (GET/POST/PATCH/…).
639
679
  *
640
680
  * Example:
641
681
  * ```ts
642
- * const ds = new PaginatedDataStore<User>('/users', { method: 'GET', hasCache: true });
682
+ * const ds = new PagedQueryStore<User>('/users', { method: 'GET', hasCache: true });
643
683
  * await ds.updatePage(0); // load the first page
644
684
  * effect(() => console.log(ds.items(), ds.loading()));
645
685
  * ```
646
686
  */
647
- declare class PaginatedDataStore<ItemsType extends object, FilterType = unknown> {
687
+ declare class PagedQueryStore<ItemsType extends object, FilterType = unknown> {
648
688
  #private;
649
689
  private route;
650
- config: PaginatedDataStoreConfig<ItemsType, FilterType>;
690
+ config: PagedQueryStoreConfig<ItemsType, FilterType>;
651
691
  private readonly defaultConfig;
652
692
  /** Current page data (reactive). */
653
693
  items: WritableSignal<ItemsType[]>;
654
- /** Merged cache of pages (flat list) handy for search/export. */
694
+ /** Merged cache of pages (flat list) — handy for search/export. */
655
695
  cached: WritableSignal<ItemsType[]>;
656
696
  /** Loading flag of the current operation. */
657
697
  loading: WritableSignal<boolean>;
658
698
  /** Current filters (applied to requests). */
659
699
  filters: Partial<FilterType>;
660
- /** Current sorting (`field,asc|desc`). */
661
- sort?: string | ReadonlyArray<string>;
700
+ /** Additional query params sent to transport requests. */
701
+ query: AnyDict;
662
702
  /** Current page index (0-based). */
663
703
  page: number;
664
704
  /** Default page size. */
@@ -670,47 +710,39 @@ declare class PaginatedDataStore<ItemsType extends object, FilterType = unknown>
670
710
  * @param route Resource URL pattern (e.g., `'/users'`)
671
711
  * @param config Store behavior: method, cache, request/response parsers, debounce, presets, etc.
672
712
  */
673
- constructor(route: string, config?: PaginatedDataStoreConfig<ItemsType, FilterType>);
674
- /** Force reload current data (with the same page/filters/sort). */
675
- refresh(): Promise<ItemsType[] | undefined>;
713
+ constructor(route: string, config?: PagedQueryStoreConfig<ItemsType, FilterType>);
714
+ /**
715
+ * Fetch data with explicit filters and query params from the first page.
716
+ * Replaces legacy `setFilters`.
717
+ */
718
+ fetch: ({ filters, query, routeParams }?: FetchInput<FilterType>) => Promise<ItemsType[] | undefined>;
719
+ /**
720
+ * Force reload current data.
721
+ * Optional args merge into active filters/query.
722
+ * Route params are intentionally not changed from `refresh`.
723
+ */
724
+ refresh({ filters, query }?: RefreshInput<FilterType>): Promise<ItemsType[] | undefined>;
676
725
  /**
677
726
  * Switch page with a request.
678
- * If cache is enabled and the page is present in LRU returns it from cache.
727
+ * If cache is enabled and the page is present in LRU — returns it from cache.
679
728
  * @param page page index (0-based)
680
729
  * @param ignoreCache ignore cache and fetch from network
681
730
  */
682
- updatePage: (page?: number, ignoreCache?: boolean) => Promise<ItemsType[] | undefined>;
731
+ updatePage: (page?: number, { ignoreCache }?: UpdatePageOptions) => Promise<ItemsType[] | undefined>;
683
732
  /**
684
733
  * Change page size (will go to the first page) and fetch.
685
734
  * @param size new size (rows per page)
686
735
  */
687
736
  updatePageSize: (size?: number) => Promise<ItemsType[] | undefined>;
688
737
  /**
689
- * Update filters (goes to the first page) and fetch.
690
- * The previous cache is cleared.
691
- */
692
- updateFilters: (filters: Partial<FilterType>) => Promise<ItemsType[] | undefined>;
693
- /**
694
- * Update the state from table events (PrimeNG, etc.) and fetch.
695
- * Supports `page/first/rows/sortField/sortOrder`.
738
+ * Helper for offset-based table events (PrimeNG, etc.) with `page/first/rows`.
696
739
  */
697
- updateQuery: ({ page: pageNum, first, rows, sortOrder, sortField }: PaginationType) => Promise<ItemsType[] | undefined>;
698
- /**
699
- * Set filters from scratch (goes to the first page and resets sorting) and fetch.
700
- * Useful for quick presets.
701
- */
702
- setFilters: (filters?: Partial<FilterType>) => Promise<ItemsType[] | undefined>;
703
- /**
704
- * Change the resource route (resets page, cache, and presets) without fetching.
705
- * Useful when one store should work with different endpoints.
706
- */
707
- updateRoute: (route: string) => void;
740
+ updateByOffset: ({ page: pageNum, first, rows }?: OffsetPaginationType, { query }?: UpdateByOffsetOptions) => Promise<ItemsType[] | undefined>;
708
741
  /**
709
742
  * Set route parameters (path variables) for the resource URL.
710
- * Does not trigger loading automatically call `refresh()` or `updatePage()` after.
743
+ * Does not trigger loading automatically - call `refresh()` or `updatePage()` after.
711
744
  *
712
745
  * @param params Dictionary of route parameters (e.g., `{ id: '123' }`)
713
- * @param opts Options object
714
746
  * @param opts.reset If `true`, resets page to 0, clears cache, total elements count, and items
715
747
  * @param opts.abort If `true`, aborts all active transport requests and sets loading to false
716
748
  *
@@ -723,19 +755,16 @@ declare class PaginatedDataStore<ItemsType extends object, FilterType = unknown>
723
755
  * store.setRouteParams({ userId: '42' }, { reset: false, abort: true });
724
756
  * ```
725
757
  */
726
- setRouteParams: (params?: AnyDict, opts?: {
727
- reset?: boolean;
728
- abort?: boolean;
729
- }) => void;
758
+ setRouteParams: (params?: AnyDict, { reset, abort }?: SetRouteParamsOptions) => void;
730
759
  /**
731
760
  * Update store config on the fly (without re-creation).
732
761
  * Does not trigger loading automatically.
733
762
  */
734
- updateConfig: (config: PaginatedDataStoreConfig<ItemsType>) => void;
763
+ updateConfig: (config: PagedQueryStoreConfig<ItemsType, FilterType>) => void;
735
764
  /**
736
765
  * Copy configuration and presets from another store of the same type.
737
766
  */
738
- copy(helper: PaginatedDataStore<ItemsType, FilterType>): void;
767
+ copy(store: PagedQueryStore<ItemsType, FilterType>): void;
739
768
  /** Clean up resources and cancel active requests. Automatically called onDestroy. */
740
769
  destroy(): void;
741
770
  private initTransport;
@@ -856,7 +885,7 @@ type ValueType = string[] | string | number | null | undefined;
856
885
  * - reactive `items` (current list) and `options` (label/value),
857
886
  * - local cache filtering (`fixed: true`) or server-side search by name (`fixed: false`),
858
887
  * - preload and restore cache from `storage` (`localStorage/session/LRU`),
859
- * - smooth integration with `PaginatedDataStore` for server fetching.
888
+ * - smooth integration with `PagedQueryStore` for server fetching.
860
889
  *
861
890
  * Example:
862
891
  * ```ts
@@ -894,7 +923,7 @@ declare class DictStore<Type extends AnyDict> {
894
923
  private cachedItems;
895
924
  /**
896
925
  * Current list of dictionary items.
897
- * Source local cache (fixed=true) or data from `PaginatedDataStore`.
926
+ * Source — local cache (fixed=true) or data from `PagedQueryStore`.
898
927
  */
899
928
  items: Signal<readonly Type[]>;
900
929
  /**
@@ -917,7 +946,7 @@ declare class DictStore<Type extends AnyDict> {
917
946
  restoreCache(): void;
918
947
  /**
919
948
  * Set a search query and filters.
920
- * With `fixed: false` initiates server search; with `fixed: true` local filtering.
949
+ * With `fixed: false` initiates server search; with `fixed: true` — local filtering.
921
950
  */
922
951
  search: (name?: string, filters?: AnyDict) => void;
923
952
  /**
@@ -1050,8 +1079,8 @@ declare class EntityStore<Entity extends AnyDict, IdKey extends keyof Entity = k
1050
1079
  private isValidId;
1051
1080
  }
1052
1081
 
1053
- type PaginatedDataProviderConfig = {
1054
- paginatedData?: PaginatedDataStoreProviderConfig;
1082
+ type PagedQueryProviderConfig = {
1083
+ pagedQuery?: PagedQueryStoreProviderConfig;
1055
1084
  };
1056
1085
  type SerializerProviderConfig = {
1057
1086
  serializer?: Partial<SerializerConfig>;
@@ -1059,9 +1088,10 @@ type SerializerProviderConfig = {
1059
1088
  type DictProviderConfig = {
1060
1089
  dict?: DictStoreProviderConfig;
1061
1090
  };
1062
- type StatumConfig = PaginatedDataProviderConfig & SerializerProviderConfig & DictProviderConfig;
1091
+ type StatumConfig = PagedQueryProviderConfig & SerializerProviderConfig & DictProviderConfig;
1063
1092
  declare const STATUM_CONFIG: InjectionToken<StatumConfig>;
1093
+ declare const provideStatum: (config: StatumConfig) => EnvironmentProviders;
1064
1094
 
1065
- export { AbortError, CacheMissError, DictLocalStore, DictStore, EntityStore, LruCache, PaginatedDataStore, ResourceStore, STATUM_CONFIG, Serializer, SerializerFieldError, createBodySerializer, createQuerySerializer, createStrictSerializer, isAbort, storageStrategy };
1066
- export type { DataType, DictLocalConfig, DictStoreConfig, DictStoreProviderConfig, EntityId, EntityStoreConfig, FieldConfig, PaginatedDataStoreConfig, PaginatedDataStoreProviderConfig, ResourceRoutesMap, ResourceStatus, ResourceStoreOptions, ResourceTraceEvent, RetryConfig, SerializedType, SerializerConfig, StorageInterface, StorageStrategy, StorageStrategyOptions, Types };
1095
+ export { AbortError, CacheMissError, DictLocalStore, DictStore, EntityStore, LruCache, PagedQueryStore, RESOURCE_PROFILES, ResourceStore, STATUM_CONFIG, Serializer, SerializerFieldError, createBodySerializer, createQuerySerializer, createResourceProfile, createStrictSerializer, isAbort, provideStatum, storageStrategy };
1096
+ export type { DataType, DictLocalConfig, DictStoreConfig, DictStoreProviderConfig, EntityId, EntityStoreConfig, FieldConfig, PagedQueryStoreConfig, PagedQueryStoreProviderConfig, ResourceProfileName, ResourceRoutesMap, ResourceStatus, ResourceStoreOptions, ResourceTraceEvent, RetryConfig, SerializedType, SerializerConfig, StatumConfig, StorageInterface, StorageStrategy, StorageStrategyOptions, Types };
1067
1097
  //# sourceMappingURL=reforgium-statum.d.ts.map