@reforgium/internal 1.3.0 → 2.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rtommievich
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -5,8 +5,28 @@
5
5
 
6
6
  Shared infrastructure package for Reforgium Angular libraries.
7
7
 
8
+ `internal` is being tightened into a foundation layer. The target boundary is documented in
9
+ [INTERNAL_V2.md](./INTERNAL_V2.md).
10
+
11
+ The first classification pass is documented in
12
+ [INTERNAL_V2_AUDIT.md](./INTERNAL_V2_AUDIT.md).
13
+
14
+ The file-by-file pass is documented in
15
+ [INTERNAL_V2_FILE_AUDIT.md](./INTERNAL_V2_FILE_AUDIT.md).
16
+
17
+ Short version:
18
+
19
+ - foundation primitives belong here
20
+ - feature semantics do not
21
+ - legacy compatibility for `angular-common-kit` does not
22
+
23
+ `@reforgium/internal` is infrastructure for Reforgium packages and is not intended as a user-facing product package.
24
+
8
25
  ## Install
9
26
 
27
+ `internal` is primarily consumed transitively by other `@reforgium/*` packages.
28
+ Direct installation is possible for workspace and low-level internal use, but it is not the recommended first entrypoint for app code.
29
+
10
30
  ```bash
11
31
  npm i @reforgium/internal
12
32
  ```
@@ -16,6 +36,8 @@ npm i @reforgium/internal
16
36
  `@reforgium/internal` exports:
17
37
 
18
38
  - `models`
39
+ - `codecs`
40
+ - `storage`
19
41
  - `tokens`
20
42
  - `utils`
21
43
 
@@ -23,11 +45,40 @@ npm i @reforgium/internal
23
45
 
24
46
  Main model groups:
25
47
 
26
- - `api.ts`: `RestMethods`, `SortToken`, `PageableRequest`, `PageableResponse<T>`, `ErrorResponse`, `QueryParams`, `Query`
27
- - `components.ts`: `Appearance`, `SelectOption`, `SelectIconOption`
48
+ - `query.models.ts`: `SortToken`, `SortDirection`, `SortRule`, `SortInput`, `QueryArrayMode`, `QueryFieldModes`, `QueryParams`, `Query`
49
+ - `transport.models.ts`: `RestMethods`, `PageableRequest`, `PageableResponse<T>`, `ErrorResponse`
28
50
  - `elements.ts`: `Direction`, `ElementRect`, `ElementSize`, `ElementPosition`, `ElementEdges`
29
51
  - `util.ts`: `AnyType`, `AnyDict`, `LiteralOf`, `ValueOf`, `Nullable`, `Nullish`, `NullableProps`, `OptionalExcept`, `RequiredExcept`, `Mutable`, JSON helper types
30
52
 
53
+ Compatibility-only model surface:
54
+
55
+ - `components.ts`: `Appearance`, `SelectOption`, `SelectIconOption`
56
+ - `api.ts`: deprecated mixed facade kept for compatibility; prefer explicit query and transport types
57
+
58
+ ### Codecs
59
+
60
+ Main codec exports:
61
+
62
+ - `Serializer`
63
+ - `SerializerFieldError`
64
+ - `SerializerConfig`
65
+ - `FieldConfig`
66
+ - `SerializedType`
67
+
68
+ These are shared bidirectional transform primitives used by `statum` and `regula`.
69
+
70
+ ### Storage
71
+
72
+ Main storage exports:
73
+
74
+ - `StorageInterface`
75
+ - `StorageStrategy`, `StorageStrategyOptions`
76
+ - `MemoryStorage`
77
+ - `LocalStorage`
78
+ - `SessionStorage`
79
+ - `LruCache`
80
+ - `storageStrategy(...)`
81
+
31
82
  ### Tokens
32
83
 
33
84
  Language tokens:
@@ -44,7 +95,8 @@ Theme/device/validation tokens:
44
95
 
45
96
  - `SELECTED_THEME`, `CHANGE_THEME`, `Themes`
46
97
  - `CURRENT_DEVICE`, `Devices`
47
- - `VALIDATION_MESSAGES`, `ValidationMessages`, `ValidationErrorData`
98
+ - `VALIDATION_MESSAGES`, `ValidationMessages`, `ValidationErrorData`:
99
+ compatibility-only legacy validation token surface
48
100
 
49
101
  Example:
50
102
 
@@ -134,6 +186,39 @@ makeQuery({ ids: [1, 2, 3], q: 'ok' }, 'multi');
134
186
  // ids=1&ids=2&ids=3&q=ok
