@ruiapp/rapid-core 0.1.83 → 0.2.1

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