Skip to content

Make WrappedFunction curry type info #138

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
May 6, 2022
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
30 changes: 15 additions & 15 deletions spec/cloudevent/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,39 +20,39 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import {expect} from 'chai';
import { expect } from 'chai';

import {alerts, storage} from 'firebase-functions/v2';
import {generateMockCloudEvent} from '../../src/cloudevent/generate';
import { alerts, storage } from 'firebase-functions/v2';
import { generateMockCloudEvent } from '../../src/cloudevent/generate';

describe('generate (CloudEvent)', () => {
describe('#generateMockCloudEvent', () => {
describe('alerts.billing.onPlanAutomatedUpdatePublished()', () => {
it('should create CloudEvent with appropriate fields', () => {
const cloudFn = alerts.billing.onPlanAutomatedUpdatePublished(() => {
});
const cloudEvent =
generateMockCloudEvent(cloudFn);
const cloudFn = alerts.billing.onPlanAutomatedUpdatePublished(() => {});
const cloudEvent = generateMockCloudEvent(cloudFn);

expect(cloudEvent.type).equal(
'google.firebase.firebasealerts.alerts.v1.published');
'google.firebase.firebasealerts.alerts.v1.published'
);
expect(cloudEvent.source).equal(
'//firebasealerts.googleapis.com/projects/42');
'//firebasealerts.googleapis.com/projects/42'
);
expect(cloudEvent.subject).equal(undefined);
});
});
describe('storage.onObjectArchived', () => {
it('should create CloudEvent with appropriate fields', () => {
const bucketName = 'bucket_name';
const cloudFn = storage.onObjectArchived(bucketName, () => {
});
const cloudEvent =
generateMockCloudEvent(cloudFn);
const cloudFn = storage.onObjectArchived(bucketName, () => {});
const cloudEvent = generateMockCloudEvent(cloudFn);

expect(cloudEvent.type).equal(
'google.cloud.storage.object.v1.archived');
'google.cloud.storage.object.v1.archived'
);
expect(cloudEvent.source).equal(
`//storage.googleapis.com/projects/_/buckets/${bucketName}`);
`//storage.googleapis.com/projects/_/buckets/${bucketName}`
);
expect(cloudEvent.subject).equal('objects/file_name');
});
});
Expand Down
11 changes: 6 additions & 5 deletions spec/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ import {
} from '../src/main';
import { features } from '../src/features';
import { FirebaseFunctionsTest } from '../src/lifecycle';
import {alerts} from 'firebase-functions/v2';
import {wrapV2} from '../src/v2';
import { alerts } from 'firebase-functions/v2';
import { wrapV2 } from '../src/v2';

