@clairejs/server 3.15.2 → 3.16.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.
Files changed (85) hide show
  1. package/.mocharc.json +3 -0
  2. package/dist/common/AbstractController.js +3 -0
  3. package/dist/common/ControllerMetadata.js +1 -0
  4. package/dist/common/FileOperation.js +6 -0
  5. package/dist/common/ServerModelMetadata.js +1 -0
  6. package/dist/common/Transactionable.js +17 -0
  7. package/dist/common/auth/AbstractPrincipalResolver.js +2 -0
  8. package/dist/common/auth/IPrincipal.js +1 -0
  9. package/dist/common/constants.js +7 -0
  10. package/dist/common/decorator.d.ts +2 -2
  11. package/dist/common/decorator.js +6 -0
  12. package/dist/common/request/EndpointMetadata.js +1 -0
  13. package/dist/common/request/HttpData.js +1 -0
  14. package/dist/common/request/HttpEndpoint.js +1 -0
  15. package/dist/common/request/JobData.js +1 -0
  16. package/dist/common/request/MountedEndpointInfo.js +1 -0
  17. package/dist/common/request/RequestOptions.js +1 -0
  18. package/dist/common/request/SocketData.js +1 -0
  19. package/dist/common/request/types.d.ts +1 -1
  20. package/dist/common/request/types.js +1 -0
  21. package/dist/controllers/FileManageController.js +90 -0
  22. package/dist/controllers/FileUploadController.js +64 -0
  23. package/dist/controllers/dto/system.js +14 -0
  24. package/dist/controllers/dto/upload.js +205 -0
  25. package/dist/http/auth/AbstractHttpAuthorizer.js +2 -0
  26. package/dist/http/common/HttpRequest.js +72 -0
  27. package/dist/http/common/HttpResponse.js +62 -0
  28. package/dist/http/controller/AbstractHttpController.js +21 -0
  29. package/dist/http/controller/AbstractHttpMiddleware.js +2 -0
  30. package/dist/http/controller/AbstractHttpRequestHandler.js +69 -0
  31. package/dist/http/controller/CrudHttpController.js +302 -0
  32. package/dist/http/controller/DefaultHttpRequestHandler.js +143 -0
  33. package/dist/http/decorators.d.ts +1 -1
  34. package/dist/http/decorators.js +86 -0
  35. package/dist/http/file-upload/AbstractFileUploadHandler.js +2 -0
  36. package/dist/http/file-upload/FileUploadHandler.js +41 -0
  37. package/dist/http/file-upload/types.d.ts +1 -1
  38. package/dist/http/file-upload/types.js +1 -0
  39. package/dist/http/repository/AbstractRepository.js +26 -0
  40. package/dist/http/repository/DtoRepository.d.ts +3 -3
  41. package/dist/http/repository/DtoRepository.js +204 -0
  42. package/dist/http/repository/ICrudRepository.js +1 -0
  43. package/dist/http/repository/ModelRepository.js +696 -0
  44. package/dist/http/security/AbstractAccessCondition.js +2 -0
  45. package/dist/http/security/access-conditions/FilterModelFieldAccessCondition.js +30 -0
  46. package/dist/http/security/access-conditions/MaximumQueryLimit.js +31 -0
  47. package/dist/http/security/cors.js +1 -0
  48. package/dist/http/utils.js +32 -0
  49. package/dist/index.js +75 -1
  50. package/dist/job/AbstractJobController.js +9 -0
  51. package/dist/job/AbstractJobRepository.js +2 -0
  52. package/dist/job/AbstractJobScheduler.js +48 -0
  53. package/dist/job/AwsJobScheduler.js +405 -0
  54. package/dist/job/LocalJobScheduler.js +273 -0
  55. package/dist/job/decorators.js +57 -0
  56. package/dist/job/interfaces.js +10 -0
  57. package/dist/logging/FileLogMedium.js +44 -0
  58. package/dist/services/AbstractFileService.js +28 -0
  59. package/dist/services/AbstractMailService.js +2 -0
  60. package/dist/services/AbstractService.js +3 -0
  61. package/dist/services/AbstractSmsService.js +2 -0
  62. package/dist/services/implementations/LocalFileService.js +42 -0
  63. package/dist/services/implementations/LocalMailService.js +27 -0
  64. package/dist/services/implementations/LocalSmsService.js +17 -0
  65. package/dist/services/implementations/S3FileService.js +107 -0
  66. package/dist/services/implementations/SesMailService.js +64 -0
  67. package/dist/socket/AbstractServerSocket.js +44 -0
  68. package/dist/socket/AbstractServerSocketManager.d.ts +1 -1
  69. package/dist/socket/AbstractServerSocketManager.js +348 -0
  70. package/dist/socket/AbstractSocketConnectionHandler.js +2 -0
  71. package/dist/socket/AbstractSocketController.d.ts +3 -3
  72. package/dist/socket/AbstractSocketController.js +12 -0
  73. package/dist/socket/AwsSocketManager.d.ts +2 -2
  74. package/dist/socket/AwsSocketManager.js +160 -0
  75. package/dist/socket/IServerSocket.js +1 -0
  76. package/dist/socket/LocalSocketManager.js +292 -0
  77. package/dist/system/ClaireServer.js +78 -0
  78. package/dist/system/ExpressWrapper.js +122 -0
  79. package/dist/system/LambdaWrapper.js +151 -0
  80. package/dist/system/ServerGlobalStore.js +1 -0
  81. package/dist/system/lamba-request-mapper.js +49 -0
  82. package/dist/system/locale/LocaleEntry.js +13 -0
  83. package/dist/system/locale/LocaleTranslation.js +47 -0
  84. package/dist/system/locale/decorators.js +14 -0
  85. package/package.json +13 -20
