diff --git a/handwritten/firestore/api-report/firestore.api.md b/handwritten/firestore/api-report/firestore.api.md index 06f0a189578..df8cc5c85c2 100644 --- a/handwritten/firestore/api-report/firestore.api.md +++ b/handwritten/firestore/api-report/firestore.api.md @@ -1827,6 +1827,9 @@ function multiply(first: Expression, second: Expression | unknown): FunctionExpr // @beta function multiply(fieldName: string, second: Expression | unknown): FunctionExpression; +// @beta +function nor(first: BooleanExpression, second: BooleanExpression, ...more: BooleanExpression[]): BooleanExpression; + // @beta function not(booleanExpr: BooleanExpression): BooleanExpression; @@ -2119,7 +2122,9 @@ declare namespace Pipelines { stringIndexOf, stringRepeat, stringReplaceAll, - stringReplaceOne + stringReplaceOne, + nor, + switchOn } } export { Pipelines } @@ -2642,6 +2647,9 @@ function sum(expression: Expression): AggregateFunction; // @beta function sum(fieldName: string): AggregateFunction; +// @beta +function switchOn(condition: BooleanExpression, result: Expression, ...others: Array): FunctionExpression; + // @public export class Timestamp implements firestore.Timestamp { // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' diff --git a/handwritten/firestore/dev/src/pipelines/expression.ts b/handwritten/firestore/dev/src/pipelines/expression.ts index 20054a59b81..b32c2358432 100644 --- a/handwritten/firestore/dev/src/pipelines/expression.ts +++ b/handwritten/firestore/dev/src/pipelines/expression.ts @@ -8608,6 +8608,70 @@ export function or( return new FunctionExpression('or', [first, second, ...more]).asBoolean(); } +/** + * @beta + * Creates an expression that performs a logical 'NOR' operation on multiple filter conditions. + * + * @example + * ```typescript + * // Check if neither the 'age' field is greater than 18 nor the 'city' field is "London" + * const condition = nor( + * greaterThan("age", 18), + * equal("city", "London") + * ); + * ``` + * + * @param first - The first filter condition. + * @param second - The second filter condition. + * @param more - Additional filter conditions to 'NOR' together. + * @returns A new `Expression` representing the logical 'NOR' operation. + */ +export function nor( + first: BooleanExpression, + second: BooleanExpression, + ...more: BooleanExpression[] +): BooleanExpression { + return new FunctionExpression('nor', [first, second, ...more]).asBoolean(); +} + +/** + * @beta + * Creates an expression that evaluates to the result corresponding to the first true condition. + * + * @remarks + * This function behaves like a `switch` statement. It accepts an alternating sequence of conditions + * and their corresponding results. + * If an odd number of arguments is provided, the final argument serves as a default fallback result. + * If no default is provided and no condition evaluates to true, it throws an error. + * + * @example + * ```typescript + * // Return "Active" if field "status" is 1, "Pending" if field "status" is 2, + * // and default to "Unknown" if none of the conditions are true. + * switchOn( + * equal(field("status"), 1), "Active", + * equal(field("status"), 2), "Pending", + * "Unknown" + * ) + * ``` + * + * @param condition - The first condition to check. + * @param result - The result if the first condition is true. + * @param others - Additional conditions and results, and optionally a default value. + * @returns A new `Expression` representing the switch operation. + */ +export function switchOn( + condition: BooleanExpression, + result: Expression, + ...others: Array +): FunctionExpression { + return new FunctionExpression('switch_on', [ + valueToDefaultExpr(condition), + valueToDefaultExpr(result), + ...others.map(valueToDefaultExpr), + ]); +} + /** * @beta * Creates an expression that returns the value of the base expression raised to the power of the exponent expression. diff --git a/handwritten/firestore/dev/src/pipelines/index.ts b/handwritten/firestore/dev/src/pipelines/index.ts index e0bc969104c..268ba985f79 100644 --- a/handwritten/firestore/dev/src/pipelines/index.ts +++ b/handwritten/firestore/dev/src/pipelines/index.ts @@ -155,5 +155,7 @@ export { stringRepeat, stringReplaceAll, stringReplaceOne, + nor, + switchOn, // TODO(new-expression): Add new expression exports above this line } from './expression'; diff --git a/handwritten/firestore/dev/system-test/pipeline.ts b/handwritten/firestore/dev/system-test/pipeline.ts index 7bdb4405b17..8377424c8d2 100644 --- a/handwritten/firestore/dev/system-test/pipeline.ts +++ b/handwritten/firestore/dev/system-test/pipeline.ts @@ -148,6 +148,8 @@ import { isType, timestampTruncate, split, + switchOn, + nor, // TODO(new-expression): add new expression imports above this line } from '../src/pipelines'; @@ -1644,6 +1646,27 @@ describe.skipClassic('Pipeline class', () => { ); }); + it('where with nor', async () => { + const snapshot = await firestore + .pipeline() + .collection(randomCol.path) + .where( + nor( + equal('genre', 'Romance'), + equal('genre', 'Dystopian'), + equal('genre', 'Fantasy'), + greaterThan('published', 1949), + ), + ) + .select('title') + .execute(); + expectResults( + snapshot, + {title: 'Crime and Punishment'}, + {title: 'The Great Gatsby'}, + ); + }); + it('supports options', async () => { const snapshot = await firestore .pipeline() @@ -5230,6 +5253,107 @@ describe.skipClassic('Pipeline class', () => { }); }); + it('supports nor', async () => { + const snapshot = await firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith( + map({ + a: false, + b: false, + c: true, + d: null, + }), + ) + .select( + nor(field('a').asBoolean(), field('b').asBoolean()).as( + 'twoConditions', + ), + nor( + field('a').asBoolean(), + field('b').asBoolean(), + field('c').asBoolean(), + ).as('threeConditions'), + nor( + field('a').asBoolean(), + field('b').asBoolean(), + field('d').asBoolean(), + ).as('threeConditionsWithNull'), + ) + .execute(); + + expectResults(snapshot, { + twoConditions: true, + threeConditions: false, + threeConditionsWithNull: null, + }); + }); + + describe('switchOn', () => { + it('supports basic switch', async () => { + const snapshot = await firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith(map({value: 1})) + .select( + switchOn( + equal(field('value'), 1), + constant('one'), + constant('NA'), + ).as('result1'), + switchOn( + equal(field('value'), 2), + constant('two'), + constant('NA'), + ).as('result2'), + ) + .execute(); + expectResults(snapshot, {result1: 'one', result2: 'NA'}); + }); + + it('supports multi-branch switch', async () => { + const snapshot = await firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith(map({value: 2})) + .select( + switchOn( + equal(field('value'), 1), + constant('one'), + equal(field('value'), 2), + constant('two'), + equal(field('value'), 3), + constant('three'), + constant('default'), + ).as('result'), + ) + .execute(); + expectResults(snapshot, {result: 'two'}); + }); + + it('throws if no match and no default', async () => { + await expect( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith(map({value: 5})) + .select( + switchOn( + equal(field('value'), 1), + constant('one'), + equal(field('value'), 2), + constant('two'), + ).as('result'), + ) + .execute(), + ).to.be.rejectedWith(/all switch cases evaluate to false/); + }); + }); + // TODO(new-expression): Add new expression tests above this line }); diff --git a/handwritten/firestore/types/firestore.d.ts b/handwritten/firestore/types/firestore.d.ts index e58444a2053..66d18a13a2e 100644 --- a/handwritten/firestore/types/firestore.d.ts +++ b/handwritten/firestore/types/firestore.d.ts @@ -10582,6 +10582,62 @@ declare namespace FirebaseFirestore { second: BooleanExpression, ...more: BooleanExpression[] ): BooleanExpression; + + /** + * @beta + * Creates an expression that performs a logical 'NOR' operation on multiple filter conditions. + * + * @example + * ```typescript + * // Check if neither the 'age' field is greater than 18 nor the 'city' field is "London" + * const condition = nor( + * greaterThan("age", 18), + * equal("city", "London") + * ); + * ``` + * @param first The first filter condition. + * @param second The second filter condition. + * @param more - Additional filter conditions to 'NOR' together. + * @returns A new {@code Expression} representing the logical 'NOR' operation. + */ + export function nor( + first: BooleanExpression, + second: BooleanExpression, + ...more: BooleanExpression[] + ): BooleanExpression; + + /** + * @beta + * Creates an expression that evaluates to the result corresponding to the first true condition. + * + * @remarks + * This function behaves like a `switch` statement. It accepts an alternating sequence of conditions + * and their corresponding results. + * If an odd number of arguments is provided, the final argument serves as a default fallback result. + * If no default is provided and no condition evaluates to true, it throws an error. + * + * @example + * ```typescript + * // Return "Active" if field "status" is 1, "Pending" if field "status" is 2, + * // and default to "Unknown" if none of the conditions are true. + * switchOn( + * equal(field("status"), 1), "Active", + * equal(field("status"), 2), "Pending", + * "Unknown" + * ) + * ``` + * + * @param condition - The first condition to check. + * @param result - The result if the first condition is true. + * @param others - Additional conditions and results, and optionally a default value. + * @returns A new {@code Expression} representing the switch operation. + */ + export function switchOn( + condition: BooleanExpression, + result: Expression, + ...others: Array + ): FunctionExpression; + /** * @beta * Creates an expression that returns the value of the base expression raised to the power of the exponent expression.