Skip to content

Parameterize db info for firestore events #1538

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 109 additions & 2 deletions spec/v2/providers/firestore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { Timestamp } from "firebase-admin/firestore";
import * as firestore from "../../../src/v2/providers/firestore";
import { PathPattern } from "../../../src/common/utilities/path-pattern";
import { onInit } from "../../../src/v2/core";
import * as params from "../../../src/params";

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

describe("firestore", () => {
let docParam: params.Expression<string>;
let nsParam: params.Expression<string>;
let dbParam: params.Expression<string>;

before(() => {
docParam = params.defineString("DOCUMENT");
nsParam = params.defineString("NAMESPACE");
dbParam = params.defineString("DATABASE");
});

after(() => {
params.clearParams();
});

describe("onDocumentWritten", () => {
it("should create a func", () => {
const expectedEp = makeExpectedEp(
Expand Down Expand Up @@ -194,6 +209,29 @@ describe("firestore", () => {
expect(func.__endpoint).to.deep.eq(expectedEp);
});

it("should create a func with param opts", () => {
const expectedEp = makeExpectedEp(
firestore.writtenEventType,
{
database: dbParam,
namespace: nsParam,
},
{
document: docParam,
}
);

const func = firestore.onDocumentWritten(
{
database: dbParam,
namespace: nsParam,
document: docParam,
},
() => true
);
expect(func.__endpoint).to.deep.eq(expectedEp);
});

it("calls init function", async () => {
const event: firestore.RawFirestoreEvent = {
...eventBase,
Expand Down Expand Up @@ -258,6 +296,29 @@ describe("firestore", () => {
expect(func.__endpoint).to.deep.eq(expectedEp);
});

it("should create a func with param opts", () => {
const expectedEp = makeExpectedEp(
firestore.createdEventType,
{
database: dbParam,
namespace: nsParam,
},
{
document: docParam,
}
);

const func = firestore.onDocumentCreated(
{
database: dbParam,
namespace: nsParam,
document: docParam,
},
() => true
);
expect(func.__endpoint).to.deep.eq(expectedEp);
});

it("calls init function", async () => {
const event: firestore.RawFirestoreEvent = {
...eventBase,
Expand Down Expand Up @@ -322,6 +383,29 @@ describe("firestore", () => {
expect(func.__endpoint).to.deep.eq(expectedEp);
});

it("should create a func with param opts", () => {
const expectedEp = makeExpectedEp(
firestore.updatedEventType,
{
database: dbParam,
namespace: nsParam,
},
{
document: docParam,
}
);

const func = firestore.onDocumentUpdated(
{
database: dbParam,
namespace: nsParam,
document: docParam,
},
() => true
);
expect(func.__endpoint).to.deep.eq(expectedEp);
});

it("calls init function", async () => {
const event: firestore.RawFirestoreEvent = {
...eventBase,
Expand Down Expand Up @@ -386,6 +470,29 @@ describe("firestore", () => {
expect(func.__endpoint).to.deep.eq(expectedEp);
});

it("should create a func with param opts", () => {
const expectedEp = makeExpectedEp(
firestore.deletedEventType,
{
database: dbParam,
namespace: nsParam,
},
{
document: docParam,
}
);

const func = firestore.onDocumentDeleted(
{
database: dbParam,
namespace: nsParam,
document: docParam,
},
() => true
);
expect(func.__endpoint).to.deep.eq(expectedEp);
});

it("calls init function", async () => {
const event: firestore.RawFirestoreEvent = {
...eventBase,
Expand Down Expand Up @@ -663,7 +770,7 @@ describe("firestore", () => {
const ep = firestore.makeEndpoint(
firestore.createdEventType,
{ region: "us-central1" },
new PathPattern("foo/{bar}"),
"foo/{bar}",
"my-db",
"my-ns"
);
Expand All @@ -686,7 +793,7 @@ describe("firestore", () => {
const ep = firestore.makeEndpoint(
firestore.createdEventType,
{ region: "us-central1" },
new PathPattern("foo/fGRodw71mHutZ4wGDuT8"),
"foo/fGRodw71mHutZ4wGDuT8",
"my-db",
"my-ns"
);
Expand Down
15 changes: 12 additions & 3 deletions src/common/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import { Expression } from "../params";

/**
* A type that splits literal string S with delimiter D.
*
Expand Down Expand Up @@ -78,10 +80,17 @@ export type Extract<Part extends string> = Part extends `{${infer Param}=**}`
*
* For flexibility reasons, ParamsOf<string> is Record<string, string>
*/
export type ParamsOf<PathPattern extends string> =
export type ParamsOf<PathPattern extends string | Expression<string>> =
// if we have lost type information, revert back to an untyped dictionary
string extends PathPattern
PathPattern extends Expression<string>
? Record<string, string>
: string extends PathPattern
? Record<string, string>
: {
[Key in Extract<Split<NullSafe<PathPattern>, "/">[number]>]: string;
// N.B. I'm not sure why PathPattern isn't detected to not be an
// Expression<string> per the check above. Since we have the check above
// The Exclude call should be safe.
[Key in Extract<
Split<NullSafe<Exclude<PathPattern, Expression<string>>>, "/">[number]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😱

>]: string;
};
52 changes: 31 additions & 21 deletions src/v2/providers/firestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
} from "../../common/providers/firestore";
import { wrapTraceContext } from "../trace";
import { withInit } from "../../common/onInit";
import { Expression } from "../../params";

export { Change };

Expand Down Expand Up @@ -106,11 +107,11 @@ export interface FirestoreEvent<T, Params = Record<string, string>> extends Clou
/** DocumentOptions extend EventHandlerOptions with provided document and optional database and namespace. */
export interface DocumentOptions<Document extends string = string> extends EventHandlerOptions {
/** The document path */
document: Document;
document: Document | Expression<string>;
/** The Firestore database */
database?: string;
database?: string | Expression<string>;
/** The Firestore namespace */
namespace?: string;
namespace?: string | Expression<string>;
}

/**
Expand Down Expand Up @@ -278,17 +279,20 @@ export function onDocumentDeleted<Document extends string>(

/** @internal */
export function getOpts(documentOrOpts: string | DocumentOptions) {
let document: string;
let database: string;
let namespace: string;
let document: string | Expression<string>;
let database: string | Expression<string>;
let namespace: string | Expression<string>;
let opts: EventHandlerOptions;
if (typeof documentOrOpts === "string") {
document = normalizePath(documentOrOpts);
database = "(default)";
namespace = "(default)";
opts = {};
} else {
document = normalizePath(documentOrOpts.document);
document =
typeof documentOrOpts.document === "string"
? normalizePath(documentOrOpts.document)
: documentOrOpts.document;
database = documentOrOpts.database || "(default)";
namespace = documentOrOpts.namespace || "(default)";
opts = { ...documentOrOpts };
Expand Down Expand Up @@ -398,21 +402,25 @@ export function makeChangedFirestoreEvent<Params>(
export function makeEndpoint(
eventType: string,
opts: EventHandlerOptions,
document: PathPattern,
database: string,
namespace: string
document: string | Expression<string>,
database: string | Expression<string>,
namespace: string | Expression<string>
): ManifestEndpoint {
const baseOpts = optionsToEndpoint(getGlobalOptions());
const specificOpts = optionsToEndpoint(opts);

const eventFilters: Record<string, string> = {
const eventFilters: Record<string, string | Expression<string>> = {
database,
namespace,
};
const eventFilterPathPatterns: Record<string, string> = {};
document.hasWildcards()
? (eventFilterPathPatterns.document = document.getValue())
: (eventFilters.document = document.getValue());
const eventFilterPathPatterns: Record<string, string | Expression<string>> = {};
const maybePattern =
typeof document === "string" ? new PathPattern(document).hasWildcards() : true;
if (maybePattern) {
eventFilterPathPatterns.document = document;
} else {
eventFilters.document = document;
}

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

const documentPattern = new PathPattern(document);

// wrap the handler
const func = (raw: CloudEvent<unknown>) => {
const event = raw as RawFirestoreEvent;
const documentPattern = new PathPattern(
typeof document === "string" ? document : document.value()
);
const params = makeParams(event.document, documentPattern) as unknown as ParamsOf<Document>;
const firestoreEvent = makeFirestoreEvent(eventType, event, params);
return wrapTraceContext(withInit(handler))(firestoreEvent);
};

func.run = handler;

func.__endpoint = makeEndpoint(eventType, opts, documentPattern, database, namespace);
func.__endpoint = makeEndpoint(eventType, opts, document, database, namespace);

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

const documentPattern = new PathPattern(document);

// wrap the handler
const func = (raw: CloudEvent<unknown>) => {
const event = raw as RawFirestoreEvent;
const documentPattern = new PathPattern(
typeof document === "string" ? document : document.value()
);
const params = makeParams(event.document, documentPattern) as unknown as ParamsOf<Document>;
const firestoreEvent = makeChangedFirestoreEvent(event, params);
return wrapTraceContext(withInit(handler))(firestoreEvent);
};

func.run = handler;

func.__endpoint = makeEndpoint(eventType, opts, documentPattern, database, namespace);
func.__endpoint = makeEndpoint(eventType, opts, document, database, namespace);

return func;
}