@ruiapp/rapid-core 0.5.1 → 0.5.3

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.
@@ -1,4 +1,4 @@
1
- import { CreateEntityOptions, EmitServerEventOptions, EntityWatcherType, GetDataAccessorOptions, GetModelOptions, IDatabaseAccessor, IDatabaseConfig, IQueryBuilder, IRpdDataAccessor, RapidServerConfig, RpdApplicationConfig, RpdDataModel, RpdDataModelProperty, RpdServerEventTypes, UpdateEntityByIdOptions } from "../types";
1
+ import { CreateEntityOptions, EmitServerEventOptions, EntityWatcherType, GetDataAccessorOptions, GetModelOptions, IDatabaseAccessor, IDatabaseClient, IDatabaseConfig, IQueryBuilder, IRpdDataAccessor, RapidServerConfig, RpdApplicationConfig, RpdDataModel, RpdDataModelProperty, RpdServerEventTypes, UpdateEntityByIdOptions } from "../types";
2
2
  import { IPluginActionHandler, ActionHandler, ActionHandlerContext } from "./actionHandler";
3
3
  import { Next, RouteContext } from "./routeContext";
4
4
  import EntityManager from "../dataAccess/entityManager";
@@ -12,8 +12,8 @@ export interface IRpdServer {
12
12
  registerFacilityFactory(factory: FacilityFactory): any;
13
13
  getFacility<TFacility = any>(name: string, options?: any): Promise<TFacility>;
14
14
  getDatabaseAccessor(): IDatabaseAccessor;
15
- queryDatabaseObject: (sql: string, params?: unknown[] | Record<string, unknown>) => Promise<any[]>;
16
- tryQueryDatabaseObject: (sql: string, params?: unknown[] | Record<string, unknown>) => Promise<any[]>;
15
+ queryDatabaseObject: (sql: string, params?: unknown[] | Record<string, unknown>, client?: IDatabaseClient) => Promise<any[]>;
16
+ tryQueryDatabaseObject: (sql: string, params?: unknown[] | Record<string, unknown>, client?: IDatabaseClient) => Promise<any[]>;
17
17
  registerMiddleware(middleware: any): void;
18
18
  registerActionHandler(plugin: RapidPlugin, options: IPluginActionHandler): void;
19
19
  getActionHandlerByCode(code: string): ActionHandler | undefined;
@@ -3,6 +3,7 @@ import { IRpdServer, RapidPlugin } from "../core/server";
3
3
  import { RouteContext } from "../core/routeContext";
4
4
  export type FindOneRelationEntitiesOptions = {
5
5
  server: IRpdServer;
6
+ routeContext?: RouteContext;
6
7
  mainModel: RpdDataModel;
7
8
  relationProperty: RpdDataModelProperty;
8
9
  relationEntityIds: any[];
@@ -10,6 +11,7 @@ export type FindOneRelationEntitiesOptions = {
10
11
  };
11
12
  export type FindManyRelationEntitiesOptions = {
12
13
  server: IRpdServer;
14
+ routeContext?: RouteContext;
13
15
  mainModel: RpdDataModel;
14
16
  relationProperty: RpdDataModelProperty;
15
17
  mainEntityIds: any[];
@@ -3,8 +3,8 @@ import { RpdDataModel, RpdDataModelProperty } from "../types";
3
3
  export declare function isRelationProperty(property: RpdDataModelProperty): boolean;
4
4
  export declare function isOneRelationProperty(property: RpdDataModelProperty): boolean;
5
5
  export declare function isManyRelationProperty(property: RpdDataModelProperty): boolean;
6
- export declare function getEntityProperties(server: IRpdServer, model: RpdDataModel): RpdDataModelProperty[];
7
- export declare function getEntityPropertiesIncludingBase(server: IRpdServer, model: RpdDataModel): RpdDataModelProperty[];
6
+ export declare function getEntityProperties(server: IRpdServer, model: RpdDataModel, predicate?: (item: RpdDataModelProperty) => boolean): RpdDataModelProperty[];
7
+ export declare function getEntityPropertiesIncludingBase(server: IRpdServer, model: RpdDataModel, predicate?: (item: RpdDataModelProperty) => boolean): RpdDataModelProperty[];
8
8
  export declare function getEntityPropertyByCode(server: IRpdServer, model: RpdDataModel, propertyCode: string): RpdDataModelProperty | undefined;
9
9
  export declare function getEntityProperty(server: IRpdServer, model: RpdDataModel, predicate: (item: RpdDataModelProperty) => boolean): RpdDataModelProperty | undefined;
10
10
  export declare function getEntityOwnPropertyByCode(model: RpdDataModel, propertyCode: string): RpdDataModelProperty | undefined;
package/dist/index.js CHANGED
@@ -999,7 +999,6 @@ function mergeHeaders(target, source) {
999
999
  else if (lodash.isObject(source)) {
1000
1000
  Object.entries(source).forEach(([key, value]) => target.set(key, value));
1001
1001
  }
1002
- return target;
1003
1002
  }
1004
1003
  function newResponse(options) {
1005
1004
  return new Response(options.body, {
@@ -1008,6 +1007,7 @@ function newResponse(options) {
1008
1007
  });
1009
1008
  }
1010
1009
  class RapidResponse {
1010
+ // TODO: remove this field.
1011
1011
  #response;
1012
1012
  status;
1013
1013
  body;
@@ -1027,13 +1027,16 @@ class RapidResponse {
1027
1027
  if (headers) {
1028
1028
  mergeHeaders(responseHeaders, headers);
1029
1029
  }
1030
- this.#response = newResponse({ body, status: status || 200, headers: responseHeaders });
1030
+ this.status = status || 200;
1031
+ this.body = body;
1032
+ this.#response = newResponse({ body, status: this.status, headers: responseHeaders });
1031
1033
  }
1032
1034
  redirect(location, status) {
1033
1035
  this.headers.set("Location", location);
1036
+ this.status = status || 302;
1034
1037
  this.#response = newResponse({
1035
1038
  headers: this.headers,
1036
- status: status || 302,
1039
+ status: this.status,
1037
1040
  });
1038
1041
  }
1039
1042
  getResponse() {
@@ -1709,6 +1712,13 @@ var bootstrapApplicationConfig = {
1709
1712
  type: "text",
1710
1713
  required: false,
1711
1714
  },
1715
+ {
1716
+ name: "entityDeletingReaction",
1717
+ code: "entityDeletingReaction",
1718
+ columnName: "entity_deleting_reaction",
1719
+ type: "text",
1720
+ required: false,
1721
+ },
1712
1722
  {
1713
1723
  name: "readonly",
1714
1724
  code: "readonly",
@@ -2116,22 +2126,41 @@ function isOneRelationProperty(property) {
2116
2126
  function isManyRelationProperty(property) {
2117
2127
  return isRelationProperty(property) && property.relation === "many";
2118
2128
  }
2119
- function getEntityPropertiesIncludingBase(server, model) {
2120
- if (!model.base) {
2129
+ function getEntityProperties(server, model, predicate) {
2130
+ if (!predicate) {
2121
2131
  return model.properties;
2122
2132
  }
2133
+ return lodash.filter(model.properties, predicate);
2134
+ }
2135
+ function getEntityPropertiesIncludingBase(server, model, predicate) {
2136
+ if (!model.base) {
2137
+ return getEntityProperties(server, model, predicate);
2138
+ }
2123
2139
  const baseModel = server.getModel({
2124
2140
  singularCode: model.base,
2125
2141
  });
2126
2142
  let baseProperties = [];
2127
2143
  if (baseModel) {
2128
- baseProperties = baseModel.properties.map((property) => {
2144
+ if (predicate) {
2145
+ baseProperties = lodash.filter(baseModel.properties, predicate);
2146
+ }
2147
+ else {
2148
+ baseProperties = baseModel.properties;
2149
+ }
2150
+ baseProperties = baseProperties.map((property) => {
2129
2151
  property = lodash.cloneDeep(property);
2130
2152
  property.isBaseProperty = true;
2131
2153
  return property;
2132
2154
  });
2133
2155
  }
2134
- return [...baseProperties, ...model.properties];
2156
+ let properties;
2157
+ if (predicate) {
2158
+ properties = lodash.filter(model.properties, predicate);
2159
+ }
2160
+ else {
2161
+ properties = model.properties;
2162
+ }
2163
+ return [...baseProperties, ...properties];
2135
2164
  }
2136
2165
  function getEntityPropertyByCode(server, model, propertyCode) {
2137
2166
  return getEntityProperty(server, model, (e) => e.code === propertyCode);
@@ -2536,6 +2565,7 @@ async function findEntities(server, dataAccessor, options) {
2536
2565
  if (isManyRelation) {
2537
2566
  const relationLinks = await findManyRelationLinksViaLinkTable({
2538
2567
  server,
2568
+ routeContext,
2539
2569
  mainModel: relationModel,
2540
2570
  relationProperty,
2541
2571
  mainEntityIds: entityIds,
@@ -2553,6 +2583,7 @@ async function findEntities(server, dataAccessor, options) {
2553
2583
  if (isManyRelation) {
2554
2584
  relatedEntities = await findManyRelatedEntitiesViaIdPropertyCode({
2555
2585
  server,
2586
+ routeContext,
2556
2587
  mainModel: model,
2557
2588
  relationProperty,
2558
2589
  mainEntityIds: entityIds,
@@ -2563,6 +2594,7 @@ async function findEntities(server, dataAccessor, options) {
2563
2594
  const targetEntityIds = lodash.uniq(lodash.reject(lodash.map(rows, (entity) => entity[relationProperty.targetIdColumnName]), isNullOrUndefined));
2564
2595
  relatedEntities = await findOneRelatedEntitiesViaIdPropertyCode({
2565
2596
  server,
2597
+ routeContext,
2566
2598
  mainModel: model,
2567
2599
  relationProperty,
2568
2600
  relationEntityIds: targetEntityIds,
@@ -2779,7 +2811,7 @@ async function convertEntityFiltersToRowFilters(routeContext, server, model, bas
2779
2811
  tableName: relationProperty.linkTableName,
2780
2812
  })} WHERE ${server.queryBuilder.quoteObject(relationProperty.targetIdColumnName)} = ANY($1::int[])`;
2781
2813
  const params = [targetEntityIds];
2782
- const links = await server.queryDatabaseObject(command, params);
2814
+ const links = await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
2783
2815
  const selfEntityIds = links.map((link) => link[relationProperty.selfIdColumnName]);
2784
2816
  replacedFilters.push({
2785
2817
  field: {
@@ -2847,7 +2879,7 @@ async function convertEntityFiltersToRowFilters(routeContext, server, model, bas
2847
2879
  return replacedFilters;
2848
2880
  }
2849
2881
  async function findManyRelationLinksViaLinkTable(options) {
2850
- const { server, relationProperty, mainModel: relationModel, mainEntityIds, selectRelationOptions } = options;
2882
+ const { server, routeContext, relationProperty, mainModel: relationModel, mainEntityIds, selectRelationOptions } = options;
2851
2883
  const command = `SELECT * FROM ${server.queryBuilder.quoteTable({
2852
2884
  schema: relationProperty.linkSchema,
2853
2885
  tableName: relationProperty.linkTableName,
@@ -2855,7 +2887,7 @@ async function findManyRelationLinksViaLinkTable(options) {
2855
2887
  ORDER BY id
2856
2888
  `;
2857
2889
  const params = [mainEntityIds];
2858
- const links = await server.queryDatabaseObject(command, params);
2890
+ const links = await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
2859
2891
  const targetEntityIds = links.map((link) => link[relationProperty.targetIdColumnName]);
2860
2892
  const dataAccessor = server.getDataAccessor({
2861
2893
  namespace: relationModel.namespace,
@@ -3157,7 +3189,7 @@ async function createEntity(server, dataAccessor, options, plugin) {
3157
3189
  tableName: property.linkTableName,
3158
3190
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
3159
3191
  const params = [newEntity.id, newTargetEntity.id];
3160
- await server.queryDatabaseObject(command, params);
3192
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3161
3193
  }
3162
3194
  newEntity[property.code].push(newTargetEntity);
3163
3195
  }
@@ -3173,7 +3205,7 @@ async function createEntity(server, dataAccessor, options, plugin) {
3173
3205
  tableName: property.linkTableName,
3174
3206
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
3175
3207
  const params = [newEntity.id, relatedEntityId];
3176
- await server.queryDatabaseObject(command, params);
3208
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3177
3209
  }
3178
3210
  else {
3179
3211
  await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName]: newEntity.id }, routeContext?.getDbTransactionClient());
@@ -3195,7 +3227,7 @@ async function createEntity(server, dataAccessor, options, plugin) {
3195
3227
  tableName: property.linkTableName,
3196
3228
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
3197
3229
  const params = [newEntity.id, relatedEntityId];
3198
- await server.queryDatabaseObject(command, params);
3230
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3199
3231
  }
3200
3232
  else {
3201
3233
  await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName]: newEntity.id }, routeContext?.getDbTransactionClient());
@@ -3466,13 +3498,13 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3466
3498
  const targetLinks = await server.queryDatabaseObject(`SELECT ${server.queryBuilder.quoteObject(property.targetIdColumnName)} FROM ${server.queryBuilder.quoteTable({
3467
3499
  schema: property.linkSchema,
3468
3500
  tableName: property.linkTableName,
3469
- })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName)} = $1`, [id]);
3501
+ })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName)} = $1`, [id], routeContext?.getDbTransactionClient());
3470
3502
  currentTargetIds = targetLinks.map((item) => item[property.targetIdColumnName]);
3471
3503
  await server.queryDatabaseObject(`DELETE FROM ${server.queryBuilder.quoteTable({
3472
3504
  schema: property.linkSchema,
3473
3505
  tableName: property.linkTableName,
3474
3506
  })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName)} = $1
3475
- AND ${server.queryBuilder.quoteObject(property.targetIdColumnName)} <> ALL($2::int[])`, [id, targetIdsToKeep]);
3507
+ AND ${server.queryBuilder.quoteObject(property.targetIdColumnName)} <> ALL($2::int[])`, [id, targetIdsToKeep], routeContext?.getDbTransactionClient());
3476
3508
  }
3477
3509
  else {
3478
3510
  const targetModel = server.getModel({
@@ -3481,7 +3513,7 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3481
3513
  const targetRows = await server.queryDatabaseObject(`SELECT id FROM ${server.queryBuilder.quoteTable({
3482
3514
  schema: targetModel.schema,
3483
3515
  tableName: targetModel.tableName,
3484
- })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName)} = $1`, [id]);
3516
+ })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName)} = $1`, [id], routeContext?.getDbTransactionClient());
3485
3517
  currentTargetIds = targetRows.map((item) => item.id);
3486
3518
  }
3487
3519
  for (const relatedEntityToBeSaved of relatedEntitiesToBeSaved) {
@@ -3504,7 +3536,7 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3504
3536
  tableName: property.linkTableName,
3505
3537
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
3506
3538
  const params = [id, newTargetEntity.id];
3507
- await server.queryDatabaseObject(command, params);
3539
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3508
3540
  }
3509
3541
  relatedEntities.push(newTargetEntity);
3510
3542
  }
@@ -3545,7 +3577,7 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3545
3577
  tableName: property.linkTableName,
3546
3578
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
3547
3579
  const params = [id, relatedEntityId];
3548
- await server.queryDatabaseObject(command, params);
3580
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3549
3581
  }
3550
3582
  else {
3551
3583
  await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName]: id }, routeContext?.getDbTransactionClient());
@@ -3569,7 +3601,7 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3569
3601
  tableName: property.linkTableName,
3570
3602
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
3571
3603
  const params = [id, relatedEntityId];
3572
- await server.queryDatabaseObject(command, params);
3604
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3573
3605
  }
3574
3606
  else {
3575
3607
  await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName]: id }, routeContext?.getDbTransactionClient());
@@ -3651,6 +3683,146 @@ function getEntityDuplicatedErrorMessage(server, model, indexConfig) {
3651
3683
  });
3652
3684
  return `已存在 ${propertyNames.join(", ")} 相同的记录。`;
3653
3685
  }
3686
+ async function deleteEntityById(server, dataAccessor, options, plugin) {
3687
+ // options is id
3688
+ if (!lodash.isObject(options)) {
3689
+ options = {
3690
+ id: options,
3691
+ };
3692
+ }
3693
+ const model = dataAccessor.getModel();
3694
+ if (model.derivedTypePropertyCode) {
3695
+ // TODO: should be allowed.
3696
+ throw newEntityOperationError("Delete base entity directly is not allowed.");
3697
+ }
3698
+ const { id, routeContext } = options;
3699
+ const entity = await findById(server, dataAccessor, {
3700
+ id,
3701
+ keepNonPropertyFields: true,
3702
+ routeContext,
3703
+ });
3704
+ if (!entity) {
3705
+ return;
3706
+ }
3707
+ if (model.softDelete) {
3708
+ if (entity.deletedAt) {
3709
+ return;
3710
+ }
3711
+ }
3712
+ await server.emitEvent({
3713
+ eventName: "entity.beforeDelete",
3714
+ payload: {
3715
+ namespace: model.namespace,
3716
+ modelSingularCode: model.singularCode,
3717
+ before: entity,
3718
+ },
3719
+ sender: plugin,
3720
+ routeContext,
3721
+ });
3722
+ if (model.softDelete) {
3723
+ const currentUserId = routeContext?.state?.userId;
3724
+ await dataAccessor.updateById(id, {
3725
+ deleted_at: getNowStringWithTimezone(),
3726
+ deleter_id: currentUserId,
3727
+ }, routeContext?.getDbTransactionClient());
3728
+ }
3729
+ else {
3730
+ const relationPropertiesWithDeletingReaction = getEntityPropertiesIncludingBase(server, model, (property) => {
3731
+ return isRelationProperty(property) && property.entityDeletingReaction && property.entityDeletingReaction !== "doNothing";
3732
+ });
3733
+ for (const relationProperty of relationPropertiesWithDeletingReaction) {
3734
+ const relationDataAccessor = server.getDataAccessor({
3735
+ singularCode: relationProperty.targetSingularCode,
3736
+ });
3737
+ if (relationProperty.entityDeletingReaction === "cascadingDelete") {
3738
+ if (relationProperty.relation === "one") {
3739
+ const relatedEntityId = entity[relationProperty.targetIdColumnName];
3740
+ if (relatedEntityId) {
3741
+ await deleteEntityById(server, relationDataAccessor, {
3742
+ routeContext,
3743
+ id: relatedEntityId,
3744
+ }, plugin);
3745
+ }
3746
+ }
3747
+ else if (relationProperty.relation === "many") {
3748
+ if (relationProperty.linkTableName) {
3749
+ const targetLinks = await server.queryDatabaseObject(`SELECT ${server.queryBuilder.quoteObject(relationProperty.targetIdColumnName)} FROM ${server.queryBuilder.quoteTable({
3750
+ schema: relationProperty.linkSchema,
3751
+ tableName: relationProperty.linkTableName,
3752
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName)} = $1`, [id], routeContext?.getDbTransactionClient());
3753
+ const targetEntityIds = targetLinks.map((item) => item[relationProperty.targetIdColumnName]);
3754
+ await server.queryDatabaseObject(`DELETE FROM ${server.queryBuilder.quoteTable({
3755
+ schema: relationProperty.linkSchema,
3756
+ tableName: relationProperty.linkTableName,
3757
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName)} = $1`, [id], routeContext?.getDbTransactionClient());
3758
+ for (const targetEntityId of targetEntityIds) {
3759
+ await deleteEntityById(server, relationDataAccessor, {
3760
+ routeContext,
3761
+ id: targetEntityId,
3762
+ }, plugin);
3763
+ }
3764
+ }
3765
+ else {
3766
+ const targetModel = server.getModel({
3767
+ singularCode: relationProperty.targetSingularCode,
3768
+ });
3769
+ const targetRows = await server.queryDatabaseObject(`SELECT id FROM ${server.queryBuilder.quoteTable({
3770
+ schema: targetModel.schema,
3771
+ tableName: targetModel.tableName,
3772
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName)} = $1`, [id], routeContext?.getDbTransactionClient());
3773
+ const targetEntityIds = targetRows.map((item) => item.id);
3774
+ for (const targetEntityId of targetEntityIds) {
3775
+ await deleteEntityById(server, relationDataAccessor, {
3776
+ routeContext,
3777
+ id: targetEntityId,
3778
+ }, plugin);
3779
+ }
3780
+ }
3781
+ }
3782
+ }
3783
+ else if (relationProperty.entityDeletingReaction === "unlink") {
3784
+ if (relationProperty.relation === "one") ;
3785
+ else if (relationProperty.relation === "many") {
3786
+ if (relationProperty.linkTableName) {
3787
+ await server.queryDatabaseObject(`DELETE FROM ${server.queryBuilder.quoteTable({
3788
+ schema: relationProperty.linkSchema,
3789
+ tableName: relationProperty.linkTableName,
3790
+ })}
3791
+ WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName)} = $1`, [id], routeContext?.getDbTransactionClient());
3792
+ }
3793
+ else {
3794
+ const relationModel = server.getModel({
3795
+ singularCode: relationProperty.targetSingularCode,
3796
+ });
3797
+ await server.queryDatabaseObject(`UPDATE ${server.queryBuilder.quoteTable({
3798
+ schema: relationModel.schema,
3799
+ tableName: relationModel.tableName,
3800
+ })}
3801
+ SET ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName)} = null
3802
+ WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName)} = $1`, [id], routeContext?.getDbTransactionClient());
3803
+ }
3804
+ }
3805
+ }
3806
+ }
3807
+ await dataAccessor.deleteById(id, routeContext?.getDbTransactionClient());
3808
+ if (model.base) {
3809
+ const baseDataAccessor = server.getDataAccessor({
3810
+ singularCode: model.base,
3811
+ });
3812
+ await baseDataAccessor.deleteById(id, routeContext?.getDbTransactionClient());
3813
+ }
3814
+ }
3815
+ await server.emitEvent({
3816
+ eventName: "entity.delete",
3817
+ payload: {
3818
+ namespace: model.namespace,
3819
+ modelSingularCode: model.singularCode,
3820
+ before: entity,
3821
+ },
3822
+ sender: plugin,
3823
+ routeContext,
3824
+ });
3825
+ }
3654
3826
  class EntityManager {
3655
3827
  #server;
3656
3828
  #dataAccessor;
@@ -3697,66 +3869,7 @@ class EntityManager {
3697
3869
  return await this.#dataAccessor.count(countRowOptions, routeContext?.getDbTransactionClient());
3698
3870
  }
3699
3871
  async deleteById(options, plugin) {
3700
- // options is id
3701
- if (!lodash.isObject(options)) {
3702
- options = {
3703
- id: options,
3704
- };
3705
- }
3706
- const model = this.getModel();
3707
- if (model.derivedTypePropertyCode) {
3708
- throw newEntityOperationError("Delete base entity directly is not allowed.");
3709
- }
3710
- const { id, routeContext } = options;
3711
- const entity = await this.findById({
3712
- id,
3713
- keepNonPropertyFields: true,
3714
- routeContext,
3715
- });
3716
- if (!entity) {
3717
- return;
3718
- }
3719
- await this.#server.emitEvent({
3720
- eventName: "entity.beforeDelete",
3721
- payload: {
3722
- namespace: model.namespace,
3723
- modelSingularCode: model.singularCode,
3724
- before: entity,
3725
- },
3726
- sender: plugin,
3727
- routeContext,
3728
- });
3729
- if (model.softDelete) {
3730
- let dataAccessor = model.base
3731
- ? this.#server.getDataAccessor({
3732
- singularCode: model.base,
3733
- })
3734
- : this.#dataAccessor;
3735
- const currentUserId = routeContext?.state?.userId;
3736
- await dataAccessor.updateById(id, {
3737
- deleted_at: getNowStringWithTimezone(),
3738
- deleter_id: currentUserId,
3739
- }, routeContext?.getDbTransactionClient());
3740
- }
3741
- else {
3742
- await this.#dataAccessor.deleteById(id, routeContext?.getDbTransactionClient());
3743
- if (model.base) {
3744
- const baseDataAccessor = this.#server.getDataAccessor({
3745
- singularCode: model.base,
3746
- });
3747
- await baseDataAccessor.deleteById(id, routeContext?.getDbTransactionClient());
3748
- }
3749
- }
3750
- await this.#server.emitEvent({
3751
- eventName: "entity.delete",
3752
- payload: {
3753
- namespace: model.namespace,
3754
- modelSingularCode: model.singularCode,
3755
- before: entity,
3756
- },
3757
- sender: plugin,
3758
- routeContext,
3759
- });
3872
+ return await deleteEntityById(this.#server, this.#dataAccessor, options, plugin);
3760
3873
  }
3761
3874
  async addRelations(options, plugin) {
3762
3875
  const server = this.#server;
@@ -3789,7 +3902,7 @@ class EntityManager {
3789
3902
  WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName)}=$2
3790
3903
  )`;
3791
3904
  const params = [id, relation.id];
3792
- await server.queryDatabaseObject(command, params);
3905
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3793
3906
  }
3794
3907
  }
3795
3908
  await server.emitEvent({
@@ -3829,7 +3942,7 @@ class EntityManager {
3829
3942
  const command = `DELETE FROM ${queryBuilder.quoteTable({ schema: relationProperty.linkSchema, tableName: relationProperty.linkTableName })}
3830
3943
  WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName)}=$2;`;
3831
3944
  const params = [id, relation.id];
3832
- await server.queryDatabaseObject(command, params);
3945
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3833
3946
  }
3834
3947
  }
3835
3948
  await server.emitEvent({
@@ -4136,18 +4249,18 @@ class RapidServer {
4136
4249
  }
4137
4250
  return await factory.createFacility(this, options);
4138
4251
  }
4139
- async queryDatabaseObject(sql, params) {
4252
+ async queryDatabaseObject(sql, params, client) {
4140
4253
  try {
4141
- return await this.#databaseAccessor.queryDatabaseObject(sql, params);
4254
+ return await this.#databaseAccessor.queryDatabaseObject(sql, params, client);
4142
4255
  }
4143
4256
  catch (err) {
4144
4257
  this.#logger.error("Failed to query database object.", { errorMessage: err.message, sql, params });
4145
4258
  throw err;
4146
4259
  }
4147
4260
  }
4148
- async tryQueryDatabaseObject(sql, params) {
4261
+ async tryQueryDatabaseObject(sql, params, client) {
4149
4262
  try {
4150
- return await this.queryDatabaseObject(sql, params);
4263
+ return await this.queryDatabaseObject(sql, params, client);
4151
4264
  }
4152
4265
  catch (err) {
4153
4266
  this.#logger.error("Failed to query database object.", { errorMessage: err.message, sql, params });
@@ -4161,19 +4274,27 @@ class RapidServer {
4161
4274
  const rapidRequest = new RapidRequest(this, request);
4162
4275
  await rapidRequest.parseBody();
4163
4276
  const routeContext = new RouteContext(this, rapidRequest);
4277
+ const { response } = routeContext;
4164
4278
  try {
4165
4279
  await this.#pluginManager.onPrepareRouteContext(routeContext);
4166
4280
  await this.#buildedRoutes(routeContext, next);
4167
4281
  }
4168
4282
  catch (ex) {
4169
4283
  this.#logger.error("handle request error:", ex);
4170
- routeContext.response.json({
4284
+ response.json({
4171
4285
  error: {
4172
4286
  message: ex.message || ex,
4173
4287
  },
4174
4288
  }, 500);
4175
4289
  }
4176
- return routeContext.response.getResponse();
4290
+ if (!response.status && !response.body) {
4291
+ response.json({
4292
+ error: {
4293
+ message: "No route handler was found to handle this request.",
4294
+ },
4295
+ }, 404);
4296
+ }
4297
+ return response.getResponse();
4177
4298
  }
4178
4299
  async beforeRunRouteActions(handlerContext) {
4179
4300
  await this.#pluginManager.beforeRunRouteActions(handlerContext);
@@ -4989,7 +5110,13 @@ async function handler$q(plugin, ctx, options) {
4989
5110
  routeContext,
4990
5111
  });
4991
5112
  if (!entity) {
4992
- throw new Error(`${options.namespace}.${options.singularCode} with id "${id}" was not found.`);
5113
+ ctx.routerContext.response.json({
5114
+ error: {
5115
+ message: `${options.namespace}.${options.singularCode} with id "${id}" was not found.`,
5116
+ },
5117
+ }, 404);
5118
+ // routerContext.json() function will not be called if the ctx.output is null or undefined.
5119
+ return;
4993
5120
  }
4994
5121
  return entity;
4995
5122
  });
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { GetDataAccessorOptions, GetModelOptions, IDatabaseAccessor, IDatabaseConfig, IQueryBuilder, IRpdDataAccessor, RpdApplicationConfig, RpdDataModel, RpdServerEventTypes, RapidServerConfig, RpdDataModelProperty, CreateEntityOptions, UpdateEntityByIdOptions, EntityWatcherType, EmitServerEventOptions } from "./types";
1
+ import { GetDataAccessorOptions, GetModelOptions, IDatabaseAccessor, IDatabaseConfig, IQueryBuilder, IRpdDataAccessor, RpdApplicationConfig, RpdDataModel, RpdServerEventTypes, RapidServerConfig, RpdDataModelProperty, CreateEntityOptions, UpdateEntityByIdOptions, EntityWatcherType, EmitServerEventOptions, IDatabaseClient } from "./types";
2
2
  import { ActionHandler, ActionHandlerContext, IPluginActionHandler } from "./core/actionHandler";
3
3
  import { IRpdServer, RapidPlugin } from "./core/server";
4
4
  import { Next } from "./core/routeContext";
@@ -41,8 +41,8 @@ export declare class RapidServer implements IRpdServer {
41
41
  configureApplication(): Promise<void>;
42
42
  registerFacilityFactory(factory: FacilityFactory): void;
43
43
  getFacility<TFacility = any>(name: string, options?: any, nullIfUnknownFacility?: boolean): Promise<TFacility>;
44
- queryDatabaseObject(sql: string, params?: unknown[] | Record<string, unknown>): Promise<any[]>;
45
- tryQueryDatabaseObject(sql: string, params?: unknown[] | Record<string, unknown>): Promise<any[]>;
44
+ queryDatabaseObject(sql: string, params?: unknown[] | Record<string, unknown>, client?: IDatabaseClient): Promise<any[]>;
45
+ tryQueryDatabaseObject(sql: string, params?: unknown[] | Record<string, unknown>, client?: IDatabaseClient): Promise<any[]>;
46
46
  get middlewares(): any[];
47
47
  handleRequest(request: Request, next: Next): Promise<Response>;
48
48
  beforeRunRouteActions(handlerContext: ActionHandlerContext): Promise<void>;
package/dist/types.d.ts CHANGED
@@ -282,12 +282,17 @@ export interface RpdDataModelProperty {
282
282
  * 当设置了 linkTableName 时,可以设置关联关系表所在的 Schema。
283
283
  */
284
284
  linkSchema?: string;
285
+ /**
286
+ * 当删除实体时,针对关系属性的联动处理。
287
+ */
288
+ entityDeletingReaction?: RpdEntityDeleteRelationPropertyReaction;
285
289
  /**
286
290
  * 当设置为`true`时,仅允许在创建时设置此属性的值,不允许更新。
287
291
  */
288
292
  readonly?: boolean;
289
293
  }
290
294
  export type RpdDataPropertyTypes = "integer" | "long" | "float" | "double" | "decimal" | "text" | "boolean" | "date" | "time" | "datetime" | "json" | "relation" | "relation[]" | "option" | "option[]" | "file" | "file[]" | "image" | "image[]";
295
+ export type RpdEntityDeleteRelationPropertyReaction = "doNothing" | "unlink" | "cascadingDelete";
291
296
  /**
292
297
  * 数据字典
293
298
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruiapp/rapid-core",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -237,6 +237,13 @@ export default {
237
237
  type: "text",
238
238
  required: false,
239
239
  },
240
+ {
241
+ name: "entityDeletingReaction",
242
+ code: "entityDeletingReaction",
243
+ columnName: "entity_deleting_reaction",
244
+ type: "text",
245
+ required: false,
246
+ },
240
247
  {
241
248
  name: "readonly",
242
249
  code: "readonly",
@@ -15,7 +15,6 @@ function mergeHeaders(target: Headers, source: HeadersInit) {
15
15
  } else if (isObject(source)) {
16
16
  Object.entries(source).forEach(([key, value]) => target.set(key, value));
17
17
  }
18
- return target;
19
18
  }
20
19
 
21
20
  interface NewResponseOptions {
@@ -32,6 +31,7 @@ function newResponse(options: NewResponseOptions) {
32
31
  }
33
32
 
34
33
  export class RapidResponse {
34
+ // TODO: remove this field.
35
35
  #response: Response;
36
36
  status: number;
37
37
  body: BodyInit;
@@ -53,14 +53,17 @@ export class RapidResponse {
53
53
  if (headers) {
54
54
  mergeHeaders(responseHeaders, headers);
55
55
  }
56
- this.#response = newResponse({ body, status: status || 200, headers: responseHeaders });
56
+ this.status = status || 200;
57
+ this.body = body;
58
+ this.#response = newResponse({ body, status: this.status, headers: responseHeaders });
57
59
  }
58
60
 
59
61
  redirect(location: string, status?: HttpStatus) {
60
62
  this.headers.set("Location", location);
63
+ this.status = status || 302;
61
64
  this.#response = newResponse({
62
65
  headers: this.headers,
63
- status: status || 302,
66
+ status: this.status,
64
67
  });
65
68
  }
66
69
 
@@ -5,6 +5,7 @@ import {
5
5
  GetDataAccessorOptions,
6
6
  GetModelOptions,
7
7
  IDatabaseAccessor,
8
+ IDatabaseClient,
8
9
  IDatabaseConfig,
9
10
  IQueryBuilder,
10
11
  IRpdDataAccessor,
@@ -32,8 +33,8 @@ export interface IRpdServer {
32
33
  getFacility<TFacility = any>(name: string, options?: any): Promise<TFacility>;
33
34
 
34
35
  getDatabaseAccessor(): IDatabaseAccessor;
35
- queryDatabaseObject: (sql: string, params?: unknown[] | Record<string, unknown>) => Promise<any[]>;
36
- tryQueryDatabaseObject: (sql: string, params?: unknown[] | Record<string, unknown>) => Promise<any[]>;
36
+ queryDatabaseObject: (sql: string, params?: unknown[] | Record<string, unknown>, client?: IDatabaseClient) => Promise<any[]>;
37
+ tryQueryDatabaseObject: (sql: string, params?: unknown[] | Record<string, unknown>, client?: IDatabaseClient) => Promise<any[]>;
37
38
  registerMiddleware(middleware: any): void;
38
39
  registerActionHandler(plugin: RapidPlugin, options: IPluginActionHandler): void;
39
40
  getActionHandlerByCode(code: string): ActionHandler | undefined;
@@ -45,6 +45,7 @@ import {
45
45
  uniq,
46
46
  } from "lodash";
47
47
  import {
48
+ getEntityProperties,
48
49
  getEntityPropertiesIncludingBase,
49
50
  getEntityProperty,
50
51
  getEntityPropertyByCode,
@@ -60,6 +61,7 @@ import { RouteContext } from "~/core/routeContext";
60
61
 
61
62
  export type FindOneRelationEntitiesOptions = {
62
63
  server: IRpdServer;
64
+ routeContext?: RouteContext;
63
65
  mainModel: RpdDataModel;
64
66
  relationProperty: RpdDataModelProperty;
65
67
  relationEntityIds: any[];
@@ -68,6 +70,7 @@ export type FindOneRelationEntitiesOptions = {
68
70
 
69
71
  export type FindManyRelationEntitiesOptions = {
70
72
  server: IRpdServer;
73
+ routeContext?: RouteContext;
71
74
  mainModel: RpdDataModel;
72
75
  relationProperty: RpdDataModelProperty;
73
76
  mainEntityIds: any[];
@@ -269,6 +272,7 @@ async function findEntities(server: IRpdServer, dataAccessor: IRpdDataAccessor,
269
272
  if (isManyRelation) {
270
273
  const relationLinks = await findManyRelationLinksViaLinkTable({
271
274
  server,
275
+ routeContext,
272
276
  mainModel: relationModel,
273
277
  relationProperty,
274
278
  mainEntityIds: entityIds,
@@ -286,6 +290,7 @@ async function findEntities(server: IRpdServer, dataAccessor: IRpdDataAccessor,
286
290
  if (isManyRelation) {
287
291
  relatedEntities = await findManyRelatedEntitiesViaIdPropertyCode({
288
292
  server,
293
+ routeContext,
289
294
  mainModel: model,
290
295
  relationProperty,
291
296
  mainEntityIds: entityIds,
@@ -300,6 +305,7 @@ async function findEntities(server: IRpdServer, dataAccessor: IRpdDataAccessor,
300
305
  );
301
306
  relatedEntities = await findOneRelatedEntitiesViaIdPropertyCode({
302
307
  server,
308
+ routeContext,
303
309
  mainModel: model,
304
310
  relationProperty,
305
311
  relationEntityIds: targetEntityIds,
@@ -546,7 +552,7 @@ async function convertEntityFiltersToRowFilters(
546
552
  tableName: relationProperty.linkTableName!,
547
553
  })} WHERE ${server.queryBuilder.quoteObject(relationProperty.targetIdColumnName!)} = ANY($1::int[])`;
548
554
  const params = [targetEntityIds];
549
- const links = await server.queryDatabaseObject(command, params);
555
+ const links = await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
550
556
  const selfEntityIds = links.map((link) => link[relationProperty.selfIdColumnName!]);
551
557
  replacedFilters.push({
552
558
  field: {
@@ -614,7 +620,7 @@ async function convertEntityFiltersToRowFilters(
614
620
  }
615
621
 
616
622
  async function findManyRelationLinksViaLinkTable(options: FindManyRelationEntitiesOptions) {
617
- const { server, relationProperty, mainModel: relationModel, mainEntityIds, selectRelationOptions } = options;
623
+ const { server, routeContext, relationProperty, mainModel: relationModel, mainEntityIds, selectRelationOptions } = options;
618
624
  const command = `SELECT * FROM ${server.queryBuilder.quoteTable({
619
625
  schema: relationProperty.linkSchema,
620
626
  tableName: relationProperty.linkTableName!,
@@ -622,7 +628,7 @@ async function findManyRelationLinksViaLinkTable(options: FindManyRelationEntiti
622
628
  ORDER BY id
623
629
  `;
624
630
  const params = [mainEntityIds];
625
- const links = await server.queryDatabaseObject(command, params);
631
+ const links = await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
626
632
  const targetEntityIds = links.map((link) => link[relationProperty.targetIdColumnName!]);
627
633
 
628
634
  const dataAccessor = server.getDataAccessor({
@@ -960,7 +966,7 @@ async function createEntity(server: IRpdServer, dataAccessor: IRpdDataAccessor,
960
966
  tableName: property.linkTableName,
961
967
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
962
968
  const params = [newEntity.id, newTargetEntity.id];
963
- await server.queryDatabaseObject(command, params);
969
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
964
970
  }
965
971
 
966
972
  newEntity[property.code].push(newTargetEntity);
@@ -977,7 +983,7 @@ async function createEntity(server: IRpdServer, dataAccessor: IRpdDataAccessor,
977
983
  tableName: property.linkTableName,
978
984
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
979
985
  const params = [newEntity.id, relatedEntityId];
980
- await server.queryDatabaseObject(command, params);
986
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
981
987
  } else {
982
988
  await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: newEntity.id }, routeContext?.getDbTransactionClient());
983
989
  targetEntity[property.selfIdColumnName!] = newEntity.id;
@@ -998,7 +1004,7 @@ async function createEntity(server: IRpdServer, dataAccessor: IRpdDataAccessor,
998
1004
  tableName: property.linkTableName,
999
1005
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1000
1006
  const params = [newEntity.id, relatedEntityId];
1001
- await server.queryDatabaseObject(command, params);
1007
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
1002
1008
  } else {
1003
1009
  await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: newEntity.id }, routeContext?.getDbTransactionClient());
1004
1010
  targetEntity[property.selfIdColumnName!] = newEntity.id;
@@ -1298,6 +1304,7 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
1298
1304
  tableName: property.linkTableName,
1299
1305
  })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName!)} = $1`,
1300
1306
  [id],
1307
+ routeContext?.getDbTransactionClient(),
1301
1308
  );
1302
1309
  currentTargetIds = targetLinks.map((item) => item[property.targetIdColumnName]);
1303
1310
 
@@ -1308,6 +1315,7 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
1308
1315
  })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName!)} = $1
1309
1316
  AND ${server.queryBuilder.quoteObject(property.targetIdColumnName!)} <> ALL($2::int[])`,
1310
1317
  [id, targetIdsToKeep],
1318
+ routeContext?.getDbTransactionClient(),
1311
1319
  );
