Skip to content

Commit c26967c

Browse files
authored
feat(firestore): Defer pipeline user data validation to execute() (#7816)
1 parent cb5c2d8 commit c26967c

4 files changed

Lines changed: 146 additions & 62 deletions

File tree

handwritten/firestore/.eslintrc.json

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,8 @@
22
"extends": "./node_modules/gts",
33
"overrides": [
44
{
5-
"files": [
6-
"dev/src/**/*.ts"
7-
],
8-
"excludedFiles": [
9-
"dev/src/v1/*.ts",
10-
"dev/src/v1beta1/*.ts"
11-
],
5+
"files": ["dev/src/**/*.ts"],
6+
"excludedFiles": ["dev/src/v1/*.ts", "dev/src/v1beta1/*.ts"],
127
"parser": "@typescript-eslint/parser",
138
"rules": {
149
"@typescript-eslint/explicit-function-return-type": [
@@ -22,17 +17,14 @@
2217
"@typescript-eslint/no-unused-vars": [
2318
"warn",
2419
{
25-
// Ignore args that are underscore only
26-
"argsIgnorePattern": "^_$"
20+
// Allow args to be unused if they start with an underscore
21+
"argsIgnorePattern": "^_"
2722
}
2823
]
2924
}
3025
},
3126
{
32-
"files": [
33-
"dev/test/*.ts",
34-
"dev/system-test/*.ts"
35-
],
27+
"files": ["dev/test/*.ts", "dev/system-test/*.ts"],
3628
"parser": "@typescript-eslint/parser",
3729
"rules": {
3830
"no-restricted-properties": [
@@ -49,8 +41,8 @@
4941
"@typescript-eslint/no-unused-vars": [
5042
"warn",
5143
{
52-
// Ignore args that are underscore only
53-
"argsIgnorePattern": "^_$"
44+
// Allow args to be unused if they start with an underscore
45+
"argsIgnorePattern": "^_"
5446
}
5547
],
5648
"@typescript-eslint/no-floating-promises": "warn"

handwritten/firestore/dev/src/pipelines/pipeline-util.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import {
3737
PipelineStreamElement,
3838
QueryCursor,
3939
} from '../reference/types';
40-
import {Serializer} from '../serializer';
40+
import {hasUserData, HasUserData, Serializer} from '../serializer';
4141
import {
4242
Deferred,
4343
getTotalTimeout,
@@ -763,3 +763,25 @@ export function aliasedAggregateToMap(
763763
new Map() as Map<string, AggregateFunction>,
764764
);
765765
}
766+
767+
/**
768+
* @internal
769+
*
770+
* Helper to read user data across a number of different formats.
771+
*/
772+
export function validateUserDataHelper<
773+
T extends Map<string, HasUserData> | HasUserData[] | HasUserData,
774+
>(expressionMap: T, ignoreUndefinedProperties: boolean): T {
775+
if (hasUserData(expressionMap)) {
776+
expressionMap._validateUserData(ignoreUndefinedProperties);
777+
} else if (Array.isArray(expressionMap)) {
778+
expressionMap.forEach(readableData => {
779+
readableData._validateUserData(ignoreUndefinedProperties);
780+
});
781+
} else {
782+
expressionMap.forEach(expr =>
783+
expr._validateUserData(ignoreUndefinedProperties),
784+
);
785+
}
786+
return expressionMap;
787+
}

handwritten/firestore/dev/src/pipelines/pipelines.ts

Lines changed: 34 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import {
4242
} from './pipeline-util';
4343
import {DocumentReference} from '../reference/document-reference';
4444
import {PipelineResponse} from '../reference/types';
45-
import {HasUserData, hasUserData, Serializer} from '../serializer';
45+
import {Serializer} from '../serializer';
4646
import {ApiMapValue} from '../types';
4747
import * as protos from '../../protos/firestore_v1_proto_api';
4848
import api = protos.google.firestore.v1;
@@ -349,7 +349,7 @@ export class PipelineSource implements firestore.Pipelines.PipelineSource {
349349
*/
350350
export class Pipeline implements firestore.Pipelines.Pipeline {
351351
constructor(
352-
private db: Firestore,
352+
private db: Firestore | undefined,
353353
private stages: Stage[],
354354
) {}
355355

@@ -433,8 +433,6 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
433433
: fieldOrOptions.fields;
434434
const normalizedFields: Map<string, Expression> = selectablesToMap(fields);
435435

436-
this._validateUserData('select', normalizedFields);
437-
438436
const internalOptions = {
439437
...options,
440438
fields: normalizedFields,
@@ -503,7 +501,6 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
503501
const convertedFields: Array<Field> = fields.map(f =>
504502
isString(f) ? field(f) : (f as Field),
505503
);
506-
this._validateUserData('removeFields', convertedFields);
507504

508505
const innerOptions = {
509506
...options,
@@ -602,8 +599,6 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
602599
const normalizedSelections: Map<string, Expression> =
603600
selectablesToMap(selections);
604601

605-
this._validateUserData('select', normalizedSelections);
606-
607602
const internalOptions = {
608603
...options,
609604
selections: normalizedSelections,
@@ -689,7 +684,6 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
689684
: conditionOrOptions.condition;
690685
const convertedCondition: BooleanExpression =
691686
condition as BooleanExpression;
692-
this._validateUserData('where', convertedCondition);
693687

694688
const internalOptions: InternalWhereStageOptions = {
695689
...options,
@@ -906,7 +900,6 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
906900
? [groupOrOptions, ...additionalGroups]
907901
: groupOrOptions.groups;
908902
const convertedGroups: Map<string, Expression> = selectablesToMap(groups);
909-
this._validateUserData('distinct', convertedGroups);
910903

911904
const internalOptions: InternalDistinctStageOptions = {
912905
...options,
@@ -996,7 +989,6 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
996989
const groups: Array<firestore.Pipelines.Selectable | string> =
997990
isAliasedAggregate(targetOrOptions) ? [] : (targetOrOptions.groups ?? []);
998991
const convertedGroups: Map<string, Expression> = selectablesToMap(groups);
999-
this._validateUserData('aggregate', convertedGroups);
1000992

1001993
const internalOptions: InternalAggregateStageOptions = {
1002994
...options,
@@ -1040,10 +1032,6 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
10401032
? toField(options.distanceField)
10411033
: undefined;
10421034

1043-
this._validateUserData('findNearest', field);
1044-
1045-
this._validateUserData('findNearest', vectorValue);
1046-
10471035
const internalOptions: InternalFindNearestStageOptions = {
10481036
...options,
10491037
field,
@@ -1177,7 +1165,6 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
11771165
? valueOrOptions
11781166
: valueOrOptions.map;
11791167
const mapExpr = fieldOrExpression(fieldNameOrExpr);
1180-
this._validateUserData('replaceWith', mapExpr);
11811168

11821169
const internalOptions: InternalReplaceWithStageOptions = {
11831170
...options,
@@ -1490,7 +1477,6 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
14901477
? [orderingOrOptions, ...additionalOrderings]
14911478
: orderingOrOptions.orderings;
14921479
const normalizedOrderings = orderings as Array<Ordering>;
1493-
this._validateUserData('sort', normalizedOrderings);
14941480

14951481
const internalOptions: InternalSortStageOptions = {
14961482
...options,
@@ -1545,11 +1531,6 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
15451531
}
15461532
});
15471533

1548-
expressionParams.forEach(param => {
1549-
if (hasUserData(param)) {
1550-
param._validateUserData(!!this.db._settings.ignoreUndefinedProperties);
1551-
}
1552-
});
15531534
return this._addStage(new RawStage(name, expressionParams, options ?? {}));
15541535
}
15551536

@@ -1602,7 +1583,19 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
16021583
transactionOrReadTime?: Uint8Array | Timestamp | api.ITransactionOptions,
16031584
pipelineExecuteOptions?: firestore.Pipelines.PipelineExecuteOptions,
16041585
): Promise<PipelineResponse> {
1586+
if (!this.db) {
1587+
throw new Error(
1588+
'This pipeline was created without a database (e.g., as a subcollection pipeline) and cannot be executed directly. It can only be used as part of another pipeline.',
1589+
);
1590+
}
1591+
1592+
// Validates user data in the entire pipeline
1593+
this._validateUserData(
1594+
this.db._settings.ignoreUndefinedProperties ?? false,
1595+
);
1596+
16051597
const util = new ExecutionUtil(this.db, this.db._serializer!);
1598+
16061599
const structuredPipeline = this._toStructuredPipeline(
16071600
pipelineExecuteOptions,
16081601
);
@@ -1641,42 +1634,37 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
16411634
* ```
16421635
*/
16431636
stream(): NodeJS.ReadableStream {
1637+
if (!this.db) {
1638+
throw new Error(
1639+
'This pipeline was created without a database (e.g., as a subcollection pipeline) and cannot be executed directly. It can only be used as part of another pipeline.',
1640+
);
1641+
}
16441642
const util = new ExecutionUtil(this.db, this.db._serializer!);
16451643
// TODO(pipelines) support options
16461644
const structuredPipeline = this._toStructuredPipeline();
16471645
return util.stream(structuredPipeline, undefined);
16481646
}
16491647

16501648
_toProto(): api.IPipeline {
1651-
const stages: IStage[] = this.stages.map(stage =>
1652-
stage._toProto(this.db._serializer!),
1649+
if (!this.db) {
1650+
throw new Error(
1651+
'This pipeline was created without a database (e.g., as a subcollection pipeline) and cannot be executed directly. It can only be used as part of another pipeline.',
1652+
);
1653+
}
1654+
1655+
const stages: IStage[] = this.stages.map(
1656+
// We use a non-null assertion here because we've already checked that
1657+
// 'db' is not null at the start of this function, but TS does not
1658+
// recognize that 'db' can no longer be undefined.
1659+
stage => stage._toProto(this.db!._serializer!),
16531660
);
16541661
return {stages};
16551662
}
16561663

1657-
/**
1658-
* @beta
1659-
* Validates user data for each expression in the expressionMap.
1660-
* @param name Name of the calling function. Used for error messages when invalid user data is encountered.
1661-
* @param val
1662-
* @returns the expressionMap argument.
1663-
* @private
1664-
*/
1665-
_validateUserData<
1666-
T extends Map<string, HasUserData> | HasUserData[] | HasUserData,
1667-
>(_: string, val: T): T {
1668-
const ignoreUndefinedProperties =
1669-
!!this.db._settings.ignoreUndefinedProperties;
1670-
if (hasUserData(val)) {
1671-
val._validateUserData(ignoreUndefinedProperties);
1672-
} else if (Array.isArray(val)) {
1673-
val.forEach(readableData => {
1674-
readableData._validateUserData(ignoreUndefinedProperties);
1675-
});
1676-
} else {
1677-
val.forEach(expr => expr._validateUserData(ignoreUndefinedProperties));
1678-
}
1679-
return val;
1664+
_validateUserData(ignoreUndefinedProperties: boolean): void {
1665+
this.stages.forEach(stage => {
1666+
stage._validateUserData(ignoreUndefinedProperties);
1667+
});
16801668
}
16811669
}
16821670

0 commit comments

Comments
 (0)