@ruiapp/rapid-core 0.3.0 → 0.3.2

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 (101) hide show
  1. package/CHANGELOG.md +7 -7
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +359 -134
  4. package/dist/plugins/license/LicensePlugin.d.ts +23 -0
  5. package/dist/plugins/license/LicensePluginTypes.d.ts +78 -0
  6. package/dist/plugins/license/LicenseService.d.ts +22 -0
  7. package/dist/plugins/license/actionHandlers/getLicense.d.ts +6 -0
  8. package/dist/plugins/license/actionHandlers/index.d.ts +3 -0
  9. package/dist/plugins/license/helpers/certHelper.d.ts +2 -0
  10. package/dist/plugins/license/helpers/cryptoHelper.d.ts +8 -0
  11. package/dist/plugins/license/models/index.d.ts +2 -0
  12. package/dist/plugins/license/routes/getLicense.d.ts +12 -0
  13. package/dist/plugins/license/routes/index.d.ts +12 -0
  14. package/dist/utilities/typeUtility.d.ts +1 -0
  15. package/package.json +1 -1
  16. package/src/bootstrapApplicationConfig.ts +631 -631
  17. package/src/core/response.ts +76 -76
  18. package/src/core/routeContext.ts +47 -47
  19. package/src/core/server.ts +142 -142
  20. package/src/dataAccess/columnTypeMapper.ts +22 -22
  21. package/src/dataAccess/dataAccessTypes.ts +163 -163
  22. package/src/dataAccess/dataAccessor.ts +135 -135
  23. package/src/dataAccess/entityManager.ts +1718 -1718
  24. package/src/dataAccess/entityMapper.ts +100 -100
  25. package/src/dataAccess/propertyMapper.ts +28 -28
  26. package/src/deno-std/http/cookie.ts +372 -372
  27. package/src/helpers/entityHelpers.ts +76 -76
  28. package/src/helpers/filterHelper.ts +148 -148
  29. package/src/helpers/metaHelper.ts +89 -89
  30. package/src/helpers/runCollectionEntityActionHandler.ts +27 -27
  31. package/src/index.ts +57 -54
  32. package/src/plugins/auth/AuthPlugin.ts +85 -85
  33. package/src/plugins/auth/actionHandlers/changePassword.ts +54 -54
  34. package/src/plugins/auth/actionHandlers/createSession.ts +75 -63
  35. package/src/plugins/auth/actionHandlers/resetPassword.ts +38 -38
  36. package/src/plugins/cronJob/CronJobPlugin.ts +112 -112
  37. package/src/plugins/dataManage/DataManagePlugin.ts +163 -163
  38. package/src/plugins/dataManage/actionHandlers/addEntityRelations.ts +20 -20
  39. package/src/plugins/dataManage/actionHandlers/countCollectionEntities.ts +16 -16
  40. package/src/plugins/dataManage/actionHandlers/createCollectionEntitiesBatch.ts +42 -42
  41. package/src/plugins/dataManage/actionHandlers/createCollectionEntity.ts +24 -24
  42. package/src/plugins/dataManage/actionHandlers/deleteCollectionEntities.ts +38 -38
  43. package/src/plugins/dataManage/actionHandlers/deleteCollectionEntityById.ts +22 -22
  44. package/src/plugins/dataManage/actionHandlers/findCollectionEntities.ts +26 -26
  45. package/src/plugins/dataManage/actionHandlers/findCollectionEntityById.ts +21 -21
  46. package/src/plugins/dataManage/actionHandlers/removeEntityRelations.ts +20 -20
  47. package/src/plugins/dataManage/actionHandlers/updateCollectionEntityById.ts +41 -41
  48. package/src/plugins/entityAccessControl/EntityAccessControlPlugin.ts +146 -146
  49. package/src/plugins/fileManage/FileManagePlugin.ts +52 -52
  50. package/src/plugins/fileManage/actionHandlers/downloadDocument.ts +65 -65
  51. package/src/plugins/fileManage/actionHandlers/downloadFile.ts +44 -44
  52. package/src/plugins/license/LicensePlugin.ts +79 -0
  53. package/src/plugins/license/LicensePluginTypes.ts +95 -0
  54. package/src/plugins/license/LicenseService.ts +112 -0
  55. package/src/plugins/license/actionHandlers/getLicense.ts +18 -0
  56. package/src/plugins/license/actionHandlers/index.ts +4 -0
  57. package/src/plugins/license/helpers/certHelper.ts +21 -0
  58. package/src/plugins/license/helpers/cryptoHelper.ts +47 -0
  59. package/src/plugins/license/models/index.ts +1 -0
  60. package/src/plugins/license/routes/getLicense.ts +15 -0
  61. package/src/plugins/license/routes/index.ts +3 -0
  62. package/src/plugins/mail/MailPlugin.ts +74 -74
  63. package/src/plugins/mail/MailPluginTypes.ts +27 -27
  64. package/src/plugins/mail/MailService.ts +38 -38
  65. package/src/plugins/mail/actionHandlers/index.ts +3 -3
  66. package/src/plugins/mail/models/index.ts +1 -1
  67. package/src/plugins/mail/routes/index.ts +1 -1
  68. package/src/plugins/metaManage/MetaManagePlugin.ts +504 -504
  69. package/src/plugins/notification/NotificationPlugin.ts +68 -68
  70. package/src/plugins/notification/NotificationPluginTypes.ts +13 -13
  71. package/src/plugins/notification/NotificationService.ts +25 -25
  72. package/src/plugins/notification/actionHandlers/index.ts +3 -3
  73. package/src/plugins/notification/models/Notification.ts +60 -57
  74. package/src/plugins/notification/models/index.ts +3 -3
  75. package/src/plugins/notification/routes/index.ts +1 -1
  76. package/src/plugins/routeManage/RouteManagePlugin.ts +62 -62
  77. package/src/plugins/sequence/SequencePlugin.ts +136 -136
  78. package/src/plugins/sequence/SequencePluginTypes.ts +69 -69
  79. package/src/plugins/sequence/SequenceService.ts +81 -81
  80. package/src/plugins/sequence/actionHandlers/generateSn.ts +32 -32
  81. package/src/plugins/sequence/segments/autoIncrement.ts +78 -78
  82. package/src/plugins/sequence/segments/dayOfMonth.ts +17 -17
  83. package/src/plugins/sequence/segments/literal.ts +14 -14
  84. package/src/plugins/sequence/segments/month.ts +17 -17
  85. package/src/plugins/sequence/segments/parameter.ts +18 -18
  86. package/src/plugins/sequence/segments/year.ts +17 -17
  87. package/src/plugins/setting/SettingPlugin.ts +68 -68
  88. package/src/plugins/setting/SettingPluginTypes.ts +37 -37
  89. package/src/plugins/setting/models/SystemSettingItem.ts +48 -42
  90. package/src/plugins/setting/models/UserSettingItem.ts +55 -49
  91. package/src/plugins/stateMachine/StateMachinePlugin.ts +186 -186
  92. package/src/plugins/stateMachine/StateMachinePluginTypes.ts +48 -48
  93. package/src/plugins/stateMachine/actionHandlers/sendStateMachineEvent.ts +51 -51
  94. package/src/plugins/webhooks/WebhooksPlugin.ts +148 -148
  95. package/src/plugins/webhooks/pluginConfig.ts +75 -75
  96. package/src/queryBuilder/queryBuilder.ts +665 -665
  97. package/src/server.ts +463 -463
  98. package/src/types.ts +701 -701
  99. package/src/utilities/errorUtility.ts +15 -15
  100. package/src/utilities/pathUtility.ts +14 -14
  101. package/src/utilities/typeUtility.ts +15 -11
