Skip to content

Commit 920c9f7

Browse files
authored
feat: add isType expression (#2483)
1 parent dd8897e commit 920c9f7

File tree

5 files changed

+268
-0
lines changed

5 files changed

+268
-0
lines changed

api-report/firestore.api.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,7 @@ abstract class Expression implements firestore.Pipelines.Expression, HasUserData
10141014
ifError(catchValue: unknown): FunctionExpression;
10151015
isAbsent(): BooleanExpression;
10161016
isError(): BooleanExpression;
1017+
isType(type: Type): BooleanExpression;
10171018
join(delimiterExpression: Expression): Expression;
10181019
join(delimiter: string): Expression;
10191020
length(): FunctionExpression;
@@ -1508,6 +1509,12 @@ function isAbsent(field: string): BooleanExpression;
15081509
// @beta
15091510
function isError(value: Expression): BooleanExpression;
15101511

1512+
// @beta
1513+
function isType(fieldName: string, type: Type): BooleanExpression;
1514+
1515+
// @beta
1516+
function isType(expression: Expression, type: Type): BooleanExpression;
1517+
15111518
// @beta
15121519
function join(arrayFieldName: string, delimiter: string): Expression;
15131520

@@ -1940,6 +1947,8 @@ declare namespace Pipelines {
19401947
currentTimestamp,
19411948
arrayConcat,
19421949
type,
1950+
isType,
1951+
Type,
19431952
timestampTruncate,
19441953
split
19451954
}
@@ -2660,6 +2669,9 @@ function trunc(fieldName: string, decimalPlaces: number | Expression): FunctionE
26602669
// @beta
26612670
function trunc(expression: Expression, decimalPlaces: number | Expression): FunctionExpression;
26622671

2672+
// @beta
2673+
type Type = 'null' | 'array' | 'boolean' | 'bytes' | 'timestamp' | 'geo_point' | 'number' | 'int32' | 'int64' | 'float64' | 'decimal128' | 'map' | 'reference' | 'string' | 'vector' | 'max_key' | 'min_key' | 'object_id' | 'regex' | 'request_timestamp';
2674+
26632675
// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
26642676
// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
26652677
//

dev/src/pipelines/expression.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,39 @@ import {
2929
import {HasUserData, Serializer, validateUserInput} from '../serializer';
3030
import {cast} from '../util';
3131

32+
/**
33+
* @beta
34+
*
35+
* An enumeration of the different types generated by the Firestore backend.
36+
*
37+
* <ul>
38+
* <li>Numerics evaluate directly to backend representation (`int64` or `float64`), not JS `number`.</li>
39+
* <li>JavaScript `Date` and firestore `Timestamp` objects strictly evaluate to `'timestamp'`.</li>
40+
* <li>Advanced configurations parsing backend types (such as `decimal128`, `max_key` or `min_key` from BSON) are also incorporated in this union string type. Note that `decimal128` is a backend-only numeric type that the JavaScript SDK cannot create natively, but can be evaluated in pipelines.</li>
41+
* </ul>
42+
*/
43+
export type Type =
44+
| 'null'
45+
| 'array'
46+
| 'boolean'
47+
| 'bytes'
48+
| 'timestamp'
49+
| 'geo_point'
50+
| 'number'
51+
| 'int32'
52+
| 'int64'
53+
| 'float64'
54+
| 'decimal128'
55+
| 'map'
56+
| 'reference'
57+
| 'string'
58+
| 'vector'
59+
| 'max_key'
60+
| 'min_key'
61+
| 'object_id'
62+
| 'regex'
63+
| 'request_timestamp';
64+
3265
/**
3366
* @beta
3467
* Represents an expression that can be evaluated to a value within the execution of a `Pipeline`.
@@ -2497,6 +2530,28 @@ export abstract class Expression
24972530
return new FunctionExpression('type', [this]);
24982531
}
24992532

2533+
/**
2534+
* @beta
2535+
* Creates an expression that checks if the result of this expression is of the given type.
2536+
*
2537+
* @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data.
2538+
*
2539+
* @example
2540+
* ```typescript
2541+
* // Check if the 'price' field is specifically an integer (not just 'number')
2542+
* field('price').isType('int64');
2543+
* ```
2544+
*
2545+
* @param type The type to check for.
2546+
* @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise.
2547+
*/
2548+
isType(type: Type): BooleanExpression {
2549+
return new FunctionExpression('is_type', [
2550+
this,
2551+
constant(type),
2552+
]).asBoolean();
2553+
}
2554+
25002555
// TODO(new-expression): Add new expression method definitions above this line
25012556

25022557
/**
@@ -8456,6 +8511,48 @@ export function type(
84568511
return fieldOrExpression(fieldNameOrExpression).type();
84578512
}
84588513

8514+
/**
8515+
* @beta
8516+
* Creates an expression that checks if the value in the specified field is of the given type.
8517+
*
8518+
* @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data.
8519+
*
8520+
* @example
8521+
* ```typescript
8522+
* // Check if the 'price' field is a floating point number (evaluating to true inside pipeline conditionals)
8523+
* isType('price', 'float64');
8524+
* ```
8525+
*
8526+
* @param fieldName The name of the field to check.
8527+
* @param type The type to check for.
8528+
* @returns A new `BooleanExpression` that evaluates to true if the field's value is of the given type, false otherwise.
8529+
*/
8530+
export function isType(fieldName: string, type: Type): BooleanExpression;
8531+
8532+
/**
8533+
* @beta
8534+
* Creates an expression that checks if the result of an expression is of the given type.
8535+
*
8536+
* @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data.
8537+
*
8538+
* @example
8539+
* ```typescript
8540+
* // Check if the result of a calculation is a number
8541+
* isType(add('count', 1), 'number')
8542+
* ```
8543+
*
8544+
* @param expression The expression to check.
8545+
* @param type The type to check for.
8546+
* @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise.
8547+
*/
8548+
export function isType(expression: Expression, type: Type): BooleanExpression;
8549+
export function isType(
8550+
fieldNameOrExpression: string | Expression,
8551+
type: Type,
8552+
): BooleanExpression {
8553+
return fieldOrExpression(fieldNameOrExpression).isType(type);
8554+
}
8555+
84598556
// TODO(new-expression): Add new top-level expression function definitions above this line
84608557

84618558
/**

dev/src/pipelines/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ export {
130130
currentTimestamp,
131131
arrayConcat,
132132
type,
133+
isType,
134+
Type,
133135
timestampTruncate,
134136
split,
135137
// TODO(new-expression): Add new expression exports above this line

dev/system-test/pipeline.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ import {
124124
currentTimestamp,
125125
arrayConcat,
126126
type,
127+
isType,
127128
timestampTruncate,
128129
split,
129130
// TODO(new-expression): add new expression imports above this line
@@ -4386,6 +4387,74 @@ describe.skipClassic('Pipeline class', () => {
43864387
});
43874388
});
43884389

4390+
it('supports isType', async () => {
4391+
const result = await firestore
4392+
.pipeline()
4393+
.collection(randomCol.path)
4394+
.replaceWith(
4395+
map({
4396+
int: constant(1),
4397+
float: constant(1.1),
4398+
str: constant('a string'),
4399+
bool: constant(true),
4400+
null: constant(null),
4401+
geoPoint: constant(new GeoPoint(0.1, 0.2)),
4402+
timestamp: constant(new Timestamp(123456, 0)),
4403+
bytes: constant(new Uint8Array([1, 2, 3])),
4404+
docRef: constant(firestore.doc(`${randomCol.path}/bar`)),
4405+
vector: constant(FieldValue.vector([1, 2, 3])),
4406+
map: map({
4407+
numberK: 1,
4408+
stringK: 'a string',
4409+
}),
4410+
array: array([1, '2', true]),
4411+
}),
4412+
)
4413+
.select(
4414+
isType(field('int'), 'int64').as('isInt64'),
4415+
isType(field('int'), 'number').as('isInt64IsNumber'),
4416+
isType(field('int'), 'decimal128').as('isInt64IsDecimal128'),
4417+
field('float').isType('float64').as('isFloat64'),
4418+
field('float').isType('number').as('isFloat64IsNumber'),
4419+
field('float').isType('decimal128').as('isFloat64IsDecimal128'),
4420+
isType('str', 'string').as('isStr'),
4421+
isType('int', 'string').as('isNumStr'),
4422+
field('bool').isType('boolean').as('isBool'),
4423+
isType('null', 'null').as('isNull'),
4424+
field('geoPoint').isType('geo_point').as('isGeoPoint'),
4425+
isType('timestamp', 'timestamp').as('isTimestamp'),
4426+
field('bytes').isType('bytes').as('isBytes'),
4427+
isType('docRef', 'reference').as('isDocRef'),
4428+
field('vector').isType('vector').as('isVector'),
4429+
isType('map', 'map').as('isMap'),
4430+
field('array').isType('array').as('isArray'),
4431+
field('str').isType('int64').as('isStrNum'),
4432+
)
4433+
.limit(1)
4434+
.execute();
4435+
4436+
expectResults(result, {
4437+
isInt64: true,
4438+
isInt64IsNumber: true,
4439+
isInt64IsDecimal128: false,
4440+
isFloat64: true,
4441+
isFloat64IsNumber: true,
4442+
isFloat64IsDecimal128: false,
4443+
isStr: true,
4444+
isNumStr: false,
4445+
isBool: true,
4446+
isNull: true,
4447+
isGeoPoint: true,
4448+
isTimestamp: true,
4449+
isBytes: true,
4450+
isDocRef: true,
4451+
isVector: true,
4452+
isMap: true,
4453+
isArray: true,
4454+
isStrNum: false,
4455+
});
4456+
});
4457+
43894458
// TODO(new-expression): Add new expression tests above this line
43904459
});
43914460

types/firestore.d.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5136,6 +5136,23 @@ declare namespace FirebaseFirestore {
51365136
*/
51375137
type(): FunctionExpression;
51385138

5139+
/**
5140+
* @beta
5141+
* Creates an expression that checks if the result of this expression is of the given type.
5142+
*
5143+
* @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data.
5144+
*
5145+
* @example
5146+
* ```typescript
5147+
* // Check if the 'price' field is specifically an integer (not just 'number')
5148+
* field('price').isType('int64');
5149+
* ```
5150+
*
5151+
* @param type The type to check for.
5152+
* @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise.
5153+
*/
5154+
isType(type: Type): BooleanExpression;
5155+
51395156
// TODO(new-expression): Add new expression method declarations above this line
51405157
/**
51415158
* @beta
@@ -10128,6 +10145,39 @@ declare namespace FirebaseFirestore {
1012810145
timezone?: string | Expression,
1012910146
): FunctionExpression;
1013010147

10148+
/**
10149+
* @beta
10150+
*
10151+
* An enumeration of the different types generated by the Firestore backend.
10152+
*
10153+
* <ul>
10154+
* <li>Numerics evaluate directly to backend representation (`int64` or `float64`), not JS `number`.</li>
10155+
* <li>JavaScript `Date` and firestore `Timestamp` objects strictly evaluate to `'timestamp'`.</li>
10156+
* <li>Advanced configurations parsing backend types (such as `decimal128`, `max_key` or `min_key` from BSON) are also incorporated in this union string type. Note that `decimal128` is a backend-only numeric type that the JavaScript SDK cannot create natively, but can be evaluated in pipelines.</li>
10157+
* </ul>
10158+
*/
10159+
export type Type =
10160+
| 'null'
10161+
| 'array'
10162+
| 'boolean'
10163+
| 'bytes'
10164+
| 'timestamp'
10165+
| 'geo_point'
10166+
| 'number'
10167+
| 'int32'
10168+
| 'int64'
10169+
| 'float64'
10170+
| 'decimal128'
10171+
| 'map'
10172+
| 'reference'
10173+
| 'string'
10174+
| 'vector'
10175+
| 'max_key'
10176+
| 'min_key'
10177+
| 'object_id'
10178+
| 'regex'
10179+
| 'request_timestamp';
10180+
1013110181
/**
1013210182
* @beta
1013310183
* Creates an expression that returns the data type of the data in the specified field.
@@ -10155,6 +10205,44 @@ declare namespace FirebaseFirestore {
1015510205
*/
1015610206
export function type(expression: Expression): FunctionExpression;
1015710207

10208+
/**
10209+
* @beta
10210+
* Creates an expression that checks if the value in the specified field is of the given type.
10211+
*
10212+
* @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data.
10213+
*
10214+
* @example
10215+
* ```typescript
10216+
* // Check if the 'price' field is a floating point number (evaluating to true inside pipeline conditionals)
10217+
* isType('price', 'float64');
10218+
* ```
10219+
*
10220+
* @param fieldName The name of the field to check.
10221+
* @param type The type to check for.
10222+
* @returns A new `BooleanExpression` that evaluates to true if the field's value is of the given type, false otherwise.
10223+
*/
10224+
export function isType(fieldName: string, type: Type): BooleanExpression;
10225+
/**
10226+
* @beta
10227+
* Creates an expression that checks if the result of an expression is of the given type.
10228+
*
10229+
* @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data.
10230+
*
10231+
* @example
10232+
* ```typescript
10233+
* // Check if the result of a calculation is a number
10234+
* isType(add('count', 1), 'number')
10235+
* ```
10236+
*
10237+
* @param expression The expression to check.
10238+
* @param type The type to check for.
10239+
* @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise.
10240+
*/
10241+
export function isType(
10242+
expression: Expression,
10243+
type: Type,
10244+
): BooleanExpression;
10245+
1015810246
// TODO(new-expression): Add new top-level expression function declarations above this line
1015910247
/**
1016010248
* @beta

0 commit comments

Comments
 (0)