Skip to content

Commit 2e3b84b

Browse files
committed
Try to extract context.params from triggered data
1 parent db5f2d3 commit 2e3b84b

File tree

2 files changed

+224
-26
lines changed

2 files changed

+224
-26
lines changed

spec/main.spec.ts

Lines changed: 133 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,15 @@ import { expect } from 'chai';
2424
import * as functions from 'firebase-functions';
2525
import { set } from 'lodash';
2626

27-
import { mockConfig, makeChange, _makeResourceName, wrap } from '../src/main';
27+
import {
28+
mockConfig,
29+
makeChange,
30+
_makeResourceName,
31+
_extractParams,
32+
wrap,
33+
} from '../src/main';
34+
import { features } from '../src/features';
35+
import { FirebaseFunctionsTest } from '../src/lifecycle';
2836

2937
describe('main', () => {
3038
describe('#wrap', () => {
@@ -74,22 +82,36 @@ describe('main', () => {
7482
expect(context.timestamp).to.equal('2018-03-28T18:58:50.370Z');
7583
});
7684

77-
it('should generate auth and authType for database functions', () => {
78-
const context = wrap(constructCF('google.firebase.database.ref.write'))(
79-
'data'
80-
).context;
81-
expect(context.auth).to.equal(null);
82-
expect(context.authType).to.equal('UNAUTHENTICATED');
83-
});
85+
context('database functions', () => {
86+
let test;
87+
let change;
8488

85-
it('should allow auth and authType to be specified for database functions', () => {
86-
const wrapped = wrap(constructCF('google.firebase.database.ref.write'));
87-
const context = wrapped('data', {
88-
auth: { uid: 'abc' },
89-
authType: 'USER',
90-
}).context;
91-
expect(context.auth).to.deep.equal({ uid: 'abc' });
92-
expect(context.authType).to.equal('USER');
89+
before(() => {
90+
test = new FirebaseFunctionsTest();
91+
test.init();
92+
change = features.database.exampleDataSnapshotChange();
93+
});
94+
95+
after(() => {
96+
test.cleanup();
97+
});
98+
99+
it('should generate auth and authType', () => {
100+
const wrapped = wrap(constructCF('google.firebase.database.ref.write'));
101+
const context = wrapped(change).context;
102+
expect(context.auth).to.equal(null);
103+
expect(context.authType).to.equal('UNAUTHENTICATED');
104+
});
105+
106+
it('should allow auth and authType to be specified', () => {
107+
const wrapped = wrap(constructCF('google.firebase.database.ref.write'));
108+
const context = wrapped(change, {
109+
auth: { uid: 'abc' },
110+
authType: 'USER',
111+
}).context;
112+
expect(context.auth).to.deep.equal({ uid: 'abc' });
113+
expect(context.authType).to.equal('USER');
114+
});
93115
});
94116

95117
it('should throw when passed invalid options', () => {
@@ -112,6 +134,83 @@ describe('main', () => {
112134
expect(context.params).to.deep.equal(params);
113135
expect(context.resource.name).to.equal('ref/a/nested/b');
114136
});
137+
138+
context('Params extraction', () => {
139+
let test;
140+
141+
before(() => {
142+
test = new FirebaseFunctionsTest();
143+
test.init();
144+
});
145+
146+
after(() => {
147+
test.cleanup();
148+
});
149+
150+
it('should extract the appropriate params for database function trigger', () => {
151+
const cf = constructCF('google.firebase.database.ref.create');
152+
cf.__trigger.eventTrigger.resource = 'companies/{company}/users/{user}';
153+
const wrapped = wrap(cf);
154+
const context = wrapped(
155+
features.database.makeDataSnapshot(
156+
{ foo: 'bar' },
157+
'companies/Google/users/Lauren'
158+
)
159+
).context;
160+
expect(context.params).to.deep.equal({
161+
company: 'Google',
162+
user: 'Lauren',
163+
});
164+
expect(context.resource.name).to.equal('companies/Google/users/Lauren');
165+
});
166+
167+
it('should extract the appropriate params for Firestore function trigger', () => {
168+
const cf = constructCF('google.firestore.document.create');
169+
cf.__trigger.eventTrigger.resource =
170+
'databases/(default)/documents/companies/{company}/users/{user}';
171+
const wrapped = wrap(cf);
172+
const context = wrapped(
173+
features.firestore.makeDocumentSnapshot(
174+
{ foo: 'bar' },
175+
'companies/Google/users/Lauren'
176+
)
177+
).context;
178+
expect(context.params).to.deep.equal({
179+
company: 'Google',
180+
user: 'Lauren',
181+
});
182+
expect(context.resource.name).to.equal(
183+
'databases/(default)/documents/companies/Google/users/Lauren'
184+
);
185+
});
186+
187+
it('should prefer provided context.params over the extracted params', () => {
188+
const cf = constructCF('google.firebase.database.ref.create');
189+
cf.__trigger.eventTrigger.resource = 'companies/{company}/users/{user}';
190+
const wrapped = wrap(cf);
191+
const context = wrapped(
192+
features.database.makeDataSnapshot(
193+
{ foo: 'bar' },
194+
'companies/Google/users/Lauren'
195+
),
196+
{
197+
params: {
198+
company: 'Alphabet',
199+
user: 'Lauren',
200+
foo: 'bar',
201+
},
202+
}
203+
).context;
204+
expect(context.params).to.deep.equal({
205+
company: 'Alphabet',
206+
user: 'Lauren',
207+
foo: 'bar',
208+
});
209+
expect(context.resource.name).to.equal(
210+
'companies/Alphabet/users/Lauren'
211+
);
212+
});
213+
});
115214
});
116215

117216
describe('#_makeResourceName', () => {
@@ -124,6 +223,24 @@ describe('main', () => {
124223
});
125224
});
126225

226+
describe('#_extractParams', () => {
227+
it('should not extract any params', () => {
228+
const params = _extractParams('users/foo', 'users/foo');
229+
expect(params).to.deep.equal({});
230+
});
231+
232+
it('should extract params', () => {
233+
const params = _extractParams(
234+
'companies/{company}/users/{user}',
235+
'companies/Google/users/Lauren'
236+
);
237+
expect(params).to.deep.equal({
238+
company: 'Google',
239+
user: 'Lauren',
240+
});
241+
});
242+
});
243+
127244
describe('#makeChange', () => {
128245
it('should make a Change object with the correct before and after', () => {
129246
const change = makeChange('before', 'after');

src/main.ts

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import {
2828
Change,
2929
https,
3030
config,
31+
database,
32+
firestore,
3133
} from 'firebase-functions';
3234

3335
/** Fields of the event context that can be overridden/customized. */
@@ -159,7 +161,7 @@ export function wrap<T>(
159161
['eventId', 'timestamp', 'params', 'auth', 'authType', 'resource'],
160162
options
161163
);
162-
const defaultContext = _makeDefaultContext(cloudFunction, options);
164+
const defaultContext = _makeDefaultContext(cloudFunction, options, data);
163165

164166
if (
165167
has(defaultContext, 'eventType') &&
@@ -218,25 +220,104 @@ function _checkOptionValidity(
218220

219221
function _makeDefaultContext<T>(
220222
cloudFunction: CloudFunction<T>,
221-
options: ContextOptions
223+
options: ContextOptions,
224+
triggerData?: T
222225
): EventContext {
223226
let eventContextOptions = options as EventContextOptions;
227+
const eventResource = cloudFunction.__trigger.eventTrigger?.resource;
228+
const eventType = cloudFunction.__trigger.eventTrigger?.eventType;
229+
230+
const optionsParams = eventContextOptions.params ?? {};
231+
let triggerParams = {};
232+
if (eventResource && eventType && triggerData) {
233+
if (eventType.startsWith('google.firebase.database.ref.')) {
234+
let data: database.DataSnapshot;
235+
if (eventType.endsWith('.write')) {
236+
// Triggered with change
237+
if (!(triggerData instanceof Change)) {
238+
throw new Error('Must be triggered by database change');
239+
}
240+
data = triggerData.before;
241+
} else {
242+
data = triggerData as any;
243+
}
244+
triggerParams = _extractDatabaseParams(eventResource, data);
245+
} else if (eventType.startsWith('google.firestore.document.')) {
246+
let data: firestore.DocumentSnapshot;
247+
if (eventType.endsWith('.write')) {
248+
// Triggered with change
249+
if (!(triggerData instanceof Change)) {
250+
throw new Error('Must be triggered by firestore document change');
251+
}
252+
data = triggerData.before;
253+
} else {
254+
data = triggerData as any;
255+
}
256+
triggerParams = _extractFirestoreDocumentParams(eventResource, data);
257+
}
258+
}
259+
const params = merge({}, triggerParams, optionsParams);
260+
224261
const defaultContext: EventContext = {
225262
eventId: _makeEventId(),
226-
resource: cloudFunction.__trigger.eventTrigger && {
227-
service: cloudFunction.__trigger.eventTrigger.service,
228-
name: _makeResourceName(
229-
cloudFunction.__trigger.eventTrigger.resource,
230-
has(eventContextOptions, 'params') && eventContextOptions.params
231-
),
263+
resource: eventResource && {
264+
service: cloudFunction.__trigger.eventTrigger?.service,
265+
name: _makeResourceName(eventResource, params),
232266
},
233-
eventType: get(cloudFunction, '__trigger.eventTrigger.eventType'),
267+
eventType,
234268
timestamp: new Date().toISOString(),
235-
params: {},
269+
params,
236270
};
237271
return defaultContext;
238272
}
239273

274+
function _extractDatabaseParams(
275+
triggerResource: string,
276+
data: database.DataSnapshot
277+
): EventContext['params'] {
278+
const path = data.ref.toString().replace(data.ref.root.toString(), '');
279+
return _extractParams(triggerResource, path);
280+
}
281+
282+
function _extractFirestoreDocumentParams(
283+
triggerResource: string,
284+
data: firestore.DocumentSnapshot
285+
): EventContext['params'] {
286+
// Resource format: databases/(default)/documents/<path>
287+
return _extractParams(
288+
triggerResource.replace(/^databases\/[^\/]+\/documents\//, ''),
289+
data.ref.path
290+
);
291+
}
292+
293+
/**
294+
* Extracts the `{wildcard}` values from `dataPath`.
295+
* E.g. A wildcard path of `users/{userId}` with `users/FOO` would result in `{ userId: 'FOO' }`.
296+
* @internal
297+
*/
298+
export function _extractParams(
299+
wildcardTriggerPath: string,
300+
dataPath: string
301+
): EventContext['params'] {
302+
// Trim start and end / and split into path components
303+
const wildcardPaths = wildcardTriggerPath
304+
.replace(/^\/?(.*?)\/?$/, '$1')
305+
.split('/');
306+
const dataPaths = dataPath.replace(/^\/?(.*?)\/?$/, '$1').split('/');
307+
const params = {};
308+
if (wildcardPaths.length === dataPaths.length) {
309+
for (let idx = 0; idx < wildcardPaths.length; idx++) {
310+
const wildcardPath = wildcardPaths[idx];
311+
const name = wildcardPath.replace(/^{([^/{}]*)}$/, '$1');
312+
if (name !== wildcardPath) {
313+
// Wildcard parameter
314+
params[name] = dataPaths[idx];
315+
}
316+
}
317+
}
318+
return params;
319+
}
320+
240321
/** Make a Change object to be used as test data for Firestore and real time database onWrite and onUpdate functions. */
241322
export function makeChange<T>(before: T, after: T): Change<T> {
242323
return Change.fromObjects(before, after);

0 commit comments

Comments
 (0)