@ruiapp/rapid-core 0.8.4 → 0.8.6

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.
@@ -0,0 +1,3 @@
1
+ import { IRpdServer } from "../core/server";
2
+ import { RpdDataModel } from "../types";
3
+ export declare function validateEntity(server: IRpdServer, model: RpdDataModel, entity: any): Promise<any>;
@@ -5,6 +5,13 @@ export declare function isOneRelationProperty(property: RpdDataModelProperty): b
5
5
  export declare function isManyRelationProperty(property: RpdDataModelProperty): boolean;
6
6
  export declare function getEntityProperties(server: IRpdServer, model: RpdDataModel, predicate?: (item: RpdDataModelProperty) => boolean): RpdDataModelProperty[];
7
7
  export declare function getEntityPropertiesIncludingBase(server: IRpdServer, model: RpdDataModel, predicate?: (item: RpdDataModelProperty) => boolean): RpdDataModelProperty[];
8
+ /**
9
+ * 根据 code 获取实体属性信息。如实体为派生实体,可能会返回基础实体中的属性信息。
10
+ * @param server
11
+ * @param model
12
+ * @param propertyCode
13
+ * @returns
14
+ */
8
15
  export declare function getEntityPropertyByCode(server: IRpdServer, model: RpdDataModel, propertyCode: string): RpdDataModelProperty | undefined;
9
16
  export declare function getEntityProperty(server: IRpdServer, model: RpdDataModel, predicate: (item: RpdDataModelProperty) => boolean): RpdDataModelProperty | undefined;
10
17
  export declare function getEntityOwnPropertyByCode(model: RpdDataModel, propertyCode: string): RpdDataModelProperty | undefined;
package/dist/index.js CHANGED
@@ -19,24 +19,6 @@ var xstate = require('xstate');
19
19
 
20
20
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
21
21
 
22
- function _interopNamespace(e) {
23
- if (e && e.__esModule) return e;
24
- var n = Object.create(null);
25
- if (e) {
26
- Object.keys(e).forEach(function (k) {
27
- if (k !== 'default') {
28
- var d = Object.getOwnPropertyDescriptor(e, k);
29
- Object.defineProperty(n, k, d.get ? d : {
30
- enumerable: true,
31
- get: function () { return e[k]; }
32
- });
33
- }
34
- });
35
- }
36
- n["default"] = e;
37
- return Object.freeze(n);
38
- }
39
-
40
22
  var Router__default = /*#__PURE__*/_interopDefaultLegacy(Router);
41
23
  var qs__default = /*#__PURE__*/_interopDefaultLegacy(qs);
42
24
  var dayjs__default = /*#__PURE__*/_interopDefaultLegacy(dayjs);
@@ -45,7 +27,6 @@ var bcrypt__default = /*#__PURE__*/_interopDefaultLegacy(bcrypt);
45
27
  var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
46
28
  var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
47
29
  var nodemailer__default = /*#__PURE__*/_interopDefaultLegacy(nodemailer);
48
- var cron__namespace = /*#__PURE__*/_interopNamespace(cron);
49
30
 
50
31
  function fixBigIntJSONSerialize() {
51
32
  BigInt.prototype.toJSON = function () {
@@ -2444,6 +2425,13 @@ function getEntityPropertiesIncludingBase(server, model, predicate) {
2444
2425
  }
2445
2426
  return [...baseProperties, ...properties];
2446
2427
  }
2428
+ /**
2429
+ * 根据 code 获取实体属性信息。如实体为派生实体,可能会返回基础实体中的属性信息。
2430
+ * @param server
2431
+ * @param model
2432
+ * @param propertyCode
2433
+ * @returns
2434
+ */
2447
2435
  function getEntityPropertyByCode(server, model, propertyCode) {
2448
2436
  return getEntityProperty(server, model, (e) => e.code === propertyCode);
2449
2437
  }
@@ -2482,6 +2470,16 @@ function getEntityPropertyByFieldName(server, model, fieldName) {
2482
2470
  return property;
2483
2471
  }
2484
2472
 
2473
+ function getNowString() {
2474
+ return dayjs__default["default"]().format("YYYY-MM-DD HH:mm:ss.SSS");
2475
+ }
2476
+ function getNowStringWithTimezone() {
2477
+ return dayjs__default["default"]().format("YYYY-MM-DD HH:mm:ss.SSSZ");
2478
+ }
2479
+ function getDateString(timeString) {
2480
+ return dayjs__default["default"](timeString).format("YYYY-MM-DD");
2481
+ }
2482
+
2485
2483
  // TODO Generate mapper and cache it.
2486
2484
  function mapDbRowToEntity(server, model, row, keepNonPropertyFields) {
2487
2485
  if (!row) {
@@ -2524,8 +2522,16 @@ function mapDbRowToEntity(server, model, row, keepNonPropertyFields) {
2524
2522
  }
2525
2523
  }
2526
2524
  else {
2525
+ let fieldValue = row[columnName];
2526
+ if (property) {
2527
+ if (property.type === "date") {
2528
+ if (fieldValue) {
2529
+ fieldValue = getDateString(fieldValue);
2530
+ }
2531
+ }
2532
+ }
2527
2533
  if (!result[propertyName]) {
2528
- result[propertyName] = row[columnName];
2534
+ result[propertyName] = fieldValue;
2529
2535
  }
2530
2536
  }
2531
2537
  });