1312
1320
  } else {
1313
1321
  const targetModel = server.getModel({
@@ -1319,6 +1327,7 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
1319
1327
  tableName: targetModel.tableName,
1320
1328
  })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName!)} = $1`,
1321
1329
  [id],
1330
+ routeContext?.getDbTransactionClient(),
1322
1331
  );
1323
1332
  currentTargetIds = targetRows.map((item) => item.id);
1324
1333
  }
@@ -1344,7 +1353,7 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
1344
1353
  tableName: property.linkTableName,
1345
1354
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1346
1355
  const params = [id, newTargetEntity.id];
1347
- await server.queryDatabaseObject(command, params);
1356
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
1348
1357
  }
1349
1358
 
1350
1359
  relatedEntities.push(newTargetEntity);
@@ -1386,7 +1395,7 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
1386
1395
  tableName: property.linkTableName,
1387
1396
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1388
1397
  const params = [id, relatedEntityId];
1389
- await server.queryDatabaseObject(command, params);
1398
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
1390
1399
  } else {
1391
1400
  await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: id }, routeContext?.getDbTransactionClient());
1392
1401
  targetEntity[property.selfIdColumnName!] = id;
@@ -1409,7 +1418,7 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
1409
1418
  tableName: property.linkTableName,
1410
1419
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1411
1420
  const params = [id, relatedEntityId];
1412
- await server.queryDatabaseObject(command, params);
1421
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
1413
1422
  } else {
1414
1423
  await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: id }, routeContext?.getDbTransactionClient());
1415
1424
  targetEntity[property.selfIdColumnName!] = id;
@@ -1508,6 +1517,199 @@ function getEntityDuplicatedErrorMessage(server: IRpdServer, model: RpdDataModel
1508
1517
  return `已存在 ${propertyNames.join(", ")} 相同的记录。`;
1509
1518
  }
1510
1519
 
1520
+ async function deleteEntityById(
1521
+ server: IRpdServer,
1522
+ dataAccessor: IRpdDataAccessor,
1523
+ options: DeleteEntityByIdOptions | string | number,
1524
+ plugin?: RapidPlugin,
1525
+ ): Promise<void> {
1526
+ // options is id
1527
+ if (!isObject(options)) {
1528
+ options = {
1529
+ id: options,
1530
+ };
1531
+ }
1532
+
1533
+ const model = dataAccessor.getModel();
1534
+ if (model.derivedTypePropertyCode) {
1535
+ // TODO: should be allowed.
1536
+ throw newEntityOperationError("Delete base entity directly is not allowed.");
1537
+ }
1538
+
1539
+ const { id, routeContext } = options;
1540
+
1541
+ const entity = await findById(server, dataAccessor, {
1542
+ id,
1543
+ keepNonPropertyFields: true,
1544
+ routeContext,
1545
+ });
1546
+
1547
+ if (!entity) {
1548
+ return;
1549
+ }
1550
+
1551
+ if (model.softDelete) {
1552
+ if (entity.deletedAt) {
1553
+ return;
1554
+ }
1555
+ }
1556
+
1557
+ await server.emitEvent({
1558
+ eventName: "entity.beforeDelete",
1559
+ payload: {
1560
+ namespace: model.namespace,
1561
+ modelSingularCode: model.singularCode,
1562
+ before: entity,
1563
+ },
1564
+ sender: plugin,
1565
+ routeContext,
1566
+ });
1567
+
1568
+ if (model.softDelete) {
1569
+ const currentUserId = routeContext?.state?.userId;
1570
+ await dataAccessor.updateById(
1571
+ id,
1572
+ {
1573
+ deleted_at: getNowStringWithTimezone(),
1574
+ deleter_id: currentUserId,
1575
+ },
1576
+ routeContext?.getDbTransactionClient(),
1577
+ );
1578
+ } else {
1579
+ const relationPropertiesWithDeletingReaction = getEntityPropertiesIncludingBase(server, model, (property) => {
1580
+ return isRelationProperty(property) && property.entityDeletingReaction && property.entityDeletingReaction !== "doNothing";
1581
+ });
1582
+
1583
+ for (const relationProperty of relationPropertiesWithDeletingReaction) {
1584
+ const relationDataAccessor = server.getDataAccessor({
1585
+ singularCode: relationProperty.targetSingularCode,
1586
+ });
1587
+ if (relationProperty.entityDeletingReaction === "cascadingDelete") {
1588
+ if (relationProperty.relation === "one") {
1589
+ const relatedEntityId = entity[relationProperty.targetIdColumnName];
1590
+ if (relatedEntityId) {
1591
+ await deleteEntityById(
1592
+ server,
1593
+ relationDataAccessor,
1594
+ {
1595
+ routeContext,
1596
+ id: relatedEntityId,
1597
+ },
1598
+ plugin,
1599
+ );
1600
+ }
1601
+ } else if (relationProperty.relation === "many") {
1602
+ if (relationProperty.linkTableName) {
1603
+ const targetLinks = await server.queryDatabaseObject(
1604
+ `SELECT ${server.queryBuilder.quoteObject(relationProperty.targetIdColumnName)} FROM ${server.queryBuilder.quoteTable({
1605
+ schema: relationProperty.linkSchema,
1606
+ tableName: relationProperty.linkTableName,
1607
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = $1`,
1608
+ [id],
1609
+ routeContext?.getDbTransactionClient(),
1610
+ );
1611
+ const targetEntityIds = targetLinks.map((item) => item[relationProperty.targetIdColumnName]);
1612
+
1613
+ await server.queryDatabaseObject(
1614
+ `DELETE FROM ${server.queryBuilder.quoteTable({
1615
+ schema: relationProperty.linkSchema,
1616
+ tableName: relationProperty.linkTableName,
1617
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = $1`,
1618
+ [id],
1619
+ routeContext?.getDbTransactionClient(),
1620
+ );
1621
+
1622
+ for (const targetEntityId of targetEntityIds) {
1623
+ await deleteEntityById(
1624
+ server,
1625
+ relationDataAccessor,
1626
+ {
1627
+ routeContext,
1628
+ id: targetEntityId,
1629
+ },
1630
+ plugin,
1631
+ );
1632
+ }
1633
+ } else {
1634
+ const targetModel = server.getModel({
1635
+ singularCode: relationProperty.targetSingularCode,
1636
+ });
1637
+ const targetRows = await server.queryDatabaseObject(
1638
+ `SELECT id FROM ${server.queryBuilder.quoteTable({
1639
+ schema: targetModel.schema,
1640
+ tableName: targetModel.tableName,
1641
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = $1`,
1642
+ [id],
1643
+ routeContext?.getDbTransactionClient(),
1644
+ );
1645
+ const targetEntityIds = targetRows.map((item) => item.id);
1646
+ for (const targetEntityId of targetEntityIds) {
1647
+ await deleteEntityById(
1648
+ server,
1649
+ relationDataAccessor,
1650
+ {
1651
+ routeContext,
1652
+ id: targetEntityId,
1653
+ },
1654
+ plugin,
1655
+ );
1656
+ }
1657
+ }
1658
+ }
1659
+ } else if (relationProperty.entityDeletingReaction === "unlink") {
1660
+ if (relationProperty.relation === "one") {
1661
+ // do nothing, entity will be deleted later.
1662
+ } else if (relationProperty.relation === "many") {
1663
+ if (relationProperty.linkTableName) {
1664
+ await server.queryDatabaseObject(
1665
+ `DELETE FROM ${server.queryBuilder.quoteTable({
1666
+ schema: relationProperty.linkSchema,
1667
+ tableName: relationProperty.linkTableName,
1668
+ })}
1669
+ WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = $1`,
1670
+ [id],
1671
+ routeContext?.getDbTransactionClient(),
1672
+ );
1673
+ } else {
1674
+ const relationModel = server.getModel({
1675
+ singularCode: relationProperty.targetSingularCode,
1676
+ });
1677
+ await server.queryDatabaseObject(
1678
+ `UPDATE ${server.queryBuilder.quoteTable({
1679
+ schema: relationModel.schema,
1680
+ tableName: relationModel.tableName,
1681
+ })}
1682
+ SET ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = null
1683
+ WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = $1`,
1684
+ [id],
1685
+ routeContext?.getDbTransactionClient(),
1686
+ );
1687
+ }
1688
+ }
1689
+ }
1690
+ }
1691
+
1692
+ await dataAccessor.deleteById(id, routeContext?.getDbTransactionClient());
1693
+ if (model.base) {
1694
+ const baseDataAccessor = server.getDataAccessor({
1695
+ singularCode: model.base,
1696
+ });
1697
+ await baseDataAccessor.deleteById(id, routeContext?.getDbTransactionClient());
1698
+ }
1699
+ }
1700
+
1701
+ await server.emitEvent({
1702
+ eventName: "entity.delete",
1703
+ payload: {
1704
+ namespace: model.namespace,
1705
+ modelSingularCode: model.singularCode,
1706
+ before: entity,
1707
+ },
1708
+ sender: plugin,
1709
+ routeContext,
1710
+ });
1711
+ }
1712
+
1511
1713
  export default class EntityManager<TEntity = any> {
1512
1714
  #server: IRpdServer;
1513
1715
  #dataAccessor: IRpdDataAccessor;
@@ -1563,76 +1765,7 @@ export default class EntityManager<TEntity = any> {
1563
1765
  }
1564
1766
 
1565
1767
  async deleteById(options: DeleteEntityByIdOptions | string | number, plugin?: RapidPlugin): Promise<void> {
1566
- // options is id
1567
- if (!isObject(options)) {
1568
- options = {
1569
- id: options,
1570
- };
1571
- }
1572
-
1573
- const model = this.getModel();
1574
- if (model.derivedTypePropertyCode) {
1575
- throw newEntityOperationError("Delete base entity directly is not allowed.");
1576
- }
1577
-
1578
- const { id, routeContext } = options;
1579
-
1580
- const entity = await this.findById({
1581
- id,
1582
- keepNonPropertyFields: true,
1583
- routeContext,
1584
- });
1585
-
1586
- if (!entity) {
1587
- return;
1588
- }
1589
-
1590
- await this.#server.emitEvent({
1591
- eventName: "entity.beforeDelete",
1592
- payload: {
1593
- namespace: model.namespace,
1594
- modelSingularCode: model.singularCode,
1595
- before: entity,
1596
- },
1597
- sender: plugin,
1598
- routeContext,
1599
- });
1600
-
1601
- if (model.softDelete) {
1602
- let dataAccessor = model.base
1603
- ? this.#server.getDataAccessor({
1604
- singularCode: model.base,
1605
- })
1606
- : this.#dataAccessor;
1607
- const currentUserId = routeContext?.state?.userId;
1608
- await dataAccessor.updateById(
1609
- id,
1610
- {
1611
- deleted_at: getNowStringWithTimezone(),
1612
- deleter_id: currentUserId,
1613
- },
1614
- routeContext?.getDbTransactionClient(),
1615
- );
1616
- } else {
1617
- await this.#dataAccessor.deleteById(id, routeContext?.getDbTransactionClient());
1618
- if (model.base) {
1619
- const baseDataAccessor = this.#server.getDataAccessor({
1620
- singularCode: model.base,
1621
- });
1622
- await baseDataAccessor.deleteById(id, routeContext?.getDbTransactionClient());
1623
- }
1624
- }
1625
-
1626
- await this.#server.emitEvent({
1627
- eventName: "entity.delete",
1628
- payload: {
1629
- namespace: model.namespace,
1630
- modelSingularCode: model.singularCode,
1631
- before: entity,
1632
- },
1633
- sender: plugin,
1634
- routeContext,
1635
- });
1768
+ return await deleteEntityById(this.#server, this.#dataAccessor, options, plugin);
1636
1769
  }
1637
1770
 
1638
1771
  async addRelations(options: AddEntityRelationsOptions, plugin?: RapidPlugin): Promise<void> {
@@ -1669,7 +1802,7 @@ export default class EntityManager<TEntity = any> {
1669
1802
  WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}=$2
1670
1803
  )`;
1671
1804
  const params = [id, relation.id];
1672
- await server.queryDatabaseObject(command, params);
1805
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
1673
1806
  }
1674
1807
  }
1675
1808
 
@@ -1714,7 +1847,7 @@ export default class EntityManager<TEntity = any> {
1714
1847
  const command = `DELETE FROM ${queryBuilder.quoteTable({ schema: relationProperty.linkSchema, tableName: relationProperty.linkTableName })}
1715
1848
  WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}=$2;`;
1716
1849
  const params = [id, relation.id];
1717
- await server.queryDatabaseObject(command, params);
1850
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
1718
1851
  }
1719
1852
  }
1720
1853
 
@@ -1,4 +1,4 @@
1
- import { cloneDeep } from "lodash";
1
+ import { cloneDeep, filter, find } from "lodash";
2
2
  import { IRpdServer } from "~/core/server";
3
3
  import { RpdDataModel, RpdDataModelProperty } from "~/types";
4
4
 
@@ -14,13 +14,17 @@ export function isManyRelationProperty(property: RpdDataModelProperty) {
14
14
  return isRelationProperty(property) && property.relation === "many";
15
15
  }
16
16
 
17
- export function getEntityProperties(server: IRpdServer, model: RpdDataModel) {
18
- return model.properties;
17
+ export function getEntityProperties(server: IRpdServer, model: RpdDataModel, predicate?: (item: RpdDataModelProperty) => boolean) {
18
+ if (!predicate) {
19
+ return model.properties;
20
+ }
21
+
22
+ return filter(model.properties, predicate);
19
23
  }
20
24
 
21
- export function getEntityPropertiesIncludingBase(server: IRpdServer, model: RpdDataModel) {
25
+ export function getEntityPropertiesIncludingBase(server: IRpdServer, model: RpdDataModel, predicate?: (item: RpdDataModelProperty) => boolean) {
22
26
  if (!model.base) {
23
- return model.properties;
27
+ return getEntityProperties(server, model, predicate);
24
28
  }
25
29
 
26
30
  const baseModel = server.getModel({
@@ -28,14 +32,25 @@ export function getEntityPropertiesIncludingBase(server: IRpdServer, model: RpdD
28
32
  });
29
33
  let baseProperties: RpdDataModelProperty[] = [];
30
34
  if (baseModel) {
31
- baseProperties = baseModel.properties.map((property) => {
35
+ if (predicate) {
36
+ baseProperties = filter(baseModel.properties, predicate);
37
+ } else {
38
+ baseProperties = baseModel.properties;
39
+ }
40
+ baseProperties = baseProperties.map((property) => {
32
41
  property = cloneDeep(property);
33
42
  property.isBaseProperty = true;
34
43
  return property;
35
44
  });
36
45
  }
37
46
 
38
- return [...baseProperties, ...model.properties];
47
+ let properties: RpdDataModelProperty[];
48
+ if (predicate) {
49
+ properties = filter(model.properties, predicate);
50
+ } else {
51
+ properties = model.properties;
52
+ }
53
+ return [...baseProperties, ...properties];
39
54
  }
40
55
 
41
56
  export function getEntityPropertyByCode(server: IRpdServer, model: RpdDataModel, propertyCode: string): RpdDataModelProperty | undefined {
@@ -14,7 +14,16 @@ export async function handler(plugin: RapidPlugin, ctx: ActionHandlerContext, op
14
14
  routeContext,
15
15
  });
16
16
  if (!entity) {
17
- throw new Error(`${options.namespace}.${options.singularCode} with id "${id}" was not found.`);
17
+ ctx.routerContext.response.json(
18
+ {
19
+ error: {
20
+ message: `${options.namespace}.${options.singularCode} with id "${id}" was not found.`,
21
+ },
22
+ },
23
+ 404,
24
+ );
25
+ // routerContext.json() function will not be called if the ctx.output is null or undefined.
26
+ return;
18
27
  }
19
28
  return entity;
20
29
  });
package/src/server.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  EntityWatcherType,
18
18
  RpdEntityCreateEventPayload,
19
19
  EmitServerEventOptions,
20
+ IDatabaseClient,
20
21
  } from "./types";
21
22
 
22
23
  import QueryBuilder from "./queryBuilder/queryBuilder";
@@ -373,18 +374,18 @@ export class RapidServer implements IRpdServer {
373
374
  return await factory.createFacility(this, options);
374
375
  }
375
376
 
376
- async queryDatabaseObject(sql: string, params?: unknown[] | Record<string, unknown>): Promise<any[]> {
377
+ async queryDatabaseObject(sql: string, params?: unknown[] | Record<string, unknown>, client?: IDatabaseClient): Promise<any[]> {
377
378
  try {
378
- return await this.#databaseAccessor.queryDatabaseObject(sql, params);
379
+ return await this.#databaseAccessor.queryDatabaseObject(sql, params, client);
379
380
  } catch (err) {
380
381
  this.#logger.error("Failed to query database object.", { errorMessage: err.message, sql, params });
381
382
  throw err;
382
383
  }
383
384
  }
384
385
 
385
- async tryQueryDatabaseObject(sql: string, params?: unknown[] | Record<string, unknown>): Promise<any[]> {
386
+ async tryQueryDatabaseObject(sql: string, params?: unknown[] | Record<string, unknown>, client?: IDatabaseClient): Promise<any[]> {
386
387
  try {
387
- return await this.queryDatabaseObject(sql, params);
388
+ return await this.queryDatabaseObject(sql, params, client);
388
389
  } catch (err) {
389
390
  this.#logger.error("Failed to query database object.", { errorMessage: err.message, sql, params });
390
391
  }
@@ -400,13 +401,14 @@ export class RapidServer implements IRpdServer {
400
401
  const rapidRequest = new RapidRequest(this, request);
401
402
  await rapidRequest.parseBody();
402
403
  const routeContext: RouteContext = new RouteContext(this, rapidRequest);
404
+ const { response } = routeContext;
403
405
 
404
406
  try {
405
407
  await this.#pluginManager.onPrepareRouteContext(routeContext);
406
408
  await this.#buildedRoutes(routeContext, next);
407
409
  } catch (ex) {
408
410
  this.#logger.error("handle request error:", ex);
409
- routeContext.response.json(
411
+ response.json(
410
412
  {
411
413
  error: {
412
414
  message: ex.message || ex,
@@ -415,7 +417,18 @@ export class RapidServer implements IRpdServer {
415
417
  500,
416
418
  );
417
419
  }
418
- return routeContext.response.getResponse();
420
+
421
+ if (!response.status && !response.body) {
422
+ response.json(
423
+ {
424
+ error: {
425
+ message: "No route handler was found to handle this request.",
426
+ },
427
+ },
428
+ 404,
429
+ );
430
+ }
431
+ return response.getResponse();
419
432
  }
420
433
 
421
434
  async beforeRunRouteActions(handlerContext: ActionHandlerContext) {
package/src/types.ts CHANGED
@@ -314,6 +314,11 @@ export interface RpdDataModelProperty {
314
314
  */
315
315
  linkSchema?: string;
316
316
 
317
+ /**
318
+ * 当删除实体时,针对关系属性的联动处理。
319
+ */
320
+ entityDeletingReaction?: RpdEntityDeleteRelationPropertyReaction;
321
+
317
322
  /**
318
323
  * 当设置为`true`时,仅允许在创建时设置此属性的值,不允许更新。
319
324
  */
@@ -341,6 +346,8 @@ export type RpdDataPropertyTypes =
341
346
  | "image"
342
347
  | "image[]";
343
348
 
349
+ export type RpdEntityDeleteRelationPropertyReaction = "doNothing" | "unlink" | "cascadingDelete";
350
+
344
351
  /**
345
352
  * 数据字典
346
353
  */