@@ -1,1718 +1,1718 @@
1
- import {
2
- AddEntityRelationsOptions,
3
- CountEntityOptions,
4
- CreateEntityOptions,
5
- DeleteEntityByIdOptions,
6
- EntityFilterOperators,
7
- EntityFilterOptions,
8
- EntityNonRelationPropertyFilterOptions,
9
- FindEntityByIdOptions,
10
- FindEntityOptions,
11
- FindEntityOrderByOptions,
12
- IRpdDataAccessor,
13
- RemoveEntityRelationsOptions,
14
- RpdDataModel,
15
- RpdDataModelIndex,
16
- RpdDataModelIndexOptions,
17
- RpdDataModelProperty,
18
- UpdateEntityByIdOptions,
19
- FindEntityFindOneRelationEntitiesOptions,
20
- FindEntityFindManyRelationEntitiesOptions,
21
- } from "~/types";
22
- import { isNullOrUndefined } from "~/utilities/typeUtility";
23
- import { mapDbRowToEntity, mapEntityToDbRow } from "./entityMapper";
24
- import { mapPropertyNameToColumnName } from "./propertyMapper";
25
- import { IRpdServer, RapidPlugin } from "~/core/server";
26
- import { getEntityPartChanges } from "~/helpers/entityHelpers";
27
- import {
28
- cloneDeep,
29
- concat,
30
- filter,
31
- find,
32
- first,
33
- forEach,
34
- get,
35
- isArray,
36
- isNumber,
37
- isObject,
38
- isPlainObject,
39
- isString,
40
- isUndefined,
41
- keys,
42
- map,
43
- pick,
44
- reject,
45
- uniq,
46
- } from "lodash";
47
- import {
48
- getEntityPropertiesIncludingBase,
49
- getEntityProperty,
50
- getEntityPropertyByCode,
51
- getEntityPropertyByFieldName,
52
- isManyRelationProperty,
53
- isOneRelationProperty,
54
- isRelationProperty,
55
- } from "../helpers/metaHelper";
56
- import { ColumnSelectOptions, CountRowOptions, FindRowOptions, FindRowOrderByOptions, RowFilterOptions } from "./dataAccessTypes";
57
- import { newEntityOperationError } from "~/utilities/errorUtility";
58
- import { getNowStringWithTimezone } from "~/utilities/timeUtility";
59
- import { RouteContext } from "~/core/routeContext";
60
-
61
- export type FindOneRelationEntitiesOptions = {
62
- server: IRpdServer;
63
- mainModel: RpdDataModel;
64
- relationProperty: RpdDataModelProperty;
65
- relationEntityIds: any[];
66
- selectRelationOptions?: FindEntityFindOneRelationEntitiesOptions;
67
- };
68
-
69
- export type FindManyRelationEntitiesOptions = {
70
- server: IRpdServer;
71
- mainModel: RpdDataModel;
72
- relationProperty: RpdDataModelProperty;
73
- mainEntityIds: any[];
74
- selectRelationOptions?: FindEntityFindManyRelationEntitiesOptions;
75
- };
76
-
77
- function convertEntityOrderByToRowOrderBy(server: IRpdServer, model: RpdDataModel, baseModel?: RpdDataModel, orderByList?: FindEntityOrderByOptions[]) {
78
- if (!orderByList) {
79
- return null;
80
- }
81
-
82
- return orderByList.map((orderBy) => {
83
- const fields = orderBy.field.split(".");
84
- let orderField: string;
85
- let relationField: string;
86
- if (fields.length === 1) {
87
- orderField = fields[0];
88
- } else {
89
- orderField = fields[1];
90
- relationField = fields[0];
91
- }
92
- if (relationField) {
93
- const relationProperty = getEntityPropertyByCode(server, model, relationField);
94
- if (!relationProperty) {
95
- throw new Error(`Property '${relationProperty}' was not found in ${model.namespace}.${model.singularCode}`);
96
- }
97
- if (!isRelationProperty(relationProperty)) {
98
- throw new Error("orderBy[].relation must be a one-relation property.");
99
- }
100
-
101
- if (isManyRelationProperty(relationProperty)) {
102
- throw new Error("orderBy[].relation must be a one-relation property.");
103
- }
104
-
105
- const relationModel = server.getModel({ singularCode: relationProperty.targetSingularCode });
106
- let relationBaseModel: RpdDataModel = null;
107
- if (relationModel.base) {
108
- relationBaseModel = server.getModel({ singularCode: relationModel.base });
109
- }
110
- let property = getEntityPropertyByFieldName(server, relationModel, orderField);
111
- if (!property) {
112
- throw new Error(`Unkown orderBy field '${orderField}' of relation '${relationField}'`);
113
- }
114
-
115
- return {
116
- field: {
117
- name: mapPropertyNameToColumnName(server, relationModel, orderField),
118
- tableName: property.isBaseProperty ? relationBaseModel.tableName : relationModel.tableName,
119
- schema: property.isBaseProperty ? relationBaseModel.schema : relationModel.schema,
120
- },
121
- relationField: {
122
- name: mapPropertyNameToColumnName(server, model, relationField),
123
- tableName: relationProperty.isBaseProperty ? baseModel.tableName : model.tableName,
124
- schema: relationProperty.isBaseProperty ? baseModel.schema : model.schema,
125
- },
126
- desc: !!orderBy.desc,
127
- } as FindRowOrderByOptions;
128
- } else {
129
- let property = getEntityPropertyByFieldName(server, model, orderField);
130
- if (!property) {
131
- throw new Error(`Unkown orderBy field '${orderField}'`);
132
- }
133
-
134
- return {
135
- field: {
136
- name: mapPropertyNameToColumnName(server, model, orderField),
137
- tableName: property.isBaseProperty ? baseModel.tableName : model.tableName,
138
- },
139
- desc: !!orderBy.desc,
140
- } as FindRowOrderByOptions;
141
- }
142
- });
143
- }
144
-
145
- async function findEntities(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: FindEntityOptions) {
146
- const model = dataAccessor.getModel();
147
- let baseModel: RpdDataModel | undefined;
148
- if (model.base) {
149
- baseModel = server.getModel({
150
- singularCode: model.base,
151
- });
152
- }
153
-
154
- let propertiesToSelect: RpdDataModelProperty[];
155
- let relationOptions = options.relations || {};
156
- let relationPropertyCodes = Object.keys(relationOptions) || [];
157
- if (!options.properties || !options.properties.length) {
158
- propertiesToSelect = getEntityPropertiesIncludingBase(server, model).filter((property) => {
159
- return !isRelationProperty(property) || relationPropertyCodes.includes(property.code);
160
- });
161
- } else {
162
- propertiesToSelect = getEntityPropertiesIncludingBase(server, model).filter(
163
- (property) => options.properties.includes(property.code) || relationPropertyCodes.includes(property.code),
164
- );
165
- }
166
-
167
- const columnsToSelect: ColumnSelectOptions[] = [];
168
-
169
- const relationPropertiesToSelect: RpdDataModelProperty[] = [];
170
- forEach(propertiesToSelect, (property) => {
171
- if (isRelationProperty(property)) {
172
- relationPropertiesToSelect.push(property);
173
-
174
- if (property.relation === "one" && !property.linkTableName) {
175
- if (!property.targetIdColumnName) {
176
- throw new Error(`'targetIdColumnName' should be configured for property '${property.code}' of model '${model.namespace}.${model.singularCode}'.`);
177
- }
178
-
179
- if (property.isBaseProperty) {
180
- columnsToSelect.push({
181
- name: property.targetIdColumnName,
182
- tableName: baseModel.tableName,
183
- });
184
- } else {
185
- columnsToSelect.push({
186
- name: property.targetIdColumnName,
187
- tableName: model.tableName,
188
- });
189
- }
190
- }
191
- } else {
192
- if (property.isBaseProperty) {
193
- columnsToSelect.push({
194
- name: property.columnName || property.code,
195
- tableName: baseModel.tableName,
196
- });
197
- } else {
198
- columnsToSelect.push({
199
- name: property.columnName || property.code,
200
- tableName: model.tableName,
201
- });
202
- }
203
- }
204
- });
205
-
206
- // if `keepNonPropertyFields` is true and `properties` are not specified, then select relation columns automatically.
207
- if (options.keepNonPropertyFields && (!options.properties || !options.properties.length)) {
208
- const oneRelationPropertiesWithNoLinkTable = getEntityPropertiesIncludingBase(server, model).filter(
209
- (property) => property.relation === "one" && !property.linkTableName,
210
- );
211
- oneRelationPropertiesWithNoLinkTable.forEach((property) => {
212
- if (property.targetIdColumnName) {
213
- columnsToSelect.push({
214
- name: property.targetIdColumnName,
215
- tableName: property.isBaseProperty ? baseModel.tableName : model.tableName,
216
- });
217
- }
218
- });
219
- }
220
-
221
- if (options.extraColumnsToSelect) {
222
- forEach(options.extraColumnsToSelect, (extraColumnToSelect: ColumnSelectOptions) => {
223
- const columnSelectOptionExists = find(columnsToSelect, (item: ColumnSelectOptions) => {
224
- if (typeof item === "string") {
225
- if (typeof extraColumnToSelect === "string") {
226
- return item === extraColumnToSelect;
227
- } else {
228
- return item == extraColumnToSelect.name;
229
- }
230
- } else {
231
- if (typeof extraColumnToSelect === "string") {
232
- return item.name === extraColumnToSelect;
233
- } else {
234
- return item.name == extraColumnToSelect.name;
235
- }
236
- }
237
- });
238
-
239
- if (!columnSelectOptionExists) {
240
- columnsToSelect.push(extraColumnToSelect);
241
- }
242
- });
243
- }
244
-
245
- const rowFilters = await convertEntityFiltersToRowFilters(server, model, baseModel, options.filters);
246
- const findRowOptions: FindRowOptions = {
247
- filters: rowFilters,
248
- orderBy: convertEntityOrderByToRowOrderBy(server, model, baseModel, options.orderBy),
249
- pagination: options.pagination,
250
- fields: columnsToSelect,
251
- };
252
- const rows = await dataAccessor.find(findRowOptions);
253
- if (!rows.length) {
254
- return [];
255
- }
256
-
257
- const entityIds = rows.map((row) => row.id);
258
- if (relationPropertiesToSelect.length) {
259
- for (const relationProperty of relationPropertiesToSelect) {
260
- const isManyRelation = relationProperty.relation === "many";
261
-
262
- if (relationProperty.linkTableName) {
263
- const relationModel = server.getModel({ singularCode: relationProperty.targetSingularCode! });
264
- if (!relationModel) {
265
- continue;
266
- }
267
-
268
- if (isManyRelation) {
269
- const relationLinks = await findManyRelationLinksViaLinkTable({
270
- server,
271
- mainModel: relationModel,
272
- relationProperty,
273
- mainEntityIds: entityIds,
274
- selectRelationOptions: relationOptions[relationProperty.code],
275
- });
276
-
277
- forEach(rows, (row: any) => {
278
- row[relationProperty.code] = filter(relationLinks, (link: any) => {
279
- return link[relationProperty.selfIdColumnName!] == row["id"];
280
- }).map((link) => mapDbRowToEntity(server, relationModel, link.targetEntity, options.keepNonPropertyFields));
281
- });
282
- }
283
- } else {
284
- let relatedEntities: any[];
285
- if (isManyRelation) {
286
- relatedEntities = await findManyRelatedEntitiesViaIdPropertyCode({
287
- server,
288
- mainModel: model,
289
- relationProperty,
290
- mainEntityIds: entityIds,
291
- selectRelationOptions: relationOptions[relationProperty.code],
292
- });
293
- } else {
294
- const targetEntityIds = uniq(
295
- reject(
296
- map(rows, (entity: any) => entity[relationProperty.targetIdColumnName!]),
297
- isNullOrUndefined,
298
- ),
299
- );
300
- relatedEntities = await findOneRelatedEntitiesViaIdPropertyCode({
301
- server,
302
- mainModel: model,
303
- relationProperty,
304
- relationEntityIds: targetEntityIds,
305
- selectRelationOptions: relationOptions[relationProperty.code],
306
- });
307
- }
308
-
309
- const targetModel = server.getModel({
310
- singularCode: relationProperty.targetSingularCode!,
311
- });
312
- rows.forEach((row) => {
313
- if (isManyRelation) {
314
- row[relationProperty.code] = filter(relatedEntities, (relatedEntity: any) => {
315
- return relatedEntity[relationProperty.selfIdColumnName!] == row.id;
316
- }).map((item) => mapDbRowToEntity(server, targetModel!, item, options.keepNonPropertyFields));
317
- } else {
318
- row[relationProperty.code] = mapDbRowToEntity(
319
- server,
320
- targetModel!,
321
- find(relatedEntities, (relatedEntity: any) => {
322
- // TODO: id property code should be configurable.
323
- return relatedEntity["id"] == row[relationProperty.targetIdColumnName!];
324
- }),
325
- options.keepNonPropertyFields,
326
- );
327
- }
328
- });
329
- }
330
- }
331
- }
332
- const entities = rows.map((item) => mapDbRowToEntity(server, model, item, options.keepNonPropertyFields));
333
-
334
- await server.emitEvent({
335
- eventName: "entity.beforeResponse",
336
- payload: {
337
- namespace: model.namespace,
338
- modelSingularCode: model.singularCode,
339
- baseModelSingularCode: model.base,
340
- entities,
341
- },
342
- sender: null,
343
- routeContext: options.routeContext,
344
- });
345
-
346
- return entities;
347
- }
348
-
349
- async function findEntity(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: FindEntityOptions) {
350
- const entities = await findEntities(server, dataAccessor, {
351
- ...options,
352
- ...{
353
- limit: 1,
354
- },
355
- });
356
- return first(entities);
357
- }
358
-
359
- async function findById(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: FindEntityByIdOptions): Promise<any> {
360
- const { id, properties, relations, keepNonPropertyFields, routeContext } = options;
361
- return await findEntity(server, dataAccessor, {
362
- filters: [
363
- {
364
- operator: "eq",
365
- field: "id",
366
- value: id,
367
- },
368
- ],
369
- properties,
370
- relations,
371
- keepNonPropertyFields,
372
- routeContext,
373
- });
374
- }
375
-
376
- async function convertEntityFiltersToRowFilters(
377
- server: IRpdServer,
378
- model: RpdDataModel,
379
- baseModel: RpdDataModel,
380
- filters: EntityFilterOptions[] | undefined,
381
- ): Promise<RowFilterOptions[]> {
382
- if (!filters || !filters.length) {
383
- return [];
384
- }
385
-
386
- const replacedFilters: RowFilterOptions[] = [];
387
- for (const filter of filters) {
388
- const { operator } = filter;
389
- if (operator === "and" || operator === "or") {
390
- replacedFilters.push({
391
- operator: operator,
392
- filters: await convertEntityFiltersToRowFilters(server, model, baseModel, filter.filters),
393
- });
394
- } else if (operator === "exists" || operator === "notExists") {
395
- const relationProperty: RpdDataModelProperty = getEntityPropertyByCode(server, model, filter.field);
396
- if (!relationProperty) {
397
- throw new Error(`Invalid filters. Property '${filter.field}' was not found in model '${model.namespace}.${model.singularCode}'`);
398
- }
399
- if (!isRelationProperty(relationProperty)) {
400
- throw new Error(
401
- `Invalid filters. Filter with 'existence' operator on property '${filter.field}' is not allowed. You can only use it on an relation property.`,
402
- );
403
- }
404
-
405
- const relatedEntityFilters = filter.filters;
406
- if (!relatedEntityFilters || !relatedEntityFilters.length) {
407
- throw new Error(`Invalid filters. 'filters' must be provided on filter with 'existence' operator.`);
408
- }
409
-
410
- if (relationProperty.relation === "one") {
411
- // Optimize when filtering by id of related entity
412
- if (relatedEntityFilters.length === 1) {
413
- const relatedEntityIdFilter = relatedEntityFilters[0];
414
- if ((relatedEntityIdFilter.operator === "eq" || relatedEntityIdFilter.operator === "in") && relatedEntityIdFilter.field === "id") {
415
- let replacedOperator: EntityFilterOperators;
416
- if (operator === "exists") {
417
- replacedOperator = relatedEntityIdFilter.operator;
418
- } else {
419
- if (relatedEntityIdFilter.operator === "eq") {
420
- replacedOperator = "ne";
421
- } else {
422
- replacedOperator = "notIn";
423
- }
424
- }
425
- replacedFilters.push({
426
- field: {
427
- name: relationProperty.targetIdColumnName!,
428
- tableName: relationProperty.isBaseProperty ? baseModel.tableName : model.tableName,
429
- },
430
- operator: replacedOperator,
431
- value: relatedEntityIdFilter.value,
432
- });
433
- continue;
434
- }
435
- }
436
-
437
- const dataAccessor = server.getDataAccessor({
438
- singularCode: relationProperty.targetSingularCode as string,
439
- });
440
- const relatedModel = dataAccessor.getModel();
441
- let relatedBaseModel: RpdDataModel;
442
- if (relatedModel.base) {
443
- relatedBaseModel = server.getModel({
444
- singularCode: relatedModel.base,
445
- });
446
- }
447
- const rows = await dataAccessor.find({
448
- filters: await convertEntityFiltersToRowFilters(server, relatedModel, relatedBaseModel, filter.filters),
449
- fields: [
450
- {
451
- name: "id",
452
- tableName: relatedModel.tableName,
453
- },
454
- ],
455
- });
456
- const entityIds = map(rows, (entity: any) => entity["id"]);
457
- replacedFilters.push({
458
- field: {
459
- name: relationProperty.targetIdColumnName,
460
- tableName: relationProperty.isBaseProperty ? baseModel.tableName : model.tableName,
461
- },
462
- operator: operator === "exists" ? "in" : "notIn",
463
- value: entityIds,
464
- });
465
- } else if (!relationProperty.linkTableName) {
466
- // many relation without link table.
467
- if (!relationProperty.selfIdColumnName) {
468
- throw new Error(`Invalid filters. 'selfIdColumnName' of property '${relationProperty.code}' was not configured`);
469
- }
470
-
471
- const targetEntityDataAccessor = server.getDataAccessor({
472
- singularCode: relationProperty.targetSingularCode as string,
473
- });
474
- const relatedModel = targetEntityDataAccessor.getModel();
475
- let relatedBaseModel: RpdDataModel;
476
- if (relatedModel.base) {
477
- relatedBaseModel = server.getModel({
478
- singularCode: relatedModel.base,
479
- });
480
- }
481
- const targetEntities = await targetEntityDataAccessor.find({
482
- filters: await convertEntityFiltersToRowFilters(server, relatedModel, relatedBaseModel, filter.filters),
483
- fields: [
484
- {
485
- name: relationProperty.selfIdColumnName,
486
- tableName: relatedModel.tableName,
487
- },
488
- ],
489
- });
490
- const selfEntityIds = map(targetEntities, (entity: any) => entity[relationProperty.selfIdColumnName!]);
491
- replacedFilters.push({
492
- field: {
493
- name: "id",
494
- tableName: model.tableName,
495
- },
496
- operator: operator === "exists" ? "in" : "notIn",
497
- value: selfEntityIds,
498
- });
499
- } else {
500
- // many relation with link table
501
- if (!relationProperty.selfIdColumnName) {
502
- throw new Error(`Invalid filters. 'selfIdColumnName' of property '${relationProperty.code}' was not configured`);
503
- }
504
-
505
- if (!relationProperty.targetIdColumnName) {
506
- throw new Error(`Invalid filters. 'targetIdColumnName' of property '${relationProperty.code}' was not configured`);
507
- }
508
-
509
- // 1. find target entities
510
- // 2. find links
511
- // 3. convert to 'in' filter
512
- const targetEntityDataAccessor = server.getDataAccessor({
513
- singularCode: relationProperty.targetSingularCode as string,
514
- });
515
- const relatedModel = targetEntityDataAccessor.getModel();
516
- let relatedBaseModel: RpdDataModel;
517
- if (relatedModel.base) {
518
- relatedBaseModel = server.getModel({
519
- singularCode: relatedModel.base,
520
- });
521
- }
522
- const targetEntities = await targetEntityDataAccessor.find({
523
- filters: await convertEntityFiltersToRowFilters(server, relatedModel, relatedBaseModel, filter.filters),
524
- fields: [
525
- {
526
- name: "id",
527
- tableName: relatedModel.tableName,
528
- },
529
- ],
530
- });
531
- const targetEntityIds = map(targetEntities, (entity: any) => entity["id"]);
532
-
533
- const command = `SELECT * FROM ${server.queryBuilder.quoteTable({
534
- schema: relationProperty.linkSchema,
535
- tableName: relationProperty.linkTableName!,
536
- })} WHERE ${server.queryBuilder.quoteObject(relationProperty.targetIdColumnName!)} = ANY($1::int[])`;
537
- const params = [targetEntityIds];
538
- const links = await server.queryDatabaseObject(command, params);
539
- const selfEntityIds = links.map((link) => link[relationProperty.selfIdColumnName!]);
540
- replacedFilters.push({
541
- field: {
542
- name: "id",
543
- tableName: model.tableName,
544
- },
545
- operator: operator === "exists" ? "in" : "notIn",
546
- value: selfEntityIds,
547
- });
548
- }
549
- } else {
550
- const filterField = (filter as EntityNonRelationPropertyFilterOptions).field;
551
- let property: RpdDataModelProperty = getEntityPropertyByCode(server, model, filterField);
552
-
553
- let filterValue = (filter as any).value;
554
-
555
- let columnName = "";
556
- if (property) {
557
- if (isOneRelationProperty(property)) {
558
- columnName = property.targetIdColumnName;
559
- if (isPlainObject(filterValue)) {
560
- filterValue = filterValue.id;
561
- }
562
- } else if (isManyRelationProperty(property)) {
563
- throw new Error(`Operator "${operator}" is not supported on many-relation property "${property.code}"`);
564
- } else {
565
- columnName = property.columnName || property.code;
566
- }
567
- } else {
568
- property = getEntityProperty(server, model, (property) => {
569
- return property.columnName === filterField;
570
- });
571
-
572
- if (property) {
573
- columnName = property.columnName;
574
- } else {
575
- property = getEntityProperty(server, model, (property) => {
576
- return property.targetIdColumnName === filterField;
577
- });
578
-
579
- if (property) {
580
- columnName = property.targetIdColumnName;
581
- if (isPlainObject(filterValue)) {
582
- filterValue = filterValue.id;
583
- }
584
- } else {
585
- columnName = filterField;
586
- }
587
- }
588
- }
589
-
590
- // TODO: do not use `any` here
591
- replacedFilters.push({
592
- operator: filter.operator,
593
- field: {
594
- name: columnName,
595
- tableName: property && property.isBaseProperty ? baseModel.tableName : model.tableName,
596
- },
597
- value: filterValue,
598
- itemType: (filter as any).itemType,
599
- } as any);
600
- }
601
- }
602
- return replacedFilters;
603
- }
604
-
605
- async function findManyRelationLinksViaLinkTable(options: FindManyRelationEntitiesOptions) {
606
- const { server, relationProperty, mainModel: relationModel, mainEntityIds, selectRelationOptions } = options;
607
- const command = `SELECT * FROM ${server.queryBuilder.quoteTable({
608
- schema: relationProperty.linkSchema,
609
- tableName: relationProperty.linkTableName!,
610
- })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = ANY($1::int[])
611
- ORDER BY id
612
- `;
613
- const params = [mainEntityIds];
614
- const links = await server.queryDatabaseObject(command, params);
615
- const targetEntityIds = links.map((link) => link[relationProperty.targetIdColumnName!]);
616
-
617
- const dataAccessor = server.getDataAccessor({
618
- namespace: relationModel.namespace,
619
- singularCode: relationModel.singularCode,
620
- });
621
-
622
- const findEntityOptions: FindEntityOptions = {
623
- filters: [
624
- {
625
- field: "id",
626
- operator: "in",
627
- value: targetEntityIds,
628
- },
629
- ],
630
- keepNonPropertyFields: true,
631
- };
632
-
633
- if (selectRelationOptions) {
634
- if (typeof selectRelationOptions !== "boolean") {
635
- if (selectRelationOptions.properties) {
636
- findEntityOptions.properties = ["id", ...selectRelationOptions.properties];
637
- }
638
- if (selectRelationOptions.relations) {
639
- findEntityOptions.relations = selectRelationOptions.relations;
640
- }
641
- if (selectRelationOptions.orderBy) {
642
- findEntityOptions.orderBy = selectRelationOptions.orderBy;
643
- }
644
- if (selectRelationOptions.pagination) {
645
- findEntityOptions.pagination = selectRelationOptions.pagination;
646
- }
647
- if (selectRelationOptions.filters) {
648
- findEntityOptions.filters = [...findEntityOptions.filters, ...selectRelationOptions.filters];
649
- }
650
- if (!isUndefined(selectRelationOptions.keepNonPropertyFields)) {
651
- findEntityOptions.keepNonPropertyFields = selectRelationOptions.keepNonPropertyFields;
652
- }
653
- }
654
- }
655
-
656
- const targetEntities = await findEntities(server, dataAccessor, findEntityOptions);
657
-
658
- forEach(links, (link: any) => {
659
- link.targetEntity = find(targetEntities, (e: any) => e["id"] == link[relationProperty.targetIdColumnName!]);
660
- });
661
-
662
- return links;
663
- }
664
-
665
- async function findManyRelatedEntitiesViaIdPropertyCode(options: FindManyRelationEntitiesOptions) {
666
- const { server, relationProperty, mainEntityIds, selectRelationOptions } = options;
667
- const dataAccessor = server.getDataAccessor({
668
- singularCode: relationProperty.targetSingularCode as string,
669
- });
670
-
671
- const findEntityOptions: FindEntityOptions = {
672
- filters: [
673
- {
674
- field: relationProperty.selfIdColumnName,
675
- operator: "in",
676
- value: mainEntityIds,
677
- },
678
- ],
679
- orderBy: [
680
- {
681
- field: "id",
682
- },
683
- ],
684
- extraColumnsToSelect: [relationProperty.selfIdColumnName],
685
- keepNonPropertyFields: true,
686
- };
687
-
688
- if (selectRelationOptions) {
689
- if (typeof selectRelationOptions !== "boolean") {
690
- if (selectRelationOptions.properties) {
691
- findEntityOptions.properties = ["id", ...selectRelationOptions.properties];
692
- }
693
- if (selectRelationOptions.relations) {
694
- findEntityOptions.relations = selectRelationOptions.relations;
695
- }
696
- if (selectRelationOptions.orderBy) {
697
- findEntityOptions.orderBy = selectRelationOptions.orderBy;
698
- }
699
- if (selectRelationOptions.pagination) {
700
- findEntityOptions.pagination = selectRelationOptions.pagination;
701
- }
702
- if (selectRelationOptions.filters) {
703
- findEntityOptions.filters = [...findEntityOptions.filters, ...selectRelationOptions.filters];
704
- }
705
- if (!isUndefined(selectRelationOptions.keepNonPropertyFields)) {
706
- findEntityOptions.keepNonPropertyFields = selectRelationOptions.keepNonPropertyFields;
707
- }
708
- }
709
- }
710
-
711
- return await findEntities(server, dataAccessor, findEntityOptions);
712
- }
713
-
714
- async function findOneRelatedEntitiesViaIdPropertyCode(options: FindOneRelationEntitiesOptions) {
715
- const { server, relationProperty, relationEntityIds, selectRelationOptions } = options;
716
-
717
- const dataAccessor = server.getDataAccessor({
718
- singularCode: relationProperty.targetSingularCode as string,
719
- });
720
-
721
- const findEntityOptions: FindEntityOptions = {
722
- filters: [
723
- {
724
- field: "id",
725
- operator: "in",
726
- value: relationEntityIds,
727
- },
728
- ],
729
- keepNonPropertyFields: true,
730
- };
731
-
732
- if (selectRelationOptions) {
733
- if (typeof selectRelationOptions !== "boolean") {
734
- if (selectRelationOptions.properties) {
735
- findEntityOptions.properties = ["id", ...selectRelationOptions.properties];
736
- }
737
- if (selectRelationOptions.relations) {
738
- findEntityOptions.relations = selectRelationOptions.relations;
739
- }
740
- }
741
- }
742
-
743
- return await findEntities(server, dataAccessor, findEntityOptions);
744
- }
745
-
746
- async function createEntity(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: CreateEntityOptions, plugin?: RapidPlugin) {
747
- const model = dataAccessor.getModel();
748
- if (model.derivedTypePropertyCode) {
749
- throw newEntityOperationError("Create base entity directly is not allowed.");
750
- }
751
-
752
- const { entity, routeContext } = options;
753
-
754
- const userId = options.routeContext?.state?.userId;
755
- if (userId) {
756
- const createdByProperty = getEntityPropertyByCode(server, model, "createdBy");
757
- if (createdByProperty) {
758
- entity.createdBy = userId;
759
- }
760
- }
761
- const createdAtProperty = getEntityPropertyByCode(server, model, "createdAt");
762
- if (createdAtProperty) {
763
- entity.createdAt = getNowStringWithTimezone();
764
- }
765
-
766
- await server.beforeCreateEntity(model, options);
767
-
768
- await server.emitEvent({
769
- eventName: "entity.beforeCreate",
770
- payload: {
771
- namespace: model.namespace,
772
- modelSingularCode: model.singularCode,
773
- baseModelSingularCode: model.base,
774
- before: entity,
775
- },
776
- sender: plugin,
777
- routeContext,
778
- });
779
-
780
- // check unique constraints
781
- if (!options.postponeUniquenessCheck) {
782
- if (model.indexes && model.indexes.length) {
783
- for (const indexConfig of model.indexes) {
784
- if (!indexConfig.unique) {
785
- continue;
786
- }
787
-
788
- const duplicate = await willEntityDuplicate(server, dataAccessor, {
789
- routeContext: options.routeContext,
790
- entityToSave: entity,
791
- indexConfig,
792
- });
793
- if (duplicate) {
794
- throw new Error(getEntityDuplicatedErrorMessage(server, model, indexConfig));
795
- }
796
- }
797
- }
798
- }
799
-
800
- const oneRelationPropertiesToCreate: RpdDataModelProperty[] = [];
801
- const manyRelationPropertiesToCreate: RpdDataModelProperty[] = [];
802
- keys(entity).forEach((propertyCode) => {
803
- const property = getEntityPropertyByCode(server, model, propertyCode);
804
- if (!property) {
805
- // Unknown property
806
- return;
807
- }
808
-
809
- if (isRelationProperty(property)) {
810
- if (property.relation === "many") {
811
- manyRelationPropertiesToCreate.push(property);
812
- } else {
813
- oneRelationPropertiesToCreate.push(property);
814
- }
815
- }
816
- });
817
-
818
- const { row, baseRow } = mapEntityToDbRow(server, model, entity);
819
-
820
- const newEntityOneRelationProps = {};
821
- // save one-relation properties
822
- for (const property of oneRelationPropertiesToCreate) {
823
- const rowToBeSaved = property.isBaseProperty ? baseRow : row;
824
- const fieldValue = entity[property.code];
825
- const targetDataAccessor = server.getDataAccessor({
826
- singularCode: property.targetSingularCode!,
827
- });
828
- if (isObject(fieldValue)) {
829
- const targetEntityId = fieldValue["id"];
830
- if (!targetEntityId) {
831
- if (!property.selfIdColumnName) {
832
- const targetEntity = fieldValue;
833
- const newTargetEntity = await createEntity(server, targetDataAccessor, {
834
- routeContext,
835
- entity: targetEntity,
836
- });
837
- newEntityOneRelationProps[property.code] = newTargetEntity;
838
- rowToBeSaved[property.targetIdColumnName!] = newTargetEntity["id"];
839
- }
840
- } else {
841
- const targetEntity = await findById(server, targetDataAccessor, {
842
- id: targetEntityId,
843
- routeContext,
844
- });
845
- if (!targetEntity) {
846
- throw newEntityOperationError(
847
- `Create ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${targetEntityId}' was not found.`,
848
- );
849
- }
850
- newEntityOneRelationProps[property.code] = targetEntity;
851
- rowToBeSaved[property.targetIdColumnName!] = targetEntityId;
852
- }
853
- } else if (isNumber(fieldValue) || isString(fieldValue)) {
854
- // fieldValue is id;
855
- const targetEntityId = fieldValue;
856
- const targetEntity = await findById(server, targetDataAccessor, {
857
- id: targetEntityId,
858
- routeContext,
859
- });
860
- if (!targetEntity) {
861
- throw newEntityOperationError(
862
- `Create ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${targetEntityId}' was not found.`,
863
- );
864
- }
865
- newEntityOneRelationProps[property.code] = targetEntity;
866
- rowToBeSaved[property.targetIdColumnName!] = targetEntityId;
867
- } else {
868
- newEntityOneRelationProps[property.code] = null;
869
- rowToBeSaved[property.targetIdColumnName!] = null;
870
- }
871
- }
872
-
873
- let newBaseRow: any;
874
- let baseDataAccessor: any;
875
- if (model.base) {
876
- baseDataAccessor = server.getDataAccessor({
877
- singularCode: model.base,
878
- });
879
- newBaseRow = await baseDataAccessor.create(baseRow);
880
-
881
- row.id = newBaseRow.id;
882
- }
883
- const newRow = await dataAccessor.create(row);
884
- const newEntity = mapDbRowToEntity(server, model, Object.assign({}, newBaseRow, newRow, newEntityOneRelationProps), true);
885
-
886
- // save one-relation properties that has selfIdColumnName
887
- for (const property of oneRelationPropertiesToCreate) {
888
- const fieldValue = entity[property.code];
889
- const targetDataAccessor = server.getDataAccessor({
890
- singularCode: property.targetSingularCode!,
891
- });
892
- if (isObject(fieldValue)) {
893
- const targetEntityId = fieldValue["id"];
894
- if (!targetEntityId) {
895
- if (property.selfIdColumnName) {
896
- const targetEntity = fieldValue;
897
- targetEntity[property.selfIdColumnName] = newEntity.id;
898
- const newTargetEntity = await createEntity(server, targetDataAccessor, {
899
- routeContext,
900
- entity: targetEntity,
901
- });
902
-
903
- let dataAccessorOfMainEntity = dataAccessor;
904
- if (property.isBaseProperty) {
905
- dataAccessorOfMainEntity = baseDataAccessor;
906
- }
907
-
908
- const relationFieldChanges = {
909
- [property.targetIdColumnName]: newTargetEntity.id,
910
- };
911
- await dataAccessorOfMainEntity.updateById(newEntity.id, relationFieldChanges);
912
- newEntity[property.code] = newTargetEntity;
913
- }
914
- }
915
- }
916
- }
917
-
918
- // save many-relation properties
919
- for (const property of manyRelationPropertiesToCreate) {
920
- newEntity[property.code] = [];
921
-
922
- const targetDataAccessor = server.getDataAccessor({
923
- singularCode: property.targetSingularCode!,
924
- });
925
-
926
- const relatedEntitiesToBeSaved = entity[property.code];
927
- if (!isArray(relatedEntitiesToBeSaved)) {
928
- throw new Error(`Value of field '${property.code}' should be an array.`);
929
- }
930
-
931
- for (const relatedEntityToBeSaved of relatedEntitiesToBeSaved) {
932
- let relatedEntityId: any;
933
- if (isObject(relatedEntityToBeSaved)) {
934
- relatedEntityId = relatedEntityToBeSaved["id"];
935
- if (!relatedEntityId) {
936
- // related entity is to be created
937
- const targetEntity = relatedEntityToBeSaved;
938
- if (!property.linkTableName) {
939
- targetEntity[property.selfIdColumnName!] = newEntity.id;
940
- }
941
- const newTargetEntity = await createEntity(server, targetDataAccessor, {
942
- routeContext,
943
- entity: targetEntity,
944
- });
945
-
946
- if (property.linkTableName) {
947
- const command = `INSERT INTO ${server.queryBuilder.quoteTable({
948
- schema: property.linkSchema,
949
- tableName: property.linkTableName,
950
- })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
951
- const params = [newEntity.id, newTargetEntity.id];
952
- await server.queryDatabaseObject(command, params);
953
- }
954
-
955
- newEntity[property.code].push(newTargetEntity);
956
- } else {
957
- // related entity is existed
958
- const targetEntity = await targetDataAccessor.findById(relatedEntityId);
959
- if (!targetEntity) {
960
- throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
961
- }
962
-
963
- if (property.linkTableName) {
964
- const command = `INSERT INTO ${server.queryBuilder.quoteTable({
965
- schema: property.linkSchema,
966
- tableName: property.linkTableName,
967
- })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
968
- const params = [newEntity.id, relatedEntityId];
969
- await server.queryDatabaseObject(command, params);
970
- } else {
971
- await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: newEntity.id });
972
- targetEntity[property.selfIdColumnName!] = newEntity.id;
973
- }
974
- newEntity[property.code].push(targetEntity);
975
- }
976
- } else {
977
- // fieldValue is id
978
- relatedEntityId = relatedEntityToBeSaved;
979
- const targetEntity = await targetDataAccessor.findById(relatedEntityId);
980
- if (!targetEntity) {
981
- throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
982
- }
983
-
984
- if (property.linkTableName) {
985
- const command = `INSERT INTO ${server.queryBuilder.quoteTable({
986
- schema: property.linkSchema,
987
- tableName: property.linkTableName,
988
- })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
989
- const params = [newEntity.id, relatedEntityId];
990
- await server.queryDatabaseObject(command, params);
991
- } else {
992
- await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: newEntity.id });
993
- targetEntity[property.selfIdColumnName!] = newEntity.id;
994
- }
995
-
996
- newEntity[property.code].push(targetEntity);
997
- }
998
- }
999
- }
1000
-
1001
- await server.emitEvent({
1002
- eventName: "entity.create",
1003
- payload: {
1004
- namespace: model.namespace,
1005
- modelSingularCode: model.singularCode,
1006
- baseModelSingularCode: model.base,
1007
- after: newEntity,
1008
- },
1009
- sender: plugin,
1010
- routeContext,
1011
- });
1012
-
1013
- return newEntity;
1014
- }
1015
-
1016
- async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: UpdateEntityByIdOptions, plugin?: RapidPlugin) {
1017
- const model = dataAccessor.getModel();
1018
- const { id, routeContext } = options;
1019
- if (!id) {
1020
- throw new Error("Id is required when updating an entity.");
1021
- }
1022
-
1023
- const entity = await findById(server, dataAccessor, {
1024
- routeContext,
1025
- id,
1026
- keepNonPropertyFields: true,
1027
- });
1028
- if (!entity) {
1029
- throw new Error(`${model.namespace}.${model.singularCode} with id "${id}" was not found.`);
1030
- }
1031
-
1032
- let { entityToSave } = options;
1033
- let changes = getEntityPartChanges(server, model, entity, entityToSave);
1034
- if (!changes && !options.operation) {
1035
- return entity;
1036
- }
1037
-
1038
- entityToSave = changes || {};
1039
-
1040
- const userId = options.routeContext?.state?.userId;
1041
- if (userId) {
1042
- const updatedByProperty = getEntityPropertyByCode(server, model, "updatedBy");
1043
- if (updatedByProperty) {
1044
- entityToSave.updatedBy = userId;
1045
- }
1046
- }
1047
- const updatedAtProperty = getEntityPropertyByCode(server, model, "updatedAt");
1048
- if (updatedAtProperty) {
1049
- entityToSave.updatedAt = getNowStringWithTimezone();
1050
- }
1051
-
1052
- await server.beforeUpdateEntity(model, options, entity);
1053
-
1054
- await server.emitEvent({
1055
- eventName: "entity.beforeUpdate",
1056
- payload: {
1057
- namespace: model.namespace,
1058
- modelSingularCode: model.singularCode,
1059
- before: entity,
1060
- changes: entityToSave,
1061
- operation: options.operation,
1062
- stateProperties: options.stateProperties,
1063
- },
1064
- sender: plugin,
1065
- routeContext: options.routeContext,
1066
- });
1067
-
1068
- changes = getEntityPartChanges(server, model, entity, entityToSave);
1069
-
1070
- // check readonly properties
1071
- Object.keys(changes).forEach((propertyName) => {
1072
- let isReadonlyProperty = false;
1073
- const property = getEntityPropertyByCode(server, model, propertyName);
1074
- if (property && property.readonly) {
1075
- isReadonlyProperty = true;
1076
- } else {
1077
- const oneRelationProperty = getEntityProperty(server, model, (item) => item.relation === "one" && item.targetIdColumnName === propertyName);
1078
- if (oneRelationProperty && oneRelationProperty.readonly) {
1079
- isReadonlyProperty = true;
1080
- }
1081
- }
1082
- if (isReadonlyProperty) {
1083
- throw new Error(`Updating "${property.name}" property is not allowed because it's readonly.`);
1084
- }
1085
- });
1086
-
1087
- // check unique constraints
1088
- if (!options.postponeUniquenessCheck) {
1089
- if (model.indexes && model.indexes.length) {
1090
- for (const indexConfig of model.indexes) {
1091
- if (!indexConfig.unique) {
1092
- continue;
1093
- }
1094
-
1095
- const duplicate = await willEntityDuplicate(server, dataAccessor, {
1096
- routeContext: options.routeContext,
1097
- entityId: id,
1098
- entityToSave: changes,
1099
- indexConfig,
1100
- });
1101
- if (duplicate) {
1102
- throw new Error(getEntityDuplicatedErrorMessage(server, model, indexConfig));
1103
- }
1104
- }
1105
- }
1106
- }
1107
-
1108
- const oneRelationPropertiesToUpdate: RpdDataModelProperty[] = [];
1109
- const manyRelationPropertiesToUpdate: RpdDataModelProperty[] = [];
1110
- keys(changes).forEach((propertyCode) => {
1111
- const property = getEntityPropertyByCode(server, model, propertyCode);
1112
- if (!property) {
1113
- // Unknown property
1114
- return;
1115
- }
1116
-
1117
- if (isRelationProperty(property)) {
1118
- if (property.relation === "many") {
1119
- manyRelationPropertiesToUpdate.push(property);
1120
- } else {
1121
- oneRelationPropertiesToUpdate.push(property);
1122
- }
1123
- }
1124
- });
1125
-
1126
- const { row, baseRow } = mapEntityToDbRow(server, model, changes);
1127
-
1128
- const updatedEntityOneRelationProps = {};
1129
- for (const property of oneRelationPropertiesToUpdate) {
1130
- const rowToBeSaved = property.isBaseProperty ? baseRow : row;
1131
- const relatedEntityToBeSaved = changes[property.code];
1132
- const targetDataAccessor = server.getDataAccessor({
1133
- singularCode: property.targetSingularCode!,
1134
- });
1135
-
1136
- if (isObject(relatedEntityToBeSaved)) {
1137
- const relatedEntityId = relatedEntityToBeSaved["id"];
1138
- if (!relatedEntityId) {
1139
- if (!property.selfIdColumnName) {
1140
- const targetEntity = relatedEntityToBeSaved;
1141
- const newTargetEntity = await createEntity(server, targetDataAccessor, {
1142
- routeContext,
1143
- entity: targetEntity,
1144
- });
1145
- updatedEntityOneRelationProps[property.code] = newTargetEntity;
1146
- rowToBeSaved[property.targetIdColumnName!] = newTargetEntity["id"];
1147
- }
1148
- } else {
1149
- let targetEntity = await findById(server, targetDataAccessor, {
1150
- id: relatedEntityId,
1151
- routeContext,
1152
- });
1153
- if (!targetEntity) {
1154
- throw newEntityOperationError(
1155
- `Update ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${relatedEntityId}' was not found.`,
1156
- );
1157
- }
1158
-
1159
- // update relation entity if options.relationPropertiesToUpdate is specified.
1160
- const updateRelationPropertiesOptions = get(options.relationPropertiesToUpdate, property.code);
1161
- let subRelationPropertiesToUpdate = undefined;
1162
- let relationEntityToUpdate = null;
1163
- if (updateRelationPropertiesOptions === true) {
1164
- relationEntityToUpdate = targetEntity;
1165
- } else if (updateRelationPropertiesOptions) {
1166
- let propertiesToUpdate = uniq([
1167
- "id",
1168
- ...(updateRelationPropertiesOptions.propertiesToUpdate || []),
1169
- ...Object.keys(updateRelationPropertiesOptions.relationPropertiesToUpdate || []),
1170
- ]);
1171
- relationEntityToUpdate = pick(relatedEntityToBeSaved, propertiesToUpdate);
1172
- subRelationPropertiesToUpdate = updateRelationPropertiesOptions.relationPropertiesToUpdate;
1173
- }
1174
- if (relationEntityToUpdate) {
1175
- targetEntity = await updateEntityById(server, targetDataAccessor, {
1176
- routeContext: routeContext,
1177
- id: relatedEntityId,
1178
- entityToSave: relationEntityToUpdate,
1179
- relationPropertiesToUpdate: subRelationPropertiesToUpdate,
1180
- });
1181
- }
1182
-
1183
- updatedEntityOneRelationProps[property.code] = targetEntity;
1184
- rowToBeSaved[property.targetIdColumnName!] = relatedEntityId;
1185
- }
1186
- } else if (isNumber(relatedEntityToBeSaved) || isString(relatedEntityToBeSaved)) {
1187
- // fieldValue is id;
1188
- const targetEntityId = relatedEntityToBeSaved;
1189
- const targetEntity = await findById(server, targetDataAccessor, {
1190
- id: targetEntityId,
1191
- routeContext,
1192
- });
1193
- if (!targetEntity) {
1194
- throw newEntityOperationError(
1195
- `Create ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${targetEntityId}' was not found.`,
1196
- );
1197
- }
1198
- updatedEntityOneRelationProps[property.code] = targetEntity;
1199
- rowToBeSaved[property.targetIdColumnName!] = targetEntityId;
1200
- } else {
1201
- updatedEntityOneRelationProps[property.code] = null;
1202
- rowToBeSaved[property.targetIdColumnName!] = null;
1203
- }
1204
- }
1205
-
1206
- let updatedRow = row;
1207
- if (Object.keys(row).length) {
1208
- updatedRow = await dataAccessor.updateById(id, row);
1209
- }
1210
- let updatedBaseRow = baseRow;
1211
- let baseDataAccessor: any;
1212
- if (model.base) {
1213
- baseDataAccessor = server.getDataAccessor({
1214
- singularCode: model.base,
1215
- });
1216
- if (Object.keys(baseRow).length) {
1217
- updatedBaseRow = await baseDataAccessor.updateById(id, updatedBaseRow);
1218
- }
1219
- }
1220
-
1221
- let updatedEntity = mapDbRowToEntity(server, model, { ...updatedRow, ...updatedBaseRow, ...updatedEntityOneRelationProps }, true);
1222
- updatedEntity = Object.assign({}, entity, updatedEntity);
1223
-
1224
- // save one-relation properties that has selfIdColumnName
1225
- for (const property of oneRelationPropertiesToUpdate) {
1226
- const fieldValue = changes[property.code];
1227
- const targetDataAccessor = server.getDataAccessor({
1228
- singularCode: property.targetSingularCode!,
1229
- });
1230
- if (isObject(fieldValue)) {
1231
- const targetEntityId = fieldValue["id"];
1232
- if (!targetEntityId) {
1233
- if (property.selfIdColumnName) {
1234
- const targetEntity = fieldValue;
1235
- targetEntity[property.selfIdColumnName] = updatedEntity.id;
1236
- const newTargetEntity = await createEntity(server, targetDataAccessor, {
1237
- routeContext,
1238
- entity: targetEntity,
1239
- });
1240
-
1241
- let dataAccessorOfMainEntity = dataAccessor;
1242
- if (property.isBaseProperty) {
1243
- dataAccessorOfMainEntity = baseDataAccessor;
1244
- }
1245
-
1246
- const relationFieldChanges = {
1247
- [property.targetIdColumnName]: newTargetEntity.id,
1248
- };
1249
- await dataAccessorOfMainEntity.updateById(updatedEntity.id, relationFieldChanges);
1250
- updatedEntity[property.code] = newTargetEntity;
1251
- changes[property.code] = newTargetEntity;
1252
- }
1253
- }
1254
- }
1255
- }
1256
-
1257
- // save many-relation properties
1258
- for (const property of manyRelationPropertiesToUpdate) {
1259
- const relatedEntities: any[] = [];
1260
- const targetDataAccessor = server.getDataAccessor({
1261
- singularCode: property.targetSingularCode!,
1262
- });
1263
-
1264
- const relatedEntitiesToBeSaved = changes[property.code];
1265
- if (!isArray(relatedEntitiesToBeSaved)) {
1266
- throw new Error(`Value of field '${property.code}' should be an array.`);
1267
- }
1268
-
1269
- const targetIdsToKeep = [];
1270
- for (const relatedEntityToBeSaved of relatedEntitiesToBeSaved) {
1271
- let relatedEntityId: any;
1272
- if (isObject(relatedEntityToBeSaved)) {
1273
- relatedEntityId = relatedEntityToBeSaved["id"];
1274
- } else {
1275
- relatedEntityId = relatedEntityToBeSaved;
1276
- }
1277
- if (relatedEntityId) {
1278
- targetIdsToKeep.push(relatedEntityId);
1279
- }
1280
- }
1281
-
1282
- let currentTargetIds: any[] = [];
1283
- if (property.linkTableName) {
1284
- const targetLinks = await server.queryDatabaseObject(
1285
- `SELECT ${server.queryBuilder.quoteObject(property.targetIdColumnName)} FROM ${server.queryBuilder.quoteTable({
1286
- schema: property.linkSchema,
1287
- tableName: property.linkTableName,
1288
- })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName!)} = $1`,
1289
- [id],
1290
- );
1291
- currentTargetIds = targetLinks.map((item) => item[property.targetIdColumnName]);
1292
-
1293
- await server.queryDatabaseObject(
1294
- `DELETE FROM ${server.queryBuilder.quoteTable({
1295
- schema: property.linkSchema,
1296
- tableName: property.linkTableName,
1297
- })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName!)} = $1
1298
- AND ${server.queryBuilder.quoteObject(property.targetIdColumnName!)} <> ALL($2::int[])`,
1299
- [id, targetIdsToKeep],
1300
- );
1301
- } else {
1302
- const targetModel = server.getModel({
1303
- singularCode: property.targetSingularCode,
1304
- });
1305
- const targetRows = await server.queryDatabaseObject(
1306
- `SELECT id FROM ${server.queryBuilder.quoteTable({
1307
- schema: targetModel.schema,
1308
- tableName: targetModel.tableName,
1309
- })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName!)} = $1`,
1310
- [id],
1311
- );
1312
- currentTargetIds = targetRows.map((item) => item.id);
1313
- }
1314
-
1315
- for (const relatedEntityToBeSaved of relatedEntitiesToBeSaved) {
1316
- let relatedEntityId: any;
1317
- if (isObject(relatedEntityToBeSaved)) {
1318
- relatedEntityId = relatedEntityToBeSaved["id"];
1319
- if (!relatedEntityId) {
1320
- // related entity is to be created
1321
- const targetEntity = relatedEntityToBeSaved;
1322
- if (!property.linkTableName) {
1323
- targetEntity[property.selfIdColumnName!] = id;
1324
- }
1325
- const newTargetEntity = await createEntity(server, targetDataAccessor, {
1326
- routeContext,
1327
- entity: targetEntity,
1328
- });
1329
-
1330
- if (property.linkTableName) {
1331
- const command = `INSERT INTO ${server.queryBuilder.quoteTable({
1332
- schema: property.linkSchema,
1333
- tableName: property.linkTableName,
1334
- })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1335
- const params = [id, newTargetEntity.id];
1336
- await server.queryDatabaseObject(command, params);
1337
- }
1338
-
1339
- relatedEntities.push(newTargetEntity);
1340
- } else {
1341
- // related entity is existed
1342
- let targetEntity = await targetDataAccessor.findById(relatedEntityId);
1343
- if (!targetEntity) {
1344
- throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' does not exist.`);
1345
- }
1346
-
1347
- // update relation entity if options.relationPropertiesToUpdate is specified.
1348
- const updateRelationPropertiesOptions = get(options.relationPropertiesToUpdate, property.code);
1349
- let subRelationPropertiesToUpdate = undefined;
1350
- let relationEntityToUpdate = null;
1351
- if (updateRelationPropertiesOptions === true) {
1352
- relationEntityToUpdate = targetEntity;
1353
- } else if (updateRelationPropertiesOptions) {
1354
- let propertiesToUpdate = uniq([
1355
- "id",
1356
- ...(updateRelationPropertiesOptions.propertiesToUpdate || []),
1357
- ...Object.keys(updateRelationPropertiesOptions.relationPropertiesToUpdate || []),
1358
- ]);
1359
- relationEntityToUpdate = pick(relatedEntityToBeSaved, propertiesToUpdate);
1360
- subRelationPropertiesToUpdate = updateRelationPropertiesOptions.relationPropertiesToUpdate;
1361
- }
1362
- if (relationEntityToUpdate) {
1363
- targetEntity = await updateEntityById(server, targetDataAccessor, {
1364
- routeContext: routeContext,
1365
- id: relatedEntityId,
1366
- entityToSave: relationEntityToUpdate,
1367
- relationPropertiesToUpdate: subRelationPropertiesToUpdate,
1368
- });
1369
- }
1370
-
1371
- if (!currentTargetIds.includes(relatedEntityId)) {
1372
- if (property.linkTableName) {
1373
- const command = `INSERT INTO ${server.queryBuilder.quoteTable({
1374
- schema: property.linkSchema,
1375
- tableName: property.linkTableName,
1376
- })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1377
- const params = [id, relatedEntityId];
1378
- await server.queryDatabaseObject(command, params);
1379
- } else {
1380
- await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: id });
1381
- targetEntity[property.selfIdColumnName!] = id;
1382
- }
1383
- }
1384
- relatedEntities.push(targetEntity);
1385
- }
1386
- } else {
1387
- // fieldValue is id
1388
- relatedEntityId = relatedEntityToBeSaved;
1389
- const targetEntity = await targetDataAccessor.findById(relatedEntityId);
1390
- if (!targetEntity) {
1391
- throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
1392
- }
1393
-
1394
- if (!currentTargetIds.includes(relatedEntityId)) {
1395
- if (property.linkTableName) {
1396
- const command = `INSERT INTO ${server.queryBuilder.quoteTable({
1397
- schema: property.linkSchema,
1398
- tableName: property.linkTableName,
1399
- })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1400
- const params = [id, relatedEntityId];
1401
- await server.queryDatabaseObject(command, params);
1402
- } else {
1403
- await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: id });
1404
- targetEntity[property.selfIdColumnName!] = id;
1405
- }
1406
- }
1407
-
1408
- relatedEntities.push(targetEntity);
1409
- }
1410
- }
1411
- updatedEntity[property.code] = relatedEntities;
1412
- }
1413
-
1414
- await server.emitEvent({
1415
- eventName: "entity.update",
1416
- payload: {
1417
- namespace: model.namespace,
1418
- modelSingularCode: model.singularCode,
1419
- // TODO: should not emit event on base model if it's not effected.
1420
- baseModelSingularCode: model.base,
1421
- before: entity,
1422
- after: updatedEntity,
1423
- changes: changes,
1424
- operation: options.operation,
1425
- stateProperties: options.stateProperties,
1426
- },
1427
- sender: plugin,
1428
- routeContext: options.routeContext,
1429
- });
1430
-
1431
- return updatedEntity;
1432
- }
1433
-
1434
- export type CheckEntityDuplicatedOptions = {
1435
- routeContext?: RouteContext;
1436
- entityId?: number;
1437
- entityToSave: any;
1438
- indexConfig: RpdDataModelIndex;
1439
- };
1440
-
1441
- async function willEntityDuplicate(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: CheckEntityDuplicatedOptions): Promise<boolean> {
1442
- const { entityId, entityToSave, routeContext, indexConfig } = options;
1443
-
1444
- let filters: EntityFilterOptions[] = [];
1445
- if (indexConfig.conditions) {
1446
- filters = cloneDeep(indexConfig.conditions);
1447
- }
1448
-
1449
- for (const propConfig of indexConfig.properties) {
1450
- let propCode: string;
1451
- if (isString(propConfig)) {
1452
- propCode = propConfig;
1453
- } else {
1454
- propCode = propConfig.code;
1455
- }
1456
-
1457
- if (!entityToSave.hasOwnProperty(propCode)) {
1458
- // skip duplicate checking when any index prop missing in entityToSave.
1459
- return false;
1460
- }
1461
-
1462
- filters.push({
1463
- operator: "eq",
1464
- field: propCode,
1465
- value: entityToSave[propCode],
1466
- });
1467
- }
1468
-
1469
- const entityInDb = await findEntity(server, dataAccessor, {
1470
- filters,
1471
- routeContext,
1472
- });
1473
-
1474
- if (entityId) {
1475
- return entityInDb && entityInDb.id !== entityId;
1476
- } else {
1477
- return !!entityInDb;
1478
- }
1479
- }
1480
-
1481
- function getEntityDuplicatedErrorMessage(server: IRpdServer, model: RpdDataModel, indexConfig: RpdDataModelIndex) {
1482
- if (indexConfig.duplicateErrorMessage) {
1483
- return indexConfig.duplicateErrorMessage;
1484
- }
1485
-
1486
- const propertyNames = indexConfig.properties.map((propConfig) => {
1487
- let propCode: string;
1488
- if (isString(propConfig)) {
1489
- propCode = propConfig;
1490
- } else {
1491
- propCode = propConfig.code;
1492
- }
1493
- const prop = getEntityPropertyByCode(server, model, propCode);
1494
- return prop.name;
1495
- });
1496
-
1497
- return `已存在 ${propertyNames.join(", ")} 相同的记录。`;
1498
- }
1499
-
1500
- export default class EntityManager<TEntity = any> {
1501
- #server: IRpdServer;
1502
- #dataAccessor: IRpdDataAccessor;
1503
-
1504
- constructor(server: IRpdServer, dataAccessor: IRpdDataAccessor) {
1505
- this.#server = server;
1506
- this.#dataAccessor = dataAccessor;
1507
- }
1508
-
1509
- getModel(): RpdDataModel {
1510
- return this.#dataAccessor.getModel();
1511
- }
1512
-
1513
- async findEntities(options: FindEntityOptions): Promise<TEntity[]> {
1514
- return await findEntities(this.#server, this.#dataAccessor, options);
1515
- }
1516
-
1517
- async findEntity(options: FindEntityOptions): Promise<TEntity | null> {
1518
- return await findEntity(this.#server, this.#dataAccessor, options);
1519
- }
1520
-
1521
- async findById(options: FindEntityByIdOptions | string | number): Promise<TEntity | null> {
1522
- // options is id
1523
- if (!isObject(options)) {
1524
- options = {
1525
- id: options,
1526
- };
1527
- }
1528
- return await findById(this.#server, this.#dataAccessor, options);
1529
- }
1530
-
1531
- async createEntity(options: CreateEntityOptions, plugin?: RapidPlugin): Promise<TEntity> {
1532
- return await createEntity(this.#server, this.#dataAccessor, options, plugin);
1533
- }
1534
-
1535
- async updateEntityById(options: UpdateEntityByIdOptions, plugin?: RapidPlugin): Promise<TEntity> {
1536
- return await updateEntityById(this.#server, this.#dataAccessor, options, plugin);
1537
- }
1538
-
1539
- async count(options: CountEntityOptions): Promise<number> {
1540
- const model = this.#dataAccessor.getModel();
1541
- let baseModel: RpdDataModel;
1542
- if (model.base) {
1543
- baseModel = this.#server.getModel({
1544
- singularCode: model.base,
1545
- });
1546
- }
1547
- const countRowOptions: CountRowOptions = {
1548
- filters: await convertEntityFiltersToRowFilters(this.#server, model, baseModel, options.filters),
1549
- };
1550
- return await this.#dataAccessor.count(countRowOptions);
1551
- }
1552
-
1553
- async deleteById(options: DeleteEntityByIdOptions | string | number, plugin?: RapidPlugin): Promise<void> {
1554
- // options is id
1555
- if (!isObject(options)) {
1556
- options = {
1557
- id: options,
1558
- };
1559
- }
1560
-
1561
- const model = this.getModel();
1562
- if (model.derivedTypePropertyCode) {
1563
- throw newEntityOperationError("Delete base entity directly is not allowed.");
1564
- }
1565
-
1566
- const { id, routeContext } = options;
1567
-
1568
- const entity = await this.findById({
1569
- id,
1570
- keepNonPropertyFields: true,
1571
- routeContext,
1572
- });
1573
-
1574
- if (!entity) {
1575
- return;
1576
- }
1577
-
1578
- await this.#server.emitEvent({
1579
- eventName: "entity.beforeDelete",
1580
- payload: {
1581
- namespace: model.namespace,
1582
- modelSingularCode: model.singularCode,
1583
- before: entity,
1584
- },
1585
- sender: plugin,
1586
- routeContext,
1587
- });
1588
-
1589
- if (model.softDelete) {
1590
- let dataAccessor = model.base
1591
- ? this.#server.getDataAccessor({
1592
- singularCode: model.base,
1593
- })
1594
- : this.#dataAccessor;
1595
- const currentUserId = routeContext?.state?.userId;
1596
- await dataAccessor.updateById(id, {
1597
- deleted_at: getNowStringWithTimezone(),
1598
- deleter_id: currentUserId,
1599
- });
1600
- } else {
1601
- await this.#dataAccessor.deleteById(id);
1602
- if (model.base) {
1603
- const baseDataAccessor = this.#server.getDataAccessor({
1604
- singularCode: model.base,
1605
- });
1606
- await baseDataAccessor.deleteById(id);
1607
- }
1608
- }
1609
-
1610
- await this.#server.emitEvent({
1611
- eventName: "entity.delete",
1612
- payload: {
1613
- namespace: model.namespace,
1614
- modelSingularCode: model.singularCode,
1615
- before: entity,
1616
- },
1617
- sender: plugin,
1618
- routeContext,
1619
- });
1620
- }
1621
-
1622
- async addRelations(options: AddEntityRelationsOptions, plugin?: RapidPlugin): Promise<void> {
1623
- const server = this.#server;
1624
- const model = this.getModel();
1625
- const { id, property, relations, routeContext } = options;
1626
- const entity = await this.findById({
1627
- id,
1628
- routeContext,
1629
- });
1630
- if (!entity) {
1631
- throw new Error(`${model.namespace}.${model.singularCode} with id "${id}" was not found.`);
1632
- }
1633
-
1634
- const relationProperty = getEntityPropertyByCode(server, model, property);
1635
- if (!relationProperty) {
1636
- throw new Error(`Property '${property}' was not found in ${model.namespace}.${model.singularCode}`);
1637
- }
1638
-
1639
- if (!(isRelationProperty(relationProperty) && relationProperty.relation === "many")) {
1640
- throw new Error(`Operation 'addRelations' is only supported on property of 'many' relation`);
1641
- }
1642
-
1643
- const { queryBuilder } = server;
1644
- if (relationProperty.linkTableName) {
1645
- for (const relation of relations) {
1646
- const command = `INSERT INTO ${queryBuilder.quoteTable({
1647
- schema: relationProperty.linkSchema,
1648
- tableName: relationProperty.linkTableName,
1649
- })} (${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}, ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)})
1650
- SELECT $1, $2 WHERE NOT EXISTS (
1651
- SELECT ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}, ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}
1652
- FROM ${queryBuilder.quoteTable({ schema: relationProperty.linkSchema, tableName: relationProperty.linkTableName })}
1653
- WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}=$2
1654
- )`;
1655
- const params = [id, relation.id];
1656
- await server.queryDatabaseObject(command, params);
1657
- }
1658
- }
1659
-
1660
- await server.emitEvent({
1661
- eventName: "entity.addRelations",
1662
- payload: {
1663
- namespace: model.namespace,
1664
- modelSingularCode: model.singularCode,
1665
- entity,
1666
- property,
1667
- relations,
1668
- },
1669
- sender: plugin,
1670
- routeContext: options.routeContext,
1671
- });
1672
- }
1673
-
1674
- async removeRelations(options: RemoveEntityRelationsOptions, plugin?: RapidPlugin): Promise<void> {
1675
- const server = this.#server;
1676
- const model = this.getModel();
1677
- const { id, property, relations, routeContext } = options;
1678
- const entity = await this.findById({
1679
- id,
1680
- routeContext,
1681
- });
1682
- if (!entity) {
1683
- throw new Error(`${model.namespace}.${model.singularCode} with id "${id}" was not found.`);
1684
- }
1685
-
1686
- const relationProperty = getEntityPropertyByCode(server, model, property);
1687
- if (!relationProperty) {
1688
- throw new Error(`Property '${property}' was not found in ${model.namespace}.${model.singularCode}`);
1689
- }
1690
-
1691
- if (!(isRelationProperty(relationProperty) && relationProperty.relation === "many")) {
1692
- throw new Error(`Operation 'removeRelations' is only supported on property of 'many' relation`);
1693
- }
1694
-
1695
- const { queryBuilder } = server;
1696
- if (relationProperty.linkTableName) {
1697
- for (const relation of relations) {
1698
- const command = `DELETE FROM ${queryBuilder.quoteTable({ schema: relationProperty.linkSchema, tableName: relationProperty.linkTableName })}
1699
- WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}=$2;`;
1700
- const params = [id, relation.id];
1701
- await server.queryDatabaseObject(command, params);
1702
- }
1703
- }
1704
-
1705
- await server.emitEvent({
1706
- eventName: "entity.removeRelations",
1707
- payload: {
1708
- namespace: model.namespace,
1709
- modelSingularCode: model.singularCode,
1710
- entity,
1711
- property,
1712
- relations,
1713
- },
1714
- sender: plugin,
1715
- routeContext: options.routeContext,
1716
- });
1717
- }
1718
- }
1
+ import {
2
+ AddEntityRelationsOptions,
3
+ CountEntityOptions,
4
+ CreateEntityOptions,
5
+ DeleteEntityByIdOptions,
6
+ EntityFilterOperators,
7
+ EntityFilterOptions,
8
+ EntityNonRelationPropertyFilterOptions,
9
+ FindEntityByIdOptions,
10
+ FindEntityOptions,
11
+ FindEntityOrderByOptions,
12
+ IRpdDataAccessor,
13
+ RemoveEntityRelationsOptions,
14
+ RpdDataModel,
15
+ RpdDataModelIndex,
16
+ RpdDataModelIndexOptions,
17
+ RpdDataModelProperty,
18
+ UpdateEntityByIdOptions,
19
+ FindEntityFindOneRelationEntitiesOptions,
20
+ FindEntityFindManyRelationEntitiesOptions,
21
+ } from "~/types";
22
+ import { isNullOrUndefined } from "~/utilities/typeUtility";
23
+ import { mapDbRowToEntity, mapEntityToDbRow } from "./entityMapper";
24
+ import { mapPropertyNameToColumnName } from "./propertyMapper";
25
+ import { IRpdServer, RapidPlugin } from "~/core/server";
26
+ import { getEntityPartChanges } from "~/helpers/entityHelpers";
27
+ import {
28
+ cloneDeep,
29
+ concat,
30
+ filter,
31
+ find,
32
+ first,
33
+ forEach,
34
+ get,
35
+ isArray,
36
+ isNumber,
37
+ isObject,
38
+ isPlainObject,
39
+ isString,
40
+ isUndefined,
41
+ keys,
42
+ map,
43
+ pick,
44
+ reject,
45
+ uniq,
46
+ } from "lodash";
47
+ import {
48
+ getEntityPropertiesIncludingBase,
49
+ getEntityProperty,
50
+ getEntityPropertyByCode,
51
+ getEntityPropertyByFieldName,
52
+ isManyRelationProperty,
53
+ isOneRelationProperty,
54
+ isRelationProperty,
55
+ } from "../helpers/metaHelper";
56
+ import { ColumnSelectOptions, CountRowOptions, FindRowOptions, FindRowOrderByOptions, RowFilterOptions } from "./dataAccessTypes";
57
+ import { newEntityOperationError } from "~/utilities/errorUtility";
58
+ import { getNowStringWithTimezone } from "~/utilities/timeUtility";
59
+ import { RouteContext } from "~/core/routeContext";
60
+
61
+ export type FindOneRelationEntitiesOptions = {
62
+ server: IRpdServer;
63
+ mainModel: RpdDataModel;
64
+ relationProperty: RpdDataModelProperty;
65
+ relationEntityIds: any[];
66
+ selectRelationOptions?: FindEntityFindOneRelationEntitiesOptions;
67
+ };
68
+
69
+ export type FindManyRelationEntitiesOptions = {
70
+ server: IRpdServer;
71
+ mainModel: RpdDataModel;
72
+ relationProperty: RpdDataModelProperty;
73
+ mainEntityIds: any[];
74
+ selectRelationOptions?: FindEntityFindManyRelationEntitiesOptions;
75
+ };
76
+
77
+ function convertEntityOrderByToRowOrderBy(server: IRpdServer, model: RpdDataModel, baseModel?: RpdDataModel, orderByList?: FindEntityOrderByOptions[]) {
78
+ if (!orderByList) {
79
+ return null;
80
+ }
81
+
82
+ return orderByList.map((orderBy) => {
83
+ const fields = orderBy.field.split(".");
84
+ let orderField: string;
85
+ let relationField: string;
86
+ if (fields.length === 1) {
87
+ orderField = fields[0];
88
+ } else {
89
+ orderField = fields[1];
90
+ relationField = fields[0];
91
+ }
92
+ if (relationField) {
93
+ const relationProperty = getEntityPropertyByCode(server, model, relationField);
94
+ if (!relationProperty) {
95
+ throw new Error(`Property '${relationProperty}' was not found in ${model.namespace}.${model.singularCode}`);
96
+ }
97
+ if (!isRelationProperty(relationProperty)) {
98
+ throw new Error("orderBy[].relation must be a one-relation property.");
99
+ }
100
+
101
+ if (isManyRelationProperty(relationProperty)) {
102
+ throw new Error("orderBy[].relation must be a one-relation property.");
103
+ }
104
+
105
+ const relationModel = server.getModel({ singularCode: relationProperty.targetSingularCode });
106
+ let relationBaseModel: RpdDataModel = null;
107
+ if (relationModel.base) {
108
+ relationBaseModel = server.getModel({ singularCode: relationModel.base });
109
+ }
110
+ let property = getEntityPropertyByFieldName(server, relationModel, orderField);
111
+ if (!property) {
112
+ throw new Error(`Unkown orderBy field '${orderField}' of relation '${relationField}'`);
113
+ }
114
+
115
+ return {
116
+ field: {
117
+ name: mapPropertyNameToColumnName(server, relationModel, orderField),
118
+ tableName: property.isBaseProperty ? relationBaseModel.tableName : relationModel.tableName,
119
+ schema: property.isBaseProperty ? relationBaseModel.schema : relationModel.schema,
120
+ },
121
+ relationField: {
122
+ name: mapPropertyNameToColumnName(server, model, relationField),
123
+ tableName: relationProperty.isBaseProperty ? baseModel.tableName : model.tableName,
124
+ schema: relationProperty.isBaseProperty ? baseModel.schema : model.schema,
125
+ },
126
+ desc: !!orderBy.desc,
127
+ } as FindRowOrderByOptions;
128
+ } else {
129
+ let property = getEntityPropertyByFieldName(server, model, orderField);
130
+ if (!property) {
131
+ throw new Error(`Unkown orderBy field '${orderField}'`);
132
+ }
133
+
134
+ return {
135
+ field: {
136
+ name: mapPropertyNameToColumnName(server, model, orderField),
137
+ tableName: property.isBaseProperty ? baseModel.tableName : model.tableName,
138
+ },
139
+ desc: !!orderBy.desc,
140
+ } as FindRowOrderByOptions;
141
+ }
142
+ });
143
+ }
144
+
145
+ async function findEntities(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: FindEntityOptions) {
146
+ const model = dataAccessor.getModel();
147
+ let baseModel: RpdDataModel | undefined;
148
+ if (model.base) {
149
+ baseModel = server.getModel({
150
+ singularCode: model.base,
151
+ });
152
+ }
153
+
154
+ let propertiesToSelect: RpdDataModelProperty[];
155
+ let relationOptions = options.relations || {};
156
+ let relationPropertyCodes = Object.keys(relationOptions) || [];
157
+ if (!options.properties || !options.properties.length) {
158
+ propertiesToSelect = getEntityPropertiesIncludingBase(server, model).filter((property) => {
159
+ return !isRelationProperty(property) || relationPropertyCodes.includes(property.code);
160
+ });
161
+ } else {
162
+ propertiesToSelect = getEntityPropertiesIncludingBase(server, model).filter(
163
+ (property) => options.properties.includes(property.code) || relationPropertyCodes.includes(property.code),
164
+ );
165
+ }
166
+
167
+ const columnsToSelect: ColumnSelectOptions[] = [];
168
+
169
+ const relationPropertiesToSelect: RpdDataModelProperty[] = [];
170
+ forEach(propertiesToSelect, (property) => {
171
+ if (isRelationProperty(property)) {
172
+ relationPropertiesToSelect.push(property);
173
+
174
+ if (property.relation === "one" && !property.linkTableName) {
175
+ if (!property.targetIdColumnName) {
176
+ throw new Error(`'targetIdColumnName' should be configured for property '${property.code}' of model '${model.namespace}.${model.singularCode}'.`);
177
+ }
178
+
179
+ if (property.isBaseProperty) {
180
+ columnsToSelect.push({
181
+ name: property.targetIdColumnName,
182
+ tableName: baseModel.tableName,
183
+ });
184
+ } else {
185
+ columnsToSelect.push({
186
+ name: property.targetIdColumnName,
187
+ tableName: model.tableName,
188
+ });
189
+ }
190
+ }
191
+ } else {
192
+ if (property.isBaseProperty) {
193
+ columnsToSelect.push({
194
+ name: property.columnName || property.code,
195
+ tableName: baseModel.tableName,
196
+ });
197
+ } else {
198
+ columnsToSelect.push({
199
+ name: property.columnName || property.code,
200
+ tableName: model.tableName,
201
+ });
202
+ }
203
+ }
204
+ });
205
+
206
+ // if `keepNonPropertyFields` is true and `properties` are not specified, then select relation columns automatically.
207
+ if (options.keepNonPropertyFields && (!options.properties || !options.properties.length)) {
208
+ const oneRelationPropertiesWithNoLinkTable = getEntityPropertiesIncludingBase(server, model).filter(
209
+ (property) => property.relation === "one" && !property.linkTableName,
210
+ );
211
+ oneRelationPropertiesWithNoLinkTable.forEach((property) => {
212
+ if (property.targetIdColumnName) {
213
+ columnsToSelect.push({
214
+ name: property.targetIdColumnName,
215
+ tableName: property.isBaseProperty ? baseModel.tableName : model.tableName,
216
+ });
217
+ }
218
+ });
219
+ }
220
+
221
+ if (options.extraColumnsToSelect) {
222
+ forEach(options.extraColumnsToSelect, (extraColumnToSelect: ColumnSelectOptions) => {
223
+ const columnSelectOptionExists = find(columnsToSelect, (item: ColumnSelectOptions) => {
224
+ if (typeof item === "string") {
225
+ if (typeof extraColumnToSelect === "string") {
226
+ return item === extraColumnToSelect;
227
+ } else {
228
+ return item == extraColumnToSelect.name;
229
+ }
230
+ } else {
231
+ if (typeof extraColumnToSelect === "string") {
232
+ return item.name === extraColumnToSelect;
233
+ } else {
234
+ return item.name == extraColumnToSelect.name;
235
+ }
236
+ }
237
+ });
238
+
239
+ if (!columnSelectOptionExists) {
240
+ columnsToSelect.push(extraColumnToSelect);
241
+ }
242
+ });
243
+ }
244
+
245
+ const rowFilters = await convertEntityFiltersToRowFilters(server, model, baseModel, options.filters);
246
+ const findRowOptions: FindRowOptions = {
247
+ filters: rowFilters,
248
+ orderBy: convertEntityOrderByToRowOrderBy(server, model, baseModel, options.orderBy),
249
+ pagination: options.pagination,
250
+ fields: columnsToSelect,
251
+ };
252
+ const rows = await dataAccessor.find(findRowOptions);
253
+ if (!rows.length) {
254
+ return [];
255
+ }
256
+
257
+ const entityIds = rows.map((row) => row.id);
258
+ if (relationPropertiesToSelect.length) {
259
+ for (const relationProperty of relationPropertiesToSelect) {
260
+ const isManyRelation = relationProperty.relation === "many";
261
+
262
+ if (relationProperty.linkTableName) {
263
+ const relationModel = server.getModel({ singularCode: relationProperty.targetSingularCode! });
264
+ if (!relationModel) {
265
+ continue;
266
+ }
267
+
268
+ if (isManyRelation) {
269
+ const relationLinks = await findManyRelationLinksViaLinkTable({
270
+ server,
271
+ mainModel: relationModel,
272
+ relationProperty,
273
+ mainEntityIds: entityIds,
274
+ selectRelationOptions: relationOptions[relationProperty.code],
275
+ });
276
+
277
+ forEach(rows, (row: any) => {
278
+ row[relationProperty.code] = filter(relationLinks, (link: any) => {
279
+ return link[relationProperty.selfIdColumnName!] == row["id"];
280
+ }).map((link) => mapDbRowToEntity(server, relationModel, link.targetEntity, options.keepNonPropertyFields));
281
+ });
282
+ }
283
+ } else {
284
+ let relatedEntities: any[];
285
+ if (isManyRelation) {
286
+ relatedEntities = await findManyRelatedEntitiesViaIdPropertyCode({
287
+ server,
288
+ mainModel: model,
289
+ relationProperty,
290
+ mainEntityIds: entityIds,
291
+ selectRelationOptions: relationOptions[relationProperty.code],
292
+ });
293
+ } else {
294
+ const targetEntityIds = uniq(
295
+ reject(
296
+ map(rows, (entity: any) => entity[relationProperty.targetIdColumnName!]),
297
+ isNullOrUndefined,
298
+ ),
299
+ );
300
+ relatedEntities = await findOneRelatedEntitiesViaIdPropertyCode({
301
+ server,
302
+ mainModel: model,
303
+ relationProperty,
304
+ relationEntityIds: targetEntityIds,
305
+ selectRelationOptions: relationOptions[relationProperty.code],
306
+ });
307
+ }
308
+
309
+ const targetModel = server.getModel({
310
+ singularCode: relationProperty.targetSingularCode!,
311
+ });
312
+ rows.forEach((row) => {
313
+ if (isManyRelation) {
314
+ row[relationProperty.code] = filter(relatedEntities, (relatedEntity: any) => {
315
+ return relatedEntity[relationProperty.selfIdColumnName!] == row.id;
316
+ }).map((item) => mapDbRowToEntity(server, targetModel!, item, options.keepNonPropertyFields));
317
+ } else {
318
+ row[relationProperty.code] = mapDbRowToEntity(
319
+ server,
320
+ targetModel!,
321
+ find(relatedEntities, (relatedEntity: any) => {
322
+ // TODO: id property code should be configurable.
323
+ return relatedEntity["id"] == row[relationProperty.targetIdColumnName!];
324
+ }),
325
+ options.keepNonPropertyFields,
326
+ );
327
+ }
328
+ });
329
+ }
330
+ }
331
+ }
332
+ const entities = rows.map((item) => mapDbRowToEntity(server, model, item, options.keepNonPropertyFields));
333
+
334
+ await server.emitEvent({
335
+ eventName: "entity.beforeResponse",
336
+ payload: {
337
+ namespace: model.namespace,
338
+ modelSingularCode: model.singularCode,
339
+ baseModelSingularCode: model.base,
340
+ entities,
341
+ },
342
+ sender: null,
343
+ routeContext: options.routeContext,
344
+ });
345
+
346
+ return entities;
347
+ }
348
+
349
+ async function findEntity(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: FindEntityOptions) {
350
+ const entities = await findEntities(server, dataAccessor, {
351
+ ...options,
352
+ ...{
353
+ limit: 1,
354
+ },
355
+ });
356
+ return first(entities);
357
+ }
358
+
359
+ async function findById(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: FindEntityByIdOptions): Promise<any> {
360
+ const { id, properties, relations, keepNonPropertyFields, routeContext } = options;
361
+ return await findEntity(server, dataAccessor, {
362
+ filters: [
363
+ {
364
+ operator: "eq",
365
+ field: "id",
366
+ value: id,
367
+ },
368
+ ],
369
+ properties,
370
+ relations,
371
+ keepNonPropertyFields,
372
+ routeContext,
373
+ });
374
+ }
375
+
376
+ async function convertEntityFiltersToRowFilters(
377
+ server: IRpdServer,
378
+ model: RpdDataModel,
379
+ baseModel: RpdDataModel,
380
+ filters: EntityFilterOptions[] | undefined,
381
+ ): Promise<RowFilterOptions[]> {
382
+ if (!filters || !filters.length) {
383
+ return [];
384
+ }
385
+
386
+ const replacedFilters: RowFilterOptions[] = [];
387
+ for (const filter of filters) {
388
+ const { operator } = filter;
389
+ if (operator === "and" || operator === "or") {
390
+ replacedFilters.push({
391
+ operator: operator,
392
+ filters: await convertEntityFiltersToRowFilters(server, model, baseModel, filter.filters),
393
+ });
394
+ } else if (operator === "exists" || operator === "notExists") {
395
+ const relationProperty: RpdDataModelProperty = getEntityPropertyByCode(server, model, filter.field);
396
+ if (!relationProperty) {
397
+ throw new Error(`Invalid filters. Property '${filter.field}' was not found in model '${model.namespace}.${model.singularCode}'`);
398
+ }
399
+ if (!isRelationProperty(relationProperty)) {
400
+ throw new Error(
401
+ `Invalid filters. Filter with 'existence' operator on property '${filter.field}' is not allowed. You can only use it on an relation property.`,
402
+ );
403
+ }
404
+
405
+ const relatedEntityFilters = filter.filters;
406
+ if (!relatedEntityFilters || !relatedEntityFilters.length) {
407
+ throw new Error(`Invalid filters. 'filters' must be provided on filter with 'existence' operator.`);
408
+ }
409
+
410
+ if (relationProperty.relation === "one") {
411
+ // Optimize when filtering by id of related entity
412
+ if (relatedEntityFilters.length === 1) {
413
+ const relatedEntityIdFilter = relatedEntityFilters[0];
414
+ if ((relatedEntityIdFilter.operator === "eq" || relatedEntityIdFilter.operator === "in") && relatedEntityIdFilter.field === "id") {
415
+ let replacedOperator: EntityFilterOperators;
416
+ if (operator === "exists") {
417
+ replacedOperator = relatedEntityIdFilter.operator;
418
+ } else {
419
+ if (relatedEntityIdFilter.operator === "eq") {
420
+ replacedOperator = "ne";
421
+ } else {
422
+ replacedOperator = "notIn";
423
+ }
424
+ }
425
+ replacedFilters.push({
426
+ field: {
427
+ name: relationProperty.targetIdColumnName!,
428
+ tableName: relationProperty.isBaseProperty ? baseModel.tableName : model.tableName,
429
+ },
430
+ operator: replacedOperator,
431
+ value: relatedEntityIdFilter.value,
432
+ });
433
+ continue;
434
+ }
435
+ }
436
+
437
+ const dataAccessor = server.getDataAccessor({
438
+ singularCode: relationProperty.targetSingularCode as string,
439
+ });
440
+ const relatedModel = dataAccessor.getModel();
441
+ let relatedBaseModel: RpdDataModel;
442
+ if (relatedModel.base) {
443
+ relatedBaseModel = server.getModel({
444
+ singularCode: relatedModel.base,
445
+ });
446
+ }
447
+ const rows = await dataAccessor.find({
448
+ filters: await convertEntityFiltersToRowFilters(server, relatedModel, relatedBaseModel, filter.filters),
449
+ fields: [
450
+ {
451
+ name: "id",
452
+ tableName: relatedModel.tableName,
453
+ },
454
+ ],
455
+ });
456
+ const entityIds = map(rows, (entity: any) => entity["id"]);
457
+ replacedFilters.push({
458
+ field: {
459
+ name: relationProperty.targetIdColumnName,
460
+ tableName: relationProperty.isBaseProperty ? baseModel.tableName : model.tableName,
461
+ },
462
+ operator: operator === "exists" ? "in" : "notIn",
463
+ value: entityIds,
464
+ });
465
+ } else if (!relationProperty.linkTableName) {
466
+ // many relation without link table.
467
+ if (!relationProperty.selfIdColumnName) {
468
+ throw new Error(`Invalid filters. 'selfIdColumnName' of property '${relationProperty.code}' was not configured`);
469
+ }
470
+
471
+ const targetEntityDataAccessor = server.getDataAccessor({
472
+ singularCode: relationProperty.targetSingularCode as string,
473
+ });
474
+ const relatedModel = targetEntityDataAccessor.getModel();
475
+ let relatedBaseModel: RpdDataModel;
476
+ if (relatedModel.base) {
477
+ relatedBaseModel = server.getModel({
478
+ singularCode: relatedModel.base,
479
+ });
480
+ }
481
+ const targetEntities = await targetEntityDataAccessor.find({
482
+ filters: await convertEntityFiltersToRowFilters(server, relatedModel, relatedBaseModel, filter.filters),
483
+ fields: [
484
+ {
485
+ name: relationProperty.selfIdColumnName,
486
+ tableName: relatedModel.tableName,
487
+ },
488
+ ],
489
+ });
490
+ const selfEntityIds = map(targetEntities, (entity: any) => entity[relationProperty.selfIdColumnName!]);
491
+ replacedFilters.push({
492
+ field: {
493
+ name: "id",
494
+ tableName: model.tableName,
495
+ },
496
+ operator: operator === "exists" ? "in" : "notIn",
497
+ value: selfEntityIds,
498
+ });
499
+ } else {
500
+ // many relation with link table
501
+ if (!relationProperty.selfIdColumnName) {
502
+ throw new Error(`Invalid filters. 'selfIdColumnName' of property '${relationProperty.code}' was not configured`);
503
+ }
504
+
505
+ if (!relationProperty.targetIdColumnName) {
506
+ throw new Error(`Invalid filters. 'targetIdColumnName' of property '${relationProperty.code}' was not configured`);
507
+ }
508
+
509
+ // 1. find target entities
510
+ // 2. find links
511
+ // 3. convert to 'in' filter
512
+ const targetEntityDataAccessor = server.getDataAccessor({
513
+ singularCode: relationProperty.targetSingularCode as string,
514
+ });
515
+ const relatedModel = targetEntityDataAccessor.getModel();
516
+ let relatedBaseModel: RpdDataModel;
517
+ if (relatedModel.base) {
518
+ relatedBaseModel = server.getModel({
519
+ singularCode: relatedModel.base,
520
+ });
521
+ }
522
+ const targetEntities = await targetEntityDataAccessor.find({
523
+ filters: await convertEntityFiltersToRowFilters(server, relatedModel, relatedBaseModel, filter.filters),
524
+ fields: [
525
+ {
526
+ name: "id",
527
+ tableName: relatedModel.tableName,
528
+ },
529
+ ],
530
+ });
531
+ const targetEntityIds = map(targetEntities, (entity: any) => entity["id"]);
532
+
533
+ const command = `SELECT * FROM ${server.queryBuilder.quoteTable({
534
+ schema: relationProperty.linkSchema,
535
+ tableName: relationProperty.linkTableName!,
536
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.targetIdColumnName!)} = ANY($1::int[])`;
537
+ const params = [targetEntityIds];
538
+ const links = await server.queryDatabaseObject(command, params);
539
+ const selfEntityIds = links.map((link) => link[relationProperty.selfIdColumnName!]);
540
+ replacedFilters.push({
541
+ field: {
542
+ name: "id",
543
+ tableName: model.tableName,
544
+ },
545
+ operator: operator === "exists" ? "in" : "notIn",
546
+ value: selfEntityIds,
547
+ });
548
+ }
549
+ } else {
550
+ const filterField = (filter as EntityNonRelationPropertyFilterOptions).field;
551
+ let property: RpdDataModelProperty = getEntityPropertyByCode(server, model, filterField);
552
+
553
+ let filterValue = (filter as any).value;
554
+
555
+ let columnName = "";
556
+ if (property) {
557
+ if (isOneRelationProperty(property)) {
558
+ columnName = property.targetIdColumnName;
559
+ if (isPlainObject(filterValue)) {
560
+ filterValue = filterValue.id;
561
+ }
562
+ } else if (isManyRelationProperty(property)) {
563
+ throw new Error(`Operator "${operator}" is not supported on many-relation property "${property.code}"`);
564
+ } else {
565
+ columnName = property.columnName || property.code;
566
+ }
567
+ } else {
568
+ property = getEntityProperty(server, model, (property) => {
569
+ return property.columnName === filterField;
570
+ });
571
+
572
+ if (property) {
573
+ columnName = property.columnName;
574
+ } else {
575
+ property = getEntityProperty(server, model, (property) => {
576
+ return property.targetIdColumnName === filterField;
577
+ });
578
+
579
+ if (property) {
580
+ columnName = property.targetIdColumnName;
581
+ if (isPlainObject(filterValue)) {
582
+ filterValue = filterValue.id;
583
+ }
584
+ } else {
585
+ columnName = filterField;
586
+ }
587
+ }
588
+ }
589
+
590
+ // TODO: do not use `any` here
591
+ replacedFilters.push({
592
+ operator: filter.operator,
593
+ field: {
594
+ name: columnName,
595
+ tableName: property && property.isBaseProperty ? baseModel.tableName : model.tableName,
596
+ },
597
+ value: filterValue,
598
+ itemType: (filter as any).itemType,
599
+ } as any);
600
+ }
601
+ }
602
+ return replacedFilters;
603
+ }
604
+
605
+ async function findManyRelationLinksViaLinkTable(options: FindManyRelationEntitiesOptions) {
606
+ const { server, relationProperty, mainModel: relationModel, mainEntityIds, selectRelationOptions } = options;
607
+ const command = `SELECT * FROM ${server.queryBuilder.quoteTable({
608
+ schema: relationProperty.linkSchema,
609
+ tableName: relationProperty.linkTableName!,
610
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = ANY($1::int[])
611
+ ORDER BY id
612
+ `;
613
+ const params = [mainEntityIds];
614
+ const links = await server.queryDatabaseObject(command, params);
615
+ const targetEntityIds = links.map((link) => link[relationProperty.targetIdColumnName!]);
616
+
617
+ const dataAccessor = server.getDataAccessor({
618
+ namespace: relationModel.namespace,
619
+ singularCode: relationModel.singularCode,
620
+ });
621
+
622
+ const findEntityOptions: FindEntityOptions = {
623
+ filters: [
624
+ {
625
+ field: "id",
626
+ operator: "in",
627
+ value: targetEntityIds,
628
+ },
629
+ ],
630
+ keepNonPropertyFields: true,
631
+ };
632
+
633
+ if (selectRelationOptions) {
634
+ if (typeof selectRelationOptions !== "boolean") {
635
+ if (selectRelationOptions.properties) {
636
+ findEntityOptions.properties = ["id", ...selectRelationOptions.properties];
637
+ }
638
+ if (selectRelationOptions.relations) {
639
+ findEntityOptions.relations = selectRelationOptions.relations;
640
+ }
641
+ if (selectRelationOptions.orderBy) {
642
+ findEntityOptions.orderBy = selectRelationOptions.orderBy;
643
+ }
644
+ if (selectRelationOptions.pagination) {
645
+ findEntityOptions.pagination = selectRelationOptions.pagination;
646
+ }
647
+ if (selectRelationOptions.filters) {
648
+ findEntityOptions.filters = [...findEntityOptions.filters, ...selectRelationOptions.filters];
649
+ }
650
+ if (!isUndefined(selectRelationOptions.keepNonPropertyFields)) {
651
+ findEntityOptions.keepNonPropertyFields = selectRelationOptions.keepNonPropertyFields;
652
+ }
653
+ }
654
+ }
655
+
656
+ const targetEntities = await findEntities(server, dataAccessor, findEntityOptions);
657
+
658
+ forEach(links, (link: any) => {
659
+ link.targetEntity = find(targetEntities, (e: any) => e["id"] == link[relationProperty.targetIdColumnName!]);
660
+ });
661
+
662
+ return links;
663
+ }
664
+
665
+ async function findManyRelatedEntitiesViaIdPropertyCode(options: FindManyRelationEntitiesOptions) {
666
+ const { server, relationProperty, mainEntityIds, selectRelationOptions } = options;
667
+ const dataAccessor = server.getDataAccessor({
668
+ singularCode: relationProperty.targetSingularCode as string,
669
+ });
670
+
671
+ const findEntityOptions: FindEntityOptions = {
672
+ filters: [
673
+ {
674
+ field: relationProperty.selfIdColumnName,
675
+ operator: "in",
676
+ value: mainEntityIds,
677
+ },
678
+ ],
679
+ orderBy: [
680
+ {
681
+ field: "id",
682
+ },
683
+ ],
684
+ extraColumnsToSelect: [relationProperty.selfIdColumnName],
685
+ keepNonPropertyFields: true,
686
+ };
687
+
688
+ if (selectRelationOptions) {
689
+ if (typeof selectRelationOptions !== "boolean") {
690
+ if (selectRelationOptions.properties) {
691
+ findEntityOptions.properties = ["id", ...selectRelationOptions.properties];
692
+ }
693
+ if (selectRelationOptions.relations) {
694
+ findEntityOptions.relations = selectRelationOptions.relations;
695
+ }
696
+ if (selectRelationOptions.orderBy) {
697
+ findEntityOptions.orderBy = selectRelationOptions.orderBy;
698
+ }
699
+ if (selectRelationOptions.pagination) {
700
+ findEntityOptions.pagination = selectRelationOptions.pagination;
701
+ }
702
+ if (selectRelationOptions.filters) {
703
+ findEntityOptions.filters = [...findEntityOptions.filters, ...selectRelationOptions.filters];
704
+ }
705
+ if (!isUndefined(selectRelationOptions.keepNonPropertyFields)) {
706
+ findEntityOptions.keepNonPropertyFields = selectRelationOptions.keepNonPropertyFields;
707
+ }
708
+ }
709
+ }
710
+
711
+ return await findEntities(server, dataAccessor, findEntityOptions);
712
+ }
713
+
714
+ async function findOneRelatedEntitiesViaIdPropertyCode(options: FindOneRelationEntitiesOptions) {
715
+ const { server, relationProperty, relationEntityIds, selectRelationOptions } = options;
716
+
717
+ const dataAccessor = server.getDataAccessor({
718
+ singularCode: relationProperty.targetSingularCode as string,
719
+ });
720
+
721
+ const findEntityOptions: FindEntityOptions = {
722
+ filters: [
723
+ {
724
+ field: "id",
725
+ operator: "in",
726
+ value: relationEntityIds,
727
+ },
728
+ ],
729
+ keepNonPropertyFields: true,
730
+ };
731
+
732
+ if (selectRelationOptions) {
733
+ if (typeof selectRelationOptions !== "boolean") {
734
+ if (selectRelationOptions.properties) {
735
+ findEntityOptions.properties = ["id", ...selectRelationOptions.properties];
736
+ }
737
+ if (selectRelationOptions.relations) {
738
+ findEntityOptions.relations = selectRelationOptions.relations;
739
+ }
740
+ }
741
+ }
742
+
743
+ return await findEntities(server, dataAccessor, findEntityOptions);
744
+ }
745
+
746
+ async function createEntity(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: CreateEntityOptions, plugin?: RapidPlugin) {
747
+ const model = dataAccessor.getModel();
748
+ if (model.derivedTypePropertyCode) {
749
+ throw newEntityOperationError("Create base entity directly is not allowed.");
750
+ }
751
+
752
+ const { entity, routeContext } = options;
753
+
754
+ const userId = options.routeContext?.state?.userId;
755
+ if (userId) {
756
+ const createdByProperty = getEntityPropertyByCode(server, model, "createdBy");
757
+ if (createdByProperty) {
758
+ entity.createdBy = userId;
759
+ }
760
+ }
761
+ const createdAtProperty = getEntityPropertyByCode(server, model, "createdAt");
762
+ if (createdAtProperty) {
763
+ entity.createdAt = getNowStringWithTimezone();
764
+ }
765
+
766
+ await server.beforeCreateEntity(model, options);
767
+
768
+ await server.emitEvent({
769
+ eventName: "entity.beforeCreate",
770
+ payload: {
771
+ namespace: model.namespace,
772
+ modelSingularCode: model.singularCode,
773
+ baseModelSingularCode: model.base,
774
+ before: entity,
775
+ },
776
+ sender: plugin,
777
+ routeContext,
778
+ });
779
+
780
+ // check unique constraints
781
+ if (!options.postponeUniquenessCheck) {
782
+ if (model.indexes && model.indexes.length) {
783
+ for (const indexConfig of model.indexes) {
784
+ if (!indexConfig.unique) {
785
+ continue;
786
+ }
787
+
788
+ const duplicate = await willEntityDuplicate(server, dataAccessor, {
789
+ routeContext: options.routeContext,
790
+ entityToSave: entity,
791
+ indexConfig,
792
+ });
793
+ if (duplicate) {
794
+ throw new Error(getEntityDuplicatedErrorMessage(server, model, indexConfig));
795
+ }
796
+ }
797
+ }
798
+ }
799
+
800
+ const oneRelationPropertiesToCreate: RpdDataModelProperty[] = [];
801
+ const manyRelationPropertiesToCreate: RpdDataModelProperty[] = [];
802
+ keys(entity).forEach((propertyCode) => {
803
+ const property = getEntityPropertyByCode(server, model, propertyCode);
804
+ if (!property) {
805
+ // Unknown property
806
+ return;
807
+ }
808
+
809
+ if (isRelationProperty(property)) {
810
+ if (property.relation === "many") {
811
+ manyRelationPropertiesToCreate.push(property);
812
+ } else {
813
+ oneRelationPropertiesToCreate.push(property);
814
+ }
815
+ }
816
+ });
817
+
818
+ const { row, baseRow } = mapEntityToDbRow(server, model, entity);
819
+
820
+ const newEntityOneRelationProps = {};
821
+ // save one-relation properties
822
+ for (const property of oneRelationPropertiesToCreate) {
823
+ const rowToBeSaved = property.isBaseProperty ? baseRow : row;
824
+ const fieldValue = entity[property.code];
825
+ const targetDataAccessor = server.getDataAccessor({
826
+ singularCode: property.targetSingularCode!,
827
+ });
828
+ if (isObject(fieldValue)) {
829
+ const targetEntityId = fieldValue["id"];
830
+ if (!targetEntityId) {
831
+ if (!property.selfIdColumnName) {
832
+ const targetEntity = fieldValue;
833
+ const newTargetEntity = await createEntity(server, targetDataAccessor, {
834
+ routeContext,
835
+ entity: targetEntity,
836
+ });
837
+ newEntityOneRelationProps[property.code] = newTargetEntity;
838
+ rowToBeSaved[property.targetIdColumnName!] = newTargetEntity["id"];
839
+ }
840
+ } else {
841
+ const targetEntity = await findById(server, targetDataAccessor, {
842
+ id: targetEntityId,
843
+ routeContext,
844
+ });
845
+ if (!targetEntity) {
846
+ throw newEntityOperationError(
847
+ `Create ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${targetEntityId}' was not found.`,
848
+ );
849
+ }
850
+ newEntityOneRelationProps[property.code] = targetEntity;
851
+ rowToBeSaved[property.targetIdColumnName!] = targetEntityId;
852
+ }
853
+ } else if (isNumber(fieldValue) || isString(fieldValue)) {
854
+ // fieldValue is id;
855
+ const targetEntityId = fieldValue;
856
+ const targetEntity = await findById(server, targetDataAccessor, {
857
+ id: targetEntityId,
858
+ routeContext,
859
+ });
860
+ if (!targetEntity) {
861
+ throw newEntityOperationError(
862
+ `Create ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${targetEntityId}' was not found.`,
863
+ );
864
+ }
865
+ newEntityOneRelationProps[property.code] = targetEntity;
866
+ rowToBeSaved[property.targetIdColumnName!] = targetEntityId;
867
+ } else {
868
+ newEntityOneRelationProps[property.code] = null;
869
+ rowToBeSaved[property.targetIdColumnName!] = null;
870
+ }
871
+ }
872
+
873
+ let newBaseRow: any;
874
+ let baseDataAccessor: any;
875
+ if (model.base) {
876
+ baseDataAccessor = server.getDataAccessor({
877
+ singularCode: model.base,
878
+ });
879
+ newBaseRow = await baseDataAccessor.create(baseRow);
880
+
881
+ row.id = newBaseRow.id;
882
+ }
883
+ const newRow = await dataAccessor.create(row);
884
+ const newEntity = mapDbRowToEntity(server, model, Object.assign({}, newBaseRow, newRow, newEntityOneRelationProps), true);
885
+
886
+ // save one-relation properties that has selfIdColumnName
887
+ for (const property of oneRelationPropertiesToCreate) {
888
+ const fieldValue = entity[property.code];
889
+ const targetDataAccessor = server.getDataAccessor({
890
+ singularCode: property.targetSingularCode!,
891
+ });
892
+ if (isObject(fieldValue)) {
893
+ const targetEntityId = fieldValue["id"];
894
+ if (!targetEntityId) {
895
+ if (property.selfIdColumnName) {
896
+ const targetEntity = fieldValue;
897
+ targetEntity[property.selfIdColumnName] = newEntity.id;
898
+ const newTargetEntity = await createEntity(server, targetDataAccessor, {
899
+ routeContext,
900
+ entity: targetEntity,
901
+ });
902
+
903
+ let dataAccessorOfMainEntity = dataAccessor;
904
+ if (property.isBaseProperty) {
905
+ dataAccessorOfMainEntity = baseDataAccessor;
906
+ }
907
+
908
+ const relationFieldChanges = {
909
+ [property.targetIdColumnName]: newTargetEntity.id,
910
+ };
911
+ await dataAccessorOfMainEntity.updateById(newEntity.id, relationFieldChanges);
912
+ newEntity[property.code] = newTargetEntity;
913
+ }
914
+ }
915
+ }
916
+ }
917
+
918
+ // save many-relation properties
919
+ for (const property of manyRelationPropertiesToCreate) {
920
+ newEntity[property.code] = [];
921
+
922
+ const targetDataAccessor = server.getDataAccessor({
923
+ singularCode: property.targetSingularCode!,
924
+ });
925
+
926
+ const relatedEntitiesToBeSaved = entity[property.code];
927
+ if (!isArray(relatedEntitiesToBeSaved)) {
928
+ throw new Error(`Value of field '${property.code}' should be an array.`);
929
+ }
930
+
931
+ for (const relatedEntityToBeSaved of relatedEntitiesToBeSaved) {
932
+ let relatedEntityId: any;
933
+ if (isObject(relatedEntityToBeSaved)) {
934
+ relatedEntityId = relatedEntityToBeSaved["id"];
935
+ if (!relatedEntityId) {
936
+ // related entity is to be created
937
+ const targetEntity = relatedEntityToBeSaved;
938
+ if (!property.linkTableName) {
939
+ targetEntity[property.selfIdColumnName!] = newEntity.id;
940
+ }
941
+ const newTargetEntity = await createEntity(server, targetDataAccessor, {
942
+ routeContext,
943
+ entity: targetEntity,
944
+ });
945
+
946
+ if (property.linkTableName) {
947
+ const command = `INSERT INTO ${server.queryBuilder.quoteTable({
948
+ schema: property.linkSchema,
949
+ tableName: property.linkTableName,
950
+ })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
951
+ const params = [newEntity.id, newTargetEntity.id];
952
+ await server.queryDatabaseObject(command, params);
953
+ }
954
+
955
+ newEntity[property.code].push(newTargetEntity);
956
+ } else {
957
+ // related entity is existed
958
+ const targetEntity = await targetDataAccessor.findById(relatedEntityId);
959
+ if (!targetEntity) {
960
+ throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
961
+ }
962
+
963
+ if (property.linkTableName) {
964
+ const command = `INSERT INTO ${server.queryBuilder.quoteTable({
965
+ schema: property.linkSchema,
966
+ tableName: property.linkTableName,
967
+ })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
968
+ const params = [newEntity.id, relatedEntityId];
969
+ await server.queryDatabaseObject(command, params);
970
+ } else {
971
+ await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: newEntity.id });
972
+ targetEntity[property.selfIdColumnName!] = newEntity.id;
973
+ }
974
+ newEntity[property.code].push(targetEntity);
975
+ }
976
+ } else {
977
+ // fieldValue is id
978
+ relatedEntityId = relatedEntityToBeSaved;
979
+ const targetEntity = await targetDataAccessor.findById(relatedEntityId);
980
+ if (!targetEntity) {
981
+ throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
982
+ }
983
+
984
+ if (property.linkTableName) {
985
+ const command = `INSERT INTO ${server.queryBuilder.quoteTable({
986
+ schema: property.linkSchema,
987
+ tableName: property.linkTableName,
988
+ })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
989
+ const params = [newEntity.id, relatedEntityId];
990
+ await server.queryDatabaseObject(command, params);
991
+ } else {
992
+ await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: newEntity.id });
993
+ targetEntity[property.selfIdColumnName!] = newEntity.id;
994
+ }
995
+
996
+ newEntity[property.code].push(targetEntity);
997
+ }
998
+ }
999
+ }
1000
+
1001
+ await server.emitEvent({
1002
+ eventName: "entity.create",
1003
+ payload: {
1004
+ namespace: model.namespace,
1005
+ modelSingularCode: model.singularCode,
1006
+ baseModelSingularCode: model.base,
1007
+ after: newEntity,
1008
+ },
1009
+ sender: plugin,
1010
+ routeContext,
1011
+ });
1012
+
1013
+ return newEntity;
1014
+ }
1015
+
1016
+ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: UpdateEntityByIdOptions, plugin?: RapidPlugin) {
1017
+ const model = dataAccessor.getModel();
1018
+ const { id, routeContext } = options;
1019
+ if (!id) {
1020
+ throw new Error("Id is required when updating an entity.");
1021
+ }
1022
+
1023
+ const entity = await findById(server, dataAccessor, {
1024
+ routeContext,
1025
+ id,
1026
+ keepNonPropertyFields: true,
1027
+ });
1028
+ if (!entity) {
1029
+ throw new Error(`${model.namespace}.${model.singularCode} with id "${id}" was not found.`);
1030
+ }
1031
+
1032
+ let { entityToSave } = options;
1033
+ let changes = getEntityPartChanges(server, model, entity, entityToSave);
1034
+ if (!changes && !options.operation) {
1035
+ return entity;
1036
+ }
1037
+
1038
+ entityToSave = changes || {};
1039
+
1040
+ const userId = options.routeContext?.state?.userId;
1041
+ if (userId) {
1042
+ const updatedByProperty = getEntityPropertyByCode(server, model, "updatedBy");
1043
+ if (updatedByProperty) {
1044
+ entityToSave.updatedBy = userId;
1045
+ }
1046
+ }
1047
+ const updatedAtProperty = getEntityPropertyByCode(server, model, "updatedAt");
1048
+ if (updatedAtProperty) {
1049
+ entityToSave.updatedAt = getNowStringWithTimezone();
1050
+ }
1051
+
1052
+ await server.beforeUpdateEntity(model, options, entity);
1053
+
1054
+ await server.emitEvent({
1055
+ eventName: "entity.beforeUpdate",
1056
+ payload: {
1057
+ namespace: model.namespace,
1058
+ modelSingularCode: model.singularCode,
1059
+ before: entity,
1060
+ changes: entityToSave,
1061
+ operation: options.operation,
1062
+ stateProperties: options.stateProperties,
1063
+ },
1064
+ sender: plugin,
1065
+ routeContext: options.routeContext,
1066
+ });
1067
+
1068
+ changes = getEntityPartChanges(server, model, entity, entityToSave);
1069
+
1070
+ // check readonly properties
1071
+ Object.keys(changes).forEach((propertyName) => {
1072
+ let isReadonlyProperty = false;
1073
+ const property = getEntityPropertyByCode(server, model, propertyName);
1074
+ if (property && property.readonly) {
1075
+ isReadonlyProperty = true;
1076
+ } else {
1077
+ const oneRelationProperty = getEntityProperty(server, model, (item) => item.relation === "one" && item.targetIdColumnName === propertyName);
1078
+ if (oneRelationProperty && oneRelationProperty.readonly) {
1079
+ isReadonlyProperty = true;
1080
+ }
1081
+ }
1082
+ if (isReadonlyProperty) {
1083
+ throw new Error(`Updating "${property.name}" property is not allowed because it's readonly.`);
1084
+ }
1085
+ });
1086
+
1087
+ // check unique constraints
1088
+ if (!options.postponeUniquenessCheck) {
1089
+ if (model.indexes && model.indexes.length) {
1090
+ for (const indexConfig of model.indexes) {
1091
+ if (!indexConfig.unique) {
1092
+ continue;
1093
+ }
1094
+
1095
+ const duplicate = await willEntityDuplicate(server, dataAccessor, {
1096
+ routeContext: options.routeContext,
1097
+ entityId: id,
1098
+ entityToSave: changes,
1099
+ indexConfig,
1100
+ });
1101
+ if (duplicate) {
1102
+ throw new Error(getEntityDuplicatedErrorMessage(server, model, indexConfig));
1103
+ }
1104
+ }
1105
+ }
1106
+ }
1107
+
1108
+ const oneRelationPropertiesToUpdate: RpdDataModelProperty[] = [];
1109
+ const manyRelationPropertiesToUpdate: RpdDataModelProperty[] = [];
1110
+ keys(changes).forEach((propertyCode) => {
1111
+ const property = getEntityPropertyByCode(server, model, propertyCode);
1112
+ if (!property) {
1113
+ // Unknown property
1114
+ return;
1115
+ }
1116
+
1117
+ if (isRelationProperty(property)) {
1118
+ if (property.relation === "many") {
1119
+ manyRelationPropertiesToUpdate.push(property);
1120
+ } else {
1121
+ oneRelationPropertiesToUpdate.push(property);
1122
+ }
1123
+ }
1124
+ });
1125
+
1126
+ const { row, baseRow } = mapEntityToDbRow(server, model, changes);
1127
+
1128
+ const updatedEntityOneRelationProps = {};
1129
+ for (const property of oneRelationPropertiesToUpdate) {
1130
+ const rowToBeSaved = property.isBaseProperty ? baseRow : row;
1131
+ const relatedEntityToBeSaved = changes[property.code];
1132
+ const targetDataAccessor = server.getDataAccessor({
1133
+ singularCode: property.targetSingularCode!,
1134
+ });
1135
+
1136
+ if (isObject(relatedEntityToBeSaved)) {
1137
+ const relatedEntityId = relatedEntityToBeSaved["id"];
1138
+ if (!relatedEntityId) {
1139
+ if (!property.selfIdColumnName) {
1140
+ const targetEntity = relatedEntityToBeSaved;
1141
+ const newTargetEntity = await createEntity(server, targetDataAccessor, {
1142
+ routeContext,
1143
+ entity: targetEntity,
1144
+ });
1145
+ updatedEntityOneRelationProps[property.code] = newTargetEntity;
1146
+ rowToBeSaved[property.targetIdColumnName!] = newTargetEntity["id"];
1147
+ }
1148
+ } else {
1149
+ let targetEntity = await findById(server, targetDataAccessor, {
1150
+ id: relatedEntityId,
1151
+ routeContext,
1152
+ });
1153
+ if (!targetEntity) {
1154
+ throw newEntityOperationError(
1155
+ `Update ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${relatedEntityId}' was not found.`,
1156
+ );
1157
+ }
1158
+
1159
+ // update relation entity if options.relationPropertiesToUpdate is specified.
1160
+ const updateRelationPropertiesOptions = get(options.relationPropertiesToUpdate, property.code);
1161
+ let subRelationPropertiesToUpdate = undefined;
1162
+ let relationEntityToUpdate = null;
1163
+ if (updateRelationPropertiesOptions === true) {
1164
+ relationEntityToUpdate = targetEntity;
1165
+ } else if (updateRelationPropertiesOptions) {
1166
+ let propertiesToUpdate = uniq([
1167
+ "id",
1168
+ ...(updateRelationPropertiesOptions.propertiesToUpdate || []),
1169
+ ...Object.keys(updateRelationPropertiesOptions.relationPropertiesToUpdate || []),
1170
+ ]);
1171
+ relationEntityToUpdate = pick(relatedEntityToBeSaved, propertiesToUpdate);
1172
+ subRelationPropertiesToUpdate = updateRelationPropertiesOptions.relationPropertiesToUpdate;
1173
+ }
1174
+ if (relationEntityToUpdate) {
1175
+ targetEntity = await updateEntityById(server, targetDataAccessor, {
1176
+ routeContext: routeContext,
1177
+ id: relatedEntityId,
1178
+ entityToSave: relationEntityToUpdate,
1179
+ relationPropertiesToUpdate: subRelationPropertiesToUpdate,
1180
+ });
1181
+ }
1182
+
1183
+ updatedEntityOneRelationProps[property.code] = targetEntity;
1184
+ rowToBeSaved[property.targetIdColumnName!] = relatedEntityId;
1185
+ }
1186
+ } else if (isNumber(relatedEntityToBeSaved) || isString(relatedEntityToBeSaved)) {
1187
+ // fieldValue is id;
1188
+ const targetEntityId = relatedEntityToBeSaved;
1189
+ const targetEntity = await findById(server, targetDataAccessor, {
1190
+ id: targetEntityId,
1191
+ routeContext,
1192
+ });
1193
+ if (!targetEntity) {
1194
+ throw newEntityOperationError(
1195
+ `Create ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${targetEntityId}' was not found.`,
1196
+ );
1197
+ }
1198
+ updatedEntityOneRelationProps[property.code] = targetEntity;
1199
+ rowToBeSaved[property.targetIdColumnName!] = targetEntityId;
1200
+ } else {
1201
+ updatedEntityOneRelationProps[property.code] = null;
1202
+ rowToBeSaved[property.targetIdColumnName!] = null;
1203
+ }
1204
+ }
1205
+
1206
+ let updatedRow = row;
1207
+ if (Object.keys(row).length) {
1208
+ updatedRow = await dataAccessor.updateById(id, row);
1209
+ }
1210
+ let updatedBaseRow = baseRow;
1211
+ let baseDataAccessor: any;
1212
+ if (model.base) {
1213
+ baseDataAccessor = server.getDataAccessor({
1214
+ singularCode: model.base,
1215
+ });
1216
+ if (Object.keys(baseRow).length) {
1217
+ updatedBaseRow = await baseDataAccessor.updateById(id, updatedBaseRow);
1218
+ }
1219
+ }
1220
+
1221
+ let updatedEntity = mapDbRowToEntity(server, model, { ...updatedRow, ...updatedBaseRow, ...updatedEntityOneRelationProps }, true);
1222
+ updatedEntity = Object.assign({}, entity, updatedEntity);
1223
+
1224
+ // save one-relation properties that has selfIdColumnName
1225
+ for (const property of oneRelationPropertiesToUpdate) {
1226
+ const fieldValue = changes[property.code];
1227
+ const targetDataAccessor = server.getDataAccessor({
1228
+ singularCode: property.targetSingularCode!,
1229
+ });
1230
+ if (isObject(fieldValue)) {
1231
+ const targetEntityId = fieldValue["id"];
1232
+ if (!targetEntityId) {
1233
+ if (property.selfIdColumnName) {
1234
+ const targetEntity = fieldValue;
1235
+ targetEntity[property.selfIdColumnName] = updatedEntity.id;
1236
+ const newTargetEntity = await createEntity(server, targetDataAccessor, {
1237
+ routeContext,
1238
+ entity: targetEntity,
1239
+ });
1240
+
1241
+ let dataAccessorOfMainEntity = dataAccessor;
1242
+ if (property.isBaseProperty) {
1243
+ dataAccessorOfMainEntity = baseDataAccessor;
1244
+ }
1245
+
1246
+ const relationFieldChanges = {
1247
+ [property.targetIdColumnName]: newTargetEntity.id,
1248
+ };
1249
+ await dataAccessorOfMainEntity.updateById(updatedEntity.id, relationFieldChanges);
1250
+ updatedEntity[property.code] = newTargetEntity;
1251
+ changes[property.code] = newTargetEntity;
1252
+ }
1253
+ }
1254
+ }
1255
+ }
1256
+
1257
+ // save many-relation properties
1258
+ for (const property of manyRelationPropertiesToUpdate) {
1259
+ const relatedEntities: any[] = [];
1260
+ const targetDataAccessor = server.getDataAccessor({
1261
+ singularCode: property.targetSingularCode!,
1262
+ });
1263
+
1264
+ const relatedEntitiesToBeSaved = changes[property.code];
1265
+ if (!isArray(relatedEntitiesToBeSaved)) {
1266
+ throw new Error(`Value of field '${property.code}' should be an array.`);
1267
+ }
1268
+
1269
+ const targetIdsToKeep = [];
1270
+ for (const relatedEntityToBeSaved of relatedEntitiesToBeSaved) {
1271
+ let relatedEntityId: any;
1272
+ if (isObject(relatedEntityToBeSaved)) {
1273
+ relatedEntityId = relatedEntityToBeSaved["id"];
1274
+ } else {
1275
+ relatedEntityId = relatedEntityToBeSaved;
1276
+ }
1277
+ if (relatedEntityId) {
1278
+ targetIdsToKeep.push(relatedEntityId);
1279
+ }
1280
+ }
1281
+
1282
+ let currentTargetIds: any[] = [];
1283
+ if (property.linkTableName) {
1284
+ const targetLinks = await server.queryDatabaseObject(
1285
+ `SELECT ${server.queryBuilder.quoteObject(property.targetIdColumnName)} FROM ${server.queryBuilder.quoteTable({
1286
+ schema: property.linkSchema,
1287
+ tableName: property.linkTableName,
1288
+ })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName!)} = $1`,
1289
+ [id],
1290
+ );
1291
+ currentTargetIds = targetLinks.map((item) => item[property.targetIdColumnName]);
1292
+
1293
+ await server.queryDatabaseObject(
1294
+ `DELETE FROM ${server.queryBuilder.quoteTable({
1295
+ schema: property.linkSchema,
1296
+ tableName: property.linkTableName,
1297
+ })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName!)} = $1
1298
+ AND ${server.queryBuilder.quoteObject(property.targetIdColumnName!)} <> ALL($2::int[])`,
1299
+ [id, targetIdsToKeep],
1300
+ );
1301
+ } else {
1302
+ const targetModel = server.getModel({
1303
+ singularCode: property.targetSingularCode,
1304
+ });
1305
+ const targetRows = await server.queryDatabaseObject(
1306
+ `SELECT id FROM ${server.queryBuilder.quoteTable({
1307
+ schema: targetModel.schema,
1308
+ tableName: targetModel.tableName,
1309
+ })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName!)} = $1`,
1310
+ [id],
1311
+ );
1312
+ currentTargetIds = targetRows.map((item) => item.id);
1313
+ }
1314
+
1315
+ for (const relatedEntityToBeSaved of relatedEntitiesToBeSaved) {
1316
+ let relatedEntityId: any;
1317
+ if (isObject(relatedEntityToBeSaved)) {
1318
+ relatedEntityId = relatedEntityToBeSaved["id"];
1319
+ if (!relatedEntityId) {
1320
+ // related entity is to be created
1321
+ const targetEntity = relatedEntityToBeSaved;
1322
+ if (!property.linkTableName) {
1323
+ targetEntity[property.selfIdColumnName!] = id;
1324
+ }
1325
+ const newTargetEntity = await createEntity(server, targetDataAccessor, {
1326
+ routeContext,
1327
+ entity: targetEntity,
1328
+ });
1329
+
1330
+ if (property.linkTableName) {
1331
+ const command = `INSERT INTO ${server.queryBuilder.quoteTable({
1332
+ schema: property.linkSchema,
1333
+ tableName: property.linkTableName,
1334
+ })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1335
+ const params = [id, newTargetEntity.id];
1336
+ await server.queryDatabaseObject(command, params);
1337
+ }
1338
+
1339
+ relatedEntities.push(newTargetEntity);
1340
+ } else {
1341
+ // related entity is existed
1342
+ let targetEntity = await targetDataAccessor.findById(relatedEntityId);
1343
+ if (!targetEntity) {
1344
+ throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' does not exist.`);
1345
+ }
1346
+
1347
+ // update relation entity if options.relationPropertiesToUpdate is specified.
1348
+ const updateRelationPropertiesOptions = get(options.relationPropertiesToUpdate, property.code);
1349
+ let subRelationPropertiesToUpdate = undefined;
1350
+ let relationEntityToUpdate = null;
1351
+ if (updateRelationPropertiesOptions === true) {
1352
+ relationEntityToUpdate = targetEntity;
1353
+ } else if (updateRelationPropertiesOptions) {
1354
+ let propertiesToUpdate = uniq([
1355
+ "id",
1356
+ ...(updateRelationPropertiesOptions.propertiesToUpdate || []),
1357
+ ...Object.keys(updateRelationPropertiesOptions.relationPropertiesToUpdate || []),
1358
+ ]);
1359
+ relationEntityToUpdate = pick(relatedEntityToBeSaved, propertiesToUpdate);
1360
+ subRelationPropertiesToUpdate = updateRelationPropertiesOptions.relationPropertiesToUpdate;
1361
+ }
1362
+ if (relationEntityToUpdate) {
1363
+ targetEntity = await updateEntityById(server, targetDataAccessor, {
1364
+ routeContext: routeContext,
1365
+ id: relatedEntityId,
1366
+ entityToSave: relationEntityToUpdate,
1367
+ relationPropertiesToUpdate: subRelationPropertiesToUpdate,
1368
+ });
1369
+ }
1370
+
1371
+ if (!currentTargetIds.includes(relatedEntityId)) {
1372
+ if (property.linkTableName) {
1373
+ const command = `INSERT INTO ${server.queryBuilder.quoteTable({
1374
+ schema: property.linkSchema,
1375
+ tableName: property.linkTableName,
1376
+ })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1377
+ const params = [id, relatedEntityId];
1378
+ await server.queryDatabaseObject(command, params);
1379
+ } else {
1380
+ await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: id });
1381
+ targetEntity[property.selfIdColumnName!] = id;
1382
+ }
1383
+ }
1384
+ relatedEntities.push(targetEntity);
1385
+ }
1386
+ } else {
1387
+ // fieldValue is id
1388
+ relatedEntityId = relatedEntityToBeSaved;
1389
+ const targetEntity = await targetDataAccessor.findById(relatedEntityId);
1390
+ if (!targetEntity) {
1391
+ throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
1392
+ }
1393
+
1394
+ if (!currentTargetIds.includes(relatedEntityId)) {
1395
+ if (property.linkTableName) {
1396
+ const command = `INSERT INTO ${server.queryBuilder.quoteTable({
1397
+ schema: property.linkSchema,
1398
+ tableName: property.linkTableName,
1399
+ })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1400
+ const params = [id, relatedEntityId];
1401
+ await server.queryDatabaseObject(command, params);
1402
+ } else {
1403
+ await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: id });
1404
+ targetEntity[property.selfIdColumnName!] = id;
1405
+ }
1406
+ }
1407
+
1408
+ relatedEntities.push(targetEntity);
1409
+ }
1410
+ }
1411
+ updatedEntity[property.code] = relatedEntities;
1412
+ }
1413
+
1414
+ await server.emitEvent({
1415
+ eventName: "entity.update",
1416
+ payload: {
1417
+ namespace: model.namespace,
1418
+ modelSingularCode: model.singularCode,
1419
+ // TODO: should not emit event on base model if it's not effected.
1420
+ baseModelSingularCode: model.base,
1421
+ before: entity,
1422
+ after: updatedEntity,
1423
+ changes: changes,
1424
+ operation: options.operation,
1425
+ stateProperties: options.stateProperties,
1426
+ },
1427
+ sender: plugin,
1428
+ routeContext: options.routeContext,
1429
+ });
1430
+
1431
+ return updatedEntity;
1432
+ }
1433
+
1434
+ export type CheckEntityDuplicatedOptions = {
1435
+ routeContext?: RouteContext;
1436
+ entityId?: number;
1437
+ entityToSave: any;
1438
+ indexConfig: RpdDataModelIndex;
1439
+ };
1440
+
1441
+ async function willEntityDuplicate(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: CheckEntityDuplicatedOptions): Promise<boolean> {
1442
+ const { entityId, entityToSave, routeContext, indexConfig } = options;
1443
+
1444
+ let filters: EntityFilterOptions[] = [];
1445
+ if (indexConfig.conditions) {
1446
+ filters = cloneDeep(indexConfig.conditions);
1447
+ }
1448
+
1449
+ for (const propConfig of indexConfig.properties) {
1450
+ let propCode: string;
1451
+ if (isString(propConfig)) {
1452
+ propCode = propConfig;
1453
+ } else {
1454
+ propCode = propConfig.code;
1455
+ }
1456
+
1457
+ if (!entityToSave.hasOwnProperty(propCode)) {
1458
+ // skip duplicate checking when any index prop missing in entityToSave.
1459
+ return false;
1460
+ }
1461
+
1462
+ filters.push({
1463
+ operator: "eq",
1464
+ field: propCode,
1465
+ value: entityToSave[propCode],
1466
+ });
1467
+ }
1468
+
1469
+ const entityInDb = await findEntity(server, dataAccessor, {
1470
+ filters,
1471
+ routeContext,
1472
+ });
1473
+
1474
+ if (entityId) {
1475
+ return entityInDb && entityInDb.id !== entityId;
1476
+ } else {
1477
+ return !!entityInDb;
1478
+ }
1479
+ }
1480
+
1481
+ function getEntityDuplicatedErrorMessage(server: IRpdServer, model: RpdDataModel, indexConfig: RpdDataModelIndex) {
1482
+ if (indexConfig.duplicateErrorMessage) {
1483
+ return indexConfig.duplicateErrorMessage;
1484
+ }
1485
+
1486
+ const propertyNames = indexConfig.properties.map((propConfig) => {
1487
+ let propCode: string;
1488
+ if (isString(propConfig)) {
1489
+ propCode = propConfig;
1490
+ } else {
1491
+ propCode = propConfig.code;
1492
+ }
1493
+ const prop = getEntityPropertyByCode(server, model, propCode);
1494
+ return prop.name;
1495
+ });
1496
+
1497
+ return `已存在 ${propertyNames.join(", ")} 相同的记录。`;
1498
+ }
1499
+
1500
+ export default class EntityManager<TEntity = any> {
1501
+ #server: IRpdServer;
1502
+ #dataAccessor: IRpdDataAccessor;
1503
+
1504
+ constructor(server: IRpdServer, dataAccessor: IRpdDataAccessor) {
1505
+ this.#server = server;
1506
+ this.#dataAccessor = dataAccessor;
1507
+ }
1508
+
1509
+ getModel(): RpdDataModel {
1510
+ return this.#dataAccessor.getModel();
1511
+ }
1512
+
1513
+ async findEntities(options: FindEntityOptions): Promise<TEntity[]> {
1514
+ return await findEntities(this.#server, this.#dataAccessor, options);
1515
+ }
1516
+
1517
+ async findEntity(options: FindEntityOptions): Promise<TEntity | null> {
1518
+ return await findEntity(this.#server, this.#dataAccessor, options);
1519
+ }
1520
+
1521
+ async findById(options: FindEntityByIdOptions | string | number): Promise<TEntity | null> {
1522
+ // options is id
1523
+ if (!isObject(options)) {
1524
+ options = {
1525
+ id: options,
1526
+ };
1527
+ }
1528
+ return await findById(this.#server, this.#dataAccessor, options);
1529
+ }
1530
+
1531
+ async createEntity(options: CreateEntityOptions, plugin?: RapidPlugin): Promise<TEntity> {
1532
+ return await createEntity(this.#server, this.#dataAccessor, options, plugin);
1533
+ }
1534
+
1535
+ async updateEntityById(options: UpdateEntityByIdOptions, plugin?: RapidPlugin): Promise<TEntity> {
1536
+ return await updateEntityById(this.#server, this.#dataAccessor, options, plugin);
1537
+ }
1538
+
1539
+ async count(options: CountEntityOptions): Promise<number> {
1540
+ const model = this.#dataAccessor.getModel();
1541
+ let baseModel: RpdDataModel;
1542
+ if (model.base) {
1543
+ baseModel = this.#server.getModel({
1544
+ singularCode: model.base,
1545
+ });
1546
+ }
1547
+ const countRowOptions: CountRowOptions = {
1548
+ filters: await convertEntityFiltersToRowFilters(this.#server, model, baseModel, options.filters),
1549
+ };
1550
+ return await this.#dataAccessor.count(countRowOptions);
1551
+ }
1552
+
1553
+ async deleteById(options: DeleteEntityByIdOptions | string | number, plugin?: RapidPlugin): Promise<void> {
1554
+ // options is id
1555
+ if (!isObject(options)) {
1556
+ options = {
1557
+ id: options,
1558
+ };
1559
+ }
1560
+
1561
+ const model = this.getModel();
1562
+ if (model.derivedTypePropertyCode) {
1563
+ throw newEntityOperationError("Delete base entity directly is not allowed.");
1564
+ }
1565
+
1566
+ const { id, routeContext } = options;
1567
+
1568
+ const entity = await this.findById({
1569
+ id,
1570
+ keepNonPropertyFields: true,
1571
+ routeContext,
1572
+ });
1573
+
1574
+ if (!entity) {
1575
+ return;
1576
+ }
1577
+
1578
+ await this.#server.emitEvent({
1579
+ eventName: "entity.beforeDelete",
1580
+ payload: {
1581
+ namespace: model.namespace,
1582
+ modelSingularCode: model.singularCode,
1583
+ before: entity,
1584
+ },
1585
+ sender: plugin,
1586
+ routeContext,
1587
+ });
1588
+
1589
+ if (model.softDelete) {
1590
+ let dataAccessor = model.base
1591
+ ? this.#server.getDataAccessor({
1592
+ singularCode: model.base,
1593
+ })
1594
+ : this.#dataAccessor;
1595
+ const currentUserId = routeContext?.state?.userId;
1596
+ await dataAccessor.updateById(id, {
1597
+ deleted_at: getNowStringWithTimezone(),
1598
+ deleter_id: currentUserId,
1599
+ });
1600
+ } else {
1601
+ await this.#dataAccessor.deleteById(id);
1602
+ if (model.base) {
1603
+ const baseDataAccessor = this.#server.getDataAccessor({
1604
+ singularCode: model.base,
1605
+ });
1606
+ await baseDataAccessor.deleteById(id);
1607
+ }
1608
+ }
1609
+
1610
+ await this.#server.emitEvent({
1611
+ eventName: "entity.delete",
1612
+ payload: {
1613
+ namespace: model.namespace,
1614
+ modelSingularCode: model.singularCode,
1615
+ before: entity,
1616
+ },
1617
+ sender: plugin,
1618
+ routeContext,
1619
+ });
1620
+ }
1621
+
1622
+ async addRelations(options: AddEntityRelationsOptions, plugin?: RapidPlugin): Promise<void> {
1623
+ const server = this.#server;
1624
+ const model = this.getModel();
1625
+ const { id, property, relations, routeContext } = options;
1626
+ const entity = await this.findById({
1627
+ id,
1628
+ routeContext,
1629
+ });
1630
+ if (!entity) {
1631
+ throw new Error(`${model.namespace}.${model.singularCode} with id "${id}" was not found.`);
1632
+ }
1633
+
1634
+ const relationProperty = getEntityPropertyByCode(server, model, property);
1635
+ if (!relationProperty) {
1636
+ throw new Error(`Property '${property}' was not found in ${model.namespace}.${model.singularCode}`);
1637
+ }
1638
+
1639
+ if (!(isRelationProperty(relationProperty) && relationProperty.relation === "many")) {
1640
+ throw new Error(`Operation 'addRelations' is only supported on property of 'many' relation`);
1641
+ }
1642
+
1643
+ const { queryBuilder } = server;
1644
+ if (relationProperty.linkTableName) {
1645
+ for (const relation of relations) {
1646
+ const command = `INSERT INTO ${queryBuilder.quoteTable({
1647
+ schema: relationProperty.linkSchema,
1648
+ tableName: relationProperty.linkTableName,
1649
+ })} (${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}, ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)})
1650
+ SELECT $1, $2 WHERE NOT EXISTS (
1651
+ SELECT ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}, ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}
1652
+ FROM ${queryBuilder.quoteTable({ schema: relationProperty.linkSchema, tableName: relationProperty.linkTableName })}
1653
+ WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}=$2
1654
+ )`;
1655
+ const params = [id, relation.id];
1656
+ await server.queryDatabaseObject(command, params);
1657
+ }
1658
+ }
1659
+
1660
+ await server.emitEvent({
1661
+ eventName: "entity.addRelations",
1662
+ payload: {
1663
+ namespace: model.namespace,
1664
+ modelSingularCode: model.singularCode,
1665
+ entity,
1666
+ property,
1667
+ relations,
1668
+ },
1669
+ sender: plugin,
1670
+ routeContext: options.routeContext,
1671
+ });
1672
+ }
1673
+
1674
+ async removeRelations(options: RemoveEntityRelationsOptions, plugin?: RapidPlugin): Promise<void> {
1675
+ const server = this.#server;
1676
+ const model = this.getModel();
1677
+ const { id, property, relations, routeContext } = options;
1678
+ const entity = await this.findById({
1679
+ id,
1680
+ routeContext,
1681
+ });
1682
+ if (!entity) {
1683
+ throw new Error(`${model.namespace}.${model.singularCode} with id "${id}" was not found.`);
1684
+ }
1685
+
1686
+ const relationProperty = getEntityPropertyByCode(server, model, property);
1687
+ if (!relationProperty) {
1688
+ throw new Error(`Property '${property}' was not found in ${model.namespace}.${model.singularCode}`);
1689
+ }
1690
+
1691
+ if (!(isRelationProperty(relationProperty) && relationProperty.relation === "many")) {
1692
+ throw new Error(`Operation 'removeRelations' is only supported on property of 'many' relation`);
1693
+ }
1694
+
1695
+ const { queryBuilder } = server;
1696
+ if (relationProperty.linkTableName) {
1697
+ for (const relation of relations) {
1698
+ const command = `DELETE FROM ${queryBuilder.quoteTable({ schema: relationProperty.linkSchema, tableName: relationProperty.linkTableName })}
1699
+ WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}=$2;`;
1700
+ const params = [id, relation.id];
1701
+ await server.queryDatabaseObject(command, params);
1702
+ }
1703
+ }
1704
+
1705
+ await server.emitEvent({
1706
+ eventName: "entity.removeRelations",
1707
+ payload: {
1708
+ namespace: model.namespace,
1709
+ modelSingularCode: model.singularCode,
1710
+ entity,
1711
+ property,
1712
+ relations,
1713
+ },
1714
+ sender: plugin,
1715
+ routeContext: options.routeContext,
1716
+ });
1717
+ }
1718
+ }