Skip to content

Commit 27b46bc

Browse files
committed
feat: add isType expression
Adds support for the `isType` expression. Ported from firebase/firebase-js-sdk#9484.
1 parent 4b4ba13 commit 27b46bc

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;
@@ -1501,6 +1502,12 @@ function isAbsent(field: string): BooleanExpression;
15011502
// @beta
15021503
function isError(value: Expression): BooleanExpression;
15031504

1505+
// @beta
1506+
function isType(fieldName: string, type: Type): BooleanExpression;
1507+
1508+
// @beta
1509+
function isType(expression: Expression, type: Type): BooleanExpression;
1510+
15041511
// @beta
15051512
function join(arrayFieldName: string, delimiter: string): Expression;
15061513

@@ -1901,6 +1908,8 @@ declare namespace Pipelines {
19011908
currentTimestamp,
19021909
arrayConcat,
19031910
type,
1911+
isType,
1912+
Type,
19041913
timestampTruncate,
19051914
split
19061915
}
@@ -2582,6 +2591,9 @@ function trim(fieldName: string, valueToTrim?: string | Expression): FunctionExp
25822591
// @beta
25832592
function trim(stringExpression: Expression, valueToTrim?: string | Expression): FunctionExpression;
25842593

2594+
// @beta
2595+
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';
2596+
25852597
// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
25862598
// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
25872599
//

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`.
@@ -2349,6 +2382,28 @@ export abstract class Expression
23492382
return new FunctionExpression('type', [this]);
23502383
}
23512384

2385+
/**
2386+
* @beta
2387+
* Creates an expression that checks if the result of this expression is of the given type.
2388+
*
2389+
* @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data.
2390+
*
2391+
* @example
2392+
* ```typescript
2393+
* // Check if the 'price' field is specifically an integer (not just 'number')
2394+
* field('price').isType('int64');
2395+
* ```
2396+
*
2397+
* @param type - The type to check for.
2398+
* @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise.
2399+
*/
2400+
isType(type: Type): BooleanExpression {
2401+
return new FunctionExpression('is_type', [
2402+
this,
2403+
constant(type),
2404+
]).asBoolean();
2405+
}
2406+
23522407
// TODO(new-expression): Add new expression method definitions above this line
23532408

23542409
/**
@@ -8021,6 +8076,48 @@ export function type(
80218076
return fieldOrExpression(fieldNameOrExpression).type();
80228077
}
80238078

8079+
/**
8080+
* @beta
8081+
* Creates an expression that checks if the value in the specified field is of the given type.
8082+
*
8083+
* @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data.
8084+
*
8085+
* @example
8086+
* ```typescript
8087+
* // Check if the 'price' field is a floating point number (evaluating to true inside pipeline conditionals)
8088+
* isType('price', 'float64');
8089+
* ```
8090+
*
8091+
* @param fieldName - The name of the field to check.
8092+
* @param type - The type to check for.
8093+
* @returns A new `BooleanExpression` that evaluates to true if the field's value is of the given type, false otherwise.
8094+
*/
8095+
export function isType(fieldName: string, type: Type): BooleanExpression;
8096+
8097+
/**
8098+
* @beta
8099+
* Creates an expression that checks if the result of an expression is of the given type.
8100+
*
8101+
* @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data.
8102+
*
8103+
* @example
8104+
* ```typescript
8105+
* // Check if the result of a calculation is a number
8106+
* isType(add('count', 1), 'number')
8107+
* ```
8108+
*
8109+
* @param expression - The expression to check.
8110+
* @param type - The type to check for.
8111+
* @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise.
8112+
*/
8113+
export function isType(expression: Expression, type: Type): BooleanExpression;
8114+
export function isType(
8115+
fieldNameOrExpression: string | Expression,
8116+
type: Type,
8117+
): BooleanExpression {
8118+
return fieldOrExpression(fieldNameOrExpression).isType(type);
8119+
}
8120+
80248121
// TODO(new-expression): Add new top-level expression function definitions above this line
80258122

80268123
/**

dev/src/pipelines/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ export {
122122
currentTimestamp,
123123
arrayConcat,
124124
type,
125+
isType,
126+
Type,
125127
timestampTruncate,
126128
split,
127129
// 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
@@ -116,6 +116,7 @@ import {
116116
currentTimestamp,
117117
arrayConcat,
118118
type,
119+
isType,
119120
timestampTruncate,
120121
split,
121122
// TODO(new-expression): add new expression imports above this line
@@ -4208,6 +4209,74 @@ describe.skipClassic('Pipeline class', () => {
42084209
});
42094210
});
42104211

4212+
it('supports isType', async () => {
4213+
const result = await firestore
4214+
.pipeline()
4215+
.collection(randomCol.path)
4216+
.replaceWith(
4217+
map({
4218+
int: constant(1),
4219+
float: constant(1.1),
4220+
str: constant('a string'),
4221+
bool: constant(true),
4222+
null: constant(null),
4223+
geoPoint: constant(new GeoPoint(0.1, 0.2)),
4224+
timestamp: constant(new Timestamp(123456, 0)),
4225+
bytes: constant(new Uint8Array([1, 2, 3])),
4226+
docRef: constant(firestore.doc(`${randomCol.path}/bar`)),
4227+
vector: constant(FieldValue.vector([1, 2, 3])),
4228+
map: map({
4229+
numberK: 1,
4230+
stringK: 'a string',
4231+
}),
4232+
array: array([1, '2', true]),
4233+
}),
4234+
)
4235+
.select(
4236+
isType(field('int'), 'int64').as('isInt64'),
4237+
isType(field('int'), 'number').as('isInt64IsNumber'),
4238+
isType(field('int'), 'decimal128').as('isInt64IsDecimal128'),
4239+
field('float').isType('float64').as('isFloat64'),
4240+
field('float').isType('number').as('isFloat64IsNumber'),
4241+
field('float').isType('decimal128').as('isFloat64IsDecimal128'),
4242+
isType('str', 'string').as('isStr'),
4243+
isType('int', 'string').as('isNumStr'),
4244+
field('bool').isType('boolean').as('isBool'),
4245+
isType('null', 'null').as('isNull'),
4246+
field('geoPoint').isType('geo_point').as('isGeoPoint'),
4247+
isType('timestamp', 'timestamp').as('isTimestamp'),
4248+
field('bytes').isType('bytes').as('isBytes'),
4249+
isType('docRef', 'reference').as('isDocRef'),
4250+
field('vector').isType('vector').as('isVector'),
4251+
isType('map', 'map').as('isMap'),
4252+
field('array').isType('array').as('isArray'),
4253+
field('str').isType('int64').as('isStrNum'),
4254+
)
4255+
.limit(1)
4256+
.execute();
4257+
4258+
expectResults(result, {
4259+
isInt64: true,
4260+
isInt64IsNumber: true,
4261+
isInt64IsDecimal128: false,
4262+
isFloat64: true,
4263+
isFloat64IsNumber: true,
4264+
isFloat64IsDecimal128: false,
4265+
isStr: true,
4266+
isNumStr: false,
4267+
isBool: true,
4268+
isNull: true,
4269+
isGeoPoint: true,
4270+
isTimestamp: true,
4271+
isBytes: true,
4272+
isDocRef: true,
4273+
isVector: true,
4274+
isMap: true,
4275+
isArray: true,
4276+
isStrNum: false,
4277+
});
4278+
});
4279+
42114280
// TODO(new-expression): Add new expression tests above this line
42124281
});
42134282

types/firestore.d.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5018,6 +5018,23 @@ declare namespace FirebaseFirestore {
50185018
*/
50195019
type(): FunctionExpression;
50205020

5021+
/**
5022+
* @beta
5023+
* Creates an expression that checks if the result of this expression is of the given type.
5024+
*
5025+
* @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data.
5026+
*
5027+
* @example
5028+
* ```typescript
5029+
* // Check if the 'price' field is specifically an integer (not just 'number')
5030+
* field('price').isType('int64');
5031+
* ```
5032+
*
5033+
* @param type - The type to check for.
5034+
* @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise.
5035+
*/
5036+
isType(type: Type): BooleanExpression;
5037+
50215038
// TODO(new-expression): Add new expression method declarations above this line
50225039
/**
50235040
* @beta
@@ -9764,6 +9781,39 @@ declare namespace FirebaseFirestore {
97649781
timezone?: string | Expression,
97659782
): FunctionExpression;
97669783

9784+
/**
9785+
* @beta
9786+
*
9787+
* An enumeration of the different types generated by the Firestore backend.
9788+
*
9789+
* <ul>
9790+
* <li>Numerics evaluate directly to backend representation (`int64` or `float64`), not JS `number`.</li>
9791+
* <li>JavaScript `Date` and firestore `Timestamp` objects strictly evaluate to `'timestamp'`.</li>
9792+
* <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>
9793+
* </ul>
9794+
*/
9795+
export type Type =
9796+
| 'null'
9797+
| 'array'
9798+
| 'boolean'
9799+
| 'bytes'
9800+
| 'timestamp'
9801+
| 'geo_point'
9802+
| 'number'
9803+
| 'int32'
9804+
| 'int64'
9805+
| 'float64'
9806+
| 'decimal128'
9807+
| 'map'
9808+
| 'reference'
9809+
| 'string'
9810+
| 'vector'
9811+
| 'max_key'
9812+
| 'min_key'
9813+
| 'object_id'
9814+
| 'regex'
9815+
| 'request_timestamp';
9816+
97679817
/**
97689818
* @beta
97699819
* Creates an expression that returns the data type of the data in the specified field.
@@ -9791,6 +9841,44 @@ declare namespace FirebaseFirestore {
97919841
*/
97929842
export function type(expression: Expression): FunctionExpression;
97939843

9844+
/**
9845+
* @beta
9846+
* Creates an expression that checks if the value in the specified field is of the given type.
9847+
*
9848+
* @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data.
9849+
*
9850+
* @example
9851+
* ```typescript
9852+
* // Check if the 'price' field is a floating point number (evaluating to true inside pipeline conditionals)
9853+
* isType('price', 'float64');
9854+
* ```
9855+
*
9856+
* @param fieldName - The name of the field to check.
9857+
* @param type - The type to check for.
9858+
* @returns A new `BooleanExpression` that evaluates to true if the field's value is of the given type, false otherwise.
9859+
*/
9860+
export function isType(fieldName: string, type: Type): BooleanExpression;
9861+
/**
9862+
* @beta
9863+
* Creates an expression that checks if the result of an expression is of the given type.
9864+
*
9865+
* @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data.
9866+
*
9867+
* @example
9868+
* ```typescript
9869+
* // Check if the result of a calculation is a number
9870+
* isType(add('count', 1), 'number')
9871+
* ```
9872+
*
9873+
* @param expression - The expression to check.
9874+
* @param type - The type to check for.
9875+
* @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise.
9876+
*/
9877+
export function isType(
9878+
expression: Expression,
9879+
type: Type,
9880+
): BooleanExpression;
9881+
97949882
// TODO(new-expression): Add new top-level expression function declarations above this line
97959883
/**
97969884
* @beta

0 commit comments

Comments
 (0)