135
187
  ```
136
188
 
189
+ `storage` example:
190
+
191
+ ```ts
192
+ import { LruCache, storageStrategy } from '@reforgium/internal';
193
+
194
+ const pages = new LruCache<number, string[]>(5);
195
+ pages.set(1, ['a', 'b']);
196
+
197
+ const persisted = storageStrategy<string, { updatedAt: number }>('persist');
198
+ persisted.set('users', { updatedAt: Date.now() });
199
+ ```
200
+
201
+ `codecs` example:
202
+
203
+ ```ts
204
+ import { Serializer } from '@reforgium/internal';
205
+
206
+ const serializer = new Serializer<{
207
+ search: string;
208
+ createdAt: Date;
209
+ }>({
210
+ mapFields: {
211
+ createdAt: { type: 'date' },
212
+ },
213
+ });
214
+
215
+ serializer.serialize({
216
+ search: ' regula ',
217
+ createdAt: new Date('2026-04-04'),
218
+ });
219
+ // { search: 'regula', createdAt: '2026-04-04' }
220
+ ```
221
+
137
222
  ## License
138
223
 
139
224
  MIT
@@ -1,5 +1,179 @@
1
1
  import { InjectionToken, makeEnvironmentProviders, signal, effect, untracked } from '@angular/core';
2
2
 
3
+ class LocalStorage {
4
+ prefix;
5
+ constructor(prefix = 're') {
6
+ this.prefix = prefix;
7
+ }
8
+ get length() {
9
+ return Object.keys(localStorage).filter((key) => key.startsWith(this.safePrefix())).length;
10
+ }
11
+ get(key) {
12
+ const storageKey = this.getSafePrefix(key);
13
+ const raw = localStorage.getItem(storageKey);
14
+ if (raw == null) {
15
+ return null;
16
+ }
17
+ try {
18
+ const parsed = JSON.parse(raw);
19
+ return parsed ?? null;
20
+ }
21
+ catch {
22
+ localStorage.removeItem(storageKey);
23
+ return null;
24
+ }
25
+ }
26
+ set(key, value) {
27
+ localStorage.setItem(this.getSafePrefix(key), JSON.stringify(value));
28
+ }
29
+ remove(key) {
30
+ localStorage.removeItem(this.getSafePrefix(key));
31
+ }
32
+ clear() {
33
+ const keys = Object.keys(localStorage).filter((key) => key.startsWith(this.safePrefix()));
34
+ keys.forEach((key) => localStorage.removeItem(key));
35
+ }
36
+ getSafePrefix(key) {
37
+ return this.prefix ? `${this.prefix}:${key}` : String(key);
38
+ }
39
+ safePrefix() {
40
+ return this.prefix ? `${this.prefix}:` : '';
41
+ }
42
+ }
43
+
44
+ class LruCache {
45
+ map = new Map();
46
+ _limit = 100;
47
+ constructor(limit = 100) {
48
+ this.limit = limit;
49
+ }
50
+ get limit() {
51
+ return this._limit;
52
+ }
53
+ get length() {
54
+ return this.map.size;
55
+ }
56
+ set limit(value) {
57
+ this._limit = Math.max(1, Math.floor(value || 0));
58
+ }
59
+ get(key) {
60
+ if (!this.map.has(key)) {
61
+ return null;
62
+ }
63
+ const val = this.map.get(key);
64
+ this.map.delete(key);
65
+ this.map.set(key, val);
66
+ return val;
67
+ }
68
+ set(key, value) {
69
+ if (this.map.has(key)) {
70
+ this.map.delete(key);
71
+ }
72
+ else if (this.map.size >= this.limit) {
73
+ const oldest = this.map.keys().next().value;
74
+ oldest !== undefined && this.map.delete(oldest);
75
+ }
76
+ this.map.set(key, value);
77
+ }
78
+ remove(key) {
79
+ return this.map.delete(key);
80
+ }
81
+ clear() {
82
+ this.map.clear();
83
+ }
84
+ has(key) {
85
+ return this.map.has(key);
86
+ }
87
+ keys() {
88
+ return Array.from(this.map.keys());
89
+ }
90
+ values() {
91
+ return Array.from(this.map.values());
92
+ }
93
+ entries() {
94
+ return Array.from(this.map.entries());
95
+ }
96
+ toArray() {
97
+ return Array.from(this.map.values());
98
+ }
99
+ fromArray(entries) {
100
+ this.map.clear();
101
+ for (const [k, v] of entries) {
102
+ this.set(k, v);
103
+ }
104
+ }
105
+ }
106
+
107
+ class MemoryStorage {
108
+ cache = new Map();
109
+ get length() {
110
+ return this.cache.size;
111
+ }
112
+ get(key) {
113
+ return this.cache.get(key) ?? null;
114
+ }
115
+ set(key, value) {
116
+ this.cache.set(key, value);
117
+ }
118
+ remove(key) {
119
+ this.cache.delete(key);
120
+ }
121
+ clear() {
122
+ this.cache.clear();
123
+ }
124
+ }
125
+
126
+ class SessionStorage {
127
+ prefix;
128
+ constructor(prefix = 're') {
129
+ this.prefix = prefix;
130
+ }
131
+ get length() {
132
+ return Object.keys(sessionStorage).filter((key) => key.startsWith(this.safePrefix())).length;
133
+ }
134
+ get(key) {
135
+ const storageKey = this.getSafePrefix(key);
136
+ const raw = sessionStorage.getItem(storageKey);
137
+ if (raw == null) {
138
+ return null;
139
+ }
140
+ try {
141
+ const parsed = JSON.parse(raw);
142
+ return parsed ?? null;
143
+ }
144
+ catch {
145
+ sessionStorage.removeItem(storageKey);
146
+ return null;
147
+ }
148
+ }
149
+ set(key, value) {
150
+ sessionStorage.setItem(this.getSafePrefix(key), JSON.stringify(value));
151
+ }
152
+ remove(key) {
153
+ sessionStorage.removeItem(this.getSafePrefix(key));
154
+ }
155
+ clear() {
156
+ const keys = Object.keys(sessionStorage).filter((key) => key.startsWith(this.safePrefix()));
157
+ keys.forEach((key) => sessionStorage.removeItem(key));
158
+ }
159
+ getSafePrefix(key) {
160
+ return this.prefix ? `${this.prefix}:${key}` : String(key);
161
+ }
162
+ safePrefix() {
163
+ return this.prefix ? `${this.prefix}:` : '';
164
+ }
165
+ }
166
+
167
+ const storageStrategy = (strategy, options = {}) => {
168
+ const fabrics = {
169
+ memory: () => new MemoryStorage(),
170
+ session: () => new SessionStorage(),
171
+ persist: () => new LocalStorage(),
172
+ lru: () => new LruCache(options.lruLimit ?? 100),
173
+ };
174
+ return fabrics[strategy]();
175
+ };
176
+
3
177
  /**
4
178
  * Built-in languages always available in the registry.
5
179
  */
@@ -73,27 +247,18 @@ const CHANGE_THEME = new InjectionToken('RE_CHANGE_THEME');
73
247
  const CURRENT_DEVICE = new InjectionToken('RE_CURRENT_DEVICE');
74
248
 
75
249
  /**
76
- * Injection token for a validation messages map.
77
- * Can be overridden in feature modules to provide custom validation messages.
78
- * Defaults to an empty object when not provided.
79
- *
80
- * @example
81
- * // In a feature module:
82
- * providers: [
83
- * {
84
- * provide: VALIDATION_MESSAGES,
85
- * useValue: {
86
- * required: () => 'This field is required',
87
- * minlength: (error) => `Min length: ${error.requiredLength}`
88
- * }
89
- * }
90
- * ]
250
+ * Legacy validation messages token.
91
251
  */
92
252
  const VALIDATION_MESSAGES = new InjectionToken('RE_VALIDATION_MESSAGES', {
93
253
  providedIn: 'root',
94
254
  factory: () => ({}),
95
255
  });
96
256
 
257
+ /**
258
+ * @deprecated Legacy validation token surface is being isolated from the `internal v2`
259
+ * foundation boundary. Do not add new consumers here.
260
+ */
261
+
97
262
  /**
98
263
  * Browser-only helper that downloads a file from a Blob object and prompts the user to save it.
99
264
  *
@@ -207,7 +372,7 @@ function base64ToBlob(base64, mimeType = '') {
207
372
  * ```
208
373
  */