@@ -2661,11 +2667,23 @@ function getEntityPartChanges(server, model, before, after) {
2661
2667
  return null;
2662
2668
  }
2663
2669
 
2664
- function getNowString() {
2665
- return dayjs__default["default"]().format("YYYY-MM-DD HH:mm:ss.SSS");
2666
- }
2667
- function getNowStringWithTimezone() {
2668
- return dayjs__default["default"]().format("YYYY-MM-DD HH:mm:ss.SSSZ");
2670
+ async function validateEntity(server, model, entity) {
2671
+ for (const propCode in entity) {
2672
+ let prop = getEntityPropertyByCode(server, model, propCode);
2673
+ if (!prop) {
2674
+ getEntityPropertyByFieldName(server, model, propCode);
2675
+ }
2676
+ if (!prop) {
2677
+ continue;
2678
+ }
2679
+ if (prop.type === "date") {
2680
+ const originValue = entity[propCode];
2681
+ if (originValue) {
2682
+ entity[propCode] = getDateString(originValue);
2683
+ }
2684
+ }
2685
+ }
2686
+ return entity;
2669
2687
  }
2670
2688
 
2671
2689
  function convertEntityOrderByToRowOrderBy(server, model, baseModel, orderByList) {
@@ -3305,7 +3323,9 @@ async function createEntity(server, dataAccessor, options, plugin) {
3305
3323
  if (model.derivedTypePropertyCode) {
3306
3324
  throw newEntityOperationError("Create base entity directly is not allowed.");
3307
3325
  }
3308
- const { entity, routeContext } = options;
3326
+ let { entity } = options;
3327
+ entity = await validateEntity(server, model, entity);
3328
+ const { routeContext } = options;
3309
3329
  const userId = options.routeContext?.state?.userId;
3310
3330
  if (userId) {
3311
3331
  const createdByProperty = getEntityPropertyByCode(server, model, "createdBy");
@@ -3556,6 +3576,8 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3556
3576
  if (!id) {
3557
3577
  throw new Error("Id is required when updating an entity.");
3558
3578
  }
3579
+ let { entityToSave } = options;
3580
+ entityToSave = await validateEntity(server, model, entityToSave);
3559
3581
  const entity = await findById(server, dataAccessor, {
3560
3582
  routeContext,
3561
3583
  id,
@@ -3564,7 +3586,6 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3564
3586
  if (!entity) {
3565
3587
  throw new Error(`${model.namespace}.${model.singularCode} with id "${id}" was not found.`);
3566
3588
  }
3567
- let { entityToSave } = options;
3568
3589
  let changes = getEntityPartChanges(server, model, entity, entityToSave);
3569
3590
  if (!changes && !options.operation) {
3570
3591
  return entity;
@@ -8486,11 +8507,20 @@ async function handler$1(plugin, ctx, options) {
8486
8507
  if (!input.code) {
8487
8508
  throw new Error(`Cron job code is required.`);
8488
8509
  }
8489
- const job = plugin.getJobConfigurationByCode(input.code);
8510
+ const cronJobService = server.getService("cronJobService");
8511
+ const job = cronJobService.getJobConfigurationByCode(input.code);
8490
8512
  if (!job) {
8491
8513
  throw new Error(`Cron job with code '${input.code}' was not found.`);
8492
8514
  }
8493
- await plugin.executeJob(server, job);
8515
+ let jobExecutionContext = {
8516
+ logger: server.getLogger(),
8517
+ routerContext,
8518
+ next: null,
8519
+ server,
8520
+ applicationConfig: null,
8521
+ input: input.input,
8522
+ };
8523
+ await cronJobService.executeJob(jobExecutionContext, job);
8494
8524
  response.json({});
8495
8525
  }
8496
8526
 
@@ -8518,8 +8548,98 @@ var runCronJob = {
8518
8548
 
8519
8549
  var pluginRoutes$1 = [runCronJob];
8520
8550
 
8551
+ class CronJobService {
8552
+ #server;
8553
+ #jobInstances;
8554
+ constructor(server) {
8555
+ this.#server = server;
8556
+ }
8557
+ /**
8558
+ * 根据编号获取定时任务配置信息
8559
+ * @param code
8560
+ * @returns
8561
+ */
8562
+ getJobConfigurationByCode(code) {
8563
+ return lodash.find(this.#server.listCronJobs(), (job) => job.code === code);
8564
+ }
8565
+ /**
8566
+ * 重新加载定时任务
8567
+ */
8568
+ reloadJobs() {
8569
+ const server = this.#server;
8570
+ if (this.#jobInstances) {
8571
+ for (const job of this.#jobInstances) {
8572
+ job.instance.stop();
8573
+ }
8574
+ }
8575
+ this.#jobInstances = [];
8576
+ const cronJobs = server.listCronJobs();
8577
+ for (const job of cronJobs) {
8578
+ if (job.disabled) {
8579
+ continue;
8580
+ }
8581
+ const jobInstance = cron.CronJob.from({
8582
+ ...(job.jobOptions || {}),
8583
+ cronTime: job.cronTime,
8584
+ onTick: async () => {
8585
+ await this.tryExecuteJob(job);
8586
+ },
8587
+ });
8588
+ jobInstance.start();
8589
+ this.#jobInstances.push({
8590
+ code: job.code,
8591
+ instance: jobInstance,
8592
+ });
8593
+ }
8594
+ }
8595
+ async tryExecuteJob(job) {
8596
+ const server = this.#server;
8597
+ const logger = server.getLogger();
8598
+ logger.info(`Executing cron job '${job.code}'...`);
8599
+ let handlerContext = {
8600
+ logger,
8601
+ routerContext: RouteContext.newSystemOperationContext(server),
8602
+ next: null,
8603
+ server,
8604
+ applicationConfig: null,
8605
+ input: null,
8606
+ };
8607
+ try {
8608
+ await this.executeJob(handlerContext, job);
8609
+ logger.info(`Completed cron job '${job.code}'...`);
8610
+ }
8611
+ catch (ex) {
8612
+ logger.error('Cron job "%s" execution error: %s', job.code, ex.message);
8613
+ if (job.onError) {
8614
+ try {
8615
+ await job.onError(handlerContext, ex);
8616
+ }
8617
+ catch (ex) {
8618
+ logger.error('Error handler of cron job "%s" execution failed: %s', job.code, ex.message);
8619
+ }
8620
+ }
8621
+ }
8622
+ }
8623
+ /**
8624
+ * 执行指定任务
8625
+ * @param job
8626
+ */
8627
+ async executeJob(handlerContext, job) {
8628
+ const server = this.#server;
8629
+ validateLicense(server);
8630
+ if (job.actionHandlerCode) {
8631
+ const actionHandler = server.getActionHandlerByCode(job.code);
8632
+ await actionHandler(handlerContext, job.handleOptions);
8633
+ }
8634
+ else {
8635
+ await job.handler(handlerContext, job.handleOptions);
8636
+ }
8637
+ }
8638
+ }
8639
+
8521
8640
  class CronJobPlugin {
8522
8641
  #server;
8642
+ #cronJobService;
8523
8643
  constructor() { }
8524
8644
  get code() {
8525
8645
  return "cronJob";
@@ -8551,55 +8671,16 @@ class CronJobPlugin {
8551
8671
  async onLoadingApplication(server, applicationConfig) { }
8552
8672
  async configureModels(server, applicationConfig) { }
8553
8673
  async configureModelProperties(server, applicationConfig) { }
8674
+ async configureServices(server, applicationConfig) {
8675
+ this.#cronJobService = new CronJobService(server);
8676
+ server.registerService("cronJobService", this.#cronJobService);
8677
+ }
8554
8678
  async configureRoutes(server, applicationConfig) {
8555
8679
  server.appendApplicationConfig({ routes: pluginRoutes$1 });
8556
8680
  }
8557
8681
  async onApplicationLoaded(server, applicationConfig) { }
8558
8682
  async onApplicationReady(server, applicationConfig) {
8559
- const cronJobs = server.listCronJobs();
8560
- for (const job of cronJobs) {
8561
- const jobInstance = cron__namespace.CronJob.from({
8562
- ...(job.jobOptions || {}),
8563
- cronTime: job.cronTime,
8564
- onTick: async () => {
8565
- await this.tryExecuteJob(server, job);
8566
- },
8567
- });
8568
- jobInstance.start();
8569
- }
8570
- }
8571
- getJobConfigurationByCode(code) {
8572
- return lodash.find(this.#server.listCronJobs(), (job) => job.code === code);
8573
- }
8574
- async tryExecuteJob(server, job) {
8575
- const logger = server.getLogger();
8576
- logger.info(`Executing cron job '${job.code}'...`);
8577
- try {
8578
- await this.executeJob(server, job);
8579
- logger.info(`Completed cron job '${job.code}'...`);
8580
- }
8581
- catch (ex) {
8582
- logger.error('Cron job "%s" execution error: %s', job.code, ex.message, { cronJobCode: job.code });
8583
- }
8584
- }
8585
- async executeJob(server, job) {
8586
- const logger = server.getLogger();
8587
- validateLicense(server);
8588
- let handlerContext = {
8589
- logger,
8590
- routerContext: RouteContext.newSystemOperationContext(server),
8591
- next: null,
8592
- server,
8593
- applicationConfig: null,
8594
- input: null,
8595
- };
8596
- if (job.actionHandlerCode) {
8597
- const actionHandler = server.getActionHandlerByCode(job.code);
8598
- await actionHandler(handlerContext, job.handleOptions);
8599
- }
8600
- else {
8601
- await job.handler(handlerContext, job.handleOptions);
8602
- }
8683
+ this.#cronJobService.reloadJobs();
8603
8684
  }
8604
8685
  }
8605
8686
 
@@ -9016,6 +9097,7 @@ exports.decodeJwt = decodeJwt;
9016
9097
  exports.deleteCookie = deleteCookie;
9017
9098
  exports.generateJwtSecretKey = generateJwtSecretKey;
9018
9099
  exports.getCookies = getCookies;
9100
+ exports.getDateString = getDateString;
9019
9101
  exports.getEntityRelationTargetId = getEntityRelationTargetId;
9020
9102
  exports.getNowString = getNowString;
9021
9103
  exports.getNowStringWithTimezone = getNowStringWithTimezone;
@@ -1,5 +1,4 @@
1
1
  import type { RpdApplicationConfig } from "../../types";
2
- import { CronJobConfiguration } from "../../types/cron-job-types";
3
2
  import { IRpdServer, RapidPlugin, RpdConfigurationItemOptions, RpdServerPluginConfigurableTargetOptions, RpdServerPluginExtendingAbilities } from "../../core/server";
4
3
  declare class CronJobPlugin implements RapidPlugin {
5
4
  #private;
@@ -18,11 +17,9 @@ declare class CronJobPlugin implements RapidPlugin {
18
17
  onLoadingApplication(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any>;
19
18
  configureModels(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any>;
20
19
  configureModelProperties(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any>;
20
+ configureServices(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any>;
21
21
  configureRoutes(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any>;
22
22
  onApplicationLoaded(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any>;
23
23
  onApplicationReady(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any>;
24
- getJobConfigurationByCode(code: string): CronJobConfiguration;
25
- tryExecuteJob(server: IRpdServer, job: CronJobConfiguration): Promise<void>;
26
- executeJob(server: IRpdServer, job: CronJobConfiguration): Promise<void>;
27
24
  }
28
25
  export default CronJobPlugin;
@@ -1,6 +1,12 @@
1
+ import { CronJob } from "cron";
1
2
  export type RunCronJobActionHandlerOptions = {
2
3
  code?: string;
3
4
  };
4
5
  export type RunCronJobInput = {
5
6
  code?: string;
7
+ input?: any;
8
+ };
9
+ export type NamedCronJobInstance = {
10
+ code: string;
11
+ instance: CronJob;
6
12
  };
@@ -0,0 +1,23 @@
1
+ import { IRpdServer } from "../../../core/server";
2
+ import { CronJobConfiguration } from "../../../types/cron-job-types";
3
+ import { ActionHandlerContext } from "../../../core/actionHandler";
4
+ export default class CronJobService {
5
+ #private;
6
+ constructor(server: IRpdServer);
7
+ /**
8
+ * 根据编号获取定时任务配置信息
9
+ * @param code
10
+ * @returns
11
+ */
12
+ getJobConfigurationByCode(code: string): CronJobConfiguration;
13
+ /**
14
+ * 重新加载定时任务
15
+ */
16
+ reloadJobs(): void;
17
+ tryExecuteJob(job: CronJobConfiguration): Promise<void>;
18
+ /**
19
+ * 执行指定任务
20
+ * @param job
21
+ */
22
+ executeJob(handlerContext: ActionHandlerContext, job: CronJobConfiguration): Promise<void>;
23
+ }
@@ -8,6 +8,10 @@ export interface CronJobConfiguration {
8
8
  * 定时任务描述
9
9
  */
10
10
  description?: string;
11
+ /**
12
+ * 是否禁用
13
+ */
14
+ disabled?: boolean;
11
15
  /**
12
16
  * crontab 表达式
13
17
  */
@@ -31,6 +35,13 @@ export interface CronJobConfiguration {
31
35
  * 处理定时任务时的设置选项
32
36
  */
33
37
  handleOptions?: any;
38
+ /**
39
+ * 定时任务执行失败时的处理
40
+ * @param ctx
41
+ * @param error
42
+ * @returns
43
+ */
44
+ onError?: (ctx: ActionHandlerContext, error: any) => Promise<void>;
34
45
  }
35
46
  export interface CronJobOptions {
36
47
  /**
@@ -1,2 +1,3 @@
1
1
  export declare function getNowString(): string;
2
2
  export declare function getNowStringWithTimezone(): string;
3
+ export declare function getDateString(timeString: any): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruiapp/rapid-core",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -2,6 +2,7 @@ import { RpdApplicationConfig } from "~/types";
2
2
  import { IRpdServer, RapidPlugin } from "./server";
3
3
  import { Next, RouteContext } from "./routeContext";
4
4
  import { Logger } from "~/facilities/log/LogFacility";
5
+ import { CronJobConfiguration } from "~/types/cron-job-types";
5
6
 
6
7
  export interface ActionHandlerContext {
7
8
  logger: Logger;
@@ -58,6 +58,7 @@ import { ColumnSelectOptions, CountRowOptions, FindRowOptions, FindRowOrderByOpt
58
58
  import { newEntityOperationError } from "~/utilities/errorUtility";
59
59
  import { getNowStringWithTimezone } from "~/utilities/timeUtility";
60
60
  import { RouteContext } from "~/core/routeContext";
61
+ import { validateEntity } from "./entityValidator";
61
62
 
62
63
  export type FindOneRelationEntitiesOptions = {
63
64
  server: IRpdServer;
@@ -777,8 +778,10 @@ async function createEntity(server: IRpdServer, dataAccessor: IRpdDataAccessor,
777
778
  throw newEntityOperationError("Create base entity directly is not allowed.");
778
779
  }
779
780
 
780
- const { entity, routeContext } = options;
781
+ let { entity } = options;
782
+ entity = await validateEntity(server, model, entity);
781
783
 
784
+ const { routeContext } = options;
782
785
  const userId = options.routeContext?.state?.userId;
783
786
  if (userId) {
784
787
  const createdByProperty = getEntityPropertyByCode(server, model, "createdBy");
@@ -1051,6 +1054,9 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
1051
1054
  throw new Error("Id is required when updating an entity.");
1052
1055
  }
1053
1056
 
1057
+ let { entityToSave } = options;
1058
+ entityToSave = await validateEntity(server, model, entityToSave);
1059
+
1054
1060
  const entity = await findById(server, dataAccessor, {
1055
1061
  routeContext,
1056
1062
  id,
@@ -1060,7 +1066,6 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
1060
1066
  throw new Error(`${model.namespace}.${model.singularCode} with id "${id}" was not found.`);
1061
1067
  }
1062
1068
 
1063
- let { entityToSave } = options;
1064
1069
  let changes = getEntityPartChanges(server, model, entity, entityToSave);
1065
1070
  if (!changes && !options.operation) {
1066
1071
  return entity;
@@ -1,6 +1,7 @@
1
1
  import type { IRpdServer } from "~/core/server";
2
2
  import { RpdDataModel } from "~/types";
3
3
  import { getEntityProperty, getEntityPropertyByCode, isRelationProperty } from "../helpers/metaHelper";
4
+ import { getDateString } from "~/utilities/timeUtility";
4
5
 
5
6
  // TODO Generate mapper and cache it.
6
7
 
@@ -45,8 +46,18 @@ export function mapDbRowToEntity(server: IRpdServer, model: RpdDataModel, row: a
45
46
  result[columnName] = row[columnName];
46
47
  }
47
48
  } else {
49
+ let fieldValue = row[columnName];
50
+
51
+ if (property) {
52
+ if (property.type === "date") {
53
+ if (fieldValue) {
54
+ fieldValue = getDateString(fieldValue);
55
+ }
56
+ }
57
+ }
58
+
48
59
  if (!result[propertyName]) {
49
- result[propertyName] = row[columnName];
60
+ result[propertyName] = fieldValue;
50
61
  }
51
62
  }
52
63
  });
@@ -0,0 +1,26 @@
1
+ import { IRpdServer } from "~/core/server";
2
+ import { getEntityPropertyByCode, getEntityPropertyByFieldName } from "~/helpers/metaHelper";
3
+ import { RpdDataModel } from "~/types";
4
+ import { getDateString } from "~/utilities/timeUtility";
5
+
6
+ export async function validateEntity(server: IRpdServer, model: RpdDataModel, entity: any) {
7
+ for (const propCode in entity) {
8
+ let prop = getEntityPropertyByCode(server, model, propCode);
9
+ if (!prop) {
10
+ getEntityPropertyByFieldName(server, model, propCode);
11
+ }
12
+
13
+ if (!prop) {
14
+ continue;
15
+ }
16
+
17
+ if (prop.type === "date") {
18
+ const originValue = entity[propCode];
19
+ if (originValue) {
20
+ entity[propCode] = getDateString(originValue);
21
+ }
22
+ }
23
+ }
24
+
25
+ return entity;
26
+ }
@@ -53,6 +53,13 @@ export function getEntityPropertiesIncludingBase(server: IRpdServer, model: RpdD
53
53
  return [...baseProperties, ...properties];
54
54
  }
55
55
 
56
+ /**
57
+ * 根据 code 获取实体属性信息。如实体为派生实体,可能会返回基础实体中的属性信息。
58
+ * @param server
59
+ * @param model
60
+ * @param propertyCode
61
+ * @returns
62
+ */
56
63
  export function getEntityPropertyByCode(server: IRpdServer, model: RpdDataModel, propertyCode: string): RpdDataModelProperty | undefined {
57
64
  return getEntityProperty(server, model, (e) => e.code === propertyCode);
58
65
  }
@@ -1,8 +1,6 @@
1
- import * as cron from "cron";
2
1
  import type { RpdApplicationConfig } from "~/types";
3
2
  import pluginActionHandlers from "./actionHandlers";
4
3
  import pluginRoutes from "./routes";
5
- import { CronJobConfiguration } from "~/types/cron-job-types";
6
4
  import {
7
5
  IRpdServer,
8
6
  RapidPlugin,
@@ -10,13 +8,11 @@ import {
10
8
  RpdServerPluginConfigurableTargetOptions,
11
9
  RpdServerPluginExtendingAbilities,
12
10
  } from "~/core/server";
13
- import { ActionHandlerContext } from "~/core/actionHandler";
14
- import { find } from "lodash";
15
- import { validateLicense } from "~/helpers/licenseHelper";
16
- import { RouteContext } from "~/core/routeContext";
11
+ import CronJobService from "./services/CronJobService";
17
12
 
18
13
  class CronJobPlugin implements RapidPlugin {
19
14
  #server: IRpdServer;
15
+ #cronJobService!: CronJobService;
20
16
 
21
17
  constructor() {}
22
18
 
@@ -64,6 +60,11 @@ class CronJobPlugin implements RapidPlugin {
64
60
 
65
61
  async configureModelProperties(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {}
66
62
 
63
+ async configureServices(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
64
+ this.#cronJobService = new CronJobService(server);
65
+ server.registerService("cronJobService", this.#cronJobService);
66
+ }
67
+
67
68
  async configureRoutes(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
68
69
  server.appendApplicationConfig({ routes: pluginRoutes });
69
70
  }
@@ -71,54 +72,7 @@ class CronJobPlugin implements RapidPlugin {
71
72
  async onApplicationLoaded(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {}
72
73
 
73
74
  async onApplicationReady(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
74
- const cronJobs = server.listCronJobs();
75
- for (const job of cronJobs) {
76
- const jobInstance = cron.CronJob.from({
77
- ...(job.jobOptions || {}),
78
- cronTime: job.cronTime,
79
- onTick: async () => {
80
- await this.tryExecuteJob(server, job);
81
- },
82
- });
83
- jobInstance.start();
84
- }
85
- }
86
-
87
- getJobConfigurationByCode(code: string) {
88
- return find(this.#server.listCronJobs(), (job) => job.code === code);
89
- }
90
-
91
- async tryExecuteJob(server: IRpdServer, job: CronJobConfiguration) {
92
- const logger = server.getLogger();
93
- logger.info(`Executing cron job '${job.code}'...`);
94
-
95
- try {
96
- await this.executeJob(server, job);
97
- logger.info(`Completed cron job '${job.code}'...`);
98
- } catch (ex: any) {
99
- logger.error('Cron job "%s" execution error: %s', job.code, ex.message, { cronJobCode: job.code });
100
- }
101
- }
102
-
103
- async executeJob(server: IRpdServer, job: CronJobConfiguration) {
104
- const logger = server.getLogger();
105
- validateLicense(server);
106
-
107
- let handlerContext: ActionHandlerContext = {
108
- logger,
109
- routerContext: RouteContext.newSystemOperationContext(server),
110
- next: null,
111
- server,
112
- applicationConfig: null,
113
- input: null,
114
- };
115
-
116
- if (job.actionHandlerCode) {
117
- const actionHandler = server.getActionHandlerByCode(job.code);
118
- await actionHandler(handlerContext, job.handleOptions);
119
- } else {
120
- await job.handler(handlerContext, job.handleOptions);
121
- }
75
+ this.#cronJobService.reloadJobs();
122
76
  }
123
77
  }
124
78
 
@@ -1,7 +1,15 @@
1
+ import { CronJob } from "cron";
2
+
1
3
  export type RunCronJobActionHandlerOptions = {
2
4
  code?: string;
3
5
  };
4
6
 
5
7
  export type RunCronJobInput = {
6
8
  code?: string;
9
+ input?: any;
10
+ };
11
+
12
+ export type NamedCronJobInstance = {
13
+ code: string;
14
+ instance: CronJob;
7
15
  };
@@ -1,6 +1,7 @@
1
1
  import { ActionHandlerContext } from "~/core/actionHandler";
2
2
  import { RunCronJobActionHandlerOptions, RunCronJobInput } from "../CronJobPluginTypes";
3
3
  import type CronJobPlugin from "../CronJobPlugin";
4
+ import CronJobService from "../services/CronJobService";
4
5
 
5
6
  export const code = "runCronJob";
6
7
 
@@ -18,12 +19,21 @@ export async function handler(plugin: CronJobPlugin, ctx: ActionHandlerContext,
18
19
  throw new Error(`Cron job code is required.`);
19
20
  }
20
21
 
21
- const job = plugin.getJobConfigurationByCode(input.code);
22
+ const cronJobService = server.getService<CronJobService>("cronJobService");
23
+ const job = cronJobService.getJobConfigurationByCode(input.code);
22
24
  if (!job) {
23
25
  throw new Error(`Cron job with code '${input.code}' was not found.`);
24
26
  }
25
27
 
26
- await plugin.executeJob(server, job);
28
+ let jobExecutionContext: ActionHandlerContext = {
29
+ logger: server.getLogger(),
30
+ routerContext,
31
+ next: null,
32
+ server,
33
+ applicationConfig: null,
34
+ input: input.input,
35
+ };
36
+ await cronJobService.executeJob(jobExecutionContext, job);
27
37
 
28
38
  response.json({});
29
39
  }
@@ -0,0 +1,108 @@
1
+ import { CronJob } from "cron";
2
+ import { IRpdServer } from "~/core/server";
3
+ import { find } from "lodash";
4
+ import { RouteContext } from "~/core/routeContext";
5
+ import { validateLicense } from "~/helpers/licenseHelper";
6
+ import { NamedCronJobInstance } from "../CronJobPluginTypes";
7
+ import { CronJobConfiguration } from "~/types/cron-job-types";
8
+ import { ActionHandlerContext } from "~/core/actionHandler";
9
+
10
+ export default class CronJobService {
11
+ #server: IRpdServer;
12
+ #jobInstances: NamedCronJobInstance[];
13
+
14
+ constructor(server: IRpdServer) {
15
+ this.#server = server;
16
+ }
17
+
18
+ /**
19
+ * 根据编号获取定时任务配置信息
20
+ * @param code
21
+ * @returns
22
+ */
23
+ getJobConfigurationByCode(code: string) {
24
+ return find(this.#server.listCronJobs(), (job) => job.code === code);
25
+ }
26
+
27
+ /**
28
+ * 重新加载定时任务
29
+ */
30
+ reloadJobs() {
31
+ const server = this.#server;
32
+
33
+ if (this.#jobInstances) {
34
+ for (const job of this.#jobInstances) {
35
+ job.instance.stop();
36
+ }
37
+ }
38
+
39
+ this.#jobInstances = [];
40
+
41
+ const cronJobs = server.listCronJobs();
42
+ for (const job of cronJobs) {
43
+ if (job.disabled) {
44
+ continue;
45
+ }
46
+
47
+ const jobInstance = CronJob.from({
48
+ ...(job.jobOptions || {}),
49
+ cronTime: job.cronTime,
50
+ onTick: async () => {
51
+ await this.tryExecuteJob(job);
52
+ },
53
+ });
54
+ jobInstance.start();
55
+
56
+ this.#jobInstances.push({
57
+ code: job.code,
58
+ instance: jobInstance,
59
+ });
60
+ }
61
+ }
62
+
63
+ async tryExecuteJob(job: CronJobConfiguration) {
64
+ const server = this.#server;
65
+ const logger = server.getLogger();
66
+ logger.info(`Executing cron job '${job.code}'...`);
67
+
68
+ let handlerContext: ActionHandlerContext = {
69
+ logger,
70
+ routerContext: RouteContext.newSystemOperationContext(server),
71
+ next: null,
72
+ server,
73
+ applicationConfig: null,
74
+ input: null,
75
+ };
76
+
77
+ try {
78
+ await this.executeJob(handlerContext, job);
79
+ logger.info(`Completed cron job '${job.code}'...`);
80
+ } catch (ex: any) {
81
+ logger.error('Cron job "%s" execution error: %s', job.code, ex.message);
82
+
83
+ if (job.onError) {
84
+ try {
85
+ await job.onError(handlerContext, ex);
86
+ } catch (ex) {
87
+ logger.error('Error handler of cron job "%s" execution failed: %s', job.code, ex.message);
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * 执行指定任务
95
+ * @param job
96
+ */
97
+ async executeJob(handlerContext: ActionHandlerContext, job: CronJobConfiguration) {
98
+ const server = this.#server;
99
+ validateLicense(server);
100
+
101
+ if (job.actionHandlerCode) {
102
+ const actionHandler = server.getActionHandlerByCode(job.code);
103
+ await actionHandler(handlerContext, job.handleOptions);
104
+ } else {
105
+ await job.handler(handlerContext, job.handleOptions);
106
+ }
107
+ }
108
+ }
@@ -11,6 +11,11 @@ export interface CronJobConfiguration {
11
11
  */
12
12
  description?: string;
13
13
 
14
+ /**
15
+ * 是否禁用
16
+ */
17
+ disabled?: boolean;
18
+
14
19
  /**
15
20
  * crontab 表达式
16
21
  */
@@ -38,6 +43,14 @@ export interface CronJobConfiguration {
38
43
  * 处理定时任务时的设置选项
39
44
  */
40
45
  handleOptions?: any;
46
+
47
+ /**
48
+ * 定时任务执行失败时的处理
49
+ * @param ctx
50
+ * @param error
51
+ * @returns
52
+ */
53
+ onError?: (ctx: ActionHandlerContext, error: any) => Promise<void>;
41
54
  }
42
55
 
43
56
  export interface CronJobOptions {
@@ -7,3 +7,7 @@ export function getNowString() {
7
7
  export function getNowStringWithTimezone() {
8
8
  return dayjs().format("YYYY-MM-DD HH:mm:ss.SSSZ");
9
9
  }
10
+
11
+ export function getDateString(timeString) {
12
+ return dayjs(timeString).format("YYYY-MM-DD");
13
+ }