@ruiapp/rapid-core 0.1.62 → 0.1.65

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 (164) hide show
  1. package/dist/dataAccess/dataAccessTypes.d.ts +7 -7
  2. package/dist/dataAccess/entityManager.d.ts +15 -1
  3. package/dist/helpers/entityHelpers.d.ts +3 -1
  4. package/dist/{dataAccess → helpers}/metaHelper.d.ts +3 -0
  5. package/dist/index.js +225 -69
  6. package/dist/plugins/setting/SettingPlugin.d.ts +21 -0
  7. package/dist/plugins/setting/SettingPluginTypes.d.ts +31 -0
  8. package/dist/plugins/setting/SettingService.d.ts +10 -0
  9. package/dist/plugins/setting/actionHandlers/getSystemSettingValues.d.ts +7 -0
  10. package/dist/plugins/setting/actionHandlers/index.d.ts +3 -0
  11. package/dist/plugins/setting/models/SystemSettingGroupSetting.d.ts +3 -0
  12. package/dist/plugins/setting/models/SystemSettingItem.d.ts +3 -0
  13. package/dist/plugins/setting/models/SystemSettingItemSetting.d.ts +3 -0
  14. package/dist/plugins/setting/models/UserSettingGroupSetting.d.ts +3 -0
  15. package/dist/plugins/setting/models/UserSettingItem.d.ts +3 -0
  16. package/dist/plugins/setting/models/UserSettingItemSetting.d.ts +3 -0
  17. package/dist/plugins/setting/models/index.d.ts +2 -0
  18. package/dist/plugins/setting/routes/getSystemSettingValues.d.ts +12 -0
  19. package/dist/plugins/setting/routes/index.d.ts +12 -0
  20. package/dist/queryBuilder/queryBuilder.d.ts +2 -2
  21. package/dist/types.d.ts +12 -1
  22. package/package.json +1 -1
  23. package/rollup.config.js +16 -16
  24. package/src/bootstrapApplicationConfig.ts +552 -545
  25. package/src/core/actionHandler.ts +22 -22
  26. package/src/core/eventManager.ts +20 -20
  27. package/src/core/facility.ts +7 -7
  28. package/src/core/http/formDataParser.ts +89 -89
  29. package/src/core/http-types.ts +4 -4
  30. package/src/core/pluginManager.ts +175 -175
  31. package/src/core/providers/runtimeProvider.ts +5 -5
  32. package/src/core/request.ts +86 -86
  33. package/src/core/response.ts +76 -76
  34. package/src/core/routeContext.ts +43 -43
  35. package/src/core/routesBuilder.ts +88 -88
  36. package/src/core/server.ts +16 -1
  37. package/src/dataAccess/dataAccessTypes.ts +109 -86
  38. package/src/dataAccess/dataAccessor.ts +137 -137
  39. package/src/dataAccess/entityManager.ts +1298 -1187
  40. package/src/dataAccess/entityMapper.ts +4 -5
  41. package/src/dataAccess/propertyMapper.ts +27 -27
  42. package/src/deno-std/assert/assert.ts +9 -9
  43. package/src/deno-std/assert/assertion_error.ts +7 -7
  44. package/src/deno-std/datetime/to_imf.ts +32 -32
  45. package/src/deno-std/encoding/base64.ts +141 -141
  46. package/src/deno-std/http/cookie.ts +8 -1
  47. package/src/facilities/log/LogFacility.ts +35 -35
  48. package/src/helpers/entityHelpers.ts +76 -21
  49. package/src/{dataAccess → helpers}/filterHelper.ts +3 -1
  50. package/src/helpers/inputHelper.ts +11 -11
  51. package/src/{dataAccess → helpers}/metaHelper.ts +15 -6
  52. package/src/helpers/runCollectionEntityActionHandler.ts +6 -1
  53. package/src/index.ts +41 -41
  54. package/src/plugins/auth/AuthPlugin.ts +7 -1
  55. package/src/plugins/auth/actionHandlers/changePassword.ts +54 -54
  56. package/src/plugins/auth/actionHandlers/createSession.ts +63 -63
  57. package/src/plugins/auth/actionHandlers/deleteSession.ts +18 -18
  58. package/src/plugins/auth/actionHandlers/getMyProfile.ts +35 -35
  59. package/src/plugins/auth/actionHandlers/index.ts +8 -8
  60. package/src/plugins/auth/actionHandlers/resetPassword.ts +38 -38
  61. package/src/plugins/auth/models/AccessToken.ts +56 -56
  62. package/src/plugins/auth/models/index.ts +3 -3
  63. package/src/plugins/auth/routes/changePassword.ts +15 -15
  64. package/src/plugins/auth/routes/getMyProfile.ts +15 -15
  65. package/src/plugins/auth/routes/index.ts +7 -7
  66. package/src/plugins/auth/routes/resetPassword.ts +15 -15
  67. package/src/plugins/auth/routes/signin.ts +15 -15
  68. package/src/plugins/auth/routes/signout.ts +15 -15
  69. package/src/plugins/cronJob/CronJobPlugin.ts +7 -1
  70. package/src/plugins/cronJob/CronJobPluginTypes.ts +49 -49
  71. package/src/plugins/cronJob/actionHandlers/index.ts +4 -4
  72. package/src/plugins/cronJob/actionHandlers/runCronJob.ts +29 -29
  73. package/src/plugins/cronJob/routes/index.ts +3 -3
  74. package/src/plugins/cronJob/routes/runCronJob.ts +15 -15
  75. package/src/plugins/dataManage/DataManagePlugin.ts +7 -1
  76. package/src/plugins/dataManage/actionHandlers/addEntityRelations.ts +20 -20
  77. package/src/plugins/dataManage/actionHandlers/countCollectionEntities.ts +15 -15
  78. package/src/plugins/dataManage/actionHandlers/createCollectionEntitiesBatch.ts +42 -42
  79. package/src/plugins/dataManage/actionHandlers/createCollectionEntity.ts +24 -24
  80. package/src/plugins/dataManage/actionHandlers/deleteCollectionEntityById.ts +7 -4
  81. package/src/plugins/dataManage/actionHandlers/findCollectionEntities.ts +26 -26
  82. package/src/plugins/dataManage/actionHandlers/findCollectionEntityById.ts +21 -21
  83. package/src/plugins/dataManage/actionHandlers/queryDatabase.ts +22 -22
  84. package/src/plugins/dataManage/actionHandlers/removeEntityRelations.ts +20 -20
  85. package/src/plugins/dataManage/actionHandlers/updateCollectionEntityById.ts +35 -35
  86. package/src/plugins/entityAccessControl/EntityAccessControlPlugin.ts +146 -146
  87. package/src/plugins/fileManage/FileManagePlugin.ts +7 -1
  88. package/src/plugins/fileManage/actionHandlers/downloadDocument.ts +36 -36
  89. package/src/plugins/fileManage/actionHandlers/downloadFile.ts +28 -28
  90. package/src/plugins/fileManage/actionHandlers/uploadFile.ts +33 -33
  91. package/src/plugins/fileManage/routes/downloadDocument.ts +15 -15
  92. package/src/plugins/fileManage/routes/downloadFile.ts +15 -15
  93. package/src/plugins/fileManage/routes/index.ts +5 -5
  94. package/src/plugins/fileManage/routes/uploadFile.ts +15 -15
  95. package/src/plugins/metaManage/MetaManagePlugin.ts +37 -9
  96. package/src/plugins/metaManage/actionHandlers/getMetaModelDetail.ts +10 -10
  97. package/src/plugins/metaManage/actionHandlers/listMetaModels.ts +9 -9
  98. package/src/plugins/metaManage/actionHandlers/listMetaRoutes.ts +9 -9
  99. package/src/plugins/routeManage/RouteManagePlugin.ts +7 -1
  100. package/src/plugins/routeManage/actionHandlers/httpProxy.ts +13 -13
  101. package/src/plugins/sequence/SequencePlugin.ts +8 -2
  102. package/src/plugins/sequence/SequencePluginTypes.ts +9 -1
  103. package/src/plugins/sequence/SequenceService.ts +81 -81
  104. package/src/plugins/sequence/actionHandlers/generateSn.ts +32 -32
  105. package/src/plugins/sequence/actionHandlers/index.ts +4 -4
  106. package/src/plugins/sequence/models/SequenceAutoIncrementRecord.ts +49 -49
  107. package/src/plugins/sequence/models/SequenceRule.ts +42 -42
  108. package/src/plugins/sequence/models/index.ts +4 -4
  109. package/src/plugins/sequence/routes/generateSn.ts +15 -15
  110. package/src/plugins/sequence/routes/index.ts +3 -3
  111. package/src/plugins/sequence/segment-utility.ts +11 -11
  112. package/src/plugins/sequence/segments/autoIncrement.ts +6 -1
  113. package/src/plugins/sequence/segments/dayOfMonth.ts +6 -1
  114. package/src/plugins/sequence/segments/index.ts +9 -9
  115. package/src/plugins/sequence/segments/literal.ts +6 -1
  116. package/src/plugins/sequence/segments/month.ts +6 -1
  117. package/src/plugins/sequence/segments/parameter.ts +6 -1
  118. package/src/plugins/sequence/segments/year.ts +6 -1
  119. package/src/plugins/serverOperation/ServerOperationPlugin.ts +91 -91
  120. package/src/plugins/serverOperation/ServerOperationPluginTypes.ts +15 -15
  121. package/src/plugins/serverOperation/actionHandlers/index.ts +4 -4
  122. package/src/plugins/serverOperation/actionHandlers/runServerOperation.ts +15 -15
  123. package/src/plugins/setting/SettingPlugin.ts +71 -0
  124. package/src/plugins/setting/SettingPluginTypes.ts +35 -0
  125. package/src/plugins/setting/SettingService.ts +17 -0
  126. package/src/plugins/setting/actionHandlers/getSystemSettingValues.ts +30 -0
  127. package/src/plugins/setting/actionHandlers/index.ts +4 -0
  128. package/src/plugins/setting/models/SystemSettingGroupSetting.ts +56 -0
  129. package/src/plugins/setting/models/SystemSettingItem.ts +42 -0
  130. package/src/plugins/setting/models/SystemSettingItemSetting.ts +72 -0
  131. package/src/plugins/setting/models/UserSettingGroupSetting.ts +56 -0
  132. package/src/plugins/setting/models/UserSettingItem.ts +49 -0
  133. package/src/plugins/setting/models/UserSettingItemSetting.ts +72 -0
  134. package/src/plugins/setting/models/index.ts +8 -0
  135. package/src/plugins/setting/routes/getSystemSettingValues.ts +15 -0
  136. package/src/plugins/setting/routes/index.ts +3 -0
  137. package/src/plugins/stateMachine/StateMachinePlugin.ts +8 -2
  138. package/src/plugins/stateMachine/StateMachinePluginTypes.ts +9 -1
  139. package/src/plugins/stateMachine/actionHandlers/index.ts +4 -4
  140. package/src/plugins/stateMachine/actionHandlers/sendStateMachineEvent.ts +51 -51
  141. package/src/plugins/stateMachine/models/StateMachine.ts +42 -42
  142. package/src/plugins/stateMachine/models/index.ts +3 -3
  143. package/src/plugins/stateMachine/routes/index.ts +3 -3
  144. package/src/plugins/stateMachine/routes/sendStateMachineEvent.ts +15 -15
  145. package/src/plugins/stateMachine/stateMachineHelper.ts +36 -36
  146. package/src/plugins/webhooks/WebhooksPlugin.ts +13 -2
  147. package/src/plugins/webhooks/pluginConfig.ts +74 -74
  148. package/src/polyfill.ts +5 -5
  149. package/src/proxy/mod.ts +38 -38
  150. package/src/proxy/types.ts +21 -21
  151. package/src/queryBuilder/index.ts +1 -1
  152. package/src/queryBuilder/queryBuilder.ts +473 -473
  153. package/src/server.ts +23 -3
  154. package/src/types.ts +593 -535
  155. package/src/utilities/accessControlUtility.ts +33 -33
  156. package/src/utilities/errorUtility.ts +4 -6
  157. package/src/utilities/fsUtility.ts +61 -61
  158. package/src/utilities/httpUtility.ts +19 -19
  159. package/src/utilities/jwtUtility.ts +26 -26
  160. package/src/utilities/typeUtility.ts +11 -11
  161. package/tsconfig.json +19 -19
  162. package/dist/utilities/rapidUtility.d.ts +0 -2
  163. package/src/utilities/rapidUtility.ts +0 -5
  164. /package/dist/{dataAccess → helpers}/filterHelper.d.ts +0 -0
