Skip to content

Commit f7fe3d5

Browse files
authored
CARTO: Do not mutate passed in loadOptions objects (#9580)
1 parent a888b9b commit f7fe3d5

File tree

7 files changed

+138
-33
lines changed

7 files changed

+138
-33
lines changed

modules/carto/src/layers/cluster-tile-layer.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import {DEFAULT_TILE_SIZE} from '../constants';
3838
import QuadbinTileset2D from './quadbin-tileset-2d';
3939
import {getQuadbinPolygon} from './quadbin-utils';
4040
import CartoSpatialTileLoader from './schema/carto-spatial-tile-loader';
41-
import {injectAccessToken, TilejsonPropType} from './utils';
41+
import {TilejsonPropType, mergeLoadOptions} from './utils';
4242
import type {TilejsonResult} from '@carto/api-client';
4343

4444
registerLoaders([CartoSpatialTileLoader]);
@@ -216,11 +216,11 @@ export default class ClusterTileLayer<
216216
static defaultProps = defaultProps;
217217

218218
getLoadOptions(): any {
219-
const loadOptions = super.getLoadOptions() || {};
220219
const tileJSON = this.props.data as TilejsonResult;
221-
injectAccessToken(loadOptions, tileJSON.accessToken);
222-
loadOptions.cartoSpatialTile = {...loadOptions.cartoSpatialTile, scheme: 'quadbin'};
223-
return loadOptions;
220+
return mergeLoadOptions(super.getLoadOptions(), {
221+
fetch: {headers: {Authorization: `Bearer ${tileJSON.accessToken}`}},
222+
cartoSpatialTile: {scheme: 'quadbin'}
223+
});
224224
}
225225

226226
renderLayers(): Layer | null | LayersList {

modules/carto/src/layers/h3-tile-layer.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
// SPDX-License-Identifier: MIT
33
// Copyright (c) vis.gl contributors
44

5-
import {CompositeLayer, CompositeLayerProps, Layer, LayersList, DefaultProps} from '@deck.gl/core';
5+
import {CompositeLayer, CompositeLayerProps, DefaultProps} from '@deck.gl/core';
66
import {H3HexagonLayer, H3HexagonLayerProps} from '@deck.gl/geo-layers';
77
import H3Tileset2D, {getHexagonResolution} from './h3-tileset-2d';
88
import SpatialIndexTileLayer, {SpatialIndexTileLayerProps} from './spatial-index-tile-layer';
99
import type {TilejsonResult} from '@carto/api-client';
10-
import {injectAccessToken, TilejsonPropType} from './utils';
10+
import {TilejsonPropType, mergeLoadOptions} from './utils';
1111
import {DEFAULT_TILE_SIZE} from '../constants';
1212

1313
export const renderSubLayers = props => {
@@ -47,11 +47,11 @@ export default class H3TileLayer<DataT = any, ExtraPropsT extends {} = {}> exten
4747
}
4848

4949
getLoadOptions(): any {
50-
const loadOptions = super.getLoadOptions() || {};
5150
const tileJSON = this.props.data as TilejsonResult;
52-
injectAccessToken(loadOptions, tileJSON.accessToken);
53-
loadOptions.cartoSpatialTile = {...loadOptions.cartoSpatialTile, scheme: 'h3'};
54-
return loadOptions;
51+
return mergeLoadOptions(super.getLoadOptions(), {
52+
fetch: {headers: {Authorization: `Bearer ${tileJSON.accessToken}`}},
53+
cartoSpatialTile: {scheme: 'h3'}
54+
});
5555
}
5656

5757
renderLayers(): SpatialIndexTileLayer | null {

modules/carto/src/layers/quadbin-tile-layer.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
// SPDX-License-Identifier: MIT
33
// Copyright (c) vis.gl contributors
44

5-
import {CompositeLayer, CompositeLayerProps, Layer, LayersList, DefaultProps} from '@deck.gl/core';
5+
import {CompositeLayer, CompositeLayerProps, DefaultProps} from '@deck.gl/core';
66
import QuadbinLayer, {QuadbinLayerProps} from './quadbin-layer';
77
import QuadbinTileset2D from './quadbin-tileset-2d';
88
import SpatialIndexTileLayer, {SpatialIndexTileLayerProps} from './spatial-index-tile-layer';
99
import {hexToBigInt} from 'quadbin';
1010
import type {TilejsonResult} from '@carto/api-client';
11-
import {injectAccessToken, TilejsonPropType} from './utils';
11+
import {TilejsonPropType, mergeLoadOptions} from './utils';
1212
import {DEFAULT_TILE_SIZE} from '../constants';
1313

1414
export const renderSubLayers = props => {
@@ -43,11 +43,11 @@ export default class QuadbinTileLayer<
4343
static defaultProps = defaultProps;
4444

4545
getLoadOptions(): any {
46-
const loadOptions = super.getLoadOptions() || {};
4746
const tileJSON = this.props.data as TilejsonResult;
48-
injectAccessToken(loadOptions, tileJSON.accessToken);
49-
loadOptions.cartoSpatialTile = {...loadOptions.cartoSpatialTile, scheme: 'quadbin'};
50-
return loadOptions;
47+
return mergeLoadOptions(super.getLoadOptions(), {
48+
fetch: {headers: {Authorization: `Bearer ${tileJSON.accessToken}`}},
49+
cartoSpatialTile: {scheme: 'quadbin'}
50+
});
5151
}
5252

5353
renderLayers(): SpatialIndexTileLayer | null {

modules/carto/src/layers/raster-tile-layer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import RasterLayer, {RasterLayerProps} from './raster-layer';
1414
import QuadbinTileset2D from './quadbin-tileset-2d';
1515
import type {TilejsonResult} from '@carto/api-client';
16-
import {injectAccessToken, TilejsonPropType} from './utils';
16+
import {TilejsonPropType, mergeLoadOptions} from './utils';
1717
import {DEFAULT_TILE_SIZE} from '../constants';
1818
import {TileLayer, TileLayerProps} from '@deck.gl/geo-layers';
1919
import {copy, PostProcessModifier} from './post-process-utils';
@@ -62,10 +62,10 @@ export default class RasterTileLayer<
6262
static defaultProps = defaultProps;
6363

6464
getLoadOptions(): any {
65-
const loadOptions = super.getLoadOptions() || {};
6665
const tileJSON = this.props.data as TilejsonResult;
67-
injectAccessToken(loadOptions, tileJSON.accessToken);
68-
return loadOptions;
66+
return mergeLoadOptions(super.getLoadOptions(), {
67+
fetch: {headers: {Authorization: `Bearer ${tileJSON.accessToken}`}}
68+
});
6969
}
7070

7171
renderLayers(): Layer | null | LayersList {

modules/carto/src/layers/utils.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,38 @@ import {_deepEqual as deepEqual} from '@deck.gl/core';
99
import type {TilejsonResult} from '@carto/api-client';
1010

1111
/**
12-
* Adds access token to Authorization header in loadOptions
12+
* Merges load options with additional options, creating a new object without mutating the input.
13+
* Handles nested objects through recursive deep merge with protection against circular references.
1314
*/
14-
export function injectAccessToken(loadOptions: any, accessToken: string): void {
15-
if (!loadOptions?.fetch?.headers?.Authorization) {
16-
loadOptions.fetch = {
17-
...loadOptions.fetch,
18-
headers: {...loadOptions.fetch?.headers, Authorization: `Bearer ${accessToken}`}
19-
};
15+
export function mergeLoadOptions(loadOptions: any, additionalOptions: any, depth = 0): any {
16+
if (!loadOptions) {
17+
return additionalOptions;
2018
}
19+
if (!additionalOptions) {
20+
return loadOptions;
21+
}
22+
23+
// Safety check against deep recursion
24+
if (depth > 10) {
25+
return additionalOptions;
26+
}
27+
28+
const result = {...loadOptions};
29+
30+
for (const key in additionalOptions) {
31+
const value = additionalOptions[key];
32+
// Skip circular references
33+
if (value === loadOptions || value === additionalOptions) {
34+
continue;
35+
}
36+
if (typeof value === 'object' && value !== null) {
37+
result[key] = mergeLoadOptions(loadOptions[key], value, depth + 1);
38+
} else {
39+
result[key] = value;
40+
}
41+
}
42+
43+
return result;
2144
}
2245

2346
export function mergeBoundaryData(geometry: VectorTile, properties: PropertiesTile): VectorTile {

modules/carto/src/layers/vector-tile-layer.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
import {GeoJsonLayer} from '@deck.gl/layers';
2121

2222
import type {TilejsonResult} from '@carto/api-client';
23-
import {TilejsonPropType, injectAccessToken, mergeBoundaryData} from './utils';
23+
import {TilejsonPropType, mergeLoadOptions, mergeBoundaryData} from './utils';
2424
import {DEFAULT_TILE_SIZE} from '../constants';
2525
import PointLabelLayer from './point-label-layer';
2626

@@ -75,11 +75,11 @@ export default class VectorTileLayer<
7575
}
7676

7777
getLoadOptions(): any {
78-
const loadOptions = super.getLoadOptions() || {};
7978
const tileJSON = this.props.data as TilejsonResult;
80-
injectAccessToken(loadOptions, tileJSON.accessToken);
81-
loadOptions.gis = {format: 'binary'}; // Use binary for MVT loading
82-
return loadOptions;
79+
return mergeLoadOptions(super.getLoadOptions(), {
80+
fetch: {headers: {Authorization: `Bearer ${tileJSON.accessToken}`}},
81+
gis: {format: 'binary'} // Use binary for MVT loading
82+
});
8383
}
8484

8585
/* eslint-disable camelcase */
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// deck.gl
2+
// SPDX-License-Identifier: MIT
3+
// Copyright (c) vis.gl contributors
4+
5+
import test from 'tape-promise/tape';
6+
import {mergeLoadOptions} from '../../src/layers/utils';
7+
8+
test('utils#mergeLoadOptions', t => {
9+
const accessToken = 'test-token';
10+
const loadOptions = {
11+
fetch: {
12+
headers: {
13+
'Content-Type': 'application/json'
14+
}
15+
}
16+
};
17+
18+
const result = mergeLoadOptions(loadOptions, {
19+
fetch: {
20+
headers: {
21+
Authorization: `Bearer ${accessToken}`
22+
}
23+
}
24+
});
25+
26+
t.deepEqual(result, {
27+
fetch: {
28+
headers: {
29+
'Content-Type': 'application/json',
30+
Authorization: `Bearer ${accessToken}`
31+
}
32+
}
33+
});
34+
35+
// Test with no existing headers
36+
const loadOptions2 = {
37+
fetch: {}
38+
};
39+
40+
const result2 = mergeLoadOptions(loadOptions2, {
41+
fetch: {
42+
headers: {
43+
Authorization: `Bearer ${accessToken}`
44+
}
45+
}
46+
});
47+
48+
t.deepEqual(result2, {
49+
fetch: {
50+
headers: {
51+
Authorization: `Bearer ${accessToken}`
52+
}
53+
}
54+
});
55+
56+
// Test with no existing fetch
57+
const loadOptions3 = {};
58+
59+
const result3 = mergeLoadOptions(loadOptions3, {
60+
fetch: {
61+
headers: {
62+
Authorization: `Bearer ${accessToken}`
63+
}
64+
}
65+
});
66+
67+
t.deepEqual(result3, {
68+
fetch: {
69+
headers: {
70+
Authorization: `Bearer ${accessToken}`
71+
}
72+
}
73+
});
74+
75+
// Test with no additional options
76+
const result4 = mergeLoadOptions(loadOptions, null);
77+
t.deepEqual(result4, loadOptions);
78+
79+
// Test with no load options
80+
const result5 = mergeLoadOptions(null, loadOptions);
81+
t.deepEqual(result5, loadOptions);
82+
});

0 commit comments

Comments
 (0)