describe('main', () => {
describe('#wrap', () => {
Expand Down Expand Up @@ -230,12 +230,13 @@ describe('main', () => {

describe('v2 functions', () => {
it('should invoke wrapV2 wrapper', () => {
const handler = (cloudEvent) => ({cloudEvent});
const handler = (cloudEvent) => ({ cloudEvent });
const cloudFn = alerts.billing.onPlanAutomatedUpdatePublished(handler);
const cloudFnWrap = wrapV2(cloudFn);

const expectedType = 'google.firebase.firebasealerts.alerts.v1.published';
expect(cloudFnWrap().cloudEvent).to.include({type: expectedType});
const expectedType =
'google.firebase.firebasealerts.alerts.v1.published';
expect(cloudFnWrap().cloudEvent).to.include({ type: expectedType });
});
});

Expand Down
91 changes: 57 additions & 34 deletions spec/v2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,22 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import {expect} from 'chai';
import { expect } from 'chai';

import {
wrapV2,
} from '../src/v2';
import { wrapV2 } from '../src/v2';

import {
CloudFunction, CloudEvent, alerts, pubsub, storage, eventarc
CloudFunction,
alerts,
pubsub,
storage,
eventarc,
https,
} from 'firebase-functions/v2';

describe('v2', () => {
describe('#wrapV2', () => {
const handler = (cloudEvent) => ({cloudEvent});
const handler = (cloudEvent) => ({ cloudEvent });

describe('alerts', () => {
describe('alerts.onAlertPublished()', () => {
Expand All @@ -58,21 +61,27 @@ describe('v2', () => {
});
describe('alerts.crashlytics.onNewNonfatalIssuePublished()', () => {
it('should update CloudEvent appropriately', () => {
const cloudFn = alerts.crashlytics.onNewNonfatalIssuePublished(handler);
const cloudFn = alerts.crashlytics.onNewNonfatalIssuePublished(
handler
);
const cloudFnWrap = wrapV2(cloudFn);
expect(cloudFnWrap().cloudEvent).to.include({});
});
});
describe('alerts.crashlytics.onRegressionAlertPublished()', () => {
it('should update CloudEvent appropriately', () => {
const cloudFn = alerts.crashlytics.onRegressionAlertPublished(handler);
const cloudFn = alerts.crashlytics.onRegressionAlertPublished(
handler
);
const cloudFnWrap = wrapV2(cloudFn);
expect(cloudFnWrap().cloudEvent).to.include({});
});
});
describe('alerts.crashlytics.onStabilityDigestPublished()', () => {
it('should update CloudEvent appropriately', () => {
const cloudFn = alerts.crashlytics.onStabilityDigestPublished(handler);
const cloudFn = alerts.crashlytics.onStabilityDigestPublished(
handler
);
const cloudFnWrap = wrapV2(cloudFn);
expect(cloudFnWrap().cloudEvent).to.include({});
});
Expand All @@ -86,14 +95,18 @@ describe('v2', () => {
});
describe('alerts.appDistribution.onNewTesterIosDevicePublished()', () => {
it('should update CloudEvent appropriately', () => {
const cloudFn = alerts.appDistribution.onNewTesterIosDevicePublished(handler);
const cloudFn = alerts.appDistribution.onNewTesterIosDevicePublished(
handler
);
const cloudFnWrap = wrapV2(cloudFn);
expect(cloudFnWrap().cloudEvent).to.include({});
});
});
describe('alerts.billing.onPlanAutomatedUpdatePublished()', () => {
it('should update CloudEvent appropriately', () => {
const cloudFn = alerts.billing.onPlanAutomatedUpdatePublished(handler);
const cloudFn = alerts.billing.onPlanAutomatedUpdatePublished(
handler
);
const cloudFnWrap = wrapV2(cloudFn);
expect(cloudFnWrap().cloudEvent).to.include({});
});
Expand All @@ -113,7 +126,7 @@ describe('v2', () => {
const eventType = 'EVENT_TYPE';
const cloudFn = eventarc.onCustomEventPublished(eventType, handler);
const cloudFnWrap = wrapV2(cloudFn);
expect(cloudFnWrap().cloudEvent).to.include({type: eventType});
expect(cloudFnWrap().cloudEvent).to.include({ type: eventType });
});
});
});
Expand Down Expand Up @@ -162,44 +175,50 @@ describe('v2', () => {
it('should update CloudEvent with json data override', () => {
const data = {
message: {
json: {firebase: 'test'}
json: { firebase: 'test' },
},
subscription: 'subscription'
subscription: 'subscription',
};
const cloudFn = pubsub.onMessagePublished('topic', handler);
const cloudFnWrap = wrapV2(cloudFn);
const cloudEventPartial = {data};
const cloudEventPartial = { data };

expect(cloudFnWrap(cloudEventPartial).cloudEvent.data.message).to.include({
expect(
cloudFnWrap(cloudEventPartial).cloudEvent.data.message
).to.include({
data: 'eyJoZWxsbyI6IndvcmxkIn0=', // Note: This is a mismatch from the json
});
expect(cloudFnWrap(cloudEventPartial).cloudEvent.data.message.json).to.include({firebase: 'test'});
expect(
cloudFnWrap(cloudEventPartial).cloudEvent.data.message.json
).to.include({ firebase: 'test' });
});
it('should update CloudEvent with json and data string overrides', () => {
const data = {
message: {
data: 'eyJmaXJlYmFzZSI6Im5vbl9qc29uX3Rlc3QifQ==',
json: {firebase: 'non_json_test'},
json: { firebase: 'non_json_test' },
},
subscription: 'subscription'
subscription: 'subscription',
};
const cloudFn = pubsub.onMessagePublished('topic', handler);
const cloudFnWrap = wrapV2(cloudFn);
const cloudEventPartial = {data};
const cloudEventPartial = { data };

expect(cloudFnWrap(cloudEventPartial).cloudEvent.data.message).to.include({
expect(
cloudFnWrap(cloudEventPartial).cloudEvent.data.message
).to.include({
data: 'eyJmaXJlYmFzZSI6Im5vbl9qc29uX3Rlc3QifQ==',
});
expect(cloudFnWrap(cloudEventPartial).cloudEvent.data.message.json)
.to.include({firebase: 'non_json_test'});
expect(
cloudFnWrap(cloudEventPartial).cloudEvent.data.message.json
).to.include({ firebase: 'non_json_test' });
});
});
});

describe('callable functions', () => {
it('return an error because they are not supported', () => {
const cloudFunction = (input) => input;
cloudFunction.run = (cloudEvent: CloudEvent) => ({cloudEvent});
const cloudFunction = https.onCall((data) => data);
cloudFunction.__endpoint = {
platform: 'gcfv2',
labels: {},
Expand All @@ -210,11 +229,12 @@ describe('v2', () => {
};

try {
const wrappedCF = wrapV2(cloudFunction as CloudFunction<any>);
const wrappedCF = wrapV2(cloudFunction as any);
wrappedCF();
} catch (e) {
expect(e.message).to.equal(
'Wrap function is not available for callableTriggers functions.');
'Wrap function is not available for callableTriggers functions.'
);
}
});
});
Expand All @@ -223,17 +243,18 @@ describe('v2', () => {
it('should create CloudEvent with appropriate fields for pubsub.onMessagePublished()', () => {
const data = {
message: {
json: '{"hello_firebase": "world_firebase"}'
json: '{"hello_firebase": "world_firebase"}',
},
subscription: 'subscription'
subscription: 'subscription',
};
const cloudFn = pubsub.onMessagePublished('topic', handler);
const cloudEvent = wrapV2(cloudFn)({data}).cloudEvent;
const cloudEvent = wrapV2(cloudFn)({ data }).cloudEvent;

expect(cloudEvent.type).equal(
'google.cloud.pubsub.topic.v1.messagePublished');
'google.cloud.pubsub.topic.v1.messagePublished'
);
expect(cloudEvent.data.message).to.include({
json: '{"hello_firebase": "world_firebase"}'
json: '{"hello_firebase": "world_firebase"}',
});
});
it('should generate source from original CloudFunction', () => {
Expand Down Expand Up @@ -283,15 +304,17 @@ describe('v2', () => {
const cloudEventOverride = {
data: {
contentType: 'application/octet-stream',
}
},
};

const bucketName = 'bucket_name';
const cloudFn = storage.onObjectArchived(bucketName, handler);

const mergedCloudEvent = wrapV2(cloudFn)(cloudEventOverride).cloudEvent;
expect(mergedCloudEvent.data?.size).equal(42);
expect(mergedCloudEvent.data?.contentType).equal('application/octet-stream');
expect(mergedCloudEvent.data?.contentType).equal(
'application/octet-stream'
);
});
});
});
Expand Down
50 changes: 33 additions & 17 deletions src/cloudevent/generate.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,62 @@
import {CloudEvent} from 'firebase-functions/v2';
import {CloudFunction} from 'firebase-functions/v2';
import {LIST_OF_MOCK_CLOUD_EVENT_PARTIALS} from './partials/partials';
import {DeepPartial} from './types';
import { CloudEvent } from 'firebase-functions/v2';
import { CloudFunction } from 'firebase-functions/v2';
import { LIST_OF_MOCK_CLOUD_EVENT_PARTIALS } from './partials/partials';
import { DeepPartial, MockCloudEventPartials } from './types';
import merge from 'ts-deepmerge';
import {getEventType} from './partials/helpers';
import { getEventType } from './partials/helpers';

/**
* @return {CloudEvent} Generated Mock CloudEvent
*/
export function generateCombinedCloudEvent<EventType>(
export function generateCombinedCloudEvent<
EventType extends CloudEvent<unknown>
>(
cloudFunction: CloudFunction<EventType>,
cloudEventPartial?: DeepPartial<CloudEvent>): CloudEvent {
cloudEventPartial?: DeepPartial<EventType>
): EventType {
const generatedCloudEvent = generateMockCloudEvent(cloudFunction);
return cloudEventPartial ? merge(generatedCloudEvent, cloudEventPartial) : generatedCloudEvent;
return cloudEventPartial
? (merge(generatedCloudEvent, cloudEventPartial) as EventType)
: generatedCloudEvent;
}

/** @internal */
export function generateMockCloudEvent<EventType>(
cloudFunction: CloudFunction<EventType>): CloudEvent {
export function generateMockCloudEvent<EventType extends CloudEvent<unknown>>(
cloudFunction: CloudFunction<EventType>
): EventType {
return {
...generateBaseCloudEvent(cloudFunction),
...generateMockCloudEventPartial<EventType>(cloudFunction)
...generateMockCloudEventPartial(cloudFunction),
};
}

/** @internal */
function generateBaseCloudEvent<EventType>(cloudFunction: CloudFunction<EventType>): CloudEvent {
function generateBaseCloudEvent<EventType extends CloudEvent<unknown>>(
cloudFunction: CloudFunction<EventType>
): EventType {
// TODO: Consider refactoring so that we don't use this utility function. This
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

// is not type safe because EventType may require additional fields, which this
// function does not know how to satisfy.
// This could possibly be augmented to take a CloudEvent<unknown> and AdditionalFields<EventType>
// where AdditionalFields uses the keyof operator to make only new fields required.
return {
specversion: '1.0',
id: makeEventId(),
data: {},
source: '', // Required field that will get overridden by Provider-specific MockCloudEventPartials
type: getEventType(cloudFunction),
time: new Date().toISOString()
};
time: new Date().toISOString(),
} as any;
}

function generateMockCloudEventPartial<EventType>(
cloudFunction: CloudFunction<EventType>): DeepPartial<CloudEvent<EventType>> {
function generateMockCloudEventPartial<EventType extends CloudEvent<unknown>>(
cloudFunction: CloudFunction<EventType>
): DeepPartial<EventType> {
for (const mockCloudEventPartial of LIST_OF_MOCK_CLOUD_EVENT_PARTIALS) {
if (mockCloudEventPartial.match(cloudFunction)) {
return mockCloudEventPartial.generatePartial(cloudFunction);
return (mockCloudEventPartial as MockCloudEventPartials<
EventType
>).generatePartial(cloudFunction);
}
}
// No matches were found
Expand Down
Loading