s3db.js 6.2.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/PLUGINS.md +2724 -0
  2. package/README.md +372 -469
  3. package/UNLICENSE +24 -0
  4. package/dist/s3db.cjs.js +30057 -18387
  5. package/dist/s3db.cjs.min.js +1 -1
  6. package/dist/s3db.d.ts +373 -72
  7. package/dist/s3db.es.js +30043 -18384
  8. package/dist/s3db.es.min.js +1 -1
  9. package/dist/s3db.iife.js +29730 -18061
  10. package/dist/s3db.iife.min.js +1 -1
  11. package/package.json +44 -69
  12. package/src/behaviors/body-only.js +110 -0
  13. package/src/behaviors/body-overflow.js +153 -0
  14. package/src/behaviors/enforce-limits.js +195 -0
  15. package/src/behaviors/index.js +39 -0
  16. package/src/behaviors/truncate-data.js +204 -0
  17. package/src/behaviors/user-managed.js +147 -0
  18. package/src/client.class.js +515 -0
  19. package/src/concerns/base62.js +61 -0
  20. package/src/concerns/calculator.js +204 -0
  21. package/src/concerns/crypto.js +142 -0
  22. package/src/concerns/id.js +8 -0
  23. package/src/concerns/index.js +5 -0
  24. package/src/concerns/try-fn.js +151 -0
  25. package/src/connection-string.class.js +75 -0
  26. package/src/database.class.js +599 -0
  27. package/src/errors.js +261 -0
  28. package/src/index.js +17 -0
  29. package/src/plugins/audit.plugin.js +442 -0
  30. package/src/plugins/cache/cache.class.js +53 -0
  31. package/src/plugins/cache/index.js +6 -0
  32. package/src/plugins/cache/memory-cache.class.js +164 -0
  33. package/src/plugins/cache/s3-cache.class.js +189 -0
  34. package/src/plugins/cache.plugin.js +275 -0
  35. package/src/plugins/consumers/index.js +24 -0
  36. package/src/plugins/consumers/rabbitmq-consumer.js +56 -0
  37. package/src/plugins/consumers/sqs-consumer.js +102 -0
  38. package/src/plugins/costs.plugin.js +81 -0
  39. package/src/plugins/fulltext.plugin.js +473 -0
  40. package/src/plugins/index.js +12 -0
  41. package/src/plugins/metrics.plugin.js +603 -0
  42. package/src/plugins/plugin.class.js +210 -0
  43. package/src/plugins/plugin.obj.js +13 -0
  44. package/src/plugins/queue-consumer.plugin.js +134 -0
  45. package/src/plugins/replicator.plugin.js +769 -0
  46. package/src/plugins/replicators/base-replicator.class.js +85 -0
  47. package/src/plugins/replicators/bigquery-replicator.class.js +328 -0
  48. package/src/plugins/replicators/index.js +44 -0
  49. package/src/plugins/replicators/postgres-replicator.class.js +427 -0
  50. package/src/plugins/replicators/s3db-replicator.class.js +352 -0
  51. package/src/plugins/replicators/sqs-replicator.class.js +427 -0
  52. package/src/resource.class.js +2626 -0
  53. package/src/s3db.d.ts +1263 -0
  54. package/src/schema.class.js +706 -0
  55. package/src/stream/index.js +16 -0
  56. package/src/stream/resource-ids-page-reader.class.js +10 -0
  57. package/src/stream/resource-ids-reader.class.js +63 -0
  58. package/src/stream/resource-reader.class.js +81 -0
  59. package/src/stream/resource-writer.class.js +92 -0
  60. package/src/validator.class.js +97 -0
