@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 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 { fetchMapStyle, createTransformRequest } from '@chaosity/location-client'
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 { setPoiVisibility, setAllPoiVisibility, POI_CATEGORIES } from '@chaosity/location-client'
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({ style, transformRequest: createTransformRequest(apiUrl, getToken) })
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', { colorScheme: 'Dark', terrain: 'Hillshade' })
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 // ISO 3166-1 alpha-3 (e.g. 'IND', 'TUR')
265
+ politicalView?: string // ISO 3166-1 alpha-3 (e.g. 'IND', 'TUR')
253
266
  terrain?: 'Hillshade' | 'Terrain3D'
254
267
  buildings?: 'Buildings3D'
255
- contourDensity?: 'Medium' // Only 'Medium' is supported by the AWS SDK
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 { setPoiVisibility, setAllPoiVisibility, POI_CATEGORIES } from '@chaosity/location-client'
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(new SuggestCommand({ QueryText: 'Vancouver' }))
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) ? config.countries : config.countries.split(','),
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, { flattenProperties: true });
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, { flattenProperties: true });
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
- Filter: {
73
- ...(config.countries ? { IncludeCountries: Array.isArray(config.countries) ? config.countries : config.countries.split(',') } : {}),
74
- ...(config.bbox ? { BoundingBox: config.bbox } : {}),
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, { flattenProperties: true });
104
- const carmenGeojsonFeatures = result.features.map(feature => ({
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 ? Math.floor((this.cachedExpiresAt - Date.now()) / 1000) : 'unknown';
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 ? 'forced refresh' : (this.cachedToken ? 'token expired' : 'no cached token');
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
- 'Authorization': `Basic ${credentials}`,
83
+ Authorization: `Basic ${credentials}`,
78
84
  'Content-Type': 'application/x-www-form-urlencoded',
79
85
  },
80
- body: new URLSearchParams({ grant_type: 'client_credentials' }).toString(),
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 { /* use statusText */ }
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 = data.expires_at ?? (Date.now() + ((data.expires_in ?? 900) * 1000));
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() >= (this.cachedExpiresAt - bufferSeconds * 1000);
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<TInput, TOutput>(command: TInput): Promise<TOutput>;
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
- 'Authorization': `Bearer ${token}`,
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 Error(errorMessage);
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 { setPoiVisibility, setAllPoiVisibility, POI_CATEGORIES } from './maps/mapPoi';
9
+ export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/mapPoi';
11
10
  export type { PoiCategory } from './maps/mapPoi';
12
- export type { ClientConfig } from './types';
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 { setPoiVisibility, setAllPoiVisibility, POI_CATEGORIES } from './maps/mapPoi';
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'
@@ -1,2 +1,2 @@
1
- import { ClientConfig } from "../types";
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;
@@ -1,4 +1,4 @@
1
- import { createTransformRequest } from "./createTransformRequest";
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
- 'Authorization': `Bearer ${token}`,
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 { Map } from 'maplibre-gl';
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: Map, language: string): void;
19
+ export declare function applyMapLanguage(map: MapLike, language: string): void;
@@ -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
- : ['coalesce', ['get', `name:${language}`], ['get', 'name:en'], ['get', 'name']];
23
- map.getStyle().layers.forEach(layer => {
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) {
@@ -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
- 'Authorization': `Bearer ${token}`,
60
- 'Accept': 'application/json',
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
- : ['coalesce', ['get', `name:${language}`], ['get', 'name:en'], ['get', 'name']];
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<TInput, TOutput>(command: TInput, options?: SendOptions): Promise<TOutput>;
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 { getClientConfig } from './getClientConfig';
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 ? (typeof result === 'string' ? result : result.token) : undefined;
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
- 'Authorization': `Bearer ${token}`
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 Error(errorMessage);
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);
@@ -1,4 +1,4 @@
1
- import { ClientConfig } from '../types';
1
+ import type { ClientConfig } from '../types';
2
2
  export interface ServerAuthConfig {
3
3
  apiUrl?: string;
4
4
  clientId?: string;
@@ -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:', { hasApiUrl: !!config.apiUrl, hasClientId: !!config.clientId });
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:', { apiUrl, clientId: clientId?.substring(0, 10) + '...' });
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
- : (result.error || 'Failed to get token'));
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
  }
@@ -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';
@@ -1,3 +1,3 @@
1
+ export { TokenProvider } from '../auth/TokenProvider';
1
2
  export { getClientConfig } from './getClientConfig';
2
3
  export { LocationServiceConnector } from './LocationServiceConnector';
3
- export { TokenProvider } from '../auth/TokenProvider';
@@ -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 Record<string, any>>(input: T): T;
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.11",
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
- "test": "vitest run",
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
- }