@openeld/openeld 0.1.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.
Files changed (95) hide show
  1. package/README.md +40 -0
  2. package/buf/buf.gen.yaml +8 -0
  3. package/buf/buf.work.yaml +3 -0
  4. package/docs/architecture/.gitkeep +0 -0
  5. package/docs/architecture/README.md +3 -0
  6. package/docs/decisions/.gitkeep +0 -0
  7. package/docs/decisions/README.md +3 -0
  8. package/docs/protobuf/.gitkeep +0 -0
  9. package/docs/protobuf/README.md +28 -0
  10. package/docs/providers/.gitkeep +0 -0
  11. package/docs/providers/README.md +73 -0
  12. package/docs/providers/capability-matrix.md +13 -0
  13. package/docs/providers/geotab.md +37 -0
  14. package/docs/providers/keeptruckin.md +35 -0
  15. package/docs/providers/motive.md +36 -0
  16. package/docs/providers/provider-template.md +56 -0
  17. package/docs/providers/samsara.md +38 -0
  18. package/docs/providers/verification-matrix.md +13 -0
  19. package/docs/services/.gitkeep +0 -0
  20. package/docs/services/README.md +3 -0
  21. package/examples/ts/.gitkeep +0 -0
  22. package/examples/ts/README.md +3 -0
  23. package/package.json +34 -0
  24. package/proto/.gitkeep +0 -0
  25. package/proto/README.md +9 -0
  26. package/proto/common/metadata/.gitkeep +0 -0
  27. package/proto/common/metadata/audit.proto +22 -0
  28. package/proto/common/metadata/source.proto +31 -0
  29. package/proto/common/primitives/.gitkeep +0 -0
  30. package/proto/common/primitives/ids.proto +21 -0
  31. package/proto/common/primitives/location.proto +23 -0
  32. package/proto/common/primitives/time.proto +24 -0
  33. package/proto/common/units/.gitkeep +0 -0
  34. package/proto/common/units/measurements.proto +28 -0
  35. package/proto/logistics/asset.proto +24 -0
  36. package/proto/logistics/asset_location.proto +19 -0
  37. package/proto/logistics/assets/.gitkeep +0 -0
  38. package/proto/logistics/carrier.proto +18 -0
  39. package/proto/logistics/compliance/.gitkeep +0 -0
  40. package/proto/logistics/data_consent.proto +24 -0
  41. package/proto/logistics/diagnostics/.gitkeep +0 -0
  42. package/proto/logistics/driver.proto +28 -0
  43. package/proto/logistics/drivers/.gitkeep +0 -0
  44. package/proto/logistics/dvir.proto +34 -0
  45. package/proto/logistics/enums.proto +197 -0
  46. package/proto/logistics/events/.gitkeep +0 -0
  47. package/proto/logistics/gps_location.proto +24 -0
  48. package/proto/logistics/hos/.gitkeep +0 -0
  49. package/proto/logistics/hos_daily_summary.proto +38 -0
  50. package/proto/logistics/hos_event.proto +41 -0
  51. package/proto/logistics/ifta_trip.proto +24 -0
  52. package/proto/logistics/locations/.gitkeep +0 -0
  53. package/proto/logistics/safety_event.proto +27 -0
  54. package/proto/logistics/trips/.gitkeep +0 -0
  55. package/proto/logistics/vehicle.proto +25 -0
  56. package/proto/logistics/vehicle_assignment.proto +25 -0
  57. package/proto/logistics/vehicles/.gitkeep +0 -0
  58. package/proto/providers/eld/geotab/.gitkeep +0 -0
  59. package/proto/providers/eld/geotab/contracts.proto +105 -0
  60. package/proto/providers/eld/keeptruckin/.gitkeep +0 -0
  61. package/proto/providers/eld/keeptruckin/contracts.proto +103 -0
  62. package/proto/providers/eld/motive/.gitkeep +0 -0
  63. package/proto/providers/eld/motive/contracts.proto +108 -0
  64. package/proto/providers/eld/samsara/.gitkeep +0 -0
  65. package/proto/providers/eld/samsara/contracts.proto +110 -0
  66. package/proto/providers/eld/shared/.gitkeep +0 -0
  67. package/proto/providers/eld/shared/common.proto +174 -0
  68. package/proto/providers/telematics/fleet_complete/.gitkeep +0 -0
  69. package/proto/providers/telematics/fleet_complete/contracts.proto +13 -0
  70. package/proto/providers/telematics/fourkites/.gitkeep +0 -0
  71. package/proto/providers/telematics/fourkites/contracts.proto +13 -0
  72. package/proto/providers/telematics/project44/.gitkeep +0 -0
  73. package/proto/providers/telematics/project44/contracts.proto +13 -0
  74. package/proto/providers/telematics/shared/.gitkeep +0 -0
  75. package/proto/providers/telematics/shared/common.proto +48 -0
  76. package/proto/providers/telematics/verizon_connect/.gitkeep +0 -0
  77. package/proto/providers/telematics/verizon_connect/contracts.proto +13 -0
  78. package/proto/services/ingestion/.gitkeep +0 -0
  79. package/proto/services/ingestion/service.proto +37 -0
  80. package/proto/services/normalization/.gitkeep +0 -0
  81. package/proto/services/normalization/service.proto +62 -0
  82. package/proto/services/query/.gitkeep +0 -0
  83. package/proto/services/query/service.proto +238 -0
  84. package/proto/services/sync/.gitkeep +0 -0
  85. package/proto/services/sync/service.proto +89 -0
  86. package/proto/v1/.gitkeep +0 -0
  87. package/proto/v1/openeld.proto +39 -0
  88. package/src/adapters/providers/.gitkeep +0 -0
  89. package/src/clients/.gitkeep +0 -0
  90. package/src/fixture-normalization.ts +471 -0
  91. package/src/generated/README.md +3 -0
  92. package/src/generated/ts/.gitkeep +0 -0
  93. package/src/index.ts +1 -0
  94. package/src/mappers/.gitkeep +0 -0
  95. package/src/registry/.gitkeep +0 -0
