@@ -36,6 +36,11 @@ const FIRESTORE_ADDRESS_ENV: string = 'FIRESTORE_EMULATOR_HOST';
36
36
/** The default address for the local Firestore emulator. */
37
37
const FIRESTORE_ADDRESS_DEFAULT : string = 'localhost:8080' ;
38
38
39
+ /** Environment variable to locate the Emulator Hub */
40
+ const HUB_HOST_ENV : string = 'FIREBASE_EMULATOR_HUB' ;
41
+ /** The default address for the Emulator hub */
42
+ const HUB_HOST_DEFAULT : string = 'localhost:4400' ;
43
+
39
44
/** The actual address for the database emulator */
40
45
let _databaseHost : string | undefined = undefined ;
41
46
@@ -307,7 +312,7 @@ export type LoadDatabaseRulesOptions = {
307
312
databaseName : string ;
308
313
rules : string ;
309
314
} ;
310
- export function loadDatabaseRules (
315
+ export async function loadDatabaseRules (
311
316
options : LoadDatabaseRulesOptions
312
317
) : Promise < void > {
313
318
if ( ! options . databaseName ) {
@@ -318,33 +323,25 @@ export function loadDatabaseRules(
318
323
throw Error ( 'must provide rules to loadDatabaseRules' ) ;
319
324
}
320
325
321
- return new Promise ( ( resolve , reject ) => {
322
- request . put (
323
- {
324
- uri : `http://${ getDatabaseHost ( ) } /.settings/rules.json?ns=${
325
- options . databaseName
326
- } `,
327
- headers : { Authorization : 'Bearer owner' } ,
328
- body : options . rules
329
- } ,
330
- ( err , resp , body ) => {
331
- if ( err ) {
332
- reject ( err ) ;
333
- } else if ( resp . statusCode !== 200 ) {
334
- reject ( JSON . parse ( body ) . error ) ;
335
- } else {
336
- resolve ( ) ;
337
- }
338
- }
339
- ) ;
326
+ const resp = await requestPromise ( request . put , {
327
+ method : 'PUT' ,
328
+ uri : `http://${ getDatabaseHost ( ) } /.settings/rules.json?ns=${
329
+ options . databaseName
330
+ } `,
331
+ headers : { Authorization : 'Bearer owner' } ,
332
+ body : options . rules
340
333
} ) ;
334
+
335
+ if ( resp . statusCode !== 200 ) {
336
+ throw new Error ( JSON . parse ( resp . body . error ) ) ;
337
+ }
341
338
}
342
339
343
340
export type LoadFirestoreRulesOptions = {
344
341
projectId : string ;
345
342
rules : string ;
346
343
} ;
347
- export function loadFirestoreRules (
344
+ export async function loadFirestoreRules (
348
345
options : LoadFirestoreRulesOptions
349
346
) : Promise < void > {
350
347
if ( ! options . projectId ) {
@@ -355,64 +352,98 @@ export function loadFirestoreRules(
355
352
throw new Error ( 'must provide rules to loadFirestoreRules' ) ;
356
353
}
357
354
358
- return new Promise ( ( resolve , reject ) => {
359
- request . put (
360
- {
361
- uri : `http://${ getFirestoreHost ( ) } /emulator/v1/projects/${
362
- options . projectId
363
- } :securityRules`,
364
- body : JSON . stringify ( {
365
- rules : {
366
- files : [ { content : options . rules } ]
367
- }
368
- } )
369
- } ,
370
- ( err , resp , body ) => {
371
- if ( err ) {
372
- reject ( err ) ;
373
- } else if ( resp . statusCode !== 200 ) {
374
- console . log ( 'body' , body ) ;
375
- reject ( JSON . parse ( body ) . error ) ;
376
- } else {
377
- resolve ( ) ;
378
- }
355
+ const resp = await requestPromise ( request . put , {
356
+ method : 'PUT' ,
357
+ uri : `http://${ getFirestoreHost ( ) } /emulator/v1/projects/${
358
+ options . projectId
359
+ } :securityRules`,
360
+ body : JSON . stringify ( {
361
+ rules : {
362
+ files : [ { content : options . rules } ]
379
363
}
380
- ) ;
364
+ } )
381
365
} ) ;
366
+
367
+ if ( resp . statusCode !== 200 ) {
368
+ throw new Error ( JSON . parse ( resp . body . error ) ) ;
369
+ }
382
370
}
383
371
384
372
export type ClearFirestoreDataOptions = {
385
373
projectId : string ;
386
374
} ;
387
- export function clearFirestoreData (
375
+ export async function clearFirestoreData (
388
376
options : ClearFirestoreDataOptions
389
377
) : Promise < void > {
390
378
if ( ! options . projectId ) {
391
379
throw new Error ( 'projectId not specified' ) ;
392
380
}
393
381
394
- return new Promise ( ( resolve , reject ) => {
395
- request . delete (
396
- {
397
- uri : `http://${ getFirestoreHost ( ) } /emulator/v1/projects/${
398
- options . projectId
399
- } /databases/(default)/documents`,
400
- body : JSON . stringify ( {
401
- database : `projects/${ options . projectId } /databases/(default)`
402
- } )
403
- } ,
404
- ( err , resp , body ) => {
405
- if ( err ) {
406
- reject ( err ) ;
407
- } else if ( resp . statusCode !== 200 ) {
408
- console . log ( 'body' , body ) ;
409
- reject ( JSON . parse ( body ) . error ) ;
410
- } else {
411
- resolve ( ) ;
412
- }
413
- }
382
+ const resp = await requestPromise ( request . delete , {
383
+ method : 'DELETE' ,
384
+ uri : `http://${ getFirestoreHost ( ) } /emulator/v1/projects/${
385
+ options . projectId
386
+ } /databases/(default)/documents`,
387
+ body : JSON . stringify ( {
388
+ database : `projects/${ options . projectId } /databases/(default)`
389
+ } )
390
+ } ) ;
391
+
392
+ if ( resp . statusCode !== 200 ) {
393
+ throw new Error ( JSON . parse ( resp . body . error ) ) ;
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Run a setup function with background Cloud Functions triggers disabled. This can be used to
399
+ * import data into the Realtime Database or Cloud Firestore emulator without triggering locally
400
+ * emulated Cloud Functions.
401
+ *
402
+ * This method only works with Firebase CLI version 8.13.0 or higher.
403
+ *
404
+ * @param fn an function which returns a promise.
405
+ */
406
+ export async function withFunctionTriggersDisabled < TResult > (
407
+ fn : ( ) => TResult | Promise < TResult >
408
+ ) : Promise < TResult > {
409
+ let hubHost = process . env [ HUB_HOST_ENV ] ;
410
+ if ( ! hubHost ) {
411
+ console . warn (
412
+ `${ HUB_HOST_ENV } is not set, assuming the Emulator hub is running at ${ HUB_HOST_DEFAULT } `
414
413
) ;
414
+ hubHost = HUB_HOST_DEFAULT ;
415
+ }
416
+
417
+ // Disable background triggers
418
+ const disableRes = await requestPromise ( request . put , {
419
+ method : 'PUT' ,
420
+ uri : `http://${ hubHost } /functions/disableBackgroundTriggers`
415
421
} ) ;
422
+ if ( disableRes . statusCode !== 200 ) {
423
+ throw new Error (
424
+ `HTTP Error ${ disableRes . statusCode } when disabling functions triggers, are you using firebase-tools 8.13.0 or higher?`
425
+ ) ;
426
+ }
427
+
428
+ // Run the user's function
429
+ let result : TResult | undefined = undefined ;
430
+ try {
431
+ result = await fn ( ) ;
432
+ } finally {
433
+ // Re-enable background triggers
434
+ const enableRes = await requestPromise ( request . put , {
435
+ method : 'PUT' ,
436
+ uri : `http://${ hubHost } /functions/enableBackgroundTriggers`
437
+ } ) ;
438
+ if ( enableRes . statusCode !== 200 ) {
439
+ throw new Error (
440
+ `HTTP Error ${ enableRes . statusCode } when enabling functions triggers, are you using firebase-tools 8.13.0 or higher?`
441
+ ) ;
442
+ }
443
+ }
444
+
445
+ // Return the user's function result
446
+ return result ;
416
447
}
417
448
418
449
export function assertFails ( pr : Promise < any > ) : any {
@@ -441,3 +472,22 @@ export function assertFails(pr: Promise<any>): any {
441
472
export function assertSucceeds ( pr : Promise < any > ) : any {
442
473
return pr ;
443
474
}
475
+
476
+ function requestPromise (
477
+ method : typeof request . get ,
478
+ options : request . CoreOptions & request . UriOptions
479
+ ) : Promise < { statusCode : number ; body : any } > {
480
+ return new Promise ( ( resolve , reject ) => {
481
+ const callback : request . RequestCallback = ( err , resp , body ) => {
482
+ if ( err ) {
483
+ reject ( err ) ;
484
+ } else {
485
+ resolve ( { statusCode : resp . statusCode , body } ) ;
486
+ }
487
+ } ;
488
+
489
+ // Unfortunately request's default method is not very test-friendly so having
490
+ // the caler pass in the method here makes this whole thing compatible with sinon
491
+ method ( options , callback ) ;
492
+ } ) ;
493
+ }
0 commit comments