@carto/api-client 0.0.1-0

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.
@@ -0,0 +1,255 @@
1
+ import {executeModel} from '../models/index.js';
2
+ import {
3
+ CategoryRequestOptions,
4
+ CategoryResponse,
5
+ FormulaRequestOptions,
6
+ FormulaResponse,
7
+ HistogramRequestOptions,
8
+ HistogramResponse,
9
+ RangeRequestOptions,
10
+ RangeResponse,
11
+ ScatterRequestOptions,
12
+ ScatterResponse,
13
+ TableRequestOptions,
14
+ TableResponse,
15
+ TimeSeriesRequestOptions,
16
+ TimeSeriesResponse,
17
+ } from './types.js';
18
+ import {Source, FilterLogicalOperator, Credentials, Filter} from '../types.js';
19
+ import {SourceOptions} from '@deck.gl/carto';
20
+ import {getApplicableFilters, normalizeObjectKeys} from '../utils.js';
21
+ import {ApiVersion, MapType} from '../constants.js';
22
+ import {
23
+ DEFAULT_API_BASE_URL,
24
+ DEFAULT_GEO_COLUMN,
25
+ } from '../constants-internal.js';
26
+ import {getClient} from '../client.js';
27
+ import {$TODO} from '../types-internal.js';
28
+
29
+ /**
30
+ * TODO(cleanup): Consolidate {@link SourceOptions} and {@link Source}.
31
+ */
32
+ export interface WidgetBaseSourceProps extends SourceOptions, Credentials {
33
+ type?: MapType;
34
+ filters?: Record<string, Filter>;
35
+ filtersLogicalOperator?: FilterLogicalOperator;
36
+ queryParameters?: unknown[];
37
+ provider?: string;
38
+ }
39
+
40
+ export type WidgetSource = WidgetBaseSource<WidgetBaseSourceProps>;
41
+
42
+ export class WidgetBaseSource<Props extends WidgetBaseSourceProps> {
43
+ readonly props: Props;
44
+ readonly credentials: Required<Credentials> & {clientId: string};
45
+ readonly connectionName: string;
46
+
47
+ static defaultProps: Partial<WidgetBaseSourceProps> = {
48
+ filters: {},
49
+ filtersLogicalOperator: 'and',
50
+ };
51
+
52
+ constructor(props: Props) {
53
+ this.props = {...WidgetBaseSource.defaultProps, ...props};
54
+ this.connectionName = props.connectionName;
55
+ this.credentials = {
56
+ apiVersion: props.apiVersion || ApiVersion.V3,
57
+ apiBaseUrl: props.apiBaseUrl || DEFAULT_API_BASE_URL,
58
+ clientId: props.clientId || getClient(),
59
+ accessToken: props.accessToken,
60
+ geoColumn: props.geoColumn || DEFAULT_GEO_COLUMN,
61
+ };
62
+ }
63
+
64
+ protected getSource(owner?: string): Source {
65
+ return {
66
+ ...(this.props as $TODO),
67
+ credentials: this.credentials,
68
+ connection: this.connectionName,
69
+ filters: getApplicableFilters(owner, this.props.filters),
70
+ };
71
+ }
72
+
73
+ async getFormula(props: FormulaRequestOptions): Promise<FormulaResponse> {
74
+ const {
75
+ filterOwner,
76
+ spatialFilter,
77
+ abortController,
78
+ operationExp,
79
+ ...params
80
+ } = props;
81
+ const {column, operation} = params;
82
+
83
+ type FormulaModelResponse = {rows: {value: number}[]};
84
+
85
+ return executeModel({
86
+ model: 'formula',
87
+ source: this.getSource(filterOwner),
88
+ spatialFilter,
89
+ params: {column: column ?? '*', operation, operationExp},
90
+ opts: {abortController},
91
+ }).then((res: FormulaModelResponse) => normalizeObjectKeys(res.rows[0]));
92
+ }
93
+
94
+ async getCategories(
95
+ props: CategoryRequestOptions
96
+ ): Promise<CategoryResponse> {
97
+ const {filterOwner, spatialFilter, abortController, ...params} = props;
98
+ const {column, operation, operationColumn} = params;
99
+
100
+ type CategoriesModelResponse = {rows: {name: string; value: number}[]};
101
+
102
+ return executeModel({
103
+ model: 'category',
104
+ source: this.getSource(filterOwner),
105
+ spatialFilter,
106
+ params: {
107
+ column,
108
+ operation,
109
+ operationColumn: operationColumn || column,
110
+ },
111
+ opts: {abortController},
112
+ }).then((res: CategoriesModelResponse) => normalizeObjectKeys(res.rows));
113
+ }
114
+
115
+ async getRange(props: RangeRequestOptions): Promise<RangeResponse> {
116
+ const {filterOwner, spatialFilter, abortController, ...params} = props;
117
+ const {column} = params;
118
+
119
+ type RangeModelResponse = {rows: {min: number; max: number}[]};
120
+
121
+ return executeModel({
122
+ model: 'range',
123
+ source: this.getSource(filterOwner),
124
+ spatialFilter,
125
+ params: {column},
126
+ opts: {abortController},
127
+ }).then((res: RangeModelResponse) => normalizeObjectKeys(res.rows[0]));
128
+ }
129
+
130
+ async getTable(props: TableRequestOptions): Promise<TableResponse> {
131
+ const {filterOwner, spatialFilter, abortController, ...params} = props;
132
+ const {columns, sortBy, sortDirection, page = 0, rowsPerPage = 10} = params;
133
+
134
+ type TableModelResponse = {
135
+ rows: Record<string, number | string>[];
136
+ metadata: {total: number};
137
+ };
138
+
139
+ return executeModel({
140
+ model: 'table',
141
+ source: this.getSource(filterOwner),
142
+ spatialFilter,
143
+ params: {
144
+ column: columns,
145
+ sortBy,
146
+ sortDirection,
147
+ limit: rowsPerPage,
148
+ offset: page * rowsPerPage,
149
+ },
150
+ opts: {abortController},
151
+ }).then((res: TableModelResponse) => ({
152
+ rows: normalizeObjectKeys(res.rows),
153
+ totalCount: res.metadata.total,
154
+ }));
155
+ }
156
+
157
+ async getScatter(props: ScatterRequestOptions): Promise<ScatterResponse> {
158
+ const {filterOwner, spatialFilter, abortController, ...params} = props;
159
+ const {xAxisColumn, xAxisJoinOperation, yAxisColumn, yAxisJoinOperation} =
160
+ params;
161
+
162
+ // Make sure this is sync with the same constant in cloud-native/maps-api
163
+ const HARD_LIMIT = 500;
164
+
165
+ type ScatterModelResponse = {rows: {x: number; y: number}[]};
166
+
167
+ return executeModel({
168
+ model: 'scatterplot',
169
+ source: this.getSource(filterOwner),
170
+ spatialFilter,
171
+ params: {
172
+ xAxisColumn,
173
+ xAxisJoinOperation,
174
+ yAxisColumn,
175
+ yAxisJoinOperation,
176
+ limit: HARD_LIMIT,
177
+ },
178
+ opts: {abortController},
179
+ })
180
+ .then((res: ScatterModelResponse) => normalizeObjectKeys(res.rows))
181
+ .then((res) => res.map(({x, y}: {x: number; y: number}) => [x, y]));
182
+ }
183
+
184
+ async getTimeSeries(
185
+ props: TimeSeriesRequestOptions
186
+ ): Promise<TimeSeriesResponse> {
187
+ const {filterOwner, abortController, spatialFilter, ...params} = props;
188
+ const {
189
+ column,
190
+ operationColumn,
191
+ joinOperation,
192
+ operation,
193
+ stepSize,
194
+ stepMultiplier,
195
+ splitByCategory,
196
+ splitByCategoryLimit,
197
+ splitByCategoryValues,
198
+ } = params;
199
+
200
+ type TimeSeriesModelResponse = {
201
+ rows: {name: string; value: number}[];
202
+ metadata: {categories: string[]};
203
+ };
204
+
205
+ return executeModel({
206
+ model: 'timeseries',
207
+ source: this.getSource(filterOwner),
208
+ spatialFilter,
209
+ params: {
210
+ column,
211
+ stepSize,
212
+ stepMultiplier,
213
+ operationColumn: operationColumn || column,
214
+ joinOperation,
215
+ operation,
216
+ splitByCategory,
217
+ splitByCategoryLimit,
218
+ splitByCategoryValues,
219
+ },
220
+ opts: {abortController},
221
+ }).then((res: TimeSeriesModelResponse) => ({
222
+ rows: normalizeObjectKeys(res.rows),
223
+ categories: res.metadata?.categories,
224
+ }));
225
+ }
226
+
227
+ async getHistogram(
228
+ props: HistogramRequestOptions
229
+ ): Promise<HistogramResponse> {
230
+ const {filterOwner, spatialFilter, abortController, ...params} = props;
231
+ const {column, operation, ticks} = params;
232
+
233
+ type HistogramModelResponse = {rows: {tick: number; value: number}[]};
234
+
235
+ const data = await executeModel({
236
+ model: 'histogram',
237
+ source: this.getSource(filterOwner),
238
+ spatialFilter,
239
+ params: {column, operation, ticks},
240
+ opts: {abortController},
241
+ }).then((res: HistogramModelResponse) => normalizeObjectKeys(res.rows));
242
+
243
+ if (data.length) {
244
+ // Given N ticks the API returns up to N+1 bins, omitting any empty bins. Bins
245
+ // include 1 bin below the lowest tick, N-1 between ticks, and 1 bin above the highest tick.
246
+ const result = Array(ticks.length + 1).fill(0);
247
+ data.forEach(
248
+ ({tick, value}: {tick: number; value: number}) => (result[tick] = value)
249
+ );
250
+ return result;
251
+ }
252
+
253
+ return [];
254
+ }
255
+ }
@@ -0,0 +1,25 @@
1
+ import {
2
+ H3QuerySourceOptions,
3
+ QuadbinQuerySourceOptions,
4
+ VectorQuerySourceOptions,
5
+ } from '@deck.gl/carto';
6
+ import {MapType} from '../constants.js';
7
+ import {WidgetBaseSource, WidgetBaseSourceProps} from './widget-base-source.js';
8
+ import {Source} from '../types.js';
9
+
10
+ type LayerQuerySourceOptions =
11
+ | VectorQuerySourceOptions
12
+ | H3QuerySourceOptions
13
+ | QuadbinQuerySourceOptions;
14
+
15
+ export class WidgetQuerySource extends WidgetBaseSource<
16
+ LayerQuerySourceOptions & WidgetBaseSourceProps
17
+ > {
18
+ protected override getSource(owner: string): Source {
19
+ return {
20
+ ...super.getSource(owner),
21
+ type: MapType.QUERY,
22
+ data: this.props.sqlQuery,
23
+ };
24
+ }
25
+ }
@@ -0,0 +1,25 @@
1
+ import {
2
+ H3TableSourceOptions,
3
+ QuadbinTableSourceOptions,
4
+ VectorTableSourceOptions,
5
+ } from '@deck.gl/carto';
6
+ import {WidgetBaseSource, WidgetBaseSourceProps} from './widget-base-source.js';
7
+ import {MapType} from '../constants.js';
8
+ import {Source} from '../types.js';
9
+
10
+ type LayerTableSourceOptions =
11
+ | VectorTableSourceOptions
12
+ | H3TableSourceOptions
13
+ | QuadbinTableSourceOptions;
14
+
15
+ export class WidgetTableSource extends WidgetBaseSource<
16
+ LayerTableSourceOptions & WidgetBaseSourceProps
17
+ > {
18
+ protected override getSource(owner: string): Source {
19
+ return {
20
+ ...super.getSource(owner),
21
+ type: MapType.TABLE,
22
+ data: this.props.tableName,
23
+ };
24
+ }
25
+ }
@@ -0,0 +1,114 @@
1
+ import {
2
+ h3TableSource as _h3TableSource,
3
+ h3QuerySource as _h3QuerySource,
4
+ vectorTableSource as _vectorTableSource,
5
+ vectorQuerySource as _vectorQuerySource,
6
+ quadbinTableSource as _quadbinTableSource,
7
+ quadbinQuerySource as _quadbinQuerySource,
8
+ VectorTableSourceOptions,
9
+ VectorQuerySourceOptions,
10
+ H3TableSourceOptions,
11
+ H3QuerySourceOptions,
12
+ QuadbinQuerySourceOptions,
13
+ QuadbinTableSourceOptions,
14
+ } from '@deck.gl/carto';
15
+ import {WidgetBaseSourceProps} from './widget-base-source.js';
16
+ import {WidgetQuerySource} from './widget-query-source.js';
17
+ import {WidgetTableSource} from './widget-table-source.js';
18
+
19
+ /******************************************************************************
20
+ * RESPONSE OBJECTS
21
+ */
22
+
23
+ type WidgetTableSourceResponse = {widgetSource: WidgetTableSource};
24
+ type WidgetQuerySourceResponse = {widgetSource: WidgetQuerySource};
25
+
26
+ export type VectorTableSourceResponse = WidgetTableSourceResponse &
27
+ Awaited<ReturnType<typeof _vectorTableSource>>;
28
+ export type VectorQuerySourceResponse = WidgetQuerySourceResponse &
29
+ Awaited<ReturnType<typeof _vectorQuerySource>>;
30
+
31
+ export type H3TableSourceResponse = WidgetTableSourceResponse &
32
+ Awaited<ReturnType<typeof _h3TableSource>>;
33
+ export type H3QuerySourceResponse = WidgetQuerySourceResponse &
34
+ Awaited<ReturnType<typeof _h3QuerySource>>;
35
+
36
+ export type QuadbinTableSourceResponse = WidgetTableSourceResponse &
37
+ Awaited<ReturnType<typeof _quadbinTableSource>>;
38
+ export type QuadbinQuerySourceResponse = WidgetQuerySourceResponse &
39
+ Awaited<ReturnType<typeof _quadbinQuerySource>>;
40
+
41
+ /******************************************************************************
42
+ * VECTOR SOURCES
43
+ */
44
+
45
+ /** Wrapper adding widget support to {@link _vectorTableSource}. */
46
+ export async function vectorTableSource(
47
+ props: VectorTableSourceOptions & WidgetBaseSourceProps
48
+ ): Promise<VectorTableSourceResponse> {
49
+ const response = await _vectorTableSource(props);
50
+ return {...response, widgetSource: new WidgetTableSource(props)};
51
+ }
52
+
53
+ /** Wrapper adding widget support to {@link _vectorQuerySource}. */
54
+ export async function vectorQuerySource(
55
+ props: VectorQuerySourceOptions & WidgetBaseSourceProps
56
+ ): Promise<VectorQuerySourceResponse> {
57
+ const response = await _vectorQuerySource(props);
58
+ return {...response, widgetSource: new WidgetQuerySource(props)};
59
+ }
60
+
61
+ /** Wrapper adding widget support to {@link _vectorTilesetSource}. */
62
+ export async function vectorTilesetSource() {
63
+ throw new Error('not implemented');
64
+ }
65
+
66
+ /******************************************************************************
67
+ * H3 SOURCES
68
+ */
69
+
70
+ /** Wrapper adding widget support to {@link _h3TableSource}. */
71
+ export async function h3TableSource(
72
+ props: H3TableSourceOptions & WidgetBaseSourceProps
73
+ ): Promise<H3TableSourceResponse> {
74
+ const response = await _h3TableSource(props);
75
+ return {...response, widgetSource: new WidgetTableSource(props)};
76
+ }
77
+
78
+ /** Wrapper adding widget support to {@link _h3QuerySource}. */
79
+ export async function h3QuerySource(
80
+ props: H3QuerySourceOptions & WidgetBaseSourceProps
81
+ ): Promise<H3QuerySourceResponse> {
82
+ const response = await _h3QuerySource(props);
83
+ return {...response, widgetSource: new WidgetQuerySource(props)};
84
+ }
85
+
86
+ /** Wrapper adding widget support to {@link _h3TilesetSource}. */
87
+ export async function h3TilesetSource() {
88
+ throw new Error('not implemented');
89
+ }
90
+
91
+ /******************************************************************************
92
+ * QUADBIN SOURCES
93
+ */
94
+
95
+ /** Wrapper adding widget support to {@link _quadbinTableSource}. */
96
+ export async function quadbinTableSource(
97
+ props: QuadbinTableSourceOptions & WidgetBaseSourceProps
98
+ ): Promise<QuadbinTableSourceResponse> {
99
+ const response = await _quadbinTableSource(props);
100
+ return {...response, widgetSource: new WidgetTableSource(props)};
101
+ }
102
+
103
+ /** Wrapper adding widget support to {@link _quadbinQuerySource}. */
104
+ export async function quadbinQuerySource(
105
+ props: QuadbinQuerySourceOptions & WidgetBaseSourceProps
106
+ ): Promise<QuadbinQuerySourceResponse> {
107
+ const response = await _quadbinQuerySource(props);
108
+ return {...response, widgetSource: new WidgetQuerySource(props)};
109
+ }
110
+
111
+ /** Wrapper adding widget support to {@link _quadbinTilesetSource}. */
112
+ export async function quadbinTilesetSource() {
113
+ throw new Error('not implemented');
114
+ }
@@ -0,0 +1,9 @@
1
+ /******************************************************************************
2
+ * INTERNAL
3
+ */
4
+
5
+ /** @internal */
6
+ export type $TODO = any;
7
+
8
+ /** @internal */
9
+ export type $IntentionalAny = any;
package/src/types.ts ADDED
@@ -0,0 +1,76 @@
1
+ import type {ApiVersion, MapType, FilterType} from './constants';
2
+
3
+ /******************************************************************************
4
+ * AUTHENTICATION
5
+ */
6
+
7
+ /** @internalRemarks Source: @carto/react-api */
8
+ export type Credentials = {
9
+ apiVersion?: ApiVersion;
10
+ apiBaseUrl?: string;
11
+ geoColumn?: string;
12
+ accessToken: string;
13
+ };
14
+
15
+ /******************************************************************************
16
+ * SOURCES
17
+ */
18
+
19
+ /** @internalRemarks Source: @carto/react-api */
20
+ export type Source = {
21
+ type: MapType;
22
+ connection: string;
23
+ credentials: Credentials;
24
+ data: string;
25
+ geoColumn?: string;
26
+ queryParameters?: unknown[];
27
+ filters?: Record<string, Filter>;
28
+ filtersLogicalOperator?: 'and' | 'or';
29
+ };
30
+
31
+ /******************************************************************************
32
+ * AGGREGATION
33
+ */
34
+
35
+ /**
36
+ * Enum for the different types of aggregations available for widgets
37
+ * @enum {string}
38
+ * @readonly
39
+ * @internalRemarks Source: @carto/constants
40
+ * @internalRemarks Converted from enum to type union, for improved declarative API.
41
+ */
42
+ export type AggregationType =
43
+ | 'count'
44
+ | 'avg'
45
+ | 'min'
46
+ | 'max'
47
+ | 'sum'
48
+ | 'custom';
49
+
50
+ /******************************************************************************
51
+ * FILTERS
52
+ */
53
+
54
+ /** @internalRemarks Source: @carto/react-api */
55
+ export type SpatialFilter = GeoJSON.Polygon | GeoJSON.MultiPolygon;
56
+
57
+ /** @internalRemarks Source: @carto/react-api, @deck.gl/carto */
58
+ export interface Filter {
59
+ [FilterType.IN]?: {owner?: string; values: number[]};
60
+ /** [a, b] both are included. */
61
+ [FilterType.BETWEEN]?: {owner?: string; values: number[][]};
62
+ /** [a, b) a is included, b is not. */
63
+ [FilterType.CLOSED_OPEN]?: {owner?: string; values: number[][]};
64
+ [FilterType.TIME]?: {owner?: string; values: number[][]};
65
+ [FilterType.STRING_SEARCH]?: {owner?: string; values: string[]};
66
+ }
67
+
68
+ /** @internalRemarks Source: @carto/react-core */
69
+ export type FilterLogicalOperator = 'and' | 'or';
70
+
71
+ /******************************************************************************
72
+ * SORTING
73
+ */
74
+
75
+ export type SortDirection = 'asc' | 'desc';
76
+ export type SortColumnType = 'number' | 'string' | 'date';
package/src/utils.ts ADDED
@@ -0,0 +1,84 @@
1
+ import {Filter} from './types.js';
2
+ import {FilterType} from './constants.js';
3
+ import {$TODO} from './types-internal.js';
4
+
5
+ const FILTER_TYPES = new Set(Object.values(FilterType));
6
+ const isFilterType = (type: string): type is FilterType =>
7
+ FILTER_TYPES.has(type as FilterType);
8
+
9
+ /**
10
+ * @privateRemarks Source: @carto/react-widgets
11
+ * @internal
12
+ */
13
+ export function getApplicableFilters(
14
+ owner?: string,
15
+ filters?: Record<string, Filter>
16
+ ): Record<string, Filter> {
17
+ if (!filters) return {};
18
+
19
+ const applicableFilters: Record<string, Filter> = {};
20
+
21
+ for (const column in filters) {
22
+ for (const type in filters[column]) {
23
+ if (!isFilterType(type)) continue;
24
+
25
+ const filter = filters[column][type];
26
+ if (filter && filter.owner !== owner) {
27
+ applicableFilters[column] ||= {};
28
+ applicableFilters[column][type] = filter as $TODO;
29
+ }
30
+ }
31
+ }
32
+
33
+ return applicableFilters;
34
+ }
35
+
36
+ type Row<T> = Record<string, T> | Record<string, T>[] | T[] | T;
37
+
38
+ /**
39
+ * Due to each data warehouse having its own behavior with columns,
40
+ * we need to normalize them and transform every key to lowercase.
41
+ *
42
+ * @internalRemarks Source: @carto/react-widgets
43
+ * @internal
44
+ */
45
+ export function normalizeObjectKeys<T, R extends Row<T>>(el: R): R {
46
+ if (Array.isArray(el)) {
47
+ return el.map((value) => normalizeObjectKeys(value)) as R;
48
+ } else if (typeof el !== 'object') {
49
+ return el;
50
+ }
51
+
52
+ return Object.entries(el as Record<string, T>).reduce((acc, [key, value]) => {
53
+ acc[key.toLowerCase()] =
54
+ typeof value === 'object' && value ? normalizeObjectKeys(value) : value;
55
+ return acc;
56
+ }, {} as Record<string, T>) as R;
57
+ }
58
+
59
+ /** @internalRemarks Source: @carto/react-core */
60
+ export function assert(condition: unknown, message: string) {
61
+ if (!condition) {
62
+ throw new Error(message);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * @internalRemarks Source: @carto/react-core
68
+ * @internal
69
+ */
70
+ export class InvalidColumnError extends Error {
71
+ protected static readonly NAME = 'InvalidColumnError';
72
+
73
+ constructor(message: string) {
74
+ super(`${InvalidColumnError.NAME}: ${message}`);
75
+ this.name = InvalidColumnError.NAME;
76
+ }
77
+
78
+ static is(error: unknown) {
79
+ return (
80
+ error instanceof InvalidColumnError ||
81
+ (error as Error).message?.includes(InvalidColumnError.NAME)
82
+ );
83
+ }
84
+ }