s3db.js 12.1.0 → 12.2.1

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 (43) hide show
  1. package/README.md +212 -196
  2. package/dist/s3db.cjs.js +1041 -1941
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.es.js +1039 -1941
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +6 -1
  7. package/src/cli/index.js +954 -43
  8. package/src/cli/migration-manager.js +270 -0
  9. package/src/concerns/calculator.js +0 -4
  10. package/src/concerns/metadata-encoding.js +1 -21
  11. package/src/concerns/plugin-storage.js +17 -4
  12. package/src/concerns/typescript-generator.d.ts +171 -0
  13. package/src/concerns/typescript-generator.js +275 -0
  14. package/src/database.class.js +171 -28
  15. package/src/index.js +15 -9
  16. package/src/plugins/api/index.js +5 -2
  17. package/src/plugins/api/routes/resource-routes.js +86 -1
  18. package/src/plugins/api/server.js +79 -3
  19. package/src/plugins/api/utils/openapi-generator.js +195 -5
  20. package/src/plugins/backup/multi-backup-driver.class.js +0 -1
  21. package/src/plugins/backup.plugin.js +7 -14
  22. package/src/plugins/concerns/plugin-dependencies.js +73 -19
  23. package/src/plugins/eventual-consistency/analytics.js +0 -2
  24. package/src/plugins/eventual-consistency/consolidation.js +2 -13
  25. package/src/plugins/eventual-consistency/index.js +0 -1
  26. package/src/plugins/eventual-consistency/install.js +1 -1
  27. package/src/plugins/geo.plugin.js +5 -6
  28. package/src/plugins/importer/index.js +1 -1
  29. package/src/plugins/index.js +2 -1
  30. package/src/plugins/relation.plugin.js +11 -11
  31. package/src/plugins/replicator.plugin.js +12 -21
  32. package/src/plugins/s3-queue.plugin.js +4 -4
  33. package/src/plugins/scheduler.plugin.js +10 -12
  34. package/src/plugins/state-machine.plugin.js +8 -12
  35. package/src/plugins/tfstate/README.md +1 -1
  36. package/src/plugins/tfstate/errors.js +3 -3
  37. package/src/plugins/tfstate/index.js +41 -67
  38. package/src/plugins/ttl.plugin.js +3 -3
  39. package/src/resource.class.js +263 -61
  40. package/src/schema.class.js +0 -2
  41. package/src/testing/factory.class.js +286 -0
  42. package/src/testing/index.js +15 -0
  43. package/src/testing/seeder.class.js +183 -0
@@ -185,14 +185,55 @@ export class Resource extends AsyncEventEmitter {
185
185
  createdBy,
186
186
  };
187
187
 
188
- // Initialize hooks system
188
+ // Initialize hooks system - expanded to cover ALL methods
189
189
  this.hooks = {
190
+ // Insert hooks
190
191
  beforeInsert: [],
191
192
  afterInsert: [],
193
+
194
+ // Update hooks
192
195
  beforeUpdate: [],
193
196
  afterUpdate: [],
197
+
198
+ // Delete hooks
194
199
  beforeDelete: [],
195
- afterDelete: []
200
+ afterDelete: [],
201
+
202
+ // Get hooks
203
+ beforeGet: [],
204
+ afterGet: [],
205
+
206
+ // List hooks
207
+ beforeList: [],
208
+ afterList: [],
209
+
210
+ // Query hooks
211
+ beforeQuery: [],
212
+ afterQuery: [],
213
+
214
+ // Patch hooks
215
+ beforePatch: [],
216
+ afterPatch: [],
217
+
218
+ // Replace hooks
219
+ beforeReplace: [],
220
+ afterReplace: [],
221
+
222
+ // Exists hooks
223
+ beforeExists: [],
224
+ afterExists: [],
225
+
226
+ // Count hooks
227
+ beforeCount: [],
228
+ afterCount: [],
229
+
230
+ // GetMany hooks
231
+ beforeGetMany: [],
232
+ afterGetMany: [],
233
+
234
+ // DeleteMany hooks
235
+ beforeDeleteMany: [],
236
+ afterDeleteMany: []
196
237
  };
197
238
 
198
239
  // Store attributes
@@ -285,20 +326,6 @@ export class Resource extends AsyncEventEmitter {
285
326
  return idSize;
286
327
  }
287
328
 
