@fabriktor/fx 0.0.72 → 0.0.75
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/dist/src/jobs/jobs.d.ts +8 -1
- package/dist/src/jobs/jobs.js +142 -61
- package/package.json +3 -3
package/dist/src/jobs/jobs.d.ts
CHANGED
|
@@ -57,7 +57,7 @@ export type CreateJobProgramResult = {
|
|
|
57
57
|
} | {
|
|
58
58
|
mode: typeof JobProgramCreateMode.Immediate;
|
|
59
59
|
};
|
|
60
|
-
export type FindLocationOverlapsInput = Pick<LocationInsert, "y" | "m" | "d" | "h" | "min" | "tz" | "end_date">;
|
|
60
|
+
export type FindLocationOverlapsInput = Pick<LocationInsert, "y" | "m" | "d" | "h" | "min" | "tz" | "end_date" | "lat" | "lon">;
|
|
61
61
|
export type FindLocationOverlapsOptions = {
|
|
62
62
|
in: FindLocationOverlapsInput;
|
|
63
63
|
source_id?: string;
|
|
@@ -74,6 +74,10 @@ export type RemoveJobMemberOptions = {
|
|
|
74
74
|
job: Job;
|
|
75
75
|
member_id: string;
|
|
76
76
|
};
|
|
77
|
+
export type UpdateOperationalLocationOptions = {
|
|
78
|
+
location: Location;
|
|
79
|
+
in: CreateOperationalLocationInput;
|
|
80
|
+
};
|
|
77
81
|
export type SetLocationMembersOptions = {
|
|
78
82
|
location: Location;
|
|
79
83
|
current_members: string[];
|
|
@@ -110,6 +114,7 @@ export interface JobsFXOperator {
|
|
|
110
114
|
createJobProgram(opts: CreateJobProgramOptions): Promise<CreateJobProgramResult>;
|
|
111
115
|
createLocationJobProgram(opts: CreateJobProgramOptions): Promise<CreateLocationJobProgramResult>;
|
|
112
116
|
findLocationOverlaps(opts: FindLocationOverlapsOptions): Promise<OperationResultOf<RPJobsFindOverlaps>>;
|
|
117
|
+
updateOperationalLocation(opts: UpdateOperationalLocationOptions): Promise<Location>;
|
|
113
118
|
addJobForeman(opts: AddJobForemanOptions): Promise<OperationResult>;
|
|
114
119
|
removeJobMember(opts: RemoveJobMemberOptions): Promise<OperationResult>;
|
|
115
120
|
setLocationMembers(opts: SetLocationMembersOptions): Promise<OperationResult>;
|
|
@@ -139,8 +144,10 @@ export declare class JobsFX implements JobsFXOperator {
|
|
|
139
144
|
findLocationOverlaps(opts: FindLocationOverlapsOptions): Promise<OperationResultOf<RPJobsFindOverlaps>>;
|
|
140
145
|
addJobForeman(opts: AddJobForemanOptions): Promise<OperationResult>;
|
|
141
146
|
removeJobMember(opts: RemoveJobMemberOptions): Promise<OperationResult>;
|
|
147
|
+
updateOperationalLocation(opts: UpdateOperationalLocationOptions): Promise<Location>;
|
|
142
148
|
setLocationMembers(opts: SetLocationMembersOptions): Promise<OperationResult>;
|
|
143
149
|
setJobMembers(opts: SetJobMembersOptions): Promise<OperationResult>;
|
|
150
|
+
private validateLocationMemberRemoval;
|
|
144
151
|
jobLifecycleControls(opts: JobLifecycleControlsOptions): JobLifecycleControls;
|
|
145
152
|
startJob(opts: JobLifecycleOptions): Promise<Job>;
|
|
146
153
|
terminateJob(opts: JobLifecycleOptions): Promise<Job>;
|
package/dist/src/jobs/jobs.js
CHANGED
|
@@ -19,9 +19,8 @@ const msgProgramStartPast = "Program start date must not be in the past.";
|
|
|
19
19
|
const msgJobStartOutsideLocation = "Job start date must fall within the parent location time span.";
|
|
20
20
|
const msgProgramStartOutsideLocation = "Program job start date must fall within the parent location time span.";
|
|
21
21
|
const msgLocationMemberAssigned = "Cannot remove a location member while they are assigned to a non-terminated job in that location.";
|
|
22
|
-
const msgDateInvalid = "Date contains an invalid UTC value.";
|
|
23
22
|
const msgLocationRangeInvalid = "Location contains an invalid scheduled time span.";
|
|
24
|
-
const msgTimezoneInvalid = "
|
|
23
|
+
const msgTimezoneInvalid = "Location timezone is invalid.";
|
|
25
24
|
const resourceLocation = "location";
|
|
26
25
|
const resourceJobProgram = "job_program";
|
|
27
26
|
const resourceJob = "job";
|
|
@@ -119,9 +118,11 @@ export class JobsFX {
|
|
|
119
118
|
field: "in",
|
|
120
119
|
msg: msgJobMembersOutsideLocation,
|
|
121
120
|
});
|
|
122
|
-
|
|
121
|
+
const timezone = locationTimezone(opts.location, "location");
|
|
123
122
|
const input = {
|
|
124
123
|
...opts.in,
|
|
124
|
+
tz: timezone,
|
|
125
|
+
timezone,
|
|
125
126
|
status: opts.start_immediately ? Started : Pending,
|
|
126
127
|
is_active: opts.start_immediately,
|
|
127
128
|
auto_start: opts.start_immediately ? true : opts.auto_start,
|
|
@@ -131,6 +132,12 @@ export class JobsFX {
|
|
|
131
132
|
location_id,
|
|
132
133
|
recurrent: false,
|
|
133
134
|
};
|
|
135
|
+
validateJobStart({
|
|
136
|
+
input,
|
|
137
|
+
location: opts.location,
|
|
138
|
+
now: this.now(),
|
|
139
|
+
start_immediately: opts.start_immediately,
|
|
140
|
+
});
|
|
134
141
|
return await this.jobs.insertOneJob({
|
|
135
142
|
in: input,
|
|
136
143
|
});
|
|
@@ -158,8 +165,18 @@ export class JobsFX {
|
|
|
158
165
|
field: "in",
|
|
159
166
|
msg: msgProgramMembersOutsideLocation,
|
|
160
167
|
});
|
|
168
|
+
const timezone = locationTimezone(opts.location, "location");
|
|
161
169
|
const input = {
|
|
162
170
|
...opts.in,
|
|
171
|
+
timezone,
|
|
172
|
+
job_start_date: {
|
|
173
|
+
...opts.in.job_start_date,
|
|
174
|
+
tz: timezone,
|
|
175
|
+
},
|
|
176
|
+
recurrence_end_date: {
|
|
177
|
+
...opts.in.recurrence_end_date,
|
|
178
|
+
tz: timezone,
|
|
179
|
+
},
|
|
163
180
|
job_current_members,
|
|
164
181
|
job_foremen,
|
|
165
182
|
job_location_id,
|
|
@@ -189,6 +206,8 @@ export class JobsFX {
|
|
|
189
206
|
}
|
|
190
207
|
async findLocationOverlaps(opts) {
|
|
191
208
|
const input = {
|
|
209
|
+
lat: opts.in.lat,
|
|
210
|
+
lon: opts.in.lon,
|
|
192
211
|
start_date: {
|
|
193
212
|
y: opts.in.y,
|
|
194
213
|
m: opts.in.m,
|
|
@@ -260,30 +279,28 @@ export class JobsFX {
|
|
|
260
279
|
},
|
|
261
280
|
});
|
|
262
281
|
}
|
|
282
|
+
async updateOperationalLocation(opts) {
|
|
283
|
+
const id = requiredID(opts.location.id, "location", msgLocationIDRequired);
|
|
284
|
+
const current_members = normalizeIDs(opts.in.current_members);
|
|
285
|
+
await this.validateLocationMemberRemoval({
|
|
286
|
+
location: opts.location,
|
|
287
|
+
current_members,
|
|
288
|
+
});
|
|
289
|
+
return await this.replaceLocation({
|
|
290
|
+
id,
|
|
291
|
+
in: {
|
|
292
|
+
...opts.in,
|
|
293
|
+
current_members,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
}
|
|
263
297
|
async setLocationMembers(opts) {
|
|
264
298
|
const id = requiredID(opts.location.id, "location", msgLocationIDRequired);
|
|
265
299
|
const current_members = normalizeIDs(opts.current_members);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
query: {
|
|
271
|
-
location_id: id,
|
|
272
|
-
},
|
|
273
|
-
});
|
|
274
|
-
const removed_set = new Set(removed);
|
|
275
|
-
for (const job of out.value ?? []) {
|
|
276
|
-
if (job.status === Terminated) {
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
if (normalizeIDs(job.current_members).some((member_id) => removed_set.has(member_id))) {
|
|
280
|
-
throw errValidation({
|
|
281
|
-
field: "current_members",
|
|
282
|
-
msg: msgLocationMemberAssigned,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
300
|
+
await this.validateLocationMemberRemoval({
|
|
301
|
+
location: opts.location,
|
|
302
|
+
current_members,
|
|
303
|
+
});
|
|
287
304
|
return await this.jobs.replaceOneLocation({
|
|
288
305
|
id,
|
|
289
306
|
in: {
|
|
@@ -316,6 +333,32 @@ export class JobsFX {
|
|
|
316
333
|
},
|
|
317
334
|
});
|
|
318
335
|
}
|
|
336
|
+
async validateLocationMemberRemoval(opts) {
|
|
337
|
+
const id = opts.location.id.trim();
|
|
338
|
+
const next = new Set(opts.current_members);
|
|
339
|
+
const removed = normalizeIDs(opts.location.current_members).filter((member_id) => !next.has(member_id));
|
|
340
|
+
if (removed.length === 0) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const out = await this.jobs.queryJobs({
|
|
344
|
+
query: {
|
|
345
|
+
owner_id: opts.location.owner_id,
|
|
346
|
+
location_id: id,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
const removed_set = new Set(removed);
|
|
350
|
+
for (const job of out.value ?? []) {
|
|
351
|
+
if (job.status === Terminated) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (normalizeIDs(job.current_members).some((member_id) => removed_set.has(member_id))) {
|
|
355
|
+
throw errValidation({
|
|
356
|
+
field: "current_members",
|
|
357
|
+
msg: msgLocationMemberAssigned,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
319
362
|
jobLifecycleControls(opts) {
|
|
320
363
|
const actor_id = requiredID(opts.actor_id, "actor_id", msgMemberIDRequired);
|
|
321
364
|
const can_manage = opts.job.owner_id === actor_id ||
|
|
@@ -420,31 +463,44 @@ function validateLocationMembers(opts) {
|
|
|
420
463
|
});
|
|
421
464
|
}
|
|
422
465
|
}
|
|
423
|
-
function validateJobStart(opts
|
|
424
|
-
const
|
|
425
|
-
if (
|
|
466
|
+
function validateJobStart(opts) {
|
|
467
|
+
const now = localDateParts(opts.now, opts.input.timezone);
|
|
468
|
+
if (now === undefined) {
|
|
469
|
+
throw errValidation({
|
|
470
|
+
field: "in",
|
|
471
|
+
msg: msgTimezoneInvalid,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
if (!opts.start_immediately && compareLocalDate(opts.input, now) < 0) {
|
|
426
475
|
throw errValidation({
|
|
427
476
|
field: "in",
|
|
428
477
|
msg: msgJobStartPast,
|
|
429
478
|
});
|
|
430
479
|
}
|
|
431
480
|
validateLocationStart({
|
|
432
|
-
|
|
481
|
+
start: opts.input,
|
|
433
482
|
location: opts.location,
|
|
434
483
|
field: "in",
|
|
435
484
|
msg: msgJobStartOutsideLocation,
|
|
436
485
|
});
|
|
437
486
|
}
|
|
438
487
|
function validateProgramStart(opts) {
|
|
439
|
-
const
|
|
440
|
-
if (
|
|
488
|
+
const now = localDateParts(opts.now, opts.input.timezone);
|
|
489
|
+
if (now === undefined) {
|
|
490
|
+
throw errValidation({
|
|
491
|
+
field: "in",
|
|
492
|
+
msg: msgTimezoneInvalid,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
if (!opts.current_day &&
|
|
496
|
+
compareLocalDate(opts.input.job_start_date, now) < 0) {
|
|
441
497
|
throw errValidation({
|
|
442
498
|
field: "in",
|
|
443
499
|
msg: msgProgramStartPast,
|
|
444
500
|
});
|
|
445
501
|
}
|
|
446
502
|
validateLocationStart({
|
|
447
|
-
|
|
503
|
+
start: opts.input.job_start_date,
|
|
448
504
|
location: opts.location,
|
|
449
505
|
field: "in",
|
|
450
506
|
msg: msgProgramStartOutsideLocation,
|
|
@@ -454,17 +510,16 @@ function validateLocationStart(opts) {
|
|
|
454
510
|
if (opts.location.type === LocationTypePersistent) {
|
|
455
511
|
return;
|
|
456
512
|
}
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
if (
|
|
460
|
-
location_end_ms === undefined ||
|
|
461
|
-
location_end_ms < location_start_ms) {
|
|
513
|
+
const start = localDateFromLocation(opts.location);
|
|
514
|
+
const end = opts.location.end_date;
|
|
515
|
+
if (compareLocalDate(end, start) < 0) {
|
|
462
516
|
throw errValidation({
|
|
463
517
|
field: opts.field,
|
|
464
518
|
msg: msgLocationRangeInvalid,
|
|
465
519
|
});
|
|
466
520
|
}
|
|
467
|
-
if (opts.
|
|
521
|
+
if (compareLocalDate(opts.start, start) < 0 ||
|
|
522
|
+
compareLocalDate(opts.start, end) > 0) {
|
|
468
523
|
throw errValidation({
|
|
469
524
|
field: opts.field,
|
|
470
525
|
msg: opts.msg,
|
|
@@ -472,7 +527,7 @@ function validateLocationStart(opts) {
|
|
|
472
527
|
}
|
|
473
528
|
}
|
|
474
529
|
function isCurrentProgramDay(input, now) {
|
|
475
|
-
const day =
|
|
530
|
+
const day = localDateParts(now, input.timezone);
|
|
476
531
|
if (day === undefined) {
|
|
477
532
|
throw errValidation({
|
|
478
533
|
field: "in",
|
|
@@ -483,7 +538,26 @@ function isCurrentProgramDay(input, now) {
|
|
|
483
538
|
input.job_start_date.m === day.m &&
|
|
484
539
|
input.job_start_date.d === day.d);
|
|
485
540
|
}
|
|
486
|
-
function
|
|
541
|
+
function locationTimezone(location, field) {
|
|
542
|
+
const out = location.address_timezone.trim();
|
|
543
|
+
if (out === "") {
|
|
544
|
+
throw errValidation({
|
|
545
|
+
field,
|
|
546
|
+
msg: msgTimezoneInvalid,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
return out;
|
|
550
|
+
}
|
|
551
|
+
function localDateFromLocation(location) {
|
|
552
|
+
return {
|
|
553
|
+
y: location.y,
|
|
554
|
+
m: location.m,
|
|
555
|
+
d: location.d,
|
|
556
|
+
h: location.h,
|
|
557
|
+
min: location.min,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function localDateParts(now, timezone) {
|
|
487
561
|
const time_zone = timezone.trim();
|
|
488
562
|
if (time_zone === "") {
|
|
489
563
|
return undefined;
|
|
@@ -495,6 +569,9 @@ function localDayParts(now, timezone) {
|
|
|
495
569
|
year: "numeric",
|
|
496
570
|
month: "numeric",
|
|
497
571
|
day: "numeric",
|
|
572
|
+
hour: "numeric",
|
|
573
|
+
minute: "numeric",
|
|
574
|
+
hourCycle: "h23",
|
|
498
575
|
}).formatToParts(now);
|
|
499
576
|
}
|
|
500
577
|
catch {
|
|
@@ -503,15 +580,40 @@ function localDayParts(now, timezone) {
|
|
|
503
580
|
const y = datePart(parts, "year");
|
|
504
581
|
const m = datePart(parts, "month");
|
|
505
582
|
const d = datePart(parts, "day");
|
|
506
|
-
|
|
583
|
+
const h = datePart(parts, "hour");
|
|
584
|
+
const min = datePart(parts, "minute");
|
|
585
|
+
if (y === undefined ||
|
|
586
|
+
m === undefined ||
|
|
587
|
+
d === undefined ||
|
|
588
|
+
h === undefined ||
|
|
589
|
+
min === undefined) {
|
|
507
590
|
return undefined;
|
|
508
591
|
}
|
|
509
592
|
return {
|
|
510
593
|
y,
|
|
511
594
|
m,
|
|
512
595
|
d,
|
|
596
|
+
h,
|
|
597
|
+
min,
|
|
513
598
|
};
|
|
514
599
|
}
|
|
600
|
+
function compareLocalDate(a, b) {
|
|
601
|
+
for (const [left, right] of [
|
|
602
|
+
[a.y, b.y],
|
|
603
|
+
[a.m, b.m],
|
|
604
|
+
[a.d, b.d],
|
|
605
|
+
[a.h, b.h],
|
|
606
|
+
[a.min, b.min],
|
|
607
|
+
]) {
|
|
608
|
+
if (left < right) {
|
|
609
|
+
return -1;
|
|
610
|
+
}
|
|
611
|
+
if (left > right) {
|
|
612
|
+
return 1;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return 0;
|
|
616
|
+
}
|
|
515
617
|
function datePart(parts, type) {
|
|
516
618
|
const raw = parts.find((part) => part.type === type)?.value;
|
|
517
619
|
if (raw === undefined) {
|
|
@@ -523,27 +625,6 @@ function datePart(parts, type) {
|
|
|
523
625
|
}
|
|
524
626
|
return out;
|
|
525
627
|
}
|
|
526
|
-
function utcMilliseconds(v, field) {
|
|
527
|
-
const out = parsedUTC(v);
|
|
528
|
-
if (out === undefined) {
|
|
529
|
-
throw errValidation({
|
|
530
|
-
field,
|
|
531
|
-
msg: msgDateInvalid,
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
return out;
|
|
535
|
-
}
|
|
536
|
-
function parsedUTC(v) {
|
|
537
|
-
const raw = v.trim();
|
|
538
|
-
if (raw === "") {
|
|
539
|
-
return undefined;
|
|
540
|
-
}
|
|
541
|
-
const out = globalThis.Date.parse(raw);
|
|
542
|
-
if (!Number.isFinite(out)) {
|
|
543
|
-
return undefined;
|
|
544
|
-
}
|
|
545
|
-
return out;
|
|
546
|
-
}
|
|
547
628
|
function requiredID(v, field, msg) {
|
|
548
629
|
const out = resolvedID(v);
|
|
549
630
|
if (out === undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabriktor/fx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.75",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"typescript": "^6.0.3"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@fabriktor/client": "0.0.
|
|
21
|
-
"@fabriktor/schema": "0.0.
|
|
20
|
+
"@fabriktor/client": "0.0.67",
|
|
21
|
+
"@fabriktor/schema": "0.0.49"
|
|
22
22
|
}
|
|
23
23
|
}
|