@ruiapp/rapid-core 0.8.9 → 0.8.11

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 { RpdDataModel } from "../../../types";
2
+ declare const _default: RpdDataModel;
3
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../..").RpdDataModel[];
2
+ export default _default;
@@ -1,4 +1,6 @@
1
1
  import { IRpdServer } from "../../../core/server";
2
+ import { RouteContext } from "../../../core/routeContext";
3
+ import { UpdateJobConfigOptions } from "../CronJobPluginTypes";
2
4
  import { CronJobConfiguration } from "../../../types/cron-job-types";
3
5
  import { ActionHandlerContext } from "../../../core/actionHandler";
4
6
  export default class CronJobService {
@@ -13,11 +15,12 @@ export default class CronJobService {
13
15
  /**
14
16
  * 重新加载定时任务
15
17
  */
16
- reloadJobs(): void;
18
+ reloadJobs(): Promise<void>;
17
19
  tryExecuteJob(job: CronJobConfiguration): Promise<void>;
18
20
  /**
19
21
  * 执行指定任务
20
22
  * @param job
21
23
  */
22
24
  executeJob(handlerContext: ActionHandlerContext, job: CronJobConfiguration): Promise<void>;
25
+ updateJobConfig(routeContext: RouteContext, options: UpdateJobConfigOptions): Promise<void>;
23
26
  }
@@ -1,3 +1,4 @@
1
1
  export declare function getNowString(): string;
2
2
  export declare function getNowStringWithTimezone(): string;
3
3
  export declare function getDateString(timeString: any): string;
4
+ export declare function formatDateTimeWithTimezone(source: any): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruiapp/rapid-core",
3
- "version": "0.8.9",
3
+ "version": "0.8.11",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,13 +1,13 @@
1
- import { isArray, isObject } from "lodash";
2
1
  import { RapidRequest } from "./request";
3
2
  import { RapidResponse } from "./response";
4
- import { HttpStatus, ResponseData } from "./http-types";
3
+ import { HttpStatus } from "./http-types";
5
4
  import { IRpdServer } from "./server";
6
- import { Logger } from "~/facilities/log/LogFacility";
7
5
  import { IDatabaseAccessor, IDatabaseClient } from "~/types";
8
6
 
9
7
  export type Next = () => Promise<void>;
10
8
 
9
+ export type TransactionState = "uninited" | "inited" | "started";
10
+
11
11
  // TODO: should divide to RequestContext and OperationContext
12
12
 
13
13
  export class RouteContext {
@@ -21,6 +21,7 @@ export class RouteContext {
21
21
  routeConfig: any;
22
22
  #server: IRpdServer;
23
23
  #dbTransactionClient: IDatabaseClient | undefined;
24
+ #dbTransactionState: TransactionState;
24
25
 
25
26
  static newSystemOperationContext(server: IRpdServer) {
26
27
  return new RouteContext(server);
@@ -29,6 +30,8 @@ export class RouteContext {
29
30
  constructor(server: IRpdServer, request?: RapidRequest) {
30
31
  this.#server = server;
31
32
  this.databaseAccessor = server.getDatabaseAccessor();
33
+ this.#dbTransactionState = "uninited";
34
+
32
35
  this.request = request;
33
36
  this.state = {};
34
37
  this.response = new RapidResponse();
@@ -71,29 +74,54 @@ export class RouteContext {
71
74
  return this.#dbTransactionClient;
72
75
  }
73
76
 
74
- async beginDbTransaction(): Promise<IDatabaseClient> {
77
+ async initDbTransactionClient(): Promise<IDatabaseClient> {
75
78
  let dbClient = this.#dbTransactionClient;
76
79
  if (dbClient) {
77
- throw new Error("Database transaction has been started. You can not start a transaction more than once in a request context.");
80
+ return dbClient;
78
81
  }
79
82
 
80
83
  dbClient = await this.databaseAccessor.getClient();
81
- await this.databaseAccessor.queryDatabaseObject("BEGIN", [], dbClient);
84
+ this.#dbTransactionState = "inited";
82
85
  this.#dbTransactionClient = dbClient;
83
86
  return dbClient;
84
87
  }
85
88
 
89
+ async beginDbTransaction(): Promise<void> {
90
+ if (!this.#dbTransactionClient) {
91
+ throw new Error("Database transaction has not been inited. You should call initDbTransactionClient() first.");
92
+ }
93
+
94
+ if (this.#dbTransactionState === "started") {
95
+ throw new Error("Database transaction has been started. You can not begin a new transaction before you commit or rollback it.");
96
+ }
97
+
98
+ await this.databaseAccessor.queryDatabaseObject("BEGIN", [], this.#dbTransactionClient);
99
+ this.#dbTransactionState = "started";
100
+ }
101
+
86
102
  async commitDbTransaction(): Promise<void> {
87
103
  if (!this.#dbTransactionClient) {
104
+ throw new Error("Database transaction has not been inited. You should call initDbTransactionClient() first.");
105
+ }
106
+
107
+ if (this.#dbTransactionState !== "started") {
88
108
  throw new Error("Database transaction has not been started. You should call beginDbTransaction() first.");
89
109
  }
110
+
90
111
  await this.databaseAccessor.queryDatabaseObject("COMMIT", [], this.#dbTransactionClient);
112
+ this.#dbTransactionState = "inited";
91
113
  }
92
114
 
93
115
  async rollbackDbTransaction(): Promise<void> {
94
116
  if (!this.#dbTransactionClient) {
117
+ throw new Error("Database transaction has not been inited. You should call initDbTransactionClient() first.");
118
+ }
119
+
120
+ if (this.#dbTransactionState !== "started") {
95
121
  throw new Error("Database transaction has not been started. You should call beginDbTransaction() first.");
96
122
  }
123
+
97
124
  await this.databaseAccessor.queryDatabaseObject("ROLLBACK", [], this.#dbTransactionClient);
125
+ this.#dbTransactionState = "inited";
98
126
  }
99
127
  }
@@ -28,7 +28,8 @@ export default async function runCollectionEntityActionHandler(
28
28
  let transactionDbClient: IDatabaseClient;
29
29
 
30
30
  try {
31
- transactionDbClient = await routeContext.beginDbTransaction();
31
+ transactionDbClient = await routeContext.initDbTransactionClient();
32
+ await routeContext.beginDbTransaction();
32
33
 
33
34
  const result = handleEntityAction(entityManager, autoMergeInput ? mergedInput : input);
34
35
  if (result instanceof Promise) {
@@ -1,6 +1,8 @@
1
1
  import type { RpdApplicationConfig } from "~/types";
2
+ import pluginModels from "./models";
2
3
  import pluginActionHandlers from "./actionHandlers";
3
4
  import pluginRoutes from "./routes";
5
+ import pluginEntityWatchers from "./entityWatchers";
4
6
  import {
5
7
  IRpdServer,
6
8
  RapidPlugin,
@@ -9,6 +11,8 @@ import {
9
11
  RpdServerPluginExtendingAbilities,
10
12
  } from "~/core/server";
11
13
  import CronJobService from "./services/CronJobService";
14
+ import { SysCronJob } from "./CronJobPluginTypes";
15
+ import { pick } from "lodash";
12
16
 
13
17
  class CronJobPlugin implements RapidPlugin {
14
18
  #server: IRpdServer;
@@ -48,15 +52,18 @@ class CronJobPlugin implements RapidPlugin {
48
52
  }
49
53
  }
50
54
 
51
- async registerEventHandlers(server: IRpdServer): Promise<any> {}
52
-
55
+ async registerEventHandlers(server: IRpdServer): Promise<any> {
56
+ for (const entityWatcher of pluginEntityWatchers) server.registerEntityWatcher(entityWatcher);
57
+ }
53
58
  async registerMessageHandlers(server: IRpdServer): Promise<any> {}
54
59
 
55
60
  async registerTaskProcessors(server: IRpdServer): Promise<any> {}
56
61
 
57
62
  async onLoadingApplication(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {}
58
63
 
59
- async configureModels(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {}
64
+ async configureModels(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
65
+ server.appendApplicationConfig({ models: pluginModels });
66
+ }
60
67
 
61
68
  async configureModelProperties(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {}
62
69
 
@@ -69,11 +76,29 @@ class CronJobPlugin implements RapidPlugin {
69
76
  server.appendApplicationConfig({ routes: pluginRoutes });
70
77
  }
71
78
 
72
- async onApplicationLoaded(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {}
79
+ async onApplicationLoaded(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
80
+ await saveCronJobsToDatabase(server);
81
+ }
73
82
 
74
83
  async onApplicationReady(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
75
- this.#cronJobService.reloadJobs();
84
+ await this.#cronJobService.reloadJobs();
76
85
  }
77
86
  }
78
87
 
79
88
  export default CronJobPlugin;
89
+
90
+ async function saveCronJobsToDatabase(server: IRpdServer) {
91
+ const cronJobManager = server.getEntityManager<SysCronJob>("sys_cron_job");
92
+
93
+ for (const cronJobToSave of server.listCronJobs()) {
94
+ const currentCronJob = await cronJobManager.findEntity({
95
+ filters: [{ operator: "eq", field: "code", value: cronJobToSave.code }],
96
+ });
97
+
98
+ if (!currentCronJob) {
99
+ await cronJobManager.createEntity({
100
+ entity: pick(cronJobToSave, ["code", "description", "cronTime", "disabled"]),
101
+ });
102
+ }
103
+ }
104
+ }
@@ -1,4 +1,5 @@
1
1
  import { CronJob } from "cron";
2
+ import { CronJobConfiguration } from "~/types/cron-job-types";
2
3
 
3
4
  export type RunCronJobActionHandlerOptions = {
4
5
  code?: string;
@@ -13,3 +14,31 @@ export type NamedCronJobInstance = {
13
14
  code: string;
14
15
  instance: CronJob;
15
16
  };
17
+
18
+ export type JobRunningResult = "success" | "failed" | "error";
19
+
20
+ export type SysCronJob = {
21
+ id: number;
22
+ code: string;
23
+ description: string;
24
+ cronTime: string;
25
+ disabled: boolean;
26
+ jobOptions: CronJobConfiguration["jobOptions"];
27
+ isRunning: boolean;
28
+ nextRunningTime: string;
29
+ lastRunningTime: string;
30
+ lastRunningResult?: JobRunningResult;
31
+ lastErrorMessage?: string;
32
+ lastErrorStack?: string;
33
+ actionHandlerCode?: string;
34
+ handler?: string;
35
+ handleOptions?: Record<string, any>;
36
+ onError?: string;
37
+ };
38
+
39
+ export type UpdateJobConfigOptions = {
40
+ code: string;
41
+ cronTime?: string;
42
+ disabled?: boolean;
43
+ jobOptions?: CronJobConfiguration["jobOptions"];
44
+ };
@@ -0,0 +1,24 @@
1
+ import type { EntityWatcher, EntityWatchHandlerContext } from "~/types";
2
+ import CronJobService from "../services/CronJobService";
3
+ import { SysCronJob } from "../CronJobPluginTypes";
4
+
5
+ export default [
6
+ {
7
+ eventName: "entity.update",
8
+ modelSingularCode: "sys_cron_job",
9
+ handler: async (ctx: EntityWatchHandlerContext<"entity.update">) => {
10
+ const { server, payload, routerContext: routeContext } = ctx;
11
+
12
+ const cronJobService = server.getService<CronJobService>("cronJobService");
13
+
14
+ const changes: Partial<SysCronJob> = payload.changes;
15
+ const after: SysCronJob = payload.after;
16
+ await cronJobService.updateJobConfig(routeContext, {
17
+ code: after.code,
18
+ cronTime: changes.cronTime,
19
+ disabled: changes.disabled,
20
+ jobOptions: changes.jobOptions,
21
+ });
22
+ },
23
+ },
24
+ ] satisfies EntityWatcher<any>[];
@@ -0,0 +1,4 @@
1
+ import { EntityWatcher } from "~/types";
2
+ import cronJobEntityWatchers from "./cronJobEntityWatchers";
3
+
4
+ export default [...cronJobEntityWatchers] satisfies EntityWatcher<any>[];
@@ -0,0 +1,129 @@
1
+ import { RpdDataModel } from "~/types";
2
+
3
+ export default {
4
+ maintainedBy: "cronJob",
5
+ namespace: "sys",
6
+ name: "sys_cron_job",
7
+ singularCode: "sys_cron_job",
8
+ pluralCode: "sys_cron_jobs",
9
+ schema: "public",
10
+ tableName: "sys_cron_jobs",
11
+ properties: [
12
+ {
13
+ name: "id",
14
+ code: "id",
15
+ columnName: "id",
16
+ type: "integer",
17
+ required: true,
18
+ autoIncrement: true,
19
+ },
20
+ {
21
+ name: "code",
22
+ code: "code",
23
+ columnName: "code",
24
+ type: "text",
25
+ required: true,
26
+ },
27
+ {
28
+ name: "description",
29
+ code: "description",
30
+ columnName: "description",
31
+ type: "text",
32
+ required: false,
33
+ },
34
+ {
35
+ name: "cronTime",
36
+ code: "cronTime",
37
+ columnName: "cron_time",
38
+ type: "text",
39
+ required: true,
40
+ },
41
+ {
42
+ name: "disabled",
43
+ code: "disabled",
44
+ columnName: "disabled",
45
+ type: "boolean",
46
+ required: true,
47
+ defaultValue: "false",
48
+ },
49
+ {
50
+ name: "jobOptions",
51
+ code: "jobOptions",
52
+ columnName: "job_options",
53
+ type: "json",
54
+ required: false,
55
+ },
56
+ {
57
+ name: "isRunning",
58
+ code: "isRunning",
59
+ columnName: "is_running",
60
+ type: "boolean",
61
+ required: true,
62
+ defaultValue: "false",
63
+ },
64
+ {
65
+ name: "nextRunningTime",
66
+ code: "nextRunningTime",
67
+ columnName: "next_running_time",
68
+ type: "datetime",
69
+ required: false,
70
+ },
71
+ {
72
+ name: "lastRunningTime",
73
+ code: "lastRunningTime",
74
+ columnName: "last_running_time",
75
+ type: "datetime",
76
+ required: false,
77
+ },
78
+ // success, failed, error
79
+ {
80
+ name: "lastRunningResult",
81
+ code: "lastRunningResult",
82
+ columnName: "last_running_result",
83
+ type: "text",
84
+ required: false,
85
+ },
86
+ {
87
+ name: "lastErrorMessage",
88
+ code: "lastErrorMessage",
89
+ columnName: "last_error_message",
90
+ type: "text",
91
+ required: false,
92
+ },
93
+ {
94
+ name: "lastErrorStack",
95
+ code: "lastErrorStack",
96
+ columnName: "last_error_stack",
97
+ type: "text",
98
+ required: false,
99
+ },
100
+ {
101
+ name: "actionHandlerCode",
102
+ code: "actionHandlerCode",
103
+ columnName: "action_handler_code",
104
+ type: "text",
105
+ required: false,
106
+ },
107
+ {
108
+ name: "handler",
109
+ code: "handler",
110
+ columnName: "handler",
111
+ type: "text",
112
+ required: false,
113
+ },
114
+ {
115
+ name: "handleOptions",
116
+ code: "handleOptions",
117
+ columnName: "handle_options",
118
+ type: "json",
119
+ required: false,
120
+ },
121
+ {
122
+ name: "onError",
123
+ code: "onError",
124
+ columnName: "on_error",
125
+ type: "text",
126
+ required: false,
127
+ },
128
+ ],
129
+ } as RpdDataModel;
@@ -0,0 +1,3 @@
1
+ import CronJob from "./CronJob";
2
+
3
+ export default [CronJob];
@@ -1,15 +1,16 @@
1
1
  import { CronJob } from "cron";
2
2
  import { IRpdServer } from "~/core/server";
3
- import { find } from "lodash";
3
+ import { find, isNil, isString, Many } from "lodash";
4
4
  import { RouteContext } from "~/core/routeContext";
5
5
  import { validateLicense } from "~/helpers/licenseHelper";
6
- import { NamedCronJobInstance } from "../CronJobPluginTypes";
6
+ import { JobRunningResult, NamedCronJobInstance, SysCronJob, UpdateJobConfigOptions } from "../CronJobPluginTypes";
7
7
  import { CronJobConfiguration } from "~/types/cron-job-types";
8
8
  import { ActionHandlerContext } from "~/core/actionHandler";
9
+ import { formatDateTimeWithTimezone, getNowStringWithTimezone } from "~/utilities/timeUtility";
9
10
 
10
11
  export default class CronJobService {
11
12
  #server: IRpdServer;
12
- #jobInstances: NamedCronJobInstance[];
13
+ #namedJobInstances: NamedCronJobInstance[];
13
14
 
14
15
  constructor(server: IRpdServer) {
15
16
  this.#server = server;
@@ -24,37 +25,93 @@ export default class CronJobService {
24
25
  return find(this.#server.listCronJobs(), (job) => job.code === code);
25
26
  }
26
27
 
28
+ #createJobInstance(job: CronJobConfiguration) {
29
+ return CronJob.from({
30
+ ...(job.jobOptions || {}),
31
+ cronTime: job.cronTime,
32
+ onTick: async () => {
33
+ await this.tryExecuteJob(job);
34
+ },
35
+ });
36
+ }
37
+
38
+ async #startJobInstance(routeContext: RouteContext, jobConfiguration: CronJobConfiguration, jobInstance: CronJob) {
39
+ const server = this.#server;
40
+ const jobCode = jobConfiguration.code;
41
+ const cronJobManager = server.getEntityManager<SysCronJob>("sys_cron_job");
42
+ const cronJobInDb = await cronJobManager.findEntity({
43
+ routeContext,
44
+ filters: [{ operator: "eq", field: "code", value: jobCode }],
45
+ });
46
+
47
+ if (cronJobInDb) {
48
+ let nextRunningTime: string | null;
49
+ nextRunningTime = formatDateTimeWithTimezone(jobInstance.nextDate().toISO());
50
+
51
+ await cronJobManager.updateEntityById({
52
+ routeContext,
53
+ id: cronJobInDb.id,
54
+ entityToSave: {
55
+ nextRunningTime,
56
+ } as Partial<SysCronJob>,
57
+ });
58
+ }
59
+
60
+ jobInstance.start();
61
+ }
62
+
63
+ async #setJobNextRunningTime(routeContext: RouteContext, jobCode: string, nextRunningTime: string | null) {
64
+ const server = this.#server;
65
+ const cronJobManager = server.getEntityManager<SysCronJob>("sys_cron_job");
66
+ const cronJobInDb = await cronJobManager.findEntity({
67
+ routeContext,
68
+ filters: [{ operator: "eq", field: "code", value: jobCode }],
69
+ });
70
+ await cronJobManager.updateEntityById({
71
+ routeContext,
72
+ id: cronJobInDb.id,
73
+ entityToSave: {
74
+ nextRunningTime,
75
+ } as Partial<SysCronJob>,
76
+ });
77
+ }
78
+
27
79
  /**
28
80
  * 重新加载定时任务
29
81
  */
30
- reloadJobs() {
82
+ async reloadJobs() {
31
83
  const server = this.#server;
84
+ const routeContext = RouteContext.newSystemOperationContext(server);
32
85
 
33
- if (this.#jobInstances) {
34
- for (const job of this.#jobInstances) {
86
+ if (this.#namedJobInstances) {
87
+ for (const job of this.#namedJobInstances) {
35
88
  job.instance.stop();
36
89
  }
37
90
  }
38
91
 
39
- this.#jobInstances = [];
92
+ this.#namedJobInstances = [];
40
93
 
41
- const cronJobs = server.listCronJobs();
42
- for (const job of cronJobs) {
43
- if (job.disabled) {
94
+ const cronJobManager = server.getEntityManager<SysCronJob>("sys_cron_job");
95
+ const cronJobConfigurationsInDb = await cronJobManager.findEntities({ routeContext });
96
+
97
+ const cronJobConfigurations = server.listCronJobs();
98
+ for (const cronJobConfig of cronJobConfigurations) {
99
+ const jobCode = cronJobConfig.code;
100
+ const jobConfigInDb = find(cronJobConfigurationsInDb, { code: jobCode });
101
+ if (jobConfigInDb) {
102
+ overrideJobConfig(cronJobConfig, jobConfigInDb);
103
+ }
104
+
105
+ if (cronJobConfig.disabled) {
106
+ await this.#setJobNextRunningTime(routeContext, jobCode, null);
44
107
  continue;
45
108
  }
46
109
 
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();
110
+ const jobInstance = this.#createJobInstance(cronJobConfig);
111
+ await this.#startJobInstance(routeContext, cronJobConfig, jobInstance);
55
112
 
56
- this.#jobInstances.push({
57
- code: job.code,
113
+ this.#namedJobInstances.push({
114
+ code: jobCode,
58
115
  instance: jobInstance,
59
116
  });
60
117
  }
@@ -63,7 +120,9 @@ export default class CronJobService {
63
120
  async tryExecuteJob(job: CronJobConfiguration) {
64
121
  const server = this.#server;
65
122
  const logger = server.getLogger();
66
- logger.info(`Executing cron job '${job.code}'...`);
123
+
124
+ const jobCode = job.code;
125
+ logger.info(`Executing cron job '${jobCode}'...`);
67
126
 
68
127
  let handlerContext: ActionHandlerContext = {
69
128
  logger,
@@ -74,20 +133,55 @@ export default class CronJobService {
74
133
  input: null,
75
134
  };
76
135
 
136
+ let result: JobRunningResult;
137
+ let lastErrorMessage: string | null;
138
+ let lastErrorStack: string | null;
77
139
  try {
78
140
  await this.executeJob(handlerContext, job);
79
- logger.info(`Completed cron job '${job.code}'...`);
141
+ result = "success";
142
+ logger.info(`Completed cron job '${jobCode}'...`);
80
143
  } catch (ex: any) {
81
- logger.error('Cron job "%s" execution error: %s', job.code, ex.message);
144
+ logger.error('Cron job "%s" execution error: %s', jobCode, ex.message);
145
+ if (isString(ex)) {
146
+ lastErrorMessage = ex;
147
+ } else {
148
+ lastErrorMessage = ex.message;
149
+ lastErrorStack = ex.stack;
150
+ }
151
+ result = "failed";
82
152
 
83
153
  if (job.onError) {
84
154
  try {
85
155
  await job.onError(handlerContext, ex);
86
156
  } catch (ex) {
87
- logger.error('Error handler of cron job "%s" execution failed: %s', job.code, ex.message);
157
+ logger.error('Error handler of cron job "%s" execution failed: %s', jobCode, ex.message);
88
158
  }
89
159
  }
90
160
  }
161
+
162
+ const cronJobManager = server.getEntityManager<SysCronJob>("sys_cron_job");
163
+ const cronJobInDb = await cronJobManager.findEntity({
164
+ filters: [{ operator: "eq", field: "code", value: jobCode }],
165
+ });
166
+
167
+ if (cronJobInDb) {
168
+ let nextRunningTime: string | null;
169
+ const namedJobInstance = find(this.#namedJobInstances, { code: jobCode });
170
+ if (namedJobInstance && namedJobInstance.instance) {
171
+ nextRunningTime = formatDateTimeWithTimezone(namedJobInstance.instance.nextDate().toISO());
172
+ }
173
+
174
+ await cronJobManager.updateEntityById({
175
+ id: cronJobInDb.id,
176
+ entityToSave: {
177
+ nextRunningTime,
178
+ lastRunningResult: result,
179
+ lastRunningTime: getNowStringWithTimezone(),
180
+ lastErrorMessage,
181
+ lastErrorStack,
182
+ } as Partial<SysCronJob>,
183
+ });
184
+ }
91
185
  }
92
186
 
93
187
  /**
@@ -105,4 +199,54 @@ export default class CronJobService {
105
199
  await job.handler(handlerContext, job.handleOptions);
106
200
  }
107
201
  }
202
+
203
+ async updateJobConfig(routeContext: RouteContext, options: UpdateJobConfigOptions) {
204
+ const server = this.#server;
205
+ const cronJobs = server.listCronJobs();
206
+ const jobCode = options.code;
207
+ if (!jobCode) {
208
+ throw new Error(`options.code is required.`);
209
+ }
210
+
211
+ const cronJobConfig = find(cronJobs, { code: jobCode });
212
+ if (!cronJobConfig) {
213
+ throw new Error(`Cron job with code "${jobCode}" not found.`);
214
+ }
215
+
216
+ if (!(["cronTime", "disabled", "jobOptions"] as (keyof typeof options)[]).some((field) => !isNil(options[field]))) {
217
+ return;
218
+ }
219
+
220
+ overrideJobConfig(cronJobConfig, options);
221
+
222
+ const namedJobInstance = find(this.#namedJobInstances, { code: jobCode });
223
+ if (namedJobInstance && namedJobInstance.instance) {
224
+ namedJobInstance.instance.stop();
225
+ namedJobInstance.instance = null;
226
+ }
227
+
228
+ if (cronJobConfig.disabled) {
229
+ await this.#setJobNextRunningTime(routeContext, jobCode, null);
230
+ } else {
231
+ const jobInstance = this.#createJobInstance(cronJobConfig);
232
+ await this.#startJobInstance(routeContext, cronJobConfig, jobInstance);
233
+
234
+ if (namedJobInstance) {
235
+ namedJobInstance.instance = jobInstance;
236
+ } else {
237
+ this.#namedJobInstances.push({
238
+ code: cronJobConfig.code,
239
+ instance: jobInstance,
240
+ });
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ function overrideJobConfig(original: Partial<SysCronJob> | CronJobConfiguration, overrides: Partial<SysCronJob>) {
247
+ (["cronTime", "disabled", "jobOptions"] as (keyof typeof overrides)[]).forEach((field: string) => {
248
+ if (!isNil(overrides[field])) {
249
+ original[field] = overrides[field];
250
+ }
251
+ });
108
252
  }
@@ -11,3 +11,7 @@ export function getNowStringWithTimezone() {
11
11
  export function getDateString(timeString) {
12
12
  return dayjs(timeString).format("YYYY-MM-DD");
13
13
  }
14
+
15
+ export function formatDateTimeWithTimezone(source: any) {
16
+ return dayjs(source).format("YYYY-MM-DD HH:mm:ss.SSSZ");
17
+ }