@neaps/api 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.
package/dist/index.mjs ADDED
@@ -0,0 +1,486 @@
1
+ import express, { Router, json } from "express";
2
+ import { findStation, getExtremesPrediction, getTimelinePrediction, stationsNear } from "neaps";
3
+ import { middleware } from "express-openapi-validator";
4
+
5
+ //#region package.json
6
+ var version = "0.1.0";
7
+
8
+ //#endregion
9
+ //#region src/openapi.ts
10
+ var openapi_default = {
11
+ openapi: "3.0.3",
12
+ info: {
13
+ title: "Neaps Tide Prediction API",
14
+ version,
15
+ description: "HTTP JSON API for tide predictions using harmonic constituents",
16
+ license: { name: "MIT" }
17
+ },
18
+ paths: {
19
+ "/extremes": { get: {
20
+ summary: "Get extremes prediction for a location",
21
+ description: "Returns high and low tide predictions for the nearest station to the given coordinates",
22
+ parameters: [
23
+ { $ref: "#/components/parameters/latitude" },
24
+ { $ref: "#/components/parameters/longitude" },
25
+ { $ref: "#/components/parameters/start" },
26
+ { $ref: "#/components/parameters/end" },
27
+ { $ref: "#/components/parameters/datum" },
28
+ { $ref: "#/components/parameters/units" }
29
+ ],
30
+ responses: {
31
+ "200": {
32
+ description: "Successful prediction",
33
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ExtremesResponse" } } }
34
+ },
35
+ "400": {
36
+ description: "Invalid parameters",
37
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
38
+ }
39
+ }
40
+ } },
41
+ "/timeline": { get: {
42
+ summary: "Get timeline prediction for a location",
43
+ description: "Returns water level predictions at regular intervals for the nearest station",
44
+ parameters: [
45
+ { $ref: "#/components/parameters/latitude" },
46
+ { $ref: "#/components/parameters/longitude" },
47
+ { $ref: "#/components/parameters/start" },
48
+ { $ref: "#/components/parameters/end" },
49
+ { $ref: "#/components/parameters/datum" },
50
+ { $ref: "#/components/parameters/units" }
51
+ ],
52
+ responses: {
53
+ "200": {
54
+ description: "Successful prediction",
55
+ content: { "application/json": { schema: { $ref: "#/components/schemas/TimelineResponse" } } }
56
+ },
57
+ "400": {
58
+ description: "Invalid parameters",
59
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
60
+ }
61
+ }
62
+ } },
63
+ "/stations": { get: {
64
+ summary: "Find stations",
65
+ description: "Find stations by ID or near a location",
66
+ parameters: [
67
+ {
68
+ name: "id",
69
+ in: "query",
70
+ description: "Station ID or source ID",
71
+ required: false,
72
+ schema: { type: "string" }
73
+ },
74
+ {
75
+ name: "latitude",
76
+ in: "query",
77
+ description: "Latitude for proximity search",
78
+ required: false,
79
+ schema: {
80
+ type: "number",
81
+ minimum: -90,
82
+ maximum: 90
83
+ }
84
+ },
85
+ {
86
+ name: "longitude",
87
+ in: "query",
88
+ description: "Longitude for proximity search",
89
+ required: false,
90
+ schema: {
91
+ type: "number",
92
+ minimum: -180,
93
+ maximum: 180
94
+ }
95
+ },
96
+ {
97
+ name: "limit",
98
+ in: "query",
99
+ description: "Maximum number of stations to return (for proximity search)",
100
+ required: false,
101
+ schema: {
102
+ type: "integer",
103
+ minimum: 1,
104
+ maximum: 100,
105
+ default: 10
106
+ }
107
+ }
108
+ ],
109
+ responses: {
110
+ "200": {
111
+ description: "Stations found",
112
+ content: { "application/json": { schema: { oneOf: [{ $ref: "#/components/schemas/Station" }, {
113
+ type: "array",
114
+ items: { $ref: "#/components/schemas/Station" }
115
+ }] } } }
116
+ },
117
+ "400": {
118
+ description: "Invalid parameters",
119
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
120
+ },
121
+ "404": {
122
+ description: "Station not found",
123
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
124
+ }
125
+ }
126
+ } },
127
+ "/stations/{id}/extremes": { get: {
128
+ summary: "Get extremes prediction for a specific station",
129
+ parameters: [
130
+ { $ref: "#/components/parameters/stationId" },
131
+ { $ref: "#/components/parameters/start" },
132
+ { $ref: "#/components/parameters/end" },
133
+ { $ref: "#/components/parameters/datum" },
134
+ { $ref: "#/components/parameters/units" }
135
+ ],
136
+ responses: {
137
+ "200": {
138
+ description: "Successful prediction",
139
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ExtremesResponse" } } }
140
+ },
141
+ "400": {
142
+ description: "Invalid parameters",
143
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
144
+ },
145
+ "404": {
146
+ description: "Station not found",
147
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
148
+ }
149
+ }
150
+ } },
151
+ "/stations/{id}/timeline": { get: {
152
+ summary: "Get timeline prediction for a specific station",
153
+ parameters: [
154
+ { $ref: "#/components/parameters/stationId" },
155
+ { $ref: "#/components/parameters/start" },
156
+ { $ref: "#/components/parameters/end" },
157
+ { $ref: "#/components/parameters/datum" },
158
+ { $ref: "#/components/parameters/units" }
159
+ ],
160
+ responses: {
161
+ "200": {
162
+ description: "Successful prediction",
163
+ content: { "application/json": { schema: { $ref: "#/components/schemas/TimelineResponse" } } }
164
+ },
165
+ "400": {
166
+ description: "Invalid parameters",
167
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
168
+ },
169
+ "404": {
170
+ description: "Station not found",
171
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
172
+ }
173
+ }
174
+ } },
175
+ "/openapi.json": { get: {
176
+ summary: "Get OpenAPI specification",
177
+ responses: { "200": {
178
+ description: "OpenAPI specification",
179
+ content: { "application/json": { schema: { type: "object" } } }
180
+ } }
181
+ } }
182
+ },
183
+ components: {
184
+ parameters: {
185
+ latitude: {
186
+ name: "latitude",
187
+ in: "query",
188
+ description: "Latitude",
189
+ required: true,
190
+ schema: {
191
+ type: "number",
192
+ minimum: -90,
193
+ maximum: 90
194
+ }
195
+ },
196
+ longitude: {
197
+ name: "longitude",
198
+ in: "query",
199
+ description: "Longitude",
200
+ required: true,
201
+ schema: {
202
+ type: "number",
203
+ minimum: -180,
204
+ maximum: 180
205
+ }
206
+ },
207
+ start: {
208
+ name: "start",
209
+ in: "query",
210
+ required: false,
211
+ description: "Start date/time (ISO 8601 format, defaults to now)",
212
+ schema: {
213
+ type: "string",
214
+ format: "date-time"
215
+ }
216
+ },
217
+ end: {
218
+ name: "end",
219
+ in: "query",
220
+ required: false,
221
+ description: "End date/time (ISO 8601 format, defaults to 7 days from start)",
222
+ schema: {
223
+ type: "string",
224
+ format: "date-time"
225
+ }
226
+ },
227
+ datum: {
228
+ name: "datum",
229
+ in: "query",
230
+ required: false,
231
+ description: "Vertical datum (defaults to MLLW if available)",
232
+ schema: {
233
+ type: "string",
234
+ enum: [
235
+ "MLLW",
236
+ "MLW",
237
+ "MTL",
238
+ "MSL",
239
+ "MHW",
240
+ "MHHW"
241
+ ]
242
+ }
243
+ },
244
+ units: {
245
+ name: "units",
246
+ in: "query",
247
+ required: false,
248
+ description: "Units for water levels (defaults to meters)",
249
+ schema: {
250
+ type: "string",
251
+ enum: ["meters", "feet"],
252
+ default: "meters"
253
+ }
254
+ },
255
+ stationId: {
256
+ name: "id",
257
+ in: "path",
258
+ required: true,
259
+ description: "Station ID or source ID",
260
+ schema: { type: "string" }
261
+ }
262
+ },
263
+ schemas: {
264
+ Station: {
265
+ type: "object",
266
+ properties: {
267
+ id: { type: "string" },
268
+ name: { type: "string" },
269
+ latitude: { type: "number" },
270
+ longitude: { type: "number" },
271
+ region: { type: "string" },
272
+ country: { type: "string" },
273
+ continent: { type: "string" },
274
+ timezone: { type: "string" },
275
+ type: {
276
+ type: "string",
277
+ enum: ["reference", "subordinate"]
278
+ },
279
+ source: {
280
+ type: "object",
281
+ additionalProperties: true
282
+ },
283
+ license: {
284
+ type: "object",
285
+ additionalProperties: true
286
+ },
287
+ disclaimers: { type: "string" },
288
+ distance: {
289
+ type: "number",
290
+ description: "Distance from query point in meters (only for proximity searches)"
291
+ },
292
+ datums: {
293
+ type: "object",
294
+ additionalProperties: { type: "number" }
295
+ },
296
+ harmonic_constituents: {
297
+ type: "array",
298
+ items: {
299
+ type: "object",
300
+ additionalProperties: true
301
+ }
302
+ },
303
+ defaultDatum: { type: "string" },
304
+ offsets: {
305
+ type: "object",
306
+ additionalProperties: true
307
+ }
308
+ },
309
+ additionalProperties: true
310
+ },
311
+ Extreme: {
312
+ type: "object",
313
+ properties: {
314
+ time: {
315
+ type: "string",
316
+ format: "date-time"
317
+ },
318
+ level: { type: "number" },
319
+ high: { type: "boolean" },
320
+ low: { type: "boolean" },
321
+ label: { type: "string" }
322
+ },
323
+ required: [
324
+ "time",
325
+ "level",
326
+ "high",
327
+ "low",
328
+ "label"
329
+ ]
330
+ },
331
+ ExtremesResponse: {
332
+ type: "object",
333
+ properties: {
334
+ datum: { type: "string" },
335
+ units: {
336
+ type: "string",
337
+ enum: ["meters", "feet"]
338
+ },
339
+ station: { $ref: "#/components/schemas/Station" },
340
+ distance: { type: "number" },
341
+ extremes: {
342
+ type: "array",
343
+ items: { $ref: "#/components/schemas/Extreme" }
344
+ }
345
+ }
346
+ },
347
+ TimelineEntry: {
348
+ type: "object",
349
+ properties: {
350
+ time: {
351
+ type: "string",
352
+ format: "date-time"
353
+ },
354
+ level: { type: "number" }
355
+ },
356
+ required: ["time", "level"]
357
+ },
358
+ TimelineResponse: {
359
+ type: "object",
360
+ properties: {
361
+ datum: { type: "string" },
362
+ units: {
363
+ type: "string",
364
+ enum: ["meters", "feet"]
365
+ },
366
+ station: { $ref: "#/components/schemas/Station" },
367
+ distance: { type: "number" },
368
+ timeline: {
369
+ type: "array",
370
+ items: { $ref: "#/components/schemas/TimelineEntry" }
371
+ }
372
+ }
373
+ },
374
+ Error: {
375
+ type: "object",
376
+ properties: {
377
+ message: { type: "string" },
378
+ errors: {
379
+ type: "array",
380
+ items: {
381
+ type: "object",
382
+ additionalProperties: true
383
+ }
384
+ }
385
+ },
386
+ required: ["message"]
387
+ }
388
+ }
389
+ }
390
+ };
391
+
392
+ //#endregion
393
+ //#region src/routes.ts
394
+ const router = Router();
395
+ router.use(json());
396
+ router.use(middleware({
397
+ apiSpec: openapi_default,
398
+ validateRequests: { coerceTypes: true },
399
+ validateResponses: import.meta.env?.VITEST
400
+ }));
401
+ router.get("/openapi.json", (req, res) => {
402
+ res.json(openapi_default);
403
+ });
404
+ router.get("/extremes", (req, res) => {
405
+ res.json(getExtremesPrediction({
406
+ ...positionOptions(req),
407
+ ...predictionOptions(req)
408
+ }));
409
+ });
410
+ router.get("/timeline", (req, res) => {
411
+ try {
412
+ res.json(getTimelinePrediction({
413
+ ...positionOptions(req),
414
+ ...predictionOptions(req)
415
+ }));
416
+ } catch (error) {
417
+ res.status(400).json({ message: error.message });
418
+ }
419
+ });
420
+ router.get("/stations", (req, res) => {
421
+ if (req.query.id) try {
422
+ return res.json(findStation(req.query.id));
423
+ } catch (error) {
424
+ return res.status(404).json({ message: error.message });
425
+ }
426
+ const { latitude, longitude } = positionOptions(req);
427
+ if (latitude === void 0 || longitude === void 0) return res.status(400).json({ message: "Either 'id' or coordinates (latitude and longitude) required" });
428
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : 10;
429
+ const stations = stationsNear({
430
+ latitude,
431
+ longitude
432
+ }, limit);
433
+ res.json(stations);
434
+ });
435
+ router.get("/stations/:id/extremes", (req, res) => {
436
+ let station;
437
+ try {
438
+ station = findStation(req.params.id);
439
+ } catch (error) {
440
+ return res.status(404).json({ message: error.message });
441
+ }
442
+ res.json(station.getExtremesPrediction(predictionOptions(req)));
443
+ });
444
+ router.get("/stations/:id/timeline", (req, res) => {
445
+ try {
446
+ const station = findStation(req.params.id);
447
+ res.json(station.getTimelinePrediction(predictionOptions(req)));
448
+ } catch (error) {
449
+ if (error.message.includes("not found")) return res.status(404).json({ message: error.message });
450
+ return res.status(400).json({ message: error.message });
451
+ }
452
+ });
453
+ router.use(((err, _req, res, next) => {
454
+ if (!err) return next();
455
+ const status = err.status ?? 500;
456
+ const message = err.message ?? "Unknown error";
457
+ res.status(status).json({
458
+ message,
459
+ errors: err.errors
460
+ });
461
+ }));
462
+ function positionOptions(req) {
463
+ return {
464
+ latitude: req.query.latitude,
465
+ longitude: req.query.longitude
466
+ };
467
+ }
468
+ function predictionOptions(req) {
469
+ return {
470
+ start: req.query.start ? new Date(req.query.start) : /* @__PURE__ */ new Date(),
471
+ end: req.query.end ? new Date(req.query.end) : new Date(Date.now() + 10080 * 60 * 1e3),
472
+ ...req.query.datum && { datum: req.query.datum },
473
+ ...req.query.units && { units: req.query.units }
474
+ };
475
+ }
476
+ var routes_default = router;
477
+
478
+ //#endregion
479
+ //#region src/index.ts
480
+ function createApp() {
481
+ return express().use("/", routes_default);
482
+ }
483
+
484
+ //#endregion
485
+ export { createApp, openapi_default as openapi, routes_default as routes };
486
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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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"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@neaps/api",
3
+ "version": "0.1.0",
4
+ "description": "HTTP JSON API for tide predictions",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/neaps/neaps.git",
8
+ "directory": "packages/api"
9
+ },
10
+ "author": "Brandon Keepers <brandon@openwaters.io>",
11
+ "license": "MIT",
12
+ "type": "module",
13
+ "types": "./dist/index.d.mts",
14
+ "exports": {
15
+ ".": "./dist/index.mjs",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "bin": {
22
+ "neaps-server": "./bin/server.js"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc && tsdown",
26
+ "prepack": "npm run build",
27
+ "start": "node bin/server.js"
28
+ },
29
+ "dependencies": {
30
+ "express": "^4.18.2",
31
+ "express-openapi-validator": "^5.1.6",
32
+ "neaps": "^0.2.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/express": "^4.17.21",
36
+ "@types/supertest": "^6.0.2",
37
+ "supertest": "^6.3.3"
38
+ }
39
+ }