@chaosity/location-client 0.1.11 → 0.1.13
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 +29 -10
- package/dist/adapters/GeoPlaces.d.ts +1 -1
- package/dist/adapters/GeoPlaces.js +31 -15
- package/dist/auth/TokenProvider.js +19 -8
- package/dist/client/GeoPlacesClient.d.ts +2 -2
- package/dist/client/GeoPlacesClient.js +15 -3
- package/dist/errors/LocationServiceException.d.ts +16 -0
- package/dist/errors/LocationServiceException.js +24 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +5 -3
- package/dist/maps/Utils.d.ts +2 -2
- package/dist/maps/Utils.js +1 -1
- package/dist/maps/createTransformRequest.js +1 -1
- package/dist/maps/mapLanguage.d.ts +3 -3
- package/dist/maps/mapLanguage.js +8 -3
- package/dist/maps/mapStyle.js +10 -5
- package/dist/server/LocationServiceConnector.d.ts +2 -1
- package/dist/server/LocationServiceConnector.js +23 -12
- package/dist/server/getClientConfig.d.ts +1 -1
- package/dist/server/getClientConfig.js +10 -4
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +1 -1
- package/dist/types/index.d.ts +27 -0
- package/dist/utils/roundPosition.d.ts +1 -1
- package/package.json +15 -3
- package/dist/server/getConnector.d.ts +0 -37
- package/dist/server/getConnector.js +0 -34
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ const client = new GeoPlacesClient({
|
|
|
81
81
|
})
|
|
82
82
|
|
|
83
83
|
const response = await client.send(
|
|
84
|
-
new SuggestCommand({ QueryText: 'Vancouver', MaxResults: 5 })
|
|
84
|
+
new SuggestCommand({ QueryText: 'Vancouver', MaxResults: 5 }),
|
|
85
85
|
)
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -90,7 +90,10 @@ const response = await client.send(
|
|
|
90
90
|
Use `fetchMapStyle` to fetch a style descriptor with authentication and optional customization, and `createTransformRequest` to attach Bearer tokens to all subsequent tile/glyph/sprite requests:
|
|
91
91
|
|
|
92
92
|
```typescript
|
|
93
|
-
import {
|
|
93
|
+
import {
|
|
94
|
+
fetchMapStyle,
|
|
95
|
+
createTransformRequest,
|
|
96
|
+
} from '@chaosity/location-client'
|
|
94
97
|
import maplibregl from 'maplibre-gl'
|
|
95
98
|
|
|
96
99
|
const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
|
|
@@ -129,7 +132,11 @@ applyMapLanguage(map, 'ja')
|
|
|
129
132
|
Toggle point-of-interest categories on or off:
|
|
130
133
|
|
|
131
134
|
```typescript
|
|
132
|
-
import {
|
|
135
|
+
import {
|
|
136
|
+
setPoiVisibility,
|
|
137
|
+
setAllPoiVisibility,
|
|
138
|
+
POI_CATEGORIES,
|
|
139
|
+
} from '@chaosity/location-client'
|
|
133
140
|
|
|
134
141
|
// Hide transit POIs
|
|
135
142
|
setPoiVisibility(map, 'transit', false)
|
|
@@ -231,7 +238,10 @@ const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
|
|
|
231
238
|
language: 'fr',
|
|
232
239
|
})
|
|
233
240
|
|
|
234
|
-
const map = new maplibregl.Map({
|
|
241
|
+
const map = new maplibregl.Map({
|
|
242
|
+
style,
|
|
243
|
+
transformRequest: createTransformRequest(apiUrl, getToken),
|
|
244
|
+
})
|
|
235
245
|
```
|
|
236
246
|
|
|
237
247
|
#### buildMapStyleUrl
|
|
@@ -241,7 +251,10 @@ Builds the style descriptor URL without fetching. Useful when you want to pass t
|
|
|
241
251
|
```typescript
|
|
242
252
|
import { buildMapStyleUrl } from '@chaosity/location-client'
|
|
243
253
|
|
|
244
|
-
const url = buildMapStyleUrl(apiUrl, 'Standard', {
|
|
254
|
+
const url = buildMapStyleUrl(apiUrl, 'Standard', {
|
|
255
|
+
colorScheme: 'Dark',
|
|
256
|
+
terrain: 'Hillshade',
|
|
257
|
+
})
|
|
245
258
|
```
|
|
246
259
|
|
|
247
260
|
#### MapStyleOptions
|
|
@@ -249,10 +262,10 @@ const url = buildMapStyleUrl(apiUrl, 'Standard', { colorScheme: 'Dark', terrain:
|
|
|
249
262
|
```typescript
|
|
250
263
|
interface MapStyleOptions {
|
|
251
264
|
colorScheme?: 'Light' | 'Dark'
|
|
252
|
-
politicalView?: string
|
|
265
|
+
politicalView?: string // ISO 3166-1 alpha-3 (e.g. 'IND', 'TUR')
|
|
253
266
|
terrain?: 'Hillshade' | 'Terrain3D'
|
|
254
267
|
buildings?: 'Buildings3D'
|
|
255
|
-
contourDensity?: 'Medium'
|
|
268
|
+
contourDensity?: 'Medium' // Only 'Medium' is supported by the AWS SDK
|
|
256
269
|
traffic?: 'All'
|
|
257
270
|
travelModes?: Array<'Truck' | 'Transit'>
|
|
258
271
|
}
|
|
@@ -271,7 +284,11 @@ applyMapLanguage(map, 'fr')
|
|
|
271
284
|
#### POI Layer Control
|
|
272
285
|
|
|
273
286
|
```typescript
|
|
274
|
-
import {
|
|
287
|
+
import {
|
|
288
|
+
setPoiVisibility,
|
|
289
|
+
setAllPoiVisibility,
|
|
290
|
+
POI_CATEGORIES,
|
|
291
|
+
} from '@chaosity/location-client'
|
|
275
292
|
import type { PoiCategory } from '@chaosity/location-client'
|
|
276
293
|
|
|
277
294
|
setPoiVisibility(map, 'transit', false)
|
|
@@ -346,7 +363,9 @@ const connector = new LocationServiceConnector({
|
|
|
346
363
|
token: config.token,
|
|
347
364
|
})
|
|
348
365
|
|
|
349
|
-
const result = await connector.send(
|
|
366
|
+
const result = await connector.send(
|
|
367
|
+
new SuggestCommand({ QueryText: 'Vancouver' }),
|
|
368
|
+
)
|
|
350
369
|
```
|
|
351
370
|
|
|
352
371
|
## Cache-Friendly Position Rounding
|
|
@@ -380,7 +399,7 @@ Full TypeScript support with types from AWS SDK:
|
|
|
380
399
|
import type { SuggestCommandOutput } from '@aws-sdk/client-geo-places'
|
|
381
400
|
|
|
382
401
|
const response: SuggestCommandOutput = await client.send(
|
|
383
|
-
new SuggestCommand({ QueryText: 'Vancouver' })
|
|
402
|
+
new SuggestCommand({ QueryText: 'Vancouver' }),
|
|
384
403
|
)
|
|
385
404
|
```
|
|
386
405
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MaplibreGeocoderApi, MaplibreGeocoderApiConfig, MaplibreGeocoderFeatureResults, MaplibreGeocoderPlaceResults, MaplibreGeocoderSuggestionResults } from '@maplibre/maplibre-gl-geocoder';
|
|
2
2
|
import type { Map } from 'maplibre-gl';
|
|
3
|
-
import { GeoPlacesClient } from '../client/GeoPlacesClient';
|
|
3
|
+
import type { GeoPlacesClient } from '../client/GeoPlacesClient';
|
|
4
4
|
/**
|
|
5
5
|
* GeoPlaces - MapLibre adapter for AWS Location Service
|
|
6
6
|
*
|
|
@@ -33,11 +33,15 @@ export class GeoPlaces {
|
|
|
33
33
|
};
|
|
34
34
|
if (config.countries) {
|
|
35
35
|
commandInput.Filter = {
|
|
36
|
-
IncludeCountries: Array.isArray(config.countries)
|
|
36
|
+
IncludeCountries: Array.isArray(config.countries)
|
|
37
|
+
? config.countries
|
|
38
|
+
: config.countries.split(','),
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
|
-
const response = await this.client.send(new GeocodeCommand(commandInput));
|
|
40
|
-
const result = geocodeResponseToFeatureCollection(response, {
|
|
41
|
+
const response = (await this.client.send(new GeocodeCommand(commandInput)));
|
|
42
|
+
const result = geocodeResponseToFeatureCollection(response, {
|
|
43
|
+
flattenProperties: true,
|
|
44
|
+
});
|
|
41
45
|
log('forwardGeocode returned %d results', result.features?.length ?? 0);
|
|
42
46
|
return result;
|
|
43
47
|
}
|
|
@@ -51,8 +55,10 @@ export class GeoPlaces {
|
|
|
51
55
|
MaxResults: config.limit || 1,
|
|
52
56
|
Language: this.normalizeLanguage(config.language),
|
|
53
57
|
};
|
|
54
|
-
const response = await this.client.send(new ReverseGeocodeCommand(commandInput));
|
|
55
|
-
const result = reverseGeocodeResponseToFeatureCollection(response, {
|
|
58
|
+
const response = (await this.client.send(new ReverseGeocodeCommand(commandInput)));
|
|
59
|
+
const result = reverseGeocodeResponseToFeatureCollection(response, {
|
|
60
|
+
flattenProperties: true,
|
|
61
|
+
});
|
|
56
62
|
log('reverseGeocode returned %d results', result.features?.length ?? 0);
|
|
57
63
|
return result;
|
|
58
64
|
}
|
|
@@ -68,14 +74,22 @@ export class GeoPlaces {
|
|
|
68
74
|
MaxResults: config.limit || 5,
|
|
69
75
|
Language: this.normalizeLanguage(config.language),
|
|
70
76
|
AdditionalFeatures: [SuggestAdditionalFeature.CORE],
|
|
71
|
-
...(config.countries || config.bbox
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
...(config.countries || config.bbox
|
|
78
|
+
? {
|
|
79
|
+
Filter: {
|
|
80
|
+
...(config.countries
|
|
81
|
+
? {
|
|
82
|
+
IncludeCountries: Array.isArray(config.countries)
|
|
83
|
+
? config.countries
|
|
84
|
+
: config.countries.split(','),
|
|
85
|
+
}
|
|
86
|
+
: {}),
|
|
87
|
+
...(config.bbox ? { BoundingBox: config.bbox } : {}),
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
: {}),
|
|
77
91
|
};
|
|
78
|
-
const response = await this.client.send(new SuggestCommand(commandInput));
|
|
92
|
+
const response = (await this.client.send(new SuggestCommand(commandInput)));
|
|
79
93
|
const suggestions = { suggestions: [] };
|
|
80
94
|
for (const item of response.ResultItems ?? []) {
|
|
81
95
|
const text = item.Title;
|
|
@@ -99,9 +113,11 @@ export class GeoPlaces {
|
|
|
99
113
|
GetPlaceAdditionalFeature.TIME_ZONE,
|
|
100
114
|
],
|
|
101
115
|
});
|
|
102
|
-
const response = await this.client.send(command);
|
|
103
|
-
const result = getPlaceResponseToFeatureCollection(response, {
|
|
104
|
-
|
|
116
|
+
const response = (await this.client.send(command));
|
|
117
|
+
const result = getPlaceResponseToFeatureCollection(response, {
|
|
118
|
+
flattenProperties: true,
|
|
119
|
+
});
|
|
120
|
+
const carmenGeojsonFeatures = result.features.map((feature) => ({
|
|
105
121
|
...feature,
|
|
106
122
|
text: response.Title || '',
|
|
107
123
|
place_name: response.Address?.Label || '',
|
|
@@ -41,12 +41,14 @@ export class TokenProvider {
|
|
|
41
41
|
}
|
|
42
42
|
async getToken(forceRefresh = false) {
|
|
43
43
|
if (!forceRefresh && this.cachedToken && !this.isExpired()) {
|
|
44
|
-
const expiresIn = this.cachedExpiresAt
|
|
44
|
+
const expiresIn = this.cachedExpiresAt
|
|
45
|
+
? Math.floor((this.cachedExpiresAt - Date.now()) / 1000)
|
|
46
|
+
: 'unknown';
|
|
45
47
|
log('Using cached token (expires in %ds)', expiresIn);
|
|
46
48
|
return {
|
|
47
49
|
success: true,
|
|
48
50
|
token: this.cachedToken,
|
|
49
|
-
expiresAt: this.cachedExpiresAt
|
|
51
|
+
expiresAt: this.cachedExpiresAt,
|
|
50
52
|
};
|
|
51
53
|
}
|
|
52
54
|
// If token fetch is already in progress, wait for it
|
|
@@ -55,7 +57,11 @@ export class TokenProvider {
|
|
|
55
57
|
return this.tokenPromise;
|
|
56
58
|
}
|
|
57
59
|
// Start new token fetch
|
|
58
|
-
const reason = forceRefresh
|
|
60
|
+
const reason = forceRefresh
|
|
61
|
+
? 'forced refresh'
|
|
62
|
+
: this.cachedToken
|
|
63
|
+
? 'token expired'
|
|
64
|
+
: 'no cached token';
|
|
59
65
|
log('Refreshing token (%s) from %s', reason, this.config.apiUrl);
|
|
60
66
|
this.tokenPromise = this.fetchToken();
|
|
61
67
|
try {
|
|
@@ -74,10 +80,12 @@ export class TokenProvider {
|
|
|
74
80
|
const response = await fetch(`${apiUrl}/auth/token`, {
|
|
75
81
|
method: 'POST',
|
|
76
82
|
headers: {
|
|
77
|
-
|
|
83
|
+
Authorization: `Basic ${credentials}`,
|
|
78
84
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
79
85
|
},
|
|
80
|
-
body: new URLSearchParams({
|
|
86
|
+
body: new URLSearchParams({
|
|
87
|
+
grant_type: 'client_credentials',
|
|
88
|
+
}).toString(),
|
|
81
89
|
});
|
|
82
90
|
if (!response.ok) {
|
|
83
91
|
const errorText = await response.text();
|
|
@@ -89,13 +97,16 @@ export class TokenProvider {
|
|
|
89
97
|
else if (errorData.error)
|
|
90
98
|
errorMessage = errorData.error;
|
|
91
99
|
}
|
|
92
|
-
catch {
|
|
100
|
+
catch {
|
|
101
|
+
/* use statusText */
|
|
102
|
+
}
|
|
93
103
|
throw new Error(errorMessage);
|
|
94
104
|
}
|
|
95
105
|
const data = await response.json();
|
|
96
106
|
this.cachedToken = data.access_token;
|
|
97
107
|
// Prefer absolute expires_at (ms) from response, fall back to expires_in (seconds)
|
|
98
|
-
this.cachedExpiresAt =
|
|
108
|
+
this.cachedExpiresAt =
|
|
109
|
+
data.expires_at ?? Date.now() + (data.expires_in ?? 900) * 1000;
|
|
99
110
|
const expiresInSec = Math.floor((this.cachedExpiresAt - Date.now()) / 1000);
|
|
100
111
|
log('Token acquired successfully (expires in %ds)', expiresInSec);
|
|
101
112
|
return {
|
|
@@ -115,7 +126,7 @@ export class TokenProvider {
|
|
|
115
126
|
isExpired(bufferSeconds = 60) {
|
|
116
127
|
if (!this.cachedExpiresAt)
|
|
117
128
|
return true;
|
|
118
|
-
return Date.now() >=
|
|
129
|
+
return Date.now() >= this.cachedExpiresAt - bufferSeconds * 1000;
|
|
119
130
|
}
|
|
120
131
|
clearCache() {
|
|
121
132
|
this.cachedToken = undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ClientConfig } from '../types';
|
|
1
|
+
import type { ClientConfig, GeoPlacesCommand } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* GeoPlacesClient - AWS Location Service compatible client with custom auth
|
|
4
4
|
*
|
|
@@ -13,6 +13,6 @@ export declare class GeoPlacesClient {
|
|
|
13
13
|
serviceId: string;
|
|
14
14
|
};
|
|
15
15
|
constructor(config: ClientConfig);
|
|
16
|
-
send<
|
|
16
|
+
send<TOutput>(command: GeoPlacesCommand): Promise<TOutput>;
|
|
17
17
|
private getEndpoint;
|
|
18
18
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import debug from 'debug';
|
|
2
1
|
import { AutocompleteCommand, GeocodeCommand, GetPlaceCommand, ReverseGeocodeCommand, SearchNearbyCommand, SearchTextCommand, SuggestCommand, } from '@aws-sdk/client-geo-places';
|
|
2
|
+
import debug from 'debug';
|
|
3
|
+
import { LocationServiceException } from '../errors/LocationServiceException';
|
|
3
4
|
import { roundPositionFields } from '../utils/roundPosition';
|
|
4
5
|
const log = debug('location-client:api');
|
|
5
6
|
const logError = debug('location-client:api:error');
|
|
@@ -39,7 +40,7 @@ export class GeoPlacesClient {
|
|
|
39
40
|
method: 'POST',
|
|
40
41
|
headers: {
|
|
41
42
|
'Content-Type': 'application/json',
|
|
42
|
-
|
|
43
|
+
Authorization: `Bearer ${token}`,
|
|
43
44
|
},
|
|
44
45
|
body: JSON.stringify(input),
|
|
45
46
|
});
|
|
@@ -48,16 +49,27 @@ export class GeoPlacesClient {
|
|
|
48
49
|
const errorText = await response.text();
|
|
49
50
|
logError('Request failed: %s %s (%dms)', response.status, response.statusText, duration);
|
|
50
51
|
let errorMessage = `API request failed: ${response.statusText}`;
|
|
52
|
+
let errorCode = 'ServiceException';
|
|
53
|
+
let requestId;
|
|
51
54
|
try {
|
|
52
55
|
const errorData = JSON.parse(errorText);
|
|
53
56
|
if (errorData.message)
|
|
54
57
|
errorMessage = errorData.message;
|
|
58
|
+
if (errorData.code)
|
|
59
|
+
errorCode = errorData.code;
|
|
60
|
+
if (errorData.requestId)
|
|
61
|
+
requestId = errorData.requestId;
|
|
55
62
|
}
|
|
56
63
|
catch {
|
|
57
64
|
if (errorText)
|
|
58
65
|
errorMessage = errorText;
|
|
59
66
|
}
|
|
60
|
-
throw new
|
|
67
|
+
throw new LocationServiceException({
|
|
68
|
+
message: errorMessage,
|
|
69
|
+
code: errorCode,
|
|
70
|
+
statusCode: response.status,
|
|
71
|
+
requestId,
|
|
72
|
+
});
|
|
61
73
|
}
|
|
62
74
|
const result = await response.json();
|
|
63
75
|
log('Request successful: %s (%dms)', response.status, duration);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface LocationServiceExceptionOptions {
|
|
2
|
+
message: string;
|
|
3
|
+
code: string;
|
|
4
|
+
statusCode: number;
|
|
5
|
+
requestId?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class LocationServiceException extends Error {
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly statusCode: number;
|
|
10
|
+
readonly requestId?: string;
|
|
11
|
+
constructor(options: LocationServiceExceptionOptions);
|
|
12
|
+
get isRetryable(): boolean;
|
|
13
|
+
get isThrottling(): boolean;
|
|
14
|
+
get isValidation(): boolean;
|
|
15
|
+
toString(): string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class LocationServiceException extends Error {
|
|
2
|
+
constructor(options) {
|
|
3
|
+
super(options.message);
|
|
4
|
+
this.name = 'LocationServiceException';
|
|
5
|
+
this.code = options.code;
|
|
6
|
+
this.statusCode = options.statusCode;
|
|
7
|
+
this.requestId = options.requestId;
|
|
8
|
+
}
|
|
9
|
+
get isRetryable() {
|
|
10
|
+
return this.statusCode === 429 || this.statusCode >= 500;
|
|
11
|
+
}
|
|
12
|
+
get isThrottling() {
|
|
13
|
+
return this.code === 'ThrottlingException' || this.statusCode === 429;
|
|
14
|
+
}
|
|
15
|
+
get isValidation() {
|
|
16
|
+
return this.code === 'ValidationException' || this.statusCode === 400;
|
|
17
|
+
}
|
|
18
|
+
toString() {
|
|
19
|
+
const parts = [`LocationServiceException: [${this.code}] ${this.message}`];
|
|
20
|
+
if (this.requestId)
|
|
21
|
+
parts.push(`(requestId: ${this.requestId})`);
|
|
22
|
+
return parts.join(' ');
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export { GeoPlacesClient } from './client/GeoPlacesClient';
|
|
2
|
+
export { LocationServiceException } from './errors/LocationServiceException';
|
|
3
|
+
export type { LocationServiceExceptionOptions } from './errors/LocationServiceException';
|
|
2
4
|
export * from '@aws-sdk/client-geo-places';
|
|
3
5
|
export * from '@aws/amazon-location-utilities-datatypes';
|
|
4
6
|
export { GeoPlaces } from './adapters/GeoPlaces';
|
|
5
7
|
export { createTransformRequest } from './maps/createTransformRequest';
|
|
6
|
-
export { transformRequest } from './maps/Utils';
|
|
7
|
-
export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle';
|
|
8
|
-
export type { MapStyleOptions } from './maps/mapStyle';
|
|
9
8
|
export { applyMapLanguage } from './maps/mapLanguage';
|
|
10
|
-
export {
|
|
9
|
+
export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/mapPoi';
|
|
11
10
|
export type { PoiCategory } from './maps/mapPoi';
|
|
12
|
-
export
|
|
11
|
+
export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle';
|
|
12
|
+
export type { MapStyleOptions } from './maps/mapStyle';
|
|
13
|
+
export { transformRequest } from './maps/Utils';
|
|
14
|
+
export type { ClientConfig, GeoPlacesCommand, MapLike } from './types';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Client (Custom - uses our auth instead of AWS SigV4)
|
|
2
2
|
export { GeoPlacesClient } from './client/GeoPlacesClient';
|
|
3
|
+
// Errors
|
|
4
|
+
export { LocationServiceException } from './errors/LocationServiceException';
|
|
3
5
|
// Re-export AWS SDK commands and types
|
|
4
6
|
export * from '@aws-sdk/client-geo-places';
|
|
5
7
|
// Re-export AWS Location Utilities (data type conversions)
|
|
@@ -8,8 +10,8 @@ export * from '@aws/amazon-location-utilities-datatypes';
|
|
|
8
10
|
export { GeoPlaces } from './adapters/GeoPlaces';
|
|
9
11
|
// Maps utilities
|
|
10
12
|
export { createTransformRequest } from './maps/createTransformRequest';
|
|
11
|
-
export { transformRequest } from './maps/Utils';
|
|
12
|
-
export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle';
|
|
13
13
|
export { applyMapLanguage } from './maps/mapLanguage';
|
|
14
|
-
export {
|
|
14
|
+
export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/mapPoi';
|
|
15
|
+
export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle';
|
|
16
|
+
export { transformRequest } from './maps/Utils';
|
|
15
17
|
// Server-only utilities are available via '@chaosity/location-client/server'
|
package/dist/maps/Utils.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { ClientConfig } from
|
|
2
|
-
export declare function transformRequest(url: string, config: ClientConfig): import("maplibre-gl").RequestParameters | undefined;
|
|
1
|
+
import type { ClientConfig } from '../types';
|
|
2
|
+
export declare function transformRequest(url: string, config: ClientConfig): import("maplibre-gl").RequestParameters | Promise<import("maplibre-gl").RequestParameters> | undefined;
|
package/dist/maps/Utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTransformRequest } from
|
|
1
|
+
import { createTransformRequest } from './createTransformRequest';
|
|
2
2
|
export function transformRequest(url, config) {
|
|
3
3
|
const token = config.getToken?.() ?? config.token;
|
|
4
4
|
return createTransformRequest(config.apiUrl, () => token)(url);
|
|
@@ -15,7 +15,7 @@ export function createTransformRequest(apiUrl, getToken) {
|
|
|
15
15
|
return { url };
|
|
16
16
|
}
|
|
17
17
|
const headers = {
|
|
18
|
-
|
|
18
|
+
Authorization: `Bearer ${token}`,
|
|
19
19
|
};
|
|
20
20
|
// Set appropriate Accept headers based on resource type
|
|
21
21
|
if (url.includes('/tiles/')) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MapLike } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Apply a preferred display language to all symbol layers on a MapLibre map.
|
|
4
4
|
*
|
|
@@ -10,10 +10,10 @@ import type { Map } from 'maplibre-gl';
|
|
|
10
10
|
* Based on the approach documented at:
|
|
11
11
|
* https://docs.aws.amazon.com/location/latest/developerguide/how-to-set-preferred-language-map.html
|
|
12
12
|
*
|
|
13
|
-
* @param map - MapLibre Map instance
|
|
13
|
+
* @param map - MapLibre Map instance (or any object matching the MapLike interface)
|
|
14
14
|
* @param language - ISO 639-1 language code (e.g. 'en', 'fr', 'de', 'ja', 'zh', 'ar')
|
|
15
15
|
*
|
|
16
16
|
* @example
|
|
17
17
|
* map.once('style.load', () => applyMapLanguage(map, 'fr'))
|
|
18
18
|
*/
|
|
19
|
-
export declare function applyMapLanguage(map:
|
|
19
|
+
export declare function applyMapLanguage(map: MapLike, language: string): void;
|
package/dist/maps/mapLanguage.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Based on the approach documented at:
|
|
10
10
|
* https://docs.aws.amazon.com/location/latest/developerguide/how-to-set-preferred-language-map.html
|
|
11
11
|
*
|
|
12
|
-
* @param map - MapLibre Map instance
|
|
12
|
+
* @param map - MapLibre Map instance (or any object matching the MapLike interface)
|
|
13
13
|
* @param language - ISO 639-1 language code (e.g. 'en', 'fr', 'de', 'ja', 'zh', 'ar')
|
|
14
14
|
*
|
|
15
15
|
* @example
|
|
@@ -19,8 +19,13 @@ export function applyMapLanguage(map, language) {
|
|
|
19
19
|
try {
|
|
20
20
|
const expression = language === 'en'
|
|
21
21
|
? ['coalesce', ['get', 'name:en'], ['get', 'name']]
|
|
22
|
-
: [
|
|
23
|
-
|
|
22
|
+
: [
|
|
23
|
+
'coalesce',
|
|
24
|
+
['get', `name:${language}`],
|
|
25
|
+
['get', 'name:en'],
|
|
26
|
+
['get', 'name'],
|
|
27
|
+
];
|
|
28
|
+
map.getStyle().layers.forEach((layer) => {
|
|
24
29
|
if (layer.type === 'symbol') {
|
|
25
30
|
const textField = map.getLayoutProperty(layer.id, 'text-field');
|
|
26
31
|
if (textField !== undefined && textField !== null) {
|
package/dist/maps/mapStyle.js
CHANGED
|
@@ -56,14 +56,14 @@ export async function fetchMapStyle(apiUrl, mapStyle, getToken, options = {}) {
|
|
|
56
56
|
const token = getToken();
|
|
57
57
|
const response = await fetch(url, {
|
|
58
58
|
headers: {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
59
|
+
Authorization: `Bearer ${token}`,
|
|
60
|
+
Accept: 'application/json',
|
|
61
|
+
},
|
|
62
62
|
});
|
|
63
63
|
if (!response.ok) {
|
|
64
64
|
throw new Error(`Failed to fetch map style: ${response.status} ${response.statusText}`);
|
|
65
65
|
}
|
|
66
|
-
const style = await response.json();
|
|
66
|
+
const style = (await response.json());
|
|
67
67
|
if (language) {
|
|
68
68
|
applyLanguageToDescriptor(style, language);
|
|
69
69
|
}
|
|
@@ -76,7 +76,12 @@ export async function fetchMapStyle(apiUrl, mapStyle, getToken, options = {}) {
|
|
|
76
76
|
function applyLanguageToDescriptor(style, language) {
|
|
77
77
|
const expression = language === 'en'
|
|
78
78
|
? ['coalesce', ['get', 'name:en'], ['get', 'name']]
|
|
79
|
-
: [
|
|
79
|
+
: [
|
|
80
|
+
'coalesce',
|
|
81
|
+
['get', `name:${language}`],
|
|
82
|
+
['get', 'name:en'],
|
|
83
|
+
['get', 'name'],
|
|
84
|
+
];
|
|
80
85
|
for (const layer of style.layers) {
|
|
81
86
|
if (layer.type === 'symbol') {
|
|
82
87
|
const layout = layer.layout;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GeoPlacesCommand } from '../types';
|
|
1
2
|
export interface ConnectorConfig {
|
|
2
3
|
apiUrl?: string;
|
|
3
4
|
token?: string;
|
|
@@ -41,6 +42,6 @@ export declare class LocationServiceConnector {
|
|
|
41
42
|
private configPromise;
|
|
42
43
|
readonly serviceId: string;
|
|
43
44
|
constructor(config?: ConnectorConfig);
|
|
44
|
-
send<
|
|
45
|
+
send<TOutput>(command: GeoPlacesCommand, options?: SendOptions): Promise<TOutput>;
|
|
45
46
|
private getEndpoint;
|
|
46
47
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AutocompleteCommand, GeocodeCommand, GetPlaceCommand, ReverseGeocodeCommand, SearchNearbyCommand, SearchTextCommand, SuggestCommand, } from '@aws-sdk/client-geo-places';
|
|
2
2
|
import debug from 'debug';
|
|
3
|
-
import {
|
|
3
|
+
import { LocationServiceException } from '../errors/LocationServiceException';
|
|
4
4
|
import { roundPositionFields } from '../utils/roundPosition';
|
|
5
|
+
import { getClientConfig } from './getClientConfig';
|
|
5
6
|
const log = debug('location-client:connector');
|
|
6
7
|
/**
|
|
7
8
|
* LocationServiceConnector - Server-side connector for Location Service API
|
|
@@ -35,9 +36,7 @@ const log = debug('location-client:connector');
|
|
|
35
36
|
export class LocationServiceConnector {
|
|
36
37
|
constructor(config) {
|
|
37
38
|
this.serviceId = 'Geo Places';
|
|
38
|
-
this.configPromise = config
|
|
39
|
-
? Promise.resolve(config)
|
|
40
|
-
: getClientConfig();
|
|
39
|
+
this.configPromise = config ? Promise.resolve(config) : getClientConfig();
|
|
41
40
|
}
|
|
42
41
|
async send(command, options) {
|
|
43
42
|
const config = await this.configPromise;
|
|
@@ -45,7 +44,11 @@ export class LocationServiceConnector {
|
|
|
45
44
|
let token;
|
|
46
45
|
if ('getToken' in config && typeof config.getToken === 'function') {
|
|
47
46
|
const result = await config.getToken();
|
|
48
|
-
token = result
|
|
47
|
+
token = result
|
|
48
|
+
? typeof result === 'string'
|
|
49
|
+
? result
|
|
50
|
+
: result.token
|
|
51
|
+
: undefined;
|
|
49
52
|
}
|
|
50
53
|
else {
|
|
51
54
|
token = config.token;
|
|
@@ -63,31 +66,39 @@ export class LocationServiceConnector {
|
|
|
63
66
|
const headers = {
|
|
64
67
|
...(options?.headers || {}),
|
|
65
68
|
'Content-Type': 'application/json',
|
|
66
|
-
|
|
69
|
+
Authorization: `Bearer ${token}`,
|
|
67
70
|
};
|
|
68
71
|
const response = await fetch(url, {
|
|
69
72
|
method: 'POST',
|
|
70
73
|
headers,
|
|
71
|
-
body: JSON.stringify(input)
|
|
74
|
+
body: JSON.stringify(input),
|
|
72
75
|
});
|
|
73
76
|
const duration = Date.now() - startTime;
|
|
74
77
|
if (!response.ok) {
|
|
75
78
|
const errorText = await response.text();
|
|
76
79
|
log('Request failed: %s %s (%dms)', response.status, response.statusText, duration);
|
|
77
|
-
// Try to parse error message from response
|
|
78
80
|
let errorMessage = `API request failed: ${response.statusText}`;
|
|
81
|
+
let errorCode = 'ServiceException';
|
|
82
|
+
let requestId;
|
|
79
83
|
try {
|
|
80
84
|
const errorData = JSON.parse(errorText);
|
|
81
|
-
if (errorData.message)
|
|
85
|
+
if (errorData.message)
|
|
82
86
|
errorMessage = errorData.message;
|
|
83
|
-
|
|
87
|
+
if (errorData.code)
|
|
88
|
+
errorCode = errorData.code;
|
|
89
|
+
if (errorData.requestId)
|
|
90
|
+
requestId = errorData.requestId;
|
|
84
91
|
}
|
|
85
92
|
catch {
|
|
86
|
-
// If not JSON, use raw text if available
|
|
87
93
|
if (errorText)
|
|
88
94
|
errorMessage = errorText;
|
|
89
95
|
}
|
|
90
|
-
throw new
|
|
96
|
+
throw new LocationServiceException({
|
|
97
|
+
message: errorMessage,
|
|
98
|
+
code: errorCode,
|
|
99
|
+
statusCode: response.status,
|
|
100
|
+
requestId,
|
|
101
|
+
});
|
|
91
102
|
}
|
|
92
103
|
const result = await response.json();
|
|
93
104
|
log('Request successful: %s (%dms)', response.status, duration);
|
|
@@ -48,7 +48,10 @@ function getTokenProvider(apiUrl, clientId, clientSecret) {
|
|
|
48
48
|
* const connector = new LocationServiceConnector({ apiUrl: config.apiUrl, token })
|
|
49
49
|
*/
|
|
50
50
|
export async function getClientConfig(config = {}) {
|
|
51
|
-
log('[getClientConfig] Starting with config:', {
|
|
51
|
+
log('[getClientConfig] Starting with config:', {
|
|
52
|
+
hasApiUrl: !!config.apiUrl,
|
|
53
|
+
hasClientId: !!config.clientId,
|
|
54
|
+
});
|
|
52
55
|
// Auto-detect from environment with fallbacks
|
|
53
56
|
const apiUrl = config.apiUrl ||
|
|
54
57
|
process.env.LOCATION_API_URL ||
|
|
@@ -59,7 +62,10 @@ export async function getClientConfig(config = {}) {
|
|
|
59
62
|
const clientSecret = config.clientSecret ||
|
|
60
63
|
process.env.LOCATION_CLIENT_SECRET ||
|
|
61
64
|
process.env.LOCATION_SERVICE_CLIENT_SECRET;
|
|
62
|
-
log('[getClientConfig] Resolved config:', {
|
|
65
|
+
log('[getClientConfig] Resolved config:', {
|
|
66
|
+
apiUrl,
|
|
67
|
+
clientId: clientId?.substring(0, 10) + '...',
|
|
68
|
+
});
|
|
63
69
|
// Validate required values
|
|
64
70
|
if (!apiUrl || !clientId || !clientSecret) {
|
|
65
71
|
console.error('[getClientConfig] Missing required configuration');
|
|
@@ -77,12 +83,12 @@ export async function getClientConfig(config = {}) {
|
|
|
77
83
|
throw new Error(isAuthError
|
|
78
84
|
? `Authentication failed for client ID "${clientId}". ` +
|
|
79
85
|
`Verify LOCATION_CLIENT_ID and LOCATION_CLIENT_SECRET match your application in the developer portal.`
|
|
80
|
-
:
|
|
86
|
+
: result.error || 'Failed to get token');
|
|
81
87
|
}
|
|
82
88
|
log('[getClientConfig] Token fetched successfully, length:', result.token.length);
|
|
83
89
|
return {
|
|
84
90
|
apiUrl,
|
|
85
91
|
token: result.token,
|
|
86
|
-
expiresAt: result.expiresAt
|
|
92
|
+
expiresAt: result.expiresAt,
|
|
87
93
|
};
|
|
88
94
|
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
export { TokenProvider } from '../auth/TokenProvider';
|
|
2
|
+
export type { TokenProviderConfig, TokenResponse } from '../auth/TokenProvider';
|
|
1
3
|
export { getClientConfig } from './getClientConfig';
|
|
2
4
|
export type { ServerAuthConfig, ServerClientConfig } from './getClientConfig';
|
|
3
5
|
export { LocationServiceConnector } from './LocationServiceConnector';
|
|
4
6
|
export type { ConnectorConfig, SendOptions } from './LocationServiceConnector';
|
|
5
|
-
export { TokenProvider } from '../auth/TokenProvider';
|
|
6
|
-
export type { TokenProviderConfig, TokenResponse } from '../auth/TokenProvider';
|
package/dist/server/index.js
CHANGED
package/dist/types/index.d.ts
CHANGED
|
@@ -5,3 +5,30 @@ export interface ClientConfig {
|
|
|
5
5
|
* called on every request so token updates are reflected without recreating the client. */
|
|
6
6
|
getToken?: () => string | undefined;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Minimal interface for AWS SDK command objects.
|
|
10
|
+
* All AWS SDK commands (AutocompleteCommand, SearchTextCommand, etc.) extend
|
|
11
|
+
* Smithy's Command base class which has an `input` property containing the
|
|
12
|
+
* request parameters. This interface captures what we actually need from
|
|
13
|
+
* commands without coupling to Smithy internals.
|
|
14
|
+
*/
|
|
15
|
+
export interface GeoPlacesCommand {
|
|
16
|
+
readonly input: object;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Minimal interface for a MapLibre Map instance.
|
|
20
|
+
* Using a structural type avoids hard coupling to a specific maplibre-gl version.
|
|
21
|
+
*/
|
|
22
|
+
export interface MapLike {
|
|
23
|
+
isStyleLoaded(): boolean | void;
|
|
24
|
+
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
25
|
+
off(event: string, listener: (...args: unknown[]) => void): void;
|
|
26
|
+
getStyle(): {
|
|
27
|
+
layers: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
type: string;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
getLayoutProperty(layerId: string, name: string): unknown;
|
|
33
|
+
setLayoutProperty(layerId: string, name: string, value: unknown): void;
|
|
34
|
+
}
|
|
@@ -12,4 +12,4 @@
|
|
|
12
12
|
* Shallow-clone the input and round position arrays that benefit from caching.
|
|
13
13
|
* Returns the original object if no position fields are present.
|
|
14
14
|
*/
|
|
15
|
-
export declare function roundPositionFields<T extends
|
|
15
|
+
export declare function roundPositionFields<T extends object>(input: T): T;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaosity/location-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Client library for Chaosity Location Service with AWS Location Service compatibility",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -18,9 +18,14 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc",
|
|
20
20
|
"dev": "tsc --watch",
|
|
21
|
-
"
|
|
21
|
+
"lint": "eslint . && prettier --check .",
|
|
22
|
+
"lint:fix": "eslint --fix . && prettier --write .",
|
|
23
|
+
"format": "prettier --write .",
|
|
24
|
+
"test": "vitest run --passWithNoTests",
|
|
22
25
|
"test:watch": "vitest",
|
|
23
|
-
"prepublishOnly": "npm run build"
|
|
26
|
+
"prepublishOnly": "npm run build",
|
|
27
|
+
"prepare": "husky",
|
|
28
|
+
"postversion": "git push --follow-tags"
|
|
24
29
|
},
|
|
25
30
|
"keywords": [
|
|
26
31
|
"aws",
|
|
@@ -55,10 +60,17 @@
|
|
|
55
60
|
}
|
|
56
61
|
},
|
|
57
62
|
"devDependencies": {
|
|
63
|
+
"@eslint/js": "^10.0.1",
|
|
58
64
|
"@types/debug": "^4.1.12",
|
|
59
65
|
"@types/node": "^25.0.3",
|
|
66
|
+
"@vitest/coverage-v8": "^3.0.0",
|
|
67
|
+
"eslint": "^10.1.0",
|
|
68
|
+
"husky": "^9.0.0",
|
|
60
69
|
"maplibre-gl": "^5.0.0",
|
|
70
|
+
"prettier": "^3.8.1",
|
|
71
|
+
"prettier-plugin-organize-imports": "^4.3.0",
|
|
61
72
|
"typescript": "^5.0.0",
|
|
73
|
+
"typescript-eslint": "^8.57.1",
|
|
62
74
|
"vitest": "^3.0.0"
|
|
63
75
|
},
|
|
64
76
|
"publishConfig": {
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { LocationServiceConnector } from './LocationServiceConnector';
|
|
2
|
-
export interface ServerAuthConfig {
|
|
3
|
-
apiUrl?: string;
|
|
4
|
-
clientId?: string;
|
|
5
|
-
clientSecret?: string;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Get LocationServiceConnector with automatic token management.
|
|
9
|
-
*
|
|
10
|
-
* Automatically reads from environment variables:
|
|
11
|
-
* - LOCATION_API_URL or LOCATION_SERVICE_API_URL
|
|
12
|
-
* - LOCATION_CLIENT_ID or LOCATION_SERVICE_CLIENT_ID
|
|
13
|
-
* - LOCATION_CLIENT_SECRET or LOCATION_SERVICE_CLIENT_SECRET
|
|
14
|
-
*
|
|
15
|
-
* You can override any value by passing it explicitly.
|
|
16
|
-
*
|
|
17
|
-
* WARNING: This function uses client credentials (clientId/clientSecret).
|
|
18
|
-
* Only call this from:
|
|
19
|
-
* - Next.js Server Components/Actions
|
|
20
|
-
* - Node.js backend servers
|
|
21
|
-
* - API routes
|
|
22
|
-
*
|
|
23
|
-
* NEVER call from browser/client code as it exposes credentials.
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* // Server Action for React provider
|
|
27
|
-
* 'use server'
|
|
28
|
-
* export async function getLocationConnector() {
|
|
29
|
-
* return getConnector()
|
|
30
|
-
* }
|
|
31
|
-
*
|
|
32
|
-
* // Or with custom config
|
|
33
|
-
* export async function getLocationConnector() {
|
|
34
|
-
* return getConnector({ apiUrl: 'https://custom.api.com' })
|
|
35
|
-
* }
|
|
36
|
-
*/
|
|
37
|
-
export declare function getConnector(config?: ServerAuthConfig): LocationServiceConnector;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { LocationServiceConnector } from './LocationServiceConnector';
|
|
2
|
-
/**
|
|
3
|
-
* Get LocationServiceConnector with automatic token management.
|
|
4
|
-
*
|
|
5
|
-
* Automatically reads from environment variables:
|
|
6
|
-
* - LOCATION_API_URL or LOCATION_SERVICE_API_URL
|
|
7
|
-
* - LOCATION_CLIENT_ID or LOCATION_SERVICE_CLIENT_ID
|
|
8
|
-
* - LOCATION_CLIENT_SECRET or LOCATION_SERVICE_CLIENT_SECRET
|
|
9
|
-
*
|
|
10
|
-
* You can override any value by passing it explicitly.
|
|
11
|
-
*
|
|
12
|
-
* WARNING: This function uses client credentials (clientId/clientSecret).
|
|
13
|
-
* Only call this from:
|
|
14
|
-
* - Next.js Server Components/Actions
|
|
15
|
-
* - Node.js backend servers
|
|
16
|
-
* - API routes
|
|
17
|
-
*
|
|
18
|
-
* NEVER call from browser/client code as it exposes credentials.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* // Server Action for React provider
|
|
22
|
-
* 'use server'
|
|
23
|
-
* export async function getLocationConnector() {
|
|
24
|
-
* return getConnector()
|
|
25
|
-
* }
|
|
26
|
-
*
|
|
27
|
-
* // Or with custom config
|
|
28
|
-
* export async function getLocationConnector() {
|
|
29
|
-
* return getConnector({ apiUrl: 'https://custom.api.com' })
|
|
30
|
-
* }
|
|
31
|
-
*/
|
|
32
|
-
export function getConnector(config) {
|
|
33
|
-
return new LocationServiceConnector(config);
|
|
34
|
-
}
|