@@ -0,0 +1,696 @@
1
+ import { DataType, getModelById, getServiceProvider, RangeQueryDto, uniqueReducer, leanData, getSystemLocale, Errors, omitData, } from "@clairejs/core";
2
+ import { getDirectFields } from "@clairejs/orm";
3
+ import { AbstractFileUploadHandler } from "../file-upload/AbstractFileUploadHandler";
4
+ import { AbstractRepository } from "./AbstractRepository";
5
+ import { LocaleTranslation } from "../../system/locale/LocaleTranslation";
6
+ import { LocaleEntry } from "../../system/locale/LocaleEntry";
7
+ export class ModelRepository extends AbstractRepository {
8
+ model;
9
+ fileUploadHandler;
10
+ constructor(model) {
11
+ super(model);
12
+ this.model = model;
13
+ }
14
+ getNestedQueries(queries) {
15
+ return this.modelMetadata.fields
16
+ .filter((f) => f.hasMany)
17
+ .filter((f) => queries?.fields && queries.fields[f.name])
18
+ .map((f) => {
19
+ const dto = f.hasMany.relationDto;
20
+ const nestedConditions = this.getRequestQueryConditionFromQuery((queries?.fields)[f.name], {
21
+ ...dto,
22
+ fields: dto.fields.filter((df) => df.name !== f.hasMany?.column),
23
+ });
24
+ return {
25
+ modelId: f.hasMany.relationDto.id,
26
+ targetField: f.hasMany.column,
27
+ queries: nestedConditions.length ? { _and: nestedConditions } : {},
28
+ };
29
+ });
30
+ }
31
+ async getUploadHandler() {
32
+ if (this.fileUploadHandler === undefined) {
33
+ const injector = getServiceProvider().getInjector();
34
+ this.fileUploadHandler = injector.resolveOptional(AbstractFileUploadHandler) || null;
35
+ await injector.initInstances();
36
+ }
37
+ return this.fileUploadHandler;
38
+ }
39
+ getRequestQueryConditionFromQuery(queries, modelMetadata) {
40
+ const result = [];
41
+ for (const fieldMetadata of getDirectFields(modelMetadata)) {
42
+ const fieldName = `${modelMetadata.id}.${fieldMetadata.name}`;
43
+ const queryValue = queries[fieldMetadata.name];
44
+ if (queryValue === undefined) {
45
+ continue;
46
+ }
47
+ if (fieldMetadata.pk || fieldMetadata.fk || fieldMetadata.isSymbol) {
48
+ //-- belongs to array of ids
49
+ result.push({ _in: { [fieldName]: queryValue } });
50
+ }
51
+ else {
52
+ if (fieldMetadata.enum) {
53
+ result.push({ _in: { [fieldName]: queryValue } });
54
+ }
55
+ else {
56
+ switch (fieldMetadata.dataType) {
57
+ case DataType.STRING:
58
+ if (!fieldMetadata.searchable) {
59
+ result.push({
60
+ _eq: { [fieldName]: queryValue },
61
+ });
62
+ }
63
+ else {
64
+ const op = fieldMetadata?.searchable.caseSensitive
65
+ ? fieldMetadata?.searchable.accentSensitive
66
+ ? "_sub"
67
+ : "_usub"
68
+ : fieldMetadata?.searchable?.accentSensitive
69
+ ? "_isub"
70
+ : "_iusub";
71
+ const cond = {
72
+ [op]: {
73
+ [fieldName]: queryValue,
74
+ },
75
+ };
76
+ result.push(cond);
77
+ }
78
+ break;
79
+ case DataType.NUMBER:
80
+ const rangeQuery = new RangeQueryDto();
81
+ Object.assign(rangeQuery, queryValue);
82
+ if (rangeQuery.min) {
83
+ if (rangeQuery.minExclusive) {
84
+ result.push({
85
+ _gt: { [fieldName]: rangeQuery.min },
86
+ });
87
+ }
88
+ else {
89
+ result.push({
90
+ _gte: { [fieldName]: rangeQuery.min },
91
+ });
92
+ }
93
+ }
94
+ if (rangeQuery.max) {
95
+ if (rangeQuery.maxExclusive) {
96
+ result.push({
97
+ _lt: { [fieldName]: rangeQuery.max },
98
+ });
99
+ }
100
+ else {
101
+ result.push({
102
+ _lte: { [fieldName]: rangeQuery.max },
103
+ });
104
+ }
105
+ }
106
+ break;
107
+ case DataType.BOOLEAN:
108
+ result.push({
109
+ _eq: { [fieldName]: queryValue },
110
+ });
111
+ break;
112
+ }
113
+ }
114
+ }
115
+ }
116
+ return result;
117
+ }
118
+ async uriHandling(records) {
119
+ const modified = [];
120
+ const fileUploadHandler = await this.getUploadHandler();
121
+ const operations = [];
122
+ const uriHandler = async (tmpUri, uriMapper, locale, newUriCb) => {
123
+ const newUri = uriMapper(tmpUri, locale);
124
+ if (!newUri) {
125
+ return;
126
+ }
127
+ //-- move from currentUri to newUri
128
+ await fileUploadHandler.copyFile(tmpUri, newUri);
129
+ //-- update record value and not persist yet
130
+ newUriCb(newUri);
131
+ };
132
+ if (fileUploadHandler) {
133
+ for (const field of this.modelMetadata.fields) {
134
+ for (const record of records) {
135
+ if (field.uriMapper) {
136
+ const tmpUri = record[field.name];
137
+ if (!tmpUri) {
138
+ continue;
139
+ }
140
+ if (!field.mimeProps?.keepOnRemove) {
141
+ modified.push(tmpUri);
142
+ }
143
+ operations.push(uriHandler(tmpUri, field.uriMapper, getSystemLocale(), (newUri) => (record[field.name] = newUri)));
144
+ }
145
+ else if (field.multiLocaleColumn) {
146
+ const targetField = this.modelMetadata.fields.find((f) => f.name === field.multiLocaleColumn);
147
+ if (targetField?.uriMapper && record[field.name]) {
148
+ //-- this field is locale for targetField, which has uri mapper
149
+ const localeUris = Object.keys(record[field.name]).map((lang) => [
150
+ lang,
151
+ record[field.name][lang],
152
+ ]);
153
+ //-- for each pair or locale and tmp uri, invoke handle logic
154
+ for (const localeUri of localeUris) {
155
+ if (!targetField.mimeProps?.keepOnRemove) {
156
+ modified.push(localeUri[1]);
157
+ }
158
+ operations.push(uriHandler(localeUri[1], targetField.uriMapper, localeUri[0], (newUri) => (record[field.name][localeUri[0]] = newUri)));
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+ //-- await all operations once to save time
166
+ await Promise.all(operations);
167
+ return async () => {
168
+ if (fileUploadHandler) {
169
+ await Promise.all(modified.map((uri) => fileUploadHandler.removeFile(uri)));
170
+ }
171
+ };
172
+ }
173
+ async beforeReturning(records) {
174
+ //-- resolve url
175
+ const fileUploadHandler = await this.getUploadHandler();
176
+ if (!fileUploadHandler) {
177
+ return;
178
+ }
179
+ const mappingOperations = [];
180
+ for (const record of records) {
181
+ for (const field of this.modelMetadata.fields) {
182
+ if (field.uriMapper) {
183
+ if (!record[field.name]) {
184
+ continue;
185
+ }
186
+ mappingOperations.push((async () => {
187
+ record[field.name] = field.mimeProps?.public
188
+ ? await fileUploadHandler.resolvePublicUrl(record[field.name])
189
+ : await fileUploadHandler.resolvePrivateUrl(record[field.name]);
190
+ })());
191
+ }
192
+ else if (field.multiLocaleColumn) {
193
+ const targetField = this.modelMetadata.fields.find((f) => f.name === field.multiLocaleColumn);
194
+ if (targetField?.uriMapper) {
195
+ const localeObject = record[field.name];
196
+ if (!localeObject) {
197
+ continue;
198
+ }
199
+ for (const locale of Object.keys(localeObject)) {
200
+ mappingOperations.push((async () => {
201
+ localeObject[locale] = targetField.mimeProps?.public
202
+ ? await fileUploadHandler.resolvePublicUrl(localeObject[locale])
203
+ : await fileUploadHandler.resolvePrivateUrl(localeObject[locale]);
204
+ })());
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+ await Promise.all(mappingOperations);
211
+ }
212
+ async createMany({ principal, body, tx, }) {
213
+ const originalRecords = body.records;
214
+ if (!originalRecords.length) {
215
+ return { records: [] };
216
+ }
217
+ const directFields = getDirectFields(this.modelMetadata);
218
+ const localeFields = directFields.filter((f) => !!f.multiLocaleColumn);
219
+ const cleanUp = await this.uriHandling(originalRecords);
220
+ const systemLocale = getSystemLocale();
221
+ if (systemLocale) {
222
+ //-- override values in record
223
+ for (const record of originalRecords) {
224
+ for (const field of localeFields) {
225
+ if (record[field.name]) {
226
+ record[field.multiLocaleColumn] = record[field.name][systemLocale];
227
+ }
228
+ }
229
+ }
230
+ }
231
+ //-- create locale entries
232
+ const translationEntryData = originalRecords
233
+ .map((record, index) => {
234
+ return localeFields
235
+ .filter((f) => !!record[f.name])
236
+ .map((f) => ({
237
+ recordIndex: index,
238
+ field: f.name,
239
+ entryObject: {},
240
+ }));
241
+ })
242
+ .flatMap((arr) => arr);
243
+ const translationEntries = await tx
244
+ .use(LocaleEntry)
245
+ .createMany(translationEntryData.map((data) => data.entryObject));
246
+ //-- translation data will have same length as translationEntries
247
+ const translationData = originalRecords
248
+ .map((record) => {
249
+ return localeFields.filter((f) => !!record[f.name]).map((f) => record[f.name]);
250
+ })
251
+ .flatMap((arr) => arr);
252
+ //-- mapping supplied data with entry ids
253
+ const translations = translationData
254
+ .map((data, index) => {
255
+ return Object.keys(data).map((localeCode) => ({
256
+ localeCode,
257
+ entryId: translationEntries[index].id,
258
+ translation: data[localeCode],
259
+ }));
260
+ })
261
+ .flatMap((arr) => arr);
262
+ //-- create translation records
263
+ await tx.use(LocaleTranslation).createMany(translations);
264
+ //-- first create records for direct fields
265
+ body.records = originalRecords.map((record, index) => {
266
+ const data = directFields.reduce((collector, field) => {
267
+ let fieldValue = record[field.name];
268
+ //-- check if this field is locale of then get the correct locale entry id
269
+ if (field.multiLocaleColumn) {
270
+ //-- find the translationEntryData
271
+ const entryDataIndex = translationEntryData.findIndex((data) => data.recordIndex === index && data.field === field.name);
272
+ if (entryDataIndex >= 0) {
273
+ fieldValue = translationEntries[entryDataIndex].id;
274
+ }
275
+ }
276
+ return Object.assign(collector, fieldValue !== undefined
277
+ ? {
278
+ [field.name]: fieldValue,
279
+ }
280
+ : {});
281
+ }, {});
282
+ return data;
283
+ });
284
+ await this.beforeCreating(principal, body.records);
285
+ let records = body.records.length ? await tx.use(this.model).createMany(body.records) : [];
286
+ await this.beforeReturning(records);
287
+ const projection = this.modelMetadata.fields
288
+ .filter((field) => !field.multiLocaleColumn && (field.pk || field.serverValue || field.mimeProps))
289
+ .map((field) => field.name);
290
+ records = this.project(records, projection);
291
+ //-- then create records for has many fields
292
+ const hasManyFields = this.modelMetadata.fields.filter((f) => !!f.hasMany);
293
+ for (const field of hasManyFields) {
294
+ //-- collect info
295
+ const hasManyRecords = records.flatMap((record, index) => {
296
+ //-- insert the id of the just-created-record to innerRecord at field designated by hasMany.column
297
+ const nestedRecords = originalRecords[index][field.name];
298
+ if (!nestedRecords) {
299
+ return [];
300
+ }
301
+ return (field.hasMany?.single ? [nestedRecords] : nestedRecords).map((innerRecord) => ({
302
+ ...innerRecord,
303
+ [field.hasMany.column]: record.id,
304
+ }));
305
+ });
306
+ if (hasManyRecords.length) {
307
+ //-- insert to db
308
+ const innerService = new ModelRepository(getModelById(field.hasMany.relationDto.id));
309
+ body.records = hasManyRecords;
310
+ const persistedInnerRecords = await innerService.createMany({ principal, body, tx });
311
+ //-- map back ids to hasManyRecords
312
+ for (let i = 0; i < persistedInnerRecords.records.length; i++) {
313
+ hasManyRecords[i] = { ...hasManyRecords[i], ...persistedInnerRecords.records[i] };
314
+ }
315
+ //-- assign back to records
316
+ for (let i = 0; i < records.length; i++) {
317
+ //-- only assign back persistedInnerRecords to avoid return redundant data
318
+ const filteredPersistedRecords = [];
319
+ for (let j = 0; j < hasManyRecords.length; j++) {
320
+ if (hasManyRecords[j][field.hasMany.column] === records[i].id) {
321
+ filteredPersistedRecords.push(persistedInnerRecords.records[j]);
322
+ }
323
+ }
324
+ records[i] = {
325
+ ...records[i],
326
+ [field.name]: field.hasMany?.single ? filteredPersistedRecords[0] : filteredPersistedRecords,
327
+ };
328
+ }
329
+ }
330
+ }
331
+ //-- everything is ok, remove original Uris
332
+ await cleanUp();
333
+ return { records };
334
+ }
335
+ async updateMany({ principal, ops, queries, body, tx, }) {
336
+ const allConditions = ops || [];
337
+ const hasManyFields = this.modelMetadata.fields.filter((f) => !!f.hasMany);
338
+ const systemLocale = getSystemLocale();
339
+ //-- does not update multi locale columns
340
+ const directUpdateFields = getDirectFields(this.modelMetadata).filter((f) => !f.multiLocaleColumn &&
341
+ (body.update[f.name] !== undefined || (f.isMultiLocale && !!systemLocale)));
342
+ const updatedFields = Object.keys(body.update);
343
+ const localeOfFields = this.modelMetadata.fields.filter((f) => updatedFields.includes(f.name) && f.multiLocaleColumn);
344
+ const cleanUp = await this.uriHandling([body.update]);
345
+ //-- calculate direct update after uri handling
346
+ const directUpdate = leanData(directUpdateFields.reduce((collector, field) => Object.assign(collector, { [field.name]: body.update[field.name] }), {})) || {};
347
+ if (systemLocale) {
348
+ //-- override values in record
349
+ for (const field of localeOfFields) {
350
+ if (body.update[field.name]) {
351
+ const translation = body.update[field.name][systemLocale];
352
+ if (translation !== undefined) {
353
+ directUpdate[field.multiLocaleColumn] = translation;
354
+ }
355
+ }
356
+ }
357
+ }
358
+ if (queries?.fields) {
359
+ const fieldOps = this.getRequestQueryConditionFromQuery(queries.fields, this.modelMetadata);
360
+ if (fieldOps.length) {
361
+ allConditions.push(...fieldOps);
362
+ }
363
+ }
364
+ const condition = allConditions.length ? { _and: [...allConditions] } : {};
365
+ const nestedQueries = this.getNestedQueries(queries);
366
+ let modified = [];
367
+ let updatedRecords = [];
368
+ if (nestedQueries.length) {
369
+ const tobeUpdated = await tx.use(this.model).getMany(condition, { projection: ["id"] }, nestedQueries);
370
+ modified = tobeUpdated.records.map((r) => r.id);
371
+ if (modified.length) {
372
+ if (directUpdateFields.length) {
373
+ updatedRecords = await tx
374
+ .use(this.model)
375
+ .updateMany({ _in: { id: modified } }, directUpdate, true);
376
+ }
377
+ else {
378
+ updatedRecords = modified.map((id) => ({ id }));
379
+ }
380
+ }
381
+ }
382
+ else {
383
+ if (directUpdateFields.length) {
384
+ updatedRecords = await tx.use(this.model).updateMany(condition, directUpdate, true);
385
+ modified = updatedRecords.map((re) => re.id);
386
+ }
387
+ else {
388
+ const tobeUpdated = await tx.use(this.model).getMany(condition, { projection: ["id"] });
389
+ modified = tobeUpdated.records.map((r) => r.id);
390
+ updatedRecords = modified.map((id) => ({ id }));
391
+ }
392
+ }
393
+ //-- body.update here had been modified by uri handling
394
+ const records = updatedRecords.map((re) => ({ ...body.update, ...re }));
395
+ //-- update translations
396
+ if (localeOfFields.length) {
397
+ //-- check if there is missing locale entry for localeFields
398
+ const records = await tx.use(this.model).getRecords({
399
+ _in: { id: modified },
400
+ }, { projection: ["id", ...localeOfFields.map((f) => f.name)] });
401
+ //-- check missing
402
+ const missing = records
403
+ .map((record) => localeOfFields
404
+ .filter((field) => !record[field.name])
405
+ .map((field) => ({
406
+ recordId: record.id,
407
+ field: field.name,
408
+ })))
409
+ .flatMap((arr) => arr);
410
+ const missingEntries = !missing.length ? [] : await tx.use(LocaleEntry).createMany(missing.map(() => ({})));
411
+ const updateEntryRecords = [];
412
+ missingEntries.forEach((entry, index) => {
413
+ const missingData = missing[index];
414
+ const record = records.find((re) => re.id === missingData.recordId);
415
+ if (record) {
416
+ record[missingData.field] = entry.id;
417
+ //-- add to updateEntryRecords
418
+ let updateRecord = updateEntryRecords.find((re) => re.id === record.id);
419
+ if (!updateRecord) {
420
+ updateRecord = { id: record.id };
421
+ updateEntryRecords.push(updateRecord);
422
+ }
423
+ updateRecord[missingData.field] = entry.id;
424
+ }
425
+ });
426
+ //-- update records
427
+ await Promise.all(updateEntryRecords.map((re) => tx.use(this.model).updateById(re.id, omitData(re, ["id"]))));
428
+ const tobeModified = [];
429
+ for (const field of localeOfFields) {
430
+ const localOfData = body.update[field.name];
431
+ //-- do not add record for system locale, it is stored directly in the model record
432
+ const locales = Object.keys(localOfData || {}).filter((key) => key !== systemLocale);
433
+ if (locales.length) {
434
+ tobeModified.push(...records
435
+ .map((re) => locales.map((locale) => ({
436
+ entryId: re[field.name],
437
+ langCode: locale,
438
+ translation: localOfData[locale],
439
+ })))
440
+ .flatMap((arr) => arr));
441
+ }
442
+ }
443
+ if (tobeModified.length) {
444
+ //-- remove old translations
445
+ await tx.use(LocaleTranslation).deleteMany({
446
+ _or: tobeModified.map((rem) => ({
447
+ _eq: { entryId: rem.entryId, localeCode: rem.langCode },
448
+ })),
449
+ });
450
+ //-- add new translations
451
+ await tx.use(LocaleTranslation).createMany(tobeModified.map((rem) => ({
452
+ entryId: rem.entryId,
453
+ localeCode: rem.langCode,
454
+ translation: rem.translation,
455
+ })));
456
+ }
457
+ }
458
+ //-- update hasMany
459
+ for (const field of hasManyFields) {
460
+ const innerUpdates = body.update[field.name];
461
+ if (innerUpdates === undefined || !modified.length) {
462
+ continue;
463
+ }
464
+ if (modified.length > 1) {
465
+ throw Errors.SYSTEM_ERROR(`Multiple records found while updating @HasMany field: ${field.name}`);
466
+ }
467
+ const theRecord = records.find((r) => r.id === modified[0]);
468
+ //-- get all inner records and compare
469
+ const model = getModelById(field.hasMany.relationDto.id);
470
+ const adapter = tx.use(model);
471
+ const allInnerRecords = await adapter.getRecords({ _eq: { [field.hasMany.column]: theRecord.id } }, { limit: field.hasMany?.single ? 1 : undefined });
472
+ const currentIds = allInnerRecords.map((r) => r.id);
473
+ const newIds = innerUpdates.map((r) => r.id).filter((id) => !!id);
474
+ const tobeRemovedIds = currentIds.filter((id) => !newIds.includes(id));
475
+ const tobeAdded = innerUpdates
476
+ .filter((r) => !r.id)
477
+ .map((r) => ({ ...r, [field.hasMany.column]: theRecord.id }));
478
+ const tobeKept = innerUpdates.filter((r) => !!r.id);
479
+ if (tobeRemovedIds.length) {
480
+ await adapter.deleteMany({ _in: { id: tobeRemovedIds } });
481
+ }
482
+ const modelService = new ModelRepository(model);
483
+ const added = await modelService.createMany({
484
+ principal,
485
+ body: { records: tobeAdded },
486
+ tx,
487
+ });
488
+ const updatedToBeKept = await Promise.all(tobeKept
489
+ .filter((r) => Object.keys(r).length > 1)
490
+ .map((r) => modelService.updateMany({
491
+ queries: { returning: true },
492
+ principal,
493
+ ops: [{ _eq: { id: r.id } }],
494
+ tx,
495
+ body: { update: leanData({ ...r, id: undefined }) },
496
+ })));
497
+ //-- return the new inners records
498
+ const finalInnerRecords = tobeAdded
499
+ .map((toAdd, index) => ({ ...toAdd, ...added.records[index] }))
500
+ .concat(updatedToBeKept.map((r) => r.modified[0]));
501
+ theRecord[field.name] = (field.hasMany?.single ? finalInnerRecords[0] : finalInnerRecords);
502
+ }
503
+ let projection = ["id"];
504
+ if (queries?.returning) {
505
+ //-- return result
506
+ projection = [
507
+ ...projection,
508
+ ...Object.keys(body.update).filter((key) => body.update[key] !== undefined),
509
+ "lastModified",
510
+ ];
511
+ }
512
+ //-- ok clean up
513
+ await cleanUp();
514
+ await this.beforeReturning(records);
515
+ return { modified: this.project(records, projection) };
516
+ }
517
+ async getMany({ queries, ops, queryProvider, }) {
518
+ const conditions = ops || [];
519
+ if (queries?.fields) {
520
+ conditions.push(...this.getRequestQueryConditionFromQuery(queries.fields, this.modelMetadata));
521
+ }
522
+ if (queries?.search) {
523
+ //-- text search on fields that were masked with @Searchable decorator
524
+ const searchOps = this.modelMetadata.fields
525
+ .filter((field) => field.searchable)
526
+ .map((field) => ({
527
+ _iusub: { [field.name]: queries.search },
528
+ }));
529
+ if (searchOps.length) {
530
+ conditions.push({ _or: searchOps });
531
+ }
532
+ }
533
+ //-- check if there is any multi-local fields in projection
534
+ const multiLocaleFields = this.modelMetadata.fields.filter((f) => f.isMultiLocale && (!queries?.projection || queries.projection.includes(f.name)));
535
+ const localeOfFields = this.modelMetadata.fields.filter((f) => multiLocaleFields.find((locale) => locale.name === f.multiLocaleColumn));
536
+ const finalProjection = queries?.projection && [
537
+ ...queries.projection.filter((fieldName) => !this.modelMetadata.fields.find((f) => f.name === fieldName && !!f.hasMany)),
538
+ ...localeOfFields.map((f) => f.name),
539
+ ];
540
+ const result = await queryProvider.use(this.model).getMany(conditions.length ? { _and: conditions } : {}, {
541
+ limit: queries?.limit,
542
+ page: queries?.page,
543
+ projection: finalProjection,
544
+ order: queries?.order === "random"
545
+ ? queries.order
546
+ : queries?.order?.map((o) => {
547
+ const key = Object.keys(o).find((k) => !!o[k]);
548
+ return [key, o[key]];
549
+ }),
550
+ },
551
+ //-- page and limit does not affect nested queries
552
+ this.getNestedQueries(queries));
553
+ const recordIds = result.records.map((r) => r.id);
554
+ const allLocaleEntries = result.records
555
+ .map((record) => localeOfFields.map((field) => record[field.name]))
556
+ .flatMap((ids) => ids)
557
+ .filter((id) => id);
558
+ const systemLocale = getSystemLocale();
559
+ const translations = !allLocaleEntries.length
560
+ ? []
561
+ : !queries?.locale
562
+ ? //-- if locale is not specified then get translation for all locales
563
+ await queryProvider.use(LocaleTranslation).getRecords({
564
+ _in: { entryId: allLocaleEntries },
565
+ }, { projection: ["entryId", "localeCode", "translation"] })
566
+ : //-- if locale is specified then get only the translation of that locale
567
+ queries.locale.toLowerCase() !== systemLocale
568
+ ? await queryProvider.use(LocaleTranslation).getRecords({
569
+ _in: { entryId: allLocaleEntries },
570
+ _eq: { localeCode: queries.locale },
571
+ }, { projection: ["entryId", "localeCode", "translation"] })
572
+ : //-- locale is system locale, no need to fetch translation
573
+ [];
574
+ for (const record of result.records) {
575
+ for (const field of localeOfFields) {
576
+ //-- map back translation
577
+ if (!queries?.locale) {
578
+ //-- map as object
579
+ const fieldTranslations = translations.filter((t) => t.entryId === record[field.name]);
580
+ if (!fieldTranslations.length) {
581
+ record[field.name] = {};
582
+ }
583
+ else {
584
+ record[field.name] = fieldTranslations.reduce((collector, t) => Object.assign(collector, { [t.localeCode]: t.translation }), {});
585
+ }
586
+ }
587
+ else {
588
+ //-- replace value
589
+ const translation = translations.find((t) => t.localeCode === queries.locale && t.entryId === record[field.name]);
590
+ if (translation) {
591
+ record[field.multiLocaleColumn] = translation.translation;
592
+ }
593
+ }
594
+ }
595
+ }
596
+ //-- get has many fields
597
+ for (const field of this.modelMetadata.fields) {
598
+ if (!field.hasMany || (queries?.projection && !queries.projection.includes(field.name))) {
599
+ continue;
600
+ }
601
+ const model = getModelById(field.hasMany.relationDto.id);
602
+ const service = new ModelRepository(model);
603
+ const innerRecords = !recordIds.length
604
+ ? { records: [] }
605
+ : await service.getMany({
606
+ queries: { fields: { [field.hasMany.column]: recordIds }, limit: field.hasMany.single ? 1 : 0 },
607
+ queryProvider,
608
+ });
609
+ //-- filter corresponding inner records for each master record
610
+ for (const record of result.records) {
611
+ const finalRecords = innerRecords.records.filter((r) => r[field.hasMany.column] === record.id);
612
+ record[field.name] = (field.hasMany.single ? finalRecords[0] : finalRecords);
613
+ }
614
+ }
615
+ await this.beforeReturning(result.records);
616
+ return {
617
+ total: result.total,
618
+ records: this.project(result.records, queries?.projection &&
619
+ [...queries.projection, ...(queries.locale ? [] : finalProjection || [])].reduce(uniqueReducer, [])),
620
+ };
621
+ }
622
+ async deleteMany({ queries, ops, tx, }) {
623
+ let allConditions = ops || [];
624
+ if (queries?.fields) {
625
+ const fieldOps = this.getRequestQueryConditionFromQuery(queries.fields, this.modelMetadata);
626
+ if (fieldOps.length) {
627
+ allConditions.push(...fieldOps);
628
+ }
629
+ }
630
+ const condition = allConditions.length ? { _and: [...allConditions, ...(ops || [])] } : {};
631
+ const nestedQueries = this.getNestedQueries(queries);
632
+ let returning = [];
633
+ const uriMapperFields = this.modelMetadata.fields.filter((f) => f.uriMapper);
634
+ const localeOfFields = this.modelMetadata.fields.filter((f) => f.multiLocaleColumn);
635
+ if (nestedQueries.length || localeOfFields.length || uriMapperFields.length) {
636
+ const tobeRemoved = await tx
637
+ .use(this.model)
638
+ .getRecords(condition, { projection: ["id", ...[...localeOfFields, ...uriMapperFields].map((f) => f.name)] }, nestedQueries);
639
+ await tx.use(this.model).deleteMany({
640
+ _in: { id: tobeRemoved.map((r) => r.id) },
641
+ });
642
+ //-- collect uri to remove
643
+ const toBeRemovedUris = [];
644
+ for (const record of tobeRemoved) {
645
+ for (const field of uriMapperFields) {
646
+ if (record[field.name] && !field.mimeProps?.keepOnRemove) {
647
+ toBeRemovedUris.push(record[field.name]);
648
+ }
649
+ }
650
+ }
651
+ const localeUriLocalEntryIds = [];
652
+ for (const record of tobeRemoved) {
653
+ for (const field of localeOfFields) {
654
+ const targetField = this.modelMetadata.fields.find((f) => f.name === field.name);
655
+ if (targetField?.uriMapper &&
656
+ !targetField.mimeProps?.keepOnRemove &&
657
+ record[targetField.name]) {
658
+ localeUriLocalEntryIds.push(record[targetField.name]);
659
+ }
660
+ }
661
+ }
662
+ if (localeUriLocalEntryIds.length) {
663
+ //-- get all uris and add to remove list
664
+ const records = await tx
665
+ .use(LocaleTranslation)
666
+ .getRecords({ _in: { entryId: localeUriLocalEntryIds } }, { projection: ["translation"] });
667
+ for (const record of records) {
668
+ if (record.translation) {
669
+ toBeRemovedUris.push(record.translation);
670
+ }
671
+ }
672
+ }
673
+ //-- remove translations
674
+ const localeEntryIds = tobeRemoved
675
+ .map((record) => localeOfFields.map((field) => record[field.name]))
676
+ .flatMap((ids) => ids)
677
+ .filter((id) => !!id);
678
+ //-- remove locale entry, will cascade locale translations
679
+ if (localeEntryIds.length) {
680
+ await tx.use(LocaleEntry).deleteMany({ _in: { id: localeEntryIds } });
681
+ }
682
+ //-- remove uris
683
+ if (toBeRemovedUris.length) {
684
+ const fileUploadHandler = await this.getUploadHandler();
685
+ if (fileUploadHandler) {
686
+ await Promise.all(toBeRemovedUris.map((uri) => fileUploadHandler.removeFile(uri)));
687
+ }
688
+ }
689
+ returning = tobeRemoved;
690
+ }
691
+ else {
692
+ returning = await tx.use(this.model).deleteMany(condition, queries?.returning);
693
+ }
694
+ return { modified: returning };
695
+ }
696
+ }