Skip to content

Ugly type safety #1548

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 3 commits into from
Apr 3, 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
40 changes: 38 additions & 2 deletions spec/v2/providers/firestore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ const eventBase = {
datacontenttype: "application/protobuf",
dataschema:
"https://github.com/googleapis/google-cloudevents/blob/main/proto/google/events/cloud/firestore/v1/data.proto",
authtype: "unknown",
authid: "1234",
id: "379ad868-5ef9-4c84-a8ba-f75f1b056663",
source: "projects/my-project/databases/my-db/documents/d",
subject: "documents/foo/fGRodw71mHutZ4wGDuT8",
Expand Down Expand Up @@ -86,6 +84,15 @@ function makeEvent(data?: any): firestore.RawFirestoreEvent {
} as firestore.RawFirestoreEvent;
}

function makeAuthEvent(data?: any): firestore.RawFirestoreAuthEvent {
return {
...eventBase,
data,
authid: "userId",
authtype: "unknown",
} as firestore.RawFirestoreAuthEvent;
}

const createdData = {
value: {
fields: {
Expand Down Expand Up @@ -910,6 +917,26 @@ describe("firestore", () => {

expect(event.data.data()).to.deep.eq({ hello: "delete world" });
});

it("should make event from a created event with auth context", () => {
const event = firestore.makeFirestoreEvent(
firestore.createdEventWithAuthContextType,
makeAuthEvent(makeEncodedProtobuf(createdProto)),
firestore.makeParams("foo/fGRodw71mHutZ4wGDuT8", new PathPattern("foo/{bar}"))
);

expect(event.data.data()).to.deep.eq({ hello: "create world" });
});

it("should include auth fields if provided in raw event", () => {
const event = firestore.makeFirestoreEvent(
firestore.createdEventWithAuthContextType,
makeAuthEvent(makeEncodedProtobuf(createdProto)),
firestore.makeParams("foo/fGRodw71mHutZ4wGDuT8", new PathPattern("foo/{bar}"))
);

expect(event).to.include({ authId: "userId", authType: "unknown" });
});
});

describe("makeChangedFirestoreEvent", () => {
Expand Down Expand Up @@ -943,6 +970,15 @@ describe("firestore", () => {
});
});

it("should include auth fields if provided in raw event", () => {
const event = firestore.makeChangedFirestoreEvent(
makeAuthEvent(makeEncodedProtobuf(writtenProto)),
firestore.makeParams("foo/fGRodw71mHutZ4wGDuT8", new PathPattern("foo/{bar}"))
);

expect(event).to.include({ authId: "userId", authType: "unknown" });
});

describe("makeEndpoint", () => {
it("should make an endpoint with a document path pattern", () => {
const expectedEp = makeExpectedEp(
Expand Down
147 changes: 97 additions & 50 deletions src/v2/providers/firestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ export interface RawFirestoreEvent extends CloudEvent<Uint8Array | RawFirestoreD
document: string;
datacontenttype?: string;
dataschema: string;
authtype?: string;
}

/** @internal */
export interface RawFirestoreAuthEvent extends RawFirestoreEvent {
authtype?: AuthType;
authid?: string;
}

Expand Down Expand Up @@ -125,17 +129,21 @@ export interface FirestoreEvent<T, Params = Record<string, string>> extends Clou
namespace: string;
/** The document path */
document: string;
/** The type of principal that triggered the event */
authType?: AuthType;
/** The unique identifier for the principal */
authId?: string;
/**
* An object containing the values of the path patterns.
* Only named capture groups will be populated - {key}, {key=*}, {key=**}
*/
params: Params;
}

export interface FirestoreAuthEvent<T, Params = Record<string, string>>
extends FirestoreEvent<T, Params> {
/** The type of principal that triggered the event */
authType: AuthType;
/** The unique identifier for the principal */
authId?: string;
}

/** DocumentOptions extend EventHandlerOptions with provided document and optional database and namespace. */
export interface DocumentOptions<Document extends string = string> extends EventHandlerOptions {
/** The document path */
Expand Down Expand Up @@ -197,9 +205,9 @@ export function onDocumentWritten<Document extends string>(
export function onDocumentWrittenWithAuthContext<Document extends string>(
document: Document,
handler: (
event: FirestoreEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>
event: FirestoreAuthEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>
) => any | Promise<any>
): CloudFunction<FirestoreEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>>;
): CloudFunction<FirestoreAuthEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>>;

/**
* Event handler that triggers when a document is created, updated, or deleted in Firestore.
Expand All @@ -211,9 +219,9 @@ export function onDocumentWrittenWithAuthContext<Document extends string>(
export function onDocumentWrittenWithAuthContext<Document extends string>(
opts: DocumentOptions<Document>,
handler: (
event: FirestoreEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>
event: FirestoreAuthEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>
) => any | Promise<any>
): CloudFunction<FirestoreEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>>;
): CloudFunction<FirestoreAuthEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>>;

/**
* Event handler that triggers when a document is created, updated, or deleted in Firestore.
Expand All @@ -223,12 +231,19 @@ export function onDocumentWrittenWithAuthContext<Document extends string>(
* @param handler - Event handler which is run every time a Firestore create, update, or delete occurs.
*/
export function onDocumentWrittenWithAuthContext<Document extends string>(
documentorOpts: Document | DocumentOptions<Document>,
documentOrOpts: Document | DocumentOptions<Document>,
handler: (
event: FirestoreEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>
event: FirestoreAuthEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>
) => any | Promise<any>
): CloudFunction<FirestoreEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>> {
return onChangedOperation(writtenEventWithAuthContextType, documentorOpts, handler);
): CloudFunction<FirestoreAuthEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>> {
// const fn = (
// event: FirestoreEvent<Change<QueryDocumentSnapshot> | undefined, ParamsOf<Document>> & {
// foo: string;
// }
// ): any => {
// return event;
// };
return onChangedOperation(writtenEventWithAuthContextType, documentOrOpts, handler);
}

/**
Expand Down Expand Up @@ -282,9 +297,9 @@ export function onDocumentCreated<Document extends string>(
export function onDocumentCreatedWithAuthContext<Document extends string>(
document: Document,
handler: (
event: FirestoreEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>
event: FirestoreAuthEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>
) => any | Promise<any>
): CloudFunction<FirestoreEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>>;
): CloudFunction<FirestoreAuthEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>>;

/**
* Event handler that triggers when a document is created in Firestore.
Expand All @@ -296,9 +311,9 @@ export function onDocumentCreatedWithAuthContext<Document extends string>(
export function onDocumentCreatedWithAuthContext<Document extends string>(
opts: DocumentOptions<Document>,
handler: (
event: FirestoreEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>
event: FirestoreAuthEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>
) => any | Promise<any>
): CloudFunction<FirestoreEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>>;
): CloudFunction<FirestoreAuthEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>>;

/**
* Event handler that triggers when a document is created in Firestore.
Expand All @@ -309,9 +324,9 @@ export function onDocumentCreatedWithAuthContext<Document extends string>(
export function onDocumentCreatedWithAuthContext<Document extends string>(
documentOrOpts: Document | DocumentOptions<Document>,
handler: (
event: FirestoreEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>
event: FirestoreAuthEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>
) => any | Promise<any>
): CloudFunction<FirestoreEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>> {
): CloudFunction<FirestoreAuthEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>> {
return onOperation(createdEventWithAuthContextType, documentOrOpts, handler);
}

Expand Down Expand Up @@ -365,9 +380,10 @@ export function onDocumentUpdated<Document extends string>(
export function onDocumentUpdatedWithAuthContext<Document extends string>(
document: Document,
handler: (
event: FirestoreEvent<Change<QueryDocumentSnapshot> | undefined, ParamsOf<Document>>
event: FirestoreAuthEvent<Change<QueryDocumentSnapshot> | undefined, ParamsOf<Document>>
) => any | Promise<any>
): CloudFunction<FirestoreEvent<Change<QueryDocumentSnapshot> | undefined, ParamsOf<Document>>>;
): CloudFunction<FirestoreAuthEvent<Change<QueryDocumentSnapshot> | undefined, ParamsOf<Document>>>;

/**
* Event handler that triggers when a document is updated in Firestore.
* This trigger also provides the authentication context of the principal who triggered the event.
Expand All @@ -378,9 +394,9 @@ export function onDocumentUpdatedWithAuthContext<Document extends string>(
export function onDocumentUpdatedWithAuthContext<Document extends string>(
opts: DocumentOptions<Document>,
handler: (
event: FirestoreEvent<Change<QueryDocumentSnapshot> | undefined, ParamsOf<Document>>
event: FirestoreAuthEvent<Change<QueryDocumentSnapshot> | undefined, ParamsOf<Document>>
) => any | Promise<any>
): CloudFunction<FirestoreEvent<Change<QueryDocumentSnapshot> | undefined, ParamsOf<Document>>>;
): CloudFunction<FirestoreAuthEvent<Change<QueryDocumentSnapshot> | undefined, ParamsOf<Document>>>;

/**
* Event handler that triggers when a document is updated in Firestore.
Expand All @@ -391,9 +407,11 @@ export function onDocumentUpdatedWithAuthContext<Document extends string>(
export function onDocumentUpdatedWithAuthContext<Document extends string>(
documentOrOpts: Document | DocumentOptions<Document>,
handler: (
event: FirestoreEvent<Change<QueryDocumentSnapshot> | undefined, ParamsOf<Document>>
event: FirestoreAuthEvent<Change<QueryDocumentSnapshot> | undefined, ParamsOf<Document>>
) => any | Promise<any>
): CloudFunction<FirestoreEvent<Change<QueryDocumentSnapshot> | undefined, ParamsOf<Document>>> {
): CloudFunction<
FirestoreAuthEvent<Change<QueryDocumentSnapshot> | undefined, ParamsOf<Document>>
> {
return onChangedOperation(updatedEventWithAuthContextType, documentOrOpts, handler);
}

Expand Down Expand Up @@ -448,9 +466,9 @@ export function onDocumentDeleted<Document extends string>(
export function onDocumentDeletedWithAuthContext<Document extends string>(
document: Document,
handler: (
event: FirestoreEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>
event: FirestoreAuthEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>
) => any | Promise<any>
): CloudFunction<FirestoreEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>>;
): CloudFunction<FirestoreAuthEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>>;

/**
* Event handler that triggers when a document is deleted in Firestore.
Expand All @@ -462,9 +480,9 @@ export function onDocumentDeletedWithAuthContext<Document extends string>(
export function onDocumentDeletedWithAuthContext<Document extends string>(
opts: DocumentOptions<Document>,
handler: (
event: FirestoreEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>
event: FirestoreAuthEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>
) => any | Promise<any>
): CloudFunction<FirestoreEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>>;
): CloudFunction<FirestoreAuthEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>>;

/**
* Event handler that triggers when a document is deleted in Firestore.
Expand All @@ -475,9 +493,9 @@ export function onDocumentDeletedWithAuthContext<Document extends string>(
export function onDocumentDeletedWithAuthContext<Document extends string>(
documentOrOpts: Document | DocumentOptions<Document>,
handler: (
event: FirestoreEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>
event: FirestoreAuthEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>
) => any | Promise<any>
): CloudFunction<FirestoreEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>> {
): CloudFunction<FirestoreAuthEvent<QueryDocumentSnapshot | undefined, ParamsOf<Document>>> {
return onOperation(deletedEventWithAuthContextType, documentOrOpts, handler);
}

Expand Down Expand Up @@ -566,43 +584,68 @@ export function makeParams(document: string, documentPattern: PathPattern) {
/** @internal */
export function makeFirestoreEvent<Params>(
eventType: string,
event: RawFirestoreEvent,
event: RawFirestoreEvent | RawFirestoreAuthEvent,
params: Params
): FirestoreEvent<QueryDocumentSnapshot | undefined, Params> {
):
| FirestoreEvent<QueryDocumentSnapshot | undefined, Params>
| FirestoreAuthEvent<QueryDocumentSnapshot | undefined, Params> {
const data = event.data
? eventType === createdEventType
? eventType === createdEventType || eventType === createdEventWithAuthContextType
? createSnapshot(event)
: createBeforeSnapshot(event)
: undefined;
const firestoreEvent: FirestoreEvent<QueryDocumentSnapshot | undefined, Params> = {
...event,
params,
data,
authType: event.authtype as AuthType,
authId: event.authid,
};

delete (firestoreEvent as any).datacontenttype;
delete (firestoreEvent as any).dataschema;

if ("authtype" in event) {
const eventWithAuth = {
...firestoreEvent,
authType: event.authtype,
authId: event.authid,
};
delete (eventWithAuth as any).authtype;
delete (eventWithAuth as any).authid;
return eventWithAuth;
}

return firestoreEvent;
}

/** @internal */
export function makeChangedFirestoreEvent<Params>(
event: RawFirestoreEvent,
event: RawFirestoreEvent | RawFirestoreAuthEvent,
params: Params
): FirestoreEvent<Change<QueryDocumentSnapshot> | undefined, Params> {
):
| FirestoreEvent<Change<DocumentSnapshot> | undefined, Params>
| FirestoreAuthEvent<Change<DocumentSnapshot> | undefined, Params> {
const data = event.data
? Change.fromObjects(createBeforeSnapshot(event), createSnapshot(event))
: undefined;
const firestoreEvent: FirestoreEvent<Change<QueryDocumentSnapshot> | undefined, Params> = {
...event,
params,
data,
authType: event.authtype as AuthType,
authId: event.authid,
};
delete (firestoreEvent as any).datacontenttype;
delete (firestoreEvent as any).dataschema;

if ("authtype" in event) {
const eventWithAuth = {
...firestoreEvent,
authType: event.authtype,
authId: event.authid,
};
delete (eventWithAuth as any).authtype;
delete (eventWithAuth as any).authid;
return eventWithAuth;
}

return firestoreEvent;
}

Expand Down Expand Up @@ -649,16 +692,19 @@ export function makeEndpoint(
}

/** @internal */
export function onOperation<Document extends string>(
export function onOperation<
Document extends string,
Event extends FirestoreEvent<QueryDocumentSnapshot, ParamsOf<Document>>
>(
eventType: string,
documentOrOpts: Document | DocumentOptions<Document>,
handler: (event: FirestoreEvent<QueryDocumentSnapshot, ParamsOf<Document>>) => any | Promise<any>
): CloudFunction<FirestoreEvent<QueryDocumentSnapshot, ParamsOf<Document>>> {
handler: (event: Event) => any | Promise<any>
): CloudFunction<Event> {
const { document, database, namespace, opts } = getOpts(documentOrOpts);

// wrap the handler
const func = (raw: CloudEvent<unknown>) => {
const event = raw as RawFirestoreEvent;
const event = raw as RawFirestoreEvent | RawFirestoreAuthEvent;
const documentPattern = new PathPattern(
typeof document === "string" ? document : document.value()
);
Expand All @@ -675,18 +721,19 @@ export function onOperation<Document extends string>(
}

/** @internal */
export function onChangedOperation<Document extends string>(
export function onChangedOperation<
Document extends string,
Event extends FirestoreEvent<Change<DocumentSnapshot>, ParamsOf<Document>>
>(
eventType: string,
documentOrOpts: Document | DocumentOptions<Document>,
handler: (
event: FirestoreEvent<Change<QueryDocumentSnapshot>, ParamsOf<Document>>
) => any | Promise<any>
): CloudFunction<FirestoreEvent<Change<QueryDocumentSnapshot>, ParamsOf<Document>>> {
handler: (event: Event) => any | Promise<any>
): CloudFunction<Event> {
const { document, database, namespace, opts } = getOpts(documentOrOpts);

// wrap the handler
const func = (raw: CloudEvent<unknown>) => {
const event = raw as RawFirestoreEvent;
const event = raw as RawFirestoreEvent | RawFirestoreAuthEvent;
const documentPattern = new PathPattern(
typeof document === "string" ? document : document.value()
);
Expand Down