@reldens/storage 0.80.0 → 0.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -385,7 +385,7 @@ class EntitiesGenerator
|
|
|
385
385
|
if(!this.prismaClient && this.server.prisma){
|
|
386
386
|
this.prismaClient = this.server.prisma;
|
|
387
387
|
}
|
|
388
|
-
Logger.debug('Extract Prisma relations metadata.');
|
|
388
|
+
//Logger.debug('Extract Prisma relations metadata.');
|
|
389
389
|
this.extractPrismaRelationsMetadata();
|
|
390
390
|
}
|
|
391
391
|
let tables = await this.server.fetchEntitiesFromDatabase();
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { BaseDriver } = require('../base-driver');
|
|
8
|
+
const { PrismaTypeCaster } = require('./prisma-type-caster');
|
|
9
|
+
const { PrismaMetadataLoader } = require('./prisma-metadata-loader');
|
|
10
|
+
const { PrismaRelationResolver } = require('./prisma-relation-resolver');
|
|
8
11
|
const { Logger, sc } = require('@reldens/utils');
|
|
9
|
-
const { Prisma } = require('@prisma/client');
|
|
10
12
|
|
|
11
13
|
class PrismaDriver extends BaseDriver
|
|
12
14
|
{
|
|
@@ -24,325 +26,59 @@ class PrismaDriver extends BaseDriver
|
|
|
24
26
|
}
|
|
25
27
|
this.prisma = props.prisma;
|
|
26
28
|
this.model = props.model;
|
|
27
|
-
this.
|
|
28
|
-
this.optionalFields = [];
|
|
29
|
-
this.fieldTypes = {};
|
|
30
|
-
this.fieldDefaults = {};
|
|
31
|
-
this.idFieldType = 'String';
|
|
32
|
-
this.referenceFields = {};
|
|
33
|
-
this.relationMetadata = {};
|
|
34
|
-
this.foreignKeyMappings = {};
|
|
35
|
-
this.jsonFields = new Set();
|
|
29
|
+
this.initHelpers();
|
|
36
30
|
this.initModelMetadata();
|
|
37
31
|
}
|
|
38
32
|
|
|
39
|
-
|
|
40
|
-
{
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
getDmmf()
|
|
65
|
-
{
|
|
66
|
-
try {
|
|
67
|
-
if(this.prisma._dmmf){
|
|
68
|
-
return this.prisma._dmmf.datamodel;
|
|
69
|
-
}
|
|
70
|
-
if(this.prisma._getDmmf && sc.isFunction(this.prisma._getDmmf)){
|
|
71
|
-
return this.prisma._getDmmf().datamodel;
|
|
72
|
-
}
|
|
73
|
-
if(this.prisma._runtimeDataModel){
|
|
74
|
-
return this.prisma._runtimeDataModel;
|
|
75
|
-
}
|
|
76
|
-
if(Prisma.dmmf && Prisma.dmmf.datamodel){
|
|
77
|
-
return Prisma.dmmf.datamodel;
|
|
78
|
-
}
|
|
79
|
-
return null;
|
|
80
|
-
} catch(error) {
|
|
81
|
-
Logger.warning('Failed to access DMMF: '+error.message);
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
processFieldMetadata(field)
|
|
87
|
-
{
|
|
88
|
-
this.fieldTypes[field.name] = field.type;
|
|
89
|
-
if('Json' === field.type){
|
|
90
|
-
this.jsonFields.add(field.name);
|
|
91
|
-
}
|
|
92
|
-
if(field.isId){
|
|
93
|
-
this.idFieldType = field.type;
|
|
94
|
-
}
|
|
95
|
-
if(field.hasDefaultValue){
|
|
96
|
-
this.fieldDefaults[field.name] = field.default;
|
|
97
|
-
}
|
|
98
|
-
if(field.isRequired && !field.hasDefaultValue && !field.isId && 'object' !== field.kind){
|
|
99
|
-
this.requiredFields.push(field.name);
|
|
100
|
-
}
|
|
101
|
-
if(field.isOptional || field.hasDefaultValue){
|
|
102
|
-
this.optionalFields.push(field.name);
|
|
103
|
-
}
|
|
104
|
-
if(field.kind === 'scalar' && field.isList === false){
|
|
105
|
-
let isNumericType = this.isNumericFieldType(field.type);
|
|
106
|
-
let isReferenceField = field.relationFromFields && 0 < field.relationFromFields.length;
|
|
107
|
-
if(isNumericType || isReferenceField){
|
|
108
|
-
this.referenceFields[field.name] = field.type;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if('object' === field.kind){
|
|
112
|
-
let relationType = field.isList ? 'many' : 'one';
|
|
113
|
-
this.relationMetadata[field.name] = {
|
|
114
|
-
type: relationType,
|
|
115
|
-
model: field.type,
|
|
116
|
-
isList: field.isList,
|
|
117
|
-
isOptional: field.isOptional
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
if('object' === field.kind && sc.hasOwn(field, 'relationFromFields') && sc.isArray(field.relationFromFields)){
|
|
121
|
-
for(let foreignKeyField of field.relationFromFields){
|
|
122
|
-
this.foreignKeyMappings[foreignKeyField] = field.name;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
getAllRelations()
|
|
128
|
-
{
|
|
129
|
-
return Object.keys(this.relationMetadata || {});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
normalizeRelationName(relationName)
|
|
133
|
-
{
|
|
134
|
-
if(sc.hasOwn(this.relationMetadata, relationName)){
|
|
135
|
-
return relationName;
|
|
136
|
-
}
|
|
137
|
-
if(relationName.startsWith('related_')){
|
|
138
|
-
let withoutPrefix = relationName.substring(8);
|
|
139
|
-
if(sc.hasOwn(this.relationMetadata, withoutPrefix)){
|
|
140
|
-
return withoutPrefix;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
let availableRelations = Object.keys(this.relationMetadata);
|
|
144
|
-
for(let availableRelation of availableRelations){
|
|
145
|
-
if(relationName.endsWith(availableRelation)){
|
|
146
|
-
return availableRelation;
|
|
147
|
-
}
|
|
148
|
-
if(availableRelation.endsWith(relationName.replace('related_', ''))){
|
|
149
|
-
return availableRelation;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return relationName;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
isNumericFieldType(fieldType)
|
|
156
|
-
{
|
|
157
|
-
return ('Int' === fieldType || 'BigInt' === fieldType || 'Float' === fieldType || 'Decimal' === fieldType);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
isBooleanFieldType(fieldType)
|
|
161
|
-
{
|
|
162
|
-
return 'Boolean' === fieldType;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
isDateTimeFieldType(fieldType)
|
|
166
|
-
{
|
|
167
|
-
return 'DateTime' === fieldType || 'Date' === fieldType;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
isJsonFieldType(fieldType)
|
|
171
|
-
{
|
|
172
|
-
return 'Json' === fieldType;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
isJsonFieldByName(fieldName)
|
|
176
|
-
{
|
|
177
|
-
if(this.jsonFields.has(fieldName)){
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
if(!this.rawModel || !this.rawModel.properties){
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
return 'json' === sc.get(this.rawModel.properties[fieldName], 'dbType', '');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
isStringFieldType(fieldType)
|
|
187
|
-
{
|
|
188
|
-
return 'String' === fieldType;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
castValue(value, fieldType, fieldName = null)
|
|
192
|
-
{
|
|
193
|
-
if('id' === fieldName){
|
|
194
|
-
return this.castToIdType(value);
|
|
195
|
-
}
|
|
196
|
-
if(this.isNullOrEmpty(value)){
|
|
197
|
-
if(sc.hasOwn(this.fieldDefaults, fieldName)){
|
|
198
|
-
return undefined;
|
|
199
|
-
}
|
|
200
|
-
if(-1 !== this.optionalFields.indexOf(fieldName)){
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
203
|
-
if(-1 !== this.requiredFields.indexOf(fieldName)){
|
|
204
|
-
Logger.warning('Required field '+fieldName+' cannot be null or empty');
|
|
205
|
-
return undefined;
|
|
206
|
-
}
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
|
-
if(this.isJsonFieldByName(fieldName)){
|
|
210
|
-
return this.castToJsonType(value);
|
|
211
|
-
}
|
|
212
|
-
if(this.isNumericFieldType(fieldType)){
|
|
213
|
-
return this.castToNumericType(value, fieldType);
|
|
214
|
-
}
|
|
215
|
-
if(this.isBooleanFieldType(fieldType)){
|
|
216
|
-
return this.castToBooleanType(value);
|
|
217
|
-
}
|
|
218
|
-
if(this.isDateTimeFieldType(fieldType)){
|
|
219
|
-
return this.castToDateType(value);
|
|
220
|
-
}
|
|
221
|
-
if(this.isStringFieldType(fieldType)){
|
|
222
|
-
return this.castToStringType(value);
|
|
223
|
-
}
|
|
224
|
-
return value;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
isNullOrEmpty(value)
|
|
228
|
-
{
|
|
229
|
-
return null === value || undefined === value || '' === value;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
castToIdType(value)
|
|
233
|
-
{
|
|
234
|
-
if(this.isNullOrEmpty(value)){
|
|
235
|
-
return value;
|
|
236
|
-
}
|
|
237
|
-
if(this.isNumericFieldType(this.idFieldType)){
|
|
238
|
-
let numeric = Number(value);
|
|
239
|
-
if(!isNaN(numeric)){
|
|
240
|
-
return ('Int' === this.idFieldType || 'BigInt' === this.idFieldType) ? Math.floor(numeric) : numeric;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return String(value);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
castToNumericType(value, fieldType)
|
|
247
|
-
{
|
|
248
|
-
if(this.isNullOrEmpty(value)){
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
let numericValue = Number(value);
|
|
252
|
-
if(isNaN(numericValue)){
|
|
253
|
-
Logger.warning('Cannot convert value to numeric: '+value);
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
if('Int' === fieldType || 'BigInt' === fieldType){
|
|
257
|
-
return Math.floor(numericValue);
|
|
258
|
-
}
|
|
259
|
-
return numericValue;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
castToBooleanType(value)
|
|
263
|
-
{
|
|
264
|
-
if(this.isNullOrEmpty(value)){
|
|
265
|
-
return null;
|
|
266
|
-
}
|
|
267
|
-
if('boolean' === typeof value){
|
|
268
|
-
return value;
|
|
269
|
-
}
|
|
270
|
-
if('number' === typeof value){
|
|
271
|
-
return 0 !== value;
|
|
272
|
-
}
|
|
273
|
-
if('string' === typeof value){
|
|
274
|
-
let lowerValue = value.toLowerCase();
|
|
275
|
-
if('true' === lowerValue || '1' === lowerValue || 'yes' === lowerValue){
|
|
276
|
-
return true;
|
|
277
|
-
}
|
|
278
|
-
if('false' === lowerValue || '0' === lowerValue || 'no' === lowerValue){
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
return Boolean(value);
|
|
33
|
+
initHelpers()
|
|
34
|
+
{
|
|
35
|
+
this.metadataLoader = new PrismaMetadataLoader({
|
|
36
|
+
prisma: this.prisma,
|
|
37
|
+
rawModel: this.rawModel
|
|
38
|
+
});
|
|
39
|
+
this.typeCaster = new PrismaTypeCaster({
|
|
40
|
+
idFieldType: 'String',
|
|
41
|
+
jsonFields: new Set(),
|
|
42
|
+
fieldTypes: {},
|
|
43
|
+
fieldDefaults: {},
|
|
44
|
+
requiredFields: [],
|
|
45
|
+
optionalFields: [],
|
|
46
|
+
rawModel: this.rawModel
|
|
47
|
+
});
|
|
48
|
+
this.relationResolver = new PrismaRelationResolver({
|
|
49
|
+
relationMetadata: {},
|
|
50
|
+
relationAliases: {},
|
|
51
|
+
metadataLoader: this.metadataLoader
|
|
52
|
+
});
|
|
283
53
|
}
|
|
284
54
|
|
|
285
|
-
|
|
286
|
-
{
|
|
287
|
-
if(dateValue instanceof Date){
|
|
288
|
-
return dateValue;
|
|
289
|
-
}
|
|
290
|
-
if(this.isNullOrEmpty(dateValue)){
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
let dateString = String(dateValue);
|
|
294
|
-
if(dateString.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)){
|
|
295
|
-
return new Date(dateString.replace(' ', 'T') + 'Z');
|
|
296
|
-
}
|
|
297
|
-
if(dateString.match(/^\d{4}-\d{2}-\d{2}$/)){
|
|
298
|
-
return new Date(dateString + 'T00:00:00Z');
|
|
299
|
-
}
|
|
300
|
-
try {
|
|
301
|
-
let parsedDate = new Date(dateString);
|
|
302
|
-
if(isNaN(parsedDate.getTime())){
|
|
303
|
-
Logger.warning('Invalid date value: '+dateString);
|
|
304
|
-
return null;
|
|
305
|
-
}
|
|
306
|
-
return parsedDate;
|
|
307
|
-
} catch(error) {
|
|
308
|
-
Logger.warning('Failed to convert date value: '+dateString);
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
castToJsonType(value)
|
|
314
|
-
{
|
|
315
|
-
if(this.isNullOrEmpty(value)){
|
|
316
|
-
return Prisma.DbNull;
|
|
317
|
-
}
|
|
318
|
-
if('object' === typeof value){
|
|
319
|
-
return value;
|
|
320
|
-
}
|
|
321
|
-
if('string' === typeof value){
|
|
322
|
-
if(value.startsWith('%') && value.endsWith('%')){
|
|
323
|
-
return value;
|
|
324
|
-
}
|
|
325
|
-
try {
|
|
326
|
-
return JSON.parse(value);
|
|
327
|
-
} catch(error) {
|
|
328
|
-
return value;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return value;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
castToStringType(value)
|
|
55
|
+
initModelMetadata()
|
|
335
56
|
{
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if('string' === typeof value){
|
|
340
|
-
if('null' === value){
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
return value;
|
|
57
|
+
let metadata = this.metadataLoader.loadModelMetadata(this.tableName());
|
|
58
|
+
if(!metadata){
|
|
59
|
+
return;
|
|
344
60
|
}
|
|
345
|
-
|
|
61
|
+
this.requiredFields = metadata.requiredFields;
|
|
62
|
+
this.optionalFields = metadata.optionalFields;
|
|
63
|
+
this.fieldTypes = metadata.fieldTypes;
|
|
64
|
+
this.fieldDefaults = metadata.fieldDefaults;
|
|
65
|
+
this.idFieldType = metadata.idFieldType;
|
|
66
|
+
this.referenceFields = metadata.referenceFields;
|
|
67
|
+
this.relationMetadata = metadata.relationMetadata;
|
|
68
|
+
this.foreignKeyMappings = metadata.foreignKeyMappings;
|
|
69
|
+
this.jsonFields = metadata.jsonFields;
|
|
70
|
+
this.typeCaster.updateMetadata({
|
|
71
|
+
idFieldType: this.idFieldType,
|
|
72
|
+
jsonFields: this.jsonFields,
|
|
73
|
+
fieldTypes: this.fieldTypes,
|
|
74
|
+
fieldDefaults: this.fieldDefaults,
|
|
75
|
+
requiredFields: this.requiredFields,
|
|
76
|
+
optionalFields: this.optionalFields
|
|
77
|
+
});
|
|
78
|
+
this.relationResolver.updateMetadata({
|
|
79
|
+
relationMetadata: this.relationMetadata
|
|
80
|
+
});
|
|
81
|
+
this.relationAliases = this.relationResolver.buildRelationAliases(this.tableName());
|
|
346
82
|
}
|
|
347
83
|
|
|
348
84
|
databaseName()
|
|
@@ -370,6 +106,11 @@ class PrismaDriver extends BaseDriver
|
|
|
370
106
|
return this.rawModel[propertyName] || null;
|
|
371
107
|
}
|
|
372
108
|
|
|
109
|
+
getAllRelations()
|
|
110
|
+
{
|
|
111
|
+
return this.relationResolver.getAllRelations();
|
|
112
|
+
}
|
|
113
|
+
|
|
373
114
|
prepareData(params)
|
|
374
115
|
{
|
|
375
116
|
let data = {};
|
|
@@ -379,7 +120,7 @@ class PrismaDriver extends BaseDriver
|
|
|
379
120
|
data[key] = params[key];
|
|
380
121
|
continue;
|
|
381
122
|
}
|
|
382
|
-
let castedValue = this.castValue(params[key], fieldType, key);
|
|
123
|
+
let castedValue = this.typeCaster.castValue(params[key], fieldType, key);
|
|
383
124
|
if(undefined !== castedValue){
|
|
384
125
|
data[key] = castedValue;
|
|
385
126
|
}
|
|
@@ -395,9 +136,9 @@ class PrismaDriver extends BaseDriver
|
|
|
395
136
|
if(sc.hasOwn(this.foreignKeyMappings, key)){
|
|
396
137
|
let relationName = this.foreignKeyMappings[key];
|
|
397
138
|
let value = params[key];
|
|
398
|
-
if(!this.isNullOrEmpty(value)){
|
|
139
|
+
if(!this.typeCaster.isNullOrEmpty(value)){
|
|
399
140
|
relations[relationName] = {
|
|
400
|
-
connect: {id: this.castToIdType(value)}
|
|
141
|
+
connect: {id: this.typeCaster.castToIdType(value)}
|
|
401
142
|
};
|
|
402
143
|
continue;
|
|
403
144
|
}
|
|
@@ -410,14 +151,14 @@ class PrismaDriver extends BaseDriver
|
|
|
410
151
|
}
|
|
411
152
|
let fieldType = this.fieldTypes[key];
|
|
412
153
|
if(!fieldType){
|
|
413
|
-
if(!this.isNullOrEmpty(params[key])){
|
|
154
|
+
if(!this.typeCaster.isNullOrEmpty(params[key])){
|
|
414
155
|
data[key] = params[key];
|
|
415
156
|
}
|
|
416
157
|
continue;
|
|
417
158
|
}
|
|
418
|
-
let castedValue = this.castValue(params[key], fieldType, key);
|
|
159
|
+
let castedValue = this.typeCaster.castValue(params[key], fieldType, key);
|
|
419
160
|
if(undefined !== castedValue){
|
|
420
|
-
if(this.isJsonFieldType(fieldType) && null === castedValue){
|
|
161
|
+
if(this.typeCaster.isJsonFieldType(fieldType) && null === castedValue){
|
|
421
162
|
continue;
|
|
422
163
|
}
|
|
423
164
|
data[key] = castedValue;
|
|
@@ -470,64 +211,69 @@ class PrismaDriver extends BaseDriver
|
|
|
470
211
|
{
|
|
471
212
|
if(sc.isArray(value)){
|
|
472
213
|
if('id' === key){
|
|
473
|
-
return value.map(id => this.castToIdType(id));
|
|
214
|
+
return value.map(id => this.typeCaster.castToIdType(id)).filter(id => null !== id);
|
|
474
215
|
}
|
|
475
216
|
let fieldType = this.fieldTypes[key];
|
|
476
217
|
if(fieldType){
|
|
477
|
-
return value.map(val => this.castValue(val, fieldType, key)).filter(val => undefined !== val);
|
|
218
|
+
return value.map(val => this.typeCaster.castValue(val, fieldType, key)).filter(val => undefined !== val);
|
|
478
219
|
}
|
|
479
220
|
return value;
|
|
480
221
|
}
|
|
481
222
|
if('id' === key){
|
|
482
|
-
return this.castToIdType(value);
|
|
223
|
+
return this.typeCaster.castToIdType(value);
|
|
483
224
|
}
|
|
484
225
|
let fieldType = this.fieldTypes[key];
|
|
485
226
|
if(fieldType){
|
|
486
|
-
let castedValue = this.castValue(value, fieldType, key);
|
|
227
|
+
let castedValue = this.typeCaster.castValue(value, fieldType, key);
|
|
487
228
|
return undefined !== castedValue ? castedValue : value;
|
|
488
229
|
}
|
|
489
230
|
return value;
|
|
490
231
|
}
|
|
491
232
|
|
|
492
|
-
|
|
233
|
+
applyOperator(value, operator, fieldName = null)
|
|
493
234
|
{
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if(sc.isArray(result)){
|
|
498
|
-
return result.map(item => this.transformSingleResult(item, includeConfig, relationMapping));
|
|
235
|
+
let upperOperator = operator ? operator.toUpperCase() : '';
|
|
236
|
+
if('LIKE' === upperOperator){
|
|
237
|
+
return this.handleLikeOperator(value, fieldName);
|
|
499
238
|
}
|
|
500
|
-
|
|
239
|
+
let operatorsMap = {
|
|
240
|
+
'=': value,
|
|
241
|
+
'!=': {not: value},
|
|
242
|
+
'>': {gt: value},
|
|
243
|
+
'>=': {gte: value},
|
|
244
|
+
'<': {lt: value},
|
|
245
|
+
'<=': {lte: value},
|
|
246
|
+
'IN': {in: value},
|
|
247
|
+
'NOT IN': {notIn: value}
|
|
248
|
+
};
|
|
249
|
+
return operatorsMap[upperOperator] || value;
|
|
501
250
|
}
|
|
502
251
|
|
|
503
|
-
|
|
252
|
+
handleLikeOperator(value, fieldName)
|
|
504
253
|
{
|
|
505
|
-
|
|
506
|
-
|
|
254
|
+
let cleanValue = String(value).replace(/%/g, '');
|
|
255
|
+
if(this.typeCaster.isJsonFieldByName(fieldName) || this.isJsonField(fieldName)){
|
|
256
|
+
return this.handleJsonTextSearch(cleanValue);
|
|
507
257
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
if(!
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
let relationMeta = this.relationMetadata[relationName];
|
|
514
|
-
if(!relationMeta){
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
let relationValue = transformed[relationName];
|
|
518
|
-
if('one' === relationMeta.type && sc.isArray(relationValue)){
|
|
519
|
-
relationValue = 0 < relationValue.length ? [...relationValue].shift() : null;
|
|
520
|
-
}
|
|
521
|
-
if('many' === relationMeta.type && !sc.isArray(relationValue)){
|
|
522
|
-
relationValue = relationValue ? [relationValue] : [];
|
|
523
|
-
}
|
|
524
|
-
let originalName = sc.get(relationMapping, relationName, relationName);
|
|
525
|
-
transformed[originalName] = relationValue;
|
|
526
|
-
if(originalName !== relationName){
|
|
527
|
-
delete transformed[relationName];
|
|
258
|
+
if('id' === fieldName || sc.hasOwn(this.referenceFields, fieldName)){
|
|
259
|
+
let numericValue = Number(cleanValue);
|
|
260
|
+
if(!isNaN(numericValue)){
|
|
261
|
+
return numericValue;
|
|
528
262
|
}
|
|
529
263
|
}
|
|
530
|
-
return
|
|
264
|
+
return {contains: cleanValue};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
handleJsonTextSearch(searchValue)
|
|
268
|
+
{
|
|
269
|
+
return {
|
|
270
|
+
string_contains: searchValue
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
isJsonField(fieldName)
|
|
275
|
+
{
|
|
276
|
+
return this.jsonFields && this.jsonFields.has(fieldName);
|
|
531
277
|
}
|
|
532
278
|
|
|
533
279
|
async create(params)
|
|
@@ -553,26 +299,30 @@ class PrismaDriver extends BaseDriver
|
|
|
553
299
|
this.ensureRequiredFields(preparedData);
|
|
554
300
|
let createData = {data: preparedData};
|
|
555
301
|
if(0 < relations.length){
|
|
556
|
-
let includeData = this.buildIncludeObjectWithMapping(relations);
|
|
302
|
+
let includeData = this.relationResolver.buildIncludeObjectWithMapping(relations);
|
|
557
303
|
createData.include = includeData.include;
|
|
558
304
|
for(let relation of relations){
|
|
559
|
-
let normalizedRelation = this.normalizeRelationName(relation);
|
|
305
|
+
let normalizedRelation = this.relationResolver.normalizeRelationName(relation);
|
|
560
306
|
if(!sc.hasOwn(params, relation) && !sc.hasOwn(params, normalizedRelation)){
|
|
561
307
|
continue;
|
|
562
308
|
}
|
|
563
309
|
let relationData = sc.get(params, relation, sc.get(params, normalizedRelation, null));
|
|
564
310
|
if(sc.isArray(relationData)){
|
|
565
311
|
createData.data[normalizedRelation] = {
|
|
566
|
-
connect: relationData.map(item => ({id: this.castToIdType(item.id)}))
|
|
312
|
+
connect: relationData.map(item => ({id: this.typeCaster.castToIdType(item.id)}))
|
|
567
313
|
};
|
|
568
314
|
continue;
|
|
569
315
|
}
|
|
570
316
|
createData.data[normalizedRelation] = {
|
|
571
|
-
connect: {id: this.castToIdType(relationData.id)}
|
|
317
|
+
connect: {id: this.typeCaster.castToIdType(relationData.id)}
|
|
572
318
|
};
|
|
573
319
|
}
|
|
574
320
|
try {
|
|
575
|
-
return this.transformRelationResults(
|
|
321
|
+
return this.relationResolver.transformRelationResults(
|
|
322
|
+
await this.model.create(createData),
|
|
323
|
+
includeData.include,
|
|
324
|
+
includeData.mapping
|
|
325
|
+
);
|
|
576
326
|
} catch(error) {
|
|
577
327
|
Logger.error('Create with relations error: '+error.message);
|
|
578
328
|
return false;
|
|
@@ -618,7 +368,7 @@ class PrismaDriver extends BaseDriver
|
|
|
618
368
|
|
|
619
369
|
async updateById(id, params)
|
|
620
370
|
{
|
|
621
|
-
let castedId = this.castToIdType(id);
|
|
371
|
+
let castedId = this.typeCaster.castToIdType(id);
|
|
622
372
|
let found = await this.loadById(castedId);
|
|
623
373
|
if(!found){
|
|
624
374
|
return false;
|
|
@@ -640,7 +390,7 @@ class PrismaDriver extends BaseDriver
|
|
|
640
390
|
let preparedData = this.prepareDataWithRelations(params);
|
|
641
391
|
let existing = false;
|
|
642
392
|
if(params.id){
|
|
643
|
-
let castedId = this.castToIdType(params.id);
|
|
393
|
+
let castedId = this.typeCaster.castToIdType(params.id);
|
|
644
394
|
existing = await this.loadById(castedId);
|
|
645
395
|
if(existing){
|
|
646
396
|
let patch = Object.assign({}, preparedData);
|
|
@@ -661,7 +411,7 @@ class PrismaDriver extends BaseDriver
|
|
|
661
411
|
if(existing){
|
|
662
412
|
try {
|
|
663
413
|
return await this.model.update({
|
|
664
|
-
where: {id: this.castToIdType(existing.id)},
|
|
414
|
+
where: {id: this.typeCaster.castToIdType(existing.id)},
|
|
665
415
|
data: preparedData
|
|
666
416
|
});
|
|
667
417
|
} catch(error) {
|
|
@@ -688,7 +438,7 @@ class PrismaDriver extends BaseDriver
|
|
|
688
438
|
|
|
689
439
|
async deleteById(id)
|
|
690
440
|
{
|
|
691
|
-
let castedId = this.castToIdType(id);
|
|
441
|
+
let castedId = this.typeCaster.castToIdType(id);
|
|
692
442
|
let found = await this.loadById(castedId);
|
|
693
443
|
if(!found){
|
|
694
444
|
return false;
|
|
@@ -726,7 +476,7 @@ class PrismaDriver extends BaseDriver
|
|
|
726
476
|
where: processedFilters
|
|
727
477
|
};
|
|
728
478
|
if(0 < relations.length){
|
|
729
|
-
query.include = this.buildIncludeObjectWithMapping(relations).include;
|
|
479
|
+
query.include = this.relationResolver.buildIncludeObjectWithMapping(relations).include;
|
|
730
480
|
}
|
|
731
481
|
try {
|
|
732
482
|
return await this.model.count(query);
|
|
@@ -811,7 +561,7 @@ class PrismaDriver extends BaseDriver
|
|
|
811
561
|
|
|
812
562
|
async loadById(id)
|
|
813
563
|
{
|
|
814
|
-
let castedId = this.castToIdType(id);
|
|
564
|
+
let castedId = this.typeCaster.castToIdType(id);
|
|
815
565
|
try {
|
|
816
566
|
return await this.model.findUnique({
|
|
817
567
|
where: {id: castedId}
|
|
@@ -826,7 +576,7 @@ class PrismaDriver extends BaseDriver
|
|
|
826
576
|
{
|
|
827
577
|
return this.executeQueryWithRelations(
|
|
828
578
|
(q) => this.model.findUnique(q),
|
|
829
|
-
{where: {id: this.castToIdType(id)}},
|
|
579
|
+
{where: {id: this.typeCaster.castToIdType(id)}},
|
|
830
580
|
relations,
|
|
831
581
|
'Load by ID with relations error: ',
|
|
832
582
|
null
|
|
@@ -835,7 +585,10 @@ class PrismaDriver extends BaseDriver
|
|
|
835
585
|
|
|
836
586
|
async loadByIds(ids)
|
|
837
587
|
{
|
|
838
|
-
let castedIds = ids.map(id => this.castToIdType(id));
|
|
588
|
+
let castedIds = ids.map(id => this.typeCaster.castToIdType(id)).filter(id => null !== id);
|
|
589
|
+
if(0 === castedIds.length){
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
839
592
|
try {
|
|
840
593
|
return await this.model.findMany({
|
|
841
594
|
where: {
|
|
@@ -935,82 +688,23 @@ class PrismaDriver extends BaseDriver
|
|
|
935
688
|
return this.processFilters(filter);
|
|
936
689
|
}
|
|
937
690
|
|
|
938
|
-
applyOperator(value, operator, fieldName = null)
|
|
939
|
-
{
|
|
940
|
-
let upperOperator = operator ? operator.toUpperCase() : '';
|
|
941
|
-
if('LIKE' === upperOperator){
|
|
942
|
-
return this.handleLikeOperator(value, fieldName);
|
|
943
|
-
}
|
|
944
|
-
let operatorsMap = {
|
|
945
|
-
'=': value,
|
|
946
|
-
'!=': {not: value},
|
|
947
|
-
'>': {gt: value},
|
|
948
|
-
'>=': {gte: value},
|
|
949
|
-
'<': {lt: value},
|
|
950
|
-
'<=': {lte: value},
|
|
951
|
-
'IN': {in: value},
|
|
952
|
-
'NOT IN': {notIn: value}
|
|
953
|
-
};
|
|
954
|
-
return operatorsMap[upperOperator] || value;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
handleLikeOperator(value, fieldName)
|
|
958
|
-
{
|
|
959
|
-
let cleanValue = String(value).replace(/%/g, '');
|
|
960
|
-
if(this.isJsonFieldByName(fieldName) || this.isJsonField(fieldName)){
|
|
961
|
-
return this.handleJsonTextSearch(cleanValue);
|
|
962
|
-
}
|
|
963
|
-
if('id' === fieldName || sc.hasOwn(this.referenceFields, fieldName)){
|
|
964
|
-
let numericValue = Number(cleanValue);
|
|
965
|
-
if(!isNaN(numericValue)){
|
|
966
|
-
return numericValue;
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
return {contains: cleanValue};
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
handleJsonTextSearch(searchValue)
|
|
973
|
-
{
|
|
974
|
-
return {
|
|
975
|
-
string_contains: searchValue
|
|
976
|
-
};
|
|
977
|
-
}
|
|
978
|
-
|
|
979
691
|
async executeQueryWithRelations(modelMethod, query, relations, errorMessage, defaultReturn)
|
|
980
692
|
{
|
|
981
693
|
if(!sc.isArray(relations) || 0 === relations.length){
|
|
982
694
|
relations = this.getAllRelations();
|
|
983
695
|
}
|
|
984
|
-
let includeData = this.buildIncludeObjectWithMapping(relations);
|
|
696
|
+
let includeData = this.relationResolver.buildIncludeObjectWithMapping(relations);
|
|
985
697
|
if(0 < Object.keys(includeData.include).length){
|
|
986
698
|
query.include = includeData.include;
|
|
987
699
|
}
|
|
988
700
|
try {
|
|
989
|
-
return this.transformRelationResults(await modelMethod(query), includeData.include, includeData.mapping);
|
|
701
|
+
return this.relationResolver.transformRelationResults(await modelMethod(query), includeData.include, includeData.mapping);
|
|
990
702
|
} catch(error) {
|
|
991
703
|
Logger.error(errorMessage+error.message);
|
|
992
704
|
return defaultReturn;
|
|
993
705
|
}
|
|
994
706
|
}
|
|
995
707
|
|
|
996
|
-
buildIncludeObjectWithMapping(relations)
|
|
997
|
-
{
|
|
998
|
-
let include = {};
|
|
999
|
-
let mapping = {};
|
|
1000
|
-
for(let relation of relations){
|
|
1001
|
-
let normalizedRelation = this.normalizeRelationName(relation);
|
|
1002
|
-
if(!sc.hasOwn(this.relationMetadata, normalizedRelation)){
|
|
1003
|
-
//Logger.debug('Relation "'+relation+'" (normalized: "'+normalizedRelation+'") not found in model '+this.tableName());
|
|
1004
|
-
continue;
|
|
1005
|
-
}
|
|
1006
|
-
include[normalizedRelation] = true;
|
|
1007
|
-
if(relation !== normalizedRelation){
|
|
1008
|
-
mapping[normalizedRelation] = relation;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
return {include, mapping};
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
708
|
}
|
|
1015
709
|
|
|
1016
710
|
module.exports.PrismaDriver = PrismaDriver;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - PrismaMetadataLoader
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { Logger, sc } = require('@reldens/utils');
|
|
8
|
+
const { Prisma } = require('@prisma/client');
|
|
9
|
+
|
|
10
|
+
class PrismaMetadataLoader
|
|
11
|
+
{
|
|
12
|
+
|
|
13
|
+
constructor(props)
|
|
14
|
+
{
|
|
15
|
+
this.prisma = sc.get(props, 'prisma', null);
|
|
16
|
+
this.rawModel = sc.get(props, 'rawModel', null);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getDmmf()
|
|
20
|
+
{
|
|
21
|
+
try {
|
|
22
|
+
if(this.prisma._dmmf){
|
|
23
|
+
return this.prisma._dmmf.datamodel;
|
|
24
|
+
}
|
|
25
|
+
if(this.prisma._getDmmf && sc.isFunction(this.prisma._getDmmf)){
|
|
26
|
+
return this.prisma._getDmmf().datamodel;
|
|
27
|
+
}
|
|
28
|
+
if(this.prisma._runtimeDataModel){
|
|
29
|
+
return this.prisma._runtimeDataModel;
|
|
30
|
+
}
|
|
31
|
+
if(Prisma.dmmf && Prisma.dmmf.datamodel){
|
|
32
|
+
return Prisma.dmmf.datamodel;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
} catch(error) {
|
|
36
|
+
Logger.warning('Failed to access DMMF: '+error.message);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
loadModelMetadata(tableName)
|
|
42
|
+
{
|
|
43
|
+
let dmmf = this.getDmmf();
|
|
44
|
+
if(!dmmf){
|
|
45
|
+
Logger.warning('Could not access Prisma DMMF metadata');
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
let modelName = tableName.toLowerCase();
|
|
49
|
+
let modelInfo = dmmf.models?.[modelName] || dmmf.models?.find(m => m.name.toLowerCase() === modelName);
|
|
50
|
+
if(!modelInfo){
|
|
51
|
+
Logger.warning('Could not find model info for: '+modelName);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
let metadata = {
|
|
55
|
+
requiredFields: [],
|
|
56
|
+
optionalFields: [],
|
|
57
|
+
fieldTypes: {},
|
|
58
|
+
fieldDefaults: {},
|
|
59
|
+
idFieldType: 'String',
|
|
60
|
+
referenceFields: {},
|
|
61
|
+
relationMetadata: {},
|
|
62
|
+
foreignKeyMappings: {},
|
|
63
|
+
jsonFields: new Set()
|
|
64
|
+
};
|
|
65
|
+
for(let field of modelInfo.fields){
|
|
66
|
+
this.processFieldMetadata(field, metadata);
|
|
67
|
+
}
|
|
68
|
+
if(this.rawModel && this.rawModel.relationTypes){
|
|
69
|
+
for(let relationName of Object.keys(this.rawModel.relationTypes)){
|
|
70
|
+
if(sc.hasOwn(metadata.relationMetadata, relationName)){
|
|
71
|
+
metadata.relationMetadata[relationName].type = this.rawModel.relationTypes[relationName];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return metadata;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
processFieldMetadata(field, metadata)
|
|
79
|
+
{
|
|
80
|
+
metadata.fieldTypes[field.name] = field.type;
|
|
81
|
+
if('Json' === field.type){
|
|
82
|
+
metadata.jsonFields.add(field.name);
|
|
83
|
+
}
|
|
84
|
+
if(field.isId){
|
|
85
|
+
metadata.idFieldType = field.type;
|
|
86
|
+
}
|
|
87
|
+
if(field.hasDefaultValue){
|
|
88
|
+
metadata.fieldDefaults[field.name] = field.default;
|
|
89
|
+
}
|
|
90
|
+
if(field.isRequired && !field.hasDefaultValue && !field.isId && 'object' !== field.kind){
|
|
91
|
+
metadata.requiredFields.push(field.name);
|
|
92
|
+
}
|
|
93
|
+
if(field.isOptional || field.hasDefaultValue){
|
|
94
|
+
metadata.optionalFields.push(field.name);
|
|
95
|
+
}
|
|
96
|
+
if(field.kind === 'scalar' && field.isList === false){
|
|
97
|
+
let isNumericType = this.isNumericFieldType(field.type);
|
|
98
|
+
let isReferenceField = field.relationFromFields && 0 < field.relationFromFields.length;
|
|
99
|
+
if(isNumericType || isReferenceField){
|
|
100
|
+
metadata.referenceFields[field.name] = field.type;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if('object' === field.kind){
|
|
104
|
+
let relationType = field.isList ? 'many' : 'one';
|
|
105
|
+
metadata.relationMetadata[field.name] = {
|
|
106
|
+
type: relationType,
|
|
107
|
+
model: field.type,
|
|
108
|
+
isList: field.isList,
|
|
109
|
+
isOptional: field.isOptional
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if('object' === field.kind && sc.hasOwn(field, 'relationFromFields') && sc.isArray(field.relationFromFields)){
|
|
113
|
+
for(let foreignKeyField of field.relationFromFields){
|
|
114
|
+
metadata.foreignKeyMappings[foreignKeyField] = field.name;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
isNumericFieldType(fieldType)
|
|
120
|
+
{
|
|
121
|
+
return ('Int' === fieldType || 'BigInt' === fieldType || 'Float' === fieldType || 'Decimal' === fieldType);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports.PrismaMetadataLoader = PrismaMetadataLoader;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - PrismaRelationResolver
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { sc } = require('@reldens/utils');
|
|
8
|
+
|
|
9
|
+
class PrismaRelationResolver
|
|
10
|
+
{
|
|
11
|
+
|
|
12
|
+
constructor(props)
|
|
13
|
+
{
|
|
14
|
+
this.relationMetadata = sc.get(props, 'relationMetadata', {});
|
|
15
|
+
this.relationAliases = sc.get(props, 'relationAliases', {});
|
|
16
|
+
this.metadataLoader = sc.get(props, 'metadataLoader', null);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
updateMetadata(metadata)
|
|
20
|
+
{
|
|
21
|
+
this.relationMetadata = sc.get(metadata, 'relationMetadata', this.relationMetadata);
|
|
22
|
+
this.relationAliases = sc.get(metadata, 'relationAliases', this.relationAliases);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
buildRelationAliases(tableName)
|
|
26
|
+
{
|
|
27
|
+
let normalizedTableName = tableName.toLowerCase();
|
|
28
|
+
for(let prismaRelation of Object.keys(this.relationMetadata)){
|
|
29
|
+
let meta = this.relationMetadata[prismaRelation];
|
|
30
|
+
let relatedModel = meta.model.toLowerCase();
|
|
31
|
+
let aliases = this.generateRelationAliases(prismaRelation, relatedModel, normalizedTableName);
|
|
32
|
+
for(let alias of aliases){
|
|
33
|
+
this.relationAliases[alias] = prismaRelation;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return this.relationAliases;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
generateRelationAliases(prismaRelation, relatedModel, tableName)
|
|
40
|
+
{
|
|
41
|
+
let aliases = [];
|
|
42
|
+
aliases.push('related_'+relatedModel);
|
|
43
|
+
aliases.push(relatedModel);
|
|
44
|
+
let fkHint = this.extractForeignKeyHint(prismaRelation, relatedModel, tableName);
|
|
45
|
+
if(fkHint){
|
|
46
|
+
aliases.push('related_'+relatedModel+'_'+fkHint);
|
|
47
|
+
aliases.push(relatedModel+'_'+fkHint);
|
|
48
|
+
}
|
|
49
|
+
return aliases;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
extractForeignKeyHint(prismaRelation, relatedModel, tableName)
|
|
53
|
+
{
|
|
54
|
+
let toSuffix = 'To'+tableName;
|
|
55
|
+
if(!prismaRelation.toLowerCase().endsWith(toSuffix.toLowerCase())){
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
let withoutToSuffix = prismaRelation.slice(0, -toSuffix.length);
|
|
59
|
+
let prefix = relatedModel+'_';
|
|
60
|
+
if(withoutToSuffix.toLowerCase().startsWith(prefix.toLowerCase())){
|
|
61
|
+
withoutToSuffix = withoutToSuffix.slice(prefix.length);
|
|
62
|
+
if(withoutToSuffix.toLowerCase().startsWith(prefix.toLowerCase())){
|
|
63
|
+
withoutToSuffix = withoutToSuffix.slice(prefix.length);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if(withoutToSuffix.endsWith('_id')){
|
|
67
|
+
return withoutToSuffix.slice(0, -3);
|
|
68
|
+
}
|
|
69
|
+
return withoutToSuffix;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getAllRelations()
|
|
73
|
+
{
|
|
74
|
+
return Object.keys(this.relationMetadata || {});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
normalizeRelationName(relationName)
|
|
78
|
+
{
|
|
79
|
+
if(sc.hasOwn(this.relationMetadata, relationName)){
|
|
80
|
+
return relationName;
|
|
81
|
+
}
|
|
82
|
+
if(sc.hasOwn(this.relationAliases, relationName)){
|
|
83
|
+
return this.relationAliases[relationName];
|
|
84
|
+
}
|
|
85
|
+
if(relationName.startsWith('related_')){
|
|
86
|
+
let withoutPrefix = relationName.substring(8);
|
|
87
|
+
if(sc.hasOwn(this.relationMetadata, withoutPrefix)){
|
|
88
|
+
return withoutPrefix;
|
|
89
|
+
}
|
|
90
|
+
if(sc.hasOwn(this.relationAliases, withoutPrefix)){
|
|
91
|
+
return this.relationAliases[withoutPrefix];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
let availableRelations = Object.keys(this.relationMetadata);
|
|
95
|
+
for(let availableRelation of availableRelations){
|
|
96
|
+
if(relationName.endsWith(availableRelation)){
|
|
97
|
+
return availableRelation;
|
|
98
|
+
}
|
|
99
|
+
if(availableRelation.endsWith(relationName.replace('related_', ''))){
|
|
100
|
+
return availableRelation;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return relationName;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
buildIncludeObjectWithMapping(relations)
|
|
107
|
+
{
|
|
108
|
+
let include = {};
|
|
109
|
+
let mapping = {};
|
|
110
|
+
for(let relation of relations){
|
|
111
|
+
if(-1 !== relation.indexOf('.')){
|
|
112
|
+
this.buildNestedInclude(relation, include, mapping);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
let normalizedRelation = this.normalizeRelationName(relation);
|
|
116
|
+
if(!sc.hasOwn(this.relationMetadata, normalizedRelation)){
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
include[normalizedRelation] = true;
|
|
120
|
+
if(relation !== normalizedRelation){
|
|
121
|
+
mapping[normalizedRelation] = relation;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {include, mapping};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
buildNestedInclude(relationPath, include, mapping)
|
|
128
|
+
{
|
|
129
|
+
let parts = relationPath.split('.');
|
|
130
|
+
let rootPart = parts.shift();
|
|
131
|
+
let normalizedRoot = this.normalizeRelationName(rootPart);
|
|
132
|
+
if(!sc.hasOwn(this.relationMetadata, normalizedRoot)){
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if(!sc.hasOwn(include, normalizedRoot) || true === include[normalizedRoot]){
|
|
136
|
+
include[normalizedRoot] = {include: {}};
|
|
137
|
+
}
|
|
138
|
+
if(rootPart !== normalizedRoot){
|
|
139
|
+
mapping[normalizedRoot] = rootPart;
|
|
140
|
+
}
|
|
141
|
+
if(0 === parts.length){
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
let nestedPath = parts.join('.');
|
|
145
|
+
let relatedModel = this.relationMetadata[normalizedRoot].model;
|
|
146
|
+
this.buildNestedIncludeForModel(nestedPath, include[normalizedRoot].include, mapping, relatedModel);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
buildNestedIncludeForModel(relationPath, include, mapping, modelName)
|
|
150
|
+
{
|
|
151
|
+
if(!this.metadataLoader){
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let dmmf = this.metadataLoader.getDmmf();
|
|
155
|
+
if(!dmmf){
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
let modelInfo = dmmf.models?.[modelName.toLowerCase()]
|
|
159
|
+
|| dmmf.models?.find(m => m.name.toLowerCase() === modelName.toLowerCase());
|
|
160
|
+
if(!modelInfo){
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
let relationFields = {};
|
|
164
|
+
let tableName = modelName.toLowerCase();
|
|
165
|
+
for(let field of modelInfo.fields){
|
|
166
|
+
if('object' === field.kind){
|
|
167
|
+
relationFields[field.name] = {
|
|
168
|
+
model: field.type,
|
|
169
|
+
isList: field.isList
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
let parts = relationPath.split('.');
|
|
174
|
+
let currentPart = parts.shift();
|
|
175
|
+
let normalizedPart = this.normalizeNestedRelation(currentPart, relationFields, tableName);
|
|
176
|
+
if(!normalizedPart){
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if(0 === parts.length){
|
|
180
|
+
include[normalizedPart] = true;
|
|
181
|
+
if(currentPart !== normalizedPart){
|
|
182
|
+
mapping[normalizedPart] = currentPart;
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if(!sc.hasOwn(include, normalizedPart) || true === include[normalizedPart]){
|
|
187
|
+
include[normalizedPart] = {include: {}};
|
|
188
|
+
}
|
|
189
|
+
if(currentPart !== normalizedPart){
|
|
190
|
+
mapping[normalizedPart] = currentPart;
|
|
191
|
+
}
|
|
192
|
+
let nestedPath = parts.join('.');
|
|
193
|
+
let nestedModel = relationFields[normalizedPart].model;
|
|
194
|
+
this.buildNestedIncludeForModel(nestedPath, include[normalizedPart].include, mapping, nestedModel);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
normalizeNestedRelation(relationName, relationFields, tableName)
|
|
198
|
+
{
|
|
199
|
+
if(sc.hasOwn(relationFields, relationName)){
|
|
200
|
+
return relationName;
|
|
201
|
+
}
|
|
202
|
+
let cleanName = relationName.replace('related_', '');
|
|
203
|
+
if(sc.hasOwn(relationFields, cleanName)){
|
|
204
|
+
return cleanName;
|
|
205
|
+
}
|
|
206
|
+
for(let fieldName of Object.keys(relationFields)){
|
|
207
|
+
let relatedModel = relationFields[fieldName].model.toLowerCase();
|
|
208
|
+
let fkHint = this.extractForeignKeyHint(fieldName, relatedModel, tableName);
|
|
209
|
+
if(fkHint){
|
|
210
|
+
let possibleNames = [
|
|
211
|
+
'related_'+relatedModel+'_'+fkHint,
|
|
212
|
+
relatedModel+'_'+fkHint,
|
|
213
|
+
'related_'+relatedModel,
|
|
214
|
+
relatedModel
|
|
215
|
+
];
|
|
216
|
+
if(-1 !== possibleNames.indexOf(cleanName) || -1 !== possibleNames.indexOf(relationName)){
|
|
217
|
+
return fieldName;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
transformRelationResults(result, includeConfig, relationMapping)
|
|
225
|
+
{
|
|
226
|
+
if(!result || !includeConfig){
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
if(sc.isArray(result)){
|
|
230
|
+
return result.map(item => this.transformSingleResult(item, includeConfig, relationMapping));
|
|
231
|
+
}
|
|
232
|
+
return this.transformSingleResult(result, includeConfig, relationMapping);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
transformSingleResult(item, includeConfig, relationMapping)
|
|
236
|
+
{
|
|
237
|
+
if(!item || !sc.isObject(item)){
|
|
238
|
+
return item;
|
|
239
|
+
}
|
|
240
|
+
let transformed = {...item};
|
|
241
|
+
for(let relationName of Object.keys(includeConfig)){
|
|
242
|
+
if(!sc.hasOwn(transformed, relationName)){
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
let relationMeta = this.relationMetadata[relationName];
|
|
246
|
+
if(!relationMeta){
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
let relationValue = transformed[relationName];
|
|
250
|
+
if('one' === relationMeta.type && sc.isArray(relationValue)){
|
|
251
|
+
relationValue = 0 < relationValue.length ? [...relationValue].shift() : null;
|
|
252
|
+
}
|
|
253
|
+
if('many' === relationMeta.type && !sc.isArray(relationValue)){
|
|
254
|
+
relationValue = relationValue ? [relationValue] : [];
|
|
255
|
+
}
|
|
256
|
+
let originalName = sc.get(relationMapping, relationName, relationName);
|
|
257
|
+
transformed[originalName] = relationValue;
|
|
258
|
+
if(originalName !== relationName){
|
|
259
|
+
delete transformed[relationName];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return transformed;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports.PrismaRelationResolver = PrismaRelationResolver;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - PrismaTypeCaster
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { Logger, sc } = require('@reldens/utils');
|
|
8
|
+
const { Prisma } = require('@prisma/client');
|
|
9
|
+
|
|
10
|
+
class PrismaTypeCaster
|
|
11
|
+
{
|
|
12
|
+
|
|
13
|
+
constructor(props)
|
|
14
|
+
{
|
|
15
|
+
this.idFieldType = sc.get(props, 'idFieldType', 'String');
|
|
16
|
+
this.jsonFields = sc.get(props, 'jsonFields', new Set());
|
|
17
|
+
this.fieldTypes = sc.get(props, 'fieldTypes', {});
|
|
18
|
+
this.fieldDefaults = sc.get(props, 'fieldDefaults', {});
|
|
19
|
+
this.requiredFields = sc.get(props, 'requiredFields', []);
|
|
20
|
+
this.optionalFields = sc.get(props, 'optionalFields', []);
|
|
21
|
+
this.rawModel = sc.get(props, 'rawModel', false);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
updateMetadata(metadata)
|
|
25
|
+
{
|
|
26
|
+
this.idFieldType = sc.get(metadata, 'idFieldType', this.idFieldType);
|
|
27
|
+
this.jsonFields = sc.get(metadata, 'jsonFields', this.jsonFields);
|
|
28
|
+
this.fieldTypes = sc.get(metadata, 'fieldTypes', this.fieldTypes);
|
|
29
|
+
this.fieldDefaults = sc.get(metadata, 'fieldDefaults', this.fieldDefaults);
|
|
30
|
+
this.requiredFields = sc.get(metadata, 'requiredFields', this.requiredFields);
|
|
31
|
+
this.optionalFields = sc.get(metadata, 'optionalFields', this.optionalFields);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
isNumericFieldType(fieldType)
|
|
35
|
+
{
|
|
36
|
+
return ('Int' === fieldType || 'BigInt' === fieldType || 'Float' === fieldType || 'Decimal' === fieldType);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
isBooleanFieldType(fieldType)
|
|
40
|
+
{
|
|
41
|
+
return 'Boolean' === fieldType;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
isDateTimeFieldType(fieldType)
|
|
45
|
+
{
|
|
46
|
+
return 'DateTime' === fieldType || 'Date' === fieldType;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
isJsonFieldType(fieldType)
|
|
50
|
+
{
|
|
51
|
+
return 'Json' === fieldType;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
isJsonFieldByName(fieldName)
|
|
55
|
+
{
|
|
56
|
+
if(this.jsonFields.has(fieldName)){
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if(!this.rawModel || !this.rawModel.properties){
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return 'json' === sc.get(this.rawModel.properties[fieldName], 'dbType', '');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
isStringFieldType(fieldType)
|
|
66
|
+
{
|
|
67
|
+
return 'String' === fieldType;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
isNullOrEmpty(value)
|
|
71
|
+
{
|
|
72
|
+
return null === value || undefined === value || '' === value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
castValue(value, fieldType, fieldName = null)
|
|
76
|
+
{
|
|
77
|
+
if('id' === fieldName){
|
|
78
|
+
return this.castToIdType(value);
|
|
79
|
+
}
|
|
80
|
+
if(this.isNullOrEmpty(value)){
|
|
81
|
+
if(sc.hasOwn(this.fieldDefaults, fieldName)){
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
if(-1 !== this.optionalFields.indexOf(fieldName)){
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if(-1 !== this.requiredFields.indexOf(fieldName)){
|
|
88
|
+
Logger.warning('Required field '+fieldName+' cannot be null or empty');
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if(this.isJsonFieldByName(fieldName)){
|
|
94
|
+
return this.castToJsonType(value);
|
|
95
|
+
}
|
|
96
|
+
if(this.isNumericFieldType(fieldType)){
|
|
97
|
+
return this.castToNumericType(value, fieldType);
|
|
98
|
+
}
|
|
99
|
+
if(this.isBooleanFieldType(fieldType)){
|
|
100
|
+
return this.castToBooleanType(value);
|
|
101
|
+
}
|
|
102
|
+
if(this.isDateTimeFieldType(fieldType)){
|
|
103
|
+
return this.castToDateType(value);
|
|
104
|
+
}
|
|
105
|
+
if(this.isStringFieldType(fieldType)){
|
|
106
|
+
return this.castToStringType(value);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
castToIdType(value)
|
|
112
|
+
{
|
|
113
|
+
if(this.isNullOrEmpty(value)){
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
if(this.isNumericFieldType(this.idFieldType)){
|
|
117
|
+
let numeric = Number(value);
|
|
118
|
+
if(!isNaN(numeric)){
|
|
119
|
+
return ('Int' === this.idFieldType || 'BigInt' === this.idFieldType) ? Math.floor(numeric) : numeric;
|
|
120
|
+
}
|
|
121
|
+
Logger.warning('Cannot cast non-numeric value "'+value+'" to ID type '+this.idFieldType);
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return String(value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
castToNumericType(value, fieldType)
|
|
128
|
+
{
|
|
129
|
+
if(this.isNullOrEmpty(value)){
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
let numericValue = Number(value);
|
|
133
|
+
if(isNaN(numericValue)){
|
|
134
|
+
Logger.warning('Cannot convert value to numeric: '+value);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if('Int' === fieldType || 'BigInt' === fieldType){
|
|
138
|
+
return Math.floor(numericValue);
|
|
139
|
+
}
|
|
140
|
+
return numericValue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
castToBooleanType(value)
|
|
144
|
+
{
|
|
145
|
+
if(this.isNullOrEmpty(value)){
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
if('boolean' === typeof value){
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
if('number' === typeof value){
|
|
152
|
+
return 0 !== value;
|
|
153
|
+
}
|
|
154
|
+
if('string' === typeof value){
|
|
155
|
+
let lowerValue = value.toLowerCase();
|
|
156
|
+
if('true' === lowerValue || '1' === lowerValue || 'yes' === lowerValue){
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if('false' === lowerValue || '0' === lowerValue || 'no' === lowerValue){
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return Boolean(value);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
castToDateType(dateValue)
|
|
167
|
+
{
|
|
168
|
+
if(dateValue instanceof Date){
|
|
169
|
+
return dateValue;
|
|
170
|
+
}
|
|
171
|
+
if(this.isNullOrEmpty(dateValue)){
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
let dateString = String(dateValue);
|
|
175
|
+
if(dateString.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)){
|
|
176
|
+
return new Date(dateString.replace(' ', 'T')+'Z');
|
|
177
|
+
}
|
|
178
|
+
if(dateString.match(/^\d{4}-\d{2}-\d{2}$/)){
|
|
179
|
+
return new Date(dateString+'T00:00:00Z');
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
let parsedDate = new Date(dateString);
|
|
183
|
+
if(isNaN(parsedDate.getTime())){
|
|
184
|
+
Logger.warning('Invalid date value: '+dateString);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return parsedDate;
|
|
188
|
+
} catch(error) {
|
|
189
|
+
Logger.warning('Failed to convert date value: '+dateString);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
castToJsonType(value)
|
|
195
|
+
{
|
|
196
|
+
if(this.isNullOrEmpty(value)){
|
|
197
|
+
return Prisma.DbNull;
|
|
198
|
+
}
|
|
199
|
+
if('object' === typeof value){
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
if('string' === typeof value){
|
|
203
|
+
if(value.startsWith('%') && value.endsWith('%')){
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
return JSON.parse(value);
|
|
208
|
+
} catch(error) {
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
castToStringType(value)
|
|
216
|
+
{
|
|
217
|
+
if(null === value || undefined === value){
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
if('string' === typeof value){
|
|
221
|
+
if('null' === value){
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
return String(value);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports.PrismaTypeCaster = PrismaTypeCaster;
|