@ruiapp/rapid-core 0.5.2 → 0.5.4

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
@@ -1712,6 +1712,13 @@ var bootstrapApplicationConfig = {
1712
1712
  type: "text",
1713
1713
  required: false,
1714
1714
  },
1715
+ {
1716
+ name: "entityDeletingReaction",
1717
+ code: "entityDeletingReaction",
1718
+ columnName: "entity_deleting_reaction",
1719
+ type: "text",
1720
+ required: false,
1721
+ },
1715
1722
  {
1716
1723
  name: "readonly",
1717
1724
  code: "readonly",
@@ -2119,22 +2126,41 @@ function isOneRelationProperty(property) {
2119
2126
  function isManyRelationProperty(property) {
2120
2127
  return isRelationProperty(property) && property.relation === "many";
2121
2128
  }
2122
- function getEntityPropertiesIncludingBase(server, model) {
2123
- if (!model.base) {
2129
+ function getEntityProperties(server, model, predicate) {
2130
+ if (!predicate) {
2124
2131
  return model.properties;
2125
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
+ }
2126
2139
  const baseModel = server.getModel({
2127
2140
  singularCode: model.base,
2128
2141
  });
2129
2142
  let baseProperties = [];
2130
2143
  if (baseModel) {
2131
- 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) => {
2132
2151
  property = lodash.cloneDeep(property);
2133
2152
  property.isBaseProperty = true;
2134
2153
  return property;
2135
2154
  });
2136
2155
  }
2137
- 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];
2138
2164
  }
2139
2165
  function getEntityPropertyByCode(server, model, propertyCode) {
2140
2166
  return getEntityProperty(server, model, (e) => e.code === propertyCode);
@@ -2539,6 +2565,7 @@ async function findEntities(server, dataAccessor, options) {
2539
2565
  if (isManyRelation) {
2540
2566
  const relationLinks = await findManyRelationLinksViaLinkTable({
2541
2567
  server,
2568
+ routeContext,
2542
2569
  mainModel: relationModel,
2543
2570
  relationProperty,
2544
2571
  mainEntityIds: entityIds,
@@ -2556,6 +2583,7 @@ async function findEntities(server, dataAccessor, options) {
2556
2583
  if (isManyRelation) {
2557
2584
  relatedEntities = await findManyRelatedEntitiesViaIdPropertyCode({
2558
2585
  server,
2586
+ routeContext,
2559
2587
  mainModel: model,
2560
2588
  relationProperty,
2561
2589
  mainEntityIds: entityIds,
@@ -2566,6 +2594,7 @@ async function findEntities(server, dataAccessor, options) {
2566
2594
  const targetEntityIds = lodash.uniq(lodash.reject(lodash.map(rows, (entity) => entity[relationProperty.targetIdColumnName]), isNullOrUndefined));
2567
2595
  relatedEntities = await findOneRelatedEntitiesViaIdPropertyCode({
2568
2596
  server,
2597
+ routeContext,
2569
2598
  mainModel: model,
2570
2599
  relationProperty,
2571
2600
  relationEntityIds: targetEntityIds,
@@ -2782,7 +2811,7 @@ async function convertEntityFiltersToRowFilters(routeContext, server, model, bas
2782
2811
  tableName: relationProperty.linkTableName,
2783
2812
  })} WHERE ${server.queryBuilder.quoteObject(relationProperty.targetIdColumnName)} = ANY($1::int[])`;
2784
2813
  const params = [targetEntityIds];
2785
- const links = await server.queryDatabaseObject(command, params);
2814
+ const links = await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
2786
2815
  const selfEntityIds = links.map((link) => link[relationProperty.selfIdColumnName]);
2787
2816
  replacedFilters.push({
2788
2817
  field: {
@@ -2850,7 +2879,7 @@ async function convertEntityFiltersToRowFilters(routeContext, server, model, bas
2850
2879
  return replacedFilters;
2851
2880
  }
2852
2881
  async function findManyRelationLinksViaLinkTable(options) {
2853
- const { server, relationProperty, mainModel: relationModel, mainEntityIds, selectRelationOptions } = options;
2882
+ const { server, routeContext, relationProperty, mainModel: relationModel, mainEntityIds, selectRelationOptions } = options;
2854
2883
  const command = `SELECT * FROM ${server.queryBuilder.quoteTable({
2855
2884
  schema: relationProperty.linkSchema,
2856
2885
  tableName: relationProperty.linkTableName,
@@ -2858,7 +2887,7 @@ async function findManyRelationLinksViaLinkTable(options) {
2858
2887
  ORDER BY id
2859
2888
  `;
2860
2889
  const params = [mainEntityIds];
2861
- const links = await server.queryDatabaseObject(command, params);
2890
+ const links = await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
2862
2891
  const targetEntityIds = links.map((link) => link[relationProperty.targetIdColumnName]);
2863
2892
  const dataAccessor = server.getDataAccessor({
2864
2893
  namespace: relationModel.namespace,
@@ -3160,7 +3189,7 @@ async function createEntity(server, dataAccessor, options, plugin) {
3160
3189
  tableName: property.linkTableName,
3161
3190
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
3162
3191
  const params = [newEntity.id, newTargetEntity.id];
3163
- await server.queryDatabaseObject(command, params);
3192
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3164
3193
  }
3165
3194
  newEntity[property.code].push(newTargetEntity);
3166
3195
  }
@@ -3176,7 +3205,7 @@ async function createEntity(server, dataAccessor, options, plugin) {
3176
3205
  tableName: property.linkTableName,
3177
3206
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
3178
3207
  const params = [newEntity.id, relatedEntityId];
3179
- await server.queryDatabaseObject(command, params);
3208
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3180
3209
  }
3181
3210
  else {
3182
3211
  await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName]: newEntity.id }, routeContext?.getDbTransactionClient());
@@ -3198,7 +3227,7 @@ async function createEntity(server, dataAccessor, options, plugin) {
3198
3227
  tableName: property.linkTableName,
3199
3228
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
3200
3229
  const params = [newEntity.id, relatedEntityId];
3201
- await server.queryDatabaseObject(command, params);
3230
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3202
3231
  }
3203
3232
  else {
3204
3233
  await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName]: newEntity.id }, routeContext?.getDbTransactionClient());
@@ -3441,9 +3470,12 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3441
3470
  }
3442
3471
  }
3443
3472
  }
3444
- // save many-relation properties
3473
+ // save many-relation properties (only 'overwrite' mode was supported right now)
3445
3474
  for (const property of manyRelationPropertiesToUpdate) {
3446
3475
  const relatedEntities = [];
3476
+ const targetModel = server.getModel({
3477
+ singularCode: property.targetSingularCode,
3478
+ });
3447
3479
  const targetDataAccessor = server.getDataAccessor({
3448
3480
  singularCode: property.targetSingularCode,
3449
3481
  });
@@ -3469,24 +3501,55 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3469
3501
  const targetLinks = await server.queryDatabaseObject(`SELECT ${server.queryBuilder.quoteObject(property.targetIdColumnName)} FROM ${server.queryBuilder.quoteTable({
3470
3502
  schema: property.linkSchema,
3471
3503
  tableName: property.linkTableName,
3472
- })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName)} = $1`, [id]);
3504
+ })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName)} = $1`, [id], routeContext?.getDbTransactionClient());
3473
3505
  currentTargetIds = targetLinks.map((item) => item[property.targetIdColumnName]);
3474
3506
  await server.queryDatabaseObject(`DELETE FROM ${server.queryBuilder.quoteTable({
3475
3507
  schema: property.linkSchema,
3476
3508
  tableName: property.linkTableName,
3477
3509
  })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName)} = $1
3478
- AND ${server.queryBuilder.quoteObject(property.targetIdColumnName)} <> ALL($2::int[])`, [id, targetIdsToKeep]);
3510
+ AND ${server.queryBuilder.quoteObject(property.targetIdColumnName)} <> ALL($2::int[])`, [id, targetIdsToKeep], routeContext?.getDbTransactionClient());
3479
3511
  }
3480
3512
  else {
3481
- const targetModel = server.getModel({
3482
- singularCode: property.targetSingularCode,
3483
- });
3484
3513
  const targetRows = await server.queryDatabaseObject(`SELECT id FROM ${server.queryBuilder.quoteTable({
3485
3514
  schema: targetModel.schema,
3486
3515
  tableName: targetModel.tableName,
3487
- })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName)} = $1`, [id]);
3516
+ })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName)} = $1`, [id], routeContext?.getDbTransactionClient());
3488
3517
  currentTargetIds = targetRows.map((item) => item.id);
3489
3518
  }
3519
+ const targetIdsToRemove = currentTargetIds.filter((currentId) => !targetIdsToKeep.includes(currentId));
3520
+ if (targetIdsToRemove.length) {
3521
+ if (property.linkTableName) ;
3522
+ else {
3523
+ const updateRelationPropertiesOptions = lodash.get(options.relationPropertiesToUpdate, property.code);
3524
+ let relationRemoveMode = "unlink";
3525
+ if (updateRelationPropertiesOptions === true) {
3526
+ relationRemoveMode = "delete";
3527
+ }
3528
+ else {
3529
+ relationRemoveMode = updateRelationPropertiesOptions.relationRemoveMode;
3530
+ }
3531
+ const relationModel = server.getModel({
3532
+ singularCode: property.targetSingularCode,
3533
+ });
3534
+ if (relationRemoveMode === "unlink") {
3535
+ await server.queryDatabaseObject(`UPDATE ${server.queryBuilder.quoteTable({
3536
+ schema: relationModel.schema,
3537
+ tableName: relationModel.tableName,
3538
+ })}
3539
+ SET ${server.queryBuilder.quoteObject(property.selfIdColumnName)} = null
3540
+ WHERE id = ANY($1::int[])`, [targetIdsToRemove], routeContext?.getDbTransactionClient());
3541
+ }
3542
+ else {
3543
+ // relationRemoveMode === "delete"
3544
+ for (const targetIdToRemove of targetIdsToRemove) {
3545
+ await deleteEntityById(server, targetDataAccessor, {
3546
+ id: targetIdToRemove,
3547
+ routeContext,
3548
+ }, plugin);
3549
+ }
3550
+ }
3551
+ }
3552
+ }
3490
3553
  for (const relatedEntityToBeSaved of relatedEntitiesToBeSaved) {
3491
3554
  let relatedEntityId;
3492
3555
  if (lodash.isObject(relatedEntityToBeSaved)) {
@@ -3507,7 +3570,7 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3507
3570
  tableName: property.linkTableName,
3508
3571
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
3509
3572
  const params = [id, newTargetEntity.id];
3510
- await server.queryDatabaseObject(command, params);
3573
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3511
3574
  }
3512
3575
  relatedEntities.push(newTargetEntity);
3513
3576
  }
@@ -3548,7 +3611,7 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3548
3611
  tableName: property.linkTableName,
3549
3612
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
3550
3613
  const params = [id, relatedEntityId];
3551
- await server.queryDatabaseObject(command, params);
3614
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3552
3615
  }
3553
3616
  else {
3554
3617
  await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName]: id }, routeContext?.getDbTransactionClient());
@@ -3572,7 +3635,7 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3572
3635
  tableName: property.linkTableName,
3573
3636
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
3574
3637
  const params = [id, relatedEntityId];
3575
- await server.queryDatabaseObject(command, params);
3638
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3576
3639
  }
3577
3640
  else {
3578
3641
  await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName]: id }, routeContext?.getDbTransactionClient());
@@ -3654,6 +3717,146 @@ function getEntityDuplicatedErrorMessage(server, model, indexConfig) {
3654
3717
  });
3655
3718
  return `已存在 ${propertyNames.join(", ")} 相同的记录。`;
3656
3719
  }
3720
+ async function deleteEntityById(server, dataAccessor, options, plugin) {
3721
+ // options is id
3722
+ if (!lodash.isObject(options)) {
3723
+ options = {
3724
+ id: options,
3725
+ };
3726
+ }
3727
+ const model = dataAccessor.getModel();
3728
+ if (model.derivedTypePropertyCode) {
3729
+ // TODO: should be allowed.
3730
+ throw newEntityOperationError("Delete base entity directly is not allowed.");
3731
+ }
3732
+ const { id, routeContext } = options;
3733
+ const entity = await findById(server, dataAccessor, {
3734
+ id,
3735
+ keepNonPropertyFields: true,
3736
+ routeContext,
3737
+ });
3738
+ if (!entity) {
3739
+ return;
3740
+ }
3741
+ if (model.softDelete) {
3742
+ if (entity.deletedAt) {
3743
+ return;
3744
+ }
3745
+ }
3746
+ await server.emitEvent({
3747
+ eventName: "entity.beforeDelete",
3748
+ payload: {
3749
+ namespace: model.namespace,
3750
+ modelSingularCode: model.singularCode,
3751
+ before: entity,
3752
+ },
3753
+ sender: plugin,
3754
+ routeContext,
3755
+ });
3756
+ if (model.softDelete) {
3757
+ const currentUserId = routeContext?.state?.userId;
3758
+ await dataAccessor.updateById(id, {
3759
+ deleted_at: getNowStringWithTimezone(),
3760
+ deleter_id: currentUserId,
3761
+ }, routeContext?.getDbTransactionClient());
3762
+ }
3763
+ else {
3764
+ const relationPropertiesWithDeletingReaction = getEntityPropertiesIncludingBase(server, model, (property) => {
3765
+ return isRelationProperty(property) && property.entityDeletingReaction && property.entityDeletingReaction !== "doNothing";
3766
+ });
3767
+ for (const relationProperty of relationPropertiesWithDeletingReaction) {
3768
+ const relationDataAccessor = server.getDataAccessor({
3769
+ singularCode: relationProperty.targetSingularCode,
3770
+ });
3771
+ if (relationProperty.entityDeletingReaction === "cascadingDelete") {
3772
+ if (relationProperty.relation === "one") {
3773
+ const relatedEntityId = entity[relationProperty.targetIdColumnName];
3774
+ if (relatedEntityId) {
3775
+ await deleteEntityById(server, relationDataAccessor, {
3776
+ routeContext,
3777
+ id: relatedEntityId,
3778
+ }, plugin);
3779
+ }
3780
+ }
3781
+ else if (relationProperty.relation === "many") {
3782
+ if (relationProperty.linkTableName) {
3783
+ const targetLinks = await server.queryDatabaseObject(`SELECT ${server.queryBuilder.quoteObject(relationProperty.targetIdColumnName)} FROM ${server.queryBuilder.quoteTable({
3784
+ schema: relationProperty.linkSchema,
3785
+ tableName: relationProperty.linkTableName,
3786
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName)} = $1`, [id], routeContext?.getDbTransactionClient());
3787
+ const targetEntityIds = targetLinks.map((item) => item[relationProperty.targetIdColumnName]);
3788
+ await server.queryDatabaseObject(`DELETE FROM ${server.queryBuilder.quoteTable({
3789
+ schema: relationProperty.linkSchema,
3790
+ tableName: relationProperty.linkTableName,
3791
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName)} = $1`, [id], routeContext?.getDbTransactionClient());
3792
+ for (const targetEntityId of targetEntityIds) {
3793
+ await deleteEntityById(server, relationDataAccessor, {
3794
+ routeContext,
3795
+ id: targetEntityId,
3796
+ }, plugin);
3797
+ }
3798
+ }
3799
+ else {
3800
+ const targetModel = server.getModel({
3801
+ singularCode: relationProperty.targetSingularCode,
3802
+ });
3803
+ const targetRows = await server.queryDatabaseObject(`SELECT id FROM ${server.queryBuilder.quoteTable({
3804
+ schema: targetModel.schema,
3805
+ tableName: targetModel.tableName,
3806
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName)} = $1`, [id], routeContext?.getDbTransactionClient());
3807
+ const targetEntityIds = targetRows.map((item) => item.id);
3808
+ for (const targetEntityId of targetEntityIds) {
3809
+ await deleteEntityById(server, relationDataAccessor, {
3810
+ routeContext,
3811
+ id: targetEntityId,
3812
+ }, plugin);
3813
+ }
3814
+ }
3815
+ }
3816
+ }
3817
+ else if (relationProperty.entityDeletingReaction === "unlink") {
3818
+ if (relationProperty.relation === "one") ;
3819
+ else if (relationProperty.relation === "many") {
3820
+ if (relationProperty.linkTableName) {
3821
+ await server.queryDatabaseObject(`DELETE FROM ${server.queryBuilder.quoteTable({
3822
+ schema: relationProperty.linkSchema,
3823
+ tableName: relationProperty.linkTableName,
3824
+ })}
3825
+ WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName)} = $1`, [id], routeContext?.getDbTransactionClient());
3826
+ }
3827
+ else {
3828
+ const relationModel = server.getModel({
3829
+ singularCode: relationProperty.targetSingularCode,
3830
+ });
3831
+ await server.queryDatabaseObject(`UPDATE ${server.queryBuilder.quoteTable({
3832
+ schema: relationModel.schema,
3833
+ tableName: relationModel.tableName,
3834
+ })}
3835
+ SET ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName)} = null
3836
+ WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName)} = $1`, [id], routeContext?.getDbTransactionClient());
3837
+ }
3838
+ }
3839
+ }
3840
+ }
3841
+ await dataAccessor.deleteById(id, routeContext?.getDbTransactionClient());
3842
+ if (model.base) {
3843
+ const baseDataAccessor = server.getDataAccessor({
3844
+ singularCode: model.base,
3845
+ });
3846
+ await baseDataAccessor.deleteById(id, routeContext?.getDbTransactionClient());
3847
+ }
3848
+ }
3849
+ await server.emitEvent({
3850
+ eventName: "entity.delete",
3851
+ payload: {
3852
+ namespace: model.namespace,
3853
+ modelSingularCode: model.singularCode,
3854
+ before: entity,
3855
+ },
3856
+ sender: plugin,
3857
+ routeContext,
3858
+ });
3859
+ }
3657
3860
  class EntityManager {
3658
3861
  #server;
3659
3862
  #dataAccessor;
@@ -3700,66 +3903,7 @@ class EntityManager {
3700
3903
  return await this.#dataAccessor.count(countRowOptions, routeContext?.getDbTransactionClient());
3701
3904
  }
3702
3905
  async deleteById(options, plugin) {
3703
- // options is id
3704
- if (!lodash.isObject(options)) {
3705
- options = {
3706
- id: options,
3707
- };
3708
- }
3709
- const model = this.getModel();
3710
- if (model.derivedTypePropertyCode) {
3711
- throw newEntityOperationError("Delete base entity directly is not allowed.");
3712
- }
3713
- const { id, routeContext } = options;
3714
- const entity = await this.findById({
3715
- id,
3716
- keepNonPropertyFields: true,
3717
- routeContext,
3718
- });
3719
- if (!entity) {
3720
- return;
3721
- }
3722
- await this.#server.emitEvent({
3723
- eventName: "entity.beforeDelete",
3724
- payload: {
3725
- namespace: model.namespace,
3726
- modelSingularCode: model.singularCode,
3727
- before: entity,
3728
- },
3729
- sender: plugin,
3730
- routeContext,
3731
- });
3732
- if (model.softDelete) {
3733
- let dataAccessor = model.base
3734
- ? this.#server.getDataAccessor({
3735
- singularCode: model.base,
3736
- })
3737
- : this.#dataAccessor;
3738
- const currentUserId = routeContext?.state?.userId;
3739
- await dataAccessor.updateById(id, {
3740
- deleted_at: getNowStringWithTimezone(),
3741
- deleter_id: currentUserId,
3742
- }, routeContext?.getDbTransactionClient());
3743
- }
3744
- else {
3745
- await this.#dataAccessor.deleteById(id, routeContext?.getDbTransactionClient());
3746
- if (model.base) {
3747
- const baseDataAccessor = this.#server.getDataAccessor({
3748
- singularCode: model.base,
3749
- });
3750
- await baseDataAccessor.deleteById(id, routeContext?.getDbTransactionClient());
3751
- }
3752
- }
3753
- await this.#server.emitEvent({
3754
- eventName: "entity.delete",
3755
- payload: {
3756
- namespace: model.namespace,
3757
- modelSingularCode: model.singularCode,
3758
- before: entity,
3759
- },
3760
- sender: plugin,
3761
- routeContext,
3762
- });
3906
+ return await deleteEntityById(this.#server, this.#dataAccessor, options, plugin);
3763
3907
  }