288
- /**
289
- * Get resource options (for backward compatibility with tests)
290
- */
291
- get options() {
292
- return {
293
- timestamps: this.config.timestamps,
294
- partitions: this.config.partitions || {},
295
- cache: this.config.cache,
296
- autoDecrypt: this.config.autoDecrypt,
297
- paranoid: this.config.paranoid,
298
- allNestedObjectsOptional: this.config.allNestedObjectsOptional
299
- };
300
- }
301
-
302
329
  export() {
303
330
  const exported = this.schema.export();
304
331
  // Add all configuration at root level
@@ -458,23 +485,82 @@ export class Resource extends AsyncEventEmitter {
458
485
  });
459
486
  }
460
487
 
461
- async validate(data) {
488
+ /**
489
+ * Validate data against resource schema without saving
490
+ * @param {Object} data - Data to validate
491
+ * @param {Object} options - Validation options
492
+ * @param {boolean} options.throwOnError - Throw error if validation fails (default: false)
493
+ * @param {boolean} options.includeId - Include ID validation (default: false)
494
+ * @param {boolean} options.mutateOriginal - Allow mutation of original data (default: false)
495
+ * @returns {Promise<{valid: boolean, isValid: boolean, errors: Array, data: Object, original: Object}>} Validation result
496
+ * @example
497
+ * // Validate before insert
498
+ * const result = await resource.validate({
499
+ * name: 'John Doe',
500
+ * email: 'invalid-email' // Will fail email validation
501
+ * });
502
+ *
503
+ * if (!result.valid) {
504
+ * console.log('Validation errors:', result.errors);
505
+ * // [{ field: 'email', message: '...', ... }]
506
+ * }
507
+ *
508
+ * // Throw on error
509
+ * try {
510
+ * await resource.validate({ email: 'bad' }, { throwOnError: true });
511
+ * } catch (err) {
512
+ * console.log('Validation failed:', err.message);
513
+ * }
514
+ */
515
+ async validate(data, options = {}) {
516
+ const {
517
+ throwOnError = false,
518
+ includeId = false,
519
+ mutateOriginal = false
520
+ } = options;
521
+
522
+ // Clone data to avoid mutation (unless mutateOriginal is true)
523
+ const dataToValidate = mutateOriginal ? data : cloneDeep(data);
524
+
525
+ // If includeId is false, remove id from validation
526
+ if (!includeId && dataToValidate.id) {
527
+ delete dataToValidate.id;
528
+ }
529
+
462
530
  const result = {
463
531
  original: cloneDeep(data),
464
532
  isValid: false,
465
533
  errors: [],
534
+ data: dataToValidate
466
535
  };
467
536
 
468
- const check = await this.schema.validate(data, { mutateOriginal: false });
537
+ try {
538
+ const check = await this.schema.validate(dataToValidate, { mutateOriginal });
469
539
 
470
- if (check === true) {
471
- result.isValid = true;
472
- } else {
473
- result.errors = check;
540
+ if (check === true) {
541
+ result.isValid = true;
542
+ } else {
543
+ result.errors = Array.isArray(check) ? check : [check];
544
+ result.isValid = false;
545
+
546
+ if (throwOnError) {
547
+ const error = new Error('Validation failed');
548
+ error.validationErrors = result.errors;
549
+ error.invalidData = data;
550
+ throw error;
551
+ }
552
+ }
553
+ } catch (err) {
554
+ // If schema.validate threw, and we're not in throwOnError mode, catch and return result
555
+ if (!throwOnError) {
556
+ result.errors = [{ message: err.message, error: err }];
557
+ result.isValid = false;
558
+ } else {
559
+ throw err;
560
+ }
474
561
  }
475
562
 
476
- result.data = data;
477
- return result
563
+ return result;
478
564
  }
479
565
 
480
566
  /**
@@ -798,7 +884,7 @@ export class Resource extends AsyncEventEmitter {
798
884
  const exists = await this.exists(id);
799
885
  if (exists) throw new Error(`Resource with id '${id}' already exists`);
800
886
  const keyDebug = this.getResourceKey(id || '(auto)');
801
- if (this.options.timestamps) {
887
+ if (this.config.timestamps) {
802
888
  attributes.createdAt = new Date().toISOString();
803
889
  attributes.updatedAt = new Date().toISOString();
804
890
  }
@@ -806,7 +892,10 @@ export class Resource extends AsyncEventEmitter {
806
892
  // Aplica defaults antes de tudo
807
893
  const attributesWithDefaults = this.applyDefaults(attributes);
808
894
  // Reconstruct the complete data for validation
809
- const completeData = { id, ...attributesWithDefaults };
895
+ // Only include id if it's defined (not undefined)
896
+ const completeData = id !== undefined
897
+ ? { id, ...attributesWithDefaults }
898
+ : { ...attributesWithDefaults };
810
899
 
811
900
  // Execute beforeInsert hooks
812
901
  const preProcessedData = await this.executeHooks('beforeInsert', completeData);
@@ -822,7 +911,7 @@ export class Resource extends AsyncEventEmitter {
822
911
  errors,
823
912
  isValid,
824
913
  data: validated,
825
- } = await this.validate(preProcessedData);
914
+ } = await this.validate(preProcessedData, { includeId: true });
826
915
 
827
916
  if (!isValid) {
828
917
  const errorMsg = (errors && errors.length && errors[0].message) ? errors[0].message : 'Insert failed';
@@ -958,7 +1047,10 @@ export class Resource extends AsyncEventEmitter {
958
1047
  async get(id) {
959
1048
  if (isObject(id)) throw new Error(`id cannot be an object`);
960
1049
  if (isEmpty(id)) throw new Error('id cannot be empty');
961
-
1050
+
1051
+ // Execute beforeGet hooks
1052
+ await this.executeHooks('beforeGet', { id });
1053
+
962
1054
  const key = this.getResourceKey(id);
963
1055
  // LOG: start of get
964
1056
  // eslint-disable-next-line no-console
@@ -1032,18 +1124,87 @@ export class Resource extends AsyncEventEmitter {
1032
1124
  data = await this.applyVersionMapping(data, objectVersion, this.version);
1033
1125
  }
1034
1126
 
1127
+ // Execute afterGet hooks
1128
+ data = await this.executeHooks('afterGet', data);
1129
+
1035
1130
  this.emit("get", data);
1036
1131
  const value = data;
1037
1132
  return value;
1038
1133
  }
1039
1134
 
1135
+ /**
1136
+ * Retrieve a resource object by ID, or return null if not found
1137
+ * @param {string} id - Resource ID
1138
+ * @returns {Promise<Object|null>} The resource object or null if not found
1139
+ * @example
1140
+ * const user = await resource.getOrNull('user-123');
1141
+ * if (user) {
1142
+ * console.log('Found user:', user.name);
1143
+ * } else {
1144
+ * console.log('User not found');
1145
+ * }
1146
+ */
1147
+ async getOrNull(id) {
1148
+ const [ok, err, data] = await tryFn(() => this.get(id));
1149
+
1150
+ // Check if error is NoSuchKey (resource doesn't exist)
1151
+ if (!ok && err && (err.name === 'NoSuchKey' || err.message?.includes('NoSuchKey'))) {
1152
+ return null;
1153
+ }
1154
+
1155
+ // Re-throw other errors (permission denied, network issues, etc.)
1156
+ if (!ok) {
1157
+ throw err;
1158
+ }
1159
+
1160
+ return data;
1161
+ }
1162
+
1163
+ /**
1164
+ * Retrieve a resource object by ID, or throw ResourceNotFoundError if not found
1165
+ * @param {string} id - Resource ID
1166
+ * @returns {Promise<Object>} The resource object
1167
+ * @throws {ResourceError} If resource does not exist
1168
+ * @example
1169
+ * // Throws error if user doesn't exist (no need for null check)
1170
+ * const user = await resource.getOrThrow('user-123');
1171
+ * console.log('User name:', user.name); // Safe to access
1172
+ */
1173
+ async getOrThrow(id) {
1174
+ const [ok, err, data] = await tryFn(() => this.get(id));
1175
+
1176
+ // Check if error is NoSuchKey (resource doesn't exist)
1177
+ if (!ok && err && (err.name === 'NoSuchKey' || err.message?.includes('NoSuchKey'))) {
1178
+ throw new ResourceError(`Resource '${this.name}' with id '${id}' not found`, {
1179
+ resourceName: this.name,
1180
+ operation: 'getOrThrow',
1181
+ id,
1182
+ code: 'RESOURCE_NOT_FOUND'
1183
+ });
1184
+ }
1185
+
1186
+ // Re-throw other errors (permission denied, network issues, etc.)
1187
+ if (!ok) {
1188
+ throw err;
1189
+ }
1190
+
1191
+ return data;
1192
+ }
1193
+
1040
1194
  /**
1041
1195
  * Check if a resource exists by ID
1042
1196
  * @returns {Promise<boolean>} True if resource exists, false otherwise
1043
1197
  */
1044
1198
  async exists(id) {
1199
+ // Execute beforeExists hooks
1200
+ await this.executeHooks('beforeExists', { id });
1201
+
1045
1202
  const key = this.getResourceKey(id);
1046
1203
  const [ok, err] = await tryFn(() => this.client.headObject(key));
1204
+
1205
+ // Execute afterExists hooks
1206
+ await this.executeHooks('afterExists', { id, exists: ok });
1207
+
1047
1208
  return ok;
1048
1209
  }
1049
1210
 
@@ -1102,7 +1263,7 @@ export class Resource extends AsyncEventEmitter {
1102
1263
  }
1103
1264
  const preProcessedData = await this.executeHooks('beforeUpdate', cloneDeep(mergedData));
1104
1265
  const completeData = { ...originalData, ...preProcessedData, id };
1105
- const { isValid, errors, data } = await this.validate(cloneDeep(completeData));
1266
+ const { isValid, errors, data } = await this.validate(cloneDeep(completeData), { includeId: true });
1106
1267
  if (!isValid) {
1107
1268
  throw new InvalidResourceItem({
1108
1269
  bucket: this.client.config.bucket,
@@ -1288,20 +1449,30 @@ export class Resource extends AsyncEventEmitter {
1288
1449
  throw new Error('fields must be a non-empty object');
1289
1450
  }
1290
1451
 
1452
+ // Execute beforePatch hooks
1453
+ await this.executeHooks('beforePatch', { id, fields, options });
1454
+
1291
1455
  const behavior = this.behavior;
1292
1456
 
1293
1457
  // Check if fields contain dot notation (nested fields)
1294
1458
  const hasNestedFields = Object.keys(fields).some(key => key.includes('.'));
1295
1459
 
1460
+ let result;
1461
+
1296
1462
  // ✅ Optimization: HEAD + COPY for metadata-only behaviors WITHOUT nested fields
1297
1463
  if ((behavior === 'enforce-limits' || behavior === 'truncate-data') && !hasNestedFields) {
1298
- return await this._patchViaCopyObject(id, fields, options);
1464
+ result = await this._patchViaCopyObject(id, fields, options);
1465
+ } else {
1466
+ // ⚠️ Fallback: GET + merge + PUT for:
1467
+ // - Behaviors with body storage
1468
+ // - Nested field updates (need full object merge)
1469
+ result = await this.update(id, fields, options);
1299
1470
  }
1300
1471
 
1301
- // ⚠️ Fallback: GET + merge + PUT for:
1302
- // - Behaviors with body storage
1303
- // - Nested field updates (need full object merge)
1304
- return await this.update(id, fields, options);
1472
+ // Execute afterPatch hooks
1473
+ const finalResult = await this.executeHooks('afterPatch', result);
1474
+
1475
+ return finalResult;
1305
1476
  }
1306
1477
 
1307
1478
  /**
@@ -1433,6 +1604,9 @@ export class Resource extends AsyncEventEmitter {
1433
1604
  throw new Error('fullData must be a non-empty object');
1434
1605
  }
1435
1606
 
1607
+ // Execute beforeReplace hooks
1608
+ await this.executeHooks('beforeReplace', { id, fullData, options });
1609
+
1436
1610
  const { partition, partitionValues } = options;
1437
1611
 
1438
1612
  // Clone data to avoid mutations
@@ -1458,7 +1632,7 @@ export class Resource extends AsyncEventEmitter {
1458
1632
  errors,
1459
1633
  isValid,
1460
1634
  data: validated,
1461
- } = await this.validate(completeData);
1635
+ } = await this.validate(completeData, { includeId: true });
1462
1636
 
1463
1637
  if (!isValid) {
1464
1638
  const errorMsg = (errors && errors.length && errors[0].message) ? errors[0].message : 'Replace failed';
@@ -1556,7 +1730,10 @@ export class Resource extends AsyncEventEmitter {
1556
1730
  }
1557
1731
  }
1558
1732
 
1559
- return replacedObject;
1733
+ // Execute afterReplace hooks
1734
+ const finalResult = await this.executeHooks('afterReplace', replacedObject);
1735
+
1736
+ return finalResult;
1560
1737
  }
1561
1738
 
1562
1739
  /**
@@ -1628,7 +1805,7 @@ export class Resource extends AsyncEventEmitter {
1628
1805
  const completeData = { ...originalData, ...preProcessedData, id };
1629
1806
 
1630
1807
  // Validate
1631
- const { isValid, errors, data } = await this.validate(cloneDeep(completeData));
1808
+ const { isValid, errors, data } = await this.validate(cloneDeep(completeData), { includeId: true });
1632
1809
  if (!isValid) {
1633
1810
  return {
1634
1811
  success: false,
@@ -1893,6 +2070,9 @@ export class Resource extends AsyncEventEmitter {
1893
2070
  * });
1894
2071
  */
1895
2072
  async count({ partition = null, partitionValues = {} } = {}) {
2073
+ // Execute beforeCount hooks
2074
+ await this.executeHooks('beforeCount', { partition, partitionValues });
2075
+
1896
2076
  let prefix;
1897
2077
 
1898
2078
  if (partition && Object.keys(partitionValues).length > 0) {
@@ -1924,6 +2104,10 @@ export class Resource extends AsyncEventEmitter {
1924
2104
  }
1925
2105
 
1926
2106
  const count = await this.client.count({ prefix });
2107
+
2108
+ // Execute afterCount hooks
2109
+ await this.executeHooks('afterCount', { count, partition, partitionValues });
2110
+
1927
2111
  this.emit("count", count);
1928
2112
  return count;
1929
2113
  }
@@ -1965,6 +2149,9 @@ export class Resource extends AsyncEventEmitter {
1965
2149
  * const results = await resource.deleteMany(deletedIds);
1966
2150
  */
1967
2151
  async deleteMany(ids) {
2152
+ // Execute beforeDeleteMany hooks
2153
+ await this.executeHooks('beforeDeleteMany', { ids });
2154
+
1968
2155
  const packages = chunk(
1969
2156
  ids.map((id) => this.getResourceKey(id)),
1970
2157
  1000
@@ -1996,6 +2183,9 @@ export class Resource extends AsyncEventEmitter {
1996
2183
  return response;
1997
2184
  });
1998
2185
 
2186
+ // Execute afterDeleteMany hooks
2187
+ await this.executeHooks('afterDeleteMany', { ids, results });
2188
+
1999
2189
  this.emit("deleteMany", ids.length);
2000
2190
  return results;
2001
2191
  }
@@ -2137,6 +2327,9 @@ export class Resource extends AsyncEventEmitter {
2137
2327
  * });
2138
2328
  */
2139
2329
  async list({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
2330
+ // Execute beforeList hooks
2331
+ await this.executeHooks('beforeList', { partition, partitionValues, limit, offset });
2332
+
2140
2333
  const [ok, err, result] = await tryFn(async () => {
2141
2334
  if (!partition) {
2142
2335
  return await this.listMain({ limit, offset });
@@ -2146,7 +2339,10 @@ export class Resource extends AsyncEventEmitter {
2146
2339
  if (!ok) {
2147
2340
  return this.handleListError(err, { partition, partitionValues });
2148
2341
  }
2149
- return result;
2342
+
2343
+ // Execute afterList hooks
2344
+ const finalResult = await this.executeHooks('afterList', result);
2345
+ return finalResult;
2150
2346
  }
2151
2347
 
2152
2348
  async listMain({ limit, offset = 0 }) {
@@ -2314,6 +2510,9 @@ export class Resource extends AsyncEventEmitter {
2314
2510
  * const users = await resource.getMany(['user-1', 'user-2', 'user-3']);
2315
2511
  */
2316
2512
  async getMany(ids) {
2513
+ // Execute beforeGetMany hooks
2514
+ await this.executeHooks('beforeGetMany', { ids });
2515
+
2317
2516
  const { results, errors } = await PromisePool.for(ids)
2318
2517
  .withConcurrency(this.client.parallelism)
2319
2518
  .handleError(async (error, id) => {
@@ -2338,8 +2537,11 @@ export class Resource extends AsyncEventEmitter {
2338
2537
  throw err;
2339
2538
  });
2340
2539
 
2540
+ // Execute afterGetMany hooks
2541
+ const finalResults = await this.executeHooks('afterGetMany', results);
2542
+
2341
2543
  this.emit("getMany", ids.length);
2342
- return results;
2544
+ return finalResults;
2343
2545
  }
2344
2546
 
2345
2547
  /**
@@ -2604,25 +2806,6 @@ export class Resource extends AsyncEventEmitter {
2604
2806
  * @returns {Object} Schema object for the version
2605
2807
  */
2606
2808
  async getSchemaForVersion(version) {
2607
- // If version is the same as current, return current schema
2608
- if (version === this.version) {
2609
- return this.schema;
2610
- }
2611
- // For different versions, try to create a compatible schema
2612
- // This is especially important for v0 objects that might have different encryption
2613
- const [ok, err, compatibleSchema] = await tryFn(() => Promise.resolve(new Schema({
2614
- name: this.name,
2615
- attributes: this.attributes,
2616
- passphrase: this.passphrase,
2617
- version: version,
2618
- options: {
2619
- ...this.config,
2620
- autoDecrypt: true,
2621
- autoEncrypt: true
2622
- }
2623
- })));
2624
- if (ok) return compatibleSchema;
2625
- // console.warn(`Failed to create compatible schema for version ${version}, using current schema:`, err.message);
2626
2809
  return this.schema;
2627
2810
  }
2628
2811
 
@@ -2732,6 +2915,9 @@ export class Resource extends AsyncEventEmitter {
2732
2915
  * );
2733
2916
  */
2734
2917
  async query(filter = {}, { limit = 100, offset = 0, partition = null, partitionValues = {} } = {}) {
2918
+ // Execute beforeQuery hooks
2919
+ await this.executeHooks('beforeQuery', { filter, limit, offset, partition, partitionValues });
2920
+
2735
2921
  if (Object.keys(filter).length === 0) {
2736
2922
  // No filter, just return paginated results
2737
2923
  return await this.list({ partition, partitionValues, limit, offset });
@@ -2772,7 +2958,10 @@ export class Resource extends AsyncEventEmitter {
2772
2958
  }
2773
2959
 
2774
2960
  // Return only up to the requested limit
2775
- return results.slice(0, limit);
2961
+ const finalResults = results.slice(0, limit);
2962
+
2963
+ // Execute afterQuery hooks
2964
+ return await this.executeHooks('afterQuery', finalResults);
2776
2965
  }
2777
2966
 
2778
2967
  /**
@@ -2891,7 +3080,7 @@ export class Resource extends AsyncEventEmitter {
2891
3080
  }
2892
3081
 
2893
3082
  /**
2894
- * Update partition objects to keep them in sync (legacy method for backward compatibility)
3083
+ * Update partition objects to keep them in sync
2895
3084
  * @param {Object} data - Updated object data
2896
3085
  */
2897
3086
  async updatePartitionReferences(data) {
@@ -3347,7 +3536,20 @@ function validateResourceConfig(config) {
3347
3536
  if (typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
3348
3537
  errors.push("Resource 'hooks' must be an object");
3349
3538
  } else {
3350
- const validHookEvents = ['beforeInsert', 'afterInsert', 'beforeUpdate', 'afterUpdate', 'beforeDelete', 'afterDelete'];
3539
+ const validHookEvents = [
3540
+ 'beforeInsert', 'afterInsert',
3541
+ 'beforeUpdate', 'afterUpdate',
3542
+ 'beforeDelete', 'afterDelete',
3543
+ 'beforeGet', 'afterGet',
3544
+ 'beforeList', 'afterList',
3545
+ 'beforeQuery', 'afterQuery',
3546
+ 'beforeExists', 'afterExists',
3547
+ 'beforeCount', 'afterCount',
3548
+ 'beforePatch', 'afterPatch',
3549
+ 'beforeReplace', 'afterReplace',
3550
+ 'beforeGetMany', 'afterGetMany',
3551
+ 'beforeDeleteMany', 'afterDeleteMany'
3552
+ ];
3351
3553
  for (const [event, hooksArr] of Object.entries(config.hooks)) {
3352
3554
  if (!validHookEvents.includes(event)) {
3353
3555
  errors.push(`Invalid hook event '${event}'. Valid events: ${validHookEvents.join(', ')}`);
@@ -307,8 +307,6 @@ export const SchemaActions = {
307
307
  return decodeFixedPointBatch(str, precision);
308
308
  }
309
309
 
310
- // Fallback: Legacy format with individual prefixes (^val,^val,^val)
311
- // This maintains backwards compatibility with data encoded before batch optimization
312
310
  const items = [];
313
311
  let current = '';
314
312
  let i = 0;