@signalk/tracks-plugin 3.0.0-beta.6 → 3.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -106,6 +106,13 @@ _`timespan` and `timespanOffset` are also accepted for Freeboard-SK compatibilit
106
106
  `timespan=23h&timespanOffset=1` means "23 hours ending an hour ago". They are not part of
107
107
  the proposed track API and are expected to be superseded by `from`/`to`._
108
108
 
109
+ _A parameter these routes do not read is rejected with `400` rather than ignored, so a
110
+ typo such as `?duratoin=6h` fails rather than quietly returning the whole retained track.
111
+ `bbox` and `radius` are **not** accepted here: they select which vessels to return and so
112
+ belong to `/tracks` below. To ask whether one vessel's track passed through a box, use the
113
+ v2 Track API, which matches any position in the window ([being designed in
114
+ SignalK/signalk-server#2504](https://github.com/SignalK/signalk-server/issues/2504))._
115
+
109
116
  ---
110
117
 
111
118
  **Reduce the number of points returned:**
package/dist/index.js CHANGED
@@ -135,6 +135,20 @@ function nearestLongitude(lng, origin) {
135
135
  * `resolution` is shared by both and expresses the minimum spacing between
136
136
  * returned points, thinning the track for wide time ranges.
137
137
  */
138
+ /**
139
+ * The query keys these routes read. Kept beside the schema so the two cannot
140
+ * drift: a key added to one without the other either fails validation or is
141
+ * rejected as unknown.
142
+ */
143
+ var QUERY_KEYS = [
144
+ "from",
145
+ "to",
146
+ "duration",
147
+ "timespan",
148
+ "timespanOffset",
149
+ "resolution",
150
+ "times"
151
+ ];
138
152
  var QuerySchema = Type.Object({
139
153
  from: Type.Optional(Type.String()),
140
154
  to: Type.Optional(Type.String()),
@@ -200,7 +214,12 @@ function firstString(value) {
200
214
  * `timespan`/`timespanOffset` pair. Absent all of them, no window is applied
201
215
  * and the whole retained track is returned, as before.
202
216
  */
203
- function parseTrackQuery(query, now = Date.now()) {
217
+ function parseTrackQuery(query, now = Date.now(), alsoAllowed = []) {
218
+ const unknown = Object.keys(query).filter((key) => !QUERY_KEYS.includes(key) && !alsoAllowed.includes(key));
219
+ if (unknown.length > 0) {
220
+ const allowed = [...QUERY_KEYS, ...alsoAllowed].join(", ");
221
+ throw new TimeWindowError(`Unknown query parameter${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")}; accepted: ${allowed}`);
222
+ }
204
223
  const raw = {
205
224
  from: firstString(query.from),
206
225
  to: firstString(query.to),
@@ -411,21 +430,44 @@ function toFiniteNumber(value) {
411
430
  const parsed = Number(trimmed);
412
431
  return Number.isFinite(parsed) ? parsed : void 0;
413
432
  }
433
+ /**
434
+ * A query parameter was present but malformed.
435
+ *
436
+ * Distinct from absent: a filter nobody asked for is not an error, but one
437
+ * that cannot be honoured must not quietly widen the query. Carries its own
438
+ * message so the handler can hand it to the client, the way `TimeWindowError`
439
+ * already does for the time window.
440
+ */
441
+ var QueryParameterError = class extends Error {};
442
+ /**
443
+ * A well-formed query the server cannot currently answer.
444
+ *
445
+ * `radius` measures from the own vessel, so it needs a self position. Not
446
+ * having one is an outage, not a client mistake -- the same distinction the
447
+ * single-vessel route already draws for a history provider it cannot read.
448
+ */
449
+ var SelfPositionUnavailableError = class extends Error {};
414
450
  function validateParameters(params, defaultMaxRadius) {
415
451
  let bbox = null;
416
452
  const rawBbox = firstValue(params.bbox);
417
453
  if (rawBbox !== void 0) {
418
454
  const b = rawBbox.split(",").map(toFiniteNumber);
419
455
  const [west, south, east, north] = b;
420
- if (b.length === 4 && west !== void 0 && south !== void 0 && east !== void 0 && north !== void 0) bbox = {
456
+ if (b.length !== 4 || west === void 0 || south === void 0 || east === void 0 || north === void 0) throw new QueryParameterError("bbox must be four comma-separated numbers: west,south,east,north");
457
+ if (south > north) throw new QueryParameterError(`bbox south (${south}) must not be greater than north (${north})`);
458
+ bbox = {
421
459
  sw: [south, west],
422
460
  ne: [north, east]
423
461
  };
424
462
  }
425
463
  let radius = null;
426
464
  const rawRadius = firstValue(params.radius);
427
- if (rawRadius !== void 0) radius = toFiniteNumber(rawRadius) ?? null;
428
- else if (defaultMaxRadius) radius = defaultMaxRadius;
465
+ if (rawRadius !== void 0) {
466
+ const parsed = toFiniteNumber(rawRadius);
467
+ if (parsed === void 0) throw new QueryParameterError(`radius must be a number in meters, got '${rawRadius}'`);
468
+ if (parsed < 0) throw new QueryParameterError(`radius must not be negative, got ${parsed}`);
469
+ radius = parsed;
470
+ } else if (defaultMaxRadius) radius = defaultMaxRadius;
429
471
  return {
430
472
  bbox,
431
473
  radius
@@ -463,7 +505,7 @@ function createMatcher(params, selfPosition, debug) {
463
505
  const inBounds = createInBounds(params.bbox);
464
506
  return params.intersects ? (track) => track.some(inBounds) : (track) => inBounds(lastPoint(track));
465
507
  } else if (params.radius !== null) {
466
- if (!selfPosition) throw new Error("No self position to calculate radius values");
508
+ if (!selfPosition) throw new SelfPositionUnavailableError("No self position to calculate radius values");
467
509
  const radius = params.radius;
468
510
  const distanceFromSelf = createDistanceTo(selfPosition, debug);
469
511
  return (track) => distanceFromSelf(lastPoint(track)) < radius;
@@ -13667,6 +13709,8 @@ function ThePlugin(app) {
13667
13709
  router.get("/vessels/:vesselId/track.gpx", gpxHandler((req) => resolveContext(String(req.params.vesselId), app.selfContext)));
13668
13710
  router.get("/self/track.gpx", gpxHandler(() => app.selfContext));
13669
13711
  router.get("/self/track", singleTrackHandler(() => app.selfContext));
13712
+ /** Only the collection route filters spatially; see AGENTS.md on v1 vs v2. */
13713
+ const SPATIAL_PARAMS = ["bbox", "radius"];
13670
13714
  const allTracksHandler = (req, res) => {
13671
13715
  app.debug(req.query);
13672
13716
  if (!tracks) {
@@ -13674,14 +13718,15 @@ function ThePlugin(app) {
13674
13718
  return;
13675
13719
  }
13676
13720
  let query;
13721
+ let params;
13677
13722
  try {
13678
- query = parseTrackQuery(req.query);
13723
+ query = parseTrackQuery(req.query, Date.now(), SPATIAL_PARAMS);
13724
+ params = validateParameters(req.query, void 0);
13679
13725
  } catch (err) {
13680
13726
  res.status(400);
13681
- res.json({ message: err instanceof TimeWindowError ? err.message : "Invalid query parameters" });
13727
+ res.json({ message: err instanceof TimeWindowError || err instanceof QueryParameterError ? err.message : "Invalid query parameters" });
13682
13728
  return;
13683
13729
  }
13684
- const params = validateParameters(req.query, void 0);
13685
13730
  const selfPosition = getVesselPosition();
13686
13731
  (query.times ? tracks.getFilteredTimedTracks(params, selfPosition, app.debug, query).then((tc) => Object.entries(tc).reduce((acc, [context, points]) => {
13687
13732
  const segments = segment(points, segmentGap);
@@ -13703,7 +13748,12 @@ function ThePlugin(app) {
13703
13748
  return acc;
13704
13749
  }, {}))).then((trks) => {
13705
13750
  res.json(trks);
13706
- }).catch(() => {
13751
+ }).catch((err) => {
13752
+ if (err instanceof SelfPositionUnavailableError) {
13753
+ res.status(503);
13754
+ res.json({ message: "No position for the own vessel, so radius cannot be measured" });
13755
+ return;
13756
+ }
13707
13757
  res.status(404);
13708
13758
  res.json({ message: `No track available for vessels.` });
13709
13759
  });