3764
3908
  async addRelations(options, plugin) {
3765
3909
  const server = this.#server;
@@ -3792,7 +3936,7 @@ class EntityManager {
3792
3936
  WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName)}=$2
3793
3937
  )`;
3794
3938
  const params = [id, relation.id];
3795
- await server.queryDatabaseObject(command, params);
3939
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3796
3940
  }
3797
3941
  }
3798
3942
  await server.emitEvent({
@@ -3832,7 +3976,7 @@ class EntityManager {
3832
3976
  const command = `DELETE FROM ${queryBuilder.quoteTable({ schema: relationProperty.linkSchema, tableName: relationProperty.linkTableName })}
3833
3977
  WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName)}=$2;`;
3834
3978
  const params = [id, relation.id];
3835
- await server.queryDatabaseObject(command, params);
3979
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
3836
3980
  }
3837
3981
  }
3838
3982
  await server.emitEvent({
@@ -4139,18 +4283,18 @@ class RapidServer {
4139
4283
  }
4140
4284
  return await factory.createFacility(this, options);
4141
4285
  }
4142
- async queryDatabaseObject(sql, params) {
4286
+ async queryDatabaseObject(sql, params, client) {
4143
4287
  try {
4144
- return await this.#databaseAccessor.queryDatabaseObject(sql, params);
4288
+ return await this.#databaseAccessor.queryDatabaseObject(sql, params, client);
4145
4289
  }
4146
4290
  catch (err) {
4147
4291
  this.#logger.error("Failed to query database object.", { errorMessage: err.message, sql, params });
4148
4292
  throw err;
4149
4293
  }
4150
4294
  }
4151
- async tryQueryDatabaseObject(sql, params) {
4295
+ async tryQueryDatabaseObject(sql, params, client) {
4152
4296
  try {
4153
- return await this.queryDatabaseObject(sql, params);
4297
+ return await this.queryDatabaseObject(sql, params, client);
4154
4298
  }
4155
4299
  catch (err) {
4156
4300
  this.#logger.error("Failed to query database object.", { errorMessage: err.message, sql, params });
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
  */
@@ -509,11 +514,15 @@ export interface UpdateEntityByIdOptions {
509
514
  /**
510
515
  * 指定需要更新关联对象的哪些属性。更新实体时,会创建关联对象,但是默认不更新关联对象的属性。
511
516
  */
512
- relationPropertiesToUpdate?: Record<string, UpdateRelationPropertiesOptions>;
517
+ relationPropertiesToUpdate?: Record<string, UpdateRelationPropertyOptions>;
513
518
  }
514
- export type UpdateRelationPropertiesOptions = true | {
519
+ export type UpdateRelationPropertyOptions = true | {
520
+ /**
521
+ * 当需移除关系时是删除关联实体,还是取消关联。默认为`delete`。此配置仅对没有配置`linkTableName`的属性有效。
522
+ */
523
+ relationRemoveMode?: "unlink" | "delete";
515
524
  propertiesToUpdate?: string[];
516
- relationPropertiesToUpdate?: Record<string, UpdateRelationPropertiesOptions>;
525
+ relationPropertiesToUpdate?: Record<string, UpdateRelationPropertyOptions>;
517
526
  };
518
527
  export interface DeleteEntityOptions {
519
528
  routeContext?: RouteContext;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruiapp/rapid-core",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
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",
@@ -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;
@@ -1265,9 +1271,12 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
1265
1271
  }
1266
1272
  }
1267
1273
 
1268
- // save many-relation properties
1274
+ // save many-relation properties (only 'overwrite' mode was supported right now)
1269
1275
  for (const property of manyRelationPropertiesToUpdate) {
1270
1276
  const relatedEntities: any[] = [];
1277
+ const targetModel = server.getModel({
1278
+ singularCode: property.targetSingularCode,
1279
+ });
1271
1280
  const targetDataAccessor = server.getDataAccessor({
1272
1281
  singularCode: property.targetSingularCode!,
1273
1282
  });
@@ -1298,6 +1307,7 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
1298
1307
  tableName: property.linkTableName,
1299
1308
  })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName!)} = $1`,
