@beepsdev/sdk 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,767 @@
1
+ // src/version.ts
2
+ var SDK_VERSION = "0.0.1";
3
+
4
+ // src/errors.ts
5
+ var BeepsError = class extends Error {
6
+ code;
7
+ status;
8
+ requestId;
9
+ retryable;
10
+ cause;
11
+ constructor(message, options) {
12
+ super(message);
13
+ this.name = "BeepsError";
14
+ this.code = options?.code;
15
+ this.status = options?.status;
16
+ this.requestId = options?.requestId;
17
+ this.retryable = options?.retryable ?? false;
18
+ this.cause = options?.cause;
19
+ }
20
+ toString() {
21
+ const parts = [
22
+ `BeepsError`,
23
+ this.code ? `Code=${this.code}` : null,
24
+ this.status ? `status=${this.status}` : null,
25
+ this.requestId ? `requestId=${this.requestId}` : null,
26
+ this.retryable ? `retryable=true` : null
27
+ ].filter(Boolean);
28
+ return `${parts.join(" ")}: ${this.message}`;
29
+ }
30
+ };
31
+ var AuthError = class extends BeepsError {
32
+ constructor(message = "Unauthorized", opts) {
33
+ super(message, { ...opts, status: opts?.status ?? 401 });
34
+ this.name = "AuthError";
35
+ }
36
+ };
37
+ var ValidationError = class extends BeepsError {
38
+ details;
39
+ constructor(message = "Validation error", opts) {
40
+ super(message, { ...opts, status: opts?.status ?? 400 });
41
+ this.name = "ValidationError";
42
+ this.details = opts?.details;
43
+ }
44
+ };
45
+ var NotFoundError = class extends BeepsError {
46
+ constructor(message = "Not found", opts) {
47
+ super(message, { ...opts, status: opts?.status ?? 404 });
48
+ this.name = "NotFoundError";
49
+ }
50
+ };
51
+ var RateLimitError = class extends BeepsError {
52
+ constructor(message = "Rate limited", opts) {
53
+ super(message, { ...opts, status: opts?.status ?? 429, retryable: true });
54
+ this.name = "RateLimitError";
55
+ }
56
+ };
57
+ var ServerError = class extends BeepsError {
58
+ constructor(message = "Server error", opts) {
59
+ super(message, { ...opts, status: opts?.status ?? 500, retryable: true });
60
+ this.name = "ServerError";
61
+ }
62
+ };
63
+ var NetworkError = class extends BeepsError {
64
+ constructor(message = "Network error", opts) {
65
+ super(message, { ...opts });
66
+ this.name = "NetworkError";
67
+ }
68
+ };
69
+ var HttpError = class extends BeepsError {
70
+ constructor(message = "HTTP error", opts) {
71
+ super(message, { ...opts });
72
+ this.name = "HttpError";
73
+ }
74
+ };
75
+ function mapHttpError(status, message, requestId) {
76
+ if (status === 401 || status === 403)
77
+ return new AuthError(message, { status, requestId });
78
+ if (status === 400)
79
+ return new ValidationError(message, { status, requestId });
80
+ if (status === 404) return new NotFoundError(message, { status, requestId });
81
+ if (status === 429) return new RateLimitError(message, { status, requestId });
82
+ if (status >= 500) return new ServerError(message, { status, requestId });
83
+ return new HttpError(message, { status, requestId });
84
+ }
85
+
86
+ // src/http.ts
87
+ var HttpClient = class {
88
+ cfg;
89
+ _fetch;
90
+ constructor(config) {
91
+ const baseURL = config.baseURL ?? "https://api.beeps.dev/v0";
92
+ const timeoutMs = config.timeoutMs ?? 1e4;
93
+ const retries = config.retries ?? { attempts: 2, backoffMs: 300 };
94
+ this.cfg = { ...config, baseURL, timeoutMs, retries };
95
+ this._fetch = config.fetch ?? fetch;
96
+ }
97
+ async post(path, body) {
98
+ return this.request("POST", path, body);
99
+ }
100
+ async get(path) {
101
+ return this.request("GET", path);
102
+ }
103
+ async put(path, body) {
104
+ return this.request("PUT", path, body);
105
+ }
106
+ async patch(path, body) {
107
+ return this.request("PATCH", path, body);
108
+ }
109
+ async delete(path) {
110
+ return this.request("DELETE", path);
111
+ }
112
+ async request(method, path, body) {
113
+ const url = this.cfg.baseURL.replace(/\/+$/, "") + "/" + path.replace(/^\/+/, "");
114
+ const controller = new AbortController();
115
+ const id = setTimeout(() => controller.abort(), this.cfg.timeoutMs);
116
+ try {
117
+ let attempt = 0;
118
+ while (true) {
119
+ try {
120
+ const res = await this._fetch(url, {
121
+ method,
122
+ headers: {
123
+ "Content-Type": "application/json",
124
+ "X-API-Key": this.cfg.apiKey,
125
+ "User-Agent": `beepsdev-sdk/${SDK_VERSION}`
126
+ },
127
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
128
+ signal: controller.signal
129
+ });
130
+ const requestId = res.headers.get("x-request-id") ?? void 0;
131
+ const isJson = (res.headers.get("content-type") || "").includes(
132
+ "application/json"
133
+ );
134
+ const payload = isJson ? await res.json().catch(() => ({})) : {};
135
+ if (res.ok) {
136
+ return payload;
137
+ }
138
+ const msg = typeof payload.error === "string" ? payload.error : typeof payload.message === "string" ? payload.message : "Request failed";
139
+ const err = mapHttpError(res.status, msg, requestId);
140
+ if (this.shouldRetry(res.status) && attempt < this.cfg.retries.attempts) {
141
+ attempt++;
142
+ await this.sleep(this.jitterBackoff(attempt));
143
+ continue;
144
+ }
145
+ throw err;
146
+ } catch (e) {
147
+ if (e instanceof BeepsError) {
148
+ throw e;
149
+ }
150
+ if (e instanceof Error && e.name === "AbortError") {
151
+ throw new NetworkError("Request timed out", { cause: e });
152
+ }
153
+ if (e instanceof TypeError) {
154
+ if (attempt < this.cfg.retries.attempts) {
155
+ attempt++;
156
+ await this.sleep(this.jitterBackoff(attempt));
157
+ continue;
158
+ }
159
+ throw new NetworkError("Network error", { cause: e });
160
+ }
161
+ throw e;
162
+ }
163
+ }
164
+ } finally {
165
+ clearTimeout(id);
166
+ }
167
+ }
168
+ shouldRetry(status) {
169
+ if (status === 429) return true;
170
+ if (status >= 500 && status !== 501 && status !== 505) return true;
171
+ return false;
172
+ }
173
+ jitterBackoff(attempt) {
174
+ const base = this.cfg.retries.backoffMs * Math.pow(2, attempt - 1);
175
+ const jitter = Math.random() * 100;
176
+ return base + jitter;
177
+ }
178
+ sleep(ms) {
179
+ return new Promise((r) => setTimeout(r, ms));
180
+ }
181
+ };
182
+
183
+ // src/resources/relay-rules.ts
184
+ var RelayRulesResource = class {
185
+ http;
186
+ constructor(http) {
187
+ this.http = http;
188
+ }
189
+ async list(relayId, params) {
190
+ const qs = new URLSearchParams();
191
+ if (params?.enabled !== void 0) {
192
+ qs.set("enabled", params.enabled.toString());
193
+ }
194
+ if (params?.ruleType) {
195
+ qs.set("ruleType", params.ruleType);
196
+ }
197
+ const path = qs.toString() ? `/relay/${relayId}/rules?${qs.toString()}` : `/relay/${relayId}/rules`;
198
+ const res = await this.http.get(path);
199
+ return res.rules;
200
+ }
201
+ async create(relayId, input) {
202
+ const res = await this.http.post(`/relay/${relayId}/rules`, input);
203
+ return res.rule;
204
+ }
205
+ async get(relayId, ruleId) {
206
+ const res = await this.http.get(`/relay/${relayId}/rules/${ruleId}`);
207
+ return res.rule;
208
+ }
209
+ async update(relayId, ruleId, input) {
210
+ const res = await this.http.put(`/relay/${relayId}/rules/${ruleId}`, input);
211
+ return res.rule;
212
+ }
213
+ async delete(relayId, ruleId) {
214
+ const res = await this.http.delete(`/relay/${relayId}/rules/${ruleId}`);
215
+ return res;
216
+ }
217
+ async reorder(relayId, input) {
218
+ const res = await this.http.put(`/relay/${relayId}/rules/reorder`, input);
219
+ return res.rules;
220
+ }
221
+ // Safe versions that return Result<T> instead of throwing
222
+ async listSafe(relayId, params) {
223
+ try {
224
+ const data = await this.list(relayId, params);
225
+ return { data };
226
+ } catch (error) {
227
+ return { error };
228
+ }
229
+ }
230
+ async createSafe(relayId, input) {
231
+ try {
232
+ const data = await this.create(relayId, input);
233
+ return { data };
234
+ } catch (error) {
235
+ return { error };
236
+ }
237
+ }
238
+ async getSafe(relayId, ruleId) {
239
+ try {
240
+ const data = await this.get(relayId, ruleId);
241
+ return { data };
242
+ } catch (error) {
243
+ return { error };
244
+ }
245
+ }
246
+ async updateSafe(relayId, ruleId, input) {
247
+ try {
248
+ const data = await this.update(relayId, ruleId, input);
249
+ return { data };
250
+ } catch (error) {
251
+ return { error };
252
+ }
253
+ }
254
+ async deleteSafe(relayId, ruleId) {
255
+ try {
256
+ const data = await this.delete(relayId, ruleId);
257
+ return { data };
258
+ } catch (error) {
259
+ return { error };
260
+ }
261
+ }
262
+ async reorderSafe(relayId, input) {
263
+ try {
264
+ const data = await this.reorder(relayId, input);
265
+ return { data };
266
+ } catch (error) {
267
+ return { error };
268
+ }
269
+ }
270
+ };
271
+
272
+ // src/resources/relay.ts
273
+ var RelayResource = class {
274
+ http;
275
+ rules;
276
+ constructor(http) {
277
+ this.http = http;
278
+ this.rules = new RelayRulesResource(http);
279
+ }
280
+ async create(input) {
281
+ const res = await this.http.post("/relay", input);
282
+ return res.relay;
283
+ }
284
+ async list() {
285
+ const res = await this.http.get("/relay");
286
+ return res.relays;
287
+ }
288
+ async lint(relayId, input = {}) {
289
+ const res = await this.http.post(
290
+ `/relay/${relayId}/lint`,
291
+ input
292
+ );
293
+ return res;
294
+ }
295
+ async simulate(relayId, input) {
296
+ const res = await this.http.post(
297
+ `/relay/${relayId}/simulate`,
298
+ input
299
+ );
300
+ return res;
301
+ }
302
+ async createSafe(input) {
303
+ try {
304
+ const data = await this.create(input);
305
+ return { data };
306
+ } catch (error) {
307
+ return { error };
308
+ }
309
+ }
310
+ async lintSafe(relayId, input = {}) {
311
+ try {
312
+ const data = await this.lint(relayId, input);
313
+ return { data };
314
+ } catch (error) {
315
+ return { error };
316
+ }
317
+ }
318
+ async simulateSafe(relayId, input) {
319
+ try {
320
+ const data = await this.simulate(relayId, input);
321
+ return { data };
322
+ } catch (error) {
323
+ return { error };
324
+ }
325
+ }
326
+ };
327
+
328
+ // src/utils/date.ts
329
+ function formatUTCDate(date) {
330
+ return date.toISOString();
331
+ }
332
+ function isIsoZ(value) {
333
+ return value.endsWith("Z") && !isNaN(Date.parse(value));
334
+ }
335
+ function normalizeDateParam(date) {
336
+ if (date instanceof Date) {
337
+ return formatUTCDate(date);
338
+ }
339
+ if (typeof date === "string") {
340
+ if (!isIsoZ(date)) {
341
+ throw new Error(
342
+ `Invalid date format. Date must be a valid ISO 8601 timestamp with Z suffix (e.g., "2024-01-29T00:00:00.000Z"). Received: "${date}"`
343
+ );
344
+ }
345
+ return date;
346
+ }
347
+ throw new Error(
348
+ `Invalid date type. Expected Date object or ISO 8601 string with Z suffix, received: ${typeof date}`
349
+ );
350
+ }
351
+
352
+ // src/resources/schedule.ts
353
+ var ScheduleResource = class {
354
+ http;
355
+ constructor(http) {
356
+ this.http = http;
357
+ }
358
+ async create(input) {
359
+ const res = await this.http.post("/schedule", input);
360
+ return res.schedule;
361
+ }
362
+ async list() {
363
+ const res = await this.http.get("/schedule");
364
+ return res.schedules;
365
+ }
366
+ async addMember(scheduleId, input) {
367
+ const res = await this.http.post(
368
+ `/schedule/${scheduleId}/members`,
369
+ input
370
+ );
371
+ return res.member;
372
+ }
373
+ async listMembers(scheduleId) {
374
+ const res = await this.http.get(
375
+ `/schedule/${scheduleId}/members`
376
+ );
377
+ return res.members;
378
+ }
379
+ async listMembersSafe(scheduleId) {
380
+ try {
381
+ const data = await this.listMembers(scheduleId);
382
+ return { data };
383
+ } catch (error) {
384
+ return { error };
385
+ }
386
+ }
387
+ async removeMember(scheduleId, userId) {
388
+ const res = await this.http.delete(
389
+ `/schedule/${scheduleId}/members/${userId}`
390
+ );
391
+ return res.member;
392
+ }
393
+ async getAssignments(scheduleId, params) {
394
+ const qs = new URLSearchParams();
395
+ if (params) {
396
+ if (params.type === "until") {
397
+ qs.set("type", "until");
398
+ const normalizedDate = normalizeDateParam(params.date);
399
+ qs.set("date", normalizedDate);
400
+ } else if (params.type === "days") {
401
+ qs.set("type", "days");
402
+ qs.set("count", String(params.count));
403
+ } else {
404
+ if (params.type) {
405
+ qs.set("type", params.type);
406
+ }
407
+ if (params.count !== void 0) {
408
+ qs.set("count", String(params.count));
409
+ }
410
+ }
411
+ }
412
+ const path = qs.toString() ? `/schedule/${scheduleId}/assignments?${qs.toString()}` : `/schedule/${scheduleId}/assignments`;
413
+ const res = await this.http.get(path);
414
+ return res.assignments;
415
+ }
416
+ async getOnCall(scheduleId) {
417
+ const res = await this.http.get(
418
+ `/schedule/${scheduleId}/on-call`
419
+ );
420
+ return res.onCall;
421
+ }
422
+ async getOnCallSafe(scheduleId) {
423
+ try {
424
+ const data = await this.getOnCall(scheduleId);
425
+ return { data };
426
+ } catch (error) {
427
+ return { error };
428
+ }
429
+ }
430
+ async createOverride(scheduleId, input) {
431
+ const body = {
432
+ userId: input.userId,
433
+ startAt: normalizeDateParam(input.startAt),
434
+ endAt: normalizeDateParam(input.endAt),
435
+ reason: input.reason
436
+ };
437
+ const res = await this.http.post(
438
+ `/schedule/${scheduleId}/overrides`,
439
+ body
440
+ );
441
+ return res.override;
442
+ }
443
+ async listOverrides(scheduleId, params) {
444
+ const qs = new URLSearchParams();
445
+ if (params) {
446
+ if (params.startAt) {
447
+ qs.set("startAt", normalizeDateParam(params.startAt));
448
+ }
449
+ if (params.endAt) {
450
+ qs.set("endAt", normalizeDateParam(params.endAt));
451
+ }
452
+ }
453
+ const path = qs.toString() ? `/schedule/${scheduleId}/overrides?${qs.toString()}` : `/schedule/${scheduleId}/overrides`;
454
+ const res = await this.http.get(path);
455
+ return res.overrides;
456
+ }
457
+ async cancelOverride(scheduleId, overrideId) {
458
+ const res = await this.http.delete(
459
+ `/schedule/${scheduleId}/overrides/${overrideId}`
460
+ );
461
+ return res.override;
462
+ }
463
+ async updateOverride(scheduleId, overrideId, input) {
464
+ const body = {};
465
+ if (input.startAt !== void 0) {
466
+ body.startAt = normalizeDateParam(input.startAt);
467
+ }
468
+ if (input.endAt !== void 0) {
469
+ body.endAt = normalizeDateParam(input.endAt);
470
+ }
471
+ if (input.reason !== void 0) {
472
+ body.reason = input.reason;
473
+ }
474
+ const res = await this.http.patch(
475
+ `/schedule/${scheduleId}/overrides/${overrideId}`,
476
+ body
477
+ );
478
+ return res.override;
479
+ }
480
+ };
481
+
482
+ // src/resources/contact-method.ts
483
+ var ContactMethodResource = class {
484
+ http;
485
+ constructor(http) {
486
+ this.http = http;
487
+ }
488
+ async list(params) {
489
+ const qs = new URLSearchParams();
490
+ qs.set("userId", params.userId);
491
+ const res = await this.http.get(
492
+ `/contact-methods?${qs.toString()}`
493
+ );
494
+ return res.contactMethods;
495
+ }
496
+ async create(input) {
497
+ const res = await this.http.post(
498
+ "/contact-methods",
499
+ input
500
+ );
501
+ return res.contactMethod;
502
+ }
503
+ async delete(id, params) {
504
+ const qs = new URLSearchParams();
505
+ qs.set("userId", params.userId);
506
+ const res = await this.http.delete(
507
+ `/contact-methods/${id}?${qs.toString()}`
508
+ );
509
+ return res;
510
+ }
511
+ async listSafe(params) {
512
+ try {
513
+ const data = await this.list(params);
514
+ return { data };
515
+ } catch (error) {
516
+ return { error };
517
+ }
518
+ }
519
+ async createSafe(input) {
520
+ try {
521
+ const data = await this.create(input);
522
+ return { data };
523
+ } catch (error) {
524
+ return { error };
525
+ }
526
+ }
527
+ async deleteSafe(id, params) {
528
+ try {
529
+ const data = await this.delete(id, params);
530
+ return { data };
531
+ } catch (error) {
532
+ return { error };
533
+ }
534
+ }
535
+ };
536
+
537
+ // src/resources/alert.ts
538
+ var AlertResource = class {
539
+ http;
540
+ constructor(http) {
541
+ this.http = http;
542
+ }
543
+ async list() {
544
+ const res = await this.http.get("/alerts");
545
+ return res.alerts;
546
+ }
547
+ async listActive() {
548
+ const res = await this.http.get("/alerts/active");
549
+ return res.alerts;
550
+ }
551
+ async listResolved() {
552
+ const res = await this.http.get("/alerts/resolved");
553
+ return res.alerts;
554
+ }
555
+ async get(alertId) {
556
+ const res = await this.http.get(`/alerts/${alertId}`);
557
+ return res.alert;
558
+ }
559
+ async acknowledge(alertId, input) {
560
+ const res = await this.http.post(
561
+ `/alerts/${alertId}/acknowledge`,
562
+ input || {}
563
+ );
564
+ return res.alert;
565
+ }
566
+ async resolve(alertId) {
567
+ const res = await this.http.post(
568
+ `/alerts/${alertId}/resolve`,
569
+ {}
570
+ );
571
+ return res.alert;
572
+ }
573
+ async assign(alertId, userId) {
574
+ const res = await this.http.post(
575
+ `/alerts/${alertId}/assign`,
576
+ { userId }
577
+ );
578
+ return res.alert;
579
+ }
580
+ async listSafe() {
581
+ try {
582
+ const data = await this.list();
583
+ return { data };
584
+ } catch (error) {
585
+ return { error };
586
+ }
587
+ }
588
+ async listActiveSafe() {
589
+ try {
590
+ const data = await this.listActive();
591
+ return { data };
592
+ } catch (error) {
593
+ return { error };
594
+ }
595
+ }
596
+ async listResolvedSafe() {
597
+ try {
598
+ const data = await this.listResolved();
599
+ return { data };
600
+ } catch (error) {
601
+ return { error };
602
+ }
603
+ }
604
+ async getSafe(alertId) {
605
+ try {
606
+ const data = await this.get(alertId);
607
+ return { data };
608
+ } catch (error) {
609
+ return { error };
610
+ }
611
+ }
612
+ async acknowledgeSafe(alertId, input) {
613
+ try {
614
+ const data = await this.acknowledge(alertId, input);
615
+ return { data };
616
+ } catch (error) {
617
+ return { error };
618
+ }
619
+ }
620
+ async resolveSafe(alertId) {
621
+ try {
622
+ const data = await this.resolve(alertId);
623
+ return { data };
624
+ } catch (error) {
625
+ return { error };
626
+ }
627
+ }
628
+ async assignSafe(alertId, userId) {
629
+ try {
630
+ const data = await this.assign(alertId, userId);
631
+ return { data };
632
+ } catch (error) {
633
+ return { error };
634
+ }
635
+ }
636
+ };
637
+
638
+ // src/resources/integration.ts
639
+ var IntegrationResource = class {
640
+ http;
641
+ constructor(http) {
642
+ this.http = http;
643
+ }
644
+ async list() {
645
+ const res = await this.http.get("/integrations");
646
+ return res.integrations;
647
+ }
648
+ async create(input) {
649
+ const res = await this.http.post("/integrations", input);
650
+ return res.integration;
651
+ }
652
+ async get(integrationId) {
653
+ const res = await this.http.get(`/integrations/${integrationId}`);
654
+ return res.integration;
655
+ }
656
+ async update(integrationId, input) {
657
+ const res = await this.http.put(`/integrations/${integrationId}`, input);
658
+ return res.integration;
659
+ }
660
+ async delete(integrationId) {
661
+ const res = await this.http.delete(`/integrations/${integrationId}`);
662
+ return res;
663
+ }
664
+ async listSafe() {
665
+ try {
666
+ const data = await this.list();
667
+ return { data };
668
+ } catch (error) {
669
+ return { error };
670
+ }
671
+ }
672
+ async createSafe(input) {
673
+ try {
674
+ const data = await this.create(input);
675
+ return { data };
676
+ } catch (error) {
677
+ return { error };
678
+ }
679
+ }
680
+ async getSafe(integrationId) {
681
+ try {
682
+ const data = await this.get(integrationId);
683
+ return { data };
684
+ } catch (error) {
685
+ return { error };
686
+ }
687
+ }
688
+ async updateSafe(integrationId, input) {
689
+ try {
690
+ const data = await this.update(integrationId, input);
691
+ return { data };
692
+ } catch (error) {
693
+ return { error };
694
+ }
695
+ }
696
+ async deleteSafe(integrationId) {
697
+ try {
698
+ const data = await this.delete(integrationId);
699
+ return { data };
700
+ } catch (error) {
701
+ return { error };
702
+ }
703
+ }
704
+ };
705
+
706
+ // src/resources/member.ts
707
+ var MemberResource = class {
708
+ http;
709
+ constructor(http) {
710
+ this.http = http;
711
+ }
712
+ async list() {
713
+ const res = await this.http.get("/members");
714
+ return res.members;
715
+ }
716
+ async listSafe() {
717
+ try {
718
+ const data = await this.list();
719
+ return { data };
720
+ } catch (error) {
721
+ return { error };
722
+ }
723
+ }
724
+ };
725
+
726
+ // src/client.ts
727
+ var BeepsClient = class {
728
+ relay;
729
+ schedule;
730
+ contactMethod;
731
+ alert;
732
+ integration;
733
+ member;
734
+ constructor(config) {
735
+ if (!config.apiKey) {
736
+ throw new Error("apiKey is required");
737
+ }
738
+ const http = new HttpClient(config);
739
+ this.relay = new RelayResource(http);
740
+ this.schedule = new ScheduleResource(http);
741
+ this.contactMethod = new ContactMethodResource(http);
742
+ this.alert = new AlertResource(http);
743
+ this.integration = new IntegrationResource(http);
744
+ this.member = new MemberResource(http);
745
+ }
746
+ };
747
+ export {
748
+ AlertResource,
749
+ AuthError,
750
+ BeepsClient,
751
+ BeepsError,
752
+ ContactMethodResource,
753
+ HttpError,
754
+ MemberResource,
755
+ NetworkError,
756
+ NotFoundError,
757
+ RateLimitError,
758
+ RelayRulesResource,
759
+ ScheduleResource,
760
+ ServerError,
761
+ ValidationError,
762
+ formatUTCDate,
763
+ isIsoZ,
764
+ mapHttpError,
765
+ normalizeDateParam
766
+ };
767
+ //# sourceMappingURL=index.js.map