@@ -1,1187 +1,1298 @@
1
- import {
2
- AddEntityRelationsOptions,
3
- CountEntityOptions,
4
- CountEntityResult,
5
- CreateEntityOptions,
6
- DeleteEntityByIdOptions,
7
- EntityFilterOperators,
8
- EntityFilterOptions,
9
- EntityNonRelationPropertyFilterOptions,
10
- FindEntityByIdOptions,
11
- FindEntityOptions,
12
- FindEntityOrderByOptions,
13
- IRpdDataAccessor,
14
- RemoveEntityRelationsOptions,
15
- RpdDataModel,
16
- RpdDataModelProperty,
17
- UpdateEntityByIdOptions,
18
- } from "~/types";
19
- import { isNullOrUndefined } from "~/utilities/typeUtility";
20
- import { mapDbRowToEntity, mapEntityToDbRow } from "./entityMapper";
21
- import { mapPropertyNameToColumnName } from "./propertyMapper";
22
- import { isRelationProperty } from "~/utilities/rapidUtility";
23
- import { IRpdServer, RapidPlugin } from "~/core/server";
24
- import { getEntityPartChanges } from "~/helpers/entityHelpers";
25
- import { filter, find, first, forEach, isArray, isObject, keys, map, reject, uniq } from "lodash";
26
- import { getEntityPropertiesIncludingBase, getEntityProperty, getEntityPropertyByCode } from "./metaHelper";
27
- import { ColumnQueryOptions, CountRowOptions, FindRowOptions, FindRowOrderByOptions, RowFilterOptions } from "./dataAccessTypes";
28
- import { newEntityOperationError } from "~/utilities/errorUtility";
29
- import { getNowStringWithTimezone } from "~/utilities/timeUtility";
30
-
31
- function convertEntityOrderByToRowOrderBy(server: IRpdServer, model: RpdDataModel, baseModel?: RpdDataModel, orderByList?: FindEntityOrderByOptions[]) {
32
- if (!orderByList) {
33
- return null;
34
- }
35
-
36
- return orderByList.map((orderBy) => {
37
- let property = getEntityPropertyByCode(server, model, orderBy.field);
38
- if (!property) {
39
- property = getEntityProperty(server, model, (item) => item.relation === "one" && item.targetIdColumnName === orderBy.field);
40
- }
41
-
42
- if (!property) {
43
- property = getEntityProperty(server, model, (item) => item.columnName === orderBy.field);
44
- }
45
-
46
- if (!property) {
47
- throw new Error(`Unkown orderBy field '${orderBy.field}'`);
48
- }
49
-
50
- return {
51
- field: {
52
- name: mapPropertyNameToColumnName(model, orderBy.field),
53
- tableName: property.isBaseProperty ? baseModel.tableName : model.tableName,
54
- },
55
- desc: !!orderBy.desc,
56
- } as FindRowOrderByOptions;
57
- });
58
- }
59
-
60
- async function findEntities(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: FindEntityOptions) {
61
- const model = dataAccessor.getModel();
62
- let baseModel: RpdDataModel | undefined;
63
- if (model.base) {
64
- baseModel = server.getModel({
65
- singularCode: model.base,
66
- });
67
- }
68
-
69
- let propertiesToSlect: RpdDataModelProperty[];
70
- if (!options.properties || !options.properties.length) {
71
- propertiesToSlect = getEntityPropertiesIncludingBase(server, model).filter((property) => !isRelationProperty(property));
72
- } else {
73
- propertiesToSlect = getEntityPropertiesIncludingBase(server, model).filter((property) => options.properties.includes(property.code));
74
- }
75
-
76
- const columnsToSelect: ColumnQueryOptions[] = [];
77
-
78
- const relationPropertiesToSelect: RpdDataModelProperty[] = [];
79
- forEach(propertiesToSlect, (property) => {
80
- if (isRelationProperty(property)) {
81
- relationPropertiesToSelect.push(property);
82
-
83
- if (property.relation === "one" && !property.linkTableName) {
84
- if (!property.targetIdColumnName) {
85
- throw new Error(`'targetIdColumnName' should be configured for property '${property.code}' of model '${model.namespace}.${model.singularCode}'.`);
86
- }
87
-
88
- if (property.isBaseProperty) {
89
- columnsToSelect.push({
90
- name: property.targetIdColumnName,
91
- tableName: baseModel.tableName,
92
- });
93
- } else {
94
- columnsToSelect.push({
95
- name: property.targetIdColumnName,
96
- tableName: model.tableName,
97
- });
98
- }
99
- }
100
- } else {
101
- if (property.isBaseProperty) {
102
- columnsToSelect.push({
103
- name: property.columnName || property.code,
104
- tableName: baseModel.tableName,
105
- });
106
- } else {
107
- columnsToSelect.push({
108
- name: property.columnName || property.code,
109
- tableName: model.tableName,
110
- });
111
- }
112
- }
113
- });
114
-
115
- // if `keepNonPropertyFields` is true and `properties` are not specified, then select relation columns automatically.
116
- if (options.keepNonPropertyFields && (!options.properties || !options.properties.length)) {
117
- const oneRelationPropertiesWithNoLinkTable = getEntityPropertiesIncludingBase(server, model).filter(
118
- (property) => property.relation === "one" && !property.linkTableName,
119
- );
120
- oneRelationPropertiesWithNoLinkTable.forEach((property) => {
121
- if (property.targetIdColumnName) {
122
- columnsToSelect.push({
123
- name: property.targetIdColumnName,
124
- tableName: property.isBaseProperty ? baseModel.tableName : model.tableName,
125
- });
126
- }
127
- });
128
- }
129
-
130
- const rowFilters = await convertEntityFiltersToRowFilters(server, model, baseModel, options.filters);
131
- const findRowOptions: FindRowOptions = {
132
- filters: rowFilters,
133
- orderBy: convertEntityOrderByToRowOrderBy(server, model, baseModel, options.orderBy),
134
- pagination: options.pagination,
135
- fields: columnsToSelect,
136
- };
137
- const rows = await dataAccessor.find(findRowOptions);
138
- if (!rows.length) {
139
- return [];
140
- }
141
-
142
- const entityIds = rows.map((row) => row.id);
143
- if (relationPropertiesToSelect.length) {
144
- for (const relationProperty of relationPropertiesToSelect) {
145
- const isManyRelation = relationProperty.relation === "many";
146
-
147
- if (relationProperty.linkTableName) {
148
- const targetModel = server.getModel({ singularCode: relationProperty.targetSingularCode! });
149
- if (!targetModel) {
150
- continue;
151
- }
152
-
153
- if (isManyRelation) {
154
- const relationLinks = await findManyRelationLinksViaLinkTable(server, targetModel, relationProperty, entityIds);
155
-
156
- forEach(rows, (row: any) => {
157
- row[relationProperty.code] = filter(relationLinks, (link: any) => {
158
- return link[relationProperty.selfIdColumnName!] == row["id"];
159
- }).map((link) => mapDbRowToEntity(server, targetModel, link.targetEntity, options.keepNonPropertyFields));
160
- });
161
- }
162
- } else {
163
- let relatedEntities: any[];
164
- if (isManyRelation) {
165
- relatedEntities = await findManyRelatedEntitiesViaIdPropertyCode(server, model, relationProperty, entityIds);
166
- } else {
167
- const targetEntityIds = uniq(
168
- reject(
169
- map(rows, (entity: any) => entity[relationProperty.targetIdColumnName!]),
170
- isNullOrUndefined,
171
- ),
172
- );
173
- relatedEntities = await findOneRelatedEntitiesViaIdPropertyCode(server, model, relationProperty, targetEntityIds);
174
- }
175
-
176
- const targetModel = server.getModel({
177
- singularCode: relationProperty.targetSingularCode!,
178
- });
179
- rows.forEach((row) => {
180
- if (isManyRelation) {
181
- row[relationProperty.code] = filter(relatedEntities, (relatedEntity: any) => {
182
- return relatedEntity[relationProperty.selfIdColumnName!] == row.id;
183
- }).map((item) => mapDbRowToEntity(server, targetModel!, item, options.keepNonPropertyFields));
184
- } else {
185
- row[relationProperty.code] = mapDbRowToEntity(
186
- server,
187
- targetModel!,
188
- find(relatedEntities, (relatedEntity: any) => {
189
- // TODO: id property code should be configurable.
190
- return relatedEntity["id"] == row[relationProperty.targetIdColumnName!];
191
- }),
192
- options.keepNonPropertyFields,
193
- );
194
- }
195
- });
196
- }
197
- }
198
- }
199
- const entities = rows.map((item) => mapDbRowToEntity(server, model, item, options.keepNonPropertyFields));
200
-
201
- await server.emitEvent({
202
- eventName: "entity.beforeResponse",
203
- payload: {
204
- namespace: model.namespace,
205
- modelSingularCode: model.singularCode,
206
- baseModelSingularCode: model.base,
207
- entities,
208
- },
209
- sender: null,
210
- routeContext: options.routeContext,
211
- });
212
-
213
- return entities;
214
- }
215
-
216
- async function findEntity(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: FindEntityOptions) {
217
- const entities = await findEntities(server, dataAccessor, options);
218
- return first(entities);
219
- }
220
-
221
- async function findById(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: FindEntityByIdOptions): Promise<any> {
222
- const { id, properties, keepNonPropertyFields, routeContext } = options;
223
- return await findEntity(server, dataAccessor, {
224
- filters: [
225
- {
226
- operator: "eq",
227
- field: "id",
228
- value: id,
229
- },
230
- ],
231
- properties,
232
- keepNonPropertyFields,
233
- routeContext,
234
- });
235
- }
236
-
237
- async function convertEntityFiltersToRowFilters(
238
- server: IRpdServer,
239
- model: RpdDataModel,
240
- baseModel: RpdDataModel,
241
- filters: EntityFilterOptions[] | undefined,
242
- ): Promise<RowFilterOptions[]> {
243
- if (!filters || !filters.length) {
244
- return [];
245
- }
246
-
247
- const replacedFilters: RowFilterOptions[] = [];
248
- for (const filter of filters) {
249
- const { operator } = filter;
250
- if (operator === "and" || operator === "or") {
251
- replacedFilters.push({
252
- operator: operator,
253
- filters: await convertEntityFiltersToRowFilters(server, model, baseModel, filter.filters),
254
- });
255
- } else if (operator === "exists" || operator === "notExists") {
256
- const relationProperty: RpdDataModelProperty = getEntityPropertyByCode(server, model, filter.field);
257
- if (!relationProperty) {
258
- throw new Error(`Invalid filters. Property '${filter.field}' was not found in model '${model.namespace}.${model.singularCode}'`);
259
- }
260
- if (!isRelationProperty(relationProperty)) {
261
- throw new Error(
262
- `Invalid filters. Filter with 'existence' operator on property '${filter.field}' is not allowed. You can only use it on an relation property.`,
263
- );
264
- }
265
-
266
- const relatedEntityFilters = filter.filters;
267
- if (!relatedEntityFilters || !relatedEntityFilters.length) {
268
- throw new Error(`Invalid filters. 'filters' must be provided on filter with 'existence' operator.`);
269
- }
270
-
271
- if (relationProperty.relation === "one") {
272
- // Optimize when filtering by id of related entity
273
- if (relatedEntityFilters.length === 1) {
274
- const relatedEntityIdFilter = relatedEntityFilters[0];
275
- if ((relatedEntityIdFilter.operator === "eq" || relatedEntityIdFilter.operator === "in") && relatedEntityIdFilter.field === "id") {
276
- let replacedOperator: EntityFilterOperators;
277
- if (operator === "exists") {
278
- replacedOperator = relatedEntityIdFilter.operator;
279
- } else {
280
- if (relatedEntityIdFilter.operator === "eq") {
281
- replacedOperator = "ne";
282
- } else {
283
- replacedOperator = "notIn";
284
- }
285
- }
286
- replacedFilters.push({
287
- field: {
288
- name: relationProperty.targetIdColumnName!,
289
- tableName: relationProperty.isBaseProperty ? baseModel.tableName : model.tableName,
290
- },
291
- operator: replacedOperator,
292
- value: relatedEntityIdFilter.value,
293
- });
294
- continue;
295
- }
296
- }
297
-
298
- const dataAccessor = server.getDataAccessor({
299
- singularCode: relationProperty.targetSingularCode as string,
300
- });
301
- const relatedModel = dataAccessor.getModel();
302
- let relatedBaseModel: RpdDataModel;
303
- if (relatedModel.base) {
304
- relatedBaseModel = server.getModel({
305
- singularCode: relatedModel.base,
306
- });
307
- }
308
- const rows = await dataAccessor.find({
309
- filters: await convertEntityFiltersToRowFilters(server, relatedModel, relatedBaseModel, filter.filters),
310
- fields: [
311
- {
312
- name: "id",
313
- tableName: relatedModel.tableName,
314
- },
315
- ],
316
- });
317
- const entityIds = map(rows, (entity: any) => entity["id"]);
318
- replacedFilters.push({
319
- field: {
320
- name: relationProperty.targetIdColumnName,
321
- tableName: relationProperty.isBaseProperty ? baseModel.tableName : model.tableName,
322
- },
323
- operator: operator === "exists" ? "in" : "notIn",
324
- value: entityIds,
325
- });
326
- } else if (!relationProperty.linkTableName) {
327
- // many relation without link table.
328
- if (!relationProperty.selfIdColumnName) {
329
- throw new Error(`Invalid filters. 'selfIdColumnName' of property '${relationProperty.code}' was not configured`);
330
- }
331
-
332
- const targetEntityDataAccessor = server.getDataAccessor({
333
- singularCode: relationProperty.targetSingularCode as string,
334
- });
335
- const relatedModel = targetEntityDataAccessor.getModel();
336
- let relatedBaseModel: RpdDataModel;
337
- if (relatedModel.base) {
338
- relatedBaseModel = server.getModel({
339
- singularCode: relatedModel.base,
340
- });
341
- }
342
- const targetEntities = await targetEntityDataAccessor.find({
343
- filters: await convertEntityFiltersToRowFilters(server, relatedModel, relatedBaseModel, filter.filters),
344
- fields: [
345
- {
346
- name: relationProperty.selfIdColumnName,
347
- tableName: relatedModel.tableName,
348
- },
349
- ],
350
- });
351
- const selfEntityIds = map(targetEntities, (entity: any) => entity[relationProperty.selfIdColumnName!]);
352
- replacedFilters.push({
353
- field: {
354
- name: "id",
355
- tableName: model.tableName,
356
- },
357
- operator: operator === "exists" ? "in" : "notIn",
358
- value: selfEntityIds,
359
- });
360
- } else {
361
- // many relation with link table
362
- if (!relationProperty.selfIdColumnName) {
363
- throw new Error(`Invalid filters. 'selfIdColumnName' of property '${relationProperty.code}' was not configured`);
364
- }
365
-
366
- if (!relationProperty.targetIdColumnName) {
367
- throw new Error(`Invalid filters. 'targetIdColumnName' of property '${relationProperty.code}' was not configured`);
368
- }
369
-
370
- // 1. find target entities
371
- // 2. find links
372
- // 3. convert to 'in' filter
373
- const targetEntityDataAccessor = server.getDataAccessor({
374
- singularCode: relationProperty.targetSingularCode as string,
375
- });
376
- const relatedModel = targetEntityDataAccessor.getModel();
377
- let relatedBaseModel: RpdDataModel;
378
- if (relatedModel.base) {
379
- relatedBaseModel = server.getModel({
380
- singularCode: relatedModel.base,
381
- });
382
- }
383
- const targetEntities = await targetEntityDataAccessor.find({
384
- filters: await convertEntityFiltersToRowFilters(server, relatedModel, relatedBaseModel, filter.filters),
385
- fields: [
386
- {
387
- name: "id",
388
- tableName: relatedModel.tableName,
389
- },
390
- ],
391
- });
392
- const targetEntityIds = map(targetEntities, (entity: any) => entity["id"]);
393
-
394
- const command = `SELECT * FROM ${server.queryBuilder.quoteTable({
395
- schema: relationProperty.linkSchema,
396
- tableName: relationProperty.linkTableName!,
397
- })} WHERE ${server.queryBuilder.quoteObject(relationProperty.targetIdColumnName!)} = ANY($1::int[])`;
398
- const params = [targetEntityIds];
399
- const links = await server.queryDatabaseObject(command, params);
400
- const selfEntityIds = links.map((link) => link[relationProperty.selfIdColumnName!]);
401
- replacedFilters.push({
402
- field: {
403
- name: "id",
404
- tableName: model.tableName,
405
- },
406
- operator: operator === "exists" ? "in" : "notIn",
407
- value: selfEntityIds,
408
- });
409
- }
410
- } else {
411
- const filterField = (filter as EntityNonRelationPropertyFilterOptions).field;
412
- let property: RpdDataModelProperty = getEntityProperty(server, model, (property) => {
413
- return property.code === filterField;
414
- });
415
-
416
- let columnName = "";
417
- if (property) {
418
- columnName = property.columnName || property.code;
419
- } else {
420
- property = getEntityProperty(server, model, (property) => {
421
- return property.columnName === filterField;
422
- });
423
-
424
- if (property) {
425
- columnName = property.columnName;
426
- } else {
427
- property = getEntityProperty(server, model, (property) => {
428
- return property.targetIdColumnName === filterField;
429
- });
430
-
431
- if (property) {
432
- columnName = property.targetIdColumnName;
433
- } else {
434
- columnName = filterField;
435
- }
436
- }
437
- }
438
-
439
- // TODO: do not use `any` here
440
- replacedFilters.push({
441
- operator: filter.operator,
442
- field: {
443
- name: columnName,
444
- tableName: property && property.isBaseProperty ? baseModel.tableName : model.tableName,
445
- },
446
- value: (filter as any).value,
447
- itemType: (filter as any).itemType,
448
- } as any);
449
- }
450
- }
451
- return replacedFilters;
452
- }
453
-
454
- async function findManyRelationLinksViaLinkTable(server: IRpdServer, targetModel: RpdDataModel, relationProperty: RpdDataModelProperty, entityIds: any[]) {
455
- const command = `SELECT * FROM ${server.queryBuilder.quoteTable({
456
- schema: relationProperty.linkSchema,
457
- tableName: relationProperty.linkTableName!,
458
- })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = ANY($1::int[])`;
459
- const params = [entityIds];
460
- const links = await server.queryDatabaseObject(command, params);
461
- const targetEntityIds = links.map((link) => link[relationProperty.targetIdColumnName!]);
462
- const findEntityOptions: FindRowOptions = {
463
- filters: [
464
- {
465
- field: {
466
- name: "id",
467
- },
468
- operator: "in",
469
- value: targetEntityIds,
470
- },
471
- ],
472
- };
473
- const dataAccessor = server.getDataAccessor({
474
- namespace: targetModel.namespace,
475
- singularCode: targetModel.singularCode,
476
- });
477
- const targetEntities = await dataAccessor.find(findEntityOptions);
478
-
479
- forEach(links, (link: any) => {
480
- link.targetEntity = find(targetEntities, (e: any) => e["id"] == link[relationProperty.targetIdColumnName!]);
481
- });
482
-
483
- return links;
484
- }
485
-
486
- function findManyRelatedEntitiesViaIdPropertyCode(server: IRpdServer, model: RpdDataModel, relationProperty: RpdDataModelProperty, entityIds: any[]) {
487
- const findEntityOptions: FindRowOptions = {
488
- filters: [
489
- {
490
- field: {
491
- name: relationProperty.selfIdColumnName,
492
- },
493
- operator: "in",
494
- value: entityIds,
495
- },
496
- ],
497
- };
498
- const dataAccessor = server.getDataAccessor({
499
- singularCode: relationProperty.targetSingularCode as string,
500
- });
501
-
502
- return dataAccessor.find(findEntityOptions);
503
- }
504
-
505
- function findOneRelatedEntitiesViaIdPropertyCode(server: IRpdServer, model: RpdDataModel, relationProperty: RpdDataModelProperty, targetEntityIds: any[]) {
506
- const findEntityOptions: FindRowOptions = {
507
- filters: [
508
- {
509
- field: {
510
- name: "id",
511
- },
512
- operator: "in",
513
- value: targetEntityIds,
514
- },
515
- ],
516
- };
517
- const dataAccessor = server.getDataAccessor({
518
- singularCode: relationProperty.targetSingularCode as string,
519
- });
520
-
521
- return dataAccessor.find(findEntityOptions);
522
- }
523
-
524
- async function createEntity(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: CreateEntityOptions, plugin?: RapidPlugin) {
525
- const model = dataAccessor.getModel();
526
- if (model.derivedTypePropertyCode) {
527
- throw newEntityOperationError("Create base entity directly is not allowed.");
528
- }
529
-
530
- const { entity, routeContext } = options;
531
-
532
- const userId = options.routeContext?.state?.userId;
533
- if (userId) {
534
- const createdByProperty = getEntityPropertyByCode(server, model, "createdBy");
535
- if (createdByProperty) {
536
- entity.createdBy = userId;
537
- }
538
- }
539
- const createdAtProperty = getEntityPropertyByCode(server, model, "createdAt");
540
- if (createdAtProperty) {
541
- entity.createdAt = getNowStringWithTimezone();
542
- }
543
-
544
- await server.beforeCreateEntity(model, options);
545
-
546
- await server.emitEvent({
547
- eventName: "entity.beforeCreate",
548
- payload: {
549
- namespace: model.namespace,
550
- modelSingularCode: model.singularCode,
551
- baseModelSingularCode: model.base,
552
- before: entity,
553
- },
554
- sender: plugin,
555
- routeContext,
556
- });
557
-
558
- const oneRelationPropertiesToCreate: RpdDataModelProperty[] = [];
559
- const manyRelationPropertiesToCreate: RpdDataModelProperty[] = [];
560
- keys(entity).forEach((propertyCode) => {
561
- const property = getEntityPropertyByCode(server, model, propertyCode);
562
- if (!property) {
563
- // Unknown property
564
- return;
565
- }
566
-
567
- if (isRelationProperty(property)) {
568
- if (property.relation === "many") {
569
- manyRelationPropertiesToCreate.push(property);
570
- } else {
571
- oneRelationPropertiesToCreate.push(property);
572
- }
573
- }
574
- });
575
-
576
- const { row, baseRow } = mapEntityToDbRow(server, model, entity);
577
-
578
- const newEntityOneRelationProps = {};
579
- // save one-relation properties
580
- for (const property of oneRelationPropertiesToCreate) {
581
- const targetRow = property.isBaseProperty ? baseRow : row;
582
- const fieldValue = entity[property.code];
583
- const targetDataAccessor = server.getDataAccessor({
584
- singularCode: property.targetSingularCode!,
585
- });
586
- if (isObject(fieldValue)) {
587
- const targetEntityId = fieldValue["id"];
588
- if (!targetEntityId) {
589
- const targetEntity = fieldValue;
590
- const newTargetEntity = await createEntity(server, targetDataAccessor, {
591
- entity: targetEntity,
592
- });
593
- newEntityOneRelationProps[property.code] = newTargetEntity;
594
- targetRow[property.targetIdColumnName!] = newTargetEntity["id"];
595
- } else {
596
- const targetEntity = await findById(server, targetDataAccessor, {
597
- id: targetEntityId,
598
- routeContext,
599
- });
600
- if (!targetEntity) {
601
- throw newEntityOperationError(
602
- `Create ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${targetEntityId}' was not found.`,
603
- );
604
- }
605
- newEntityOneRelationProps[property.code] = targetEntity;
606
- targetRow[property.targetIdColumnName!] = targetEntityId;
607
- }
608
- } else {
609
- // fieldValue is id;
610
- const targetEntityId = fieldValue;
611
- const targetEntity = await findById(server, targetDataAccessor, {
612
- id: targetEntityId,
613
- routeContext,
614
- });
615
- if (!targetEntity) {
616
- throw newEntityOperationError(
617
- `Create ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${targetEntityId}' was not found.`,
618
- );
619
- }
620
- newEntityOneRelationProps[property.code] = targetEntity;
621
- targetRow[property.targetIdColumnName!] = targetEntityId;
622
- }
623
- }
624
-
625
- let newBaseRow: any;
626
- if (model.base) {
627
- const baseDataAccessor = server.getDataAccessor({
628
- singularCode: model.base,
629
- });
630
- newBaseRow = await baseDataAccessor.create(baseRow);
631
-
632
- row.id = newBaseRow.id;
633
- }
634
- const newRow = await dataAccessor.create(row);
635
- const newEntity = mapDbRowToEntity(server, model, Object.assign({}, newBaseRow, newRow, newEntityOneRelationProps), true);
636
-
637
- // save many-relation properties
638
- for (const property of manyRelationPropertiesToCreate) {
639
- newEntity[property.code] = [];
640
-
641
- const targetDataAccessor = server.getDataAccessor({
642
- singularCode: property.targetSingularCode!,
643
- });
644
-
645
- const relatedEntitiesToBeSaved = entity[property.code];
646
- if (!isArray(relatedEntitiesToBeSaved)) {
647
- throw new Error(`Value of field '${property.code}' should be an array.`);
648
- }
649
-
650
- for (const relatedEntityToBeSaved of relatedEntitiesToBeSaved) {
651
- let relatedEntityId: any;
652
- if (isObject(relatedEntityToBeSaved)) {
653
- relatedEntityId = relatedEntityToBeSaved["id"];
654
- if (!relatedEntityId) {
655
- // related entity is to be created
656
- const targetEntity = relatedEntityToBeSaved;
657
- if (!property.linkTableName) {
658
- targetEntity[property.selfIdColumnName!] = newEntity.id;
659
- }
660
- const newTargetEntity = await createEntity(server, targetDataAccessor, {
661
- entity: targetEntity,
662
- });
663
-
664
- if (property.linkTableName) {
665
- const command = `INSERT INTO ${server.queryBuilder.quoteTable({
666
- schema: property.linkSchema,
667
- tableName: property.linkTableName,
668
- })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
669
- const params = [newEntity.id, newTargetEntity.id];
670
- await server.queryDatabaseObject(command, params);
671
- }
672
-
673
- newEntity[property.code].push(newTargetEntity);
674
- } else {
675
- // related entity is existed
676
- const targetEntity = await targetDataAccessor.findById(relatedEntityId);
677
- if (!targetEntity) {
678
- throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
679
- }
680
-
681
- if (property.linkTableName) {
682
- const command = `INSERT INTO ${server.queryBuilder.quoteTable({
683
- schema: property.linkSchema,
684
- tableName: property.linkTableName,
685
- })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
686
- const params = [newEntity.id, relatedEntityId];
687
- await server.queryDatabaseObject(command, params);
688
- } else {
689
- await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: newEntity.id });
690
- targetEntity[property.selfIdColumnName!] = newEntity.id;
691
- }
692
- newEntity[property.code].push(targetEntity);
693
- }
694
- } else {
695
- // fieldValue is id
696
- relatedEntityId = relatedEntityToBeSaved;
697
- const targetEntity = await targetDataAccessor.findById(relatedEntityId);
698
- if (!targetEntity) {
699
- throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
700
- }
701
-
702
- if (property.linkTableName) {
703
- const command = `INSERT INTO ${server.queryBuilder.quoteTable({
704
- schema: property.linkSchema,
705
- tableName: property.linkTableName,
706
- })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
707
- const params = [newEntity.id, relatedEntityId];
708
- await server.queryDatabaseObject(command, params);
709
- } else {
710
- await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: newEntity.id });
711
- targetEntity[property.selfIdColumnName!] = newEntity.id;
712
- }
713
-
714
- newEntity[property.code].push(targetEntity);
715
- }
716
- }
717
- }
718
-
719
- await server.emitEvent({
720
- eventName: "entity.create",
721
- payload: {
722
- namespace: model.namespace,
723
- modelSingularCode: model.singularCode,
724
- baseModelSingularCode: model.base,
725
- after: newEntity,
726
- },
727
- sender: plugin,
728
- routeContext,
729
- });
730
-
731
- return newEntity;
732
- }
733
-
734
- async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: UpdateEntityByIdOptions, plugin?: RapidPlugin) {
735
- const model = dataAccessor.getModel();
736
- const { id, routeContext } = options;
737
- if (!id) {
738
- throw new Error("Id is required when updating an entity.");
739
- }
740
-
741
- const entity = await findById(server, dataAccessor, {
742
- id,
743
- routeContext,
744
- });
745
- if (!entity) {
746
- throw new Error(`${model.namespace}.${model.singularCode} with id "${id}" was not found.`);
747
- }
748
-
749
- let { entityToSave } = options;
750
- let changes = getEntityPartChanges(entity, entityToSave);
751
- if (!changes && !options.operation) {
752
- return entity;
753
- }
754
-
755
- entityToSave = changes || {};
756
-
757
- const userId = options.routeContext?.state?.userId;
758
- if (userId) {
759
- const updatedByProperty = getEntityPropertyByCode(server, model, "updatedBy");
760
- if (updatedByProperty) {
761
- entityToSave.updatedBy = userId;
762
- }
763
- }
764
- const updatedAtProperty = getEntityPropertyByCode(server, model, "updatedAt");
765
- if (updatedAtProperty) {
766
- entityToSave.updatedAt = getNowStringWithTimezone();
767
- }
768
-
769
- await server.beforeUpdateEntity(model, options, entity);
770
-
771
- await server.emitEvent({
772
- eventName: "entity.beforeUpdate",
773
- payload: {
774
- namespace: model.namespace,
775
- modelSingularCode: model.singularCode,
776
- before: entity,
777
- changes: entityToSave,
778
- operation: options.operation,
779
- stateProperties: options.stateProperties,
780
- },
781
- sender: plugin,
782
- routeContext: options.routeContext,
783
- });
784
-
785
- changes = getEntityPartChanges(entity, entityToSave);
786
-
787
- const oneRelationPropertiesToUpdate: RpdDataModelProperty[] = [];
788
- const manyRelationPropertiesToUpdate: RpdDataModelProperty[] = [];
789
- keys(changes).forEach((propertyCode) => {
790
- const property = getEntityPropertyByCode(server, model, propertyCode);
791
- if (!property) {
792
- // Unknown property
793
- return;
794
- }
795
-
796
- if (isRelationProperty(property)) {
797
- if (property.relation === "many") {
798
- manyRelationPropertiesToUpdate.push(property);
799
- } else {
800
- oneRelationPropertiesToUpdate.push(property);
801
- }
802
- }
803
- });
804
-
805
- const { row, baseRow } = mapEntityToDbRow(server, model, changes);
806
-
807
- const updatedEntityOneRelationProps = {};
808
- for (const property of oneRelationPropertiesToUpdate) {
809
- const targetRow = property.isBaseProperty ? baseRow : row;
810
- const fieldValue = changes[property.code];
811
- const targetDataAccessor = server.getDataAccessor({
812
- singularCode: property.targetSingularCode!,
813
- });
814
-
815
- if (isObject(fieldValue)) {
816
- const targetEntityId = fieldValue["id"];
817
- if (!targetEntityId) {
818
- const targetEntity = fieldValue;
819
- const newTargetEntity = await createEntity(server, targetDataAccessor, {
820
- entity: targetEntity,
821
- });
822
- updatedEntityOneRelationProps[property.code] = newTargetEntity;
823
- targetRow[property.targetIdColumnName!] = newTargetEntity["id"];
824
- } else {
825
- const targetEntity = await findById(server, targetDataAccessor, {
826
- id: targetEntityId,
827
- routeContext,
828
- });
829
- if (!targetEntity) {
830
- throw newEntityOperationError(
831
- `Create ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${targetEntityId}' was not found.`,
832
- );
833
- }
834
- updatedEntityOneRelationProps[property.code] = targetEntity;
835
- targetRow[property.targetIdColumnName!] = targetEntityId;
836
- }
837
- } else {
838
- // fieldValue is id;
839
- const targetEntityId = fieldValue;
840
- const targetEntity = await findById(server, targetDataAccessor, {
841
- id: targetEntityId,
842
- routeContext,
843
- });
844
- if (!targetEntity) {
845
- throw newEntityOperationError(
846
- `Create ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${targetEntityId}' was not found.`,
847
- );
848
- }
849
- updatedEntityOneRelationProps[property.code] = targetEntity;
850
- targetRow[property.targetIdColumnName!] = targetEntityId;
851
- }
852
- }
853
-
854
- let updatedRow = row;
855
- if (Object.keys(row).length) {
856
- updatedRow = await dataAccessor.updateById(id, row);
857
- }
858
- let updatedBaseRow = baseRow;
859
- if (model.base && Object.keys(baseRow).length) {
860
- const baseDataAccessor = server.getDataAccessor({
861
- singularCode: model.base,
862
- });
863
- updatedBaseRow = await baseDataAccessor.updateById(id, updatedBaseRow);
864
- }
865
-
866
- let updatedEntity = mapDbRowToEntity(server, model, { ...updatedRow, ...updatedBaseRow, ...updatedEntityOneRelationProps }, true);
867
- updatedEntity = Object.assign({}, entity, updatedEntity);
868
-
869
- // save many-relation properties
870
- for (const property of manyRelationPropertiesToUpdate) {
871
- const relatedEntities: any[] = [];
872
- const targetDataAccessor = server.getDataAccessor({
873
- singularCode: property.targetSingularCode!,
874
- });
875
-
876
- const relatedEntitiesToBeSaved = changes[property.code];
877
- if (!isArray(relatedEntitiesToBeSaved)) {
878
- throw new Error(`Value of field '${property.code}' should be an array.`);
879
- }
880
-
881
- if (property.linkTableName) {
882
- // TODO: should support removing relation
883
- await server.queryDatabaseObject(
884
- `DELETE FROM ${server.queryBuilder.quoteTable({
885
- schema: property.linkSchema,
886
- tableName: property.linkTableName,
887
- })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName!)} = $1`,
888
- [id],
889
- );
890
- }
891
-
892
- for (const relatedEntityToBeSaved of relatedEntitiesToBeSaved) {
893
- let relatedEntityId: any;
894
- if (isObject(relatedEntityToBeSaved)) {
895
- relatedEntityId = relatedEntityToBeSaved["id"];
896
- if (!relatedEntityId) {
897
- // related entity is to be created
898
- const targetEntity = relatedEntityToBeSaved;
899
- if (!property.linkTableName) {
900
- targetEntity[property.selfIdColumnName!] = id;
901
- }
902
- const newTargetEntity = await createEntity(server, targetDataAccessor, {
903
- entity: targetEntity,
904
- });
905
-
906
- if (property.linkTableName) {
907
- const command = `INSERT INTO ${server.queryBuilder.quoteTable({
908
- schema: property.linkSchema,
909
- tableName: property.linkTableName,
910
- })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
911
- const params = [id, newTargetEntity.id];
912
- await server.queryDatabaseObject(command, params);
913
- }
914
-
915
- relatedEntities.push(newTargetEntity);
916
- } else {
917
- // related entity is existed
918
- const targetEntity = await targetDataAccessor.findById(relatedEntityId);
919
- if (!targetEntity) {
920
- throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
921
- }
922
-
923
- if (property.linkTableName) {
924
- const command = `INSERT INTO ${server.queryBuilder.quoteTable({
925
- schema: property.linkSchema,
926
- tableName: property.linkTableName,
927
- })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
928
- const params = [id, relatedEntityId];
929
- await server.queryDatabaseObject(command, params);
930
- } else {
931
- await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: id });
932
- targetEntity[property.selfIdColumnName!] = id;
933
- }
934
- relatedEntities.push(targetEntity);
935
- }
936
- } else {
937
- // fieldValue is id
938
- relatedEntityId = relatedEntityToBeSaved;
939
- const targetEntity = await targetDataAccessor.findById(relatedEntityId);
940
- if (!targetEntity) {
941
- throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
942
- }
943
-
944
- if (property.linkTableName) {
945
- const command = `INSERT INTO ${server.queryBuilder.quoteTable({
946
- schema: property.linkSchema,
947
- tableName: property.linkTableName,
948
- })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
949
- const params = [id, relatedEntityId];
950
- await server.queryDatabaseObject(command, params);
951
- } else {
952
- await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: id });
953
- targetEntity[property.selfIdColumnName!] = id;
954
- }
955
-
956
- relatedEntities.push(targetEntity);
957
- }
958
- }
959
- updatedEntity[property.code] = relatedEntities;
960
- }
961
-
962
- await server.emitEvent({
963
- eventName: "entity.update",
964
- payload: {
965
- namespace: model.namespace,
966
- modelSingularCode: model.singularCode,
967
- // TODO: should not emit event on base model if it's not effected.
968
- baseModelSingularCode: model.base,
969
- before: entity,
970
- after: updatedEntity,
971
- changes: changes,
972
- operation: options.operation,
973
- stateProperties: options.stateProperties,
974
- },
975
- sender: plugin,
976
- routeContext: options.routeContext,
977
- });
978
-
979
- return updatedEntity;
980
- }
981
-
982
- export default class EntityManager<TEntity = any> {
983
- #server: IRpdServer;
984
- #dataAccessor: IRpdDataAccessor;
985
-
986
- constructor(server: IRpdServer, dataAccessor: IRpdDataAccessor) {
987
- this.#server = server;
988
- this.#dataAccessor = dataAccessor;
989
- }
990
-
991
- getModel(): RpdDataModel {
992
- return this.#dataAccessor.getModel();
993
- }
994
-
995
- async findEntities(options: FindEntityOptions): Promise<TEntity[]> {
996
- return await findEntities(this.#server, this.#dataAccessor, options);
997
- }
998
-
999
- async findEntity(options: FindEntityOptions): Promise<TEntity | null> {
1000
- return await findEntity(this.#server, this.#dataAccessor, options);
1001
- }
1002
-
1003
- async findById(options: FindEntityByIdOptions | string | number): Promise<TEntity | null> {
1004
- // options is id
1005
- if (!isObject(options)) {
1006
- options = {
1007
- id: options,
1008
- };
1009
- }
1010
- return await findById(this.#server, this.#dataAccessor, options);
1011
- }
1012
-
1013
- async createEntity(options: CreateEntityOptions, plugin?: RapidPlugin): Promise<TEntity> {
1014
- return await createEntity(this.#server, this.#dataAccessor, options, plugin);
1015
- }
1016
-
1017
- async updateEntityById(options: UpdateEntityByIdOptions, plugin?: RapidPlugin): Promise<TEntity> {
1018
- return await updateEntityById(this.#server, this.#dataAccessor, options, plugin);
1019
- }
1020
-
1021
- async count(options: CountEntityOptions): Promise<CountEntityResult> {
1022
- const model = this.#dataAccessor.getModel();
1023
- let baseModel: RpdDataModel;
1024
- if (model.base) {
1025
- baseModel = this.#server.getModel({
1026
- singularCode: model.base,
1027
- });
1028
- }
1029
- const countRowOptions: CountRowOptions = {
1030
- filters: await convertEntityFiltersToRowFilters(this.#server, model, baseModel, options.filters),
1031
- };
1032
- return await this.#dataAccessor.count(countRowOptions);
1033
- }
1034
-
1035
- async deleteById(options: DeleteEntityByIdOptions | string | number, plugin?: RapidPlugin): Promise<void> {
1036
- // options is id
1037
- if (!isObject(options)) {
1038
- options = {
1039
- id: options,
1040
- };
1041
- }
1042
-
1043
- const model = this.getModel();
1044
- if (model.derivedTypePropertyCode) {
1045
- throw newEntityOperationError("Delete base entity directly is not allowed.");
1046
- }
1047
-
1048
- const { id, routeContext } = options;
1049
-
1050
- const entity = await this.findById({
1051
- id,
1052
- keepNonPropertyFields: true,
1053
- routeContext,
1054
- });
1055
-
1056
- if (!entity) {
1057
- return;
1058
- }
1059
-
1060
- await this.#server.emitEvent({
1061
- eventName: "entity.beforeDelete",
1062
- payload: {
1063
- namespace: model.namespace,
1064
- modelSingularCode: model.singularCode,
1065
- before: entity,
1066
- },
1067
- sender: plugin,
1068
- routeContext,
1069
- });
1070
-
1071
- await this.#dataAccessor.deleteById(id);
1072
- if (model.base) {
1073
- const baseDataAccessor = this.#server.getDataAccessor({
1074
- singularCode: model.base,
1075
- });
1076
- await baseDataAccessor.deleteById(id);
1077
- }
1078
-
1079
- await this.#server.emitEvent({
1080
- eventName: "entity.delete",
1081
- payload: {
1082
- namespace: model.namespace,
1083
- modelSingularCode: model.singularCode,
1084
- before: entity,
1085
- },
1086
- sender: plugin,
1087
- routeContext,
1088
- });
1089
- }
1090
-
1091
- async addRelations(options: AddEntityRelationsOptions, plugin?: RapidPlugin): Promise<void> {
1092
- const server = this.#server;
1093
- const model = this.getModel();
1094
- const { id, property, relations, routeContext } = options;
1095
- const entity = await this.findById({
1096
- id,
1097
- routeContext,
1098
- });
1099
- if (!entity) {
1100
- throw new Error(`${model.namespace}.${model.singularCode} with id "${id}" was not found.`);
1101
- }
1102
-
1103
- const relationProperty = getEntityPropertyByCode(server, model, property);
1104
- if (!relationProperty) {
1105
- throw new Error(`Property '${property}' was not found in ${model.namespace}.${model.singularCode}`);
1106
- }
1107
-
1108
- if (!(isRelationProperty(relationProperty) && relationProperty.relation === "many")) {
1109
- throw new Error(`Operation 'addRelations' is only supported on property of 'many' relation`);
1110
- }
1111
-
1112
- const { queryBuilder } = server;
1113
- if (relationProperty.linkTableName) {
1114
- for (const relation of relations) {
1115
- const command = `INSERT INTO ${queryBuilder.quoteTable({
1116
- schema: relationProperty.linkSchema,
1117
- tableName: relationProperty.linkTableName,
1118
- })} (${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}, ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)})
1119
- SELECT $1, $2 WHERE NOT EXISTS (
1120
- SELECT ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}, ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}
1121
- FROM ${queryBuilder.quoteTable({ schema: relationProperty.linkSchema, tableName: relationProperty.linkTableName })}
1122
- WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}=$2
1123
- )`;
1124
- const params = [id, relation.id];
1125
- await server.queryDatabaseObject(command, params);
1126
- }
1127
- }
1128
-
1129
- await server.emitEvent({
1130
- eventName: "entity.addRelations",
1131
- payload: {
1132
- namespace: model.namespace,
1133
- modelSingularCode: model.singularCode,
1134
- entity,
1135
- property,
1136
- relations,
1137
- },
1138
- sender: plugin,
1139
- routeContext: options.routeContext,
1140
- });
1141
- }
1142
-
1143
- async removeRelations(options: RemoveEntityRelationsOptions, plugin?: RapidPlugin): Promise<void> {
1144
- const server = this.#server;
1145
- const model = this.getModel();
1146
- const { id, property, relations, routeContext } = options;
1147
- const entity = await this.findById({
1148
- id,
1149
- routeContext,
1150
- });
1151
- if (!entity) {
1152
- throw new Error(`${model.namespace}.${model.singularCode} with id "${id}" was not found.`);
1153
- }
1154
-
1155
- const relationProperty = getEntityPropertyByCode(server, model, property);
1156
- if (!relationProperty) {
1157
- throw new Error(`Property '${property}' was not found in ${model.namespace}.${model.singularCode}`);
1158
- }
1159
-
1160
- if (!(isRelationProperty(relationProperty) && relationProperty.relation === "many")) {
1161
- throw new Error(`Operation 'removeRelations' is only supported on property of 'many' relation`);
1162
- }
1163
-
1164
- const { queryBuilder } = server;
1165
- if (relationProperty.linkTableName) {
1166
- for (const relation of relations) {
1167
- const command = `DELETE FROM ${queryBuilder.quoteTable({ schema: relationProperty.linkSchema, tableName: relationProperty.linkTableName })}
1168
- WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}=$2;`;
1169
- const params = [id, relation.id];
1170
- await server.queryDatabaseObject(command, params);
1171
- }
1172
- }
1173
-
1174
- await server.emitEvent({
1175
- eventName: "entity.removeRelations",
1176
- payload: {
1177
- namespace: model.namespace,
1178
- modelSingularCode: model.singularCode,
1179
- entity,
1180
- property,
1181
- relations,
1182
- },
1183
- sender: plugin,
1184
- routeContext: options.routeContext,
1185
- });
1186
- }
1187
- }
1
+ import {
2
+ AddEntityRelationsOptions,
3
+ CountEntityOptions,
4
+ CountEntityResult,
5
+ CreateEntityOptions,
6
+ DeleteEntityByIdOptions,
7
+ EntityFilterOperators,
8
+ EntityFilterOptions,
9
+ EntityNonRelationPropertyFilterOptions,
10
+ FindEntityByIdOptions,
11
+ FindEntityOptions,
12
+ FindEntityOrderByOptions,
13
+ FindEntitySelectRelationOptions,
14
+ IRpdDataAccessor,
15
+ RemoveEntityRelationsOptions,
16
+ RpdDataModel,
17
+ RpdDataModelProperty,
18
+ UpdateEntityByIdOptions,
19
+ } from "~/types";
20
+ import { isNullOrUndefined } from "~/utilities/typeUtility";
21
+ import { mapDbRowToEntity, mapEntityToDbRow } from "./entityMapper";
22
+ import { mapPropertyNameToColumnName } from "./propertyMapper";
23
+ import { IRpdServer, RapidPlugin } from "~/core/server";
24
+ import { getEntityPartChanges } from "~/helpers/entityHelpers";
25
+ import { filter, find, first, forEach, isArray, isNumber, isObject, keys, map, reject, uniq } from "lodash";
26
+ import { getEntityPropertiesIncludingBase, getEntityProperty, getEntityPropertyByCode, isRelationProperty } from "../helpers/metaHelper";
27
+ import { ColumnSelectOptions, CountRowOptions, FindRowOptions, FindRowOrderByOptions, RowFilterOptions } from "./dataAccessTypes";
28
+ import { newEntityOperationError } from "~/utilities/errorUtility";
29
+ import { getNowStringWithTimezone } from "~/utilities/timeUtility";
30
+
31
+ export type FindOneRelationEntitiesOptions = {
32
+ server: IRpdServer;
33
+ mainModel: RpdDataModel;
34
+ relationProperty: RpdDataModelProperty;
35
+ relationEntityIds: any[];
36
+ selectRelationOptions?: FindEntitySelectRelationOptions;
37
+ };
38
+
39
+ export type FindManyRelationEntitiesOptions = {
40
+ server: IRpdServer;
41
+ mainModel: RpdDataModel;
42
+ relationProperty: RpdDataModelProperty;
43
+ mainEntityIds: any[];
44
+ selectRelationOptions?: FindEntitySelectRelationOptions;
45
+ };
46
+
47
+ function convertEntityOrderByToRowOrderBy(server: IRpdServer, model: RpdDataModel, baseModel?: RpdDataModel, orderByList?: FindEntityOrderByOptions[]) {
48
+ if (!orderByList) {
49
+ return null;
50
+ }
51
+
52
+ return orderByList.map((orderBy) => {
53
+ let property = getEntityPropertyByCode(server, model, orderBy.field);
54
+ if (!property) {
55
+ property = getEntityProperty(server, model, (item) => item.relation === "one" && item.targetIdColumnName === orderBy.field);
56
+ }
57
+
58
+ if (!property) {
59
+ property = getEntityProperty(server, model, (item) => item.columnName === orderBy.field);
60
+ }
61
+
62
+ if (!property) {
63
+ throw new Error(`Unkown orderBy field '${orderBy.field}'`);
64
+ }
65
+
66
+ return {
67
+ field: {
68
+ name: mapPropertyNameToColumnName(model, orderBy.field),
69
+ tableName: property.isBaseProperty ? baseModel.tableName : model.tableName,
70
+ },
71
+ desc: !!orderBy.desc,
72
+ } as FindRowOrderByOptions;
73
+ });
74
+ }
75
+
76
+ async function findEntities(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: FindEntityOptions) {
77
+ const model = dataAccessor.getModel();
78
+ let baseModel: RpdDataModel | undefined;
79
+ if (model.base) {
80
+ baseModel = server.getModel({
81
+ singularCode: model.base,
82
+ });
83
+ }
84
+
85
+ let propertiesToSelect: RpdDataModelProperty[];
86
+ let relationOptions = options.relations || {};
87
+ let relationPropertyCodes = Object.keys(relationOptions) || [];
88
+ if (!options.properties || !options.properties.length) {
89
+ propertiesToSelect = getEntityPropertiesIncludingBase(server, model).filter(
90
+ (property) => !isRelationProperty(property) || relationPropertyCodes.includes(property.code),
91
+ );
92
+ } else {
93
+ propertiesToSelect = getEntityPropertiesIncludingBase(server, model).filter(
94
+ (property) => options.properties.includes(property.code) || relationPropertyCodes.includes(property.code),
95
+ );
96
+ }
97
+
98
+ const columnsToSelect: ColumnSelectOptions[] = [];
99
+
100
+ const relationPropertiesToSelect: RpdDataModelProperty[] = [];
101
+ forEach(propertiesToSelect, (property) => {
102
+ if (isRelationProperty(property)) {
103
+ relationPropertiesToSelect.push(property);
104
+
105
+ if (property.relation === "one" && !property.linkTableName) {
106
+ if (!property.targetIdColumnName) {
107
+ throw new Error(`'targetIdColumnName' should be configured for property '${property.code}' of model '${model.namespace}.${model.singularCode}'.`);
108
+ }
109
+
110
+ if (property.isBaseProperty) {
111
+ columnsToSelect.push({
112
+ name: property.targetIdColumnName,
113
+ tableName: baseModel.tableName,
114
+ });
115
+ } else {
116
+ columnsToSelect.push({
117
+ name: property.targetIdColumnName,
118
+ tableName: model.tableName,
119
+ });
120
+ }
121
+ }
122
+ } else {
123
+ if (property.isBaseProperty) {
124
+ columnsToSelect.push({
125
+ name: property.columnName || property.code,
126
+ tableName: baseModel.tableName,
127
+ });
128
+ } else {
129
+ columnsToSelect.push({
130
+ name: property.columnName || property.code,
131
+ tableName: model.tableName,
132
+ });
133
+ }
134
+ }
135
+ });
136
+
137
+ // if `keepNonPropertyFields` is true and `properties` are not specified, then select relation columns automatically.
138
+ if (options.keepNonPropertyFields && (!options.properties || !options.properties.length)) {
139
+ const oneRelationPropertiesWithNoLinkTable = getEntityPropertiesIncludingBase(server, model).filter(
140
+ (property) => property.relation === "one" && !property.linkTableName,
141
+ );
142
+ oneRelationPropertiesWithNoLinkTable.forEach((property) => {
143
+ if (property.targetIdColumnName) {
144
+ columnsToSelect.push({
145
+ name: property.targetIdColumnName,
146
+ tableName: property.isBaseProperty ? baseModel.tableName : model.tableName,
147
+ });
148
+ }
149
+ });
150
+ }
151
+
152
+ if (options.extraColumnsToSelect) {
153
+ forEach(options.extraColumnsToSelect, (extraColumnToSelect: ColumnSelectOptions) => {
154
+ const columnSelectOptionExists = find(columnsToSelect, (item: ColumnSelectOptions) => {
155
+ if (typeof item === "string") {
156
+ if (typeof extraColumnToSelect === "string") {
157
+ return item === extraColumnToSelect;
158
+ } else {
159
+ return item == extraColumnToSelect.name;
160
+ }
161
+ } else {
162
+ if (typeof extraColumnToSelect === "string") {
163
+ return item.name === extraColumnToSelect;
164
+ } else {
165
+ return item.name == extraColumnToSelect.name;
166
+ }
167
+ }
168
+ });
169
+
170
+ if (!columnSelectOptionExists) {
171
+ columnsToSelect.push(extraColumnToSelect);
172
+ }
173
+ });
174
+ }
175
+
176
+ const rowFilters = await convertEntityFiltersToRowFilters(server, model, baseModel, options.filters);
177
+ const findRowOptions: FindRowOptions = {
178
+ filters: rowFilters,
179
+ orderBy: convertEntityOrderByToRowOrderBy(server, model, baseModel, options.orderBy),
180
+ pagination: options.pagination,
181
+ fields: columnsToSelect,
182
+ };
183
+ const rows = await dataAccessor.find(findRowOptions);
184
+ if (!rows.length) {
185
+ return [];
186
+ }
187
+
188
+ const entityIds = rows.map((row) => row.id);
189
+ if (relationPropertiesToSelect.length) {
190
+ for (const relationProperty of relationPropertiesToSelect) {
191
+ const isManyRelation = relationProperty.relation === "many";
192
+
193
+ if (relationProperty.linkTableName) {
194
+ const relationModel = server.getModel({ singularCode: relationProperty.targetSingularCode! });
195
+ if (!relationModel) {
196
+ continue;
197
+ }
198
+
199
+ if (isManyRelation) {
200
+ const relationLinks = await findManyRelationLinksViaLinkTable({
201
+ server,
202
+ mainModel: relationModel,
203
+ relationProperty,
204
+ mainEntityIds: entityIds,
205
+ selectRelationOptions: relationOptions[relationProperty.code],
206
+ });
207
+
208
+ forEach(rows, (row: any) => {
209
+ row[relationProperty.code] = filter(relationLinks, (link: any) => {
210
+ return link[relationProperty.selfIdColumnName!] == row["id"];
211
+ }).map((link) => mapDbRowToEntity(server, relationModel, link.targetEntity, options.keepNonPropertyFields));
212
+ });
213
+ }
214
+ } else {
215
+ let relatedEntities: any[];
216
+ if (isManyRelation) {
217
+ relatedEntities = await findManyRelatedEntitiesViaIdPropertyCode({
218
+ server,
219
+ mainModel: model,
220
+ relationProperty,
221
+ mainEntityIds: entityIds,
222
+ selectRelationOptions: relationOptions[relationProperty.code],
223
+ });
224
+ } else {
225
+ const targetEntityIds = uniq(
226
+ reject(
227
+ map(rows, (entity: any) => entity[relationProperty.targetIdColumnName!]),
228
+ isNullOrUndefined,
229
+ ),
230
+ );
231
+ relatedEntities = await findOneRelatedEntitiesViaIdPropertyCode({
232
+ server,
233
+ mainModel: model,
234
+ relationProperty,
235
+ relationEntityIds: targetEntityIds,
236
+ selectRelationOptions: relationOptions[relationProperty.code],
237
+ });
238
+ }
239
+
240
+ const targetModel = server.getModel({
241
+ singularCode: relationProperty.targetSingularCode!,
242
+ });
243
+ rows.forEach((row) => {
244
+ if (isManyRelation) {
245
+ row[relationProperty.code] = filter(relatedEntities, (relatedEntity: any) => {
246
+ return relatedEntity[relationProperty.selfIdColumnName!] == row.id;
247
+ }).map((item) => mapDbRowToEntity(server, targetModel!, item, options.keepNonPropertyFields));
248
+ } else {
249
+ row[relationProperty.code] = mapDbRowToEntity(
250
+ server,
251
+ targetModel!,
252
+ find(relatedEntities, (relatedEntity: any) => {
253
+ // TODO: id property code should be configurable.
254
+ return relatedEntity["id"] == row[relationProperty.targetIdColumnName!];
255
+ }),
256
+ options.keepNonPropertyFields,
257
+ );
258
+ }
259
+ });
260
+ }
261
+ }
262
+ }
263
+ const entities = rows.map((item) => mapDbRowToEntity(server, model, item, options.keepNonPropertyFields));
264
+
265
+ await server.emitEvent({
266
+ eventName: "entity.beforeResponse",
267
+ payload: {
268
+ namespace: model.namespace,
269
+ modelSingularCode: model.singularCode,
270
+ baseModelSingularCode: model.base,
271
+ entities,
272
+ },
273
+ sender: null,
274
+ routeContext: options.routeContext,
275
+ });
276
+
277
+ return entities;
278
+ }
279
+
280
+ async function findEntity(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: FindEntityOptions) {
281
+ const entities = await findEntities(server, dataAccessor, options);
282
+ return first(entities);
283
+ }
284
+
285
+ async function findById(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: FindEntityByIdOptions): Promise<any> {
286
+ const { id, properties, keepNonPropertyFields, routeContext } = options;
287
+ return await findEntity(server, dataAccessor, {
288
+ filters: [
289
+ {
290
+ operator: "eq",
291
+ field: "id",
292
+ value: id,
293
+ },
294
+ ],
295
+ properties,
296
+ keepNonPropertyFields,
297
+ routeContext,
298
+ });
299
+ }
300
+
301
+ async function convertEntityFiltersToRowFilters(
302
+ server: IRpdServer,
303
+ model: RpdDataModel,
304
+ baseModel: RpdDataModel,
305
+ filters: EntityFilterOptions[] | undefined,
306
+ ): Promise<RowFilterOptions[]> {
307
+ if (!filters || !filters.length) {
308
+ return [];
309
+ }
310
+
311
+ const replacedFilters: RowFilterOptions[] = [];
312
+ for (const filter of filters) {
313
+ const { operator } = filter;
314
+ if (operator === "and" || operator === "or") {
315
+ replacedFilters.push({
316
+ operator: operator,
317
+ filters: await convertEntityFiltersToRowFilters(server, model, baseModel, filter.filters),
318
+ });
319
+ } else if (operator === "exists" || operator === "notExists") {
320
+ const relationProperty: RpdDataModelProperty = getEntityPropertyByCode(server, model, filter.field);
321
+ if (!relationProperty) {
322
+ throw new Error(`Invalid filters. Property '${filter.field}' was not found in model '${model.namespace}.${model.singularCode}'`);
323
+ }
324
+ if (!isRelationProperty(relationProperty)) {
325
+ throw new Error(
326
+ `Invalid filters. Filter with 'existence' operator on property '${filter.field}' is not allowed. You can only use it on an relation property.`,
327
+ );
328
+ }
329
+
330
+ const relatedEntityFilters = filter.filters;
331
+ if (!relatedEntityFilters || !relatedEntityFilters.length) {
332
+ throw new Error(`Invalid filters. 'filters' must be provided on filter with 'existence' operator.`);
333
+ }
334
+
335
+ if (relationProperty.relation === "one") {
336
+ // Optimize when filtering by id of related entity
337
+ if (relatedEntityFilters.length === 1) {
338
+ const relatedEntityIdFilter = relatedEntityFilters[0];
339
+ if ((relatedEntityIdFilter.operator === "eq" || relatedEntityIdFilter.operator === "in") && relatedEntityIdFilter.field === "id") {
340
+ let replacedOperator: EntityFilterOperators;
341
+ if (operator === "exists") {
342
+ replacedOperator = relatedEntityIdFilter.operator;
343
+ } else {
344
+ if (relatedEntityIdFilter.operator === "eq") {
345
+ replacedOperator = "ne";
346
+ } else {
347
+ replacedOperator = "notIn";
348
+ }
349
+ }
350
+ replacedFilters.push({
351
+ field: {
352
+ name: relationProperty.targetIdColumnName!,
353
+ tableName: relationProperty.isBaseProperty ? baseModel.tableName : model.tableName,
354
+ },
355
+ operator: replacedOperator,
356
+ value: relatedEntityIdFilter.value,
357
+ });
358
+ continue;
359
+ }
360
+ }
361
+
362
+ const dataAccessor = server.getDataAccessor({
363
+ singularCode: relationProperty.targetSingularCode as string,
364
+ });
365
+ const relatedModel = dataAccessor.getModel();
366
+ let relatedBaseModel: RpdDataModel;
367
+ if (relatedModel.base) {
368
+ relatedBaseModel = server.getModel({
369
+ singularCode: relatedModel.base,
370
+ });
371
+ }
372
+ const rows = await dataAccessor.find({
373
+ filters: await convertEntityFiltersToRowFilters(server, relatedModel, relatedBaseModel, filter.filters),
374
+ fields: [
375
+ {
376
+ name: "id",
377
+ tableName: relatedModel.tableName,
378
+ },
379
+ ],
380
+ });
381
+ const entityIds = map(rows, (entity: any) => entity["id"]);
382
+ replacedFilters.push({
383
+ field: {
384
+ name: relationProperty.targetIdColumnName,
385
+ tableName: relationProperty.isBaseProperty ? baseModel.tableName : model.tableName,
386
+ },
387
+ operator: operator === "exists" ? "in" : "notIn",
388
+ value: entityIds,
389
+ });
390
+ } else if (!relationProperty.linkTableName) {
391
+ // many relation without link table.
392
+ if (!relationProperty.selfIdColumnName) {
393
+ throw new Error(`Invalid filters. 'selfIdColumnName' of property '${relationProperty.code}' was not configured`);
394
+ }
395
+
396
+ const targetEntityDataAccessor = server.getDataAccessor({
397
+ singularCode: relationProperty.targetSingularCode as string,
398
+ });
399
+ const relatedModel = targetEntityDataAccessor.getModel();
400
+ let relatedBaseModel: RpdDataModel;
401
+ if (relatedModel.base) {
402
+ relatedBaseModel = server.getModel({
403
+ singularCode: relatedModel.base,
404
+ });
405
+ }
406
+ const targetEntities = await targetEntityDataAccessor.find({
407
+ filters: await convertEntityFiltersToRowFilters(server, relatedModel, relatedBaseModel, filter.filters),
408
+ fields: [
409
+ {
410
+ name: relationProperty.selfIdColumnName,
411
+ tableName: relatedModel.tableName,
412
+ },
413
+ ],
414
+ });
415
+ const selfEntityIds = map(targetEntities, (entity: any) => entity[relationProperty.selfIdColumnName!]);
416
+ replacedFilters.push({
417
+ field: {
418
+ name: "id",
419
+ tableName: model.tableName,
420
+ },
421
+ operator: operator === "exists" ? "in" : "notIn",
422
+ value: selfEntityIds,
423
+ });
424
+ } else {
425
+ // many relation with link table
426
+ if (!relationProperty.selfIdColumnName) {
427
+ throw new Error(`Invalid filters. 'selfIdColumnName' of property '${relationProperty.code}' was not configured`);
428
+ }
429
+
430
+ if (!relationProperty.targetIdColumnName) {
431
+ throw new Error(`Invalid filters. 'targetIdColumnName' of property '${relationProperty.code}' was not configured`);
432
+ }
433
+
434
+ // 1. find target entities
435
+ // 2. find links
436
+ // 3. convert to 'in' filter
437
+ const targetEntityDataAccessor = server.getDataAccessor({
438
+ singularCode: relationProperty.targetSingularCode as string,
439
+ });
440
+ const relatedModel = targetEntityDataAccessor.getModel();
441
+ let relatedBaseModel: RpdDataModel;
442
+ if (relatedModel.base) {
443
+ relatedBaseModel = server.getModel({
444
+ singularCode: relatedModel.base,
445
+ });
446
+ }
447
+ const targetEntities = await targetEntityDataAccessor.find({
448
+ filters: await convertEntityFiltersToRowFilters(server, relatedModel, relatedBaseModel, filter.filters),
449
+ fields: [
450
+ {
451
+ name: "id",
452
+ tableName: relatedModel.tableName,
453
+ },
454
+ ],
455
+ });
456
+ const targetEntityIds = map(targetEntities, (entity: any) => entity["id"]);
457
+
458
+ const command = `SELECT * FROM ${server.queryBuilder.quoteTable({
459
+ schema: relationProperty.linkSchema,
460
+ tableName: relationProperty.linkTableName!,
461
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.targetIdColumnName!)} = ANY($1::int[])`;
462
+ const params = [targetEntityIds];
463
+ const links = await server.queryDatabaseObject(command, params);
464
+ const selfEntityIds = links.map((link) => link[relationProperty.selfIdColumnName!]);
465
+ replacedFilters.push({
466
+ field: {
467
+ name: "id",
468
+ tableName: model.tableName,
469
+ },
470
+ operator: operator === "exists" ? "in" : "notIn",
471
+ value: selfEntityIds,
472
+ });
473
+ }
474
+ } else {
475
+ const filterField = (filter as EntityNonRelationPropertyFilterOptions).field;
476
+ let property: RpdDataModelProperty = getEntityProperty(server, model, (property) => {
477
+ return property.code === filterField;
478
+ });
479
+
480
+ let columnName = "";
481
+ if (property) {
482
+ columnName = property.columnName || property.code;
483
+ } else {
484
+ property = getEntityProperty(server, model, (property) => {
485
+ return property.columnName === filterField;
486
+ });
487
+
488
+ if (property) {
489
+ columnName = property.columnName;
490
+ } else {
491
+ property = getEntityProperty(server, model, (property) => {
492
+ return property.targetIdColumnName === filterField;
493
+ });
494
+
495
+ if (property) {
496
+ columnName = property.targetIdColumnName;
497
+ } else {
498
+ columnName = filterField;
499
+ }
500
+ }
501
+ }
502
+
503
+ // TODO: do not use `any` here
504
+ replacedFilters.push({
505
+ operator: filter.operator,
506
+ field: {
507
+ name: columnName,
508
+ tableName: property && property.isBaseProperty ? baseModel.tableName : model.tableName,
509
+ },
510
+ value: (filter as any).value,
511
+ itemType: (filter as any).itemType,
512
+ } as any);
513
+ }
514
+ }
515
+ return replacedFilters;
516
+ }
517
+
518
+ async function findManyRelationLinksViaLinkTable(options: FindManyRelationEntitiesOptions) {
519
+ const { server, relationProperty, mainModel: relationModel, mainEntityIds, selectRelationOptions } = options;
520
+ const command = `SELECT * FROM ${server.queryBuilder.quoteTable({
521
+ schema: relationProperty.linkSchema,
522
+ tableName: relationProperty.linkTableName!,
523
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = ANY($1::int[])`;
524
+ const params = [mainEntityIds];
525
+ const links = await server.queryDatabaseObject(command, params);
526
+ const targetEntityIds = links.map((link) => link[relationProperty.targetIdColumnName!]);
527
+
528
+ const dataAccessor = server.getDataAccessor({
529
+ namespace: relationModel.namespace,
530
+ singularCode: relationModel.singularCode,
531
+ });
532
+
533
+ const findEntityOptions: FindEntityOptions = {
534
+ filters: [
535
+ {
536
+ field: "id",
537
+ operator: "in",
538
+ value: targetEntityIds,
539
+ },
540
+ ],
541
+ keepNonPropertyFields: true,
542
+ };
543
+
544
+ if (selectRelationOptions) {
545
+ if (typeof selectRelationOptions !== "boolean") {
546
+ if (selectRelationOptions.properties) {
547
+ findEntityOptions.properties = ["id", ...selectRelationOptions.properties];
548
+ }
549
+ if (selectRelationOptions.relations) {
550
+ findEntityOptions.relations = selectRelationOptions.relations;
551
+ }
552
+ }
553
+ }
554
+
555
+ const targetEntities = await findEntities(server, dataAccessor, findEntityOptions);
556
+
557
+ forEach(links, (link: any) => {
558
+ link.targetEntity = find(targetEntities, (e: any) => e["id"] == link[relationProperty.targetIdColumnName!]);
559
+ });
560
+
561
+ return links;
562
+ }
563
+
564
+ async function findManyRelatedEntitiesViaIdPropertyCode(options: FindManyRelationEntitiesOptions) {
565
+ const { server, relationProperty, mainEntityIds, selectRelationOptions } = options;
566
+ const dataAccessor = server.getDataAccessor({
567
+ singularCode: relationProperty.targetSingularCode as string,
568
+ });
569
+
570
+ const findEntityOptions: FindEntityOptions = {
571
+ filters: [
572
+ {
573
+ field: relationProperty.selfIdColumnName,
574
+ operator: "in",
575
+ value: mainEntityIds,
576
+ },
577
+ ],
578
+ extraColumnsToSelect: [relationProperty.selfIdColumnName],
579
+ keepNonPropertyFields: true,
580
+ };
581
+
582
+ if (selectRelationOptions) {
583
+ if (typeof selectRelationOptions !== "boolean") {
584
+ if (selectRelationOptions.properties) {
585
+ findEntityOptions.properties = ["id", ...selectRelationOptions.properties];
586
+ }
587
+ if (selectRelationOptions.relations) {
588
+ findEntityOptions.relations = selectRelationOptions.relations;
589
+ }
590
+ }
591
+ }
592
+
593
+ return await findEntities(server, dataAccessor, findEntityOptions);
594
+ }
595
+
596
+ async function findOneRelatedEntitiesViaIdPropertyCode(options: FindOneRelationEntitiesOptions) {
597
+ const { server, relationProperty, relationEntityIds, selectRelationOptions } = options;
598
+
599
+ const dataAccessor = server.getDataAccessor({
600
+ singularCode: relationProperty.targetSingularCode as string,
601
+ });
602
+
603
+ const findEntityOptions: FindEntityOptions = {
604
+ filters: [
605
+ {
606
+ field: "id",
607
+ operator: "in",
608
+ value: relationEntityIds,
609
+ },
610
+ ],
611
+ keepNonPropertyFields: true,
612
+ };
613
+
614
+ if (selectRelationOptions) {
615
+ if (typeof selectRelationOptions !== "boolean") {
616
+ if (selectRelationOptions.properties) {
617
+ findEntityOptions.properties = ["id", ...selectRelationOptions.properties];
618
+ }
619
+ if (selectRelationOptions.relations) {
620
+ findEntityOptions.relations = selectRelationOptions.relations;
621
+ }
622
+ }
623
+ }
624
+
625
+ return await findEntities(server, dataAccessor, findEntityOptions);
626
+ }
627
+
628
+ async function createEntity(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: CreateEntityOptions, plugin?: RapidPlugin) {
629
+ const model = dataAccessor.getModel();
630
+ if (model.derivedTypePropertyCode) {
631
+ throw newEntityOperationError("Create base entity directly is not allowed.");
632
+ }
633
+
634
+ const { entity, routeContext } = options;
635
+
636
+ const userId = options.routeContext?.state?.userId;
637
+ if (userId) {
638
+ const createdByProperty = getEntityPropertyByCode(server, model, "createdBy");
639
+ if (createdByProperty) {
640
+ entity.createdBy = userId;
641
+ }
642
+ }
643
+ const createdAtProperty = getEntityPropertyByCode(server, model, "createdAt");
644
+ if (createdAtProperty) {
645
+ entity.createdAt = getNowStringWithTimezone();
646
+ }
647
+
648
+ await server.beforeCreateEntity(model, options);
649
+
650
+ await server.emitEvent({
651
+ eventName: "entity.beforeCreate",
652
+ payload: {
653
+ namespace: model.namespace,
654
+ modelSingularCode: model.singularCode,
655
+ baseModelSingularCode: model.base,
656
+ before: entity,
657
+ },
658
+ sender: plugin,
659
+ routeContext,
660
+ });
661
+
662
+ const oneRelationPropertiesToCreate: RpdDataModelProperty[] = [];
663
+ const manyRelationPropertiesToCreate: RpdDataModelProperty[] = [];
664
+ keys(entity).forEach((propertyCode) => {
665
+ const property = getEntityPropertyByCode(server, model, propertyCode);
666
+ if (!property) {
667
+ // Unknown property
668
+ return;
669
+ }
670
+
671
+ if (isRelationProperty(property)) {
672
+ if (property.relation === "many") {
673
+ manyRelationPropertiesToCreate.push(property);
674
+ } else {
675
+ oneRelationPropertiesToCreate.push(property);
676
+ }
677
+ }
678
+ });
679
+
680
+ const { row, baseRow } = mapEntityToDbRow(server, model, entity);
681
+
682
+ const newEntityOneRelationProps = {};
683
+ // save one-relation properties
684
+ for (const property of oneRelationPropertiesToCreate) {
685
+ const targetRow = property.isBaseProperty ? baseRow : row;
686
+ const fieldValue = entity[property.code];
687
+ const targetDataAccessor = server.getDataAccessor({
688
+ singularCode: property.targetSingularCode!,
689
+ });
690
+ if (isObject(fieldValue)) {
691
+ const targetEntityId = fieldValue["id"];
692
+ if (!targetEntityId) {
693
+ const targetEntity = fieldValue;
694
+ const newTargetEntity = await createEntity(server, targetDataAccessor, {
695
+ entity: targetEntity,
696
+ });
697
+ newEntityOneRelationProps[property.code] = newTargetEntity;
698
+ targetRow[property.targetIdColumnName!] = newTargetEntity["id"];
699
+ } else {
700
+ const targetEntity = await findById(server, targetDataAccessor, {
701
+ id: targetEntityId,
702
+ routeContext,
703
+ });
704
+ if (!targetEntity) {
705
+ throw newEntityOperationError(
706
+ `Create ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${targetEntityId}' was not found.`,
707
+ );
708
+ }
709
+ newEntityOneRelationProps[property.code] = targetEntity;
710
+ targetRow[property.targetIdColumnName!] = targetEntityId;
711
+ }
712
+ } else if (isNumber(fieldValue)) {
713
+ // fieldValue is id;
714
+ const targetEntityId = fieldValue;
715
+ const targetEntity = await findById(server, targetDataAccessor, {
716
+ id: targetEntityId,
717
+ routeContext,
718
+ });
719
+ if (!targetEntity) {
720
+ throw newEntityOperationError(
721
+ `Create ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${targetEntityId}' was not found.`,
722
+ );
723
+ }
724
+ newEntityOneRelationProps[property.code] = targetEntity;
725
+ targetRow[property.targetIdColumnName!] = targetEntityId;
726
+ } else {
727
+ newEntityOneRelationProps[property.code] = null;
728
+ targetRow[property.targetIdColumnName!] = null;
729
+ }
730
+ }
731
+
732
+ let newBaseRow: any;
733
+ if (model.base) {
734
+ const baseDataAccessor = server.getDataAccessor({
735
+ singularCode: model.base,
736
+ });
737
+ newBaseRow = await baseDataAccessor.create(baseRow);
738
+
739
+ row.id = newBaseRow.id;
740
+ }
741
+ const newRow = await dataAccessor.create(row);
742
+ const newEntity = mapDbRowToEntity(server, model, Object.assign({}, newBaseRow, newRow, newEntityOneRelationProps), true);
743
+
744
+ // save many-relation properties
745
+ for (const property of manyRelationPropertiesToCreate) {
746
+ newEntity[property.code] = [];
747
+
748
+ const targetDataAccessor = server.getDataAccessor({
749
+ singularCode: property.targetSingularCode!,
750
+ });
751
+
752
+ const relatedEntitiesToBeSaved = entity[property.code];
753
+ if (!isArray(relatedEntitiesToBeSaved)) {
754
+ throw new Error(`Value of field '${property.code}' should be an array.`);
755
+ }
756
+
757
+ for (const relatedEntityToBeSaved of relatedEntitiesToBeSaved) {
758
+ let relatedEntityId: any;
759
+ if (isObject(relatedEntityToBeSaved)) {
760
+ relatedEntityId = relatedEntityToBeSaved["id"];
761
+ if (!relatedEntityId) {
762
+ // related entity is to be created
763
+ const targetEntity = relatedEntityToBeSaved;
764
+ if (!property.linkTableName) {
765
+ targetEntity[property.selfIdColumnName!] = newEntity.id;
766
+ }
767
+ const newTargetEntity = await createEntity(server, targetDataAccessor, {
768
+ entity: targetEntity,
769
+ });
770
+
771
+ if (property.linkTableName) {
772
+ const command = `INSERT INTO ${server.queryBuilder.quoteTable({
773
+ schema: property.linkSchema,
774
+ tableName: property.linkTableName,
775
+ })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
776
+ const params = [newEntity.id, newTargetEntity.id];
777
+ await server.queryDatabaseObject(command, params);
778
+ }
779
+
780
+ newEntity[property.code].push(newTargetEntity);
781
+ } else {
782
+ // related entity is existed
783
+ const targetEntity = await targetDataAccessor.findById(relatedEntityId);
784
+ if (!targetEntity) {
785
+ throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
786
+ }
787
+
788
+ if (property.linkTableName) {
789
+ const command = `INSERT INTO ${server.queryBuilder.quoteTable({
790
+ schema: property.linkSchema,
791
+ tableName: property.linkTableName,
792
+ })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
793
+ const params = [newEntity.id, relatedEntityId];
794
+ await server.queryDatabaseObject(command, params);
795
+ } else {
796
+ await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: newEntity.id });
797
+ targetEntity[property.selfIdColumnName!] = newEntity.id;
798
+ }
799
+ newEntity[property.code].push(targetEntity);
800
+ }
801
+ } else {
802
+ // fieldValue is id
803
+ relatedEntityId = relatedEntityToBeSaved;
804
+ const targetEntity = await targetDataAccessor.findById(relatedEntityId);
805
+ if (!targetEntity) {
806
+ throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
807
+ }
808
+
809
+ if (property.linkTableName) {
810
+ const command = `INSERT INTO ${server.queryBuilder.quoteTable({
811
+ schema: property.linkSchema,
812
+ tableName: property.linkTableName,
813
+ })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
814
+ const params = [newEntity.id, relatedEntityId];
815
+ await server.queryDatabaseObject(command, params);
816
+ } else {
817
+ await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: newEntity.id });
818
+ targetEntity[property.selfIdColumnName!] = newEntity.id;
819
+ }
820
+
821
+ newEntity[property.code].push(targetEntity);
822
+ }
823
+ }
824
+ }
825
+
826
+ await server.emitEvent({
827
+ eventName: "entity.create",
828
+ payload: {
829
+ namespace: model.namespace,
830
+ modelSingularCode: model.singularCode,
831
+ baseModelSingularCode: model.base,
832
+ after: newEntity,
833
+ },
834
+ sender: plugin,
835
+ routeContext,
836
+ });
837
+
838
+ return newEntity;
839
+ }
840
+
841
+ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: UpdateEntityByIdOptions, plugin?: RapidPlugin) {
842
+ const model = dataAccessor.getModel();
843
+ const { id, routeContext } = options;
844
+ if (!id) {
845
+ throw new Error("Id is required when updating an entity.");
846
+ }
847
+
848
+ const entity = await findById(server, dataAccessor, {
849
+ routeContext,
850
+ id,
851
+ keepNonPropertyFields: true,
852
+ });
853
+ if (!entity) {
854
+ throw new Error(`${model.namespace}.${model.singularCode} with id "${id}" was not found.`);
855
+ }
856
+
857
+ let { entityToSave } = options;
858
+ let changes = getEntityPartChanges(server, model, entity, entityToSave);
859
+ if (!changes && !options.operation) {
860
+ return entity;
861
+ }
862
+
863
+ entityToSave = changes || {};
864
+
865
+ const userId = options.routeContext?.state?.userId;
866
+ if (userId) {
867
+ const updatedByProperty = getEntityPropertyByCode(server, model, "updatedBy");
868
+ if (updatedByProperty) {
869
+ entityToSave.updatedBy = userId;
870
+ }
871
+ }
872
+ const updatedAtProperty = getEntityPropertyByCode(server, model, "updatedAt");
873
+ if (updatedAtProperty) {
874
+ entityToSave.updatedAt = getNowStringWithTimezone();
875
+ }
876
+
877
+ await server.beforeUpdateEntity(model, options, entity);
878
+
879
+ await server.emitEvent({
880
+ eventName: "entity.beforeUpdate",
881
+ payload: {
882
+ namespace: model.namespace,
883
+ modelSingularCode: model.singularCode,
884
+ before: entity,
885
+ changes: entityToSave,
886
+ operation: options.operation,
887
+ stateProperties: options.stateProperties,
888
+ },
889
+ sender: plugin,
890
+ routeContext: options.routeContext,
891
+ });
892
+
893
+ changes = getEntityPartChanges(server, model, entity, entityToSave);
894
+
895
+ const oneRelationPropertiesToUpdate: RpdDataModelProperty[] = [];
896
+ const manyRelationPropertiesToUpdate: RpdDataModelProperty[] = [];
897
+ keys(changes).forEach((propertyCode) => {
898
+ const property = getEntityPropertyByCode(server, model, propertyCode);
899
+ if (!property) {
900
+ // Unknown property
901
+ return;
902
+ }
903
+
904
+ if (isRelationProperty(property)) {
905
+ if (property.relation === "many") {
906
+ manyRelationPropertiesToUpdate.push(property);
907
+ } else {
908
+ oneRelationPropertiesToUpdate.push(property);
909
+ }
910
+ }
911
+ });
912
+
913
+ const { row, baseRow } = mapEntityToDbRow(server, model, changes);
914
+
915
+ const updatedEntityOneRelationProps = {};
916
+ for (const property of oneRelationPropertiesToUpdate) {
917
+ const targetRow = property.isBaseProperty ? baseRow : row;
918
+ const fieldValue = changes[property.code];
919
+ const targetDataAccessor = server.getDataAccessor({
920
+ singularCode: property.targetSingularCode!,
921
+ });
922
+
923
+ if (isObject(fieldValue)) {
924
+ const targetEntityId = fieldValue["id"];
925
+ if (!targetEntityId) {
926
+ const targetEntity = fieldValue;
927
+ const newTargetEntity = await createEntity(server, targetDataAccessor, {
928
+ entity: targetEntity,
929
+ });
930
+ updatedEntityOneRelationProps[property.code] = newTargetEntity;
931
+ targetRow[property.targetIdColumnName!] = newTargetEntity["id"];
932
+ } else {
933
+ const targetEntity = await findById(server, targetDataAccessor, {
934
+ id: targetEntityId,
935
+ routeContext,
936
+ });
937
+ if (!targetEntity) {
938
+ throw newEntityOperationError(
939
+ `Create ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${targetEntityId}' was not found.`,
940
+ );
941
+ }
942
+ updatedEntityOneRelationProps[property.code] = targetEntity;
943
+ targetRow[property.targetIdColumnName!] = targetEntityId;
944
+ }
945
+ } else if (isNumber(fieldValue)) {
946
+ // fieldValue is id;
947
+ const targetEntityId = fieldValue;
948
+ const targetEntity = await findById(server, targetDataAccessor, {
949
+ id: targetEntityId,
950
+ routeContext,
951
+ });
952
+ if (!targetEntity) {
953
+ throw newEntityOperationError(
954
+ `Create ${model.singularCode} entity failed. Property '${property.code}' was invalid. Related ${property.targetSingularCode} entity with id '${targetEntityId}' was not found.`,
955
+ );
956
+ }
957
+ updatedEntityOneRelationProps[property.code] = targetEntity;
958
+ targetRow[property.targetIdColumnName!] = targetEntityId;
959
+ } else {
960
+ updatedEntityOneRelationProps[property.code] = null;
961
+ targetRow[property.targetIdColumnName!] = null;
962
+ }
963
+ }
964
+
965
+ let updatedRow = row;
966
+ if (Object.keys(row).length) {
967
+ updatedRow = await dataAccessor.updateById(id, row);
968
+ }
969
+ let updatedBaseRow = baseRow;
970
+ if (model.base && Object.keys(baseRow).length) {
971
+ const baseDataAccessor = server.getDataAccessor({
972
+ singularCode: model.base,
973
+ });
974
+ updatedBaseRow = await baseDataAccessor.updateById(id, updatedBaseRow);
975
+ }
976
+
977
+ let updatedEntity = mapDbRowToEntity(server, model, { ...updatedRow, ...updatedBaseRow, ...updatedEntityOneRelationProps }, true);
978
+ updatedEntity = Object.assign({}, entity, updatedEntity);
979
+
980
+ // save many-relation properties
981
+ for (const property of manyRelationPropertiesToUpdate) {
982
+ const relatedEntities: any[] = [];
983
+ const targetDataAccessor = server.getDataAccessor({
984
+ singularCode: property.targetSingularCode!,
985
+ });
986
+
987
+ const relatedEntitiesToBeSaved = changes[property.code];
988
+ if (!isArray(relatedEntitiesToBeSaved)) {
989
+ throw new Error(`Value of field '${property.code}' should be an array.`);
990
+ }
991
+
992
+ if (property.linkTableName) {
993
+ // TODO: should support removing relation
994
+ await server.queryDatabaseObject(
995
+ `DELETE FROM ${server.queryBuilder.quoteTable({
996
+ schema: property.linkSchema,
997
+ tableName: property.linkTableName,
998
+ })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName!)} = $1`,
999
+ [id],
1000
+ );
1001
+ }
1002
+
1003
+ for (const relatedEntityToBeSaved of relatedEntitiesToBeSaved) {
1004
+ let relatedEntityId: any;
1005
+ if (isObject(relatedEntityToBeSaved)) {
1006
+ relatedEntityId = relatedEntityToBeSaved["id"];
1007
+ if (!relatedEntityId) {
1008
+ // related entity is to be created
1009
+ const targetEntity = relatedEntityToBeSaved;
1010
+ if (!property.linkTableName) {
1011
+ targetEntity[property.selfIdColumnName!] = id;
1012
+ }
1013
+ const newTargetEntity = await createEntity(server, targetDataAccessor, {
1014
+ entity: targetEntity,
1015
+ });
1016
+
1017
+ if (property.linkTableName) {
1018
+ const command = `INSERT INTO ${server.queryBuilder.quoteTable({
1019
+ schema: property.linkSchema,
1020
+ tableName: property.linkTableName,
1021
+ })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1022
+ const params = [id, newTargetEntity.id];
1023
+ await server.queryDatabaseObject(command, params);
1024
+ }
1025
+
1026
+ relatedEntities.push(newTargetEntity);
1027
+ } else {
1028
+ // related entity is existed
1029
+ const targetEntity = await targetDataAccessor.findById(relatedEntityId);
1030
+ if (!targetEntity) {
1031
+ throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
1032
+ }
1033
+
1034
+ if (property.linkTableName) {
1035
+ const command = `INSERT INTO ${server.queryBuilder.quoteTable({
1036
+ schema: property.linkSchema,
1037
+ tableName: property.linkTableName,
1038
+ })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1039
+ const params = [id, relatedEntityId];
1040
+ await server.queryDatabaseObject(command, params);
1041
+ } else {
1042
+ await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: id });
1043
+ targetEntity[property.selfIdColumnName!] = id;
1044
+ }
1045
+ relatedEntities.push(targetEntity);
1046
+ }
1047
+ } else {
1048
+ // fieldValue is id
1049
+ relatedEntityId = relatedEntityToBeSaved;
1050
+ const targetEntity = await targetDataAccessor.findById(relatedEntityId);
1051
+ if (!targetEntity) {
1052
+ throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
1053
+ }
1054
+
1055
+ if (property.linkTableName) {
1056
+ const command = `INSERT INTO ${server.queryBuilder.quoteTable({
1057
+ schema: property.linkSchema,
1058
+ tableName: property.linkTableName,
1059
+ })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1060
+ const params = [id, relatedEntityId];
1061
+ await server.queryDatabaseObject(command, params);
1062
+ } else {
1063
+ await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: id });
1064
+ targetEntity[property.selfIdColumnName!] = id;
1065
+ }
1066
+
1067
+ relatedEntities.push(targetEntity);
1068
+ }
1069
+ }
1070
+ updatedEntity[property.code] = relatedEntities;
1071
+ }
1072
+
1073
+ await server.emitEvent({
1074
+ eventName: "entity.update",
1075
+ payload: {
1076
+ namespace: model.namespace,
1077
+ modelSingularCode: model.singularCode,
1078
+ // TODO: should not emit event on base model if it's not effected.
1079
+ baseModelSingularCode: model.base,
1080
+ before: entity,
1081
+ after: updatedEntity,
1082
+ changes: changes,
1083
+ operation: options.operation,
1084
+ stateProperties: options.stateProperties,
1085
+ },
1086
+ sender: plugin,
1087
+ routeContext: options.routeContext,
1088
+ });
1089
+
1090
+ return updatedEntity;
1091
+ }
1092
+
1093
+ export default class EntityManager<TEntity = any> {
1094
+ #server: IRpdServer;
1095
+ #dataAccessor: IRpdDataAccessor;
1096
+
1097
+ constructor(server: IRpdServer, dataAccessor: IRpdDataAccessor) {
1098
+ this.#server = server;
1099
+ this.#dataAccessor = dataAccessor;
1100
+ }
1101
+
1102
+ getModel(): RpdDataModel {
1103
+ return this.#dataAccessor.getModel();
1104
+ }
1105
+
1106
+ async findEntities(options: FindEntityOptions): Promise<TEntity[]> {
1107
+ return await findEntities(this.#server, this.#dataAccessor, options);
1108
+ }
1109
+
1110
+ async findEntity(options: FindEntityOptions): Promise<TEntity | null> {
1111
+ return await findEntity(this.#server, this.#dataAccessor, options);
1112
+ }
1113
+
1114
+ async findById(options: FindEntityByIdOptions | string | number): Promise<TEntity | null> {
1115
+ // options is id
1116
+ if (!isObject(options)) {
1117
+ options = {
1118
+ id: options,
1119
+ };
1120
+ }
1121
+ return await findById(this.#server, this.#dataAccessor, options);
1122
+ }
1123
+
1124
+ async createEntity(options: CreateEntityOptions, plugin?: RapidPlugin): Promise<TEntity> {
1125
+ return await createEntity(this.#server, this.#dataAccessor, options, plugin);
1126
+ }
1127
+
1128
+ async updateEntityById(options: UpdateEntityByIdOptions, plugin?: RapidPlugin): Promise<TEntity> {
1129
+ return await updateEntityById(this.#server, this.#dataAccessor, options, plugin);
1130
+ }
1131
+
1132
+ async count(options: CountEntityOptions): Promise<CountEntityResult> {
1133
+ const model = this.#dataAccessor.getModel();
1134
+ let baseModel: RpdDataModel;
1135
+ if (model.base) {
1136
+ baseModel = this.#server.getModel({
1137
+ singularCode: model.base,
1138
+ });
1139
+ }
1140
+ const countRowOptions: CountRowOptions = {
1141
+ filters: await convertEntityFiltersToRowFilters(this.#server, model, baseModel, options.filters),
1142
+ };
1143
+ return await this.#dataAccessor.count(countRowOptions);
1144
+ }
1145
+
1146
+ async deleteById(options: DeleteEntityByIdOptions | string | number, plugin?: RapidPlugin): Promise<void> {
1147
+ // options is id
1148
+ if (!isObject(options)) {
1149
+ options = {
1150
+ id: options,
1151
+ };
1152
+ }
1153
+
1154
+ const model = this.getModel();
1155
+ if (model.derivedTypePropertyCode) {
1156
+ throw newEntityOperationError("Delete base entity directly is not allowed.");
1157
+ }
1158
+
1159
+ const { id, routeContext } = options;
1160
+
1161
+ const entity = await this.findById({
1162
+ id,
1163
+ keepNonPropertyFields: true,
1164
+ routeContext,
1165
+ });
1166
+
1167
+ if (!entity) {
1168
+ return;
1169
+ }
1170
+
1171
+ await this.#server.emitEvent({
1172
+ eventName: "entity.beforeDelete",
1173
+ payload: {
1174
+ namespace: model.namespace,
1175
+ modelSingularCode: model.singularCode,
1176
+ before: entity,
1177
+ },
1178
+ sender: plugin,
1179
+ routeContext,
1180
+ });
1181
+
1182
+ await this.#dataAccessor.deleteById(id);
1183
+ if (model.base) {
1184
+ const baseDataAccessor = this.#server.getDataAccessor({
1185
+ singularCode: model.base,
1186
+ });
1187
+ await baseDataAccessor.deleteById(id);
1188
+ }
1189
+
1190
+ await this.#server.emitEvent({
1191
+ eventName: "entity.delete",
1192
+ payload: {
1193
+ namespace: model.namespace,
1194
+ modelSingularCode: model.singularCode,
1195
+ before: entity,
1196
+ },
1197
+ sender: plugin,
1198
+ routeContext,
1199
+ });
1200
+ }
1201
+
1202
+ async addRelations(options: AddEntityRelationsOptions, plugin?: RapidPlugin): Promise<void> {
1203
+ const server = this.#server;
1204
+ const model = this.getModel();
1205
+ const { id, property, relations, routeContext } = options;
1206
+ const entity = await this.findById({
1207
+ id,
1208
+ routeContext,
1209
+ });
1210
+ if (!entity) {
1211
+ throw new Error(`${model.namespace}.${model.singularCode} with id "${id}" was not found.`);
1212
+ }
1213
+
1214
+ const relationProperty = getEntityPropertyByCode(server, model, property);
1215
+ if (!relationProperty) {
1216
+ throw new Error(`Property '${property}' was not found in ${model.namespace}.${model.singularCode}`);
1217
+ }
1218
+
1219
+ if (!(isRelationProperty(relationProperty) && relationProperty.relation === "many")) {
1220
+ throw new Error(`Operation 'addRelations' is only supported on property of 'many' relation`);
1221
+ }
1222
+
1223
+ const { queryBuilder } = server;
1224
+ if (relationProperty.linkTableName) {
1225
+ for (const relation of relations) {
1226
+ const command = `INSERT INTO ${queryBuilder.quoteTable({
1227
+ schema: relationProperty.linkSchema,
1228
+ tableName: relationProperty.linkTableName,
1229
+ })} (${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}, ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)})
1230
+ SELECT $1, $2 WHERE NOT EXISTS (
1231
+ SELECT ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}, ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}
1232
+ FROM ${queryBuilder.quoteTable({ schema: relationProperty.linkSchema, tableName: relationProperty.linkTableName })}
1233
+ WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}=$2
1234
+ )`;
1235
+ const params = [id, relation.id];
1236
+ await server.queryDatabaseObject(command, params);
1237
+ }
1238
+ }
1239
+
1240
+ await server.emitEvent({
1241
+ eventName: "entity.addRelations",
1242
+ payload: {
1243
+ namespace: model.namespace,
1244
+ modelSingularCode: model.singularCode,
1245
+ entity,
1246
+ property,
1247
+ relations,
1248
+ },
1249
+ sender: plugin,
1250
+ routeContext: options.routeContext,
1251
+ });
1252
+ }
1253
+
1254
+ async removeRelations(options: RemoveEntityRelationsOptions, plugin?: RapidPlugin): Promise<void> {
1255
+ const server = this.#server;
1256
+ const model = this.getModel();
1257
+ const { id, property, relations, routeContext } = options;
1258
+ const entity = await this.findById({
1259
+ id,
1260
+ routeContext,
1261
+ });
1262
+ if (!entity) {
1263
+ throw new Error(`${model.namespace}.${model.singularCode} with id "${id}" was not found.`);
1264
+ }
1265
+
1266
+ const relationProperty = getEntityPropertyByCode(server, model, property);
1267
+ if (!relationProperty) {
1268
+ throw new Error(`Property '${property}' was not found in ${model.namespace}.${model.singularCode}`);
1269
+ }
1270
+
1271
+ if (!(isRelationProperty(relationProperty) && relationProperty.relation === "many")) {
1272
+ throw new Error(`Operation 'removeRelations' is only supported on property of 'many' relation`);
1273
+ }
1274
+
1275
+ const { queryBuilder } = server;
1276
+ if (relationProperty.linkTableName) {
1277
+ for (const relation of relations) {
1278
+ const command = `DELETE FROM ${queryBuilder.quoteTable({ schema: relationProperty.linkSchema, tableName: relationProperty.linkTableName })}
1279
+ WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}=$2;`;
1280
+ const params = [id, relation.id];
1281
+ await server.queryDatabaseObject(command, params);
1282
+ }
1283
+ }
1284
+
1285
+ await server.emitEvent({
1286
+ eventName: "entity.removeRelations",
1287
+ payload: {
1288
+ namespace: model.namespace,
1289
+ modelSingularCode: model.singularCode,
1290
+ entity,
1291
+ property,
1292
+ relations,
1293
+ },
1294
+ sender: plugin,
1295
+ routeContext: options.routeContext,
1296
+ });
1297
+ }
1298
+ }