@neaps/api 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # @neaps/api
2
2
 
3
- HTTP JSON API for tide predictions using NOAA harmonic constituents.
3
+ HTTP JSON API for tide predictions using [neaps](https://github.com/neaps/neaps).
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @neaps/api express
8
+ npm install @neaps/api
9
9
  ```
10
10
 
11
11
  ## Usage
@@ -40,14 +40,14 @@ mainApp.listen(3000, () => {
40
40
 
41
41
  ## API Endpoints
42
42
 
43
- ### GET /extremes
43
+ ### GET /tides/extremes
44
44
 
45
45
  Get high and low tide predictions for the nearest station to given coordinates.
46
46
 
47
47
  **Query Parameters:**
48
48
 
49
- - `lat` or `latitude` (required): Latitude (-90 to 90)
50
- - `lon`, `lng`, or `longitude` (required): Longitude (-180 to 180)
49
+ - `latitude` (required): Latitude (-90 to 90)
50
+ - `longitude` (required): Longitude (-180 to 180)
51
51
  - `start` (required): Start date/time in ISO 8601 format
52
52
  - `end` (required): End date/time in ISO 8601 format
53
53
  - `datum` (optional): Vertical datum (MLLW, MLW, MTL, MSL, MHW, MHHW)
@@ -56,10 +56,10 @@ Get high and low tide predictions for the nearest station to given coordinates.
56
56
  **Example:**
57
57
 
58
58
  ```bash
59
- curl "http://localhost:3000/extremes?lat=26.772&lon=-80.05&start=2025-12-17T00:00:00Z&end=2025-12-18T00:00:00Z&datum=MLLW&units=feet"
59
+ curl "http://localhost:3000/tides/extremes?latitude=26.772&longitude=-80.05&start=2025-12-17T00:00:00Z&end=2025-12-18T00:00:00Z&datum=MLLW&units=feet"
60
60
  ```
61
61
 
62
- ### GET /timeline
62
+ ### GET /tides/timeline
63
63
 
64
64
  Get water level predictions at regular intervals for the nearest station.
65
65
 
@@ -68,31 +68,31 @@ Get water level predictions at regular intervals for the nearest station.
68
68
  **Example:**
69
69
 
70
70
  ```bash
71
- curl "http://localhost:3000/timeline?lat=26.772&lon=-80.05&start=2025-12-17T00:00:00Z&end=2025-12-18T00:00:00Z"
71
+ curl "http://localhost:3000/tides/timeline?latitude=26.772&longitude=-80.05&start=2025-12-17T00:00:00Z&end=2025-12-18T00:00:00Z"
72
72
  ```
73
73
 
74
- ### GET /stations
74
+ ### GET /tides/stations
75
75
 
76
76
  Find stations by ID or near a location.
77
77
 
78
78
  **Query Parameters:**
79
79
 
80
80
  - `id` (optional): Station ID or source ID
81
- - `lat` or `latitude` (optional): Latitude for proximity search
82
- - `lon`, `lng`, or `longitude` (optional): Longitude for proximity search
81
+ - `latitude` (optional): Latitude for proximity search
82
+ - `longitude` (optional): Longitude for proximity search
83
83
  - `limit` (optional): Maximum number of stations to return (1-100, defaults to 10)
84
84
 
85
85
  **Examples:**
86
86
 
87
87
  ```bash
88
88
  # Find a specific station
89
- curl "http://localhost:3000/stations?id=noaa/8722588"
89
+ curl "http://localhost:3000/tides/stations?id=noaa/8722588"
90
90
 
91
91
  # Find stations near coordinates
92
- curl "http://localhost:3000/stations?lat=26.772&lon=-80.05&limit=5"
92
+ curl "http://localhost:3000/tides/stations?latitude=26.772&longitude=-80.05&limit=5"
93
93
  ```
94
94
 
95
- ### GET /stations/:id/extremes
95
+ ### GET /tides/stations/:id/extremes
96
96
 
97
97
  Get extremes prediction for a specific station.
98
98
 
@@ -110,10 +110,10 @@ Get extremes prediction for a specific station.
110
110
  **Example:**
111
111
 
112
112
  ```bash
113
- curl "http://localhost:3000/stations/noaa%2F8722588/extremes?start=2025-12-17T00:00:00Z&end=2025-12-18T00:00:00Z"
113
+ curl "http://localhost:3000/tides/stations/noaa%2F8722588/extremes?start=2025-12-17T00:00:00Z&end=2025-12-18T00:00:00Z"
114
114
  ```
115
115
 
116
- ### GET /stations/:id/timeline
116
+ ### GET /tides/stations/:id/timeline
117
117
 
118
118
  Get timeline prediction for a specific station.
119
119
 
@@ -121,7 +121,7 @@ Get timeline prediction for a specific station.
121
121
 
122
122
  **Note:** Timeline predictions are not supported for subordinate stations.
123
123
 
124
- ### GET /openapi.json
124
+ ### GET /tides/openapi.json
125
125
 
126
126
  Get the OpenAPI 3.0 specification for this API.
127
127
 
package/dist/index.d.mts CHANGED
@@ -1024,7 +1024,7 @@ declare const _default: {
1024
1024
  };
1025
1025
  };
1026
1026
  readonly paths: {
1027
- readonly "/extremes": {
1027
+ readonly "/tides/extremes": {
1028
1028
  readonly get: {
1029
1029
  readonly summary: "Get extremes prediction for a location";
1030
1030
  readonly description: "Returns high and low tide predictions for the nearest station to the given coordinates";
@@ -1065,7 +1065,7 @@ declare const _default: {
1065
1065
  };
1066
1066
  };
1067
1067
  };
1068
- readonly "/timeline": {
1068
+ readonly "/tides/timeline": {
1069
1069
  readonly get: {
1070
1070
  readonly summary: "Get timeline prediction for a location";
1071
1071
  readonly description: "Returns water level predictions at regular intervals for the nearest station";
@@ -1106,7 +1106,7 @@ declare const _default: {
1106
1106
  };
1107
1107
  };
1108
1108
  };
1109
- readonly "/stations": {
1109
+ readonly "/tides/stations": {
1110
1110
  readonly get: {
1111
1111
  readonly summary: "Find stations";
1112
1112
  readonly description: "Find stations by ID or near a location";
@@ -1191,7 +1191,7 @@ declare const _default: {
1191
1191
  };
1192
1192
  };
1193
1193
  };
1194
- readonly "/stations/{id}/extremes": {
1194
+ readonly "/tides/stations/{id}/extremes": {
1195
1195
  readonly get: {
1196
1196
  readonly summary: "Get extremes prediction for a specific station";
1197
1197
  readonly parameters: readonly [{
@@ -1239,7 +1239,7 @@ declare const _default: {
1239
1239
  };
1240
1240
  };
1241
1241
  };
1242
- readonly "/stations/{id}/timeline": {
1242
+ readonly "/tides/stations/{id}/timeline": {
1243
1243
  readonly get: {
1244
1244
  readonly summary: "Get timeline prediction for a specific station";
1245
1245
  readonly parameters: readonly [{
@@ -1287,7 +1287,7 @@ declare const _default: {
1287
1287
  };
1288
1288
  };
1289
1289
  };
1290
- readonly "/openapi.json": {
1290
+ readonly "/tides/openapi.json": {
1291
1291
  readonly get: {
1292
1292
  readonly summary: "Get OpenAPI specification";
1293
1293
  readonly responses: {
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import { findStation, getExtremesPrediction, getTimelinePrediction, stationsNear
3
3
  import { middleware } from "express-openapi-validator";
4
4
 
5
5
  //#region package.json
6
- var version = "0.1.0";
6
+ var version = "0.2.0";
7
7
 
8
8
  //#endregion
9
9
  //#region src/openapi.ts
@@ -16,7 +16,7 @@ var openapi_default = {
16
16
  license: { name: "MIT" }
17
17
  },
18
18
  paths: {
19
- "/extremes": { get: {
19
+ "/tides/extremes": { get: {
20
20
  summary: "Get extremes prediction for a location",
21
21
  description: "Returns high and low tide predictions for the nearest station to the given coordinates",
22
22
  parameters: [
@@ -38,7 +38,7 @@ var openapi_default = {
38
38
  }
39
39
  }
40
40
  } },
41
- "/timeline": { get: {
41
+ "/tides/timeline": { get: {
42
42
  summary: "Get timeline prediction for a location",
43
43
  description: "Returns water level predictions at regular intervals for the nearest station",
44
44
  parameters: [
@@ -60,7 +60,7 @@ var openapi_default = {
60
60
  }
61
61
  }
62
62
  } },
63
- "/stations": { get: {
63
+ "/tides/stations": { get: {
64
64
  summary: "Find stations",
65
65
  description: "Find stations by ID or near a location",
66
66
  parameters: [
@@ -124,7 +124,7 @@ var openapi_default = {
124
124
  }
125
125
  }
126
126
  } },
127
- "/stations/{id}/extremes": { get: {
127
+ "/tides/stations/{id}/extremes": { get: {
128
128
  summary: "Get extremes prediction for a specific station",
129
129
  parameters: [
130
130
  { $ref: "#/components/parameters/stationId" },
@@ -148,7 +148,7 @@ var openapi_default = {
148
148
  }
149
149
  }
150
150
  } },
151
- "/stations/{id}/timeline": { get: {
151
+ "/tides/stations/{id}/timeline": { get: {
152
152
  summary: "Get timeline prediction for a specific station",
153
153
  parameters: [
154
154
  { $ref: "#/components/parameters/stationId" },
@@ -172,7 +172,7 @@ var openapi_default = {
172
172
  }
173
173
  }
174
174
  } },
175
- "/openapi.json": { get: {
175
+ "/tides/openapi.json": { get: {
176
176
  summary: "Get OpenAPI specification",
177
177
  responses: { "200": {
178
178
  description: "OpenAPI specification",
@@ -398,16 +398,16 @@ router.use(middleware({
398
398
  validateRequests: { coerceTypes: true },
399
399
  validateResponses: import.meta.env?.VITEST
400
400
  }));
401
- router.get("/openapi.json", (req, res) => {
401
+ router.get("/tides/openapi.json", (req, res) => {
402
402
  res.json(openapi_default);
403
403
  });
404
- router.get("/extremes", (req, res) => {
404
+ router.get("/tides/extremes", (req, res) => {
405
405
  res.json(getExtremesPrediction({
406
406
  ...positionOptions(req),
407
407
  ...predictionOptions(req)
408
408
  }));
409
409
  });
410
- router.get("/timeline", (req, res) => {
410
+ router.get("/tides/timeline", (req, res) => {
411
411
  try {
412
412
  res.json(getTimelinePrediction({
413
413
  ...positionOptions(req),
@@ -417,7 +417,7 @@ router.get("/timeline", (req, res) => {
417
417
  res.status(400).json({ message: error.message });
418
418
  }
419
419
  });
420
- router.get("/stations", (req, res) => {
420
+ router.get("/tides/stations", (req, res) => {
421
421
  if (req.query.id) try {
422
422
  return res.json(findStation(req.query.id));
423
423
  } catch (error) {
@@ -432,7 +432,7 @@ router.get("/stations", (req, res) => {
432
432
  }, limit);
433
433
  res.json(stations);
434
434
  });
435
- router.get("/stations/:id/extremes", (req, res) => {
435
+ router.get("/tides/stations/:id/extremes", (req, res) => {
436
436
  let station;
437
437
  try {
438
438
  station = findStation(req.params.id);
@@ -441,7 +441,7 @@ router.get("/stations/:id/extremes", (req, res) => {
441
441
  }
442
442
  res.json(station.getExtremesPrediction(predictionOptions(req)));
443
443
  });
444
- router.get("/stations/:id/timeline", (req, res) => {
444
+ router.get("/tides/stations/:id/timeline", (req, res) => {
445
445
  try {
446
446
  const station = findStation(req.params.id);
447
447
  res.json(station.getTimelinePrediction(predictionOptions(req)));
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["pkg.version","openapiValidator","openapi","routes"],"sources":["../package.json","../src/openapi.ts","../src/routes.ts","../src/index.ts"],"sourcesContent":["","import pkg from \"../package.json\" with { type: \"json\" };\n\nexport default {\n openapi: \"3.0.3\",\n info: {\n title: \"Neaps Tide Prediction API\",\n version: pkg.version,\n description: \"HTTP JSON API for tide predictions using harmonic constituents\",\n license: {\n name: \"MIT\",\n },\n },\n paths: {\n \"/extremes\": {\n get: {\n summary: \"Get extremes prediction for a location\",\n description:\n \"Returns high and low tide predictions for the nearest station to the given coordinates\",\n parameters: [\n { $ref: \"#/components/parameters/latitude\" },\n { $ref: \"#/components/parameters/longitude\" },\n { $ref: \"#/components/parameters/start\" },\n { $ref: \"#/components/parameters/end\" },\n { $ref: \"#/components/parameters/datum\" },\n { $ref: \"#/components/parameters/units\" },\n ],\n responses: {\n \"200\": {\n description: \"Successful prediction\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/ExtremesResponse\",\n },\n },\n },\n },\n \"400\": {\n description: \"Invalid parameters\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n },\n },\n },\n \"/timeline\": {\n get: {\n summary: \"Get timeline prediction for a location\",\n description: \"Returns water level predictions at regular intervals for the nearest station\",\n parameters: [\n { $ref: \"#/components/parameters/latitude\" },\n { $ref: \"#/components/parameters/longitude\" },\n { $ref: \"#/components/parameters/start\" },\n { $ref: \"#/components/parameters/end\" },\n { $ref: \"#/components/parameters/datum\" },\n { $ref: \"#/components/parameters/units\" },\n ],\n responses: {\n \"200\": {\n description: \"Successful prediction\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/TimelineResponse\",\n },\n },\n },\n },\n \"400\": {\n description: \"Invalid parameters\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n },\n },\n },\n \"/stations\": {\n get: {\n summary: \"Find stations\",\n description: \"Find stations by ID or near a location\",\n parameters: [\n {\n name: \"id\",\n in: \"query\",\n description: \"Station ID or source ID\",\n required: false,\n schema: {\n type: \"string\",\n },\n },\n {\n name: \"latitude\",\n in: \"query\",\n description: \"Latitude for proximity search\",\n required: false,\n schema: {\n type: \"number\",\n minimum: -90,\n maximum: 90,\n },\n },\n {\n name: \"longitude\",\n in: \"query\",\n description: \"Longitude for proximity search\",\n required: false,\n schema: {\n type: \"number\",\n minimum: -180,\n maximum: 180,\n },\n },\n {\n name: \"limit\",\n in: \"query\",\n description: \"Maximum number of stations to return (for proximity search)\",\n required: false,\n schema: {\n type: \"integer\",\n minimum: 1,\n maximum: 100,\n default: 10,\n },\n },\n ],\n responses: {\n \"200\": {\n description: \"Stations found\",\n content: {\n \"application/json\": {\n schema: {\n oneOf: [\n {\n $ref: \"#/components/schemas/Station\",\n },\n {\n type: \"array\",\n items: {\n $ref: \"#/components/schemas/Station\",\n },\n },\n ],\n },\n },\n },\n },\n \"400\": {\n description: \"Invalid parameters\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n \"404\": {\n description: \"Station not found\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n },\n },\n },\n \"/stations/{id}/extremes\": {\n get: {\n summary: \"Get extremes prediction for a specific station\",\n parameters: [\n { $ref: \"#/components/parameters/stationId\" },\n { $ref: \"#/components/parameters/start\" },\n { $ref: \"#/components/parameters/end\" },\n { $ref: \"#/components/parameters/datum\" },\n { $ref: \"#/components/parameters/units\" },\n ],\n responses: {\n \"200\": {\n description: \"Successful prediction\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/ExtremesResponse\",\n },\n },\n },\n },\n \"400\": {\n description: \"Invalid parameters\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n \"404\": {\n description: \"Station not found\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n },\n },\n },\n \"/stations/{id}/timeline\": {\n get: {\n summary: \"Get timeline prediction for a specific station\",\n parameters: [\n { $ref: \"#/components/parameters/stationId\" },\n { $ref: \"#/components/parameters/start\" },\n { $ref: \"#/components/parameters/end\" },\n { $ref: \"#/components/parameters/datum\" },\n { $ref: \"#/components/parameters/units\" },\n ],\n responses: {\n \"200\": {\n description: \"Successful prediction\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/TimelineResponse\",\n },\n },\n },\n },\n \"400\": {\n description: \"Invalid parameters\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n \"404\": {\n description: \"Station not found\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n },\n },\n },\n \"/openapi.json\": {\n get: {\n summary: \"Get OpenAPI specification\",\n responses: {\n \"200\": {\n description: \"OpenAPI specification\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n },\n },\n },\n },\n },\n },\n },\n },\n components: {\n parameters: {\n latitude: {\n name: \"latitude\",\n in: \"query\",\n description: \"Latitude\",\n required: true,\n schema: {\n type: \"number\",\n minimum: -90,\n maximum: 90,\n },\n },\n longitude: {\n name: \"longitude\",\n in: \"query\",\n description: \"Longitude\",\n required: true,\n schema: {\n type: \"number\",\n minimum: -180,\n maximum: 180,\n },\n },\n start: {\n name: \"start\",\n in: \"query\",\n required: false,\n description: \"Start date/time (ISO 8601 format, defaults to now)\",\n schema: {\n type: \"string\",\n format: \"date-time\",\n },\n },\n end: {\n name: \"end\",\n in: \"query\",\n required: false,\n description: \"End date/time (ISO 8601 format, defaults to 7 days from start)\",\n schema: {\n type: \"string\",\n format: \"date-time\",\n },\n },\n datum: {\n name: \"datum\",\n in: \"query\",\n required: false,\n description: \"Vertical datum (defaults to MLLW if available)\",\n schema: {\n type: \"string\",\n enum: [\"MLLW\", \"MLW\", \"MTL\", \"MSL\", \"MHW\", \"MHHW\"],\n },\n },\n units: {\n name: \"units\",\n in: \"query\",\n required: false,\n description: \"Units for water levels (defaults to meters)\",\n schema: {\n type: \"string\",\n enum: [\"meters\", \"feet\"],\n default: \"meters\",\n },\n },\n stationId: {\n name: \"id\",\n in: \"path\",\n required: true,\n description: \"Station ID or source ID\",\n schema: {\n type: \"string\",\n },\n },\n },\n schemas: {\n Station: {\n type: \"object\",\n properties: {\n id: {\n type: \"string\",\n },\n name: {\n type: \"string\",\n },\n latitude: {\n type: \"number\",\n },\n longitude: {\n type: \"number\",\n },\n region: {\n type: \"string\",\n },\n country: {\n type: \"string\",\n },\n continent: {\n type: \"string\",\n },\n timezone: {\n type: \"string\",\n },\n type: {\n type: \"string\",\n enum: [\"reference\", \"subordinate\"],\n },\n source: {\n type: \"object\",\n additionalProperties: true,\n },\n license: {\n type: \"object\",\n additionalProperties: true,\n },\n disclaimers: {\n type: \"string\",\n },\n distance: {\n type: \"number\",\n description: \"Distance from query point in meters (only for proximity searches)\",\n },\n datums: {\n type: \"object\",\n additionalProperties: {\n type: \"number\",\n },\n },\n harmonic_constituents: {\n type: \"array\",\n items: {\n type: \"object\",\n additionalProperties: true,\n },\n },\n defaultDatum: {\n type: \"string\",\n },\n offsets: {\n type: \"object\",\n additionalProperties: true,\n },\n },\n additionalProperties: true,\n },\n Extreme: {\n type: \"object\",\n properties: {\n time: {\n type: \"string\",\n format: \"date-time\",\n },\n level: {\n type: \"number\",\n },\n high: {\n type: \"boolean\",\n },\n low: {\n type: \"boolean\",\n },\n label: {\n type: \"string\",\n },\n },\n required: [\"time\", \"level\", \"high\", \"low\", \"label\"],\n },\n ExtremesResponse: {\n type: \"object\",\n properties: {\n datum: {\n type: \"string\",\n },\n units: {\n type: \"string\",\n enum: [\"meters\", \"feet\"],\n },\n station: {\n $ref: \"#/components/schemas/Station\",\n },\n distance: {\n type: \"number\",\n },\n extremes: {\n type: \"array\",\n items: {\n $ref: \"#/components/schemas/Extreme\",\n },\n },\n },\n },\n TimelineEntry: {\n type: \"object\",\n properties: {\n time: {\n type: \"string\",\n format: \"date-time\",\n },\n level: {\n type: \"number\",\n },\n },\n required: [\"time\", \"level\"],\n },\n TimelineResponse: {\n type: \"object\",\n properties: {\n datum: {\n type: \"string\",\n },\n units: {\n type: \"string\",\n enum: [\"meters\", \"feet\"],\n },\n station: {\n $ref: \"#/components/schemas/Station\",\n },\n distance: {\n type: \"number\",\n },\n timeline: {\n type: \"array\",\n items: {\n $ref: \"#/components/schemas/TimelineEntry\",\n },\n },\n },\n },\n Error: {\n type: \"object\",\n properties: {\n message: {\n type: \"string\",\n },\n errors: {\n type: \"array\",\n items: {\n type: \"object\",\n additionalProperties: true,\n },\n },\n },\n required: [\"message\"],\n },\n },\n },\n} as const;\n","import { json, Router, Request, Response, type ErrorRequestHandler } from \"express\";\nimport { getExtremesPrediction, getTimelinePrediction, findStation, stationsNear } from \"neaps\";\nimport { middleware as openapiValidator } from \"express-openapi-validator\";\nimport openapi from \"./openapi.js\";\n\nconst router = Router();\n\nrouter.use(json());\n\nrouter.use(\n openapiValidator({\n apiSpec: openapi,\n validateRequests: {\n coerceTypes: true,\n },\n validateResponses: import.meta.env?.VITEST,\n }),\n);\n\nrouter.get(\"/openapi.json\", (req, res) => {\n res.json(openapi);\n});\n\nrouter.get(\"/extremes\", (req: Request, res: Response) => {\n res.json(\n getExtremesPrediction({\n ...positionOptions(req),\n ...predictionOptions(req),\n }),\n );\n});\n\nrouter.get(\"/timeline\", (req: Request, res: Response) => {\n try {\n res.json(\n getTimelinePrediction({\n ...positionOptions(req),\n ...predictionOptions(req),\n }),\n );\n } catch (error) {\n res.status(400).json({ message: (error as Error).message });\n }\n});\n\nrouter.get(\"/stations\", (req: Request, res: Response) => {\n if (req.query.id) {\n try {\n return res.json(findStation(req.query.id as string));\n } catch (error) {\n return res.status(404).json({ message: (error as Error).message });\n }\n }\n\n const { latitude, longitude } = positionOptions(req);\n\n if (latitude === undefined || longitude === undefined) {\n return res.status(400).json({\n message: \"Either 'id' or coordinates (latitude and longitude) required\",\n });\n }\n\n const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10;\n\n const stations = stationsNear({ latitude, longitude }, limit);\n res.json(stations);\n});\n\nrouter.get(\"/stations/:id/extremes\", (req: Request, res: Response) => {\n let station: ReturnType<typeof findStation>;\n\n try {\n station = findStation(req.params.id);\n } catch (error) {\n return res.status(404).json({ message: (error as Error).message });\n }\n\n res.json(station.getExtremesPrediction(predictionOptions(req)));\n});\n\nrouter.get(\"/stations/:id/timeline\", (req: Request, res: Response) => {\n try {\n const station = findStation(req.params.id);\n res.json(station.getTimelinePrediction(predictionOptions(req)));\n } catch (error) {\n if ((error as Error).message.includes(\"not found\")) {\n return res.status(404).json({ message: (error as Error).message });\n }\n // Subordinate station errors and other application errors\n return res.status(400).json({ message: (error as Error).message });\n }\n});\n\nrouter.use(((err, _req, res, next) => {\n if (!err) return next();\n\n const status = err.status ?? 500;\n const message = err.message ?? \"Unknown error\";\n\n res.status(status).json({ message, errors: err.errors });\n}) satisfies ErrorRequestHandler);\n\nfunction positionOptions(req: Request) {\n return {\n latitude: req.query.latitude as unknown as number,\n longitude: req.query.longitude as unknown as number,\n };\n}\n\nfunction predictionOptions(req: Request) {\n return {\n start: req.query.start ? new Date(req.query.start as string) : new Date(),\n end: req.query.end\n ? new Date(req.query.end as string)\n : new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),\n ...(req.query.datum && { datum: req.query.datum as string }),\n ...(req.query.units && { units: req.query.units as \"meters\" | \"feet\" }),\n };\n}\n\nexport default router;\n","import express from \"express\";\nimport routes from \"./routes.js\";\nimport openapi from \"./openapi.js\";\n\nexport function createApp() {\n return express().use(\"/\", routes);\n}\n\nexport { routes, openapi };\n"],"mappings":";;;;;;;;;ACEA,sBAAe;CACb,SAAS;CACT,MAAM;EACJ,OAAO;EACEA;EACT,aAAa;EACb,SAAS,EACP,MAAM,OACP;EACF;CACD,OAAO;EACL,aAAa,EACX,KAAK;GACH,SAAS;GACT,aACE;GACF,YAAY;IACV,EAAE,MAAM,oCAAoC;IAC5C,EAAE,MAAM,qCAAqC;IAC7C,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,+BAA+B;IACvC,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,iCAAiC;IAC1C;GACD,WAAW;IACT,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,yCACP,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACF;GACF,EACF;EACD,aAAa,EACX,KAAK;GACH,SAAS;GACT,aAAa;GACb,YAAY;IACV,EAAE,MAAM,oCAAoC;IAC5C,EAAE,MAAM,qCAAqC;IAC7C,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,+BAA+B;IACvC,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,iCAAiC;IAC1C;GACD,WAAW;IACT,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,yCACP,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACF;GACF,EACF;EACD,aAAa,EACX,KAAK;GACH,SAAS;GACT,aAAa;GACb,YAAY;IACV;KACE,MAAM;KACN,IAAI;KACJ,aAAa;KACb,UAAU;KACV,QAAQ,EACN,MAAM,UACP;KACF;IACD;KACE,MAAM;KACN,IAAI;KACJ,aAAa;KACb,UAAU;KACV,QAAQ;MACN,MAAM;MACN,SAAS;MACT,SAAS;MACV;KACF;IACD;KACE,MAAM;KACN,IAAI;KACJ,aAAa;KACb,UAAU;KACV,QAAQ;MACN,MAAM;MACN,SAAS;MACT,SAAS;MACV;KACF;IACD;KACE,MAAM;KACN,IAAI;KACJ,aAAa;KACb,UAAU;KACV,QAAQ;MACN,MAAM;MACN,SAAS;MACT,SAAS;MACT,SAAS;MACV;KACF;IACF;GACD,WAAW;IACT,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,OAAO,CACL,EACE,MAAM,gCACP,EACD;MACE,MAAM;MACN,OAAO,EACL,MAAM,gCACP;MACF,CACF,EACF,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACF;GACF,EACF;EACD,2BAA2B,EACzB,KAAK;GACH,SAAS;GACT,YAAY;IACV,EAAE,MAAM,qCAAqC;IAC7C,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,+BAA+B;IACvC,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,iCAAiC;IAC1C;GACD,WAAW;IACT,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,yCACP,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACF;GACF,EACF;EACD,2BAA2B,EACzB,KAAK;GACH,SAAS;GACT,YAAY;IACV,EAAE,MAAM,qCAAqC;IAC7C,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,+BAA+B;IACvC,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,iCAAiC;IAC1C;GACD,WAAW;IACT,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,yCACP,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACF;GACF,EACF;EACD,iBAAiB,EACf,KAAK;GACH,SAAS;GACT,WAAW,EACT,OAAO;IACL,aAAa;IACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,UACP,EACF,EACF;IACF,EACF;GACF,EACF;EACF;CACD,YAAY;EACV,YAAY;GACV,UAAU;IACR,MAAM;IACN,IAAI;IACJ,aAAa;IACb,UAAU;IACV,QAAQ;KACN,MAAM;KACN,SAAS;KACT,SAAS;KACV;IACF;GACD,WAAW;IACT,MAAM;IACN,IAAI;IACJ,aAAa;IACb,UAAU;IACV,QAAQ;KACN,MAAM;KACN,SAAS;KACT,SAAS;KACV;IACF;GACD,OAAO;IACL,MAAM;IACN,IAAI;IACJ,UAAU;IACV,aAAa;IACb,QAAQ;KACN,MAAM;KACN,QAAQ;KACT;IACF;GACD,KAAK;IACH,MAAM;IACN,IAAI;IACJ,UAAU;IACV,aAAa;IACb,QAAQ;KACN,MAAM;KACN,QAAQ;KACT;IACF;GACD,OAAO;IACL,MAAM;IACN,IAAI;IACJ,UAAU;IACV,aAAa;IACb,QAAQ;KACN,MAAM;KACN,MAAM;MAAC;MAAQ;MAAO;MAAO;MAAO;MAAO;MAAO;KACnD;IACF;GACD,OAAO;IACL,MAAM;IACN,IAAI;IACJ,UAAU;IACV,aAAa;IACb,QAAQ;KACN,MAAM;KACN,MAAM,CAAC,UAAU,OAAO;KACxB,SAAS;KACV;IACF;GACD,WAAW;IACT,MAAM;IACN,IAAI;IACJ,UAAU;IACV,aAAa;IACb,QAAQ,EACN,MAAM,UACP;IACF;GACF;EACD,SAAS;GACP,SAAS;IACP,MAAM;IACN,YAAY;KACV,IAAI,EACF,MAAM,UACP;KACD,MAAM,EACJ,MAAM,UACP;KACD,UAAU,EACR,MAAM,UACP;KACD,WAAW,EACT,MAAM,UACP;KACD,QAAQ,EACN,MAAM,UACP;KACD,SAAS,EACP,MAAM,UACP;KACD,WAAW,EACT,MAAM,UACP;KACD,UAAU,EACR,MAAM,UACP;KACD,MAAM;MACJ,MAAM;MACN,MAAM,CAAC,aAAa,cAAc;MACnC;KACD,QAAQ;MACN,MAAM;MACN,sBAAsB;MACvB;KACD,SAAS;MACP,MAAM;MACN,sBAAsB;MACvB;KACD,aAAa,EACX,MAAM,UACP;KACD,UAAU;MACR,MAAM;MACN,aAAa;MACd;KACD,QAAQ;MACN,MAAM;MACN,sBAAsB,EACpB,MAAM,UACP;MACF;KACD,uBAAuB;MACrB,MAAM;MACN,OAAO;OACL,MAAM;OACN,sBAAsB;OACvB;MACF;KACD,cAAc,EACZ,MAAM,UACP;KACD,SAAS;MACP,MAAM;MACN,sBAAsB;MACvB;KACF;IACD,sBAAsB;IACvB;GACD,SAAS;IACP,MAAM;IACN,YAAY;KACV,MAAM;MACJ,MAAM;MACN,QAAQ;MACT;KACD,OAAO,EACL,MAAM,UACP;KACD,MAAM,EACJ,MAAM,WACP;KACD,KAAK,EACH,MAAM,WACP;KACD,OAAO,EACL,MAAM,UACP;KACF;IACD,UAAU;KAAC;KAAQ;KAAS;KAAQ;KAAO;KAAQ;IACpD;GACD,kBAAkB;IAChB,MAAM;IACN,YAAY;KACV,OAAO,EACL,MAAM,UACP;KACD,OAAO;MACL,MAAM;MACN,MAAM,CAAC,UAAU,OAAO;MACzB;KACD,SAAS,EACP,MAAM,gCACP;KACD,UAAU,EACR,MAAM,UACP;KACD,UAAU;MACR,MAAM;MACN,OAAO,EACL,MAAM,gCACP;MACF;KACF;IACF;GACD,eAAe;IACb,MAAM;IACN,YAAY;KACV,MAAM;MACJ,MAAM;MACN,QAAQ;MACT;KACD,OAAO,EACL,MAAM,UACP;KACF;IACD,UAAU,CAAC,QAAQ,QAAQ;IAC5B;GACD,kBAAkB;IAChB,MAAM;IACN,YAAY;KACV,OAAO,EACL,MAAM,UACP;KACD,OAAO;MACL,MAAM;MACN,MAAM,CAAC,UAAU,OAAO;MACzB;KACD,SAAS,EACP,MAAM,gCACP;KACD,UAAU,EACR,MAAM,UACP;KACD,UAAU;MACR,MAAM;MACN,OAAO,EACL,MAAM,sC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"}
1
+ {"version":3,"file":"index.mjs","names":["pkg.version","openapiValidator","openapi","routes"],"sources":["../package.json","../src/openapi.ts","../src/routes.ts","../src/index.ts"],"sourcesContent":["","import pkg from \"../package.json\" with { type: \"json\" };\n\nexport default {\n openapi: \"3.0.3\",\n info: {\n title: \"Neaps Tide Prediction API\",\n version: pkg.version,\n description: \"HTTP JSON API for tide predictions using harmonic constituents\",\n license: {\n name: \"MIT\",\n },\n },\n paths: {\n \"/tides/extremes\": {\n get: {\n summary: \"Get extremes prediction for a location\",\n description:\n \"Returns high and low tide predictions for the nearest station to the given coordinates\",\n parameters: [\n { $ref: \"#/components/parameters/latitude\" },\n { $ref: \"#/components/parameters/longitude\" },\n { $ref: \"#/components/parameters/start\" },\n { $ref: \"#/components/parameters/end\" },\n { $ref: \"#/components/parameters/datum\" },\n { $ref: \"#/components/parameters/units\" },\n ],\n responses: {\n \"200\": {\n description: \"Successful prediction\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/ExtremesResponse\",\n },\n },\n },\n },\n \"400\": {\n description: \"Invalid parameters\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n },\n },\n },\n \"/tides/timeline\": {\n get: {\n summary: \"Get timeline prediction for a location\",\n description: \"Returns water level predictions at regular intervals for the nearest station\",\n parameters: [\n { $ref: \"#/components/parameters/latitude\" },\n { $ref: \"#/components/parameters/longitude\" },\n { $ref: \"#/components/parameters/start\" },\n { $ref: \"#/components/parameters/end\" },\n { $ref: \"#/components/parameters/datum\" },\n { $ref: \"#/components/parameters/units\" },\n ],\n responses: {\n \"200\": {\n description: \"Successful prediction\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/TimelineResponse\",\n },\n },\n },\n },\n \"400\": {\n description: \"Invalid parameters\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n },\n },\n },\n \"/tides/stations\": {\n get: {\n summary: \"Find stations\",\n description: \"Find stations by ID or near a location\",\n parameters: [\n {\n name: \"id\",\n in: \"query\",\n description: \"Station ID or source ID\",\n required: false,\n schema: {\n type: \"string\",\n },\n },\n {\n name: \"latitude\",\n in: \"query\",\n description: \"Latitude for proximity search\",\n required: false,\n schema: {\n type: \"number\",\n minimum: -90,\n maximum: 90,\n },\n },\n {\n name: \"longitude\",\n in: \"query\",\n description: \"Longitude for proximity search\",\n required: false,\n schema: {\n type: \"number\",\n minimum: -180,\n maximum: 180,\n },\n },\n {\n name: \"limit\",\n in: \"query\",\n description: \"Maximum number of stations to return (for proximity search)\",\n required: false,\n schema: {\n type: \"integer\",\n minimum: 1,\n maximum: 100,\n default: 10,\n },\n },\n ],\n responses: {\n \"200\": {\n description: \"Stations found\",\n content: {\n \"application/json\": {\n schema: {\n oneOf: [\n {\n $ref: \"#/components/schemas/Station\",\n },\n {\n type: \"array\",\n items: {\n $ref: \"#/components/schemas/Station\",\n },\n },\n ],\n },\n },\n },\n },\n \"400\": {\n description: \"Invalid parameters\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n \"404\": {\n description: \"Station not found\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n },\n },\n },\n \"/tides/stations/{id}/extremes\": {\n get: {\n summary: \"Get extremes prediction for a specific station\",\n parameters: [\n { $ref: \"#/components/parameters/stationId\" },\n { $ref: \"#/components/parameters/start\" },\n { $ref: \"#/components/parameters/end\" },\n { $ref: \"#/components/parameters/datum\" },\n { $ref: \"#/components/parameters/units\" },\n ],\n responses: {\n \"200\": {\n description: \"Successful prediction\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/ExtremesResponse\",\n },\n },\n },\n },\n \"400\": {\n description: \"Invalid parameters\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n \"404\": {\n description: \"Station not found\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n },\n },\n },\n \"/tides/stations/{id}/timeline\": {\n get: {\n summary: \"Get timeline prediction for a specific station\",\n parameters: [\n { $ref: \"#/components/parameters/stationId\" },\n { $ref: \"#/components/parameters/start\" },\n { $ref: \"#/components/parameters/end\" },\n { $ref: \"#/components/parameters/datum\" },\n { $ref: \"#/components/parameters/units\" },\n ],\n responses: {\n \"200\": {\n description: \"Successful prediction\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/TimelineResponse\",\n },\n },\n },\n },\n \"400\": {\n description: \"Invalid parameters\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n \"404\": {\n description: \"Station not found\",\n content: {\n \"application/json\": {\n schema: {\n $ref: \"#/components/schemas/Error\",\n },\n },\n },\n },\n },\n },\n },\n \"/tides/openapi.json\": {\n get: {\n summary: \"Get OpenAPI specification\",\n responses: {\n \"200\": {\n description: \"OpenAPI specification\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n },\n },\n },\n },\n },\n },\n },\n },\n components: {\n parameters: {\n latitude: {\n name: \"latitude\",\n in: \"query\",\n description: \"Latitude\",\n required: true,\n schema: {\n type: \"number\",\n minimum: -90,\n maximum: 90,\n },\n },\n longitude: {\n name: \"longitude\",\n in: \"query\",\n description: \"Longitude\",\n required: true,\n schema: {\n type: \"number\",\n minimum: -180,\n maximum: 180,\n },\n },\n start: {\n name: \"start\",\n in: \"query\",\n required: false,\n description: \"Start date/time (ISO 8601 format, defaults to now)\",\n schema: {\n type: \"string\",\n format: \"date-time\",\n },\n },\n end: {\n name: \"end\",\n in: \"query\",\n required: false,\n description: \"End date/time (ISO 8601 format, defaults to 7 days from start)\",\n schema: {\n type: \"string\",\n format: \"date-time\",\n },\n },\n datum: {\n name: \"datum\",\n in: \"query\",\n required: false,\n description: \"Vertical datum (defaults to MLLW if available)\",\n schema: {\n type: \"string\",\n enum: [\"MLLW\", \"MLW\", \"MTL\", \"MSL\", \"MHW\", \"MHHW\"],\n },\n },\n units: {\n name: \"units\",\n in: \"query\",\n required: false,\n description: \"Units for water levels (defaults to meters)\",\n schema: {\n type: \"string\",\n enum: [\"meters\", \"feet\"],\n default: \"meters\",\n },\n },\n stationId: {\n name: \"id\",\n in: \"path\",\n required: true,\n description: \"Station ID or source ID\",\n schema: {\n type: \"string\",\n },\n },\n },\n schemas: {\n Station: {\n type: \"object\",\n properties: {\n id: {\n type: \"string\",\n },\n name: {\n type: \"string\",\n },\n latitude: {\n type: \"number\",\n },\n longitude: {\n type: \"number\",\n },\n region: {\n type: \"string\",\n },\n country: {\n type: \"string\",\n },\n continent: {\n type: \"string\",\n },\n timezone: {\n type: \"string\",\n },\n type: {\n type: \"string\",\n enum: [\"reference\", \"subordinate\"],\n },\n source: {\n type: \"object\",\n additionalProperties: true,\n },\n license: {\n type: \"object\",\n additionalProperties: true,\n },\n disclaimers: {\n type: \"string\",\n },\n distance: {\n type: \"number\",\n description: \"Distance from query point in meters (only for proximity searches)\",\n },\n datums: {\n type: \"object\",\n additionalProperties: {\n type: \"number\",\n },\n },\n harmonic_constituents: {\n type: \"array\",\n items: {\n type: \"object\",\n additionalProperties: true,\n },\n },\n defaultDatum: {\n type: \"string\",\n },\n offsets: {\n type: \"object\",\n additionalProperties: true,\n },\n },\n additionalProperties: true,\n },\n Extreme: {\n type: \"object\",\n properties: {\n time: {\n type: \"string\",\n format: \"date-time\",\n },\n level: {\n type: \"number\",\n },\n high: {\n type: \"boolean\",\n },\n low: {\n type: \"boolean\",\n },\n label: {\n type: \"string\",\n },\n },\n required: [\"time\", \"level\", \"high\", \"low\", \"label\"],\n },\n ExtremesResponse: {\n type: \"object\",\n properties: {\n datum: {\n type: \"string\",\n },\n units: {\n type: \"string\",\n enum: [\"meters\", \"feet\"],\n },\n station: {\n $ref: \"#/components/schemas/Station\",\n },\n distance: {\n type: \"number\",\n },\n extremes: {\n type: \"array\",\n items: {\n $ref: \"#/components/schemas/Extreme\",\n },\n },\n },\n },\n TimelineEntry: {\n type: \"object\",\n properties: {\n time: {\n type: \"string\",\n format: \"date-time\",\n },\n level: {\n type: \"number\",\n },\n },\n required: [\"time\", \"level\"],\n },\n TimelineResponse: {\n type: \"object\",\n properties: {\n datum: {\n type: \"string\",\n },\n units: {\n type: \"string\",\n enum: [\"meters\", \"feet\"],\n },\n station: {\n $ref: \"#/components/schemas/Station\",\n },\n distance: {\n type: \"number\",\n },\n timeline: {\n type: \"array\",\n items: {\n $ref: \"#/components/schemas/TimelineEntry\",\n },\n },\n },\n },\n Error: {\n type: \"object\",\n properties: {\n message: {\n type: \"string\",\n },\n errors: {\n type: \"array\",\n items: {\n type: \"object\",\n additionalProperties: true,\n },\n },\n },\n required: [\"message\"],\n },\n },\n },\n} as const;\n","import { json, Router, Request, Response, type ErrorRequestHandler } from \"express\";\nimport { getExtremesPrediction, getTimelinePrediction, findStation, stationsNear } from \"neaps\";\nimport { middleware as openapiValidator } from \"express-openapi-validator\";\nimport openapi from \"./openapi.js\";\n\nconst router = Router();\n\nrouter.use(json());\n\nrouter.use(\n openapiValidator({\n apiSpec: openapi,\n validateRequests: {\n coerceTypes: true,\n },\n validateResponses: import.meta.env?.VITEST,\n }),\n);\n\nrouter.get(\"/tides/openapi.json\", (req, res) => {\n res.json(openapi);\n});\n\nrouter.get(\"/tides/extremes\", (req: Request, res: Response) => {\n res.json(\n getExtremesPrediction({\n ...positionOptions(req),\n ...predictionOptions(req),\n }),\n );\n});\n\nrouter.get(\"/tides/timeline\", (req: Request, res: Response) => {\n try {\n res.json(\n getTimelinePrediction({\n ...positionOptions(req),\n ...predictionOptions(req),\n }),\n );\n } catch (error) {\n res.status(400).json({ message: (error as Error).message });\n }\n});\n\nrouter.get(\"/tides/stations\", (req: Request, res: Response) => {\n if (req.query.id) {\n try {\n return res.json(findStation(req.query.id as string));\n } catch (error) {\n return res.status(404).json({ message: (error as Error).message });\n }\n }\n\n const { latitude, longitude } = positionOptions(req);\n\n if (latitude === undefined || longitude === undefined) {\n return res.status(400).json({\n message: \"Either 'id' or coordinates (latitude and longitude) required\",\n });\n }\n\n const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10;\n\n const stations = stationsNear({ latitude, longitude }, limit);\n res.json(stations);\n});\n\nrouter.get(\"/tides/stations/:id/extremes\", (req: Request, res: Response) => {\n let station: ReturnType<typeof findStation>;\n\n try {\n station = findStation(req.params.id);\n } catch (error) {\n return res.status(404).json({ message: (error as Error).message });\n }\n\n res.json(station.getExtremesPrediction(predictionOptions(req)));\n});\n\nrouter.get(\"/tides/stations/:id/timeline\", (req: Request, res: Response) => {\n try {\n const station = findStation(req.params.id);\n res.json(station.getTimelinePrediction(predictionOptions(req)));\n } catch (error) {\n if ((error as Error).message.includes(\"not found\")) {\n return res.status(404).json({ message: (error as Error).message });\n }\n // Subordinate station errors and other application errors\n return res.status(400).json({ message: (error as Error).message });\n }\n});\n\nrouter.use(((err, _req, res, next) => {\n if (!err) return next();\n\n const status = err.status ?? 500;\n const message = err.message ?? \"Unknown error\";\n\n res.status(status).json({ message, errors: err.errors });\n}) satisfies ErrorRequestHandler);\n\nfunction positionOptions(req: Request) {\n return {\n latitude: req.query.latitude as unknown as number,\n longitude: req.query.longitude as unknown as number,\n };\n}\n\nfunction predictionOptions(req: Request) {\n return {\n start: req.query.start ? new Date(req.query.start as string) : new Date(),\n end: req.query.end\n ? new Date(req.query.end as string)\n : new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),\n ...(req.query.datum && { datum: req.query.datum as string }),\n ...(req.query.units && { units: req.query.units as \"meters\" | \"feet\" }),\n };\n}\n\nexport default router;\n","import express from \"express\";\nimport routes from \"./routes.js\";\nimport openapi from \"./openapi.js\";\n\nexport function createApp() {\n return express().use(\"/\", routes);\n}\n\nexport { routes, openapi };\n"],"mappings":";;;;;;;;;ACEA,sBAAe;CACb,SAAS;CACT,MAAM;EACJ,OAAO;EACEA;EACT,aAAa;EACb,SAAS,EACP,MAAM,OACP;EACF;CACD,OAAO;EACL,mBAAmB,EACjB,KAAK;GACH,SAAS;GACT,aACE;GACF,YAAY;IACV,EAAE,MAAM,oCAAoC;IAC5C,EAAE,MAAM,qCAAqC;IAC7C,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,+BAA+B;IACvC,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,iCAAiC;IAC1C;GACD,WAAW;IACT,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,yCACP,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACF;GACF,EACF;EACD,mBAAmB,EACjB,KAAK;GACH,SAAS;GACT,aAAa;GACb,YAAY;IACV,EAAE,MAAM,oCAAoC;IAC5C,EAAE,MAAM,qCAAqC;IAC7C,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,+BAA+B;IACvC,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,iCAAiC;IAC1C;GACD,WAAW;IACT,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,yCACP,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACF;GACF,EACF;EACD,mBAAmB,EACjB,KAAK;GACH,SAAS;GACT,aAAa;GACb,YAAY;IACV;KACE,MAAM;KACN,IAAI;KACJ,aAAa;KACb,UAAU;KACV,QAAQ,EACN,MAAM,UACP;KACF;IACD;KACE,MAAM;KACN,IAAI;KACJ,aAAa;KACb,UAAU;KACV,QAAQ;MACN,MAAM;MACN,SAAS;MACT,SAAS;MACV;KACF;IACD;KACE,MAAM;KACN,IAAI;KACJ,aAAa;KACb,UAAU;KACV,QAAQ;MACN,MAAM;MACN,SAAS;MACT,SAAS;MACV;KACF;IACD;KACE,MAAM;KACN,IAAI;KACJ,aAAa;KACb,UAAU;KACV,QAAQ;MACN,MAAM;MACN,SAAS;MACT,SAAS;MACT,SAAS;MACV;KACF;IACF;GACD,WAAW;IACT,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,OAAO,CACL,EACE,MAAM,gCACP,EACD;MACE,MAAM;MACN,OAAO,EACL,MAAM,gCACP;MACF,CACF,EACF,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACF;GACF,EACF;EACD,iCAAiC,EAC/B,KAAK;GACH,SAAS;GACT,YAAY;IACV,EAAE,MAAM,qCAAqC;IAC7C,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,+BAA+B;IACvC,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,iCAAiC;IAC1C;GACD,WAAW;IACT,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,yCACP,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACF;GACF,EACF;EACD,iCAAiC,EAC/B,KAAK;GACH,SAAS;GACT,YAAY;IACV,EAAE,MAAM,qCAAqC;IAC7C,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,+BAA+B;IACvC,EAAE,MAAM,iCAAiC;IACzC,EAAE,MAAM,iCAAiC;IAC1C;GACD,WAAW;IACT,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,yCACP,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACD,OAAO;KACL,aAAa;KACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,8BACP,EACF,EACF;KACF;IACF;GACF,EACF;EACD,uBAAuB,EACrB,KAAK;GACH,SAAS;GACT,WAAW,EACT,OAAO;IACL,aAAa;IACb,SAAS,EACP,oBAAoB,EAClB,QAAQ,EACN,MAAM,UACP,EACF,EACF;IACF,EACF;GACF,EACF;EACF;CACD,YAAY;EACV,YAAY;GACV,UAAU;IACR,MAAM;IACN,IAAI;IACJ,aAAa;IACb,UAAU;IACV,QAAQ;KACN,MAAM;KACN,SAAS;KACT,SAAS;KACV;IACF;GACD,WAAW;IACT,MAAM;IACN,IAAI;IACJ,aAAa;IACb,UAAU;IACV,QAAQ;KACN,MAAM;KACN,SAAS;KACT,SAAS;KACV;IACF;GACD,OAAO;IACL,MAAM;IACN,IAAI;IACJ,UAAU;IACV,aAAa;IACb,QAAQ;KACN,MAAM;KACN,QAAQ;KACT;IACF;GACD,KAAK;IACH,MAAM;IACN,IAAI;IACJ,UAAU;IACV,aAAa;IACb,QAAQ;KACN,MAAM;KACN,QAAQ;KACT;IACF;GACD,OAAO;IACL,MAAM;IACN,IAAI;IACJ,UAAU;IACV,aAAa;IACb,QAAQ;KACN,MAAM;KACN,MAAM;MAAC;MAAQ;MAAO;MAAO;MAAO;MAAO;MAAO;KACnD;IACF;GACD,OAAO;IACL,MAAM;IACN,IAAI;IACJ,UAAU;IACV,aAAa;IACb,QAAQ;KACN,MAAM;KACN,MAAM,CAAC,UAAU,OAAO;KACxB,SAAS;KACV;IACF;GACD,WAAW;IACT,MAAM;IACN,IAAI;IACJ,UAAU;IACV,aAAa;IACb,QAAQ,EACN,MAAM,UACP;IACF;GACF;EACD,SAAS;GACP,SAAS;IACP,MAAM;IACN,YAAY;KACV,IAAI,EACF,MAAM,UACP;KACD,MAAM,EACJ,MAAM,UACP;KACD,UAAU,EACR,MAAM,UACP;KACD,WAAW,EACT,MAAM,UACP;KACD,QAAQ,EACN,MAAM,UACP;KACD,SAAS,EACP,MAAM,UACP;KACD,WAAW,EACT,MAAM,UACP;KACD,UAAU,EACR,MAAM,UACP;KACD,MAAM;MACJ,MAAM;MACN,MAAM,CAAC,aAAa,cAAc;MACnC;KACD,QAAQ;MACN,MAAM;MACN,sBAAsB;MACvB;KACD,SAAS;MACP,MAAM;MACN,sBAAsB;MACvB;KACD,aAAa,EACX,MAAM,UACP;KACD,UAAU;MACR,MAAM;MACN,aAAa;MACd;KACD,QAAQ;MACN,MAAM;MACN,sBAAsB,EACpB,MAAM,UACP;MACF;KACD,uBAAuB;MACrB,MAAM;MACN,OAAO;OACL,MAAM;OACN,sBAAsB;OACvB;MACF;KACD,cAAc,EACZ,MAAM,UACP;KACD,SAAS;MACP,MAAM;MACN,sBAAsB;MACvB;KACF;IACD,sBAAsB;IACvB;GACD,SAAS;IACP,MAAM;IACN,YAAY;KACV,MAAM;MACJ,MAAM;MACN,QAAQ;MACT;KACD,OAAO,EACL,MAAM,UACP;KACD,MAAM,EACJ,MAAM,WACP;KACD,KAAK,EACH,MAAM,WACP;KACD,OAAO,EACL,MAAM,UACP;KACF;IACD,UAAU;KAAC;KAAQ;KAAS;KAAQ;KAAO;KAAQ;IACpD;GACD,kBAAkB;IAChB,MAAM;IACN,YAAY;KACV,OAAO,EACL,MAAM,UACP;KACD,OAAO;MACL,MAAM;MACN,MAAM,CAAC,UAAU,OAAO;MACzB;KACD,SAAS,EACP,MAAM,gCACP;KACD,UAAU,EACR,MAAM,UACP;KACD,UAAU;MACR,MAAM;MACN,OAAO,EACL,MAAM,gCACP;MACF;KACF;IACF;GACD,eAAe;IACb,MAAM;IACN,YAAY;KACV,MAAM;MACJ,MAAM;MACN,QAAQ;MACT;KACD,OAAO,EACL,MAAM,UACP;KACF;IACD,UAAU,CAAC,QAAQ,QAAQ;IAC5B;GACD,kBAAkB;IAChB,MAAM;IACN,YAAY;KACV,OAAO,EACL,MAAM,UACP;KACD,OAAO;MACL,MAAM;MACN,MAAM,CAAC,UAAU,OAAO;MACzB;KACD,SAAS,EACP,MAAM,gCACP;KACD,UAAU,EACR,MAAM,UACP;KACD,UAAU;MACR,MAAM;MACN,OAAO,EA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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neaps/api",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "HTTP JSON API for tide predictions",
5
5
  "repository": {
6
6
  "type": "git",