1300
1309
  [id],
1310
+ routeContext?.getDbTransactionClient(),
1301
1311
  );
1302
1312
  currentTargetIds = targetLinks.map((item) => item[property.targetIdColumnName]);
1303
1313
 
@@ -1308,21 +1318,63 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
1308
1318
  })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName!)} = $1
1309
1319
  AND ${server.queryBuilder.quoteObject(property.targetIdColumnName!)} <> ALL($2::int[])`,
1310
1320
  [id, targetIdsToKeep],
1321
+ routeContext?.getDbTransactionClient(),
1311
1322
  );
1312
1323
  } else {
1313
- const targetModel = server.getModel({
1314
- singularCode: property.targetSingularCode,
1315
- });
1316
1324
  const targetRows = await server.queryDatabaseObject(
1317
1325
  `SELECT id FROM ${server.queryBuilder.quoteTable({
1318
1326
  schema: targetModel.schema,
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
  }
1325
1334
 
1335
+ const targetIdsToRemove = currentTargetIds.filter((currentId) => !targetIdsToKeep.includes(currentId));
1336
+ if (targetIdsToRemove.length) {
1337
+ if (property.linkTableName) {
1338
+ // do nothing. we've remove the link rows before.
1339
+ } else {
1340
+ const updateRelationPropertiesOptions = get(options.relationPropertiesToUpdate, property.code);
1341
+ let relationRemoveMode: "unlink" | "delete" = "unlink";
1342
+ if (updateRelationPropertiesOptions === true) {
1343
+ relationRemoveMode = "delete";
1344
+ } else {
1345
+ relationRemoveMode = updateRelationPropertiesOptions.relationRemoveMode;
1346
+ }
1347
+ const relationModel = server.getModel({
1348
+ singularCode: property.targetSingularCode,
1349
+ });
1350
+ if (relationRemoveMode === "unlink") {
1351
+ await server.queryDatabaseObject(
1352
+ `UPDATE ${server.queryBuilder.quoteTable({
1353
+ schema: relationModel.schema,
1354
+ tableName: relationModel.tableName,
1355
+ })}
1356
+ SET ${server.queryBuilder.quoteObject(property.selfIdColumnName!)} = null
1357
+ WHERE id = ANY($1::int[])`,
1358
+ [targetIdsToRemove],
1359
+ routeContext?.getDbTransactionClient(),
1360
+ );
1361
+ } else {
1362
+ // relationRemoveMode === "delete"
1363
+ for (const targetIdToRemove of targetIdsToRemove) {
1364
+ await deleteEntityById(
1365
+ server,
1366
+ targetDataAccessor,
1367
+ {
1368
+ id: targetIdToRemove,
1369
+ routeContext,
1370
+ },
1371
+ plugin,
1372
+ );
1373
+ }
1374
+ }
1375
+ }
1376
+ }
1377
+
1326
1378
  for (const relatedEntityToBeSaved of relatedEntitiesToBeSaved) {
1327
1379
  let relatedEntityId: any;
1328
1380
  if (isObject(relatedEntityToBeSaved)) {
@@ -1344,7 +1396,7 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
1344
1396
  tableName: property.linkTableName,
1345
1397
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1346
1398
  const params = [id, newTargetEntity.id];
1347
- await server.queryDatabaseObject(command, params);
1399
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
1348
1400
  }
1349
1401
 
1350
1402
  relatedEntities.push(newTargetEntity);
@@ -1386,7 +1438,7 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
1386
1438
  tableName: property.linkTableName,
1387
1439
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1388
1440
  const params = [id, relatedEntityId];
1389
- await server.queryDatabaseObject(command, params);
1441
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
1390
1442
  } else {
1391
1443
  await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: id }, routeContext?.getDbTransactionClient());
1392
1444
  targetEntity[property.selfIdColumnName!] = id;
@@ -1409,7 +1461,7 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
1409
1461
  tableName: property.linkTableName,
1410
1462
  })} (${server.queryBuilder.quoteObject(property.selfIdColumnName!)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
1411
1463
  const params = [id, relatedEntityId];
1412
- await server.queryDatabaseObject(command, params);
1464
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
1413
1465
  } else {
1414
1466
  await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName!]: id }, routeContext?.getDbTransactionClient());