@@ -0,0 +1,706 @@
1
+ import { flatten, unflatten } from "flat";
2
+
3
+ import {
4
+ set,
5
+ get,
6
+ uniq,
7
+ merge,
8
+ invert,
9
+ isEmpty,
10
+ isString,
11
+ cloneDeep,
12
+ } from "lodash-es";
13
+
14
+ import { encrypt, decrypt } from "./concerns/crypto.js";
15
+ import { ValidatorManager } from "./validator.class.js";
16
+ import { tryFn, tryFnSync } from "./concerns/try-fn.js";
17
+ import { SchemaError } from "./errors.js";
18
+ import { encode as toBase62, decode as fromBase62, encodeDecimal, decodeDecimal } from "./concerns/base62.js";
19
+
20
+ /**
21
+ * Generate base62 mapping for attributes
22
+ * @param {string[]} keys - Array of attribute keys
23
+ * @returns {Object} Mapping object with base62 keys
24
+ */
25
+ function generateBase62Mapping(keys) {
26
+ const mapping = {};
27
+ const reversedMapping = {};
28
+ keys.forEach((key, index) => {
29
+ const base62Key = toBase62(index);
30
+ mapping[key] = base62Key;
31
+ reversedMapping[base62Key] = key;
32
+ });
33
+ return { mapping, reversedMapping };
34
+ }
35
+
36
+ export const SchemaActions = {
37
+ trim: (value) => value == null ? value : value.trim(),
38
+
39
+ encrypt: async (value, { passphrase }) => {
40
+ if (value === null || value === undefined) return value;
41
+ const [ok, err, res] = await tryFn(() => encrypt(value, passphrase));
42
+ return ok ? res : value;
43
+ },
44
+ decrypt: async (value, { passphrase }) => {
45
+ if (value === null || value === undefined) return value;
46
+ const [ok, err, raw] = await tryFn(() => decrypt(value, passphrase));
47
+ if (!ok) return value;
48
+ if (raw === 'null') return null;
49
+ if (raw === 'undefined') return undefined;
50
+ return raw;
51
+ },
52
+
53
+ toString: (value) => value == null ? value : String(value),
54
+
55
+ fromArray: (value, { separator }) => {
56
+ if (value === null || value === undefined || !Array.isArray(value)) {
57
+ return value;
58
+ }
59
+ if (value.length === 0) {
60
+ return '';
61
+ }
62
+ const escapedItems = value.map(item => {
63
+ if (typeof item === 'string') {
64
+ return item
65
+ .replace(/\\/g, '\\\\')
66
+ .replace(new RegExp(`\\${separator}`, 'g'), `\\${separator}`);
67
+ }
68
+ return String(item);
69
+ });
70
+ return escapedItems.join(separator);
71
+ },
72
+
73
+ toArray: (value, { separator }) => {
74
+ if (Array.isArray(value)) {
75
+ return value;
76
+ }
77
+ if (value === null || value === undefined) {
78
+ return value;
79
+ }
80
+ if (value === '') {
81
+ return [];
82
+ }
83
+ const items = [];
84
+ let current = '';
85
+ let i = 0;
86
+ const str = String(value);
87
+ while (i < str.length) {
88
+ if (str[i] === '\\' && i + 1 < str.length) {
89
+ // If next char is separator or backslash, add it literally
90
+ current += str[i + 1];
91
+ i += 2;
92
+ } else if (str[i] === separator) {
93
+ items.push(current);
94
+ current = '';
95
+ i++;
96
+ } else {
97
+ current += str[i];
98
+ i++;
99
+ }
100
+ }
101
+ items.push(current);
102
+ return items;
103
+ },
104
+
105
+ toJSON: (value) => {
106
+ if (value === null) return null;
107
+ if (value === undefined) return undefined;
108
+ if (typeof value === 'string') {
109
+ const [ok, err, parsed] = tryFnSync(() => JSON.parse(value));
110
+ if (ok && typeof parsed === 'object') return value;
111
+ return value;
112
+ }
113
+ const [ok, err, json] = tryFnSync(() => JSON.stringify(value));
114
+ return ok ? json : value;
115
+ },
116
+ fromJSON: (value) => {
117
+ if (value === null) return null;
118
+ if (value === undefined) return undefined;
119
+ if (typeof value !== 'string') return value;
120
+ if (value === '') return '';
121
+ const [ok, err, parsed] = tryFnSync(() => JSON.parse(value));
122
+ return ok ? parsed : value;
123
+ },
124
+
125
+ toNumber: (value) => isString(value) ? value.includes('.') ? parseFloat(value) : parseInt(value) : value,
126
+
127
+ toBool: (value) => [true, 1, 'true', '1', 'yes', 'y'].includes(value),
128
+ fromBool: (value) => [true, 1, 'true', '1', 'yes', 'y'].includes(value) ? '1' : '0',
129
+ fromBase62: (value) => {
130
+ if (value === null || value === undefined || value === '') return value;
131
+ if (typeof value === 'number') return value;
132
+ if (typeof value === 'string') {
133
+ const n = fromBase62(value);
134
+ return isNaN(n) ? undefined : n;
135
+ }
136
+ return undefined;
137
+ },
138
+ toBase62: (value) => {
139
+ if (value === null || value === undefined || value === '') return value;
140
+ if (typeof value === 'number') {
141
+ return toBase62(value);
142
+ }
143
+ if (typeof value === 'string') {
144
+ const n = Number(value);
145
+ return isNaN(n) ? value : toBase62(n);
146
+ }
147
+ return value;
148
+ },
149
+ fromBase62Decimal: (value) => {
150
+ if (value === null || value === undefined || value === '') return value;
151
+ if (typeof value === 'number') return value;
152
+ if (typeof value === 'string') {
153
+ const n = decodeDecimal(value);
154
+ return isNaN(n) ? undefined : n;
155
+ }
156
+ return undefined;
157
+ },
158
+ toBase62Decimal: (value) => {
159
+ if (value === null || value === undefined || value === '') return value;
160
+ if (typeof value === 'number') {
161
+ return encodeDecimal(value);
162
+ }
163
+ if (typeof value === 'string') {
164
+ const n = Number(value);
165
+ return isNaN(n) ? value : encodeDecimal(n);
166
+ }
167
+ return value;
168
+ },
169
+ fromArrayOfNumbers: (value, { separator }) => {
170
+ if (value === null || value === undefined || !Array.isArray(value)) {
171
+ return value;
172
+ }
173
+ if (value.length === 0) {
174
+ return '';
175
+ }
176
+ const base62Items = value.map(item => {
177
+ if (typeof item === 'number' && !isNaN(item)) {
178
+ return toBase62(item);
179
+ }
180
+ // fallback: try to parse as number, else keep as is
181
+ const n = Number(item);
182
+ return isNaN(n) ? '' : toBase62(n);
183
+ });
184
+ return base62Items.join(separator);
185
+ },
186
+ toArrayOfNumbers: (value, { separator }) => {
187
+ if (Array.isArray(value)) {
188
+ return value.map(v => (typeof v === 'number' ? v : fromBase62(v)));
189
+ }
190
+ if (value === null || value === undefined) {
191
+ return value;
192
+ }
193
+ if (value === '') {
194
+ return [];
195
+ }
196
+ const str = String(value);
197
+ const items = [];
198
+ let current = '';
199
+ let i = 0;
200
+ while (i < str.length) {
201
+ if (str[i] === '\\' && i + 1 < str.length) {
202
+ current += str[i + 1];
203
+ i += 2;
204
+ } else if (str[i] === separator) {
205
+ items.push(current);
206
+ current = '';
207
+ i++;
208
+ } else {
209
+ current += str[i];
210
+ i++;
211
+ }
212
+ }
213
+ items.push(current);
214
+ return items.map(v => {
215
+ if (typeof v === 'number') return v;
216
+ if (typeof v === 'string' && v !== '') {
217
+ const n = fromBase62(v);
218
+ return isNaN(n) ? NaN : n;
219
+ }
220
+ return NaN;
221
+ });
222
+ },
223
+ fromArrayOfDecimals: (value, { separator }) => {
224
+ if (value === null || value === undefined || !Array.isArray(value)) {
225
+ return value;
226
+ }
227
+ if (value.length === 0) {
228
+ return '';
229
+ }
230
+ const base62Items = value.map(item => {
231
+ if (typeof item === 'number' && !isNaN(item)) {
232
+ return encodeDecimal(item);
233
+ }
234
+ // fallback: try to parse as number, else keep as is
235
+ const n = Number(item);
236
+ return isNaN(n) ? '' : encodeDecimal(n);
237
+ });
238
+ return base62Items.join(separator);
239
+ },
240
+ toArrayOfDecimals: (value, { separator }) => {
241
+ if (Array.isArray(value)) {
242
+ return value.map(v => (typeof v === 'number' ? v : decodeDecimal(v)));
243
+ }
244
+ if (value === null || value === undefined) {
245
+ return value;
246
+ }
247
+ if (value === '') {
248
+ return [];
249
+ }
250
+ const str = String(value);
251
+ const items = [];
252
+ let current = '';
253
+ let i = 0;
254
+ while (i < str.length) {
255
+ if (str[i] === '\\' && i + 1 < str.length) {
256
+ current += str[i + 1];
257
+ i += 2;
258
+ } else if (str[i] === separator) {
259
+ items.push(current);
260
+ current = '';
261
+ i++;
262
+ } else {
263
+ current += str[i];
264
+ i++;
265
+ }
266
+ }
267
+ items.push(current);
268
+ return items.map(v => {
269
+ if (typeof v === 'number') return v;
270
+ if (typeof v === 'string' && v !== '') {
271
+ const n = decodeDecimal(v);
272
+ return isNaN(n) ? NaN : n;
273
+ }
274
+ return NaN;
275
+ });
276
+ },
277
+
278
+ }
279
+
280
+ export class Schema {
281
+ constructor(args) {
282
+ const {
283
+ map,
284
+ name,
285
+ attributes,
286
+ passphrase,
287
+ version = 1,
288
+ options = {}
289
+ } = args;
290
+
291
+ this.name = name;
292
+ this.version = version;
293
+ this.attributes = attributes || {};
294
+ this.passphrase = passphrase ?? "secret";
295
+ this.options = merge({}, this.defaultOptions(), options);
296
+ this.allNestedObjectsOptional = this.options.allNestedObjectsOptional ?? false;
297
+
298
+ // Preprocess attributes to handle nested objects for validator compilation
299
+ const processedAttributes = this.preprocessAttributesForValidation(this.attributes);
300
+
301
+ this.validator = new ValidatorManager({ autoEncrypt: false }).compile(merge(
302
+ { $$async: true },
303
+ processedAttributes,
304
+ ))
305
+
306
+ if (this.options.generateAutoHooks) this.generateAutoHooks();
307
+
308
+ if (!isEmpty(map)) {
309
+ this.map = map;
310
+ this.reversedMap = invert(map);
311
+ }
312
+ else {
313
+ const flatAttrs = flatten(this.attributes, { safe: true });
314
+ const leafKeys = Object.keys(flatAttrs).filter(k => !k.includes('$$'));
315
+
316
+ // Also include parent object keys for objects that can be empty
317
+ const objectKeys = this.extractObjectKeys(this.attributes);
318
+
319
+ // Combine leaf keys and object keys, removing duplicates
320
+ const allKeys = [...new Set([...leafKeys, ...objectKeys])];
321
+
322
+ // Generate base62 mapping instead of sequential numbers
323
+ const { mapping, reversedMapping } = generateBase62Mapping(allKeys);
324
+ this.map = mapping;
325
+ this.reversedMap = reversedMapping;
326
+
327
+
328
+ }
329
+ }
330
+
331
+ defaultOptions() {
332
+ return {
333
+ autoEncrypt: true,
334
+ autoDecrypt: true,
335
+ arraySeparator: "|",
336
+ generateAutoHooks: true,
337
+
338
+ hooks: {
339
+ beforeMap: {},
340
+ afterMap: {},
341
+ beforeUnmap: {},
342
+ afterUnmap: {},
343
+ }
344
+ }
345
+ }
346
+
347
+ addHook(hook, attribute, action) {
348
+ if (!this.options.hooks[hook][attribute]) this.options.hooks[hook][attribute] = [];
349
+ this.options.hooks[hook][attribute] = uniq([...this.options.hooks[hook][attribute], action])
350
+ }
351
+
352
+ extractObjectKeys(obj, prefix = '') {
353
+ const objectKeys = [];
354
+
355
+ for (const [key, value] of Object.entries(obj)) {
356
+ if (key.startsWith('$$')) continue; // Skip schema metadata
357
+
358
+ const fullKey = prefix ? `${prefix}.${key}` : key;
359
+
360
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
361
+ // This is an object, add its key
362
+ objectKeys.push(fullKey);
363
+
364
+ // Check if it has nested objects
365
+ if (value.$$type === 'object') {
366
+ // Recursively extract nested object keys
367
+ objectKeys.push(...this.extractObjectKeys(value, fullKey));
368
+ }
369
+ }
370
+ }
371
+
372
+ return objectKeys;
373
+ }
374
+
375
+ generateAutoHooks() {
376
+ const schema = flatten(cloneDeep(this.attributes), { safe: true });
377
+
378
+ for (const [name, definition] of Object.entries(schema)) {
379
+ // Handle arrays first to avoid conflicts
380
+ if (definition.includes("array")) {
381
+ if (definition.includes('items:string')) {
382
+ this.addHook("beforeMap", name, "fromArray");
383
+ this.addHook("afterUnmap", name, "toArray");
384
+ } else if (definition.includes('items:number')) {
385
+ // Check if the array items should be treated as integers
386
+ const isIntegerArray = definition.includes("integer:true") ||
387
+ definition.includes("|integer:") ||
388
+ definition.includes("|integer");
389
+
390
+ if (isIntegerArray) {
391
+ // Use standard base62 for arrays of integers
392
+ this.addHook("beforeMap", name, "fromArrayOfNumbers");
393
+ this.addHook("afterUnmap", name, "toArrayOfNumbers");
394
+ } else {
395
+ // Use decimal-aware base62 for arrays of decimals
396
+ this.addHook("beforeMap", name, "fromArrayOfDecimals");
397
+ this.addHook("afterUnmap", name, "toArrayOfDecimals");
398
+ }
399
+ }
400
+ // Skip other processing for arrays to avoid conflicts
401
+ continue;
402
+ }
403
+
404
+ // Handle secrets
405
+ if (definition.includes("secret")) {
406
+ if (this.options.autoEncrypt) {
407
+ this.addHook("beforeMap", name, "encrypt");
408
+ }
409
+ if (this.options.autoDecrypt) {
410
+ this.addHook("afterUnmap", name, "decrypt");
411
+ }
412
+ // Skip other processing for secrets
413
+ continue;
414
+ }
415
+
416
+ // Handle numbers (only for non-array fields)
417
+ if (definition.includes("number")) {
418
+ // Check if it's specifically an integer field
419
+ const isInteger = definition.includes("integer:true") ||
420
+ definition.includes("|integer:") ||
421
+ definition.includes("|integer");
422
+
423
+ if (isInteger) {
424
+ // Use standard base62 for integers
425
+ this.addHook("beforeMap", name, "toBase62");
426
+ this.addHook("afterUnmap", name, "fromBase62");
427
+ } else {
428
+ // Use decimal-aware base62 for decimal numbers
429
+ this.addHook("beforeMap", name, "toBase62Decimal");
430
+ this.addHook("afterUnmap", name, "fromBase62Decimal");
431
+ }
432
+ continue;
433
+ }
434
+
435
+ // Handle booleans
436
+ if (definition.includes("boolean")) {
437
+ this.addHook("beforeMap", name, "fromBool");
438
+ this.addHook("afterUnmap", name, "toBool");
439
+ continue;
440
+ }
441
+
442
+ // Handle JSON fields
443
+ if (definition.includes("json")) {
444
+ this.addHook("beforeMap", name, "toJSON");
445
+ this.addHook("afterUnmap", name, "fromJSON");
446
+ continue;
447
+ }
448
+
449
+ // Handle object fields - add JSON serialization hooks
450
+ if (definition === "object" || definition.includes("object")) {
451
+ this.addHook("beforeMap", name, "toJSON");
452
+ this.addHook("afterUnmap", name, "fromJSON");
453
+ continue;
454
+ }
455
+ }
456
+ }
457
+
458
+ static import(data) {
459
+ let {
460
+ map,
461
+ name,
462
+ options,
463
+ version,
464
+ attributes
465
+ } = isString(data) ? JSON.parse(data) : data;
466
+
467
+ // Corrige atributos aninhados que possam ter sido serializados como string JSON
468
+ const [ok, err, attrs] = tryFnSync(() => Schema._importAttributes(attributes));
469
+ if (!ok) throw new SchemaError('Failed to import schema attributes', { original: err, input: attributes });
470
+ attributes = attrs;
471
+
472
+ const schema = new Schema({
473
+ map,
474
+ name,
475
+ options,
476
+ version,
477
+ attributes
478
+ });
479
+ return schema;
480
+ }
481
+
482
+ /**
483
+ * Recursively import attributes, parsing only stringified objects (legacy)
484
+ */
485
+ static _importAttributes(attrs) {
486
+ if (typeof attrs === 'string') {
487
+ // Tenta detectar se é um objeto serializado como string JSON
488
+ const [ok, err, parsed] = tryFnSync(() => JSON.parse(attrs));
489
+ if (ok && typeof parsed === 'object' && parsed !== null) {
490
+ const [okNested, errNested, nested] = tryFnSync(() => Schema._importAttributes(parsed));
491
+ if (!okNested) throw new SchemaError('Failed to parse nested schema attribute', { original: errNested, input: attrs });
492
+ return nested;
493
+ }
494
+ return attrs;
495
+ }
496
+ if (Array.isArray(attrs)) {
497
+ const [okArr, errArr, arr] = tryFnSync(() => attrs.map(a => Schema._importAttributes(a)));
498
+ if (!okArr) throw new SchemaError('Failed to import array schema attributes', { original: errArr, input: attrs });
499
+ return arr;
500
+ }
501
+ if (typeof attrs === 'object' && attrs !== null) {
502
+ const out = {};
503
+ for (const [k, v] of Object.entries(attrs)) {
504
+ const [okObj, errObj, val] = tryFnSync(() => Schema._importAttributes(v));
505
+ if (!okObj) throw new SchemaError('Failed to import object schema attribute', { original: errObj, key: k, input: v });
506
+ out[k] = val;
507
+ }
508
+ return out;
509
+ }
510
+ return attrs;
511
+ }
512
+
513
+ export() {
514
+ const data = {
515
+ version: this.version,
516
+ name: this.name,
517
+ options: this.options,
518
+ attributes: this._exportAttributes(this.attributes),
519
+ map: this.map,
520
+ };
521
+ return data;
522
+ }
523
+
524
+ /**
525
+ * Recursively export attributes, keeping objects as objects and only serializing leaves as string
526
+ */
527
+ _exportAttributes(attrs) {
528
+ if (typeof attrs === 'string') {
529
+ return attrs;
530
+ }
531
+ if (Array.isArray(attrs)) {
532
+ return attrs.map(a => this._exportAttributes(a));
533
+ }
534
+ if (typeof attrs === 'object' && attrs !== null) {
535
+ const out = {};
536
+ for (const [k, v] of Object.entries(attrs)) {
537
+ out[k] = this._exportAttributes(v);
538
+ }
539
+ return out;
540
+ }
541
+ return attrs;
542
+ }
543
+
544
+ async applyHooksActions(resourceItem, hook) {
545
+ const cloned = cloneDeep(resourceItem);
546
+ for (const [attribute, actions] of Object.entries(this.options.hooks[hook])) {
547
+ for (const action of actions) {
548
+ const value = get(cloned, attribute)
549
+ if (value !== undefined && typeof SchemaActions[action] === 'function') {
550
+ set(cloned, attribute, await SchemaActions[action](value, {
551
+ passphrase: this.passphrase,
552
+ separator: this.options.arraySeparator,
553
+ }))
554
+ }
555
+ }
556
+ }
557
+ return cloned;
558
+ }
559
+
560
+ async validate(resourceItem, { mutateOriginal = false } = {}) {
561
+ let data = mutateOriginal ? resourceItem : cloneDeep(resourceItem)
562
+ const result = await this.validator(data);
563
+ return result
564
+ }
565
+
566
+ async mapper(resourceItem) {
567
+ let obj = cloneDeep(resourceItem);
568
+ // Always apply beforeMap hooks for all fields
569
+ obj = await this.applyHooksActions(obj, "beforeMap");
570
+ // Then flatten the object
571
+ const flattenedObj = flatten(obj, { safe: true });
572
+ const rest = { '_v': this.version + '' };
573
+ for (const [key, value] of Object.entries(flattenedObj)) {
574
+ const mappedKey = this.map[key] || key;
575
+ // Always map numbers to base36
576
+ const attrDef = this.getAttributeDefinition(key);
577
+ if (typeof value === 'number' && typeof attrDef === 'string' && attrDef.includes('number')) {
578
+ rest[mappedKey] = toBase62(value);
579
+ } else if (typeof value === 'string') {
580
+ if (value === '[object Object]') {
581
+ rest[mappedKey] = '{}';
582
+ } else if (value.startsWith('{') || value.startsWith('[')) {
583
+ rest[mappedKey] = value;
584
+ } else {
585
+ rest[mappedKey] = value;
586
+ }
587
+ } else if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
588
+ rest[mappedKey] = JSON.stringify(value);
589
+ } else {
590
+ rest[mappedKey] = value;
591
+ }
592
+ }
593
+ await this.applyHooksActions(rest, "afterMap");
594
+ return rest;
595
+ }
596
+
597
+ async unmapper(mappedResourceItem, mapOverride) {
598
+ let obj = cloneDeep(mappedResourceItem);
599
+ delete obj._v;
600
+ obj = await this.applyHooksActions(obj, "beforeUnmap");
601
+ const reversedMap = mapOverride ? invert(mapOverride) : this.reversedMap;
602
+ const rest = {};
603
+ for (const [key, value] of Object.entries(obj)) {
604
+ const originalKey = reversedMap && reversedMap[key] ? reversedMap[key] : key;
605
+ let parsedValue = value;
606
+ const attrDef = this.getAttributeDefinition(originalKey);
607
+ // Always unmap base62 strings to numbers for number fields (but not array fields or decimal fields)
608
+ if (typeof attrDef === 'string' && attrDef.includes('number') && !attrDef.includes('array') && !attrDef.includes('decimal')) {
609
+ if (typeof parsedValue === 'string' && parsedValue !== '') {
610
+ parsedValue = fromBase62(parsedValue);
611
+ } else if (typeof parsedValue === 'number') {
612
+ // Already a number, do nothing
613
+ } else {
614
+ parsedValue = undefined;
615
+ }
616
+ } else if (typeof value === 'string') {
617
+ if (value === '[object Object]') {
618
+ parsedValue = {};
619
+ } else if (value.startsWith('{') || value.startsWith('[')) {
620
+ const [ok, err, parsed] = tryFnSync(() => JSON.parse(value));
621
+ if (ok) parsedValue = parsed;
622
+ }
623
+ }
624
+ // PATCH: ensure arrays are always arrays
625
+ if (this.attributes) {
626
+ if (typeof attrDef === 'string' && attrDef.includes('array')) {
627
+ if (Array.isArray(parsedValue)) {
628
+ // Already an array
629
+ } else if (typeof parsedValue === 'string' && parsedValue.trim().startsWith('[')) {
630
+ const [okArr, errArr, arr] = tryFnSync(() => JSON.parse(parsedValue));
631
+ if (okArr && Array.isArray(arr)) {
632
+ parsedValue = arr;
633
+ }
634
+ } else {
635
+ parsedValue = SchemaActions.toArray(parsedValue, { separator: this.options.arraySeparator });
636
+ }
637
+ }
638
+ }
639
+ // PATCH: apply afterUnmap hooks for type restoration
640
+ if (this.options.hooks && this.options.hooks.afterUnmap && this.options.hooks.afterUnmap[originalKey]) {
641
+ for (const action of this.options.hooks.afterUnmap[originalKey]) {
642
+ if (typeof SchemaActions[action] === 'function') {
643
+ parsedValue = await SchemaActions[action](parsedValue, {
644
+ passphrase: this.passphrase,
645
+ separator: this.options.arraySeparator,
646
+ });
647
+ }
648
+ }
649
+ }
650
+ rest[originalKey] = parsedValue;
651
+ }
652
+ await this.applyHooksActions(rest, "afterUnmap");
653
+ const result = unflatten(rest);
654
+ for (const [key, value] of Object.entries(mappedResourceItem)) {
655
+ if (key.startsWith('$')) {
656
+ result[key] = value;
657
+ }
658
+ }
659
+ return result;
660
+ }
661
+
662
+ // Helper to get attribute definition by dot notation key
663
+ getAttributeDefinition(key) {
664
+ const parts = key.split('.');
665
+ let def = this.attributes;
666
+ for (const part of parts) {
667
+ if (!def) return undefined;
668
+ def = def[part];
669
+ }
670
+ return def;
671
+ }
672
+
673
+ /**
674
+ * Preprocess attributes to convert nested objects into validator-compatible format
675
+ * @param {Object} attributes - Original attributes
676
+ * @returns {Object} Processed attributes for validator
677
+ */
678
+ preprocessAttributesForValidation(attributes) {
679
+ const processed = {};
680
+
681
+ for (const [key, value] of Object.entries(attributes)) {
682
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
683
+ const isExplicitRequired = value.$$type && value.$$type.includes('required');
684
+ const isExplicitOptional = value.$$type && value.$$type.includes('optional');
685
+ const objectConfig = {
686
+ type: 'object',
687
+ properties: this.preprocessAttributesForValidation(value),
688
+ strict: false
689
+ };
690
+ // Se for explicitamente required, não marca como opcional
691
+ if (isExplicitRequired) {
692
+ // nada
693
+ } else if (isExplicitOptional || this.allNestedObjectsOptional) {
694
+ objectConfig.optional = true;
695
+ }
696
+ processed[key] = objectConfig;
697
+ } else {
698
+ processed[key] = value;
699
+ }
700
+ }
701
+
702
+ return processed;
703
+ }
704
+ }
705
+
706
+ export default Schema