@@ -0,0 +1,471 @@
1
+ export type SyncMode = "cursor" | "page" | "version";
2
+
3
+ export interface CanonicalDriver {
4
+ id: string;
5
+ firstName: string | null;
6
+ lastName: string | null;
7
+ status: string | null;
8
+ username: string | null;
9
+ }
10
+
11
+ export interface CanonicalVehicle {
12
+ id: string;
13
+ name: string | null;
14
+ vin: string | null;
15
+ licensePlate: string | null;
16
+ }
17
+
18
+ export interface CanonicalHosEvent {
19
+ id: string;
20
+ driverId: string | null;
21
+ vehicleId: string | null;
22
+ status: string | null;
23
+ startTime: string | null;
24
+ endTime: string | null;
25
+ latitude: number | null;
26
+ longitude: number | null;
27
+ note: string | null;
28
+ }
29
+
30
+ export interface CanonicalGpsLocation {
31
+ id: string;
32
+ vehicleId: string | null;
33
+ driverId: string | null;
34
+ timestamp: string | null;
35
+ latitude: number | null;
36
+ longitude: number | null;
37
+ speedKph: number | null;
38
+ headingDegrees: number | null;
39
+ }
40
+
41
+ export interface CanonicalDvir {
42
+ id: string;
43
+ driverId: string | null;
44
+ vehicleId: string | null;
45
+ inspectionTime: string | null;
46
+ isSafeToDrive: boolean | null;
47
+ defectCount: number;
48
+ }
49
+
50
+ export interface CanonicalSnapshot {
51
+ provider: string;
52
+ fixtureAuthority: "official-docs";
53
+ drivers: CanonicalDriver[];
54
+ vehicles: CanonicalVehicle[];
55
+ hosEvents: CanonicalHosEvent[];
56
+ gpsLocations: CanonicalGpsLocation[];
57
+ dvirs: CanonicalDvir[];
58
+ sync: {
59
+ kind: SyncMode;
60
+ token: string;
61
+ hasMore: boolean | null;
62
+ };
63
+ }
64
+
65
+ const MPH_TO_KPH = 1.60934;
66
+
67
+ function round(value: number): number {
68
+ return Math.round(value * 100) / 100;
69
+ }
70
+
71
+ function toId(value: unknown): string {
72
+ return String(value);
73
+ }
74
+
75
+ function splitName(name: string | null | undefined): {
76
+ firstName: string | null;
77
+ lastName: string | null;
78
+ } {
79
+ if (!name) {
80
+ return { firstName: null, lastName: null };
81
+ }
82
+
83
+ const [firstName, ...remainingParts] = name.trim().split(/\s+/);
84
+
85
+ if (!firstName) {
86
+ return { firstName: null, lastName: null };
87
+ }
88
+
89
+ return {
90
+ firstName,
91
+ lastName: remainingParts.join(" ") || null,
92
+ };
93
+ }
94
+
95
+ function optionalString(value: unknown): string | null {
96
+ if (typeof value !== "string") {
97
+ return null;
98
+ }
99
+
100
+ return value.length > 0 ? value : null;
101
+ }
102
+
103
+ function optionalNumber(value: unknown): number | null {
104
+ return typeof value === "number" ? value : null;
105
+ }
106
+
107
+ function uniqueById<T extends { id: string }>(values: T[]): T[] {
108
+ const unique = new Map<string, T>();
109
+
110
+ for (const value of values) {
111
+ unique.set(value.id, value);
112
+ }
113
+
114
+ return Array.from(unique.values()).sort((left, right) =>
115
+ left.id.localeCompare(right.id),
116
+ );
117
+ }
118
+
119
+ function sortById<T extends { id: string }>(values: T[]): T[] {
120
+ return [...values].sort((left, right) => left.id.localeCompare(right.id));
121
+ }
122
+
123
+ export function buildSamsaraCanonicalSnapshot(fixtures: {
124
+ drivers: any;
125
+ vehicles: any;
126
+ hosLogs: any;
127
+ vehicleLocations: any;
128
+ dvirs: any;
129
+ feedCursor: any;
130
+ }): CanonicalSnapshot {
131
+ const drivers = uniqueById<CanonicalDriver>(
132
+ (fixtures.drivers.data ?? []).map((driver: any): CanonicalDriver => {
133
+ const { firstName, lastName } = splitName(driver.name);
134
+
135
+ return {
136
+ id: toId(driver.id),
137
+ firstName,
138
+ lastName,
139
+ status: optionalString(driver.driverActivationStatus),
140
+ username: optionalString(driver.username),
141
+ };
142
+ }),
143
+ );
144
+
145
+ const vehicles = uniqueById<CanonicalVehicle>(
146
+ (fixtures.vehicles.data ?? []).map((vehicle: any): CanonicalVehicle => ({
147
+ id: toId(vehicle.id),
148
+ name: optionalString(vehicle.name),
149
+ vin: optionalString(vehicle.vin),
150
+ licensePlate: optionalString(vehicle.licensePlate),
151
+ })),
152
+ );
153
+
154
+ const hosEvents = sortById<CanonicalHosEvent>(
155
+ (fixtures.hosLogs.data ?? []).map((log: any): CanonicalHosEvent => ({
156
+ id: toId(log.id),
157
+ driverId: optionalString(log.driverId),
158
+ vehicleId: optionalString(log.vehicleId),
159
+ status: optionalString(log.hosStatusType),
160
+ startTime: optionalString(log.startTime),
161
+ endTime: optionalString(log.endTime),
162
+ latitude: optionalNumber(log.startLocation?.latitude),
163
+ longitude: optionalNumber(log.startLocation?.longitude),
164
+ note: optionalString(log.remark),
165
+ })),
166
+ );
167
+
168
+ const gpsLocations = sortById<CanonicalGpsLocation>(
169
+ (fixtures.vehicleLocations.data ?? []).map(
170
+ (location: any): CanonicalGpsLocation => ({
171
+ id: toId(location.vehicleId),
172
+ vehicleId: optionalString(location.vehicleId),
173
+ driverId: null,
174
+ timestamp: optionalString(location.time),
175
+ latitude: optionalNumber(location.gps?.latitude),
176
+ longitude: optionalNumber(location.gps?.longitude),
177
+ speedKph:
178
+ typeof location.gps?.speedMilesPerHour === "number"
179
+ ? round(location.gps.speedMilesPerHour * MPH_TO_KPH)
180
+ : null,
181
+ headingDegrees: optionalNumber(location.gps?.headingDegrees),
182
+ }),
183
+ ),
184
+ );
185
+
186
+ const dvirs = sortById<CanonicalDvir>(
187
+ (fixtures.dvirs.data ?? []).map((dvir: any): CanonicalDvir => ({
188
+ id: toId(dvir.id),
189
+ driverId: optionalString(dvir.driverId),
190
+ vehicleId: optionalString(dvir.vehicleId),
191
+ inspectionTime: optionalString(dvir.createdAtTime),
192
+ isSafeToDrive:
193
+ typeof dvir.safetyStatus === "string"
194
+ ? dvir.safetyStatus === "safe"
195
+ : null,
196
+ defectCount: Array.isArray(dvir.defects) ? dvir.defects.length : 0,
197
+ })),
198
+ );
199
+
200
+ return {
201
+ provider: "samsara",
202
+ fixtureAuthority: "official-docs",
203
+ drivers,
204
+ vehicles,
205
+ hosEvents,
206
+ gpsLocations,
207
+ dvirs,
208
+ sync: {
209
+ kind: "cursor",
210
+ token: toId(fixtures.feedCursor.pagination?.endCursor ?? ""),
211
+ hasMore:
212
+ typeof fixtures.feedCursor.pagination?.hasNextPage === "boolean"
213
+ ? fixtures.feedCursor.pagination.hasNextPage
214
+ : null,
215
+ },
216
+ };
217
+ }
218
+
219
+ export function buildMotiveCanonicalSnapshot(fixtures: {
220
+ drivers: any;
221
+ vehicles: any;
222
+ hosLogs: any;
223
+ vehicleLocations: any;
224
+ pageSync: any;
225
+ }): CanonicalSnapshot {
226
+ const canonicalDrivers: CanonicalDriver[] = [];
227
+
228
+ for (const entry of fixtures.drivers.users ?? []) {
229
+ const driver = entry.user;
230
+ canonicalDrivers.push({
231
+ id: toId(driver.id),
232
+ firstName: optionalString(driver.first_name),
233
+ lastName: optionalString(driver.last_name),
234
+ status: optionalString(driver.status),
235
+ username: optionalString(driver.username),
236
+ });
237
+ }
238
+
239
+ for (const entry of fixtures.hosLogs.logs ?? []) {
240
+ const driver = entry.log?.driver;
241
+ if (driver) {
242
+ canonicalDrivers.push({
243
+ id: toId(driver.id),
244
+ firstName: optionalString(driver.first_name),
245
+ lastName: optionalString(driver.last_name),
246
+ status: optionalString(driver.status),
247
+ username: optionalString(driver.username),
248
+ });
249
+ }
250
+ }
251
+
252
+ for (const entry of fixtures.vehicleLocations.vehicles ?? []) {
253
+ const driver = entry.vehicle?.current_driver;
254
+ if (driver) {
255
+ canonicalDrivers.push({
256
+ id: toId(driver.id),
257
+ firstName: optionalString(driver.first_name),
258
+ lastName: optionalString(driver.last_name),
259
+ status: optionalString(driver.status),
260
+ username: optionalString(driver.username),
261
+ });
262
+ }
263
+ }
264
+
265
+ for (const entry of fixtures.pageSync.users ?? []) {
266
+ const driver = entry.user;
267
+ canonicalDrivers.push({
268
+ id: toId(driver.id),
269
+ firstName: optionalString(driver.first_name),
270
+ lastName: optionalString(driver.last_name),
271
+ status: optionalString(driver.status),
272
+ username: optionalString(driver.username),
273
+ });
274
+ }
275
+
276
+ const drivers = uniqueById<CanonicalDriver>(canonicalDrivers);
277
+
278
+ const canonicalVehicles: CanonicalVehicle[] = [];
279
+
280
+ for (const entry of fixtures.drivers.users ?? []) {
281
+ const vehicle = entry.user?.current_vehicle;
282
+ if (vehicle) {
283
+ canonicalVehicles.push({
284
+ id: toId(vehicle.id),
285
+ name: optionalString(vehicle.number),
286
+ vin: optionalString(vehicle.vin),
287
+ licensePlate: null,
288
+ });
289
+ }
290
+ }
291
+
292
+ for (const entry of fixtures.vehicles.vehicles ?? []) {
293
+ const vehicle = entry.vehicle;
294
+ canonicalVehicles.push({
295
+ id: toId(vehicle.id),
296
+ name: optionalString(vehicle.number),
297
+ vin: optionalString(vehicle.vin),
298
+ licensePlate: optionalString(vehicle.license_plate_number),
299
+ });
300
+ }
301
+
302
+ for (const entry of fixtures.hosLogs.logs ?? []) {
303
+ for (const vehicleEntry of entry.log?.vehicles ?? []) {
304
+ const vehicle = vehicleEntry.vehicle;
305
+ canonicalVehicles.push({
306
+ id: toId(vehicle.id),
307
+ name: optionalString(vehicle.number),
308
+ vin: optionalString(vehicle.vin),
309
+ licensePlate: null,
310
+ });
311
+ }
312
+ }
313
+
314
+ for (const entry of fixtures.vehicleLocations.vehicles ?? []) {
315
+ const vehicle = entry.vehicle;
316
+ canonicalVehicles.push({
317
+ id: toId(vehicle.id),
318
+ name: optionalString(vehicle.number),
319
+ vin: optionalString(vehicle.vin),
320
+ licensePlate: null,
321
+ });
322
+ }
323
+
324
+ const vehicles = uniqueById<CanonicalVehicle>(canonicalVehicles);
325
+
326
+ const hosEvents = sortById<CanonicalHosEvent>(
327
+ (fixtures.hosLogs.logs ?? []).flatMap((entry: any) =>
328
+ (entry.log?.events ?? []).map((eventEntry: any): CanonicalHosEvent => ({
329
+ id: toId(eventEntry.event.id),
330
+ driverId: toId(entry.log.driver.id),
331
+ vehicleId: toId(entry.log.vehicles?.[0]?.vehicle?.id ?? ""),
332
+ status: optionalString(eventEntry.event.type),
333
+ startTime: optionalString(eventEntry.event.start_time),
334
+ endTime: optionalString(eventEntry.event.end_time),
335
+ latitude: null,
336
+ longitude: null,
337
+ note: optionalString(eventEntry.event.location),
338
+ })),
339
+ ),
340
+ );
341
+
342
+ const gpsLocations = sortById<CanonicalGpsLocation>(
343
+ (fixtures.vehicleLocations.vehicles ?? []).map(
344
+ (entry: any): CanonicalGpsLocation => ({
345
+ id: toId(entry.vehicle.id),
346
+ vehicleId: toId(entry.vehicle.id),
347
+ driverId: toId(entry.vehicle.current_driver?.id ?? ""),
348
+ timestamp: optionalString(entry.vehicle.current_location?.located_at),
349
+ latitude: optionalNumber(entry.vehicle.current_location?.lat),
350
+ longitude: optionalNumber(entry.vehicle.current_location?.lon),
351
+ speedKph:
352
+ typeof entry.vehicle.current_location?.speed === "number"
353
+ ? round(entry.vehicle.current_location.speed * MPH_TO_KPH)
354
+ : null,
355
+ headingDegrees: optionalNumber(entry.vehicle.current_location?.bearing),
356
+ }),
357
+ ),
358
+ );
359
+
360
+ const dvirs = sortById<CanonicalDvir>(
361
+ (fixtures.hosLogs.logs ?? []).flatMap((entry: any) =>
362
+ (entry.log?.inspection_reports ?? []).map(
363
+ (reportEntry: any): CanonicalDvir => ({
364
+ id: toId(reportEntry.inspection_report.id),
365
+ driverId: toId(entry.log.driver.id),
366
+ vehicleId: toId(reportEntry.inspection_report.vehicle.id),
367
+ inspectionTime: optionalString(reportEntry.inspection_report.time),
368
+ isSafeToDrive:
369
+ typeof reportEntry.inspection_report.status === "string"
370
+ ? reportEntry.inspection_report.status !== "rejected"
371
+ : null,
372
+ defectCount: 0,
373
+ }),
374
+ ),
375
+ ),
376
+ );
377
+
378
+ return {
379
+ provider: "motive",
380
+ fixtureAuthority: "official-docs",
381
+ drivers,
382
+ vehicles,
383
+ hosEvents,
384
+ gpsLocations,
385
+ dvirs,
386
+ sync: {
387
+ kind: "page",
388
+ token: `${fixtures.pageSync.pagination?.page_no ?? ""}/${fixtures.pageSync.pagination?.per_page ?? ""}`,
389
+ hasMore:
390
+ typeof fixtures.pageSync.pagination?.total === "number" &&
391
+ typeof fixtures.pageSync.pagination?.per_page === "number"
392
+ ? fixtures.pageSync.pagination.total > fixtures.pageSync.pagination.per_page
393
+ : null,
394
+ },
395
+ };
396
+ }
397
+
398
+ export function buildGeotabCanonicalSnapshot(fixtures: {
399
+ users: any;
400
+ devices: any;
401
+ dutyStatusLogs: any;
402
+ driverRegulations: any;
403
+ logRecords: any;
404
+ getfeed: any;
405
+ }): CanonicalSnapshot {
406
+ const drivers = uniqueById<CanonicalDriver>(
407
+ (fixtures.users.result ?? []).map((user: any): CanonicalDriver => ({
408
+ id: toId(user.Id),
409
+ firstName: optionalString(user.FirstName),
410
+ lastName: optionalString(user.LastName),
411
+ status:
412
+ typeof user.ActiveTo === "string" && user.ActiveTo.startsWith("9999")
413
+ ? "active"
414
+ : "inactive",
415
+ username: optionalString(user.Name),
416
+ })),
417
+ );
418
+
419
+ const vehicles = uniqueById<CanonicalVehicle>(
420
+ (fixtures.devices.result ?? []).map((device: any): CanonicalVehicle => ({
421
+ id: toId(device.Id),
422
+ name: optionalString(device.Name),
423
+ vin: optionalString(device.SerialNumber),
424
+ licensePlate: null,
425
+ })),
426
+ );
427
+
428
+ const hosEvents = sortById<CanonicalHosEvent>(
429
+ (fixtures.dutyStatusLogs.result ?? []).map((log: any): CanonicalHosEvent => ({
430
+ id: toId(log.Id),
431
+ driverId: toId(log.Driver?.Id ?? ""),
432
+ vehicleId: toId(log.Device?.Id ?? ""),
433
+ status: optionalString(log.Status),
434
+ startTime: optionalString(log.DateTime),
435
+ endTime: null,
436
+ latitude: optionalNumber(log.Location?.Latitude),
437
+ longitude: optionalNumber(log.Location?.Longitude),
438
+ note: optionalString(log.Origin),
439
+ })),
440
+ );
441
+
442
+ const gpsLocations = sortById<CanonicalGpsLocation>(
443
+ (fixtures.logRecords.result ?? []).map(
444
+ (record: any): CanonicalGpsLocation => ({
445
+ id: toId(record.Id),
446
+ vehicleId: toId(record.Device?.Id ?? ""),
447
+ driverId: null,
448
+ timestamp: optionalString(record.DateTime),
449
+ latitude: optionalNumber(record.Latitude),
450
+ longitude: optionalNumber(record.Longitude),
451
+ speedKph: optionalNumber(record.Speed),
452
+ headingDegrees: null,
453
+ }),
454
+ ),
455
+ );
456
+
457
+ return {
458
+ provider: "geotab",
459
+ fixtureAuthority: "official-docs",
460
+ drivers,
461
+ vehicles,
462
+ hosEvents,
463
+ gpsLocations,
464
+ dvirs: [],
465
+ sync: {
466
+ kind: "version",
467
+ token: toId(fixtures.getfeed.feedResult?.toVersion ?? ""),
468
+ hasMore: null,
469
+ },
470
+ };
471
+ }
@@ -0,0 +1,3 @@
1
+ # Generated TypeScript
2
+
3
+ This directory is reserved for generated TypeScript artifacts that may be surfaced through the package entrypoint later.
File without changes
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./fixture-normalization";
File without changes
File without changes