1415
1467
  targetEntity[property.selfIdColumnName!] = id;
@@ -1508,6 +1560,199 @@ function getEntityDuplicatedErrorMessage(server: IRpdServer, model: RpdDataModel
1508
1560
  return `已存在 ${propertyNames.join(", ")} 相同的记录。`;
1509
1561
  }
1510
1562
 
1563
+ async function deleteEntityById(
1564
+ server: IRpdServer,
1565
+ dataAccessor: IRpdDataAccessor,
1566
+ options: DeleteEntityByIdOptions | string | number,
1567
+ plugin?: RapidPlugin,
1568
+ ): Promise<void> {
1569
+ // options is id
1570
+ if (!isObject(options)) {
1571
+ options = {
1572
+ id: options,
1573
+ };
1574
+ }
1575
+
1576
+ const model = dataAccessor.getModel();
1577
+ if (model.derivedTypePropertyCode) {
1578
+ // TODO: should be allowed.
1579
+ throw newEntityOperationError("Delete base entity directly is not allowed.");
1580
+ }
1581
+
1582
+ const { id, routeContext } = options;
1583
+
1584
+ const entity = await findById(server, dataAccessor, {
1585
+ id,
1586
+ keepNonPropertyFields: true,
1587
+ routeContext,
1588
+ });
1589
+
1590
+ if (!entity) {
1591
+ return;
1592
+ }
1593
+
1594
+ if (model.softDelete) {
1595
+ if (entity.deletedAt) {
1596
+ return;
1597
+ }
1598
+ }
1599
+
1600
+ await server.emitEvent({
1601
+ eventName: "entity.beforeDelete",
1602
+ payload: {
1603
+ namespace: model.namespace,
1604
+ modelSingularCode: model.singularCode,
1605
+ before: entity,
1606
+ },
1607
+ sender: plugin,
1608
+ routeContext,
1609
+ });
1610
+
1611
+ if (model.softDelete) {
1612
+ const currentUserId = routeContext?.state?.userId;
1613
+ await dataAccessor.updateById(
1614
+ id,
1615
+ {
1616
+ deleted_at: getNowStringWithTimezone(),
1617
+ deleter_id: currentUserId,
1618
+ },
1619
+ routeContext?.getDbTransactionClient(),
1620
+ );
1621
+ } else {
1622
+ const relationPropertiesWithDeletingReaction = getEntityPropertiesIncludingBase(server, model, (property) => {
1623
+ return isRelationProperty(property) && property.entityDeletingReaction && property.entityDeletingReaction !== "doNothing";
1624
+ });
1625
+
1626
+ for (const relationProperty of relationPropertiesWithDeletingReaction) {
1627
+ const relationDataAccessor = server.getDataAccessor({
1628
+ singularCode: relationProperty.targetSingularCode,
1629
+ });
1630
+ if (relationProperty.entityDeletingReaction === "cascadingDelete") {
1631
+ if (relationProperty.relation === "one") {
1632
+ const relatedEntityId = entity[relationProperty.targetIdColumnName];
1633
+ if (relatedEntityId) {
1634
+ await deleteEntityById(
1635
+ server,
1636
+ relationDataAccessor,
1637
+ {
1638
+ routeContext,
1639
+ id: relatedEntityId,
1640
+ },
1641
+ plugin,
1642
+ );
1643
+ }
1644
+ } else if (relationProperty.relation === "many") {
1645
+ if (relationProperty.linkTableName) {
1646
+ const targetLinks = await server.queryDatabaseObject(
1647
+ `SELECT ${server.queryBuilder.quoteObject(relationProperty.targetIdColumnName)} FROM ${server.queryBuilder.quoteTable({
1648
+ schema: relationProperty.linkSchema,
1649
+ tableName: relationProperty.linkTableName,
1650
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = $1`,
1651
+ [id],
1652
+ routeContext?.getDbTransactionClient(),
1653
+ );
1654
+ const targetEntityIds = targetLinks.map((item) => item[relationProperty.targetIdColumnName]);
1655
+
1656
+ await server.queryDatabaseObject(
1657
+ `DELETE FROM ${server.queryBuilder.quoteTable({
1658
+ schema: relationProperty.linkSchema,
1659
+ tableName: relationProperty.linkTableName,
1660
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = $1`,
1661
+ [id],
1662
+ routeContext?.getDbTransactionClient(),
1663
+ );
1664
+
1665
+ for (const targetEntityId of targetEntityIds) {
1666
+ await deleteEntityById(
1667
+ server,
1668
+ relationDataAccessor,
1669
+ {
1670
+ routeContext,
1671
+ id: targetEntityId,
1672
+ },
1673
+ plugin,
1674
+ );
1675
+ }
1676
+ } else {
1677
+ const targetModel = server.getModel({
1678
+ singularCode: relationProperty.targetSingularCode,
1679
+ });
1680
+ const targetRows = await server.queryDatabaseObject(
1681
+ `SELECT id FROM ${server.queryBuilder.quoteTable({
1682
+ schema: targetModel.schema,
1683
+ tableName: targetModel.tableName,
1684
+ })} WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = $1`,
1685
+ [id],
1686
+ routeContext?.getDbTransactionClient(),
1687
+ );
1688
+ const targetEntityIds = targetRows.map((item) => item.id);
1689
+ for (const targetEntityId of targetEntityIds) {
1690
+ await deleteEntityById(
1691
+ server,
1692
+ relationDataAccessor,
1693
+ {
1694
+ routeContext,
1695
+ id: targetEntityId,
1696
+ },
1697
+ plugin,
1698
+ );
1699
+ }
1700
+ }
1701
+ }
1702
+ } else if (relationProperty.entityDeletingReaction === "unlink") {
1703
+ if (relationProperty.relation === "one") {
1704
+ // do nothing, entity will be deleted later.
1705
+ } else if (relationProperty.relation === "many") {
1706
+ if (relationProperty.linkTableName) {
1707
+ await server.queryDatabaseObject(
1708
+ `DELETE FROM ${server.queryBuilder.quoteTable({
1709
+ schema: relationProperty.linkSchema,
1710
+ tableName: relationProperty.linkTableName,
1711
+ })}
1712
+ WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = $1`,
1713
+ [id],
1714
+ routeContext?.getDbTransactionClient(),
1715
+ );
1716
+ } else {
1717
+ const relationModel = server.getModel({
1718
+ singularCode: relationProperty.targetSingularCode,
1719
+ });
1720
+ await server.queryDatabaseObject(
1721
+ `UPDATE ${server.queryBuilder.quoteTable({
1722
+ schema: relationModel.schema,
1723
+ tableName: relationModel.tableName,
1724
+ })}
1725
+ SET ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = null
1726
+ WHERE ${server.queryBuilder.quoteObject(relationProperty.selfIdColumnName!)} = $1`,
1727
+ [id],
1728
+ routeContext?.getDbTransactionClient(),
1729
+ );
1730
+ }
1731
+ }
1732
+ }
1733
+ }
1734
+
1735
+ await dataAccessor.deleteById(id, routeContext?.getDbTransactionClient());
1736
+ if (model.base) {
1737
+ const baseDataAccessor = server.getDataAccessor({
1738
+ singularCode: model.base,
1739
+ });
1740
+ await baseDataAccessor.deleteById(id, routeContext?.getDbTransactionClient());
1741
+ }
1742
+ }
1743
+
1744
+ await server.emitEvent({
1745
+ eventName: "entity.delete",
1746
+ payload: {
1747
+ namespace: model.namespace,
1748
+ modelSingularCode: model.singularCode,
1749
+ before: entity,
1750
+ },
1751
+ sender: plugin,
1752
+ routeContext,
1753
+ });
1754
+ }
1755
+
1511
1756
  export default class EntityManager<TEntity = any> {
1512
1757
  #server: IRpdServer;
1513
1758
  #dataAccessor: IRpdDataAccessor;
@@ -1563,76 +1808,7 @@ export default class EntityManager<TEntity = any> {
1563
1808
  }
1564
1809
 
1565
1810
  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
- });
1811
+ return await deleteEntityById(this.#server, this.#dataAccessor, options, plugin);
1636
1812
  }
