spice-js 2.7.18 → 2.7.19
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/models/SpiceModel.js +90 -25
- package/package.json +1 -1
- package/src/models/SpiceModel.js +110 -14
- package/tests/models/SpiceModel.test.js +156 -0
- package/src/models/SpiceModel.js.bak +0 -2186
|
@@ -1,2186 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { MapType, DataType } from "..";
|
|
4
|
-
let co = require("co");
|
|
5
|
-
var regeneratorRuntime = require("regenerator-runtime");
|
|
6
|
-
import ResourceLifecycleTriggered from "../events/events/ResourceLifecycleTriggered";
|
|
7
|
-
import { hasSQLInjection } from "../utility/Security";
|
|
8
|
-
import { fixCollection } from "../utility/fix";
|
|
9
|
-
|
|
10
|
-
var SDate = require("sonover-date"),
|
|
11
|
-
UUID = require("uuid"),
|
|
12
|
-
_ = require("lodash"),
|
|
13
|
-
_database = Symbol(),
|
|
14
|
-
_ctx = Symbol(),
|
|
15
|
-
_props = Symbol(),
|
|
16
|
-
_args = Symbol(),
|
|
17
|
-
_hooks = Symbol(),
|
|
18
|
-
_columns = Symbol(),
|
|
19
|
-
_disable_lifecycle_events = Symbol(),
|
|
20
|
-
_external_modifier_loaded = Symbol(),
|
|
21
|
-
_skip_cache = Symbol(),
|
|
22
|
-
_serializers = Symbol(),
|
|
23
|
-
_level = Symbol(),
|
|
24
|
-
_mapping_dept = Symbol(),
|
|
25
|
-
_mapping_dept_exempt = Symbol(),
|
|
26
|
-
_current_path = Symbol();
|
|
27
|
-
|
|
28
|
-
//const _type = Symbol("type");
|
|
29
|
-
|
|
30
|
-
// Helper to resolve spice global (for testing compatibility)
|
|
31
|
-
// In production, spice is injected globally. In tests, we need to access it via global.
|
|
32
|
-
function getSpice() {
|
|
33
|
-
if (typeof spice !== 'undefined') {
|
|
34
|
-
return spice;
|
|
35
|
-
}
|
|
36
|
-
if (typeof global !== 'undefined' && global.spice) {
|
|
37
|
-
return global.spice;
|
|
38
|
-
}
|
|
39
|
-
if (typeof globalThis !== 'undefined' && globalThis.spice) {
|
|
40
|
-
return globalThis.spice;
|
|
41
|
-
}
|
|
42
|
-
throw new Error('spice global not found');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let that;
|
|
46
|
-
if (!Promise.allSettled) {
|
|
47
|
-
Promise.allSettled = (promises) =>
|
|
48
|
-
Promise.all(
|
|
49
|
-
promises.map((promise, i) =>
|
|
50
|
-
promise
|
|
51
|
-
.then((value) => ({
|
|
52
|
-
status: "fulfilled",
|
|
53
|
-
value,
|
|
54
|
-
}))
|
|
55
|
-
.catch((reason) => ({
|
|
56
|
-
status: "rejected",
|
|
57
|
-
reason,
|
|
58
|
-
})),
|
|
59
|
-
),
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
export default class SpiceModel {
|
|
63
|
-
constructor(args = {}) {
|
|
64
|
-
try {
|
|
65
|
-
var dbtype =
|
|
66
|
-
spice.config.database.connections[args.connection].type || "couchbase";
|
|
67
|
-
let Database = require(`spice-${dbtype}`);
|
|
68
|
-
this[_mapping_dept] =
|
|
69
|
-
args?.args?.mapping_dept || process.env.MAPPING_DEPT || 3;
|
|
70
|
-
this[_mapping_dept_exempt] = args?.args?.mapping_dept_exempt || [];
|
|
71
|
-
this.type = "";
|
|
72
|
-
this.collection = args.connection;
|
|
73
|
-
this[_args] = args.args;
|
|
74
|
-
this[_external_modifier_loaded] = false;
|
|
75
|
-
this[_disable_lifecycle_events] =
|
|
76
|
-
args.args?.disable_lifecycle_events || false;
|
|
77
|
-
this[_ctx] = args?.args?.ctx;
|
|
78
|
-
this[_skip_cache] = args?.args?.skip_cache || false;
|
|
79
|
-
this[_level] = args?.args?._level || 0;
|
|
80
|
-
this[_current_path] = args?.args?._current_path || "";
|
|
81
|
-
this[_columns] = args?.args?._columns || "";
|
|
82
|
-
this[_hooks] = {
|
|
83
|
-
create: {
|
|
84
|
-
before: [],
|
|
85
|
-
after: [],
|
|
86
|
-
},
|
|
87
|
-
get: {
|
|
88
|
-
before: [],
|
|
89
|
-
after: [],
|
|
90
|
-
},
|
|
91
|
-
list: {
|
|
92
|
-
before: [],
|
|
93
|
-
after: [],
|
|
94
|
-
},
|
|
95
|
-
update: {
|
|
96
|
-
before: [],
|
|
97
|
-
after: [],
|
|
98
|
-
},
|
|
99
|
-
delete: {
|
|
100
|
-
before: [],
|
|
101
|
-
after: [],
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
this[_serializers] = {
|
|
105
|
-
read: {
|
|
106
|
-
modifiers: [],
|
|
107
|
-
cleaners: ["deleted", "type"],
|
|
108
|
-
},
|
|
109
|
-
write: {
|
|
110
|
-
modifiers: [],
|
|
111
|
-
cleaners: [],
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
this.deleted = false;
|
|
116
|
-
this[_database] = new Database(args.connection || "default", {
|
|
117
|
-
collection: args.collection,
|
|
118
|
-
scope: args.scope,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
function removeDynamicProps(props) {
|
|
122
|
-
let returnVal = {};
|
|
123
|
-
for (let i in props) {
|
|
124
|
-
if (
|
|
125
|
-
!_.has(props[i], "source") ||
|
|
126
|
-
_.get(props[i], "source") == "static"
|
|
127
|
-
) {
|
|
128
|
-
returnVal[i] = props[i];
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return returnVal;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function applySchemaOverrides(props, resource) {
|
|
135
|
-
spice.schema_extenders = spice.schema_extenders || [];
|
|
136
|
-
|
|
137
|
-
function applyExtenderModifier(prop, resource, extender) {
|
|
138
|
-
if (extender.modifier) {
|
|
139
|
-
return extender.modifier(prop, resource);
|
|
140
|
-
}
|
|
141
|
-
return prop;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function hasExtender(resource, prop, extender) {
|
|
145
|
-
if (extender.selector(prop, resource)) {
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function resourceHasExtender(resource, extender) {
|
|
152
|
-
return (
|
|
153
|
-
extender.resource.includes(resource) ||
|
|
154
|
-
extender.resource.includes("*")
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
for (let file_name in spice.schema_extenders) {
|
|
159
|
-
let extender = spice.schema_extenders[file_name];
|
|
160
|
-
if (resourceHasExtender(resource, extender)) {
|
|
161
|
-
for (let j in props) {
|
|
162
|
-
if (hasExtender(resource, props[j], extender)) {
|
|
163
|
-
props[j] = applyExtenderModifier(props[j], resource, extender);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
if (extender?.creator) {
|
|
167
|
-
props = _.merge(props, extender.creator(resource));
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return props;
|
|
172
|
-
}
|
|
173
|
-
args.props = applySchemaOverrides(
|
|
174
|
-
removeDynamicProps(args.props),
|
|
175
|
-
args.collection,
|
|
176
|
-
);
|
|
177
|
-
this[_props] = args.props;
|
|
178
|
-
|
|
179
|
-
// Get dynamic schema for zero-downtime field updates
|
|
180
|
-
// Merge with static props to ensure new fields are also copied to instance
|
|
181
|
-
let effectiveProps = args.props;
|
|
182
|
-
if (typeof spice !== "undefined" && spice.schemas && args.collection) {
|
|
183
|
-
const dynamicSchema =
|
|
184
|
-
spice.schemas[args.collection] ||
|
|
185
|
-
spice.schemas[args.collection.toLowerCase()];
|
|
186
|
-
if (dynamicSchema) {
|
|
187
|
-
effectiveProps = { ...args.props, ...dynamicSchema };
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
for (let i in effectiveProps) {
|
|
192
|
-
if (args.args != undefined) {
|
|
193
|
-
if (args.args[i] != undefined) {
|
|
194
|
-
const propDef = effectiveProps[i] || {};
|
|
195
|
-
switch (propDef.type) {
|
|
196
|
-
case String:
|
|
197
|
-
case "string": {
|
|
198
|
-
this[i] = args.args[i];
|
|
199
|
-
break;
|
|
200
|
-
}
|
|
201
|
-
case Number:
|
|
202
|
-
case "number": {
|
|
203
|
-
this[i] =
|
|
204
|
-
_.isNumber(args.args[i]) ?
|
|
205
|
-
args.args[i]
|
|
206
|
-
: Number(args.args[i]);
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
case Boolean:
|
|
210
|
-
case "boolean": {
|
|
211
|
-
this[i] =
|
|
212
|
-
_.isBoolean(args.args[i]) ?
|
|
213
|
-
args.args[i]
|
|
214
|
-
: args.args[i] == "true" ||
|
|
215
|
-
args.args[i] == 1 ||
|
|
216
|
-
args.args[i] == "True";
|
|
217
|
-
break;
|
|
218
|
-
}
|
|
219
|
-
case Date:
|
|
220
|
-
case "date": {
|
|
221
|
-
this[i] = args.args[i];
|
|
222
|
-
break;
|
|
223
|
-
}
|
|
224
|
-
case Array:
|
|
225
|
-
case "array": {
|
|
226
|
-
this[i] =
|
|
227
|
-
_.isArray(args.args[i]) ?
|
|
228
|
-
args.args[i]
|
|
229
|
-
: JSON.parse(args.args[i]);
|
|
230
|
-
break;
|
|
231
|
-
}
|
|
232
|
-
case Object:
|
|
233
|
-
case "object": {
|
|
234
|
-
this[i] =
|
|
235
|
-
_.isObject(args.args[i]) ?
|
|
236
|
-
args.args[i]
|
|
237
|
-
: JSON.parse(args.args[i]);
|
|
238
|
-
break;
|
|
239
|
-
}
|
|
240
|
-
default: {
|
|
241
|
-
this[i] = args.args[i];
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
that = this;
|
|
248
|
-
this.createModifiersFromProps();
|
|
249
|
-
} catch (e) {
|
|
250
|
-
console.warn(e.stack);
|
|
251
|
-
}
|
|
252
|
-
// }
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
get database() {
|
|
256
|
-
return this[_database];
|
|
257
|
-
}
|
|
258
|
-
set database(value) {
|
|
259
|
-
this[_database] = value;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
get props() {
|
|
263
|
-
// Dynamically look up schema from spice.schemas for zero-downtime updates
|
|
264
|
-
// Fall back to stored props for backward compatibility
|
|
265
|
-
if (this.type && typeof spice !== "undefined" && spice.schemas) {
|
|
266
|
-
const dynamicSchema =
|
|
267
|
-
spice.schemas[this.type] || spice.schemas[this.type.toLowerCase()];
|
|
268
|
-
if (dynamicSchema) {
|
|
269
|
-
return dynamicSchema;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return this[_props];
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
set props(value) {
|
|
276
|
-
this[_props] = value;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
is_date(item) {
|
|
280
|
-
return item.length > 27 && item.indexOf("|") > -1;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async propsToBeRemoved(data) {
|
|
284
|
-
if (!_.isArray(data)) {
|
|
285
|
-
data = Array.of(data);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (this[_ctx]) {
|
|
289
|
-
if (this[_ctx]?.state?.process_fields) {
|
|
290
|
-
let returned = await new this[_ctx].state.process_fields().process(
|
|
291
|
-
this[_ctx],
|
|
292
|
-
data,
|
|
293
|
-
this.type,
|
|
294
|
-
);
|
|
295
|
-
return returned;
|
|
296
|
-
} else {
|
|
297
|
-
return data;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return [];
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
createValidationString({ property_name, props, operation, complete }) {
|
|
304
|
-
let operationExempt = {
|
|
305
|
-
PUT: ["required", "unique"],
|
|
306
|
-
};
|
|
307
|
-
let returnVal = [];
|
|
308
|
-
for (let i in props) {
|
|
309
|
-
if (!_.includes(operationExempt[operation] || [], i) || complete) {
|
|
310
|
-
if (i == "required" && props[i] == true) {
|
|
311
|
-
returnVal.push("required");
|
|
312
|
-
}
|
|
313
|
-
if (i == "email" && props[i] == true) {
|
|
314
|
-
returnVal.push("email");
|
|
315
|
-
}
|
|
316
|
-
if (i == "json" && props[i] == true) {
|
|
317
|
-
returnVal.push("json");
|
|
318
|
-
}
|
|
319
|
-
if (i == "ip" && props[i] == true) {
|
|
320
|
-
returnVal.push("ip");
|
|
321
|
-
}
|
|
322
|
-
if (i == "url" && props[i] == true) {
|
|
323
|
-
returnVal.push("url");
|
|
324
|
-
}
|
|
325
|
-
if (i == "date" && props[i] == true) {
|
|
326
|
-
returnVal.push("date");
|
|
327
|
-
}
|
|
328
|
-
if (i == "requires") {
|
|
329
|
-
returnVal.push(`shouldExist:${props[i]}`);
|
|
330
|
-
}
|
|
331
|
-
if (i == "unique" && props[i] == true) {
|
|
332
|
-
returnVal.push(`unique:${this.constructor.name},${property_name}`);
|
|
333
|
-
}
|
|
334
|
-
if (i == "type") {
|
|
335
|
-
switch (props[i]) {
|
|
336
|
-
case String:
|
|
337
|
-
case DataType.STRING: {
|
|
338
|
-
returnVal.push(`string`);
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
case Number:
|
|
342
|
-
case DataType.NUMBER: {
|
|
343
|
-
//returnVal.push(`numeric`);
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
case Boolean:
|
|
347
|
-
case DataType.BOOLEAN: {
|
|
348
|
-
returnVal.push(`in:true,false,True,False,TRUE,FALSE,1,0`);
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
351
|
-
case Date:
|
|
352
|
-
case DataType.DATE: {
|
|
353
|
-
returnVal.push(`date`);
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
case Object:
|
|
357
|
-
case DataType.OBJECT: {
|
|
358
|
-
//returnVal.push(`json`);
|
|
359
|
-
break;
|
|
360
|
-
}
|
|
361
|
-
case Array:
|
|
362
|
-
case DataType.ARRAY: {
|
|
363
|
-
//returnVal.push(`json`);
|
|
364
|
-
break;
|
|
365
|
-
}
|
|
366
|
-
default: {
|
|
367
|
-
returnVal.push(`string`);
|
|
368
|
-
break;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return _.join(returnVal, "|");
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
createFilterString(property, props) {
|
|
378
|
-
let returnVal = [];
|
|
379
|
-
for (let i in props) {
|
|
380
|
-
if (i == "lowercase" && props[i] == true) {
|
|
381
|
-
returnVal.push("lowercase");
|
|
382
|
-
}
|
|
383
|
-
if (i == "trim" && props[i] == true) {
|
|
384
|
-
returnVal.push("trim");
|
|
385
|
-
}
|
|
386
|
-
if (i == "uppercase" && props[i] == true) {
|
|
387
|
-
returnVal.push("uppercase");
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
return _.join(returnVal, "|");
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
getValidationRules(
|
|
394
|
-
method = "POST",
|
|
395
|
-
{ complete = false, pick = [], omit = [] } = {},
|
|
396
|
-
) {
|
|
397
|
-
let rules = {};
|
|
398
|
-
let filters = {};
|
|
399
|
-
let working_properties = this.props;
|
|
400
|
-
if (pick.length > 0) {
|
|
401
|
-
working_properties = _.pick(this.props, pick);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (omit.length > 0) {
|
|
405
|
-
working_properties = _.omit(this.props, omit);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
for (let i in working_properties) {
|
|
409
|
-
let created_rules = this.createValidationString({
|
|
410
|
-
property_name: i,
|
|
411
|
-
props: this.props[i],
|
|
412
|
-
operation: method,
|
|
413
|
-
complete,
|
|
414
|
-
});
|
|
415
|
-
if (created_rules.length > 0) {
|
|
416
|
-
rules[i] = created_rules;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
let created_filter = this.createFilterString(i, this.props[i]);
|
|
420
|
-
if (created_filter.length > 0) {
|
|
421
|
-
filters[i] = created_filter;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return { rules, messages: {}, filters: { before: filters } };
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
makeQueryFromFilter(filter) {
|
|
428
|
-
let return_string = "";
|
|
429
|
-
for (let key in filter) {
|
|
430
|
-
let item_string = "";
|
|
431
|
-
if (filter[key].length > 1) {
|
|
432
|
-
item_string = "(";
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
let item_count = 0;
|
|
436
|
-
|
|
437
|
-
for (let item of filter[key]) {
|
|
438
|
-
if (item_count > 0) {
|
|
439
|
-
item_string = item_string + " OR ";
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
if (this.is_date(item)) {
|
|
443
|
-
let to_index = item.indexOf("|");
|
|
444
|
-
let first = item.substring(0, to_index);
|
|
445
|
-
let second = item.substring(to_index + 1, item.length);
|
|
446
|
-
item_string =
|
|
447
|
-
item_string +
|
|
448
|
-
" " +
|
|
449
|
-
key +
|
|
450
|
-
" BETWEEN '" +
|
|
451
|
-
first +
|
|
452
|
-
"' AND '" +
|
|
453
|
-
second +
|
|
454
|
-
"'";
|
|
455
|
-
} else {
|
|
456
|
-
item_string = item_string + " " + key + " LIKE '" + item + "'";
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
item_count++;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if (filter[key].length > 1) {
|
|
463
|
-
item_string = item_string + ")";
|
|
464
|
-
}
|
|
465
|
-
if (return_string != "") {
|
|
466
|
-
return_string = return_string + " AND ";
|
|
467
|
-
}
|
|
468
|
-
return_string = return_string + item_string;
|
|
469
|
-
}
|
|
470
|
-
return return_string;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
shouldUseCache(resource_type) {
|
|
474
|
-
// If '_skip_cache' property of this object is true, then we shouldn't cache.
|
|
475
|
-
if (this[_skip_cache] == true) {
|
|
476
|
-
return false;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// If the system configuration for spice has a cache status set to "disable",
|
|
480
|
-
// then we shouldn't cache, so return false.
|
|
481
|
-
if (spice.config.cache?.status == "disabled") {
|
|
482
|
-
return false;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// If 'spice.cache[resource_type]' is not undefined,
|
|
486
|
-
// it implies that this resource type is already in the cache or is cacheable.
|
|
487
|
-
return spice.cache[resource_type] != undefined;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
getCacheConfig(resource_type) {
|
|
491
|
-
return spice.cache[resource_type] || {};
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
getCacheProviderObject(resource_type) {
|
|
495
|
-
return spice.cache_providers[
|
|
496
|
-
this.getCacheConfig(resource_type).driver ||
|
|
497
|
-
this.getCacheConfig(resource_type).provider ||
|
|
498
|
-
spice.config.cache.default_driver
|
|
499
|
-
];
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
async exists(item_type, key) {
|
|
503
|
-
let obj = this.getCacheProviderObject(item_type);
|
|
504
|
-
if (obj) return await obj.exists(key);
|
|
505
|
-
return false;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
async shouldForceRefresh(response) {
|
|
509
|
-
let obj = this.getCacheProviderObject(this.type);
|
|
510
|
-
let monitor_record = await obj.get(`monitor::${this.type}`);
|
|
511
|
-
if (monitor_record == undefined) {
|
|
512
|
-
return false;
|
|
513
|
-
}
|
|
514
|
-
if (monitor_record.time > response?.time) {
|
|
515
|
-
return true;
|
|
516
|
-
}
|
|
517
|
-
return false;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
setMonitor() {
|
|
521
|
-
let current_time = new Date().getTime();
|
|
522
|
-
let obj = this.getCacheProviderObject(this.type);
|
|
523
|
-
let key = `monitor::${this.type}`;
|
|
524
|
-
let value = { time: current_time };
|
|
525
|
-
obj.set(key, value, { ttl: 0 });
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
async get(args) {
|
|
529
|
-
// Profiling: use track() for proper async context forking
|
|
530
|
-
const p = this[_ctx]?.profiler;
|
|
531
|
-
|
|
532
|
-
const doGet = async () => {
|
|
533
|
-
if (args.mapping_dept) this[_mapping_dept] = args.mapping_dept;
|
|
534
|
-
if (args.mapping_dept_exempt)
|
|
535
|
-
this[_mapping_dept_exempt] = args.mapping_dept_exempt;
|
|
536
|
-
|
|
537
|
-
if (!args) {
|
|
538
|
-
args = {};
|
|
539
|
-
}
|
|
540
|
-
if (_.isString(args.id)) {
|
|
541
|
-
if (args.skip_hooks !== true) {
|
|
542
|
-
await this.run_hook(args, "get", "before");
|
|
543
|
-
}
|
|
544
|
-
// ⚡ Include columns in cache key if specified
|
|
545
|
-
let key = `get::${this.type}::${args.id}${args.columns ? `::${args.columns}` : ""}`;
|
|
546
|
-
let results = {};
|
|
547
|
-
|
|
548
|
-
// Extract nestings from columns if specified
|
|
549
|
-
const nestings = this.extractNestings(args?.columns || "", this.type);
|
|
550
|
-
|
|
551
|
-
// Build join metadata and prepare columns
|
|
552
|
-
this.buildJoinMetadata(nestings, args);
|
|
553
|
-
let columns = args.columns;
|
|
554
|
-
let columnArray = [];
|
|
555
|
-
if (columns) {
|
|
556
|
-
columns.split(",").forEach((col) => {
|
|
557
|
-
const parts = col.trim().split("."); // remove the first segment
|
|
558
|
-
// Also match if parts[0] is source_property or '`' + source_property + '`'
|
|
559
|
-
const firstPart = parts[0];
|
|
560
|
-
// Remove backticks if present
|
|
561
|
-
const cleanFirstPart =
|
|
562
|
-
(
|
|
563
|
-
firstPart &&
|
|
564
|
-
firstPart.startsWith("`") &&
|
|
565
|
-
firstPart.endsWith("`")
|
|
566
|
-
) ?
|
|
567
|
-
firstPart.slice(1, -1)
|
|
568
|
-
: firstPart;
|
|
569
|
-
|
|
570
|
-
//if (parts.length > 0 && (firstPart === source_property || cleanFirstPart === source_property)) {
|
|
571
|
-
// Use cleanFirstPart for the map key to handle both cases consistently
|
|
572
|
-
// Remove backticks from each column segment, if present, before joining
|
|
573
|
-
columnArray.push(
|
|
574
|
-
parts
|
|
575
|
-
.slice(1)
|
|
576
|
-
.map((segment) =>
|
|
577
|
-
segment && segment.startsWith("`") && segment.endsWith("`") ?
|
|
578
|
-
segment.slice(1, -1)
|
|
579
|
-
: segment,
|
|
580
|
-
)
|
|
581
|
-
.join("."),
|
|
582
|
-
);
|
|
583
|
-
// }
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
if (this.shouldUseCache(this.type)) {
|
|
588
|
-
// Retrieve the cached results
|
|
589
|
-
const cached_results = await this.getCacheProviderObject(
|
|
590
|
-
this.type,
|
|
591
|
-
).get(key);
|
|
592
|
-
// Check if the cache is empty
|
|
593
|
-
const isCacheEmpty = cached_results?.value === undefined;
|
|
594
|
-
// Check if the cached result should be refreshed
|
|
595
|
-
const shouldRefresh = await this.shouldForceRefresh(cached_results);
|
|
596
|
-
|
|
597
|
-
if (isCacheEmpty || shouldRefresh) {
|
|
598
|
-
// Retrieve from the database and update cache
|
|
599
|
-
if (p) {
|
|
600
|
-
results = await p.track(`${this.type}.get.database`, async () => {
|
|
601
|
-
return await this.database.get(args.id, {
|
|
602
|
-
columns: columnArray,
|
|
603
|
-
});
|
|
604
|
-
});
|
|
605
|
-
} else {
|
|
606
|
-
results = await this.database.get(args.id, {
|
|
607
|
-
columns: columnArray,
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
await this.getCacheProviderObject(this.type).set(
|
|
611
|
-
key,
|
|
612
|
-
{ value: results, time: new Date().getTime() },
|
|
613
|
-
this.getCacheConfig(this.type),
|
|
614
|
-
);
|
|
615
|
-
} else {
|
|
616
|
-
// Use the cached value
|
|
617
|
-
results = cached_results.value;
|
|
618
|
-
}
|
|
619
|
-
} else {
|
|
620
|
-
// Directly fetch from the database if caching is disabled
|
|
621
|
-
if (p) {
|
|
622
|
-
results = await p.track(`${this.type}.get.database`, async () => {
|
|
623
|
-
return await this.database.get(args.id, { columns: columnArray });
|
|
624
|
-
});
|
|
625
|
-
} else {
|
|
626
|
-
results = await this.database.get(args.id, {
|
|
627
|
-
columns: columnArray,
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
if (results.type !== undefined && results.type !== this.type) {
|
|
633
|
-
throw new Error(`${this.type} does not exist type`);
|
|
634
|
-
}
|
|
635
|
-
if (results._type !== undefined && results._type !== this.type) {
|
|
636
|
-
throw new Error(`${this.type} does not exist _type`);
|
|
637
|
-
}
|
|
638
|
-
if (results.deleted === undefined || results.deleted === false) {
|
|
639
|
-
if (
|
|
640
|
-
args.skip_read_serialize !== true &&
|
|
641
|
-
args.skip_serialize !== true
|
|
642
|
-
) {
|
|
643
|
-
// ⚡ Pass columns to do_serialize so it can skip irrelevant modifiers
|
|
644
|
-
results = await this.do_serialize(
|
|
645
|
-
results,
|
|
646
|
-
"read",
|
|
647
|
-
{},
|
|
648
|
-
args,
|
|
649
|
-
await this.propsToBeRemoved(results),
|
|
650
|
-
);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// ⚡ OPTIMIZED: Filter results by columns if specified
|
|
654
|
-
if (args.columns) {
|
|
655
|
-
results = this.filterResultsByColumns([results], args.columns)[0];
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
if (args.skip_hooks !== true) {
|
|
659
|
-
await this.run_hook(results, "get", "after");
|
|
660
|
-
}
|
|
661
|
-
return results;
|
|
662
|
-
} else {
|
|
663
|
-
throw new Error(`${this.type} does not exist`);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
};
|
|
667
|
-
|
|
668
|
-
try {
|
|
669
|
-
if (p) {
|
|
670
|
-
return await p.track(`${this.type}.get`, doGet, { id: args?.id });
|
|
671
|
-
}
|
|
672
|
-
return await doGet();
|
|
673
|
-
} catch (e) {
|
|
674
|
-
console.warn(e.message, e);
|
|
675
|
-
throw e;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
async query(query, scope) {
|
|
680
|
-
try {
|
|
681
|
-
let results = await this.database.query(query, scope);
|
|
682
|
-
return results;
|
|
683
|
-
} catch (error) {
|
|
684
|
-
throw error;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
async getMulti(args) {
|
|
689
|
-
// ⚡ Profiling: use track() for proper async context forking
|
|
690
|
-
const p = this[_ctx]?.profiler;
|
|
691
|
-
|
|
692
|
-
const doGetMulti = async () => {
|
|
693
|
-
if (!args) {
|
|
694
|
-
args = {};
|
|
695
|
-
}
|
|
696
|
-
if (args.skip_hooks != true) {
|
|
697
|
-
await this.run_hook(this, "list", "before");
|
|
698
|
-
}
|
|
699
|
-
// PATCH: Prevent empty/null IDs from causing query explosions
|
|
700
|
-
_.remove(args.ids, (o) => o == undefined || o === "" || o === null);
|
|
701
|
-
|
|
702
|
-
// PATCH: Early exit if no valid IDs after filtering
|
|
703
|
-
if (!args.ids || args.ids.length === 0) {
|
|
704
|
-
return []; // No IDs to fetch
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
let key = `multi-get::${this.type}::${_.join(args.ids, "|")}:${args.columns}`;
|
|
708
|
-
let results = [];
|
|
709
|
-
if (args.ids.length > 0) {
|
|
710
|
-
if (this.shouldUseCache(this.type)) {
|
|
711
|
-
let cached_results = await this.getCacheProviderObject(this.type).get(
|
|
712
|
-
key,
|
|
713
|
-
);
|
|
714
|
-
results = cached_results?.value;
|
|
715
|
-
if (
|
|
716
|
-
cached_results?.value === undefined ||
|
|
717
|
-
(await this.shouldForceRefresh(cached_results))
|
|
718
|
-
) {
|
|
719
|
-
results = await this.database.multi_get(args.ids, {
|
|
720
|
-
keep_type: true,
|
|
721
|
-
columns:
|
|
722
|
-
args.columns && args.columns.length > 0 && args.columns != "" ?
|
|
723
|
-
[...this.extractBaseColumns(args.columns), "type"]
|
|
724
|
-
: [],
|
|
725
|
-
});
|
|
726
|
-
this.getCacheProviderObject(this.type).set(
|
|
727
|
-
key,
|
|
728
|
-
{ value: results, time: new Date().getTime() },
|
|
729
|
-
this.getCacheConfig(this.type),
|
|
730
|
-
);
|
|
731
|
-
}
|
|
732
|
-
} else {
|
|
733
|
-
results = await this.database.multi_get(args.ids, {
|
|
734
|
-
keep_type: true,
|
|
735
|
-
columns:
|
|
736
|
-
args.columns && args.columns.length > 0 && args.columns != "" ?
|
|
737
|
-
[...this.extractBaseColumns(args.columns), "type"]
|
|
738
|
-
: [],
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
_.remove(results, (o) => o.type != this.type);
|
|
743
|
-
if (args.skip_read_serialize != true && args.skip_serialize != true) {
|
|
744
|
-
results = await this.do_serialize(
|
|
745
|
-
results,
|
|
746
|
-
"read",
|
|
747
|
-
{},
|
|
748
|
-
args,
|
|
749
|
-
await this.propsToBeRemoved(results),
|
|
750
|
-
);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
if (args.skip_hooks != true) {
|
|
754
|
-
await this.run_hook(results, "list", "after", args.context);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
return results;
|
|
758
|
-
};
|
|
759
|
-
|
|
760
|
-
try {
|
|
761
|
-
if (p) {
|
|
762
|
-
return await p.track(`${this.type}.getMulti`, doGetMulti, {
|
|
763
|
-
ids_count: args?.ids?.length || 0,
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
return await doGetMulti();
|
|
767
|
-
} catch (e) {
|
|
768
|
-
console.warn(e.stack);
|
|
769
|
-
throw e;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
async exist(data) {
|
|
774
|
-
try {
|
|
775
|
-
if (_.isString(data)) {
|
|
776
|
-
let result = await this.database.get(data);
|
|
777
|
-
if (result.type)
|
|
778
|
-
if (result.type != this.type) {
|
|
779
|
-
return false;
|
|
780
|
-
}
|
|
781
|
-
if (result._type)
|
|
782
|
-
if (result._type != this.type) {
|
|
783
|
-
return false;
|
|
784
|
-
}
|
|
785
|
-
} else {
|
|
786
|
-
if (data.type)
|
|
787
|
-
if (data.type != this.type) {
|
|
788
|
-
return false;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
if (data._type)
|
|
792
|
-
if (data._type != this.type) {
|
|
793
|
-
return false;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
return true;
|
|
797
|
-
} catch (e) {
|
|
798
|
-
throw e;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
async update(args) {
|
|
803
|
-
// Profiling: use track() for proper async context forking
|
|
804
|
-
const p = this[_ctx]?.profiler;
|
|
805
|
-
|
|
806
|
-
const doUpdate = async () => {
|
|
807
|
-
this.updated_at = new SDate().now();
|
|
808
|
-
let results = await this.database.get(args.id);
|
|
809
|
-
let item_exist = await this.exist(results);
|
|
810
|
-
if (!item_exist) {
|
|
811
|
-
throw new Error(`${this.type} does not exist. in update`);
|
|
812
|
-
}
|
|
813
|
-
delete results["id"];
|
|
814
|
-
_.defaults(this, results);
|
|
815
|
-
let cover_obj = {
|
|
816
|
-
old: results,
|
|
817
|
-
new: this,
|
|
818
|
-
id: args.id,
|
|
819
|
-
};
|
|
820
|
-
|
|
821
|
-
let form;
|
|
822
|
-
if (args.skip_write_serialize != true && args.skip_serialize != true) {
|
|
823
|
-
cover_obj.new = await this.do_serialize(
|
|
824
|
-
this,
|
|
825
|
-
"write",
|
|
826
|
-
cover_obj.new,
|
|
827
|
-
args,
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
if (args.skip_hooks != true) {
|
|
832
|
-
await this.run_hook(cover_obj, "update", "before", results);
|
|
833
|
-
}
|
|
834
|
-
let db_data = cover_obj.new || this;
|
|
835
|
-
|
|
836
|
-
if (p) {
|
|
837
|
-
await p.track(`${this.type}.update.database`, async () => {
|
|
838
|
-
await this.database.update(args.id, db_data, args._ttl);
|
|
839
|
-
});
|
|
840
|
-
} else {
|
|
841
|
-
await this.database.update(args.id, db_data, args._ttl);
|
|
842
|
-
}
|
|
843
|
-
this.setMonitor();
|
|
844
|
-
|
|
845
|
-
if (args.skip_read_serialize != true && args.skip_serialize != true) {
|
|
846
|
-
cover_obj.new = await this.do_serialize(
|
|
847
|
-
cover_obj.new,
|
|
848
|
-
"read",
|
|
849
|
-
{},
|
|
850
|
-
args,
|
|
851
|
-
await this.propsToBeRemoved(cover_obj.new),
|
|
852
|
-
);
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
if (args.skip_hooks != true) {
|
|
856
|
-
await this.run_hook(
|
|
857
|
-
{
|
|
858
|
-
...this,
|
|
859
|
-
id: args.id,
|
|
860
|
-
},
|
|
861
|
-
"update",
|
|
862
|
-
"after",
|
|
863
|
-
results,
|
|
864
|
-
);
|
|
865
|
-
}
|
|
866
|
-
this.id = args.id;
|
|
867
|
-
return { ...cover_obj.new, id: args.id };
|
|
868
|
-
};
|
|
869
|
-
|
|
870
|
-
try {
|
|
871
|
-
if (p) {
|
|
872
|
-
return await p.track(`${this.type}.update`, doUpdate, { id: args?.id });
|
|
873
|
-
}
|
|
874
|
-
return await doUpdate();
|
|
875
|
-
} catch (e) {
|
|
876
|
-
console.warn("Error on update", e, e.stack);
|
|
877
|
-
throw e;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
async create(args = {}) {
|
|
882
|
-
// Profiling: use track() for proper async context forking
|
|
883
|
-
const p = this[_ctx]?.profiler;
|
|
884
|
-
|
|
885
|
-
const doCreate = async () => {
|
|
886
|
-
let form;
|
|
887
|
-
this.created_at = new SDate().now();
|
|
888
|
-
args = _.defaults(args, { id_prefix: this.type });
|
|
889
|
-
if (args.body) {
|
|
890
|
-
form = _.defaults({}, args.body);
|
|
891
|
-
form.created_at = this.created_at;
|
|
892
|
-
form.updated_at = this.created_at;
|
|
893
|
-
delete form["bucket"];
|
|
894
|
-
}
|
|
895
|
-
let workingForm = form || this;
|
|
896
|
-
this.updated_at = new SDate().now();
|
|
897
|
-
|
|
898
|
-
let id = `${args.id_prefix}-${UUID.v4()}`;
|
|
899
|
-
if (args && args.id) {
|
|
900
|
-
id = args.id;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
if (args.skip_write_serialize != true && args.skip_serialize != true) {
|
|
904
|
-
workingForm = await this.do_serialize(workingForm, "write", {}, args);
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
if (args.skip_hooks != true) {
|
|
908
|
-
await this.run_hook(workingForm, "create", "before");
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
let results;
|
|
912
|
-
if (p) {
|
|
913
|
-
results = await p.track(`${this.type}.create.database`, async () => {
|
|
914
|
-
return await this.database.insert(id, workingForm, args._ttl);
|
|
915
|
-
});
|
|
916
|
-
} else {
|
|
917
|
-
results = await this.database.insert(id, workingForm, args._ttl);
|
|
918
|
-
}
|
|
919
|
-
this.setMonitor();
|
|
920
|
-
|
|
921
|
-
if (args.skip_read_serialize != true && args.skip_serialize != true) {
|
|
922
|
-
results = await this.do_serialize(
|
|
923
|
-
results,
|
|
924
|
-
"read",
|
|
925
|
-
{},
|
|
926
|
-
args,
|
|
927
|
-
await this.propsToBeRemoved(results),
|
|
928
|
-
);
|
|
929
|
-
}
|
|
930
|
-
if (args.skip_hooks != true) {
|
|
931
|
-
await this.run_hook(
|
|
932
|
-
{
|
|
933
|
-
...results,
|
|
934
|
-
id,
|
|
935
|
-
},
|
|
936
|
-
"create",
|
|
937
|
-
"after",
|
|
938
|
-
);
|
|
939
|
-
}
|
|
940
|
-
return { ...results, id };
|
|
941
|
-
};
|
|
942
|
-
|
|
943
|
-
try {
|
|
944
|
-
if (p) {
|
|
945
|
-
return await p.track(`${this.type}.create`, doCreate);
|
|
946
|
-
}
|
|
947
|
-
return await doCreate();
|
|
948
|
-
} catch (e) {
|
|
949
|
-
console.warn(e.stack);
|
|
950
|
-
throw e;
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
async touch(args) {
|
|
955
|
-
let item_exist = await this.exist(args.id);
|
|
956
|
-
|
|
957
|
-
if (!item_exist) {
|
|
958
|
-
throw new Error(`${this.type} does not exist.`);
|
|
959
|
-
}
|
|
960
|
-
try {
|
|
961
|
-
let touch_response = {};
|
|
962
|
-
touch_response = await this.database.touch(args.id, args._ttl);
|
|
963
|
-
return touch_response;
|
|
964
|
-
} catch (e) {
|
|
965
|
-
console.warn(e.stack);
|
|
966
|
-
throw e;
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
async delete(args) {
|
|
971
|
-
// Profiling: use track() for proper async context forking
|
|
972
|
-
const p = this[_ctx]?.profiler;
|
|
973
|
-
|
|
974
|
-
const doDelete = async () => {
|
|
975
|
-
let item_exist = await this.exist(args.id);
|
|
976
|
-
|
|
977
|
-
if (!item_exist) {
|
|
978
|
-
throw new Error(`${this.type} does not exist.`);
|
|
979
|
-
}
|
|
980
|
-
let results = await this.database.get(args.id);
|
|
981
|
-
if (args.skip_hooks != true) {
|
|
982
|
-
await this.run_hook(args, "delete", "before");
|
|
983
|
-
}
|
|
984
|
-
let delete_response = {};
|
|
985
|
-
|
|
986
|
-
const dbOperation = async () => {
|
|
987
|
-
if (args.hard) {
|
|
988
|
-
delete_response = await this.database.delete(args.id);
|
|
989
|
-
this.setMonitor();
|
|
990
|
-
} else {
|
|
991
|
-
delete results["id"];
|
|
992
|
-
results.deleted = true;
|
|
993
|
-
delete_response = await this.database.update(args.id, results);
|
|
994
|
-
}
|
|
995
|
-
};
|
|
996
|
-
|
|
997
|
-
if (p) {
|
|
998
|
-
await p.track(`${this.type}.delete.database`, dbOperation);
|
|
999
|
-
} else {
|
|
1000
|
-
await dbOperation();
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
if (args.skip_hooks != true) {
|
|
1004
|
-
await this.run_hook(results, "delete", "after", results);
|
|
1005
|
-
}
|
|
1006
|
-
return {};
|
|
1007
|
-
};
|
|
1008
|
-
|
|
1009
|
-
try {
|
|
1010
|
-
if (p) {
|
|
1011
|
-
return await p.track(`${this.type}.delete`, doDelete, { id: args?.id });
|
|
1012
|
-
}
|
|
1013
|
-
return await doDelete();
|
|
1014
|
-
} catch (e) {
|
|
1015
|
-
console.warn(e.stack);
|
|
1016
|
-
throw e;
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
toString() {
|
|
1021
|
-
JSON.stringify(this);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
buildArrayProjection(alias, field) {
|
|
1025
|
-
const safeAlias = String(alias).replace(/`/g, "");
|
|
1026
|
-
const safeField = String(field).replace(/`/g, "");
|
|
1027
|
-
return `ARRAY rc.${safeField} FOR rc IN IFMISSINGORNULL(${safeAlias}, []) END AS \`${safeAlias}_${safeField}\``;
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
prepColumns(columns, protectedAliases = [], arrayAliases = []) {
|
|
1031
|
-
if (!columns || columns === "") return columns;
|
|
1032
|
-
|
|
1033
|
-
const q = (s = "") => {
|
|
1034
|
-
const t = (s || "").trim();
|
|
1035
|
-
if (t.startsWith("`") && t.endsWith("`")) return t;
|
|
1036
|
-
return "`" + t.replace(/`/g, "``") + "`";
|
|
1037
|
-
};
|
|
1038
|
-
|
|
1039
|
-
const protectedSet = new Set(protectedAliases);
|
|
1040
|
-
const arraySet = new Set(arrayAliases);
|
|
1041
|
-
|
|
1042
|
-
const tokens = columns.split(",");
|
|
1043
|
-
|
|
1044
|
-
const out = tokens.map((raw) => {
|
|
1045
|
-
let col = (raw || "").trim();
|
|
1046
|
-
if (col === "" || col === "meta().id") return undefined;
|
|
1047
|
-
|
|
1048
|
-
if (/^\s*ARRAY\s+/i.test(col) || /\w+\s*\(/.test(col)) return col;
|
|
1049
|
-
|
|
1050
|
-
const m = col.match(
|
|
1051
|
-
/^\s*`?(\w+)`?\.`?(\w+)`?(?:\s+AS\s+`?([\w]+)`?)?\s*$/i,
|
|
1052
|
-
);
|
|
1053
|
-
if (m) {
|
|
1054
|
-
const alias = m[1];
|
|
1055
|
-
const field = m[2];
|
|
1056
|
-
const explicitAs = m[3];
|
|
1057
|
-
|
|
1058
|
-
// If alias matches this.type, don't prepend again
|
|
1059
|
-
if (alias === this.type) {
|
|
1060
|
-
const qualified = `${q(alias)}.${q(field)}`;
|
|
1061
|
-
return explicitAs ? `${qualified} AS ${q(explicitAs)}` : qualified;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
// Check if alias has a map - if so, simplify to root column only (raw ID)
|
|
1065
|
-
const prop = this.props[alias];
|
|
1066
|
-
if (prop?.map) {
|
|
1067
|
-
// Return base table qualified root column, ignoring the .field part
|
|
1068
|
-
return `${q(this.type)}.${q(alias)}`;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
if (arraySet.has(alias)) {
|
|
1072
|
-
let proj = this.buildArrayProjection(alias, field);
|
|
1073
|
-
if (explicitAs && explicitAs !== `${alias}_${field}`) {
|
|
1074
|
-
proj = proj.replace(/AS\s+`[^`]+`$/i, `AS ${q(explicitAs)}`);
|
|
1075
|
-
}
|
|
1076
|
-
return proj;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
if (protectedSet.has(alias)) {
|
|
1080
|
-
const aliased = `${q(alias)}.${field.startsWith("`") ? field : q(field)}`;
|
|
1081
|
-
return explicitAs ? `${aliased} AS ${q(explicitAs)}` : aliased;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
const qualified = `${q(this.type)}.${q(alias)}.${q(field)}`;
|
|
1085
|
-
return explicitAs ? `${qualified} AS ${q(explicitAs)}` : qualified;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
const looksProtected = [...protectedSet].some(
|
|
1089
|
-
(a) =>
|
|
1090
|
-
col === a ||
|
|
1091
|
-
col === q(a) ||
|
|
1092
|
-
col.startsWith(`${a}.`) ||
|
|
1093
|
-
col.startsWith(`${q(a)}.`),
|
|
1094
|
-
);
|
|
1095
|
-
|
|
1096
|
-
if (!looksProtected) {
|
|
1097
|
-
if (!col.includes(".")) {
|
|
1098
|
-
const fieldQuoted = col.startsWith("`") ? col : q(col);
|
|
1099
|
-
return `${q(this.type)}.${fieldQuoted}`;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
const parts = col.split(".");
|
|
1103
|
-
const safeParts = parts.map((p) => {
|
|
1104
|
-
const t = p.trim();
|
|
1105
|
-
if (t === "" || /\w+\s*\(/.test(t)) return t;
|
|
1106
|
-
return t.startsWith("`") ? t : q(t);
|
|
1107
|
-
});
|
|
1108
|
-
return `${q(this.type)}.${safeParts.join(".")}`;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// For protected aliases (joined resources), return the base table's column (IDs)
|
|
1112
|
-
// instead of the joined resource itself. The JOIN is only for WHERE filtering,
|
|
1113
|
-
// and the serializer will map the IDs later.
|
|
1114
|
-
const unquotedCol = col.replace(/`/g, "");
|
|
1115
|
-
if (protectedSet.has(unquotedCol)) {
|
|
1116
|
-
return `${q(this.type)}.${q(unquotedCol)}`;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
if (!col.includes(".") && !col.startsWith("`")) {
|
|
1120
|
-
return q(col);
|
|
1121
|
-
}
|
|
1122
|
-
return col;
|
|
1123
|
-
});
|
|
1124
|
-
|
|
1125
|
-
// Deduplicate columns (e.g., when multiple mapped dot-notations share the same root)
|
|
1126
|
-
return _.join(_.uniq(_.compact(out)), ",");
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
filterResultsByColumns(data, columns) {
|
|
1130
|
-
if (columns && columns !== "") {
|
|
1131
|
-
// Remove backticks and replace meta().id with id
|
|
1132
|
-
const cleanedColumns = columns
|
|
1133
|
-
.replace(/`/g, "")
|
|
1134
|
-
.replace(/meta\(\)\.id/g, "id");
|
|
1135
|
-
|
|
1136
|
-
// Process each column by splitting on comma and trimming
|
|
1137
|
-
const columnList = cleanedColumns
|
|
1138
|
-
.split(",")
|
|
1139
|
-
.map((col) => col.trim())
|
|
1140
|
-
.filter((col) => col !== "") // PATCH: Remove empty column names
|
|
1141
|
-
.map((col) => {
|
|
1142
|
-
// Check for alias with AS (case-insensitive)
|
|
1143
|
-
const aliasMatch = col.match(/\s+AS\s+([\w]+)/i);
|
|
1144
|
-
if (aliasMatch) {
|
|
1145
|
-
return aliasMatch[1].trim();
|
|
1146
|
-
}
|
|
1147
|
-
// Otherwise, if a dot is present, take the part after the last dot
|
|
1148
|
-
if (col.includes(".")) {
|
|
1149
|
-
return col.split(".").pop().trim();
|
|
1150
|
-
}
|
|
1151
|
-
return col;
|
|
1152
|
-
});
|
|
1153
|
-
|
|
1154
|
-
// Ensure that essential keys are always picked
|
|
1155
|
-
const requiredKeys = ["_permissions", "_permissions_", "id"];
|
|
1156
|
-
const finalKeys = [...new Set([...columnList, ...requiredKeys])];
|
|
1157
|
-
|
|
1158
|
-
return data.map((entry) => _.pick(entry, finalKeys));
|
|
1159
|
-
}
|
|
1160
|
-
return data;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
extractNestings(string, localType) {
|
|
1164
|
-
let returnVal = [];
|
|
1165
|
-
|
|
1166
|
-
// Extract loop variables from ANY/EVERY expressions
|
|
1167
|
-
// Loop variables (e.g., "vote" in "EVERY vote IN committee_votes") should be excluded
|
|
1168
|
-
// But the collection being iterated (e.g., "committee_votes") should be INCLUDED
|
|
1169
|
-
// if it's a joinable mapped property - buildJoinMetadata will filter non-joinables
|
|
1170
|
-
let anyEveryRegex = /(ANY|EVERY)\s+(\w+)\s+IN\s+`?(\w+)`?/gi;
|
|
1171
|
-
let anyEveryMatch;
|
|
1172
|
-
const loopVariables = new Set();
|
|
1173
|
-
const anyEveryCollections = new Set();
|
|
1174
|
-
|
|
1175
|
-
while ((anyEveryMatch = anyEveryRegex.exec(string)) !== null) {
|
|
1176
|
-
loopVariables.add(anyEveryMatch[2]); // e.g., "vote" - exclude from nestings
|
|
1177
|
-
anyEveryCollections.add(anyEveryMatch[3]); // e.g., "committee_votes" - include in nestings
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
// Now extract dot notation patterns, but skip loop variables
|
|
1181
|
-
let regex = /(`?\w+`?)\.(`?\w+`?)/g;
|
|
1182
|
-
let match;
|
|
1183
|
-
|
|
1184
|
-
while ((match = regex.exec(string)) !== null) {
|
|
1185
|
-
let first = match[1].replace(/`/g, "");
|
|
1186
|
-
// Skip if it's the local type or a loop variable from ANY/EVERY
|
|
1187
|
-
if (first !== localType && !loopVariables.has(first)) {
|
|
1188
|
-
returnVal.push(first);
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
// Add collections from ANY/EVERY expressions as potential nestings
|
|
1193
|
-
// buildJoinMetadata will filter out non-joinable ones (embedded arrays vs mapped collections)
|
|
1194
|
-
for (const collection of anyEveryCollections) {
|
|
1195
|
-
if (collection !== localType) {
|
|
1196
|
-
returnVal.push(collection);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
return [...new Set(returnVal)];
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
createJoinSection(mappedNestings) {
|
|
1204
|
-
if (!mappedNestings || mappedNestings.length === 0) return "";
|
|
1205
|
-
|
|
1206
|
-
return mappedNestings
|
|
1207
|
-
.map(({ alias, reference, is_array }) => {
|
|
1208
|
-
const keyspace = fixCollection(reference);
|
|
1209
|
-
if (is_array === true) {
|
|
1210
|
-
return `LEFT NEST \`${keyspace}\` AS \`${alias}\` ON KEYS \`${this.type}\`.\`${alias}\``;
|
|
1211
|
-
} else {
|
|
1212
|
-
return `LEFT JOIN \`${keyspace}\` AS \`${alias}\` ON KEYS \`${this.type}\`.\`${alias}\``;
|
|
1213
|
-
}
|
|
1214
|
-
})
|
|
1215
|
-
.join(" ");
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
formatSortComponent(sortComponent) {
|
|
1219
|
-
const parts = sortComponent.split(" ");
|
|
1220
|
-
parts[0] = `\`${parts[0]}\``;
|
|
1221
|
-
return parts.join(" ");
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
/**
|
|
1225
|
-
* Builds join metadata from nestings for SQL joins and prepares columns.
|
|
1226
|
-
* @param {string[]} nestings - Array of alias names extracted from query/columns/sort
|
|
1227
|
-
* @param {Object} args - Arguments object containing columns (will be mutated with prepared columns)
|
|
1228
|
-
* @returns {Object} - { mappedNestings, protectedAliases, arrayAliases }
|
|
1229
|
-
*/
|
|
1230
|
-
buildJoinMetadata(nestings, args) {
|
|
1231
|
-
// Normalize any double backticks to single backticks early (handles URL encoding issues)
|
|
1232
|
-
if (args.columns && typeof args.columns === "string") {
|
|
1233
|
-
args.columns = args.columns.replace(/``/g, "`");
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// Decide which aliases we can join: only when map.type===MODEL AND reference is a STRING keyspace.
|
|
1237
|
-
const mappedNestings = _.compact(
|
|
1238
|
-
_.uniq(nestings).map((alias) => {
|
|
1239
|
-
const prop = this.props[alias];
|
|
1240
|
-
if (!prop?.map || prop.map.type !== MapType.MODEL) return null;
|
|
1241
|
-
|
|
1242
|
-
const ref = prop.map.reference;
|
|
1243
|
-
if (typeof ref !== "string") {
|
|
1244
|
-
// reference is a class/function/array-of-classes → no SQL join; serializer will handle it
|
|
1245
|
-
return null;
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
const is_array =
|
|
1249
|
-
prop.type === "array" ||
|
|
1250
|
-
prop.type === Array ||
|
|
1251
|
-
prop.type === DataType.ARRAY;
|
|
1252
|
-
|
|
1253
|
-
return {
|
|
1254
|
-
alias,
|
|
1255
|
-
reference: ref.toLowerCase(), // keyspace to join
|
|
1256
|
-
is_array,
|
|
1257
|
-
type: prop.type,
|
|
1258
|
-
value_field: prop.map.value_field,
|
|
1259
|
-
destination: prop.map.destination || alias,
|
|
1260
|
-
};
|
|
1261
|
-
}),
|
|
1262
|
-
);
|
|
1263
|
-
|
|
1264
|
-
const protectedAliases = mappedNestings.map((m) => m.alias);
|
|
1265
|
-
const arrayAliases = mappedNestings
|
|
1266
|
-
.filter((m) => m.is_array)
|
|
1267
|
-
.map((m) => m.alias);
|
|
1268
|
-
|
|
1269
|
-
// Columns: first prepare (prefix base table, rewrite array alias.field → ARRAY proj),
|
|
1270
|
-
// then normalize names and add default aliases
|
|
1271
|
-
//console.log("Columns in BuildJoinMetadata", args.columns);
|
|
1272
|
-
|
|
1273
|
-
this[_columns] = args.columns ?? "";
|
|
1274
|
-
|
|
1275
|
-
args.columns = this.prepColumns(
|
|
1276
|
-
args.columns,
|
|
1277
|
-
protectedAliases,
|
|
1278
|
-
arrayAliases,
|
|
1279
|
-
);
|
|
1280
|
-
|
|
1281
|
-
args.columns = this.fixColumnName(args.columns, protectedAliases);
|
|
1282
|
-
//console.log("Columns in BuildJoinMetadata after fixColumnName", args.columns);
|
|
1283
|
-
return { mappedNestings, protectedAliases, arrayAliases };
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
/* removeSpaceAndSpecialCharacters(str) {
|
|
1287
|
-
return str.replace(/[^a-zA-Z0-9]/g, "");
|
|
1288
|
-
} */
|
|
1289
|
-
|
|
1290
|
-
fixColumnName(columns, protectedAliases = []) {
|
|
1291
|
-
if (!columns || typeof columns !== "string") return columns;
|
|
1292
|
-
const protectedSet = new Set(protectedAliases);
|
|
1293
|
-
|
|
1294
|
-
const tokens = columns.split(",").map((s) => s.trim());
|
|
1295
|
-
const aliasTracker = {};
|
|
1296
|
-
|
|
1297
|
-
const out = tokens.map((col) => {
|
|
1298
|
-
if (!col) return undefined;
|
|
1299
|
-
|
|
1300
|
-
// Do not rewrite ARRAY projections
|
|
1301
|
-
if (/^\s*ARRAY\s+/i.test(col)) return col;
|
|
1302
|
-
|
|
1303
|
-
// If token is literally this.type.alias → compress to `alias`
|
|
1304
|
-
for (const a of protectedSet) {
|
|
1305
|
-
const re = new RegExp(
|
|
1306
|
-
"^`?" +
|
|
1307
|
-
_.escapeRegExp(this.type) +
|
|
1308
|
-
"`?\\.`?" +
|
|
1309
|
-
_.escapeRegExp(a) +
|
|
1310
|
-
"`?$",
|
|
1311
|
-
);
|
|
1312
|
-
if (re.test(col)) {
|
|
1313
|
-
return `\`${a}\``;
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
// Extract explicit AS
|
|
1318
|
-
const aliasRegex = /\s+AS\s+`?([\w]+)`?$/i;
|
|
1319
|
-
const aliasMatch = col.match(aliasRegex);
|
|
1320
|
-
const explicitAlias = aliasMatch ? aliasMatch[1] : null;
|
|
1321
|
-
const colWithoutAlias =
|
|
1322
|
-
explicitAlias ? col.replace(aliasRegex, "").trim() : col;
|
|
1323
|
-
|
|
1324
|
-
// `table`.`col` or table.col
|
|
1325
|
-
const columnRegex = /^`?([\w]+)`?\.`?([\w]+)`?$/;
|
|
1326
|
-
const match = colWithoutAlias.match(columnRegex);
|
|
1327
|
-
|
|
1328
|
-
let tableName = this.type;
|
|
1329
|
-
let columnName = colWithoutAlias.replace(/`/g, "");
|
|
1330
|
-
if (match) {
|
|
1331
|
-
tableName = match[1];
|
|
1332
|
-
columnName = match[2];
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
// For joined table columns, add default alias <table_col> if none
|
|
1336
|
-
if (tableName && tableName !== this.type) {
|
|
1337
|
-
let newAlias = explicitAlias || `${tableName}`;
|
|
1338
|
-
if (!explicitAlias) {
|
|
1339
|
-
if (aliasTracker.hasOwnProperty(newAlias)) {
|
|
1340
|
-
aliasTracker[newAlias]++;
|
|
1341
|
-
newAlias = `${newAlias}`;
|
|
1342
|
-
} else {
|
|
1343
|
-
aliasTracker[newAlias] = 0;
|
|
1344
|
-
}
|
|
1345
|
-
return `${colWithoutAlias} AS \`${newAlias}\``;
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
// If column is already qualified with base table (e.g., `user`.`field`), return as-is
|
|
1350
|
-
if (tableName === this.type && match) {
|
|
1351
|
-
return col;
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
// If column is already backtick-quoted, return as-is
|
|
1355
|
-
if (col.startsWith("`") && col.endsWith("`")) {
|
|
1356
|
-
return col;
|
|
1357
|
-
}
|
|
1358
|
-
return `\`${col}\``;
|
|
1359
|
-
});
|
|
1360
|
-
|
|
1361
|
-
return _.join(_.compact(out), ", ");
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
async list(args = {}) {
|
|
1365
|
-
// Profiling: use track() for proper async context forking
|
|
1366
|
-
const p = this[_ctx]?.profiler;
|
|
1367
|
-
|
|
1368
|
-
const doList = async () => {
|
|
1369
|
-
if (args.mapping_dept) this[_mapping_dept] = args.mapping_dept;
|
|
1370
|
-
if (args.mapping_dept_exempt)
|
|
1371
|
-
this[_mapping_dept_exempt] = args.mapping_dept_exempt;
|
|
1372
|
-
|
|
1373
|
-
// Find alias tokens from query/columns/sort
|
|
1374
|
-
const nestings = [
|
|
1375
|
-
...this.extractNestings(args?.query || "", this.type),
|
|
1376
|
-
//...this.extractNestings(args?.columns || "", this.type),
|
|
1377
|
-
...this.extractNestings(args?.sort || "", this.type),
|
|
1378
|
-
];
|
|
1379
|
-
|
|
1380
|
-
// Build join metadata and prepare columns
|
|
1381
|
-
const { mappedNestings } = this.buildJoinMetadata(nestings, args);
|
|
1382
|
-
|
|
1383
|
-
// Build JOIN/NEST from the mapped keyspaces
|
|
1384
|
-
args._join = this.createJoinSection(mappedNestings);
|
|
1385
|
-
|
|
1386
|
-
// WHERE
|
|
1387
|
-
let query = "";
|
|
1388
|
-
const deletedCondition = `(\`${this.type}\`.deleted = false OR \`${this.type}\`.deleted IS MISSING)`;
|
|
1389
|
-
if (args.is_full_text === "true" || args.is_custom_query === "true") {
|
|
1390
|
-
query = args.query || "";
|
|
1391
|
-
} else if (args.filters) {
|
|
1392
|
-
query = this.makeQueryFromFilter(args.filters);
|
|
1393
|
-
} else if (args.query) {
|
|
1394
|
-
query = `${args.query} AND ${deletedCondition}`;
|
|
1395
|
-
} else {
|
|
1396
|
-
query = deletedCondition;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
if (hasSQLInjection(query)) return [];
|
|
1400
|
-
|
|
1401
|
-
// LIMIT/OFFSET/SORT
|
|
1402
|
-
args.limit = Number(args.limit) || undefined;
|
|
1403
|
-
args.offset = Number(args.offset) || 0;
|
|
1404
|
-
args.sort =
|
|
1405
|
-
args.sort ?
|
|
1406
|
-
args.sort
|
|
1407
|
-
.split(",")
|
|
1408
|
-
.map((item) =>
|
|
1409
|
-
item.includes(".") ? item : (
|
|
1410
|
-
`\`${this.type}\`.${this.formatSortComponent(item)}`
|
|
1411
|
-
),
|
|
1412
|
-
)
|
|
1413
|
-
.join(",")
|
|
1414
|
-
: `\`${this.type}\`.created_at DESC`;
|
|
1415
|
-
|
|
1416
|
-
if (args.skip_hooks !== true) {
|
|
1417
|
-
await this.run_hook(this, "list", "before");
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
const cacheKey = `list::${this.type}::${args._join}::${query}::${args.limit}::${args.offset}::${args.sort}::${args.do_count}::${args.statement_consistent}::${args.columns}::${args.is_full_text}::${args.is_custom_query}`;
|
|
1421
|
-
let results;
|
|
1422
|
-
|
|
1423
|
-
if (this.shouldUseCache(this.type)) {
|
|
1424
|
-
const cached = await this.getCacheProviderObject(this.type).get(
|
|
1425
|
-
cacheKey,
|
|
1426
|
-
);
|
|
1427
|
-
results = cached?.value;
|
|
1428
|
-
const isEmpty = cached?.value === undefined;
|
|
1429
|
-
const refresh = await this.shouldForceRefresh(cached);
|
|
1430
|
-
if (isEmpty || refresh) {
|
|
1431
|
-
results = await this.fetchResults(args, query);
|
|
1432
|
-
this.getCacheProviderObject(this.type).set(
|
|
1433
|
-
cacheKey,
|
|
1434
|
-
{ value: results, time: new Date().getTime() },
|
|
1435
|
-
this.getCacheConfig(this.type),
|
|
1436
|
-
);
|
|
1437
|
-
}
|
|
1438
|
-
} else {
|
|
1439
|
-
results = await this.fetchResults(args, query);
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
// Serializer still handles class-based refs and value_field
|
|
1443
|
-
if (args.skip_read_serialize !== true && args.skip_serialize !== true) {
|
|
1444
|
-
results.data = await this.do_serialize(
|
|
1445
|
-
results.data,
|
|
1446
|
-
"read",
|
|
1447
|
-
{},
|
|
1448
|
-
args,
|
|
1449
|
-
await this.propsToBeRemoved(results.data),
|
|
1450
|
-
);
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
if (args.skip_hooks !== true) {
|
|
1454
|
-
await this.run_hook(results.data, "list", "after");
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
results.data = this.filterResultsByColumns(results.data, args.columns);
|
|
1458
|
-
//console.log("results.data", results.data);
|
|
1459
|
-
return results;
|
|
1460
|
-
};
|
|
1461
|
-
|
|
1462
|
-
try {
|
|
1463
|
-
if (p) {
|
|
1464
|
-
return await p.track(`${this.type}.list`, doList, {
|
|
1465
|
-
limit: args?.limit,
|
|
1466
|
-
offset: args?.offset,
|
|
1467
|
-
});
|
|
1468
|
-
}
|
|
1469
|
-
return await doList();
|
|
1470
|
-
} catch (e) {
|
|
1471
|
-
console.warn(e.stack);
|
|
1472
|
-
throw e;
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
async fetchResults(args, query) {
|
|
1477
|
-
// Profiling: use track() for proper async context forking
|
|
1478
|
-
const p = this[_ctx]?.profiler;
|
|
1479
|
-
|
|
1480
|
-
const doFetch = async () => {
|
|
1481
|
-
if (args.is_custom_query === "true" && args.ids.length > 0) {
|
|
1482
|
-
return await this.database.query(query);
|
|
1483
|
-
} else if (args.is_full_text === "true") {
|
|
1484
|
-
return await this.database.full_text_search(
|
|
1485
|
-
this.type,
|
|
1486
|
-
query || "",
|
|
1487
|
-
args.limit,
|
|
1488
|
-
args.offset,
|
|
1489
|
-
args._join,
|
|
1490
|
-
);
|
|
1491
|
-
} else {
|
|
1492
|
-
let result = await this.database.search(
|
|
1493
|
-
this.type,
|
|
1494
|
-
args.columns || "",
|
|
1495
|
-
query || "",
|
|
1496
|
-
args.limit,
|
|
1497
|
-
args.offset,
|
|
1498
|
-
args.sort,
|
|
1499
|
-
args.do_count,
|
|
1500
|
-
args.statement_consistent,
|
|
1501
|
-
args._join,
|
|
1502
|
-
);
|
|
1503
|
-
return result;
|
|
1504
|
-
}
|
|
1505
|
-
};
|
|
1506
|
-
|
|
1507
|
-
if (p) {
|
|
1508
|
-
return await p.track(`${this.type}.fetchResults`, doFetch);
|
|
1509
|
-
}
|
|
1510
|
-
return await doFetch();
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
addHook({ operation, when, execute }) {
|
|
1514
|
-
this[_hooks][operation][when].push(execute);
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
async run_hook(data, op, when, old_data) {
|
|
1518
|
-
try {
|
|
1519
|
-
if (this[_disable_lifecycle_events] == false) {
|
|
1520
|
-
let resourceLifecycleTriggered = new ResourceLifecycleTriggered({
|
|
1521
|
-
data: {
|
|
1522
|
-
data,
|
|
1523
|
-
operation: op,
|
|
1524
|
-
when,
|
|
1525
|
-
old_data,
|
|
1526
|
-
resource: this.type,
|
|
1527
|
-
ctx: this[_ctx],
|
|
1528
|
-
},
|
|
1529
|
-
});
|
|
1530
|
-
resourceLifecycleTriggered.dispatch();
|
|
1531
|
-
}
|
|
1532
|
-
if (this[_hooks] && this[_hooks][op] && this[_hooks][op][when]) {
|
|
1533
|
-
for (let i of this[_hooks][op][when]) {
|
|
1534
|
-
data = await i(data, old_data);
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
return data;
|
|
1538
|
-
} catch (e) {
|
|
1539
|
-
throw e;
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
shouldSerializerRun(args, type) {
|
|
1544
|
-
if (args) {
|
|
1545
|
-
if (args.skip_serialize == true) {
|
|
1546
|
-
return false;
|
|
1547
|
-
}
|
|
1548
|
-
if (type == "read") {
|
|
1549
|
-
if (args.skip_read_serialize == true) {
|
|
1550
|
-
return false;
|
|
1551
|
-
}
|
|
1552
|
-
} else if (type == "write") {
|
|
1553
|
-
if (args.skip_write_serialize == true) {
|
|
1554
|
-
return false;
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
return true;
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
// Check if a field is exempt from mapping depth limits
|
|
1562
|
-
// Supports deep references like "group.permissions"
|
|
1563
|
-
isFieldExempt(source_property) {
|
|
1564
|
-
const currentPath = this[_current_path];
|
|
1565
|
-
const fullPath =
|
|
1566
|
-
currentPath ? `${currentPath}.${source_property}` : source_property;
|
|
1567
|
-
|
|
1568
|
-
return this[_mapping_dept_exempt].some((exemptPattern) => {
|
|
1569
|
-
// Exact match: "group" or "group.permissions"
|
|
1570
|
-
if (exemptPattern === source_property || exemptPattern === fullPath) {
|
|
1571
|
-
return true;
|
|
1572
|
-
}
|
|
1573
|
-
// Pattern starts with current full path (e.g., "group.permissions" starts with "group")
|
|
1574
|
-
if (exemptPattern.startsWith(fullPath + ".")) {
|
|
1575
|
-
return true;
|
|
1576
|
-
}
|
|
1577
|
-
// Full path starts with pattern (e.g., we're at "group.permissions" and "group" is exempt)
|
|
1578
|
-
if (fullPath.startsWith(exemptPattern + ".")) {
|
|
1579
|
-
return true;
|
|
1580
|
-
}
|
|
1581
|
-
return false;
|
|
1582
|
-
});
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
async mapToObject(
|
|
1586
|
-
data,
|
|
1587
|
-
Class,
|
|
1588
|
-
source_property,
|
|
1589
|
-
store_property,
|
|
1590
|
-
property,
|
|
1591
|
-
args,
|
|
1592
|
-
type,
|
|
1593
|
-
mapping_dept,
|
|
1594
|
-
level,
|
|
1595
|
-
columns,
|
|
1596
|
-
) {
|
|
1597
|
-
// ⚡ Get profiler for proper async context forking
|
|
1598
|
-
const p = this[_ctx]?.profiler;
|
|
1599
|
-
// create a array of all columns in args.columns skipping the first_segment of the Column string and pull out all the columns of where the new first segment matches source_property
|
|
1600
|
-
// Create a set of columns where the first segment matches source_property
|
|
1601
|
-
let columnArray = [];
|
|
1602
|
-
if (columns) {
|
|
1603
|
-
columns.split(",").forEach((col) => {
|
|
1604
|
-
const parts = col.trim().split("."); // remove the first segment
|
|
1605
|
-
// Also match if parts[0] is source_property or '`' + source_property + '`'
|
|
1606
|
-
const firstPart = parts[0];
|
|
1607
|
-
// Remove backticks if present
|
|
1608
|
-
const cleanFirstPart =
|
|
1609
|
-
firstPart && firstPart.startsWith("`") && firstPart.endsWith("`") ?
|
|
1610
|
-
firstPart.slice(1, -1)
|
|
1611
|
-
: firstPart;
|
|
1612
|
-
|
|
1613
|
-
if (
|
|
1614
|
-
parts.length > 0 &&
|
|
1615
|
-
(firstPart === source_property || cleanFirstPart === source_property)
|
|
1616
|
-
) {
|
|
1617
|
-
// Use cleanFirstPart for the map key to handle both cases consistently
|
|
1618
|
-
// Remove backticks from each column segment, if present, before joining
|
|
1619
|
-
columnArray.push(
|
|
1620
|
-
parts
|
|
1621
|
-
.slice(1)
|
|
1622
|
-
.map((segment) =>
|
|
1623
|
-
segment && segment.startsWith("`") && segment.endsWith("`") ?
|
|
1624
|
-
segment.slice(1, -1)
|
|
1625
|
-
: segment,
|
|
1626
|
-
)
|
|
1627
|
-
.join("."),
|
|
1628
|
-
);
|
|
1629
|
-
}
|
|
1630
|
-
});
|
|
1631
|
-
}
|
|
1632
|
-
let original_is_array = _.isArray(data);
|
|
1633
|
-
if (!original_is_array) {
|
|
1634
|
-
data = Array.of(data);
|
|
1635
|
-
}
|
|
1636
|
-
const isExempt = this.isFieldExempt(source_property);
|
|
1637
|
-
if (isExempt || this[_level] + 1 < this[_mapping_dept]) {
|
|
1638
|
-
let classes = _.compact(_.isArray(Class) ? Class : [Class]);
|
|
1639
|
-
|
|
1640
|
-
let ids = [];
|
|
1641
|
-
_.each(data, (result) => {
|
|
1642
|
-
let value = result[source_property];
|
|
1643
|
-
|
|
1644
|
-
// Check if value is in the empty string key (column aliasing issue)
|
|
1645
|
-
if (
|
|
1646
|
-
(value === undefined || (_.isObject(value) && _.isEmpty(value))) &&
|
|
1647
|
-
result[""] &&
|
|
1648
|
-
result[""][source_property]
|
|
1649
|
-
) {
|
|
1650
|
-
value = result[""][source_property];
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
if (_.isString(value) && value !== "") {
|
|
1654
|
-
// PATCH: Use strict inequality
|
|
1655
|
-
// Value is a raw ID string
|
|
1656
|
-
ids = _.union(ids, [value]);
|
|
1657
|
-
} else if (
|
|
1658
|
-
_.isObject(value) &&
|
|
1659
|
-
_.isString(value.id) &&
|
|
1660
|
-
value.id !== ""
|
|
1661
|
-
) {
|
|
1662
|
-
// PATCH: Use strict inequality
|
|
1663
|
-
// Value is already a joined object with an id field
|
|
1664
|
-
ids = _.union(ids, [value.id]);
|
|
1665
|
-
}
|
|
1666
|
-
});
|
|
1667
|
-
|
|
1668
|
-
// PATCH: Remove empty strings, nulls, and deduplicate IDs
|
|
1669
|
-
ids = _.compact(_.uniq(ids));
|
|
1670
|
-
|
|
1671
|
-
// PATCH: Early exit if no valid IDs
|
|
1672
|
-
if (ids.length === 0) {
|
|
1673
|
-
return original_is_array ? data : data[0];
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
// Build the path for child models
|
|
1677
|
-
const childPath =
|
|
1678
|
-
this[_current_path] ?
|
|
1679
|
-
`${this[_current_path]}.${source_property}`
|
|
1680
|
-
: source_property;
|
|
1681
|
-
|
|
1682
|
-
// ⚡ Wrap in profiler track() to ensure proper async context for child operations
|
|
1683
|
-
const fetchRelated = async () => {
|
|
1684
|
-
return await Promise.allSettled(
|
|
1685
|
-
_.map(classes, (obj) => {
|
|
1686
|
-
let objInstance = new obj({
|
|
1687
|
-
...this[_args],
|
|
1688
|
-
skip_cache: this[_skip_cache],
|
|
1689
|
-
_level: this[_level] + 1,
|
|
1690
|
-
mapping_dept: this[_mapping_dept],
|
|
1691
|
-
mapping_dept_exempt: this[_mapping_dept_exempt],
|
|
1692
|
-
_columns: columnArray.join(","),
|
|
1693
|
-
_current_path: childPath,
|
|
1694
|
-
});
|
|
1695
|
-
|
|
1696
|
-
return objInstance.getMulti({
|
|
1697
|
-
skip_hooks: true,
|
|
1698
|
-
ids: ids,
|
|
1699
|
-
columns: columnArray,
|
|
1700
|
-
});
|
|
1701
|
-
}),
|
|
1702
|
-
);
|
|
1703
|
-
};
|
|
1704
|
-
|
|
1705
|
-
var returned_all;
|
|
1706
|
-
if (p && ids.length > 0) {
|
|
1707
|
-
returned_all = await p.track(
|
|
1708
|
-
`${this.type}.map.${source_property}`,
|
|
1709
|
-
fetchRelated,
|
|
1710
|
-
{ ids_count: ids.length },
|
|
1711
|
-
);
|
|
1712
|
-
} else {
|
|
1713
|
-
returned_all = await fetchRelated();
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
let ug = _.flatten(
|
|
1717
|
-
_.compact(
|
|
1718
|
-
_.map(returned_all, (returned_obj) => {
|
|
1719
|
-
if (returned_obj.status == "fulfilled") {
|
|
1720
|
-
return returned_obj.value;
|
|
1721
|
-
}
|
|
1722
|
-
}),
|
|
1723
|
-
),
|
|
1724
|
-
);
|
|
1725
|
-
|
|
1726
|
-
/* if(source_property == "group") {
|
|
1727
|
-
console.log("Returned All", source_property, store_property, ug);
|
|
1728
|
-
} */
|
|
1729
|
-
|
|
1730
|
-
data = _.map(data, (result) => {
|
|
1731
|
-
let sourceValue = result[source_property];
|
|
1732
|
-
|
|
1733
|
-
// Check if value is in the empty string key (column aliasing issue)
|
|
1734
|
-
if (
|
|
1735
|
-
(sourceValue === undefined ||
|
|
1736
|
-
(_.isObject(sourceValue) && _.isEmpty(sourceValue))) &&
|
|
1737
|
-
result[""] &&
|
|
1738
|
-
result[""][source_property]
|
|
1739
|
-
) {
|
|
1740
|
-
sourceValue = result[""][source_property];
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
// Get the ID to match against - either a string ID or the id property of an object
|
|
1744
|
-
const sourceId =
|
|
1745
|
-
_.isString(sourceValue) ? sourceValue
|
|
1746
|
-
: _.isObject(sourceValue) ? sourceValue.id
|
|
1747
|
-
: null;
|
|
1748
|
-
// PATCH: Use strict equality to prevent type coercion issues
|
|
1749
|
-
let result_found =
|
|
1750
|
-
_.find(ug, (g) => {
|
|
1751
|
-
return g.id === sourceId && sourceId !== null && sourceId !== "";
|
|
1752
|
-
}) || {};
|
|
1753
|
-
result[store_property] = result_found;
|
|
1754
|
-
return result;
|
|
1755
|
-
});
|
|
1756
|
-
}
|
|
1757
|
-
return original_is_array ? data : data[0];
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
async mapToObjectArray(
|
|
1761
|
-
data,
|
|
1762
|
-
Class,
|
|
1763
|
-
source_property,
|
|
1764
|
-
store_property,
|
|
1765
|
-
property,
|
|
1766
|
-
args,
|
|
1767
|
-
type,
|
|
1768
|
-
mapping_dept,
|
|
1769
|
-
level,
|
|
1770
|
-
columns,
|
|
1771
|
-
) {
|
|
1772
|
-
// ⚡ Get profiler for proper async context forking
|
|
1773
|
-
const p = this[_ctx]?.profiler;
|
|
1774
|
-
|
|
1775
|
-
let columnArray = [];
|
|
1776
|
-
if (columns) {
|
|
1777
|
-
columns.split(",").forEach((col) => {
|
|
1778
|
-
const parts = col.trim().split("."); // remove the first segment
|
|
1779
|
-
// Also match if parts[0] is source_property or '`' + source_property + '`'
|
|
1780
|
-
const firstPart = parts[0];
|
|
1781
|
-
// Remove backticks if present
|
|
1782
|
-
const cleanFirstPart =
|
|
1783
|
-
firstPart && firstPart.startsWith("`") && firstPart.endsWith("`") ?
|
|
1784
|
-
firstPart.slice(1, -1)
|
|
1785
|
-
: firstPart;
|
|
1786
|
-
|
|
1787
|
-
if (
|
|
1788
|
-
parts.length > 0 &&
|
|
1789
|
-
(firstPart === source_property || cleanFirstPart === source_property)
|
|
1790
|
-
) {
|
|
1791
|
-
// Use cleanFirstPart for the map key to handle both cases consistently
|
|
1792
|
-
// Remove backticks from each column segment, if present, before joining
|
|
1793
|
-
columnArray.push(
|
|
1794
|
-
parts
|
|
1795
|
-
.slice(1)
|
|
1796
|
-
.map((segment) =>
|
|
1797
|
-
segment && segment.startsWith("`") && segment.endsWith("`") ?
|
|
1798
|
-
segment.slice(1, -1)
|
|
1799
|
-
: segment,
|
|
1800
|
-
)
|
|
1801
|
-
.join("."),
|
|
1802
|
-
);
|
|
1803
|
-
}
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
let original_is_array = _.isArray(data);
|
|
1808
|
-
if (!original_is_array) {
|
|
1809
|
-
data = Array.of(data);
|
|
1810
|
-
}
|
|
1811
|
-
const isExempt = this.isFieldExempt(source_property);
|
|
1812
|
-
if (isExempt || this[_level] + 1 < this[_mapping_dept]) {
|
|
1813
|
-
let ids = [];
|
|
1814
|
-
_.each(data, (result) => {
|
|
1815
|
-
let value = [];
|
|
1816
|
-
|
|
1817
|
-
if (_.isArray(result[source_property])) {
|
|
1818
|
-
value = result[source_property];
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
if (_.isString(result[source_property])) {
|
|
1822
|
-
value = [result[source_property]];
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
// Extract IDs - handle both string IDs and objects with id property
|
|
1826
|
-
let items = _.compact(
|
|
1827
|
-
_.map(value, (obj) => {
|
|
1828
|
-
if (_.isString(obj) && obj !== "") {
|
|
1829
|
-
// PATCH: Use strict inequality
|
|
1830
|
-
return obj;
|
|
1831
|
-
} else if (_.isObject(obj) && _.isString(obj.id) && obj.id !== "") {
|
|
1832
|
-
// PATCH: Use strict inequality
|
|
1833
|
-
return obj.id;
|
|
1834
|
-
}
|
|
1835
|
-
return null;
|
|
1836
|
-
}),
|
|
1837
|
-
);
|
|
1838
|
-
ids = _.union(ids, items);
|
|
1839
|
-
});
|
|
1840
|
-
|
|
1841
|
-
// PATCH: Deduplicate IDs to prevent redundant queries
|
|
1842
|
-
ids = _.uniq(ids);
|
|
1843
|
-
|
|
1844
|
-
// PATCH: Early exit if no valid items
|
|
1845
|
-
if (ids.length === 0) {
|
|
1846
|
-
return original_is_array ? data : data[0];
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
// Build the path for child models
|
|
1850
|
-
const childPath =
|
|
1851
|
-
this[_current_path] ?
|
|
1852
|
-
`${this[_current_path]}.${source_property}`
|
|
1853
|
-
: source_property;
|
|
1854
|
-
|
|
1855
|
-
let classes = _.compact(_.isArray(Class) ? Class : [Class]);
|
|
1856
|
-
|
|
1857
|
-
// ⚡ Wrap in profiler track() to ensure proper async context for child operations
|
|
1858
|
-
const fetchRelated = async () => {
|
|
1859
|
-
return await Promise.allSettled(
|
|
1860
|
-
_.map(classes, (obj) => {
|
|
1861
|
-
return new obj({
|
|
1862
|
-
...this[_args],
|
|
1863
|
-
skip_cache: this[_skip_cache],
|
|
1864
|
-
_level: this[_level] + 1,
|
|
1865
|
-
mapping_dept: this[_mapping_dept],
|
|
1866
|
-
mapping_dept_exempt: this[_mapping_dept_exempt],
|
|
1867
|
-
_columns: columnArray.join(","),
|
|
1868
|
-
_current_path: childPath,
|
|
1869
|
-
}).getMulti({
|
|
1870
|
-
skip_hooks: true,
|
|
1871
|
-
ids: ids,
|
|
1872
|
-
columns: columnArray,
|
|
1873
|
-
});
|
|
1874
|
-
}),
|
|
1875
|
-
);
|
|
1876
|
-
};
|
|
1877
|
-
|
|
1878
|
-
var returned_all;
|
|
1879
|
-
if (p && ids.length > 0) {
|
|
1880
|
-
returned_all = await p.track(
|
|
1881
|
-
`${this.type}.mapArray.${source_property}`,
|
|
1882
|
-
fetchRelated,
|
|
1883
|
-
{ ids_count: ids.length },
|
|
1884
|
-
);
|
|
1885
|
-
} else {
|
|
1886
|
-
returned_all = await fetchRelated();
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
var returned_objects = _.flatten(
|
|
1890
|
-
_.compact(
|
|
1891
|
-
_.map(returned_all, (returned_obj) => {
|
|
1892
|
-
if (returned_obj.status == "fulfilled") return returned_obj.value;
|
|
1893
|
-
}),
|
|
1894
|
-
),
|
|
1895
|
-
);
|
|
1896
|
-
|
|
1897
|
-
_.each(data, (result) => {
|
|
1898
|
-
if (_.isString(result[store_property])) {
|
|
1899
|
-
result[store_property] = [result[store_property]];
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
if (!_.has(result, source_property)) {
|
|
1903
|
-
result[source_property] = [];
|
|
1904
|
-
return;
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
result[store_property] = _.map(result[source_property], (item) => {
|
|
1908
|
-
// Get the ID to match - either a string ID or the id property of an object
|
|
1909
|
-
const itemId =
|
|
1910
|
-
_.isString(item) ? item
|
|
1911
|
-
: _.isObject(item) ? item.id
|
|
1912
|
-
: null;
|
|
1913
|
-
return _.find(returned_objects, (p) => p.id === itemId);
|
|
1914
|
-
});
|
|
1915
|
-
result[store_property] = _.reject(
|
|
1916
|
-
result[store_property],
|
|
1917
|
-
(obj) => obj === null || obj === undefined,
|
|
1918
|
-
);
|
|
1919
|
-
});
|
|
1920
|
-
}
|
|
1921
|
-
return original_is_array ? data : data[0];
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
addModifier({ when, execute }) {
|
|
1925
|
-
if (this[_serializers][when]) {
|
|
1926
|
-
this[_serializers][when]["modifiers"].push(execute);
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
createModifiersFromProps() {
|
|
1931
|
-
this.createMofifier(this.props);
|
|
1932
|
-
this.addExternalModifiers();
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
addExternalModifiers(scope = "*") {
|
|
1936
|
-
let that = this;
|
|
1937
|
-
_.each([...spice.getModifiers(scope)], function (modifier) {
|
|
1938
|
-
that.addModifier(modifier);
|
|
1939
|
-
});
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
/**
|
|
1943
|
-
* Extracts the first segment of each column path for database queries.
|
|
1944
|
-
* e.g., ["name", "permissions.permission"] → ["name", "permissions"]
|
|
1945
|
-
* @param {string[]} columns - Array of column paths
|
|
1946
|
-
* @returns {string[]} - Array of base column names (deduplicated)
|
|
1947
|
-
*/
|
|
1948
|
-
extractBaseColumns(columns) {
|
|
1949
|
-
if (!columns || !Array.isArray(columns)) return columns;
|
|
1950
|
-
return [
|
|
1951
|
-
...new Set(
|
|
1952
|
-
columns.map((col) => {
|
|
1953
|
-
const firstDot = col.indexOf(".");
|
|
1954
|
-
return firstDot > -1 ? col.substring(0, firstDot) : col;
|
|
1955
|
-
}),
|
|
1956
|
-
),
|
|
1957
|
-
];
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
/**
|
|
1961
|
-
* Checks if a field is present in the columns string.
|
|
1962
|
-
* Used to skip modifier execution when the field isn't requested.
|
|
1963
|
-
* @param {string} fieldName - The field name to check for
|
|
1964
|
-
* @param {string} columns - Comma-separated column string
|
|
1965
|
-
* @returns {boolean} - True if field is in columns or columns is empty (fetch all)
|
|
1966
|
-
*/
|
|
1967
|
-
isFieldInColumns(fieldName, columns) {
|
|
1968
|
-
// No columns filter = include all
|
|
1969
|
-
if (!columns || columns === "") return true;
|
|
1970
|
-
|
|
1971
|
-
const tokens = columns.split(",");
|
|
1972
|
-
for (const col of tokens) {
|
|
1973
|
-
const trimmed = col.trim();
|
|
1974
|
-
// Check if this column starts with the field name (exact match or nested like "fieldName.subfield")
|
|
1975
|
-
const firstPart = trimmed.split(".")[0];
|
|
1976
|
-
// Handle backtick-wrapped column names
|
|
1977
|
-
const cleanFirstPart =
|
|
1978
|
-
firstPart.startsWith("`") && firstPart.endsWith("`") ?
|
|
1979
|
-
firstPart.slice(1, -1)
|
|
1980
|
-
: firstPart;
|
|
1981
|
-
|
|
1982
|
-
if (cleanFirstPart === fieldName) {
|
|
1983
|
-
return true;
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
return false;
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
createMofifier(properties) {
|
|
1990
|
-
for (let i in properties) {
|
|
1991
|
-
if (properties[i].map) {
|
|
1992
|
-
switch (properties[i].map.type) {
|
|
1993
|
-
case MapType.MODEL: {
|
|
1994
|
-
switch (properties[i].type) {
|
|
1995
|
-
case String:
|
|
1996
|
-
case "string": {
|
|
1997
|
-
this.addModifier({
|
|
1998
|
-
when: properties[i].map.when || "read",
|
|
1999
|
-
execute: async (
|
|
2000
|
-
data,
|
|
2001
|
-
old_data,
|
|
2002
|
-
ctx,
|
|
2003
|
-
type,
|
|
2004
|
-
args,
|
|
2005
|
-
mapping_dept,
|
|
2006
|
-
level,
|
|
2007
|
-
columns,
|
|
2008
|
-
) => {
|
|
2009
|
-
// Skip if columns are specified but this field isn't included
|
|
2010
|
-
if (!this.isFieldInColumns(i, columns)) {
|
|
2011
|
-
return data;
|
|
2012
|
-
}
|
|
2013
|
-
return await this.mapToObject(
|
|
2014
|
-
data,
|
|
2015
|
-
_.isString(properties[i].map.reference) ?
|
|
2016
|
-
spice.models[properties[i].map.reference]
|
|
2017
|
-
: properties[i].map.reference,
|
|
2018
|
-
i,
|
|
2019
|
-
properties[i].map.destination || i,
|
|
2020
|
-
properties[i],
|
|
2021
|
-
args,
|
|
2022
|
-
type,
|
|
2023
|
-
mapping_dept,
|
|
2024
|
-
level,
|
|
2025
|
-
columns,
|
|
2026
|
-
);
|
|
2027
|
-
},
|
|
2028
|
-
});
|
|
2029
|
-
break;
|
|
2030
|
-
}
|
|
2031
|
-
case Array:
|
|
2032
|
-
case "array": {
|
|
2033
|
-
this.addModifier({
|
|
2034
|
-
when: properties[i].map.when || "read",
|
|
2035
|
-
execute: async (
|
|
2036
|
-
data,
|
|
2037
|
-
old_data,
|
|
2038
|
-
ctx,
|
|
2039
|
-
type,
|
|
2040
|
-
args,
|
|
2041
|
-
mapping_dept,
|
|
2042
|
-
level,
|
|
2043
|
-
columns,
|
|
2044
|
-
) => {
|
|
2045
|
-
// Skip if columns are specified but this field isn't included
|
|
2046
|
-
if (!this.isFieldInColumns(i, columns)) {
|
|
2047
|
-
return data;
|
|
2048
|
-
}
|
|
2049
|
-
return await this.mapToObjectArray(
|
|
2050
|
-
data,
|
|
2051
|
-
_.isString(properties[i].map.reference) ?
|
|
2052
|
-
spice.models[properties[i].map.reference]
|
|
2053
|
-
: properties[i].map.reference,
|
|
2054
|
-
i,
|
|
2055
|
-
properties[i].map.destination || i,
|
|
2056
|
-
properties[i],
|
|
2057
|
-
args,
|
|
2058
|
-
type,
|
|
2059
|
-
mapping_dept,
|
|
2060
|
-
level,
|
|
2061
|
-
columns,
|
|
2062
|
-
);
|
|
2063
|
-
},
|
|
2064
|
-
});
|
|
2065
|
-
break;
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
break;
|
|
2069
|
-
}
|
|
2070
|
-
case MapType.LOOKUP: {
|
|
2071
|
-
break;
|
|
2072
|
-
}
|
|
2073
|
-
}
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
async do_serialize(data, type, old_data, args, path_to_be_removed = []) {
|
|
2079
|
-
// Profiling: use track() for proper async context forking
|
|
2080
|
-
const p = this[_ctx]?.profiler;
|
|
2081
|
-
|
|
2082
|
-
const doSerialize = async () => {
|
|
2083
|
-
// Early exit if serialization should not run.
|
|
2084
|
-
if (!this.shouldSerializerRun(args, type)) {
|
|
2085
|
-
return data;
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
// Add external modifiers only once.
|
|
2089
|
-
if (this.type && !this[_external_modifier_loaded]) {
|
|
2090
|
-
this.addExternalModifiers(this.type);
|
|
2091
|
-
this[_external_modifier_loaded] = true;
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
// Cache the modifiers lookup for the specified type.
|
|
2095
|
-
const modifiers = this[_serializers]?.[type]?.modifiers || [];
|
|
2096
|
-
for (let i = 0; i < modifiers.length; i++) {
|
|
2097
|
-
const modifier = modifiers[i];
|
|
2098
|
-
try {
|
|
2099
|
-
const result = await modifier(
|
|
2100
|
-
data,
|
|
2101
|
-
old_data,
|
|
2102
|
-
this[_ctx],
|
|
2103
|
-
this.type,
|
|
2104
|
-
args,
|
|
2105
|
-
this[_mapping_dept],
|
|
2106
|
-
this[_level],
|
|
2107
|
-
this[_columns],
|
|
2108
|
-
);
|
|
2109
|
-
// Guard against modifiers that return undefined
|
|
2110
|
-
if (result !== undefined) {
|
|
2111
|
-
data = result;
|
|
2112
|
-
} else {
|
|
2113
|
-
console.warn(
|
|
2114
|
-
`Modifier #${i} for type=${this.type} returned undefined, keeping previous data`,
|
|
2115
|
-
);
|
|
2116
|
-
}
|
|
2117
|
-
} catch (error) {
|
|
2118
|
-
console.error(
|
|
2119
|
-
`Modifier error in do_serialize (type=${this.type}, modifier #${i}):`,
|
|
2120
|
-
error instanceof Error ?
|
|
2121
|
-
error.stack
|
|
2122
|
-
: `Non-Error thrown: ${JSON.stringify(error)}`,
|
|
2123
|
-
);
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
// Ensure data is always an array for consistent processing.
|
|
2128
|
-
const originalIsArray = Array.isArray(data);
|
|
2129
|
-
if (!originalIsArray) {
|
|
2130
|
-
data = [data];
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
// Compute the defaults from properties using reduce.
|
|
2134
|
-
const defaults = Object.keys(this.props).reduce((acc, key) => {
|
|
2135
|
-
const def = this.props[key]?.defaults?.[type];
|
|
2136
|
-
if (def !== undefined) {
|
|
2137
|
-
acc[key] =
|
|
2138
|
-
_.isFunction(def) ?
|
|
2139
|
-
def({ old_data: data, new_data: old_data })
|
|
2140
|
-
: def;
|
|
2141
|
-
}
|
|
2142
|
-
return acc;
|
|
2143
|
-
}, {});
|
|
2144
|
-
|
|
2145
|
-
// Merge defaults into each object.
|
|
2146
|
-
data = data.map((item) => _.defaults(item, defaults));
|
|
2147
|
-
|
|
2148
|
-
// If type is "read", clean the data by omitting certain props.
|
|
2149
|
-
if (type === "read") {
|
|
2150
|
-
// Collect hidden properties from schema.
|
|
2151
|
-
const hiddenProps = Object.keys(this.props).filter(
|
|
2152
|
-
(key) => this.props[key]?.hide,
|
|
2153
|
-
);
|
|
2154
|
-
// Combine default props to remove.
|
|
2155
|
-
const propsToClean = [
|
|
2156
|
-
"deleted",
|
|
2157
|
-
"type",
|
|
2158
|
-
"collection",
|
|
2159
|
-
...path_to_be_removed,
|
|
2160
|
-
...hiddenProps,
|
|
2161
|
-
];
|
|
2162
|
-
data = data.map((item) => _.omit(item, propsToClean));
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
// Return in the original format (array or single object).
|
|
2166
|
-
return originalIsArray ? data : data[0];
|
|
2167
|
-
};
|
|
2168
|
-
|
|
2169
|
-
try {
|
|
2170
|
-
if (p) {
|
|
2171
|
-
return await p.track(`${this.type}.do_serialize`, doSerialize, {
|
|
2172
|
-
type,
|
|
2173
|
-
});
|
|
2174
|
-
}
|
|
2175
|
-
return await doSerialize();
|
|
2176
|
-
} catch (error) {
|
|
2177
|
-
console.error(
|
|
2178
|
-
"Error in do_serialize:",
|
|
2179
|
-
error instanceof Error ?
|
|
2180
|
-
error.stack
|
|
2181
|
-
: `Non-Error thrown: ${JSON.stringify(error)}`,
|
|
2182
|
-
);
|
|
2183
|
-
throw error;
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
}
|