209
374
  const debounceSignal = (src, ms = 150, opts) => {
210
- const out = signal(src(), ...(ngDevMode ? [{ debugName: "out" }] : []));
375
+ const out = signal(src(), ...(ngDevMode ? [{ debugName: "out" }] : /* istanbul ignore next */ []));
211
376
  let timeoutRef;
212
377
  effect((onCleanup) => {
213
378
  const v = src();
@@ -246,7 +411,7 @@ const debounceSignal = (src, ms = 150, opts) => {
246
411
  * ```
247
412
  */
248
413
  const throttleSignal = (src, ms = 100, opts) => {
249
- const out = signal(src(), ...(ngDevMode ? [{ debugName: "out" }] : []));
414
+ const out = signal(src(), ...(ngDevMode ? [{ debugName: "out" }] : /* istanbul ignore next */ []));
250
415
  let last = 0;
251
416
  let queued;
252
417
  let timeoutRef;
@@ -305,6 +470,9 @@ const pad = (n) => n.toString().padStart(2, '0');
305
470
  * @returns {string} Formatted date string. Returns an empty string if the input date is invalid.
306
471
  */
307
472
  const formatDate = (date, format = 'yyyy-MM-dd') => {
473
+ if (!date) {
474
+ return '';
475
+ }
308
476
  if (isNaN(date['getTime']?.())) {
309
477
  return '';
310
478
  }
@@ -565,7 +733,7 @@ const concatArray = (value, mode, key) => {
565
733
  * if the input is invalid.
566
734
  */
567
735
  function formatToSpacedNumber(num) {
568
- const stringed = String(num).replaceAll(' ', '');
736
+ const stringed = String(num).replace(/ /g, '');
569
737
  if (num === null || num === undefined || Number.isNaN(+stringed)) {
570
738
  return '';
571
739
  }
@@ -1111,9 +1279,308 @@ const generateId = (limit = 15, radix = 36) => Math.random()
1111
1279
  .toString(radix)
1112
1280
  .substring(2, limit + 2);
1113
1281
 
1282
+ const serializeString = (config, value) => config.mapString.format?.(value) ?? (config.mapString.trim ? value.trim() : value);
1283
+ const serializeNumber = (config, value) => config.mapNumber.format?.(value) ?? Number(value);
1284
+ const serializeBoolean = (config, value) => config.mapBoolean.format?.(value) ?? (value ? (config.mapBoolean.true ?? true) : (config.mapBoolean.false ?? false));
1285
+ const serializeDate = (config, value) => config.mapDate.format?.(value) ?? formatDate(value, config.mapDate.dateFormat);
1286
+
1287
+ const getTypedField = (field) => {
1288
+ return field && 'type' in field ? field : undefined;
1289
+ };
1290
+ class SerializerFieldError extends Error {
1291
+ field;
1292
+ stage;
1293
+ originalError;
1294
+ constructor(field, stage, originalError) {
1295
+ const originalMessage = originalError instanceof Error ? originalError.message : String(originalError);
1296
+ super(`Serializer ${stage} error for field "${field}": ${originalMessage}`);
1297
+ this.field = field;
1298
+ this.stage = stage;
1299
+ this.originalError = originalError;
1300
+ this.name = 'SerializerFieldError';
1301
+ }
1302
+ }
1303
+ class Serializer {
1304
+ config;
1305
+ constructor(config = {}) {
1306
+ this.config = this.mergeConfig({
1307
+ mapString: { trim: true },
1308
+ mapNumber: { fromString: false },
1309
+ mapBoolean: {},
1310
+ mapArray: { concatType: 'comma' },
1311
+ mapObject: { deep: true },
1312
+ mapDate: { dateFormat: 'yyyy-MM-dd' },
1313
+ mapPeriod: {
1314
+ dateFormat: 'yyyy-MM-dd',
1315
+ transformMode: { mode: 'split', dateFromKeyPostfix: 'From', dateToKeyPostfix: 'To' },
1316
+ },
1317
+ mapNullable: { remove: true, includeEmptyString: false },
1318
+ }, config);
1319
+ }
1320
+ serialize(obj, _seen = new WeakSet()) {
1321
+ const result = {};
1322
+ if (obj != null && typeof obj === 'object') {
1323
+ _seen.add(obj);
1324
+ }
1325
+ for (const [key, value] of Object.entries(obj ?? {})) {
1326
+ const fields = this.config.mapFields?.[key];
1327
+ const typedField = getTypedField(fields);
1328
+ if (fields && 'format' in fields) {
1329
+ try {
1330
+ result[key] = fields.format(value, obj);
1331
+ }
1332
+ catch (error) {
1333
+ throw new SerializerFieldError(key, 'format', error);
1334
+ }
1335
+ continue;
1336
+ }
1337
+ if (typedField?.type === 'nullable' || isNullable(value, this.config.mapNullable?.includeEmptyString)) {
1338
+ if (this.config.mapNullable.remove) {
1339
+ continue;
1340
+ }
1341
+ }
1342
+ if (typedField?.type === 'period' || isDatePeriod(value)) {
1343
+ const transform = this.config.mapPeriod.transformMode;
1344
+ const [from, to] = value;
1345
+ if (transform?.mode === 'split') {
1346
+ result[`${key}${transform.dateFromKeyPostfix}`] = formatDate(from, this.config.mapPeriod.dateFormat);
1347
+ result[`${key}${transform.dateToKeyPostfix}`] = formatDate(to, this.config.mapPeriod.dateFormat);
1348
+ continue;
1349
+ }
1350
+ }
1351
+ result[key] = this.serializeElement(value, key, _seen);
1352
+ }
1353
+ if (obj != null && typeof obj === 'object') {
1354
+ _seen.delete(obj);
1355
+ }
1356
+ return result;
1357
+ }
1358
+ deserialize = (val) => {
1359
+ const data = typeof val === 'string' ? this.parseInputString(val) : val;
1360
+ const result = {};
1361
+ for (const [key, value] of Object.entries(data ?? {})) {
1362
+ const field = this.config.mapFields?.[key];
1363
+ const typedField = getTypedField(field);
1364
+ if (field && 'parse' in field) {
1365
+ try {
1366
+ result[key] = field.parse(value, data);
1367
+ }
1368
+ catch (error) {
1369
+ throw new SerializerFieldError(key, 'parse', error);
1370
+ }
1371
+ continue;
1372
+ }
1373
+ if (typedField?.type === 'nullable' ||
1374
+ (typedField?.type !== 'array' && isNullable(value, this.config.mapNullable?.includeEmptyString))) {
1375
+ if (this.config.mapNullable.remove) {
1376
+ continue;
1377
+ }
1378
+ }
1379
+ const periodTransform = this.config.mapPeriod.transformMode;
1380
+ if (periodTransform.mode === 'split') {
1381
+ const isFrom = (key || '').endsWith(periodTransform.dateFromKeyPostfix);
1382
+ const isTo = (key || '').endsWith(periodTransform.dateToKeyPostfix);
1383
+ const keyJoint = (key || '')
1384
+ .replace(periodTransform.dateFromKeyPostfix, '')
1385
+ .replace(periodTransform.dateToKeyPostfix, '');
1386
+ const jointField = this.config.mapFields?.[keyJoint];
1387
+ const fieldType = jointField && 'type' in jointField ? jointField.type : undefined;
1388
+ if (fieldType === 'period' && (isFrom || isTo)) {
1389
+ result[keyJoint] ??= [null, null];
1390
+ if (isFrom) {
1391
+ result[keyJoint][0] = parseToDate(value, this.config.mapPeriod.dateFormat);
1392
+ }
1393
+ else if (isTo) {
1394
+ result[keyJoint][1] = parseToDate(value, this.config.mapPeriod.dateFormat);
1395
+ }
1396
+ continue;
1397
+ }
1398
+ }
1399
+ result[key] = this.deserializeElement(value, key);
1400
+ }
1401
+ return result;
1402
+ };
1403
+ deserializeQuery = (query) => {
1404
+ return this.deserialize(this.parseQuery(query));
1405
+ };
1406
+ deserializeJson = (json) => {
1407
+ return this.deserialize(this.parseJsonObject(json));
1408
+ };
1409
+ toQuery = (val) => {
1410
+ return buildQueryParams(this.serialize(val), this.config.mapArray.concatType, this.resolveArrayFieldModes());
1411
+ };
1412
+ withConfig(config) {
1413
+ return new Serializer(this.mergeConfig(this.config, config));
1414
+ }
1415
+ serializeElement(value, key, _seen = new WeakSet()) {
1416
+ const fields = this.config.mapFields?.[key || ''];
1417
+ const typedField = getTypedField(fields);
1418
+ if (fields && 'format' in fields) {
1419
+ return;
1420
+ }
1421
+ if (typedField?.type === 'nullable' || isNullable(value, this.config.mapNullable?.includeEmptyString)) {
1422
+ const nullableVal = value ?? null;
1423
+ return this.config.mapNullable.format?.(nullableVal) || this.config.mapNullable.replaceWith || nullableVal;
1424
+ }
1425
+ if (typedField?.type === 'string') {
1426
+ return serializeString(this.config, value);
1427
+ }
1428
+ if (typedField?.type === 'number' || isNumber(value, this.config.mapNumber.fromString)) {
1429
+ return serializeNumber(this.config, value);
1430
+ }
1431
+ if (typeof value === 'string') {
1432
+ return serializeString(this.config, value);
1433
+ }
1434
+ if (typedField?.type === 'boolean' || typeof value === 'boolean') {
1435
+ return serializeBoolean(this.config, value);
1436
+ }
1437
+ if (typedField?.type === 'date' || value instanceof Date) {
1438
+ return serializeDate(this.config, value);
1439
+ }
1440
+ if (typedField?.type === 'period' || isDatePeriod(value)) {
1441
+ const mapPeriod = this.config.mapPeriod;
1442
+ if (mapPeriod.format) {
1443
+ return mapPeriod.format(value);
1444
+ }
1445
+ const [from, to] = value;
1446
+ const transform = mapPeriod.transformMode;
1447
+ if (transform.mode === 'join') {
1448
+ const period = [
1449
+ formatDate(from, this.config.mapPeriod.dateFormat),
1450
+ formatDate(to, this.config.mapPeriod.dateFormat),
1451
+ ];
1452
+ return period.join(transform.concat);
1453
+ }
1454
+ }
1455
+ if (typedField?.type === 'array' || Array.isArray(value)) {
1456
+ return value.map((it) => this.serializeElement(it, undefined, _seen));
1457
+ }
1458
+ if (typedField?.type === 'object' || isObject(value)) {
1459
+ if (this.config.mapObject.deep && !this.config.mapObject.format && value != null && _seen.has(value)) {
1460
+ return undefined;
1461
+ }
1462
+ return (this.config.mapObject.format?.(value) ??
1463
+ (this.config.mapObject.deep ? this.serialize(value, _seen) : JSON.stringify(value)));
1464
+ }
1465
+ }
1466
+ deserializeElement(value, key) {
1467
+ const field = this.config.mapFields?.[key || ''];
1468
+ const typedField = getTypedField(field);
1469
+ if (field && 'format' in field) {
1470
+ return;
1471
+ }
1472
+ if (typedField?.type === 'array' || Array.isArray(value)) {
1473
+ const array = Array.isArray(value) ? value : [value];
1474
+ if (this.config.mapArray.removeNullable) {
1475
+ if (!isNullable(value, this.config.mapNullable?.includeEmptyString)) {
1476
+ return array.map((it) => this.deserializeElement(it));
1477
+ }
1478
+ return;
1479
+ }
1480
+ return !value ? [] : array.map((it) => this.deserializeElement(it));
1481
+ }
1482
+ if (typedField?.type === 'object') {
1483
+ try {
1484
+ if (this.config.mapObject.deep) {
1485
+ return isObject(value) ? this.deserialize(value) : value;
1486
+ }
1487
+ return typeof value === 'string' ? JSON.parse(value) : value;
1488
+ }
1489
+ catch {
1490
+ return value;
1491
+ }
1492
+ }
1493
+ if (typedField?.type === 'nullable' || isNullable(value, this.config.mapNullable?.includeEmptyString)) {
1494
+ return this.config.mapNullable.parse?.(value) || value;
1495
+ }
1496
+ if (typedField?.type === 'boolean' ||
1497
+ typeof value === 'boolean' ||
1498
+ value === this.config.mapBoolean.true ||
1499
+ value === this.config.mapBoolean.false) {
1500
+ return this.config.mapBoolean.parse?.(value) ?? (value === this.config.mapBoolean.true || value === true);
1501
+ }
1502
+ const maybeDate = parseToDate(value, this.config.mapDate.dateFormat);
1503
+ if (typedField?.type === 'date' || maybeDate) {
1504
+ return this.config.mapDate.parse?.(value) || maybeDate;
1505
+ }
1506
+ const periodTransform = this.config.mapPeriod.transformMode;
1507
+ if (periodTransform.mode === 'join') {
1508
+ const maybePeriod = parseToDatePeriod(value, this.config.mapPeriod.dateFormat);
1509
+ if (typedField?.type === 'period' || (maybePeriod || []).some(Boolean)) {
1510
+ return this.config.mapPeriod.parse?.(value) || maybePeriod;
1511
+ }
1512
+ }
1513
+ if (typedField?.type === 'number' || isNumber(value, this.config.mapNumber.fromString)) {
1514
+ return this.config.mapNumber.parse?.(value) || Number(String(value).trim());
1515
+ }
1516
+ if (typedField?.type === 'string' || typeof value === 'string') {
1517
+ const parsed = this.config.mapString.parse?.(value);
1518
+ if (parsed !== undefined) {
1519
+ return parsed;
1520
+ }
1521
+ return this.config.mapString.trim ? String(value).trim() : value;
1522
+ }
1523
+ return value;
1524
+ }
1525
+ parseQuery(val) {
1526
+ return parseQueryParamsByMode(val, undefined, this.resolveArrayFieldModes());
1527
+ }
1528
+ parseJsonObject(val) {
1529
+ try {
1530
+ const parsed = JSON.parse(val);
1531
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1532
+ return parsed;
1533
+ }
1534
+ }
1535
+ catch (error) {
1536
+ throw new Error(`Invalid JSON input: ${error instanceof Error ? error.message : String(error)}`);
1537
+ }
1538
+ throw new Error('Invalid JSON input: expected a JSON object');
1539
+ }
1540
+ parseInputString(val) {
1541
+ try {
1542
+ return this.parseJsonObject(val);
1543
+ }
1544
+ catch {
1545
+ return this.parseQuery(val);
1546
+ }
1547
+ }
1548
+ mergeConfig(base, override) {
1549
+ return {
1550
+ ...base,
1551
+ ...override,
1552
+ mapString: { ...base.mapString, ...override.mapString },
1553
+ mapNumber: { ...base.mapNumber, ...override.mapNumber },
1554
+ mapBoolean: { ...base.mapBoolean, ...override.mapBoolean },
1555
+ mapDate: { ...base.mapDate, ...override.mapDate },
1556
+ mapPeriod: {
1557
+ ...base.mapPeriod,
1558
+ ...override.mapPeriod,
1559
+ transformMode: {
1560
+ ...base.mapPeriod.transformMode,
1561
+ ...override.mapPeriod?.transformMode,
1562
+ },
1563
+ },
1564
+ mapNullable: { ...base.mapNullable, ...override.mapNullable },
1565
+ mapArray: { ...base.mapArray, ...override.mapArray },
1566
+ mapObject: { ...base.mapObject, ...override.mapObject },
1567
+ mapFields: { ...(base.mapFields || {}), ...(override.mapFields || {}) },
1568
+ };
1569
+ }
1570
+ resolveArrayFieldModes() {
1571
+ const result = {};
1572
+ Object.entries(this.config.mapFields || {}).forEach(([key, field]) => {
1573
+ if ('type' in field && field.type === 'array') {
1574
+ result[key] = field.concatType ?? this.config.mapArray.concatType;
1575
+ }
1576
+ });
1577
+ return result;
1578
+ }
1579
+ }
1580
+
1114
1581
  /**
1115
1582
  * Generated bundle index. Do not edit.
1116
1583
  */
1117
1584
 
1118
- export { BUILTIN_LANGS, CHANGE_LANG, CHANGE_THEME, CURRENT_DEVICE, REGISTER_LANG, SELECTED_LANG, SELECTED_THEME, TRANSLATION, VALIDATION_MESSAGES, appendQueryParams, appendQueryParamsByMode, base64ToBlob, buildQueryParams, compareRoutes, concatArray, copyText, debounceSignal, deepEqual, downloadByBlob, downloadByUrl, fillUrlWithParams, formatDate, formatToLocaledDate, formatToSpacedNumber, generateId, getAvailableHeight, getChainedValue, getCorrectedPosition, isDatePeriod, isNullable, isNumber, isObject, makeQuery, materializeRoutePath, mergeQueryParams, normalizeSortInput, normalizeUrl, parseQueryArray, parseQueryParams, parseQueryParamsByMode, parseSortToken, parseToDate, parseToDatePeriod, provideLangs, reformatDateToISO, sortInputToTokens, sortRuleToToken, throttleSignal, toDate, toSortToken, truncate };
1585
+ export { BUILTIN_LANGS, CHANGE_LANG, CHANGE_THEME, CURRENT_DEVICE, LocalStorage, LruCache, MemoryStorage, REGISTER_LANG, SELECTED_LANG, SELECTED_THEME, Serializer, SerializerFieldError, SessionStorage, TRANSLATION, VALIDATION_MESSAGES, appendQueryParams, appendQueryParamsByMode, base64ToBlob, buildQueryParams, compareRoutes, concatArray, copyText, debounceSignal, deepEqual, downloadByBlob, downloadByUrl, fillUrlWithParams, formatDate, formatToLocaledDate, formatToSpacedNumber, generateId, getAvailableHeight, getChainedValue, getCorrectedPosition, isDatePeriod, isNullable, isNumber, isObject, makeQuery, materializeRoutePath, mergeQueryParams, normalizeSortInput, normalizeUrl, parseQueryArray, parseQueryParams, parseQueryParamsByMode, parseSortToken, parseToDate, parseToDatePeriod, provideLangs, reformatDateToISO, sortInputToTokens, sortRuleToToken, storageStrategy, throttleSignal, toDate, toSortToken, truncate };
1119
1586
  //# sourceMappingURL=reforgium-internal.mjs.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "1.3.0",
2
+ "version": "2.0.2",
3
3
  "name": "@reforgium/internal",
4
- "description": "reforgium Libs Internal package. Shared libs models and tokens.",
4
+ "description": "Hidden Reforgium foundation package for shared primitives, tokens, and infrastructure.",
5
5
  "author": "rtommievich",
6
6
  "license": "MIT",
7
7
  "type": "module",
@@ -21,12 +21,11 @@
21
21
  },
22
22
  "keywords": [
23
23
  "reforgium",
24
- "common",
25
24
  "internal",
26
- "angular",
27
- "signal"
25
+ "foundation",
26
+ "infrastructure"
28
27
  ],
29
- "types": "../../dist/@reforgium/internal/types/reforgium-internal.d.ts",
28
+ "types": "./src/public-api.ts",
30
29
  "typings": "types/reforgium-internal.d.ts",
31
30
  "module": "fesm2022/reforgium-internal.mjs",
32
31
  "files": [
@@ -37,12 +36,6 @@
37
36
  "CHANGELOG.md",
38
37
  "LICENSE*"
39
38
  ],
40
- "dependencies": {
41
- "tslib": "^2.8.1"
42
- },
43
- "peerDependencies": {
44
- "@angular/core": ">=19.0.0"
45
- },
46
39
  "exports": {
47
40
  "./package.json": {
48
41
  "default": "./package.json"
@@ -51,5 +44,11 @@
51
44
  "types": "./types/reforgium-internal.d.ts",
52
45
  "default": "./fesm2022/reforgium-internal.mjs"
53
46
  }
47
+ },
48
+ "dependencies": {
49
+ "tslib": "^2.8.1"
50
+ },
51
+ "peerDependencies": {
52
+ "@angular/core": ">=19.0.0"
54
53
  }
55
54
  }
@@ -1,64 +1,29 @@
1
1
  import { EnvironmentProviders, InjectionToken, Signal, Injector } from '@angular/core';
2
2
 
3
3
  /**
4
- * Color semantics (appearance)
5
- * Used for buttons, icons, notifications, etc.
4
+ * Legacy UI-facing appearance semantics.
6
5
  *
7
- * @typedef {('primary' | 'success' | 'warning' | 'error' | 'info' | 'light' | 'gray' | 'inherit')} Appearance
8
- *
9
- * Possible values:
10
- * - `primary` - main accent color
11
- * - `success` - successful action completion
12
- * - `warning` - warning
13
- * - `error` - error
14
- * - `info` - informational message
15
- * - `light` - light theme
16
- * - `gray` - gray/neutral color
17
- * - `inherit` - inherit color from parent
6
+ * This model remains only for compatibility with legacy consumers.
7
+ * It is not part of the desired `internal v2` foundation boundary.
18
8
  */
19
9
  type Appearance = 'primary' | 'success' | 'warning' | 'error' | 'info' | 'light' | 'gray' | 'inherit';
20
10
  /**
21
- * Base model for a selection option.
22
- * Universal for selecting, lists, autocompletes, and radio groups.
23
- *
24
- * @template ValueType - Type of the option value (string or number). Default is `string | number`.
25
- *
26
- * @property {string} label - Displayed label of the option for the user
27
- * @property {ValueType} value - Value of the option used programmatically
11
+ * Legacy option model for UI selects.
28
12
  *
29
- * @example
30
- * ```typescript
31
- * const option: SelectOption<string> = { label: 'Option 1', value: 'option1' };
32
- * const numericOption: SelectOption<number> = { label: 'First', value: 1 };
33
- * ```
13
+ * This model remains only for compatibility with legacy consumers.
14
+ * It is not part of the desired `internal v2` foundation boundary.
34
15
  */
35
16
  type SelectOption<ValueType extends string | number = string | number> = {
36
- /** Displayed label */
37
17
  label: string;
38
- /** Value (string or number) */
39
18
  value: ValueType;
40
19
  };
41
20
  /**
42
- * Option with icon (for icon-select or dropdown with graphics)
43
- * Extends the base model {@link SelectOption} by adding an icon field.
21
+ * Legacy icon option model for UI selects.
44
22
  *
45
- * @template ValueType - Type of the option value (string or number). Default is `string | number`.
46
- *
47
- * @property {string} label - Displayed label of the option (inherited from SelectOption)
48
- * @property {ValueType} value - Value of the option (inherited from SelectOption)
49
- * @property {string} icon - Name of the icon to display next to the option (read-only)
50
- *
51
- * @example
52
- * ```typescript
53
- * const iconOption: SelectIconOption<string> = {
54
- * label: 'Settings',
55
- * value: 'settings',
56
- * icon: 'settings-icon'
57
- * };
58
- * ```
23
+ * This model remains only for compatibility with legacy consumers.
24
+ * It is not part of the desired `internal v2` foundation boundary.
59
25
  */
60
26
  type SelectIconOption<ValueType extends string | number = string | number> = SelectOption<ValueType> & {
61
- /** Icon name */
62
27
  readonly icon: string;
63
28
  };
64
29
 
@@ -124,6 +89,75 @@ type ElementEdges = {
124
89
  */
125
90
  type ElementRect = Readonly<Partial<ElementPosition> & ElementSize & ElementEdges>;
126
91
 
92
+ /**
93
+ * Single sort token in format "field,direction".
94
+ * Used to specify sorting criteria for a single field.
95
+ *
96
+ * @template F - Field name type, defaults to string
97
+ */
98
+ type SortToken<F extends string = string> = `${F},${'asc' | 'desc'}`;
99
+ type SortDirection = 'asc' | 'desc';
100
+ type SortRule<F extends string = string> = {
101
+ sort: F;
102
+ order: SortDirection;
103
+ };
104
+ type SortInput<F extends string = string> = SortRule<F> | ReadonlyArray<SortRule<F>> | null | undefined;
105
+ type QueryArrayMode = 'comma' | 'json' | 'multi';
106
+ type QueryFieldModes = Partial<Record<string, QueryArrayMode>>;
107
+ /**
108
+ * Common query parameters interface for API-like requests.
109
+ *
110
+ * Kept as a generic query contract rather than a transport implementation detail.
111
+ */
112
+ interface QueryParams<Filters = unknown> {
113
+ page?: number;
114
+ size?: number;
115
+ sort?: SortToken | ReadonlyArray<SortToken>;
116
+ filters?: Partial<Filters>;
117
+ }
118
+ type QueryScalar = string | number | boolean;
119
+ type Query = Record<string, QueryScalar | ReadonlyArray<QueryScalar>>;
120
+
121
+ /**
122
+ * Supported REST API HTTP methods.
123
+ *
124
+ * This is transport-facing and remains in `internal` only while no clearer transport
125
+ * contract layer exists.
126
+ */
127
+ type RestMethods = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
128
+ /**
129
+ * Pagination request with optional multi-field sorting.
130
+ *
131
+ * This remains transport/data-domain flavored and should not define the long-term
132
+ * `internal v2` foundation boundary by itself.
133
+ */
134
+ type PageableRequest = {
135
+ page: number;
136
+ size?: number;
137
+ sort?: string | ReadonlyArray<string>;
138
+ };
139
+ /**
140
+ * Paginated response structure.
141
+ */
142
+ type PageableResponse<T> = {
143
+ pageable?: {
144
+ offset?: number;
145
+ pageNumber: number;
146
+ pageSize: number;
147
+ };
148
+ totalElements: number;
149
+ content: T[];
150
+ };
151
+ /**
152
+ * Business-level error response structure.
153
+ */
154
+ type ErrorResponse = {
155
+ errorCode: number | string;
156
+ message: string;
157
+ fields?: Record<string, string>;
158
+ details?: unknown;
159
+ };
160
+
127
161
  /**
128
162
  * Universal type "anything".
129
163
  * Prefer to use `unknown` to avoid `any` leaks.
@@ -216,161 +250,72 @@ type JsonObject = {
216
250
  [k: string]: JsonValue;
217
251
  };
218
252
 
219
- /**
220
- * Supported REST API HTTP methods.
221
- * Defines the standard set of HTTP methods used for API requests.
222
- */
223
- type RestMethods = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
224
- /**
225
- * Single sort token in format "field,direction".
226
- * Used to specify sorting criteria for a single field.
227
- *
228
- * @template F - Field name type, defaults to string
229
- * @example
230
- * ```typescript
231
- * const sort: SortToken = "name,asc";
232
- * const sort2: SortToken<"id" | "createdAt"> = "id,desc";
233
- * ```
234
- */
235
- type SortToken<F extends string = string> = `${F},${'asc' | 'desc'}`;
236
- type SortDirection = 'asc' | 'desc';
237
- type SortRule<F extends string = string> = {
238
- sort: F;
239
- order: SortDirection;
253
+ type StorageInterface<Key, Type = AnyType> = {
254
+ prefix?: string;
255
+ get(key: Key): Type | null;
256
+ set(key: Key, value: Type): void;
257
+ remove(key: Key): void | boolean;
258
+ clear(): void;
259
+ get length(): number;
240
260
  };
241
- type SortInput<F extends string = string> = SortRule<F> | ReadonlyArray<SortRule<F>> | null | undefined;
242
- type QueryArrayMode = 'comma' | 'json' | 'multi';
243
- type QueryFieldModes = Partial<Record<string, QueryArrayMode>>;
244
- /**
245
- * Pagination request with optional multi-field sorting.
246
- * Used to request paginated data from API endpoints.
247
- */
248
- type PageableRequest = {
249
- /**
250
- * Page number (0-based indexing).
251
- * The first page is 0, the second page is 1, etc.
252
- */
253
- page: number;
254
- /**
255
- * Optional page size limit.
256
- * Specifies the maximum number of items to return per page.
257
- */
258
- size?: number;
259
- /**
260
- * Optional sorting criteria.
261
- * Can be a single sort token or an array of sort tokens for multi-field sorting.
262
- * Format: "field,direction" where direction is "asc" or "desc".
263
- * @example ["name,asc", "createdAt,desc"]
264
- */
265
- sort?: SortToken | ReadonlyArray<SortToken>;
261
+ type StorageStrategy = 'memory' | 'lru' | 'session' | 'persist';
262
+ type StorageStrategyOptions = {
263
+ lruLimit?: number;
266
264
  };
267
- /**
268
- * Paginated response structure.
269
- * Compatible with Spring Data pagination format but type-safe.
270
- *
271
- * @template T - Type of items in the content array
272
- */
273
- type PageableResponse<T> = {
274
- /**
275
- * Optional pagination metadata.
276
- * Contains information about the current page position and size.
277
- */
278
- pageable?: {
279
- /**
280
- * Optional zero-based offset of the first item in the current page.
281
- */
282
- offset?: number;
283
- /**
284
- * Current page number (0-based indexing).
285
- */
286
- pageNumber: number;
287
- /**
288
- * Number of items per page.
289
- */
290
- pageSize: number;
291
- };
292
- /**
293
- * Total number of elements across all pages.
294
- */
295
- totalElements: number;
296
- /**
297
- * Array of items for the current page.
298
- */
299
- content: T[];
300
- };
301
- /**
302
- * Business-level error response structure.
303
- * Used to communicate structured error information from API endpoints.
304
- */
305
- type ErrorResponse = {
306
- /**
307
- * Error code identifier.
308
- * Can be a numeric code or string constant for error categorization.
309
- */
310
- errorCode: number | string;
311
- /**
312
- * Human-readable error message.
313
- * Provides a description of the error for display or logging.
314
- */
315
- message: string;
316
- /**
317
- * Optional field-level validation errors map.
318
- * Maps form/DTO field names to their respective error messages.
319
- * Supports multiple errors per field when concatenated in the string value.
320
- */
321
- fields?: Record<string, string>;
322
- /**
323
- * Optional arbitrary error details.
324
- * Can contain additional context such as error code enums, hints, or debugging information.
325
- */
326
- details?: unknown;
327
- };
328
- /**
329
- * Common query parameters interface for API requests.
330
- * Combines pagination, sorting, and filtering capabilities.
331
- *
332
- * @template Filters - Type of the domain model used for filtering, defaults to unknown
333
- */
334
- interface QueryParams<Filters = unknown> {
335
- /**
336
- * Optional page number for pagination (0-based indexing).
337
- */
338
- page?: number;
339
- /**
340
- * Optional page size limit.
341
- */
342
- size?: number;
343
- /**
344
- * Optional sorting criteria.
345
- * Can be a single sort token or an array of sort tokens for multi-field sorting.
346
- */
347
- sort?: SortToken | ReadonlyArray<SortToken>;
348
- /**
349
- * Optional filters as a partial subset of the domain model.
350
- * Allows filtering by any combination of model properties.
351
- */
352
- filters?: Partial<Filters>;
265
+
266
+ declare class LocalStorage<Key = string, Type extends AnyType = AnyDict> implements StorageInterface<Key, Type> {
267
+ prefix: string;
268
+ constructor(prefix?: string);
269
+ get length(): number;
270
+ get(key: Key): Type | null;
271
+ set(key: Key, value: Type): void;
272
+ remove(key: Key): void;
273
+ clear(): void;
274
+ private getSafePrefix;
275
+ private safePrefix;
276
+ }
277
+
278
+ declare class LruCache<KeyT, ValueT> implements StorageInterface<KeyT, ValueT> {
279
+ private map;
280
+ private _limit;
281
+ constructor(limit?: number);
282
+ get limit(): number;
283
+ get length(): number;
284
+ set limit(value: number);
285
+ get(key: KeyT): NonNullable<ValueT> | null;
286
+ set(key: KeyT, value: ValueT): void;
287
+ remove(key: KeyT): boolean;
288
+ clear(): void;
289
+ has(key: KeyT): boolean;
290
+ keys(): KeyT[];
291
+ values(): ValueT[];
292
+ entries(): [KeyT, ValueT][];
293
+ toArray(): ValueT[];
294
+ fromArray(entries: [KeyT, ValueT][]): void;
353
295
  }
354
- /**
355
- * Allowed scalar value types for query parameters.
356
- * Represents primitive types that can be safely serialized in URL query strings.
357
- */
358
- type QueryScalar = string | number | boolean;
359
- /**
360
- * Universal query parameters dictionary with array support.
361
- * Maps parameter names to scalar values or arrays of scalar values.
362
- * Useful for building flexible query strings with multiple values per parameter.
363
- *
364
- * @example
365
- * ```typescript
366
- * const query: Query = {
367
- * status: "active",
368
- * tags: ["typescript", "angular"],
369
- * limit: 10
370
- * };
371
- * ```
372
- */
373
- type Query = Record<string, QueryScalar | ReadonlyArray<QueryScalar>>;
296
+
297
+ declare class MemoryStorage<Key = string, Type extends AnyType = AnyDict> implements StorageInterface<Key, Type> {
298
+ private cache;
299
+ get length(): number;
300
+ get(key: Key): Type | null;
301
+ set(key: Key, value: Type): void;
302
+ remove(key: Key): void;
303
+ clear(): void;
304
+ }
305
+
306
+ declare class SessionStorage<Key = string, Type extends AnyType = AnyDict> implements StorageInterface<Key, Type> {
307
+ prefix: string;
308
+ constructor(prefix?: string);
309
+ get length(): number;
310
+ get(key: Key): Type | null;
311
+ set(key: Key, value: Type): void;
312
+ remove(key: Key): void;
313
+ clear(): void;
314
+ private getSafePrefix;
315
+ private safePrefix;
316
+ }
317
+
318
+ declare const storageStrategy: <Key = string, Type extends AnyType = AnyDict>(strategy: StorageStrategy, options?: StorageStrategyOptions) => StorageInterface<Key, Type>;
374
319
 
375
320
  /**
376
321
  * Built-in language codes.
@@ -467,7 +412,7 @@ declare const TRANSLATION: InjectionToken<TranslationProvider>;
467
412
  * `'light'` — light theme
468
413
  * `'dark'` — dark theme
469
414
  */
470
- type Themes = 'light' | 'dark';
415
+ type Themes = 'light' | 'dark' | (string & {});
471
416
  /**
472
417
  * Provider for changing the application theme.
473
418
  *
@@ -522,12 +467,11 @@ type Devices = 'mobile' | 'tablet' | 'desktop-s' | 'desktop';
522
467
  declare const CURRENT_DEVICE: InjectionToken<Signal<Devices>>;
523
468
 
524
469
  /**
525
- * Represents flexible validation error data without enforced fields.
526
- * Contains common properties like requiredLength and actualLength
527
- * and allows additional string properties through index signature.
470
+ * Legacy validation token surface kept only for compatibility with legacy consumers.
528
471
  *
529
- * @property {string} requiredLength - The required length for validation.
530
- * @property {string} actualLength - The actual length of the validated value.
472
+ * This token set is form/UI-facing and does not belong to the desired `internal v2`
473
+ * foundation boundary. Keep usage isolated and migrate consumers away from the top-level
474
+ * `@reforgium/internal` surface over time.
531
475
  */
532
476
  type ValidationErrorData = {
533
477
  requiredLength: string;
@@ -535,32 +479,11 @@ type ValidationErrorData = {
535
479
  [key: string]: string;
536
480
  };
537
481
  /**
538
- * Map of validation error names to message generator functions.
539
- * Each key represents an error name, and the value is a function
540
- * that takes validation error data and returns a formatted error message.
541
- *
542
- * @example
543
- * const messages: ValidationMessages = {
544
- * minlength: (error) => `Minimum length is ${error.requiredLength}`
545
- * };
482
+ * Legacy validation message map.
546
483
  */
547
484
  type ValidationMessages = Record<string, (error: ValidationErrorData) => string>;
548
485
  /**
549
- * Injection token for a validation messages map.
550
- * Can be overridden in feature modules to provide custom validation messages.
551
- * Defaults to an empty object when not provided.
552
- *
553
- * @example
554
- * // In a feature module:
555
- * providers: [
556
- * {
557
- * provide: VALIDATION_MESSAGES,
558
- * useValue: {
559
- * required: () => 'This field is required',
560
- * minlength: (error) => `Min length: ${error.requiredLength}`
561
- * }
562
- * }
563
- * ]
486
+ * Legacy validation messages token.
564
487
  */
565
488
  declare const VALIDATION_MESSAGES: InjectionToken<ValidationMessages>;
566
489
 
@@ -668,7 +591,7 @@ declare const throttleSignal: <T>(src: Signal<T>, ms?: number, opts?: {
668
591
  *
669
592
  * @returns {string} Formatted date string. Returns an empty string if the input date is invalid.
670
593
  */
671
- declare const formatDate: (date: Date, format?: string) => string;
594
+ declare const formatDate: (date?: Date | null, format?: string) => string;
672
595
  /**
673
596
  * Formats the given date into a `day month year` string using the `ru-RU` locale.
674
597
  * The month is abbreviated and normalized without a trailing dot.
@@ -729,7 +652,7 @@ declare const toDate: (v: unknown) => Date;
729
652
  * reformatDateToISO("24.11.2025", "dd.MM.yyyy")
730
653
  * → "2025-11-24T00:00:00.000Z"
731
654
  */
732
- declare function reformatDateToISO(dateStr: string, mask: string): string | null;
655
+ declare function reformatDateToISO(dateStr?: string | null, mask?: string | null): string | null;
733
656
 
734
657
  /**
735
658
  * Determines if the given value is null/undefined.
@@ -1005,6 +928,96 @@ declare function deepEqual(a: AnyType, b: AnyType, seen?: Map<any, any>): boolea
1005
928
  */
1006
929
  declare const generateId: (limit?: number, radix?: number) => string;
1007
930
 
1008
- export { BUILTIN_LANGS, CHANGE_LANG, CHANGE_THEME, CURRENT_DEVICE, REGISTER_LANG, SELECTED_LANG, SELECTED_THEME, TRANSLATION, VALIDATION_MESSAGES, appendQueryParams, appendQueryParamsByMode, base64ToBlob, buildQueryParams, compareRoutes, concatArray, copyText, debounceSignal, deepEqual, downloadByBlob, downloadByUrl, fillUrlWithParams, formatDate, formatToLocaledDate, formatToSpacedNumber, generateId, getAvailableHeight, getChainedValue, getCorrectedPosition, isDatePeriod, isNullable, isNumber, isObject, makeQuery, materializeRoutePath, mergeQueryParams, normalizeSortInput, normalizeUrl, parseQueryArray, parseQueryParams, parseQueryParamsByMode, parseSortToken, parseToDate, parseToDatePeriod, provideLangs, reformatDateToISO, sortInputToTokens, sortRuleToToken, throttleSignal, toDate, toSortToken, truncate };
1009
- export type { AnyDict, AnyType, Appearance, Devices, Direction, ElementEdges, ElementPosition, ElementRect, ElementSize, ErrorResponse, JsonObject, JsonPrimitive, JsonValue, Langs, LiteralOf, Mutable, Nullable, NullableProps, Nullish, OptionalExcept, PageableRequest, PageableResponse, Query, QueryArrayMode, QueryFieldModes, QueryParams, QueryScalar, RequiredExcept, RestMethods, SelectIconOption, SelectOption, SortDirection, SortInput, SortRule, SortToken, Themes, ValidationErrorData, ValidationMessages, ValueOf };
931
+ type Types = 'string' | 'number' | 'boolean' | 'array' | 'object' | 'date' | 'period' | 'nullable';
932
+ type FieldConcatType = 'comma' | 'multi' | 'json';
933
+ type SerializedPrimitives = string | number | boolean | null;
934
+ type SerializedTypeType = SerializedPrimitives | SerializedPrimitives[];
935
+ type DatePeriod = [Date | null, Date | null];
936
+ type ParseFormatConfig<TypeFrom, TypeTo = SerializedTypeType> = {
937
+ parse?: (val: TypeTo) => TypeFrom;
938
+ format?: (val: TypeFrom) => TypeTo;
939
+ };
940
+ type FormatConfig<TypeFrom, TypeTo = SerializedTypeType> = {
941
+ format?: (val: TypeFrom) => TypeTo;
942
+ };
943
+ type FieldConfig = ({
944
+ type: Types;
945
+ concatType?: FieldConcatType;
946
+ } | FieldsTypeConfig<DataType, AnyType>) & {
947
+ concatType?: FieldConcatType;
948
+ };
949
+ type FieldsTypeConfig<EntityType extends DataType, TypeFrom, TypeTo = SerializedTypeType> = {
950
+ parse: (val: TypeTo, data: SerializedType) => TypeFrom;
951
+ format: (val: TypeFrom, data: EntityType) => TypeTo;
952
+ };
953
+ type PeriodSplitMode = {
954
+ mode: 'split';
955
+ dateFromKeyPostfix: string;
956
+ dateToKeyPostfix: string;
957
+ };
958
+ type PeriodJoinMode = {
959
+ mode: 'join';
960
+ concat: string;
961
+ };
962
+ type SerializerConfig = {
963
+ mapString: {
964
+ trim?: boolean;
965
+ } & ParseFormatConfig<string>;
966
+ mapNumber: {
967
+ fromString?: boolean;
968
+ } & ParseFormatConfig<number>;
969
+ mapBoolean: {
970
+ true?: string;
971
+ false?: string;
972
+ } & ParseFormatConfig<boolean>;
973
+ mapDate: {
974
+ dateFormat?: string;
975
+ } & ParseFormatConfig<Date>;
976
+ mapPeriod: {
977
+ transformMode?: PeriodSplitMode | PeriodJoinMode;
978
+ dateFormat?: string;
979
+ } & ParseFormatConfig<DatePeriod>;
980
+ mapNullable: {
981
+ remove?: boolean;
982
+ replaceWith?: string;
983
+ includeEmptyString?: boolean;
984
+ } & ParseFormatConfig<null | undefined>;
985
+ mapArray: {
986
+ concatType: FieldConcatType;
987
+ removeNullable?: boolean;
988
+ } & FormatConfig<AnyType[]>;
989
+ mapObject: {
990
+ deep: boolean;
991
+ } & FormatConfig<DataType>;
992
+ mapFields?: Record<string, FieldConfig>;
993
+ };
994
+ type SerializedType = Record<string, SerializedTypeType>;
995
+ type DataType = AnyDict;
996
+
997
+ declare class SerializerFieldError extends Error {
998
+ readonly field: string;
999
+ readonly stage: 'parse' | 'format';
1000
+ readonly originalError: unknown;
1001
+ constructor(field: string, stage: 'parse' | 'format', originalError: unknown);
1002
+ }
1003
+ declare class Serializer<EntityType extends DataType> {
1004
+ readonly config: SerializerConfig;
1005
+ constructor(config?: Partial<SerializerConfig>);
1006
+ serialize(obj: EntityType, _seen?: WeakSet<object>): SerializedType;
1007
+ deserialize: (val: string | AnyDict) => EntityType;
1008
+ deserializeQuery: (query: string) => EntityType;
1009
+ deserializeJson: (json: string) => EntityType;
1010
+ toQuery: (val: EntityType) => string;
1011
+ withConfig(config: Partial<SerializerConfig>): Serializer<EntityType>;
1012
+ private serializeElement;
1013
+ private deserializeElement;
1014
+ private parseQuery;
1015
+ private parseJsonObject;
1016
+ private parseInputString;
1017
+ private mergeConfig;
1018
+ private resolveArrayFieldModes;
1019
+ }
1020
+
1021
+ export { BUILTIN_LANGS, CHANGE_LANG, CHANGE_THEME, CURRENT_DEVICE, LocalStorage, LruCache, MemoryStorage, REGISTER_LANG, SELECTED_LANG, SELECTED_THEME, Serializer, SerializerFieldError, SessionStorage, TRANSLATION, VALIDATION_MESSAGES, appendQueryParams, appendQueryParamsByMode, base64ToBlob, buildQueryParams, compareRoutes, concatArray, copyText, debounceSignal, deepEqual, downloadByBlob, downloadByUrl, fillUrlWithParams, formatDate, formatToLocaledDate, formatToSpacedNumber, generateId, getAvailableHeight, getChainedValue, getCorrectedPosition, isDatePeriod, isNullable, isNumber, isObject, makeQuery, materializeRoutePath, mergeQueryParams, normalizeSortInput, normalizeUrl, parseQueryArray, parseQueryParams, parseQueryParamsByMode, parseSortToken, parseToDate, parseToDatePeriod, provideLangs, reformatDateToISO, sortInputToTokens, sortRuleToToken, storageStrategy, throttleSignal, toDate, toSortToken, truncate };
1022
+ export type { AnyDict, AnyType, Appearance, DataType, Devices, Direction, ElementEdges, ElementPosition, ElementRect, ElementSize, ErrorResponse, FieldConcatType, FieldConfig, FormatConfig, JsonObject, JsonPrimitive, JsonValue, Langs, LiteralOf, Mutable, Nullable, NullableProps, Nullish, OptionalExcept, PageableRequest, PageableResponse, ParseFormatConfig, Query, QueryArrayMode, QueryFieldModes, QueryParams, QueryScalar, RequiredExcept, RestMethods, SelectIconOption, SelectOption, SerializedType, SerializerConfig, SortDirection, SortInput, SortRule, SortToken, StorageInterface, StorageStrategy, StorageStrategyOptions, Themes, Types, ValidationErrorData, ValidationMessages, ValueOf };
1010
1023
  //# sourceMappingURL=reforgium-internal.d.ts.map