1637
1813
 
1638
1814
  async addRelations(options: AddEntityRelationsOptions, plugin?: RapidPlugin): Promise<void> {
@@ -1669,7 +1845,7 @@ export default class EntityManager<TEntity = any> {
1669
1845
  WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}=$2
1670
1846
  )`;
1671
1847
  const params = [id, relation.id];
1672
- await server.queryDatabaseObject(command, params);
1848
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
1673
1849
  }
1674
1850
  }
1675
1851
 
@@ -1714,7 +1890,7 @@ export default class EntityManager<TEntity = any> {
1714
1890
  const command = `DELETE FROM ${queryBuilder.quoteTable({ schema: relationProperty.linkSchema, tableName: relationProperty.linkTableName })}
1715
1891
  WHERE ${queryBuilder.quoteObject(relationProperty.selfIdColumnName!)}=$1 AND ${queryBuilder.quoteObject(relationProperty.targetIdColumnName!)}=$2;`;
1716
1892
  const params = [id, relation.id];
1717
- await server.queryDatabaseObject(command, params);
1893
+ await server.queryDatabaseObject(command, params, routeContext?.getDbTransactionClient());
1718
1894
  }
1719
1895
  }
1720
1896
 
@@ -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 {
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
  }
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
  */
@@ -652,14 +659,21 @@ export interface UpdateEntityByIdOptions {
652
659
  /**
653
660
  * 指定需要更新关联对象的哪些属性。更新实体时,会创建关联对象,但是默认不更新关联对象的属性。
654
661
  */
655
- relationPropertiesToUpdate?: Record<string, UpdateRelationPropertiesOptions>;
662
+ relationPropertiesToUpdate?: Record<string, UpdateRelationPropertyOptions>;
656
663
  }
657
664
 
658
- export type UpdateRelationPropertiesOptions =
665
+ export type UpdateRelationPropertyOptions =
659
666
  | true
660
667
  | {
668
+ // TODO: impl savingMode 'patch'
669
+ // savingMode?: "overwrite" | "patch";
670
+
671
+ /**
672
+ * 当需移除关系时是删除关联实体,还是取消关联。默认为`delete`。此配置仅对没有配置`linkTableName`的属性有效。
673
+ */
674
+ relationRemoveMode?: "unlink" | "delete";
661
675
  propertiesToUpdate?: string[];
662
- relationPropertiesToUpdate?: Record<string, UpdateRelationPropertiesOptions>;
676
+ relationPropertiesToUpdate?: Record<string, UpdateRelationPropertyOptions>;
663
677
  };
664
678
 
665
679
  export interface DeleteEntityOptions {