@carto/api-client 0.4.2 → 0.4.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/build/api-client.cjs +933 -925
  3. package/build/api-client.cjs.map +1 -1
  4. package/build/api-client.modern.js +873 -865
  5. package/build/api-client.modern.js.map +1 -1
  6. package/build/index.d.ts +1 -1
  7. package/build/sources/boundary-query-source.d.ts +2 -1
  8. package/build/sources/boundary-table-source.d.ts +2 -1
  9. package/build/sources/h3-query-source.d.ts +2 -1
  10. package/build/sources/h3-table-source.d.ts +2 -1
  11. package/build/sources/h3-tileset-source.d.ts +2 -1
  12. package/build/sources/index.d.ts +13 -14
  13. package/build/sources/quadbin-query-source.d.ts +2 -1
  14. package/build/sources/quadbin-table-source.d.ts +2 -1
  15. package/build/sources/quadbin-tileset-source.d.ts +2 -1
  16. package/build/sources/raster-source.d.ts +2 -1
  17. package/build/sources/types.d.ts +16 -0
  18. package/build/sources/vector-query-source.d.ts +2 -1
  19. package/build/sources/vector-table-source.d.ts +2 -1
  20. package/build/sources/vector-tileset-source.d.ts +2 -1
  21. package/package.json +1 -1
  22. package/src/index.ts +1 -36
  23. package/src/sources/boundary-query-source.ts +4 -2
  24. package/src/sources/boundary-table-source.ts +4 -2
  25. package/src/sources/h3-query-source.ts +3 -1
  26. package/src/sources/h3-table-source.ts +3 -1
  27. package/src/sources/h3-tileset-source.ts +4 -2
  28. package/src/sources/index.ts +54 -24
  29. package/src/sources/quadbin-query-source.ts +4 -1
  30. package/src/sources/quadbin-table-source.ts +4 -1
  31. package/src/sources/quadbin-tileset-source.ts +4 -2
  32. package/src/sources/raster-source.ts +4 -2
  33. package/src/sources/types.ts +18 -0
  34. package/src/sources/vector-query-source.ts +9 -1
  35. package/src/sources/vector-table-source.ts +10 -1
  36. package/src/sources/vector-tileset-source.ts +4 -2
@@ -381,449 +381,603 @@ const DEFAULT_AGGREGATION_RES_LEVEL_H3 = 4;
381
381
  */
382
382
  const DEFAULT_AGGREGATION_RES_LEVEL_QUADBIN = 6;
383
383
 
