@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.
@@ -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>;
@@ -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 = "Program timezone is invalid.";
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
- validateJobStart(opts, this.now());
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
- const next = new Set(current_members);
267
- const removed = normalizeIDs(opts.location.current_members).filter((member_id) => !next.has(member_id));
268
- if (removed.length > 0) {
269
- const out = await this.jobs.queryJobs({
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, now) {
424
- const start_ms = utcMilliseconds(opts.in.utc, "in");
425
- if (!opts.start_immediately && start_ms < now.getTime()) {
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
- start_ms,
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 start_ms = utcMilliseconds(opts.input.job_start_date.utc, "in");
440
- if (!opts.current_day && start_ms < opts.now.getTime()) {
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
- start_ms,
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 location_start_ms = parsedUTC(opts.location.utc);
458
- const location_end_ms = parsedUTC(opts.location.end_date.utc);
459
- if (location_start_ms === undefined ||
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.start_ms < location_start_ms || opts.start_ms > location_end_ms) {
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 = localDayParts(now, input.timezone);
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 localDayParts(now, timezone) {
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
- if (y === undefined || m === undefined || d === undefined) {
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.72",
3
+ "version": "0.0.75",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
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.65",
21
- "@fabriktor/schema": "0.0.47"
20
+ "@fabriktor/client": "0.0.67",
21
+ "@fabriktor/schema": "0.0.49"
22
22
  }
23
23
  }