Skip to content

Commit 3d8d595

Browse files
authoredMar 13, 2024··
Parameterize db info for firestore events (#1538)
1 parent 1ddebc2 commit 3d8d595

File tree

3 files changed

+152
-26
lines changed

3 files changed

+152
-26
lines changed
 

‎spec/v2/providers/firestore.spec.ts

+109-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { Timestamp } from "firebase-admin/firestore";
2626
import * as firestore from "../../../src/v2/providers/firestore";
2727
import { PathPattern } from "../../../src/common/utilities/path-pattern";
2828
import { onInit } from "../../../src/v2/core";
29+
import * as params from "../../../src/params";
2930

3031
/** static-complied protobuf */
3132
const DocumentEventData = google.events.cloud.firestore.v1.DocumentEventData;
@@ -148,6 +149,20 @@ const writtenData = {
148149
const writtenProto = DocumentEventData.create(writtenData);
149150

150151
describe("firestore", () => {
152+
let docParam: params.Expression<string>;
153+
let nsParam: params.Expression<string>;
154+
let dbParam: params.Expression<string>;
155+
156+
before(() => {
157+
docParam = params.defineString("DOCUMENT");
158+
nsParam = params.defineString("NAMESPACE");
159+
dbParam = params.defineString("DATABASE");
160+
});
161+
162+
after(() => {
163+
params.clearParams();
164+
});
165+
151166
describe("onDocumentWritten", () => {
152167
it("should create a func", () => {
153168
const expectedEp = makeExpectedEp(
@@ -194,6 +209,29 @@ describe("firestore", () => {
194209
expect(func.__endpoint).to.deep.eq(expectedEp);
195210
});
196211

212+
it("should create a func with param opts", () => {
213+
const expectedEp = makeExpectedEp(
214+
firestore.writtenEventType,
215+
{
216+
database: dbParam,
217+
namespace: nsParam,
218+
},
219+
{
220+
document: docParam,
221+
}
222+
);
223+
224+
const func = firestore.onDocumentWritten(
225+
{
226+
database: dbParam,
227+
namespace: nsParam,
228+
document: docParam,
229+
},
230+
() => true
231+
);
232+
expect(func.__endpoint).to.deep.eq(expectedEp);
233+
});
234+
197235
it("calls init function", async () => {
198236
const event: firestore.RawFirestoreEvent = {
199237
...eventBase,
@@ -258,6 +296,29 @@ describe("firestore", () => {
258296
expect(func.__endpoint).to.deep.eq(expectedEp);
259297
});
260298

299+
it("should create a func with param opts", () => {
300+
const expectedEp = makeExpectedEp(
301+
firestore.createdEventType,
302+
{
303+
database: dbParam,
304+
namespace: nsParam,
305+
},
306+
{
307+
document: docParam,
308+
}
309+
);
310+
311+
const func = firestore.onDocumentCreated(
312+
{
313+
database: dbParam,
314+
namespace: nsParam,
315+
document: docParam,
316+
},
317+
() => true
318+
);
319+
expect(func.__endpoint).to.deep.eq(expectedEp);
320+
});
321+
261322
it("calls init function", async () => {
262323
const event: firestore.RawFirestoreEvent = {
263324
...eventBase,
@@ -322,6 +383,29 @@ describe("firestore", () => {
322383
expect(func.__endpoint).to.deep.eq(expectedEp);
323384
});
324385

386+
it("should create a func with param opts", () => {
387+
const expectedEp = makeExpectedEp(
388+
firestore.updatedEventType,
389+
{
390+
database: dbParam,
391+
namespace: nsParam,
392+
},
393+
{
394+
document: docParam,
395+
}
396+
);
397+
398+
const func = firestore.onDocumentUpdated(
399+
{
400+
database: dbParam,
401+
namespace: nsParam,
402+
document: docParam,
403+
},
404+
() => true
405+
);
406+
expect(func.__endpoint).to.deep.eq(expectedEp);
407+
});
408+
325409
it("calls init function", async () => {
326410
const event: firestore.RawFirestoreEvent = {
327411
...eventBase,
@@ -386,6 +470,29 @@ describe("firestore", () => {
386470
expect(func.__endpoint).to.deep.eq(expectedEp);
387471
});
388472

473+
it("should create a func with param opts", () => {
474+
const expectedEp = makeExpectedEp(
475+
firestore.deletedEventType,
476+
{
477+
database: dbParam,
478+
namespace: nsParam,
479+
},
480+
{
481+
document: docParam,
482+
}
483+
);
484+
485+
const func = firestore.onDocumentDeleted(
486+
{
487+
database: dbParam,
488+
namespace: nsParam,
489+
document: docParam,
490+
},
491+
() => true
492+
);
493+
expect(func.__endpoint).to.deep.eq(expectedEp);
494+
});
495+
389496
it("calls init function", async () => {
390497
const event: firestore.RawFirestoreEvent = {
391498
...eventBase,
@@ -663,7 +770,7 @@ describe("firestore", () => {
663770
const ep = firestore.makeEndpoint(
664771
firestore.createdEventType,
665772
{ region: "us-central1" },
666-
new PathPattern("foo/{bar}"),
773+
"foo/{bar}",
667774
"my-db",
668775
"my-ns"
669776
);
@@ -686,7 +793,7 @@ describe("firestore", () => {
686793
const ep = firestore.makeEndpoint(
687794
firestore.createdEventType,
688795
{ region: "us-central1" },
689-
new PathPattern("foo/fGRodw71mHutZ4wGDuT8"),
796+
"foo/fGRodw71mHutZ4wGDuT8",
690797
"my-db",
691798
"my-ns"
692799
);

‎src/common/params.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
// SOFTWARE.
2222

23+
import { Expression } from "../params";
24+
2325
/**
2426
* A type that splits literal string S with delimiter D.
2527
*
@@ -78,10 +80,17 @@ export type Extract<Part extends string> = Part extends `{${infer Param}=**}`
7880
*
7981
* For flexibility reasons, ParamsOf<string> is Record<string, string>
8082
*/
81-
export type ParamsOf<PathPattern extends string> =
83+
export type ParamsOf<PathPattern extends string | Expression<string>> =
8284
// if we have lost type information, revert back to an untyped dictionary
83-
string extends PathPattern
85+
PathPattern extends Expression<string>
86+
? Record<string, string>
87+
: string extends PathPattern
8488
? Record<string, string>
8589
: {
86-
[Key in Extract<Split<NullSafe<PathPattern>, "/">[number]>]: string;
90+
// N.B. I'm not sure why PathPattern isn't detected to not be an
91+
// Expression<string> per the check above. Since we have the check above
92+
// The Exclude call should be safe.
93+
[Key in Extract<
94+
Split<NullSafe<Exclude<PathPattern, Expression<string>>>, "/">[number]
95+
>]: string;
8796
};

‎src/v2/providers/firestore.ts

+31-21
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
} from "../../common/providers/firestore";
3737
import { wrapTraceContext } from "../trace";
3838
import { withInit } from "../../common/onInit";
39+
import { Expression } from "../../params";
3940

4041
export { Change };
4142

@@ -106,11 +107,11 @@ export interface FirestoreEvent<T, Params = Record<string, string>> extends Clou
106107
/** DocumentOptions extend EventHandlerOptions with provided document and optional database and namespace. */
107108
export interface DocumentOptions<Document extends string = string> extends EventHandlerOptions {
108109
/** The document path */
109-
document: Document;
110+
document: Document | Expression<string>;
110111
/** The Firestore database */
111-
database?: string;
112+
database?: string | Expression<string>;
112113
/** The Firestore namespace */
113-
namespace?: string;
114+
namespace?: string | Expression<string>;
114115
}
115116

116117
/**
@@ -278,17 +279,20 @@ export function onDocumentDeleted<Document extends string>(
278279

279280
/** @internal */
280281
export function getOpts(documentOrOpts: string | DocumentOptions) {
281-
let document: string;
282-
let database: string;
283-
let namespace: string;
282+
let document: string | Expression<string>;
283+
let database: string | Expression<string>;
284+
let namespace: string | Expression<string>;
284285
let opts: EventHandlerOptions;
285286
if (typeof documentOrOpts === "string") {
286287
document = normalizePath(documentOrOpts);
287288
database = "(default)";
288289
namespace = "(default)";
289290
opts = {};
290291
} else {
291-
document = normalizePath(documentOrOpts.document);
292+
document =
293+
typeof documentOrOpts.document === "string"
294+
? normalizePath(documentOrOpts.document)
295+
: documentOrOpts.document;
292296
database = documentOrOpts.database || "(default)";
293297
namespace = documentOrOpts.namespace || "(default)";
294298
opts = { ...documentOrOpts };
@@ -398,21 +402,25 @@ export function makeChangedFirestoreEvent<Params>(
398402
export function makeEndpoint(
399403
eventType: string,
400404
opts: EventHandlerOptions,
401-
document: PathPattern,
402-
database: string,
403-
namespace: string
405+
document: string | Expression<string>,
406+
database: string | Expression<string>,
407+
namespace: string | Expression<string>
404408
): ManifestEndpoint {
405409
const baseOpts = optionsToEndpoint(getGlobalOptions());
406410
const specificOpts = optionsToEndpoint(opts);
407411

408-
const eventFilters: Record<string, string> = {
412+
const eventFilters: Record<string, string | Expression<string>> = {
409413
database,
410414
namespace,
411415
};
412-
const eventFilterPathPatterns: Record<string, string> = {};
413-
document.hasWildcards()
414-
? (eventFilterPathPatterns.document = document.getValue())
415-
: (eventFilters.document = document.getValue());
416+
const eventFilterPathPatterns: Record<string, string | Expression<string>> = {};
417+
const maybePattern =
418+
typeof document === "string" ? new PathPattern(document).hasWildcards() : true;
419+
if (maybePattern) {
420+
eventFilterPathPatterns.document = document;
421+
} else {
422+
eventFilters.document = document;
423+
}
416424

417425
return {
418426
...initV2Endpoint(getGlobalOptions(), opts),
@@ -440,19 +448,20 @@ export function onOperation<Document extends string>(
440448
): CloudFunction<FirestoreEvent<QueryDocumentSnapshot, ParamsOf<Document>>> {
441449
const { document, database, namespace, opts } = getOpts(documentOrOpts);
442450

443-
const documentPattern = new PathPattern(document);
444-
445451
// wrap the handler
446452
const func = (raw: CloudEvent<unknown>) => {
447453
const event = raw as RawFirestoreEvent;
454+
const documentPattern = new PathPattern(
455+
typeof document === "string" ? document : document.value()
456+
);
448457
const params = makeParams(event.document, documentPattern) as unknown as ParamsOf<Document>;
449458
const firestoreEvent = makeFirestoreEvent(eventType, event, params);
450459
return wrapTraceContext(withInit(handler))(firestoreEvent);
451460
};
452461

453462
func.run = handler;
454463

455-
func.__endpoint = makeEndpoint(eventType, opts, documentPattern, database, namespace);
464+
func.__endpoint = makeEndpoint(eventType, opts, document, database, namespace);
456465

457466
return func;
458467
}
@@ -467,19 +476,20 @@ export function onChangedOperation<Document extends string>(
467476
): CloudFunction<FirestoreEvent<Change<QueryDocumentSnapshot>, ParamsOf<Document>>> {
468477
const { document, database, namespace, opts } = getOpts(documentOrOpts);
469478

470-
const documentPattern = new PathPattern(document);
471-
472479
// wrap the handler
473480
const func = (raw: CloudEvent<unknown>) => {
474481
const event = raw as RawFirestoreEvent;
482+
const documentPattern = new PathPattern(
483+
typeof document === "string" ? document : document.value()
484+
);
475485
const params = makeParams(event.document, documentPattern) as unknown as ParamsOf<Document>;
476486
const firestoreEvent = makeChangedFirestoreEvent(event, params);
477487
return wrapTraceContext(withInit(handler))(firestoreEvent);
478488
};
479489

480490
func.run = handler;
481491

482-
func.__endpoint = makeEndpoint(eventType, opts, documentPattern, database, namespace);
492+
func.__endpoint = makeEndpoint(eventType, opts, document, database, namespace);
483493

484494
return func;
485495
}

0 commit comments

Comments
 (0)
Please sign in to comment.