384
- /**
385
- * Return more descriptive error from API
386
- * @internalRemarks Source: @carto/react-api
387
- */
388
-
389
- /** @internalRemarks Source: @carto/react-api */
390
-
391
- function _catch(body, recover) {
392
- try {
393
- var result = body();
394
- } catch (e) {
395
- return recover(e);
396
- }
397
- if (result && result.then) {
398
- return result.then(void 0, recover);
399
- }
400
- return result;
384
+ // deck.gl
385
+ // SPDX-License-Identifier: MIT
386
+ // Copyright (c) vis.gl contributors
387
+ function joinPath() {
388
+ return [].slice.call(arguments).map(part => part.endsWith('/') ? part.slice(0, -1) : part).join('/');
401
389
  }
402
- const makeCall = function (_ref2) {
390
+ function buildV3Path(apiBaseUrl, version, endpoint) {
391
+ return joinPath(apiBaseUrl, version, endpoint, ...[].slice.call(arguments, 3));
392
+ }
393
+ /** @internal Required by fetchMap(). */
394
+ function buildPublicMapUrl(_ref) {
403
395
  let {
404
- url,
405
- accessToken,
406
- opts
396
+ apiBaseUrl,
397
+ cartoMapId
398
+ } = _ref;
399
+ return buildV3Path(apiBaseUrl, 'v3', 'maps', 'public', cartoMapId);
400
+ }
401
+ /** @internal Required by fetchMap(). */
402
+ function buildStatsUrl(_ref2) {
403
+ let {
404
+ attribute,
405
+ apiBaseUrl,
406
+ connectionName,
407
+ source,
408
+ type
407
409
  } = _ref2;
408
- try {
409
- let _exit;
410
- function _temp2(_result) {
411
- if (_exit) ;
412
- if (!response.ok) {
413
- dealWithApiError({
414
- response,
415
- data
416
- });
417
- }
418
- return data;
419
- }
420
- let response;
421
- let data;
422
- const isPost = opts?.method === 'POST';
423
- const _temp = _catch(function () {
424
- return Promise.resolve(fetch(url.toString(), {
425
- headers: {
426
- Authorization: `Bearer ${accessToken}`,
427
- ...(isPost && {
428
- 'Content-Type': 'application/json'
429
- })
430
- },
431
- ...(isPost && {
432
- method: opts?.method,
433
- body: opts?.body
434
- }),
435
- signal: opts?.abortController?.signal,
436
- ...opts?.otherOptions
437
- })).then(function (_fetch) {
438
- response = _fetch;
439
- return Promise.resolve(response.json()).then(function (_response$json) {
440
- data = _response$json;
441
- });
442
- });
443
- }, function (error) {
444
- if (error.name === 'AbortError') throw error;
445
- throw new Error(`Failed request: ${error}`);
446
- });
447
- return Promise.resolve(_temp && _temp.then ? _temp.then(_temp2) : _temp2(_temp));
448
- } catch (e) {
449
- return Promise.reject(e);
410
+ if (type === 'query') {
411
+ return buildV3Path(apiBaseUrl, 'v3', 'stats', connectionName, attribute);
450
412
  }
451
- };
452
- function dealWithApiError(_ref) {
413
+ // type === 'table'
414
+ return buildV3Path(apiBaseUrl, 'v3', 'stats', connectionName, source, attribute);
415
+ }
416
+ function buildSourceUrl(_ref3) {
453
417
  let {
454
- response,
455
- data
456
- } = _ref;
457
- if (data.error === 'Column not found') {
458
- throw new InvalidColumnError(`${data.error} ${data.column_name}`);
459
- }
460
- if (typeof data.error === 'string' && data.error?.includes('Missing columns')) {
461
- throw new InvalidColumnError(data.error);
462
- }
463
- switch (response.status) {
464
- case 401:
465
- throw new Error('Unauthorized access. Invalid credentials');
466
- case 403:
467
- throw new Error('Forbidden access to the requested data');
468
- default:
469
- const msg = data && data.error && typeof data.error === 'string' ? data.error : JSON.stringify(data?.hint || data.error?.[0]);
470
- throw new Error(msg);
471
- }
418
+ apiBaseUrl,
419
+ connectionName,
420
+ endpoint
421
+ } = _ref3;
422
+ return buildV3Path(apiBaseUrl, 'v3', 'maps', connectionName, endpoint);
423
+ }
424
+ function buildQueryUrl(_ref4) {
425
+ let {
426
+ apiBaseUrl,
427
+ connectionName
428
+ } = _ref4;
429
+ return buildV3Path(apiBaseUrl, 'v3', 'sql', connectionName, 'query');
472
430
  }
473
431
 
474
- /** @internalRemarks Source: @carto/react-api */
475
- const AVAILABLE_MODELS = ['category', 'histogram', 'formula', 'pick', 'timeseries', 'range', 'scatterplot', 'table'];
476
- const {
477
- V3
478
- } = exports.ApiVersion;
479
- const REQUEST_GET_MAX_URL_LENGTH = 2048;
432
+ // deck.gl
433
+ // SPDX-License-Identifier: MIT
434
+ // Copyright (c) vis.gl contributors
480
435
  /**
481
- * Execute a SQL model request.
482
- * @internalRemarks Source: @carto/react-api
436
+ *
437
+ * Custom error for reported errors in CARTO Maps API.
438
+ * Provides useful debugging information in console and context for applications.
439
+ *
483
440
  */
484
- function executeModel(props) {
485
- assert(props.source, 'executeModel: missing source');
486
- assert(props.model, 'executeModel: missing model');
487
- assert(props.params, 'executeModel: missing params');
488
- assert(AVAILABLE_MODELS.includes(props.model), `executeModel: model provided isn't valid. Available models: ${AVAILABLE_MODELS.join(', ')}`);
489
- const {
490
- model,
491
- source,
492
- params,
493
- opts
494
- } = props;
495
- const {
496
- type,
497
- apiVersion,
498
- apiBaseUrl,
499
- accessToken,
500
- connectionName,
501
- clientId
502
- } = source;
503
- assert(apiBaseUrl, 'executeModel: missing apiBaseUrl');
504
- assert(accessToken, 'executeModel: missing accessToken');
505
- assert(apiVersion === V3, 'executeModel: SQL Model API requires CARTO 3+');
506
- assert(type !== 'tileset', 'executeModel: Tilesets not supported');
507
- let url = `${apiBaseUrl}/v3/sql/${connectionName}/model/${model}`;
508
- const {
509
- data,
510
- filters,
511
- filtersLogicalOperator = 'and',
512
- geoColumn = DEFAULT_GEO_COLUMN
513
- } = source;
514
- const queryParameters = source.queryParameters ? JSON.stringify(source.queryParameters) : '';
515
- const queryParams = {
516
- type,
517
- client: clientId,
518
- source: data,
519
- params: JSON.stringify(params),
520
- queryParameters,
521
- filters: JSON.stringify(filters),
522
- filtersLogicalOperator
523
- };
524
- // Picking Model API requires 'spatialDataColumn'.
525
- if (model === 'pick') {
526
- queryParams.spatialDataColumn = geoColumn;
527
- }
528
- // API supports multiple filters, we apply it only to geoColumn
529
- const spatialFilters = source.spatialFilter ? {
530
- [geoColumn]: source.spatialFilter
531
- } : undefined;
532
- if (spatialFilters) {
533
- queryParams.spatialFilters = JSON.stringify(spatialFilters);
534
- }
535
- const urlWithSearchParams = url + '?' + new URLSearchParams(queryParams).toString();
536
- const isGet = urlWithSearchParams.length <= REQUEST_GET_MAX_URL_LENGTH;
537
- if (isGet) {
538
- url = urlWithSearchParams;
539
- } else {
540
- // undo the JSON.stringify, @TODO find a better pattern
541
- queryParams.params = params;
542
- queryParams.filters = filters;
543
- queryParams.queryParameters = source.queryParameters;
544
- if (spatialFilters) {
545
- queryParams.spatialFilters = spatialFilters;
441
+ class CartoAPIError extends Error {
442
+ constructor(error, errorContext, response, responseJson) {
443
+ let responseString = 'Failed to connect';
444
+ if (response) {
445
+ responseString = 'Server returned: ';
446
+ if (response.status === 400) {
447
+ responseString += 'Bad request';
448
+ } else if (response.status === 401 || response.status === 403) {
449
+ responseString += 'Unauthorized access';
450
+ } else if (response.status === 404) {
451
+ responseString += 'Not found';
452
+ } else {
453
+ responseString += 'Error';
454
+ }
455
+ responseString += ` (${response.status}):`;
546
456
  }
547
- }
548
- return makeCall({
549
- url,
550
- accessToken: source.accessToken,
551
- opts: {
552
- ...opts,
553
- method: isGet ? 'GET' : 'POST',
554
- ...(!isGet && {
555
- body: JSON.stringify(queryParams)
556
- })
457
+ responseString += ` ${error.message || error}`;
458
+ let message = `${errorContext.requestType} API request failed`;
459
+ message += `\n${responseString}`;
460
+ for (const key of Object.keys(errorContext)) {
461
+ if (key === 'requestType') continue;
462
+ message += `\n${formatErrorKey(key)}: ${errorContext[key]}`;
557
463
  }
558
- });
464
+ message += '\n';
465
+ super(message);
466
+ /** Source error from server */
467
+ this.error = void 0;
468
+ /** Context (API call & parameters) in which error occured */
469
+ this.errorContext = void 0;
470
+ /** Response from server */
471
+ this.response = void 0;
472
+ /** JSON Response from server */
473
+ this.responseJson = void 0;
474
+ this.name = 'CartoAPIError';
475
+ this.response = response;
476
+ this.responseJson = responseJson;
477
+ this.error = error;
478
+ this.errorContext = errorContext;
479
+ }
559
480
  }
560
-
561
481
  /**
562
- * Source for Widget API requests on a data source defined by a SQL query.
563
- *
564
- * Abstract class. Use {@link WidgetQuerySource} or {@link WidgetTableSource}.
482
+ * Converts camelCase to Camel Case
565
483
  */
566
- class WidgetBaseSource {
567
- constructor(props) {
568
- this.props = void 0;
569
- this.props = {
570
- ...WidgetBaseSource.defaultProps,
571
- ...props
484
+ function formatErrorKey(key) {
485
+ return key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase());
486
+ }
487
+
488
+ // deck.gl
489
+ const requestWithParameters = function (_ref) {
490
+ let {
491
+ baseUrl,
492
+ parameters = {},
493
+ headers: customHeaders = {},
494
+ errorContext,
495
+ maxLengthURL = DEFAULT_MAX_LENGTH_URL,
496
+ localCache
497
+ } = _ref;
498
+ try {
499
+ // Parameters added to all requests issued with `requestWithParameters()`.
500
+ // These parameters override parameters already in the base URL, but not
501
+ // user-provided parameters.
502
+ parameters = {
503
+ v: V3_MINOR_VERSION,
504
+ client: getClient(),
505
+ ...(typeof deck !== 'undefined' && deck.VERSION && {
506
+ deckglVersion: deck.VERSION
507
+ }),
508
+ ...parameters
572
509
  };
573
- }
574
- _getModelSource(owner) {
575
- const props = this.props;
576
- return {
577
- apiVersion: props.apiVersion,
578
- apiBaseUrl: props.apiBaseUrl,
579
- clientId: props.clientId,
580
- accessToken: props.accessToken,
581
- connectionName: props.connectionName,
582
- filters: getApplicableFilters(owner, props.filters),
583
- filtersLogicalOperator: props.filtersLogicalOperator,
584
- geoColumn: props.geoColumn
510
+ baseUrl = excludeURLParameters(baseUrl, Object.keys(parameters));
511
+ const key = createCacheKey(baseUrl, parameters, customHeaders);
512
+ const {
513
+ cache: REQUEST_CACHE,
514
+ canReadCache,
515
+ canStoreInCache
516
+ } = getCacheSettings(localCache);
517
+ if (canReadCache && REQUEST_CACHE.has(key)) {
518
+ return Promise.resolve(REQUEST_CACHE.get(key));
519
+ }
520
+ const url = createURLWithParameters(baseUrl, parameters);
521
+ const headers = {
522
+ ...DEFAULT_HEADERS,
523
+ ...customHeaders
585
524
  };
525
+ /* global fetch */
526
+ const fetchPromise = url.length > maxLengthURL ? fetch(baseUrl, {
527
+ method: 'POST',
528
+ body: JSON.stringify(parameters),
529
+ headers
530
+ }) : fetch(url, {
531
+ headers
532
+ });
533
+ let response;
534
+ let responseJson;
535
+ const jsonPromise = fetchPromise.then(_response => {
536
+ response = _response;
537
+ return response.json();
538
+ }).then(json => {
539
+ responseJson = json;
540
+ if (!response || !response.ok) {
541
+ throw new Error(json.error);
542
+ }
543
+ return json;
544
+ }).catch(error => {
545
+ if (canStoreInCache) {
546
+ REQUEST_CACHE.delete(key);
547
+ }
548
+ throw new CartoAPIError(error, errorContext, response, responseJson);
549
+ });
550
+ if (canStoreInCache) {
551
+ REQUEST_CACHE.set(key, jsonPromise);
552
+ }
553
+ return Promise.resolve(jsonPromise);
554
+ } catch (e) {
555
+ return Promise.reject(e);
586
556
  }
587
- /****************************************************************************
588
- * CATEGORIES
589
- */
590
- /**
591
- * Returns a list of labeled datapoints for categorical data. Suitable for
592
- * charts including grouped bar charts, pie charts, and tree charts.
593
- */
594
- getCategories(options) {
595
- try {
596
- const _this = this;
597
- const {
598
- filterOwner,
599
- spatialFilter,
600
- abortController,
601
- ...params
602
- } = options;
603
- const {
604
- column,
605
- operation,
606
- operationColumn
607
- } = params;
608
- return Promise.resolve(executeModel({
609
- model: 'category',
610
- source: {
611
- ..._this.getModelSource(filterOwner),
612
- spatialFilter
613
- },
614
- params: {
615
- column,
616
- operation,
617
- operationColumn: operationColumn || column
618
- },
619
- opts: {
620
- abortController
621
- }
622
- }).then(res => normalizeObjectKeys(res.rows)));
623
- } catch (e) {
624
- return Promise.reject(e);
557
+ };
558
+ const DEFAULT_HEADERS = {
559
+ Accept: 'application/json',
560
+ 'Content-Type': 'application/json'
561
+ };
562
+ const DEFAULT_REQUEST_CACHE = new Map();
563
+ function getCacheSettings(localCache) {
564
+ const canReadCache = localCache?.cacheControl?.includes('no-cache') ? false : true;
565
+ const canStoreInCache = localCache?.cacheControl?.includes('no-store') ? false : true;
566
+ const cache = localCache?.cache || DEFAULT_REQUEST_CACHE;
567
+ return {
568
+ cache,
569
+ canReadCache,
570
+ canStoreInCache
571
+ };
572
+ }
573
+ function createCacheKey(baseUrl, parameters, headers) {
574
+ const parameterEntries = Object.entries(parameters).sort((_ref2, _ref3) => {
575
+ let [a] = _ref2;
576
+ let [b] = _ref3;
577
+ return a > b ? 1 : -1;
578
+ });
579
+ const headerEntries = Object.entries(headers).sort((_ref4, _ref5) => {
580
+ let [a] = _ref4;
581
+ let [b] = _ref5;
582
+ return a > b ? 1 : -1;
583
+ });
584
+ return JSON.stringify({
585
+ baseUrl,
586
+ parameters: parameterEntries,
587
+ headers: headerEntries
588
+ });
589
+ }
590
+ /**
591
+ * Appends query string parameters to a URL. Existing URL parameters are kept,
592
+ * unless there is a conflict, in which case the new parameters override
593
+ * those already in the URL.
594
+ */
595
+ function createURLWithParameters(baseUrlString, parameters) {
596
+ const baseUrl = new URL(baseUrlString);
597
+ for (const [key, value] of Object.entries(parameters)) {
598
+ if (isPureObject(value) || Array.isArray(value)) {
599
+ baseUrl.searchParams.set(key, JSON.stringify(value));
600
+ } else {
601
+ baseUrl.searchParams.set(key, value.toString());
625
602
  }
626
603
  }
627
- /****************************************************************************
628
- * FEATURES
629
- */
630
- /**
631
- * Given a list of feature IDs (as found in `_carto_feature_id`) returns all
632
- * matching features. In datasets containing features with duplicate geometries,
633
- * feature IDs may be duplicated (IDs are a hash of geometry) and so more
634
- * results may be returned than IDs in the request.
635
- * @internal
636
- * @experimental
637
- */
638
- getFeatures(options) {
639
- try {
640
- const _this2 = this;
641
- const {
642
- filterOwner,
643
- spatialFilter,
644
- abortController,
645
- ...params
646
- } = options;
647
- const {
648
- columns,
649
- dataType,
650
- featureIds,
651
- z,
652
- limit,
653
- tileResolution
654
- } = params;
655
- return Promise.resolve(executeModel({
656
- model: 'pick',
657
- source: {
658
- ..._this2.getModelSource(filterOwner),
659
- spatialFilter
660
- },
661
- params: {
662
- columns,
663
- dataType,
664
- featureIds,
665
- z,
666
- limit: limit || 1000,
667
- tileResolution: tileResolution || DEFAULT_TILE_RESOLUTION
668
- },
669
- opts: {
670
- abortController
671
- }
672
- // Avoid `normalizeObjectKeys()`, which changes column names.
673
- }).then(_ref => {
674
- let {
675
- rows
676
- } = _ref;
677
- return {
678
- rows
679
- };
680
- }));
681
- } catch (e) {
682
- return Promise.reject(e);
604
+ return baseUrl.toString();
605
+ }
606
+ /**
607
+ * Deletes query string parameters from a URL.
608
+ */
609
+ function excludeURLParameters(baseUrlString, parameters) {
610
+ const baseUrl = new URL(baseUrlString);
611
+ for (const param of parameters) {
612
+ if (baseUrl.searchParams.has(param)) {
613
+ baseUrl.searchParams.delete(param);
683
614
  }
684
615
  }
685
- /****************************************************************************
686
- * FORMULA
687
- */
688
- /**
689
- * Returns a scalar numerical statistic over all matching data. Suitable
690
- * for 'headline' or 'scorecard' figures such as counts and sums.
691
- */
692
- getFormula(options) {
693
- try {
694
- const _this3 = this;
695
- const {
696
- filterOwner,
697
- spatialFilter,
698
- abortController,
699
- operationExp,
700
- ...params
701
- } = options;
702
- const {
703
- column,
704
- operation
705
- } = params;
706
- return Promise.resolve(executeModel({
707
- model: 'formula',
708
- source: {
709
- ..._this3.getModelSource(filterOwner),
710
- spatialFilter
711
- },
712
- params: {
713
- column: column ?? '*',
714
- operation,
715
- operationExp
716
- },
717
- opts: {
718
- abortController
719
- }
720
- }).then(res => normalizeObjectKeys(res.rows[0])));
721
- } catch (e) {
722
- return Promise.reject(e);
616
+ return baseUrl.toString();
617
+ }
618
+
619
+ // deck.gl
620
+ const baseSource = function (endpoint, options, urlParameters) {
621
+ try {
622
+ const {
623
+ accessToken,
624
+ connectionName,
625
+ cache,
626
+ ...optionalOptions
627
+ } = options;
628
+ const mergedOptions = {
629
+ ...SOURCE_DEFAULTS,
630
+ accessToken,
631
+ connectionName,
632
+ endpoint
633
+ };
634
+ for (const key in optionalOptions) {
635
+ if (optionalOptions[key]) {
636
+ mergedOptions[key] = optionalOptions[key];
637
+ }
723
638
  }
724
- }
725
- /****************************************************************************
726
- * HISTOGRAM
727
- */
728
- /**
729
- * Returns a list of labeled datapoints for 'bins' of data defined as ticks
730
- * over a numerical range. Suitable for histogram charts.
731
- */
732
- getHistogram(options) {
733
- try {
734
- const _this4 = this;
735
- const {
736
- filterOwner,
737
- spatialFilter,
738
- abortController,
739
- ...params
740
- } = options;
741
- const {
742
- column,
743
- operation,
744
- ticks
745
- } = params;
746
- return Promise.resolve(executeModel({
747
- model: 'histogram',
748
- source: {
749
- ..._this4.getModelSource(filterOwner),
750
- spatialFilter
751
- },
752
- params: {
753
- column,
754
- operation,
755
- ticks
756
- },
757
- opts: {
758
- abortController
759
- }
760
- }).then(res => normalizeObjectKeys(res.rows))).then(function (data) {
761
- if (data.length) {
762
- // Given N ticks the API returns up to N+1 bins, omitting any empty bins. Bins
763
- // include 1 bin below the lowest tick, N-1 between ticks, and 1 bin above the highest tick.
764
- const result = Array(ticks.length + 1).fill(0);
765
- data.forEach(_ref2 => {
766
- let {
767
- tick,
768
- value
769
- } = _ref2;
770
- return result[tick] = value;
639
+ const baseUrl = buildSourceUrl(mergedOptions);
640
+ const {
641
+ clientId,
642
+ maxLengthURL,
643
+ format,
644
+ localCache
645
+ } = mergedOptions;
646
+ const headers = {
647
+ Authorization: `Bearer ${options.accessToken}`,
648
+ ...options.headers
649
+ };
650
+ const parameters = {
651
+ client: clientId,
652
+ ...urlParameters
653
+ };
654
+ const errorContext = {
655
+ requestType: 'Map instantiation',
656
+ connection: options.connectionName,
657
+ type: endpoint,
658
+ source: JSON.stringify(parameters, undefined, 2)
659
+ };
660
+ return Promise.resolve(requestWithParameters({
661
+ baseUrl,
662
+ parameters,
663
+ headers,
664
+ errorContext,
665
+ maxLengthURL,
666
+ localCache
667
+ })).then(function (mapInstantiation) {
668
+ let _exit;
669
+ function _temp2(_result) {
670
+ return _exit ? _result : Promise.resolve(requestWithParameters({
671
+ baseUrl: dataUrl,
672
+ headers,
673
+ errorContext,
674
+ maxLengthURL,
675
+ localCache
676
+ }));
677
+ }
678
+ const dataUrl = mapInstantiation[format].url[0];
679
+ if (cache) {
680
+ cache.value = parseInt(new URL(dataUrl).searchParams.get('cache') || '', 10);
681
+ }
682
+ errorContext.requestType = 'Map data';
683
+ const _temp = function () {
684
+ if (format === 'tilejson') {
685
+ return Promise.resolve(requestWithParameters({
686
+ baseUrl: dataUrl,
687
+ headers,
688
+ errorContext,
689
+ maxLengthURL,
690
+ localCache
691
+ })).then(function (json) {
692
+ if (accessToken) {
693
+ json.accessToken = accessToken;
694
+ }
695
+ _exit = 1;
696
+ return json;
771
697
  });
772
- return result;
773
698
  }
774
- return [];
775
- });
776
- } catch (e) {
777
- return Promise.reject(e);
699
+ }();
700
+ return _temp && _temp.then ? _temp.then(_temp2) : _temp2(_temp);
701
+ });
702
+ } catch (e) {
703
+ return Promise.reject(e);
704
+ }
705
+ };
706
+ const SOURCE_DEFAULTS = {
707
+ apiBaseUrl: DEFAULT_API_BASE_URL,
708
+ clientId: getClient(),
709
+ format: 'tilejson',
710
+ headers: {},
711
+ maxLengthURL: DEFAULT_MAX_LENGTH_URL
712
+ };
713
+
714
+ // deck.gl
715
+ const boundaryQuerySource = function (options) {
716
+ try {
717
+ const {
718
+ columns,
719
+ filters,
720
+ tilesetTableName,
721
+ propertiesSqlQuery,
722
+ queryParameters
723
+ } = options;
724
+ const urlParameters = {
725
+ tilesetTableName,
726
+ propertiesSqlQuery
727
+ };
728
+ if (columns) {
729
+ urlParameters.columns = columns.join(',');
730
+ }
731
+ if (filters) {
732
+ urlParameters.filters = filters;
733
+ }
734
+ if (queryParameters) {
735
+ urlParameters.queryParameters = queryParameters;
778
736
  }
737
+ return Promise.resolve(baseSource('boundary', options, urlParameters));
738
+ } catch (e) {
739
+ return Promise.reject(e);
779
740
  }
780
- /****************************************************************************
781
- * RANGE
782
- */
783
- /**
784
- * Returns a range (min and max) for a numerical column of matching rows.
785
- * Suitable for displaying certain 'headline' or 'scorecard' statistics,
786
- * or rendering a range slider UI for filtering.
787
- */
788
- getRange(options) {
789
- try {
790
- const _this5 = this;
791
- const {
792
- filterOwner,
793
- spatialFilter,
794
- abortController,
795
- ...params
796
- } = options;
797
- const {
798
- column
799
- } = params;
800
- return Promise.resolve(executeModel({
801
- model: 'range',
802
- source: {
803
- ..._this5.getModelSource(filterOwner),
804
- spatialFilter
805
- },
806
- params: {
807
- column
808
- },
809
- opts: {
810
- abortController
811
- }
812
- }).then(res => normalizeObjectKeys(res.rows[0])));
813
- } catch (e) {
814
- return Promise.reject(e);
741
+ };
742
+
743
+ // deck.gl
744
+ const boundaryTableSource = function (options) {
745
+ try {
746
+ const {
747
+ filters,
748
+ tilesetTableName,
749
+ columns,
750
+ propertiesTableName
751
+ } = options;
752
+ const urlParameters = {
753
+ tilesetTableName,
754
+ propertiesTableName
755
+ };
756
+ if (columns) {
757
+ urlParameters.columns = columns.join(',');
758
+ }
759
+ if (filters) {
760
+ urlParameters.filters = filters;
815
761
  }
762
+ return Promise.resolve(baseSource('boundary', options, urlParameters));
763
+ } catch (e) {
764
+ return Promise.reject(e);
816
765
  }
817
- /****************************************************************************
818
- * SCATTER
819
- */
820
- /**
821
- * Returns a list of bivariate datapoints defined as numerical 'x' and 'y'
822
- * values. Suitable for rendering scatter plots.
823
- */
824
- getScatter(options) {
825
- try {
826
- const _this6 = this;
766
+ };
767
+
768
+ /**
769
+ * Return more descriptive error from API
770
+ * @internalRemarks Source: @carto/react-api
771
+ */
772
+
773
+ /** @internalRemarks Source: @carto/react-api */
774
+
775
+ function _catch(body, recover) {
776
+ try {
777
+ var result = body();
778
+ } catch (e) {
779
+ return recover(e);
780
+ }
781
+ if (result && result.then) {
782
+ return result.then(void 0, recover);
783
+ }
784
+ return result;
785
+ }
786
+ const makeCall = function (_ref2) {
787
+ let {
788
+ url,
789
+ accessToken,
790
+ opts
791
+ } = _ref2;
792
+ try {
793
+ let _exit;
794
+ function _temp2(_result) {
795
+ if (_exit) ;
796
+ if (!response.ok) {
797
+ dealWithApiError({
798
+ response,
799
+ data
800
+ });
801
+ }
802
+ return data;
803
+ }
804
+ let response;
805
+ let data;
806
+ const isPost = opts?.method === 'POST';
807
+ const _temp = _catch(function () {
808
+ return Promise.resolve(fetch(url.toString(), {
809
+ headers: {
810
+ Authorization: `Bearer ${accessToken}`,
811
+ ...(isPost && {
812
+ 'Content-Type': 'application/json'
813
+ })
814
+ },
815
+ ...(isPost && {
816
+ method: opts?.method,
817
+ body: opts?.body
818
+ }),
819
+ signal: opts?.abortController?.signal,
820
+ ...opts?.otherOptions
821
+ })).then(function (_fetch) {
822
+ response = _fetch;
823
+ return Promise.resolve(response.json()).then(function (_response$json) {
824
+ data = _response$json;
825
+ });
826
+ });
827
+ }, function (error) {
828
+ if (error.name === 'AbortError') throw error;
829
+ throw new Error(`Failed request: ${error}`);
830
+ });
831
+ return Promise.resolve(_temp && _temp.then ? _temp.then(_temp2) : _temp2(_temp));
832
+ } catch (e) {
833
+ return Promise.reject(e);
834
+ }
835
+ };
836
+ function dealWithApiError(_ref) {
837
+ let {
838
+ response,
839
+ data
840
+ } = _ref;
841
+ if (data.error === 'Column not found') {
842
+ throw new InvalidColumnError(`${data.error} ${data.column_name}`);
843
+ }
844
+ if (typeof data.error === 'string' && data.error?.includes('Missing columns')) {
845
+ throw new InvalidColumnError(data.error);
846
+ }
847
+ switch (response.status) {
848
+ case 401:
849
+ throw new Error('Unauthorized access. Invalid credentials');
850
+ case 403:
851
+ throw new Error('Forbidden access to the requested data');
852
+ default:
853
+ const msg = data && data.error && typeof data.error === 'string' ? data.error : JSON.stringify(data?.hint || data.error?.[0]);
854
+ throw new Error(msg);
855
+ }
856
+ }
857
+
858
+ /** @internalRemarks Source: @carto/react-api */
859
+ const AVAILABLE_MODELS = ['category', 'histogram', 'formula', 'pick', 'timeseries', 'range', 'scatterplot', 'table'];
860
+ const {
861
+ V3
862
+ } = exports.ApiVersion;
863
+ const REQUEST_GET_MAX_URL_LENGTH = 2048;
864
+ /**
865
+ * Execute a SQL model request.
866
+ * @internalRemarks Source: @carto/react-api
867
+ */
868
+ function executeModel(props) {
869
+ assert(props.source, 'executeModel: missing source');
870
+ assert(props.model, 'executeModel: missing model');
871
+ assert(props.params, 'executeModel: missing params');
872
+ assert(AVAILABLE_MODELS.includes(props.model), `executeModel: model provided isn't valid. Available models: ${AVAILABLE_MODELS.join(', ')}`);
873
+ const {
874
+ model,
875
+ source,
876
+ params,
877
+ opts
878
+ } = props;
879
+ const {
880
+ type,
881
+ apiVersion,
882
+ apiBaseUrl,
883
+ accessToken,
884
+ connectionName,
885
+ clientId
886
+ } = source;
887
+ assert(apiBaseUrl, 'executeModel: missing apiBaseUrl');
888
+ assert(accessToken, 'executeModel: missing accessToken');
889
+ assert(apiVersion === V3, 'executeModel: SQL Model API requires CARTO 3+');
890
+ assert(type !== 'tileset', 'executeModel: Tilesets not supported');
891
+ let url = `${apiBaseUrl}/v3/sql/${connectionName}/model/${model}`;
892
+ const {
893
+ data,
894
+ filters,
895
+ filtersLogicalOperator = 'and',
896
+ geoColumn = DEFAULT_GEO_COLUMN
897
+ } = source;
898
+ const queryParameters = source.queryParameters ? JSON.stringify(source.queryParameters) : '';
899
+ const queryParams = {
900
+ type,
901
+ client: clientId,
902
+ source: data,
903
+ params: JSON.stringify(params),
904
+ queryParameters,
905
+ filters: JSON.stringify(filters),
906
+ filtersLogicalOperator
907
+ };
908
+ // Picking Model API requires 'spatialDataColumn'.
909
+ if (model === 'pick') {
910
+ queryParams.spatialDataColumn = geoColumn;
911
+ }
912
+ // API supports multiple filters, we apply it only to geoColumn
913
+ const spatialFilters = source.spatialFilter ? {
914
+ [geoColumn]: source.spatialFilter
915
+ } : undefined;
916
+ if (spatialFilters) {
917
+ queryParams.spatialFilters = JSON.stringify(spatialFilters);
918
+ }
919
+ const urlWithSearchParams = url + '?' + new URLSearchParams(queryParams).toString();
920
+ const isGet = urlWithSearchParams.length <= REQUEST_GET_MAX_URL_LENGTH;
921
+ if (isGet) {
922
+ url = urlWithSearchParams;
923
+ } else {
924
+ // undo the JSON.stringify, @TODO find a better pattern
925
+ queryParams.params = params;
926
+ queryParams.filters = filters;
927
+ queryParams.queryParameters = source.queryParameters;
928
+ if (spatialFilters) {
929
+ queryParams.spatialFilters = spatialFilters;
930
+ }
931
+ }
932
+ return makeCall({
933
+ url,
934
+ accessToken: source.accessToken,
935
+ opts: {
936
+ ...opts,
937
+ method: isGet ? 'GET' : 'POST',
938
+ ...(!isGet && {
939
+ body: JSON.stringify(queryParams)
940
+ })
941
+ }
942
+ });
943
+ }
944
+
945
+ /**
946
+ * Source for Widget API requests on a data source defined by a SQL query.
947
+ *
948
+ * Abstract class. Use {@link WidgetQuerySource} or {@link WidgetTableSource}.
949
+ */
950
+ class WidgetBaseSource {
951
+ constructor(props) {
952
+ this.props = void 0;
953
+ this.props = {
954
+ ...WidgetBaseSource.defaultProps,
955
+ ...props
956
+ };
957
+ }
958
+ _getModelSource(owner) {
959
+ const props = this.props;
960
+ return {
961
+ apiVersion: props.apiVersion,
962
+ apiBaseUrl: props.apiBaseUrl,
963
+ clientId: props.clientId,
964
+ accessToken: props.accessToken,
965
+ connectionName: props.connectionName,
966
+ filters: getApplicableFilters(owner, props.filters),
967
+ filtersLogicalOperator: props.filtersLogicalOperator,
968
+ geoColumn: props.geoColumn
969
+ };
970
+ }
971
+ /****************************************************************************
972
+ * CATEGORIES
973
+ */
974
+ /**
975
+ * Returns a list of labeled datapoints for categorical data. Suitable for
976
+ * charts including grouped bar charts, pie charts, and tree charts.
977
+ */
978
+ getCategories(options) {
979
+ try {
980
+ const _this = this;
827
981
  const {
828
982
  filterOwner,
829
983
  spatialFilter,
@@ -831,50 +985,43 @@ class WidgetBaseSource {
831
985
  ...params
832
986
  } = options;
833
987
  const {
834
- xAxisColumn,
835
- xAxisJoinOperation,
836
- yAxisColumn,
837
- yAxisJoinOperation
988
+ column,
989
+ operation,
990
+ operationColumn
838
991
  } = params;
839
- // Make sure this is sync with the same constant in cloud-native/maps-api
840
- const HARD_LIMIT = 500;
841
992
  return Promise.resolve(executeModel({
842
- model: 'scatterplot',
993
+ model: 'category',
843
994
  source: {
844
- ..._this6.getModelSource(filterOwner),
995
+ ..._this.getModelSource(filterOwner),
845
996
  spatialFilter
846
997
  },
847
998
  params: {
848
- xAxisColumn,
849
- xAxisJoinOperation,
850
- yAxisColumn,
851
- yAxisJoinOperation,
852
- limit: HARD_LIMIT
999
+ column,
1000
+ operation,
1001
+ operationColumn: operationColumn || column
853
1002
  },
854
1003
  opts: {
855
1004
  abortController
856
1005
  }
857
- }).then(res => normalizeObjectKeys(res.rows)).then(res => res.map(_ref3 => {
858
- let {
859
- x,
860
- y
861
- } = _ref3;
862
- return [x, y];
863
- })));
1006
+ }).then(res => normalizeObjectKeys(res.rows)));
864
1007
  } catch (e) {
865
1008
  return Promise.reject(e);
866
1009
  }
867
1010
  }
868
1011
  /****************************************************************************
869
- * TABLE
1012
+ * FEATURES
870
1013
  */
871
1014
  /**
872
- * Returns a list of arbitrary data rows, with support for pagination and
873
- * sorting. Suitable for displaying tables and lists.
1015
+ * Given a list of feature IDs (as found in `_carto_feature_id`) returns all
1016
+ * matching features. In datasets containing features with duplicate geometries,
1017
+ * feature IDs may be duplicated (IDs are a hash of geometry) and so more
1018
+ * results may be returned than IDs in the request.
1019
+ * @internal
1020
+ * @experimental
874
1021
  */
875
- getTable(options) {
1022
+ getFeatures(options) {
876
1023
  try {
877
- const _this7 = this;
1024
+ const _this2 = this;
878
1025
  const {
879
1026
  filterOwner,
880
1027
  spatialFilter,
@@ -883,549 +1030,402 @@ class WidgetBaseSource {
883
1030
  } = options;
884
1031
  const {
885
1032
  columns,
886
- sortBy,
887
- sortDirection,
888
- offset = 0,
889
- limit = 10
1033
+ dataType,
1034
+ featureIds,
1035
+ z,
1036
+ limit,
1037
+ tileResolution
890
1038
  } = params;
891
1039
  return Promise.resolve(executeModel({
892
- model: 'table',
1040
+ model: 'pick',
893
1041
  source: {
894
- ..._this7.getModelSource(filterOwner),
1042
+ ..._this2.getModelSource(filterOwner),
895
1043
  spatialFilter
896
1044
  },
897
1045
  params: {
898
- column: columns,
899
- sortBy,
900
- sortDirection,
901
- limit,
902
- offset
1046
+ columns,
1047
+ dataType,
1048
+ featureIds,
1049
+ z,
1050
+ limit: limit || 1000,
1051
+ tileResolution: tileResolution || DEFAULT_TILE_RESOLUTION
903
1052
  },
904
1053
  opts: {
905
1054
  abortController
906
1055
  }
907
- }).then(res => ({
908
1056
  // Avoid `normalizeObjectKeys()`, which changes column names.
909
- rows: res.rows ?? res.ROWS,
910
- totalCount: res.metadata?.total ?? res.METADATA?.TOTAL
911
- })));
1057
+ }).then(_ref => {
1058
+ let {
1059
+ rows
1060
+ } = _ref;
1061
+ return {
1062
+ rows
1063
+ };
1064
+ }));
912
1065
  } catch (e) {
913
1066
  return Promise.reject(e);
914
1067
  }
915
1068
  }
916
1069
  /****************************************************************************
917
- * TIME SERIES
1070
+ * FORMULA
918
1071
  */
919
1072
  /**
920
- * Returns a series of labeled numerical values, grouped into equally-sized
921
- * time intervals. Suitable for rendering time series charts.
1073
+ * Returns a scalar numerical statistic over all matching data. Suitable
1074
+ * for 'headline' or 'scorecard' figures such as counts and sums.
922
1075
  */
923
- getTimeSeries(options) {
1076
+ getFormula(options) {
924
1077
  try {
925
- const _this8 = this;
1078
+ const _this3 = this;
926
1079
  const {
927
1080
  filterOwner,
1081
+ spatialFilter,
928
1082
  abortController,
1083
+ operationExp,
1084
+ ...params
1085
+ } = options;
1086
+ const {
1087
+ column,
1088
+ operation
1089
+ } = params;
1090
+ return Promise.resolve(executeModel({
1091
+ model: 'formula',
1092
+ source: {
1093
+ ..._this3.getModelSource(filterOwner),
1094
+ spatialFilter
1095
+ },
1096
+ params: {
1097
+ column: column ?? '*',
1098
+ operation,
1099
+ operationExp
1100
+ },
1101
+ opts: {
1102
+ abortController
1103
+ }
1104
+ }).then(res => normalizeObjectKeys(res.rows[0])));
1105
+ } catch (e) {
1106
+ return Promise.reject(e);
1107
+ }
1108
+ }
1109
+ /****************************************************************************
1110
+ * HISTOGRAM
1111
+ */
1112
+ /**
1113
+ * Returns a list of labeled datapoints for 'bins' of data defined as ticks
1114
+ * over a numerical range. Suitable for histogram charts.
1115
+ */
1116
+ getHistogram(options) {
1117
+ try {
1118
+ const _this4 = this;
1119
+ const {
1120
+ filterOwner,
929
1121
  spatialFilter,
1122
+ abortController,
930
1123
  ...params
931
1124
  } = options;
932
1125
  const {
933
1126
  column,
934
- operationColumn,
935
- joinOperation,
936
1127
  operation,
937
- stepSize,
938
- stepMultiplier,
939
- splitByCategory,
940
- splitByCategoryLimit,
941
- splitByCategoryValues
1128
+ ticks
942
1129
  } = params;
943
1130
  return Promise.resolve(executeModel({
944
- model: 'timeseries',
1131
+ model: 'histogram',
945
1132
  source: {
946
- ..._this8.getModelSource(filterOwner),
1133
+ ..._this4.getModelSource(filterOwner),
947
1134
  spatialFilter
948
1135
  },
949
1136
  params: {
950
1137
  column,
951
- stepSize,
952
- stepMultiplier,
953
- operationColumn: operationColumn || column,
954
- joinOperation,
955
1138
  operation,
956
- splitByCategory,
957
- splitByCategoryLimit,
958
- splitByCategoryValues
1139
+ ticks
959
1140
  },
960
1141
  opts: {
961
1142
  abortController
962
1143
  }
963
- }).then(res => ({
964
- rows: normalizeObjectKeys(res.rows),
965
- categories: res.metadata?.categories
966
- })));
1144
+ }).then(res => normalizeObjectKeys(res.rows))).then(function (data) {
1145
+ if (data.length) {
1146
+ // Given N ticks the API returns up to N+1 bins, omitting any empty bins. Bins
1147
+ // include 1 bin below the lowest tick, N-1 between ticks, and 1 bin above the highest tick.
1148
+ const result = Array(ticks.length + 1).fill(0);
1149
+ data.forEach(_ref2 => {
1150
+ let {
1151
+ tick,
1152
+ value
1153
+ } = _ref2;
1154
+ return result[tick] = value;
1155
+ });
1156
+ return result;
1157
+ }
1158
+ return [];
1159
+ });
967
1160
  } catch (e) {
968
1161
  return Promise.reject(e);
969
1162
  }
970
1163
  }
971
- }
972
- WidgetBaseSource.defaultProps = {
973
- apiVersion: exports.ApiVersion.V3,
974
- apiBaseUrl: DEFAULT_API_BASE_URL,
975
- clientId: getClient(),
976
- filters: {},
977
- filtersLogicalOperator: 'and',
978
- geoColumn: DEFAULT_GEO_COLUMN
979
- };
980
-
981
- /**
982
- * Source for Widget API requests on a data source defined by a SQL query.
983
- *
984
- * Generally not intended to be constructed directly. Instead, call
985
- * {@link vectorQuerySource}, {@link h3QuerySource}, or {@link quadbinQuerySource},
986
- * which can be shared with map layers. Sources contain a `widgetSource` property,
987
- * for use by widget implementations.
988
- *
989
- * Example:
990
- *
991
- * ```javascript
992
- * import { vectorQuerySource } from '@carto/api-client';
993
- *
994
- * const data = vectorQuerySource({
995
- * accessToken: '••••',
996
- * connectionName: 'carto_dw',
997
- * sqlQuery: 'SELECT * FROM carto-demo-data.demo_tables.retail_stores'
998
- * });
999
- *
1000
- * const { widgetSource } = await data;
1001
- * ```
1002
- */
1003
- class WidgetQuerySource extends WidgetBaseSource {
1004
- getModelSource(owner) {
1005
- return {
1006
- ...super._getModelSource(owner),
1007
- type: 'query',
1008
- data: this.props.sqlQuery,
1009
- queryParameters: this.props.queryParameters
1010
- };
1011
- }
1012
- }
1013
-
1014
- /**
1015
- * Source for Widget API requests on a data source defined as a table.
1016
- *
1017
- * Generally not intended to be constructed directly. Instead, call
1018
- * {@link vectorTableSource}, {@link h3TableSource}, or {@link quadbinTableSource},
1019
- * which can be shared with map layers. Sources contain a `widgetSource` property,
1020
- * for use by widget implementations.
1021
- *
1022
- * Example:
1023
- *
1024
- * ```javascript
1025
- * import { vectorTableSource } from '@carto/api-client';
1026
- *
1027
- * const data = vectorTableSource({
1028
- * accessToken: '••••',
1029
- * connectionName: 'carto_dw',
1030
- * tableName: 'carto-demo-data.demo_tables.retail_stores'
1031
- * });
1032
- *
1033
- * const { widgetSource } = await data;
1034
- * ```
1035
- */
1036
- class WidgetTableSource extends WidgetBaseSource {
1037
- getModelSource(owner) {
1038
- return {
1039
- ...super._getModelSource(owner),
1040
- type: 'table',
1041
- data: this.props.tableName
1042
- };
1164
+ /****************************************************************************
1165
+ * RANGE
1166
+ */
1167
+ /**
1168
+ * Returns a range (min and max) for a numerical column of matching rows.
1169
+ * Suitable for displaying certain 'headline' or 'scorecard' statistics,
1170
+ * or rendering a range slider UI for filtering.
1171
+ */
1172
+ getRange(options) {
1173
+ try {
1174
+ const _this5 = this;
1175
+ const {
1176
+ filterOwner,
1177
+ spatialFilter,
1178
+ abortController,
1179
+ ...params
1180
+ } = options;
1181
+ const {
1182
+ column
1183
+ } = params;
1184
+ return Promise.resolve(executeModel({
1185
+ model: 'range',
1186
+ source: {
1187
+ ..._this5.getModelSource(filterOwner),
1188
+ spatialFilter
1189
+ },
1190
+ params: {
1191
+ column
1192
+ },
1193
+ opts: {
1194
+ abortController
1195
+ }
1196
+ }).then(res => normalizeObjectKeys(res.rows[0])));
1197
+ } catch (e) {
1198
+ return Promise.reject(e);
1199
+ }
1043
1200
  }
1044
- }
1045
-
1046
- // deck.gl
1047
- // SPDX-License-Identifier: MIT
1048
- // Copyright (c) vis.gl contributors
1049
- /**
1050
- *
1051
- * Custom error for reported errors in CARTO Maps API.
1052
- * Provides useful debugging information in console and context for applications.
1053
- *
1054
- */
1055
- class CartoAPIError extends Error {
1056
- constructor(error, errorContext, response, responseJson) {
1057
- let responseString = 'Failed to connect';
1058
- if (response) {
1059
- responseString = 'Server returned: ';
1060
- if (response.status === 400) {
1061
- responseString += 'Bad request';
1062
- } else if (response.status === 401 || response.status === 403) {
1063
- responseString += 'Unauthorized access';
1064
- } else if (response.status === 404) {
1065
- responseString += 'Not found';
1066
- } else {
1067
- responseString += 'Error';
1068
- }
1069
- responseString += ` (${response.status}):`;
1070
- }
1071
- responseString += ` ${error.message || error}`;
1072
- let message = `${errorContext.requestType} API request failed`;
1073
- message += `\n${responseString}`;
1074
- for (const key of Object.keys(errorContext)) {
1075
- if (key === 'requestType') continue;
1076
- message += `\n${formatErrorKey(key)}: ${errorContext[key]}`;
1077
- }
1078
- message += '\n';
1079
- super(message);
1080
- /** Source error from server */
1081
- this.error = void 0;
1082
- /** Context (API call & parameters) in which error occured */
1083
- this.errorContext = void 0;
1084
- /** Response from server */
1085
- this.response = void 0;
1086
- /** JSON Response from server */
1087
- this.responseJson = void 0;
1088
- this.name = 'CartoAPIError';
1089
- this.response = response;
1090
- this.responseJson = responseJson;
1091
- this.error = error;
1092
- this.errorContext = errorContext;
1093
- }
1094
- }
1095
- /**
1096
- * Converts camelCase to Camel Case
1097
- */
1098
- function formatErrorKey(key) {
1099
- return key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase());
1100
- }
1101
-
1102
- // deck.gl
1103
- // SPDX-License-Identifier: MIT
1104
- // Copyright (c) vis.gl contributors
1105
- function joinPath() {
1106
- return [].slice.call(arguments).map(part => part.endsWith('/') ? part.slice(0, -1) : part).join('/');
1107
- }
1108
- function buildV3Path(apiBaseUrl, version, endpoint) {
1109
- return joinPath(apiBaseUrl, version, endpoint, ...[].slice.call(arguments, 3));
1110
- }
1111
- /** @internal Required by fetchMap(). */
1112
- function buildPublicMapUrl(_ref) {
1113
- let {
1114
- apiBaseUrl,
1115
- cartoMapId
1116
- } = _ref;
1117
- return buildV3Path(apiBaseUrl, 'v3', 'maps', 'public', cartoMapId);
1118
- }
1119
- /** @internal Required by fetchMap(). */
1120
- function buildStatsUrl(_ref2) {
1121
- let {
1122
- attribute,
1123
- apiBaseUrl,
1124
- connectionName,
1125
- source,
1126
- type
1127
- } = _ref2;
1128
- if (type === 'query') {
1129
- return buildV3Path(apiBaseUrl, 'v3', 'stats', connectionName, attribute);
1130
- }
1131
- // type === 'table'
1132
- return buildV3Path(apiBaseUrl, 'v3', 'stats', connectionName, source, attribute);
1133
- }
1134
- function buildSourceUrl(_ref3) {
1135
- let {
1136
- apiBaseUrl,
1137
- connectionName,
1138
- endpoint
1139
- } = _ref3;
1140
- return buildV3Path(apiBaseUrl, 'v3', 'maps', connectionName, endpoint);
1141
- }
1142
- function buildQueryUrl(_ref4) {
1143
- let {
1144
- apiBaseUrl,
1145
- connectionName
1146
- } = _ref4;
1147
- return buildV3Path(apiBaseUrl, 'v3', 'sql', connectionName, 'query');
1148
- }
1149
-
1150
- // deck.gl
1151
- const requestWithParameters = function (_ref) {
1152
- let {
1153
- baseUrl,
1154
- parameters = {},
1155
- headers: customHeaders = {},
1156
- errorContext,
1157
- maxLengthURL = DEFAULT_MAX_LENGTH_URL,
1158
- localCache
1159
- } = _ref;
1160
- try {
1161
- // Parameters added to all requests issued with `requestWithParameters()`.
1162
- // These parameters override parameters already in the base URL, but not
1163
- // user-provided parameters.
1164
- parameters = {
1165
- v: V3_MINOR_VERSION,
1166
- client: getClient(),
1167
- ...(typeof deck !== 'undefined' && deck.VERSION && {
1168
- deckglVersion: deck.VERSION
1169
- }),
1170
- ...parameters
1171
- };
1172
- baseUrl = excludeURLParameters(baseUrl, Object.keys(parameters));
1173
- const key = createCacheKey(baseUrl, parameters, customHeaders);
1174
- const {
1175
- cache: REQUEST_CACHE,
1176
- canReadCache,
1177
- canStoreInCache
1178
- } = getCacheSettings(localCache);
1179
- if (canReadCache && REQUEST_CACHE.has(key)) {
1180
- return Promise.resolve(REQUEST_CACHE.get(key));
1181
- }
1182
- const url = createURLWithParameters(baseUrl, parameters);
1183
- const headers = {
1184
- ...DEFAULT_HEADERS,
1185
- ...customHeaders
1186
- };
1187
- /* global fetch */
1188
- const fetchPromise = url.length > maxLengthURL ? fetch(baseUrl, {
1189
- method: 'POST',
1190
- body: JSON.stringify(parameters),
1191
- headers
1192
- }) : fetch(url, {
1193
- headers
1194
- });
1195
- let response;
1196
- let responseJson;
1197
- const jsonPromise = fetchPromise.then(_response => {
1198
- response = _response;
1199
- return response.json();
1200
- }).then(json => {
1201
- responseJson = json;
1202
- if (!response || !response.ok) {
1203
- throw new Error(json.error);
1204
- }
1205
- return json;
1206
- }).catch(error => {
1207
- if (canStoreInCache) {
1208
- REQUEST_CACHE.delete(key);
1209
- }
1210
- throw new CartoAPIError(error, errorContext, response, responseJson);
1211
- });
1212
- if (canStoreInCache) {
1213
- REQUEST_CACHE.set(key, jsonPromise);
1214
- }
1215
- return Promise.resolve(jsonPromise);
1216
- } catch (e) {
1217
- return Promise.reject(e);
1218
- }
1219
- };
1220
- const DEFAULT_HEADERS = {
1221
- Accept: 'application/json',
1222
- 'Content-Type': 'application/json'
1223
- };
1224
- const DEFAULT_REQUEST_CACHE = new Map();
1225
- function getCacheSettings(localCache) {
1226
- const canReadCache = localCache?.cacheControl?.includes('no-cache') ? false : true;
1227
- const canStoreInCache = localCache?.cacheControl?.includes('no-store') ? false : true;
1228
- const cache = localCache?.cache || DEFAULT_REQUEST_CACHE;
1229
- return {
1230
- cache,
1231
- canReadCache,
1232
- canStoreInCache
1233
- };
1234
- }
1235
- function createCacheKey(baseUrl, parameters, headers) {
1236
- const parameterEntries = Object.entries(parameters).sort((_ref2, _ref3) => {
1237
- let [a] = _ref2;
1238
- let [b] = _ref3;
1239
- return a > b ? 1 : -1;
1240
- });
1241
- const headerEntries = Object.entries(headers).sort((_ref4, _ref5) => {
1242
- let [a] = _ref4;
1243
- let [b] = _ref5;
1244
- return a > b ? 1 : -1;
1245
- });
1246
- return JSON.stringify({
1247
- baseUrl,
1248
- parameters: parameterEntries,
1249
- headers: headerEntries
1250
- });
1251
- }
1252
- /**
1253
- * Appends query string parameters to a URL. Existing URL parameters are kept,
1254
- * unless there is a conflict, in which case the new parameters override
1255
- * those already in the URL.
1256
- */
1257
- function createURLWithParameters(baseUrlString, parameters) {
1258
- const baseUrl = new URL(baseUrlString);
1259
- for (const [key, value] of Object.entries(parameters)) {
1260
- if (isPureObject(value) || Array.isArray(value)) {
1261
- baseUrl.searchParams.set(key, JSON.stringify(value));
1262
- } else {
1263
- baseUrl.searchParams.set(key, value.toString());
1264
- }
1265
- }
1266
- return baseUrl.toString();
1267
- }
1268
- /**
1269
- * Deletes query string parameters from a URL.
1270
- */
1271
- function excludeURLParameters(baseUrlString, parameters) {
1272
- const baseUrl = new URL(baseUrlString);
1273
- for (const param of parameters) {
1274
- if (baseUrl.searchParams.has(param)) {
1275
- baseUrl.searchParams.delete(param);
1201
+ /****************************************************************************
1202
+ * SCATTER
1203
+ */
1204
+ /**
1205
+ * Returns a list of bivariate datapoints defined as numerical 'x' and 'y'
1206
+ * values. Suitable for rendering scatter plots.
1207
+ */
1208
+ getScatter(options) {
1209
+ try {
1210
+ const _this6 = this;
1211
+ const {
1212
+ filterOwner,
1213
+ spatialFilter,
1214
+ abortController,
1215
+ ...params
1216
+ } = options;
1217
+ const {
1218
+ xAxisColumn,
1219
+ xAxisJoinOperation,
1220
+ yAxisColumn,
1221
+ yAxisJoinOperation
1222
+ } = params;
1223
+ // Make sure this is sync with the same constant in cloud-native/maps-api
1224
+ const HARD_LIMIT = 500;
1225
+ return Promise.resolve(executeModel({
1226
+ model: 'scatterplot',
1227
+ source: {
1228
+ ..._this6.getModelSource(filterOwner),
1229
+ spatialFilter
1230
+ },
1231
+ params: {
1232
+ xAxisColumn,
1233
+ xAxisJoinOperation,
1234
+ yAxisColumn,
1235
+ yAxisJoinOperation,
1236
+ limit: HARD_LIMIT
1237
+ },
1238
+ opts: {
1239
+ abortController
1240
+ }
1241
+ }).then(res => normalizeObjectKeys(res.rows)).then(res => res.map(_ref3 => {
1242
+ let {
1243
+ x,
1244
+ y
1245
+ } = _ref3;
1246
+ return [x, y];
1247
+ })));
1248
+ } catch (e) {
1249
+ return Promise.reject(e);
1276
1250
  }
1277
1251
  }
1278
- return baseUrl.toString();
1279
- }
1280
-
1281
- // deck.gl
1282
- const baseSource = function (endpoint, options, urlParameters) {
1283
- try {
1284
- const {
1285
- accessToken,
1286
- connectionName,
1287
- cache,
1288
- ...optionalOptions
1289
- } = options;
1290
- const mergedOptions = {
1291
- ...SOURCE_DEFAULTS,
1292
- accessToken,
1293
- connectionName,
1294
- endpoint
1295
- };
1296
- for (const key in optionalOptions) {
1297
- if (optionalOptions[key]) {
1298
- mergedOptions[key] = optionalOptions[key];
1299
- }
1252
+ /****************************************************************************
1253
+ * TABLE
1254
+ */
1255
+ /**
1256
+ * Returns a list of arbitrary data rows, with support for pagination and
1257
+ * sorting. Suitable for displaying tables and lists.
1258
+ */
1259
+ getTable(options) {
1260
+ try {
1261
+ const _this7 = this;
1262
+ const {
1263
+ filterOwner,
1264
+ spatialFilter,
1265
+ abortController,
1266
+ ...params
1267
+ } = options;
1268
+ const {
1269
+ columns,
1270
+ sortBy,
1271
+ sortDirection,
1272
+ offset = 0,
1273
+ limit = 10
1274
+ } = params;
1275
+ return Promise.resolve(executeModel({
1276
+ model: 'table',
1277
+ source: {
1278
+ ..._this7.getModelSource(filterOwner),
1279
+ spatialFilter
1280
+ },
1281
+ params: {
1282
+ column: columns,
1283
+ sortBy,
1284
+ sortDirection,
1285
+ limit,
1286
+ offset
1287
+ },
1288
+ opts: {
1289
+ abortController
1290
+ }
1291
+ }).then(res => ({
1292
+ // Avoid `normalizeObjectKeys()`, which changes column names.
1293
+ rows: res.rows ?? res.ROWS,
1294
+ totalCount: res.metadata?.total ?? res.METADATA?.TOTAL
1295
+ })));
1296
+ } catch (e) {
1297
+ return Promise.reject(e);
1300
1298
  }
1301
- const baseUrl = buildSourceUrl(mergedOptions);
1302
- const {
1303
- clientId,
1304
- maxLengthURL,
1305
- format,
1306
- localCache
1307
- } = mergedOptions;
1308
- const headers = {
1309
- Authorization: `Bearer ${options.accessToken}`,
1310
- ...options.headers
1311
- };
1312
- const parameters = {
1313
- client: clientId,
1314
- ...urlParameters
1315
- };
1316
- const errorContext = {
1317
- requestType: 'Map instantiation',
1318
- connection: options.connectionName,
1319
- type: endpoint,
1320
- source: JSON.stringify(parameters, undefined, 2)
1321
- };
1322
- return Promise.resolve(requestWithParameters({
1323
- baseUrl,
1324
- parameters,
1325
- headers,
1326
- errorContext,
1327
- maxLengthURL,
1328
- localCache
1329
- })).then(function (mapInstantiation) {
1330
- let _exit;
1331
- function _temp2(_result) {
1332
- return _exit ? _result : Promise.resolve(requestWithParameters({
1333
- baseUrl: dataUrl,
1334
- headers,
1335
- errorContext,
1336
- maxLengthURL,
1337
- localCache
1338
- }));
1339
- }
1340
- const dataUrl = mapInstantiation[format].url[0];
1341
- if (cache) {
1342
- cache.value = parseInt(new URL(dataUrl).searchParams.get('cache') || '', 10);
1343
- }
1344
- errorContext.requestType = 'Map data';
1345
- const _temp = function () {
1346
- if (format === 'tilejson') {
1347
- return Promise.resolve(requestWithParameters({
1348
- baseUrl: dataUrl,
1349
- headers,
1350
- errorContext,
1351
- maxLengthURL,
1352
- localCache
1353
- })).then(function (json) {
1354
- if (accessToken) {
1355
- json.accessToken = accessToken;
1356
- }
1357
- _exit = 1;
1358
- return json;
1359
- });
1299
+ }
1300
+ /****************************************************************************
1301
+ * TIME SERIES
1302
+ */
1303
+ /**
1304
+ * Returns a series of labeled numerical values, grouped into equally-sized
1305
+ * time intervals. Suitable for rendering time series charts.
1306
+ */
1307
+ getTimeSeries(options) {
1308
+ try {
1309
+ const _this8 = this;
1310
+ const {
1311
+ filterOwner,
1312
+ abortController,
1313
+ spatialFilter,
1314
+ ...params
1315
+ } = options;
1316
+ const {
1317
+ column,
1318
+ operationColumn,
1319
+ joinOperation,
1320
+ operation,
1321
+ stepSize,
1322
+ stepMultiplier,
1323
+ splitByCategory,
1324
+ splitByCategoryLimit,
1325
+ splitByCategoryValues
1326
+ } = params;
1327
+ return Promise.resolve(executeModel({
1328
+ model: 'timeseries',
1329
+ source: {
1330
+ ..._this8.getModelSource(filterOwner),
1331
+ spatialFilter
1332
+ },
1333
+ params: {
1334
+ column,
1335
+ stepSize,
1336
+ stepMultiplier,
1337
+ operationColumn: operationColumn || column,
1338
+ joinOperation,
1339
+ operation,
1340
+ splitByCategory,
1341
+ splitByCategoryLimit,
1342
+ splitByCategoryValues
1343
+ },
1344
+ opts: {
1345
+ abortController
1360
1346
  }
1361
- }();
1362
- return _temp && _temp.then ? _temp.then(_temp2) : _temp2(_temp);
1363
- });
1364
- } catch (e) {
1365
- return Promise.reject(e);
1347
+ }).then(res => ({
1348
+ rows: normalizeObjectKeys(res.rows),
1349
+ categories: res.metadata?.categories
1350
+ })));
1351
+ } catch (e) {
1352
+ return Promise.reject(e);
1353
+ }
1366
1354
  }
1367
- };
1368
- const SOURCE_DEFAULTS = {
1355
+ }
1356
+ WidgetBaseSource.defaultProps = {
1357
+ apiVersion: exports.ApiVersion.V3,
1369
1358
  apiBaseUrl: DEFAULT_API_BASE_URL,
1370
1359
  clientId: getClient(),
1371
- format: 'tilejson',
1372
- headers: {},
1373
- maxLengthURL: DEFAULT_MAX_LENGTH_URL
1360
+ filters: {},
1361
+ filtersLogicalOperator: 'and',
1362
+ geoColumn: DEFAULT_GEO_COLUMN
1374
1363
  };
1375
1364
 
1376
- // deck.gl
1377
- const boundaryQuerySource = function (options) {
1378
- try {
1379
- const {
1380
- columns,
1381
- filters,
1382
- tilesetTableName,
1383
- propertiesSqlQuery,
1384
- queryParameters
1385
- } = options;
1386
- const urlParameters = {
1387
- tilesetTableName,
1388
- propertiesSqlQuery
1365
+ /**
1366
+ * Source for Widget API requests on a data source defined by a SQL query.
1367
+ *
1368
+ * Generally not intended to be constructed directly. Instead, call
1369
+ * {@link vectorQuerySource}, {@link h3QuerySource}, or {@link quadbinQuerySource},
1370
+ * which can be shared with map layers. Sources contain a `widgetSource` property,
1371
+ * for use by widget implementations.
1372
+ *
1373
+ * Example:
1374
+ *
1375
+ * ```javascript
1376
+ * import { vectorQuerySource } from '@carto/api-client';
1377
+ *
1378
+ * const data = vectorQuerySource({
1379
+ * accessToken: '••••',
1380
+ * connectionName: 'carto_dw',
1381
+ * sqlQuery: 'SELECT * FROM carto-demo-data.demo_tables.retail_stores'
1382
+ * });
1383
+ *
1384
+ * const { widgetSource } = await data;
1385
+ * ```
1386
+ */
1387
+ class WidgetQuerySource extends WidgetBaseSource {
1388
+ getModelSource(owner) {
1389
+ return {
1390
+ ...super._getModelSource(owner),
1391
+ type: 'query',
1392
+ data: this.props.sqlQuery,
1393
+ queryParameters: this.props.queryParameters
1389
1394
  };
1390
- if (columns) {
1391
- urlParameters.columns = columns.join(',');
1392
- }
1393
- if (filters) {
1394
- urlParameters.filters = filters;
1395
- }
1396
- if (queryParameters) {
1397
- urlParameters.queryParameters = queryParameters;
1398
- }
1399
- return Promise.resolve(baseSource('boundary', options, urlParameters));
1400
- } catch (e) {
1401
- return Promise.reject(e);
1402
1395
  }
1403
- };
1396
+ }
1404
1397
 
1405
- // deck.gl
1406
- const boundaryTableSource = function (options) {
1407
- try {
1408
- const {
1409
- filters,
1410
- tilesetTableName,
1411
- columns,
1412
- propertiesTableName
1413
- } = options;
1414
- const urlParameters = {
1415
- tilesetTableName,
1416
- propertiesTableName
1398
+ /**
1399
+ * Source for Widget API requests on a data source defined as a table.
1400
+ *
1401
+ * Generally not intended to be constructed directly. Instead, call
1402
+ * {@link vectorTableSource}, {@link h3TableSource}, or {@link quadbinTableSource},
1403
+ * which can be shared with map layers. Sources contain a `widgetSource` property,
1404
+ * for use by widget implementations.
1405
+ *
1406
+ * Example:
1407
+ *
1408
+ * ```javascript
1409
+ * import { vectorTableSource } from '@carto/api-client';
1410
+ *
1411
+ * const data = vectorTableSource({
1412
+ * accessToken: '••••',
1413
+ * connectionName: 'carto_dw',
1414
+ * tableName: 'carto-demo-data.demo_tables.retail_stores'
1415
+ * });
1416
+ *
1417
+ * const { widgetSource } = await data;
1418
+ * ```
1419
+ */
1420
+ class WidgetTableSource extends WidgetBaseSource {
1421
+ getModelSource(owner) {
1422
+ return {
1423
+ ...super._getModelSource(owner),
1424
+ type: 'table',
1425
+ data: this.props.tableName
1417
1426
  };
1418
- if (columns) {
1419
- urlParameters.columns = columns.join(',');
1420
- }
1421
- if (filters) {
1422
- urlParameters.filters = filters;
1423
- }
1424
- return Promise.resolve(baseSource('boundary', options, urlParameters));
1425
- } catch (e) {
1426
- return Promise.reject(e);
1427
1427
  }
1428
- };
1428
+ }
1429
1429
 
1430
1430
  // deck.gl
1431
1431
  const h3QuerySource = function (options) {
@@ -1617,7 +1617,8 @@ const vectorQuerySource = function (options) {
1617
1617
  spatialDataColumn = 'geom',
1618
1618
  sqlQuery,
1619
1619
  tileResolution = DEFAULT_TILE_RESOLUTION,
1620
- queryParameters
1620
+ queryParameters,
1621
+ aggregationExp
1621
1622
  } = options;
1622
1623
  const urlParameters = {
1623
1624
  spatialDataColumn,
@@ -1634,6 +1635,9 @@ const vectorQuerySource = function (options) {
1634
1635
  if (queryParameters) {
1635
1636
  urlParameters.queryParameters = queryParameters;
1636
1637
  }
1638
+ if (aggregationExp) {
1639
+ urlParameters.aggregationExp = aggregationExp;
1640
+ }
1637
1641
  return Promise.resolve(baseSource('query', options, urlParameters).then(result => ({
1638
1642
  ...result,
1639
1643
  widgetSource: new WidgetQuerySource(options)
@@ -1651,7 +1655,8 @@ const vectorTableSource = function (options) {
1651
1655
  filters,
1652
1656
  spatialDataColumn = 'geom',
1653
1657
  tableName,
1654
- tileResolution = DEFAULT_TILE_RESOLUTION
1658
+ tileResolution = DEFAULT_TILE_RESOLUTION,
1659
+ aggregationExp
1655
1660
  } = options;
1656
1661
  const urlParameters = {
1657
1662
  name: tableName,
@@ -1665,6 +1670,9 @@ const vectorTableSource = function (options) {
1665
1670
  if (filters) {
1666
1671
  urlParameters.filters = filters;
1667
1672
  }
1673
+ if (aggregationExp) {
1674
+ urlParameters.aggregationExp = aggregationExp;
1675
+ }
1668
1676
  return Promise.resolve(baseSource('table', options, urlParameters).then(result => ({
1669
1677
  ...result,
1670
1678
  widgetSource: new WidgetTableSource(options)