spice-js 2.6.70 → 2.6.72

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.
@@ -42,6 +42,10 @@ if (!Promise.allSettled) {
42
42
  );
43
43
  }
44
44
  export default class SpiceModel {
45
+ // ⚡ Static caches for performance optimization
46
+ static _defaultsCache = {};
47
+ static _hiddenPropsCache = {};
48
+
45
49
  constructor(args = {}) {
46
50
  try {
47
51
  var dbtype =
@@ -165,15 +169,17 @@ export default class SpiceModel {
165
169
  }
166
170
  case Number:
167
171
  case "number": {
168
- this[i] = _.isNumber(args.args[i])
169
- ? args.args[i]
172
+ this[i] =
173
+ _.isNumber(args.args[i]) ?
174
+ args.args[i]
170
175
  : Number(args.args[i]);
171
176
  break;
172
177
  }
173
178
  case Boolean:
174
179
  case "boolean": {
175
- this[i] = _.isBoolean(args.args[i])
176
- ? args.args[i]
180
+ this[i] =
181
+ _.isBoolean(args.args[i]) ?
182
+ args.args[i]
177
183
  : args.args[i] == "true" ||
178
184
  args.args[i] == 1 ||
179
185
  args.args[i] == "True";
@@ -186,15 +192,17 @@ export default class SpiceModel {
186
192
  }
187
193
  case Array:
188
194
  case "array": {
189
- this[i] = _.isArray(args.args[i])
190
- ? args.args[i]
195
+ this[i] =
196
+ _.isArray(args.args[i]) ?
197
+ args.args[i]
191
198
  : JSON.parse(args.args[i]);
192
199
  break;
193
200
  }
194
201
  case Object:
195
202
  case "object": {
196
- this[i] = _.isObject(args.args[i])
197
- ? args.args[i]
203
+ this[i] =
204
+ _.isObject(args.args[i]) ?
205
+ args.args[i]
198
206
  : JSON.parse(args.args[i]);
199
207
  break;
200
208
  }
@@ -478,6 +486,9 @@ export default class SpiceModel {
478
486
  }
479
487
 
480
488
  async get(args) {
489
+ // Profiling: zero overhead when disabled (single falsy check)
490
+ const p = this[_ctx]?.profiler,
491
+ c = p?.start(`${this.type}.get`, { id: args?.id });
481
492
  try {
482
493
  if (args.mapping_dept) this[_mapping_dept] = args.mapping_dept;
483
494
 
@@ -504,7 +515,13 @@ export default class SpiceModel {
504
515
 
505
516
  if (isCacheEmpty || shouldRefresh) {
506
517
  // Retrieve from the database and update cache
507
- results = await this.database.get(args.id);
518
+ const pDb = p,
519
+ cDb = pDb?.start(`${this.type}.get.database`);
520
+ try {
521
+ results = await this.database.get(args.id);
522
+ } finally {
523
+ cDb && pDb.end(cDb);
524
+ }
508
525
  await this.getCacheProviderObject(this.type).set(
509
526
  key,
510
527
  { value: results, time: new Date().getTime() },
@@ -516,7 +533,13 @@ export default class SpiceModel {
516
533
  }
517
534
  } else {
518
535
  // Directly fetch from the database if caching is disabled
519
- results = await this.database.get(args.id);
536
+ const pDb = p,
537
+ cDb = pDb?.start(`${this.type}.get.database`);
538
+ try {
539
+ results = await this.database.get(args.id);
540
+ } finally {
541
+ cDb && pDb.end(cDb);
542
+ }
520
543
  }
521
544
 
522
545
  if (results.type !== undefined && results.type !== this.type) {
@@ -549,6 +572,8 @@ export default class SpiceModel {
549
572
  } catch (e) {
550
573
  console.log(e.message, e);
551
574
  throw e;
575
+ } finally {
576
+ c && p.end(c);
552
577
  }
553
578
  }
554
579
 
@@ -645,6 +670,9 @@ export default class SpiceModel {
645
670
  }
646
671
 
647
672
  async update(args) {
673
+ // Profiling: zero overhead when disabled (single falsy check)
674
+ const p = this[_ctx]?.profiler,
675
+ c = p?.start(`${this.type}.update`, { id: args?.id });
648
676
  try {
649
677
  this.updated_at = new SDate().now();
650
678
  let results = await this.database.get(args.id);
@@ -675,7 +703,13 @@ export default class SpiceModel {
675
703
  }
676
704
  let db_data = cover_obj.new || this;
677
705
 
678
- await this.database.update(args.id, db_data, args._ttl);
706
+ const pDb = p,
707
+ cDb = pDb?.start(`${this.type}.update.database`);
708
+ try {
709
+ await this.database.update(args.id, db_data, args._ttl);
710
+ } finally {
711
+ cDb && pDb.end(cDb);
712
+ }
679
713
  this.setMonitor();
680
714
 
681
715
  if (args.skip_read_serialize != true && args.skip_serialize != true) {
@@ -704,10 +738,15 @@ export default class SpiceModel {
704
738
  } catch (e) {
705
739
  console.log("Error on update", e, e.stack);
706
740
  throw e;
741
+ } finally {
742
+ c && p.end(c);
707
743
  }
708
744
  }
709
745
 
710
746
  async create(args = {}) {
747
+ // Profiling: zero overhead when disabled (single falsy check)
748
+ const p = this[_ctx]?.profiler,
749
+ c = p?.start(`${this.type}.create`);
711
750
  try {
712
751
  let form;
713
752
  this.created_at = new SDate().now();
@@ -734,7 +773,14 @@ export default class SpiceModel {
734
773
  await this.run_hook(workingForm, "create", "before");
735
774
  }
736
775
 
737
- let results = await this.database.insert(id, workingForm, args._ttl);
776
+ const pDb = p,
777
+ cDb = pDb?.start(`${this.type}.create.database`);
778
+ let results;
779
+ try {
780
+ results = await this.database.insert(id, workingForm, args._ttl);
781
+ } finally {
782
+ cDb && pDb.end(cDb);
783
+ }
738
784
  this.setMonitor();
739
785
 
740
786
  if (args.skip_read_serialize != true && args.skip_serialize != true) {
@@ -760,6 +806,8 @@ export default class SpiceModel {
760
806
  } catch (e) {
761
807
  console.log(e.stack);
762
808
  throw e;
809
+ } finally {
810
+ c && p.end(c);
763
811
  }
764
812
  }
765
813
 
@@ -780,24 +828,33 @@ export default class SpiceModel {
780
828
  }
781
829
 
782
830
  async delete(args) {
783
- let item_exist = await this.exist(args.id);
784
-
785
- if (!item_exist) {
786
- throw new Error(`${this.type} does not exist.`);
787
- }
788
- let results = await this.database.get(args.id);
831
+ // Profiling: zero overhead when disabled (single falsy check)
832
+ const p = this[_ctx]?.profiler,
833
+ c = p?.start(`${this.type}.delete`, { id: args?.id });
789
834
  try {
835
+ let item_exist = await this.exist(args.id);
836
+
837
+ if (!item_exist) {
838
+ throw new Error(`${this.type} does not exist.`);
839
+ }
840
+ let results = await this.database.get(args.id);
790
841
  if (args.skip_hooks != true) {
791
842
  await this.run_hook(args, "delete", "before");
792
843
  }
793
844
  let delete_response = {};
794
- if (args.hard) {
795
- delete_response = await this.database.delete(args.id);
796
- this.setMonitor();
797
- } else {
798
- delete results["id"];
799
- results.deleted = true;
800
- delete_response = await this.database.update(args.id, results);
845
+ const pDb = p,
846
+ cDb = pDb?.start(`${this.type}.delete.database`);
847
+ try {
848
+ if (args.hard) {
849
+ delete_response = await this.database.delete(args.id);
850
+ this.setMonitor();
851
+ } else {
852
+ delete results["id"];
853
+ results.deleted = true;
854
+ delete_response = await this.database.update(args.id, results);
855
+ }
856
+ } finally {
857
+ cDb && pDb.end(cDb);
801
858
  }
802
859
  if (args.skip_hooks != true) {
803
860
  await this.run_hook(results, "delete", "after", results);
@@ -806,6 +863,8 @@ export default class SpiceModel {
806
863
  } catch (e) {
807
864
  console.log(e.stack);
808
865
  throw e;
866
+ } finally {
867
+ c && p.end(c);
809
868
  }
810
869
  }
811
870
 
@@ -1013,9 +1072,8 @@ export default class SpiceModel {
1013
1072
  const aliasRegex = /\s+AS\s+`?([\w]+)`?$/i;
1014
1073
  const aliasMatch = col.match(aliasRegex);
1015
1074
  const explicitAlias = aliasMatch ? aliasMatch[1] : null;
1016
- const colWithoutAlias = explicitAlias
1017
- ? col.replace(aliasRegex, "").trim()
1018
- : col;
1075
+ const colWithoutAlias =
1076
+ explicitAlias ? col.replace(aliasRegex, "").trim() : col;
1019
1077
 
1020
1078
  // `table`.`col` or table.col
1021
1079
  const columnRegex = /^`?([\w]+)`?\.`?([\w]+)`?$/;
@@ -1049,6 +1107,12 @@ export default class SpiceModel {
1049
1107
  }
1050
1108
 
1051
1109
  async list(args = {}) {
1110
+ // Profiling: zero overhead when disabled (single falsy check)
1111
+ const p = this[_ctx]?.profiler,
1112
+ c = p?.start(`${this.type}.list`, {
1113
+ limit: args?.limit,
1114
+ offset: args?.offset,
1115
+ });
1052
1116
  try {
1053
1117
  if (args.mapping_dept) this[_mapping_dept] = args.mapping_dept;
1054
1118
 
@@ -1122,13 +1186,14 @@ export default class SpiceModel {
1122
1186
  // LIMIT/OFFSET/SORT
1123
1187
  args.limit = Number(args.limit) || undefined;
1124
1188
  args.offset = Number(args.offset) || 0;
1125
- args.sort = args.sort
1126
- ? args.sort
1189
+ args.sort =
1190
+ args.sort ?
1191
+ args.sort
1127
1192
  .split(",")
1128
1193
  .map((item) =>
1129
- item.includes(".")
1130
- ? item
1131
- : `\`${this.type}\`.${this.formatSortComponent(item)}`
1194
+ item.includes(".") ? item : (
1195
+ `\`${this.type}\`.${this.formatSortComponent(item)}`
1196
+ )
1132
1197
  )
1133
1198
  .join(",")
1134
1199
  : `\`${this.type}\`.created_at DESC`;
@@ -1179,33 +1244,42 @@ export default class SpiceModel {
1179
1244
  } catch (e) {
1180
1245
  console.log(e.stack);
1181
1246
  throw e;
1247
+ } finally {
1248
+ c && p.end(c);
1182
1249
  }
1183
1250
  }
1184
1251
 
1185
1252
  async fetchResults(args, query) {
1186
- if (args.is_custom_query === "true" && args.ids.length > 0) {
1187
- return await this.database.query(query);
1188
- } else if (args.is_full_text === "true") {
1189
- return await this.database.full_text_search(
1190
- this.type,
1191
- query || "",
1192
- args.limit,
1193
- args.offset,
1194
- args._join
1195
- );
1196
- } else {
1197
- let result = await this.database.search(
1198
- this.type,
1199
- args.columns || "",
1200
- query || "",
1201
- args.limit,
1202
- args.offset,
1203
- args.sort,
1204
- args.do_count,
1205
- args.statement_consistent,
1206
- args._join
1207
- );
1208
- return result;
1253
+ // Profiling: zero overhead when disabled (single falsy check)
1254
+ const p = this[_ctx]?.profiler,
1255
+ c = p?.start(`${this.type}.fetchResults`);
1256
+ try {
1257
+ if (args.is_custom_query === "true" && args.ids.length > 0) {
1258
+ return await this.database.query(query);
1259
+ } else if (args.is_full_text === "true") {
1260
+ return await this.database.full_text_search(
1261
+ this.type,
1262
+ query || "",
1263
+ args.limit,
1264
+ args.offset,
1265
+ args._join
1266
+ );
1267
+ } else {
1268
+ let result = await this.database.search(
1269
+ this.type,
1270
+ args.columns || "",
1271
+ query || "",
1272
+ args.limit,
1273
+ args.offset,
1274
+ args.sort,
1275
+ args.do_count,
1276
+ args.statement_consistent,
1277
+ args._join
1278
+ );
1279
+ return result;
1280
+ }
1281
+ } finally {
1282
+ c && p.end(c);
1209
1283
  }
1210
1284
  }
1211
1285
 
@@ -1257,6 +1331,47 @@ export default class SpiceModel {
1257
1331
  return true;
1258
1332
  }
1259
1333
 
1334
+ // ⚡ OPTIMIZED: Cache defaults metadata per model type
1335
+ getDefaultsMetadata(type) {
1336
+ const cacheKey = `${this.type}::${type}`;
1337
+
1338
+ if (!SpiceModel._defaultsCache[cacheKey]) {
1339
+ const staticDefaults = {};
1340
+ const dynamicDefaults = [];
1341
+
1342
+ // Pre-compute once per model type
1343
+ for (const key in this.props) {
1344
+ const def = this.props[key]?.defaults?.[type];
1345
+ if (def !== undefined) {
1346
+ if (_.isFunction(def)) {
1347
+ dynamicDefaults.push({ key, fn: def });
1348
+ } else {
1349
+ staticDefaults[key] = def;
1350
+ }
1351
+ }
1352
+ }
1353
+
1354
+ SpiceModel._defaultsCache[cacheKey] = {
1355
+ staticDefaults,
1356
+ dynamicDefaults,
1357
+ hasDefaults:
1358
+ Object.keys(staticDefaults).length > 0 || dynamicDefaults.length > 0,
1359
+ };
1360
+ }
1361
+
1362
+ return SpiceModel._defaultsCache[cacheKey];
1363
+ }
1364
+
1365
+ // ⚡ OPTIMIZED: Cache hidden props per model type
1366
+ getHiddenProps() {
1367
+ if (!SpiceModel._hiddenPropsCache[this.type]) {
1368
+ SpiceModel._hiddenPropsCache[this.type] = Object.keys(this.props).filter(
1369
+ (key) => this.props[key]?.hide
1370
+ );
1371
+ }
1372
+ return SpiceModel._hiddenPropsCache[this.type];
1373
+ }
1374
+
1260
1375
  async mapToObject(data, Class, source_property, store_property, property) {
1261
1376
  let original_is_array = _.isArray(data);
1262
1377
  if (!original_is_array) {
@@ -1417,9 +1532,9 @@ export default class SpiceModel {
1417
1532
  execute: async (data) => {
1418
1533
  return await this.mapToObject(
1419
1534
  data,
1420
- _.isString(properties[i].map.reference)
1421
- ? spice.models[properties[i].map.reference]
1422
- : properties[i].map.reference,
1535
+ _.isString(properties[i].map.reference) ?
1536
+ spice.models[properties[i].map.reference]
1537
+ : properties[i].map.reference,
1423
1538
  i,
1424
1539
  properties[i].map.destination || i,
1425
1540
  properties[i]
@@ -1435,9 +1550,9 @@ export default class SpiceModel {
1435
1550
  execute: async (data) => {
1436
1551
  return await this.mapToObjectArray(
1437
1552
  data,
1438
- _.isString(properties[i].map.reference)
1439
- ? spice.models[properties[i].map.reference]
1440
- : properties[i].map.reference,
1553
+ _.isString(properties[i].map.reference) ?
1554
+ spice.models[properties[i].map.reference]
1555
+ : properties[i].map.reference,
1441
1556
  i,
1442
1557
  properties[i].map.destination || i,
1443
1558
  properties[i]
@@ -1458,6 +1573,9 @@ export default class SpiceModel {
1458
1573
  }
1459
1574
 
1460
1575
  async do_serialize(data, type, old_data, args, path_to_be_removed = []) {
1576
+ // Profiling: zero overhead when disabled (single falsy check)
1577
+ const p = this[_ctx]?.profiler,
1578
+ c = p?.start(`${this.type}.do_serialize`, { type });
1461
1579
  try {
1462
1580
  // Early exit if serialization should not run.
1463
1581
  if (!this.shouldSerializerRun(args, type)) {
@@ -1486,26 +1604,30 @@ export default class SpiceModel {
1486
1604
  data = [data];
1487
1605
  }
1488
1606
 
1489
- // Compute the defaults from properties using reduce.
1490
- const defaults = Object.keys(this.props).reduce((acc, key) => {
1491
- const def = this.props[key]?.defaults?.[type];
1492
- if (def !== undefined) {
1493
- acc[key] = _.isFunction(def)
1494
- ? def({ old_data: data, new_data: old_data })
1495
- : def;
1496
- }
1497
- return acc;
1498
- }, {});
1607
+ // OPTIMIZED: Use cached defaults metadata instead of computing every time
1608
+ const { staticDefaults, dynamicDefaults, hasDefaults } =
1609
+ this.getDefaultsMetadata(type);
1499
1610
 
1500
- // Merge defaults into each object.
1501
- data = data.map((item) => _.defaults(item, defaults));
1611
+ if (hasDefaults) {
1612
+ data = data.map((item) => {
1613
+ // Apply static defaults first (fast - no function calls)
1614
+ const result = _.defaults({}, item, staticDefaults);
1615
+
1616
+ // Only compute dynamic defaults if there are any
1617
+ for (const { key, fn } of dynamicDefaults) {
1618
+ if (result[key] === undefined) {
1619
+ result[key] = fn({ old_data: data, new_data: old_data });
1620
+ }
1621
+ }
1622
+
1623
+ return result;
1624
+ });
1625
+ }
1502
1626
 
1503
1627
  // If type is "read", clean the data by omitting certain props.
1504
1628
  if (type === "read") {
1505
- // Collect hidden properties from schema.
1506
- const hiddenProps = Object.keys(this.props).filter(
1507
- (key) => this.props[key]?.hide
1508
- );
1629
+ // OPTIMIZED: Use cached hidden props instead of computing every time
1630
+ const hiddenProps = this.getHiddenProps();
1509
1631
  // Combine default props to remove.
1510
1632
  const propsToClean = [
1511
1633
  "deleted",
@@ -1521,6 +1643,8 @@ export default class SpiceModel {
1521
1643
  } catch (error) {
1522
1644
  console.error("Error in do_serialize:", error.stack);
1523
1645
  throw error;
1646
+ } finally {
1647
+ c && p.end(c);
1524
1648
  }
1525
1649
  }
1526
1650
  }