@cleverbrush/scheduler 1.1.10 → 2.0.0
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.
- package/README.md +160 -52
- package/dist/ScheduleCalculator.d.ts +43 -1
- package/dist/index.d.ts +6 -9
- package/dist/index.js +2 -408
- package/dist/index.js.map +1 -0
- package/dist/jobRepository.d.ts +13 -1
- package/dist/types.d.ts +281 -274
- package/package.json +15 -4
- package/dist/ScheduleCalculator.js +0 -208
- package/dist/jobRepository.js +0 -79
- package/dist/types.js +0 -115
package/package.json
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
"email": "andrew_zol@cleverbrush.com"
|
|
6
6
|
},
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@cleverbrush/schema": "
|
|
8
|
+
"@cleverbrush/schema": "^2.0.0"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/node": "^25.4.0"
|
|
9
12
|
},
|
|
10
13
|
"description": "Job Scheduler for NodeJS",
|
|
11
14
|
"files": [
|
|
@@ -20,6 +23,13 @@
|
|
|
20
23
|
],
|
|
21
24
|
"license": "BSD 3-Clause",
|
|
22
25
|
"main": "./dist/index.js",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"sideEffects": false,
|
|
23
33
|
"name": "@cleverbrush/scheduler",
|
|
24
34
|
"readme": "https://github.com/cleverbrush/framework/tree/master/libs/scheduler#readme",
|
|
25
35
|
"repository": {
|
|
@@ -27,10 +37,11 @@
|
|
|
27
37
|
"url": "github:cleverbrush/framework"
|
|
28
38
|
},
|
|
29
39
|
"scripts": {
|
|
30
|
-
"watch": "tsc --watch",
|
|
31
|
-
"build": "tsc"
|
|
40
|
+
"watch": "tsc --build --watch",
|
|
41
|
+
"build": "tsup && tsc --project tsconfig.build.json --emitDeclarationOnly",
|
|
42
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo"
|
|
32
43
|
},
|
|
33
44
|
"type": "module",
|
|
34
45
|
"types": "./dist/index.d.ts",
|
|
35
|
-
"version": "
|
|
46
|
+
"version": "2.0.0"
|
|
36
47
|
}
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
const MS_IN_DAY = 1000 * 60 * 60 * 24;
|
|
2
|
-
const MS_IN_WEEK = MS_IN_DAY * 7;
|
|
3
|
-
const getDayOfWeek = (date) => {
|
|
4
|
-
const res = date.getUTCDay();
|
|
5
|
-
if (res === 0)
|
|
6
|
-
return 7;
|
|
7
|
-
return res;
|
|
8
|
-
};
|
|
9
|
-
const getNumberOfDaysInMonth = (date) => {
|
|
10
|
-
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0, 0, 0, 0, 0)).getDate();
|
|
11
|
-
};
|
|
12
|
-
export class ScheduleCalculator {
|
|
13
|
-
#schedule;
|
|
14
|
-
#currentDate = new Date();
|
|
15
|
-
#hour = 9;
|
|
16
|
-
#minute = 0;
|
|
17
|
-
#maxRepeat = -1;
|
|
18
|
-
#repeatCount = 0;
|
|
19
|
-
#hasNext = false;
|
|
20
|
-
#next;
|
|
21
|
-
constructor(schedule) {
|
|
22
|
-
if (!schedule)
|
|
23
|
-
throw new Error('schedule is required');
|
|
24
|
-
this.#schedule = { ...schedule };
|
|
25
|
-
if (typeof schedule.startsOn !== 'undefined') {
|
|
26
|
-
this.#currentDate = schedule.startsOn;
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
this.#schedule.startsOn = new Date();
|
|
30
|
-
this.#currentDate = this.#schedule.startsOn;
|
|
31
|
-
}
|
|
32
|
-
if (schedule.every !== 'minute') {
|
|
33
|
-
if (typeof schedule.hour === 'number') {
|
|
34
|
-
this.#hour = schedule.hour;
|
|
35
|
-
}
|
|
36
|
-
if (typeof schedule.minute === 'number') {
|
|
37
|
-
this.#minute = schedule.minute;
|
|
38
|
-
}
|
|
39
|
-
if (schedule.every === 'day' &&
|
|
40
|
-
new Date(Date.UTC(this.#currentDate.getUTCFullYear(), this.#currentDate.getUTCMonth(), this.#currentDate.getUTCDate(), this.#hour, this.#minute, 0, 0)).getTime() < this.#currentDate.getTime()) {
|
|
41
|
-
const date = new Date(Date.UTC(this.#currentDate.getUTCFullYear(), this.#currentDate.getUTCMonth(), this.#currentDate.getUTCDate() + 1, this.#hour, this.#minute, 0, 0));
|
|
42
|
-
this.#currentDate = date;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (typeof schedule.maxOccurences === 'number') {
|
|
46
|
-
this.#maxRepeat = schedule.maxOccurences;
|
|
47
|
-
}
|
|
48
|
-
const next = this.#getNext();
|
|
49
|
-
if (typeof next !== 'undefined') {
|
|
50
|
-
this.#next = next;
|
|
51
|
-
this.#hasNext = true;
|
|
52
|
-
}
|
|
53
|
-
let leftToSkip = typeof this.#schedule.skipFirst === 'number'
|
|
54
|
-
? this.#schedule.skipFirst
|
|
55
|
-
: 0;
|
|
56
|
-
while (leftToSkip-- > 0 && this.#hasNext) {
|
|
57
|
-
this.next();
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
#getNext() {
|
|
61
|
-
let candidate = null;
|
|
62
|
-
let dayOfWeek;
|
|
63
|
-
switch (this.#schedule.every) {
|
|
64
|
-
case 'minute':
|
|
65
|
-
candidate =
|
|
66
|
-
this.#repeatCount === 0
|
|
67
|
-
? this.#schedule.startsOn
|
|
68
|
-
: new Date(Date.UTC(this.#currentDate.getUTCFullYear(), this.#currentDate.getUTCMonth(), this.#currentDate.getUTCDate(), this.#currentDate.getUTCHours(), this.#currentDate.getUTCMinutes() +
|
|
69
|
-
this.#schedule.interval, this.#currentDate.getUTCSeconds(), this.#currentDate.getUTCMilliseconds()));
|
|
70
|
-
break;
|
|
71
|
-
case 'day':
|
|
72
|
-
{
|
|
73
|
-
const date = new Date(this.#currentDate.getTime() +
|
|
74
|
-
MS_IN_DAY *
|
|
75
|
-
(this.#repeatCount === 0
|
|
76
|
-
? 0
|
|
77
|
-
: this.#schedule.interval - 1));
|
|
78
|
-
candidate = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), this.#hour, this.#minute, 0, 0));
|
|
79
|
-
}
|
|
80
|
-
break;
|
|
81
|
-
case 'week':
|
|
82
|
-
{
|
|
83
|
-
let date = this.#currentDate;
|
|
84
|
-
do {
|
|
85
|
-
let found = false;
|
|
86
|
-
dayOfWeek = getDayOfWeek(date);
|
|
87
|
-
if (Number.isNaN(dayOfWeek))
|
|
88
|
-
return;
|
|
89
|
-
for (let i = 0; i < 7 - dayOfWeek + 1; i++) {
|
|
90
|
-
date = new Date(date.getTime() + (i == 0 ? 0 : MS_IN_DAY));
|
|
91
|
-
if (this.#schedule.endsOn &&
|
|
92
|
-
date > this.#schedule.endsOn) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (this.#schedule.dayOfWeek.includes(getDayOfWeek(date))) {
|
|
96
|
-
const dateWithTime = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), this.#hour, this.#minute, 0, 0));
|
|
97
|
-
if (dateWithTime >
|
|
98
|
-
this.#schedule.startsOn) {
|
|
99
|
-
candidate = dateWithTime;
|
|
100
|
-
found = true;
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (found)
|
|
106
|
-
break;
|
|
107
|
-
date = new Date(date.getTime() +
|
|
108
|
-
MS_IN_DAY +
|
|
109
|
-
(this.#repeatCount == 0
|
|
110
|
-
? 0
|
|
111
|
-
: (this.#schedule.interval - 1) *
|
|
112
|
-
MS_IN_WEEK));
|
|
113
|
-
} while ((this.#schedule.endsOn &&
|
|
114
|
-
date <= this.#schedule.endsOn) ||
|
|
115
|
-
!this.#schedule.endsOn);
|
|
116
|
-
}
|
|
117
|
-
break;
|
|
118
|
-
case 'month':
|
|
119
|
-
{
|
|
120
|
-
let dateTime;
|
|
121
|
-
const cDate = this.#currentDate;
|
|
122
|
-
let iteration = 0;
|
|
123
|
-
do {
|
|
124
|
-
const date = this.#schedule.day === 'last'
|
|
125
|
-
? getNumberOfDaysInMonth(new Date(Date.UTC(cDate.getUTCFullYear(), cDate.getUTCMonth() +
|
|
126
|
-
iteration *
|
|
127
|
-
(this.#repeatCount === 0
|
|
128
|
-
? 1
|
|
129
|
-
: this.#schedule
|
|
130
|
-
.interval), 1, 0, 0, 0, 0)))
|
|
131
|
-
: this.#schedule.day;
|
|
132
|
-
dateTime = new Date(Date.UTC(cDate.getUTCFullYear(), cDate.getUTCMonth() +
|
|
133
|
-
iteration *
|
|
134
|
-
(this.#repeatCount === 0
|
|
135
|
-
? 1
|
|
136
|
-
: this.#schedule.interval), date, this.#hour, this.#minute, 0, 0));
|
|
137
|
-
iteration++;
|
|
138
|
-
} while (dateTime < this.#schedule.startsOn ||
|
|
139
|
-
dateTime <= this.#currentDate);
|
|
140
|
-
candidate = dateTime;
|
|
141
|
-
}
|
|
142
|
-
break;
|
|
143
|
-
case 'year':
|
|
144
|
-
{
|
|
145
|
-
let dateTime;
|
|
146
|
-
const cDate = this.#currentDate;
|
|
147
|
-
let iteration = 0;
|
|
148
|
-
do {
|
|
149
|
-
const date = this.#schedule.day === 'last'
|
|
150
|
-
? getNumberOfDaysInMonth(new Date(Date.UTC(cDate.getUTCFullYear() +
|
|
151
|
-
iteration *
|
|
152
|
-
(this.#repeatCount === 0
|
|
153
|
-
? 1
|
|
154
|
-
: this.#schedule
|
|
155
|
-
.interval), this.#schedule.month - 1, 1, 0, 0, 0, 0)))
|
|
156
|
-
: this.#schedule.day;
|
|
157
|
-
dateTime = new Date(Date.UTC(cDate.getUTCFullYear() +
|
|
158
|
-
iteration *
|
|
159
|
-
(this.#repeatCount === 0
|
|
160
|
-
? 1
|
|
161
|
-
: this.#schedule.interval), this.#schedule.month - 1, date, this.#hour, this.#minute, 0, 0));
|
|
162
|
-
iteration++;
|
|
163
|
-
} while (dateTime < this.#schedule.startsOn ||
|
|
164
|
-
dateTime <= this.#currentDate);
|
|
165
|
-
candidate = dateTime;
|
|
166
|
-
}
|
|
167
|
-
break;
|
|
168
|
-
default:
|
|
169
|
-
throw new Error('unknown schedule type');
|
|
170
|
-
}
|
|
171
|
-
if (!candidate)
|
|
172
|
-
return;
|
|
173
|
-
if (typeof this.#schedule.endsOn !== 'undefined' &&
|
|
174
|
-
candidate > this.#schedule.endsOn) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
return candidate;
|
|
178
|
-
}
|
|
179
|
-
hasNext(span) {
|
|
180
|
-
if (!this.#hasNext) {
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
if (typeof span !== 'number')
|
|
184
|
-
return this.#hasNext;
|
|
185
|
-
if (!this.#next)
|
|
186
|
-
return false;
|
|
187
|
-
return this.#next.getTime() - new Date().getTime() <= span;
|
|
188
|
-
}
|
|
189
|
-
next() {
|
|
190
|
-
if (!this.#hasNext)
|
|
191
|
-
throw new Error('schedule is over');
|
|
192
|
-
const result = this.#next;
|
|
193
|
-
this.#currentDate = new Date(result.getTime() +
|
|
194
|
-
(['day', 'week'].includes(this.#schedule.every) ? MS_IN_DAY : 0));
|
|
195
|
-
this.#repeatCount++;
|
|
196
|
-
const next = this.#getNext();
|
|
197
|
-
this.#next = next;
|
|
198
|
-
this.#hasNext = typeof next !== 'undefined';
|
|
199
|
-
if (this.#maxRepeat > 0 && this.#repeatCount >= this.#maxRepeat) {
|
|
200
|
-
this.#next = undefined;
|
|
201
|
-
this.#hasNext = false;
|
|
202
|
-
}
|
|
203
|
-
return {
|
|
204
|
-
date: result,
|
|
205
|
-
index: this.#repeatCount
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
}
|
package/dist/jobRepository.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
export class InMemoryJobRepository {
|
|
2
|
-
_jobs = [];
|
|
3
|
-
_jobInstances = [];
|
|
4
|
-
_instanceId = 1;
|
|
5
|
-
async getJobs() {
|
|
6
|
-
return this._jobs;
|
|
7
|
-
}
|
|
8
|
-
async removeJob(jobId) {
|
|
9
|
-
const job = this.getJobById(jobId);
|
|
10
|
-
if (!job)
|
|
11
|
-
throw new Error(`job with id ${jobId} doesn't exist`);
|
|
12
|
-
this._jobs = this._jobs.filter((j) => j.id !== jobId);
|
|
13
|
-
}
|
|
14
|
-
async createJob(item) {
|
|
15
|
-
const job = {
|
|
16
|
-
...item,
|
|
17
|
-
status: 'active'
|
|
18
|
-
};
|
|
19
|
-
this._jobs.push(job);
|
|
20
|
-
return job;
|
|
21
|
-
}
|
|
22
|
-
async getInstances(jobId) {
|
|
23
|
-
return this._jobInstances.filter((ji) => ji.jobId === jobId);
|
|
24
|
-
}
|
|
25
|
-
async addInstance(jobId, instance) {
|
|
26
|
-
const newInstance = {
|
|
27
|
-
...instance,
|
|
28
|
-
id: this._instanceId++,
|
|
29
|
-
jobId
|
|
30
|
-
};
|
|
31
|
-
this._jobInstances.push(newInstance);
|
|
32
|
-
return newInstance;
|
|
33
|
-
}
|
|
34
|
-
async getJobById(jobId) {
|
|
35
|
-
return this._jobs.find((j) => j.id === jobId);
|
|
36
|
-
}
|
|
37
|
-
async setJobStatus(jobId, status) {
|
|
38
|
-
const job = await this.getJobById(jobId);
|
|
39
|
-
if (!job)
|
|
40
|
-
return null;
|
|
41
|
-
job.status = status;
|
|
42
|
-
return job;
|
|
43
|
-
}
|
|
44
|
-
async saveJob(job) {
|
|
45
|
-
const index = this._jobs.findIndex((j) => j.id === job.id);
|
|
46
|
-
if (index !== -1) {
|
|
47
|
-
this._jobs[index] = {
|
|
48
|
-
...job
|
|
49
|
-
};
|
|
50
|
-
return this._jobs[index];
|
|
51
|
-
}
|
|
52
|
-
const result = {
|
|
53
|
-
...job
|
|
54
|
-
};
|
|
55
|
-
this._jobs.push(result);
|
|
56
|
-
return result;
|
|
57
|
-
}
|
|
58
|
-
async getInstancesWithStatus(jobId, status) {
|
|
59
|
-
return (await this.getInstances(jobId)).filter((i) => i.status === status);
|
|
60
|
-
}
|
|
61
|
-
async getInstanceById(id) {
|
|
62
|
-
return this._jobInstances.find((ji) => ji.id === id);
|
|
63
|
-
}
|
|
64
|
-
async saveInstance(instance) {
|
|
65
|
-
const oldIndex = this._jobInstances.findIndex((ji) => ji.id === instance.id);
|
|
66
|
-
if (oldIndex !== -1) {
|
|
67
|
-
this._jobInstances[oldIndex] = {
|
|
68
|
-
...instance
|
|
69
|
-
};
|
|
70
|
-
return this._jobInstances[oldIndex];
|
|
71
|
-
}
|
|
72
|
-
const result = {
|
|
73
|
-
...instance,
|
|
74
|
-
id: this._instanceId++
|
|
75
|
-
};
|
|
76
|
-
this._jobInstances.push(result);
|
|
77
|
-
return result;
|
|
78
|
-
}
|
|
79
|
-
}
|
package/dist/types.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { array, boolean, date, func, number, object, string, union } from '@cleverbrush/schema';
|
|
2
|
-
const ScheduleSchemaBase = object({
|
|
3
|
-
/** Number of intervals (days, months, minutes or weeks)
|
|
4
|
-
* between repeats. Interval type depends of `every` value */
|
|
5
|
-
interval: number().min(1).max(356),
|
|
6
|
-
/** Hour (0-23) */
|
|
7
|
-
hour: number().min(0).max(23).optional(),
|
|
8
|
-
/** Minute (0-59) */
|
|
9
|
-
minute: number().min(0).max(59).optional(),
|
|
10
|
-
/** Do not start earlier than this date */
|
|
11
|
-
startsOn: date().acceptJsonString().optional(),
|
|
12
|
-
/** Do not repeat after this date */
|
|
13
|
-
endsOn: date().acceptJsonString().optional(),
|
|
14
|
-
/** Max number of repeats (min 1) */
|
|
15
|
-
maxOccurences: number().min(1).optional(),
|
|
16
|
-
/** Skip this number of repeats. Min value is 1. */
|
|
17
|
-
skipFirst: number().min(1).optional()
|
|
18
|
-
}).addValidator((val) => {
|
|
19
|
-
if ('endsOn' in val &&
|
|
20
|
-
'maxOccurences' in val &&
|
|
21
|
-
typeof val.endsOn !== 'undefined') {
|
|
22
|
-
return {
|
|
23
|
-
valid: false,
|
|
24
|
-
errors: [{ message: 'either endsOn or maxOccurences is required' }]
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
return { valid: true };
|
|
28
|
-
});
|
|
29
|
-
const ScheduleMinuteSchema = ScheduleSchemaBase.omit('hour')
|
|
30
|
-
.omit('minute')
|
|
31
|
-
.addProps({
|
|
32
|
-
/** Repeat every minute */
|
|
33
|
-
every: string('minute')
|
|
34
|
-
});
|
|
35
|
-
const ScheduleDaySchema = ScheduleSchemaBase.addProps({
|
|
36
|
-
/** Repeat every day */
|
|
37
|
-
every: string('day')
|
|
38
|
-
});
|
|
39
|
-
const ScheduleWeekSchema = ScheduleSchemaBase.addProps({
|
|
40
|
-
/** Repeat every week */
|
|
41
|
-
every: string('week'),
|
|
42
|
-
/** Days of week for schedule */
|
|
43
|
-
dayOfWeek: array()
|
|
44
|
-
.of(number().min(1).max(7))
|
|
45
|
-
.minLength(1)
|
|
46
|
-
.maxLength(7)
|
|
47
|
-
.addValidator((val) => {
|
|
48
|
-
const map = {};
|
|
49
|
-
for (let i = 0; i < val.length; i++) {
|
|
50
|
-
if (map[val[i]]) {
|
|
51
|
-
return {
|
|
52
|
-
valid: false,
|
|
53
|
-
errors: [{ message: 'no duplicates allowed' }]
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
map[val[i]] = true;
|
|
57
|
-
}
|
|
58
|
-
return {
|
|
59
|
-
valid: true
|
|
60
|
-
};
|
|
61
|
-
})
|
|
62
|
-
});
|
|
63
|
-
const ScheduleMonthSchema = ScheduleSchemaBase.addProps({
|
|
64
|
-
/** Repeat every month */
|
|
65
|
-
every: string('month'),
|
|
66
|
-
/** Day - 'last' or number from 1 to 28 */
|
|
67
|
-
day: union(string('last')).or(number().min(1).max(28))
|
|
68
|
-
});
|
|
69
|
-
const ScheduleYearSchema = ScheduleSchemaBase.addProps({
|
|
70
|
-
/** Repeat every year */
|
|
71
|
-
every: string('year'),
|
|
72
|
-
/** Day - 'last' or number from 1 to 28 */
|
|
73
|
-
day: union(string('last')).or(number().min(1).max(28)),
|
|
74
|
-
/** Month - number from 1 to 12 */
|
|
75
|
-
month: number().min(1).max(12)
|
|
76
|
-
});
|
|
77
|
-
const ScheduleSchema = union(ScheduleMinuteSchema)
|
|
78
|
-
.or(ScheduleDaySchema)
|
|
79
|
-
.or(ScheduleWeekSchema)
|
|
80
|
-
.or(ScheduleMonthSchema)
|
|
81
|
-
.or(ScheduleYearSchema);
|
|
82
|
-
const CreateJobRequestSchema = object({
|
|
83
|
-
/** Id of job, must be uniq */
|
|
84
|
-
id: string(),
|
|
85
|
-
/** Path to js file (relative to root folder) */
|
|
86
|
-
path: string().minLength(1),
|
|
87
|
-
/** Job's schedule */
|
|
88
|
-
schedule: ScheduleSchema,
|
|
89
|
-
/** Timeout for job (in milliseconds) */
|
|
90
|
-
timeout: number().min(0).optional(),
|
|
91
|
-
/** Arbitrary props for job (can be a callback returning props or Promise<props>) */
|
|
92
|
-
props: union(object().acceptUnknownProps()).or(func()).optional(),
|
|
93
|
-
/** Job will be considered as disabled when more than that count of runs fails consequently
|
|
94
|
-
* unlimited if negative
|
|
95
|
-
*/
|
|
96
|
-
maxConsequentFails: number().optional(),
|
|
97
|
-
/**
|
|
98
|
-
* Job will be retried right away this times. Job will be retried on next schedule run if this number is exceeded.
|
|
99
|
-
*/
|
|
100
|
-
maxRetries: number().optional().min(1),
|
|
101
|
-
/**
|
|
102
|
-
* If true, job will not be runned if previous run is not finished yet.
|
|
103
|
-
*/
|
|
104
|
-
noConcurrentRuns: boolean().optional()
|
|
105
|
-
});
|
|
106
|
-
export const Schemas = {
|
|
107
|
-
ScheduleSchemaBase,
|
|
108
|
-
ScheduleSchema,
|
|
109
|
-
CreateJobRequestSchema,
|
|
110
|
-
ScheduleMinuteSchema,
|
|
111
|
-
ScheduleDaySchema,
|
|
112
|
-
ScheduleWeekSchema,
|
|
113
|
-
ScheduleMonthSchema,
|
|
114
|
-
ScheduleYearSchema
|
|
115
|
-
};
|