From b1fb61a77ecba53c58b0b5a58db9149d9dd8a6a8 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Tue, 12 Nov 2024 15:48:15 -0500 Subject: [PATCH 1/6] feat(picking): Add Picking API --- src/api/endpoints.ts | 10 ++ src/index.ts | 1 + src/picking.ts | 188 +++++++++++++++++++++++++++++++++++++ src/sources/base-source.ts | 26 ++--- src/utils.ts | 13 +++ 5 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 src/picking.ts diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index e36a178..28af6f0 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -82,3 +82,13 @@ export function buildQueryUrl({ }): string { return buildV3Path(apiBaseUrl, 'v3', 'sql', connectionName, 'query'); } + +export function buildPickingUrl({ + apiBaseUrl, + connectionName, +}: { + apiBaseUrl: string; + connectionName: string; +}): string { + return buildV3Path(apiBaseUrl, 'v3', 'sql', connectionName, 'model', 'pick'); +} diff --git a/src/index.ts b/src/index.ts index dda6dec..31848a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ export * from './client.js'; export * from './constants.js'; export * from './filters.js'; export * from './geo.js'; +export * from './picking.js'; export * from './widget-sources/index.js'; export * from './types.js'; diff --git a/src/picking.ts b/src/picking.ts new file mode 100644 index 0000000..f939987 --- /dev/null +++ b/src/picking.ts @@ -0,0 +1,188 @@ +import { + FilterLogicalOperator, + Filters, + MapType, + QueryParameters, + SpatialFilter, +} from './types'; +import {DEFAULT_API_BASE_URL} from './constants'; +import { + DEFAULT_GEO_COLUMN, + DEFAULT_TILE_RESOLUTION, +} from './constants-internal'; +import {TileResolution} from './sources/types'; +import {getClient} from './client'; +import {APIErrorContext, requestWithParameters} from './api'; +import {assignOptions} from './utils'; +import {buildPickingUrl} from './api/endpoints'; + +/** @internal */ +export type BasePickingOptions = { + /** TODO */ + accessToken: string; + + /** TODO */ + connectionName: string; + + /** TODO */ + apiBaseUrl?: string; + + /** TODO */ + clientId?: string; + + /** Source type. */ + type: 'table' | 'query'; + + /** + * Feature IDs, as found in `_carto_feature_id`. Feature IDs are a hash + * of geometry, and features with identical geometry will have the same + * feature ID. Order is important; features in the result set will be + * sorted according to the order of IDs in the request. + */ + featureIds: string[]; + + /** + * Columns to be returned for each picked object. Note that for datasets + * containing features with identical geometry, more than one result per + * requested feature ID may be returned. To match results back to the + * requested feature ID, include `_carto_feature_id` in the columns list. + */ + columns: string[]; + + /** Topology of objects to be picked. */ + dataType: 'points' | 'lines' | 'polygons'; + + /** Required for points, otherwise optional. */ + z?: number; + + /** + * Maximum number of objects to return in the result set. For datasets + * containing features with identical geometry, those features will have + * the same feature IDs, and so more results may be returned than feature IDs + * given in the request. + */ + limit?: number; + + /** + * Must match `tileResolution` used when obtaining the `_carto_feature_id` + * column, typically in a layer's tile requests. + */ + tileResolution?: TileResolution; + + /** TODO */ + queryParameters?: QueryParameters; + + /** + * Optional filters applied before picking, to improve performance and limit + * results. + */ + filters?: Filters; + + /** TODO */ + filtersLogicalOperator?: FilterLogicalOperator; + + /** + * Optional spatial filter applied before picking to improve performance. + */ + spatialFilters?: SpatialFilter; + + /** Spatial data column, default is 'geom'. */ + spatialDataColumn?: string; +}; + +/** @internal */ +export type TablePickingOptions = BasePickingOptions & { + type: 'table'; + tableName: string; +}; + +/** @internal */ +export type QueryPickingOptions = BasePickingOptions & { + type: 'query'; + sqlQuery: string; +}; + +/** @internal */ +export type PickObjectsRequest = TablePickingOptions | QueryPickingOptions; + +const PICKING_DEFAULTS = { + apiBaseUrl: DEFAULT_API_BASE_URL, + clientId: getClient(), + limit: 1000, + tileResolution: DEFAULT_TILE_RESOLUTION, + spatialDataColumn: DEFAULT_GEO_COLUMN, +}; + +/** @internal */ +export type PickObjectsResponse = { + rows: Record[]; + meta: { + cacheHit: boolean; + totalBytesProcessed: string; + location: string; + }; +}; + +type PickObjectsRequestInternal = { + type: MapType; + client: string; + source: string; + params: { + columns: string[]; + dataType: 'points' | 'lines' | 'polygons'; + featureIds: string[]; + limit: number; + tileResolution: number; + }; + queryParameters?: unknown; + filters?: Filters; + filtersLogicalOperator?: FilterLogicalOperator; + spatialFilters?: SpatialFilter; + spatialDataType: 'geo'; + spatialDataColumn: string; +}; + +/** @internal */ +export async function pickObjects( + options: PickObjectsRequest +): Promise { + const {accessToken, apiBaseUrl, connectionName, clientId, ...rest} = + assignOptions( + {...PICKING_DEFAULTS}, + options + ); + + const baseUrl = buildPickingUrl({apiBaseUrl, connectionName}); + const headers = {Authorization: `Bearer ${options.accessToken}`}; + const parameters: PickObjectsRequestInternal = { + type: rest.type, + client: clientId, + source: rest.type === 'table' ? rest.tableName : rest.sqlQuery, + params: { + columns: rest.columns, + dataType: rest.dataType, + featureIds: rest.featureIds, + limit: rest.limit, + tileResolution: rest.tileResolution, + }, + queryParameters: rest.queryParameters, + filters: rest.filters, + filtersLogicalOperator: rest.filtersLogicalOperator, + spatialFilters: rest.spatialFilters, + spatialDataType: 'geo', + spatialDataColumn: rest.spatialDataColumn, + }; + const errorContext: APIErrorContext = { + requestType: 'SQL', + connection: connectionName, + type: rest.type, + source: JSON.stringify(parameters, undefined, 2), + }; + + return requestWithParameters({ + baseUrl, + parameters, + headers, + errorContext, + }); +} diff --git a/src/sources/base-source.ts b/src/sources/base-source.ts index 3b2f530..43651ec 100644 --- a/src/sources/base-source.ts +++ b/src/sources/base-source.ts @@ -17,6 +17,7 @@ import type { import {MapType} from '../types'; import {APIErrorContext} from '../api'; import {getClient} from '../client'; +import {assignOptions} from '../utils'; export const SOURCE_DEFAULTS: SourceOptionalOptions = { apiBaseUrl: DEFAULT_API_BASE_URL, @@ -32,18 +33,19 @@ export async function baseSource>( urlParameters: UrlParameters ): Promise { const {accessToken, connectionName, cache, ...optionalOptions} = options; - const mergedOptions = { - ...SOURCE_DEFAULTS, - accessToken, - connectionName, - endpoint, - }; - for (const key in optionalOptions) { - if (optionalOptions[key as keyof typeof optionalOptions]) { - (mergedOptions as any)[key] = - optionalOptions[key as keyof typeof optionalOptions]; - } - } + + const mergedOptions = assignOptions< + SourceOptionalOptions & SourceRequiredOptions & {endpoint: MapType} + >( + { + ...SOURCE_DEFAULTS, + accessToken, + connectionName, + endpoint, + }, + optionalOptions + ); + const baseUrl = buildSourceUrl(mergedOptions); const {clientId, maxLengthURL, format} = mergedOptions; const headers = { diff --git a/src/utils.ts b/src/utils.ts index e4048fc..2c6f6b5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -97,3 +97,16 @@ export const isObject: (x: unknown) => boolean = (x) => /** @internal */ export const isPureObject: (x: any) => boolean = (x) => isObject(x) && x.constructor === {}.constructor; + +/** @internal */ +export function assignOptions>( + base: Record, + optional: Record +): T { + for (const key in optional) { + if (optional[key]) { + base[key] = optional[key]; + } + } + return base as T; +} From ca3c372640cfa545b018768016370dd9aa90b21a Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Tue, 12 Nov 2024 16:58:09 -0500 Subject: [PATCH 2/6] clean up --- src/picking.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/picking.ts b/src/picking.ts index f939987..f2d5482 100644 --- a/src/picking.ts +++ b/src/picking.ts @@ -18,16 +18,9 @@ import {buildPickingUrl} from './api/endpoints'; /** @internal */ export type BasePickingOptions = { - /** TODO */ accessToken: string; - - /** TODO */ connectionName: string; - - /** TODO */ apiBaseUrl?: string; - - /** TODO */ clientId?: string; /** Source type. */ @@ -69,7 +62,6 @@ export type BasePickingOptions = { */ tileResolution?: TileResolution; - /** TODO */ queryParameters?: QueryParameters; /** @@ -78,7 +70,6 @@ export type BasePickingOptions = { */ filters?: Filters; - /** TODO */ filtersLogicalOperator?: FilterLogicalOperator; /** From c3cb8fec9daf6121542c08dcec69e207b0c1291d Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Tue, 12 Nov 2024 17:44:33 -0500 Subject: [PATCH 3/6] refactor(picking): Move picking API to WidgetSource --- src/api/endpoints.ts | 10 -- src/index.ts | 1 - src/models/model.ts | 1 + src/picking.ts | 179 ----------------------- src/sources/base-source.ts | 26 ++-- src/utils.ts | 13 -- src/widget-sources/types.ts | 39 +++++ src/widget-sources/widget-base-source.ts | 41 +++++- 8 files changed, 92 insertions(+), 218 deletions(-) delete mode 100644 src/picking.ts diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index 28af6f0..e36a178 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -82,13 +82,3 @@ export function buildQueryUrl({ }): string { return buildV3Path(apiBaseUrl, 'v3', 'sql', connectionName, 'query'); } - -export function buildPickingUrl({ - apiBaseUrl, - connectionName, -}: { - apiBaseUrl: string; - connectionName: string; -}): string { - return buildV3Path(apiBaseUrl, 'v3', 'sql', connectionName, 'model', 'pick'); -} diff --git a/src/index.ts b/src/index.ts index 31848a7..dda6dec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,6 @@ export * from './client.js'; export * from './constants.js'; export * from './filters.js'; export * from './geo.js'; -export * from './picking.js'; export * from './widget-sources/index.js'; export * from './types.js'; diff --git a/src/models/model.ts b/src/models/model.ts index 08c2edd..b31af27 100644 --- a/src/models/model.ts +++ b/src/models/model.ts @@ -16,6 +16,7 @@ const AVAILABLE_MODELS = [ 'category', 'histogram', 'formula', + 'pick', 'timeseries', 'range', 'scatterplot', diff --git a/src/picking.ts b/src/picking.ts deleted file mode 100644 index f2d5482..0000000 --- a/src/picking.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { - FilterLogicalOperator, - Filters, - MapType, - QueryParameters, - SpatialFilter, -} from './types'; -import {DEFAULT_API_BASE_URL} from './constants'; -import { - DEFAULT_GEO_COLUMN, - DEFAULT_TILE_RESOLUTION, -} from './constants-internal'; -import {TileResolution} from './sources/types'; -import {getClient} from './client'; -import {APIErrorContext, requestWithParameters} from './api'; -import {assignOptions} from './utils'; -import {buildPickingUrl} from './api/endpoints'; - -/** @internal */ -export type BasePickingOptions = { - accessToken: string; - connectionName: string; - apiBaseUrl?: string; - clientId?: string; - - /** Source type. */ - type: 'table' | 'query'; - - /** - * Feature IDs, as found in `_carto_feature_id`. Feature IDs are a hash - * of geometry, and features with identical geometry will have the same - * feature ID. Order is important; features in the result set will be - * sorted according to the order of IDs in the request. - */ - featureIds: string[]; - - /** - * Columns to be returned for each picked object. Note that for datasets - * containing features with identical geometry, more than one result per - * requested feature ID may be returned. To match results back to the - * requested feature ID, include `_carto_feature_id` in the columns list. - */ - columns: string[]; - - /** Topology of objects to be picked. */ - dataType: 'points' | 'lines' | 'polygons'; - - /** Required for points, otherwise optional. */ - z?: number; - - /** - * Maximum number of objects to return in the result set. For datasets - * containing features with identical geometry, those features will have - * the same feature IDs, and so more results may be returned than feature IDs - * given in the request. - */ - limit?: number; - - /** - * Must match `tileResolution` used when obtaining the `_carto_feature_id` - * column, typically in a layer's tile requests. - */ - tileResolution?: TileResolution; - - queryParameters?: QueryParameters; - - /** - * Optional filters applied before picking, to improve performance and limit - * results. - */ - filters?: Filters; - - filtersLogicalOperator?: FilterLogicalOperator; - - /** - * Optional spatial filter applied before picking to improve performance. - */ - spatialFilters?: SpatialFilter; - - /** Spatial data column, default is 'geom'. */ - spatialDataColumn?: string; -}; - -/** @internal */ -export type TablePickingOptions = BasePickingOptions & { - type: 'table'; - tableName: string; -}; - -/** @internal */ -export type QueryPickingOptions = BasePickingOptions & { - type: 'query'; - sqlQuery: string; -}; - -/** @internal */ -export type PickObjectsRequest = TablePickingOptions | QueryPickingOptions; - -const PICKING_DEFAULTS = { - apiBaseUrl: DEFAULT_API_BASE_URL, - clientId: getClient(), - limit: 1000, - tileResolution: DEFAULT_TILE_RESOLUTION, - spatialDataColumn: DEFAULT_GEO_COLUMN, -}; - -/** @internal */ -export type PickObjectsResponse = { - rows: Record[]; - meta: { - cacheHit: boolean; - totalBytesProcessed: string; - location: string; - }; -}; - -type PickObjectsRequestInternal = { - type: MapType; - client: string; - source: string; - params: { - columns: string[]; - dataType: 'points' | 'lines' | 'polygons'; - featureIds: string[]; - limit: number; - tileResolution: number; - }; - queryParameters?: unknown; - filters?: Filters; - filtersLogicalOperator?: FilterLogicalOperator; - spatialFilters?: SpatialFilter; - spatialDataType: 'geo'; - spatialDataColumn: string; -}; - -/** @internal */ -export async function pickObjects( - options: PickObjectsRequest -): Promise { - const {accessToken, apiBaseUrl, connectionName, clientId, ...rest} = - assignOptions( - {...PICKING_DEFAULTS}, - options - ); - - const baseUrl = buildPickingUrl({apiBaseUrl, connectionName}); - const headers = {Authorization: `Bearer ${options.accessToken}`}; - const parameters: PickObjectsRequestInternal = { - type: rest.type, - client: clientId, - source: rest.type === 'table' ? rest.tableName : rest.sqlQuery, - params: { - columns: rest.columns, - dataType: rest.dataType, - featureIds: rest.featureIds, - limit: rest.limit, - tileResolution: rest.tileResolution, - }, - queryParameters: rest.queryParameters, - filters: rest.filters, - filtersLogicalOperator: rest.filtersLogicalOperator, - spatialFilters: rest.spatialFilters, - spatialDataType: 'geo', - spatialDataColumn: rest.spatialDataColumn, - }; - const errorContext: APIErrorContext = { - requestType: 'SQL', - connection: connectionName, - type: rest.type, - source: JSON.stringify(parameters, undefined, 2), - }; - - return requestWithParameters({ - baseUrl, - parameters, - headers, - errorContext, - }); -} diff --git a/src/sources/base-source.ts b/src/sources/base-source.ts index 43651ec..3b2f530 100644 --- a/src/sources/base-source.ts +++ b/src/sources/base-source.ts @@ -17,7 +17,6 @@ import type { import {MapType} from '../types'; import {APIErrorContext} from '../api'; import {getClient} from '../client'; -import {assignOptions} from '../utils'; export const SOURCE_DEFAULTS: SourceOptionalOptions = { apiBaseUrl: DEFAULT_API_BASE_URL, @@ -33,19 +32,18 @@ export async function baseSource>( urlParameters: UrlParameters ): Promise { const {accessToken, connectionName, cache, ...optionalOptions} = options; - - const mergedOptions = assignOptions< - SourceOptionalOptions & SourceRequiredOptions & {endpoint: MapType} - >( - { - ...SOURCE_DEFAULTS, - accessToken, - connectionName, - endpoint, - }, - optionalOptions - ); - + const mergedOptions = { + ...SOURCE_DEFAULTS, + accessToken, + connectionName, + endpoint, + }; + for (const key in optionalOptions) { + if (optionalOptions[key as keyof typeof optionalOptions]) { + (mergedOptions as any)[key] = + optionalOptions[key as keyof typeof optionalOptions]; + } + } const baseUrl = buildSourceUrl(mergedOptions); const {clientId, maxLengthURL, format} = mergedOptions; const headers = { diff --git a/src/utils.ts b/src/utils.ts index 2c6f6b5..e4048fc 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -97,16 +97,3 @@ export const isObject: (x: unknown) => boolean = (x) => /** @internal */ export const isPureObject: (x: any) => boolean = (x) => isObject(x) && x.constructor === {}.constructor; - -/** @internal */ -export function assignOptions>( - base: Record, - optional: Record -): T { - for (const key in optional) { - if (optional[key]) { - base[key] = optional[key]; - } - } - return base as T; -} diff --git a/src/widget-sources/types.ts b/src/widget-sources/types.ts index 05b035c..fa0502b 100644 --- a/src/widget-sources/types.ts +++ b/src/widget-sources/types.ts @@ -1,3 +1,4 @@ +import {TileResolution} from '../sources/types'; import { GroupDateType, SortColumnType, @@ -23,6 +24,41 @@ export interface CategoryRequestOptions extends BaseRequestOptions { operationColumn?: string; } +export interface FeaturesRequestOptions extends BaseRequestOptions { + /** + * Feature IDs, as found in `_carto_feature_id`. Feature IDs are a hash + * of geometry, and features with identical geometry will have the same + * feature ID. Order is important; features in the result set will be + * sorted according to the order of IDs in the request. + */ + featureIds: string[]; + + /** + * Columns to be returned for each picked object. Note that for datasets + * containing features with identical geometry, more than one result per + * requested feature ID may be returned. To match results back to the + * requested feature ID, include `_carto_feature_id` in the columns list. + */ + columns: string[]; + + /** Topology of objects to be picked. */ + dataType: 'points' | 'lines' | 'polygons'; + + /** + * Maximum number of objects to return in the result set. For datasets + * containing features with identical geometry, those features will have + * the same feature IDs, and so more results may be returned than feature IDs + * given in the request. + */ + limit?: number; + + /** + * Must match `tileResolution` used when obtaining the `_carto_feature_id` + * column, typically in a layer's tile requests. + */ + tileResolution?: TileResolution; +} + /** Options for {@link WidgetBaseSource#getFormula}. */ export interface FormulaRequestOptions extends BaseRequestOptions { column: string; @@ -77,6 +113,9 @@ export interface TimeSeriesRequestOptions extends BaseRequestOptions { * WIDGET API RESPONSES */ +/** Response from {@link WidgetBaseSource#getFeatures}. */ +export type FeaturesResponse = {rows: Record[]}; + /** Response from {@link WidgetBaseSource#getFormula}. */ export type FormulaResponse = {value: number}; diff --git a/src/widget-sources/widget-base-source.ts b/src/widget-sources/widget-base-source.ts index 3dde07d..dc3aa03 100644 --- a/src/widget-sources/widget-base-source.ts +++ b/src/widget-sources/widget-base-source.ts @@ -2,6 +2,8 @@ import {executeModel} from '../models/index.js'; import { CategoryRequestOptions, CategoryResponse, + FeaturesRequestOptions, + FeaturesResponse, FormulaRequestOptions, FormulaResponse, HistogramRequestOptions, @@ -21,7 +23,10 @@ import {getClient} from '../client.js'; import {ModelSource} from '../models/model.js'; import {SourceOptions} from '../sources/index.js'; import {ApiVersion, DEFAULT_API_BASE_URL} from '../constants.js'; -import {DEFAULT_GEO_COLUMN} from '../constants-internal.js'; +import { + DEFAULT_GEO_COLUMN, + DEFAULT_TILE_RESOLUTION, +} from '../constants-internal.js'; export interface WidgetBaseSourceProps extends Omit { apiVersion?: ApiVersion; @@ -105,6 +110,40 @@ export abstract class WidgetBaseSource { }).then((res: CategoriesModelResponse) => normalizeObjectKeys(res.rows)); } + /**************************************************************************** + * FEATURES + */ + + /** + * Given a list of feature IDs (as found in `_carto_feature_id`) returns all + * matching features. In datasets containing features with duplicate geometries, + * feature IDs may be duplicated (IDs are a hash of geometry) and so more + * results may be returned than IDs in the request. + * @internal + * @experimental + */ + async getFeatures( + options: FeaturesRequestOptions + ): Promise { + const {filterOwner, spatialFilter, abortController, ...params} = options; + const {columns, dataType, featureIds, limit, tileResolution} = params; + + type FeaturesModelResponse = {rows: Record[]}; + + return executeModel({ + model: 'pick', + source: {...this.getModelSource(filterOwner), spatialFilter}, + params: { + columns, + dataType, + featureIds, + limit: limit || 1000, + tileResolution: tileResolution || DEFAULT_TILE_RESOLUTION, + }, + opts: {abortController}, + }).then((res: FeaturesModelResponse) => normalizeObjectKeys(res)); + } + /**************************************************************************** * FORMULA */ From 52a7ba0dbbf8c7841ea174e7deaa26b09492808a Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Tue, 12 Nov 2024 17:51:08 -0500 Subject: [PATCH 4/6] chore(picking): Fix response formatting, add tests --- src/widget-sources/widget-base-source.ts | 4 +- .../widget-sources/widget-base-source.test.ts | 49 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/widget-sources/widget-base-source.ts b/src/widget-sources/widget-base-source.ts index dc3aa03..4a88520 100644 --- a/src/widget-sources/widget-base-source.ts +++ b/src/widget-sources/widget-base-source.ts @@ -141,7 +141,9 @@ export abstract class WidgetBaseSource { tileResolution: tileResolution || DEFAULT_TILE_RESOLUTION, }, opts: {abortController}, - }).then((res: FeaturesModelResponse) => normalizeObjectKeys(res)); + }).then((res: FeaturesModelResponse) => ({ + rows: normalizeObjectKeys(res.rows), + })); } /**************************************************************************** diff --git a/test/widget-sources/widget-base-source.test.ts b/test/widget-sources/widget-base-source.test.ts index 92ddfe8..0f3350b 100644 --- a/test/widget-sources/widget-base-source.test.ts +++ b/test/widget-sources/widget-base-source.test.ts @@ -224,6 +224,55 @@ test('filters - owner', async () => { } }); +/****************************************************************************** + * getFeatures + */ + +test('getFeatures', async () => { + const widgetSource = new WidgetTestSource({ + accessToken: '', + connectionName: 'carto_dw', + }); + + const expectedRows = [ + {_carto_feature_id: 'a', name: 'Veggie Mart', revenue: 1200}, + {_carto_feature_id: 'b', name: 'EZ Drive Thru', revenue: 400}, + {_carto_feature_id: 'c', name: "Buddy's Convenience", revenue: 800}, + ]; + + const mockFetch = vi + .fn() + .mockResolvedValueOnce( + createMockResponse({rows: expectedRows, meta: {foo: 'bar'}}) + ); + vi.stubGlobal('fetch', mockFetch); + + const actualFeatures = await widgetSource.getFeatures({ + columns: ['_carto_feature_id', 'name', 'revenue'], + featureIds: ['a', 'b', 'c'], + dataType: 'points', + }); + + expect(mockFetch).toHaveBeenCalledOnce(); + expect(actualFeatures).toEqual({rows: expectedRows}); + + const params = new URL(mockFetch.mock.lastCall[0]).searchParams.entries(); + expect(Object.fromEntries(params)).toMatchObject({ + type: 'test', + source: 'test-data', + params: JSON.stringify({ + columns: ['_carto_feature_id', 'name', 'revenue'], + dataType: 'points', + featureIds: ['a', 'b', 'c'], + limit: 1000, + tileResolution: 0.5, + }), + queryParameters: '', + filters: JSON.stringify({}), + filtersLogicalOperator: 'and', + }); +}); + /****************************************************************************** * getFormula */ From 623c54ee267bc4226ecc3946a22dd256310eb52c Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Thu, 14 Nov 2024 15:06:07 -0500 Subject: [PATCH 5/6] fix(picking): Fixes for Picking Model API calls --- src/models/model.ts | 18 +++++++++++++----- src/widget-sources/types.ts | 3 +++ src/widget-sources/widget-base-source.ts | 3 ++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/models/model.ts b/src/models/model.ts index b31af27..cfd6057 100644 --- a/src/models/model.ts +++ b/src/models/model.ts @@ -75,7 +75,13 @@ export function executeModel(props: { let url = `${apiBaseUrl}/v3/sql/${connectionName}/model/${model}`; - const {filters, filtersLogicalOperator = 'and', data} = source; + const { + data, + filters, + filtersLogicalOperator = 'and', + geoColumn = DEFAULT_GEO_COLUMN, + } = source; + const queryParameters = source.queryParameters ? JSON.stringify(source.queryParameters) : ''; @@ -90,12 +96,14 @@ export function executeModel(props: { filtersLogicalOperator, }; + // Picking Model API requires 'spatialDataColumn'. + if (model === 'pick') { + queryParams.spatialDataColumn = geoColumn; + } + // API supports multiple filters, we apply it only to geoColumn const spatialFilters = source.spatialFilter - ? { - [source.geoColumn ? source.geoColumn : DEFAULT_GEO_COLUMN]: - source.spatialFilter, - } + ? {[geoColumn]: source.spatialFilter} : undefined; if (spatialFilters) { diff --git a/src/widget-sources/types.ts b/src/widget-sources/types.ts index fa0502b..37f1094 100644 --- a/src/widget-sources/types.ts +++ b/src/widget-sources/types.ts @@ -44,6 +44,9 @@ export interface FeaturesRequestOptions extends BaseRequestOptions { /** Topology of objects to be picked. */ dataType: 'points' | 'lines' | 'polygons'; + /** Zoom level, required if using 'points' data type. */ + z?: number; + /** * Maximum number of objects to return in the result set. For datasets * containing features with identical geometry, those features will have diff --git a/src/widget-sources/widget-base-source.ts b/src/widget-sources/widget-base-source.ts index 4a88520..3768aba 100644 --- a/src/widget-sources/widget-base-source.ts +++ b/src/widget-sources/widget-base-source.ts @@ -126,7 +126,7 @@ export abstract class WidgetBaseSource { options: FeaturesRequestOptions ): Promise { const {filterOwner, spatialFilter, abortController, ...params} = options; - const {columns, dataType, featureIds, limit, tileResolution} = params; + const {columns, dataType, featureIds, z, limit, tileResolution} = params; type FeaturesModelResponse = {rows: Record[]}; @@ -137,6 +137,7 @@ export abstract class WidgetBaseSource { columns, dataType, featureIds, + z, limit: limit || 1000, tileResolution: tileResolution || DEFAULT_TILE_RESOLUTION, }, From 8a383cf7e2e4ffb0ca7c3b96294c40d15f6ae813 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Thu, 14 Nov 2024 15:09:00 -0500 Subject: [PATCH 6/6] Clean up TSDoc --- src/widget-sources/types.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/widget-sources/types.ts b/src/widget-sources/types.ts index 37f1094..12f2e9e 100644 --- a/src/widget-sources/types.ts +++ b/src/widget-sources/types.ts @@ -24,6 +24,11 @@ export interface CategoryRequestOptions extends BaseRequestOptions { operationColumn?: string; } +/** + * Options for {@link WidgetBaseSource#getFeatures}. + * @experimental + * @internal + */ export interface FeaturesRequestOptions extends BaseRequestOptions { /** * Feature IDs, as found in `_carto_feature_id`. Feature IDs are a hash @@ -116,7 +121,11 @@ export interface TimeSeriesRequestOptions extends BaseRequestOptions { * WIDGET API RESPONSES */ -/** Response from {@link WidgetBaseSource#getFeatures}. */ +/** + * Response from {@link WidgetBaseSource#getFeatures}. + * @experimental + * @internal + */ export type FeaturesResponse = {rows: Record[]}; /** Response from {@link WidgetBaseSource#getFormula}. */