@ruiapp/rapid-core 0.5.11 → 0.5.13

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