Skip to content

Commit 642ba88

Browse files
authored
feat: add endpoint for maps plugin to get info about custom map (#898)
1 parent f21a675 commit 642ba88

File tree

3 files changed

+166
-1
lines changed

3 files changed

+166
-1
lines changed

src/fastify-plugins/maps.js

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import fs from 'node:fs/promises'
2+
import path from 'node:path'
13
import { fetch } from 'undici'
24
import { Server as SMPServerPlugin } from 'styled-map-package'
35

46
import { noop } from '../utils.js'
7+
import { NotFoundError, ENOENTError } from './utils.js'
58

69
/** @import { FastifyPluginAsync } from 'fastify' */
10+
/** @import { Stats } from 'node:fs' */
711

812
export const CUSTOM_MAP_PREFIX = 'custom'
913
export const FALLBACK_MAP_PREFIX = 'fallback'
@@ -19,9 +23,62 @@ export const FALLBACK_MAP_PREFIX = 'fallback'
1923
/** @type {FastifyPluginAsync<MapsPluginOpts>} */
2024
export async function plugin(fastify, opts) {
2125
if (opts.customMapPath) {
26+
const { customMapPath } = opts
27+
28+
fastify.get(`/${CUSTOM_MAP_PREFIX}/info`, async () => {
29+
const baseUrl = new URL(fastify.prefix, fastify.listeningOrigin)
30+
31+
if (!baseUrl.href.endsWith('/')) {
32+
baseUrl.href += '/'
33+
}
34+
35+
const customStyleJsonUrl = new URL(
36+
`${CUSTOM_MAP_PREFIX}/style.json`,
37+
baseUrl
38+
)
39+
const response = await fetch(customStyleJsonUrl)
40+
41+
if (response.status === 404) {
42+
throw new NotFoundError(customStyleJsonUrl.href)
43+
}
44+
45+
if (!response.ok) {
46+
throw new Error(`Failed to get style from ${customStyleJsonUrl.href}`)
47+
}
48+
49+
/** @type {Stats | undefined} */
50+
let stats
51+
52+
try {
53+
stats = await fs.stat(customMapPath)
54+
} catch (err) {
55+
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
56+
throw new ENOENTError(customMapPath)
57+
}
58+
59+
throw err
60+
}
61+
62+
const style = await response.json()
63+
64+
const styleJsonName =
65+
typeof style === 'object' &&
66+
style &&
67+
'name' in style &&
68+
typeof style.name === 'string'
69+
? style.name
70+
: undefined
71+
72+
return {
73+
created: stats.ctime,
74+
size: stats.size,
75+
name: styleJsonName || path.parse(customMapPath).name,
76+
}
77+
})
78+
2279
fastify.register(SMPServerPlugin, {
2380
prefix: CUSTOM_MAP_PREFIX,
24-
filepath: opts.customMapPath,
81+
filepath: customMapPath,
2582
})
2683
}
2784

src/fastify-plugins/utils.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ export const NotFoundError = createError(
77
404
88
)
99

10+
export const ENOENTError = createError(
11+
'FST_ENOENT',
12+
"ENOENT: no such file or directory '%s'",
13+
404
14+
)
15+
1016
/**
1117
* @param {import('node:http').Server} server
1218
* @param {{ timeout?: number }} [options]

test/fastify-plugins/maps.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import assert from 'node:assert/strict'
2+
import fs from 'node:fs/promises'
3+
import path from 'node:path'
24
import test from 'node:test'
35
import Fastify from 'fastify'
46
import { Reader } from 'styled-map-package'
@@ -223,6 +225,106 @@ test('/style.json resolves style.json of fallback map when custom and online are
223225
)
224226
})
225227

228+
test('custom map info endpoint not available when custom map path not provided', async (t) => {
229+
const server = setup(t)
230+
231+
server.register(MapServerPlugin, {
232+
defaultOnlineStyleUrl: DEFAULT_ONLINE_STYLE_URL,
233+
fallbackMapPath: DEFAULT_FALLBACK_MAP_FILE_PATH,
234+
})
235+
236+
const address = await server.listen()
237+
238+
mockAgent.enableNetConnect(new URL(address).host)
239+
240+
const response = await server.inject({
241+
method: 'GET',
242+
url: `/${CUSTOM_MAP_PREFIX}/info`,
243+
})
244+
245+
assert.equal(response.statusCode, 404)
246+
})
247+
248+
test('custom map info endpoint returns not found error when custom map does not exist', async (t) => {
249+
const server = setup(t)
250+
251+
const nonExistentFile =
252+
path.parse(SAMPLE_SMP_FIXTURE_PATH).dir + '/does/not/exist.smp'
253+
254+
server.register(MapServerPlugin, {
255+
customMapPath: nonExistentFile,
256+
defaultOnlineStyleUrl: DEFAULT_ONLINE_STYLE_URL,
257+
fallbackMapPath: DEFAULT_FALLBACK_MAP_FILE_PATH,
258+
})
259+
260+
const address = await server.listen()
261+
262+
mockAgent.enableNetConnect(new URL(address).host)
263+
264+
const response = await server.inject({
265+
method: 'GET',
266+
url: `/${CUSTOM_MAP_PREFIX}/info`,
267+
})
268+
269+
assert.equal(response.statusCode, 404)
270+
assert.match(response.json().error, /Not Found/)
271+
})
272+
273+
test('custom map info endpoint returns server error when custom map is invalid', async (t) => {
274+
const server = setup(t)
275+
276+
const invalidFile = new URL(import.meta.url).pathname
277+
278+
server.register(MapServerPlugin, {
279+
customMapPath: invalidFile,
280+
defaultOnlineStyleUrl: DEFAULT_ONLINE_STYLE_URL,
281+
fallbackMapPath: DEFAULT_FALLBACK_MAP_FILE_PATH,
282+
})
283+
284+
const address = await server.listen()
285+
286+
mockAgent.enableNetConnect(new URL(address).host)
287+
288+
const response = await server.inject({
289+
method: 'GET',
290+
url: `/${CUSTOM_MAP_PREFIX}/info`,
291+
})
292+
293+
assert.equal(response.statusCode, 500)
294+
assert.match(response.json().error, /Internal Server Error/)
295+
})
296+
297+
test('custom map info endpoint returns expected info when valid custom map is available', async (t) => {
298+
const server = setup(t)
299+
300+
const smpStats = await fs.stat(SAMPLE_SMP_FIXTURE_PATH)
301+
302+
server.register(MapServerPlugin, {
303+
customMapPath: SAMPLE_SMP_FIXTURE_PATH,
304+
defaultOnlineStyleUrl: DEFAULT_ONLINE_STYLE_URL,
305+
fallbackMapPath: DEFAULT_FALLBACK_MAP_FILE_PATH,
306+
})
307+
308+
const address = await server.listen()
309+
310+
mockAgent.enableNetConnect(new URL(address).host)
311+
312+
const response = await server.inject({
313+
method: 'GET',
314+
url: `/${CUSTOM_MAP_PREFIX}/info`,
315+
})
316+
317+
assert.equal(response.statusCode, 200)
318+
319+
const info = response.json()
320+
321+
assert.deepEqual(info, {
322+
created: smpStats.ctime.toISOString(),
323+
size: smpStats.size,
324+
name: 'MapLibre',
325+
})
326+
})
327+
226328
/**
227329
* @param {import('node:test').TestContext} t
228330
*/

0 commit comments

Comments
 (0)