@clairejs/server 3.15.2 → 3.16.1
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/.mocharc.json +3 -0
- package/README.md +5 -0
- package/dist/common/AbstractController.js +3 -0
- package/dist/common/ControllerMetadata.js +1 -0
- package/dist/common/FileOperation.js +6 -0
- package/dist/common/ServerModelMetadata.js +1 -0
- package/dist/common/Transactionable.js +17 -0
- package/dist/common/auth/AbstractPrincipalResolver.js +2 -0
- package/dist/common/auth/IPrincipal.js +1 -0
- package/dist/common/constants.js +7 -0
- package/dist/common/decorator.d.ts +2 -2
- package/dist/common/decorator.js +6 -0
- package/dist/common/request/EndpointMetadata.js +1 -0
- package/dist/common/request/HttpData.js +1 -0
- package/dist/common/request/HttpEndpoint.js +1 -0
- package/dist/common/request/JobData.js +1 -0
- package/dist/common/request/MountedEndpointInfo.js +1 -0
- package/dist/common/request/RequestOptions.js +1 -0
- package/dist/common/request/SocketData.js +1 -0
- package/dist/common/request/types.d.ts +1 -1
- package/dist/common/request/types.js +1 -0
- package/dist/controllers/FileManageController.js +90 -0
- package/dist/controllers/FileUploadController.js +64 -0
- package/dist/controllers/dto/system.js +14 -0
- package/dist/controllers/dto/upload.js +205 -0
- package/dist/http/auth/AbstractHttpAuthorizer.js +2 -0
- package/dist/http/common/HttpRequest.js +72 -0
- package/dist/http/common/HttpResponse.js +62 -0
- package/dist/http/controller/AbstractHttpController.js +21 -0
- package/dist/http/controller/AbstractHttpMiddleware.js +2 -0
- package/dist/http/controller/AbstractHttpRequestHandler.js +69 -0
- package/dist/http/controller/CrudHttpController.js +303 -0
- package/dist/http/controller/DefaultHttpRequestHandler.js +143 -0
- package/dist/http/decorators.d.ts +1 -1
- package/dist/http/decorators.js +86 -0
- package/dist/http/file-upload/AbstractFileUploadHandler.js +2 -0
- package/dist/http/file-upload/FileUploadHandler.js +41 -0
- package/dist/http/file-upload/types.d.ts +1 -1
- package/dist/http/file-upload/types.js +1 -0
- package/dist/http/repository/AbstractRepository.js +26 -0
- package/dist/http/repository/DtoRepository.d.ts +3 -3
- package/dist/http/repository/DtoRepository.js +204 -0
- package/dist/http/repository/ICrudRepository.js +1 -0
- package/dist/http/repository/ModelRepository.js +696 -0
- package/dist/http/security/AbstractAccessCondition.js +2 -0
- package/dist/http/security/access-conditions/FilterModelFieldAccessCondition.js +30 -0
- package/dist/http/security/access-conditions/MaximumQueryLimit.js +31 -0
- package/dist/http/security/cors.js +1 -0
- package/dist/http/utils.js +32 -0
- package/dist/index.js +75 -1
- package/dist/job/AbstractJobController.js +9 -0
- package/dist/job/AbstractJobRepository.js +2 -0
- package/dist/job/AbstractJobScheduler.js +48 -0
- package/dist/job/AwsJobScheduler.js +405 -0
- package/dist/job/LocalJobScheduler.js +273 -0
- package/dist/job/decorators.js +57 -0
- package/dist/job/interfaces.js +10 -0
- package/dist/logging/FileLogMedium.js +44 -0
- package/dist/services/AbstractFileService.js +28 -0
- package/dist/services/AbstractMailService.js +2 -0
- package/dist/services/AbstractService.js +3 -0
- package/dist/services/AbstractSmsService.js +2 -0
- package/dist/services/implementations/LocalFileService.js +42 -0
- package/dist/services/implementations/LocalMailService.js +27 -0
- package/dist/services/implementations/LocalSmsService.js +17 -0
- package/dist/services/implementations/S3FileService.js +107 -0
- package/dist/services/implementations/SesMailService.js +64 -0
- package/dist/socket/AbstractServerSocket.js +44 -0
- package/dist/socket/AbstractServerSocketManager.d.ts +1 -1
- package/dist/socket/AbstractServerSocketManager.js +348 -0
- package/dist/socket/AbstractSocketConnectionHandler.js +2 -0
- package/dist/socket/AbstractSocketController.d.ts +3 -3
- package/dist/socket/AbstractSocketController.js +12 -0
- package/dist/socket/AwsSocketManager.d.ts +2 -2
- package/dist/socket/AwsSocketManager.js +160 -0
- package/dist/socket/IServerSocket.js +1 -0
- package/dist/socket/LocalSocketManager.js +292 -0
- package/dist/system/ClaireServer.js +78 -0
- package/dist/system/ExpressWrapper.js +122 -0
- package/dist/system/LambdaWrapper.js +151 -0
- package/dist/system/ServerGlobalStore.js +1 -0
- package/dist/system/lamba-request-mapper.js +49 -0
- package/dist/system/locale/LocaleEntry.js +13 -0
- package/dist/system/locale/LocaleTranslation.js +47 -0
- package/dist/system/locale/decorators.js +14 -0
- 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
|
+
}
|