@mapcreator/api 0.0.0-mc2896.0 → 0.0.0-mc2896.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mapcreator/api",
3
- "version": "0.0.0-mc2896.0",
3
+ "version": "0.0.0-mc2896.1",
4
4
  "description": "Mapcreator JavaScript API",
5
5
  "license": "BSD-3-Clause",
6
6
  "main": "./cjs/index.js",
@@ -10,6 +10,8 @@ export const boundingBoxRevivers: Revivers<
10
10
  boundingBox: (data: { bounding_box: string }) => JSON.parse(data.bounding_box) as Polygon,
11
11
  };
12
12
 
13
+ export type SAGALanguage = 'en' | 'da' | 'nl' | 'fi' | 'fr' | 'de' | 'it' | 'no' | 'pl' | 'pt' | 'es' | 'sv' | 'ja' | 'zh' | 'ru' | 'uk' | 'lv' | 'kk' | '';
14
+
13
15
  export type ApiSearchPoint = {
14
16
  lat: number;
15
17
  lng: number;
@@ -30,12 +32,10 @@ type SingleOrGroupedAreaBase = {
30
32
  boundingBox: Polygon;
31
33
  };
32
34
 
33
- // TODO don't export this once search on click is out
34
35
  export type GroupedArea = SingleOrGroupedAreaBase & {
35
36
  isGroup: true;
36
37
  };
37
38
 
38
- // TODO don't export this once search on click is out
39
39
  export type SingleArea = SingleOrGroupedAreaBase & {
40
40
  isGroup: false;
41
41
  properties: Record<string, string>;
@@ -44,70 +44,100 @@ export type SingleArea = SingleOrGroupedAreaBase & {
44
44
  featureId: number;
45
45
  };
46
46
 
47
- export type SingleOrGroupedArea = {
48
- id: number;
49
- title: string;
50
- subtitle: string;
51
- svgPreview: string;
52
- boundingBox: Polygon;
53
- isGroup: boolean;
54
- properties: Record<string, string> | null;
55
- vectorSource: string | null;
56
- sourceLayer: string | null;
57
- featureId: number | null;
58
- };
47
+ type ApiSingleArea = {
48
+ data: {
49
+ id: number;
50
+ title: string;
51
+ subtitle: string;
52
+ svg_preview: string;
53
+ bounding_box: string;
54
+ is_group: false;
55
+ properties: string;
56
+ vector_source: string;
57
+ source_layer: string;
58
+ feature_id: number;
59
+ };
60
+ } & Omit<ApiSuccess, 'data'> | ApiError;
61
+
62
+ export type ApiSingleAreaData = Flatten<Exclude<ApiSingleArea, ApiError>['data']>;
63
+
64
+ export type ApiSingleAreaArray = {
65
+ data: ApiSingleAreaData[];
66
+ } & Omit<ApiSuccess, 'data'> | ApiError;
59
67
 
60
- export type ApiSingleOrGroupedArea = {
68
+ type ApiGroupedArea = {
61
69
  data: {
62
70
  id: number;
63
71
  title: string;
64
72
  subtitle: string;
65
73
  svg_preview: string;
66
74
  bounding_box: string;
67
- is_group: boolean;
68
- properties: string | null;
69
- vector_source: string | null;
70
- source_layer: string | null;
71
- feature_id: number | null;
75
+ is_group: true;
76
+ properties: null;
77
+ vector_source: null;
78
+ source_layer: null;
79
+ feature_id: null;
72
80
  };
73
81
  } & Omit<ApiSuccess, 'data'> | ApiError;
74
82
 
75
- export type ApiSingleOrGroupedAreaData = Flatten<Exclude<ApiSingleOrGroupedArea, ApiError>['data']>;
83
+ export type ApiGroupedAreaData = Flatten<Exclude<ApiGroupedArea, ApiError>['data']>;
84
+
85
+ export type ApiGroupedAreaArray = {
86
+ data: ApiGroupedAreaData[];
87
+ } & Omit<ApiSuccess, 'data'> | ApiError;
88
+
89
+ export const singleAreaRevivers: Revivers<ApiSingleArea, SingleArea> = {
90
+ ...boundingBoxRevivers,
91
+ properties: (data: ApiSingleAreaData) =>
92
+ JSON.parse(data.properties) as Record<string, string>,
93
+ };
76
94
 
77
- export const singleOrGroupedAreaRevivers: Revivers<ApiSingleOrGroupedArea, SingleOrGroupedArea> = {
95
+ export const groupedAreaRevivers: Revivers<ApiGroupedArea, GroupedArea> = {
78
96
  ...boundingBoxRevivers,
79
- properties: (data: ApiSingleOrGroupedAreaData) =>
80
- (data.properties != null ? JSON.parse(data.properties) as Record<string, string> : null),
81
97
  };
82
98
 
83
- /**
84
- * TODO When SAGA search on click is implemented, remove mode and make searchBounds required
85
- */
86
- export async function searchSingleOrGroupedAreas(
87
- language: string,
99
+ export async function searchSingleAreas(
100
+ language: SAGALanguage,
88
101
  search: RequireAtLeastOne<{
89
- searchBounds?: ApiSearchBounds;
102
+ searchBounds: ApiSearchBounds;
90
103
  query?: string;
91
104
  searchPoint?: ApiSearchPoint;
92
105
  }>,
93
- mode: 'polygon' | 'group' | 'both' = 'both',
94
- ): Promise<SingleOrGroupedArea[]> {
106
+ ): Promise<SingleArea[]> {
95
107
  const pathname = '/v1/choropleth/polygons/search';
96
108
  const query = getSearchParams({
97
109
  language,
98
110
  ...search.searchBounds,
99
111
  ...(search.query && { query: search.query }),
100
112
  ...(search.searchPoint && { point: search.searchPoint }),
101
- mode,
113
+ mode: 'polygon',
102
114
  });
103
115
  const path = `${pathname}?${query}`;
104
- const options = { revivers: singleOrGroupedAreaRevivers };
116
+ const options = { revivers: singleAreaRevivers };
105
117
 
106
- type ApiSingleOrGroupedAreaArray = {
107
- data: ApiSingleOrGroupedAreaData[];
108
- } & Omit<ApiSuccess, 'data'> | ApiError;
118
+ return request<ApiSingleAreaArray, SingleArea>(path, null, null, options);
119
+ }
120
+
121
+ export async function searchGroupedAreas(
122
+ language: SAGALanguage,
123
+ search: RequireAtLeastOne<{
124
+ searchBounds: ApiSearchBounds;
125
+ query?: string;
126
+ searchPoint?: ApiSearchPoint;
127
+ }>,
128
+ ): Promise<GroupedArea[]> {
129
+ const pathname = '/v1/choropleth/polygons/search';
130
+ const query = getSearchParams({
131
+ language,
132
+ ...search.searchBounds,
133
+ ...(search.query && { query: search.query }),
134
+ ...(search.searchPoint && { point: search.searchPoint }),
135
+ mode: 'group',
136
+ });
137
+ const path = `${pathname}?${query}`;
138
+ const options = { revivers: groupedAreaRevivers };
109
139
 
110
- return request<ApiSingleOrGroupedAreaArray, SingleOrGroupedArea>(path, null, null, options);
140
+ return request<ApiGroupedAreaArray, GroupedArea>(path, null, null, options);
111
141
  }
112
142
 
113
143
  export type GroupedAreaChild = {
@@ -139,7 +169,7 @@ export const groupedAreaChildRevivers: Revivers<ApiGroupedAreaChild, GroupedArea
139
169
  properties: (data: ApiGroupedAreaChildData) => JSON.parse(data.properties) as Record<string, string>,
140
170
  };
141
171
 
142
- export async function groupedAreaChildren(groupId: number, language: string): Promise<GroupedAreaChild[]> {
172
+ export async function groupedAreaChildren(groupId: number, language: SAGALanguage): Promise<GroupedAreaChild[]> {
143
173
  const pathname = `/v1/choropleth/groups/${groupId}/children-optimized`;
144
174
  const query = getSearchParams({ language });
145
175
  const path = `${pathname}?${query}`;
@@ -178,7 +208,7 @@ export type ApiMatchedGroupData = Flatten<Exclude<ApiMatchedGroup, ApiError>['da
178
208
 
179
209
  export async function getGroupsByDataSample(
180
210
  sample: Record<string, string[]>,
181
- language: string,
211
+ language: SAGALanguage,
182
212
  rowCount: number,
183
213
  ): Promise<MatchedGroup[]> {
184
214
  const path = `/v1/choropleth/groups/sample`;
@@ -215,7 +245,7 @@ export async function getBoundPolygons(
215
245
  groupId: number,
216
246
  property: string,
217
247
  data: string[],
218
- language: string,
248
+ language: SAGALanguage,
219
249
  ): Promise<BoundPolygon[]> {
220
250
  const path = `/v1/choropleth/groups/bind`;
221
251
  const body = { group_id: groupId, property, data, language };
@@ -7,6 +7,9 @@ import {
7
7
  type Flatten,
8
8
  HTTPError,
9
9
  NetworkError,
10
+ TimeoutError,
11
+ defaultTimeout,
12
+ makeTimeoutSignal,
10
13
  request,
11
14
  } from '../utils.js';
12
15
 
@@ -63,9 +66,22 @@ export async function getInsetMap(insetMapId: number): Promise<InsetMap> {
63
66
 
64
67
  export async function getInsetMapTopoJson<TopoJSON>(insetMapId: number): Promise<TopoJSON> {
65
68
  const href = `${apiHost}/v1/inset-maps/${insetMapId}/json`;
66
- const headers = getAuthorizationHeaders('GET');
67
- const response = await fetch(href, { headers, ...!token && { credentials: 'include' } })
68
- .catch((error: Error) => { throw new NetworkError(error?.message ?? error) });
69
+ const headers = getAuthorizationHeaders();
70
+
71
+ const timeout = defaultTimeout;
72
+ const { signal, cleanup } = makeTimeoutSignal(timeout);
73
+
74
+ const response = await fetch(href, { headers, signal, ...!token && { credentials: 'include' } })
75
+ .catch((error: Error) => {
76
+ if (error instanceof TimeoutError) throw error;
77
+
78
+ if (error?.name === 'TimeoutError' || error?.name === 'AbortError') {
79
+ throw new TimeoutError(timeout, error);
80
+ }
81
+
82
+ throw new NetworkError(error);
83
+ })
84
+ .finally(cleanup);
69
85
 
70
86
  if (response.ok) {
71
87
  return response.json().catch(() => {
@@ -83,6 +99,7 @@ export async function getInsetMapTopoJson<TopoJSON>(insetMapId: number): Promise
83
99
  case 403:
84
100
  case 404:
85
101
  case 406:
102
+ case 422:
86
103
  case 429:
87
104
  throw new APIError({
88
105
  success: false,
@@ -12,9 +12,12 @@ import {
12
12
  HTTPError,
13
13
  NetworkError,
14
14
  type Revivers,
15
+ TimeoutError,
15
16
  defaultListHeader,
17
+ defaultTimeout,
16
18
  deletedNoneParam,
17
19
  lastJobRevision,
20
+ makeTimeoutSignal,
18
21
  request,
19
22
  } from '../utils.js';
20
23
 
@@ -227,9 +230,22 @@ export async function getJobRevisionOutput(jobId: number): Promise<JobRevisionOu
227
230
 
228
231
  async function fetchOutput(jobId: number): Promise<JobRevisionOutput> {
229
232
  const href = `${apiHost}/v1/jobs/${jobId}/revisions/${lastJobRevision}/result/output`;
230
- const headers = getAuthorizationHeaders('GET');
231
- const response = await fetch(href, { headers, ...!token && { credentials: 'include' } })
232
- .catch((error: Error) => { throw new NetworkError(error?.message ?? error) });
233
+ const headers = getAuthorizationHeaders();
234
+
235
+ const timeout = defaultTimeout;
236
+ const { signal, cleanup } = makeTimeoutSignal(timeout);
237
+
238
+ const response = await fetch(href, { headers, signal, ...!token && { credentials: 'include' } })
239
+ .catch((error: Error) => {
240
+ if (error instanceof TimeoutError) throw error;
241
+
242
+ if (error?.name === 'TimeoutError' || error?.name === 'AbortError') {
243
+ throw new TimeoutError(timeout, error);
244
+ }
245
+
246
+ throw new NetworkError(error);
247
+ })
248
+ .finally(cleanup);
233
249
 
234
250
  if (response.ok) {
235
251
  const blob = await response.blob().catch(() => {
@@ -263,6 +279,7 @@ async function fetchOutput(jobId: number): Promise<JobRevisionOutput> {
263
279
  case 403:
264
280
  case 404:
265
281
  case 406:
282
+ case 422:
266
283
  case 429:
267
284
  throw new APIError(
268
285
  (await response.json().catch(() => ({
@@ -6,19 +6,18 @@ import { type ApiDimensionData, type Dimension, dimensionRevivers } from './dime
6
6
  import { type ApiFeatureData, type Feature, featureRevivers } from './feature.js';
7
7
  import { type ApiJobTypeData, type JobType, jobTypeRevivers } from './jobType.js';
8
8
  import { type ApiMessageData, type Message, messageRevivers } from './message.js';
9
- import { type ApiSvg, type ApiSvgData, type Svg, svgRevivers } from './svg.js';
10
9
  import { type ApiSvgSetData, type SvgSet, svgSetRevivers } from './svgSet.js';
11
10
  import { type ApiLayerData, type Layer, layerRevivers } from './layer.js';
12
11
  import { type ApiColorData, type Color, colorRevivers } from './color.js';
13
12
  import { type ApiUserData, type User, userRevivers } from './user.js';
14
13
  import { type ApiFontData, type Font, fontRevivers } from './font.js';
14
+ import { type ApiSvgData, type Svg, svgRevivers } from './svg.js';
15
15
  import type { CamelCasedProperties } from 'type-fest';
16
16
  import {
17
17
  type ApiCommon,
18
18
  type ApiCommonData,
19
19
  type ApiError,
20
20
  type ApiSuccess,
21
- type Revivers,
22
21
  getContext,
23
22
  myUser,
24
23
  processData,
@@ -112,11 +111,6 @@ export async function loadResources(): Promise<Resources> {
112
111
 
113
112
  allMessages.sort((m1, m2) => m2.date.localeCompare(m1.date));
114
113
 
115
- const mcSvgRevivers: Revivers<ApiSvg, Svg> = {
116
- ...svgRevivers,
117
- name: (data: ApiSvgData) => `mc-image-${data.name}`,
118
- };
119
-
120
114
  return {
121
115
  // @ts-expect-error TS2345
122
116
  user: processData.call(getContext(userRevivers), user) as User,
@@ -127,7 +121,7 @@ export async function loadResources(): Promise<Resources> {
127
121
  feature => feature.name,
128
122
  ) as CustomFeature[],
129
123
  colors: raw.colors.map(processData, getContext(colorRevivers)) as Color[],
130
- svgs: raw.svgs.map(processData, getContext(mcSvgRevivers)) as Svg[],
124
+ svgs: raw.svgs.map(processData, getContext(svgRevivers)) as Svg[],
131
125
  dimensions: raw.dimensions.map(processData, getContext(dimensionRevivers)) as Dimension[],
132
126
  dimensionSets: raw.dimensionSets.map(processData, getContext(dimensionSetRevivers)) as DimensionSet[],
133
127
  mapstyleSets: raw.mapstyleSets.map(processData, getContext(mapstyleSetRevivers)) as MapstyleSet[],
package/src/index.ts CHANGED
@@ -39,6 +39,7 @@ export {
39
39
  APIError,
40
40
  HTTPError,
41
41
  NetworkError,
42
+ TimeoutError,
42
43
  getSearchParams,
43
44
  request,
44
45
  } from './utils.js';
package/src/oauth.ts CHANGED
@@ -69,18 +69,10 @@ export async function authenticate(): Promise<never> {
69
69
  }
70
70
 
71
71
  // eslint-disable-next-line @typescript-eslint/naming-convention
72
- type AuthHeaders = { Authorization: string } | { 'X-XSRF-Token': string } | undefined;
73
-
74
- export function getAuthorizationHeaders(method: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'): AuthHeaders {
75
- const cookie = !token && method !== 'GET' && method !== 'HEAD'
76
- ? document.cookie.split(/ *; */).find(pair => pair.startsWith('XSRF-TOKEN'))?.split('=')[1]
77
- : undefined;
78
-
79
- return token
80
- ? { Authorization: token.toString() }
81
- : cookie
82
- ? { 'X-XSRF-Token': decodeURIComponent(cookie) }
83
- : undefined;
72
+ type AuthHeaders = { Authorization: string } | undefined;
73
+
74
+ export function getAuthorizationHeaders(): AuthHeaders {
75
+ return token ? { Authorization: token.toString() } : undefined;
84
76
  }
85
77
 
86
78
  export async function logout(): Promise<never> {
package/src/utils.ts CHANGED
@@ -39,12 +39,20 @@ export const staticContext = { keysToRemove: new Set(keysToRemove), keysToAdd: [
39
39
  export const defaultTimeout = 30000;
40
40
 
41
41
  // class AuthorizationError extends Error {}
42
- export class NetworkError extends Error {}
42
+ export class NetworkError extends Error {
43
+ constructor(error: unknown) {
44
+ super((error as Error)?.message ?? 'Network Error', error instanceof Error ? { cause: error } : {});
45
+ this.name = 'NetworkError';
46
+ }
47
+ }
43
48
 
44
49
  export class TimeoutError extends Error {
45
50
  readonly timeout: number;
46
- constructor(timeout: number, message?: string) {
47
- super(message ?? `Request timed out after ${timeout}ms`);
51
+ constructor(timeout: number, error?: unknown) {
52
+ super(
53
+ (error as Error)?.message ?? `Request timed out after ${timeout}ms`,
54
+ error instanceof Error ? { cause: error } : {},
55
+ );
48
56
  this.name = 'TimeoutError';
49
57
  this.timeout = timeout;
50
58
  }
@@ -54,15 +62,19 @@ export class HTTPError extends Error {
54
62
  readonly statusCode: number;
55
63
  constructor(response: Response) {
56
64
  super(response.statusText);
65
+ this.name = 'HTTPError';
57
66
  this.statusCode = response.status;
58
67
  }
59
68
  }
60
69
 
61
70
  export class APIError extends Error {
62
- readonly code: string;
71
+ readonly code: string | undefined;
72
+ readonly validationErrors: Record<string, string[]> | undefined;
63
73
  constructor(apiError: ApiError) {
64
- super(apiError.error.message);
65
- this.code = apiError.error.type;
74
+ super(apiError?.error?.message ?? 'API Error');
75
+ this.name = 'APIError';
76
+ this.code = apiError?.error?.type;
77
+ this.validationErrors = apiError?.error?.validation_errors;
66
78
  }
67
79
  }
68
80
 
@@ -159,10 +171,10 @@ export async function request<
159
171
  if (error instanceof TimeoutError) throw error;
160
172
 
161
173
  if (error?.name === 'TimeoutError' || error?.name === 'AbortError') {
162
- throw new TimeoutError(timeout);
174
+ throw new TimeoutError(timeout, error);
163
175
  }
164
176
 
165
- throw new NetworkError(error?.message ?? error);
177
+ throw new NetworkError(error);
166
178
  })
167
179
  .finally(cleanup);
168
180
 
@@ -237,6 +249,7 @@ export async function request<
237
249
  case 403:
238
250
  case 404:
239
251
  case 406:
252
+ case 422:
240
253
  throw new APIError(
241
254
  (await response.json().catch(() => ({
242
255
  success: false,
@@ -288,7 +301,7 @@ function getRequestInit<I extends ApiCommon, O extends Record<string, unknown>>(
288
301
  }
289
302
 
290
303
  const method = extraOptions?.method ?? (body != null ? 'POST' : 'GET'); // don't touch `!=` please
291
- const authorization = getAuthorizationHeaders(method);
304
+ const authorization = getAuthorizationHeaders();
292
305
  const headers = { Accept: 'application/json', ...authorization, ...contentType, ...extraHeaders };
293
306
 
294
307
  return { body, headers, method, ...!token && { credentials: 'include' } } as RequestInit;
@@ -334,7 +347,7 @@ export function processData<I extends ApiCommon, O extends Record<string, unknow
334
347
  return result as O;
335
348
  } /* eslint-enable @typescript-eslint/prefer-for-of */
336
349
 
337
- function makeTimeoutSignal(timeout: number): { signal: AbortSignal; cleanup: () => void } {
350
+ export function makeTimeoutSignal(timeout: number): { signal: AbortSignal; cleanup: () => void } {
338
351
  if (typeof AbortSignal.timeout === 'function') {
339
352
  return {
340
353
  signal: AbortSignal.timeout(timeout),
@@ -351,7 +364,7 @@ function makeTimeoutSignal(timeout: number): { signal: AbortSignal; cleanup: ()
351
364
  }
352
365
  }
353
366
 
354
- function combineSignals(a: AbortSignal, b: AbortSignal): AbortSignal {
367
+ export function combineSignals(a: AbortSignal, b: AbortSignal): AbortSignal {
355
368
  if (typeof AbortSignal.any === 'function') {
356
369
  return AbortSignal.any([a, b]);
357
370
  } else {