Skip to content

Commit 87ca228

Browse files
authored
Make event params strongly typed (#1155)
Implements go/cf3-typed-params. WIP: currently only common and v2 are implemented. Had to update `prettier` to the next major version so it could read TS 4.1 features without erroring out. I created #1160 as a base for this change to make it easier to separate mechanical from hand-crafted changes. If this is approved, however, I'll commit both changes to `launch.next`
1 parent 87d27ef commit 87ca228

File tree

13 files changed

+441
-119
lines changed

13 files changed

+441
-119
lines changed

spec/common/metaprogramming.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// The MIT License (MIT)
2+
//
3+
// Copyright (c) 2022 Firebase
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
// This method will fail to compile if value is not of the explicit parameter type.
23+
/* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function */
24+
export function expectType<Type>(value: Type) {}
25+
export function expectNever<Type extends never>() {}

spec/common/params.spec.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// The MIT License (MIT)
2+
//
3+
// Copyright (c) 2022 Firebase
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE ignoreUnusedWarning OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
23+
import { Extract, ParamsOf, Split } from "../../src/common/params";
24+
import { expectNever, expectType } from "./metaprogramming";
25+
26+
describe("Params namespace", () => {
27+
describe("Split", () => {
28+
// Note the subtle difference in the first two cases:
29+
// if passed a string (instead of a string literal) then split returns a
30+
// string[], which means "any number of elements as long as they are a string"
31+
// but if passed a literal string "" then split returns [] which means "a
32+
// tuple of zero elements".
33+
34+
it("handles generic strings", () => {
35+
expectType<Split<string, "/">>([] as string[]);
36+
});
37+
38+
it("handles empty strings", () => {
39+
expectType<Split<"", "/">>([]);
40+
});
41+
42+
it("handles just a slash", () => {
43+
expectType<Split<"/", "/">>([]);
44+
});
45+
46+
it("handles literal strings with one component", () => {
47+
expectType<Split<"a", "/">>(["a"]);
48+
});
49+
50+
it("handles literal strings with more than one component", () => {
51+
expectType<Split<"a/b/c", "/">>(["a", "b", "c"]);
52+
});
53+
54+
it("strips leading slashes", () => {
55+
expectType<Split<"/a/b/c", "/">>(["a", "b", "c"]);
56+
});
57+
});
58+
59+
describe("Extract", () => {
60+
it("extracts nothing from strings without params", () => {
61+
expectNever<Extract<"uid">>();
62+
});
63+
64+
it("extracts {segment} captures", () => {
65+
expectType<Extract<"{uid}">>("uid");
66+
});
67+
68+
it("extracts {segment=*} captures", () => {
69+
expectType<Extract<"{uid=*}">>("uid");
70+
});
71+
72+
it("extracts {segment=**} captures", () => {
73+
expectType<Extract<"{uid=**}">>("uid");
74+
});
75+
});
76+
77+
describe("ParamsOf", () => {
78+
it("falls back to Record<string, string> without better type info", () => {
79+
expectType<ParamsOf<string>>({} as Record<string, string>);
80+
});
81+
82+
it("is the empty object when there are no params", () => {
83+
expectType<ParamsOf<string>>({} as Record<string, never>);
84+
});
85+
86+
it("extracts a single param", () => {
87+
expectType<ParamsOf<"ignoreUnusedWarningrs/{uid}">>({
88+
uid: "uid",
89+
} as const);
90+
});
91+
92+
it("extracts multiple params", () => {
93+
expectType<ParamsOf<"ignoreUnusedWarningrs/{uid}/logs/{log=**}">>({
94+
uid: "hello",
95+
log: "world",
96+
} as const);
97+
});
98+
});
99+
});

spec/v1/providers/database.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import * as config from '../../../src/common/config';
2626
import { applyChange } from '../../../src/common/utilities/utils';
2727
import * as functions from '../../../src/v1';
2828
import * as database from '../../../src/v1/providers/database';
29+
import { expectType } from '../../common/metaprogramming';
2930

3031
describe('Database Functions', () => {
3132
describe('DatabaseBuilder', () => {
@@ -120,6 +121,18 @@ describe('Database Functions', () => {
120121

121122
return handler(event.data, event.context);
122123
});
124+
125+
it('Should have params of the correct type', () => {
126+
database.ref('foo').onWrite((event, context) => {
127+
expectType<Record<string, never>>(context.params);
128+
});
129+
database.ref('foo/{bar}').onWrite((event, context) => {
130+
expectType<{ bar: string }>(context.params);
131+
});
132+
database.ref('foo/{bar}/{baz}').onWrite((event, context) => {
133+
expectType<{ bar: string; baz: string }>(context.params);
134+
});
135+
});
123136
});
124137

125138
describe('#onCreate()', () => {
@@ -168,6 +181,18 @@ describe('Database Functions', () => {
168181

169182
return handler(event.data, event.context);
170183
});
184+
185+
it('Should have params of the correct type', () => {
186+
database.ref('foo').onCreate((event, context) => {
187+
expectType<Record<string, never>>(context.params);
188+
});
189+
database.ref('foo/{bar}').onCreate((event, context) => {
190+
expectType<{ bar: string }>(context.params);
191+
});
192+
database.ref('foo/{bar}/{baz}').onCreate((event, context) => {
193+
expectType<{ bar: string; baz: string }>(context.params);
194+
});
195+
});
171196
});
172197

173198
describe('#onUpdate()', () => {
@@ -216,6 +241,18 @@ describe('Database Functions', () => {
216241

217242
return handler(event.data, event.context);
218243
});
244+
245+
it('Should have params of the correct type', () => {
246+
database.ref('foo').onUpdate((event, context) => {
247+
expectType<Record<string, never>>(context.params);
248+
});
249+
database.ref('foo/{bar}').onUpdate((event, context) => {
250+
expectType<{ bar: string }>(context.params);
251+
});
252+
database.ref('foo/{bar}/{baz}').onUpdate((event, context) => {
253+
expectType<{ bar: string; baz: string }>(context.params);
254+
});
255+
});
219256
});
220257

221258
describe('#onDelete()', () => {
@@ -265,6 +302,18 @@ describe('Database Functions', () => {
265302
return handler(event.data, event.context);
266303
});
267304
});
305+
306+
it('Should have params of the correct type', () => {
307+
database.ref('foo').onDelete((event, context) => {
308+
expectType<Record<string, never>>(context.params);
309+
});
310+
database.ref('foo/{bar}').onDelete((event, context) => {
311+
expectType<{ bar: string }>(context.params);
312+
});
313+
database.ref('foo/{bar}/{baz}').onDelete((event, context) => {
314+
expectType<{ bar: string; baz: string }>(context.params);
315+
});
316+
});
268317
});
269318

270319
describe('handler namespace', () => {

spec/v1/providers/firestore.spec.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { Timestamp } from 'firebase-admin/firestore';
2525

2626
import * as functions from '../../../src/v1';
2727
import * as firestore from '../../../src/v1/providers/firestore';
28+
import { expectType } from '../../common/metaprogramming';
2829

2930
describe('Firestore Functions', () => {
3031
function constructValue(fields: any) {
@@ -117,7 +118,9 @@ describe('Firestore Functions', () => {
117118
'projects/project1/databases/(default)/documents/users/{uid}';
118119
const cloudFunction = firestore
119120
.document('users/{uid}')
120-
.onWrite(() => null);
121+
.onWrite((snap, context) => {
122+
expectType<{ uid: string }>(context.params);
123+
});
121124

122125
expect(cloudFunction.__endpoint).to.deep.equal(
123126
expectedEndpoint(resource, 'document.write')
@@ -130,7 +133,9 @@ describe('Firestore Functions', () => {
130133
const cloudFunction = firestore
131134
.namespace('v2')
132135
.document('users/{uid}')
133-
.onWrite(() => null);
136+
.onWrite((snap, context) => {
137+
expectType<{ uid: string }>(context.params);
138+
});
134139

135140
expect(cloudFunction.__endpoint).to.deep.equal(
136141
expectedEndpoint(resource, 'document.write')
@@ -156,7 +161,9 @@ describe('Firestore Functions', () => {
156161
.database('myDB')
157162
.namespace('v2')
158163
.document('users/{uid}')
159-
.onWrite(() => null);
164+
.onWrite((snap, context) => {
165+
expectType<{ uid: string }>(context.params);
166+
});
160167

161168
expect(cloudFunction.__endpoint).to.deep.equal(
162169
expectedEndpoint(resource, 'document.write')
@@ -171,7 +178,9 @@ describe('Firestore Functions', () => {
171178
memory: '256MB',
172179
})
173180
.firestore.document('doc')
174-
.onCreate((snap) => snap);
181+
.onCreate((snap, context) => {
182+
expectType<Record<string, string>>(context.params);
183+
});
175184

176185
expect(fn.__endpoint.region).to.deep.equal(['us-east1']);
177186
expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256);

0 commit comments

Comments
 (0)