Skip to content

Commit 9f401ca

Browse files
authored
Make WrappedFunction curry type info (#138)
* Make WrappedFunction curry type info * add missing file
1 parent f6e9d93 commit 9f401ca

28 files changed

+543
-442
lines changed

spec/cloudevent/generate.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,39 +20,39 @@
2020
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
// SOFTWARE.
2222

23-
import {expect} from 'chai';
23+
import { expect } from 'chai';
2424

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

2828
describe('generate (CloudEvent)', () => {
2929
describe('#generateMockCloudEvent', () => {
3030
describe('alerts.billing.onPlanAutomatedUpdatePublished()', () => {
3131
it('should create CloudEvent with appropriate fields', () => {
32-
const cloudFn = alerts.billing.onPlanAutomatedUpdatePublished(() => {
33-
});
34-
const cloudEvent =
35-
generateMockCloudEvent(cloudFn);
32+
const cloudFn = alerts.billing.onPlanAutomatedUpdatePublished(() => {});
33+
const cloudEvent = generateMockCloudEvent(cloudFn);
3634

3735
expect(cloudEvent.type).equal(
38-
'google.firebase.firebasealerts.alerts.v1.published');
36+
'google.firebase.firebasealerts.alerts.v1.published'
37+
);
3938
expect(cloudEvent.source).equal(
40-
'//firebasealerts.googleapis.com/projects/42');
39+
'//firebasealerts.googleapis.com/projects/42'
40+
);
4141
expect(cloudEvent.subject).equal(undefined);
4242
});
4343
});
4444
describe('storage.onObjectArchived', () => {
4545
it('should create CloudEvent with appropriate fields', () => {
4646
const bucketName = 'bucket_name';
47-
const cloudFn = storage.onObjectArchived(bucketName, () => {
48-
});
49-
const cloudEvent =
50-
generateMockCloudEvent(cloudFn);
47+
const cloudFn = storage.onObjectArchived(bucketName, () => {});
48+
const cloudEvent = generateMockCloudEvent(cloudFn);
5149

5250
expect(cloudEvent.type).equal(
53-
'google.cloud.storage.object.v1.archived');
51+
'google.cloud.storage.object.v1.archived'
52+
);
5453
expect(cloudEvent.source).equal(
55-
`//storage.googleapis.com/projects/_/buckets/${bucketName}`);
54+
`//storage.googleapis.com/projects/_/buckets/${bucketName}`
55+
);
5656
expect(cloudEvent.subject).equal('objects/file_name');
5757
});
5858
});

spec/main.spec.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ import {
3333
} from '../src/main';
3434
import { features } from '../src/features';
3535
import { FirebaseFunctionsTest } from '../src/lifecycle';
36-
import {alerts} from 'firebase-functions/v2';
37-
import {wrapV2} from '../src/v2';
36+
import { alerts } from 'firebase-functions/v2';
37+
import { wrapV2 } from '../src/v2';
3838

3939
describe('main', () => {
4040
describe('#wrap', () => {
@@ -230,12 +230,13 @@ describe('main', () => {
230230

231231
describe('v2 functions', () => {
232232
it('should invoke wrapV2 wrapper', () => {
233-
const handler = (cloudEvent) => ({cloudEvent});
233+
const handler = (cloudEvent) => ({ cloudEvent });
234234
const cloudFn = alerts.billing.onPlanAutomatedUpdatePublished(handler);
235235
const cloudFnWrap = wrapV2(cloudFn);
236236

237-
const expectedType = 'google.firebase.firebasealerts.alerts.v1.published';
238-
expect(cloudFnWrap().cloudEvent).to.include({type: expectedType});
237+
const expectedType =
238+
'google.firebase.firebasealerts.alerts.v1.published';
239+
expect(cloudFnWrap().cloudEvent).to.include({ type: expectedType });
239240
});
240241
});
241242

spec/v2.spec.ts

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,22 @@
2020
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
// SOFTWARE.
2222

23-
import {expect} from 'chai';
23+
import { expect } from 'chai';
2424

25-
import {
26-
wrapV2,
27-
} from '../src/v2';
25+
import { wrapV2 } from '../src/v2';
2826

2927
import {
30-
CloudFunction, CloudEvent, alerts, pubsub, storage, eventarc
28+
CloudFunction,
29+
alerts,
30+
pubsub,
31+
storage,
32+
eventarc,
33+
https,
3134
} from 'firebase-functions/v2';
3235

3336
describe('v2', () => {
3437
describe('#wrapV2', () => {
35-
const handler = (cloudEvent) => ({cloudEvent});
38+
const handler = (cloudEvent) => ({ cloudEvent });
3639

3740
describe('alerts', () => {
3841
describe('alerts.onAlertPublished()', () => {
@@ -58,21 +61,27 @@ describe('v2', () => {
5861
});
5962
describe('alerts.crashlytics.onNewNonfatalIssuePublished()', () => {
6063
it('should update CloudEvent appropriately', () => {
61-
const cloudFn = alerts.crashlytics.onNewNonfatalIssuePublished(handler);
64+
const cloudFn = alerts.crashlytics.onNewNonfatalIssuePublished(
65+
handler
66+
);
6267
const cloudFnWrap = wrapV2(cloudFn);
6368
expect(cloudFnWrap().cloudEvent).to.include({});
6469
});
6570
});
6671
describe('alerts.crashlytics.onRegressionAlertPublished()', () => {
6772
it('should update CloudEvent appropriately', () => {
68-
const cloudFn = alerts.crashlytics.onRegressionAlertPublished(handler);
73+
const cloudFn = alerts.crashlytics.onRegressionAlertPublished(
74+
handler
75+
);
6976
const cloudFnWrap = wrapV2(cloudFn);
7077
expect(cloudFnWrap().cloudEvent).to.include({});
7178
});
7279
});
7380
describe('alerts.crashlytics.onStabilityDigestPublished()', () => {
7481
it('should update CloudEvent appropriately', () => {
75-
const cloudFn = alerts.crashlytics.onStabilityDigestPublished(handler);
82+
const cloudFn = alerts.crashlytics.onStabilityDigestPublished(
83+
handler
84+
);
7685
const cloudFnWrap = wrapV2(cloudFn);
7786
expect(cloudFnWrap().cloudEvent).to.include({});
7887
});
@@ -86,14 +95,18 @@ describe('v2', () => {
8695
});
8796
describe('alerts.appDistribution.onNewTesterIosDevicePublished()', () => {
8897
it('should update CloudEvent appropriately', () => {
89-
const cloudFn = alerts.appDistribution.onNewTesterIosDevicePublished(handler);
98+
const cloudFn = alerts.appDistribution.onNewTesterIosDevicePublished(
99+
handler
100+
);
90101
const cloudFnWrap = wrapV2(cloudFn);
91102
expect(cloudFnWrap().cloudEvent).to.include({});
92103
});
93104
});
94105
describe('alerts.billing.onPlanAutomatedUpdatePublished()', () => {
95106
it('should update CloudEvent appropriately', () => {
96-
const cloudFn = alerts.billing.onPlanAutomatedUpdatePublished(handler);
107+
const cloudFn = alerts.billing.onPlanAutomatedUpdatePublished(
108+
handler
109+
);
97110
const cloudFnWrap = wrapV2(cloudFn);
98111
expect(cloudFnWrap().cloudEvent).to.include({});
99112
});
@@ -113,7 +126,7 @@ describe('v2', () => {
113126
const eventType = 'EVENT_TYPE';
114127
const cloudFn = eventarc.onCustomEventPublished(eventType, handler);
115128
const cloudFnWrap = wrapV2(cloudFn);
116-
expect(cloudFnWrap().cloudEvent).to.include({type: eventType});
129+
expect(cloudFnWrap().cloudEvent).to.include({ type: eventType });
117130
});
118131
});
119132
});
@@ -162,44 +175,50 @@ describe('v2', () => {
162175
it('should update CloudEvent with json data override', () => {
163176
const data = {
164177
message: {
165-
json: {firebase: 'test'}
178+
json: { firebase: 'test' },
166179
},
167-
subscription: 'subscription'
180+
subscription: 'subscription',
168181
};
169182
const cloudFn = pubsub.onMessagePublished('topic', handler);
170183
const cloudFnWrap = wrapV2(cloudFn);
171-
const cloudEventPartial = {data};
184+
const cloudEventPartial = { data };
172185

173-
expect(cloudFnWrap(cloudEventPartial).cloudEvent.data.message).to.include({
186+
expect(
187+
cloudFnWrap(cloudEventPartial).cloudEvent.data.message
188+
).to.include({
174189
data: 'eyJoZWxsbyI6IndvcmxkIn0=', // Note: This is a mismatch from the json
175190
});
176-
expect(cloudFnWrap(cloudEventPartial).cloudEvent.data.message.json).to.include({firebase: 'test'});
191+
expect(
192+
cloudFnWrap(cloudEventPartial).cloudEvent.data.message.json
193+
).to.include({ firebase: 'test' });
177194
});
178195
it('should update CloudEvent with json and data string overrides', () => {
179196
const data = {
180197
message: {
181198
data: 'eyJmaXJlYmFzZSI6Im5vbl9qc29uX3Rlc3QifQ==',
182-
json: {firebase: 'non_json_test'},
199+
json: { firebase: 'non_json_test' },
183200
},
184-
subscription: 'subscription'
201+
subscription: 'subscription',
185202
};
186203
const cloudFn = pubsub.onMessagePublished('topic', handler);
187204
const cloudFnWrap = wrapV2(cloudFn);
188-
const cloudEventPartial = {data};
205+
const cloudEventPartial = { data };
189206

190-
expect(cloudFnWrap(cloudEventPartial).cloudEvent.data.message).to.include({
207+
expect(
208+
cloudFnWrap(cloudEventPartial).cloudEvent.data.message
209+
).to.include({
191210
data: 'eyJmaXJlYmFzZSI6Im5vbl9qc29uX3Rlc3QifQ==',
192211
});
193-
expect(cloudFnWrap(cloudEventPartial).cloudEvent.data.message.json)
194-
.to.include({firebase: 'non_json_test'});
212+
expect(
213+
cloudFnWrap(cloudEventPartial).cloudEvent.data.message.json
214+
).to.include({ firebase: 'non_json_test' });
195215
});
196216
});
197217
});
198218

199219
describe('callable functions', () => {
200220
it('return an error because they are not supported', () => {
201-
const cloudFunction = (input) => input;
202-
cloudFunction.run = (cloudEvent: CloudEvent) => ({cloudEvent});
221+
const cloudFunction = https.onCall((data) => data);
203222
cloudFunction.__endpoint = {
204223
platform: 'gcfv2',
205224
labels: {},
@@ -210,11 +229,12 @@ describe('v2', () => {
210229
};
211230

212231
try {
213-
const wrappedCF = wrapV2(cloudFunction as CloudFunction<any>);
232+
const wrappedCF = wrapV2(cloudFunction as any);
214233
wrappedCF();
215234
} catch (e) {
216235
expect(e.message).to.equal(
217-
'Wrap function is not available for callableTriggers functions.');
236+
'Wrap function is not available for callableTriggers functions.'
237+
);
218238
}
219239
});
220240
});
@@ -223,17 +243,18 @@ describe('v2', () => {
223243
it('should create CloudEvent with appropriate fields for pubsub.onMessagePublished()', () => {
224244
const data = {
225245
message: {
226-
json: '{"hello_firebase": "world_firebase"}'
246+
json: '{"hello_firebase": "world_firebase"}',
227247
},
228-
subscription: 'subscription'
248+
subscription: 'subscription',
229249
};
230250
const cloudFn = pubsub.onMessagePublished('topic', handler);
231-
const cloudEvent = wrapV2(cloudFn)({data}).cloudEvent;
251+
const cloudEvent = wrapV2(cloudFn)({ data }).cloudEvent;
232252

233253
expect(cloudEvent.type).equal(
234-
'google.cloud.pubsub.topic.v1.messagePublished');
254+
'google.cloud.pubsub.topic.v1.messagePublished'
255+
);
235256
expect(cloudEvent.data.message).to.include({
236-
json: '{"hello_firebase": "world_firebase"}'
257+
json: '{"hello_firebase": "world_firebase"}',
237258
});
238259
});
239260
it('should generate source from original CloudFunction', () => {
@@ -283,15 +304,17 @@ describe('v2', () => {
283304
const cloudEventOverride = {
284305
data: {
285306
contentType: 'application/octet-stream',
286-
}
307+
},
287308
};
288309

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

292313
const mergedCloudEvent = wrapV2(cloudFn)(cloudEventOverride).cloudEvent;
293314
expect(mergedCloudEvent.data?.size).equal(42);
294-
expect(mergedCloudEvent.data?.contentType).equal('application/octet-stream');
315+
expect(mergedCloudEvent.data?.contentType).equal(
316+
'application/octet-stream'
317+
);
295318
});
296319
});
297320
});

src/cloudevent/generate.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,62 @@
1-
import {CloudEvent} from 'firebase-functions/v2';
2-
import {CloudFunction} from 'firebase-functions/v2';
3-
import {LIST_OF_MOCK_CLOUD_EVENT_PARTIALS} from './partials/partials';
4-
import {DeepPartial} from './types';
1+
import { CloudEvent } from 'firebase-functions/v2';
2+
import { CloudFunction } from 'firebase-functions/v2';
3+
import { LIST_OF_MOCK_CLOUD_EVENT_PARTIALS } from './partials/partials';
4+
import { DeepPartial, MockCloudEventPartials } from './types';
55
import merge from 'ts-deepmerge';
6-
import {getEventType} from './partials/helpers';
6+
import { getEventType } from './partials/helpers';
77

88
/**
99
* @return {CloudEvent} Generated Mock CloudEvent
1010
*/
11-
export function generateCombinedCloudEvent<EventType>(
11+
export function generateCombinedCloudEvent<
12+
EventType extends CloudEvent<unknown>
13+
>(
1214
cloudFunction: CloudFunction<EventType>,
13-
cloudEventPartial?: DeepPartial<CloudEvent>): CloudEvent {
15+
cloudEventPartial?: DeepPartial<EventType>
16+
): EventType {
1417
const generatedCloudEvent = generateMockCloudEvent(cloudFunction);
15-
return cloudEventPartial ? merge(generatedCloudEvent, cloudEventPartial) : generatedCloudEvent;
18+
return cloudEventPartial
19+
? (merge(generatedCloudEvent, cloudEventPartial) as EventType)
20+
: generatedCloudEvent;
1621
}
1722

1823
/** @internal */
19-
export function generateMockCloudEvent<EventType>(
20-
cloudFunction: CloudFunction<EventType>): CloudEvent {
24+
export function generateMockCloudEvent<EventType extends CloudEvent<unknown>>(
25+
cloudFunction: CloudFunction<EventType>
26+
): EventType {
2127
return {
2228
...generateBaseCloudEvent(cloudFunction),
23-
...generateMockCloudEventPartial<EventType>(cloudFunction)
29+
...generateMockCloudEventPartial(cloudFunction),
2430
};
2531
}
2632

2733
/** @internal */
28-
function generateBaseCloudEvent<EventType>(cloudFunction: CloudFunction<EventType>): CloudEvent {
34+
function generateBaseCloudEvent<EventType extends CloudEvent<unknown>>(
35+
cloudFunction: CloudFunction<EventType>
36+
): EventType {
37+
// TODO: Consider refactoring so that we don't use this utility function. This
38+
// is not type safe because EventType may require additional fields, which this
39+
// function does not know how to satisfy.
40+
// This could possibly be augmented to take a CloudEvent<unknown> and AdditionalFields<EventType>
41+
// where AdditionalFields uses the keyof operator to make only new fields required.
2942
return {
3043
specversion: '1.0',
3144
id: makeEventId(),
3245
data: {},
3346
source: '', // Required field that will get overridden by Provider-specific MockCloudEventPartials
3447
type: getEventType(cloudFunction),
35-
time: new Date().toISOString()
36-
};
48+
time: new Date().toISOString(),
49+
} as any;
3750
}
3851

39-
function generateMockCloudEventPartial<EventType>(
40-
cloudFunction: CloudFunction<EventType>): DeepPartial<CloudEvent<EventType>> {
52+
function generateMockCloudEventPartial<EventType extends CloudEvent<unknown>>(
53+
cloudFunction: CloudFunction<EventType>
54+
): DeepPartial<EventType> {
4155
for (const mockCloudEventPartial of LIST_OF_MOCK_CLOUD_EVENT_PARTIALS) {
4256
if (mockCloudEventPartial.match(cloudFunction)) {
43-
return mockCloudEventPartial.generatePartial(cloudFunction);
57+
return (mockCloudEventPartial as MockCloudEventPartials<
58+
EventType
59+
>).generatePartial(cloudFunction);
4460
}
4561
}
4662
// No matches were found

0 commit comments

Comments
 (0)