Skip to content
This repository was archived by the owner on Mar 4, 2026. It is now read-only.

Commit 6bf944f

Browse files
committed
feat: add remaining map expressions
Adds support for the remaining `map` pipeline expressions; `mapSet`, `mapValues`, `mapEntries`, `mapKeys`. Ported from: firebase/firebase-js-sdk#9483
1 parent 4b4ba13 commit 6bf944f

File tree

5 files changed

+453
-0
lines changed

5 files changed

+453
-0
lines changed

api-report/firestore.api.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,10 +1027,14 @@ abstract class Expression implements firestore.Pipelines.Expression, HasUserData
10271027
log10(): FunctionExpression;
10281028
logicalMaximum(second: Expression | unknown, ...others: Array<Expression | unknown>): FunctionExpression;
10291029
logicalMinimum(second: Expression | unknown, ...others: Array<Expression | unknown>): FunctionExpression;
1030+
mapEntries(): FunctionExpression;
10301031
mapGet(subfield: string): FunctionExpression;
1032+
mapKeys(): FunctionExpression;
10311033
mapMerge(secondMap: Record<string, unknown> | Expression, ...otherMaps: Array<Record<string, unknown> | Expression>): FunctionExpression;
10321034
mapRemove(key: string): FunctionExpression;
10331035
mapRemove(keyExpr: Expression): FunctionExpression;
1036+
mapSet(key: string | Expression, value: unknown, ...moreKeyValues: unknown[]): FunctionExpression;
1037+
mapValues(): FunctionExpression;
10341038
maximum(): AggregateFunction;
10351039
minimum(): AggregateFunction;
10361040
mod(expression: Expression): FunctionExpression;
@@ -1582,12 +1586,18 @@ function logicalMinimum(fieldName: string, second: Expression | unknown, ...othe
15821586
// @beta
15831587
function map(elements: Record<string, unknown>): FunctionExpression;
15841588

1589+
// @beta
1590+
function mapEntries(map: unknown): FunctionExpression;
1591+
15851592
// @beta
15861593
function mapGet(fieldName: string, subField: string): FunctionExpression;
15871594

15881595
// @beta
15891596
function mapGet(mapExpression: Expression, subField: string): FunctionExpression;
15901597

1598+
// @beta
1599+
function mapKeys(map: unknown): FunctionExpression;
1600+
15911601
// @beta
15921602
function mapMerge(mapField: string, secondMap: Record<string, unknown> | Expression, ...otherMaps: Array<Record<string, unknown> | Expression>): FunctionExpression;
15931603

@@ -1606,6 +1616,12 @@ function mapRemove(mapField: string, keyExpr: Expression): FunctionExpression;
16061616
// @beta
16071617
function mapRemove(mapExpr: Expression, keyExpr: Expression): FunctionExpression;
16081618

1619+
// @beta
1620+
function mapSet(map: unknown, key: string | Expression, value: unknown, ...moreKeyValues: unknown[]): FunctionExpression;
1621+
1622+
// @beta
1623+
function mapValues(map: unknown): FunctionExpression;
1624+
16091625
// Warning: (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
16101626
//
16111627
// @public
@@ -1825,6 +1841,10 @@ declare namespace Pipelines {
18251841
dotProduct,
18261842
euclideanDistance,
18271843
mapGet,
1844+
mapEntries,
1845+
mapKeys,
1846+
mapSet,
1847+
mapValues,
18281848
lessThanOrEqual,
18291849
equalAny,
18301850
map,

dev/src/pipelines/expression.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,95 @@ export abstract class Expression
12741274
return new FunctionExpression('map_get', [this, constant(subfield)]);
12751275
}
12761276

1277+
/**
1278+
* @beta
1279+
* Creates an expression that returns a new map with the specified entries added or updated.
1280+
*
1281+
* @remarks
1282+
* Note that `mapSet` only performs shallow updates to the map. Setting a value to `null`
1283+
* will retain the key with a `null` value. To remove a key entirely, use `mapRemove`.
1284+
*
1285+
* @example
1286+
* ```typescript
1287+
* // Set the 'city' to "San Francisco" in the 'address' map
1288+
* field("address").mapSet("city", "San Francisco");
1289+
* ```
1290+
*
1291+
* @param key - The key to set. Must be a string or a constant string expression.
1292+
* @param value - The value to set.
1293+
* @param moreKeyValues - Additional key-value pairs to set.
1294+
* @returns A new `Expression` representing the map with the entries set.
1295+
*/
1296+
mapSet(
1297+
key: string | Expression,
1298+
value: unknown,
1299+
...moreKeyValues: unknown[]
1300+
): FunctionExpression {
1301+
const args = [
1302+
this,
1303+
valueToDefaultExpr(key),
1304+
valueToDefaultExpr(value),
1305+
...moreKeyValues.map(valueToDefaultExpr),
1306+
];
1307+
return new FunctionExpression('map_set', args);
1308+
}
1309+
1310+
/**
1311+
* @beta
1312+
* Creates an expression that returns the keys of a map.
1313+
*
1314+
* Note: While the backend generally preserves insertion order, relying on the
1315+
* order of the output array is not guaranteed and should be avoided.
1316+
*
1317+
* @example
1318+
* ```typescript
1319+
* // Get the keys of the 'address' map
1320+
* field("address").mapKeys();
1321+
* ```
1322+
*
1323+
* @returns A new `Expression` representing the keys of the map.
1324+
*/
1325+
mapKeys(): FunctionExpression {
1326+
return new FunctionExpression('map_keys', [this]);
1327+
}
1328+
1329+
/**
1330+
* @beta
1331+
* Creates an expression that returns the values of a map.
1332+
*
1333+
* Note: While the backend generally preserves insertion order, relying on the
1334+
* order of the output array is not guaranteed and should be avoided.
1335+
*
1336+
* @example
1337+
* ```typescript
1338+
* // Get the values of the 'address' map
1339+
* field("address").mapValues();
1340+
* ```
1341+
*
1342+
* @returns A new `Expression` representing the values of the map.
1343+
*/
1344+
mapValues(): FunctionExpression {
1345+
return new FunctionExpression('map_values', [this]);
1346+
}
1347+
1348+
/**
1349+
* @beta
1350+
* Creates an expression that returns the entries of a map as an array of maps,
1351+
* where each map contains a `"k"` property for the key and a `"v"` property for the value.
1352+
* For example: `[{ k: "key1", v: "value1" }, ...]`.
1353+
*
1354+
* @example
1355+
* ```typescript
1356+
* // Get the entries of the 'address' map
1357+
* field("address").mapEntries();
1358+
* ```
1359+
*
1360+
* @returns A new `Expression` representing the entries of the map.
1361+
*/
1362+
mapEntries(): FunctionExpression {
1363+
return new FunctionExpression('map_entries', [this]);
1364+
}
1365+
12771366
/**
12781367
* @beta
12791368
* Creates an aggregation that counts the number of stage inputs with valid evaluations of the
@@ -6342,6 +6431,93 @@ export function mapGet(
63426431
return fieldOrExpression(fieldOrExpr).mapGet(subField);
63436432
}
63446433

6434+
/**
6435+
* @beta
6436+
* Creates an expression that returns a new map with the specified entries added or updated.
6437+
*
6438+
* Note that `mapSet` only performs shallow updates to the map. Setting a value to `null`
6439+
* will retain the key with a `null` value. To remove a key entirely, use `mapRemove`.
6440+
*
6441+
* @example
6442+
* ```typescript
6443+
* // Set the 'city' to "San Francisco" in the 'address' map field
6444+
* mapSet("address", "city", "San Francisco");
6445+
* ```
6446+
*
6447+
* @param map - The map to set entries in.
6448+
* @param key - The key to set. Must be a string or a constant string expression.
6449+
* @param value - The value to set.
6450+
* @param moreKeyValues - Additional key-value pairs to set.
6451+
* @returns A new `Expression` representing the map with the entries set.
6452+
*/
6453+
export function mapSet(
6454+
map: unknown,
6455+
key: string | Expression,
6456+
value: unknown,
6457+
...moreKeyValues: unknown[]
6458+
): FunctionExpression {
6459+
return fieldOrExpression(map).mapSet(key, value, ...moreKeyValues);
6460+
}
6461+
6462+
/**
6463+
* @beta
6464+
* Creates an expression that returns the keys of a map.
6465+
*
6466+
* Note: While the backend generally preserves insertion order, relying on the
6467+
* order of the output array is not guaranteed and should be avoided.
6468+
*
6469+
* @example
6470+
* ```typescript
6471+
* // Get the keys of the 'address' map field
6472+
* mapKeys("address");
6473+
* ```
6474+
*
6475+
* @param map - The map to get the keys of.
6476+
* @returns A new `Expression` representing the keys of the map.
6477+
*/
6478+
export function mapKeys(map: unknown): FunctionExpression {
6479+
return fieldOrExpression(map).mapKeys();
6480+
}
6481+
6482+
/**
6483+
* @beta
6484+
* Creates an expression that returns the values of a map.
6485+
*
6486+
* Note: While the backend generally preserves insertion order, relying on the
6487+
* order of the output array is not guaranteed and should be avoided.
6488+
*
6489+
* @example
6490+
* ```typescript
6491+
* // Get the values of the 'address' map field
6492+
* mapValues("address");
6493+
* ```
6494+
*
6495+
* @param map - The map to get the values of.
6496+
* @returns A new `Expression` representing the values of the map.
6497+
*/
6498+
export function mapValues(map: unknown): FunctionExpression {
6499+
return fieldOrExpression(map).mapValues();
6500+
}
6501+
6502+
/**
6503+
* @beta
6504+
* Creates an expression that returns the entries of a map as an array of maps,
6505+
* where each map contains a `"k"` property for the key and a `"v"` property for the value.
6506+
* For example: `[{ k: "key1", v: "value1" }, ...]`.
6507+
*
6508+
* @example
6509+
* ```typescript
6510+
* // Get the entries of the 'address' map field
6511+
* mapEntries("address");
6512+
* ```
6513+
*
6514+
* @param map - The map to get the entries of.
6515+
* @returns A new `Expression` representing the entries of the map.
6516+
*/
6517+
export function mapEntries(map: unknown): FunctionExpression {
6518+
return fieldOrExpression(map).mapEntries();
6519+
}
6520+
63456521
/**
63466522
* @beta
63476523
* Creates an aggregation that counts the total number of stage inputs.

dev/src/pipelines/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export {
4646
dotProduct,
4747
euclideanDistance,
4848
mapGet,
49+
mapEntries,
50+
mapKeys,
51+
mapSet,
52+
mapValues,
4953
lessThanOrEqual,
5054
equalAny,
5155
map,

dev/system-test/pipeline.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ import {
8989
dotProduct,
9090
euclideanDistance,
9191
mapGet,
92+
mapEntries,
93+
mapKeys,
94+
mapSet,
95+
mapValues,
9296
lessThanOrEqual,
9397
equalAny,
9498
notEqualAny,
@@ -2830,6 +2834,103 @@ describe.skipClassic('Pipeline class', () => {
28302834
);
28312835
});
28322836

2837+
it('test mapSet', async () => {
2838+
const snapshot = await firestore
2839+
.pipeline()
2840+
.collection(randomCol.path)
2841+
.limit(1)
2842+
.replaceWith(map({}))
2843+
.addFields(
2844+
mapSet(map({}), 'a', 1).as('simple'),
2845+
mapSet(map({a: 1}), 'b', 2).as('add'),
2846+
mapSet(map({a: 1}), 'a', 2).as('overwrite'),
2847+
mapSet(map({a: 1, b: 2}), 'a', 3, 'c', 4).as('multi'),
2848+
mapSet(map({a: 1}), 'a', field('non_existent')).as('remove'),
2849+
mapSet(map({a: 1}), 'b', null).as('setNull'),
2850+
mapSet(map({a: {b: 1}}), 'a.b', 2).as('setDotted'),
2851+
mapSet(map({}), '', 'empty').as('setEmptyKey'),
2852+
mapSet(map({a: 1}), 'b', add(constant(1), constant(2))).as(
2853+
'setExprVal',
2854+
),
2855+
mapSet(map({}), 'obj', map({hidden: true})).as('setNestedMap'),
2856+
mapSet(map({}), '~!@#$%^&*()_+', 'special').as('setSpecialChars'),
2857+
)
2858+
.execute();
2859+
expectResults(snapshot, {
2860+
simple: {a: 1},
2861+
add: {a: 1, b: 2},
2862+
overwrite: {a: 2},
2863+
multi: {a: 3, b: 2, c: 4},
2864+
remove: {},
2865+
setNull: {a: 1, b: null},
2866+
setDotted: {a: {b: 1}, 'a.b': 2},
2867+
setEmptyKey: {'': 'empty'},
2868+
setExprVal: {a: 1, b: 3},
2869+
setNestedMap: {obj: {hidden: true}},
2870+
setSpecialChars: {'~!@#$%^&*()_+': 'special'},
2871+
});
2872+
});
2873+
2874+
it('test mapKeys', async () => {
2875+
const snapshot = await firestore
2876+
.pipeline()
2877+
.collection(randomCol.path)
2878+
.limit(1)
2879+
.replaceWith(map({a: 1, b: 2, c: 3}))
2880+
.addFields(
2881+
mapKeys(map({a: 1, b: 2})).as('keys'),
2882+
mapKeys(map({})).as('empty_keys'),
2883+
mapKeys(map({a: {nested: true}})).as('nested_keys'),
2884+
)
2885+
.execute();
2886+
2887+
const res = snapshot.results[0].data();
2888+
expect(res.keys).to.have.members(['a', 'b']);
2889+
expect(res.empty_keys).to.deep.equal([]);
2890+
expect(res.nested_keys).to.have.members(['a']);
2891+
});
2892+
2893+
it('test mapValues', async () => {
2894+
const snapshot = await firestore
2895+
.pipeline()
2896+
.collection(randomCol.path)
2897+
.limit(1)
2898+
.replaceWith(map({a: 1, b: 2}))
2899+
.addFields(
2900+
mapValues(map({a: 1, b: 2})).as('values'),
2901+
mapValues(map({})).as('empty_values'),
2902+
mapValues(map({a: {nested: true}})).as('nested_values'),
2903+
)
2904+
.execute();
2905+
const res = snapshot.results[0].data();
2906+
expect(res.values).to.have.members([1, 2]);
2907+
expect(res.empty_values).to.deep.equal([]);
2908+
expect(res.nested_values).to.deep.include.members([{nested: true}]);
2909+
});
2910+
2911+
it('test mapEntries', async () => {
2912+
const snapshot = await firestore
2913+
.pipeline()
2914+
.collection(randomCol.path)
2915+
.limit(1)
2916+
.replaceWith(map({a: 1, b: 2}))
2917+
.addFields(
2918+
mapEntries(map({a: 1, b: 2})).as('entries'),
2919+
mapEntries(map({})).as('empty_entries'),
2920+
mapEntries(map({a: {nested: true}})).as('nested_entries'),
2921+
)
2922+
.execute();
2923+
const res = snapshot.results[0].data();
2924+
expect(res.entries).to.deep.include.members([
2925+
{k: 'a', v: 1},
2926+
{k: 'b', v: 2},
2927+
]);
2928+
expect(res.empty_entries).to.deep.equal([]);
2929+
expect(res.nested_entries).to.deep.include.members([
2930+
{k: 'a', v: {nested: true}},
2931+
]);
2932+
});
2933+
28332934
it('testDistanceFunctions', async () => {
28342935
const sourceVector = FieldValue.vector([0.1, 0.1]);
28352936
const targetVector = FieldValue.vector([0.5, 0.8]);

0 commit comments

Comments
 (0)