@freighttech/terminal-tracking 0.7.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/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/generated/entities/terminal_config/index.js +53 -0
- package/dist/generated/entities/terminal_config/index.js.map +7 -0
- package/dist/generated/entities/terminal_event/index.js +59 -0
- package/dist/generated/entities/terminal_event/index.js.map +7 -0
- package/dist/generated/entities/terminal_tracking_job/index.js +37 -0
- package/dist/generated/entities/terminal_tracking_job/index.js.map +7 -0
- package/dist/generated/entities/terminal_vessel_visit/index.js +41 -0
- package/dist/generated/entities/terminal_vessel_visit/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +26 -0
- package/dist/generated/entities.ids.generated.js.map +7 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/terminal_tracking/acl.js +14 -0
- package/dist/modules/terminal_tracking/acl.js.map +7 -0
- package/dist/modules/terminal_tracking/api/openapi.js +27 -0
- package/dist/modules/terminal_tracking/api/openapi.js.map +7 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/route.js +140 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/test/route.js +58 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/test/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/details/route.js +109 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/details/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/poll/route.js +58 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/poll/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/route.js +189 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/utils.js +12 -0
- package/dist/modules/terminal_tracking/api/utils.js.map +7 -0
- package/dist/modules/terminal_tracking/api/vessel-visits/route.js +83 -0
- package/dist/modules/terminal_tracking/api/vessel-visits/route.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.js +669 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.meta.js +19 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.meta.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.js +275 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.meta.js +19 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.meta.js.map +7 -0
- package/dist/modules/terminal_tracking/ce.js +35 -0
- package/dist/modules/terminal_tracking/ce.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/index.js +4 -0
- package/dist/modules/terminal_tracking/commands/index.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/terminal-configs.js +107 -0
- package/dist/modules/terminal_tracking/commands/terminal-configs.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/terminal-tracking.js +27 -0
- package/dist/modules/terminal_tracking/commands/terminal-tracking.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/tracking-jobs.js +56 -0
- package/dist/modules/terminal_tracking/commands/tracking-jobs.js.map +7 -0
- package/dist/modules/terminal_tracking/components/ContainerDetailsDrawer.js +371 -0
- package/dist/modules/terminal_tracking/components/ContainerDetailsDrawer.js.map +7 -0
- package/dist/modules/terminal_tracking/data/entities.js +337 -0
- package/dist/modules/terminal_tracking/data/entities.js.map +7 -0
- package/dist/modules/terminal_tracking/data/validators.js +98 -0
- package/dist/modules/terminal_tracking/data/validators.js.map +7 -0
- package/dist/modules/terminal_tracking/di.js +39 -0
- package/dist/modules/terminal_tracking/di.js.map +7 -0
- package/dist/modules/terminal_tracking/encryption.js +14 -0
- package/dist/modules/terminal_tracking/encryption.js.map +7 -0
- package/dist/modules/terminal_tracking/events.js +45 -0
- package/dist/modules/terminal_tracking/events.js.map +7 -0
- package/dist/modules/terminal_tracking/i18n/de.json +175 -0
- package/dist/modules/terminal_tracking/i18n/en.json +178 -0
- package/dist/modules/terminal_tracking/i18n/es.json +175 -0
- package/dist/modules/terminal_tracking/i18n/pl.json +206 -0
- package/dist/modules/terminal_tracking/index.js +15 -0
- package/dist/modules/terminal_tracking/index.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/adapter.js +164 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/auth/basic.js +23 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/auth/basic.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/incos-semantics.js +146 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/incos-semantics.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/types.js +1 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/types.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/adapter.js +106 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/auth/token.js +120 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/auth/token.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/gct-semantics.js +77 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/gct-semantics.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/types.js +1 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/types.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/index.js +12 -0
- package/dist/modules/terminal_tracking/lib/adapters/index.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/adapter.js +121 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/auth/ropc.js +81 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/auth/ropc.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/http.js +66 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/http.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/n4-semantics.js +95 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/n4-semantics.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.js +70 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.js +44 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/proxy.js +15 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/proxy.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/types.js +1 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/types.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/zoned-time.js +27 -0
- package/dist/modules/terminal_tracking/lib/adapters/zoned-time.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/availability.js +40 -0
- package/dist/modules/terminal_tracking/lib/availability.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/ensure-schedule.js +51 -0
- package/dist/modules/terminal_tracking/lib/ensure-schedule.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/holds.js +240 -0
- package/dist/modules/terminal_tracking/lib/holds.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/logger.js +19 -0
- package/dist/modules/terminal_tracking/lib/logger.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/rate-limiter.js +30 -0
- package/dist/modules/terminal_tracking/lib/rate-limiter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/stops.js +48 -0
- package/dist/modules/terminal_tracking/lib/stops.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/terminal-adapter.js +1 -0
- package/dist/modules/terminal_tracking/lib/terminal-adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/totp.js +41 -0
- package/dist/modules/terminal_tracking/lib/totp.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.js +101 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.js +39 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.js +13 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.js +15 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.js +13 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.js.map +7 -0
- package/dist/modules/terminal_tracking/services/terminalMatcherService.js +61 -0
- package/dist/modules/terminal_tracking/services/terminalMatcherService.js.map +7 -0
- package/dist/modules/terminal_tracking/services/terminalRegistry.js +24 -0
- package/dist/modules/terminal_tracking/services/terminalRegistry.js.map +7 -0
- package/dist/modules/terminal_tracking/services/terminalTrackingService.js +687 -0
- package/dist/modules/terminal_tracking/services/terminalTrackingService.js.map +7 -0
- package/dist/modules/terminal_tracking/setup.js +23 -0
- package/dist/modules/terminal_tracking/setup.js.map +7 -0
- package/dist/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.js +22 -0
- package/dist/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.js.map +7 -0
- package/generated/entities/terminal_config/index.ts +36 -0
- package/generated/entities/terminal_event/index.ts +39 -0
- package/generated/entities/terminal_tracking_job/index.ts +28 -0
- package/generated/entities/terminal_vessel_visit/index.ts +30 -0
- package/generated/entities.ids.generated.ts +52 -0
- package/package.json +80 -0
- package/src/index.ts +44 -0
- package/src/modules/terminal_tracking/__integration__/TC-TRACK-301-gct-config-test-connection.spec.ts +66 -0
- package/src/modules/terminal_tracking/__integration__/TC-TRACK-317-bct-config-test-connection.spec.ts +68 -0
- package/src/modules/terminal_tracking/__integration__/helpers.ts +148 -0
- package/src/modules/terminal_tracking/__integration__/meta.ts +4 -0
- package/src/modules/terminal_tracking/acl.ts +10 -0
- package/src/modules/terminal_tracking/api/openapi.ts +28 -0
- package/src/modules/terminal_tracking/api/terminal-configs/route.ts +140 -0
- package/src/modules/terminal_tracking/api/terminal-configs/test/route.ts +64 -0
- package/src/modules/terminal_tracking/api/tracking-jobs/details/route.ts +116 -0
- package/src/modules/terminal_tracking/api/tracking-jobs/poll/route.ts +64 -0
- package/src/modules/terminal_tracking/api/tracking-jobs/route.ts +243 -0
- package/src/modules/terminal_tracking/api/utils.ts +10 -0
- package/src/modules/terminal_tracking/api/vessel-visits/route.ts +95 -0
- package/src/modules/terminal_tracking/backend/terminal-configs/page.meta.ts +16 -0
- package/src/modules/terminal_tracking/backend/terminal-configs/page.tsx +728 -0
- package/src/modules/terminal_tracking/backend/terminal-containers/page.meta.ts +16 -0
- package/src/modules/terminal_tracking/backend/terminal-containers/page.tsx +328 -0
- package/src/modules/terminal_tracking/ce.ts +31 -0
- package/src/modules/terminal_tracking/commands/index.ts +3 -0
- package/src/modules/terminal_tracking/commands/terminal-configs.ts +132 -0
- package/src/modules/terminal_tracking/commands/terminal-tracking.ts +34 -0
- package/src/modules/terminal_tracking/commands/tracking-jobs.ts +69 -0
- package/src/modules/terminal_tracking/components/ContainerDetailsDrawer.tsx +540 -0
- package/src/modules/terminal_tracking/data/entities.ts +386 -0
- package/src/modules/terminal_tracking/data/validators.ts +113 -0
- package/src/modules/terminal_tracking/di.ts +46 -0
- package/src/modules/terminal_tracking/encryption.ts +12 -0
- package/src/modules/terminal_tracking/events.ts +48 -0
- package/src/modules/terminal_tracking/i18n/de.json +175 -0
- package/src/modules/terminal_tracking/i18n/en.json +178 -0
- package/src/modules/terminal_tracking/i18n/es.json +175 -0
- package/src/modules/terminal_tracking/i18n/pl.json +206 -0
- package/src/modules/terminal_tracking/index.ts +13 -0
- package/src/modules/terminal_tracking/lib/__tests__/availability.test.ts +136 -0
- package/src/modules/terminal_tracking/lib/__tests__/ensure-schedule.test.ts +67 -0
- package/src/modules/terminal_tracking/lib/__tests__/holds.test.ts +73 -0
- package/src/modules/terminal_tracking/lib/__tests__/stops.test.ts +80 -0
- package/src/modules/terminal_tracking/lib/__tests__/totp.test.ts +38 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/__tests__/adapter.test.ts +196 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/__tests__/incos-semantics.test.ts +303 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/adapter.ts +219 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/auth/basic.ts +34 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/incos-semantics.ts +291 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/types.ts +113 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/__tests__/adapter.test.ts +238 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/__tests__/gct-semantics.test.ts +133 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/adapter.ts +152 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/auth/token.ts +219 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/gct-semantics.ts +136 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/types.ts +58 -0
- package/src/modules/terminal_tracking/lib/adapters/index.ts +16 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/adapter-proxy.test.ts +69 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/data-table.test.ts +179 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/http.test.ts +98 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/n4-semantics.test.ts +140 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/proxy.test.ts +19 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/vessel-data-table.test.ts +67 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/adapter.ts +163 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/auth/ropc.ts +104 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/http.ts +127 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/n4-semantics.ts +156 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.ts +102 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.ts +60 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/proxy.ts +24 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/types.ts +56 -0
- package/src/modules/terminal_tracking/lib/adapters/zoned-time.ts +65 -0
- package/src/modules/terminal_tracking/lib/availability.ts +119 -0
- package/src/modules/terminal_tracking/lib/ensure-schedule.ts +83 -0
- package/src/modules/terminal_tracking/lib/holds.ts +344 -0
- package/src/modules/terminal_tracking/lib/logger.ts +21 -0
- package/src/modules/terminal_tracking/lib/rate-limiter.ts +52 -0
- package/src/modules/terminal_tracking/lib/stops.ts +124 -0
- package/src/modules/terminal_tracking/lib/terminal-adapter.ts +136 -0
- package/src/modules/terminal_tracking/lib/totp.ts +63 -0
- package/src/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.ts +104 -0
- package/src/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.ts +40 -0
- package/src/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.ts +11 -0
- package/src/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.ts +13 -0
- package/src/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.ts +16 -0
- package/src/modules/terminal_tracking/services/__tests__/availabilityEmit.test.ts +277 -0
- package/src/modules/terminal_tracking/services/__tests__/createJob.test.ts +59 -0
- package/src/modules/terminal_tracking/services/__tests__/pollAllBatch.test.ts +147 -0
- package/src/modules/terminal_tracking/services/__tests__/terminalMatcher.test.ts +70 -0
- package/src/modules/terminal_tracking/services/__tests__/vesselResolution.test.ts +453 -0
- package/src/modules/terminal_tracking/services/terminalMatcherService.ts +97 -0
- package/src/modules/terminal_tracking/services/terminalRegistry.ts +30 -0
- package/src/modules/terminal_tracking/services/terminalTrackingService.ts +912 -0
- package/src/modules/terminal_tracking/setup.ts +24 -0
- package/src/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.ts +32 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { acquireToken, invalidateToken, TerminalAuthError } from "./auth/ropc.js";
|
|
2
|
+
import { parseUnitDataTable } from "./parsers/data-table.js";
|
|
3
|
+
import { parseVesselDataTable } from "./parsers/vessel-data-table.js";
|
|
4
|
+
import { mapRowToEvent, parseN4DateTime } from "./n4-semantics.js";
|
|
5
|
+
import { n4Request } from "./http.js";
|
|
6
|
+
import { terminalLogger } from "../../logger.js";
|
|
7
|
+
function joinUrl(base, path) {
|
|
8
|
+
return `${base.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
9
|
+
}
|
|
10
|
+
function parseN4Json(text, terminalCode, what) {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(text);
|
|
13
|
+
} catch {
|
|
14
|
+
throw new Error(`${what} response was not valid JSON for ${terminalCode}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
class N4TerminalAdapter {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.adapterType = "n4";
|
|
20
|
+
this.defaultEndpoints = {
|
|
21
|
+
unit: "/unit",
|
|
22
|
+
vessel: "/VESSEL",
|
|
23
|
+
trainVisits: "/TRAIN_VISITS"
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async getUnitMany(containerNumbers, config, token) {
|
|
27
|
+
const joined = containerNumbers.map((c) => encodeURIComponent(c)).join(",");
|
|
28
|
+
const url = `${joinUrl(config.baseUrl, config.endpoints.unit)}?UNIT_NBR=${joined}`;
|
|
29
|
+
const { status, ok, text } = await n4Request(
|
|
30
|
+
url,
|
|
31
|
+
{ method: "GET", headers: { Authorization: `Bearer ${token}`, Accept: "application/json" } },
|
|
32
|
+
{ proxyUrl: config.proxyUrl, label: `${config.terminalCode} /unit` }
|
|
33
|
+
);
|
|
34
|
+
if (status === 401) {
|
|
35
|
+
invalidateToken(config);
|
|
36
|
+
throw new TerminalAuthError(`Unit request unauthorized (401) for ${config.terminalCode}`);
|
|
37
|
+
}
|
|
38
|
+
if (!ok) {
|
|
39
|
+
throw new Error(`Unit request failed (${status}) for ${config.terminalCode}`);
|
|
40
|
+
}
|
|
41
|
+
return parseN4Json(text, config.terminalCode, "Unit");
|
|
42
|
+
}
|
|
43
|
+
async getVessel(visitRef, config, token) {
|
|
44
|
+
const url = `${joinUrl(config.baseUrl, config.endpoints.vessel)}?VISIT_REF=${encodeURIComponent(visitRef)}`;
|
|
45
|
+
const { status, ok, text } = await n4Request(
|
|
46
|
+
url,
|
|
47
|
+
{ method: "GET", headers: { Authorization: `Bearer ${token}`, Accept: "application/json" } },
|
|
48
|
+
{ proxyUrl: config.proxyUrl, label: `${config.terminalCode} /VESSEL` }
|
|
49
|
+
);
|
|
50
|
+
if (status === 401) {
|
|
51
|
+
invalidateToken(config);
|
|
52
|
+
throw new TerminalAuthError(`Vessel request unauthorized (401) for ${config.terminalCode}`);
|
|
53
|
+
}
|
|
54
|
+
if (!ok) {
|
|
55
|
+
throw new Error(`Vessel request failed (${status}) for ${config.terminalCode}`);
|
|
56
|
+
}
|
|
57
|
+
return parseN4Json(text, config.terminalCode, "Vessel");
|
|
58
|
+
}
|
|
59
|
+
async fetchVesselVisit(input) {
|
|
60
|
+
const { visitRef, config } = input;
|
|
61
|
+
if (!config.endpoints.vessel) return null;
|
|
62
|
+
const token = await acquireToken(config);
|
|
63
|
+
const body = await this.getVessel(visitRef, config, token);
|
|
64
|
+
const row = parseVesselDataTable(body);
|
|
65
|
+
if (!row) return null;
|
|
66
|
+
return {
|
|
67
|
+
visitRef: row.visitRef,
|
|
68
|
+
vesselName: row.vesselName,
|
|
69
|
+
ibVoyage: row.ibVoyage,
|
|
70
|
+
obVoyage: row.obVoyage,
|
|
71
|
+
line: row.line,
|
|
72
|
+
phase: row.phase,
|
|
73
|
+
eta: parseN4DateTime(row.eta),
|
|
74
|
+
etd: parseN4DateTime(row.etd),
|
|
75
|
+
ata: parseN4DateTime(row.ata),
|
|
76
|
+
atd: parseN4DateTime(row.atd),
|
|
77
|
+
beginReceive: parseN4DateTime(row.beginReceive),
|
|
78
|
+
dryCutoff: parseN4DateTime(row.dryCutoff),
|
|
79
|
+
rawData: row.raw
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async fetchEvents(input) {
|
|
83
|
+
const { containerNumber, config } = input;
|
|
84
|
+
const { events } = await this.fetchEventsBatch({ containerNumbers: [containerNumber], config });
|
|
85
|
+
return { events, containerNumber };
|
|
86
|
+
}
|
|
87
|
+
async fetchEventsBatch(input) {
|
|
88
|
+
const { containerNumbers, config } = input;
|
|
89
|
+
if (containerNumbers.length === 0) return { events: [] };
|
|
90
|
+
const token = await acquireToken(config);
|
|
91
|
+
const body = await this.getUnitMany(containerNumbers, config, token);
|
|
92
|
+
const rows = parseUnitDataTable(body);
|
|
93
|
+
const events = [];
|
|
94
|
+
for (const row of rows) {
|
|
95
|
+
const event = mapRowToEvent(row, config);
|
|
96
|
+
if (event) events.push(event);
|
|
97
|
+
}
|
|
98
|
+
terminalLogger.debug("fetchEventsBatch", {
|
|
99
|
+
terminalCode: config.terminalCode,
|
|
100
|
+
containers: containerNumbers.length,
|
|
101
|
+
rows: rows.length,
|
|
102
|
+
events: events.length
|
|
103
|
+
});
|
|
104
|
+
return { events };
|
|
105
|
+
}
|
|
106
|
+
async testConnection(config) {
|
|
107
|
+
const started = Date.now();
|
|
108
|
+
try {
|
|
109
|
+
await acquireToken(config);
|
|
110
|
+
return { success: true, message: "Token acquired", latencyMs: Date.now() - started };
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const code = err instanceof TerminalAuthError ? err.code : void 0;
|
|
113
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
114
|
+
return { success: false, message: code ? `${message} (${code})` : message, latencyMs: Date.now() - started };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export {
|
|
119
|
+
N4TerminalAdapter
|
|
120
|
+
};
|
|
121
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/terminal_tracking/lib/adapters/n4/adapter.ts"],
|
|
4
|
+
"sourcesContent": ["import type {\n TerminalAdapter,\n TerminalFetchResult,\n TerminalFetchedEvent,\n TerminalAdapterTestResult,\n ResolvedTerminalConfig,\n NormalizedVesselVisit,\n} from '../../terminal-adapter'\nimport { acquireToken, invalidateToken, TerminalAuthError } from './auth/ropc'\nimport { parseUnitDataTable, type N4DataTableResponse } from './parsers/data-table'\nimport { parseVesselDataTable } from './parsers/vessel-data-table'\nimport { mapRowToEvent, parseN4DateTime } from './n4-semantics'\nimport { n4Request } from './http'\nimport { terminalLogger } from '../../logger'\n\nfunction joinUrl(base: string, path: string): string {\n return `${base.replace(/\\/$/, '')}/${path.replace(/^\\//, '')}`\n}\n\n/** Parse an N4 JSON body, turning a malformed payload into a clear adapter error. */\nfunction parseN4Json(text: string, terminalCode: string, what: string): N4DataTableResponse {\n try {\n return JSON.parse(text) as N4DataTableResponse\n } catch {\n throw new Error(`${what} response was not valid JSON for ${terminalCode}`)\n }\n}\n\n/**\n * Config-driven adapter for Navis N4 terminals. One instance serves all N4\n * terminals; per-terminal differences live in the TerminalConfig.\n */\nexport class N4TerminalAdapter implements TerminalAdapter {\n readonly adapterType = 'n4'\n\n readonly defaultEndpoints = {\n unit: '/unit',\n vessel: '/VESSEL',\n trainVisits: '/TRAIN_VISITS',\n }\n\n private async getUnitMany(\n containerNumbers: string[],\n config: ResolvedTerminalConfig,\n token: string,\n ): Promise<N4DataTableResponse> {\n // N4 /unit accepts a comma-separated UNIT_NBR list; encode each part so the\n // commas stay as the list separator.\n const joined = containerNumbers.map((c) => encodeURIComponent(c)).join(',')\n const url = `${joinUrl(config.baseUrl, config.endpoints.unit)}?UNIT_NBR=${joined}`\n const { status, ok, text } = await n4Request(\n url,\n { method: 'GET', headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } },\n { proxyUrl: config.proxyUrl, label: `${config.terminalCode} /unit` },\n )\n if (status === 401) {\n invalidateToken(config)\n throw new TerminalAuthError(`Unit request unauthorized (401) for ${config.terminalCode}`)\n }\n if (!ok) {\n throw new Error(`Unit request failed (${status}) for ${config.terminalCode}`)\n }\n return parseN4Json(text, config.terminalCode, 'Unit')\n }\n\n private async getVessel(\n visitRef: string,\n config: ResolvedTerminalConfig,\n token: string,\n ): Promise<N4DataTableResponse> {\n const url = `${joinUrl(config.baseUrl, config.endpoints.vessel as string)}?VISIT_REF=${encodeURIComponent(visitRef)}`\n const { status, ok, text } = await n4Request(\n url,\n { method: 'GET', headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } },\n { proxyUrl: config.proxyUrl, label: `${config.terminalCode} /VESSEL` },\n )\n if (status === 401) {\n invalidateToken(config)\n throw new TerminalAuthError(`Vessel request unauthorized (401) for ${config.terminalCode}`)\n }\n if (!ok) {\n throw new Error(`Vessel request failed (${status}) for ${config.terminalCode}`)\n }\n return parseN4Json(text, config.terminalCode, 'Vessel')\n }\n\n async fetchVesselVisit(input: {\n visitRef: string\n config: ResolvedTerminalConfig\n }): Promise<NormalizedVesselVisit | null> {\n const { visitRef, config } = input\n if (!config.endpoints.vessel) return null\n\n const token = await acquireToken(config)\n const body = await this.getVessel(visitRef, config, token)\n const row = parseVesselDataTable(body)\n if (!row) return null\n\n return {\n visitRef: row.visitRef,\n vesselName: row.vesselName,\n ibVoyage: row.ibVoyage,\n obVoyage: row.obVoyage,\n line: row.line,\n phase: row.phase,\n eta: parseN4DateTime(row.eta),\n etd: parseN4DateTime(row.etd),\n ata: parseN4DateTime(row.ata),\n atd: parseN4DateTime(row.atd),\n beginReceive: parseN4DateTime(row.beginReceive),\n dryCutoff: parseN4DateTime(row.dryCutoff),\n rawData: row.raw,\n }\n }\n\n async fetchEvents(input: {\n containerNumber: string\n config: ResolvedTerminalConfig\n }): Promise<TerminalFetchResult> {\n const { containerNumber, config } = input\n const { events } = await this.fetchEventsBatch({ containerNumbers: [containerNumber], config })\n return { events, containerNumber }\n }\n\n async fetchEventsBatch(input: {\n containerNumbers: string[]\n config: ResolvedTerminalConfig\n }): Promise<{ events: TerminalFetchedEvent[] }> {\n const { containerNumbers, config } = input\n if (containerNumbers.length === 0) return { events: [] }\n\n const token = await acquireToken(config)\n const body = await this.getUnitMany(containerNumbers, config, token)\n const rows = parseUnitDataTable(body)\n\n const events: TerminalFetchedEvent[] = []\n for (const row of rows) {\n const event = mapRowToEvent(row, config)\n if (event) events.push(event)\n }\n\n terminalLogger.debug('fetchEventsBatch', {\n terminalCode: config.terminalCode,\n containers: containerNumbers.length,\n rows: rows.length,\n events: events.length,\n })\n\n return { events }\n }\n\n async testConnection(config: ResolvedTerminalConfig): Promise<TerminalAdapterTestResult> {\n const started = Date.now()\n try {\n await acquireToken(config)\n return { success: true, message: 'Token acquired', latencyMs: Date.now() - started }\n } catch (err) {\n const code = err instanceof TerminalAuthError ? err.code : undefined\n const message = err instanceof Error ? err.message : String(err)\n return { success: false, message: code ? `${message} (${code})` : message, latencyMs: Date.now() - started }\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { n4Request } from "../http.js";
|
|
2
|
+
const tokenCache = /* @__PURE__ */ new Map();
|
|
3
|
+
class TerminalAuthError extends Error {
|
|
4
|
+
constructor(message, code) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = "TerminalAuthError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function cacheKey(config) {
|
|
11
|
+
const principal = config.authType === "oauth2_password" ? String(config.authConfig?.username ?? "") : String(config.clientId ?? "");
|
|
12
|
+
return `${config.terminalCode}:${principal}`;
|
|
13
|
+
}
|
|
14
|
+
function buildBody(config) {
|
|
15
|
+
const auth = config.authConfig ?? {};
|
|
16
|
+
const body = new URLSearchParams();
|
|
17
|
+
if (config.clientId) body.set("client_id", config.clientId);
|
|
18
|
+
if (config.scope) body.set("scope", config.scope);
|
|
19
|
+
if (config.authType === "oauth2_password") {
|
|
20
|
+
body.set("grant_type", "password");
|
|
21
|
+
body.set("username", String(auth.username ?? ""));
|
|
22
|
+
body.set("password", String(auth.password ?? ""));
|
|
23
|
+
} else {
|
|
24
|
+
body.set("grant_type", "client_credentials");
|
|
25
|
+
if (auth.clientSecret) body.set("client_secret", String(auth.clientSecret));
|
|
26
|
+
}
|
|
27
|
+
return body;
|
|
28
|
+
}
|
|
29
|
+
function extractErrorCode(text) {
|
|
30
|
+
const m = text.match(/AADB2C\d+|AADSTS\d+/);
|
|
31
|
+
if (m) return m[0];
|
|
32
|
+
try {
|
|
33
|
+
const j = JSON.parse(text);
|
|
34
|
+
return j.error;
|
|
35
|
+
} catch {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function acquireToken(config) {
|
|
40
|
+
if (!config.tokenUrl) throw new TerminalAuthError("Terminal config has no tokenUrl");
|
|
41
|
+
const key = cacheKey(config);
|
|
42
|
+
const cached = tokenCache.get(key);
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
if (cached && cached.expiresAt > now + 3e4) return cached.token;
|
|
45
|
+
const { status, ok, text } = await n4Request(
|
|
46
|
+
config.tokenUrl,
|
|
47
|
+
{
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
50
|
+
body: buildBody(config).toString()
|
|
51
|
+
},
|
|
52
|
+
{ proxyUrl: config.proxyUrl, label: `${config.terminalCode} token` }
|
|
53
|
+
);
|
|
54
|
+
if (!ok) {
|
|
55
|
+
throw new TerminalAuthError(
|
|
56
|
+
`Token request failed (${status})${extractErrorCode(text) ? `: ${extractErrorCode(text)}` : ""}`,
|
|
57
|
+
extractErrorCode(text)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(text);
|
|
63
|
+
} catch {
|
|
64
|
+
throw new TerminalAuthError("Token response was not JSON");
|
|
65
|
+
}
|
|
66
|
+
if (!parsed.access_token) {
|
|
67
|
+
throw new TerminalAuthError("No access_token in token response", extractErrorCode(text));
|
|
68
|
+
}
|
|
69
|
+
const ttlSeconds = Number(parsed.expires_in ?? 3600);
|
|
70
|
+
tokenCache.set(key, { token: parsed.access_token, expiresAt: now + ttlSeconds * 1e3 });
|
|
71
|
+
return parsed.access_token;
|
|
72
|
+
}
|
|
73
|
+
function invalidateToken(config) {
|
|
74
|
+
tokenCache.delete(cacheKey(config));
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
TerminalAuthError,
|
|
78
|
+
acquireToken,
|
|
79
|
+
invalidateToken
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=ropc.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../src/modules/terminal_tracking/lib/adapters/n4/auth/ropc.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ResolvedTerminalConfig } from '../../../terminal-adapter'\nimport { n4Request } from '../http'\n\ntype CachedToken = { token: string; expiresAt: number }\n\n// Process-level token cache, keyed by terminal + principal.\nconst tokenCache = new Map<string, CachedToken>()\n\nexport class TerminalAuthError extends Error {\n constructor(\n message: string,\n readonly code?: string,\n ) {\n super(message)\n this.name = 'TerminalAuthError'\n }\n}\n\nfunction cacheKey(config: ResolvedTerminalConfig): string {\n const principal =\n config.authType === 'oauth2_password'\n ? String((config.authConfig as Record<string, unknown>)?.username ?? '')\n : String(config.clientId ?? '')\n return `${config.terminalCode}:${principal}`\n}\n\nfunction buildBody(config: ResolvedTerminalConfig): URLSearchParams {\n const auth = (config.authConfig ?? {}) as Record<string, unknown>\n const body = new URLSearchParams()\n if (config.clientId) body.set('client_id', config.clientId)\n if (config.scope) body.set('scope', config.scope)\n\n if (config.authType === 'oauth2_password') {\n body.set('grant_type', 'password')\n body.set('username', String(auth.username ?? ''))\n body.set('password', String(auth.password ?? ''))\n } else {\n body.set('grant_type', 'client_credentials')\n if (auth.clientSecret) body.set('client_secret', String(auth.clientSecret))\n }\n return body\n}\n\n/** Extract the `AADB2C\u2026` / OAuth error code from a token error body. */\nfunction extractErrorCode(text: string): string | undefined {\n const m = text.match(/AADB2C\\d+|AADSTS\\d+/)\n if (m) return m[0]\n try {\n const j = JSON.parse(text) as { error?: string }\n return j.error\n } catch {\n return undefined\n }\n}\n\n/**\n * Acquire (and cache) an OAuth access token for the terminal. Supports\n * Azure B2C ROPC (password grant) and client-credentials. Surfaces the exact\n * `AADB2C\u2026` code on failure (e.g. AADB2C90225 for a not-yet-changed password).\n */\nexport async function acquireToken(config: ResolvedTerminalConfig): Promise<string> {\n if (!config.tokenUrl) throw new TerminalAuthError('Terminal config has no tokenUrl')\n\n const key = cacheKey(config)\n const cached = tokenCache.get(key)\n const now = Date.now()\n if (cached && cached.expiresAt > now + 30_000) return cached.token\n\n const { status, ok, text } = await n4Request(\n config.tokenUrl,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: buildBody(config).toString(),\n },\n { proxyUrl: config.proxyUrl, label: `${config.terminalCode} token` },\n )\n\n if (!ok) {\n throw new TerminalAuthError(\n `Token request failed (${status})${extractErrorCode(text) ? `: ${extractErrorCode(text)}` : ''}`,\n extractErrorCode(text),\n )\n }\n\n let parsed: { access_token?: string; expires_in?: string | number }\n try {\n parsed = JSON.parse(text)\n } catch {\n throw new TerminalAuthError('Token response was not JSON')\n }\n if (!parsed.access_token) {\n throw new TerminalAuthError('No access_token in token response', extractErrorCode(text))\n }\n\n const ttlSeconds = Number(parsed.expires_in ?? 3600)\n tokenCache.set(key, { token: parsed.access_token, expiresAt: now + ttlSeconds * 1000 })\n return parsed.access_token\n}\n\n/** Clear the cached token for a terminal (used after auth failures). */\nexport function invalidateToken(config: ResolvedTerminalConfig): void {\n tokenCache.delete(cacheKey(config))\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,iBAAiB;AAK1B,MAAM,aAAa,oBAAI,IAAyB;AAEzC,MAAM,0BAA0B,MAAM;AAAA,EAC3C,YACE,SACS,MACT;AACA,UAAM,OAAO;AAFJ;AAGT,SAAK,OAAO;AAAA,EACd;AACF;AAEA,SAAS,SAAS,QAAwC;AACxD,QAAM,YACJ,OAAO,aAAa,oBAChB,OAAQ,OAAO,YAAwC,YAAY,EAAE,IACrE,OAAO,OAAO,YAAY,EAAE;AAClC,SAAO,GAAG,OAAO,YAAY,IAAI,SAAS;AAC5C;AAEA,SAAS,UAAU,QAAiD;AAClE,QAAM,OAAQ,OAAO,cAAc,CAAC;AACpC,QAAM,OAAO,IAAI,gBAAgB;AACjC,MAAI,OAAO,SAAU,MAAK,IAAI,aAAa,OAAO,QAAQ;AAC1D,MAAI,OAAO,MAAO,MAAK,IAAI,SAAS,OAAO,KAAK;AAEhD,MAAI,OAAO,aAAa,mBAAmB;AACzC,SAAK,IAAI,cAAc,UAAU;AACjC,SAAK,IAAI,YAAY,OAAO,KAAK,YAAY,EAAE,CAAC;AAChD,SAAK,IAAI,YAAY,OAAO,KAAK,YAAY,EAAE,CAAC;AAAA,EAClD,OAAO;AACL,SAAK,IAAI,cAAc,oBAAoB;AAC3C,QAAI,KAAK,aAAc,MAAK,IAAI,iBAAiB,OAAO,KAAK,YAAY,CAAC;AAAA,EAC5E;AACA,SAAO;AACT;AAGA,SAAS,iBAAiB,MAAkC;AAC1D,QAAM,IAAI,KAAK,MAAM,qBAAqB;AAC1C,MAAI,EAAG,QAAO,EAAE,CAAC;AACjB,MAAI;AACF,UAAM,IAAI,KAAK,MAAM,IAAI;AACzB,WAAO,EAAE;AAAA,EACX,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOA,eAAsB,aAAa,QAAiD;AAClF,MAAI,CAAC,OAAO,SAAU,OAAM,IAAI,kBAAkB,iCAAiC;AAEnF,QAAM,MAAM,SAAS,MAAM;AAC3B,QAAM,SAAS,WAAW,IAAI,GAAG;AACjC,QAAM,MAAM,KAAK,IAAI;AACrB,MAAI,UAAU,OAAO,YAAY,MAAM,IAAQ,QAAO,OAAO;AAE7D,QAAM,EAAE,QAAQ,IAAI,KAAK,IAAI,MAAM;AAAA,IACjC,OAAO;AAAA,IACP;AAAA,MACE,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,MAC/D,MAAM,UAAU,MAAM,EAAE,SAAS;AAAA,IACnC;AAAA,IACA,EAAE,UAAU,OAAO,UAAU,OAAO,GAAG,OAAO,YAAY,SAAS;AAAA,EACrE;AAEA,MAAI,CAAC,IAAI;AACP,UAAM,IAAI;AAAA,MACR,yBAAyB,MAAM,IAAI,iBAAiB,IAAI,IAAI,KAAK,iBAAiB,IAAI,CAAC,KAAK,EAAE;AAAA,MAC9F,iBAAiB,IAAI;AAAA,IACvB;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,IAAI;AAAA,EAC1B,QAAQ;AACN,UAAM,IAAI,kBAAkB,6BAA6B;AAAA,EAC3D;AACA,MAAI,CAAC,OAAO,cAAc;AACxB,UAAM,IAAI,kBAAkB,qCAAqC,iBAAiB,IAAI,CAAC;AAAA,EACzF;AAEA,QAAM,aAAa,OAAO,OAAO,cAAc,IAAI;AACnD,aAAW,IAAI,KAAK,EAAE,OAAO,OAAO,cAAc,WAAW,MAAM,aAAa,IAAK,CAAC;AACtF,SAAO,OAAO;AAChB;AAGO,SAAS,gBAAgB,QAAsC;AACpE,aAAW,OAAO,SAAS,MAAM,CAAC;AACpC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { fetch } from "undici";
|
|
2
|
+
import { proxyDispatcher } from "./proxy.js";
|
|
3
|
+
import { terminalLogger } from "../../logger.js";
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
5
|
+
const DEFAULT_RETRIES = 2;
|
|
6
|
+
const DEFAULT_BACKOFF_MS = 250;
|
|
7
|
+
const TRANSIENT_CODES = /* @__PURE__ */ new Set([
|
|
8
|
+
"UND_ERR_SOCKET",
|
|
9
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
10
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
11
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
12
|
+
"ECONNRESET",
|
|
13
|
+
"ECONNREFUSED",
|
|
14
|
+
"EPIPE",
|
|
15
|
+
"ETIMEDOUT",
|
|
16
|
+
"EAI_AGAIN"
|
|
17
|
+
]);
|
|
18
|
+
function isTransientNetworkError(err) {
|
|
19
|
+
if (err instanceof DOMException && err.name === "TimeoutError") return true;
|
|
20
|
+
const e = err;
|
|
21
|
+
const code = e?.code ?? e?.cause?.code;
|
|
22
|
+
if (code && TRANSIENT_CODES.has(code)) return true;
|
|
23
|
+
const msg = `${e?.message ?? ""} ${e?.cause?.message ?? ""}`;
|
|
24
|
+
return /terminated|other side closed|socket hang ?up|fetch failed/i.test(msg);
|
|
25
|
+
}
|
|
26
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
27
|
+
async function n4Request(url, init, options = {}) {
|
|
28
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
29
|
+
const retries = options.retries ?? DEFAULT_RETRIES;
|
|
30
|
+
const backoffMs = options.retryBackoffMs ?? DEFAULT_BACKOFF_MS;
|
|
31
|
+
const dispatcher = proxyDispatcher(options.proxyUrl);
|
|
32
|
+
const label = options.label ?? url;
|
|
33
|
+
let lastErr;
|
|
34
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(url, {
|
|
37
|
+
...init,
|
|
38
|
+
dispatcher,
|
|
39
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
40
|
+
});
|
|
41
|
+
const text = await res.text();
|
|
42
|
+
return { status: res.status, ok: res.ok, text };
|
|
43
|
+
} catch (err) {
|
|
44
|
+
lastErr = err;
|
|
45
|
+
if (attempt < retries && isTransientNetworkError(err)) {
|
|
46
|
+
const wait = backoffMs * 2 ** attempt;
|
|
47
|
+
terminalLogger.warn("N4 request transient failure \u2014 retrying", {
|
|
48
|
+
label,
|
|
49
|
+
attempt: attempt + 1,
|
|
50
|
+
maxAttempts: retries + 1,
|
|
51
|
+
waitMs: wait,
|
|
52
|
+
message: err instanceof Error ? err.message : String(err)
|
|
53
|
+
});
|
|
54
|
+
await sleep(wait);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw lastErr;
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
isTransientNetworkError,
|
|
64
|
+
n4Request
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/terminal_tracking/lib/adapters/n4/http.ts"],
|
|
4
|
+
"sourcesContent": ["import { fetch, type RequestInit as UndiciRequestInit } from 'undici'\nimport { proxyDispatcher, type FetchInitWithDispatcher } from './proxy'\nimport { terminalLogger } from '../../logger'\n\n/** Outcome of an N4 HTTP request: status + the fully-read response body. */\nexport type N4Response = { status: number; ok: boolean; text: string }\n\nexport type N4RequestOptions = {\n /** Terminal proxy URL \u2014 egresses through an undici ProxyAgent when set. */\n proxyUrl?: string | null\n /** Per-attempt timeout (whole attempt: connect + headers + body). Default 30s. */\n timeoutMs?: number\n /** Extra attempts after the first, on transient network failures. Default 2. */\n retries?: number\n /** Base backoff between attempts (doubles each retry). Default 250ms. */\n retryBackoffMs?: number\n /** Short label for logs (e.g. `\"<terminalCode> /unit\"`). */\n label?: string\n}\n\nconst DEFAULT_TIMEOUT_MS = 30_000\nconst DEFAULT_RETRIES = 2\nconst DEFAULT_BACKOFF_MS = 250\n\n// undici raises these for a dropped/half-open/timed-out connection \u2014 all safe to\n// retry (the request either never reached the server or got no usable response).\nconst TRANSIENT_CODES = new Set([\n 'UND_ERR_SOCKET',\n 'UND_ERR_CONNECT_TIMEOUT',\n 'UND_ERR_HEADERS_TIMEOUT',\n 'UND_ERR_BODY_TIMEOUT',\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'EPIPE',\n 'ETIMEDOUT',\n 'EAI_AGAIN',\n])\n\n/**\n * A transient network failure (connection dropped/reset/timed out) \u2014 distinct\n * from an HTTP error *status*, which is a real answer and must not be retried.\n */\nexport function isTransientNetworkError(err: unknown): boolean {\n // AbortSignal.timeout() rejects with a TimeoutError DOMException.\n if (err instanceof DOMException && err.name === 'TimeoutError') return true\n const e = err as { code?: string; message?: string; cause?: { code?: string; message?: string } }\n const code = e?.code ?? e?.cause?.code\n if (code && TRANSIENT_CODES.has(code)) return true\n // undici surfaces a mid-flight peer socket close as `TypeError: terminated`\n // (cause: UND_ERR_SOCKET \"other side closed\"); match on message as a backstop.\n const msg = `${e?.message ?? ''} ${e?.cause?.message ?? ''}`\n return /terminated|other side closed|socket hang ?up|fetch failed/i.test(msg)\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))\n\n/**\n * Perform one N4 HTTP request and ALWAYS fully read the response body \u2014 even on a\n * non-2xx status \u2014 returning `{ status, ok, text }`. Callers inspect `status`\n * and parse `text`; they never touch the live response stream.\n *\n * Why drain the body unconditionally: undici holds the keep-alive socket open\n * while a response body is unread. The previous code threw on a non-ok status\n * (e.g. Baltic Hub's Cloudflare allowlist 403) WITHOUT reading the body, leaving\n * a dangling stream. When the peer later closed that socket, undici errored the\n * orphaned body controller with no reader attached, surfacing an *unhandled*\n * `TypeError: terminated`. Under Node's default unhandled-rejection policy that\n * killed the queue-worker process and took the whole server down. Reading the\n * body inside this `try` means any mid-stream close rejects an awaited call here,\n * where it is caught and (when transient) retried \u2014 it can never escape.\n *\n * Each attempt is bounded by an AbortSignal timeout, and transient connection\n * drops are retried with exponential backoff so a single Cloudflare/tunnel\n * keep-alive reset doesn't fail an entire poll cycle.\n */\nexport async function n4Request(\n url: string,\n init: Omit<FetchInitWithDispatcher, 'dispatcher' | 'signal'>,\n options: N4RequestOptions = {},\n): Promise<N4Response> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const retries = options.retries ?? DEFAULT_RETRIES\n const backoffMs = options.retryBackoffMs ?? DEFAULT_BACKOFF_MS\n const dispatcher = proxyDispatcher(options.proxyUrl)\n const label = options.label ?? url\n\n let lastErr: unknown\n for (let attempt = 0; attempt <= retries; attempt += 1) {\n try {\n // `fetch` is imported from undici (not the global) so the request driver\n // and the `dispatcher` (a ProxyAgent from the SAME undici) belong to one\n // instance. With the global fetch \u2014 Node's *built-in* undici \u2014 driving a\n // dispatcher from this package's *bundled* undici, a peer socket reset\n // makes the bundled dispatcher call the built-in fetch's onAborted, which\n // throws `TypeError: terminated` from a detached socket-close handler that\n // no try/catch here can see \u2014 it escaped as an uncaughtException and killed\n // the worker. Same-instance, that reset rejects the awaited call below,\n // where it is caught and (when transient) retried.\n const res = await fetch(url, {\n ...init,\n dispatcher,\n signal: AbortSignal.timeout(timeoutMs),\n } as UndiciRequestInit)\n // Drain the body within this try \u2014 see the doc comment above. This is the\n // line that prevents the dangling-stream unhandled rejection.\n const text = await res.text()\n return { status: res.status, ok: res.ok, text }\n } catch (err) {\n lastErr = err\n if (attempt < retries && isTransientNetworkError(err)) {\n const wait = backoffMs * 2 ** attempt\n terminalLogger.warn('N4 request transient failure \u2014 retrying', {\n label,\n attempt: attempt + 1,\n maxAttempts: retries + 1,\n waitMs: wait,\n message: err instanceof Error ? err.message : String(err),\n })\n await sleep(wait)\n continue\n }\n throw err\n }\n }\n // The loop always returns or throws; this satisfies the type checker.\n throw lastErr\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,aAAoD;AAC7D,SAAS,uBAAqD;AAC9D,SAAS,sBAAsB;AAkB/B,MAAM,qBAAqB;AAC3B,MAAM,kBAAkB;AACxB,MAAM,qBAAqB;AAI3B,MAAM,kBAAkB,oBAAI,IAAI;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAMM,SAAS,wBAAwB,KAAuB;AAE7D,MAAI,eAAe,gBAAgB,IAAI,SAAS,eAAgB,QAAO;AACvE,QAAM,IAAI;AACV,QAAM,OAAO,GAAG,QAAQ,GAAG,OAAO;AAClC,MAAI,QAAQ,gBAAgB,IAAI,IAAI,EAAG,QAAO;AAG9C,QAAM,MAAM,GAAG,GAAG,WAAW,EAAE,IAAI,GAAG,OAAO,WAAW,EAAE;AAC1D,SAAO,6DAA6D,KAAK,GAAG;AAC9E;AAEA,MAAM,QAAQ,CAAC,OAA8B,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAqB7F,eAAsB,UACpB,KACA,MACA,UAA4B,CAAC,GACR;AACrB,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,YAAY,QAAQ,kBAAkB;AAC5C,QAAM,aAAa,gBAAgB,QAAQ,QAAQ;AACnD,QAAM,QAAQ,QAAQ,SAAS;AAE/B,MAAI;AACJ,WAAS,UAAU,GAAG,WAAW,SAAS,WAAW,GAAG;AACtD,QAAI;AAUF,YAAM,MAAM,MAAM,MAAM,KAAK;AAAA,QAC3B,GAAG;AAAA,QACH;AAAA,QACA,QAAQ,YAAY,QAAQ,SAAS;AAAA,MACvC,CAAsB;AAGtB,YAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,aAAO,EAAE,QAAQ,IAAI,QAAQ,IAAI,IAAI,IAAI,KAAK;AAAA,IAChD,SAAS,KAAK;AACZ,gBAAU;AACV,UAAI,UAAU,WAAW,wBAAwB,GAAG,GAAG;AACrD,cAAM,OAAO,YAAY,KAAK;AAC9B,uBAAe,KAAK,gDAA2C;AAAA,UAC7D;AAAA,UACA,SAAS,UAAU;AAAA,UACnB,aAAa,UAAU;AAAA,UACvB,QAAQ;AAAA,UACR,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,QAC1D,CAAC;AACD,cAAM,MAAM,IAAI;AAChB;AAAA,MACF;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAEA,QAAM;AACR;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DEFAULT_TERMINAL_TIMEZONE, zonedNaiveToUtc } from "../zoned-time.js";
|
|
2
|
+
const STATE_MAP = {
|
|
3
|
+
inbound: { code: "S20_INBOUND", eventType: "EQUIPMENT", eventCode: "GTIN", classifier: "ACT", timestampField: "timeIn" },
|
|
4
|
+
ecin: { code: "S30_ECIN", eventType: "EQUIPMENT", eventCode: "GTIN", classifier: "ACT", timestampField: "timeIn" },
|
|
5
|
+
yard: { code: "S40_YARD", eventType: "EQUIPMENT", eventCode: "DISC", classifier: "ACT", timestampField: "timeIn" },
|
|
6
|
+
loaded: { code: "S60_LOADED", eventType: "EQUIPMENT", eventCode: "LOAD", classifier: "ACT", timestampField: "loaded" },
|
|
7
|
+
departed: { code: "S70_DEPARTED", eventType: "TRANSPORT", eventCode: "DEPA", classifier: "ACT", timestampField: "timeOut" }
|
|
8
|
+
};
|
|
9
|
+
function semanticEventIdFor(eventCode) {
|
|
10
|
+
switch (eventCode) {
|
|
11
|
+
case "GTIN":
|
|
12
|
+
return "terminal_tracking.equipment.gate_in";
|
|
13
|
+
case "DISC":
|
|
14
|
+
return "terminal_tracking.equipment.discharged";
|
|
15
|
+
case "LOAD":
|
|
16
|
+
return "terminal_tracking.equipment.loaded";
|
|
17
|
+
case "DEPA":
|
|
18
|
+
return "terminal_tracking.transport.departed";
|
|
19
|
+
default:
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function buildSourceEventId(terminalCode, ufvGkey, eventCode) {
|
|
24
|
+
return `${terminalCode}:${ufvGkey}:${eventCode}`;
|
|
25
|
+
}
|
|
26
|
+
function parseN4DateTime(value, timeZone = DEFAULT_TERMINAL_TIMEZONE) {
|
|
27
|
+
if (!value) return null;
|
|
28
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/.exec(value.trim());
|
|
29
|
+
if (!m) return null;
|
|
30
|
+
const [, yyyy, mm, dd, hh, mi, ss] = m;
|
|
31
|
+
return zonedNaiveToUtc(+yyyy, +mm, +dd, +hh, +mi, ss ? +ss : 0, timeZone);
|
|
32
|
+
}
|
|
33
|
+
function inferMode(visitRef) {
|
|
34
|
+
if (!visitRef) return null;
|
|
35
|
+
const v = visitRef.toUpperCase();
|
|
36
|
+
if (v.includes("TRUCK")) return "TRUCK";
|
|
37
|
+
if (/_IMP|_EXP|\bW\d/.test(v)) return "RAIL";
|
|
38
|
+
return "VESSEL";
|
|
39
|
+
}
|
|
40
|
+
function parseCargoCategory(rawData) {
|
|
41
|
+
const category = String(rawData?.["Category"] ?? "").trim().toLowerCase();
|
|
42
|
+
if (category === "import") return "import";
|
|
43
|
+
if (category === "export") return "export";
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function pickVesselVisit(event) {
|
|
47
|
+
const category = parseCargoCategory(event.rawData);
|
|
48
|
+
if (category === "import") return event.visitRefIn ? { ref: event.visitRefIn, dir: "in" } : null;
|
|
49
|
+
if (category === "export") return event.visitRefOut ? { ref: event.visitRefOut, dir: "out" } : null;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function mapRowToEvent(row, config) {
|
|
53
|
+
const stateKey = (row.tState ?? "").trim().toLowerCase();
|
|
54
|
+
const mapping = STATE_MAP[stateKey];
|
|
55
|
+
if (!mapping) return null;
|
|
56
|
+
const eventDateTime = parseN4DateTime(row[mapping.timestampField]) ?? parseN4DateTime(row.timeOut) ?? parseN4DateTime(row.loaded) ?? parseN4DateTime(row.timeIn) ?? /* @__PURE__ */ new Date();
|
|
57
|
+
const seals = (row.seals ?? []).map((number) => ({ number, source: config.terminalCode }));
|
|
58
|
+
return {
|
|
59
|
+
source: "terminal",
|
|
60
|
+
sourceEventId: buildSourceEventId(config.terminalCode, row.ufvGkey, mapping.eventCode),
|
|
61
|
+
eventType: mapping.eventType,
|
|
62
|
+
eventCode: mapping.eventCode,
|
|
63
|
+
eventClassifierCode: mapping.classifier,
|
|
64
|
+
eventDateTime,
|
|
65
|
+
containerNumber: row.unitNbr,
|
|
66
|
+
ufvGkey: row.ufvGkey,
|
|
67
|
+
transitState: mapping.code,
|
|
68
|
+
visitState: row.vState ?? null,
|
|
69
|
+
facilityCode: config.facilityCode ?? null,
|
|
70
|
+
facilityCodeListProvider: config.facilityCodeListProvider ?? null,
|
|
71
|
+
unlocode: config.unlocode ?? null,
|
|
72
|
+
visitRefIn: row.ibActualVisit ?? null,
|
|
73
|
+
visitRefOut: row.obActualVisit ?? null,
|
|
74
|
+
vesselName: null,
|
|
75
|
+
voyageNumber: null,
|
|
76
|
+
modeOfTransport: inferMode(row.obActualVisit) ?? inferMode(row.ibActualVisit),
|
|
77
|
+
seals: seals.length ? seals : null,
|
|
78
|
+
vgmWeightKg: row.vgmWeight ?? null,
|
|
79
|
+
impediments: row.impediments ?? null,
|
|
80
|
+
// The N4 `Loaded` timestamp, parsed independently of the current state so a
|
|
81
|
+
// container that has since departed still carries when it was loaded.
|
|
82
|
+
loadedAt: parseN4DateTime(row.loaded),
|
|
83
|
+
rawData: row.raw
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export {
|
|
87
|
+
buildSourceEventId,
|
|
88
|
+
inferMode,
|
|
89
|
+
mapRowToEvent,
|
|
90
|
+
parseCargoCategory,
|
|
91
|
+
parseN4DateTime,
|
|
92
|
+
pickVesselVisit,
|
|
93
|
+
semanticEventIdFor
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=n4-semantics.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/terminal_tracking/lib/adapters/n4/n4-semantics.ts"],
|
|
4
|
+
"sourcesContent": ["import type { N4UnitRow } from './types'\nimport { DEFAULT_TERMINAL_TIMEZONE, zonedNaiveToUtc } from '../zoned-time'\nimport type { TerminalFetchedEvent, ResolvedTerminalConfig } from '../../terminal-adapter'\nimport type {\n TerminalEventType,\n TerminalEventClassifierCode,\n TerminalModeOfTransport,\n} from '../../../data/entities'\n\ntype StateMapping = {\n code: string // canonical N4 transit-state code\n eventType: TerminalEventType\n eventCode: string\n classifier: TerminalEventClassifierCode\n timestampField: 'timeIn' | 'timeOut' | 'loaded'\n}\n\n/**\n * Maps an N4 friendly T-State (as returned by `/unit`) to a normalized event.\n * The `/unit` response uses friendly names (\"Inbound\", \"Yard\", \"Loaded\",\n * \"Departed\"); the `S*` codes are the request-side filter values.\n */\nconst STATE_MAP: Record<string, StateMapping> = {\n inbound: { code: 'S20_INBOUND', eventType: 'EQUIPMENT', eventCode: 'GTIN', classifier: 'ACT', timestampField: 'timeIn' },\n ecin: { code: 'S30_ECIN', eventType: 'EQUIPMENT', eventCode: 'GTIN', classifier: 'ACT', timestampField: 'timeIn' },\n yard: { code: 'S40_YARD', eventType: 'EQUIPMENT', eventCode: 'DISC', classifier: 'ACT', timestampField: 'timeIn' },\n loaded: { code: 'S60_LOADED', eventType: 'EQUIPMENT', eventCode: 'LOAD', classifier: 'ACT', timestampField: 'loaded' },\n departed: { code: 'S70_DEPARTED', eventType: 'TRANSPORT', eventCode: 'DEPA', classifier: 'ACT', timestampField: 'timeOut' },\n}\n\n/** Map an eventCode to the semantic module event id (or null if none). */\nexport function semanticEventIdFor(eventCode: string): string | null {\n switch (eventCode) {\n case 'GTIN':\n return 'terminal_tracking.equipment.gate_in'\n case 'DISC':\n return 'terminal_tracking.equipment.discharged'\n case 'LOAD':\n return 'terminal_tracking.equipment.loaded'\n case 'DEPA':\n return 'terminal_tracking.transport.departed'\n default:\n return null\n }\n}\n\nexport function buildSourceEventId(terminalCode: string, ufvGkey: string, eventCode: string): string {\n return `${terminalCode}:${ufvGkey}:${eventCode}`\n}\n\n/**\n * Parse an N4 timestamp (\"yyyy-mm-dd hh:mm[:ss]\"). N4 returns terminal-local\n * time without an offset, so we interpret it in the terminal's timezone\n * (Europe/Warsaw by default) and return the corresponding UTC instant.\n */\nexport function parseN4DateTime(\n value: string | null | undefined,\n timeZone: string = DEFAULT_TERMINAL_TIMEZONE,\n): Date | null {\n if (!value) return null\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})[ T](\\d{2}):(\\d{2})(?::(\\d{2}))?$/.exec(value.trim())\n if (!m) return null\n const [, yyyy, mm, dd, hh, mi, ss] = m\n return zonedNaiveToUtc(+yyyy, +mm, +dd, +hh, +mi, ss ? +ss : 0, timeZone)\n}\n\nexport function inferMode(visitRef: string | null | undefined): TerminalModeOfTransport | null {\n if (!visitRef) return null\n const v = visitRef.toUpperCase()\n if (v.includes('TRUCK')) return 'TRUCK'\n // Rail visit ids look like 'TUX3W31-25_IMP' / '..._EXP'\n if (/_IMP|_EXP|\\bW\\d/.test(v)) return 'RAIL'\n return 'VESSEL'\n}\n\n/**\n * The container's direction of travel through the terminal, from the N4\n * `Category` column. Anything else N4 reports (notably \"Storage\" for empties)\n * is not a direction and normalizes to null.\n */\nexport type TerminalCargoCategory = 'import' | 'export'\n\n/** Normalize an N4 `Category` value to a direction (null when not directional). */\nexport function parseCargoCategory(rawData: Record<string, unknown> | null | undefined): TerminalCargoCategory | null {\n const category = String(rawData?.['Category'] ?? '').trim().toLowerCase()\n if (category === 'import') return 'import'\n if (category === 'export') return 'export'\n return null\n}\n\n/**\n * Pick the vessel-side visit ref for a leg, by its N4 `Category`:\n * an **Import** leg's vessel is the inbound visit (\"I/B Actual Visit\"); an\n * **Export** leg's vessel is the outbound visit (\"O/B Actual Visit\"). The `dir`\n * tells the caller which voyage (inbound vs outbound) to attach. Returns null\n * when the category is unknown or the relevant side is missing.\n */\nexport function pickVesselVisit(\n event: Pick<TerminalFetchedEvent, 'visitRefIn' | 'visitRefOut' | 'rawData'>,\n): { ref: string; dir: 'in' | 'out' } | null {\n const category = parseCargoCategory(event.rawData)\n if (category === 'import') return event.visitRefIn ? { ref: event.visitRefIn, dir: 'in' } : null\n if (category === 'export') return event.visitRefOut ? { ref: event.visitRefOut, dir: 'out' } : null\n return null\n}\n\n/**\n * Map a single N4 unit-facility-visit row to a normalized terminal event,\n * keyed by the row's current transit state. Returns null when the state is not\n * mappable (e.g. advised-only, retired).\n */\nexport function mapRowToEvent(\n row: N4UnitRow,\n config: ResolvedTerminalConfig,\n): TerminalFetchedEvent | null {\n const stateKey = (row.tState ?? '').trim().toLowerCase()\n const mapping = STATE_MAP[stateKey]\n if (!mapping) return null\n\n const eventDateTime =\n parseN4DateTime(row[mapping.timestampField]) ??\n parseN4DateTime(row.timeOut) ??\n parseN4DateTime(row.loaded) ??\n parseN4DateTime(row.timeIn) ??\n new Date()\n\n const seals = (row.seals ?? []).map((number) => ({ number, source: config.terminalCode }))\n\n return {\n source: 'terminal',\n sourceEventId: buildSourceEventId(config.terminalCode, row.ufvGkey, mapping.eventCode),\n eventType: mapping.eventType,\n eventCode: mapping.eventCode,\n eventClassifierCode: mapping.classifier,\n eventDateTime,\n containerNumber: row.unitNbr,\n ufvGkey: row.ufvGkey,\n transitState: mapping.code,\n visitState: row.vState ?? null,\n facilityCode: config.facilityCode ?? null,\n facilityCodeListProvider: config.facilityCodeListProvider ?? null,\n unlocode: config.unlocode ?? null,\n visitRefIn: row.ibActualVisit ?? null,\n visitRefOut: row.obActualVisit ?? null,\n vesselName: null,\n voyageNumber: null,\n modeOfTransport: inferMode(row.obActualVisit) ?? inferMode(row.ibActualVisit),\n seals: seals.length ? seals : null,\n vgmWeightKg: row.vgmWeight ?? null,\n impediments: row.impediments ?? null,\n // The N4 `Loaded` timestamp, parsed independently of the current state so a\n // container that has since departed still carries when it was loaded.\n loadedAt: parseN4DateTime(row.loaded),\n rawData: row.raw,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { parseTerminalStops } from "../../../stops.js";
|
|
2
|
+
function asArray(v) {
|
|
3
|
+
if (v == null) return [];
|
|
4
|
+
return Array.isArray(v) ? v : [v];
|
|
5
|
+
}
|
|
6
|
+
function toNum(v) {
|
|
7
|
+
if (v == null || v === "") return null;
|
|
8
|
+
const n = Number(v);
|
|
9
|
+
return Number.isFinite(n) ? n : null;
|
|
10
|
+
}
|
|
11
|
+
function splitList(v) {
|
|
12
|
+
if (v == null || v === "") return null;
|
|
13
|
+
return v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
function parseUnitDataTable(body) {
|
|
16
|
+
const table = body?.["query-response"]?.["data-table"];
|
|
17
|
+
if (!table) return [];
|
|
18
|
+
const columns = asArray(table.columns?.column).map((c) => String(c));
|
|
19
|
+
const rows = asArray(table.rows?.row ?? void 0);
|
|
20
|
+
const idx = (header) => columns.indexOf(header);
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const row of rows) {
|
|
23
|
+
const fields = asArray(row.field);
|
|
24
|
+
const get = (header) => {
|
|
25
|
+
const i = idx(header);
|
|
26
|
+
if (i < 0 || i >= fields.length) return null;
|
|
27
|
+
const v = fields[i];
|
|
28
|
+
return v == null || v === "" ? null : String(v);
|
|
29
|
+
};
|
|
30
|
+
const raw = {};
|
|
31
|
+
columns.forEach((col, i) => {
|
|
32
|
+
raw[col] = fields[i] == null || fields[i] === "" ? null : String(fields[i]);
|
|
33
|
+
});
|
|
34
|
+
const ufvGkey = row["@primary-key"] ? String(row["@primary-key"]) : "";
|
|
35
|
+
const unitNbr = get("Unit Nbr") ?? "";
|
|
36
|
+
if (!ufvGkey || !unitNbr) continue;
|
|
37
|
+
out.push({
|
|
38
|
+
ufvGkey,
|
|
39
|
+
unitNbr,
|
|
40
|
+
tState: get("T-State"),
|
|
41
|
+
vState: get("V-State"),
|
|
42
|
+
category: get("Category"),
|
|
43
|
+
lineOp: get("Line Op"),
|
|
44
|
+
typeIso: get("Type ISO"),
|
|
45
|
+
frghtKind: get("Frght Kind"),
|
|
46
|
+
ibActualVisit: get("I/B Actual Visit"),
|
|
47
|
+
obActualVisit: get("O/B Actual Visit"),
|
|
48
|
+
timeIn: get("Time In"),
|
|
49
|
+
timeOut: get("Time Out"),
|
|
50
|
+
impediments: splitList(get("Unit Impediments")),
|
|
51
|
+
cenNumber: get("CEN Number"),
|
|
52
|
+
vgmWeight: toNum(get("VGM Weight")),
|
|
53
|
+
cargoWtKg: toNum(get("Cargo Wt (kg)")),
|
|
54
|
+
tareWt: toNum(get("Tare Wt")),
|
|
55
|
+
weightKg: toNum(get("Weight (kg)")),
|
|
56
|
+
seals: [get("Seal Nbr1"), get("Seal Nbr2"), get("Seal Nbr3"), get("Seal Nbr4")].filter(
|
|
57
|
+
(s) => !!s
|
|
58
|
+
),
|
|
59
|
+
dskNumber: get("DSK Number"),
|
|
60
|
+
loaded: get("Loaded"),
|
|
61
|
+
stops: parseTerminalStops(raw),
|
|
62
|
+
raw
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
parseUnitDataTable
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=data-table.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../src/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.ts"],
|
|
4
|
+
"sourcesContent": ["import type { N4UnitRow } from '../types'\nimport { parseTerminalStops } from '../../../stops'\n\n/** Coerce a value that may be a single item or an array into an array. */\nfunction asArray<T>(v: T | T[] | null | undefined): T[] {\n if (v == null) return []\n return Array.isArray(v) ? v : [v]\n}\n\nfunction toNum(v: string | null | undefined): number | null {\n if (v == null || v === '') return null\n const n = Number(v)\n return Number.isFinite(n) ? n : null\n}\n\nfunction splitList(v: string | null | undefined): string[] | null {\n if (v == null || v === '') return null\n return v\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n}\n\nexport type N4DataTableResponse = {\n 'query-response'?: {\n 'data-table'?: {\n '@count'?: string\n columns?: { column?: string | string[] }\n rows?: { row?: N4RawRow | N4RawRow[] } | null\n }\n }\n}\n\ntype N4RawRow = {\n '@primary-key'?: string\n field?: (string | null) | (string | null)[]\n}\n\n/**\n * Parse the N4 `/unit` `query-response.data-table` JSON envelope into typed\n * rows. Maps `field[]` onto `columns.column[]` by index, reads `@primary-key`\n * as the Ufv_Gkey, and tolerates single-vs-array collapsing from XML\u2192JSON.\n */\nexport function parseUnitDataTable(body: N4DataTableResponse): N4UnitRow[] {\n const table = body?.['query-response']?.['data-table']\n if (!table) return []\n\n const columns = asArray(table.columns?.column).map((c) => String(c))\n const rows = asArray(table.rows?.row ?? undefined)\n\n const idx = (header: string): number => columns.indexOf(header)\n const out: N4UnitRow[] = []\n\n for (const row of rows) {\n const fields = asArray(row.field)\n const get = (header: string): string | null => {\n const i = idx(header)\n if (i < 0 || i >= fields.length) return null\n const v = fields[i]\n return v == null || v === '' ? null : String(v)\n }\n\n const raw: Record<string, string | null> = {}\n columns.forEach((col, i) => {\n raw[col] = fields[i] == null || fields[i] === '' ? null : String(fields[i])\n })\n\n const ufvGkey = row['@primary-key'] ? String(row['@primary-key']) : ''\n const unitNbr = get('Unit Nbr') ?? ''\n if (!ufvGkey || !unitNbr) continue\n\n out.push({\n ufvGkey,\n unitNbr,\n tState: get('T-State'),\n vState: get('V-State'),\n category: get('Category'),\n lineOp: get('Line Op'),\n typeIso: get('Type ISO'),\n frghtKind: get('Frght Kind'),\n ibActualVisit: get('I/B Actual Visit'),\n obActualVisit: get('O/B Actual Visit'),\n timeIn: get('Time In'),\n timeOut: get('Time Out'),\n impediments: splitList(get('Unit Impediments')),\n cenNumber: get('CEN Number'),\n vgmWeight: toNum(get('VGM Weight')),\n cargoWtKg: toNum(get('Cargo Wt (kg)')),\n tareWt: toNum(get('Tare Wt')),\n weightKg: toNum(get('Weight (kg)')),\n seals: [get('Seal Nbr1'), get('Seal Nbr2'), get('Seal Nbr3'), get('Seal Nbr4')].filter(\n (s): s is string => !!s,\n ),\n dskNumber: get('DSK Number'),\n loaded: get('Loaded'),\n stops: parseTerminalStops(raw),\n raw,\n })\n }\n\n return out\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAA0B;AAGnC,SAAS,QAAW,GAAoC;AACtD,MAAI,KAAK,KAAM,QAAO,CAAC;AACvB,SAAO,MAAM,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAC;AAClC;AAEA,SAAS,MAAM,GAA6C;AAC1D,MAAI,KAAK,QAAQ,MAAM,GAAI,QAAO;AAClC,QAAM,IAAI,OAAO,CAAC;AAClB,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAClC;AAEA,SAAS,UAAU,GAA+C;AAChE,MAAI,KAAK,QAAQ,MAAM,GAAI,QAAO;AAClC,SAAO,EACJ,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO;AACnB;AAsBO,SAAS,mBAAmB,MAAwC;AACzE,QAAM,QAAQ,OAAO,gBAAgB,IAAI,YAAY;AACrD,MAAI,CAAC,MAAO,QAAO,CAAC;AAEpB,QAAM,UAAU,QAAQ,MAAM,SAAS,MAAM,EAAE,IAAI,CAAC,MAAM,OAAO,CAAC,CAAC;AACnE,QAAM,OAAO,QAAQ,MAAM,MAAM,OAAO,MAAS;AAEjD,QAAM,MAAM,CAAC,WAA2B,QAAQ,QAAQ,MAAM;AAC9D,QAAM,MAAmB,CAAC;AAE1B,aAAW,OAAO,MAAM;AACtB,UAAM,SAAS,QAAQ,IAAI,KAAK;AAChC,UAAM,MAAM,CAAC,WAAkC;AAC7C,YAAM,IAAI,IAAI,MAAM;AACpB,UAAI,IAAI,KAAK,KAAK,OAAO,OAAQ,QAAO;AACxC,YAAM,IAAI,OAAO,CAAC;AAClB,aAAO,KAAK,QAAQ,MAAM,KAAK,OAAO,OAAO,CAAC;AAAA,IAChD;AAEA,UAAM,MAAqC,CAAC;AAC5C,YAAQ,QAAQ,CAAC,KAAK,MAAM;AAC1B,UAAI,GAAG,IAAI,OAAO,CAAC,KAAK,QAAQ,OAAO,CAAC,MAAM,KAAK,OAAO,OAAO,OAAO,CAAC,CAAC;AAAA,IAC5E,CAAC;AAED,UAAM,UAAU,IAAI,cAAc,IAAI,OAAO,IAAI,cAAc,CAAC,IAAI;AACpE,UAAM,UAAU,IAAI,UAAU,KAAK;AACnC,QAAI,CAAC,WAAW,CAAC,QAAS;AAE1B,QAAI,KAAK;AAAA,MACP;AAAA,MACA;AAAA,MACA,QAAQ,IAAI,SAAS;AAAA,MACrB,QAAQ,IAAI,SAAS;AAAA,MACrB,UAAU,IAAI,UAAU;AAAA,MACxB,QAAQ,IAAI,SAAS;AAAA,MACrB,SAAS,IAAI,UAAU;AAAA,MACvB,WAAW,IAAI,YAAY;AAAA,MAC3B,eAAe,IAAI,kBAAkB;AAAA,MACrC,eAAe,IAAI,kBAAkB;AAAA,MACrC,QAAQ,IAAI,SAAS;AAAA,MACrB,SAAS,IAAI,UAAU;AAAA,MACvB,aAAa,UAAU,IAAI,kBAAkB,CAAC;AAAA,MAC9C,WAAW,IAAI,YAAY;AAAA,MAC3B,WAAW,MAAM,IAAI,YAAY,CAAC;AAAA,MAClC,WAAW,MAAM,IAAI,eAAe,CAAC;AAAA,MACrC,QAAQ,MAAM,IAAI,SAAS,CAAC;AAAA,MAC5B,UAAU,MAAM,IAAI,aAAa,CAAC;AAAA,MAClC,OAAO,CAAC,IAAI,WAAW,GAAG,IAAI,WAAW,GAAG,IAAI,WAAW,GAAG,IAAI,WAAW,CAAC,EAAE;AAAA,QAC9E,CAAC,MAAmB,CAAC,CAAC;AAAA,MACxB;AAAA,MACA,WAAW,IAAI,YAAY;AAAA,MAC3B,QAAQ,IAAI,QAAQ;AAAA,MACpB,OAAO,mBAAmB,GAAG;AAAA,MAC7B;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function asArray(v) {
|
|
2
|
+
if (v == null) return [];
|
|
3
|
+
return Array.isArray(v) ? v : [v];
|
|
4
|
+
}
|
|
5
|
+
function parseVesselDataTable(body) {
|
|
6
|
+
const table = body?.["query-response"]?.["data-table"];
|
|
7
|
+
if (!table) return null;
|
|
8
|
+
const columns = asArray(table.columns?.column).map((c) => String(c));
|
|
9
|
+
const rows = asArray(table.rows?.row ?? void 0);
|
|
10
|
+
if (rows.length === 0) return null;
|
|
11
|
+
const row = rows[0];
|
|
12
|
+
const fields = asArray(row.field);
|
|
13
|
+
const get = (header) => {
|
|
14
|
+
const i = columns.indexOf(header);
|
|
15
|
+
if (i < 0 || i >= fields.length) return null;
|
|
16
|
+
const v = fields[i];
|
|
17
|
+
return v == null || v === "" ? null : String(v);
|
|
18
|
+
};
|
|
19
|
+
const raw = {};
|
|
20
|
+
columns.forEach((col, i) => {
|
|
21
|
+
raw[col] = fields[i] == null || fields[i] === "" ? null : String(fields[i]);
|
|
22
|
+
});
|
|
23
|
+
const visitRef = get("Visit");
|
|
24
|
+
if (!visitRef) return null;
|
|
25
|
+
return {
|
|
26
|
+
visitRef,
|
|
27
|
+
vesselName: get("Vessel Name"),
|
|
28
|
+
ibVoyage: get("I/B Vyg"),
|
|
29
|
+
obVoyage: get("O/B Vyg"),
|
|
30
|
+
line: get("Line"),
|
|
31
|
+
phase: get("Phase"),
|
|
32
|
+
eta: get("ETA"),
|
|
33
|
+
etd: get("ETD"),
|
|
34
|
+
ata: get("ATA"),
|
|
35
|
+
atd: get("ATD"),
|
|
36
|
+
beginReceive: get("Begin Receive"),
|
|
37
|
+
dryCutoff: get("DryCutoff"),
|
|
38
|
+
raw
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
parseVesselDataTable
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=vessel-data-table.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../src/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.ts"],
|
|
4
|
+
"sourcesContent": ["import type { N4DataTableResponse } from './data-table'\nimport type { N4VesselRow } from '../types'\n\n/** Coerce a value that may be a single item or an array into an array. */\nfunction asArray<T>(v: T | T[] | null | undefined): T[] {\n if (v == null) return []\n return Array.isArray(v) ? v : [v]\n}\n\n/**\n * Parse the N4 `/VESSEL` `query-response.data-table` envelope into a single\n * vessel-visit row. A `/VESSEL?VISIT_REF=` query targets exactly one visit, so\n * we return the first row (or null when there are none). Columns are mapped by\n * header name, mirroring parseUnitDataTable.\n *\n * Real columns: Visit, Vessel Name, I/B Vyg, O/B Vyg, Line, Phase, ETA, ETD,\n * ATA, ATD, Begin Receive, DryCutoff. The `Visit` column equals the unit's\n * `visitRefIn`/`visitRefOut` (the join key).\n */\nexport function parseVesselDataTable(body: N4DataTableResponse): N4VesselRow | null {\n const table = body?.['query-response']?.['data-table']\n if (!table) return null\n\n const columns = asArray(table.columns?.column).map((c) => String(c))\n const rows = asArray(table.rows?.row ?? undefined)\n if (rows.length === 0) return null\n\n const row = rows[0]\n const fields = asArray(row.field)\n const get = (header: string): string | null => {\n const i = columns.indexOf(header)\n if (i < 0 || i >= fields.length) return null\n const v = fields[i]\n return v == null || v === '' ? null : String(v)\n }\n\n const raw: Record<string, string | null> = {}\n columns.forEach((col, i) => {\n raw[col] = fields[i] == null || fields[i] === '' ? null : String(fields[i])\n })\n\n const visitRef = get('Visit')\n if (!visitRef) return null\n\n return {\n visitRef,\n vesselName: get('Vessel Name'),\n ibVoyage: get('I/B Vyg'),\n obVoyage: get('O/B Vyg'),\n line: get('Line'),\n phase: get('Phase'),\n eta: get('ETA'),\n etd: get('ETD'),\n ata: get('ATA'),\n atd: get('ATD'),\n beginReceive: get('Begin Receive'),\n dryCutoff: get('DryCutoff'),\n raw,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAIA,SAAS,QAAW,GAAoC;AACtD,MAAI,KAAK,KAAM,QAAO,CAAC;AACvB,SAAO,MAAM,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAC;AAClC;AAYO,SAAS,qBAAqB,MAA+C;AAClF,QAAM,QAAQ,OAAO,gBAAgB,IAAI,YAAY;AACrD,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,UAAU,QAAQ,MAAM,SAAS,MAAM,EAAE,IAAI,CAAC,MAAM,OAAO,CAAC,CAAC;AACnE,QAAM,OAAO,QAAQ,MAAM,MAAM,OAAO,MAAS;AACjD,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,QAAM,MAAM,KAAK,CAAC;AAClB,QAAM,SAAS,QAAQ,IAAI,KAAK;AAChC,QAAM,MAAM,CAAC,WAAkC;AAC7C,UAAM,IAAI,QAAQ,QAAQ,MAAM;AAChC,QAAI,IAAI,KAAK,KAAK,OAAO,OAAQ,QAAO;AACxC,UAAM,IAAI,OAAO,CAAC;AAClB,WAAO,KAAK,QAAQ,MAAM,KAAK,OAAO,OAAO,CAAC;AAAA,EAChD;AAEA,QAAM,MAAqC,CAAC;AAC5C,UAAQ,QAAQ,CAAC,KAAK,MAAM;AAC1B,QAAI,GAAG,IAAI,OAAO,CAAC,KAAK,QAAQ,OAAO,CAAC,MAAM,KAAK,OAAO,OAAO,OAAO,CAAC,CAAC;AAAA,EAC5E,CAAC;AAED,QAAM,WAAW,IAAI,OAAO;AAC5B,MAAI,CAAC,SAAU,QAAO;AAEtB,SAAO;AAAA,IACL;AAAA,IACA,YAAY,IAAI,aAAa;AAAA,IAC7B,UAAU,IAAI,SAAS;AAAA,IACvB,UAAU,IAAI,SAAS;AAAA,IACvB,MAAM,IAAI,MAAM;AAAA,IAChB,OAAO,IAAI,OAAO;AAAA,IAClB,KAAK,IAAI,KAAK;AAAA,IACd,KAAK,IAAI,KAAK;AAAA,IACd,KAAK,IAAI,KAAK;AAAA,IACd,KAAK,IAAI,KAAK;AAAA,IACd,cAAc,IAAI,eAAe;AAAA,IACjC,WAAW,IAAI,WAAW;AAAA,IAC1B;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ProxyAgent } from "undici";
|
|
2
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3
|
+
function proxyDispatcher(proxyUrl) {
|
|
4
|
+
if (!proxyUrl) return void 0;
|
|
5
|
+
let agent = cache.get(proxyUrl);
|
|
6
|
+
if (!agent) {
|
|
7
|
+
agent = new ProxyAgent(proxyUrl);
|
|
8
|
+
cache.set(proxyUrl, agent);
|
|
9
|
+
}
|
|
10
|
+
return agent;
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
proxyDispatcher
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/terminal_tracking/lib/adapters/n4/proxy.ts"],
|
|
4
|
+
"sourcesContent": ["import { ProxyAgent, type Dispatcher } from 'undici'\n\n/**\n * Build (and memoise) an undici `ProxyAgent` for a terminal's `proxyUrl`, to be\n * passed as the `dispatcher` on the N4 `fetch` calls so they egress through a\n * whitelisted IP. Returns undefined when no proxy is configured (direct).\n *\n * Note: undici proxying is HTTP(S) CONNECT only \u2014 `proxyUrl` must be an\n * http(s) forward proxy (e.g. tinyproxy/squid), not a SOCKS proxy.\n */\nconst cache = new Map<string, ProxyAgent>()\n\nexport function proxyDispatcher(proxyUrl?: string | null): Dispatcher | undefined {\n if (!proxyUrl) return undefined\n let agent = cache.get(proxyUrl)\n if (!agent) {\n agent = new ProxyAgent(proxyUrl)\n cache.set(proxyUrl, agent)\n }\n return agent\n}\n\n/** Init type for global fetch including undici's `dispatcher` (not in DOM types). */\nexport type FetchInitWithDispatcher = RequestInit & { dispatcher?: Dispatcher }\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,kBAAmC;AAU5C,MAAM,QAAQ,oBAAI,IAAwB;AAEnC,SAAS,gBAAgB,UAAkD;AAChF,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI,QAAQ,MAAM,IAAI,QAAQ;AAC9B,MAAI,CAAC,OAAO;AACV,YAAQ,IAAI,WAAW,QAAQ;AAC/B,UAAM,IAAI,UAAU,KAAK;AAAA,EAC3B;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|