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.
- package/build/bootstrap/map.js +17 -29
- package/build/bootstrap/routes.js +45 -9
- package/build/bootstrap/schema_extenders.js +38 -9
- package/build/bootstrap/validation.js +38 -25
- package/build/index.js +72 -6
- package/build/models/SpiceModel.js +214 -49
- package/bun.lock +2281 -0
- package/package.json +1 -1
- package/src/bootstrap/map.js +17 -12
- package/src/bootstrap/routes.js +36 -10
- package/src/bootstrap/schema_extenders.js +33 -10
- package/src/bootstrap/validation.js +38 -19
- package/src/index.js +51 -4
- package/src/models/SpiceModel.js +202 -78
package/src/models/SpiceModel.js
CHANGED
|
@@ -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] =
|
|
169
|
-
|
|
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] =
|
|
176
|
-
|
|
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] =
|
|
190
|
-
|
|
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] =
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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 =
|
|
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 =
|
|
1126
|
-
|
|
1189
|
+
args.sort =
|
|
1190
|
+
args.sort ?
|
|
1191
|
+
args.sort
|
|
1127
1192
|
.split(",")
|
|
1128
1193
|
.map((item) =>
|
|
1129
|
-
item.includes(".")
|
|
1130
|
-
|
|
1131
|
-
|
|
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
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
-
|
|
1422
|
-
|
|
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
|
-
|
|
1440
|
-
|
|
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
|
-
//
|
|
1490
|
-
const
|
|
1491
|
-
|
|
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
|
-
|
|
1501
|
-
|
|
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
|
-
//
|
|
1506
|
-
const hiddenProps =
|
|
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
|
}
|