From 986bc14d79f0a004859e00a6f5b8b690f1dfbd8b Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Tue, 20 Mar 2018 16:36:21 +0000 Subject: [PATCH 001/705] [firebase-release] Removed change log and reset repo after 0.9.0 release --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 30a28a74e..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +0,0 @@ -feature - Adds HTTPS Callable Functions, a kind of HTTPS trigger that can be called from a Firebase client SDK. From b6c395f713ba93e31361333779e1c7c60be08884 Mon Sep 17 00:00:00 2001 From: Jaewon Seo Date: Wed, 21 Mar 2018 14:29:45 -0700 Subject: [PATCH 002/705] Correct the type signature of DeltaDocumentSnapshot (#162) --- src/providers/firestore.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/providers/firestore.ts b/src/providers/firestore.ts index 767c3ff2b..e534c0b4a 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -73,12 +73,12 @@ export class NamespaceBuilder { export interface DeltaDocumentSnapshot { exists: Boolean; - ref: any; + ref: firebase.firestore.DocumentReference; id: string; - createTime: string; - updateTime: string; - readTime: string; - previous: any; + createTime?: string; + updateTime?: string; + readTime?: string; + previous: DeltaDocumentSnapshot; data: () => any; get: (key: string) => any; }; From 3c6a7e3006a354595484cfcaa043ceecb50d5019 Mon Sep 17 00:00:00 2001 From: Bryan Klimt Date: Wed, 21 Mar 2018 16:16:21 -0700 Subject: [PATCH 003/705] Change https callables to use apps.admin.auth instead of firebase.auth. (#206) * Change https callables to use apps.admin.auth instead of firebase.auth. * explicitly mock the credential in the admin app * Use the projectId from admin app instead of a constant --- spec/providers/https.spec.ts | 26 ++++++++++++++++++++------ src/providers/https.ts | 3 ++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/spec/providers/https.spec.ts b/spec/providers/https.spec.ts index 03279ad57..b9c523007 100644 --- a/spec/providers/https.spec.ts +++ b/spec/providers/https.spec.ts @@ -27,9 +27,8 @@ import * as jwt from 'jsonwebtoken'; import * as mocks from '../fixtures/credential/key.json'; import * as nock from 'nock'; import * as _ from 'lodash'; -import { config } from '../../src/index'; +import { apps } from '../../src/apps'; import { expect } from 'chai'; -import { fakeConfig } from '../support/helpers'; describe('CloudHttpsBuilder', () => { describe('#onRequest', () => { @@ -202,13 +201,28 @@ export function generateIdToken(projectId: string): string { } describe('callable.FunctionBuilder', () => { + let oldCredential: firebase.credential.Credential = undefined; + before(() => { - config.singleton = fakeConfig(); - firebase.initializeApp(config.singleton.firebase); + let credential = { + getAccessToken: () => { + return Promise.resolve({ + expires_in: 1000, + access_token: 'fake', + }); + }, + getCertificate: () => { + return { + projectId: 'aProjectId', + }; + }, + }; + oldCredential = apps().admin.options.credential; + apps().admin.options.credential = credential; }); after(() => { - delete config.singleton; + apps().admin.options.credential = oldCredential; }); describe('#onCall', () => { @@ -361,7 +375,7 @@ describe('callable.FunctionBuilder', () => { it('should handle auth', async () => { const mock = mockFetchPublicKeys(); - const projectId = config.singleton.firebase['projectId']; + const projectId = apps().admin.options.projectId; const idToken = generateIdToken(projectId); await runTest({ httpRequest: request(null, 'application/json', { diff --git a/src/providers/https.ts b/src/providers/https.ts index 340ecb67b..2930910a5 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -23,6 +23,7 @@ import { HttpsFunction } from '../cloud-functions'; import * as express from 'express'; import * as firebase from 'firebase-admin'; +import { apps } from '../apps'; import * as _ from 'lodash'; import * as cors from 'cors'; @@ -382,7 +383,7 @@ export function onCall( } const idToken = match[1]; try { - const authToken = await firebase.auth().verifyIdToken(idToken); + const authToken = await apps().admin.auth().verifyIdToken(idToken); context.auth = { uid: authToken.uid, token: authToken, From 46c6ffe3a12b65f1ceab09bf29d44d7399fd2f27 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Wed, 21 Mar 2018 17:16:14 -0700 Subject: [PATCH 004/705] Changelog for v0.9.1 (#207) --- changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index e69de29bb..e5579e23a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1,2 @@ +fixed - Fixed bug where HTTPS callable function will reject all requests with an auth token if the function has not called firebase.initializeApp. +fixed - Corrected type signature for firestore.DeltaDocumentSnapshot. From db8a48ce14caef06282730a19d4733eea91112de Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Thu, 22 Mar 2018 00:17:42 +0000 Subject: [PATCH 005/705] [firebase-release] Updated SDK for Cloud Functions to 0.9.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 205386a80..0d8e2a078 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "0.9.0", + "version": "0.9.1", "description": "Firebase SDK for Cloud Functions", "main": "lib/index.js", "scripts": { From 8f2d0ab15ca2ad2a423b9fa31819697f25cac1cf Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Thu, 22 Mar 2018 00:17:54 +0000 Subject: [PATCH 006/705] [firebase-release] Removed change log and reset repo after 0.9.1 release --- changelog.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index e5579e23a..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,2 +0,0 @@ -fixed - Fixed bug where HTTPS callable function will reject all requests with an auth token if the function has not called firebase.initializeApp. -fixed - Corrected type signature for firestore.DeltaDocumentSnapshot. From 914843b02e1259416f794acfb6755281857b0539 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Mon, 2 Apr 2018 15:22:50 -0700 Subject: [PATCH 007/705] Add changes for v1.0.0 (#192) --- .npmignore | 1 + .travis.yml | 2 +- README.md | 19 +- integration_test/functions/package.json | 4 +- integration_test/functions/src/auth-tests.ts | 56 +- .../functions/src/database-tests.ts | 48 +- .../functions/src/firestore-tests.ts | 32 +- integration_test/functions/src/https-tests.ts | 7 +- integration_test/functions/src/index.ts | 7 +- .../functions/src/pubsub-tests.ts | 37 +- integration_test/functions/src/testing.ts | 17 +- integration_test/run_tests.sh | 1 + package.json | 17 +- spec/apps.spec.ts | 100 +-- spec/cloud-functions.spec.ts | 258 +++++-- spec/config.spec.ts | 61 +- spec/providers/analytics.spec.input.ts | 116 ++- spec/providers/analytics.spec.ts | 340 ++++----- spec/providers/auth.spec.ts | 221 +++--- spec/providers/crashlytics.spec.ts | 92 ++- spec/providers/database.spec.ts | 663 ++++++++---------- spec/providers/firestore.spec.ts | 134 ++-- spec/providers/https.spec.ts | 16 +- spec/providers/pubsub.spec.ts | 146 ++-- spec/providers/storage.spec.ts | 281 ++++++-- spec/support/helpers.ts | 51 -- spec/utils.spec.ts | 6 +- src/apps.ts | 68 +- src/cloud-functions.ts | 269 +++++-- src/config.ts | 72 +- src/index.ts | 21 + src/providers/analytics.ts | 30 +- src/providers/auth.ts | 121 ++-- src/providers/crashlytics.ts | 59 +- src/providers/database.ts | 213 +++--- src/providers/firestore.ts | 158 +++-- src/providers/pubsub.ts | 25 +- src/providers/storage.ts | 131 +++- tsconfig.json | 2 +- tsconfig.release.json | 2 +- upgrade-warning | 15 + 41 files changed, 2295 insertions(+), 1624 deletions(-) delete mode 100644 spec/support/helpers.ts create mode 100644 upgrade-warning diff --git a/.npmignore b/.npmignore index a39630c8e..4472a5eff 100644 --- a/.npmignore +++ b/.npmignore @@ -5,6 +5,7 @@ coverage tsconfig.* tslint.* .travis.yml +.github # Don't include the raw typescript src diff --git a/.travis.yml b/.travis.yml index 0ada5f174..fe5ba40c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: node_js node_js: -- '6.11.1' +- '6.11.5' - stable sudo: false diff --git a/README.md b/README.md index 331eac6bc..df3ad35af 100644 --- a/README.md +++ b/README.md @@ -4,27 +4,26 @@ The `firebase-functions` package provides an SDK for defining Cloud Functions fo Cloud Functions is a hosted, private, and scalable Node.js environment where you can run JavaScript code. The Firebase SDK for Cloud Functions integrates the Firebase platform by letting you write code that responds to events and invokes functionality exposed by other Firebase features. -_This is a Beta release of Google Cloud Functions. This API might be changed in backward-incompatible ways and is not subject to any SLA or deprecation policy._ - - ## Learn more Learn more about the Firebase SDK for Cloud Functions in the [Firebase documentation](https://firebase.google.com/docs/functions/) or [check out our samples](https://github.com/firebase/functions-samples). +## Migrating to v1 + +To migrate from a beta version of firebase-functions to v1, please refer to the [migration guide](https://firebase.google.com/docs/functions/beta-v1-diff). + ## Usage ```js // functions/index.js -var functions = require('firebase-functions'); -var notifyUsers = require('./notify-users'); +const functions = require('firebase-functions'); +const notifyUsers = require('./notify-users'); exports.newPost = functions.database .ref('/posts/{postId}') - .onWrite(function(event) { - // only execute function on creation - if (!event.data.previous.exists()) { - notifyUsers(event.data.val()); - } + .onCreate((snapshot, context) => { + console.log('Received new post with ID:', context.params.postId); + return notifyUsers(snapshot.val()); }); ``` diff --git a/integration_test/functions/package.json b/integration_test/functions/package.json index 5475252d9..5e021fc7c 100644 --- a/integration_test/functions/package.json +++ b/integration_test/functions/package.json @@ -7,8 +7,8 @@ "dependencies": { "@google-cloud/pubsub": "^0.6.0", "@types/lodash": "^4.14.41", - "firebase": ">3.6.9", - "firebase-admin": ">=5.4.2", + "firebase": "^4.9.1", + "firebase-admin": "~5.10.0", "firebase-functions": "./firebase-functions.tgz", "lodash": "^4.17.2" }, diff --git a/integration_test/functions/src/auth-tests.ts b/integration_test/functions/src/auth-tests.ts index 20f1bb948..367f6fe87 100644 --- a/integration_test/functions/src/auth-tests.ts +++ b/integration_test/functions/src/auth-tests.ts @@ -1,52 +1,52 @@ import * as functions from 'firebase-functions'; import { TestSuite, expectEq } from './testing'; +import * as admin from 'firebase-admin'; +import UserMetadata = admin.auth.UserRecord; -export const createUserTests: any = functions.auth.user().onCreate(receivedEvent => { - let user = receivedEvent.data; - let testId: string = user.displayName; +export const createUserTests: any = functions.auth.user().onCreate((u, c) => { + let testId: string = u.displayName; console.log(`testId is ${testId}`); - return new TestSuite('auth user onCreate') - .it('should have a project as resource', event => expectEq( - event.resource, `projects/${process.env.GCLOUD_PROJECT}`)) + return new TestSuite('auth user onCreate') + .it('should have a project as resource', (user, context) => expectEq( + context.resource, `projects/${process.env.GCLOUD_PROJECT}`)) - .it('should not have a path', event => expectEq(event.path, undefined)) + .it('should not have a path', (user, context) => expectEq((context as any).path, undefined)) - .it('should have the correct eventType', event => expectEq( - event.eventType, 'providers/firebase.auth/eventTypes/user.create')) + .it('should have the correct eventType', (user, context) => expectEq( + context.eventType, 'providers/firebase.auth/eventTypes/user.create')) - .it('should have an eventId', event => event.eventId) + .it('should have an eventId', (user, context)=> context.eventId) - .it('should have a timestamp', event => event.timestamp) + .it('should have a timestamp', (user, context) => context.timestamp) - .it('should not have auth', event => expectEq(event.auth, undefined)) + .it('should not have auth', (user, context) => expectEq((context as any).auth, undefined)) - .it('should not have action', event => expectEq(event.action, undefined)) + .it('should not have action', (user, context) => expectEq((context as any).action, undefined)) - .run(testId, receivedEvent); + .run(testId, u, c); }); -export const deleteUserTests: any = functions.auth.user().onDelete(receivedEvent => { - let user = receivedEvent.data; - let testId: string = user.displayName; +export const deleteUserTests: any = functions.auth.user().onDelete((u, c) => { + let testId: string = u.displayName; console.log(`testId is ${testId}`); - return new TestSuite('auth user onDelete') - .it('should have a project as resource', event => expectEq( - event.resource, `projects/${process.env.GCLOUD_PROJECT}`)) + return new TestSuite('auth user onDelete') + .it('should have a project as resource', (user, context) => expectEq( + context.resource, `projects/${process.env.GCLOUD_PROJECT}`)) - .it('should not have a path', event => expectEq(event.path, undefined)) + .it('should not have a path', (user, context) => expectEq((context as any).path, undefined)) - .it('should have the correct eventType', event => expectEq( - event.eventType, 'providers/firebase.auth/eventTypes/user.delete')) + .it('should have the correct eventType', (user, context) => expectEq( + context.eventType, 'providers/firebase.auth/eventTypes/user.delete')) - .it('should have an eventId', event => event.eventId) + .it('should have an eventId', (user, context) => context.eventId) - .it('should have a timestamp', event => event.timestamp) + .it('should have a timestamp', (user, context) => context.timestamp) - .it('should not have auth', event => expectEq(event.auth, undefined)) + .it('should not have auth', (user, context) => expectEq((context as any).auth, undefined)) - .it('should not have action', event => expectEq(event.action, undefined)) + .it('should not have action', (user, context) => expectEq((context as any).action, undefined)) - .run(testId, receivedEvent); + .run(testId, u, c); }); diff --git a/integration_test/functions/src/database-tests.ts b/integration_test/functions/src/database-tests.ts index ea0400368..e7ca21a8f 100644 --- a/integration_test/functions/src/database-tests.ts +++ b/integration_test/functions/src/database-tests.ts @@ -1,28 +1,28 @@ import * as functions from 'firebase-functions'; -import { TestSuite, expectReject, expectEq, expectMatches } from './testing'; +import { TestSuite, expectEq, expectMatches } from './testing'; +import * as admin from 'firebase-admin'; +import DataSnapshot = admin.database.DataSnapshot; +import { Change } from '../../../src/cloud-functions'; const testIdFieldName = 'testId'; -export const databaseTests: any = functions.database.ref('dbTests/{testId}/start').onWrite(receivedEvent => { - if (receivedEvent.data.val() === null) { +export const databaseTests: any = functions.database.ref('dbTests/{testId}/start').onWrite((ch, ctx) => { + if (ch.after.val() === null) { console.log( - 'Event for ' + receivedEvent.params[testIdFieldName] + 'Event for ' + ctx.params[testIdFieldName] + ' is null; presuming data cleanup, so skipping.'); return; } - return new TestSuite('database ref onWrite') + return new TestSuite>('database ref onWrite') - .it('should not have event.app', event => !event.app) + .it('should not have event.app', (change, context) => !(context as any).app) - .it('should not give user refs access to admin data', expectReject(event => - event.data.ref.parent.child('adminOnly').update({ disallowed: 0 }))) + .it('should give refs access to admin data', (change) => + change.after.ref.parent.child('adminOnly').update({ allowed: 1 }).then(() => true)) - .it('should give admin refs access to admin data', event => - event.data.adminRef.parent.child('adminOnly').update({ allowed: 1 }).then(() => true)) - - .it('should have a correct ref url', event => { - const url = event.data.ref.toString(); + .it('should have a correct ref url', (change) => { + const url = change.after.ref.toString(); return Promise.resolve().then(() => { return expectMatches(url, new RegExp(`^https://${process.env.GCLOUD_PROJECT}.firebaseio.com/dbTests`)); }).then(() => { @@ -30,22 +30,20 @@ export const databaseTests: any = functions.database.ref('dbTests/{testId}/start }); }) - .it('should have refs resources', event => expectEq( - event.resource, - `projects/_/instances/${process.env.GCLOUD_PROJECT}/refs/dbTests/${event.params.testId}/start`)) - - .it('should not include path', event => expectEq(event.path, undefined)) + .it('should have refs resources', (change, context) => expectEq( + context.resource.name, + `projects/_/instances/${process.env.GCLOUD_PROJECT}/refs/dbTests/${context.params.testId}/start`)) - .it('should have the right eventType', event => expectEq( - event.eventType, 'providers/google.firebase.database/eventTypes/ref.write')) + .it('should not include path', (change, context) => expectEq((context as any).path, undefined)) - .it('should have eventId', event => event.eventId) + .it('should have the right eventType', (change, context) => expectEq( + context.eventType, 'google.firebase.database.ref.write')) - .it('should have timestamp', event => event.timestamp) + .it('should have eventId', (change, context) => context.eventId) - .it('should not be admin-authenticated', event => expectEq(event.auth.admin, false)) + .it('should have timestamp', (change, context) => context.timestamp) - .it('should not have action', event => expectEq(event.action, undefined)) + .it('should not have action', (change, context) => expectEq((context as any).action, undefined)) - .run(receivedEvent.params[testIdFieldName], receivedEvent); + .run(ctx.params[testIdFieldName], ch, ctx); }); diff --git a/integration_test/functions/src/firestore-tests.ts b/integration_test/functions/src/firestore-tests.ts index bd1bb4897..4e7cfd912 100644 --- a/integration_test/functions/src/firestore-tests.ts +++ b/integration_test/functions/src/firestore-tests.ts @@ -1,31 +1,31 @@ import * as functions from 'firebase-functions'; import { TestSuite, expectEq, expectDeepEq } from './testing'; +import * as admin from 'firebase-admin'; +import DocumentSnapshot = admin.firestore.DocumentSnapshot; const testIdFieldName = 'documentId'; -export const firestoreTests: any = functions.firestore.document('tests/{documentId}').onCreate(receivedEvent => { - return new TestSuite('firestore document onWrite') +export const firestoreTests: any = functions.firestore.document('tests/{documentId}').onCreate((s, c) => { + return new TestSuite('firestore document onWrite') - .it('should not have event.app', event => !event.app) + .it('should not have event.app', (snap, context) => !(context as any).app) - .it('should give refs write access', event => - event.data.ref.set({ allowed: 1 }, {merge: true}).then(() => true)) + .it('should give refs write access', (snap) => + snap.ref.set({ allowed: 1 }, {merge: true}).then(() => true)) - .it('should have well-formatted resource', event => expectEq( - event.resource, - `projects/${process.env.GCLOUD_PROJECT}/databases/(default)/documents/tests/${event.params.documentId}`) + .it('should have well-formatted resource', (snap, context) => expectEq( + context.resource.name, + `projects/${process.env.GCLOUD_PROJECT}/databases/(default)/documents/tests/${context.params.documentId}`) ) - .it('should have the right eventType', event => expectEq( - event.eventType, 'providers/cloud.firestore/eventTypes/document.create')) + .it('should have the right eventType', (snap, context) => expectEq( + context.eventType, 'providers/cloud.firestore/eventTypes/document.create')) - .it('should have eventId', event => event.eventId) + .it('should have eventId', (snap, context) => context.eventId) - .it('should have timestamp', event => event.timestamp) + .it('should have timestamp', (snap, context) => context.timestamp) - .it('should have the correct data', event => expectDeepEq(event.data.data(), {test: event.params.documentId})) + .it('should have the correct data', (snap, context) => expectDeepEq(snap.data(), {test: context.params.documentId})) - .it('previous.exists should be false', event => expectEq(event.data.previous.exists, false)) - - .run(receivedEvent.params[testIdFieldName], receivedEvent); + .run(c.params[testIdFieldName], s, c); }); diff --git a/integration_test/functions/src/https-tests.ts b/integration_test/functions/src/https-tests.ts index cc06de82c..4595214e8 100644 --- a/integration_test/functions/src/https-tests.ts +++ b/integration_test/functions/src/https-tests.ts @@ -1,8 +1,9 @@ import * as functions from 'firebase-functions'; +import * as _ from 'lodash'; import { TestSuite, expectEq } from './testing'; -export const callableTests: any = functions.https.onCall((data, context) => { +export const callableTests: any = functions.https.onCall(d => { return new TestSuite('https onCall') - .it('should have the correct data', event => expectEq(event.data.foo, 'bar')) - .run(data.testId, { data: data, context: context }); + .it('should have the correct data', data => expectEq(_.get(data, 'foo'), 'bar')) + .run(d.testId, d); }); diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 905401c3b..9979e372b 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -2,7 +2,6 @@ import * as functions from 'firebase-functions'; import * as firebase from 'firebase'; import * as https from 'https'; import * as admin from 'firebase-admin'; -import * as _ from 'lodash'; import { Request, Response } from 'express'; export * from './pubsub-tests'; @@ -12,8 +11,10 @@ export * from './firestore-tests'; export * from './https-tests'; const numTests = Object.keys(exports).length; // Assumption: every exported function is its own test. -firebase.initializeApp(_.omit(functions.config().firebase, 'credential')); // Explicitly decline admin privileges. -admin.initializeApp(functions.config().firebase); +// Client SDK doesn't support auto initialization: +firebase.initializeApp(JSON.parse(process.env.FIREBASE_CONFIG)); +console.log('initializing admin'); +admin.initializeApp(); // TODO(klimt): Get rid of this once the JS client SDK supports callable triggers. function callHttpsTrigger(name: string, data: any) { diff --git a/integration_test/functions/src/pubsub-tests.ts b/integration_test/functions/src/pubsub-tests.ts index 9eec7938b..044dd2382 100644 --- a/integration_test/functions/src/pubsub-tests.ts +++ b/integration_test/functions/src/pubsub-tests.ts @@ -1,41 +1,42 @@ import * as functions from 'firebase-functions'; import { TestSuite, expectEq, evaluate } from './testing'; +import PubsubMessage = functions.pubsub.Message; // TODO(inlined) use multiple queues to run inline. // Expected message data: {"hello": "world"} -export const pubsubTests: any = functions.pubsub.topic('pubsubTests').onPublish(receivedEvent => { +export const pubsubTests: any = functions.pubsub.topic('pubsubTests').onPublish((m, c) => { let testId: string; try { - testId = receivedEvent.data.json.testId; + testId = m.json.testId; } catch (e) { /* Ignored. Covered in another test case that `event.data.json` works. */ } - return new TestSuite('pubsub onPublish') - .it('should have a topic as resource', event => expectEq( - event.resource, `projects/${process.env.GCLOUD_PROJECT}/topics/pubsubTests`)) + return new TestSuite('pubsub onPublish') + .it('should have a topic as resource', (message, context) => expectEq( + context.resource.name, `projects/${process.env.GCLOUD_PROJECT}/topics/pubsubTests`)) - .it('should not have a path', event => expectEq(event.path, undefined)) + .it('should not have a path', (message, context) => expectEq((context as any).path, undefined)) - .it('should have the correct eventType', event => expectEq( - event.eventType, 'providers/cloud.pubsub/eventTypes/topic.publish')) + .it('should have the correct eventType', (message, context) => expectEq( + context.eventType, 'providers/cloud.pubsub/eventTypes/topic.publish')) - .it('should have an eventId', event => event.eventId) + .it('should have an eventId', (message, context) => context.eventId) - .it('should have a timestamp', event => event.timestamp) + .it('should have a timestamp', (message, context) => context.timestamp) - .it('should not have auth', event => expectEq(event.auth, undefined)) + .it('should not have auth', (message, context) => expectEq((context as any).auth, undefined)) - .it('should not have action', event => expectEq(event.action, undefined)) + .it('should not have action', (message, context) => expectEq((context as any).action, undefined)) - .it('should have pubsub data', event => { - const decoded = (new Buffer(event.data.data, 'base64')).toString(); + .it('should have pubsub data', (message) => { + const decoded = (new Buffer(message.data, 'base64')).toString(); const parsed = JSON.parse(decoded); - return evaluate(parsed.hasOwnProperty('testId'), 'Raw data was: ' + event.data.data); + return evaluate(parsed.hasOwnProperty('testId'), 'Raw data was: ' + message.data); }) - .it('should decode JSON payloads with the json helper', event => - evaluate(event.data.json.hasOwnProperty('testId'), event.data.json)) + .it('should decode JSON payloads with the json helper', (message) => + evaluate(message.json.hasOwnProperty('testId'), message.json)) - .run(testId, receivedEvent); + .run(testId, m, c); }); diff --git a/integration_test/functions/src/testing.ts b/integration_test/functions/src/testing.ts index 59bd203fb..765dfacf0 100644 --- a/integration_test/functions/src/testing.ts +++ b/integration_test/functions/src/testing.ts @@ -1,29 +1,30 @@ import * as firebase from 'firebase-admin'; import * as _ from 'lodash'; +import { EventContext } from 'firebase-functions'; -export type TestCase = (event) => any -export type TestCaseMap = { [key: string]: TestCase }; +export type TestCase = (data: T, context?: EventContext) => any +export type TestCaseMap = { [key: string]: TestCase }; -export class TestSuite { +export class TestSuite { private name: string; - private tests: TestCaseMap; + private tests: TestCaseMap; - constructor(name: string, tests: TestCaseMap = {}) { + constructor(name: string, tests: TestCaseMap = {}) { this.name = name; this.tests = tests; } - it(name: string, testCase: TestCase): TestSuite { + it(name: string, testCase: TestCase): TestSuite { this.tests[name] = testCase; return this; } - run(testId: string, event): Promise { + run(testId: string, data: T, context?: EventContext): Promise { let running: Array> = []; for (let testName in this.tests) { if (!this.tests.hasOwnProperty(testName)) { continue; } const run = Promise.resolve() - .then(() => this.tests[testName](event)) + .then(() => this.tests[testName](data, context)) .then( (result) => { console.log(`${result ? 'Passed' : 'Failed with successful op'}: ${testName}`); diff --git a/integration_test/run_tests.sh b/integration_test/run_tests.sh index 11329d81c..d37bb49b8 100755 --- a/integration_test/run_tests.sh +++ b/integration_test/run_tests.sh @@ -73,6 +73,7 @@ function cleanup { delete_all_functions rm $DIR/functions/firebase-functions.tgz rm -f $DIR/functions/firebase-debug.log + rm -rf $DIR/functions/node_modules/firebase-functions } build_sdk diff --git a/package.json b/package.json index 0d8e2a078..ff884b2a7 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,13 @@ "main": "lib/index.js", "scripts": { "build": "node_modules/.bin/tsc -p tsconfig.release.json", - "build:pack": "npm prune --production && rm -rf lib && npm install && node_modules/.bin/tsc -p tsconfig.release.json && npm pack && npm install", + "build:pack": "rm -rf lib && npm install && node_modules/.bin/tsc -p tsconfig.release.json && npm pack", "build:release": "npm install --production && npm install typescript firebase-admin && node_modules/.bin/tsc -p tsconfig.release.json", "lint": "node_modules/.bin/tslint src/{**/*,*}.ts spec/{**/*,*}.ts integration_test/functions/src/{**/*,*}.ts", "pretest": "node_modules/.bin/tsc && cp -r spec/fixtures .tmp/spec", "test": "mocha .tmp/spec/index.spec.js", - "posttest": "npm run lint && rm -rf .tmp" + "posttest": "npm run lint && rm -rf .tmp", + "postinstall": "node ./upgrade-warning" }, "repository": { "type": "git", @@ -48,22 +49,20 @@ "typescript": "^2.0.3" }, "peerDependencies": { - "firebase-admin": "~5.10.0" + "firebase-admin": "~5.11.0" }, "dependencies": { "@types/cors": "^2.8.1", - "@types/express": "^4.0.33", + "@types/express": "^4.11.1", "@types/jsonwebtoken": "^7.1.32", "@types/lodash": "^4.14.34", - "@types/sha1": "^1.1.0", "cors": "^2.8.4", - "express": "^4.0.33", + "express": "^4.16.2", "jsonwebtoken": "^7.1.9", - "lodash": "^4.6.1", - "sha1": "^1.1.1" + "lodash": "^4.6.1" }, "engines": { - "node": ">=4.0.0" + "node": ">=6.0.0" }, "typings": "lib/index.d.ts" } diff --git a/spec/apps.spec.ts b/spec/apps.spec.ts index 0626b5b67..6b3d3e822 100644 --- a/spec/apps.spec.ts +++ b/spec/apps.spec.ts @@ -21,7 +21,6 @@ // SOFTWARE. import { expect } from 'chai'; -import { fakeConfig } from './support/helpers'; import { apps as appsNamespace } from '../src/apps'; import * as firebase from 'firebase-admin'; import * as _ from 'lodash'; @@ -31,7 +30,7 @@ describe('apps', () => { let apps: appsNamespace.Apps; let claims; beforeEach(() => { - apps = new appsNamespace.Apps(fakeConfig()); + apps = new appsNamespace.Apps(); // mock claims intentionally contains dots, square brackets, and nested paths claims = {'token': {'firebase': {'identities':{'google.com':['111']}}}}; }); @@ -42,28 +41,6 @@ describe('apps', () => { }); }); - it('should load the admin app for admin impersonation', function () { - expect(apps.forMode({ admin: true })).to.equal(apps.admin); - }); - - it('should load the anonymous app for anonymous impersonation', function () { - expect(apps.forMode({ admin: false })).to.equal(apps.noauth); - }); - - it('should create a user app for user impersonation', function () { - const auth = { admin: false, variable: claims }; - const key = apps._appName(auth); - expect(function () { - return firebase.app(key); - }).to.throw(Error); - - const userApp = apps.forMode(auth); - expect(firebase.app(key)).to.equal(userApp); - - const userAppAgain = apps.forMode(auth); - expect(userApp).to.equal(userAppAgain); - }); - describe('retain/release', () => { let clock; @@ -75,75 +52,39 @@ describe('apps', () => { clock.restore(); }); - it('should retain/release ref counters appropriately without auth', function() { - apps.retain({}); - expect(apps['_refCounter']).to.deep.equal({ - __admin__: 1, - __noauth__: 1, - }); - apps.release({}); - clock.tick(appsNamespace.garbageCollectionInterval); - return Promise.resolve().then(() => { - expect(apps['_refCounter']).to.deep.equal({ - __admin__: 0, - __noauth__: 0, - }); - }); - }); - - it('should retain/release ref counters appropriately with admin auth', function() { - apps.retain({auth: {admin: true}}); - expect(apps['_refCounter']).to.deep.equal({ - __admin__: 2, - }); - apps.release({auth: {admin: true}}); - clock.tick(appsNamespace.garbageCollectionInterval); - return Promise.resolve().then(() => { - expect(apps['_refCounter']).to.deep.equal({ - __admin__: 0, - }); - }); - }); - - it('should retain/release ref counters appropriately with user auth', function() { - const payload = {auth: {admin: false, variable: claims}}; - const userAppName = apps._appName(payload.auth); - apps.retain(payload); + it('should retain/release ref counters appropriately', function() { + apps.retain(); expect(apps['_refCounter']).to.deep.equal({ __admin__: 1, - [userAppName]: 1, }); - apps.release(payload); + apps.release(); clock.tick(appsNamespace.garbageCollectionInterval); return Promise.resolve().then(() => { expect(apps['_refCounter']).to.deep.equal({ __admin__: 0, - [userAppName]: 0, }); }); }); it('should only decrement counter after garbageCollectionInterval is up', function() { - apps.retain({}); - apps.release({}); + apps.retain(); + apps.release(); clock.tick(appsNamespace.garbageCollectionInterval / 2); expect(apps['_refCounter']).to.deep.equal({ __admin__: 1, - __noauth__: 1, }); clock.tick(appsNamespace.garbageCollectionInterval / 2); return Promise.resolve().then(() => { expect(apps['_refCounter']).to.deep.equal({ __admin__: 0, - __noauth__: 0, }); }); }); it('should call _destroyApp if app no longer used', function() { let spy = sinon.spy(apps, '_destroyApp'); - apps.retain({}); - apps.release({}); + apps.retain(); + apps.release(); clock.tick(appsNamespace.garbageCollectionInterval); return Promise.resolve().then(() => { expect(spy.called).to.be.true; @@ -152,10 +93,10 @@ describe('apps', () => { it('should not call _destroyApp if app used again while waiting for release', function() { let spy = sinon.spy(apps, '_destroyApp'); - apps.retain({}); - apps.release({}); + apps.retain(); + apps.release(); clock.tick(appsNamespace.garbageCollectionInterval / 2); - apps.retain({}); + apps.retain(); clock.tick(appsNamespace.garbageCollectionInterval / 2); return Promise.resolve().then(() => { expect(spy.called).to.be.false; @@ -163,42 +104,37 @@ describe('apps', () => { }); it('should increment ref counter for each subsequent retain', function() { - apps.retain({}); + apps.retain(); expect(apps['_refCounter']).to.deep.equal({ __admin__: 1, - __noauth__: 1, }); - apps.retain({}); + apps.retain(); expect(apps['_refCounter']).to.deep.equal({ __admin__: 2, - __noauth__: 2, }); - apps.retain({}); + apps.retain(); expect(apps['_refCounter']).to.deep.equal({ __admin__: 3, - __noauth__: 3, }); }); it('should work with staggering sets of retain/release', function() { - apps.retain({}); - apps.release({}); + apps.retain(); + apps.release(); clock.tick(appsNamespace.garbageCollectionInterval / 2); - apps.retain({}); - apps.release({}); + apps.retain(); + apps.release(); clock.tick(appsNamespace.garbageCollectionInterval / 2); return Promise.resolve().then(() => { // Counters are still 1 due second set of retain/release expect(apps['_refCounter']).to.deep.equal({ __admin__: 1, - __noauth__: 1, }); clock.tick(appsNamespace.garbageCollectionInterval / 2); }).then(() => { // It's now been a full interval since the second set of retain/release expect(apps['_refCounter']).to.deep.equal({ __admin__: 0, - __noauth__: 0, }); }); }); diff --git a/spec/cloud-functions.spec.ts b/spec/cloud-functions.spec.ts index ca16e5782..9f0e9a30b 100644 --- a/spec/cloud-functions.spec.ts +++ b/spec/cloud-functions.spec.ts @@ -22,13 +22,14 @@ import * as _ from 'lodash'; import { expect } from 'chai'; -import { Event, makeCloudFunction, MakeCloudFunctionArgs } from '../src/cloud-functions'; +import { Event, LegacyEvent, makeCloudFunction, MakeCloudFunctionArgs, Change } from '../src/cloud-functions'; describe('makeCloudFunction', () => { const cloudFunctionArgs: MakeCloudFunctionArgs = { provider: 'mock.provider', eventType: 'mock.event', - resource: 'resource', + service: 'service', + triggerResource: () => 'resource', handler: () => null, }; @@ -36,21 +37,19 @@ describe('makeCloudFunction', () => { let cf = makeCloudFunction(cloudFunctionArgs); expect(cf.__trigger).to.deep.equal({ eventTrigger: { - eventType: 'providers/mock.provider/eventTypes/mock.event', + eventType: 'mock.provider.mock.event', resource: 'resource', + service: 'service', }, }); }); - it('should preserve payload metadata', () => { - let args: any = _.assign({}, cloudFunctionArgs, {handler: (e) => e}); + it('should construct the right context for legacy event format', () => { + let args: any = _.assign({}, cloudFunctionArgs, {handler: (data, context) => context}); let cf = makeCloudFunction(args); - let test: Event = { + let test: LegacyEvent = { eventId: '00000', timestamp: '2016-11-04T21:29:03.496Z', - auth: { - admin: true, - }, eventType: 'providers/provider/eventTypes/event', resource: 'resource', data: 'data', @@ -59,63 +58,244 @@ describe('makeCloudFunction', () => { return expect(cf(test)).to.eventually.deep.equal({ eventId: '00000', timestamp: '2016-11-04T21:29:03.496Z', - auth: { - admin: true, + eventType: 'mock.provider.mock.event', + resource: { + service: 'service', + name: 'resource', + }, + params: {}, + }); + }); + + it('should construct the right context for new event format', () => { + let args: any = _.assign({}, cloudFunctionArgs, { handler: (data, context) => context }); + let cf = makeCloudFunction(args); + let test: Event = { + context: { + eventId: '00000', + timestamp: '2016-11-04T21:29:03.496Z', + eventType: 'provider.event', + resource: { + service: 'provider', + name: 'resource', + }, }, - eventType: 'providers/provider/eventTypes/event', - resource: 'resource', data: 'data', + }; + + return expect(cf(test)).to.eventually.deep.equal({ + eventId: '00000', + timestamp: '2016-11-04T21:29:03.496Z', + eventType: 'provider.event', + resource: { + service: 'provider', + name: 'resource', + }, params: {}, }); }); }); describe('makeParams', () => { - - const cloudFunctionArgs: MakeCloudFunctionArgs = { - provider: 'mock.provider', - eventType: 'mock.event', - resource: 'projects/_/instances/pid/ref/{foo}/nested/{bar}', - handler: () => null, + const args: MakeCloudFunctionArgs = { + provider: 'provider', + eventType: 'event', + service: 'service', + triggerResource: () => 'projects/_/instances/pid/ref/{foo}/nested/{bar}', + handler: (data, context) => context.params, }; + const cf = makeCloudFunction(args); - it('should construct params from the event resource', () => { - let args: any = _.assign({}, cloudFunctionArgs, {handler: (e) => e}); - let cf = makeCloudFunction(args); - - const testEvent: Event = { + it('should construct params from the event resource of legacy events', () => { + const testEvent: LegacyEvent = { resource: 'projects/_/instances/pid/ref/a/nested/b', + eventType: 'event', data: 'data', }; return expect(cf(testEvent)).to.eventually.deep.equal({ - resource: 'projects/_/instances/pid/ref/a/nested/b', - data: 'data', - params: { - foo: 'a', - bar: 'b', + foo: 'a', + bar: 'b', + }); + }); + + it('should construct params from the event resource of new format events', () => { + const testEvent: Event = { + context: { + eventId: '111', + timestamp: '2016-11-04T21:29:03.496Z', + resource: { + service: 'service', + name: 'projects/_/instances/pid/ref/a/nested/b', + }, + eventType: 'event', }, + data: 'data', + }; + + return expect(cf(testEvent)).to.eventually.deep.equal({ + foo: 'a', + bar: 'b', }); }); +}); - it('should construct params from the event params', () => { - let args: any = _.assign({}, cloudFunctionArgs, {handler: (e) => e}); - let cf = makeCloudFunction(args); +describe('makeAuth and makeAuthType', () => { + const args: MakeCloudFunctionArgs = { + provider: 'google.firebase.database', + eventType: 'event', + service: 'service', + triggerResource: () => 'projects/_/instances/pid/ref/{foo}/nested/{bar}', + handler: (data, context) => { + return { + auth: context.auth, + authMode: context.authType, + }; + }, + }; + let cf = makeCloudFunction(args); - const testEvent: Event = { + it('should construct correct auth and authType for admin user', () => { + const testEvent: LegacyEvent = { data: 'data', - params: { - foo: 'a', - bar: 'b', + auth: { + admin: true, }, }; return expect(cf(testEvent)).to.eventually.deep.equal({ + auth: undefined, + authMode: 'ADMIN', + }); + }); + + it('should construct correct auth and authType for unauthenticated user', () => { + const testEvent: LegacyEvent = { data: 'data', - params: { - foo: 'a', - bar: 'b', + auth: { + admin: false, }, + }; + + return expect(cf(testEvent)).to.eventually.deep.equal({ + auth: null, + authMode: 'UNAUTHENTICATED', + }); + }); + + it('should construct correct auth and authType for a user', () => { + const testEvent: LegacyEvent = { + data: 'data', + auth: { + admin: false, + variable: { + uid: 'user', + provider: 'google', + token: { + sub: 'user', + }, + }, + }, + }; + + return expect(cf(testEvent)).to.eventually.deep.equal({ + auth: { + uid: 'user', + token: { + sub: 'user', + }, + }, + authMode: 'USER', + }); + }); +}); + +describe('Change', () => { + describe('applyFieldMask', () => { + const after = { + foo: 'bar', + num: 2, + obj: { + a: 1, + b: 2, + }, + }; + + it('should handle deleted values', () => { + const sparseBefore = { baz: 'qux' }; + const fieldMask = 'baz'; + expect(Change.applyFieldMask(sparseBefore, after, fieldMask)).to.deep.equal( { + foo: 'bar', + num: 2, + obj: { + a: 1, + b: 2, + }, + baz: 'qux', + }); + }); + + it('should handle created values', () => { + const sparseBefore = {}; + const fieldMask = 'num,obj.a'; + expect(Change.applyFieldMask(sparseBefore, after, fieldMask)).to.deep.equal({ + foo: 'bar', + obj: { + b: 2, + }, + }); + }); + + it('should handle mutated values', () => { + const sparseBefore = { + num: 3, + obj: { + a: 3, + }, + }; + const fieldMask = 'num,obj.a'; + expect(Change.applyFieldMask(sparseBefore, after, fieldMask)).to.deep.equal({ + foo: 'bar', + num: 3, + obj: { + a: 3, + b: 2, + }, + }); + }); + }); + + describe('fromJSON', () => { + it('should create a Change object with a `before` and `after`', () => { + let created = Change.fromJSON({ + before: { foo: 'bar' }, + after: { foo: 'faz' }, + }); + expect(created instanceof Change).to.equal(true); + expect(created.before).to.deep.equal({ foo: 'bar' }); + expect(created.after).to.deep.equal({ foo: 'faz' }); + }); + + it('should apply the customizer function to `before` and `after`', () => { + function customizer(input) { + _.set(input, 'another', 'value'); + return input; + } + let created = Change.fromJSON( + { + before: { foo: 'bar' }, + after: { foo: 'faz' }, + }, + customizer, + ); + expect(created.before).to.deep.equal({ + foo: 'bar', + another: 'value', + }); + expect(created.after).to.deep.equal({ + foo: 'faz', + another: 'value', + }); }); }); }); diff --git a/spec/config.spec.ts b/spec/config.spec.ts index bba51392b..aae3ea94d 100644 --- a/spec/config.spec.ts +++ b/spec/config.spec.ts @@ -22,14 +22,14 @@ import * as mockRequire from 'mock-require'; import { expect } from 'chai'; -import { config } from '../src/config'; -import { unsetSingleton } from './support/helpers'; +import { config, firebaseConfig } from '../src/config'; describe('config()', () => { afterEach(() => { mockRequire.stopAll(); - unsetSingleton(); + delete config.singleton; + delete process.env.FIREBASE_CONFIG; delete process.env.FIREBASE_PROJECT; delete process.env.CLOUD_RUNTIME_CONFIG; }); @@ -37,31 +37,42 @@ describe('config()', () => { it('loads config values from .runtimeconfig.json', () => { mockRequire('../../../.runtimeconfig.json', { foo: 'bar', firebase: {} }); let loaded = config(); - expect(loaded).to.have.property('firebase'); + expect(loaded).to.not.have.property('firebase'); expect(loaded).to.have.property('foo','bar'); }); - it('injects a Firebase credential', () => { - mockRequire('../../../.runtimeconfig.json', { firebase: {} }); - expect(config()).to.deep.property('firebase.credential'); - }); - - it('throws an error if .runtimeconfig.json not present', () => { + it('does not provide firebase config if .runtimeconfig.json not invalid', () => { mockRequire('../../../.runtimeconfig.json', 'does-not-exist'); - expect(config).to.throw('not available'); + expect(firebaseConfig()).to.be.null; }); - it('throws an error if Firebase configs not present', () => { + it('does not provide firebase config if .ruuntimeconfig.json has no firebase property', () => { mockRequire('../../../.runtimeconfig.json', {}); - expect(config).to.throw('Firebase config variables are not available.'); + expect(firebaseConfig()).to.be.null; }); it('loads Firebase configs from FIREBASE_PROJECT env variable', () => { process.env.FIREBASE_PROJECT = JSON.stringify({ databaseURL: 'foo@firebaseio.com', }); - let firebaseConfig = config().firebase; - expect(firebaseConfig).to.have.property('databaseURL', 'foo@firebaseio.com'); + expect(firebaseConfig()).to.have.property('databaseURL', 'foo@firebaseio.com'); + }); + + it('loads Firebase configs from FIREBASE_CONFIG env variable', () => { + process.env.FIREBASE_CONFIG = JSON.stringify({ + databaseURL: 'foo@firebaseio.com', + }); + expect(firebaseConfig()).to.have.property('databaseURL', 'foo@firebaseio.com'); + }); + + it('prefers FIREBASE_CONFIG over FIREBASE_PROJECT', () => { + process.env.FIREBASE_CONFIG = JSON.stringify({ + databaseURL: 'firebase_config', + }); + process.env.FIREBASE_PROJECT = JSON.stringify({ + databaseURL: 'firebase_project', + }); + expect(firebaseConfig()).to.have.property('databaseURL', 'firebase_config'); }); it('behaves well when both FIREBASE_PROJECT and .runtimeconfig.json present', () => { @@ -74,24 +85,21 @@ describe('config()', () => { }, foo: 'bar', }); - let loaded = config(); - expect(loaded.firebase).to.have.property('databaseURL', 'foo@firebaseio.com'); - expect(loaded).to.have.property('foo', 'bar'); + expect(firebaseConfig()).to.have.property('databaseURL', 'foo@firebaseio.com'); + expect(config()).to.have.property('foo', 'bar'); }); it('accepts alternative locations for config file', () => { process.env.CLOUD_RUNTIME_CONFIG = 'another.json'; mockRequire('another.json', { foo: 'bar', firebase: {} }); - let loaded = config(); - expect(loaded).to.have.property('firebase'); - expect(loaded).to.have.property('foo','bar'); + expect(firebaseConfig()).to.not.be.null; + expect(config()).to.have.property('foo','bar'); }); it('accepts full JSON in env.CLOUD_RUNTIME_CONFIG', () => { process.env.CLOUD_RUNTIME_CONFIG = JSON.stringify({foo: 'bar', firebase:{} }); - let loaded = config(); - expect(loaded).to.have.property('firebase'); - expect(loaded).to.have.property('foo', 'bar'); + expect(firebaseConfig()).to.not.be.null; + expect(config()).to.have.property('foo', 'bar'); }); it('behaves well when both env.CLOUD_RUNTIME_CONFIG and env.FIREBASE_PROJECT are set', () => { @@ -99,8 +107,7 @@ describe('config()', () => { process.env.FIREBASE_PROJECT = JSON.stringify({ databaseURL: 'foo@firebaseio.com', }); - let loaded = config(); - expect(loaded.firebase).to.have.property('databaseURL', 'foo@firebaseio.com'); - expect(loaded).to.have.property('foo', 'bar'); + expect(firebaseConfig()).to.have.property('databaseURL', 'foo@firebaseio.com'); + expect(config()).to.have.property('foo', 'bar'); }); }); diff --git a/spec/providers/analytics.spec.input.ts b/spec/providers/analytics.spec.input.ts index b46d672d0..84ac4c3ce 100644 --- a/spec/providers/analytics.spec.input.ts +++ b/spec/providers/analytics.spec.input.ts @@ -22,7 +22,6 @@ /* tslint:disable:max-line-length */ import { AnalyticsEvent } from '../../src/providers/analytics'; -import { Event } from '../../src/cloud-functions'; // A payload, as it might arrive over the wire. Every possible field is filled out at least once. export const fullPayload = JSON.parse(`{ @@ -125,70 +124,63 @@ export const fullPayload = JSON.parse(`{ }`); // The event data that we expect would be constructed if the payload above were to arrive. -export const fullEvent: Event = { - eventId: '1486080145623867projects/analytics-integration-fd82a/events/i_made_this_upproviders/google.firebase.analytics/eventTypes/event.sendprojects/f949d1bb9ef782579-tp/topics/cloud-functions-u54ejabpzs4prfjh7433eklhae', - eventType: 'providers/google.firebase.analytics/eventTypes/event.send', - resource: 'projects/analytics-integration-fd82a/events/i_made_this_up', - timestamp: '2017-03-29T23:59:59.986371388Z', - params: {}, - data: { - reportingDate: '20170202', - name: 'Loaded_In_Background', - params: { - build: '1350', - calls_remaining: 10, - fraction_calls_dropped: 0.0123456, - average_call_rating: 4.5, +export const data: AnalyticsEvent = { + reportingDate: '20170202', + name: 'Loaded_In_Background', + params: { + build: '1350', + calls_remaining: 10, + fraction_calls_dropped: 0.0123456, + average_call_rating: 4.5, + }, + logTime: '2017-02-02T23:06:26.124Z', + previousLogTime: '2017-02-02T23:01:19.797Z', + valueInUSD: 1234.5, + user: { + userId: 'abcdefghijklmnop!', + appInfo: { + appId: 'com.mobileday.MobileDay', + appInstanceId: 'E3C9939401814B9B954725A740B8C7BC', + appPlatform: 'IOS', + appStore: 'iTunes', + appVersion: '5.2.0', }, - logTime: '2017-02-02T23:06:26.124Z', - previousLogTime: '2017-02-02T23:01:19.797Z', - valueInUSD: 1234.5, - user: { - userId: 'abcdefghijklmnop!', - appInfo: { - appId: 'com.mobileday.MobileDay', - appInstanceId: 'E3C9939401814B9B954725A740B8C7BC', - appPlatform: 'IOS', - appStore: 'iTunes', - appVersion: '5.2.0', - }, - bundleInfo: { - bundleSequenceId: 6034, - serverTimestampOffset: 371, - }, - deviceInfo: { - deviceCategory: 'mobile', - deviceModel: 'iPhone7,2', - deviceTimeZoneOffsetSeconds: -21600, - mobileBrandName: 'Apple', - mobileMarketingName: 'iPhone 6', - mobileModelName: 'iPhone 6', - platformVersion: '10.2.1', - userDefaultLanguage: 'en-us', - deviceId: '599F9C00-92DC-4B5C-9464-7971F01F8370', - resettableDeviceId: '599F9C00-92DC-4B5C-9464-7971F01F8370', - limitedAdTracking: true, + bundleInfo: { + bundleSequenceId: 6034, + serverTimestampOffset: 371, + }, + deviceInfo: { + deviceCategory: 'mobile', + deviceModel: 'iPhone7,2', + deviceTimeZoneOffsetSeconds: -21600, + mobileBrandName: 'Apple', + mobileMarketingName: 'iPhone 6', + mobileModelName: 'iPhone 6', + platformVersion: '10.2.1', + userDefaultLanguage: 'en-us', + deviceId: '599F9C00-92DC-4B5C-9464-7971F01F8370', + resettableDeviceId: '599F9C00-92DC-4B5C-9464-7971F01F8370', + limitedAdTracking: true, + }, + firstOpenTime: '2016-04-28T15:00:35.819Z', + geoInfo: { + city: 'Plano', + continent: '021', + country: 'United States', + region: 'Texas', + }, + userProperties: { + build: { + setTime: '2017-02-02T23:06:26.090Z', + value: '1350', }, - firstOpenTime: '2016-04-28T15:00:35.819Z', - geoInfo: { - city: 'Plano', - continent: '021', - country: 'United States', - region: 'Texas', + calls_remaining: { + setTime: '2017-02-02T23:06:26.094Z', + value: '10', }, - userProperties: { - build: { - setTime: '2017-02-02T23:06:26.090Z', - value: '1350', - }, - calls_remaining: { - setTime: '2017-02-02T23:06:26.094Z', - value: '10', - }, - version: { - setTime: '2017-02-02T23:06:26.085Z', - value: '5.2.0', - }, + version: { + setTime: '2017-02-02T23:06:26.085Z', + value: '5.2.0', }, }, }, diff --git a/spec/providers/analytics.spec.ts b/spec/providers/analytics.spec.ts index 95920fb27..cef1d8328 100644 --- a/spec/providers/analytics.spec.ts +++ b/spec/providers/analytics.spec.ts @@ -22,204 +22,220 @@ import * as analytics from '../../src/providers/analytics'; import { expect } from 'chai'; -import { Event } from '../../src/cloud-functions'; +import { LegacyEvent } from '../../src/cloud-functions'; import * as analytics_spec_input from './analytics.spec.input'; -describe('AnalyticsEventBuilder', () => { - before(() => { - process.env.GCLOUD_PROJECT = 'project1'; - }); +describe('Analytics Functions', () => { + describe('EventBuilder', () => { + before(() => { + process.env.GCLOUD_PROJECT = 'project1'; + }); - after(() => { - delete process.env.GCLOUD_PROJECT; - }); + after(() => { + delete process.env.GCLOUD_PROJECT; + }); - describe('#onLog', () => { - it('should return a TriggerDefinition with appropriate values', () => { - const cloudFunction = analytics.event('first_open').onLog(() => null); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'providers/google.firebase.analytics/eventTypes/event.log', - resource: 'projects/project1/events/first_open', - }, + describe('#onLog', () => { + it('should return a TriggerDefinition with appropriate values', () => { + const cloudFunction = analytics.event('first_open').onLog(() => null); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'providers/google.firebase.analytics/eventTypes/event.log', + resource: 'projects/project1/events/first_open', + service: 'app-measurement.com', + }, + }); }); }); - }); - describe('#dataConstructor', () => { - it('should handle an event with the appropriate fields', () => { - const cloudFunction = analytics.event('first_open').onLog((ev: Event) => ev.data); - - // The event data delivered over the wire will be the JSON for an AnalyticsEvent: - // https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data - let event = { - eventId: 'f2e2f0bf-2e47-4d92-b009-e7a375ecbd3e', - eventType: 'providers/google.firebase.analytics/eventTypes/event.log', - resource: 'projects/myUnitTestProject/events/first_open', - notSupported: { - }, - data: { - userDim: { + describe('#dataConstructor', () => { + it('should handle an event with the appropriate fields', () => { + const cloudFunction = analytics.event('first_open').onLog((data: analytics.AnalyticsEvent) => data); + + // The event data delivered over the wire will be the JSON for an AnalyticsEvent: + // https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data + let event: LegacyEvent = { + eventId: 'f2e2f0bf-2e47-4d92-b009-e7a375ecbd3e', + eventType: 'providers/google.firebase.analytics/eventTypes/event.log', + resource: 'projects/myUnitTestProject/events/first_open', + data: { + userDim: { + userId: 'hi!', + }, + }, + }; + + return expect(cloudFunction(event)).to.eventually.deep.equal({ + params: {}, + user: { userId: 'hi!', + userProperties: {}, }, - }, - }; - - return expect(cloudFunction(event)).to.eventually.deep.equal({ - params: {}, - user: { - userId: 'hi!', - userProperties: {}, - }, + }); }); - }); - it('should remove xValues', () => { - const cloudFunction = analytics.event('first_open').onLog((ev: Event) => ev.data); - - // Incoming events will have four kinds of "xValue" fields: "intValue", - // "stringValue", "doubleValue" and "floatValue". We expect those to get - // flattened away, leaving just their values. - let event = { - data: { - eventDim: - [ - { - date: '20170202', - name: 'Loaded_In_Background', - params: { - build: { - stringValue: '1350', - }, - calls_remaining: { - intValue: '10', - }, - goats_teleported: { - doubleValue: 1.1, + it('should remove xValues', () => { + const cloudFunction = analytics.event('first_open').onLog((data: analytics.AnalyticsEvent) => data); + + // Incoming events will have four kinds of "xValue" fields: "intValue", + // "stringValue", "doubleValue" and "floatValue". We expect those to get + // flattened away, leaving just their values. + let event: LegacyEvent = { + data: { + eventDim: + [ + { + date: '20170202', + name: 'Loaded_In_Background', + params: { + build: { + stringValue: '1350', + }, + calls_remaining: { + intValue: '10', + }, + goats_teleported: { + doubleValue: 1.1, + }, + boat_boyancy: { + floatValue: 133.7, + }, }, - boat_boyancy: { - floatValue: 133.7, + }, + ], + userDim: { + userProperties: { + foo: { + value: { + stringValue: 'bar', + }, }, }, }, - ], - userDim: { + }, + }; + + return expect(cloudFunction(event)).to.eventually.deep.equal({ + reportingDate: '20170202', + name: 'Loaded_In_Background', + params: { + build: '1350', + calls_remaining: 10, + goats_teleported: 1.1, + boat_boyancy: 133.7, + }, + user: { userProperties: { foo: { - value: { - stringValue: 'bar', - }, + value: 'bar', }, }, }, - }, - }; - - return expect(cloudFunction(event)).to.eventually.deep.equal({ - reportingDate: '20170202', - name: 'Loaded_In_Background', - params: { - build: '1350', - calls_remaining: 10, - goats_teleported: 1.1, - boat_boyancy: 133.7, - }, - user: { - userProperties: { - foo: { - value: 'bar', - }, - }, - }, + }); }); - }); - it('should change microsecond timestamps to ISO strings, and offsets to millis', () => { - const cloudFunction = analytics.event('first_open').onLog((ev: Event) => ev.data); - - let event = { - data: { - eventDim: - [ - { - date: '20170202', - name: 'Loaded_In_Background', - timestampMicros: '1489080600000000', - previousTimestampMicros: '526657020000000', + it('should change microsecond timestamps to ISO strings, and offsets to millis', () => { + const cloudFunction = analytics.event('first_open').onLog((data: analytics.AnalyticsEvent) => data); + + let event: LegacyEvent = { + data: { + eventDim: + [ + { + date: '20170202', + name: 'Loaded_In_Background', + timestampMicros: '1489080600000000', + previousTimestampMicros: '526657020000000', + }, + ], + userDim: { + firstOpenTimestampMicros: '577978620000000', + userProperties: { + foo: { + setTimestampUsec: '514820220000000', + }, + }, + bundleInfo: { + serverTimestampOffsetMicros: 9876789, + }, }, - ], - userDim: { - firstOpenTimestampMicros: '577978620000000', + }, + }; + + return expect(cloudFunction(event)).to.eventually.deep.equal({ + reportingDate: '20170202', + name: 'Loaded_In_Background', + params: {}, + logTime: '2017-03-09T17:30:00.000Z', + previousLogTime: '1986-09-09T13:37:00.000Z', + user: { + firstOpenTime: '1988-04-25T13:37:00.000Z', userProperties: { foo: { - setTimestampUsec: '514820220000000', + setTime: '1986-04-25T13:37:00.000Z', }, }, bundleInfo: { - serverTimestampOffsetMicros: 9876789, + serverTimestampOffset: 9877, }, }, - }, - }; - - return expect(cloudFunction(event)).to.eventually.deep.equal({ - reportingDate: '20170202', - name: 'Loaded_In_Background', - params: {}, - logTime: '2017-03-09T17:30:00.000Z', - previousLogTime: '1986-09-09T13:37:00.000Z', - user: { - firstOpenTime: '1988-04-25T13:37:00.000Z', - userProperties: { - foo: { - setTime: '1986-04-25T13:37:00.000Z', - }, - }, - bundleInfo: { - serverTimestampOffset: 9877, + }); + }); + + it('should populate currency fields', () => { + const cloudFunction = analytics.event('first_open').onLog((data: analytics.AnalyticsEvent) => data); + + // Incoming events will have four kinds of "xValue" fields: "intValue", + // "stringValue", "doubleValue" and "floatValue". We expect those to get + // flattened away, leaving just their values. + // + // xValues in eventDim[...].params should also populate a 'rawValue' field + // that always contains a string. + // + // Separately, the input has a number of microsecond timestamps that we'd + // like to rename and scale down to milliseconds. + let event: LegacyEvent = { + data: { + eventDim: + [ + { + date: '20170202', + name: 'Loaded_In_Background', + valueInUsd: 123.4, + }, + ], }, - }, + }; + + return expect(cloudFunction(event)).to.eventually.deep.equal({ + reportingDate: '20170202', + name: 'Loaded_In_Background', + params: {}, + valueInUSD: 123.4, // Field renamed Usd -> USD. + }); }); - }); - it('should populate currency fields', () => { - const cloudFunction = analytics.event('first_open').onLog((ev: Event) => ev.data); - - // Incoming events will have four kinds of "xValue" fields: "intValue", - // "stringValue", "doubleValue" and "floatValue". We expect those to get - // flattened away, leaving just their values. - // - // xValues in eventDim[...].params should also populate a 'rawValue' field - // that always contains a string. - // - // Separately, the input has a number of microsecond timestamps that we'd - // like to rename and scale down to milliseconds. - let event = { - data: { - eventDim: - [ - { - date: '20170202', - name: 'Loaded_In_Background', - valueInUsd: 123.4, - }, - ], - }, - }; - - return expect(cloudFunction(event)).to.eventually.deep.equal({ - reportingDate: '20170202', - name: 'Loaded_In_Background', - params: {}, - valueInUSD: 123.4, // Field renamed Usd -> USD. + it('should recognize all the fields the payload can contain', () => { + const cloudFunction = analytics.event('first_open').onLog((data: analytics.AnalyticsEvent) => data); + // The payload in analytics_spec_input contains all possible fields at least once. + return expect(cloudFunction(analytics_spec_input.fullPayload)) + .to.eventually.deep.equal(analytics_spec_input.data); }); }); + }); + + describe('process.env.GCLOUD_PROJECT not set', () => { + it('should not throw if __trigger is not accessed', () => { + expect(() => analytics.event('event').onLog(() => null)).to.not.throw(Error); + }); + + it('should throw when trigger is accessed', () => { + expect(() => analytics.event('event').onLog(() => null).__trigger).to.throw(Error); + }); - it('should recognize all the fields the payload can contain', () => { - const cloudFunction = analytics.event('first_open').onLog((ev: Event) => ev); - // The payload in analytics_spec_input contains all possible fields at least once. - return expect(cloudFunction(analytics_spec_input.fullPayload)) - .to.eventually.deep.equal(analytics_spec_input.fullEvent); + it('should not throw when #run is called', () => { + let cf = analytics.event('event').onLog(() => null); + expect(cf.run).to.not.throw(Error); }); }); }); diff --git a/spec/providers/auth.spec.ts b/spec/providers/auth.spec.ts index 1ea251d79..5ab40bd5a 100644 --- a/spec/providers/auth.spec.ts +++ b/spec/providers/auth.spec.ts @@ -22,115 +22,160 @@ import * as auth from '../../src/providers/auth'; import { expect } from 'chai'; -import { Event } from '../../src/cloud-functions'; import * as firebase from 'firebase-admin'; -describe('AuthBuilder', () => { - let handler: (e: Event) => PromiseLike | any; +describe('Auth Functions', () => { + describe('AuthBuilder', () => { + let handler: (user: firebase.auth.UserRecord) => PromiseLike | any; - before(() => { - process.env.GCLOUD_PROJECT = 'project1'; - }); + before(() => { + process.env.GCLOUD_PROJECT = 'project1'; + }); - after(() => { - delete process.env.GCLOUD_PROJECT; - }); + after(() => { + delete process.env.GCLOUD_PROJECT; + }); - describe('#onCreate', () => { - it('should return a TriggerDefinition with appropriate values', () => { - const cloudFunction = auth.user().onCreate(() => null); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'providers/firebase.auth/eventTypes/user.create', - resource: 'projects/project1', - }, + describe('#onCreate', () => { + it('should return a TriggerDefinition with appropriate values', () => { + const cloudFunction = auth.user().onCreate(() => null); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'providers/firebase.auth/eventTypes/user.create', + resource: 'projects/project1', + service: 'firebaseauth.googleapis.com', + }, + }); }); }); - }); - describe('#onDelete', () => { - it('should return a TriggerDefinition with appropriate values', () => { - const cloudFunction = auth.user().onDelete(handler); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'providers/firebase.auth/eventTypes/user.delete', - resource: 'projects/project1', - }, + describe('#onDelete', () => { + it('should return a TriggerDefinition with appropriate values', () => { + const cloudFunction = auth.user().onDelete(handler); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'providers/firebase.auth/eventTypes/user.delete', + resource: 'projects/project1', + service: 'firebaseauth.googleapis.com', + }, + }); }); }); - }); - describe('#_dataConstructor', () => { - let cloudFunctionCreate; - let cloudFunctionDelete; - let event; + describe('#_dataConstructor', () => { + let cloudFunctionCreate; + let cloudFunctionDelete; + let event; - before(() => { - cloudFunctionCreate = auth.user().onCreate((ev: Event) => ev.data); - cloudFunctionDelete = auth.user().onDelete((ev: Event) => ev.data); - event = { - data: { - metadata: { - createdAt: '2016-12-15T19:37:37.059Z', - lastSignedInAt: '2017-01-01T00:00:00.000Z', + before(() => { + cloudFunctionCreate = auth.user().onCreate((data: firebase.auth.UserRecord) => data); + cloudFunctionDelete = auth.user().onDelete((data: firebase.auth.UserRecord) => data); + event = { + data: { + metadata: { + createdAt: '2016-12-15T19:37:37.059Z', + lastSignedInAt: '2017-01-01T00:00:00.000Z', + }, }, - }, - }; - }); + }; + }); - it('should transform old wire format for UserRecord into v5.0.0 format', () => { - return Promise.all([ - cloudFunctionCreate(event).then(data => { - expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); - expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); - }), - cloudFunctionDelete(event).then(data => { - expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); - expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); - }), - ]); + it('should transform wire format for UserRecord into v5.0.0 format', () => { + return Promise.all([ + cloudFunctionCreate(event).then(data => { + expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); + expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); + }), + cloudFunctionDelete(event).then(data => { + expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); + expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); + }), + ]); + }); + + it('should handle new wire format if/when there is a change', () => { + const newEvent = { + data: { + metadata: { + creationTime: '2016-12-15T19:37:37.059Z', + lastSignInTime: '2017-01-01T00:00:00.000Z', + }, + }, + }; + + return Promise.all([ + cloudFunctionCreate(newEvent).then(data => { + expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); + expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); + }), + cloudFunctionDelete(newEvent).then(data => { + expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); + expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); + }), + ]); + }); }); + }); - // createdAt and lastSignedIn are fields of admin.auth.UserMetadata below v5.0.0 - // We want to add shims to still expose these fields so that user's code do not break - // The shim and this test should be removed in v1.0.0 of firebase-functions - it('should still retain createdAt and lastSignedIn', () => { - return Promise.all([ - cloudFunctionCreate(event).then(data => { - expect(data.metadata.createdAt).to.deep.equal(new Date('2016-12-15T19:37:37.059Z')); - expect(data.metadata.lastSignedInAt).to.deep.equal(new Date('2017-01-01T00:00:00.000Z')); - }), - cloudFunctionDelete(event).then(data => { - expect(data.metadata.createdAt).to.deep.equal(new Date('2016-12-15T19:37:37.059Z')); - expect(data.metadata.lastSignedInAt).to.deep.equal(new Date('2017-01-01T00:00:00.000Z')); - }), - ]); + describe('userRecordConstructor', () => { + it('will provide falsey values for fields that are not in raw wire data', () => { + const record = auth.userRecordConstructor({ uid: '123'}); + expect(record.toJSON()).to.deep.equal({ + uid: '123', + email: null, + emailVerified: false, + displayName: null, + photoURL: null, + phoneNumber: null, + disabled: false, + providerData: [], + customClaims: {}, + passwordSalt: null, + passwordHash: null, + tokensValidAfterTime: null, + metadata: { + creationTime: null, + lastSignInTime: null, + }, + }); }); - it('should handle new wire format if/when there is a change', () => { - const newEvent = { - data: { - metadata: { - creationTime: '2016-12-15T19:37:37.059Z', - lastSignInTime: '2017-01-01T00:00:00.000Z', - }, + it('will not interfere with fields that are in raw wire data', () => { + const raw = { + uid: '123', + email: 'email@gmail.com', + emailVerified: true, + displayName: 'User', + photoURL: 'url', + phoneNumber: '1233332222', + disabled: true, + providerData: [], + customClaims: {}, + passwordSalt: 'abc', + passwordHash: 'def', + tokensValidAfterTime: '2027-02-02T23:01:19.797Z', + metadata: { + creationTime: '2017-02-02T23:06:26.124Z', + lastSignInTime: '2017-02-02T23:01:19.797Z', }, }; + const record = auth.userRecordConstructor(raw); + expect(record.toJSON()).to.deep.equal(raw); + }); + }); + + describe('process.env.GCLOUD_PROJECT not set', () => { + it('should not throw if __trigger is not accessed', () => { + expect(() => auth.user().onCreate(() => null)).to.not.throw(Error); + }); + + it('should throw when trigger is accessed', () => { + expect(() => auth.user().onCreate(() => null).__trigger).to.throw(Error); + }); - return Promise.all([ - cloudFunctionCreate(newEvent).then(data => { - expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); - expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); - expect(data.metadata.createdAt).to.deep.equal(new Date('2016-12-15T19:37:37.059Z')); - expect(data.metadata.lastSignedInAt).to.deep.equal(new Date('2017-01-01T00:00:00.000Z')); - }), - cloudFunctionDelete(newEvent).then(data => { - expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); - expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); - expect(data.metadata.createdAt).to.deep.equal(new Date('2016-12-15T19:37:37.059Z')); - expect(data.metadata.lastSignedInAt).to.deep.equal(new Date('2017-01-01T00:00:00.000Z')); - }), - ]); + it('should not throw when #run is called', () => { + let cf = auth.user().onCreate(() => null); + expect(cf.run).to.not.throw(Error); }); }); }); diff --git a/spec/providers/crashlytics.spec.ts b/spec/providers/crashlytics.spec.ts index 06c941e32..2b678eefb 100644 --- a/spec/providers/crashlytics.spec.ts +++ b/spec/providers/crashlytics.spec.ts @@ -21,57 +21,73 @@ // SOFTWARE. import * as crashlytics from '../../src/providers/crashlytics'; -import { config } from '../../src/index'; import { apps as appsNamespace } from '../../src/apps'; import { expect } from 'chai'; -import { fakeConfig } from '../support/helpers'; describe('Crashlytics Functions', () => { - before(() => { - config.singleton = fakeConfig(); - appsNamespace.init(config.singleton); - process.env.GCLOUD_PROJECT = 'project1'; - }); + describe('Issue Builder', () => { + before(() => { + appsNamespace.init(); + process.env.GCLOUD_PROJECT = 'project1'; + }); - after(() => { - delete appsNamespace.singleton; - delete config.singleton; - delete process.env.GCLOUD_PROJECT; - }); + after(() => { + delete appsNamespace.singleton; + delete process.env.GCLOUD_PROJECT; + }); - describe('#onNewDetected', () => { - it('should return a TriggerDefinition with appropriate values', () => { - const cloudFunction = crashlytics.issue().onNewDetected((event) => null); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'providers/firebase.crashlytics/eventTypes/issue.new', - resource: 'projects/project1', - }, + describe('#onNew', () => { + it('should return a TriggerDefinition with appropriate values', () => { + const cloudFunction = crashlytics.issue().onNew(data => null); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'providers/firebase.crashlytics/eventTypes/issue.new', + resource: 'projects/project1', + service: 'fabric.io', + }, + }); }); }); - }); - describe('#onRegressed', () => { - it('should return a TriggerDefinition with appropriate values', () => { - const cloudFunction = crashlytics.issue().onRegressed((event) => null); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'providers/firebase.crashlytics/eventTypes/issue.regressed', - resource: 'projects/project1', - }, + describe('#onRegressed', () => { + it('should return a TriggerDefinition with appropriate values', () => { + const cloudFunction = crashlytics.issue().onRegressed(data => null); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'providers/firebase.crashlytics/eventTypes/issue.regressed', + resource: 'projects/project1', + service: 'fabric.io', + }, + }); }); }); - }); - describe('#onVelocityAlert', () => { - it('should return a TriggerDefinition with appropriate values', () => { - const cloudFunction = crashlytics.issue().onVelocityAlert((event) => null); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'providers/firebase.crashlytics/eventTypes/issue.velocityAlert', - resource: 'projects/project1', - }, + describe('#onVelocityAlert', () => { + it('should return a TriggerDefinition with appropriate values', () => { + const cloudFunction = crashlytics.issue().onVelocityAlert(data => null); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'providers/firebase.crashlytics/eventTypes/issue.velocityAlert', + resource: 'projects/project1', + service: 'fabric.io', + }, + }); }); }); }); + + describe('process.env.GCLOUD_PROJECT not set', () => { + it('should not throw if __trigger is not accessed', () => { + expect(() => crashlytics.issue().onNew(() => null)).to.not.throw(Error); + }); + + it('should throw if __trigger is accessed', () => { + expect(() => crashlytics.issue().onNew(() => null).__trigger).to.throw(Error); + }); + + it('should not throw when #run is called', () => { + let cf = crashlytics.issue().onNew(() => null); + expect(cf.run).to.not.throw(Error); + }); + }); }); diff --git a/spec/providers/database.spec.ts b/spec/providers/database.spec.ts index 6b06cc2fa..bebdac29a 100644 --- a/spec/providers/database.spec.ts +++ b/spec/providers/database.spec.ts @@ -22,442 +22,399 @@ import * as database from '../../src/providers/database'; import { expect as expect } from 'chai'; -import { fakeConfig } from '../support/helpers'; import { apps as appsNamespace } from '../../src/apps'; -import { config } from '../../src/index'; +import { applyChange } from '../../src/utils'; -describe('DatabaseBuilder', () => { +describe('Database Functions', () => { - before(() => { - config.singleton = fakeConfig(); - appsNamespace.init(config.singleton); - }); - - after(() => { - delete appsNamespace.singleton; - delete config.singleton; - }); + describe('DatabaseBuilder', () => { + // TODO add tests for building a data or change based on the type of operation - describe('#onWrite()', () => { - it('should return "ref.write" as the event type', () => { - let eventType = database.ref('foo').onWrite(() => null).__trigger.eventTrigger.eventType; - expect(eventType).to.eq('providers/google.firebase.database/eventTypes/ref.write'); - }); - - it('should construct a proper resource path', () => { - let resource = database.ref('foo').onWrite(() => null).__trigger.eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); + before(() => { + process.env.FIREBASE_CONFIG = JSON.stringify({ + databaseURL: 'https://subdomain.firebaseio.com', + }); + appsNamespace.init(); }); - it('should let developers choose a database instance', () => { - let func = database.instance('custom').ref('foo').onWrite(() => null); - let resource = func.__trigger.eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/custom/refs/foo'); + after(() => { + delete process.env.FIREBASE_CONFIG; + delete appsNamespace.singleton; }); - it('should return a handler that emits events with a proper DeltaSnapshot', () => { - let handler = database.ref('/users/{id}').onWrite(event => { - expect(event.data.val()).to.deep.equal({ foo: 'bar' }); + describe('#onWrite()', () => { + it('should return "ref.write" as the event type', () => { + let eventType = database.ref('foo').onWrite(() => null).__trigger.eventTrigger.eventType; + expect(eventType).to.eq('providers/google.firebase.database/eventTypes/ref.write'); }); - return handler({ - data: { - data: null, - delta: { foo: 'bar' }, - }, - resource: 'projects/_/instances/subdomains/refs/users', - } as any); - }); - }); - - describe('#onCreate()', () => { - it('should return "ref.create" as the event type', () => { - let eventType = database.ref('foo').onCreate(() => null).__trigger.eventTrigger.eventType; - expect(eventType).to.eq('providers/google.firebase.database/eventTypes/ref.create'); - }); + it('should construct a proper resource path', () => { + let resource = database.ref('foo').onWrite(() => null).__trigger.eventTrigger.resource; + expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); + }); - it('should construct a proper resource path', () => { - let resource = database.ref('foo').onCreate(() => null).__trigger.eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); - }); + it('should let developers choose a database instance', () => { + let func = database.instance('custom').ref('foo').onWrite(() => null); + let resource = func.__trigger.eventTrigger.resource; + expect(resource).to.eq('projects/_/instances/custom/refs/foo'); + }); - it('should let developers choose a database instance', () => { - let func = database.instance('custom').ref('foo').onCreate(() => null); - let resource = func.__trigger.eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/custom/refs/foo'); + it('should return a handler that emits events with a proper DataSnapshot', () => { + let handler = database.ref('/users/{id}').onWrite(change => { + expect(change.after.val()).to.deep.equal({ foo: 'bar' }); + }); + + return handler({ + data: { + data: null, + delta: { foo: 'bar' }, + }, + resource: 'projects/_/instances/subdomains/refs/users', + eventType: 'providers/google.firebase.database/eventTypes/ref.write', + }); + }); }); - it('should return a handler that emits events with a proper DeltaSnapshot', () => { - let handler = database.ref('/users/{id}').onCreate(event => { - expect(event.data.val()).to.deep.equal({ foo: 'bar' }); + describe('#onCreate()', () => { + it('should return "ref.create" as the event type', () => { + let eventType = database.ref('foo').onCreate(() => null).__trigger.eventTrigger.eventType; + expect(eventType).to.eq('providers/google.firebase.database/eventTypes/ref.create'); }); - return handler({ - data: { - data: null, - delta: { foo: 'bar' }, - }, - resource: 'projects/_/instances/subdomains/refs/users', - } as any); - }); - }); - - describe('#onUpdate()', () => { - it('should return "ref.update" as the event type', () => { - let eventType = database.ref('foo').onUpdate(() => null).__trigger.eventTrigger.eventType; - expect(eventType).to.eq('providers/google.firebase.database/eventTypes/ref.update'); - }); + it('should construct a proper resource path', () => { + let resource = database.ref('foo').onCreate(() => null).__trigger.eventTrigger.resource; + expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); + }); - it('should construct a proper resource path', () => { - let resource = database.ref('foo').onUpdate(() => null).__trigger.eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); - }); + it('should let developers choose a database instance', () => { + let func = database.instance('custom').ref('foo').onCreate(() => null); + let resource = func.__trigger.eventTrigger.resource; + expect(resource).to.eq('projects/_/instances/custom/refs/foo'); + }); - it('should let developers choose a database instance', () => { - let func = database.instance('custom').ref('foo').onUpdate(() => null); - let resource = func.__trigger.eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/custom/refs/foo'); + it('should return a handler that emits events with a proper DataSnapshot', () => { + let handler = database.ref('/users/{id}').onCreate(data => { + expect(data.val()).to.deep.equal({ foo: 'bar' }); + }); + + return handler({ + data: { + data: null, + delta: { foo: 'bar' }, + }, + resource: 'projects/_/instances/subdomains/refs/users', + eventType: 'providers/google.firebase.database/eventTypes/ref.create', + }); + }); }); - it('should return a handler that emits events with a proper DeltaSnapshot', () => { - let handler = database.ref('/users/{id}').onUpdate(event => { - expect(event.data.val()).to.deep.equal({ foo: 'bar' }); + describe('#onUpdate()', () => { + it('should return "ref.update" as the event type', () => { + let eventType = database.ref('foo').onUpdate(() => null).__trigger.eventTrigger.eventType; + expect(eventType).to.eq('providers/google.firebase.database/eventTypes/ref.update'); }); - return handler({ - data: { - data: null, - delta: { foo: 'bar' }, - }, - resource: 'projects/_/instances/subdomains/refs/users', - } as any); - }); - }); - - describe('#onDelete()', () => { - it('should return "ref.delete" as the event type', () => { - let eventType = database.ref('foo').onDelete(() => null).__trigger.eventTrigger.eventType; - expect(eventType).to.eq('providers/google.firebase.database/eventTypes/ref.delete'); - }); + it('should construct a proper resource path', () => { + let resource = database.ref('foo').onUpdate(() => null).__trigger.eventTrigger.resource; + expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); + }); - it('should construct a proper resource path', () => { - let resource = database.ref('foo').onDelete(() => null).__trigger.eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); - }); + it('should let developers choose a database instance', () => { + let func = database.instance('custom').ref('foo').onUpdate(() => null); + let resource = func.__trigger.eventTrigger.resource; + expect(resource).to.eq('projects/_/instances/custom/refs/foo'); + }); - it('should let developers choose a database instance', () => { - let func = database.instance('custom').ref('foo').onDelete(() => null); - let resource = func.__trigger.eventTrigger.resource; - expect(resource).to.eq('projects/_/instances/custom/refs/foo'); + it('should return a handler that emits events with a proper DataSnapshot', () => { + let handler = database.ref('/users/{id}').onUpdate(change => { + expect(change.after.val()).to.deep.equal({ foo: 'bar' }); + }); + + return handler({ + data: { + data: null, + delta: { foo: 'bar' }, + }, + resource: 'projects/_/instances/subdomains/refs/users', + eventType: 'providers/google.firebase.database/eventTypes/ref.update', + }); + }); }); - it('should return a handler that emits events with a proper DeltaSnapshot', () => { - let handler = database.ref('/users/{id}').onDelete(event => { - expect(event.data.val()).to.deep.equal({ foo: 'bar' }); + describe('#onDelete()', () => { + it('should return "ref.delete" as the event type', () => { + let eventType = database.ref('foo').onDelete(() => null).__trigger.eventTrigger.eventType; + expect(eventType).to.eq('providers/google.firebase.database/eventTypes/ref.delete'); }); - return handler({ - data: { - data: null, - delta: { foo: 'bar' }, - }, - resource: 'projects/_/instances/subdomains/refs/users', - } as any); - }); - }); - -}); + it('should construct a proper resource path', () => { + let resource = database.ref('foo').onDelete(() => null).__trigger.eventTrigger.resource; + expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); + }); -describe('resourceToInstanceAndPath', () => { - it('should return the correct instance and path strings', () => { - let [instance, path] = database.resourceToInstanceAndPath('projects/_/instances/foo/refs/bar'); - expect(instance).to.equal('https://foo.firebaseio.com'); - expect(path).to.equal('/bar'); - }); -}); + it('should let developers choose a database instance', () => { + let func = database.instance('custom').ref('foo').onDelete(() => null); + let resource = func.__trigger.eventTrigger.resource; + expect(resource).to.eq('projects/_/instances/custom/refs/foo'); + }); -describe('DeltaSnapshot', () => { - let subject; - const apps = new appsNamespace.Apps(fakeConfig()); - - let populate = (old: any, change: any) => { - let [instance, path] = database.resourceToInstanceAndPath('projects/_/instances/other-subdomain/refs/foo'); - subject = new database.DeltaSnapshot( - apps.admin, - apps.admin, - old, - change, - path, - instance - ); - }; - - describe('#ref: firebase.database.Reference', () => { - it('should return a ref for correct instance, not the default instance', () => { - populate({}, {}); - expect(subject.ref.toJSON()).to.equal('https://other-subdomain.firebaseio.com/foo'); + it('should return a handler that emits events with a proper DataSnapshot', () => { + let handler = database.ref('/users/{id}').onDelete(data => { + expect(data.val()).to.deep.equal({ foo: 'bar' }); + }); + + return handler({ + data: { + data: { foo: 'bar' }, + delta: null, + }, + resource: 'projects/_/instances/subdomains/refs/users', + eventType: 'providers/google.firebase.database/eventTypes/ref.delete', + }); + }); }); - }); - describe('#adminRef(): firebase.database.Reference', () => { - it('should return an adminRef for correct instance, not the default instance', () => { - populate({}, {}); - expect(subject.adminRef.toJSON()).to.equal('https://other-subdomain.firebaseio.com/foo'); - }); }); - describe('#val(): any', () => { - it('should return child values based on the child path', () => { - populate({ a: { b: 'c' } }, { a: { d: 'e' } }); - expect(subject.child('a').val()).to.deep.equal({ b: 'c', d: 'e' }); + describe('process.env.FIREBASE_CONFIG not set', () => { + it('should not throw if __trigger is not accessed', () => { + expect(() => database.ref('/path').onWrite(() => null)).to.not.throw(Error); }); - it('should return null for children past a leaf', () => { - populate({ a: 23 }, { b: 33 }); - expect(subject.child('a/b').val()).to.be.null; - expect(subject.child('b/c').val()).to.be.null; + it('should throw when trigger is accessed', () => { + expect(() => database.ref('/path').onWrite(() => null).__trigger).to.throw(Error); }); - it('should return a leaf value', () => { - populate(null, 23); - expect(subject.val()).to.eq(23); - populate({ a: 23 }, { b: 23, a: null }); - expect(subject.child('b').val()).to.eq(23); + it('should not throw when #run is called', () => { + let cf = database.ref('/path').onWrite(() => null); + expect(cf.run).to.not.throw(Error); }); + }); - it('should coerce object into array if all keys are integers', () => { - populate(null, { 0: 'a', 1: 'b', 2: { c: 'd' } }); - expect(subject.val()).to.deep.equal(['a', 'b', { c: 'd' }]); - populate(null, { 0: 'a', 2: 'b', 3: { c: 'd' } }); - expect(subject.val()).to.deep.equal(['a', , 'b', { c: 'd' }]); - populate(null, { 'foo': { 0: 'a', 1: 'b' } }); - expect(subject.val()).to.deep.equal({ foo: ['a', 'b'] }); + describe('resourceToInstanceAndPath', () => { + it('should return the correct instance and path strings', () => { + let [instance, path] = database.resourceToInstanceAndPath('projects/_/instances/foo/refs/bar'); + expect(instance).to.equal('https://foo.firebaseio.com'); + expect(path).to.equal('/bar'); }); + }); - // Regression test: zero-values (including children) were accidentally forwarded as 'null'. - it('should deal with zero-values appropriately', () => { - populate(null, 0); - expect(subject.val()).to.equal(0); - populate(null, { myKey: 0 }); - expect(subject.val()).to.deep.equal({ myKey: 0 }); - - // Null values are still reported as null. - populate({ myKey: 'foo', myOtherKey: 'bar' }, { myKey: null }); - expect(subject.val()).to.deep.equal({ myOtherKey: 'bar' }); - }); + describe('DataSnapshot', () => { + let subject; + const apps = new appsNamespace.Apps(); - // Regression test: .val() was returning array of nulls when there's a property called length (BUG#37683995) - it('should return correct values when data has "length" property', () => { - populate(null, { length: 3, foo: 'bar' }); - expect(subject.val()).to.deep.equal({ length: 3, foo: 'bar'}); - }); - }); + let populate = (data: any) => { + let [instance, path] = database.resourceToInstanceAndPath('projects/_/instances/other-subdomain/refs/foo'); + subject = new database.DataSnapshot( + data, + path, + apps.admin, + instance + ); + }; - describe('#child(): DeltaSnapshot', () => { - it('should work with multiple calls', () => { - populate(null, { a: { b: { c: 'd' } } }); - expect(subject.child('a').child('b/c').val()).to.equal('d'); + describe('#ref: firebase.database.Reference', () => { + it('should return a ref for correct instance, not the default instance', () => { + populate({}); + expect(subject.ref.toJSON()).to.equal('https://other-subdomain.firebaseio.com/foo'); + }); }); - }); - describe('#exists(): boolean', () => { - it('should be true for an object value', () => { - populate(null, { a: { b: 'c' } }); - expect(subject.child('a').exists()).to.be.true; - }); + describe('#val(): any', () => { + it('should return child values based on the child path', () => { + populate(applyChange({ a: { b: 'c' } }, { a: { d: 'e' } })); + expect(subject.child('a').val()).to.deep.equal({ b: 'c', d: 'e' }); + }); - it('should be true for a leaf value', () => { - populate(null, { a: { b: 'c' } }); - expect(subject.child('a/b').exists()).to.be.true; - }); + it('should return null for children past a leaf', () => { + populate(applyChange({ a: 23 }, { b: 33 })); + expect(subject.child('a/b').val()).to.be.null; + expect(subject.child('b/c').val()).to.be.null; + }); - it('should be false for a non-existent value', () => { - populate(null, { a: { b: 'c' } }); - expect(subject.child('d').exists()).to.be.false; - }); + it('should return a leaf value', () => { + populate(23); + expect(subject.val()).to.eq(23); + populate({ b: 23, a: null }); + expect(subject.child('b').val()).to.eq(23); + }); - it('should be false for a value pathed beyond a leaf', () => { - populate(null, { a: { b: 'c' } }); - expect(subject.child('a/b/c').exists()).to.be.false; - }); - }); + it('should coerce object into array if all keys are integers', () => { + populate({ 0: 'a', 1: 'b', 2: { c: 'd' } }); + expect(subject.val()).to.deep.equal(['a', 'b', { c: 'd' }]); + populate({ 0: 'a', 2: 'b', 3: { c: 'd' } }); + expect(subject.val()).to.deep.equal(['a', , 'b', { c: 'd' }]); + populate({ 'foo': { 0: 'a', 1: 'b' } }); + expect(subject.val()).to.deep.equal({ foo: ['a', 'b'] }); + }); - describe('#previous: DeltaSnapshot', () => { - it('should cause val() to return old data only', () => { - populate({ a: 'b' }, { a: 'c', d: 'c' }); - expect(subject.previous.child('a').val()).to.equal('b'); - }); + // Regression test: zero-values (including children) were accidentally forwarded as 'null'. + it('should deal with zero-values appropriately', () => { + populate(0); + expect(subject.val()).to.equal(0); + populate({ myKey: 0 }); + expect(subject.val()).to.deep.equal({ myKey: 0 }); - it('should return a null if the new value is present', () => { - populate(null, 23); - expect(subject.previous.val()).to.be.null; - }); - }); + // Null values are still reported as null. + populate({ myKey: null }); + expect(subject.val()).to.deep.equal({ myKey: null }); + }); - describe('#current: DeltaSnapshot', () => { - it('should cause a previous snapshot to return new data', () => { - populate({ a: 'b' }, { a: 'c', d: 'c' }); - expect(subject.previous.child('a').current.val()).to.equal('c'); + // Regression test: .val() was returning array of nulls when there's a property called length (BUG#37683995) + it('should return correct values when data has "length" property', () => { + populate({ length: 3, foo: 'bar' }); + expect(subject.val()).to.deep.equal({ length: 3, foo: 'bar'}); + }); }); - it('should return a null if the new value is null', () => { - populate(23, null); - expect(subject.previous.current.val()).to.be.null; + describe('#child(): DataSnapshot', () => { + it('should work with multiple calls', () => { + populate({ a: { b: { c: 'd' } } }); + expect(subject.child('a').child('b/c').val()).to.equal('d'); + }); }); - }); - describe('#changed(): boolean', () => { - it('should be true only when the current value has changed', () => { - populate({ a: { b: 'c' } }, { a: { d: 'e' } }); - expect(subject.child('a').changed()).to.be.true; - expect(subject.child('a/b').changed()).to.be.false; - expect(subject.child('a/d').changed()).to.be.true; - }); + describe('#exists(): boolean', () => { + it('should be true for an object value', () => { + populate({ a: { b: 'c' } }); + expect(subject.child('a').exists()).to.be.true; + }); - it('should be true when going to or from a null value', () => { - populate(null, 'foo'); - expect(subject.changed()).to.be.true; - populate('foo', null); - expect(subject.changed()).to.be.true; - }); - }); + it('should be true for a leaf value', () => { + populate({ a: { b: 'c' } }); + expect(subject.child('a/b').exists()).to.be.true; + }); - describe('#forEach(action: (a: DeltaSnapshot) => boolean): boolean', () => { - it('should iterate through child snapshots', () => { - populate({ a: 'b' }, { c: 'd' }); - let out = ''; - subject.forEach(snap => { - out += snap.val(); + it('should be false for a non-existent value', () => { + populate({ a: { b: 'c' } }); + expect(subject.child('d').exists()).to.be.false; }); - expect(out).to.equal('bd'); - }); - it('should have correct key values for child snapshots', () => { - populate({ a: 'b' }, { c: 'd' }); - let out = ''; - subject.forEach(snap => { - out += snap.key; + it('should be false for a value pathed beyond a leaf', () => { + populate({ a: { b: 'c' } }); + expect(subject.child('a/b/c').exists()).to.be.false; }); - expect(out).to.equal('ac'); }); - it('should not execute for leaf or null nodes', () => { - populate(null, 23); - let count = 0; - let counter = snap => count++; + describe('#forEach(action: (a: DataSnapshot) => boolean): boolean', () => { + it('should iterate through child snapshots', () => { + populate({ a: 'b', c: 'd' }); + let out = ''; + subject.forEach(snap => { + out += snap.val(); + }); + expect(out).to.equal('bd'); + }); - expect(subject.forEach(counter)).to.equal(false); - populate(23, null); + it('should have correct key values for child snapshots', () => { + populate({ a: 'b', c: 'd' }); + let out = ''; + subject.forEach(snap => { + out += snap.key; + }); + expect(out).to.equal('ac'); + }); - expect(subject.forEach(counter)).to.equal(false); - expect(count).to.eq(0); - }); + it('should not execute for leaf or null nodes', () => { + populate(23); + let count = 0; + let counter = snap => count++; - it('should cancel further enumeration if callback returns true', () => { - populate(null, { a: 'b', c: 'd', e: 'f', g: 'h' }); - let out = ''; - const ret = subject.forEach(snap => { - if (snap.val() === 'f') { - return true; - } - out += snap.val(); - }); - expect(out).to.equal('bd'); - expect(ret).to.equal(true); - }); + expect(subject.forEach(counter)).to.equal(false); + expect(count).to.eq(0); + }); - it('should not cancel further enumeration if callback returns a truthy value', () => { - populate(null, { a: 'b', c: 'd', e: 'f', g: 'h' }); - let out = ''; - const ret = subject.forEach(snap => { - out += snap.val(); - return 1; + it('should cancel further enumeration if callback returns true', () => { + populate({ a: 'b', c: 'd', e: 'f', g: 'h' }); + let out = ''; + const ret = subject.forEach(snap => { + if (snap.val() === 'f') { + return true; + } + out += snap.val(); + }); + expect(out).to.equal('bd'); + expect(ret).to.equal(true); }); - expect(out).to.equal('bdfh'); - expect(ret).to.equal(false); - }); - it('should not cancel further enumeration if callback does not return', () => { - populate(null, { a: 'b', c: 'd', e: 'f', g: 'h' }); - let out = ''; - const ret = subject.forEach(snap => { - out += snap.val(); + it('should not cancel further enumeration if callback returns a truthy value', () => { + populate({ a: 'b', c: 'd', e: 'f', g: 'h' }); + let out = ''; + const ret = subject.forEach(snap => { + out += snap.val(); + return 1; + }); + expect(out).to.equal('bdfh'); + expect(ret).to.equal(false); }); - expect(out).to.equal('bdfh'); - expect(ret).to.equal(false); - }); - }); - describe('#numChildren()', () => { - it('should be key count for objects', () => { - populate(null, { a: 'b', c: 'd' }); - expect(subject.numChildren()).to.eq(2); + it('should not cancel further enumeration if callback does not return', () => { + populate({ a: 'b', c: 'd', e: 'f', g: 'h' }); + let out = ''; + const ret = subject.forEach(snap => { + out += snap.val(); + }); + expect(out).to.equal('bdfh'); + expect(ret).to.equal(false); + }); }); - it('should be 0 for non-objects', () => { - populate(null, 23); - expect(subject.numChildren()).to.eq(0); - }); - }); + describe('#numChildren()', () => { + it('should be key count for objects', () => { + populate({ a: 'b', c: 'd' }); + expect(subject.numChildren()).to.eq(2); + }); - describe('#hasChild(childPath): boolean', () => { - it('should return true for a child or deep child', () => { - populate(null, { a: { b: 'c' }, d: 23 }); - expect(subject.hasChild('a/b')).to.be.true; - expect(subject.hasChild('d')).to.be.true; + it('should be 0 for non-objects', () => { + populate(23); + expect(subject.numChildren()).to.eq(0); + }); }); - it('should return false if a child is missing', () => { - populate(null, { a: 'b' }); - expect(subject.hasChild('c')).to.be.false; - expect(subject.hasChild('a/b')).to.be.false; - }); - }); + describe('#hasChild(childPath): boolean', () => { + it('should return true for a child or deep child', () => { + populate({ a: { b: 'c' }, d: 23 }); + expect(subject.hasChild('a/b')).to.be.true; + expect(subject.hasChild('d')).to.be.true; + }); - describe('#key: string', () => { - it('should return the key name', () => { - expect(subject.key).to.equal('foo'); + it('should return false if a child is missing', () => { + populate({ a: 'b' }); + expect(subject.hasChild('c')).to.be.false; + expect(subject.hasChild('a/b')).to.be.false; + }); }); - it('should return null for the root', () => { - let [instance, path] = database.resourceToInstanceAndPath('projects/_/instances/refs/refs'); - const snapshot = new database.DeltaSnapshot( - apps.admin, - apps.admin, - null, - null, - path, - instance - ); - expect(snapshot.key).to.be.null; - }); + describe('#key: string', () => { + it('should return the key name', () => { + expect(subject.key).to.equal('foo'); + }); - it('should return null for explicit root', () => { - let [instance, path] = database.resourceToInstanceAndPath('projects/_/instances/refs/refs'); - expect(new database.DeltaSnapshot( - apps.admin, - apps.admin, - null, - {}, - path, - instance - ).key).to.be.null; - }); + it('should return null for the root', () => { + let [instance, path] = database.resourceToInstanceAndPath('projects/_/instances/foo/refs/'); + const snapshot = new database.DataSnapshot( + null, + path, + apps.admin, + instance + ); + expect(snapshot.key).to.be.null; + }); - it('should work for child paths', () => { - expect(subject.child('foo/bar').key).to.equal('bar'); + it('should work for child paths', () => { + expect(subject.child('foo/bar').key).to.equal('bar'); + }); }); - }); - describe('#toJSON(): Object', () => { - it('should return the current value', () => { - populate(null, { a: 'b' }); - expect(subject.toJSON()).to.deep.equal(subject.val()); - expect(subject.previous.toJSON()).to.deep.equal(subject.previous.val()); - }); - it('should be stringifyable', () => { - populate(null, { a: 'b' }); - expect(JSON.stringify(subject)).to.deep.equal('{"a":"b"}'); + describe('#toJSON(): Object', () => { + it('should return the current value', () => { + populate({ a: 'b' }); + expect(subject.toJSON()).to.deep.equal(subject.val()); + }); + it('should be stringifyable', () => { + populate({ a: 'b' }); + expect(JSON.stringify(subject)).to.deep.equal('{"a":"b"}'); + }); }); }); }); diff --git a/spec/providers/firestore.spec.ts b/spec/providers/firestore.spec.ts index b37f6992f..86ed024bb 100644 --- a/spec/providers/firestore.spec.ts +++ b/spec/providers/firestore.spec.ts @@ -33,39 +33,31 @@ describe('Firestore Functions', () => { }; }; - beforeEach(() => { - process.env.GCLOUD_PROJECT = 'project1'; - process.env.FIREBASE_PROJECT = JSON.stringify({ - databaseUrl: 'project1@firebaseio.com', - }); - }); - - afterEach(() => { - delete process.env.GCLOUD_PROJECT; - delete process.env.FIREBASE_PROJECT; - }); - describe('document builders and event types', () => { function expectedTrigger(resource: string, eventType: string) { return { eventTrigger: { resource, - eventType: `providers/${firestore.provider}/eventTypes/${eventType}`, + eventType: `providers/cloud.firestore/eventTypes/${eventType}`, + service: 'firestore.googleapis.com', }, }; } + before(() => { + process.env.GCLOUD_PROJECT = 'project1'; + }); + + after(() => { + delete process.env.GCLOUD_PROJECT; + }); + it('should allow terse constructors', () => { let resource = 'projects/project1/databases/(default)/documents/users/{uid}'; let cloudFunction = firestore.document('users/{uid}').onWrite(() => null); expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger(resource, 'document.write')); }); - it('should throw useful error when GCLOUD_PROJECT missing', () => { - delete process.env.GCLOUD_PROJECT; - expect(() => firestore.document('users/{uid}')).to.throw(Error, 'GCLOUD_PROJECT'); - }); - it('should allow custom namespaces', () => { let resource = 'projects/project1/databases/(default)/documents@v2/users/{uid}'; let cloudFunction = firestore.namespace('v2').document('users/{uid}').onWrite(() => null); @@ -103,14 +95,29 @@ describe('Firestore Functions', () => { }); }); + describe('process.env.GCLOUD_PROJECT not set', () => { + it('should not throw if __trigger is not accessed', () => { + expect(() => firestore.document('input').onCreate(() => null)).to.not.throw(Error); + }); + + it('should throw when trigger is accessed', () => { + expect(() => firestore.document('input').onCreate(() => null).__trigger).to.throw(Error); + }); + + it('should not throw when #run is called', () => { + let cf = firestore.document('input').onCreate(() => null); + expect(cf.run).to.not.throw(Error); + }); + }); + describe('dataConstructor', () => { - function constructEvent(oldValue: object, value: object) { + function constructEvent(oldValue: object, value: object, eventType: string) { return { 'data': { 'oldValue': oldValue, 'value': value, }, - 'resource': 'projects/pid/databases/(default)/documents/collection/123', + 'context': {}, }; } @@ -137,53 +144,54 @@ describe('Firestore Functions', () => { } it('constructs appropriate fields and getters for event.data on "document.write" events', () => { - let testFunction = firestore.document('path').onWrite((event) => { - expect(event.data.data()).to.deep.equal({key1: true, key2: 123}); - expect(event.data.get('key1')).to.equal(true); - expect(event.data.previous.data()).to.deep.equal({key1: false, key2: 111}); - expect(event.data.previous.get('key1')).to.equal(false); + let testFunction = firestore.document('path').onWrite((change) => { + expect(change.before.data()).to.deep.equal({key1: false, key2: 111}); + expect(change.before.get('key1')).to.equal(false); + expect(change.after.data()).to.deep.equal({key1: true, key2: 123}); + expect(change.after.get('key1')).to.equal(true); + return true; // otherwise will get warning about returning undefined }); - let data = constructEvent(createOldValue(), createValue()); + let data = constructEvent(createOldValue(), createValue(), 'document.write'); return testFunction(data); - }); + }).timeout(5000); it('constructs appropriate fields and getters for event.data on "document.create" events', () => { - let testFunction = firestore.document('path').onCreate((event) => { - expect(event.data.data()).to.deep.equal({key1: true, key2: 123}); - expect(event.data.get('key1')).to.equal(true); - expect(event.data.previous).to.not.equal(null); - expect(event.data.previous.exists).to.be.false; + let testFunction = firestore.document('path').onCreate((data) => { + expect(data.data()).to.deep.equal({key1: true, key2: 123}); + expect(data.get('key1')).to.equal(true); + return true; // otherwise will get warning about returning undefined }); - let data = constructEvent({}, createValue()); + let data = constructEvent({}, createValue(), 'document.create'); return testFunction(data); - }); + }).timeout(5000); it('constructs appropriate fields and getters for event.data on "document.update" events', () => { - let testFunction = firestore.document('path').onUpdate((event) => { - expect(event.data.data()).to.deep.equal({key1: true, key2: 123}); - expect(event.data.get('key1')).to.equal(true); - expect(event.data.previous.data()).to.deep.equal({key1: false, key2: 111}); - expect(event.data.previous.get('key1')).to.equal(false); + let testFunction = firestore.document('path').onUpdate((change) => { + expect(change.before.data()).to.deep.equal({key1: false, key2: 111}); + expect(change.before.get('key1')).to.equal(false); + expect(change.after.data()).to.deep.equal({key1: true, key2: 123}); + expect(change.after.get('key1')).to.equal(true); + return true; // otherwise will get warning about returning undefined }); - let data = constructEvent(createOldValue(), createValue()); + let data = constructEvent(createOldValue(), createValue(), 'document.update'); return testFunction(data); - }); + }).timeout(5000); it('constructs appropriate fields and getters for event.data on "document.delete" events', () => { - let testFunction = firestore.document('path').onDelete((event) => { - expect(event.data.exists).to.equal(false); - expect(event.data.previous.data()).to.deep.equal({key1: false, key2: 111}); - expect(event.data.previous.get('key1')).to.equal(false); + let testFunction = firestore.document('path').onDelete((data) => { + expect(data.data()).to.deep.equal({key1: false, key2: 111}); + expect(data.get('key1')).to.equal(false); + return true; // otherwise will get warning about returning undefined }); - let data = constructEvent(createOldValue(), {}); + let data = constructEvent(createOldValue(), {}, 'document.delete'); return testFunction(data); - }); + }).timeout(5000); }); - describe('DeltaDocumentSnapshot', () => { + describe('SnapshotConstructor', () => { describe('#data()', () => { it('should parse int values', () => { - let snapshot = firestore.dataConstructor({ + let snapshot = firestore.snapshotConstructor({ data: { value: constructValue({'key': {'integerValue': '123'}}), }, @@ -192,7 +200,7 @@ describe('Firestore Functions', () => { }); it('should parse double values', () => { - let snapshot = firestore.dataConstructor({ + let snapshot = firestore.snapshotConstructor({ data: { value: constructValue({'key': {'doubleValue': 12.34}}), }, @@ -201,7 +209,7 @@ describe('Firestore Functions', () => { }); it('should parse null values', () => { - let snapshot = firestore.dataConstructor({ + let snapshot = firestore.snapshotConstructor({ data: { value: constructValue({'key': {'nullValue': null}}), }, @@ -210,7 +218,7 @@ describe('Firestore Functions', () => { }); it('should parse boolean values', () => { - let snapshot = firestore.dataConstructor({ + let snapshot = firestore.snapshotConstructor({ data: { value: constructValue({'key': {'booleanValue': true}}), }, @@ -219,7 +227,7 @@ describe('Firestore Functions', () => { }); it('should parse string values', () => { - let snapshot = firestore.dataConstructor({ + let snapshot = firestore.snapshotConstructor({ data: { value: constructValue({'key': {'stringValue': 'foo'}}), }, @@ -238,7 +246,7 @@ describe('Firestore Functions', () => { }, }, }); - let snapshot = firestore.dataConstructor({ + let snapshot = firestore.snapshotConstructor({ data: { value: raw }, }); expect(snapshot.data()).to.deep.equal({'key': [1, 2]}); @@ -259,7 +267,7 @@ describe('Firestore Functions', () => { }, }, }); - let snapshot = firestore.dataConstructor({ + let snapshot = firestore.snapshotConstructor({ data: { value: raw }, }); expect(snapshot.data()).to.deep.equal({'keyParent': {'key1':'val1', 'key2':'val2'}}); @@ -280,7 +288,7 @@ describe('Firestore Functions', () => { }, }, }); - let snapshot = firestore.dataConstructor({ + let snapshot = firestore.snapshotConstructor({ data: { value: raw }, }); expect(snapshot.data()).to.deep.equal({'geoPointValue': { @@ -295,7 +303,7 @@ describe('Firestore Functions', () => { 'referenceValue': 'projects/proj1/databases/(default)/documents/doc1/id', }, }); - let snapshot = firestore.dataConstructor({ + let snapshot = firestore.snapshotConstructor({ data: { value: raw }, }); expect(snapshot.data()['referenceVal'].path).to.equal('doc1/id'); @@ -307,7 +315,7 @@ describe('Firestore Functions', () => { 'timestampValue': '2017-06-13T00:58:40.349Z', }, }); - let snapshot = firestore.dataConstructor({ + let snapshot = firestore.snapshotConstructor({ data: { value: raw }, }); expect(snapshot.data()).to.deep.equal({'timestampVal': new Date('2017-06-13T00:58:40.349Z')}); @@ -319,7 +327,7 @@ describe('Firestore Functions', () => { 'timestampValue': '2017-06-13T00:58:40Z', }, }); - let snapshot = firestore.dataConstructor({ + let snapshot = firestore.snapshotConstructor({ data: { value: raw }, }); expect(snapshot.data()).to.deep.equal({'timestampVal': new Date('2017-06-13T00:58:40Z')}); @@ -333,7 +341,7 @@ describe('Firestore Functions', () => { 'bytesValue': 'Zm9vYmFy', }, }); - let snapshot = firestore.dataConstructor({ + let snapshot = firestore.snapshotConstructor({ data: { value: raw }, }); expect(snapshot.data()).to.deep.equal({'binaryVal': new Buffer('foobar')}); @@ -344,7 +352,7 @@ describe('Firestore Functions', () => { let snapshot; before(() => { - snapshot = firestore.dataConstructor({ + snapshot = firestore.snapshotConstructor({ 'data': { 'value': { 'fields': {'key': {'integerValue': '1'}}, @@ -385,7 +393,7 @@ describe('Firestore Functions', () => { describe('Handle empty and non-existent documents', () => { it('constructs non-existent DocumentSnapshot when whole document deleted', () => { - let snapshot = firestore.dataConstructor({ + let snapshot = firestore.snapshotConstructor({ 'data': { 'value': {}, // value is empty when the whole document is deleted }, @@ -396,7 +404,7 @@ describe('Firestore Functions', () => { }); it('constructs existent DocumentSnapshot with empty data when all fields of document deleted', () => { - let snapshot = firestore.dataConstructor({ + let snapshot = firestore.snapshotConstructor({ 'data': { 'value': { // value is not empty when document still exists 'createTime': '2017-06-02T18:48:58.920638Z', diff --git a/spec/providers/https.spec.ts b/spec/providers/https.spec.ts index b9c523007..d75cfb60c 100644 --- a/spec/providers/https.spec.ts +++ b/spec/providers/https.spec.ts @@ -27,7 +27,7 @@ import * as jwt from 'jsonwebtoken'; import * as mocks from '../fixtures/credential/key.json'; import * as nock from 'nock'; import * as _ from 'lodash'; -import { apps } from '../../src/apps'; +import { apps as appsNamespace } from '../../src/apps'; import { expect } from 'chai'; describe('CloudHttpsBuilder', () => { @@ -201,7 +201,7 @@ export function generateIdToken(projectId: string): string { } describe('callable.FunctionBuilder', () => { - let oldCredential: firebase.credential.Credential = undefined; + let app; before(() => { let credential = { @@ -217,12 +217,16 @@ describe('callable.FunctionBuilder', () => { }; }, }; - oldCredential = apps().admin.options.credential; - apps().admin.options.credential = credential; + app = firebase.initializeApp({ + projectId: 'aProjectId', + credential: credential, + }); + Object.defineProperty(appsNamespace(), 'admin', { get: () => app }); }); after(() => { - apps().admin.options.credential = oldCredential; + app.delete(); + delete appsNamespace.singleton; }); describe('#onCall', () => { @@ -375,7 +379,7 @@ describe('callable.FunctionBuilder', () => { it('should handle auth', async () => { const mock = mockFetchPublicKeys(); - const projectId = apps().admin.options.projectId; + const projectId = appsNamespace().admin.options.projectId; const idToken = generateIdToken(projectId); await runTest({ httpRequest: request(null, 'application/json', { diff --git a/spec/providers/pubsub.spec.ts b/spec/providers/pubsub.spec.ts index 307f38da8..e4330bff8 100644 --- a/spec/providers/pubsub.spec.ts +++ b/spec/providers/pubsub.spec.ts @@ -23,91 +23,109 @@ import * as pubsub from '../../src/providers/pubsub'; import { expect } from 'chai'; -describe('pubsub.Message', () => { - describe('#json', () => { - it('should return json decoded from base64', () => { - let message = new pubsub.Message({ - data: new Buffer('{"hello":"world"}', 'utf8').toString('base64'), +describe('Pubsub Functions', () => { + describe('pubsub.Message', () => { + describe('#json', () => { + it('should return json decoded from base64', () => { + let message = new pubsub.Message({ + data: new Buffer('{"hello":"world"}', 'utf8').toString('base64'), + }); + + expect(message.json.hello).to.equal('world'); }); - expect(message.json.hello).to.equal('world'); - }); + it('should preserve passed in json', () => { + let message = new pubsub.Message({ + data: new Buffer('{"hello":"world"}', 'utf8').toString('base64'), + json: {goodbye: 'world'}, + }); - it('should preserve passed in json', () => { - let message = new pubsub.Message({ - data: new Buffer('{"hello":"world"}', 'utf8').toString('base64'), - json: {goodbye: 'world'}, + expect(message.json.goodbye).to.equal('world'); }); - - expect(message.json.goodbye).to.equal('world'); }); - }); - describe('#toJSON', () => { - it('should be JSON stringify-able', () => { - let encoded = new Buffer('{"hello":"world"}', 'utf8').toString('base64'); - let message = new pubsub.Message({ - data: encoded, - }); + describe('#toJSON', () => { + it('should be JSON stringify-able', () => { + let encoded = new Buffer('{"hello":"world"}', 'utf8').toString('base64'); + let message = new pubsub.Message({ + data: encoded, + }); - expect(JSON.parse(JSON.stringify(message))).to.deep.equal({ - data: encoded, - attributes: {}, + expect(JSON.parse(JSON.stringify(message))).to.deep.equal({ + data: encoded, + attributes: {}, + }); }); }); }); -}); -describe('pubsub.FunctionBuilder', () => { + describe('pubsub.FunctionBuilder', () => { - before(() => { - process.env.GCLOUD_PROJECT = 'project1'; - }); + before(() => { + process.env.GCLOUD_PROJECT = 'project1'; + }); - after(() => { - delete process.env.GCLOUD_PROJECT; - }); + after(() => { + delete process.env.GCLOUD_PROJECT; + }); - describe('#onPublish', () => { - it('should return a TriggerDefinition with appropriate values', () => { - // Pick up project from process.env.GCLOUD_PROJECT - const result = pubsub.topic('toppy').onPublish(() => null); - expect(result.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'providers/cloud.pubsub/eventTypes/topic.publish', - resource: 'projects/project1/topics/toppy', - }, + describe('#onPublish', () => { + it('should return a TriggerDefinition with appropriate values', () => { + // Pick up project from process.env.GCLOUD_PROJECT + const result = pubsub.topic('toppy').onPublish(() => null); + expect(result.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'google.pubsub.topic.publish', + resource: 'projects/project1/topics/toppy', + service: 'pubsub.googleapis.com', + }, + }); }); - }); - it ('should throw with improperly formatted topics', () => { - expect(() => pubsub.topic('bad/topic/format')).to.throw(Error); - }); + it ('should throw with improperly formatted topics', () => { + expect(() => pubsub.topic('bad/topic/format')).to.throw(Error); + }); - it('should properly handle a new-style event', () => { - const raw = new Buffer('{"hello":"world"}', 'utf8').toString('base64'); - const event = { - data: { - data: raw, - attributes: { - foo: 'bar', + it('should properly handle a new-style event', () => { + const raw = new Buffer('{"hello":"world"}', 'utf8').toString('base64'); + const event = { + data: { + data: raw, + attributes: { + foo: 'bar', + }, }, - }, - }; - - const result = pubsub.topic('toppy').onPublish(ev => { - return { - raw: ev.data.data, - json: ev.data.json, - attributes: ev.data.attributes, }; - }); - return expect(result(event)).to.eventually.deep.equal({ - raw, - json: {hello: 'world'}, - attributes: {foo: 'bar'}, + const result = pubsub.topic('toppy').onPublish(data => { + return { + raw: data.data, + json: data.json, + attributes: data.attributes, + }; + }); + + return expect(result(event)).to.eventually.deep.equal({ + raw, + json: {hello: 'world'}, + attributes: {foo: 'bar'}, + }); }); }); }); + + describe('process.env.GCLOUD_PROJECT not set', () => { + it('should not throw if __trigger is not accessed', () => { + expect(() => pubsub.topic('toppy').onPublish(() => null)).to.not.throw(Error); + }); + + it('should throw when trigger is accessed', () => { + expect(() => pubsub.topic('toppy').onPublish(() => null).__trigger).to.throw(Error); + }); + + it('should not throw when #run is called', () => { + let cf = pubsub.topic('toppy').onPublish(() => null); + expect(cf.run).to.not.throw(Error); + }); + }); }); diff --git a/spec/providers/storage.spec.ts b/spec/providers/storage.spec.ts index fa74f1ecf..182789e1b 100644 --- a/spec/providers/storage.spec.ts +++ b/spec/providers/storage.spec.ts @@ -22,67 +22,256 @@ import * as storage from '../../src/providers/storage'; import { expect as expect } from 'chai'; -import { fakeConfig } from '../support/helpers'; -import { config } from '../../src/index'; -describe('storage.FunctionBuilder', () => { - before(() => { - config.singleton = fakeConfig(); - }); +describe('Storage Functions', () => { + describe('ObjectBuilder', () => { + before(() => { + process.env.FIREBASE_CONFIG = JSON.stringify({ + storageBucket: 'bucket', + }); + }); - after(() => { - delete config.singleton; - }); + after(() => { + delete process.env.FIREBASE_CONFIG; + }); - describe('#onChange', () => { - it('should return a TriggerDefinition with appropriate values', () => { - let cloudFunction = storage.bucket('bucky').object().onChange(() => null); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'providers/cloud.storage/eventTypes/object.change', - resource: 'projects/_/buckets/bucky', - }, + describe('#onArchive', () => { + it('should return a TriggerDefinition with appropriate values', () => { + let cloudFunction = storage.bucket('bucky').object().onArchive(() => null); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'google.storage.object.archive', + resource: 'projects/_/buckets/bucky', + service: 'storage.googleapis.com', + }, + }); + }); + + it('should use the default bucket when none is provided', () => { + let cloudFunction = storage.object().onArchive(() => null); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'google.storage.object.archive', + resource: 'projects/_/buckets/bucket', + service: 'storage.googleapis.com', + }, + }); + }); + + it('should allow fully qualified bucket names', () => { + let subjectQualified = new storage.ObjectBuilder(() => 'projects/_/buckets/bucky'); + let result = subjectQualified.onArchive(() => null); + expect(result.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'google.storage.object.archive', + resource: 'projects/_/buckets/bucky', + service: 'storage.googleapis.com', + }, + }); + }); + + it('should throw with improperly formatted buckets', () => { + expect(() => storage.bucket('bad/bucket/format').object().onArchive(() => null).__trigger) + .to.throw(Error); }); - }); - it('should use the default bucket when none is provided', () => { - let cloudFunction = storage.object().onChange(() => null); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'providers/cloud.storage/eventTypes/object.change', - resource: 'projects/_/buckets/bucket', - }, + it('should not mess with media links using non-literal slashes', () => { + let cloudFunction = storage.object().onArchive(data => { + return data.mediaLink; + }); + let goodMediaLinkEvent = { + data: { + mediaLink: 'https://www.googleapis.com/storage/v1/b/mybucket.appspot.com' + + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', + }, + }; + return cloudFunction(goodMediaLinkEvent).then(result => { + expect(result).equals(goodMediaLinkEvent.data.mediaLink); + }); }); }); - it('should allow fully qualified bucket names', () => { - let subjectQualified = new storage.ObjectBuilder('projects/_/buckets/bucky'); - let result = subjectQualified.onChange(() => null); - expect(result.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'providers/cloud.storage/eventTypes/object.change', - resource: 'projects/_/buckets/bucky', - }, + describe('#onDelete', () => { + it('should return a TriggerDefinition with appropriate values', () => { + let cloudFunction = storage.bucket('bucky').object().onDelete(() => null); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'google.storage.object.delete', + resource: 'projects/_/buckets/bucky', + service: 'storage.googleapis.com', + }, + }); + }); + + it('should use the default bucket when none is provided', () => { + let cloudFunction = storage.object().onDelete(() => null); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'google.storage.object.delete', + resource: 'projects/_/buckets/bucket', + service: 'storage.googleapis.com', + }, + }); + }); + + it('should allow fully qualified bucket names', () => { + let subjectQualified = new storage.ObjectBuilder(() => 'projects/_/buckets/bucky'); + let result = subjectQualified.onDelete(() => null); + expect(result.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'google.storage.object.delete', + resource: 'projects/_/buckets/bucky', + service: 'storage.googleapis.com', + }, + }); + }); + + it('should throw with improperly formatted buckets', () => { + expect(() => storage.bucket('bad/bucket/format').object().onDelete(() => null).__trigger) + .to.throw(Error); + }); + + it('should not mess with media links using non-literal slashes', () => { + let cloudFunction = storage.object().onDelete(data => { + return data.mediaLink; + }); + let goodMediaLinkEvent = { + data: { + mediaLink: 'https://www.googleapis.com/storage/v1/b/mybucket.appspot.com' + + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', + }, + }; + return cloudFunction(goodMediaLinkEvent).then(result => { + expect(result).equals(goodMediaLinkEvent.data.mediaLink); + }); }); }); - it('should throw with improperly formatted buckets', () => { - expect(() => storage.bucket('bad/bucket/format')).to.throw(Error); + describe('#onFinalize', () => { + it('should return a TriggerDefinition with appropriate values', () => { + let cloudFunction = storage.bucket('bucky').object().onFinalize(() => null); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'google.storage.object.finalize', + resource: 'projects/_/buckets/bucky', + service: 'storage.googleapis.com', + }, + }); + }); + + it('should use the default bucket when none is provided', () => { + let cloudFunction = storage.object().onFinalize(() => null); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'google.storage.object.finalize', + resource: 'projects/_/buckets/bucket', + service: 'storage.googleapis.com', + }, + }); + }); + + it('should allow fully qualified bucket names', () => { + let subjectQualified = new storage.ObjectBuilder(() => 'projects/_/buckets/bucky'); + let result = subjectQualified.onFinalize(() => null); + expect(result.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'google.storage.object.finalize', + resource: 'projects/_/buckets/bucky', + service: 'storage.googleapis.com', + }, + }); + }); + + it('should throw with improperly formatted buckets', () => { + expect(() => storage.bucket('bad/bucket/format').object().onFinalize(() => null).__trigger) + .to.throw(Error); + }); + + it('should not mess with media links using non-literal slashes', () => { + let cloudFunction = storage.object().onFinalize(data => { + return data.mediaLink; + }); + let goodMediaLinkEvent = { + data: { + mediaLink: 'https://www.googleapis.com/storage/v1/b/mybucket.appspot.com' + + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', + }, + }; + return cloudFunction(goodMediaLinkEvent).then(result => { + expect(result).equals(goodMediaLinkEvent.data.mediaLink); + }); + }); }); - it('should not mess with media links using non-literal slashes', () => { - let cloudFunction = storage.object().onChange((event) => { - return event.data.mediaLink; + describe('#onMetadataUpdate', () => { + it('should return a TriggerDefinition with appropriate values', () => { + let cloudFunction = storage.bucket('bucky').object().onMetadataUpdate(() => null); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'google.storage.object.metadataUpdate', + resource: 'projects/_/buckets/bucky', + service: 'storage.googleapis.com', + }, + }); }); - let goodMediaLinkEvent = { - data: { - mediaLink: 'https://www.googleapis.com/storage/v1/b/mybucket.appspot.com' - + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', - }, - }; - return cloudFunction(goodMediaLinkEvent).then(result => { - expect(result).equals(goodMediaLinkEvent.data.mediaLink); + + it('should use the default bucket when none is provided', () => { + let cloudFunction = storage.object().onMetadataUpdate(() => null); + expect(cloudFunction.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'google.storage.object.metadataUpdate', + resource: 'projects/_/buckets/bucket', + service: 'storage.googleapis.com', + }, + }); }); + + it('should allow fully qualified bucket names', () => { + let subjectQualified = new storage.ObjectBuilder(() => 'projects/_/buckets/bucky'); + let result = subjectQualified.onMetadataUpdate(() => null); + expect(result.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'google.storage.object.metadataUpdate', + resource: 'projects/_/buckets/bucky', + service: 'storage.googleapis.com', + }, + }); + }); + + it('should throw with improperly formatted buckets', () => { + expect(() => storage.bucket('bad/bucket/format').object().onMetadataUpdate(() => null).__trigger) + .to.throw(Error); + }); + + it('should not mess with media links using non-literal slashes', () => { + let cloudFunction = storage.object().onMetadataUpdate(data => { + return data.mediaLink; + }); + let goodMediaLinkEvent = { + data: { + mediaLink: 'https://www.googleapis.com/storage/v1/b/mybucket.appspot.com' + + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', + }, + }; + return cloudFunction(goodMediaLinkEvent).then(result => { + expect(result).equals(goodMediaLinkEvent.data.mediaLink); + }); + }); + }); + }); + + describe('process.env.FIREBASE_CONFIG not set', () => { + it('should not throw if __trigger is not accessed', () => { + expect(() => storage.object().onArchive(() => null)).to.not.throw(Error); + }); + + it('should throw when trigger is accessed', () => { + expect(() => storage.object().onArchive(() => null).__trigger).to.throw(Error); + }); + + it('should not throw when #run is called', () => { + let cf = storage.object().onArchive(() => null); + expect(cf.run).to.not.throw(Error); }); }); }); diff --git a/spec/support/helpers.ts b/spec/support/helpers.ts deleted file mode 100644 index f9dff566d..000000000 --- a/spec/support/helpers.ts +++ /dev/null @@ -1,51 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2017 Firebase -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import * as _ from 'lodash'; -import { config } from '../../src/config'; - -export function fakeConfig(data?: Object) { - return _.extend({}, data, { - firebase: { - projectId: 'aProjectId', - databaseURL: 'https://subdomain.firebaseio.com', - storageBucket: 'bucket', - credential: { - getAccessToken: () => { - return Promise.resolve({ - expires_in: 1000, - access_token: 'fake', - }); - }, - getCertificate: () => { - return { - projectId: 'aProjectId', - }; - }, - }, - }, - }); -} - -export function unsetSingleton() { - delete config.singleton; -} diff --git a/spec/utils.spec.ts b/spec/utils.spec.ts index d4cfb489d..cd195a28f 100644 --- a/spec/utils.spec.ts +++ b/spec/utils.spec.ts @@ -19,9 +19,9 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -// -import {normalizePath, pathParts, valAt, applyChange} from '../src/utils'; -import {expect} from 'chai'; + +import { normalizePath, pathParts, valAt, applyChange } from '../src/utils'; +import { expect } from 'chai'; describe ('utils', () => { describe('.normalizePath(path: string)', () => { diff --git a/src/apps.ts b/src/apps.ts index 815788079..ac3a41b94 100644 --- a/src/apps.ts +++ b/src/apps.ts @@ -22,13 +22,12 @@ import * as _ from 'lodash'; import * as firebase from 'firebase-admin'; -import {config} from './index'; -import sha1 = require('sha1'); +import { firebaseConfig } from './config'; /** @internal */ export function apps(): apps.Apps { if (typeof apps.singleton === 'undefined') { - apps.init(config()); + apps.init(); } return apps.singleton; } @@ -47,7 +46,7 @@ export namespace apps { export let singleton: apps.Apps; - export let init = (config: config.Config) => singleton = new Apps(config); + export let init = () => singleton = new Apps(); export interface AuthMode { admin: boolean; @@ -61,11 +60,9 @@ export namespace apps { /** @internal */ export class Apps { - private _config: config.Config; private _refCounter: RefCounter; - constructor(config: config.Config) { - this._config = config; + constructor() { this._refCounter = {}; } @@ -78,19 +75,6 @@ export namespace apps { } } - _appName(auth: AuthMode): string { - if (!auth || typeof auth !== 'object') { - return '__noauth__'; - } else if (auth.admin) { - return '__admin__'; - } else if (!auth.variable) { - return '__noauth__'; - } else { - // Use hash of auth variable as name of user-authenticated app - return sha1(JSON.stringify(auth.variable)) as string; - } - } - _destroyApp(appName: string) { if (!this._appAlive(appName)) { return; @@ -98,25 +82,20 @@ export namespace apps { firebase.app(appName).delete().catch(_.noop); } - retain(payload) { - let auth: AuthMode = _.get(payload, 'auth', null); + retain() { let increment = n => { return (n || 0) + 1; }; - // Increment counter for admin because function might use event.data.adminRef + // Increment counter for admin because function might use event.data.ref _.update(this._refCounter, '__admin__', increment); - // Increment counter according to auth type because function might use event.data.ref - _.update(this._refCounter, this._appName(auth), increment); } - release(payload) { - let auth: AuthMode = _.get(payload, 'auth', null); + release() { let decrement = n => { return n - 1; }; return delay(garbageCollectionInterval).then(() => { _.update(this._refCounter, '__admin__', decrement); - _.update(this._refCounter, this._appName(auth), decrement); _.forEach(this._refCounter, (count, key) => { if (count <= 0) { this._destroyApp(key); @@ -132,39 +111,8 @@ export namespace apps { return firebase.initializeApp(this.firebaseArgs, '__admin__'); } - get noauth(): firebase.app.App { - if (this._appAlive('__noauth__')) { - return firebase.app('__noauth__'); - } - const param = _.extend({}, this.firebaseArgs, { - databaseAuthVariableOverride: null, - }); - return firebase.initializeApp(param, '__noauth__'); - } - - forMode(auth: AuthMode): firebase.app.App { - if (typeof auth !== 'object') { - return this.noauth; - } - if (auth.admin) { - return this.admin; - } - if (!auth.variable) { - return this.noauth; - } - - const appName = this._appName(auth); - if (this._appAlive(appName)) { - return firebase.app(appName); - } - const param = _.extend({}, this.firebaseArgs, { - databaseAuthVariableOverride: auth.variable, - }); - return firebase.initializeApp(param, appName); - } - private get firebaseArgs() { - return _.get(this._config, 'firebase', {}); + return _.assign({}, firebaseConfig(), {credential: firebase.credential.applicationDefault()}); } } } diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 361c62cfb..739f7d6b6 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -26,17 +26,129 @@ import { Request, Response } from 'express'; export { Request, Response }; const WILDCARD_REGEX = new RegExp('{[^/{}]*}', 'g'); -/** An event to be handled in a developer's Cloud Function */ -export interface Event { - eventId?: string; - timestamp?: string; +/** Legacy wire format for an event + * @internal + */ +export interface LegacyEvent { + data: any; eventType?: string; resource?: string; + eventId?: string; + timestamp?: string; params?: { [option: string]: any }; - data: T; + auth?: apps.AuthMode; +} + +/** Wire format for an event + * @internal + */ +export interface Event { + context: { + eventId: string; + timestamp: string; + eventType: string; + resource: Resource; + }; + data: any; +} + +/** The context in which an event occurred. + * An EventContext describes: + * - The time an event occurred. + * - A unique identifier of the event. + * - The resource on which the event occurred, if applicable. + * - Authorization of the request that triggered the event, if applicable and available. + */ +export interface EventContext { + /** ID of the event */ + eventId: string; + /** Timestamp for when the event occured (ISO string) */ + timestamp: string; + /** Type of event */ + eventType: string; + /** Resource that triggered the event */ + resource: Resource; + /** Key-value pairs that represent the values of wildcards in a database reference */ + params: { [option: string]: any }; // added by SDK, but may be {} + /** Type of authentication for the triggering action, valid value are: 'ADMIN', 'USER', + * 'UNAUTHENTICATED'. Only available for database functions. + */ + authType?: 'ADMIN' | 'USER' | 'UNAUTHENTICATED'; + /** Firebase auth variable for the user whose action triggered the function. Field will be + * null for unauthenticated users, and will not exist for admin users. Only available + * for database functions. + */ + auth?: { + uid: string, + token: object, + }; +} + +/** Change describes a change of state - "before" represents the state prior + * to the event, "after" represents the state after the event. + */ +export class Change { + constructor( + public before?: T, + public after?: T, + ) {}; +} + +/** ChangeJson is the JSON format used to construct a Change object. */ +export interface ChangeJson { + /** Key-value pairs representing state of data before the change. + * If `fieldMask` is set, then only fields that changed are present in `before`. + */ + before?: any; + /** Key-value pairs representing state of data after the change. */ + after?: any; + /** Comma-separated string that represents names of field that changed. */ + fieldMask?: string; +} + +export namespace Change { + function reinterpretCast(x: any) { return x as T; } + + /** Factory method for creating a Change from a `before` object and an `after` object. */ + export function fromObjects(before: T, after: T) { + return new Change(before, after); + } + + /** Factory method for creating a Change from a JSON and an optional customizer function to be + * applied to both the `before` and the `after` fields. + */ + export function fromJSON(json: ChangeJson, customizer: (any) => T = reinterpretCast): Change { + let before = _.assign({}, json.before); + if (json.fieldMask) { + before = applyFieldMask(before, json.after, json.fieldMask); + } + return Change.fromObjects(customizer(before || {}), customizer(json.after || {})); + } /** @internal */ - auth?: apps.AuthMode; + export function applyFieldMask(sparseBefore, after, fieldMask) { + let before = _.assign({}, after); + let masks = fieldMask.split(','); + _.forEach(masks, mask => { + const val = _.get(sparseBefore, mask); + if (typeof val === 'undefined') { + _.unset(before, mask); + } else { + _.set(before, mask, val); + } + }); + return before; + } +} + +/** Resource is a standard format for defining a resource (google.rpc.context.AttributeContext.Resource). + * In Cloud Functions, it is the resource that triggered the function - such as a storage bucket. + */ +export interface Resource { + service: string; + name: string; + type?: string; + labels?: { [tag: string]: string }; } /** TriggerAnnotated is used internally by the firebase CLI to understand what type of Cloud Function to deploy. */ @@ -46,10 +158,16 @@ export interface TriggerAnnotated { eventTrigger?: { eventType: string; resource: string; + service: string; } }; } +/** A Runnable has a `run` method which directly invokes the user-defined function - useful for unit testing. */ +export interface Runnable { + run: (data: T, context?: EventContext) => PromiseLike | any; +} + /** * An HttpsFunction is both an object that exports its trigger definitions at __trigger and * can be called as a function that takes an express.js Request and Response object. @@ -60,59 +178,68 @@ export type HttpsFunction = TriggerAnnotated & ((req: Request, resp: Response) = * A CloudFunction is both an object that exports its trigger definitions at __trigger and * can be called as a function using the raw JS API for Google Cloud Functions. */ -export type CloudFunction = TriggerAnnotated & ((event: Event | Event) => PromiseLike | any); +export type CloudFunction = Runnable & TriggerAnnotated & ((input: any) => PromiseLike | any); /** @internal */ export interface MakeCloudFunctionArgs { + // TODO should remove `provider` and require a fully qualified `eventType` + // once all providers have migrated to new format. provider: string; eventType: string; - resource: string; - dataConstructor?: (raw: Event) => EventData; - handler: (event?: Event) => PromiseLike | any; - before?: (raw: Event) => void; - after?: (raw: Event) => void; -} - -function _makeParams(event: Event, triggerResource: string): { [option: string]: any } { - if (!event.resource) { // In unit testing, "resource" may not be populated for a test event. - return event.params || {}; - } - - let wildcards = triggerResource.match(WILDCARD_REGEX); - let params = {}; - if (wildcards) { - let triggerResourceParts = _.split(triggerResource, '/'); - let eventResourceParts = _.split(event.resource, '/'); - _.forEach(wildcards, wildcard => { - let wildcardNoBraces = wildcard.slice(1,-1); - - let position = _.indexOf(triggerResourceParts, wildcard); - params[wildcardNoBraces] = eventResourceParts[position]; - }); - } - - return params; + triggerResource: () => string; + service: string; + dataConstructor?: (raw: Event | LegacyEvent) => EventData; + handler: (data: EventData, context?: EventContext) => PromiseLike | any; + before?: (raw: Event | LegacyEvent) => void; + after?: (raw: Event | LegacyEvent) => void; + legacyEventType?: string; } /** @internal */ export function makeCloudFunction({ provider, eventType, - resource, - dataConstructor = (raw: Event) => raw.data, + triggerResource, + service, + dataConstructor = (raw: Event | LegacyEvent) => raw.data, handler, before = () => { return; }, after = () => { return; }, + legacyEventType, }: MakeCloudFunctionArgs): CloudFunction { - let cloudFunction: any = async (event: Event) => { + let cloudFunction: any = async (event: Event | LegacyEvent) => { + if (!_.has(event, 'data')) { + throw Error('Cloud function needs to be called with an event parameter.' + + 'If you are writing unit tests, please use the Node module firebase-functions-fake.'); + } try { before(event); - let typedEvent: Event = _.cloneDeep(event); - typedEvent.data = dataConstructor(event); - typedEvent.params = _makeParams(event, resource) || {}; + let dataOrChange = dataConstructor(event); + let context; + if (isEvent(event)) { // new event format + context = _.cloneDeep(event.context); + } else { // legacy event format + context = { + eventId: event.eventId, + timestamp: event.timestamp, + eventType: provider + '.' + eventType, + resource: { + service: service, + name: event.resource, + }, + }; + if (provider === 'google.firebase.database') { + context.authType = _detectAuthType(event); + if (context.authType !== 'ADMIN') { + context.auth = _makeAuth(event, context.authType); + } + } + } - let promise = handler(typedEvent); + context.params = _makeParams(context, triggerResource); + + let promise = handler(dataOrChange, context); if (typeof promise === 'undefined') { console.warn('Function returned undefined, expected Promise or value'); } @@ -121,13 +248,63 @@ export function makeCloudFunction({ after(event); } }; - - cloudFunction.__trigger = { - eventTrigger: { - resource, - eventType: `providers/${provider}/eventTypes/${eventType}`, + Object.defineProperty(cloudFunction, '__trigger', { + get: () => { + return { + eventTrigger: { + resource: triggerResource(), + eventType: legacyEventType || provider + '.' + eventType, + service, + }, + }; }, + }); + cloudFunction.run = handler; + return cloudFunction; +} + +function isEvent(event: Event | LegacyEvent): event is Event { + return _.has(event, 'context'); +} + +function _makeParams(context: EventContext, triggerResourceGetter: () => string): { [option: string]: any } { + if (context.params) { // In unit testing, user may directly provide `context.params`. + return context.params; + } + if (!context.resource) { // In unit testing, `resource` may be unpopulated for a test event. + return {}; + } + let triggerResource = triggerResourceGetter(); + let wildcards = triggerResource.match(WILDCARD_REGEX); + let params = {}; + if (wildcards) { + let triggerResourceParts = _.split(triggerResource, '/'); + let eventResourceParts = _.split(context.resource.name, '/'); + _.forEach(wildcards, wildcard => { + let wildcardNoBraces = wildcard.slice(1,-1); + let position = _.indexOf(triggerResourceParts, wildcard); + params[wildcardNoBraces] = eventResourceParts[position]; + }); + } + return params; +} + +function _makeAuth(event: LegacyEvent, authType: string) { + if (authType === 'UNAUTHENTICATED') { + return null; + } + return { + uid: _.get(event, 'auth.variable.uid'), + token: _.get(event, 'auth.variable.token'), }; +} - return cloudFunction; +function _detectAuthType(event: LegacyEvent) { + if (_.get(event, 'auth.admin')) { + return 'ADMIN'; + } + if (_.has(event, 'auth.variable')) { + return 'USER'; + } + return 'UNAUTHENTICATED'; } diff --git a/src/config.ts b/src/config.ts index dddaeab48..a1cfc7090 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,13 +21,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import * as _ from 'lodash'; import * as firebase from 'firebase-admin'; export function config(): config.Config { if (typeof config.singleton === 'undefined') { - const cred = firebase.credential.applicationDefault(); - init(cred); + init(); } return config.singleton; } @@ -35,38 +33,66 @@ export function config(): config.Config { export namespace config { // Config type is usable as a object (dot notation allowed), and firebase // property will also code complete. - export type Config = { [key: string]: any } & { firebase: firebase.AppOptions }; + export type Config = { [key: string]: any }; /** @internal */ export let singleton: config.Config; } -function init (credential: firebase.credential.Credential) { - let firebaseEnv = {}; - if (process.env.FIREBASE_PROJECT) { - firebaseEnv = { firebase: JSON.parse(process.env.FIREBASE_PROJECT) }; +/* @internal */ +export function firebaseConfig(): firebase.AppOptions | null { + + // The FIREBASE_PROJECT environment variable was introduced to help local emulation with `firebase-tools` 3.18 + // Unfortunately, API review decided that the name should be FIREBASE_CONFIG to avoid confusions that Firebase has + // a separate project from Google Cloud. This accepts both versions, preferring the documented name. + const env = process.env.FIREBASE_CONFIG || process.env.FIREBASE_PROJECT; + if (env) { + return JSON.parse(env); } - let merged = firebaseEnv; + // Could have Runtime Config with Firebase in it as an ENV value. try { - merged = _.merge({}, JSON.parse(process.env.CLOUD_RUNTIME_CONFIG), firebaseEnv); - } catch (e) { - try { - let path = process.env.CLOUD_RUNTIME_CONFIG || '../../../.runtimeconfig.json'; - merged = _.merge({}, require(path), firebaseEnv); - } catch (e) { - // Do nothing + const config = JSON.parse(process.env.CLOUD_RUNTIME_CONFIG); + if (config.firebase) { + return config.firebase; } + } catch (e) { + // Do nothing } - if (!hasFirebase(merged)) { - throw new Error('Firebase config variables are not available. ' + - 'Please use the latest version of the Firebase CLI to deploy this function.'); + + // Could have Runtime Config with Firebase in it as an ENV location or default. + try { + const path = process.env.CLOUD_RUNTIME_CONFIG || '../../../.runtimeconfig.json'; + const config = require(path); + if (config.firebase) { + return config.firebase; + } + } catch (e) { + // Do nothing } - _.set(merged, 'firebase.credential', credential); - config.singleton = merged; + return null; } -function hasFirebase (merged: { [key: string]: any }): merged is config.Config { - return _.has(merged, 'firebase'); +function init() { + try { + const parsed = JSON.parse(process.env.CLOUD_RUNTIME_CONFIG); + delete parsed.firebase; + config.singleton = parsed; + return; + } catch (e) { + // Do nothing + } + + try { + let path = process.env.CLOUD_RUNTIME_CONFIG || '../../../.runtimeconfig.json'; + const parsed = require(path); + delete parsed.firebase; + config.singleton = parsed; + return; + } catch (e) { + // Do nothing + } + + config.singleton = {}; } diff --git a/src/index.ts b/src/index.ts index 5cb5d8f7d..f5ebbb6d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,8 +29,29 @@ import * as firestore from './providers/firestore'; import * as https from './providers/https'; import * as pubsub from './providers/pubsub'; import * as storage from './providers/storage'; +import { firebaseConfig } from './config'; export { analytics, auth, crashlytics, database, firestore, https, pubsub, storage }; // Exported root types: export * from './config'; export * from './cloud-functions'; + +// TEMPORARY WORKAROUND (BUG 63586213): +// Until the Cloud Functions builder can publish FIREBASE_CONFIG, automatically provide it on import based on what +// we can deduce. +if (!process.env.FIREBASE_CONFIG) { + const cfg = firebaseConfig(); + if (cfg) { + process.env.FIREBASE_CONFIG = JSON.stringify(cfg); + + } else if (process.env.GCLOUD_PROJECT) { + console.warn('Warning, estimating Firebase Config based on GCLOUD_PROJECT. Intializing firebase-admin may fail'); + process.env.FIREBASE_CONFIG = JSON.stringify({ + databaseURL: `https://${process.env.GCLOUD_PROJECT}.firebaseio.com`, + storageBucket: `${process.env.GCLOUD_PROJECT}.appspot.com`, + projectId: process.env.GCLOUD_PROJECT, + }); + } else { + console.warn('Warning, FIREBASE_CONFIG environment variable is missing. Initializing firebase-admin will fail'); + } +} diff --git a/src/providers/analytics.ts b/src/providers/analytics.ts index eaec4b52f..834d2a4ab 100644 --- a/src/providers/analytics.ts +++ b/src/providers/analytics.ts @@ -20,11 +20,13 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { makeCloudFunction, CloudFunction, Event } from '../cloud-functions'; +import { makeCloudFunction, CloudFunction, Event, EventContext } from '../cloud-functions'; import * as _ from 'lodash'; /** @internal */ -export const provider = 'google.firebase.analytics'; +export const provider = 'google.analytics'; +/** @internal */ +export const service = 'app-measurement.com'; /** * Registers a Cloud Function to handle analytics events. @@ -36,8 +38,12 @@ export const provider = 'google.firebase.analytics'; * interface. */ export function event(analyticsEventType: string) { - return new AnalyticsEventBuilder( - 'projects/' + process.env.GCLOUD_PROJECT + '/events/' + analyticsEventType); + return new AnalyticsEventBuilder(() => { + if (!process.env.GCLOUD_PROJECT) { + throw new Error('process.env.GCLOUD_PROJECT is not set.'); + } + return 'projects/' + process.env.GCLOUD_PROJECT + '/events/' + analyticsEventType; + }); } /** @@ -47,7 +53,7 @@ export function event(analyticsEventType: string) { */ export class AnalyticsEventBuilder { /** @internal */ - constructor(private resource: string) { } + constructor(private triggerResource: () => string) { } /** * Event handler that fires every time a Firebase Analytics event occurs. @@ -60,19 +66,17 @@ export class AnalyticsEventBuilder { * Cloud Function you can export. */ onLog( - handler: (event: Event) => PromiseLike | any - ): CloudFunction { - const dataConstructor = (raw: Event) => { - if (raw.data instanceof AnalyticsEvent) { - return raw.data; - } + handler: (event: AnalyticsEvent, context?: EventContext) => PromiseLike | any): CloudFunction { + const dataConstructor = (raw: Event) => { return new AnalyticsEvent(raw.data); }; return makeCloudFunction({ - provider, handler, + provider, eventType: 'event.log', - resource: this.resource, + service, + legacyEventType: `providers/google.firebase.analytics/eventTypes/event.log`, + triggerResource: this.triggerResource, dataConstructor, }); } diff --git a/src/providers/auth.ts b/src/providers/auth.ts index b95f103ec..4bfc631ea 100644 --- a/src/providers/auth.ts +++ b/src/providers/auth.ts @@ -20,38 +20,30 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { makeCloudFunction, CloudFunction, Event } from '../cloud-functions'; +import { makeCloudFunction, CloudFunction, EventContext, LegacyEvent } from '../cloud-functions'; import * as firebase from 'firebase-admin'; import * as _ from 'lodash'; /** @internal */ -export const provider = 'firebase.auth'; +export const provider = 'google.firebase.auth'; +/** @internal */ +export const service = 'firebaseauth.googleapis.com'; /** Handle events in the Firebase Auth user lifecycle. */ export function user() { - return new UserBuilder('projects/' + process.env.GCLOUD_PROJECT); + return new UserBuilder(() => { + if (!process.env.GCLOUD_PROJECT) { + throw new Error('process.env.GCLOUD_PROJECT is not set.'); + } + return 'projects/' + process.env.GCLOUD_PROJECT; + }); } export class UserRecordMetadata implements firebase.auth.UserMetadata { constructor(public creationTime: string, public lastSignInTime: string) { }; - // Remove in v1.0.0 - /** @internal */ - get lastSignedInAt() { - console.warn('WARNING: "lastSignedInAt" will be removed in firebase-functions v1.0.0. ' + - 'Please start using "lastSignInTime", which is an ISO string.'); - return new Date(this.lastSignInTime); - } - - // Remove in v1.0.0 - /** @internal */ - get createdAt() { - console.warn('WARNING: "createdAt" will be removed in firebase-functions v1.0.0. ' + - 'Please start using "creationTime", which is an ISO string.'); - return new Date(this.creationTime); - } - + /** Returns a plain JavaScript object with the properties of UserRecordMetadata. */ toJSON() { return { creationTime: this.creationTime, @@ -62,47 +54,35 @@ export class UserRecordMetadata implements firebase.auth.UserMetadata { /** Builder used to create Cloud Functions for Firebase Auth user lifecycle events. */ export class UserBuilder { - private static dataConstructor(raw: any): firebase.auth.UserRecord { - // The UserRecord returned here is an interface. The firebase-admin/auth/user-record module - // also has a class of the same name, which is one implementation of the interface. - - // Transform payload to firebase-admin v5.0.0 format - let data = _.clone(raw.data); - if (data.metadata) { - let meta = data.metadata; - data.metadata = new UserRecordMetadata( - meta.createdAt || meta.creationTime, - meta.lastSignedInAt || meta.lastSignInTime, - ); - } - - return data; + private static dataConstructor(raw: LegacyEvent): firebase.auth.UserRecord { + return userRecordConstructor(raw.data); } /** @internal */ - constructor(private resource: string) { } + constructor(private triggerResource: () => string) { } /** Respond to the creation of a Firebase Auth user. */ - onCreate( - handler: (event: Event) => PromiseLike | any - ): CloudFunction { - return makeCloudFunction({ - provider, handler, - resource: this.resource, - eventType: 'user.create', - dataConstructor: UserBuilder.dataConstructor, - }); + onCreate(handler: (user: UserRecord, context?: EventContext) => PromiseLike | any): CloudFunction { + return this.onOperation(handler, 'user.create'); } /** Respond to the deletion of a Firebase Auth user. */ - onDelete( - handler: (event: Event) => PromiseLike | any + onDelete(handler: (user: UserRecord, context?: EventContext) => PromiseLike | any): CloudFunction { + return this.onOperation(handler, 'user.delete'); + } + + private onOperation( + handler: (user: UserRecord, context?: EventContext) => PromiseLike | any, + eventType: string ): CloudFunction { return makeCloudFunction({ - provider, handler, - resource: this.resource, - eventType: 'user.delete', + handler, + provider, + eventType, + service, + triggerResource: this.triggerResource, dataConstructor: UserBuilder.dataConstructor, + legacyEventType: `providers/firebase.auth/eventTypes/${eventType}`, }); } } @@ -112,3 +92,46 @@ export class UserBuilder { * SDK. */ export type UserRecord = firebase.auth.UserRecord; + +export function userRecordConstructor(wireData: Object): firebase.auth.UserRecord { + // Falsey values from the wire format proto get lost when converted to JSON, this adds them back. + let falseyValues = { + email: null, + emailVerified: false, + displayName: null, + photoURL: null, + phoneNumber: null, + disabled: false, + providerData: [], + customClaims: {}, + passwordSalt: null, + passwordHash: null, + tokensValidAfterTime: null, + }; + let record = _.assign({}, falseyValues, wireData); + + let meta = _.get(record, 'metadata'); + if (meta) { + _.set(record, 'metadata', new UserRecordMetadata( + // Transform payload to firebase-admin v5.0.0 format because wire format is different (BUG 63167395) + meta.createdAt || meta.creationTime, + meta.lastSignedInAt || meta.lastSignInTime, + )); + } else { + _.set(record, 'metadata', new UserRecordMetadata(null, null)); + } + _.forEach(record.providerData, entry => { + _.set(entry, 'toJSON', () => { + return entry; + }); + }); + _.set(record, 'toJSON', () => { + const json: any = _.pick(record, ['uid', 'email', 'emailVerified', 'displayName', + 'photoURL', 'phoneNumber', 'disabled', 'passwordHash', 'passwordSalt', 'tokensValidAfterTime']); + json.metadata = _.get(record, 'metadata').toJSON(); + json.customClaims = _.cloneDeep(record.customClaims); + json.providerData = _.map(record.providerData, entry => entry.toJSON()); + return json; + }); + return record as firebase.auth.UserRecord; +} diff --git a/src/providers/crashlytics.ts b/src/providers/crashlytics.ts index e06911bf7..31643af39 100644 --- a/src/providers/crashlytics.ts +++ b/src/providers/crashlytics.ts @@ -20,51 +20,62 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { makeCloudFunction, CloudFunction, Event } from '../cloud-functions'; +import { makeCloudFunction, CloudFunction, EventContext } from '../cloud-functions'; /** @internal */ -export const provider = 'firebase.crashlytics'; +export const provider = 'google.firebase.crashlytics'; +/** @internal */ +export const service = 'fabric.io'; /** * Handle events related to Crashlytics issues. An issue in Crashlytics is an * aggregation of crashes which have a shared root cause. */ export function issue() { - return new IssueBuilder('projects/' + process.env.GCLOUD_PROJECT); + return new IssueBuilder(() => { + if (!process.env.GCLOUD_PROJECT) { + throw new Error('process.env.GCLOUD_PROJECT is not set.'); + } + return 'projects/' + process.env.GCLOUD_PROJECT; + }); } /** Builder used to create Cloud Functions for Crashlytics issue events. */ export class IssueBuilder { /** @internal */ - constructor(private resource: string) { } + constructor(private triggerResource: () => string) { } + + /** @internal */ + onNewDetected(handler: any): Error { + throw new Error('"onNewDetected" is now deprecated, please use "onNew"'); + } /** Handle Crashlytics New Issue events. */ - onNewDetected(handler: (event: Event) => PromiseLike | any - ): CloudFunction { - return makeCloudFunction({ - provider, handler, - resource: this.resource, - eventType: 'issue.new', - }); + onNew(handler: (issue: Issue, context?: EventContext) => PromiseLike | any): CloudFunction { + return this.onEvent(handler, 'issue.new'); } /** Handle Crashlytics Regressed Issue events. */ - onRegressed(handler: (event: Event) => PromiseLike | any - ): CloudFunction { - return makeCloudFunction({ - provider, handler, - resource: this.resource, - eventType: 'issue.regressed', - }); + onRegressed(handler: (issue: Issue, context?: EventContext) => PromiseLike | any): CloudFunction { + return this.onEvent(handler, 'issue.regressed'); } /** Handle Crashlytics Velocity Alert events. */ - onVelocityAlert(handler: (event: Event) => PromiseLike | any + onVelocityAlert(handler: (issue: Issue, context?: EventContext) => PromiseLike | any): CloudFunction { + return this.onEvent(handler, 'issue.velocityAlert'); + } + + private onEvent( + handler: (issue: Issue, context?: EventContext) => PromiseLike | any, + eventType: string ): CloudFunction { return makeCloudFunction({ - provider, handler, - resource: this.resource, - eventType: 'issue.velocityAlert', + handler, + provider, + eventType, + service, + legacyEventType: `providers/firebase.crashlytics/eventTypes/${eventType}`, + triggerResource: this.triggerResource, }); } } @@ -72,7 +83,7 @@ export class IssueBuilder { /** * Interface representing a Crashlytics issue event that was logged for a specific issue. */ -export class Issue { +export interface Issue { /** Fabric Issue ID. */ issueId: string; @@ -92,7 +103,7 @@ export class Issue { velocityAlert?: VelocityAlert; } -export class VelocityAlert { +export interface VelocityAlert { /** The percentage of sessions which have been impacted by this issue. Example: .04 */ crashPercentage: number; diff --git a/src/providers/database.ts b/src/providers/database.ts index 11aef3207..9450c35e1 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -22,13 +22,15 @@ import * as _ from 'lodash'; import { apps } from '../apps'; -import { Event, CloudFunction, makeCloudFunction } from '../cloud-functions'; -import { normalizePath, applyChange, pathParts, valAt, joinPath } from '../utils'; +import { LegacyEvent, CloudFunction, makeCloudFunction, EventContext, Change } from '../cloud-functions'; +import { normalizePath, applyChange, pathParts, joinPath } from '../utils'; import * as firebase from 'firebase-admin'; -import { config } from '../index'; +import { firebaseConfig } from '../config'; /** @internal */ export const provider = 'google.firebase.database'; +/** @internal */ +export const service = 'firebaseio.com'; // NOTE(inlined): Should we relax this a bit to allow staging or alternate implementations of our API? const databaseURLRegex = new RegExp('https://([^.]+).firebaseio.com'); @@ -46,7 +48,7 @@ export class InstanceBuilder { ref(path: string): RefBuilder { const normalized = normalizePath(path); - return new RefBuilder(apps(), `projects/_/instances/${this.instance}/refs/${normalized}`); + return new RefBuilder(apps(), () => `projects/_/instances/${this.instance}/refs/${normalized}`); } } @@ -74,73 +76,117 @@ export class InstanceBuilder { * triggered the Cloud Function. */ export function ref(path: string): RefBuilder { - const normalized = normalizePath(path); - const databaseURL = config().firebase.databaseURL; - if (!databaseURL) { - throw new Error('Missing expected config value firebase.databaseURL, ' + - 'config is actually' + JSON.stringify(config())); - } - const match = databaseURL.match(databaseURLRegex); - if (!match) { - throw new Error('Invalid value for config firebase.databaseURL: ' + databaseURL); - } - const subdomain = match[1]; - let resource = `projects/_/instances/${subdomain}/refs/${normalized}`; - return new RefBuilder(apps(), resource); + const resourceGetter = () => { + const normalized = normalizePath(path); + const databaseURL = firebaseConfig().databaseURL; + if (!databaseURL) { + throw new Error('Missing expected firebase config value databaseURL, ' + + 'config is actually' + JSON.stringify(firebaseConfig()) + + '\n If you are unit testing, please set process.env.FIREBASE_CONFIG'); + } + const match = databaseURL.match(databaseURLRegex); + if (!match) { + throw new Error('Invalid value for config firebase.databaseURL: ' + databaseURL); + } + const subdomain = match[1]; + return `projects/_/instances/${subdomain}/refs/${normalized}`; + }; + + return new RefBuilder(apps(), resourceGetter); } /** Builder used to create Cloud Functions for Firebase Realtime Database References. */ export class RefBuilder { /** @internal */ - constructor(private apps: apps.Apps, private resource: string) { } + constructor(private apps: apps.Apps, private triggerResource: () => string) { } /** Respond to any write that affects a ref. */ - onWrite(handler: (event: Event) => PromiseLike | any): CloudFunction { - return this.onOperation(handler, 'ref.write'); - } - - /** Respond to new data on a ref. */ - onCreate(handler: (event: Event) => PromiseLike | any): CloudFunction { - return this.onOperation(handler, 'ref.create'); + onWrite(handler: ( + change: Change, + context?: EventContext) => PromiseLike | any, + ): CloudFunction> { + return this.onOperation(handler, 'ref.write', this.changeConstructor); } /** Respond to update on a ref. */ - onUpdate(handler: (event: Event) => PromiseLike | any): CloudFunction { - return this.onOperation(handler, 'ref.update'); + onUpdate(handler: ( + change: Change, + context?: EventContext) => PromiseLike | any, + ): CloudFunction> { + return this.onOperation(handler, 'ref.update', this.changeConstructor); } - /** Respond to all data being deleted from a ref. */ - onDelete(handler: (event: Event) => PromiseLike | any): CloudFunction { - return this.onOperation(handler, 'ref.delete'); + /** Respond to new data on a ref. */ + onCreate(handler: ( + snapshot: DataSnapshot, + context?: EventContext) => PromiseLike | any, + ): CloudFunction { + let dataConstructor = (raw: LegacyEvent) => { + let [dbInstance, path] = resourceToInstanceAndPath(raw.resource); + return new DataSnapshot( + raw.data.delta, + path, + this.apps.admin, + dbInstance + ); + }; + return this.onOperation(handler, 'ref.create', dataConstructor); } - private onOperation( - handler: (event: Event) => PromiseLike | any, - eventType: string): CloudFunction { - - const dataConstructor = (raw: Event) => { - if (raw.data instanceof DeltaSnapshot) { - return raw.data; - } + /** Respond to all data being deleted from a ref. */ + onDelete(handler: ( + snapshot: DataSnapshot, + context?: EventContext) => PromiseLike | any, + ): CloudFunction { + let dataConstructor = (raw: LegacyEvent) => { let [dbInstance, path] = resourceToInstanceAndPath(raw.resource); - return new DeltaSnapshot( - this.apps.forMode(raw.auth), - this.apps.admin, + return new DataSnapshot( raw.data.data, - raw.data.delta, path, + this.apps.admin, dbInstance ); }; + return this.onOperation(handler, 'ref.delete', dataConstructor); + } + + private onOperation( + handler: (data: T, context?: EventContext) => PromiseLike | any, + eventType: string, + dataConstructor): CloudFunction { + return makeCloudFunction({ - provider, handler, - eventType: eventType, - resource: this.resource, - dataConstructor, - before: (event) => this.apps.retain(event), - after: (event) => this.apps.release(event), + handler, + provider, + service, + eventType, + legacyEventType: `providers/${provider}/eventTypes/${eventType}`, + triggerResource: this.triggerResource, + dataConstructor: dataConstructor, + before: (event) => this.apps.retain(), + after: (event) => this.apps.release(), }); } + + private changeConstructor = (raw: LegacyEvent): Change => { + let [dbInstance, path] = resourceToInstanceAndPath(raw.resource); + let before = new DataSnapshot( + raw.data.data, + path, + this.apps.admin, + dbInstance + ); + let after = new DataSnapshot( + applyChange(raw.data.data, raw.data.delta), + path, + this.apps.admin, + dbInstance + ); + return { + before: before, + after: after, + }; + }; } /* Utility function to extract database reference from resource string */ @@ -160,47 +206,43 @@ export function resourceToInstanceAndPath(resource) { return [dbInstance, path]; } -export class DeltaSnapshot { - private _adminRef: firebase.database.Reference; +export class DataSnapshot { + public instance: string; private _ref: firebase.database.Reference; private _path: string; private _data: any; - private _delta: any; - private _newData: any; - private _childPath: string; - private _isPrevious: boolean; constructor( - private app: firebase.app.App, - private adminApp: firebase.app.App, data: any, - delta: any, path?: string, // path will be undefined for the database root - public instance?: string, + private app?: firebase.app.App, + instance?: string, ) { - if (delta !== undefined) { - this._path = path; - this._data = data; - this._delta = delta; - this._newData = applyChange(this._data, this._delta); + if (instance) { // SDK always supplies instance, but user's unit tests may not + this.instance = instance; + } else if (app) { + this.instance = app.options.databaseURL; + } else if (process.env.GCLOUD_PROJECT) { + this.instance = 'https://' + process.env.GCLOUD_PROJECT + '.firebaseio.com'; } + + this._path = path; + this._data = data; } + /** Ref returns a reference to the database with full admin access. */ get ref(): firebase.database.Reference { + if (!this.app) { // may be unpopulated in user's unit tests + throw new Error('Please supply a Firebase app in the constructor for DataSnapshot' + + ' in order to use the .ref method.'); + } if (!this._ref) { this._ref = this.app.database(this.instance).ref(this._fullPath()); } return this._ref; } - get adminRef(): firebase.database.Reference { - if (!this._adminRef) { - this._adminRef = this.adminApp.database(this.instance).ref(this._fullPath()); - } - return this._adminRef; - } - get key(): string { let last = _.last(pathParts(this._fullPath())); return (!last || last === '') ? null : last; @@ -208,7 +250,7 @@ export class DeltaSnapshot { val(): any { let parts = pathParts(this._childPath); - let source = this._isPrevious ? this._data : this._newData; + let source = this._data; let node = _.cloneDeep(parts.length ? _.get(source, parts, null) : source); return this._checkAndConvertToArray(node); } @@ -225,26 +267,14 @@ export class DeltaSnapshot { return !_.isNull(this.val()); } - child(childPath: string): DeltaSnapshot { + child(childPath: string): DataSnapshot { if (!childPath) { return this; } - return this._dup(this._isPrevious, childPath); + return this._dup(childPath); } - get previous(): DeltaSnapshot { - return this._isPrevious ? this : this._dup(true); - } - - get current(): DeltaSnapshot { - return this._isPrevious ? this._dup(false) : this; - } - - changed(): boolean { - return valAt(this._delta, this._childPath) !== undefined; - } - - forEach(action: (a: DeltaSnapshot) => boolean): boolean { + forEach(action: (a: DataSnapshot) => boolean): boolean { let val = this.val(); if (_.isPlainObject(val)) { return _.some(val, (value, key: string) => action(this.child(key)) === true); @@ -311,14 +341,9 @@ export class DeltaSnapshot { return obj; } - private _dup(previous: boolean, childPath?: string): DeltaSnapshot { - let dup = new DeltaSnapshot(this.app, this.adminApp, undefined, undefined, undefined, this.instance); - [dup._path, dup._data, dup._delta, dup._childPath, dup._newData] = - [this._path, this._data, this._delta, this._childPath, this._newData]; - - if (previous) { - dup._isPrevious = true; - } + private _dup(childPath?: string): DataSnapshot { + let dup = new DataSnapshot(this._data, undefined, this.app, this.instance); + [dup._path, dup._childPath] = [this._path, this._childPath]; if (childPath) { dup._childPath = joinPath(dup._childPath, childPath); diff --git a/src/providers/firestore.ts b/src/providers/firestore.ts index e534c0b4a..640b250b8 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -24,23 +24,28 @@ import { posix } from 'path'; import * as _ from 'lodash'; import * as firebase from 'firebase-admin'; import { apps } from '../apps'; -import { makeCloudFunction, CloudFunction, Event } from '../cloud-functions'; +import { makeCloudFunction, CloudFunction, LegacyEvent, Change, + EventContext } from '../cloud-functions'; import { dateToTimestampProto } from '../encoder'; /** @internal */ -export const provider = 'cloud.firestore'; +export const provider = 'google.firestore'; +/** @internal */ +export const service = 'firestore.googleapis.com'; +export type DocumentSnapshot = firebase.firestore.DocumentSnapshot; /** @internal */ export const defaultDatabase = '(default)'; let firestoreInstance; +/** @internal */ +// Multiple databases are not yet supported by Firestore. export function database(database: string = defaultDatabase) { - if (!process.env.GCLOUD_PROJECT) { - throw new Error('Environment variable GCLOUD_PROJECT is not set.'); - } - return new DatabaseBuilder(posix.join('projects', process.env.GCLOUD_PROJECT, 'databases', database)); + return new DatabaseBuilder(database); } +/** @internal */ +// Multiple databases are not yet supported by Firestore. export function namespace(namespace: string) { return database().namespace(namespace); } @@ -51,116 +56,127 @@ export function document(path: string) { export class DatabaseBuilder { /** @internal */ - constructor(private resource: string) { } + constructor(private database: string) { } namespace(namespace: string) { - return new NamespaceBuilder(`${posix.join(this.resource, 'documents')}@${namespace}`); + return new NamespaceBuilder(this.database, namespace); } document(path: string) { - return (new NamespaceBuilder(posix.join(this.resource, 'documents'))).document(path); + return new NamespaceBuilder(this.database).document(path); } } export class NamespaceBuilder { /** @internal */ - constructor(private resource: string) { } + constructor(private database: string, private namespace?: string) { } document(path: string) { - return new DocumentBuilder(posix.join(this.resource, path)); + return new DocumentBuilder(() => { + if (!process.env.GCLOUD_PROJECT) { + throw new Error('process.env.GCLOUD_PROJECT is not set.'); + } + let database = posix.join('projects', process.env.GCLOUD_PROJECT, 'databases', this.database); + return posix.join( + database, + this.namespace ? `documents@${this.namespace}` : 'documents', + path); + }); } } -export interface DeltaDocumentSnapshot { - exists: Boolean; - ref: firebase.firestore.DocumentReference; - id: string; - createTime?: string; - updateTime?: string; - readTime?: string; - previous: DeltaDocumentSnapshot; - data: () => any; - get: (key: string) => any; -}; - -function isDeltaDocumentSnapshot(data: any): data is DeltaDocumentSnapshot { - return 'exists' in data; -}; - -function getValueProto(event, valueFieldName) { - let data = event.data; +function _getValueProto(data: any, resource: string, valueFieldName: string) { if (_.isEmpty(_.get(data, valueFieldName))) { // Firestore#snapshot_ takes resource string instead of proto for a non-existent snapshot - return event.resource; + return resource; } let proto = { fields: _.get(data, [valueFieldName, 'fields'], {}), createTime: dateToTimestampProto(_.get(data, [valueFieldName, 'createTime'])), updateTime: dateToTimestampProto(_.get(data, [valueFieldName, 'updateTime'])), - name: _.get(data, [valueFieldName, 'name'], event.resource), + name: _.get(data, [valueFieldName, 'name'], resource), }; return proto; }; /** @internal */ -export function dataConstructor(raw: Event) { - if (isDeltaDocumentSnapshot(raw.data)) { - return raw.data; - } +export function snapshotConstructor(event: LegacyEvent): DocumentSnapshot { if (!firestoreInstance) { firestoreInstance = firebase.firestore(apps().admin); } - let valueProto = getValueProto(raw, 'value'); - let readTime = dateToTimestampProto(_.get(raw.data, 'value.readTime')); - let snapshot = firestoreInstance.snapshot_(valueProto, readTime, 'json') as DeltaDocumentSnapshot; - Object.defineProperty(snapshot, 'previous', { - get: () => { - let oldValueProto = getValueProto(raw, 'oldValue'); - let oldReadTime = dateToTimestampProto(_.get(raw.data, 'oldValue.readTime')); - return firestoreInstance.snapshot_(oldValueProto, oldReadTime, 'json') as DeltaDocumentSnapshot; - }, - }); - return snapshot; + let valueProto = _getValueProto(event.data, event.resource, 'value'); + let readTime = dateToTimestampProto(_.get(event, 'data.value.readTime')); + return firestoreInstance.snapshot_(valueProto, readTime, 'json'); }; +/** @internal */ +// TODO remove this function when wire format changes to new format +export function beforeSnapshotConstructor(event: LegacyEvent): DocumentSnapshot { + if (!firestoreInstance) { + firestoreInstance = firebase.firestore(apps().admin); + } + let oldValueProto = _getValueProto(event.data, event.resource, 'oldValue'); + let oldReadTime = dateToTimestampProto(_.get(event, 'data.oldValue.readTime')); + return firestoreInstance.snapshot_(oldValueProto, oldReadTime, 'json'); +} + +function changeConstructor(raw: LegacyEvent) { + return Change.fromObjects( + beforeSnapshotConstructor(raw), + snapshotConstructor(raw) + ); +} + export class DocumentBuilder { /** @internal */ - constructor(private resource: string) { + constructor(private triggerResource: () => string) { // TODO what validation do we want to do here? } /** Respond to all document writes (creates, updates, or deletes). */ - onWrite(handler: (event: Event) => PromiseLike | - any): CloudFunction { - return this.onOperation(handler, 'document.write'); - } + onWrite(handler: ( + change: Change, + context?: EventContext) => PromiseLike | any, + ): CloudFunction> { + return this.onOperation(handler, 'document.write', changeConstructor); + }; - /** Respond only to document creations. */ - onCreate(handler: (event: Event) => PromiseLike | - any): CloudFunction { - return this.onOperation(handler, 'document.create'); + /** Respond only to document updates. */ + onUpdate(handler: ( + change: Change, + context?: EventContext) => PromiseLike | any, + ): CloudFunction> { + return this.onOperation(handler, 'document.update', changeConstructor); } - /** Respond only to document updates. */ - onUpdate(handler: (event: Event) => PromiseLike | - any): CloudFunction { - return this.onOperation(handler, 'document.update'); + /** Respond only to document creations. */ + onCreate(handler: ( + snapshot: DocumentSnapshot, + context?: EventContext) => PromiseLike | any, + ): CloudFunction { + return this.onOperation(handler, 'document.create', snapshotConstructor); } /** Respond only to document deletions. */ - onDelete(handler: (event: Event) => PromiseLike | - any): CloudFunction { - return this.onOperation(handler, 'document.delete'); + onDelete(handler: ( + snapshot: DocumentSnapshot, + context?: EventContext) => PromiseLike | any, + ): CloudFunction { + return this.onOperation(handler, 'document.delete', beforeSnapshotConstructor); } - private onOperation( - handler: (event: Event) => PromiseLike | any, - eventType: string): CloudFunction { - return makeCloudFunction({ - provider, handler, - resource: this.resource, - eventType: eventType, - dataConstructor, - }); + private onOperation( + handler: (data: T, context?: EventContext) => PromiseLike | any, + eventType: string, + dataConstructor): CloudFunction { + return makeCloudFunction({ + handler, + provider, + eventType, + service, + triggerResource: this.triggerResource, + legacyEventType: `providers/cloud.firestore/eventTypes/${eventType}`, + dataConstructor, + }); } } diff --git a/src/providers/pubsub.ts b/src/providers/pubsub.ts index efcc85517..c11e816e1 100644 --- a/src/providers/pubsub.ts +++ b/src/providers/pubsub.ts @@ -20,10 +20,12 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import {Event, CloudFunction, makeCloudFunction} from '../cloud-functions'; +import { CloudFunction, makeCloudFunction, EventContext } from '../cloud-functions'; /** @internal */ -export const provider = 'cloud.pubsub'; +export const provider = 'google.pubsub'; +/** @internal */ +export const service = 'pubsub.googleapis.com'; /** Handle events on a Cloud Pub/Sub topic. */ export function topic(topic: string): TopicBuilder { @@ -31,22 +33,29 @@ export function topic(topic: string): TopicBuilder { throw new Error('Topic name may not have a /'); } - return new TopicBuilder(`projects/${process.env.GCLOUD_PROJECT}/topics/${topic}`); + return new TopicBuilder(() => { + if (!process.env.GCLOUD_PROJECT) { + throw new Error('process.env.GCLOUD_PROJECT is not set.'); + } + return `projects/${process.env.GCLOUD_PROJECT}/topics/${topic}`; + }); } /** Builder used to create Cloud Functions for Google Pub/Sub topics. */ export class TopicBuilder { /** @internal */ - constructor(private resource: string) { } + constructor(private triggerResource: () => string) { } /** Handle a Pub/Sub message that was published to a Cloud Pub/Sub topic */ - onPublish(handler: (event: Event) => PromiseLike | any): CloudFunction { + onPublish(handler: (message: Message, context?: EventContext) => PromiseLike | any): CloudFunction { return makeCloudFunction({ - provider, handler, - resource: this.resource, + handler, + provider, + service, + triggerResource: this.triggerResource, eventType: 'topic.publish', - dataConstructor: (raw) => raw.data instanceof Message ? raw.data : new Message(raw.data), + dataConstructor: (raw) => new Message(raw.data), }); } } diff --git a/src/providers/storage.ts b/src/providers/storage.ts index 60cbbeef5..f806889b1 100644 --- a/src/providers/storage.ts +++ b/src/providers/storage.ts @@ -20,66 +20,118 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { Event, CloudFunction, makeCloudFunction } from '../cloud-functions'; -import { config } from '../index'; +import { CloudFunction, EventContext, makeCloudFunction } from '../cloud-functions'; +import { firebaseConfig } from '../config'; /** @internal */ -export const provider = 'cloud.storage'; +export const provider = 'google.storage'; +/** @internal */ +export const service = 'storage.googleapis.com'; /** * The optional bucket function allows you to choose which buckets' events to handle. * This step can be bypassed by calling object() directly, which will use the bucket that * the Firebase SDK for Cloud Storage uses. */ -export function bucket(bucket: string): BucketBuilder { - if (!/^[a-z\d][a-z\d\\._-]{1,230}[a-z\d]$/.test(bucket)) { - throw new Error('Invalid bucket name ${bucket}'); - } - return new BucketBuilder(`projects/_/buckets/${bucket}`); +export function bucket(bucket?: string): BucketBuilder { + const resourceGetter = () => { + bucket = bucket || firebaseConfig().storageBucket; + if (!bucket) { + throw new Error('Missing bucket name. If you are unit testing, please provide a bucket name' + + ' through `functions.storage.bucket(bucketName)`, or set process.env.FIREBASE_CONFIG.'); + } + if (!/^[a-z\d][a-z\d\\._-]{1,230}[a-z\d]$/.test(bucket)) { + throw new Error('Invalid bucket name ${bucket}'); + } + return `projects/_/buckets/${bucket}`; + }; + return new BucketBuilder(resourceGetter); } export function object(): ObjectBuilder { - return bucket(config().firebase.storageBucket).object(); + return bucket().object(); } export class BucketBuilder { /** @internal */ - constructor(private resource) { } + constructor(private triggerResource: () => string) { } /** Handle events for objects in this bucket. */ object() { - return new ObjectBuilder(this.resource); + return new ObjectBuilder(this.triggerResource); } } export class ObjectBuilder { /** @internal */ - constructor(private resource) { } + constructor(private triggerResource: () => string) { } - /** - * Handle any change to any object. - */ - onChange(handler: (event: Event) => PromiseLike | any): CloudFunction { - return makeCloudFunction( - { provider, handler: handler, resource: this.resource, eventType: 'object.change' }); + /** @internal */ + onChange(handler: any): Error { + throw new Error('"onChange" is now deprecated, please use "onArchive", "onDelete", ' + + '"onFinalize", or "onMetadataUpdate".'); + } + + /** Respond to archiving of an object, this is only for buckets that enabled object versioning. */ + onArchive(handler: ( + object: ObjectMetadata, + context?: EventContext) => PromiseLike | any, + ): CloudFunction { + return this.onOperation(handler, 'object.archive'); + } + + /** Respond to the deletion of an object (not to archiving, if object versioning is enabled). */ + onDelete(handler: ( + object: ObjectMetadata, + context?: EventContext) => PromiseLike | any, + ): CloudFunction { + return this.onOperation(handler, 'object.delete'); + } + + /** Respond to the successful creation of an object. */ + onFinalize(handler: ( + object: ObjectMetadata, + context?: EventContext) => PromiseLike | any, + ): CloudFunction { + return this.onOperation(handler, 'object.finalize'); + } + + /** Respond to metadata updates of existing objects. */ + onMetadataUpdate(handler: ( + object: ObjectMetadata, + context?: EventContext) => PromiseLike | any, + ): CloudFunction { + return this.onOperation(handler, 'object.metadataUpdate'); + } + + private onOperation( + handler: (object: ObjectMetadata, context?: EventContext) => PromiseLike | any, + eventType: string): CloudFunction { + return makeCloudFunction({ + handler, + provider, + service, + eventType, + triggerResource: this.triggerResource, + }); } } export interface ObjectMetadata { kind: string; id: string; - resourceState: string; + bucket: string; + storageClass: string; + size: string; + timeCreated: string; + updated: string; selfLink?: string; name?: string; - bucket: string; - generation?: number; - metageneration?: number; + generation?: string; contentType?: string; - timeCreated?: string; - updated?: string; + metageneration?: string; timeDeleted?: string; - storageClass?: string; - size?: number; + timeStorageClassUpdated?: string; md5Hash?: string; mediaLink?: string; contentEncoding?: string; @@ -89,8 +141,33 @@ export interface ObjectMetadata { metadata?: { [key: string]: string; }; + acl?: [ + { + kind?: string, + id?: string, + selfLink?: string, + bucket?: string, + object?: string, + generation?: string, + entity?: string, + role?: string, + email?: string, + entityId?: string, + domain?: string, + projectTeam?: { + projectNumber?: string, + team?: string + }, + etag?: string + } + ]; + owner?: { + entity?: string, + entityId?: string + }; crc32c?: string; - componentCount?: number; + componentCount?: string; + etag?: string; customerEncryption?: { encryptionAlgorithm?: string, keySha256?: string, diff --git a/tsconfig.json b/tsconfig.json index 7be92e55a..04fc40239 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "lib": ["es6", "es2015.promise"], + "lib": ["es6"], "module": "commonjs", "noImplicitAny": false, "outDir": ".tmp", diff --git a/tsconfig.release.json b/tsconfig.release.json index 1e8b54f29..b114d4697 100644 --- a/tsconfig.release.json +++ b/tsconfig.release.json @@ -1,7 +1,7 @@ { "compilerOptions": { "declaration": true, - "lib": ["es6", "es2015.promise"], + "lib": ["es6"], "module": "commonjs", "noImplicitAny": false, "outDir": "lib", diff --git a/upgrade-warning b/upgrade-warning new file mode 100644 index 000000000..9f2ecc220 --- /dev/null +++ b/upgrade-warning @@ -0,0 +1,15 @@ +#!/usr/bin/env node + +'use strict'; + +const message = ` +======== WARNING! ======== + +This upgrade of firebase-functions contains breaking changes if you are upgrading from a version below v1.0.0. + +To see a complete list of these breaking changes, please go to: + +https://firebase.google.com/docs/functions/beta-v1-diff +`; + +console.log(message); From 76bee818059962facaa503d2de50dd5d2b72d2de Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Tue, 3 Apr 2018 08:30:59 -0700 Subject: [PATCH 008/705] Changelog for v1.0.0 (#201) --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index e69de29bb..8848e31de 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +breaking - v1 release of `firebase-functions` contains several breaking changes. Please see migration guide at https://firebase.google.com/docs/functions/beta-v1-diff. From 5d3ce09d677530ccc9c0a718ee6bba222389bad0 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Tue, 3 Apr 2018 08:44:02 -0700 Subject: [PATCH 009/705] Revise changelog --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 8848e31de..27acd926d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +1 @@ -breaking - v1 release of `firebase-functions` contains several breaking changes. Please see migration guide at https://firebase.google.com/docs/functions/beta-v1-diff. +important - v1 release of `firebase-functions` contains several breaking changes. Please see migration guide at https://firebase.google.com/docs/functions/beta-v1-diff. From 4263cd50180aa9a808bf1ac6790dccd3bf1d2d26 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Tue, 3 Apr 2018 15:45:36 +0000 Subject: [PATCH 010/705] [firebase-release] Updated SDK for Cloud Functions to 1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ff884b2a7..f22b1dbf6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "0.9.1", + "version": "1.0.0", "description": "Firebase SDK for Cloud Functions", "main": "lib/index.js", "scripts": { From 0a0aea9fca234b12e182f4971fc6e4621bc4bff2 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Tue, 3 Apr 2018 15:45:45 +0000 Subject: [PATCH 011/705] [firebase-release] Removed change log and reset repo after 1.0.0 release --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 27acd926d..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +0,0 @@ -important - v1 release of `firebase-functions` contains several breaking changes. Please see migration guide at https://firebase.google.com/docs/functions/beta-v1-diff. From 92889e561af9287f787aeb71a194f16b52b93a7c Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Fri, 6 Apr 2018 10:47:42 -0700 Subject: [PATCH 012/705] Update firebase-admin to v5.12.0 (#219) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f22b1dbf6..2f5468790 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@types/sinon": "^1.16.29", "chai": "^3.5.0", "chai-as-promised": "^5.2.0", - "firebase-admin": "~5.10.0", + "firebase-admin": "~5.12.0", "istanbul": "^0.4.2", "mocha": "^2.4.5", "mock-require": "^2.0.1", @@ -49,7 +49,7 @@ "typescript": "^2.0.3" }, "peerDependencies": { - "firebase-admin": "~5.11.0" + "firebase-admin": "~5.12.0" }, "dependencies": { "@types/cors": "^2.8.1", From 5dd8aac224674a2bf1309e02576bfcaf942d2327 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Fri, 6 Apr 2018 11:29:45 -0700 Subject: [PATCH 013/705] Changelog for v1.0.1 (#221) --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index e69de29bb..d2c998da6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +changed - Update firebase-admin peer dependency to v5.12.0 From bc7f7aa1f621bbf76376bd477e30d04d8e8548ec Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Fri, 6 Apr 2018 18:31:25 +0000 Subject: [PATCH 014/705] [firebase-release] Updated SDK for Cloud Functions to 1.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f5468790..40a7e54e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "1.0.0", + "version": "1.0.1", "description": "Firebase SDK for Cloud Functions", "main": "lib/index.js", "scripts": { From d26ec60274ba3c46b6b26a84cbc366e3b448ff5b Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Fri, 6 Apr 2018 18:31:34 +0000 Subject: [PATCH 015/705] [firebase-release] Removed change log and reset repo after 1.0.1 release --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index d2c998da6..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +0,0 @@ -changed - Update firebase-admin peer dependency to v5.12.0 From bcb006f0965338fa5a6d9fb34389515727196ca6 Mon Sep 17 00:00:00 2001 From: Daniela Yassuda Date: Tue, 10 Apr 2018 00:51:08 +0200 Subject: [PATCH 016/705] Disallow implicit any typings (noImplicitAny: true) (#215) --- spec/apps.spec.ts | 2 +- spec/cloud-functions.spec.ts | 11 ++++++----- spec/providers/auth.spec.ts | 17 +++++++++-------- spec/providers/database.spec.ts | 14 +++++++------- spec/providers/firestore.spec.ts | 4 ++-- spec/providers/https.spec.ts | 4 ++-- spec/providers/storage.spec.ts | 8 ++++---- spec/utils.spec.ts | 2 +- src/apps.ts | 4 ++-- src/cloud-functions.ts | 8 ++++---- src/encoder.ts | 2 +- src/providers/analytics.ts | 4 ++-- src/providers/auth.ts | 2 +- src/providers/database.ts | 12 ++++++------ src/providers/firestore.ts | 6 +++--- src/providers/https.ts | 4 ++-- src/utils.ts | 2 +- tsconfig.json | 2 +- 18 files changed, 55 insertions(+), 53 deletions(-) diff --git a/spec/apps.spec.ts b/spec/apps.spec.ts index 6b3d3e822..20c654cd0 100644 --- a/spec/apps.spec.ts +++ b/spec/apps.spec.ts @@ -42,7 +42,7 @@ describe('apps', () => { }); describe('retain/release', () => { - let clock; + let clock: sinon.SinonFakeTimers; beforeEach(() => { clock = sinon.useFakeTimers(); diff --git a/spec/cloud-functions.spec.ts b/spec/cloud-functions.spec.ts index 9f0e9a30b..7566ea494 100644 --- a/spec/cloud-functions.spec.ts +++ b/spec/cloud-functions.spec.ts @@ -22,7 +22,8 @@ import * as _ from 'lodash'; import { expect } from 'chai'; -import { Event, LegacyEvent, makeCloudFunction, MakeCloudFunctionArgs, Change } from '../src/cloud-functions'; +import { Event, EventContext, LegacyEvent, + makeCloudFunction, MakeCloudFunctionArgs, Change } from '../src/cloud-functions'; describe('makeCloudFunction', () => { const cloudFunctionArgs: MakeCloudFunctionArgs = { @@ -45,7 +46,7 @@ describe('makeCloudFunction', () => { }); it('should construct the right context for legacy event format', () => { - let args: any = _.assign({}, cloudFunctionArgs, {handler: (data, context) => context}); + let args: any = _.assign({}, cloudFunctionArgs, {handler: (data: any, context: EventContext) => context}); let cf = makeCloudFunction(args); let test: LegacyEvent = { eventId: '00000', @@ -68,7 +69,7 @@ describe('makeCloudFunction', () => { }); it('should construct the right context for new event format', () => { - let args: any = _.assign({}, cloudFunctionArgs, { handler: (data, context) => context }); + let args: any = _.assign({}, cloudFunctionArgs, { handler: (data: any, context: EventContext) => context }); let cf = makeCloudFunction(args); let test: Event = { context: { @@ -277,9 +278,9 @@ describe('Change', () => { }); it('should apply the customizer function to `before` and `after`', () => { - function customizer(input) { + function customizer(input: any) { _.set(input, 'another', 'value'); - return input; + return input as T; } let created = Change.fromJSON( { diff --git a/spec/providers/auth.spec.ts b/spec/providers/auth.spec.ts index 5ab40bd5a..989f4cf22 100644 --- a/spec/providers/auth.spec.ts +++ b/spec/providers/auth.spec.ts @@ -23,6 +23,7 @@ import * as auth from '../../src/providers/auth'; import { expect } from 'chai'; import * as firebase from 'firebase-admin'; +import { CloudFunction } from '../../src'; describe('Auth Functions', () => { describe('AuthBuilder', () => { @@ -63,9 +64,9 @@ describe('Auth Functions', () => { }); describe('#_dataConstructor', () => { - let cloudFunctionCreate; - let cloudFunctionDelete; - let event; + let cloudFunctionCreate: CloudFunction; + let cloudFunctionDelete: CloudFunction; + let event: any; before(() => { cloudFunctionCreate = auth.user().onCreate((data: firebase.auth.UserRecord) => data); @@ -82,11 +83,11 @@ describe('Auth Functions', () => { it('should transform wire format for UserRecord into v5.0.0 format', () => { return Promise.all([ - cloudFunctionCreate(event).then(data => { + cloudFunctionCreate(event).then((data: any) => { expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); }), - cloudFunctionDelete(event).then(data => { + cloudFunctionDelete(event).then((data: any) => { expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); }), @@ -104,11 +105,11 @@ describe('Auth Functions', () => { }; return Promise.all([ - cloudFunctionCreate(newEvent).then(data => { + cloudFunctionCreate(newEvent).then((data: any) => { expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); }), - cloudFunctionDelete(newEvent).then(data => { + cloudFunctionDelete(newEvent).then((data: any) => { expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); }), @@ -141,7 +142,7 @@ describe('Auth Functions', () => { }); it('will not interfere with fields that are in raw wire data', () => { - const raw = { + const raw: any = { uid: '123', email: 'email@gmail.com', emailVerified: true, diff --git a/spec/providers/database.spec.ts b/spec/providers/database.spec.ts index bebdac29a..d0cc20f9e 100644 --- a/spec/providers/database.spec.ts +++ b/spec/providers/database.spec.ts @@ -200,7 +200,7 @@ describe('Database Functions', () => { }); describe('DataSnapshot', () => { - let subject; + let subject: any; const apps = new appsNamespace.Apps(); let populate = (data: any) => { @@ -300,7 +300,7 @@ describe('Database Functions', () => { it('should iterate through child snapshots', () => { populate({ a: 'b', c: 'd' }); let out = ''; - subject.forEach(snap => { + subject.forEach((snap: any) => { out += snap.val(); }); expect(out).to.equal('bd'); @@ -309,7 +309,7 @@ describe('Database Functions', () => { it('should have correct key values for child snapshots', () => { populate({ a: 'b', c: 'd' }); let out = ''; - subject.forEach(snap => { + subject.forEach((snap: any) => { out += snap.key; }); expect(out).to.equal('ac'); @@ -318,7 +318,7 @@ describe('Database Functions', () => { it('should not execute for leaf or null nodes', () => { populate(23); let count = 0; - let counter = snap => count++; + let counter = (snap: any) => count++; expect(subject.forEach(counter)).to.equal(false); expect(count).to.eq(0); @@ -327,7 +327,7 @@ describe('Database Functions', () => { it('should cancel further enumeration if callback returns true', () => { populate({ a: 'b', c: 'd', e: 'f', g: 'h' }); let out = ''; - const ret = subject.forEach(snap => { + const ret = subject.forEach((snap: any) => { if (snap.val() === 'f') { return true; } @@ -340,7 +340,7 @@ describe('Database Functions', () => { it('should not cancel further enumeration if callback returns a truthy value', () => { populate({ a: 'b', c: 'd', e: 'f', g: 'h' }); let out = ''; - const ret = subject.forEach(snap => { + const ret = subject.forEach((snap: any) => { out += snap.val(); return 1; }); @@ -351,7 +351,7 @@ describe('Database Functions', () => { it('should not cancel further enumeration if callback does not return', () => { populate({ a: 'b', c: 'd', e: 'f', g: 'h' }); let out = ''; - const ret = subject.forEach(snap => { + const ret = subject.forEach((snap: any) => { out += snap.val(); }); expect(out).to.equal('bdfh'); diff --git a/spec/providers/firestore.spec.ts b/spec/providers/firestore.spec.ts index 86ed024bb..a8a44c1fa 100644 --- a/spec/providers/firestore.spec.ts +++ b/spec/providers/firestore.spec.ts @@ -24,7 +24,7 @@ import * as firestore from '../../src/providers/firestore'; import { expect } from 'chai'; describe('Firestore Functions', () => { - let constructValue = (fields) => { + let constructValue = (fields: any) => { return { 'fields': fields, 'name': 'projects/pid/databases/(default)/documents/collection/123', @@ -349,7 +349,7 @@ describe('Firestore Functions', () => { }); describe('Other DocumentSnapshot methods', () => { - let snapshot; + let snapshot: FirebaseFirestore.DocumentSnapshot; before(() => { snapshot = firestore.snapshotConstructor({ diff --git a/spec/providers/https.spec.ts b/spec/providers/https.spec.ts index d75cfb60c..c8c7bd239 100644 --- a/spec/providers/https.spec.ts +++ b/spec/providers/https.spec.ts @@ -87,7 +87,7 @@ function runHandler(handler: express.Handler, request: express.Request): Promise } // Headers are only set by the cors handler. - public setHeader(name, value: string) { + public setHeader(name: string, value: string) { this.headers[name] = value; } @@ -201,7 +201,7 @@ export function generateIdToken(projectId: string): string { } describe('callable.FunctionBuilder', () => { - let app; + let app: firebase.app.App; before(() => { let credential = { diff --git a/spec/providers/storage.spec.ts b/spec/providers/storage.spec.ts index 182789e1b..c6946082f 100644 --- a/spec/providers/storage.spec.ts +++ b/spec/providers/storage.spec.ts @@ -85,7 +85,7 @@ describe('Storage Functions', () => { + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', }, }; - return cloudFunction(goodMediaLinkEvent).then(result => { + return cloudFunction(goodMediaLinkEvent).then((result: any) => { expect(result).equals(goodMediaLinkEvent.data.mediaLink); }); }); @@ -141,7 +141,7 @@ describe('Storage Functions', () => { + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', }, }; - return cloudFunction(goodMediaLinkEvent).then(result => { + return cloudFunction(goodMediaLinkEvent).then((result: any) => { expect(result).equals(goodMediaLinkEvent.data.mediaLink); }); }); @@ -197,7 +197,7 @@ describe('Storage Functions', () => { + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', }, }; - return cloudFunction(goodMediaLinkEvent).then(result => { + return cloudFunction(goodMediaLinkEvent).then((result: any) => { expect(result).equals(goodMediaLinkEvent.data.mediaLink); }); }); @@ -253,7 +253,7 @@ describe('Storage Functions', () => { + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', }, }; - return cloudFunction(goodMediaLinkEvent).then(result => { + return cloudFunction(goodMediaLinkEvent).then((result: any) => { expect(result).equals(goodMediaLinkEvent.data.mediaLink); }); }); diff --git a/spec/utils.spec.ts b/spec/utils.spec.ts index cd195a28f..1a2ad54c1 100644 --- a/spec/utils.spec.ts +++ b/spec/utils.spec.ts @@ -72,7 +72,7 @@ describe ('utils', () => { it('should return the merged value of two objects', () => { let from = {a: {b: 'foo', c: 23, d: 444}, d: {e: 42}}; - let to = {a: {b: 'bar', c: null}, d: null, e: {f: 'g'}}; + let to: any = {a: {b: 'bar', c: null}, d: null, e: {f: 'g'}}; let result = {a: {b: 'bar', d: 444}, e: {f: 'g'}}; expect(applyChange(from, to)).to.deep.equal(result); }); diff --git a/src/apps.ts b/src/apps.ts index ac3a41b94..37d7febbc 100644 --- a/src/apps.ts +++ b/src/apps.ts @@ -83,7 +83,7 @@ export namespace apps { } retain() { - let increment = n => { + let increment = (n?: number) => { return (n || 0) + 1; }; // Increment counter for admin because function might use event.data.ref @@ -91,7 +91,7 @@ export namespace apps { } release() { - let decrement = n => { + let decrement = (n: number) => { return n - 1; }; return delay(garbageCollectionInterval).then(() => { diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 739f7d6b6..cc5305055 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -117,7 +117,7 @@ export namespace Change { /** Factory method for creating a Change from a JSON and an optional customizer function to be * applied to both the `before` and the `after` fields. */ - export function fromJSON(json: ChangeJson, customizer: (any) => T = reinterpretCast): Change { + export function fromJSON(json: ChangeJson, customizer: (x: any) => T = reinterpretCast): Change { let before = _.assign({}, json.before); if (json.fieldMask) { before = applyFieldMask(before, json.after, json.fieldMask); @@ -126,7 +126,7 @@ export namespace Change { } /** @internal */ - export function applyFieldMask(sparseBefore, after, fieldMask) { + export function applyFieldMask(sparseBefore: any, after: any, fieldMask: string) { let before = _.assign({}, after); let masks = fieldMask.split(','); _.forEach(masks, mask => { @@ -216,7 +216,7 @@ export function makeCloudFunction({ before(event); let dataOrChange = dataConstructor(event); - let context; + let context: any; if (isEvent(event)) { // new event format context = _.cloneDeep(event.context); } else { // legacy event format @@ -276,7 +276,7 @@ function _makeParams(context: EventContext, triggerResourceGetter: () => string) } let triggerResource = triggerResourceGetter(); let wildcards = triggerResource.match(WILDCARD_REGEX); - let params = {}; + let params: { [option: string]: any } = {}; if (wildcards) { let triggerResourceParts = _.split(triggerResource, '/'); let eventResourceParts = _.split(context.resource.name, '/'); diff --git a/src/encoder.ts b/src/encoder.ts index 59d3cb223..dfa010a0b 100644 --- a/src/encoder.ts +++ b/src/encoder.ts @@ -20,7 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -export function dateToTimestampProto(timeString) { +export function dateToTimestampProto(timeString?: string) { if (typeof timeString === 'undefined') { return; } diff --git a/src/providers/analytics.ts b/src/providers/analytics.ts index 834d2a4ab..77e5b693d 100644 --- a/src/providers/analytics.ts +++ b/src/providers/analytics.ts @@ -346,13 +346,13 @@ export class ExportBundleInfo { } function copyFieldTo( - from: any, to: T, fromField: string, toField: K, transform: (any) => T[K] = _.identity): void { + from: any, to: T, fromField: string, toField: K, transform: (val: any) => T[K] = _.identity): void { if (from[fromField] !== undefined) { to[toField] = transform(from[fromField]); } } -function copyField(from: any, to: T, field: K, transform: (any) => T[K] = _.identity): void { +function copyField(from: any, to: T, field: K, transform: (val: any) => T[K] = _.identity): void { copyFieldTo(from, to, field, field, transform); } diff --git a/src/providers/auth.ts b/src/providers/auth.ts index 4bfc631ea..c8b95c432 100644 --- a/src/providers/auth.ts +++ b/src/providers/auth.ts @@ -95,7 +95,7 @@ export type UserRecord = firebase.auth.UserRecord; export function userRecordConstructor(wireData: Object): firebase.auth.UserRecord { // Falsey values from the wire format proto get lost when converted to JSON, this adds them back. - let falseyValues = { + let falseyValues: any = { email: null, emailVerified: false, displayName: null, diff --git a/src/providers/database.ts b/src/providers/database.ts index 9450c35e1..db42e9a9a 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -22,7 +22,7 @@ import * as _ from 'lodash'; import { apps } from '../apps'; -import { LegacyEvent, CloudFunction, makeCloudFunction, EventContext, Change } from '../cloud-functions'; +import { LegacyEvent, CloudFunction, makeCloudFunction, Event, EventContext, Change } from '../cloud-functions'; import { normalizePath, applyChange, pathParts, joinPath } from '../utils'; import * as firebase from 'firebase-admin'; import { firebaseConfig } from '../config'; @@ -153,7 +153,7 @@ export class RefBuilder { private onOperation( handler: (data: T, context?: EventContext) => PromiseLike | any, eventType: string, - dataConstructor): CloudFunction { + dataConstructor: (raw: Event | LegacyEvent) => any): CloudFunction { return makeCloudFunction({ handler, @@ -191,7 +191,7 @@ export class RefBuilder { /* Utility function to extract database reference from resource string */ /** @internal */ -export function resourceToInstanceAndPath(resource) { +export function resourceToInstanceAndPath(resource: string) { let resourceRegex = `projects/([^/]+)/instances/([^/]+)/refs(/.+)?`; let match = resource.match(new RegExp(resourceRegex)); if (!match) { @@ -305,14 +305,14 @@ export class DataSnapshot { } /* Recursive function to check if keys are numeric & convert node object to array if they are */ - private _checkAndConvertToArray(node): any { + private _checkAndConvertToArray(node: any): any { if (node === null || typeof node === 'undefined') { return null; } if (typeof node !== 'object') { return node; } - let obj = {}; + let obj: any = {}; let numKeys = 0; let maxKey = 0; let allIntegerKeys = true; @@ -331,7 +331,7 @@ export class DataSnapshot { if (allIntegerKeys && maxKey < 2 * numKeys) { // convert to array. - let array = []; + let array: any = []; _.forOwn(obj, (val, key) => { array[key] = val; }); diff --git a/src/providers/firestore.ts b/src/providers/firestore.ts index 640b250b8..e0db9c3dd 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -25,7 +25,7 @@ import * as _ from 'lodash'; import * as firebase from 'firebase-admin'; import { apps } from '../apps'; import { makeCloudFunction, CloudFunction, LegacyEvent, Change, - EventContext } from '../cloud-functions'; + Event, EventContext } from '../cloud-functions'; import { dateToTimestampProto } from '../encoder'; /** @internal */ @@ -36,7 +36,7 @@ export type DocumentSnapshot = firebase.firestore.DocumentSnapshot; /** @internal */ export const defaultDatabase = '(default)'; -let firestoreInstance; +let firestoreInstance: any; /** @internal */ // Multiple databases are not yet supported by Firestore. @@ -168,7 +168,7 @@ export class DocumentBuilder { private onOperation( handler: (data: T, context?: EventContext) => PromiseLike | any, eventType: string, - dataConstructor): CloudFunction { + dataConstructor: (raw: Event | LegacyEvent) => any): CloudFunction { return makeCloudFunction({ handler, provider, diff --git a/src/providers/https.ts b/src/providers/https.ts index 2930910a5..3e44efd0e 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -29,7 +29,7 @@ import * as cors from 'cors'; export function onRequest(handler: (req: express.Request, resp: express.Response) => void): HttpsFunction { // lets us add __trigger without altering handler: - let cloudFunction: any = (req, res) => { handler(req, res); }; + let cloudFunction: any = (req: express.Request, res: express.Response) => { handler(req, res); }; cloudFunction.__trigger = {httpsTrigger: {}}; return cloudFunction; @@ -321,7 +321,7 @@ export function encode(data: any): any { * This is exposed only for testing. */ /** @internal */ -export function decode(data: any) { +export function decode(data: any): any { if (data === null) { return data; } diff --git a/src/utils.ts b/src/utils.ts index 114a8e7e5..522dadb9f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -49,7 +49,7 @@ export function applyChange(src: any, dest: any) { return pruneNulls(_.merge({}, src, dest)); } -export function pruneNulls(obj: Object) { +export function pruneNulls(obj: any) { for (let key in obj) { if (obj[key] === null) { delete obj[key]; diff --git a/tsconfig.json b/tsconfig.json index 04fc40239..573a354fe 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "lib": ["es6"], "module": "commonjs", - "noImplicitAny": false, + "noImplicitAny": true, "outDir": ".tmp", "sourceMap": true, "target": "es6", From d44f345ee5e5766b3eaa49f74d637f62dff92d81 Mon Sep 17 00:00:00 2001 From: Slawek Walkowski Date: Tue, 24 Apr 2018 20:09:33 +0200 Subject: [PATCH 017/705] Update firebase-admin in integration tests to v5.12.0 (#229) --- integration_test/functions/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_test/functions/package.json b/integration_test/functions/package.json index 5e021fc7c..866f84c6b 100644 --- a/integration_test/functions/package.json +++ b/integration_test/functions/package.json @@ -8,7 +8,7 @@ "@google-cloud/pubsub": "^0.6.0", "@types/lodash": "^4.14.41", "firebase": "^4.9.1", - "firebase-admin": "~5.10.0", + "firebase-admin": "~5.12.0", "firebase-functions": "./firebase-functions.tgz", "lodash": "^4.17.2" }, From 71a41d7204fbdbed77c3efae1728b9aadfd6c4e1 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Tue, 24 Apr 2018 14:16:48 -0700 Subject: [PATCH 018/705] Update jsonwebtoken to v8.2.1 (#231) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 40a7e54e3..c62a8d7a6 100644 --- a/package.json +++ b/package.json @@ -54,11 +54,11 @@ "dependencies": { "@types/cors": "^2.8.1", "@types/express": "^4.11.1", - "@types/jsonwebtoken": "^7.1.32", + "@types/jsonwebtoken": "^7.2.6", "@types/lodash": "^4.14.34", "cors": "^2.8.4", "express": "^4.16.2", - "jsonwebtoken": "^7.1.9", + "jsonwebtoken": "^8.2.1", "lodash": "^4.6.1" }, "engines": { From 9cc9069c9e71e282b621fdee46e62e7069b1f7f3 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Tue, 24 Apr 2018 14:22:42 -0700 Subject: [PATCH 019/705] Changelog for v1.0.2 (#233) --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index e69de29bb..31356b44c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +fixed - Fixed bug where developers writing functions in TypeScript had to disable `noImplicitAny` in their tsconfig.json in order to avoid compiler errors. From 21eed822dd60b7261501a7e999980730ffc3b888 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Tue, 24 Apr 2018 21:27:01 +0000 Subject: [PATCH 020/705] [firebase-release] Updated SDK for Cloud Functions to 1.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c62a8d7a6..5826d0b72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "1.0.1", + "version": "1.0.2", "description": "Firebase SDK for Cloud Functions", "main": "lib/index.js", "scripts": { From 950a9df3d81a31a6dcc8f9b782fd1fe48619f867 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Tue, 24 Apr 2018 21:27:23 +0000 Subject: [PATCH 021/705] [firebase-release] Removed change log and reset repo after 1.0.2 release --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 31356b44c..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +0,0 @@ -fixed - Fixed bug where developers writing functions in TypeScript had to disable `noImplicitAny` in their tsconfig.json in order to avoid compiler errors. From 427ae7a7da167c0c64413e7c0b5ed3f567ed5782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Natan=20S=C4=85gol?= Date: Wed, 16 May 2018 17:44:51 +0000 Subject: [PATCH 022/705] Change `context` parameter in handlers to be not optional (#232) --- integration_test/functions/src/testing.ts | 4 ++-- src/cloud-functions.ts | 4 ++-- src/providers/analytics.ts | 2 +- src/providers/auth.ts | 6 +++--- src/providers/crashlytics.ts | 8 ++++---- src/providers/database.ts | 10 +++++----- src/providers/firestore.ts | 10 +++++----- src/providers/pubsub.ts | 2 +- src/providers/storage.ts | 10 +++++----- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/integration_test/functions/src/testing.ts b/integration_test/functions/src/testing.ts index 765dfacf0..73d3855f1 100644 --- a/integration_test/functions/src/testing.ts +++ b/integration_test/functions/src/testing.ts @@ -2,7 +2,7 @@ import * as firebase from 'firebase-admin'; import * as _ from 'lodash'; import { EventContext } from 'firebase-functions'; -export type TestCase = (data: T, context?: EventContext) => any +export type TestCase = (data: T, context: EventContext) => any export type TestCaseMap = { [key: string]: TestCase }; export class TestSuite { @@ -19,7 +19,7 @@ export class TestSuite { return this; } - run(testId: string, data: T, context?: EventContext): Promise { + run(testId: string, data: T, context: EventContext): Promise { let running: Array> = []; for (let testName in this.tests) { if (!this.tests.hasOwnProperty(testName)) { continue; } diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index cc5305055..8db2f15ea 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -165,7 +165,7 @@ export interface TriggerAnnotated { /** A Runnable has a `run` method which directly invokes the user-defined function - useful for unit testing. */ export interface Runnable { - run: (data: T, context?: EventContext) => PromiseLike | any; + run: (data: T, context: EventContext) => PromiseLike | any; } /** @@ -189,7 +189,7 @@ export interface MakeCloudFunctionArgs { triggerResource: () => string; service: string; dataConstructor?: (raw: Event | LegacyEvent) => EventData; - handler: (data: EventData, context?: EventContext) => PromiseLike | any; + handler: (data: EventData, context: EventContext) => PromiseLike | any; before?: (raw: Event | LegacyEvent) => void; after?: (raw: Event | LegacyEvent) => void; legacyEventType?: string; diff --git a/src/providers/analytics.ts b/src/providers/analytics.ts index 77e5b693d..322b0d6e7 100644 --- a/src/providers/analytics.ts +++ b/src/providers/analytics.ts @@ -66,7 +66,7 @@ export class AnalyticsEventBuilder { * Cloud Function you can export. */ onLog( - handler: (event: AnalyticsEvent, context?: EventContext) => PromiseLike | any): CloudFunction { + handler: (event: AnalyticsEvent, context: EventContext) => PromiseLike | any): CloudFunction { const dataConstructor = (raw: Event) => { return new AnalyticsEvent(raw.data); }; diff --git a/src/providers/auth.ts b/src/providers/auth.ts index c8b95c432..813a69ab9 100644 --- a/src/providers/auth.ts +++ b/src/providers/auth.ts @@ -62,17 +62,17 @@ export class UserBuilder { constructor(private triggerResource: () => string) { } /** Respond to the creation of a Firebase Auth user. */ - onCreate(handler: (user: UserRecord, context?: EventContext) => PromiseLike | any): CloudFunction { + onCreate(handler: (user: UserRecord, context: EventContext) => PromiseLike | any): CloudFunction { return this.onOperation(handler, 'user.create'); } /** Respond to the deletion of a Firebase Auth user. */ - onDelete(handler: (user: UserRecord, context?: EventContext) => PromiseLike | any): CloudFunction { + onDelete(handler: (user: UserRecord, context: EventContext) => PromiseLike | any): CloudFunction { return this.onOperation(handler, 'user.delete'); } private onOperation( - handler: (user: UserRecord, context?: EventContext) => PromiseLike | any, + handler: (user: UserRecord, context: EventContext) => PromiseLike | any, eventType: string ): CloudFunction { return makeCloudFunction({ diff --git a/src/providers/crashlytics.ts b/src/providers/crashlytics.ts index 31643af39..418936e14 100644 --- a/src/providers/crashlytics.ts +++ b/src/providers/crashlytics.ts @@ -51,22 +51,22 @@ export class IssueBuilder { } /** Handle Crashlytics New Issue events. */ - onNew(handler: (issue: Issue, context?: EventContext) => PromiseLike | any): CloudFunction { + onNew(handler: (issue: Issue, context: EventContext) => PromiseLike | any): CloudFunction { return this.onEvent(handler, 'issue.new'); } /** Handle Crashlytics Regressed Issue events. */ - onRegressed(handler: (issue: Issue, context?: EventContext) => PromiseLike | any): CloudFunction { + onRegressed(handler: (issue: Issue, context: EventContext) => PromiseLike | any): CloudFunction { return this.onEvent(handler, 'issue.regressed'); } /** Handle Crashlytics Velocity Alert events. */ - onVelocityAlert(handler: (issue: Issue, context?: EventContext) => PromiseLike | any): CloudFunction { + onVelocityAlert(handler: (issue: Issue, context: EventContext) => PromiseLike | any): CloudFunction { return this.onEvent(handler, 'issue.velocityAlert'); } private onEvent( - handler: (issue: Issue, context?: EventContext) => PromiseLike | any, + handler: (issue: Issue, context: EventContext) => PromiseLike | any, eventType: string ): CloudFunction { return makeCloudFunction({ diff --git a/src/providers/database.ts b/src/providers/database.ts index db42e9a9a..d21ac2409 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -103,7 +103,7 @@ export class RefBuilder { /** Respond to any write that affects a ref. */ onWrite(handler: ( change: Change, - context?: EventContext) => PromiseLike | any, + context: EventContext) => PromiseLike | any, ): CloudFunction> { return this.onOperation(handler, 'ref.write', this.changeConstructor); } @@ -111,7 +111,7 @@ export class RefBuilder { /** Respond to update on a ref. */ onUpdate(handler: ( change: Change, - context?: EventContext) => PromiseLike | any, + context: EventContext) => PromiseLike | any, ): CloudFunction> { return this.onOperation(handler, 'ref.update', this.changeConstructor); } @@ -119,7 +119,7 @@ export class RefBuilder { /** Respond to new data on a ref. */ onCreate(handler: ( snapshot: DataSnapshot, - context?: EventContext) => PromiseLike | any, + context: EventContext) => PromiseLike | any, ): CloudFunction { let dataConstructor = (raw: LegacyEvent) => { let [dbInstance, path] = resourceToInstanceAndPath(raw.resource); @@ -136,7 +136,7 @@ export class RefBuilder { /** Respond to all data being deleted from a ref. */ onDelete(handler: ( snapshot: DataSnapshot, - context?: EventContext) => PromiseLike | any, + context: EventContext) => PromiseLike | any, ): CloudFunction { let dataConstructor = (raw: LegacyEvent) => { let [dbInstance, path] = resourceToInstanceAndPath(raw.resource); @@ -151,7 +151,7 @@ export class RefBuilder { } private onOperation( - handler: (data: T, context?: EventContext) => PromiseLike | any, + handler: (data: T, context: EventContext) => PromiseLike | any, eventType: string, dataConstructor: (raw: Event | LegacyEvent) => any): CloudFunction { diff --git a/src/providers/firestore.ts b/src/providers/firestore.ts index e0db9c3dd..c9dec2795 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -136,7 +136,7 @@ export class DocumentBuilder { /** Respond to all document writes (creates, updates, or deletes). */ onWrite(handler: ( change: Change, - context?: EventContext) => PromiseLike | any, + context: EventContext) => PromiseLike | any, ): CloudFunction> { return this.onOperation(handler, 'document.write', changeConstructor); }; @@ -144,7 +144,7 @@ export class DocumentBuilder { /** Respond only to document updates. */ onUpdate(handler: ( change: Change, - context?: EventContext) => PromiseLike | any, + context: EventContext) => PromiseLike | any, ): CloudFunction> { return this.onOperation(handler, 'document.update', changeConstructor); } @@ -152,7 +152,7 @@ export class DocumentBuilder { /** Respond only to document creations. */ onCreate(handler: ( snapshot: DocumentSnapshot, - context?: EventContext) => PromiseLike | any, + context: EventContext) => PromiseLike | any, ): CloudFunction { return this.onOperation(handler, 'document.create', snapshotConstructor); } @@ -160,13 +160,13 @@ export class DocumentBuilder { /** Respond only to document deletions. */ onDelete(handler: ( snapshot: DocumentSnapshot, - context?: EventContext) => PromiseLike | any, + context: EventContext) => PromiseLike | any, ): CloudFunction { return this.onOperation(handler, 'document.delete', beforeSnapshotConstructor); } private onOperation( - handler: (data: T, context?: EventContext) => PromiseLike | any, + handler: (data: T, context: EventContext) => PromiseLike | any, eventType: string, dataConstructor: (raw: Event | LegacyEvent) => any): CloudFunction { return makeCloudFunction({ diff --git a/src/providers/pubsub.ts b/src/providers/pubsub.ts index c11e816e1..c2359b73c 100644 --- a/src/providers/pubsub.ts +++ b/src/providers/pubsub.ts @@ -48,7 +48,7 @@ export class TopicBuilder { constructor(private triggerResource: () => string) { } /** Handle a Pub/Sub message that was published to a Cloud Pub/Sub topic */ - onPublish(handler: (message: Message, context?: EventContext) => PromiseLike | any): CloudFunction { + onPublish(handler: (message: Message, context: EventContext) => PromiseLike | any): CloudFunction { return makeCloudFunction({ handler, provider, diff --git a/src/providers/storage.ts b/src/providers/storage.ts index f806889b1..f99ef3fa7 100644 --- a/src/providers/storage.ts +++ b/src/providers/storage.ts @@ -75,7 +75,7 @@ export class ObjectBuilder { /** Respond to archiving of an object, this is only for buckets that enabled object versioning. */ onArchive(handler: ( object: ObjectMetadata, - context?: EventContext) => PromiseLike | any, + context: EventContext) => PromiseLike | any, ): CloudFunction { return this.onOperation(handler, 'object.archive'); } @@ -83,7 +83,7 @@ export class ObjectBuilder { /** Respond to the deletion of an object (not to archiving, if object versioning is enabled). */ onDelete(handler: ( object: ObjectMetadata, - context?: EventContext) => PromiseLike | any, + context: EventContext) => PromiseLike | any, ): CloudFunction { return this.onOperation(handler, 'object.delete'); } @@ -91,7 +91,7 @@ export class ObjectBuilder { /** Respond to the successful creation of an object. */ onFinalize(handler: ( object: ObjectMetadata, - context?: EventContext) => PromiseLike | any, + context: EventContext) => PromiseLike | any, ): CloudFunction { return this.onOperation(handler, 'object.finalize'); } @@ -99,13 +99,13 @@ export class ObjectBuilder { /** Respond to metadata updates of existing objects. */ onMetadataUpdate(handler: ( object: ObjectMetadata, - context?: EventContext) => PromiseLike | any, + context: EventContext) => PromiseLike | any, ): CloudFunction { return this.onOperation(handler, 'object.metadataUpdate'); } private onOperation( - handler: (object: ObjectMetadata, context?: EventContext) => PromiseLike | any, + handler: (object: ObjectMetadata, context: EventContext) => PromiseLike | any, eventType: string): CloudFunction { return makeCloudFunction({ handler, From 728cbf357cc8d883eab80e1b9bc0bb4b32764033 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Mon, 21 May 2018 13:49:26 -0700 Subject: [PATCH 023/705] Update firebase-admin peer-dependency to v5.12.1 (#246) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5826d0b72..5e20b1d1a 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@types/sinon": "^1.16.29", "chai": "^3.5.0", "chai-as-promised": "^5.2.0", - "firebase-admin": "~5.12.0", + "firebase-admin": "~5.12.1", "istanbul": "^0.4.2", "mocha": "^2.4.5", "mock-require": "^2.0.1", @@ -49,7 +49,7 @@ "typescript": "^2.0.3" }, "peerDependencies": { - "firebase-admin": "~5.12.0" + "firebase-admin": "~5.12.1" }, "dependencies": { "@types/cors": "^2.8.1", From c2dc3e981419e118fb21a59d40d0fd15b227ab64 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Mon, 21 May 2018 13:49:43 -0700 Subject: [PATCH 024/705] Changelog for v1.0.3 (#244) --- changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index e69de29bb..c5c64d57d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1,2 @@ +fixed - Fixed TypeScript type for `context` in event handlers to be always defined. +changed - Updated firebase-admin peer-dependency to v5.12.1. From 83a6302cc2a2ebb06ab0124a4f4ca594727ce92c Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Mon, 21 May 2018 20:51:19 +0000 Subject: [PATCH 025/705] [firebase-release] Updated SDK for Cloud Functions to 1.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5e20b1d1a..10878574a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "1.0.2", + "version": "1.0.3", "description": "Firebase SDK for Cloud Functions", "main": "lib/index.js", "scripts": { From 3f70cc2a683c43ee22140ee4428add67cac7d823 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Mon, 21 May 2018 20:51:30 +0000 Subject: [PATCH 026/705] [firebase-release] Removed change log and reset repo after 1.0.3 release --- changelog.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index c5c64d57d..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,2 +0,0 @@ -fixed - Fixed TypeScript type for `context` in event handlers to be always defined. -changed - Updated firebase-admin peer-dependency to v5.12.1. From 5ff3b6c9a8fcd29145bef5e03d949df4069c6382 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Mon, 4 Jun 2018 13:22:48 -0700 Subject: [PATCH 027/705] Fix integration test. (#250) --- .travis.yml | 3 ++- integration_test/functions/package.json | 4 ++-- integration_test/functions/src/auth-tests.ts | 8 ++++---- integration_test/functions/src/database-tests.ts | 3 +-- integration_test/functions/src/firestore-tests.ts | 2 +- integration_test/functions/src/index.ts | 7 ++++--- integration_test/functions/src/pubsub-tests.ts | 2 +- integration_test/functions/src/testing.ts | 4 ++-- integration_test/functions/tsconfig.json | 5 +---- package.json | 2 +- 10 files changed, 19 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index fe5ba40c7..8f5bf9f17 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: node_js node_js: -- '6.11.5' +- '6.14.0' +- '8' - stable sudo: false diff --git a/integration_test/functions/package.json b/integration_test/functions/package.json index 866f84c6b..aa727bc91 100644 --- a/integration_test/functions/package.json +++ b/integration_test/functions/package.json @@ -8,13 +8,13 @@ "@google-cloud/pubsub": "^0.6.0", "@types/lodash": "^4.14.41", "firebase": "^4.9.1", - "firebase-admin": "~5.12.0", + "firebase-admin": "~5.12.1", "firebase-functions": "./firebase-functions.tgz", "lodash": "^4.17.2" }, "main": "lib/index.js", "devDependencies": { - "typescript": "^2.0.10" + "typescript": "^2.8.3" }, "private": true } diff --git a/integration_test/functions/src/auth-tests.ts b/integration_test/functions/src/auth-tests.ts index 367f6fe87..b3e5fe745 100644 --- a/integration_test/functions/src/auth-tests.ts +++ b/integration_test/functions/src/auth-tests.ts @@ -9,12 +9,12 @@ export const createUserTests: any = functions.auth.user().onCreate((u, c) => { return new TestSuite('auth user onCreate') .it('should have a project as resource', (user, context) => expectEq( - context.resource, `projects/${process.env.GCLOUD_PROJECT}`)) + context.resource.name, `projects/${process.env.GCLOUD_PROJECT}`)) .it('should not have a path', (user, context) => expectEq((context as any).path, undefined)) .it('should have the correct eventType', (user, context) => expectEq( - context.eventType, 'providers/firebase.auth/eventTypes/user.create')) + context.eventType, 'google.firebase.auth.user.create')) .it('should have an eventId', (user, context)=> context.eventId) @@ -33,12 +33,12 @@ export const deleteUserTests: any = functions.auth.user().onDelete((u, c) => { return new TestSuite('auth user onDelete') .it('should have a project as resource', (user, context) => expectEq( - context.resource, `projects/${process.env.GCLOUD_PROJECT}`)) + context.resource.name, `projects/${process.env.GCLOUD_PROJECT}`)) .it('should not have a path', (user, context) => expectEq((context as any).path, undefined)) .it('should have the correct eventType', (user, context) => expectEq( - context.eventType, 'providers/firebase.auth/eventTypes/user.delete')) + context.eventType, 'google.firebase.auth.user.delete')) .it('should have an eventId', (user, context) => context.eventId) diff --git a/integration_test/functions/src/database-tests.ts b/integration_test/functions/src/database-tests.ts index e7ca21a8f..22d0a6b07 100644 --- a/integration_test/functions/src/database-tests.ts +++ b/integration_test/functions/src/database-tests.ts @@ -2,7 +2,6 @@ import * as functions from 'firebase-functions'; import { TestSuite, expectEq, expectMatches } from './testing'; import * as admin from 'firebase-admin'; import DataSnapshot = admin.database.DataSnapshot; -import { Change } from '../../../src/cloud-functions'; const testIdFieldName = 'testId'; @@ -14,7 +13,7 @@ export const databaseTests: any = functions.database.ref('dbTests/{testId}/start return; } - return new TestSuite>('database ref onWrite') + return new TestSuite>('database ref onWrite') .it('should not have event.app', (change, context) => !(context as any).app) diff --git a/integration_test/functions/src/firestore-tests.ts b/integration_test/functions/src/firestore-tests.ts index 4e7cfd912..545fd61fd 100644 --- a/integration_test/functions/src/firestore-tests.ts +++ b/integration_test/functions/src/firestore-tests.ts @@ -19,7 +19,7 @@ export const firestoreTests: any = functions.firestore.document('tests/{document ) .it('should have the right eventType', (snap, context) => expectEq( - context.eventType, 'providers/cloud.firestore/eventTypes/document.create')) + context.eventType, 'google.firestore.document.create')) .it('should have eventId', (snap, context) => context.eventId) diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 9979e372b..58c3d4b31 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -11,8 +11,9 @@ export * from './firestore-tests'; export * from './https-tests'; const numTests = Object.keys(exports).length; // Assumption: every exported function is its own test. -// Client SDK doesn't support auto initialization: -firebase.initializeApp(JSON.parse(process.env.FIREBASE_CONFIG)); +import 'firebase-functions'; // temporary shim until process.env.FIREBASE_CONFIG available natively in GCF(BUG 63586213) +const firebaseConfig = JSON.parse(process.env.FIREBASE_CONFIG); +firebase.initializeApp(firebaseConfig); console.log('initializing admin'); admin.initializeApp(); @@ -21,7 +22,7 @@ function callHttpsTrigger(name: string, data: any) { return new Promise((resolve, reject) => { const request = https.request({ method: 'POST', - host: 'us-central1-' + functions.config().firebase.projectId + '.cloudfunctions.net', + host: 'us-central1-' + firebaseConfig.projectId + '.cloudfunctions.net', path: '/' + name, headers: { 'Content-Type': 'application/json', diff --git a/integration_test/functions/src/pubsub-tests.ts b/integration_test/functions/src/pubsub-tests.ts index 044dd2382..7919a23f7 100644 --- a/integration_test/functions/src/pubsub-tests.ts +++ b/integration_test/functions/src/pubsub-tests.ts @@ -19,7 +19,7 @@ export const pubsubTests: any = functions.pubsub.topic('pubsubTests').onPublish( .it('should not have a path', (message, context) => expectEq((context as any).path, undefined)) .it('should have the correct eventType', (message, context) => expectEq( - context.eventType, 'providers/cloud.pubsub/eventTypes/topic.publish')) + context.eventType, 'google.pubsub.topic.publish')) .it('should have an eventId', (message, context) => context.eventId) diff --git a/integration_test/functions/src/testing.ts b/integration_test/functions/src/testing.ts index 73d3855f1..765dfacf0 100644 --- a/integration_test/functions/src/testing.ts +++ b/integration_test/functions/src/testing.ts @@ -2,7 +2,7 @@ import * as firebase from 'firebase-admin'; import * as _ from 'lodash'; import { EventContext } from 'firebase-functions'; -export type TestCase = (data: T, context: EventContext) => any +export type TestCase = (data: T, context?: EventContext) => any export type TestCaseMap = { [key: string]: TestCase }; export class TestSuite { @@ -19,7 +19,7 @@ export class TestSuite { return this; } - run(testId: string, data: T, context: EventContext): Promise { + run(testId: string, data: T, context?: EventContext): Promise { let running: Array> = []; for (let testName in this.tests) { if (!this.tests.hasOwnProperty(testName)) { continue; } diff --git a/integration_test/functions/tsconfig.json b/integration_test/functions/tsconfig.json index 494cc1dec..554bd3a6b 100644 --- a/integration_test/functions/tsconfig.json +++ b/integration_test/functions/tsconfig.json @@ -1,9 +1,6 @@ { "compilerOptions": { - "lib": [ - "es6", - "es2015.promise" - ], + "lib": ["es6"], "module": "commonjs", "target": "es6", "noImplicitAny": false, diff --git a/package.json b/package.json index 10878574a..efae65e26 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "nock": "^9.0.0", "sinon": "^1.17.4", "tslint": "^3.15.1", - "typescript": "^2.0.3" + "typescript": "^2.8.3" }, "peerDependencies": { "firebase-admin": "~5.12.1" From e00301d9bf89b49be769b33ec73a2cc0d9d495ab Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Mon, 4 Jun 2018 19:00:07 -0700 Subject: [PATCH 028/705] Lock in TypeScript version (#260) --- integration_test/functions/package.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration_test/functions/package.json b/integration_test/functions/package.json index aa727bc91..746d9526d 100644 --- a/integration_test/functions/package.json +++ b/integration_test/functions/package.json @@ -14,7 +14,7 @@ }, "main": "lib/index.js", "devDependencies": { - "typescript": "^2.8.3" + "typescript": "~2.8.3" }, "private": true } diff --git a/package.json b/package.json index efae65e26..512a47a72 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "nock": "^9.0.0", "sinon": "^1.17.4", "tslint": "^3.15.1", - "typescript": "^2.8.3" + "typescript": "~2.8.3" }, "peerDependencies": { "firebase-admin": "~5.12.1" From b727a066185c41baed48a42727a0563bdb37fdb7 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Mon, 4 Jun 2018 19:02:30 -0700 Subject: [PATCH 029/705] Changelog for v1.0.4 (#259) --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index e69de29bb..8915afc5d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +fixed - Fixed integration test. From 2a923ac3c78af5b937cf3dfb16eea5d74d6f2477 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Tue, 5 Jun 2018 02:06:34 +0000 Subject: [PATCH 030/705] [firebase-release] Updated SDK for Cloud Functions to 1.0.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 512a47a72..36b6d0720 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "1.0.3", + "version": "1.0.4", "description": "Firebase SDK for Cloud Functions", "main": "lib/index.js", "scripts": { From 6f1bc8889fd7cd69e0a33ad5217c67db27ac918b Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Tue, 5 Jun 2018 02:06:44 +0000 Subject: [PATCH 031/705] [firebase-release] Removed change log and reset repo after 1.0.4 release --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 8915afc5d..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +0,0 @@ -fixed - Fixed integration test. From 33435b5e1a169bc4ab9afbda1f9bf03b3f1565fc Mon Sep 17 00:00:00 2001 From: Laurent Pellegrino Date: Wed, 20 Jun 2018 20:33:19 +0200 Subject: [PATCH 032/705] Pass the raw Express request to CallableContext (#269) --- spec/providers/https.spec.ts | 18 ++++++++++++++++++ src/providers/https.ts | 7 ++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/spec/providers/https.spec.ts b/spec/providers/https.spec.ts index c8c7bd239..3d80f60ef 100644 --- a/spec/providers/https.spec.ts +++ b/spec/providers/https.spec.ts @@ -443,6 +443,24 @@ describe('callable.FunctionBuilder', () => { }, }); }); + + it('should expose raw request', async () => { + const mockRequest = request(null, 'application/json', {}); + await runTest({ + httpRequest: mockRequest, + expectedData: null, + callableFunction: (data, context) => { + expect(context.rawRequest).to.not.be.undefined; + expect(context.rawRequest).to.equal(mockRequest); + return null; + }, + expectedHttpResponse: { + status: 200, + headers: expectedResponseHeaders, + body: {result: null}, + }, + }); + }); }); }); diff --git a/src/providers/https.ts b/src/providers/https.ts index 3e44efd0e..6ea209dab 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -217,6 +217,11 @@ export interface CallableContext { * An unverified token for a Firebase Instance ID. */ instanceIdToken?: string; + + /** + * The raw request handled by the callable. + */ + rawRequest: express.Request; } // The allowed interface for an http request for a callable function. @@ -373,7 +378,7 @@ export function onCall( throw new HttpsError('invalid-argument', 'Bad Request'); } - const context: CallableContext = {}; + const context: CallableContext = { rawRequest: req }; const authorization = req.header('Authorization'); if (authorization) { From d713b6678027a2bca4f4699ba8fcced896723d62 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Wed, 20 Jun 2018 11:50:28 -0700 Subject: [PATCH 033/705] Changelog for v1.0.5 (#272) --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index e69de29bb..47c57e2b8 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +feature - Make raw HTTP request available to callable HTTP functions via `context.rawRequest`. From f41d00df064dcd1e4a1de6fa8a4e7917896f7962 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Wed, 20 Jun 2018 18:52:14 +0000 Subject: [PATCH 034/705] [firebase-release] Updated SDK for Cloud Functions to 1.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 36b6d0720..85f2828c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "1.0.4", + "version": "1.1.0", "description": "Firebase SDK for Cloud Functions", "main": "lib/index.js", "scripts": { From 54b2d889bfe5a6a02cd02bdfa40a7c90f908de43 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Wed, 20 Jun 2018 18:52:24 +0000 Subject: [PATCH 035/705] [firebase-release] Removed change log and reset repo after 1.1.0 release --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 47c57e2b8..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +0,0 @@ -feature - Make raw HTTP request available to callable HTTP functions via `context.rawRequest`. From df3db62d6e7b4d68ac3bda987ebd96ee00d3e248 Mon Sep 17 00:00:00 2001 From: Bryan Klimt Date: Thu, 21 Jun 2018 15:15:36 -0400 Subject: [PATCH 036/705] Add trigger labels support and label callables. (#273) --- spec/providers/https.spec.ts | 5 ++++- src/cloud-functions.ts | 3 ++- src/providers/https.ts | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/spec/providers/https.spec.ts b/spec/providers/https.spec.ts index 3d80f60ef..6e3a89032 100644 --- a/spec/providers/https.spec.ts +++ b/spec/providers/https.spec.ts @@ -234,7 +234,10 @@ describe('callable.FunctionBuilder', () => { const result = https.onCall((data) => { return 'response'; }); - expect(result.__trigger).to.deep.equal({httpsTrigger: {}}); + expect(result.__trigger).to.deep.equal({ + httpsTrigger: {}, + labels: { 'deployment-callable': 'true' }, + }); }); it('should handle success', () => { diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 8db2f15ea..3717f8ca1 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -159,7 +159,8 @@ export interface TriggerAnnotated { eventType: string; resource: string; service: string; - } + }, + labels?: { [key: string]: string } }; } diff --git a/src/providers/https.ts b/src/providers/https.ts index 6ea209dab..82f5c56ba 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -435,7 +435,10 @@ export function onCall( return corsHandler(req, res, () => func(req, res)); }; - corsFunc.__trigger = {httpsTrigger: {}}; + corsFunc.__trigger = { + httpsTrigger: {}, + labels: { 'deployment-callable': 'true' }, + }; return corsFunc; } From 6c1cdca5720144dd2bac98742244aedf57c1bf8c Mon Sep 17 00:00:00 2001 From: Kota Yoshitsugu Date: Tue, 3 Jul 2018 04:24:46 +0900 Subject: [PATCH 037/705] Fix error message in storage bucket function (#276) --- src/providers/storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/storage.ts b/src/providers/storage.ts index f99ef3fa7..6650a304b 100644 --- a/src/providers/storage.ts +++ b/src/providers/storage.ts @@ -41,7 +41,7 @@ export function bucket(bucket?: string): BucketBuilder { ' through `functions.storage.bucket(bucketName)`, or set process.env.FIREBASE_CONFIG.'); } if (!/^[a-z\d][a-z\d\\._-]{1,230}[a-z\d]$/.test(bucket)) { - throw new Error('Invalid bucket name ${bucket}'); + throw new Error(`Invalid bucket name ${bucket}`); } return `projects/_/buckets/${bucket}`; }; From 01665efef3f0ad8fe5041417e51c1f6f23f52b51 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Fri, 6 Jul 2018 16:03:48 -0700 Subject: [PATCH 038/705] Use prettier instead of linter (#204) --- .prettierrc | 4 + integration_test/functions/src/auth-tests.ts | 46 ++- .../functions/src/database-tests.ts | 88 +++-- .../functions/src/firestore-tests.ts | 42 ++- integration_test/functions/src/https-tests.ts | 4 +- integration_test/functions/src/index.ts | 178 ++++++----- .../functions/src/pubsub-tests.ts | 89 +++--- integration_test/functions/src/testing.ts | 68 ++-- package.json | 11 +- spec/apps.spec.ts | 26 +- spec/cloud-functions.spec.ts | 32 +- spec/config.spec.ts | 30 +- spec/fixtures/credential/key.d.ts | 2 +- spec/fixtures/http.ts | 48 ++- spec/providers/analytics.spec.ts | 47 ++- spec/providers/auth.spec.ts | 42 ++- spec/providers/crashlytics.spec.ts | 10 +- spec/providers/database.spec.ts | 106 +++++-- spec/providers/firestore.spec.ts | 300 +++++++++++------- spec/providers/https.spec.ts | 196 +++++++----- spec/providers/pubsub.spec.ts | 21 +- spec/providers/storage.spec.ts | 98 ++++-- spec/testing.spec.ts | 2 +- spec/utils.spec.ts | 22 +- src/apps.ts | 11 +- src/cloud-functions.ts | 75 +++-- src/config.ts | 8 +- src/encoder.ts | 2 +- src/index.ts | 22 +- src/providers/analytics.ts | 93 +++++- src/providers/auth.ts | 57 +++- src/providers/crashlytics.ts | 24 +- src/providers/database.ts | 120 ++++--- src/providers/firestore.ts | 87 +++-- src/providers/https.ts | 94 ++++-- src/providers/pubsub.ts | 28 +- src/providers/storage.ts | 100 +++--- src/utils.ts | 6 +- 38 files changed, 1457 insertions(+), 782 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..c1a6f6671 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "es5" +} diff --git a/integration_test/functions/src/auth-tests.ts b/integration_test/functions/src/auth-tests.ts index b3e5fe745..7b7f8686c 100644 --- a/integration_test/functions/src/auth-tests.ts +++ b/integration_test/functions/src/auth-tests.ts @@ -8,21 +8,29 @@ export const createUserTests: any = functions.auth.user().onCreate((u, c) => { console.log(`testId is ${testId}`); return new TestSuite('auth user onCreate') - .it('should have a project as resource', (user, context) => expectEq( - context.resource.name, `projects/${process.env.GCLOUD_PROJECT}`)) + .it('should have a project as resource', (user, context) => + expectEq(context.resource.name, `projects/${process.env.GCLOUD_PROJECT}`) + ) - .it('should not have a path', (user, context) => expectEq((context as any).path, undefined)) + .it('should not have a path', (user, context) => + expectEq((context as any).path, undefined) + ) - .it('should have the correct eventType', (user, context) => expectEq( - context.eventType, 'google.firebase.auth.user.create')) + .it('should have the correct eventType', (user, context) => + expectEq(context.eventType, 'google.firebase.auth.user.create') + ) - .it('should have an eventId', (user, context)=> context.eventId) + .it('should have an eventId', (user, context) => context.eventId) .it('should have a timestamp', (user, context) => context.timestamp) - .it('should not have auth', (user, context) => expectEq((context as any).auth, undefined)) + .it('should not have auth', (user, context) => + expectEq((context as any).auth, undefined) + ) - .it('should not have action', (user, context) => expectEq((context as any).action, undefined)) + .it('should not have action', (user, context) => + expectEq((context as any).action, undefined) + ) .run(testId, u, c); }); @@ -32,21 +40,29 @@ export const deleteUserTests: any = functions.auth.user().onDelete((u, c) => { console.log(`testId is ${testId}`); return new TestSuite('auth user onDelete') - .it('should have a project as resource', (user, context) => expectEq( - context.resource.name, `projects/${process.env.GCLOUD_PROJECT}`)) + .it('should have a project as resource', (user, context) => + expectEq(context.resource.name, `projects/${process.env.GCLOUD_PROJECT}`) + ) - .it('should not have a path', (user, context) => expectEq((context as any).path, undefined)) + .it('should not have a path', (user, context) => + expectEq((context as any).path, undefined) + ) - .it('should have the correct eventType', (user, context) => expectEq( - context.eventType, 'google.firebase.auth.user.delete')) + .it('should have the correct eventType', (user, context) => + expectEq(context.eventType, 'google.firebase.auth.user.delete') + ) .it('should have an eventId', (user, context) => context.eventId) .it('should have a timestamp', (user, context) => context.timestamp) - .it('should not have auth', (user, context) => expectEq((context as any).auth, undefined)) + .it('should not have auth', (user, context) => + expectEq((context as any).auth, undefined) + ) - .it('should not have action', (user, context) => expectEq((context as any).action, undefined)) + .it('should not have action', (user, context) => + expectEq((context as any).action, undefined) + ) .run(testId, u, c); }); diff --git a/integration_test/functions/src/database-tests.ts b/integration_test/functions/src/database-tests.ts index 22d0a6b07..183d3d785 100644 --- a/integration_test/functions/src/database-tests.ts +++ b/integration_test/functions/src/database-tests.ts @@ -5,44 +5,72 @@ import DataSnapshot = admin.database.DataSnapshot; const testIdFieldName = 'testId'; -export const databaseTests: any = functions.database.ref('dbTests/{testId}/start').onWrite((ch, ctx) => { - if (ch.after.val() === null) { - console.log( - 'Event for ' + ctx.params[testIdFieldName] - + ' is null; presuming data cleanup, so skipping.'); - return; - } +export const databaseTests: any = functions.database + .ref('dbTests/{testId}/start') + .onWrite((ch, ctx) => { + if (ch.after.val() === null) { + console.log( + 'Event for ' + + ctx.params[testIdFieldName] + + ' is null; presuming data cleanup, so skipping.' + ); + return; + } - return new TestSuite>('database ref onWrite') + return new TestSuite>('database ref onWrite') - .it('should not have event.app', (change, context) => !(context as any).app) + .it( + 'should not have event.app', + (change, context) => !(context as any).app + ) - .it('should give refs access to admin data', (change) => - change.after.ref.parent.child('adminOnly').update({ allowed: 1 }).then(() => true)) + .it('should give refs access to admin data', change => + change.after.ref.parent + .child('adminOnly') + .update({ allowed: 1 }) + .then(() => true) + ) - .it('should have a correct ref url', (change) => { - const url = change.after.ref.toString(); - return Promise.resolve().then(() => { - return expectMatches(url, new RegExp(`^https://${process.env.GCLOUD_PROJECT}.firebaseio.com/dbTests`)); - }).then(() => { - return expectMatches(url, /\/start$/); - }); - }) + .it('should have a correct ref url', change => { + const url = change.after.ref.toString(); + return Promise.resolve() + .then(() => { + return expectMatches( + url, + new RegExp( + `^https://${process.env.GCLOUD_PROJECT}.firebaseio.com/dbTests` + ) + ); + }) + .then(() => { + return expectMatches(url, /\/start$/); + }); + }) - .it('should have refs resources', (change, context) => expectEq( - context.resource.name, - `projects/_/instances/${process.env.GCLOUD_PROJECT}/refs/dbTests/${context.params.testId}/start`)) + .it('should have refs resources', (change, context) => + expectEq( + context.resource.name, + `projects/_/instances/${process.env.GCLOUD_PROJECT}/refs/dbTests/${ + context.params.testId + }/start` + ) + ) - .it('should not include path', (change, context) => expectEq((context as any).path, undefined)) + .it('should not include path', (change, context) => + expectEq((context as any).path, undefined) + ) - .it('should have the right eventType', (change, context) => expectEq( - context.eventType, 'google.firebase.database.ref.write')) + .it('should have the right eventType', (change, context) => + expectEq(context.eventType, 'google.firebase.database.ref.write') + ) - .it('should have eventId', (change, context) => context.eventId) + .it('should have eventId', (change, context) => context.eventId) - .it('should have timestamp', (change, context) => context.timestamp) + .it('should have timestamp', (change, context) => context.timestamp) - .it('should not have action', (change, context) => expectEq((context as any).action, undefined)) + .it('should not have action', (change, context) => + expectEq((context as any).action, undefined) + ) - .run(ctx.params[testIdFieldName], ch, ctx); -}); + .run(ctx.params[testIdFieldName], ch, ctx); + }); diff --git a/integration_test/functions/src/firestore-tests.ts b/integration_test/functions/src/firestore-tests.ts index 545fd61fd..26def7fff 100644 --- a/integration_test/functions/src/firestore-tests.ts +++ b/integration_test/functions/src/firestore-tests.ts @@ -5,27 +5,37 @@ import DocumentSnapshot = admin.firestore.DocumentSnapshot; const testIdFieldName = 'documentId'; -export const firestoreTests: any = functions.firestore.document('tests/{documentId}').onCreate((s, c) => { - return new TestSuite('firestore document onWrite') +export const firestoreTests: any = functions.firestore + .document('tests/{documentId}') + .onCreate((s, c) => { + return new TestSuite('firestore document onWrite') - .it('should not have event.app', (snap, context) => !(context as any).app) + .it('should not have event.app', (snap, context) => !(context as any).app) - .it('should give refs write access', (snap) => - snap.ref.set({ allowed: 1 }, {merge: true}).then(() => true)) + .it('should give refs write access', snap => + snap.ref.set({ allowed: 1 }, { merge: true }).then(() => true) + ) - .it('should have well-formatted resource', (snap, context) => expectEq( - context.resource.name, - `projects/${process.env.GCLOUD_PROJECT}/databases/(default)/documents/tests/${context.params.documentId}`) - ) + .it('should have well-formatted resource', (snap, context) => + expectEq( + context.resource.name, + `projects/${ + process.env.GCLOUD_PROJECT + }/databases/(default)/documents/tests/${context.params.documentId}` + ) + ) - .it('should have the right eventType', (snap, context) => expectEq( - context.eventType, 'google.firestore.document.create')) + .it('should have the right eventType', (snap, context) => + expectEq(context.eventType, 'google.firestore.document.create') + ) - .it('should have eventId', (snap, context) => context.eventId) + .it('should have eventId', (snap, context) => context.eventId) - .it('should have timestamp', (snap, context) => context.timestamp) + .it('should have timestamp', (snap, context) => context.timestamp) - .it('should have the correct data', (snap, context) => expectDeepEq(snap.data(), {test: context.params.documentId})) + .it('should have the correct data', (snap, context) => + expectDeepEq(snap.data(), { test: context.params.documentId }) + ) - .run(c.params[testIdFieldName], s, c); -}); + .run(c.params[testIdFieldName], s, c); + }); diff --git a/integration_test/functions/src/https-tests.ts b/integration_test/functions/src/https-tests.ts index 4595214e8..4184f124d 100644 --- a/integration_test/functions/src/https-tests.ts +++ b/integration_test/functions/src/https-tests.ts @@ -4,6 +4,8 @@ import { TestSuite, expectEq } from './testing'; export const callableTests: any = functions.https.onCall(d => { return new TestSuite('https onCall') - .it('should have the correct data', data => expectEq(_.get(data, 'foo'), 'bar')) + .it('should have the correct data', data => + expectEq(_.get(data, 'foo'), 'bar') + ) .run(d.testId, d); }); diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 58c3d4b31..42626dbec 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -9,7 +9,7 @@ export * from './database-tests'; export * from './auth-tests'; export * from './firestore-tests'; export * from './https-tests'; -const numTests = Object.keys(exports).length; // Assumption: every exported function is its own test. +const numTests = Object.keys(exports).length; // Assumption: every exported function is its own test. import 'firebase-functions'; // temporary shim until process.env.FIREBASE_CONFIG available natively in GCF(BUG 63586213) const firebaseConfig = JSON.parse(process.env.FIREBASE_CONFIG); @@ -20,81 +20,117 @@ admin.initializeApp(); // TODO(klimt): Get rid of this once the JS client SDK supports callable triggers. function callHttpsTrigger(name: string, data: any) { return new Promise((resolve, reject) => { - const request = https.request({ - method: 'POST', - host: 'us-central1-' + firebaseConfig.projectId + '.cloudfunctions.net', - path: '/' + name, - headers: { - 'Content-Type': 'application/json', + const request = https.request( + { + method: 'POST', + host: 'us-central1-' + firebaseConfig.projectId + '.cloudfunctions.net', + path: '/' + name, + headers: { + 'Content-Type': 'application/json', + }, }, - }, (response) => { - let body = ''; - response.on('data', (chunk) => { body += chunk; }); - response.on('end', () => resolve(body)); - }); + response => { + let body = ''; + response.on('data', chunk => { + body += chunk; + }); + response.on('end', () => resolve(body)); + } + ); request.on('error', reject); - request.write(JSON.stringify({data})); + request.write(JSON.stringify({ data })); request.end(); }); } -export const integrationTests: any = functions.https.onRequest((req: Request, resp: Response) => { - let pubsub: any = require('@google-cloud/pubsub')(); +export const integrationTests: any = functions.https.onRequest( + (req: Request, resp: Response) => { + let pubsub: any = require('@google-cloud/pubsub')(); - const testId = firebase.database().ref().push().key; - return Promise.all([ - // A database write to trigger the Firebase Realtime Database tests. - // The database write happens without admin privileges, so that the triggered function's "event.data.ref" also - // doesn't have admin privileges. - firebase.database().ref(`dbTests/${testId}/start`).set({ '.sv': 'timestamp' }), - // A Pub/Sub publish to trigger the Cloud Pub/Sub tests. - pubsub.topic('pubsubTests').publish({ testId }), - // A user creation to trigger the Firebase Auth user creation tests. - admin.auth().createUser({ - email: `${testId}@fake.com`, - password: 'secret', - displayName: `${testId}`, - }).then(userRecord => { - // A user deletion to trigger the Firebase Auth user deletion tests. - admin.auth().deleteUser(userRecord.uid); - }), - // A firestore write to trigger the Cloud Firestore tests. - admin.firestore().collection('tests').doc(testId).set({test: testId}), - // Invoke a callable HTTPS trigger. - callHttpsTrigger('callableTests', {foo: 'bar', testId}), - - ]).then(() => { - // On test completion, check that all tests pass and reply "PASS", or provide further details. - console.log('Waiting for all tests to report they pass...'); - let ref = admin.database().ref(`testRuns/${testId}`); - return new Promise((resolve, reject) => { - let testsExecuted = 0; - ref.on('child_added', (snapshot) => { - testsExecuted += 1; - if (!snapshot.val().passed) { - reject(new Error(`test ${snapshot.key} failed; see database for details.`)); - return; - } - console.log(`${snapshot.key} passed (${testsExecuted} of ${numTests})`); - if (testsExecuted < numTests) { - // Not all tests have completed. Wait longer. - return; - } - // All tests have passed! - resolve(); + const testId = firebase + .database() + .ref() + .push().key; + return Promise.all([ + // A database write to trigger the Firebase Realtime Database tests. + // The database write happens without admin privileges, so that the triggered function's "event.data.ref" also + // doesn't have admin privileges. + firebase + .database() + .ref(`dbTests/${testId}/start`) + .set({ '.sv': 'timestamp' }), + // A Pub/Sub publish to trigger the Cloud Pub/Sub tests. + pubsub.topic('pubsubTests').publish({ testId }), + // A user creation to trigger the Firebase Auth user creation tests. + admin + .auth() + .createUser({ + email: `${testId}@fake.com`, + password: 'secret', + displayName: `${testId}`, + }) + .then(userRecord => { + // A user deletion to trigger the Firebase Auth user deletion tests. + admin.auth().deleteUser(userRecord.uid); + }), + // A firestore write to trigger the Cloud Firestore tests. + admin + .firestore() + .collection('tests') + .doc(testId) + .set({ test: testId }), + // Invoke a callable HTTPS trigger. + callHttpsTrigger('callableTests', { foo: 'bar', testId }), + ]) + .then(() => { + // On test completion, check that all tests pass and reply "PASS", or provide further details. + console.log('Waiting for all tests to report they pass...'); + let ref = admin.database().ref(`testRuns/${testId}`); + return new Promise((resolve, reject) => { + let testsExecuted = 0; + ref.on('child_added', snapshot => { + testsExecuted += 1; + if (!snapshot.val().passed) { + reject( + new Error( + `test ${snapshot.key} failed; see database for details.` + ) + ); + return; + } + console.log( + `${snapshot.key} passed (${testsExecuted} of ${numTests})` + ); + if (testsExecuted < numTests) { + // Not all tests have completed. Wait longer. + return; + } + // All tests have passed! + resolve(); + }); + }) + .then(() => { + ref.off(); // No more need to listen. + return Promise.resolve(); + }) + .catch(err => { + ref.off(); // No more need to listen. + return Promise.reject(err); + }); + }) + .then(() => { + console.log('All tests pass!'); + resp.status(200).send('PASS'); + }) + .catch(err => { + console.log(`Some tests failed: ${err}`); + resp + .status(500) + .send( + `FAIL - details at https://${ + process.env.GCLOUD_PROJECT + }.firebaseio.com/testRuns/${testId}` + ); }); - }).then(() => { - ref.off(); // No more need to listen. - return Promise.resolve(); - }).catch(err => { - ref.off(); // No more need to listen. - return Promise.reject(err); - }); - }).then(() => { - console.log('All tests pass!'); - resp.status(200).send('PASS'); - }).catch(err => { - console.log(`Some tests failed: ${err}`); - resp.status(500).send(`FAIL - details at https://${process.env.GCLOUD_PROJECT}.firebaseio.com/testRuns/${testId}`); - }); -}); + } +); diff --git a/integration_test/functions/src/pubsub-tests.ts b/integration_test/functions/src/pubsub-tests.ts index 7919a23f7..3ce63251e 100644 --- a/integration_test/functions/src/pubsub-tests.ts +++ b/integration_test/functions/src/pubsub-tests.ts @@ -4,39 +4,56 @@ import PubsubMessage = functions.pubsub.Message; // TODO(inlined) use multiple queues to run inline. // Expected message data: {"hello": "world"} -export const pubsubTests: any = functions.pubsub.topic('pubsubTests').onPublish((m, c) => { - let testId: string; - try { - testId = m.json.testId; - } catch (e) { - /* Ignored. Covered in another test case that `event.data.json` works. */ - } - - return new TestSuite('pubsub onPublish') - .it('should have a topic as resource', (message, context) => expectEq( - context.resource.name, `projects/${process.env.GCLOUD_PROJECT}/topics/pubsubTests`)) - - .it('should not have a path', (message, context) => expectEq((context as any).path, undefined)) - - .it('should have the correct eventType', (message, context) => expectEq( - context.eventType, 'google.pubsub.topic.publish')) - - .it('should have an eventId', (message, context) => context.eventId) - - .it('should have a timestamp', (message, context) => context.timestamp) - - .it('should not have auth', (message, context) => expectEq((context as any).auth, undefined)) - - .it('should not have action', (message, context) => expectEq((context as any).action, undefined)) - - .it('should have pubsub data', (message) => { - const decoded = (new Buffer(message.data, 'base64')).toString(); - const parsed = JSON.parse(decoded); - return evaluate(parsed.hasOwnProperty('testId'), 'Raw data was: ' + message.data); - }) - - .it('should decode JSON payloads with the json helper', (message) => - evaluate(message.json.hasOwnProperty('testId'), message.json)) - - .run(testId, m, c); -}); +export const pubsubTests: any = functions.pubsub + .topic('pubsubTests') + .onPublish((m, c) => { + let testId: string; + try { + testId = m.json.testId; + } catch (e) { + /* Ignored. Covered in another test case that `event.data.json` works. */ + } + + return new TestSuite('pubsub onPublish') + .it('should have a topic as resource', (message, context) => + expectEq( + context.resource.name, + `projects/${process.env.GCLOUD_PROJECT}/topics/pubsubTests` + ) + ) + + .it('should not have a path', (message, context) => + expectEq((context as any).path, undefined) + ) + + .it('should have the correct eventType', (message, context) => + expectEq(context.eventType, 'google.pubsub.topic.publish') + ) + + .it('should have an eventId', (message, context) => context.eventId) + + .it('should have a timestamp', (message, context) => context.timestamp) + + .it('should not have auth', (message, context) => + expectEq((context as any).auth, undefined) + ) + + .it('should not have action', (message, context) => + expectEq((context as any).action, undefined) + ) + + .it('should have pubsub data', message => { + const decoded = new Buffer(message.data, 'base64').toString(); + const parsed = JSON.parse(decoded); + return evaluate( + parsed.hasOwnProperty('testId'), + 'Raw data was: ' + message.data + ); + }) + + .it('should decode JSON payloads with the json helper', message => + evaluate(message.json.hasOwnProperty('testId'), message.json) + ) + + .run(testId, m, c); + }); diff --git a/integration_test/functions/src/testing.ts b/integration_test/functions/src/testing.ts index 765dfacf0..50eaa7966 100644 --- a/integration_test/functions/src/testing.ts +++ b/integration_test/functions/src/testing.ts @@ -2,7 +2,7 @@ import * as firebase from 'firebase-admin'; import * as _ from 'lodash'; import { EventContext } from 'firebase-functions'; -export type TestCase = (data: T, context?: EventContext) => any +export type TestCase = (data: T, context?: EventContext) => any; export type TestCaseMap = { [key: string]: TestCase }; export class TestSuite { @@ -22,30 +22,39 @@ export class TestSuite { run(testId: string, data: T, context?: EventContext): Promise { let running: Array> = []; for (let testName in this.tests) { - if (!this.tests.hasOwnProperty(testName)) { continue; } + if (!this.tests.hasOwnProperty(testName)) { + continue; + } const run = Promise.resolve() .then(() => this.tests[testName](data, context)) .then( - (result) => { - console.log(`${result ? 'Passed' : 'Failed with successful op'}: ${testName}`); - return { name: testName, passed: !!result }; - }, - (error) => { - console.error(`Failed: ${testName}`, error); - return { name: testName, passed: 0, error: error }; - } + result => { + console.log( + `${result ? 'Passed' : 'Failed with successful op'}: ${testName}` + ); + return { name: testName, passed: !!result }; + }, + error => { + console.error(`Failed: ${testName}`, error); + return { name: testName, passed: 0, error: error }; + } ); running.push(run); } - return Promise.all(running).then((results) => { - let sum = 0; - results.forEach((val) => sum = sum + val.passed); - const summary = `passed ${sum} of ${running.length}`; - const passed = sum === running.length; - console.log(summary); - const result = { passed, summary, tests: results }; - return firebase.database().ref(`testRuns/${testId}/${this.name}`).set(result); - }).then(() => null); + return Promise.all(running) + .then(results => { + let sum = 0; + results.forEach(val => (sum = sum + val.passed)); + const summary = `passed ${sum} of ${running.length}`; + const passed = sum === running.length; + console.log(summary); + const result = { passed, summary, tests: results }; + return firebase + .database() + .ref(`testRuns/${testId}/${this.name}`) + .set(result); + }) + .then(() => null); } } @@ -67,30 +76,33 @@ export function evaluate(value, errMsg) { export function expectEq(left, right) { return evaluate( left === right, - JSON.stringify(left) + ' does not equal ' + JSON.stringify(right)); + JSON.stringify(left) + ' does not equal ' + JSON.stringify(right) + ); } export function expectDeepEq(left, right) { return evaluate( _.isEqual(left, right), - JSON.stringify(left) + ' does not equal ' + JSON.stringify(right)); + JSON.stringify(left) + ' does not equal ' + JSON.stringify(right) + ); } export function expectMatches(input: string, regexp) { return evaluate( input.match(regexp), - "Input '" + input + "' did not match regexp '" + regexp + "'"); + "Input '" + input + "' did not match regexp '" + regexp + "'" + ); } export function expectReject(f) { - return function (event) { + return function(event) { return Promise.resolve() .then(() => f(event)) .then( - () => { - throw new Error('Test should have returned a rejected promise'); - }, - () => true, // A rejection is what we expected, and so is a positive result. - ); + () => { + throw new Error('Test should have returned a rejected promise'); + }, + () => true // A rejection is what we expected, and so is a positive result. + ); }; } diff --git a/package.json b/package.json index 85f2828c0..316ef0422 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "1.1.0", + "version": "1.0.4", "description": "Firebase SDK for Cloud Functions", "main": "lib/index.js", "scripts": { @@ -8,8 +8,10 @@ "build:pack": "rm -rf lib && npm install && node_modules/.bin/tsc -p tsconfig.release.json && npm pack", "build:release": "npm install --production && npm install typescript firebase-admin && node_modules/.bin/tsc -p tsconfig.release.json", "lint": "node_modules/.bin/tslint src/{**/*,*}.ts spec/{**/*,*}.ts integration_test/functions/src/{**/*,*}.ts", + "format": "prettier --write '**/*.ts'", "pretest": "node_modules/.bin/tsc && cp -r spec/fixtures .tmp/spec", - "test": "mocha .tmp/spec/index.spec.js", + "test": "npm run mocha", + "mocha": "mocha .tmp/spec/index.spec.js", "posttest": "npm run lint && rm -rf .tmp", "postinstall": "node ./upgrade-warning" }, @@ -44,9 +46,10 @@ "mocha": "^2.4.5", "mock-require": "^2.0.1", "nock": "^9.0.0", + "prettier": "^1.13.7", "sinon": "^1.17.4", - "tslint": "^3.15.1", - "typescript": "~2.8.3" + "typescript": "~2.8.3", + "tslint": "^3.15.1" }, "peerDependencies": { "firebase-admin": "~5.12.1" diff --git a/spec/apps.spec.ts b/spec/apps.spec.ts index 20c654cd0..9cf0a35cc 100644 --- a/spec/apps.spec.ts +++ b/spec/apps.spec.ts @@ -32,7 +32,7 @@ describe('apps', () => { beforeEach(() => { apps = new appsNamespace.Apps(); // mock claims intentionally contains dots, square brackets, and nested paths - claims = {'token': {'firebase': {'identities':{'google.com':['111']}}}}; + claims = { token: { firebase: { identities: { 'google.com': ['111'] } } } }; }); afterEach(() => { @@ -125,18 +125,20 @@ describe('apps', () => { apps.retain(); apps.release(); clock.tick(appsNamespace.garbageCollectionInterval / 2); - return Promise.resolve().then(() => { - // Counters are still 1 due second set of retain/release - expect(apps['_refCounter']).to.deep.equal({ - __admin__: 1, - }); - clock.tick(appsNamespace.garbageCollectionInterval / 2); - }).then(() => { - // It's now been a full interval since the second set of retain/release - expect(apps['_refCounter']).to.deep.equal({ - __admin__: 0, + return Promise.resolve() + .then(() => { + // Counters are still 1 due second set of retain/release + expect(apps['_refCounter']).to.deep.equal({ + __admin__: 1, + }); + clock.tick(appsNamespace.garbageCollectionInterval / 2); + }) + .then(() => { + // It's now been a full interval since the second set of retain/release + expect(apps['_refCounter']).to.deep.equal({ + __admin__: 0, + }); }); - }); }); }); }); diff --git a/spec/cloud-functions.spec.ts b/spec/cloud-functions.spec.ts index 7566ea494..7f3564339 100644 --- a/spec/cloud-functions.spec.ts +++ b/spec/cloud-functions.spec.ts @@ -22,8 +22,14 @@ import * as _ from 'lodash'; import { expect } from 'chai'; -import { Event, EventContext, LegacyEvent, - makeCloudFunction, MakeCloudFunctionArgs, Change } from '../src/cloud-functions'; +import { + Event, + EventContext, + LegacyEvent, + makeCloudFunction, + MakeCloudFunctionArgs, + Change, +} from '../src/cloud-functions'; describe('makeCloudFunction', () => { const cloudFunctionArgs: MakeCloudFunctionArgs = { @@ -46,7 +52,9 @@ describe('makeCloudFunction', () => { }); it('should construct the right context for legacy event format', () => { - let args: any = _.assign({}, cloudFunctionArgs, {handler: (data: any, context: EventContext) => context}); + let args: any = _.assign({}, cloudFunctionArgs, { + handler: (data: any, context: EventContext) => context, + }); let cf = makeCloudFunction(args); let test: LegacyEvent = { eventId: '00000', @@ -69,7 +77,9 @@ describe('makeCloudFunction', () => { }); it('should construct the right context for new event format', () => { - let args: any = _.assign({}, cloudFunctionArgs, { handler: (data: any, context: EventContext) => context }); + let args: any = _.assign({}, cloudFunctionArgs, { + handler: (data: any, context: EventContext) => context, + }); let cf = makeCloudFunction(args); let test: Event = { context: { @@ -225,7 +235,9 @@ describe('Change', () => { it('should handle deleted values', () => { const sparseBefore = { baz: 'qux' }; const fieldMask = 'baz'; - expect(Change.applyFieldMask(sparseBefore, after, fieldMask)).to.deep.equal( { + expect( + Change.applyFieldMask(sparseBefore, after, fieldMask) + ).to.deep.equal({ foo: 'bar', num: 2, obj: { @@ -239,7 +251,9 @@ describe('Change', () => { it('should handle created values', () => { const sparseBefore = {}; const fieldMask = 'num,obj.a'; - expect(Change.applyFieldMask(sparseBefore, after, fieldMask)).to.deep.equal({ + expect( + Change.applyFieldMask(sparseBefore, after, fieldMask) + ).to.deep.equal({ foo: 'bar', obj: { b: 2, @@ -255,7 +269,9 @@ describe('Change', () => { }, }; const fieldMask = 'num,obj.a'; - expect(Change.applyFieldMask(sparseBefore, after, fieldMask)).to.deep.equal({ + expect( + Change.applyFieldMask(sparseBefore, after, fieldMask) + ).to.deep.equal({ foo: 'bar', num: 3, obj: { @@ -287,7 +303,7 @@ describe('Change', () => { before: { foo: 'bar' }, after: { foo: 'faz' }, }, - customizer, + customizer ); expect(created.before).to.deep.equal({ foo: 'bar', diff --git a/spec/config.spec.ts b/spec/config.spec.ts index aae3ea94d..c72efa633 100644 --- a/spec/config.spec.ts +++ b/spec/config.spec.ts @@ -25,7 +25,6 @@ import { expect } from 'chai'; import { config, firebaseConfig } from '../src/config'; describe('config()', () => { - afterEach(() => { mockRequire.stopAll(); delete config.singleton; @@ -38,7 +37,7 @@ describe('config()', () => { mockRequire('../../../.runtimeconfig.json', { foo: 'bar', firebase: {} }); let loaded = config(); expect(loaded).to.not.have.property('firebase'); - expect(loaded).to.have.property('foo','bar'); + expect(loaded).to.have.property('foo', 'bar'); }); it('does not provide firebase config if .runtimeconfig.json not invalid', () => { @@ -55,14 +54,20 @@ describe('config()', () => { process.env.FIREBASE_PROJECT = JSON.stringify({ databaseURL: 'foo@firebaseio.com', }); - expect(firebaseConfig()).to.have.property('databaseURL', 'foo@firebaseio.com'); + expect(firebaseConfig()).to.have.property( + 'databaseURL', + 'foo@firebaseio.com' + ); }); it('loads Firebase configs from FIREBASE_CONFIG env variable', () => { process.env.FIREBASE_CONFIG = JSON.stringify({ databaseURL: 'foo@firebaseio.com', }); - expect(firebaseConfig()).to.have.property('databaseURL', 'foo@firebaseio.com'); + expect(firebaseConfig()).to.have.property( + 'databaseURL', + 'foo@firebaseio.com' + ); }); it('prefers FIREBASE_CONFIG over FIREBASE_PROJECT', () => { @@ -85,7 +90,10 @@ describe('config()', () => { }, foo: 'bar', }); - expect(firebaseConfig()).to.have.property('databaseURL', 'foo@firebaseio.com'); + expect(firebaseConfig()).to.have.property( + 'databaseURL', + 'foo@firebaseio.com' + ); expect(config()).to.have.property('foo', 'bar'); }); @@ -93,11 +101,14 @@ describe('config()', () => { process.env.CLOUD_RUNTIME_CONFIG = 'another.json'; mockRequire('another.json', { foo: 'bar', firebase: {} }); expect(firebaseConfig()).to.not.be.null; - expect(config()).to.have.property('foo','bar'); + expect(config()).to.have.property('foo', 'bar'); }); it('accepts full JSON in env.CLOUD_RUNTIME_CONFIG', () => { - process.env.CLOUD_RUNTIME_CONFIG = JSON.stringify({foo: 'bar', firebase:{} }); + process.env.CLOUD_RUNTIME_CONFIG = JSON.stringify({ + foo: 'bar', + firebase: {}, + }); expect(firebaseConfig()).to.not.be.null; expect(config()).to.have.property('foo', 'bar'); }); @@ -107,7 +118,10 @@ describe('config()', () => { process.env.FIREBASE_PROJECT = JSON.stringify({ databaseURL: 'foo@firebaseio.com', }); - expect(firebaseConfig()).to.have.property('databaseURL', 'foo@firebaseio.com'); + expect(firebaseConfig()).to.have.property( + 'databaseURL', + 'foo@firebaseio.com' + ); expect(config()).to.have.property('foo', 'bar'); }); }); diff --git a/spec/fixtures/credential/key.d.ts b/spec/fixtures/credential/key.d.ts index fc5556fa8..1c1e4dbe7 100644 --- a/spec/fixtures/credential/key.d.ts +++ b/spec/fixtures/credential/key.d.ts @@ -1,5 +1,5 @@ /* tslint:disable */ -declare module "*key.json" { +declare module '*key.json' { const type: string; const user_id: string; const project_id: string; diff --git a/spec/fixtures/http.ts b/spec/fixtures/http.ts index d88a163c0..ae9545780 100644 --- a/spec/fixtures/http.ts +++ b/spec/fixtures/http.ts @@ -39,14 +39,15 @@ export function mockRCVariableFetch( data: any, token: string = 'thetoken' ): nock.Scope { - let mock: nock.Scope = nock('https://runtimeconfig.googleapis.com') - .get(`/v1beta1/projects/${projectId}/configs/firebase/variables/${varName}`); + let mock: nock.Scope = nock('https://runtimeconfig.googleapis.com').get( + `/v1beta1/projects/${projectId}/configs/firebase/variables/${varName}` + ); if (token) { mock = mock.matchHeader('Authorization', `Bearer ${token}`); } - return mock.reply(200, {text: JSON.stringify(data)}); + return mock.reply(200, { text: JSON.stringify(data) }); } export function mockMetaVariableWatch( @@ -55,8 +56,9 @@ export function mockMetaVariableWatch( token: string = 'thetoken', updateTime: string = new Date().toISOString() ): nock.Scope { - let mock: nock.Scope = nock('https://runtimeconfig.googleapis.com') - .post(`/v1beta1/projects/${projectId}/configs/firebase/variables/meta:watch`); + let mock: nock.Scope = nock('https://runtimeconfig.googleapis.com').post( + `/v1beta1/projects/${projectId}/configs/firebase/variables/meta:watch` + ); if (token) { mock = mock.matchHeader('Authorization', `Bearer ${token}`); @@ -69,9 +71,14 @@ export function mockMetaVariableWatch( }); } -export function mockMetaVariableWatchTimeout(projectId: string, delay: number, token?: string): nock.Scope { - let mock: nock.Scope = nock('https://runtimeconfig.googleapis.com') - .post(`/v1beta1/projects/${projectId}/configs/firebase/variables/meta:watch`); +export function mockMetaVariableWatchTimeout( + projectId: string, + delay: number, + token?: string +): nock.Scope { + let mock: nock.Scope = nock('https://runtimeconfig.googleapis.com').post( + `/v1beta1/projects/${projectId}/configs/firebase/variables/meta:watch` + ); if (token) { mock = mock.matchHeader('Authorization', `Bearer ${token}`); @@ -80,18 +87,29 @@ export function mockMetaVariableWatchTimeout(projectId: string, delay: number, t return mock.delay(delay).reply(502); } -export function mockCreateToken(token: AccessToken = {access_token: 'aToken', expires_in: 3600}): nock.Scope { - let mock: nock.Scope = nock('https://accounts.google.com').post('/o/oauth2/token'); +export function mockCreateToken( + token: AccessToken = { access_token: 'aToken', expires_in: 3600 } +): nock.Scope { + let mock: nock.Scope = nock('https://accounts.google.com').post( + '/o/oauth2/token' + ); return mock.reply(200, token); } -export function mockRefreshToken(token: AccessToken = {access_token: 'aToken', expires_in: 3600}): nock.Scope { - let mock: nock.Scope = nock('https://www.googleapis.com').post('/oauth2/v4/token'); +export function mockRefreshToken( + token: AccessToken = { access_token: 'aToken', expires_in: 3600 } +): nock.Scope { + let mock: nock.Scope = nock('https://www.googleapis.com').post( + '/oauth2/v4/token' + ); return mock.reply(200, token); } -export function mockMetadataServiceToken(token: AccessToken = {access_token: 'aToken', expires_in: 3600}): nock.Scope { - let mock: nock.Scope = nock('http://metadata.google.internal') - .get('/computeMetadata/v1beta1/instance/service-accounts/default/token'); +export function mockMetadataServiceToken( + token: AccessToken = { access_token: 'aToken', expires_in: 3600 } +): nock.Scope { + let mock: nock.Scope = nock('http://metadata.google.internal').get( + '/computeMetadata/v1beta1/instance/service-accounts/default/token' + ); return mock.reply(200, token); } diff --git a/spec/providers/analytics.spec.ts b/spec/providers/analytics.spec.ts index cef1d8328..e2a58081d 100644 --- a/spec/providers/analytics.spec.ts +++ b/spec/providers/analytics.spec.ts @@ -40,7 +40,8 @@ describe('Analytics Functions', () => { const cloudFunction = analytics.event('first_open').onLog(() => null); expect(cloudFunction.__trigger).to.deep.equal({ eventTrigger: { - eventType: 'providers/google.firebase.analytics/eventTypes/event.log', + eventType: + 'providers/google.firebase.analytics/eventTypes/event.log', resource: 'projects/project1/events/first_open', service: 'app-measurement.com', }, @@ -50,7 +51,9 @@ describe('Analytics Functions', () => { describe('#dataConstructor', () => { it('should handle an event with the appropriate fields', () => { - const cloudFunction = analytics.event('first_open').onLog((data: analytics.AnalyticsEvent) => data); + const cloudFunction = analytics + .event('first_open') + .onLog((data: analytics.AnalyticsEvent) => data); // The event data delivered over the wire will be the JSON for an AnalyticsEvent: // https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data @@ -75,15 +78,16 @@ describe('Analytics Functions', () => { }); it('should remove xValues', () => { - const cloudFunction = analytics.event('first_open').onLog((data: analytics.AnalyticsEvent) => data); + const cloudFunction = analytics + .event('first_open') + .onLog((data: analytics.AnalyticsEvent) => data); // Incoming events will have four kinds of "xValue" fields: "intValue", // "stringValue", "doubleValue" and "floatValue". We expect those to get // flattened away, leaving just their values. let event: LegacyEvent = { data: { - eventDim: - [ + eventDim: [ { date: '20170202', name: 'Loaded_In_Background', @@ -135,12 +139,13 @@ describe('Analytics Functions', () => { }); it('should change microsecond timestamps to ISO strings, and offsets to millis', () => { - const cloudFunction = analytics.event('first_open').onLog((data: analytics.AnalyticsEvent) => data); + const cloudFunction = analytics + .event('first_open') + .onLog((data: analytics.AnalyticsEvent) => data); let event: LegacyEvent = { data: { - eventDim: - [ + eventDim: [ { date: '20170202', name: 'Loaded_In_Background', @@ -183,7 +188,9 @@ describe('Analytics Functions', () => { }); it('should populate currency fields', () => { - const cloudFunction = analytics.event('first_open').onLog((data: analytics.AnalyticsEvent) => data); + const cloudFunction = analytics + .event('first_open') + .onLog((data: analytics.AnalyticsEvent) => data); // Incoming events will have four kinds of "xValue" fields: "intValue", // "stringValue", "doubleValue" and "floatValue". We expect those to get @@ -196,8 +203,7 @@ describe('Analytics Functions', () => { // like to rename and scale down to milliseconds. let event: LegacyEvent = { data: { - eventDim: - [ + eventDim: [ { date: '20170202', name: 'Loaded_In_Background', @@ -211,26 +217,33 @@ describe('Analytics Functions', () => { reportingDate: '20170202', name: 'Loaded_In_Background', params: {}, - valueInUSD: 123.4, // Field renamed Usd -> USD. + valueInUSD: 123.4, // Field renamed Usd -> USD. }); }); it('should recognize all the fields the payload can contain', () => { - const cloudFunction = analytics.event('first_open').onLog((data: analytics.AnalyticsEvent) => data); + const cloudFunction = analytics + .event('first_open') + .onLog((data: analytics.AnalyticsEvent) => data); // The payload in analytics_spec_input contains all possible fields at least once. - return expect(cloudFunction(analytics_spec_input.fullPayload)) - .to.eventually.deep.equal(analytics_spec_input.data); + return expect( + cloudFunction(analytics_spec_input.fullPayload) + ).to.eventually.deep.equal(analytics_spec_input.data); }); }); }); describe('process.env.GCLOUD_PROJECT not set', () => { it('should not throw if __trigger is not accessed', () => { - expect(() => analytics.event('event').onLog(() => null)).to.not.throw(Error); + expect(() => analytics.event('event').onLog(() => null)).to.not.throw( + Error + ); }); it('should throw when trigger is accessed', () => { - expect(() => analytics.event('event').onLog(() => null).__trigger).to.throw(Error); + expect( + () => analytics.event('event').onLog(() => null).__trigger + ).to.throw(Error); }); it('should not throw when #run is called', () => { diff --git a/spec/providers/auth.spec.ts b/spec/providers/auth.spec.ts index 989f4cf22..603b0e00e 100644 --- a/spec/providers/auth.spec.ts +++ b/spec/providers/auth.spec.ts @@ -69,8 +69,12 @@ describe('Auth Functions', () => { let event: any; before(() => { - cloudFunctionCreate = auth.user().onCreate((data: firebase.auth.UserRecord) => data); - cloudFunctionDelete = auth.user().onDelete((data: firebase.auth.UserRecord) => data); + cloudFunctionCreate = auth + .user() + .onCreate((data: firebase.auth.UserRecord) => data); + cloudFunctionDelete = auth + .user() + .onDelete((data: firebase.auth.UserRecord) => data); event = { data: { metadata: { @@ -84,12 +88,20 @@ describe('Auth Functions', () => { it('should transform wire format for UserRecord into v5.0.0 format', () => { return Promise.all([ cloudFunctionCreate(event).then((data: any) => { - expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); - expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); + expect(data.metadata.creationTime).to.equal( + '2016-12-15T19:37:37.059Z' + ); + expect(data.metadata.lastSignInTime).to.equal( + '2017-01-01T00:00:00.000Z' + ); }), cloudFunctionDelete(event).then((data: any) => { - expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); - expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); + expect(data.metadata.creationTime).to.equal( + '2016-12-15T19:37:37.059Z' + ); + expect(data.metadata.lastSignInTime).to.equal( + '2017-01-01T00:00:00.000Z' + ); }), ]); }); @@ -106,12 +118,20 @@ describe('Auth Functions', () => { return Promise.all([ cloudFunctionCreate(newEvent).then((data: any) => { - expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); - expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); + expect(data.metadata.creationTime).to.equal( + '2016-12-15T19:37:37.059Z' + ); + expect(data.metadata.lastSignInTime).to.equal( + '2017-01-01T00:00:00.000Z' + ); }), cloudFunctionDelete(newEvent).then((data: any) => { - expect(data.metadata.creationTime).to.equal('2016-12-15T19:37:37.059Z'); - expect(data.metadata.lastSignInTime).to.equal('2017-01-01T00:00:00.000Z'); + expect(data.metadata.creationTime).to.equal( + '2016-12-15T19:37:37.059Z' + ); + expect(data.metadata.lastSignInTime).to.equal( + '2017-01-01T00:00:00.000Z' + ); }), ]); }); @@ -120,7 +140,7 @@ describe('Auth Functions', () => { describe('userRecordConstructor', () => { it('will provide falsey values for fields that are not in raw wire data', () => { - const record = auth.userRecordConstructor({ uid: '123'}); + const record = auth.userRecordConstructor({ uid: '123' }); expect(record.toJSON()).to.deep.equal({ uid: '123', email: null, diff --git a/spec/providers/crashlytics.spec.ts b/spec/providers/crashlytics.spec.ts index 2b678eefb..517324590 100644 --- a/spec/providers/crashlytics.spec.ts +++ b/spec/providers/crashlytics.spec.ts @@ -54,7 +54,8 @@ describe('Crashlytics Functions', () => { const cloudFunction = crashlytics.issue().onRegressed(data => null); expect(cloudFunction.__trigger).to.deep.equal({ eventTrigger: { - eventType: 'providers/firebase.crashlytics/eventTypes/issue.regressed', + eventType: + 'providers/firebase.crashlytics/eventTypes/issue.regressed', resource: 'projects/project1', service: 'fabric.io', }, @@ -67,7 +68,8 @@ describe('Crashlytics Functions', () => { const cloudFunction = crashlytics.issue().onVelocityAlert(data => null); expect(cloudFunction.__trigger).to.deep.equal({ eventTrigger: { - eventType: 'providers/firebase.crashlytics/eventTypes/issue.velocityAlert', + eventType: + 'providers/firebase.crashlytics/eventTypes/issue.velocityAlert', resource: 'projects/project1', service: 'fabric.io', }, @@ -82,7 +84,9 @@ describe('Crashlytics Functions', () => { }); it('should throw if __trigger is accessed', () => { - expect(() => crashlytics.issue().onNew(() => null).__trigger).to.throw(Error); + expect(() => crashlytics.issue().onNew(() => null).__trigger).to.throw( + Error + ); }); it('should not throw when #run is called', () => { diff --git a/spec/providers/database.spec.ts b/spec/providers/database.spec.ts index d0cc20f9e..55f8c2b4d 100644 --- a/spec/providers/database.spec.ts +++ b/spec/providers/database.spec.ts @@ -21,12 +21,11 @@ // SOFTWARE. import * as database from '../../src/providers/database'; -import { expect as expect } from 'chai'; +import { expect } from 'chai'; import { apps as appsNamespace } from '../../src/apps'; import { applyChange } from '../../src/utils'; describe('Database Functions', () => { - describe('DatabaseBuilder', () => { // TODO add tests for building a data or change based on the type of operation @@ -44,17 +43,24 @@ describe('Database Functions', () => { describe('#onWrite()', () => { it('should return "ref.write" as the event type', () => { - let eventType = database.ref('foo').onWrite(() => null).__trigger.eventTrigger.eventType; - expect(eventType).to.eq('providers/google.firebase.database/eventTypes/ref.write'); + let eventType = database.ref('foo').onWrite(() => null).__trigger + .eventTrigger.eventType; + expect(eventType).to.eq( + 'providers/google.firebase.database/eventTypes/ref.write' + ); }); it('should construct a proper resource path', () => { - let resource = database.ref('foo').onWrite(() => null).__trigger.eventTrigger.resource; + let resource = database.ref('foo').onWrite(() => null).__trigger + .eventTrigger.resource; expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); }); it('should let developers choose a database instance', () => { - let func = database.instance('custom').ref('foo').onWrite(() => null); + let func = database + .instance('custom') + .ref('foo') + .onWrite(() => null); let resource = func.__trigger.eventTrigger.resource; expect(resource).to.eq('projects/_/instances/custom/refs/foo'); }); @@ -77,17 +83,24 @@ describe('Database Functions', () => { describe('#onCreate()', () => { it('should return "ref.create" as the event type', () => { - let eventType = database.ref('foo').onCreate(() => null).__trigger.eventTrigger.eventType; - expect(eventType).to.eq('providers/google.firebase.database/eventTypes/ref.create'); + let eventType = database.ref('foo').onCreate(() => null).__trigger + .eventTrigger.eventType; + expect(eventType).to.eq( + 'providers/google.firebase.database/eventTypes/ref.create' + ); }); it('should construct a proper resource path', () => { - let resource = database.ref('foo').onCreate(() => null).__trigger.eventTrigger.resource; + let resource = database.ref('foo').onCreate(() => null).__trigger + .eventTrigger.resource; expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); }); it('should let developers choose a database instance', () => { - let func = database.instance('custom').ref('foo').onCreate(() => null); + let func = database + .instance('custom') + .ref('foo') + .onCreate(() => null); let resource = func.__trigger.eventTrigger.resource; expect(resource).to.eq('projects/_/instances/custom/refs/foo'); }); @@ -110,17 +123,24 @@ describe('Database Functions', () => { describe('#onUpdate()', () => { it('should return "ref.update" as the event type', () => { - let eventType = database.ref('foo').onUpdate(() => null).__trigger.eventTrigger.eventType; - expect(eventType).to.eq('providers/google.firebase.database/eventTypes/ref.update'); + let eventType = database.ref('foo').onUpdate(() => null).__trigger + .eventTrigger.eventType; + expect(eventType).to.eq( + 'providers/google.firebase.database/eventTypes/ref.update' + ); }); it('should construct a proper resource path', () => { - let resource = database.ref('foo').onUpdate(() => null).__trigger.eventTrigger.resource; + let resource = database.ref('foo').onUpdate(() => null).__trigger + .eventTrigger.resource; expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); }); it('should let developers choose a database instance', () => { - let func = database.instance('custom').ref('foo').onUpdate(() => null); + let func = database + .instance('custom') + .ref('foo') + .onUpdate(() => null); let resource = func.__trigger.eventTrigger.resource; expect(resource).to.eq('projects/_/instances/custom/refs/foo'); }); @@ -143,17 +163,24 @@ describe('Database Functions', () => { describe('#onDelete()', () => { it('should return "ref.delete" as the event type', () => { - let eventType = database.ref('foo').onDelete(() => null).__trigger.eventTrigger.eventType; - expect(eventType).to.eq('providers/google.firebase.database/eventTypes/ref.delete'); + let eventType = database.ref('foo').onDelete(() => null).__trigger + .eventTrigger.eventType; + expect(eventType).to.eq( + 'providers/google.firebase.database/eventTypes/ref.delete' + ); }); it('should construct a proper resource path', () => { - let resource = database.ref('foo').onDelete(() => null).__trigger.eventTrigger.resource; + let resource = database.ref('foo').onDelete(() => null).__trigger + .eventTrigger.resource; expect(resource).to.eq('projects/_/instances/subdomain/refs/foo'); }); it('should let developers choose a database instance', () => { - let func = database.instance('custom').ref('foo').onDelete(() => null); + let func = database + .instance('custom') + .ref('foo') + .onDelete(() => null); let resource = func.__trigger.eventTrigger.resource; expect(resource).to.eq('projects/_/instances/custom/refs/foo'); }); @@ -173,16 +200,19 @@ describe('Database Functions', () => { }); }); }); - }); describe('process.env.FIREBASE_CONFIG not set', () => { it('should not throw if __trigger is not accessed', () => { - expect(() => database.ref('/path').onWrite(() => null)).to.not.throw(Error); + expect(() => database.ref('/path').onWrite(() => null)).to.not.throw( + Error + ); }); it('should throw when trigger is accessed', () => { - expect(() => database.ref('/path').onWrite(() => null).__trigger).to.throw(Error); + expect( + () => database.ref('/path').onWrite(() => null).__trigger + ).to.throw(Error); }); it('should not throw when #run is called', () => { @@ -193,7 +223,9 @@ describe('Database Functions', () => { describe('resourceToInstanceAndPath', () => { it('should return the correct instance and path strings', () => { - let [instance, path] = database.resourceToInstanceAndPath('projects/_/instances/foo/refs/bar'); + let [instance, path] = database.resourceToInstanceAndPath( + 'projects/_/instances/foo/refs/bar' + ); expect(instance).to.equal('https://foo.firebaseio.com'); expect(path).to.equal('/bar'); }); @@ -204,19 +236,18 @@ describe('Database Functions', () => { const apps = new appsNamespace.Apps(); let populate = (data: any) => { - let [instance, path] = database.resourceToInstanceAndPath('projects/_/instances/other-subdomain/refs/foo'); - subject = new database.DataSnapshot( - data, - path, - apps.admin, - instance + let [instance, path] = database.resourceToInstanceAndPath( + 'projects/_/instances/other-subdomain/refs/foo' ); + subject = new database.DataSnapshot(data, path, apps.admin, instance); }; describe('#ref: firebase.database.Reference', () => { it('should return a ref for correct instance, not the default instance', () => { populate({}); - expect(subject.ref.toJSON()).to.equal('https://other-subdomain.firebaseio.com/foo'); + expect(subject.ref.toJSON()).to.equal( + 'https://other-subdomain.firebaseio.com/foo' + ); }); }); @@ -244,7 +275,7 @@ describe('Database Functions', () => { expect(subject.val()).to.deep.equal(['a', 'b', { c: 'd' }]); populate({ 0: 'a', 2: 'b', 3: { c: 'd' } }); expect(subject.val()).to.deep.equal(['a', , 'b', { c: 'd' }]); - populate({ 'foo': { 0: 'a', 1: 'b' } }); + populate({ foo: { 0: 'a', 1: 'b' } }); expect(subject.val()).to.deep.equal({ foo: ['a', 'b'] }); }); @@ -262,15 +293,20 @@ describe('Database Functions', () => { // Regression test: .val() was returning array of nulls when there's a property called length (BUG#37683995) it('should return correct values when data has "length" property', () => { - populate({ length: 3, foo: 'bar' }); - expect(subject.val()).to.deep.equal({ length: 3, foo: 'bar'}); + populate({ length: 3, foo: 'bar' }); + expect(subject.val()).to.deep.equal({ length: 3, foo: 'bar' }); }); }); describe('#child(): DataSnapshot', () => { it('should work with multiple calls', () => { populate({ a: { b: { c: 'd' } } }); - expect(subject.child('a').child('b/c').val()).to.equal('d'); + expect( + subject + .child('a') + .child('b/c') + .val() + ).to.equal('d'); }); }); @@ -391,7 +427,9 @@ describe('Database Functions', () => { }); it('should return null for the root', () => { - let [instance, path] = database.resourceToInstanceAndPath('projects/_/instances/foo/refs/'); + let [instance, path] = database.resourceToInstanceAndPath( + 'projects/_/instances/foo/refs/' + ); const snapshot = new database.DataSnapshot( null, path, diff --git a/spec/providers/firestore.spec.ts b/spec/providers/firestore.spec.ts index a8a44c1fa..cdd9d4c0e 100644 --- a/spec/providers/firestore.spec.ts +++ b/spec/providers/firestore.spec.ts @@ -26,10 +26,10 @@ import { expect } from 'chai'; describe('Firestore Functions', () => { let constructValue = (fields: any) => { return { - 'fields': fields, - 'name': 'projects/pid/databases/(default)/documents/collection/123', - 'createTime': '2017-06-02T18:48:58.920638Z', - 'updateTime': '2017-07-02T18:48:58.920638Z', + fields: fields, + name: 'projects/pid/databases/(default)/documents/collection/123', + createTime: '2017-06-02T18:48:58.920638Z', + updateTime: '2017-07-02T18:48:58.920638Z', }; }; @@ -53,55 +53,92 @@ describe('Firestore Functions', () => { }); it('should allow terse constructors', () => { - let resource = 'projects/project1/databases/(default)/documents/users/{uid}'; + let resource = + 'projects/project1/databases/(default)/documents/users/{uid}'; let cloudFunction = firestore.document('users/{uid}').onWrite(() => null); - expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger(resource, 'document.write')); + expect(cloudFunction.__trigger).to.deep.equal( + expectedTrigger(resource, 'document.write') + ); }); it('should allow custom namespaces', () => { - let resource = 'projects/project1/databases/(default)/documents@v2/users/{uid}'; - let cloudFunction = firestore.namespace('v2').document('users/{uid}').onWrite(() => null); - expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger(resource, 'document.write')); + let resource = + 'projects/project1/databases/(default)/documents@v2/users/{uid}'; + let cloudFunction = firestore + .namespace('v2') + .document('users/{uid}') + .onWrite(() => null); + expect(cloudFunction.__trigger).to.deep.equal( + expectedTrigger(resource, 'document.write') + ); }); it('should allow custom databases', () => { let resource = 'projects/project1/databases/myDB/documents/users/{uid}'; - let cloudFunction = firestore.database('myDB').document('users/{uid}').onWrite(() => null); - expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger(resource, 'document.write')); + let cloudFunction = firestore + .database('myDB') + .document('users/{uid}') + .onWrite(() => null); + expect(cloudFunction.__trigger).to.deep.equal( + expectedTrigger(resource, 'document.write') + ); }); it('should allow both custom database and namespace', () => { - let resource = 'projects/project1/databases/myDB/documents@v2/users/{uid}'; - let cloudFunction = firestore.database('myDB').namespace('v2').document('users/{uid}').onWrite(() => null); - expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger(resource, 'document.write')); + let resource = + 'projects/project1/databases/myDB/documents@v2/users/{uid}'; + let cloudFunction = firestore + .database('myDB') + .namespace('v2') + .document('users/{uid}') + .onWrite(() => null); + expect(cloudFunction.__trigger).to.deep.equal( + expectedTrigger(resource, 'document.write') + ); }); it('onCreate should have the "document.create" eventType', () => { - let resource = 'projects/project1/databases/(default)/documents/users/{uid}'; - let eventType = firestore.document('users/{uid}').onCreate(() => null).__trigger.eventTrigger.eventType; - expect(eventType).to.eq(expectedTrigger(resource, 'document.create').eventTrigger.eventType); + let resource = + 'projects/project1/databases/(default)/documents/users/{uid}'; + let eventType = firestore.document('users/{uid}').onCreate(() => null) + .__trigger.eventTrigger.eventType; + expect(eventType).to.eq( + expectedTrigger(resource, 'document.create').eventTrigger.eventType + ); }); it('onUpdate should have the "document.update" eventType', () => { - let resource = 'projects/project1/databases/(default)/documents/users/{uid}'; - let eventType = firestore.document('users/{uid}').onUpdate(() => null).__trigger.eventTrigger.eventType; - expect(eventType).to.eq(expectedTrigger(resource, 'document.update').eventTrigger.eventType); + let resource = + 'projects/project1/databases/(default)/documents/users/{uid}'; + let eventType = firestore.document('users/{uid}').onUpdate(() => null) + .__trigger.eventTrigger.eventType; + expect(eventType).to.eq( + expectedTrigger(resource, 'document.update').eventTrigger.eventType + ); }); it('onDelete should have the "document.delete" eventType', () => { - let resource = 'projects/project1/databases/(default)/documents/users/{uid}'; - let eventType = firestore.document('users/{uid}').onDelete(() => null).__trigger.eventTrigger.eventType; - expect(eventType).to.eq(expectedTrigger(resource, 'document.delete').eventTrigger.eventType); + let resource = + 'projects/project1/databases/(default)/documents/users/{uid}'; + let eventType = firestore.document('users/{uid}').onDelete(() => null) + .__trigger.eventTrigger.eventType; + expect(eventType).to.eq( + expectedTrigger(resource, 'document.delete').eventTrigger.eventType + ); }); }); describe('process.env.GCLOUD_PROJECT not set', () => { it('should not throw if __trigger is not accessed', () => { - expect(() => firestore.document('input').onCreate(() => null)).to.not.throw(Error); + expect(() => + firestore.document('input').onCreate(() => null) + ).to.not.throw(Error); }); it('should throw when trigger is accessed', () => { - expect(() => firestore.document('input').onCreate(() => null).__trigger).to.throw(Error); + expect( + () => firestore.document('input').onCreate(() => null).__trigger + ).to.throw(Error); }); it('should not throw when #run is called', () => { @@ -111,53 +148,61 @@ describe('Firestore Functions', () => { }); describe('dataConstructor', () => { - function constructEvent(oldValue: object, value: object, eventType: string) { + function constructEvent( + oldValue: object, + value: object, + eventType: string + ) { return { - 'data': { - 'oldValue': oldValue, - 'value': value, + data: { + oldValue: oldValue, + value: value, }, - 'context': {}, + context: {}, }; } function createOldValue() { return constructValue({ - 'key1': { - 'booleanValue': false, + key1: { + booleanValue: false, }, - 'key2': { - 'integerValue': '111', + key2: { + integerValue: '111', }, }); } function createValue() { return constructValue({ - 'key1': { - 'booleanValue': true, + key1: { + booleanValue: true, }, - 'key2': { - 'integerValue': '123', + key2: { + integerValue: '123', }, }); } it('constructs appropriate fields and getters for event.data on "document.write" events', () => { - let testFunction = firestore.document('path').onWrite((change) => { - expect(change.before.data()).to.deep.equal({key1: false, key2: 111}); + let testFunction = firestore.document('path').onWrite(change => { + expect(change.before.data()).to.deep.equal({ key1: false, key2: 111 }); expect(change.before.get('key1')).to.equal(false); - expect(change.after.data()).to.deep.equal({key1: true, key2: 123}); + expect(change.after.data()).to.deep.equal({ key1: true, key2: 123 }); expect(change.after.get('key1')).to.equal(true); return true; // otherwise will get warning about returning undefined }); - let data = constructEvent(createOldValue(), createValue(), 'document.write'); + let data = constructEvent( + createOldValue(), + createValue(), + 'document.write' + ); return testFunction(data); }).timeout(5000); it('constructs appropriate fields and getters for event.data on "document.create" events', () => { - let testFunction = firestore.document('path').onCreate((data) => { - expect(data.data()).to.deep.equal({key1: true, key2: 123}); + let testFunction = firestore.document('path').onCreate(data => { + expect(data.data()).to.deep.equal({ key1: true, key2: 123 }); expect(data.get('key1')).to.equal(true); return true; // otherwise will get warning about returning undefined }); @@ -166,20 +211,24 @@ describe('Firestore Functions', () => { }).timeout(5000); it('constructs appropriate fields and getters for event.data on "document.update" events', () => { - let testFunction = firestore.document('path').onUpdate((change) => { - expect(change.before.data()).to.deep.equal({key1: false, key2: 111}); + let testFunction = firestore.document('path').onUpdate(change => { + expect(change.before.data()).to.deep.equal({ key1: false, key2: 111 }); expect(change.before.get('key1')).to.equal(false); - expect(change.after.data()).to.deep.equal({key1: true, key2: 123}); + expect(change.after.data()).to.deep.equal({ key1: true, key2: 123 }); expect(change.after.get('key1')).to.equal(true); return true; // otherwise will get warning about returning undefined }); - let data = constructEvent(createOldValue(), createValue(), 'document.update'); + let data = constructEvent( + createOldValue(), + createValue(), + 'document.update' + ); return testFunction(data); }).timeout(5000); it('constructs appropriate fields and getters for event.data on "document.delete" events', () => { - let testFunction = firestore.document('path').onDelete((data) => { - expect(data.data()).to.deep.equal({key1: false, key2: 111}); + let testFunction = firestore.document('path').onDelete(data => { + expect(data.data()).to.deep.equal({ key1: false, key2: 111 }); expect(data.get('key1')).to.equal(false); return true; // otherwise will get warning about returning undefined }); @@ -193,75 +242,72 @@ describe('Firestore Functions', () => { it('should parse int values', () => { let snapshot = firestore.snapshotConstructor({ data: { - value: constructValue({'key': {'integerValue': '123'}}), + value: constructValue({ key: { integerValue: '123' } }), }, }); - expect(snapshot.data()).to.deep.equal({'key': 123}); + expect(snapshot.data()).to.deep.equal({ key: 123 }); }); it('should parse double values', () => { let snapshot = firestore.snapshotConstructor({ data: { - value: constructValue({'key': {'doubleValue': 12.34}}), + value: constructValue({ key: { doubleValue: 12.34 } }), }, }); - expect(snapshot.data()).to.deep.equal({'key': 12.34}); + expect(snapshot.data()).to.deep.equal({ key: 12.34 }); }); it('should parse null values', () => { let snapshot = firestore.snapshotConstructor({ data: { - value: constructValue({'key': {'nullValue': null}}), + value: constructValue({ key: { nullValue: null } }), }, }); - expect(snapshot.data()).to.deep.equal({'key': null}); + expect(snapshot.data()).to.deep.equal({ key: null }); }); it('should parse boolean values', () => { let snapshot = firestore.snapshotConstructor({ data: { - value: constructValue({'key': {'booleanValue': true}}), + value: constructValue({ key: { booleanValue: true } }), }, }); - expect(snapshot.data()).to.deep.equal({'key': true}); + expect(snapshot.data()).to.deep.equal({ key: true }); }); it('should parse string values', () => { let snapshot = firestore.snapshotConstructor({ data: { - value: constructValue({'key': {'stringValue': 'foo'}}), + value: constructValue({ key: { stringValue: 'foo' } }), }, }); - expect(snapshot.data()).to.deep.equal({'key': 'foo'}); + expect(snapshot.data()).to.deep.equal({ key: 'foo' }); }); it('should parse array values', () => { let raw = constructValue({ - 'key': { - 'arrayValue': { - 'values': [ - { 'integerValue': '1' }, - { 'integerValue': '2' }, - ], + key: { + arrayValue: { + values: [{ integerValue: '1' }, { integerValue: '2' }], }, }, }); let snapshot = firestore.snapshotConstructor({ data: { value: raw }, }); - expect(snapshot.data()).to.deep.equal({'key': [1, 2]}); + expect(snapshot.data()).to.deep.equal({ key: [1, 2] }); }); it('should parse object values', () => { let raw = constructValue({ - 'keyParent': { - 'mapValue': { - 'fields': { - 'key1': { - 'stringValue': 'val1', + keyParent: { + mapValue: { + fields: { + key1: { + stringValue: 'val1', }, - 'key2': { - 'stringValue': 'val2', + key2: { + stringValue: 'val2', }, }, }, @@ -270,19 +316,21 @@ describe('Firestore Functions', () => { let snapshot = firestore.snapshotConstructor({ data: { value: raw }, }); - expect(snapshot.data()).to.deep.equal({'keyParent': {'key1':'val1', 'key2':'val2'}}); + expect(snapshot.data()).to.deep.equal({ + keyParent: { key1: 'val1', key2: 'val2' }, + }); }); it('should parse GeoPoint values', () => { let raw = constructValue({ - 'geoPointValue': { - 'mapValue': { - 'fields': { - 'latitude': { - 'doubleValue': 40.73, + geoPointValue: { + mapValue: { + fields: { + latitude: { + doubleValue: 40.73, }, - 'longitude': { - 'doubleValue': -73.93, + longitude: { + doubleValue: -73.93, }, }, }, @@ -291,16 +339,19 @@ describe('Firestore Functions', () => { let snapshot = firestore.snapshotConstructor({ data: { value: raw }, }); - expect(snapshot.data()).to.deep.equal({'geoPointValue': { - 'latitude': 40.73, - 'longitude': -73.93, - }}); + expect(snapshot.data()).to.deep.equal({ + geoPointValue: { + latitude: 40.73, + longitude: -73.93, + }, + }); }); it('should parse reference values', () => { let raw = constructValue({ - 'referenceVal': { - 'referenceValue': 'projects/proj1/databases/(default)/documents/doc1/id', + referenceVal: { + referenceValue: + 'projects/proj1/databases/(default)/documents/doc1/id', }, }); let snapshot = firestore.snapshotConstructor({ @@ -311,40 +362,45 @@ describe('Firestore Functions', () => { it('should parse timestamp values with precision to the millisecond', () => { let raw = constructValue({ - 'timestampVal': { - 'timestampValue': '2017-06-13T00:58:40.349Z', + timestampVal: { + timestampValue: '2017-06-13T00:58:40.349Z', }, }); let snapshot = firestore.snapshotConstructor({ data: { value: raw }, }); - expect(snapshot.data()).to.deep.equal({'timestampVal': new Date('2017-06-13T00:58:40.349Z')}); + expect(snapshot.data()).to.deep.equal({ + timestampVal: new Date('2017-06-13T00:58:40.349Z'), + }); }); it('should parse timestamp values with precision to the second', () => { let raw = constructValue({ - 'timestampVal': { - 'timestampValue': '2017-06-13T00:58:40Z', + timestampVal: { + timestampValue: '2017-06-13T00:58:40Z', }, }); let snapshot = firestore.snapshotConstructor({ data: { value: raw }, }); - expect(snapshot.data()).to.deep.equal({'timestampVal': new Date('2017-06-13T00:58:40Z')}); - + expect(snapshot.data()).to.deep.equal({ + timestampVal: new Date('2017-06-13T00:58:40Z'), + }); }); it('should parse binary values', () => { // Format defined in https://developers.google.com/discovery/v1/type-format let raw = constructValue({ - 'binaryVal': { - 'bytesValue': 'Zm9vYmFy', + binaryVal: { + bytesValue: 'Zm9vYmFy', }, }); let snapshot = firestore.snapshotConstructor({ data: { value: raw }, }); - expect(snapshot.data()).to.deep.equal({'binaryVal': new Buffer('foobar')}); + expect(snapshot.data()).to.deep.equal({ + binaryVal: new Buffer('foobar'), + }); }); }); @@ -353,13 +409,13 @@ describe('Firestore Functions', () => { before(() => { snapshot = firestore.snapshotConstructor({ - 'data': { - 'value': { - 'fields': {'key': {'integerValue': '1'}}, - 'createTime': '2017-06-17T14:45:17.876479Z', - 'updateTime': '2017-08-31T18:05:26.928527Z', - 'readTime': '2017-07-31T18:23:26.928527Z', - 'name': 'projects/pid/databases/(default)/documents/collection/123', + data: { + value: { + fields: { key: { integerValue: '1' } }, + createTime: '2017-06-17T14:45:17.876479Z', + updateTime: '2017-08-31T18:05:26.928527Z', + readTime: '2017-07-31T18:23:26.928527Z', + name: 'projects/pid/databases/(default)/documents/collection/123', }, }, }); @@ -370,7 +426,10 @@ describe('Firestore Functions', () => { }); it('should support #ref', () => { - expect(Object.keys(snapshot.ref)).to.deep.equal(['_firestore', '_referencePath']); + expect(Object.keys(snapshot.ref)).to.deep.equal([ + '_firestore', + '_referencePath', + ]); expect(snapshot.ref.path).to.equal('collection/123'); }); @@ -379,25 +438,31 @@ describe('Firestore Functions', () => { }); it('should support #createTime', () => { - expect(Date.parse(snapshot.createTime)).to.equal(Date.parse('2017-06-17T14:45:17.876479Z')); + expect(Date.parse(snapshot.createTime)).to.equal( + Date.parse('2017-06-17T14:45:17.876479Z') + ); }); it('should support #updateTime', () => { - expect(Date.parse(snapshot.updateTime)).to.equal(Date.parse('2017-08-31T18:05:26.928527Z')); + expect(Date.parse(snapshot.updateTime)).to.equal( + Date.parse('2017-08-31T18:05:26.928527Z') + ); }); it('should support #readTime', () => { - expect(Date.parse(snapshot.readTime)).to.equal(Date.parse('2017-07-31T18:23:26.928527Z')); + expect(Date.parse(snapshot.readTime)).to.equal( + Date.parse('2017-07-31T18:23:26.928527Z') + ); }); }); describe('Handle empty and non-existent documents', () => { it('constructs non-existent DocumentSnapshot when whole document deleted', () => { let snapshot = firestore.snapshotConstructor({ - 'data': { - 'value': {}, // value is empty when the whole document is deleted + data: { + value: {}, // value is empty when the whole document is deleted }, - 'resource': 'projects/pid/databases/(default)/documents/collection/123', + resource: 'projects/pid/databases/(default)/documents/collection/123', }); expect(snapshot.exists).to.be.false; expect(snapshot.ref.path).to.equal('collection/123'); @@ -405,11 +470,12 @@ describe('Firestore Functions', () => { it('constructs existent DocumentSnapshot with empty data when all fields of document deleted', () => { let snapshot = firestore.snapshotConstructor({ - 'data': { - 'value': { // value is not empty when document still exists - 'createTime': '2017-06-02T18:48:58.920638Z', - 'updateTime': '2017-07-02T18:48:58.920638Z', - 'name': 'projects/pid/databases/(default)/documents/collection/123', + data: { + value: { + // value is not empty when document still exists + createTime: '2017-06-02T18:48:58.920638Z', + updateTime: '2017-07-02T18:48:58.920638Z', + name: 'projects/pid/databases/(default)/documents/collection/123', }, }, }); diff --git a/spec/providers/https.spec.ts b/spec/providers/https.spec.ts index 6e3a89032..cc20c1cb0 100644 --- a/spec/providers/https.spec.ts +++ b/spec/providers/https.spec.ts @@ -36,7 +36,7 @@ describe('CloudHttpsBuilder', () => { let result = https.onRequest((req, resp) => { resp.send(200); }); - expect(result.__trigger).to.deep.equal({httpsTrigger: {}}); + expect(result.__trigger).to.deep.equal({ httpsTrigger: {} }); }); }); }); @@ -46,7 +46,7 @@ describe('CloudHttpsBuilder', () => { */ interface RunHandlerResult { status: number; - headers: {[name: string]: string}; + headers: { [name: string]: string }; body: any; } @@ -73,13 +73,16 @@ interface CallTest { * Runs an express handler with a given request asynchronously and returns the * data populated into the response. */ -function runHandler(handler: express.Handler, request: express.Request): Promise { +function runHandler( + handler: express.Handler, + request: express.Request +): Promise { return new Promise((resolve, reject) => { // MockResponse mocks an express.Response. // This class lives here so it can reference resolve and reject. class MockResponse { private statusCode = 0; - private headers: {[name: string]: string} = {}; + private headers: { [name: string]: string } = {}; public status(code: number) { this.statusCode = code; @@ -129,9 +132,12 @@ async function runTest(test: CallTest): Promise { // MockRequest mocks an express.Request. class MockRequest { - public method: 'POST'|'GET'|'OPTIONS' = 'POST'; + public method: 'POST' | 'GET' | 'OPTIONS' = 'POST'; - constructor(readonly body: any, readonly headers: {[name: string]: string}) { + constructor( + readonly body: any, + readonly headers: { [name: string]: string } + ) { // This block intentionally left blank. } @@ -142,12 +148,13 @@ class MockRequest { // Creates a mock request with the given data and content-type. function request( - data: any, - contentType: string = 'application/json', - context: { - authorization?: string; - instanceIdToken?: string; - } = {}) { + data: any, + contentType: string = 'application/json', + context: { + authorization?: string; + instanceIdToken?: string; + } = {} +) { const body: any = {}; if (!_.isUndefined(data)) { body.data = data; @@ -155,9 +162,9 @@ function request( const headers = { 'content-type': contentType, - 'authorization': context.authorization, + authorization: context.authorization, 'firebase-instance-id-token': context.instanceIdToken, - 'origin': 'example.com', + origin: 'example.com', }; return new MockRequest(body, headers); @@ -173,9 +180,10 @@ const expectedResponseHeaders = { * verifying an id token. */ function mockFetchPublicKeys(): nock.Scope { - let mock: nock.Scope = nock('https://www.googleapis.com:443') - .get('/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'); - const mockedResponse = {[mocks.key_id]: mocks.public_key}; + let mock: nock.Scope = nock('https://www.googleapis.com:443').get( + '/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com' + ); + const mockedResponse = { [mocks.key_id]: mocks.public_key }; const headers = { 'cache-control': 'public, max-age=1, must-revalidate, no-transform', }; @@ -189,7 +197,7 @@ export function generateIdToken(projectId: string): string { const claims = {}; const options = { audience: projectId, - expiresIn: 60 * 60, // 1 hour in seconds + expiresIn: 60 * 60, // 1 hour in seconds issuer: 'https://securetoken.google.com/' + projectId, subject: mocks.user_id, algorithm: 'RS256', @@ -231,7 +239,7 @@ describe('callable.FunctionBuilder', () => { describe('#onCall', () => { it('should return a Trigger with appropriate values', () => { - const result = https.onCall((data) => { + const result = https.onCall(data => { return 'response'; }); expect(result.__trigger).to.deep.equal({ @@ -242,13 +250,13 @@ describe('callable.FunctionBuilder', () => { it('should handle success', () => { return runTest({ - httpRequest: request({foo: 'bar'}), - expectedData: {foo: 'bar'}, - callableFunction: (data, context) => ({baz: 'qux'}), + httpRequest: request({ foo: 'bar' }), + expectedData: { foo: 'bar' }, + callableFunction: (data, context) => ({ baz: 'qux' }), expectedHttpResponse: { status: 200, headers: expectedResponseHeaders, - body: {result: {baz: 'qux'}}, + body: { result: { baz: 'qux' } }, }, }); }); @@ -261,7 +269,7 @@ describe('callable.FunctionBuilder', () => { expectedHttpResponse: { status: 200, headers: expectedResponseHeaders, - body: {result: null}, + body: { result: null }, }, }); }); @@ -270,11 +278,13 @@ describe('callable.FunctionBuilder', () => { return runTest({ httpRequest: request(null), expectedData: null, - callableFunction: (data, context) => { return; }, + callableFunction: (data, context) => { + return; + }, expectedHttpResponse: { status: 200, headers: expectedResponseHeaders, - body: {result: null}, + body: { result: null }, }, }); }); @@ -285,11 +295,15 @@ describe('callable.FunctionBuilder', () => { return runTest({ httpRequest: req, expectedData: null, - callableFunction: (data, context) => { return; }, + callableFunction: (data, context) => { + return; + }, expectedHttpResponse: { status: 400, headers: expectedResponseHeaders, - body: {error: {status: 'INVALID_ARGUMENT', message: 'Bad Request'}}, + body: { + error: { status: 'INVALID_ARGUMENT', message: 'Bad Request' }, + }, }, }); }); @@ -298,11 +312,13 @@ describe('callable.FunctionBuilder', () => { return runTest({ httpRequest: request(null, 'application/json; charset=utf-8'), expectedData: null, - callableFunction: (data, context) => { return; }, + callableFunction: (data, context) => { + return; + }, expectedHttpResponse: { status: 200, headers: expectedResponseHeaders, - body: {result: null}, + body: { result: null }, }, }); }); @@ -311,11 +327,15 @@ describe('callable.FunctionBuilder', () => { return runTest({ httpRequest: request(null, 'text/plain'), expectedData: null, - callableFunction: (data, context) => { return; }, + callableFunction: (data, context) => { + return; + }, expectedHttpResponse: { status: 400, headers: expectedResponseHeaders, - body: {error: {status: 'INVALID_ARGUMENT', message: 'Bad Request'}}, + body: { + error: { status: 'INVALID_ARGUMENT', message: 'Bad Request' }, + }, }, }); }); @@ -326,11 +346,15 @@ describe('callable.FunctionBuilder', () => { return runTest({ httpRequest: req, expectedData: null, - callableFunction: (data, context) => { return; }, + callableFunction: (data, context) => { + return; + }, expectedHttpResponse: { status: 400, headers: expectedResponseHeaders, - body: {error: {status: 'INVALID_ARGUMENT', message: 'Bad Request'}}, + body: { + error: { status: 'INVALID_ARGUMENT', message: 'Bad Request' }, + }, }, }); }); @@ -345,7 +369,7 @@ describe('callable.FunctionBuilder', () => { expectedHttpResponse: { status: 500, headers: expectedResponseHeaders, - body: {error: {status: 'INTERNAL', message: 'INTERNAL'}}, + body: { error: { status: 'INTERNAL', message: 'INTERNAL' } }, }, }); }); @@ -360,7 +384,7 @@ describe('callable.FunctionBuilder', () => { expectedHttpResponse: { status: 500, headers: expectedResponseHeaders, - body: {error: {status: 'INTERNAL', message: 'INTERNAL'}}, + body: { error: { status: 'INTERNAL', message: 'INTERNAL' } }, }, }); }); @@ -375,7 +399,7 @@ describe('callable.FunctionBuilder', () => { expectedHttpResponse: { status: 404, headers: expectedResponseHeaders, - body: {error: {status: 'NOT_FOUND', message: 'i am error'}}, + body: { error: { status: 'NOT_FOUND', message: 'i am error' } }, }, }); }); @@ -402,7 +426,7 @@ describe('callable.FunctionBuilder', () => { expectedHttpResponse: { status: 200, headers: expectedResponseHeaders, - body: {result: null}, + body: { result: null }, }, }); mock.done(); @@ -414,7 +438,9 @@ describe('callable.FunctionBuilder', () => { authorization: 'Bearer FAKE', }), expectedData: null, - callableFunction: (data, context) => { return; }, + callableFunction: (data, context) => { + return; + }, expectedHttpResponse: { status: 401, headers: expectedResponseHeaders, @@ -442,7 +468,7 @@ describe('callable.FunctionBuilder', () => { expectedHttpResponse: { status: 200, headers: expectedResponseHeaders, - body: {result: null}, + body: { result: null }, }, }); }); @@ -460,7 +486,7 @@ describe('callable.FunctionBuilder', () => { expectedHttpResponse: { status: 200, headers: expectedResponseHeaders, - body: {result: null}, + body: { result: null }, }, }); }); @@ -473,11 +499,14 @@ describe('callable CORS', () => { throw "This shouldn't have gotten called for an OPTIONS preflight."; }); - const request = new MockRequest({}, { - 'Access-Control-Request-Method': 'POST', - 'Access-Control-Request-Headers': 'origin', - Origin: 'example.com', - }); + const request = new MockRequest( + {}, + { + 'Access-Control-Request-Method': 'POST', + 'Access-Control-Request-Headers': 'origin', + Origin: 'example.com', + } + ); request.method = 'OPTIONS'; const response = await runHandler(func, request as any); @@ -512,15 +541,16 @@ describe('callable', () => { }); it('encodes long', () => { - expect(https.encode(-9223372036854775000)).to.equal( - -9223372036854775000); + expect(https.encode(-9223372036854775000)).to.equal(-9223372036854775000); }); it('decodes long', () => { - expect(https.decode({ - '@type': 'type.googleapis.com/google.protobuf.Int64Value', - 'value': '-9223372036854775000', - })).to.equal(-9223372036854775000); + expect( + https.decode({ + '@type': 'type.googleapis.com/google.protobuf.Int64Value', + value: '-9223372036854775000', + }) + ).to.equal(-9223372036854775000); }); it('encodes unsigned long', () => { @@ -528,10 +558,12 @@ describe('callable', () => { }); it('decodes unsigned long', () => { - expect(https.decode({ - '@type': 'type.googleapis.com/google.protobuf.UInt64Value', - 'value': '9223372036854800000', - })).to.equal(9223372036854800000); + expect( + https.decode({ + '@type': 'type.googleapis.com/google.protobuf.UInt64Value', + value: '9223372036854800000', + }) + ).to.equal(9223372036854800000); }); it('encodes double', () => { @@ -557,21 +589,31 @@ describe('callable', () => { }); it('decodes array', () => { - expect(https.decode( - [1, '2', [3, { - value: '1099511627776', - '@type': 'type.googleapis.com/google.protobuf.Int64Value', - }]])).to.deep.equal([1, '2', [3, 1099511627776]]); + expect( + https.decode([ + 1, + '2', + [ + 3, + { + value: '1099511627776', + '@type': 'type.googleapis.com/google.protobuf.Int64Value', + }, + ], + ]) + ).to.deep.equal([1, '2', [3, 1099511627776]]); }); it('encodes object', () => { // TODO(klimt): Make this test more interesting once there's some type // that needs encoding that can be created from JavaScript. - expect(https.encode({ - foo: 1, - bar: 'hello', - baz: [1, 2, 3], - })).to.deep.equal({ + expect( + https.encode({ + foo: 1, + bar: 'hello', + baz: [1, 2, 3], + }) + ).to.deep.equal({ foo: 1, bar: 'hello', baz: [1, 2, 3], @@ -579,14 +621,20 @@ describe('callable', () => { }); it('decodes object', () => { - expect(https.decode({ - foo: 1, - bar: 'hello', - baz: [1, 2, { - value: '1099511627776', - '@type': 'type.googleapis.com/google.protobuf.Int64Value', - }], - })).to.deep.equal({ + expect( + https.decode({ + foo: 1, + bar: 'hello', + baz: [ + 1, + 2, + { + value: '1099511627776', + '@type': 'type.googleapis.com/google.protobuf.Int64Value', + }, + ], + }) + ).to.deep.equal({ foo: 1, bar: 'hello', baz: [1, 2, 1099511627776], diff --git a/spec/providers/pubsub.spec.ts b/spec/providers/pubsub.spec.ts index e4330bff8..83df21ab0 100644 --- a/spec/providers/pubsub.spec.ts +++ b/spec/providers/pubsub.spec.ts @@ -37,7 +37,7 @@ describe('Pubsub Functions', () => { it('should preserve passed in json', () => { let message = new pubsub.Message({ data: new Buffer('{"hello":"world"}', 'utf8').toString('base64'), - json: {goodbye: 'world'}, + json: { goodbye: 'world' }, }); expect(message.json.goodbye).to.equal('world'); @@ -46,7 +46,9 @@ describe('Pubsub Functions', () => { describe('#toJSON', () => { it('should be JSON stringify-able', () => { - let encoded = new Buffer('{"hello":"world"}', 'utf8').toString('base64'); + let encoded = new Buffer('{"hello":"world"}', 'utf8').toString( + 'base64' + ); let message = new pubsub.Message({ data: encoded, }); @@ -60,7 +62,6 @@ describe('Pubsub Functions', () => { }); describe('pubsub.FunctionBuilder', () => { - before(() => { process.env.GCLOUD_PROJECT = 'project1'; }); @@ -82,7 +83,7 @@ describe('Pubsub Functions', () => { }); }); - it ('should throw with improperly formatted topics', () => { + it('should throw with improperly formatted topics', () => { expect(() => pubsub.topic('bad/topic/format')).to.throw(Error); }); @@ -107,8 +108,8 @@ describe('Pubsub Functions', () => { return expect(result(event)).to.eventually.deep.equal({ raw, - json: {hello: 'world'}, - attributes: {foo: 'bar'}, + json: { hello: 'world' }, + attributes: { foo: 'bar' }, }); }); }); @@ -116,11 +117,15 @@ describe('Pubsub Functions', () => { describe('process.env.GCLOUD_PROJECT not set', () => { it('should not throw if __trigger is not accessed', () => { - expect(() => pubsub.topic('toppy').onPublish(() => null)).to.not.throw(Error); + expect(() => pubsub.topic('toppy').onPublish(() => null)).to.not.throw( + Error + ); }); it('should throw when trigger is accessed', () => { - expect(() => pubsub.topic('toppy').onPublish(() => null).__trigger).to.throw(Error); + expect( + () => pubsub.topic('toppy').onPublish(() => null).__trigger + ).to.throw(Error); }); it('should not throw when #run is called', () => { diff --git a/spec/providers/storage.spec.ts b/spec/providers/storage.spec.ts index c6946082f..befea8be9 100644 --- a/spec/providers/storage.spec.ts +++ b/spec/providers/storage.spec.ts @@ -21,7 +21,7 @@ // SOFTWARE. import * as storage from '../../src/providers/storage'; -import { expect as expect } from 'chai'; +import { expect } from 'chai'; describe('Storage Functions', () => { describe('ObjectBuilder', () => { @@ -37,7 +37,10 @@ describe('Storage Functions', () => { describe('#onArchive', () => { it('should return a TriggerDefinition with appropriate values', () => { - let cloudFunction = storage.bucket('bucky').object().onArchive(() => null); + let cloudFunction = storage + .bucket('bucky') + .object() + .onArchive(() => null); expect(cloudFunction.__trigger).to.deep.equal({ eventTrigger: { eventType: 'google.storage.object.archive', @@ -59,7 +62,9 @@ describe('Storage Functions', () => { }); it('should allow fully qualified bucket names', () => { - let subjectQualified = new storage.ObjectBuilder(() => 'projects/_/buckets/bucky'); + let subjectQualified = new storage.ObjectBuilder( + () => 'projects/_/buckets/bucky' + ); let result = subjectQualified.onArchive(() => null); expect(result.__trigger).to.deep.equal({ eventTrigger: { @@ -71,8 +76,13 @@ describe('Storage Functions', () => { }); it('should throw with improperly formatted buckets', () => { - expect(() => storage.bucket('bad/bucket/format').object().onArchive(() => null).__trigger) - .to.throw(Error); + expect( + () => + storage + .bucket('bad/bucket/format') + .object() + .onArchive(() => null).__trigger + ).to.throw(Error); }); it('should not mess with media links using non-literal slashes', () => { @@ -81,8 +91,9 @@ describe('Storage Functions', () => { }); let goodMediaLinkEvent = { data: { - mediaLink: 'https://www.googleapis.com/storage/v1/b/mybucket.appspot.com' - + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', + mediaLink: + 'https://www.googleapis.com/storage/v1/b/mybucket.appspot.com' + + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', }, }; return cloudFunction(goodMediaLinkEvent).then((result: any) => { @@ -93,7 +104,10 @@ describe('Storage Functions', () => { describe('#onDelete', () => { it('should return a TriggerDefinition with appropriate values', () => { - let cloudFunction = storage.bucket('bucky').object().onDelete(() => null); + let cloudFunction = storage + .bucket('bucky') + .object() + .onDelete(() => null); expect(cloudFunction.__trigger).to.deep.equal({ eventTrigger: { eventType: 'google.storage.object.delete', @@ -115,7 +129,9 @@ describe('Storage Functions', () => { }); it('should allow fully qualified bucket names', () => { - let subjectQualified = new storage.ObjectBuilder(() => 'projects/_/buckets/bucky'); + let subjectQualified = new storage.ObjectBuilder( + () => 'projects/_/buckets/bucky' + ); let result = subjectQualified.onDelete(() => null); expect(result.__trigger).to.deep.equal({ eventTrigger: { @@ -127,8 +143,13 @@ describe('Storage Functions', () => { }); it('should throw with improperly formatted buckets', () => { - expect(() => storage.bucket('bad/bucket/format').object().onDelete(() => null).__trigger) - .to.throw(Error); + expect( + () => + storage + .bucket('bad/bucket/format') + .object() + .onDelete(() => null).__trigger + ).to.throw(Error); }); it('should not mess with media links using non-literal slashes', () => { @@ -137,8 +158,9 @@ describe('Storage Functions', () => { }); let goodMediaLinkEvent = { data: { - mediaLink: 'https://www.googleapis.com/storage/v1/b/mybucket.appspot.com' - + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', + mediaLink: + 'https://www.googleapis.com/storage/v1/b/mybucket.appspot.com' + + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', }, }; return cloudFunction(goodMediaLinkEvent).then((result: any) => { @@ -149,7 +171,10 @@ describe('Storage Functions', () => { describe('#onFinalize', () => { it('should return a TriggerDefinition with appropriate values', () => { - let cloudFunction = storage.bucket('bucky').object().onFinalize(() => null); + let cloudFunction = storage + .bucket('bucky') + .object() + .onFinalize(() => null); expect(cloudFunction.__trigger).to.deep.equal({ eventTrigger: { eventType: 'google.storage.object.finalize', @@ -171,7 +196,9 @@ describe('Storage Functions', () => { }); it('should allow fully qualified bucket names', () => { - let subjectQualified = new storage.ObjectBuilder(() => 'projects/_/buckets/bucky'); + let subjectQualified = new storage.ObjectBuilder( + () => 'projects/_/buckets/bucky' + ); let result = subjectQualified.onFinalize(() => null); expect(result.__trigger).to.deep.equal({ eventTrigger: { @@ -183,8 +210,13 @@ describe('Storage Functions', () => { }); it('should throw with improperly formatted buckets', () => { - expect(() => storage.bucket('bad/bucket/format').object().onFinalize(() => null).__trigger) - .to.throw(Error); + expect( + () => + storage + .bucket('bad/bucket/format') + .object() + .onFinalize(() => null).__trigger + ).to.throw(Error); }); it('should not mess with media links using non-literal slashes', () => { @@ -193,8 +225,9 @@ describe('Storage Functions', () => { }); let goodMediaLinkEvent = { data: { - mediaLink: 'https://www.googleapis.com/storage/v1/b/mybucket.appspot.com' - + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', + mediaLink: + 'https://www.googleapis.com/storage/v1/b/mybucket.appspot.com' + + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', }, }; return cloudFunction(goodMediaLinkEvent).then((result: any) => { @@ -205,7 +238,10 @@ describe('Storage Functions', () => { describe('#onMetadataUpdate', () => { it('should return a TriggerDefinition with appropriate values', () => { - let cloudFunction = storage.bucket('bucky').object().onMetadataUpdate(() => null); + let cloudFunction = storage + .bucket('bucky') + .object() + .onMetadataUpdate(() => null); expect(cloudFunction.__trigger).to.deep.equal({ eventTrigger: { eventType: 'google.storage.object.metadataUpdate', @@ -227,7 +263,9 @@ describe('Storage Functions', () => { }); it('should allow fully qualified bucket names', () => { - let subjectQualified = new storage.ObjectBuilder(() => 'projects/_/buckets/bucky'); + let subjectQualified = new storage.ObjectBuilder( + () => 'projects/_/buckets/bucky' + ); let result = subjectQualified.onMetadataUpdate(() => null); expect(result.__trigger).to.deep.equal({ eventTrigger: { @@ -239,8 +277,13 @@ describe('Storage Functions', () => { }); it('should throw with improperly formatted buckets', () => { - expect(() => storage.bucket('bad/bucket/format').object().onMetadataUpdate(() => null).__trigger) - .to.throw(Error); + expect( + () => + storage + .bucket('bad/bucket/format') + .object() + .onMetadataUpdate(() => null).__trigger + ).to.throw(Error); }); it('should not mess with media links using non-literal slashes', () => { @@ -249,8 +292,9 @@ describe('Storage Functions', () => { }); let goodMediaLinkEvent = { data: { - mediaLink: 'https://www.googleapis.com/storage/v1/b/mybucket.appspot.com' - + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', + mediaLink: + 'https://www.googleapis.com/storage/v1/b/mybucket.appspot.com' + + '/o/nestedfolder%2Fanotherfolder%2Fmyobject.file?generation=12345&alt=media', }, }; return cloudFunction(goodMediaLinkEvent).then((result: any) => { @@ -266,7 +310,9 @@ describe('Storage Functions', () => { }); it('should throw when trigger is accessed', () => { - expect(() => storage.object().onArchive(() => null).__trigger).to.throw(Error); + expect(() => storage.object().onArchive(() => null).__trigger).to.throw( + Error + ); }); it('should not throw when #run is called', () => { diff --git a/spec/testing.spec.ts b/spec/testing.spec.ts index 83254e881..4df5362df 100644 --- a/spec/testing.spec.ts +++ b/spec/testing.spec.ts @@ -26,7 +26,7 @@ import * as testing from '../src/testing'; // TODO(rjh): As actual testing methods become available, replace this with actual tests. describe('testing', () => { - it('should be accessible through the entrypoint', function () { + it('should be accessible through the entrypoint', function() { expect(testing.whereAreTheBugs()).to.not.equal('Earth'); }); }); diff --git a/spec/utils.spec.ts b/spec/utils.spec.ts index 1a2ad54c1..cedf43597 100644 --- a/spec/utils.spec.ts +++ b/spec/utils.spec.ts @@ -23,7 +23,7 @@ import { normalizePath, pathParts, valAt, applyChange } from '../src/utils'; import { expect } from 'chai'; -describe ('utils', () => { +describe('utils', () => { describe('.normalizePath(path: string)', () => { it('should strip leading and trailing slash', () => { expect(normalizePath('/my/path/is/{rad}/')).to.eq('my/path/is/{rad}'); @@ -46,34 +46,34 @@ describe ('utils', () => { it('should be null if null along any point in the path', () => { expect(valAt(null)).to.be.null; expect(valAt(null, '/foo')).to.be.null; - expect(valAt({a: {b: null}}, '/a/b/c')).to.be.null; + expect(valAt({ a: { b: null } }, '/a/b/c')).to.be.null; }); it('should be null if accessing a path past a leaf value', () => { - expect(valAt({a: 2}, '/a/b')).to.be.null; + expect(valAt({ a: 2 }, '/a/b')).to.be.null; }); it('should be the leaf value if one is present', () => { - expect(valAt({a: {b: 23}}, '/a/b')).to.eq(23); - expect(valAt({a: {b: 23}}, '/a')).to.deep.equal({b: 23}); + expect(valAt({ a: { b: 23 } }, '/a/b')).to.eq(23); + expect(valAt({ a: { b: 23 } }, '/a')).to.deep.equal({ b: 23 }); }); it('should be undefined if in unexplored territory', () => { - expect(valAt({a: 23}, '/b')).to.be.undefined; + expect(valAt({ a: 23 }, '/b')).to.be.undefined; }); }); describe('.applyChange(from: any, to: any): any', () => { it('should return the to value for non-object values of from and to', () => { - expect(applyChange({a: 'b'}, null)).to.eq(null); - expect(applyChange(null, {a: 'b'})).to.deep.equal({a: 'b'}); + expect(applyChange({ a: 'b' }, null)).to.eq(null); + expect(applyChange(null, { a: 'b' })).to.deep.equal({ a: 'b' }); expect(applyChange(23, null)).to.be.null; }); it('should return the merged value of two objects', () => { - let from = {a: {b: 'foo', c: 23, d: 444}, d: {e: 42}}; - let to: any = {a: {b: 'bar', c: null}, d: null, e: {f: 'g'}}; - let result = {a: {b: 'bar', d: 444}, e: {f: 'g'}}; + let from = { a: { b: 'foo', c: 23, d: 444 }, d: { e: 42 } }; + let to: any = { a: { b: 'bar', c: null }, d: null, e: { f: 'g' } }; + let result = { a: { b: 'bar', d: 444 }, e: { f: 'g' } }; expect(applyChange(from, to)).to.deep.equal(result); }); }); diff --git a/src/apps.ts b/src/apps.ts index 37d7febbc..15caff41c 100644 --- a/src/apps.ts +++ b/src/apps.ts @@ -46,7 +46,7 @@ export namespace apps { export let singleton: apps.Apps; - export let init = () => singleton = new Apps(); + export let init = () => (singleton = new Apps()); export interface AuthMode { admin: boolean; @@ -79,7 +79,10 @@ export namespace apps { if (!this._appAlive(appName)) { return; } - firebase.app(appName).delete().catch(_.noop); + firebase + .app(appName) + .delete() + .catch(_.noop); } retain() { @@ -112,7 +115,9 @@ export namespace apps { } private get firebaseArgs() { - return _.assign({}, firebaseConfig(), {credential: firebase.credential.applicationDefault()}); + return _.assign({}, firebaseConfig(), { + credential: firebase.credential.applicationDefault(), + }); } } } diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 3717f8ca1..bf36b3dce 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -79,8 +79,8 @@ export interface EventContext { * for database functions. */ auth?: { - uid: string, - token: object, + uid: string; + token: object; }; } @@ -88,10 +88,7 @@ export interface EventContext { * to the event, "after" represents the state after the event. */ export class Change { - constructor( - public before?: T, - public after?: T, - ) {}; + constructor(public before?: T, public after?: T) {} } /** ChangeJson is the JSON format used to construct a Change object. */ @@ -107,7 +104,9 @@ export interface ChangeJson { } export namespace Change { - function reinterpretCast(x: any) { return x as T; } + function reinterpretCast(x: any) { + return x as T; + } /** Factory method for creating a Change from a `before` object and an `after` object. */ export function fromObjects(before: T, after: T) { @@ -117,16 +116,26 @@ export namespace Change { /** Factory method for creating a Change from a JSON and an optional customizer function to be * applied to both the `before` and the `after` fields. */ - export function fromJSON(json: ChangeJson, customizer: (x: any) => T = reinterpretCast): Change { + export function fromJSON( + json: ChangeJson, + customizer: (x: any) => T = reinterpretCast + ): Change { let before = _.assign({}, json.before); if (json.fieldMask) { before = applyFieldMask(before, json.after, json.fieldMask); } - return Change.fromObjects(customizer(before || {}), customizer(json.after || {})); + return Change.fromObjects( + customizer(before || {}), + customizer(json.after || {}) + ); } /** @internal */ - export function applyFieldMask(sparseBefore: any, after: any, fieldMask: string) { + export function applyFieldMask( + sparseBefore: any, + after: any, + fieldMask: string + ) { let before = _.assign({}, after); let masks = fieldMask.split(','); _.forEach(masks, mask => { @@ -154,13 +163,13 @@ export interface Resource { /** TriggerAnnotated is used internally by the firebase CLI to understand what type of Cloud Function to deploy. */ export interface TriggerAnnotated { __trigger: { - httpsTrigger?: {}, + httpsTrigger?: {}; eventTrigger?: { eventType: string; resource: string; service: string; - }, - labels?: { [key: string]: string } + }; + labels?: { [key: string]: string }; }; } @@ -173,13 +182,16 @@ export interface Runnable { * An HttpsFunction is both an object that exports its trigger definitions at __trigger and * can be called as a function that takes an express.js Request and Response object. */ -export type HttpsFunction = TriggerAnnotated & ((req: Request, resp: Response) => void); +export type HttpsFunction = TriggerAnnotated & + ((req: Request, resp: Response) => void); /** * A CloudFunction is both an object that exports its trigger definitions at __trigger and * can be called as a function using the raw JS API for Google Cloud Functions. */ -export type CloudFunction = Runnable & TriggerAnnotated & ((input: any) => PromiseLike | any); +export type CloudFunction = Runnable & + TriggerAnnotated & + ((input: any) => PromiseLike | any); /** @internal */ export interface MakeCloudFunctionArgs { @@ -204,23 +216,31 @@ export function makeCloudFunction({ service, dataConstructor = (raw: Event | LegacyEvent) => raw.data, handler, - before = () => { return; }, - after = () => { return; }, + before = () => { + return; + }, + after = () => { + return; + }, legacyEventType, }: MakeCloudFunctionArgs): CloudFunction { let cloudFunction: any = async (event: Event | LegacyEvent) => { if (!_.has(event, 'data')) { - throw Error('Cloud function needs to be called with an event parameter.' + - 'If you are writing unit tests, please use the Node module firebase-functions-fake.'); + throw Error( + 'Cloud function needs to be called with an event parameter.' + + 'If you are writing unit tests, please use the Node module firebase-functions-fake.' + ); } try { before(event); let dataOrChange = dataConstructor(event); let context: any; - if (isEvent(event)) { // new event format + if (isEvent(event)) { + // new event format context = _.cloneDeep(event.context); - } else { // legacy event format + } else { + // legacy event format context = { eventId: event.eventId, timestamp: event.timestamp, @@ -268,11 +288,16 @@ function isEvent(event: Event | LegacyEvent): event is Event { return _.has(event, 'context'); } -function _makeParams(context: EventContext, triggerResourceGetter: () => string): { [option: string]: any } { - if (context.params) { // In unit testing, user may directly provide `context.params`. +function _makeParams( + context: EventContext, + triggerResourceGetter: () => string +): { [option: string]: any } { + if (context.params) { + // In unit testing, user may directly provide `context.params`. return context.params; } - if (!context.resource) { // In unit testing, `resource` may be unpopulated for a test event. + if (!context.resource) { + // In unit testing, `resource` may be unpopulated for a test event. return {}; } let triggerResource = triggerResourceGetter(); @@ -282,7 +307,7 @@ function _makeParams(context: EventContext, triggerResourceGetter: () => string) let triggerResourceParts = _.split(triggerResource, '/'); let eventResourceParts = _.split(context.resource.name, '/'); _.forEach(wildcards, wildcard => { - let wildcardNoBraces = wildcard.slice(1,-1); + let wildcardNoBraces = wildcard.slice(1, -1); let position = _.indexOf(triggerResourceParts, wildcard); params[wildcardNoBraces] = eventResourceParts[position]; }); diff --git a/src/config.ts b/src/config.ts index a1cfc7090..2ff026d2a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,3 @@ - // The MIT License (MIT) // // Copyright (c) 2017 Firebase @@ -41,7 +40,6 @@ export namespace config { /* @internal */ export function firebaseConfig(): firebase.AppOptions | null { - // The FIREBASE_PROJECT environment variable was introduced to help local emulation with `firebase-tools` 3.18 // Unfortunately, API review decided that the name should be FIREBASE_CONFIG to avoid confusions that Firebase has // a separate project from Google Cloud. This accepts both versions, preferring the documented name. @@ -62,7 +60,8 @@ export function firebaseConfig(): firebase.AppOptions | null { // Could have Runtime Config with Firebase in it as an ENV location or default. try { - const path = process.env.CLOUD_RUNTIME_CONFIG || '../../../.runtimeconfig.json'; + const path = + process.env.CLOUD_RUNTIME_CONFIG || '../../../.runtimeconfig.json'; const config = require(path); if (config.firebase) { return config.firebase; @@ -85,7 +84,8 @@ function init() { } try { - let path = process.env.CLOUD_RUNTIME_CONFIG || '../../../.runtimeconfig.json'; + let path = + process.env.CLOUD_RUNTIME_CONFIG || '../../../.runtimeconfig.json'; const parsed = require(path); delete parsed.firebase; config.singleton = parsed; diff --git a/src/encoder.ts b/src/encoder.ts index dfa010a0b..ddc7a4c55 100644 --- a/src/encoder.ts +++ b/src/encoder.ts @@ -33,4 +33,4 @@ export function dateToTimestampProto(timeString?: string) { nanos = parseInt(nanoString, 10) * Math.pow(10, trailingZeroes); } return { seconds, nanos }; -}; +} diff --git a/src/index.ts b/src/index.ts index f5ebbb6d7..ccc0470f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,7 +30,16 @@ import * as https from './providers/https'; import * as pubsub from './providers/pubsub'; import * as storage from './providers/storage'; import { firebaseConfig } from './config'; -export { analytics, auth, crashlytics, database, firestore, https, pubsub, storage }; +export { + analytics, + auth, + crashlytics, + database, + firestore, + https, + pubsub, + storage, +}; // Exported root types: export * from './config'; @@ -40,18 +49,21 @@ export * from './cloud-functions'; // Until the Cloud Functions builder can publish FIREBASE_CONFIG, automatically provide it on import based on what // we can deduce. if (!process.env.FIREBASE_CONFIG) { - const cfg = firebaseConfig(); + const cfg = firebaseConfig(); if (cfg) { process.env.FIREBASE_CONFIG = JSON.stringify(cfg); - } else if (process.env.GCLOUD_PROJECT) { - console.warn('Warning, estimating Firebase Config based on GCLOUD_PROJECT. Intializing firebase-admin may fail'); + console.warn( + 'Warning, estimating Firebase Config based on GCLOUD_PROJECT. Intializing firebase-admin may fail' + ); process.env.FIREBASE_CONFIG = JSON.stringify({ databaseURL: `https://${process.env.GCLOUD_PROJECT}.firebaseio.com`, storageBucket: `${process.env.GCLOUD_PROJECT}.appspot.com`, projectId: process.env.GCLOUD_PROJECT, }); } else { - console.warn('Warning, FIREBASE_CONFIG environment variable is missing. Initializing firebase-admin will fail'); + console.warn( + 'Warning, FIREBASE_CONFIG environment variable is missing. Initializing firebase-admin will fail' + ); } } diff --git a/src/providers/analytics.ts b/src/providers/analytics.ts index 322b0d6e7..c3836cec2 100644 --- a/src/providers/analytics.ts +++ b/src/providers/analytics.ts @@ -20,7 +20,12 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { makeCloudFunction, CloudFunction, Event, EventContext } from '../cloud-functions'; +import { + makeCloudFunction, + CloudFunction, + Event, + EventContext, +} from '../cloud-functions'; import * as _ from 'lodash'; /** @internal */ @@ -42,7 +47,9 @@ export function event(analyticsEventType: string) { if (!process.env.GCLOUD_PROJECT) { throw new Error('process.env.GCLOUD_PROJECT is not set.'); } - return 'projects/' + process.env.GCLOUD_PROJECT + '/events/' + analyticsEventType; + return ( + 'projects/' + process.env.GCLOUD_PROJECT + '/events/' + analyticsEventType + ); }); } @@ -53,7 +60,7 @@ export function event(analyticsEventType: string) { */ export class AnalyticsEventBuilder { /** @internal */ - constructor(private triggerResource: () => string) { } + constructor(private triggerResource: () => string) {} /** * Event handler that fires every time a Firebase Analytics event occurs. @@ -66,7 +73,11 @@ export class AnalyticsEventBuilder { * Cloud Function you can export. */ onLog( - handler: (event: AnalyticsEvent, context: EventContext) => PromiseLike | any): CloudFunction { + handler: ( + event: AnalyticsEvent, + context: EventContext + ) => PromiseLike | any + ): CloudFunction { const dataConstructor = (raw: Event) => { return new AnalyticsEvent(raw.data); }; @@ -118,7 +129,7 @@ export class AnalyticsEvent { /** @internal */ constructor(wireFormat: any) { - this.params = {}; // In case of absent field, show empty (not absent) map. + this.params = {}; // In case of absent field, show empty (not absent) map. if (wireFormat.eventDim && wireFormat.eventDim.length > 0) { // If there's an eventDim, there'll always be exactly one. let eventDim = wireFormat.eventDim[0]; @@ -127,9 +138,20 @@ export class AnalyticsEvent { copyFieldTo(eventDim, this, 'valueInUsd', 'valueInUSD'); copyFieldTo(eventDim, this, 'date', 'reportingDate'); copyTimestampToString(eventDim, this, 'timestampMicros', 'logTime'); - copyTimestampToString(eventDim, this, 'previousTimestampMicros', 'previousLogTime'); + copyTimestampToString( + eventDim, + this, + 'previousTimestampMicros', + 'previousLogTime' + ); } - copyFieldTo(wireFormat, this, 'userDim', 'user', dim => new UserDimensions(dim)); + copyFieldTo( + wireFormat, + this, + 'userDim', + 'user', + dim => new UserDimensions(dim) + ); } } @@ -172,12 +194,24 @@ export class UserDimensions { /** @internal */ constructor(wireFormat: any) { // These are interfaces or primitives, no transformation needed. - copyFields(wireFormat, this, ['userId', 'deviceInfo', 'geoInfo', 'appInfo']); + copyFields(wireFormat, this, [ + 'userId', + 'deviceInfo', + 'geoInfo', + 'appInfo', + ]); // The following fields do need transformations of some sort. - copyTimestampToString(wireFormat, this, 'firstOpenTimestampMicros', 'firstOpenTime'); - this.userProperties = {}; // With no entries in the wire format, present an empty (as opposed to absent) map. - copyField(wireFormat, this, 'userProperties', r => _.mapValues(r, p => new UserPropertyValue(p))); + copyTimestampToString( + wireFormat, + this, + 'firstOpenTimestampMicros', + 'firstOpenTime' + ); + this.userProperties = {}; // With no entries in the wire format, present an empty (as opposed to absent) map. + copyField(wireFormat, this, 'userProperties', r => + _.mapValues(r, p => new UserPropertyValue(p)) + ); copyField(wireFormat, this, 'bundleInfo', r => new ExportBundleInfo(r)); // BUG(36000368) Remove when no longer necessary @@ -341,18 +375,33 @@ export class ExportBundleInfo { /** @internal */ constructor(wireFormat: any) { copyField(wireFormat, this, 'bundleSequenceId'); - copyTimestampToMillis(wireFormat, this, 'serverTimestampOffsetMicros', 'serverTimestampOffset'); + copyTimestampToMillis( + wireFormat, + this, + 'serverTimestampOffsetMicros', + 'serverTimestampOffset' + ); } } function copyFieldTo( - from: any, to: T, fromField: string, toField: K, transform: (val: any) => T[K] = _.identity): void { + from: any, + to: T, + fromField: string, + toField: K, + transform: (val: any) => T[K] = _.identity +): void { if (from[fromField] !== undefined) { to[toField] = transform(from[fromField]); } } -function copyField(from: any, to: T, field: K, transform: (val: any) => T[K] = _.identity): void { +function copyField( + from: any, + to: T, + field: K, + transform: (val: any) => T[K] = _.identity +): void { copyFieldTo(from, to, field, field, transform); } @@ -418,7 +467,12 @@ function unwrapValue(wrapped: any): any { // The JSON payload delivers timestamp fields as strings of timestamps denoted in microseconds. // The JavaScript convention is to use numbers denoted in milliseconds. This method // makes it easy to convert a field of one type into the other. -function copyTimestampToMillis(from: any, to: T, fromName: string, toName: K) { +function copyTimestampToMillis( + from: any, + to: T, + fromName: string, + toName: K +) { if (from[fromName] !== undefined) { to[toName] = _.round(from[fromName] / 1000); } @@ -427,8 +481,13 @@ function copyTimestampToMillis(from: any, to: T, fromName: // The JSON payload delivers timestamp fields as strings of timestamps denoted in microseconds. // In our SDK, we'd like to present timestamp as ISO-format strings. This method makes it easy // to convert a field of one type into the other. -function copyTimestampToString(from: any, to: T, fromName: string, toName: K) { +function copyTimestampToString( + from: any, + to: T, + fromName: string, + toName: K +) { if (from[fromName] !== undefined) { - to[toName] = (new Date(from[fromName] / 1000)).toISOString(); + to[toName] = new Date(from[fromName] / 1000).toISOString(); } } diff --git a/src/providers/auth.ts b/src/providers/auth.ts index 813a69ab9..47b5c0ece 100644 --- a/src/providers/auth.ts +++ b/src/providers/auth.ts @@ -20,7 +20,12 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { makeCloudFunction, CloudFunction, EventContext, LegacyEvent } from '../cloud-functions'; +import { + makeCloudFunction, + CloudFunction, + EventContext, + LegacyEvent, +} from '../cloud-functions'; import * as firebase from 'firebase-admin'; import * as _ from 'lodash'; @@ -40,8 +45,7 @@ export function user() { } export class UserRecordMetadata implements firebase.auth.UserMetadata { - - constructor(public creationTime: string, public lastSignInTime: string) { }; + constructor(public creationTime: string, public lastSignInTime: string) {} /** Returns a plain JavaScript object with the properties of UserRecordMetadata. */ toJSON() { @@ -59,20 +63,27 @@ export class UserBuilder { } /** @internal */ - constructor(private triggerResource: () => string) { } + constructor(private triggerResource: () => string) {} /** Respond to the creation of a Firebase Auth user. */ - onCreate(handler: (user: UserRecord, context: EventContext) => PromiseLike | any): CloudFunction { + onCreate( + handler: (user: UserRecord, context: EventContext) => PromiseLike | any + ): CloudFunction { return this.onOperation(handler, 'user.create'); } /** Respond to the deletion of a Firebase Auth user. */ - onDelete(handler: (user: UserRecord, context: EventContext) => PromiseLike | any): CloudFunction { + onDelete( + handler: (user: UserRecord, context: EventContext) => PromiseLike | any + ): CloudFunction { return this.onOperation(handler, 'user.delete'); } private onOperation( - handler: (user: UserRecord, context: EventContext) => PromiseLike | any, + handler: ( + user: UserRecord, + context: EventContext + ) => PromiseLike | any, eventType: string ): CloudFunction { return makeCloudFunction({ @@ -93,7 +104,9 @@ export class UserBuilder { */ export type UserRecord = firebase.auth.UserRecord; -export function userRecordConstructor(wireData: Object): firebase.auth.UserRecord { +export function userRecordConstructor( + wireData: Object +): firebase.auth.UserRecord { // Falsey values from the wire format proto get lost when converted to JSON, this adds them back. let falseyValues: any = { email: null, @@ -112,11 +125,15 @@ export function userRecordConstructor(wireData: Object): firebase.auth.UserRecor let meta = _.get(record, 'metadata'); if (meta) { - _.set(record, 'metadata', new UserRecordMetadata( - // Transform payload to firebase-admin v5.0.0 format because wire format is different (BUG 63167395) - meta.createdAt || meta.creationTime, - meta.lastSignedInAt || meta.lastSignInTime, - )); + _.set( + record, + 'metadata', + new UserRecordMetadata( + // Transform payload to firebase-admin v5.0.0 format because wire format is different (BUG 63167395) + meta.createdAt || meta.creationTime, + meta.lastSignedInAt || meta.lastSignInTime + ) + ); } else { _.set(record, 'metadata', new UserRecordMetadata(null, null)); } @@ -126,8 +143,18 @@ export function userRecordConstructor(wireData: Object): firebase.auth.UserRecor }); }); _.set(record, 'toJSON', () => { - const json: any = _.pick(record, ['uid', 'email', 'emailVerified', 'displayName', - 'photoURL', 'phoneNumber', 'disabled', 'passwordHash', 'passwordSalt', 'tokensValidAfterTime']); + const json: any = _.pick(record, [ + 'uid', + 'email', + 'emailVerified', + 'displayName', + 'photoURL', + 'phoneNumber', + 'disabled', + 'passwordHash', + 'passwordSalt', + 'tokensValidAfterTime', + ]); json.metadata = _.get(record, 'metadata').toJSON(); json.customClaims = _.cloneDeep(record.customClaims); json.providerData = _.map(record.providerData, entry => entry.toJSON()); diff --git a/src/providers/crashlytics.ts b/src/providers/crashlytics.ts index 418936e14..f4dda117b 100644 --- a/src/providers/crashlytics.ts +++ b/src/providers/crashlytics.ts @@ -20,15 +20,19 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { makeCloudFunction, CloudFunction, EventContext } from '../cloud-functions'; +import { + makeCloudFunction, + CloudFunction, + EventContext, +} from '../cloud-functions'; /** @internal */ export const provider = 'google.firebase.crashlytics'; /** @internal */ export const service = 'fabric.io'; -/** - * Handle events related to Crashlytics issues. An issue in Crashlytics is an +/** + * Handle events related to Crashlytics issues. An issue in Crashlytics is an * aggregation of crashes which have a shared root cause. */ export function issue() { @@ -43,7 +47,7 @@ export function issue() { /** Builder used to create Cloud Functions for Crashlytics issue events. */ export class IssueBuilder { /** @internal */ - constructor(private triggerResource: () => string) { } + constructor(private triggerResource: () => string) {} /** @internal */ onNewDetected(handler: any): Error { @@ -51,17 +55,23 @@ export class IssueBuilder { } /** Handle Crashlytics New Issue events. */ - onNew(handler: (issue: Issue, context: EventContext) => PromiseLike | any): CloudFunction { + onNew( + handler: (issue: Issue, context: EventContext) => PromiseLike | any + ): CloudFunction { return this.onEvent(handler, 'issue.new'); } /** Handle Crashlytics Regressed Issue events. */ - onRegressed(handler: (issue: Issue, context: EventContext) => PromiseLike | any): CloudFunction { + onRegressed( + handler: (issue: Issue, context: EventContext) => PromiseLike | any + ): CloudFunction { return this.onEvent(handler, 'issue.regressed'); } /** Handle Crashlytics Velocity Alert events. */ - onVelocityAlert(handler: (issue: Issue, context: EventContext) => PromiseLike | any): CloudFunction { + onVelocityAlert( + handler: (issue: Issue, context: EventContext) => PromiseLike | any + ): CloudFunction { return this.onEvent(handler, 'issue.velocityAlert'); } diff --git a/src/providers/database.ts b/src/providers/database.ts index d21ac2409..e3788f899 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -22,7 +22,14 @@ import * as _ from 'lodash'; import { apps } from '../apps'; -import { LegacyEvent, CloudFunction, makeCloudFunction, Event, EventContext, Change } from '../cloud-functions'; +import { + LegacyEvent, + CloudFunction, + makeCloudFunction, + Event, + EventContext, + Change, +} from '../cloud-functions'; import { normalizePath, applyChange, pathParts, joinPath } from '../utils'; import * as firebase from 'firebase-admin'; import { firebaseConfig } from '../config'; @@ -48,7 +55,10 @@ export class InstanceBuilder { ref(path: string): RefBuilder { const normalized = normalizePath(path); - return new RefBuilder(apps(), () => `projects/_/instances/${this.instance}/refs/${normalized}`); + return new RefBuilder( + apps(), + () => `projects/_/instances/${this.instance}/refs/${normalized}` + ); } } @@ -80,13 +90,18 @@ export function ref(path: string): RefBuilder { const normalized = normalizePath(path); const databaseURL = firebaseConfig().databaseURL; if (!databaseURL) { - throw new Error('Missing expected firebase config value databaseURL, ' + - 'config is actually' + JSON.stringify(firebaseConfig()) + - '\n If you are unit testing, please set process.env.FIREBASE_CONFIG'); + throw new Error( + 'Missing expected firebase config value databaseURL, ' + + 'config is actually' + + JSON.stringify(firebaseConfig()) + + '\n If you are unit testing, please set process.env.FIREBASE_CONFIG' + ); } const match = databaseURL.match(databaseURLRegex); if (!match) { - throw new Error('Invalid value for config firebase.databaseURL: ' + databaseURL); + throw new Error( + 'Invalid value for config firebase.databaseURL: ' + databaseURL + ); } const subdomain = match[1]; return `projects/_/instances/${subdomain}/refs/${normalized}`; @@ -98,28 +113,34 @@ export function ref(path: string): RefBuilder { /** Builder used to create Cloud Functions for Firebase Realtime Database References. */ export class RefBuilder { /** @internal */ - constructor(private apps: apps.Apps, private triggerResource: () => string) { } + constructor(private apps: apps.Apps, private triggerResource: () => string) {} /** Respond to any write that affects a ref. */ - onWrite(handler: ( - change: Change, - context: EventContext) => PromiseLike | any, + onWrite( + handler: ( + change: Change, + context: EventContext + ) => PromiseLike | any ): CloudFunction> { return this.onOperation(handler, 'ref.write', this.changeConstructor); } /** Respond to update on a ref. */ - onUpdate(handler: ( - change: Change, - context: EventContext) => PromiseLike | any, + onUpdate( + handler: ( + change: Change, + context: EventContext + ) => PromiseLike | any ): CloudFunction> { return this.onOperation(handler, 'ref.update', this.changeConstructor); } /** Respond to new data on a ref. */ - onCreate(handler: ( - snapshot: DataSnapshot, - context: EventContext) => PromiseLike | any, + onCreate( + handler: ( + snapshot: DataSnapshot, + context: EventContext + ) => PromiseLike | any ): CloudFunction { let dataConstructor = (raw: LegacyEvent) => { let [dbInstance, path] = resourceToInstanceAndPath(raw.resource); @@ -134,18 +155,15 @@ export class RefBuilder { } /** Respond to all data being deleted from a ref. */ - onDelete(handler: ( - snapshot: DataSnapshot, - context: EventContext) => PromiseLike | any, + onDelete( + handler: ( + snapshot: DataSnapshot, + context: EventContext + ) => PromiseLike | any ): CloudFunction { let dataConstructor = (raw: LegacyEvent) => { let [dbInstance, path] = resourceToInstanceAndPath(raw.resource); - return new DataSnapshot( - raw.data.data, - path, - this.apps.admin, - dbInstance - ); + return new DataSnapshot(raw.data.data, path, this.apps.admin, dbInstance); }; return this.onOperation(handler, 'ref.delete', dataConstructor); } @@ -153,8 +171,8 @@ export class RefBuilder { private onOperation( handler: (data: T, context: EventContext) => PromiseLike | any, eventType: string, - dataConstructor: (raw: Event | LegacyEvent) => any): CloudFunction { - + dataConstructor: (raw: Event | LegacyEvent) => any + ): CloudFunction { return makeCloudFunction({ handler, provider, @@ -163,8 +181,8 @@ export class RefBuilder { legacyEventType: `providers/${provider}/eventTypes/${eventType}`, triggerResource: this.triggerResource, dataConstructor: dataConstructor, - before: (event) => this.apps.retain(), - after: (event) => this.apps.release(), + before: event => this.apps.retain(), + after: event => this.apps.release(), }); } @@ -195,12 +213,16 @@ export function resourceToInstanceAndPath(resource: string) { let resourceRegex = `projects/([^/]+)/instances/([^/]+)/refs(/.+)?`; let match = resource.match(new RegExp(resourceRegex)); if (!match) { - throw new Error(`Unexpected resource string for Firebase Realtime Database event: ${resource}. ` + - 'Expected string in the format of "projects/_/instances/{firebaseioSubdomain}/refs/{ref=**}"'); + throw new Error( + `Unexpected resource string for Firebase Realtime Database event: ${resource}. ` + + 'Expected string in the format of "projects/_/instances/{firebaseioSubdomain}/refs/{ref=**}"' + ); } let [, project, dbInstanceName, path] = match; if (project !== '_') { - throw new Error(`Expect project to be '_' in a Firebase Realtime Database event`); + throw new Error( + `Expect project to be '_' in a Firebase Realtime Database event` + ); } let dbInstance = 'https://' + dbInstanceName + '.firebaseio.com'; return [dbInstance, path]; @@ -217,14 +239,16 @@ export class DataSnapshot { data: any, path?: string, // path will be undefined for the database root private app?: firebase.app.App, - instance?: string, + instance?: string ) { - if (instance) { // SDK always supplies instance, but user's unit tests may not + if (instance) { + // SDK always supplies instance, but user's unit tests may not this.instance = instance; } else if (app) { this.instance = app.options.databaseURL; } else if (process.env.GCLOUD_PROJECT) { - this.instance = 'https://' + process.env.GCLOUD_PROJECT + '.firebaseio.com'; + this.instance = + 'https://' + process.env.GCLOUD_PROJECT + '.firebaseio.com'; } this._path = path; @@ -233,9 +257,12 @@ export class DataSnapshot { /** Ref returns a reference to the database with full admin access. */ get ref(): firebase.database.Reference { - if (!this.app) { // may be unpopulated in user's unit tests - throw new Error('Please supply a Firebase app in the constructor for DataSnapshot' + - ' in order to use the .ref method.'); + if (!this.app) { + // may be unpopulated in user's unit tests + throw new Error( + 'Please supply a Firebase app in the constructor for DataSnapshot' + + ' in order to use the .ref method.' + ); } if (!this._ref) { this._ref = this.app.database(this.instance).ref(this._fullPath()); @@ -245,7 +272,7 @@ export class DataSnapshot { get key(): string { let last = _.last(pathParts(this._fullPath())); - return (!last || last === '') ? null : last; + return !last || last === '' ? null : last; } val(): any { @@ -256,10 +283,12 @@ export class DataSnapshot { } // TODO(inlined): figure out what to do here - exportVal(): any { return this.val(); } + exportVal(): any { + return this.val(); + } // TODO(inlined): figure out what to do here - getPriority(): string|number|null { + getPriority(): string | number | null { return 0; } @@ -277,7 +306,10 @@ export class DataSnapshot { forEach(action: (a: DataSnapshot) => boolean): boolean { let val = this.val(); if (_.isPlainObject(val)) { - return _.some(val, (value, key: string) => action(this.child(key)) === true); + return _.some( + val, + (value, key: string) => action(this.child(key)) === true + ); } return false; } @@ -317,7 +349,9 @@ export class DataSnapshot { let maxKey = 0; let allIntegerKeys = true; for (let key in node) { - if (!node.hasOwnProperty(key)) { continue; } + if (!node.hasOwnProperty(key)) { + continue; + } let childNode = node[key]; obj[key] = this._checkAndConvertToArray(childNode); numKeys++; diff --git a/src/providers/firestore.ts b/src/providers/firestore.ts index c9dec2795..426caa650 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -24,8 +24,14 @@ import { posix } from 'path'; import * as _ from 'lodash'; import * as firebase from 'firebase-admin'; import { apps } from '../apps'; -import { makeCloudFunction, CloudFunction, LegacyEvent, Change, - Event, EventContext } from '../cloud-functions'; +import { + makeCloudFunction, + CloudFunction, + LegacyEvent, + Change, + Event, + EventContext, +} from '../cloud-functions'; import { dateToTimestampProto } from '../encoder'; /** @internal */ @@ -56,7 +62,7 @@ export function document(path: string) { export class DatabaseBuilder { /** @internal */ - constructor(private database: string) { } + constructor(private database: string) {} namespace(namespace: string) { return new NamespaceBuilder(this.database, namespace); @@ -69,18 +75,24 @@ export class DatabaseBuilder { export class NamespaceBuilder { /** @internal */ - constructor(private database: string, private namespace?: string) { } + constructor(private database: string, private namespace?: string) {} document(path: string) { return new DocumentBuilder(() => { if (!process.env.GCLOUD_PROJECT) { throw new Error('process.env.GCLOUD_PROJECT is not set.'); } - let database = posix.join('projects', process.env.GCLOUD_PROJECT, 'databases', this.database); + let database = posix.join( + 'projects', + process.env.GCLOUD_PROJECT, + 'databases', + this.database + ); return posix.join( database, this.namespace ? `documents@${this.namespace}` : 'documents', - path); + path + ); }); } } @@ -92,12 +104,16 @@ function _getValueProto(data: any, resource: string, valueFieldName: string) { } let proto = { fields: _.get(data, [valueFieldName, 'fields'], {}), - createTime: dateToTimestampProto(_.get(data, [valueFieldName, 'createTime'])), - updateTime: dateToTimestampProto(_.get(data, [valueFieldName, 'updateTime'])), + createTime: dateToTimestampProto( + _.get(data, [valueFieldName, 'createTime']) + ), + updateTime: dateToTimestampProto( + _.get(data, [valueFieldName, 'updateTime']) + ), name: _.get(data, [valueFieldName, 'name'], resource), }; return proto; -}; +} /** @internal */ export function snapshotConstructor(event: LegacyEvent): DocumentSnapshot { @@ -107,16 +123,20 @@ export function snapshotConstructor(event: LegacyEvent): DocumentSnapshot { let valueProto = _getValueProto(event.data, event.resource, 'value'); let readTime = dateToTimestampProto(_.get(event, 'data.value.readTime')); return firestoreInstance.snapshot_(valueProto, readTime, 'json'); -}; +} /** @internal */ // TODO remove this function when wire format changes to new format -export function beforeSnapshotConstructor(event: LegacyEvent): DocumentSnapshot { +export function beforeSnapshotConstructor( + event: LegacyEvent +): DocumentSnapshot { if (!firestoreInstance) { firestoreInstance = firebase.firestore(apps().admin); } let oldValueProto = _getValueProto(event.data, event.resource, 'oldValue'); - let oldReadTime = dateToTimestampProto(_.get(event, 'data.oldValue.readTime')); + let oldReadTime = dateToTimestampProto( + _.get(event, 'data.oldValue.readTime') + ); return firestoreInstance.snapshot_(oldValueProto, oldReadTime, 'json'); } @@ -134,41 +154,54 @@ export class DocumentBuilder { } /** Respond to all document writes (creates, updates, or deletes). */ - onWrite(handler: ( - change: Change, - context: EventContext) => PromiseLike | any, + onWrite( + handler: ( + change: Change, + context: EventContext + ) => PromiseLike | any ): CloudFunction> { return this.onOperation(handler, 'document.write', changeConstructor); - }; + } /** Respond only to document updates. */ - onUpdate(handler: ( - change: Change, - context: EventContext) => PromiseLike | any, + onUpdate( + handler: ( + change: Change, + context: EventContext + ) => PromiseLike | any ): CloudFunction> { return this.onOperation(handler, 'document.update', changeConstructor); } /** Respond only to document creations. */ - onCreate(handler: ( - snapshot: DocumentSnapshot, - context: EventContext) => PromiseLike | any, + onCreate( + handler: ( + snapshot: DocumentSnapshot, + context: EventContext + ) => PromiseLike | any ): CloudFunction { return this.onOperation(handler, 'document.create', snapshotConstructor); } /** Respond only to document deletions. */ - onDelete(handler: ( - snapshot: DocumentSnapshot, - context: EventContext) => PromiseLike | any, + onDelete( + handler: ( + snapshot: DocumentSnapshot, + context: EventContext + ) => PromiseLike | any ): CloudFunction { - return this.onOperation(handler, 'document.delete', beforeSnapshotConstructor); + return this.onOperation( + handler, + 'document.delete', + beforeSnapshotConstructor + ); } private onOperation( handler: (data: T, context: EventContext) => PromiseLike | any, eventType: string, - dataConstructor: (raw: Event | LegacyEvent) => any): CloudFunction { + dataConstructor: (raw: Event | LegacyEvent) => any + ): CloudFunction { return makeCloudFunction({ handler, provider, diff --git a/src/providers/https.ts b/src/providers/https.ts index 82f5c56ba..f647d6f1f 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -27,10 +27,14 @@ import { apps } from '../apps'; import * as _ from 'lodash'; import * as cors from 'cors'; -export function onRequest(handler: (req: express.Request, resp: express.Response) => void): HttpsFunction { +export function onRequest( + handler: (req: express.Request, resp: express.Response) => void +): HttpsFunction { // lets us add __trigger without altering handler: - let cloudFunction: any = (req: express.Request, res: express.Response) => { handler(req, res); }; - cloudFunction.__trigger = {httpsTrigger: {}}; + let cloudFunction: any = (req: express.Request, res: express.Response) => { + handler(req, res); + }; + cloudFunction.__trigger = { httpsTrigger: {} }; return cloudFunction; } @@ -101,22 +105,22 @@ export type FunctionsErrorCode = * to the HTTP format error code string, and make sure it's in the supported set. */ const errorCodeMap: { [name: string]: string } = { - 'ok': 'OK', - 'cancelled': 'CANCELLED', - 'unknown': 'UNKNOWN', + ok: 'OK', + cancelled: 'CANCELLED', + unknown: 'UNKNOWN', 'invalid-argument': 'INVALID_ARGUMENT', 'deadline-exceeded': 'DEADLINE_EXCEEDED', 'not-found': 'NOT_FOUND', 'already-exists': 'ALREADY_EXISTS', 'permission-denied': 'PERMISSION_DENIED', - 'unauthenticated': 'UNAUTHENTICATED', + unauthenticated: 'UNAUTHENTICATED', 'resource-exhausted': 'RESOURCE_EXHAUSTED', 'failed-precondition': 'FAILED_PRECONDITION', - 'aborted': 'ABORTED', + aborted: 'ABORTED', 'out-of-range': 'OUT_OF_RANGE', - 'unimplemented': 'UNIMPLEMENTED', - 'internal': 'INTERNAL', - 'unavailable': 'UNAVAILABLE', + unimplemented: 'UNIMPLEMENTED', + internal: 'INTERNAL', + unavailable: 'UNAVAILABLE', 'data-loss': 'DATA_LOSS', }; @@ -166,25 +170,43 @@ export class HttpsError extends Error { */ get httpStatus(): number { switch (this.code) { - case 'ok': return 200; - case 'cancelled': return 499; - case 'unknown': return 500; - case 'invalid-argument': return 400; - case 'deadline-exceeded': return 504; - case 'not-found': return 404; - case 'already-exists': return 409; - case 'permission-denied': return 403; - case 'unauthenticated': return 401; - case 'resource-exhausted': return 429; - case 'failed-precondition': return 400; - case 'aborted': return 409; - case 'out-of-range': return 400; - case 'unimplemented': return 501; - case 'internal': return 500; - case 'unavailable': return 503; - case 'data-loss': return 500; + case 'ok': + return 200; + case 'cancelled': + return 499; + case 'unknown': + return 500; + case 'invalid-argument': + return 400; + case 'deadline-exceeded': + return 504; + case 'not-found': + return 404; + case 'already-exists': + return 409; + case 'permission-denied': + return 403; + case 'unauthenticated': + return 401; + case 'resource-exhausted': + return 429; + case 'failed-precondition': + return 400; + case 'aborted': + return 409; + case 'out-of-range': + return 400; + case 'unimplemented': + return 501; + case 'internal': + return 500; + case 'unavailable': + return 503; + case 'data-loss': + return 500; // This should never happen as long as the type system is doing its job. - default: throw 'Invalid error code: ' + this.code; + default: + throw 'Invalid error code: ' + this.code; } } @@ -229,13 +251,13 @@ interface HttpRequest extends express.Request { body: { data: any; }; -}; +} // The format for the http body response to a callable function. interface HttpResponseBody { result?: any; error?: HttpsError; -}; +} // Returns true if req is a properly formatted callable request. function isValidRequest(req: express.Request): req is HttpRequest { @@ -333,7 +355,7 @@ export function decode(data: any): any { if (data['@type']) { switch (data['@type']) { case LONG_TYPE: - // Fall through and handle this the same as unsigned. + // Fall through and handle this the same as unsigned. case UNSIGNED_LONG_TYPE: { // Technically, this could work return a valid number for malformed // data if there was a number followed by garbage. But it's just not @@ -370,7 +392,8 @@ const corsHandler = cors({ origin: true, methods: 'POST' }); * @param handler A method that takes a data and context and returns a value. */ export function onCall( - handler: (data: any, context: CallableContext) => any | Promise): HttpsFunction { + handler: (data: any, context: CallableContext) => any | Promise +): HttpsFunction { const func = async (req: express.Request, res: express.Response) => { try { if (!isValidRequest(req)) { @@ -388,7 +411,9 @@ export function onCall( } const idToken = match[1]; try { - const authToken = await apps().admin.auth().verifyIdToken(idToken); + const authToken = await apps() + .admin.auth() + .verifyIdToken(idToken); context.auth = { uid: authToken.uid, token: authToken, @@ -416,7 +441,6 @@ export function onCall( // If there was some result, encode it in the body. const responseBody: HttpResponseBody = { result }; res.status(200).send(responseBody); - } catch (error) { if (!(error instanceof HttpsError)) { // This doesn't count as an 'explicit' error. diff --git a/src/providers/pubsub.ts b/src/providers/pubsub.ts index c2359b73c..842d662e5 100644 --- a/src/providers/pubsub.ts +++ b/src/providers/pubsub.ts @@ -20,7 +20,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { CloudFunction, makeCloudFunction, EventContext } from '../cloud-functions'; +import { + CloudFunction, + makeCloudFunction, + EventContext, +} from '../cloud-functions'; /** @internal */ export const provider = 'google.pubsub'; @@ -43,19 +47,20 @@ export function topic(topic: string): TopicBuilder { /** Builder used to create Cloud Functions for Google Pub/Sub topics. */ export class TopicBuilder { - /** @internal */ - constructor(private triggerResource: () => string) { } + constructor(private triggerResource: () => string) {} /** Handle a Pub/Sub message that was published to a Cloud Pub/Sub topic */ - onPublish(handler: (message: Message, context: EventContext) => PromiseLike | any): CloudFunction { + onPublish( + handler: (message: Message, context: EventContext) => PromiseLike | any + ): CloudFunction { return makeCloudFunction({ handler, provider, service, triggerResource: this.triggerResource, eventType: 'topic.publish', - dataConstructor: (raw) => new Message(raw.data), + dataConstructor: raw => new Message(raw.data), }); } } @@ -69,19 +74,20 @@ export class TopicBuilder { */ export class Message { readonly data: string; - readonly attributes: {[key: string]: string }; + readonly attributes: { [key: string]: string }; private _json: any; constructor(data: any) { - [this.data, this.attributes, this._json] = - [data.data, data.attributes || {}, data.json]; + [this.data, this.attributes, this._json] = [ + data.data, + data.attributes || {}, + data.json, + ]; } get json(): any { if (typeof this._json === 'undefined') { - this._json = JSON.parse( - new Buffer(this.data, 'base64').toString('utf8') - ); + this._json = JSON.parse(new Buffer(this.data, 'base64').toString('utf8')); } return this._json; diff --git a/src/providers/storage.ts b/src/providers/storage.ts index 6650a304b..d7bd4a995 100644 --- a/src/providers/storage.ts +++ b/src/providers/storage.ts @@ -20,7 +20,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { CloudFunction, EventContext, makeCloudFunction } from '../cloud-functions'; +import { + CloudFunction, + EventContext, + makeCloudFunction, +} from '../cloud-functions'; import { firebaseConfig } from '../config'; /** @internal */ @@ -37,8 +41,10 @@ export function bucket(bucket?: string): BucketBuilder { const resourceGetter = () => { bucket = bucket || firebaseConfig().storageBucket; if (!bucket) { - throw new Error('Missing bucket name. If you are unit testing, please provide a bucket name' + - ' through `functions.storage.bucket(bucketName)`, or set process.env.FIREBASE_CONFIG.'); + throw new Error( + 'Missing bucket name. If you are unit testing, please provide a bucket name' + + ' through `functions.storage.bucket(bucketName)`, or set process.env.FIREBASE_CONFIG.' + ); } if (!/^[a-z\d][a-z\d\\._-]{1,230}[a-z\d]$/.test(bucket)) { throw new Error(`Invalid bucket name ${bucket}`); @@ -54,7 +60,7 @@ export function object(): ObjectBuilder { export class BucketBuilder { /** @internal */ - constructor(private triggerResource: () => string) { } + constructor(private triggerResource: () => string) {} /** Handle events for objects in this bucket. */ object() { @@ -64,49 +70,63 @@ export class BucketBuilder { export class ObjectBuilder { /** @internal */ - constructor(private triggerResource: () => string) { } + constructor(private triggerResource: () => string) {} /** @internal */ onChange(handler: any): Error { - throw new Error('"onChange" is now deprecated, please use "onArchive", "onDelete", ' + - '"onFinalize", or "onMetadataUpdate".'); + throw new Error( + '"onChange" is now deprecated, please use "onArchive", "onDelete", ' + + '"onFinalize", or "onMetadataUpdate".' + ); } /** Respond to archiving of an object, this is only for buckets that enabled object versioning. */ - onArchive(handler: ( - object: ObjectMetadata, - context: EventContext) => PromiseLike | any, + onArchive( + handler: ( + object: ObjectMetadata, + context: EventContext + ) => PromiseLike | any ): CloudFunction { return this.onOperation(handler, 'object.archive'); } /** Respond to the deletion of an object (not to archiving, if object versioning is enabled). */ - onDelete(handler: ( - object: ObjectMetadata, - context: EventContext) => PromiseLike | any, + onDelete( + handler: ( + object: ObjectMetadata, + context: EventContext + ) => PromiseLike | any ): CloudFunction { return this.onOperation(handler, 'object.delete'); } /** Respond to the successful creation of an object. */ - onFinalize(handler: ( - object: ObjectMetadata, - context: EventContext) => PromiseLike | any, + onFinalize( + handler: ( + object: ObjectMetadata, + context: EventContext + ) => PromiseLike | any ): CloudFunction { return this.onOperation(handler, 'object.finalize'); } /** Respond to metadata updates of existing objects. */ - onMetadataUpdate(handler: ( - object: ObjectMetadata, - context: EventContext) => PromiseLike | any, + onMetadataUpdate( + handler: ( + object: ObjectMetadata, + context: EventContext + ) => PromiseLike | any ): CloudFunction { return this.onOperation(handler, 'object.metadataUpdate'); } private onOperation( - handler: (object: ObjectMetadata, context: EventContext) => PromiseLike | any, - eventType: string): CloudFunction { + handler: ( + object: ObjectMetadata, + context: EventContext + ) => PromiseLike | any, + eventType: string + ): CloudFunction { return makeCloudFunction({ handler, provider, @@ -143,33 +163,33 @@ export interface ObjectMetadata { }; acl?: [ { - kind?: string, - id?: string, - selfLink?: string, - bucket?: string, - object?: string, - generation?: string, - entity?: string, - role?: string, - email?: string, - entityId?: string, - domain?: string, + kind?: string; + id?: string; + selfLink?: string; + bucket?: string; + object?: string; + generation?: string; + entity?: string; + role?: string; + email?: string; + entityId?: string; + domain?: string; projectTeam?: { - projectNumber?: string, - team?: string - }, - etag?: string + projectNumber?: string; + team?: string; + }; + etag?: string; } ]; owner?: { - entity?: string, - entityId?: string + entity?: string; + entityId?: string; }; crc32c?: string; componentCount?: string; etag?: string; customerEncryption?: { - encryptionAlgorithm?: string, - keySha256?: string, + encryptionAlgorithm?: string; + keySha256?: string; }; } diff --git a/src/utils.ts b/src/utils.ts index 522dadb9f..d483f0697 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -26,7 +26,7 @@ export function normalizePath(path: string): string { if (!path) { return ''; } - return path.replace(/^\//,'').replace(/\/$/, ''); + return path.replace(/^\//, '').replace(/\/$/, ''); } export function pathParts(path: string): string[] { @@ -37,7 +37,9 @@ export function pathParts(path: string): string[] { } export function joinPath(base: string, child: string) { - return pathParts(base).concat(pathParts(child)).join('/'); + return pathParts(base) + .concat(pathParts(child)) + .join('/'); } export function applyChange(src: any, dest: any) { From 65047d003ece50f9cad01fb7d44ccf37bdfbaf07 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Wed, 11 Jul 2018 14:11:53 -0700 Subject: [PATCH 039/705] Support Node 8 Google Cloud Functions Runtime (#203) --- integration_test/functions/package.json | 12 +- integration_test/functions/src/index.ts | 14 +- integration_test/functions/tsconfig.json | 2 +- package.json | 6 +- spec/cloud-functions.spec.ts | 68 +++++++-- spec/providers/firestore.spec.ts | 169 +++++++++++++++-------- src/cloud-functions.ts | 99 +++++++------ src/providers/auth.ts | 4 +- src/providers/database.ts | 21 +-- src/providers/firestore.ts | 23 +-- 10 files changed, 277 insertions(+), 141 deletions(-) diff --git a/integration_test/functions/package.json b/integration_test/functions/package.json index 746d9526d..1250f39a5 100644 --- a/integration_test/functions/package.json +++ b/integration_test/functions/package.json @@ -5,16 +5,20 @@ "build": "./node_modules/.bin/tsc" }, "dependencies": { - "@google-cloud/pubsub": "^0.6.0", - "@types/lodash": "^4.14.41", - "firebase": "^4.9.1", + "@google-cloud/pubsub": "~0.19.0", + "@types/google-cloud__pubsub": "^0.18.0", + "@types/lodash": "~4.14.41", + "firebase": "~5.2.0", "firebase-admin": "~5.12.1", "firebase-functions": "./firebase-functions.tgz", - "lodash": "^4.17.2" + "lodash": "~4.17.2" }, "main": "lib/index.js", "devDependencies": { "typescript": "~2.8.3" }, + "engines": { + "node": "8" + }, "private": true } diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 42626dbec..4ea9c7b59 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -4,6 +4,9 @@ import * as https from 'https'; import * as admin from 'firebase-admin'; import { Request, Response } from 'express'; +import * as PubSub from '@google-cloud/pubsub'; +const pubsub = PubSub(); + export * from './pubsub-tests'; export * from './database-tests'; export * from './auth-tests'; @@ -14,7 +17,6 @@ const numTests = Object.keys(exports).length; // Assumption: every exported func import 'firebase-functions'; // temporary shim until process.env.FIREBASE_CONFIG available natively in GCF(BUG 63586213) const firebaseConfig = JSON.parse(process.env.FIREBASE_CONFIG); firebase.initializeApp(firebaseConfig); -console.log('initializing admin'); admin.initializeApp(); // TODO(klimt): Get rid of this once the JS client SDK supports callable triggers. @@ -45,22 +47,22 @@ function callHttpsTrigger(name: string, data: any) { export const integrationTests: any = functions.https.onRequest( (req: Request, resp: Response) => { - let pubsub: any = require('@google-cloud/pubsub')(); - const testId = firebase .database() .ref() .push().key; return Promise.all([ // A database write to trigger the Firebase Realtime Database tests. - // The database write happens without admin privileges, so that the triggered function's "event.data.ref" also - // doesn't have admin privileges. + // The database write happens without admin privileges. firebase .database() .ref(`dbTests/${testId}/start`) .set({ '.sv': 'timestamp' }), // A Pub/Sub publish to trigger the Cloud Pub/Sub tests. - pubsub.topic('pubsubTests').publish({ testId }), + pubsub + .topic('pubsubTests') + .publisher() + .publish(Buffer.from(JSON.stringify({ testId }))), // A user creation to trigger the Firebase Auth user creation tests. admin .auth() diff --git a/integration_test/functions/tsconfig.json b/integration_test/functions/tsconfig.json index 554bd3a6b..a8e9362db 100644 --- a/integration_test/functions/tsconfig.json +++ b/integration_test/functions/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "lib": ["es6"], + "lib": ["es6", "dom"], "module": "commonjs", "target": "es6", "noImplicitAny": false, diff --git a/package.json b/package.json index 316ef0422..0ad4d32ce 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,11 @@ "build": "node_modules/.bin/tsc -p tsconfig.release.json", "build:pack": "rm -rf lib && npm install && node_modules/.bin/tsc -p tsconfig.release.json && npm pack", "build:release": "npm install --production && npm install typescript firebase-admin && node_modules/.bin/tsc -p tsconfig.release.json", - "lint": "node_modules/.bin/tslint src/{**/*,*}.ts spec/{**/*,*}.ts integration_test/functions/src/{**/*,*}.ts", "format": "prettier --write '**/*.ts'", "pretest": "node_modules/.bin/tsc && cp -r spec/fixtures .tmp/spec", "test": "npm run mocha", "mocha": "mocha .tmp/spec/index.spec.js", - "posttest": "npm run lint && rm -rf .tmp", + "posttest": "npm run format && rm -rf .tmp", "postinstall": "node ./upgrade-warning" }, "repository": { @@ -48,8 +47,7 @@ "nock": "^9.0.0", "prettier": "^1.13.7", "sinon": "^1.17.4", - "typescript": "~2.8.3", - "tslint": "^3.15.1" + "typescript": "~2.8.3" }, "peerDependencies": { "firebase-admin": "~5.12.1" diff --git a/spec/cloud-functions.spec.ts b/spec/cloud-functions.spec.ts index 7f3564339..cde9db5a9 100644 --- a/spec/cloud-functions.spec.ts +++ b/spec/cloud-functions.spec.ts @@ -38,10 +38,17 @@ describe('makeCloudFunction', () => { service: 'service', triggerResource: () => 'resource', handler: () => null, + legacyEventType: 'providers/provider/eventTypes/event', }; it('should put a __trigger on the returned CloudFunction', () => { - let cf = makeCloudFunction(cloudFunctionArgs); + let cf = makeCloudFunction({ + provider: 'mock.provider', + eventType: 'mock.event', + service: 'service', + triggerResource: () => 'resource', + handler: () => null, + }); expect(cf.__trigger).to.deep.equal({ eventTrigger: { eventType: 'mock.provider.mock.event', @@ -51,6 +58,17 @@ describe('makeCloudFunction', () => { }); }); + it('should have legacy event type in __trigger if provided', () => { + let cf = makeCloudFunction(cloudFunctionArgs); + expect(cf.__trigger).to.deep.equal({ + eventTrigger: { + eventType: 'providers/provider/eventTypes/event', + resource: 'resource', + service: 'service', + }, + }); + }); + it('should construct the right context for legacy event format', () => { let args: any = _.assign({}, cloudFunctionArgs, { handler: (data: any, context: EventContext) => context, @@ -105,6 +123,39 @@ describe('makeCloudFunction', () => { params: {}, }); }); + + it('should handle Node 8 function signature', () => { + let args: any = _.assign({}, cloudFunctionArgs, { + handler: (data: any, context: EventContext) => { + return { data, context }; + }, + }); + let cf = makeCloudFunction(args); + let testContext = { + eventId: '00000', + timestamp: '2016-11-04T21:29:03.496Z', + eventType: 'provider.event', + resource: { + service: 'provider', + name: 'resource', + }, + }; + let testData = 'data'; + + return expect(cf(testData, testContext)).to.eventually.deep.equal({ + data: 'data', + context: { + eventId: '00000', + timestamp: '2016-11-04T21:29:03.496Z', + eventType: 'provider.event', + resource: { + service: 'provider', + name: 'resource', + }, + params: {}, + }, + }); + }); }); describe('makeParams', () => { @@ -114,13 +165,14 @@ describe('makeParams', () => { service: 'service', triggerResource: () => 'projects/_/instances/pid/ref/{foo}/nested/{bar}', handler: (data, context) => context.params, + legacyEventType: 'legacyEvent', }; const cf = makeCloudFunction(args); it('should construct params from the event resource of legacy events', () => { const testEvent: LegacyEvent = { resource: 'projects/_/instances/pid/ref/a/nested/b', - eventType: 'event', + eventType: 'legacyEvent', data: 'data', }; @@ -160,14 +212,14 @@ describe('makeAuth and makeAuthType', () => { handler: (data, context) => { return { auth: context.auth, - authMode: context.authType, + authType: context.authType, }; }, }; let cf = makeCloudFunction(args); it('should construct correct auth and authType for admin user', () => { - const testEvent: LegacyEvent = { + const testEvent = { data: 'data', auth: { admin: true, @@ -176,12 +228,12 @@ describe('makeAuth and makeAuthType', () => { return expect(cf(testEvent)).to.eventually.deep.equal({ auth: undefined, - authMode: 'ADMIN', + authType: 'ADMIN', }); }); it('should construct correct auth and authType for unauthenticated user', () => { - const testEvent: LegacyEvent = { + const testEvent = { data: 'data', auth: { admin: false, @@ -190,7 +242,7 @@ describe('makeAuth and makeAuthType', () => { return expect(cf(testEvent)).to.eventually.deep.equal({ auth: null, - authMode: 'UNAUTHENTICATED', + authType: 'UNAUTHENTICATED', }); }); @@ -216,7 +268,7 @@ describe('makeAuth and makeAuthType', () => { sub: 'user', }, }, - authMode: 'USER', + authType: 'USER', }); }); }); diff --git a/spec/providers/firestore.spec.ts b/spec/providers/firestore.spec.ts index cdd9d4c0e..16a1f41fd 100644 --- a/spec/providers/firestore.spec.ts +++ b/spec/providers/firestore.spec.ts @@ -21,17 +21,37 @@ // SOFTWARE. import * as firestore from '../../src/providers/firestore'; +import * as _ from 'lodash'; import { expect } from 'chai'; describe('Firestore Functions', () => { - let constructValue = (fields: any) => { + function constructValue(fields: any) { return { fields: fields, name: 'projects/pid/databases/(default)/documents/collection/123', createTime: '2017-06-02T18:48:58.920638Z', updateTime: '2017-07-02T18:48:58.920638Z', }; - }; + } + + function makeEvent(data: any, context?: { [key: string]: any }) { + context = context || {}; + return { + data: data, + context: _.merge( + { + eventId: '123', + timestamp: '2018-07-03T00:49:04.264Z', + eventType: 'google.firestore.document.create', + resource: { + name: 'projects/myproj/databases/(default)/documents/tests/test1', + service: 'service', + }, + }, + context + ), + }; + } describe('document builders and event types', () => { function expectedTrigger(resource: string, eventType: string) { @@ -158,7 +178,11 @@ describe('Firestore Functions', () => { oldValue: oldValue, value: value, }, - context: {}, + context: { + resource: { + name: 'resource', + }, + }, }; } @@ -184,6 +208,14 @@ describe('Firestore Functions', () => { }); } + before(() => { + process.env.GCLOUD_PROJECT = 'project1'; + }); + + after(() => { + delete process.env.GCLOUD_PROJECT; + }); + it('constructs appropriate fields and getters for event.data on "document.write" events', () => { let testFunction = firestore.document('path').onWrite(change => { expect(change.before.data()).to.deep.equal({ key1: false, key2: 111 }); @@ -240,47 +272,47 @@ describe('Firestore Functions', () => { describe('SnapshotConstructor', () => { describe('#data()', () => { it('should parse int values', () => { - let snapshot = firestore.snapshotConstructor({ - data: { + let snapshot = firestore.snapshotConstructor( + makeEvent({ value: constructValue({ key: { integerValue: '123' } }), - }, - }); + }) + ); expect(snapshot.data()).to.deep.equal({ key: 123 }); }); it('should parse double values', () => { - let snapshot = firestore.snapshotConstructor({ - data: { + let snapshot = firestore.snapshotConstructor( + makeEvent({ value: constructValue({ key: { doubleValue: 12.34 } }), - }, - }); + }) + ); expect(snapshot.data()).to.deep.equal({ key: 12.34 }); }); it('should parse null values', () => { - let snapshot = firestore.snapshotConstructor({ - data: { + let snapshot = firestore.snapshotConstructor( + makeEvent({ value: constructValue({ key: { nullValue: null } }), - }, - }); + }) + ); expect(snapshot.data()).to.deep.equal({ key: null }); }); it('should parse boolean values', () => { - let snapshot = firestore.snapshotConstructor({ - data: { + let snapshot = firestore.snapshotConstructor( + makeEvent({ value: constructValue({ key: { booleanValue: true } }), - }, - }); + }) + ); expect(snapshot.data()).to.deep.equal({ key: true }); }); it('should parse string values', () => { - let snapshot = firestore.snapshotConstructor({ - data: { + let snapshot = firestore.snapshotConstructor( + makeEvent({ value: constructValue({ key: { stringValue: 'foo' } }), - }, - }); + }) + ); expect(snapshot.data()).to.deep.equal({ key: 'foo' }); }); @@ -292,9 +324,11 @@ describe('Firestore Functions', () => { }, }, }); - let snapshot = firestore.snapshotConstructor({ - data: { value: raw }, - }); + let snapshot = firestore.snapshotConstructor( + makeEvent({ + value: raw, + }) + ); expect(snapshot.data()).to.deep.equal({ key: [1, 2] }); }); @@ -313,9 +347,11 @@ describe('Firestore Functions', () => { }, }, }); - let snapshot = firestore.snapshotConstructor({ - data: { value: raw }, - }); + let snapshot = firestore.snapshotConstructor( + makeEvent({ + value: raw, + }) + ); expect(snapshot.data()).to.deep.equal({ keyParent: { key1: 'val1', key2: 'val2' }, }); @@ -336,9 +372,11 @@ describe('Firestore Functions', () => { }, }, }); - let snapshot = firestore.snapshotConstructor({ - data: { value: raw }, - }); + let snapshot = firestore.snapshotConstructor( + makeEvent({ + value: raw, + }) + ); expect(snapshot.data()).to.deep.equal({ geoPointValue: { latitude: 40.73, @@ -354,9 +392,11 @@ describe('Firestore Functions', () => { 'projects/proj1/databases/(default)/documents/doc1/id', }, }); - let snapshot = firestore.snapshotConstructor({ - data: { value: raw }, - }); + let snapshot = firestore.snapshotConstructor( + makeEvent({ + value: raw, + }) + ); expect(snapshot.data()['referenceVal'].path).to.equal('doc1/id'); }); @@ -366,9 +406,11 @@ describe('Firestore Functions', () => { timestampValue: '2017-06-13T00:58:40.349Z', }, }); - let snapshot = firestore.snapshotConstructor({ - data: { value: raw }, - }); + let snapshot = firestore.snapshotConstructor( + makeEvent({ + value: raw, + }) + ); expect(snapshot.data()).to.deep.equal({ timestampVal: new Date('2017-06-13T00:58:40.349Z'), }); @@ -380,9 +422,11 @@ describe('Firestore Functions', () => { timestampValue: '2017-06-13T00:58:40Z', }, }); - let snapshot = firestore.snapshotConstructor({ - data: { value: raw }, - }); + let snapshot = firestore.snapshotConstructor( + makeEvent({ + value: raw, + }) + ); expect(snapshot.data()).to.deep.equal({ timestampVal: new Date('2017-06-13T00:58:40Z'), }); @@ -395,9 +439,11 @@ describe('Firestore Functions', () => { bytesValue: 'Zm9vYmFy', }, }); - let snapshot = firestore.snapshotConstructor({ - data: { value: raw }, - }); + let snapshot = firestore.snapshotConstructor( + makeEvent({ + value: raw, + }) + ); expect(snapshot.data()).to.deep.equal({ binaryVal: new Buffer('foobar'), }); @@ -408,8 +454,8 @@ describe('Firestore Functions', () => { let snapshot: FirebaseFirestore.DocumentSnapshot; before(() => { - snapshot = firestore.snapshotConstructor({ - data: { + snapshot = firestore.snapshotConstructor( + makeEvent({ value: { fields: { key: { integerValue: '1' } }, createTime: '2017-06-17T14:45:17.876479Z', @@ -417,8 +463,8 @@ describe('Firestore Functions', () => { readTime: '2017-07-31T18:23:26.928527Z', name: 'projects/pid/databases/(default)/documents/collection/123', }, - }, - }); + }) + ); }); it('should support #exists', () => { @@ -458,27 +504,34 @@ describe('Firestore Functions', () => { describe('Handle empty and non-existent documents', () => { it('constructs non-existent DocumentSnapshot when whole document deleted', () => { - let snapshot = firestore.snapshotConstructor({ - data: { - value: {}, // value is empty when the whole document is deleted - }, - resource: 'projects/pid/databases/(default)/documents/collection/123', - }); + let snapshot = firestore.snapshotConstructor( + makeEvent( + { + value: {}, // value is empty when the whole document is deleted + }, + { + resource: { + name: + 'projects/pid/databases/(default)/documents/collection/123', + }, + } + ) + ); expect(snapshot.exists).to.be.false; expect(snapshot.ref.path).to.equal('collection/123'); }); it('constructs existent DocumentSnapshot with empty data when all fields of document deleted', () => { - let snapshot = firestore.snapshotConstructor({ - data: { + let snapshot = firestore.snapshotConstructor( + makeEvent({ value: { // value is not empty when document still exists createTime: '2017-06-02T18:48:58.920638Z', updateTime: '2017-07-02T18:48:58.920638Z', name: 'projects/pid/databases/(default)/documents/collection/123', }, - }, - }); + }) + ); expect(snapshot.exists).to.be.true; expect(snapshot.ref.path).to.equal('collection/123'); expect(snapshot.data()).to.deep.equal({}); diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index bf36b3dce..2f873f9b7 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -191,7 +191,7 @@ export type HttpsFunction = TriggerAnnotated & */ export type CloudFunction = Runnable & TriggerAnnotated & - ((input: any) => PromiseLike | any); + ((input: any, context?: any) => PromiseLike | any); /** @internal */ export interface MakeCloudFunctionArgs { @@ -201,10 +201,10 @@ export interface MakeCloudFunctionArgs { eventType: string; triggerResource: () => string; service: string; - dataConstructor?: (raw: Event | LegacyEvent) => EventData; + dataConstructor?: (raw: Event) => EventData; handler: (data: EventData, context: EventContext) => PromiseLike | any; - before?: (raw: Event | LegacyEvent) => void; - after?: (raw: Event | LegacyEvent) => void; + before?: (raw: Event) => void; + after?: (raw: Event) => void; legacyEventType?: string; } @@ -214,7 +214,7 @@ export function makeCloudFunction({ eventType, triggerResource, service, - dataConstructor = (raw: Event | LegacyEvent) => raw.data, + dataConstructor = (raw: Event) => raw.data, handler, before = () => { return; @@ -224,42 +224,55 @@ export function makeCloudFunction({ }, legacyEventType, }: MakeCloudFunctionArgs): CloudFunction { - let cloudFunction: any = async (event: Event | LegacyEvent) => { - if (!_.has(event, 'data')) { - throw Error( - 'Cloud function needs to be called with an event parameter.' + - 'If you are writing unit tests, please use the Node module firebase-functions-fake.' - ); + let cloudFunction: any = async (eventOrData: any, context?: any) => { + let data: any; + if (isContext(context)) { + // In Node 8 runtime, function called with 2 params: data & context + data = eventOrData; + } else { + // In Node 6 runtime, function called with single event param + data = _.get(eventOrData, 'data'); + if (isEvent(eventOrData)) { + // new eventflow v1beta2 format + context = _.cloneDeep(eventOrData.context); + } else { + // eventflow v1beta1 format + context = _.omit(eventOrData, 'data'); + } } - try { - before(event); - let dataOrChange = dataConstructor(event); - let context: any; - if (isEvent(event)) { - // new event format - context = _.cloneDeep(event.context); + if (legacyEventType && context.eventType === legacyEventType) { + // v1beta1 event flow has different format for context, transform them to new format. + context = { + eventId: context.eventId, + timestamp: context.timestamp, + eventType: provider + '.' + eventType, + resource: { + service: service, + name: context.resource, + }, + }; + } + + let event: Event = { + data, + context, + }; + + if (provider === 'google.firebase.database') { + context.authType = _detectAuthType(event); + if (context.authType !== 'ADMIN') { + context.auth = _makeAuth(event, context.authType); } else { - // legacy event format - context = { - eventId: event.eventId, - timestamp: event.timestamp, - eventType: provider + '.' + eventType, - resource: { - service: service, - name: event.resource, - }, - }; - if (provider === 'google.firebase.database') { - context.authType = _detectAuthType(event); - if (context.authType !== 'ADMIN') { - context.auth = _makeAuth(event, context.authType); - } - } + delete context.auth; } + } + context.params = context.params || _makeParams(context, triggerResource); - context.params = _makeParams(context, triggerResource); + try { + before(event); + let dataOrChange = dataConstructor(event); let promise = handler(dataOrChange, context); if (typeof promise === 'undefined') { console.warn('Function returned undefined, expected Promise or value'); @@ -288,6 +301,10 @@ function isEvent(event: Event | LegacyEvent): event is Event { return _.has(event, 'context'); } +function isContext(context: EventContext | any): context is EventContext { + return _.has(context, 'eventId'); +} + function _makeParams( context: EventContext, triggerResourceGetter: () => string @@ -315,21 +332,21 @@ function _makeParams( return params; } -function _makeAuth(event: LegacyEvent, authType: string) { +function _makeAuth(event: Event, authType: string) { if (authType === 'UNAUTHENTICATED') { return null; } return { - uid: _.get(event, 'auth.variable.uid'), - token: _.get(event, 'auth.variable.token'), + uid: _.get(event, 'context.auth.variable.uid'), + token: _.get(event, 'context.auth.variable.token'), }; } -function _detectAuthType(event: LegacyEvent) { - if (_.get(event, 'auth.admin')) { +function _detectAuthType(event: Event) { + if (_.get(event, 'context.auth.admin')) { return 'ADMIN'; } - if (_.has(event, 'auth.variable')) { + if (_.has(event, 'context.auth.variable')) { return 'USER'; } return 'UNAUTHENTICATED'; diff --git a/src/providers/auth.ts b/src/providers/auth.ts index 47b5c0ece..b35a9491c 100644 --- a/src/providers/auth.ts +++ b/src/providers/auth.ts @@ -24,7 +24,7 @@ import { makeCloudFunction, CloudFunction, EventContext, - LegacyEvent, + Event, } from '../cloud-functions'; import * as firebase from 'firebase-admin'; import * as _ from 'lodash'; @@ -58,7 +58,7 @@ export class UserRecordMetadata implements firebase.auth.UserMetadata { /** Builder used to create Cloud Functions for Firebase Auth user lifecycle events. */ export class UserBuilder { - private static dataConstructor(raw: LegacyEvent): firebase.auth.UserRecord { + private static dataConstructor(raw: Event): firebase.auth.UserRecord { return userRecordConstructor(raw.data); } diff --git a/src/providers/database.ts b/src/providers/database.ts index e3788f899..5bb8ca3e8 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -23,7 +23,6 @@ import * as _ from 'lodash'; import { apps } from '../apps'; import { - LegacyEvent, CloudFunction, makeCloudFunction, Event, @@ -142,8 +141,10 @@ export class RefBuilder { context: EventContext ) => PromiseLike | any ): CloudFunction { - let dataConstructor = (raw: LegacyEvent) => { - let [dbInstance, path] = resourceToInstanceAndPath(raw.resource); + let dataConstructor = (raw: Event) => { + let [dbInstance, path] = resourceToInstanceAndPath( + raw.context.resource.name + ); return new DataSnapshot( raw.data.delta, path, @@ -161,8 +162,10 @@ export class RefBuilder { context: EventContext ) => PromiseLike | any ): CloudFunction { - let dataConstructor = (raw: LegacyEvent) => { - let [dbInstance, path] = resourceToInstanceAndPath(raw.resource); + let dataConstructor = (raw: Event) => { + let [dbInstance, path] = resourceToInstanceAndPath( + raw.context.resource.name + ); return new DataSnapshot(raw.data.data, path, this.apps.admin, dbInstance); }; return this.onOperation(handler, 'ref.delete', dataConstructor); @@ -171,7 +174,7 @@ export class RefBuilder { private onOperation( handler: (data: T, context: EventContext) => PromiseLike | any, eventType: string, - dataConstructor: (raw: Event | LegacyEvent) => any + dataConstructor: (raw: Event | Event) => any ): CloudFunction { return makeCloudFunction({ handler, @@ -186,8 +189,10 @@ export class RefBuilder { }); } - private changeConstructor = (raw: LegacyEvent): Change => { - let [dbInstance, path] = resourceToInstanceAndPath(raw.resource); + private changeConstructor = (raw: Event): Change => { + let [dbInstance, path] = resourceToInstanceAndPath( + raw.context.resource.name + ); let before = new DataSnapshot( raw.data.data, path, diff --git a/src/providers/firestore.ts b/src/providers/firestore.ts index 426caa650..6de8dd970 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -27,7 +27,6 @@ import { apps } from '../apps'; import { makeCloudFunction, CloudFunction, - LegacyEvent, Change, Event, EventContext, @@ -116,31 +115,37 @@ function _getValueProto(data: any, resource: string, valueFieldName: string) { } /** @internal */ -export function snapshotConstructor(event: LegacyEvent): DocumentSnapshot { +export function snapshotConstructor(event: Event): DocumentSnapshot { if (!firestoreInstance) { firestoreInstance = firebase.firestore(apps().admin); } - let valueProto = _getValueProto(event.data, event.resource, 'value'); + let valueProto = _getValueProto( + event.data, + event.context.resource.name, + 'value' + ); let readTime = dateToTimestampProto(_.get(event, 'data.value.readTime')); return firestoreInstance.snapshot_(valueProto, readTime, 'json'); } /** @internal */ // TODO remove this function when wire format changes to new format -export function beforeSnapshotConstructor( - event: LegacyEvent -): DocumentSnapshot { +export function beforeSnapshotConstructor(event: Event): DocumentSnapshot { if (!firestoreInstance) { firestoreInstance = firebase.firestore(apps().admin); } - let oldValueProto = _getValueProto(event.data, event.resource, 'oldValue'); + let oldValueProto = _getValueProto( + event.data, + event.context.resource.name, + 'oldValue' + ); let oldReadTime = dateToTimestampProto( _.get(event, 'data.oldValue.readTime') ); return firestoreInstance.snapshot_(oldValueProto, oldReadTime, 'json'); } -function changeConstructor(raw: LegacyEvent) { +function changeConstructor(raw: Event) { return Change.fromObjects( beforeSnapshotConstructor(raw), snapshotConstructor(raw) @@ -200,7 +205,7 @@ export class DocumentBuilder { private onOperation( handler: (data: T, context: EventContext) => PromiseLike | any, eventType: string, - dataConstructor: (raw: Event | LegacyEvent) => any + dataConstructor: (raw: Event) => any ): CloudFunction { return makeCloudFunction({ handler, From 6d156d1f7a2e359acabe831361a2b72039d83cb8 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Fri, 13 Jul 2018 10:29:25 -0700 Subject: [PATCH 040/705] Allow selection of region, timeout and memory allocation (#202) --- .../functions/src/firestore-tests.ts | 7 +- integration_test/functions/src/index.ts | 12 +- spec/function-builder.spec.ts | 113 ++++++++ spec/index.spec.ts | 1 + spec/providers/analytics.spec.ts | 16 ++ spec/providers/auth.spec.ts | 20 +- spec/providers/crashlytics.spec.ts | 19 +- spec/providers/database.spec.ts | 16 ++ spec/providers/firestore.spec.ts | 16 ++ spec/providers/https.spec.ts | 31 ++- spec/providers/pubsub.spec.ts | 18 +- spec/providers/storage.spec.ts | 30 ++- src/cloud-functions.ts | 35 ++- src/function-builder.ts | 250 ++++++++++++++++++ src/index.ts | 5 +- src/providers/analytics.ts | 29 +- src/providers/auth.ts | 18 +- src/providers/crashlytics.ts | 14 +- src/providers/database.ts | 75 ++++-- src/providers/firestore.ts | 61 +++-- src/providers/https.ts | 46 +++- src/providers/pubsub.ts | 23 +- src/providers/storage.ts | 43 ++- 23 files changed, 799 insertions(+), 99 deletions(-) create mode 100644 spec/function-builder.spec.ts create mode 100644 src/function-builder.ts diff --git a/integration_test/functions/src/firestore-tests.ts b/integration_test/functions/src/firestore-tests.ts index 26def7fff..1a9dd1d7e 100644 --- a/integration_test/functions/src/firestore-tests.ts +++ b/integration_test/functions/src/firestore-tests.ts @@ -5,8 +5,11 @@ import DocumentSnapshot = admin.firestore.DocumentSnapshot; const testIdFieldName = 'documentId'; -export const firestoreTests: any = functions.firestore - .document('tests/{documentId}') +export const firestoreTests: any = functions + .runWith({ + timeoutSeconds: 540, + }) + .firestore.document('tests/{documentId}') .onCreate((s, c) => { return new TestSuite('firestore document onWrite') diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 4ea9c7b59..64e3aca5a 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -45,8 +45,13 @@ function callHttpsTrigger(name: string, data: any) { }); } -export const integrationTests: any = functions.https.onRequest( - (req: Request, resp: Response) => { +export const integrationTests: any = functions + .runWith({ + timeoutSeconds: 540, + }) + .https.onRequest((req: Request, resp: Response) => { + let pubsub: any = require('@google-cloud/pubsub')(); + const testId = firebase .database() .ref() @@ -134,5 +139,4 @@ export const integrationTests: any = functions.https.onRequest( }.firebaseio.com/testRuns/${testId}` ); }); - } -); + }); diff --git a/spec/function-builder.spec.ts b/spec/function-builder.spec.ts new file mode 100644 index 000000000..a1733fb1d --- /dev/null +++ b/spec/function-builder.spec.ts @@ -0,0 +1,113 @@ +// The MIT License (MIT) +// +// Copyright (c) 2017 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { expect } from 'chai'; + +import * as functions from '../src/index'; + +describe('FunctionBuilder', () => { + before(() => { + process.env.GCLOUD_PROJECT = 'not-a-project'; + }); + + after(() => { + delete process.env.GCLOUD_PROJECT; + }); + + it('should allow region to be set', () => { + let fn = functions + .region('my-region') + .auth.user() + .onCreate(user => user); + + expect(fn.__trigger.regions).to.deep.equal(['my-region']); + }); + + it('should allow multiple regions to be set', () => { + let fn = functions + .region('my-region', 'my-other-region') + .auth.user() + .onCreate(user => user); + + expect(fn.__trigger.regions).to.deep.equal([ + 'my-region', + 'my-other-region', + ]); + }); + + it('should allow runtime options to be set', () => { + let fn = functions + .runWith({ + timeoutSeconds: 90, + memory: '256MB', + }) + .auth.user() + .onCreate(user => user); + + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal('90s'); + }); + + it('should allow both region and runtime options to be set', () => { + let fn = functions + .region('my-region') + .runWith({ + timeoutSeconds: 90, + memory: '256MB', + }) + .auth.user() + .onCreate(user => user); + + expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal('90s'); + }); + + it('should allow both region and runtime options to be set (reverse order)', () => { + let fn = functions + .runWith({ + timeoutSeconds: 90, + memory: '256MB', + }) + .region('my-region') + .auth.user() + .onCreate(user => user); + + expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal('90s'); + }); + + it('should throw an error if user chooses an unsupported memory allocation', () => { + expect(() => { + return functions.runWith({ + memory: 'unsupported', + } as any); + }).to.throw(Error); + + expect(() => { + return functions.region('some-region').runWith({ + memory: 'unsupported', + } as any); + }).to.throw(Error); + }); +}); diff --git a/spec/index.spec.ts b/spec/index.spec.ts index 918ca80d1..6c64e5cba 100644 --- a/spec/index.spec.ts +++ b/spec/index.spec.ts @@ -33,6 +33,7 @@ import './apps.spec'; import './cloud-functions.spec'; import './config.spec'; import './testing.spec'; +import './function-builder.spec'; import './providers/analytics.spec'; import './providers/auth.spec'; import './providers/database.spec'; diff --git a/spec/providers/analytics.spec.ts b/spec/providers/analytics.spec.ts index e2a58081d..e56e354f6 100644 --- a/spec/providers/analytics.spec.ts +++ b/spec/providers/analytics.spec.ts @@ -24,6 +24,7 @@ import * as analytics from '../../src/providers/analytics'; import { expect } from 'chai'; import { LegacyEvent } from '../../src/cloud-functions'; import * as analytics_spec_input from './analytics.spec.input'; +import * as functions from '../../src/index'; describe('Analytics Functions', () => { describe('EventBuilder', () => { @@ -35,6 +36,21 @@ describe('Analytics Functions', () => { delete process.env.GCLOUD_PROJECT; }); + it('should allow both region and runtime options to be set', () => { + let fn = functions + .region('my-region') + .runWith({ + timeoutSeconds: 90, + memory: '256MB', + }) + .analytics.event('event') + .onLog(event => event); + + expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal('90s'); + }); + describe('#onLog', () => { it('should return a TriggerDefinition with appropriate values', () => { const cloudFunction = analytics.event('first_open').onLog(() => null); diff --git a/spec/providers/auth.spec.ts b/spec/providers/auth.spec.ts index 603b0e00e..f63840f2d 100644 --- a/spec/providers/auth.spec.ts +++ b/spec/providers/auth.spec.ts @@ -20,10 +20,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import * as auth from '../../src/providers/auth'; import { expect } from 'chai'; import * as firebase from 'firebase-admin'; -import { CloudFunction } from '../../src'; +import * as auth from '../../src/providers/auth'; +import { CloudFunction } from '../../src/cloud-functions'; +import * as functions from '../../src/index'; describe('Auth Functions', () => { describe('AuthBuilder', () => { @@ -37,6 +38,21 @@ describe('Auth Functions', () => { delete process.env.GCLOUD_PROJECT; }); + it('should allow both region and runtime options to be set', () => { + let fn = functions + .region('my-region') + .runWith({ + timeoutSeconds: 90, + memory: '256MB', + }) + .auth.user() + .onCreate(() => null); + + expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal('90s'); + }); + describe('#onCreate', () => { it('should return a TriggerDefinition with appropriate values', () => { const cloudFunction = auth.user().onCreate(() => null); diff --git a/spec/providers/crashlytics.spec.ts b/spec/providers/crashlytics.spec.ts index 517324590..391ae016a 100644 --- a/spec/providers/crashlytics.spec.ts +++ b/spec/providers/crashlytics.spec.ts @@ -20,9 +20,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import { expect } from 'chai'; + import * as crashlytics from '../../src/providers/crashlytics'; import { apps as appsNamespace } from '../../src/apps'; -import { expect } from 'chai'; +import * as functions from '../../src/index'; describe('Crashlytics Functions', () => { describe('Issue Builder', () => { @@ -36,6 +38,21 @@ describe('Crashlytics Functions', () => { delete process.env.GCLOUD_PROJECT; }); + it('should allow both region and runtime options to be set', () => { + let fn = functions + .region('my-region') + .runWith({ + timeoutSeconds: 90, + memory: '256MB', + }) + .crashlytics.issue() + .onNew(issue => issue); + + expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal('90s'); + }); + describe('#onNew', () => { it('should return a TriggerDefinition with appropriate values', () => { const cloudFunction = crashlytics.issue().onNew(data => null); diff --git a/spec/providers/database.spec.ts b/spec/providers/database.spec.ts index 55f8c2b4d..87d502243 100644 --- a/spec/providers/database.spec.ts +++ b/spec/providers/database.spec.ts @@ -24,6 +24,7 @@ import * as database from '../../src/providers/database'; import { expect } from 'chai'; import { apps as appsNamespace } from '../../src/apps'; import { applyChange } from '../../src/utils'; +import * as functions from '../../src/index'; describe('Database Functions', () => { describe('DatabaseBuilder', () => { @@ -41,6 +42,21 @@ describe('Database Functions', () => { delete appsNamespace.singleton; }); + it('should allow both region and runtime options to be set', () => { + let fn = functions + .region('my-region') + .runWith({ + timeoutSeconds: 90, + memory: '256MB', + }) + .database.ref('/') + .onCreate(snap => snap); + + expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal('90s'); + }); + describe('#onWrite()', () => { it('should return "ref.write" as the event type', () => { let eventType = database.ref('foo').onWrite(() => null).__trigger diff --git a/spec/providers/firestore.spec.ts b/spec/providers/firestore.spec.ts index 16a1f41fd..5a03c10d1 100644 --- a/spec/providers/firestore.spec.ts +++ b/spec/providers/firestore.spec.ts @@ -23,6 +23,7 @@ import * as firestore from '../../src/providers/firestore'; import * as _ from 'lodash'; import { expect } from 'chai'; +import * as functions from '../../src/index'; describe('Firestore Functions', () => { function constructValue(fields: any) { @@ -117,6 +118,21 @@ describe('Firestore Functions', () => { ); }); + it('should allow both region and runtime options to be set', () => { + let fn = functions + .region('my-region') + .runWith({ + timeoutSeconds: 90, + memory: '256MB', + }) + .firestore.document('doc') + .onCreate(snap => snap); + + expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal('90s'); + }); + it('onCreate should have the "document.create" eventType', () => { let resource = 'projects/project1/databases/(default)/documents/users/{uid}'; diff --git a/spec/providers/https.spec.ts b/spec/providers/https.spec.ts index cc20c1cb0..7c6deb458 100644 --- a/spec/providers/https.spec.ts +++ b/spec/providers/https.spec.ts @@ -20,6 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import { expect } from 'chai'; import * as express from 'express'; import * as firebase from 'firebase-admin'; import * as https from '../../src/providers/https'; @@ -28,7 +29,7 @@ import * as mocks from '../fixtures/credential/key.json'; import * as nock from 'nock'; import * as _ from 'lodash'; import { apps as appsNamespace } from '../../src/apps'; -import { expect } from 'chai'; +import * as functions from '../../src/index'; describe('CloudHttpsBuilder', () => { describe('#onRequest', () => { @@ -38,6 +39,20 @@ describe('CloudHttpsBuilder', () => { }); expect(result.__trigger).to.deep.equal({ httpsTrigger: {} }); }); + + it('should allow both region and runtime options to be set', () => { + let fn = functions + .region('my-region') + .runWith({ + timeoutSeconds: 90, + memory: '256MB', + }) + .https.onRequest(() => null); + + expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal('90s'); + }); }); }); @@ -490,6 +505,20 @@ describe('callable.FunctionBuilder', () => { }, }); }); + + it('should allow both region and runtime options to be set', () => { + let fn = functions + .region('my-region') + .runWith({ + timeoutSeconds: 90, + memory: '256MB', + }) + .https.onCall(() => null); + + expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal('90s'); + }); }); }); diff --git a/spec/providers/pubsub.spec.ts b/spec/providers/pubsub.spec.ts index 83df21ab0..1363955fb 100644 --- a/spec/providers/pubsub.spec.ts +++ b/spec/providers/pubsub.spec.ts @@ -20,8 +20,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import * as pubsub from '../../src/providers/pubsub'; import { expect } from 'chai'; +import * as pubsub from '../../src/providers/pubsub'; +import * as functions from '../../src/index'; describe('Pubsub Functions', () => { describe('pubsub.Message', () => { @@ -70,6 +71,21 @@ describe('Pubsub Functions', () => { delete process.env.GCLOUD_PROJECT; }); + it('should allow both region and runtime options to be set', () => { + let fn = functions + .region('my-region') + .runWith({ + timeoutSeconds: 90, + memory: '256MB', + }) + .pubsub.topic('toppy') + .onPublish(() => null); + + expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal('90s'); + }); + describe('#onPublish', () => { it('should return a TriggerDefinition with appropriate values', () => { // Pick up project from process.env.GCLOUD_PROJECT diff --git a/spec/providers/storage.spec.ts b/spec/providers/storage.spec.ts index befea8be9..5003db4e6 100644 --- a/spec/providers/storage.spec.ts +++ b/spec/providers/storage.spec.ts @@ -20,8 +20,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import * as storage from '../../src/providers/storage'; import { expect } from 'chai'; +import * as storage from '../../src/providers/storage'; +import * as functions from '../../src/index'; describe('Storage Functions', () => { describe('ObjectBuilder', () => { @@ -35,6 +36,21 @@ describe('Storage Functions', () => { delete process.env.FIREBASE_CONFIG; }); + it('should allow both region and runtime options to be set', () => { + let fn = functions + .region('my-region') + .runWith({ + timeoutSeconds: 90, + memory: '256MB', + }) + .storage.object() + .onArchive(() => null); + + expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); + expect(fn.__trigger.timeout).to.deep.equal('90s'); + }); + describe('#onArchive', () => { it('should return a TriggerDefinition with appropriate values', () => { let cloudFunction = storage @@ -63,7 +79,8 @@ describe('Storage Functions', () => { it('should allow fully qualified bucket names', () => { let subjectQualified = new storage.ObjectBuilder( - () => 'projects/_/buckets/bucky' + () => 'projects/_/buckets/bucky', + {} ); let result = subjectQualified.onArchive(() => null); expect(result.__trigger).to.deep.equal({ @@ -130,7 +147,8 @@ describe('Storage Functions', () => { it('should allow fully qualified bucket names', () => { let subjectQualified = new storage.ObjectBuilder( - () => 'projects/_/buckets/bucky' + () => 'projects/_/buckets/bucky', + {} ); let result = subjectQualified.onDelete(() => null); expect(result.__trigger).to.deep.equal({ @@ -197,7 +215,8 @@ describe('Storage Functions', () => { it('should allow fully qualified bucket names', () => { let subjectQualified = new storage.ObjectBuilder( - () => 'projects/_/buckets/bucky' + () => 'projects/_/buckets/bucky', + {} ); let result = subjectQualified.onFinalize(() => null); expect(result.__trigger).to.deep.equal({ @@ -264,7 +283,8 @@ describe('Storage Functions', () => { it('should allow fully qualified bucket names', () => { let subjectQualified = new storage.ObjectBuilder( - () => 'projects/_/buckets/bucky' + () => 'projects/_/buckets/bucky', + {} ); let result = subjectQualified.onMetadataUpdate(() => null); expect(result.__trigger).to.deep.equal({ diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 2f873f9b7..23bbff189 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -23,7 +23,9 @@ import { apps } from './apps'; import * as _ from 'lodash'; import { Request, Response } from 'express'; +import { DeploymentOptions } from './function-builder'; export { Request, Response }; + const WILDCARD_REGEX = new RegExp('{[^/{}]*}', 'g'); /** Legacy wire format for an event @@ -170,6 +172,9 @@ export interface TriggerAnnotated { service: string; }; labels?: { [key: string]: string }; + regions?: string[]; + timeout?: string; + availableMemoryMb?: number; }; } @@ -206,6 +211,7 @@ export interface MakeCloudFunctionArgs { before?: (raw: Event) => void; after?: (raw: Event) => void; legacyEventType?: string; + opts?: { [key: string]: any }; } /** @internal */ @@ -223,6 +229,7 @@ export function makeCloudFunction({ return; }, legacyEventType, + opts = {}, }: MakeCloudFunctionArgs): CloudFunction { let cloudFunction: any = async (eventOrData: any, context?: any) => { let data: any; @@ -284,15 +291,18 @@ export function makeCloudFunction({ }; Object.defineProperty(cloudFunction, '__trigger', { get: () => { - return { + let trigger: any = _.assign(optsToTrigger(opts), { eventTrigger: { resource: triggerResource(), eventType: legacyEventType || provider + '.' + eventType, service, }, - }; + }); + + return trigger; }, }); + cloudFunction.run = handler; return cloudFunction; } @@ -351,3 +361,24 @@ function _detectAuthType(event: Event) { } return 'UNAUTHENTICATED'; } + +export function optsToTrigger(opts: DeploymentOptions) { + let trigger: any = {}; + if (opts.regions) { + trigger.regions = opts.regions; + } + if (opts.timeoutSeconds) { + trigger.timeout = opts.timeoutSeconds.toString() + 's'; + } + if (opts.memory) { + const memoryLookup = { + '128MB': 128, + '256MB': 256, + '512MB': 512, + '1GB': 1024, + '2GB': 2048, + }; + trigger.availableMemoryMb = _.get(memoryLookup, opts.memory); + } + return trigger; +} diff --git a/src/function-builder.ts b/src/function-builder.ts new file mode 100644 index 000000000..bee575a49 --- /dev/null +++ b/src/function-builder.ts @@ -0,0 +1,250 @@ +// The MIT License (MIT) +// +// Copyright (c) 2017 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import * as _ from 'lodash'; +import * as express from 'express'; + +import * as analytics from './providers/analytics'; +import * as auth from './providers/auth'; +import * as crashlytics from './providers/crashlytics'; +import * as database from './providers/database'; +import * as firestore from './providers/firestore'; +import * as https from './providers/https'; +import * as pubsub from './providers/pubsub'; +import * as storage from './providers/storage'; +import { HttpsFunction } from './cloud-functions'; + +/** + * Configure the regions that the function is deployed to. + * @param regions One or more parameters indicating the regions. + * For example: `functions.region('us-central1', 'us-east1')` + */ +export function region(...regions: string[]) { + return new FunctionBuilder({ regions }); +} + +/** + * Configure runtime options for the function. + * @param runtimeOptions Object with 2 optional fields: + * 1. `timeoutSeconds`: timeout for the function in seconds. + * 2. `memory`: amount of memory to allocate to the function, + * possible values are: '128MB', '256MB', '512MB', '1GB', and '2GB'. + */ +export function runWith(runtimeOptions: { + timeoutSeconds?: number; + memory?: '128MB' | '256MB' | '512MB' | '1GB' | '2GB'; +}) { + if ( + runtimeOptions.memory && + !_.includes( + ['128MB', '256MB', '512MB', '1GB', '2GB'], + runtimeOptions.memory + ) + ) { + throw new Error( + "The only valid memory allocation values are: '128MB', '256MB', '512MB', '1GB', and '2GB'" + ); + } + return new FunctionBuilder(runtimeOptions); +} + +export interface DeploymentOptions { + regions?: string[]; + timeoutSeconds?: number; + memory?: string; +} + +export class FunctionBuilder { + constructor(private options: DeploymentOptions) {} + + /** + * Configure the regions that the function is deployed to. + * @param regions One or more parameters indicating the region. + * For example: `functions.region('us-central1', 'us-east1')` + */ + region = (...regions: string[]) => { + this.options.regions = regions; + return this; + }; + + /** + * Configure runtime options for the function. + * @param runtimeOptions Object with 2 optional fields: + * 1. timeoutSeconds: timeout for the function in seconds. + * 2. memory: amount of memory to allocate to the function, possible values are: + * '128MB', '256MB', '512MB', '1GB', and '2GB'. + */ + runWith = (runtimeOptions: { + timeoutSeconds?: number; + memory?: '128MB' | '256MB' | '512MB' | '1GB' | '2GB'; + }) => { + if ( + runtimeOptions.memory && + !_.includes( + ['128MB', '256MB', '512MB', '1GB', '2GB'], + runtimeOptions.memory + ) + ) { + throw new Error( + "The only valid memory allocation values are: '128MB', '256MB', '512MB', '1GB', and '2GB'" + ); + } + this.options = _.assign(this.options, runtimeOptions); + return this; + }; + + get https() { + return { + /** + * Handle HTTP requests. + * @param handler A function that takes a request and response object, + * same signature as an Express app. + */ + onRequest: ( + handler: (req: express.Request, resp: express.Response) => void + ) => https._onRequestWithOpts(handler, this.options) as HttpsFunction, + /** + * Declares a callable method for clients to call using a Firebase SDK. + * @param handler A method that takes a data and context and returns a value. + */ + onCall: ( + handler: ( + data: any, + context: https.CallableContext + ) => any | Promise + ) => https._onCallWithOpts(handler, this.options) as HttpsFunction, + }; + } + + get database() { + return { + /** + * Selects a database instance that will trigger the function. + * If omitted, will pick the default database for your project. + * @param instance The Realtime Database instance to use. + */ + instance: (instance: string) => + database._instanceWithOpts(instance, this.options), + /** + * Select Firebase Realtime Database Reference to listen to. + * + * This method behaves very similarly to the method of the same name in the + * client and Admin Firebase SDKs. Any change to the Database that affects the + * data at or below the provided `path` will fire an event in Cloud Functions. + * + * There are three important differences between listening to a Realtime + * Database event in Cloud Functions and using the Realtime Database in the + * client and Admin SDKs: + * 1. Cloud Functions allows wildcards in the `path` name. Any `path` component + * in curly brackets (`{}`) is a wildcard that matches all strings. The value + * that matched a certain invocation of a Cloud Function is returned as part + * of the `context.params` object. For example, `ref("messages/{messageId}")` + * matches changes at `/messages/message1` or `/messages/message2`, resulting + * in `context.params.messageId` being set to `"message1"` or `"message2"`, + * respectively. + * 2. Cloud Functions do not fire an event for data that already existed before + * the Cloud Function was deployed. + * 3. Cloud Function events have access to more information, including information + * about the user who triggered the Cloud Function. + * @param ref Path of the database to listen to. + */ + ref: (path: string) => database._refWithOpts(path, this.options), + }; + } + + get firestore() { + return { + /** + * Select the Firestore document to listen to for events. + * @param path Full database path to listen to. This includes the name of + * the collection that the document is a part of. For example, if the + * collection is named "users" and the document is named "Ada", then the + * path is "/users/Ada". + */ + document: (path: string) => + firestore._documentWithOpts(path, this.options), + /** @internal */ + namespace: (namespace: string) => + firestore._namespaceWithOpts(namespace, this.options), + /** @internal */ + database: (database: string) => + firestore._databaseWithOpts(database, this.options), + }; + } + + get crashlytics() { + return { + /** + * Handle events related to Crashlytics issues. An issue in Crashlytics is an + * aggregation of crashes which have a shared root cause. + */ + issue: () => crashlytics._issueWithOpts(this.options), + }; + } + + get analytics() { + return { + /** + * Select analytics events to listen to for events. + * @param analyticsEventType Name of the analytics event type. + */ + event: (analyticsEventType: string) => + analytics._eventWithOpts(analyticsEventType, this.options), + }; + } + + get storage() { + return { + /** + * The optional bucket function allows you to choose which buckets' events to handle. + * This step can be bypassed by calling object() directly, which will use the default + * Cloud Storage for Firebase bucket. + * @param bucket Name of the Google Cloud Storage bucket to listen to. + */ + bucket: (bucket?: string) => + storage._bucketWithOpts(this.options, bucket), + + /** + * Handle events related to Cloud Storage objects. + */ + object: () => storage._objectWithOpts(this.options), + }; + } + + get pubsub() { + return { + /** Select Cloud Pub/Sub topic to listen to. + * @param topic Name of Pub/Sub topic, must belong to the same project as the function. + */ + topic: (topic: string) => pubsub._topicWithOpts(topic, this.options), + }; + } + + get auth() { + return { + /** + * Handle events related to Firebase authentication users. + */ + user: () => auth._userWithOpts(this.options), + }; + } +} diff --git a/src/index.ts b/src/index.ts index ccc0470f4..cbcdb7f4b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ // Providers: import * as analytics from './providers/analytics'; import * as auth from './providers/auth'; + import * as crashlytics from './providers/crashlytics'; import * as database from './providers/database'; import * as firestore from './providers/firestore'; @@ -30,6 +31,7 @@ import * as https from './providers/https'; import * as pubsub from './providers/pubsub'; import * as storage from './providers/storage'; import { firebaseConfig } from './config'; + export { analytics, auth, @@ -41,9 +43,10 @@ export { storage, }; -// Exported root types: +// // Exported root types: export * from './config'; export * from './cloud-functions'; +export * from './function-builder'; // TEMPORARY WORKAROUND (BUG 63586213): // Until the Cloud Functions builder can publish FIREBASE_CONFIG, automatically provide it on import based on what diff --git a/src/providers/analytics.ts b/src/providers/analytics.ts index c3836cec2..6802e61a1 100644 --- a/src/providers/analytics.ts +++ b/src/providers/analytics.ts @@ -20,13 +20,15 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import * as _ from 'lodash'; + import { makeCloudFunction, CloudFunction, Event, EventContext, } from '../cloud-functions'; -import * as _ from 'lodash'; +import { DeploymentOptions } from '../function-builder'; /** @internal */ export const provider = 'google.analytics'; @@ -34,15 +36,18 @@ export const provider = 'google.analytics'; export const service = 'app-measurement.com'; /** - * Registers a Cloud Function to handle analytics events. - * - * @param {string} analyticsEventType Name of the analytics event type to which - * this Cloud Function is scoped. - * - * @return {!functions.analytics.AnalyticsEventBuilder} Analytics event builder - * interface. + * Select analytics events to listen to for events. + * @param analyticsEventType Name of the analytics event type. */ export function event(analyticsEventType: string) { + return _eventWithOpts(analyticsEventType, {}); +} + +/** @internal */ +export function _eventWithOpts( + analyticsEventType: string, + opts: DeploymentOptions +) { return new AnalyticsEventBuilder(() => { if (!process.env.GCLOUD_PROJECT) { throw new Error('process.env.GCLOUD_PROJECT is not set.'); @@ -50,7 +55,7 @@ export function event(analyticsEventType: string) { return ( 'projects/' + process.env.GCLOUD_PROJECT + '/events/' + analyticsEventType ); - }); + }, opts); } /** @@ -60,7 +65,10 @@ export function event(analyticsEventType: string) { */ export class AnalyticsEventBuilder { /** @internal */ - constructor(private triggerResource: () => string) {} + constructor( + private triggerResource: () => string, + private opts: DeploymentOptions + ) {} /** * Event handler that fires every time a Firebase Analytics event occurs. @@ -89,6 +97,7 @@ export class AnalyticsEventBuilder { legacyEventType: `providers/google.firebase.analytics/eventTypes/event.log`, triggerResource: this.triggerResource, dataConstructor, + opts: this.opts, }); } } diff --git a/src/providers/auth.ts b/src/providers/auth.ts index b35a9491c..d271f2384 100644 --- a/src/providers/auth.ts +++ b/src/providers/auth.ts @@ -28,20 +28,28 @@ import { } from '../cloud-functions'; import * as firebase from 'firebase-admin'; import * as _ from 'lodash'; +import { DeploymentOptions } from '../function-builder'; /** @internal */ export const provider = 'google.firebase.auth'; /** @internal */ export const service = 'firebaseauth.googleapis.com'; -/** Handle events in the Firebase Auth user lifecycle. */ +/** + * Handle events related to Firebase authentication users. + */ export function user() { + return _userWithOpts({}); +} + +/** @internal */ +export function _userWithOpts(opts: DeploymentOptions) { return new UserBuilder(() => { if (!process.env.GCLOUD_PROJECT) { throw new Error('process.env.GCLOUD_PROJECT is not set.'); } return 'projects/' + process.env.GCLOUD_PROJECT; - }); + }, opts); } export class UserRecordMetadata implements firebase.auth.UserMetadata { @@ -63,7 +71,10 @@ export class UserBuilder { } /** @internal */ - constructor(private triggerResource: () => string) {} + constructor( + private triggerResource: () => string, + private opts?: DeploymentOptions + ) {} /** Respond to the creation of a Firebase Auth user. */ onCreate( @@ -94,6 +105,7 @@ export class UserBuilder { triggerResource: this.triggerResource, dataConstructor: UserBuilder.dataConstructor, legacyEventType: `providers/firebase.auth/eventTypes/${eventType}`, + opts: this.opts, }); } } diff --git a/src/providers/crashlytics.ts b/src/providers/crashlytics.ts index f4dda117b..2ad1db6b3 100644 --- a/src/providers/crashlytics.ts +++ b/src/providers/crashlytics.ts @@ -25,6 +25,7 @@ import { CloudFunction, EventContext, } from '../cloud-functions'; +import { DeploymentOptions } from '../function-builder'; /** @internal */ export const provider = 'google.firebase.crashlytics'; @@ -36,18 +37,26 @@ export const service = 'fabric.io'; * aggregation of crashes which have a shared root cause. */ export function issue() { + return _issueWithOpts({}); +} + +/** @internal */ +export function _issueWithOpts(opts: DeploymentOptions) { return new IssueBuilder(() => { if (!process.env.GCLOUD_PROJECT) { throw new Error('process.env.GCLOUD_PROJECT is not set.'); } return 'projects/' + process.env.GCLOUD_PROJECT; - }); + }, opts); } /** Builder used to create Cloud Functions for Crashlytics issue events. */ export class IssueBuilder { /** @internal */ - constructor(private triggerResource: () => string) {} + constructor( + private triggerResource: () => string, + private opts: DeploymentOptions + ) {} /** @internal */ onNewDetected(handler: any): Error { @@ -86,6 +95,7 @@ export class IssueBuilder { service, legacyEventType: `providers/firebase.crashlytics/eventTypes/${eventType}`, triggerResource: this.triggerResource, + opts: this.opts, }); } } diff --git a/src/providers/database.ts b/src/providers/database.ts index 5bb8ca3e8..ed0557294 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -32,6 +32,7 @@ import { import { normalizePath, applyChange, pathParts, joinPath } from '../utils'; import * as firebase from 'firebase-admin'; import { firebaseConfig } from '../config'; +import { DeploymentOptions } from '../function-builder'; /** @internal */ export const provider = 'google.firebase.database'; @@ -42,27 +43,16 @@ export const service = 'firebaseio.com'; const databaseURLRegex = new RegExp('https://([^.]+).firebaseio.com'); /** - * Pick the Realtime Database instance to use. If omitted, will pick the default database for your project. + * Selects a database instance that will trigger the function. + * If omitted, will pick the default database for your project. + * @param instance The Realtime Database instance to use. */ -export function instance(instance: string): InstanceBuilder { - return new InstanceBuilder(instance); -} - -export class InstanceBuilder { - /* @internal */ - constructor(private instance: string) {} - - ref(path: string): RefBuilder { - const normalized = normalizePath(path); - return new RefBuilder( - apps(), - () => `projects/_/instances/${this.instance}/refs/${normalized}` - ); - } +export function instance(instance: string) { + return _instanceWithOpts(instance, {}); } /** - * Handle events at a Firebase Realtime Database Reference. + * Select Firebase Realtime Database Reference to listen to. * * This method behaves very similarly to the method of the same name in the * client and Admin Firebase SDKs. Any change to the Database that affects the @@ -74,17 +64,47 @@ export class InstanceBuilder { * 1. Cloud Functions allows wildcards in the `path` name. Any `path` component * in curly brackets (`{}`) is a wildcard that matches all strings. The value * that matched a certain invocation of a Cloud Function is returned as part - * of the `event.params` object. For example, `ref("messages/{messageId}")` + * of the `context.params` object. For example, `ref("messages/{messageId}")` * matches changes at `/messages/message1` or `/messages/message2`, resulting - * in `event.params.messageId` being set to `"message1"` or `"message2"`, + * in `context.params.messageId` being set to `"message1"` or `"message2"`, * respectively. * 2. Cloud Functions do not fire an event for data that already existed before * the Cloud Function was deployed. - * 3. Cloud Function events have access to more information, including a - * snapshot of the previous event data and information about the user who - * triggered the Cloud Function. + * 3. Cloud Function events have access to more information, including information + * about the user who triggered the Cloud Function. + * @param ref Path of the database to listen to. */ -export function ref(path: string): RefBuilder { +export function ref(path: string) { + return _refWithOpts(path, {}); +} + +/** @internal */ +export function _instanceWithOpts( + instance: string, + opts: DeploymentOptions +): InstanceBuilder { + return new InstanceBuilder(instance, opts); +} + +export class InstanceBuilder { + /* @internal */ + constructor(private instance: string, private opts: DeploymentOptions) {} + + ref(path: string): RefBuilder { + const normalized = normalizePath(path); + return new RefBuilder( + apps(), + () => `projects/_/instances/${this.instance}/refs/${normalized}`, + this.opts + ); + } +} + +/** @internal */ +export function _refWithOpts( + path: string, + opts: DeploymentOptions +): RefBuilder { const resourceGetter = () => { const normalized = normalizePath(path); const databaseURL = firebaseConfig().databaseURL; @@ -106,13 +126,17 @@ export function ref(path: string): RefBuilder { return `projects/_/instances/${subdomain}/refs/${normalized}`; }; - return new RefBuilder(apps(), resourceGetter); + return new RefBuilder(apps(), resourceGetter, opts); } /** Builder used to create Cloud Functions for Firebase Realtime Database References. */ export class RefBuilder { /** @internal */ - constructor(private apps: apps.Apps, private triggerResource: () => string) {} + constructor( + private apps: apps.Apps, + private triggerResource: () => string, + private opts: DeploymentOptions + ) {} /** Respond to any write that affects a ref. */ onWrite( @@ -186,6 +210,7 @@ export class RefBuilder { dataConstructor: dataConstructor, before: event => this.apps.retain(), after: event => this.apps.release(), + opts: this.opts, }); } diff --git a/src/providers/firestore.ts b/src/providers/firestore.ts index 6de8dd970..20dee281f 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -32,49 +32,76 @@ import { EventContext, } from '../cloud-functions'; import { dateToTimestampProto } from '../encoder'; +import { DeploymentOptions } from '../function-builder'; /** @internal */ export const provider = 'google.firestore'; /** @internal */ export const service = 'firestore.googleapis.com'; -export type DocumentSnapshot = firebase.firestore.DocumentSnapshot; - /** @internal */ export const defaultDatabase = '(default)'; let firestoreInstance: any; +export type DocumentSnapshot = firebase.firestore.DocumentSnapshot; +/** + * Select the Firestore document to listen to for events. + * @param path Full database path to listen to. This includes the name of + * the collection that the document is a part of. For example, if the + * collection is named "users" and the document is named "Ada", then the + * path is "/users/Ada". + */ +export function document(path: string) { + return _documentWithOpts(path, {}); +} +/** @internal */ +// Multiple namespaces are not yet supported by Firestore. +export function namespace(namespace: string) { + return _namespaceWithOpts(namespace, {}); +} /** @internal */ // Multiple databases are not yet supported by Firestore. -export function database(database: string = defaultDatabase) { - return new DatabaseBuilder(database); +export function database(database: string) { + return _databaseWithOpts(database, {}); } /** @internal */ -// Multiple databases are not yet supported by Firestore. -export function namespace(namespace: string) { - return database().namespace(namespace); +export function _databaseWithOpts( + database: string = defaultDatabase, + opts: DeploymentOptions +) { + return new DatabaseBuilder(database, opts); } -export function document(path: string) { - return database().document(path); +/** @internal */ +export function _namespaceWithOpts(namespace: string, opts: DeploymentOptions) { + return _databaseWithOpts(defaultDatabase, opts).namespace(namespace); +} + +/** @internal */ +export function _documentWithOpts(path: string, opts: DeploymentOptions) { + return _databaseWithOpts(defaultDatabase, opts).document(path); } export class DatabaseBuilder { /** @internal */ - constructor(private database: string) {} + constructor(private database: string, private opts: DeploymentOptions) {} namespace(namespace: string) { - return new NamespaceBuilder(this.database, namespace); + return new NamespaceBuilder(this.database, this.opts, namespace); } document(path: string) { - return new NamespaceBuilder(this.database).document(path); + return new NamespaceBuilder(this.database, this.opts).document(path); } } export class NamespaceBuilder { /** @internal */ - constructor(private database: string, private namespace?: string) {} + constructor( + private database: string, + private opts: DeploymentOptions, + private namespace?: string + ) {} document(path: string) { return new DocumentBuilder(() => { @@ -92,7 +119,7 @@ export class NamespaceBuilder { this.namespace ? `documents@${this.namespace}` : 'documents', path ); - }); + }, this.opts); } } @@ -154,7 +181,10 @@ function changeConstructor(raw: Event) { export class DocumentBuilder { /** @internal */ - constructor(private triggerResource: () => string) { + constructor( + private triggerResource: () => string, + private opts: DeploymentOptions + ) { // TODO what validation do we want to do here? } @@ -215,6 +245,7 @@ export class DocumentBuilder { triggerResource: this.triggerResource, legacyEventType: `providers/cloud.firestore/eventTypes/${eventType}`, dataConstructor, + opts: this.opts, }); } } diff --git a/src/providers/https.ts b/src/providers/https.ts index f647d6f1f..c4399541e 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -20,22 +20,46 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { HttpsFunction } from '../cloud-functions'; import * as express from 'express'; import * as firebase from 'firebase-admin'; -import { apps } from '../apps'; import * as _ from 'lodash'; import * as cors from 'cors'; +import { apps } from '../apps'; +import { HttpsFunction, optsToTrigger } from '../cloud-functions'; +import { DeploymentOptions } from '../function-builder'; +/** + * Handle HTTP requests. + * @param handler A function that takes a request and response object, + * same signature as an Express app. + */ export function onRequest( handler: (req: express.Request, resp: express.Response) => void +) { + return _onRequestWithOpts(handler, {}); +} + +/** + * Declares a callable method for clients to call using a Firebase SDK. + * @param handler A method that takes a data and context and returns a value. + */ +export function onCall( + handler: (data: any, context: CallableContext) => any | Promise +) { + return _onCallWithOpts(handler, {}); +} + +/** @internal */ +export function _onRequestWithOpts( + handler: (req: express.Request, resp: express.Response) => void, + opts: DeploymentOptions ): HttpsFunction { // lets us add __trigger without altering handler: let cloudFunction: any = (req: express.Request, res: express.Response) => { handler(req, res); }; - cloudFunction.__trigger = { httpsTrigger: {} }; - + cloudFunction.__trigger = _.assign(optsToTrigger(opts), { httpsTrigger: {} }); + // TODO parse the opts return cloudFunction; } @@ -387,12 +411,10 @@ export function decode(data: any): any { const corsHandler = cors({ origin: true, methods: 'POST' }); -/** - * Declares a callable method for clients to call using a Firebase SDK. - * @param handler A method that takes a data and context and returns a value. - */ -export function onCall( - handler: (data: any, context: CallableContext) => any | Promise +/** @internal */ +export function _onCallWithOpts( + handler: (data: any, context: CallableContext) => any | Promise, + opts: DeploymentOptions ): HttpsFunction { const func = async (req: express.Request, res: express.Response) => { try { @@ -459,10 +481,10 @@ export function onCall( return corsHandler(req, res, () => func(req, res)); }; - corsFunc.__trigger = { + corsFunc.__trigger = _.assign(optsToTrigger(opts), { httpsTrigger: {}, labels: { 'deployment-callable': 'true' }, - }; + }); return corsFunc; } diff --git a/src/providers/pubsub.ts b/src/providers/pubsub.ts index 842d662e5..43dad1fc1 100644 --- a/src/providers/pubsub.ts +++ b/src/providers/pubsub.ts @@ -25,14 +25,25 @@ import { makeCloudFunction, EventContext, } from '../cloud-functions'; +import { DeploymentOptions } from '../function-builder'; /** @internal */ export const provider = 'google.pubsub'; /** @internal */ export const service = 'pubsub.googleapis.com'; -/** Handle events on a Cloud Pub/Sub topic. */ -export function topic(topic: string): TopicBuilder { +/** Select Cloud Pub/Sub topic to listen to. + * @param topic Name of Pub/Sub topic, must belong to the same project as the function. + */ +export function topic(topic: string) { + return _topicWithOpts(topic, {}); +} + +/** @internal */ +export function _topicWithOpts( + topic: string, + opts: DeploymentOptions +): TopicBuilder { if (topic.indexOf('/') !== -1) { throw new Error('Topic name may not have a /'); } @@ -42,13 +53,16 @@ export function topic(topic: string): TopicBuilder { throw new Error('process.env.GCLOUD_PROJECT is not set.'); } return `projects/${process.env.GCLOUD_PROJECT}/topics/${topic}`; - }); + }, opts); } /** Builder used to create Cloud Functions for Google Pub/Sub topics. */ export class TopicBuilder { /** @internal */ - constructor(private triggerResource: () => string) {} + constructor( + private triggerResource: () => string, + private opts: DeploymentOptions + ) {} /** Handle a Pub/Sub message that was published to a Cloud Pub/Sub topic */ onPublish( @@ -61,6 +75,7 @@ export class TopicBuilder { triggerResource: this.triggerResource, eventType: 'topic.publish', dataConstructor: raw => new Message(raw.data), + opts: this.opts, }); } } diff --git a/src/providers/storage.ts b/src/providers/storage.ts index d7bd4a995..eb7821b99 100644 --- a/src/providers/storage.ts +++ b/src/providers/storage.ts @@ -26,6 +26,7 @@ import { makeCloudFunction, } from '../cloud-functions'; import { firebaseConfig } from '../config'; +import { DeploymentOptions } from '../function-builder'; /** @internal */ export const provider = 'google.storage'; @@ -34,10 +35,26 @@ export const service = 'storage.googleapis.com'; /** * The optional bucket function allows you to choose which buckets' events to handle. - * This step can be bypassed by calling object() directly, which will use the bucket that - * the Firebase SDK for Cloud Storage uses. + * This step can be bypassed by calling object() directly, which will use the default + * Cloud Storage for Firebase bucket. + * @param bucket Name of the Google Cloud Storage bucket to listen to. */ -export function bucket(bucket?: string): BucketBuilder { +export function bucket(bucket?: string) { + return _bucketWithOpts({}, bucket); +} + +/** + * Handle events related to Cloud Storage objects. + */ +export function object() { + return _objectWithOpts({}); +} + +/** @internal */ +export function _bucketWithOpts( + opts: DeploymentOptions, + bucket?: string +): BucketBuilder { const resourceGetter = () => { bucket = bucket || firebaseConfig().storageBucket; if (!bucket) { @@ -51,26 +68,33 @@ export function bucket(bucket?: string): BucketBuilder { } return `projects/_/buckets/${bucket}`; }; - return new BucketBuilder(resourceGetter); + return new BucketBuilder(resourceGetter, opts); } -export function object(): ObjectBuilder { - return bucket().object(); +/** @internal */ +export function _objectWithOpts(opts: DeploymentOptions): ObjectBuilder { + return _bucketWithOpts(opts).object(); } export class BucketBuilder { /** @internal */ - constructor(private triggerResource: () => string) {} + constructor( + private triggerResource: () => string, + private opts: DeploymentOptions + ) {} /** Handle events for objects in this bucket. */ object() { - return new ObjectBuilder(this.triggerResource); + return new ObjectBuilder(this.triggerResource, this.opts); } } export class ObjectBuilder { /** @internal */ - constructor(private triggerResource: () => string) {} + constructor( + private triggerResource: () => string, + private opts: DeploymentOptions + ) {} /** @internal */ onChange(handler: any): Error { @@ -133,6 +157,7 @@ export class ObjectBuilder { service, eventType, triggerResource: this.triggerResource, + opts: this.opts, }); } } From 1f0464321888cbe70256a617279108a00c317d40 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Mon, 16 Jul 2018 18:00:40 -0700 Subject: [PATCH 041/705] Add .run method to HTTPS callable functions (#206) --- spec/providers/https.spec.ts | 16 ++++++++++++++++ src/cloud-functions.ts | 2 +- src/providers/https.ts | 6 ++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/spec/providers/https.spec.ts b/spec/providers/https.spec.ts index 7c6deb458..420953114 100644 --- a/spec/providers/https.spec.ts +++ b/spec/providers/https.spec.ts @@ -519,6 +519,22 @@ describe('callable.FunctionBuilder', () => { expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); }); + + it('has a .run method', () => { + const cf = https.onCall((data, context) => { + return { data, context }; + }); + + const data = 'data'; + const context = { + instanceIdToken: 'token', + auth: { + uid: 'abc', + token: 'token', + }, + }; + expect(cf.run(data, context)).to.deep.equal({ data, context }); + }); }); }); diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 23bbff189..9d8f38cd0 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -180,7 +180,7 @@ export interface TriggerAnnotated { /** A Runnable has a `run` method which directly invokes the user-defined function - useful for unit testing. */ export interface Runnable { - run: (data: T, context: EventContext) => PromiseLike | any; + run: (data: T, context: any) => PromiseLike | any; } /** diff --git a/src/providers/https.ts b/src/providers/https.ts index c4399541e..d8bc7517c 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -25,7 +25,7 @@ import * as firebase from 'firebase-admin'; import * as _ from 'lodash'; import * as cors from 'cors'; import { apps } from '../apps'; -import { HttpsFunction, optsToTrigger } from '../cloud-functions'; +import { HttpsFunction, optsToTrigger, Runnable } from '../cloud-functions'; import { DeploymentOptions } from '../function-builder'; /** @@ -415,7 +415,7 @@ const corsHandler = cors({ origin: true, methods: 'POST' }); export function _onCallWithOpts( handler: (data: any, context: CallableContext) => any | Promise, opts: DeploymentOptions -): HttpsFunction { +): HttpsFunction & Runnable { const func = async (req: express.Request, res: express.Response) => { try { if (!isValidRequest(req)) { @@ -486,5 +486,7 @@ export function _onCallWithOpts( labels: { 'deployment-callable': 'true' }, }); + corsFunc.run = handler; + return corsFunc; } From 4b8724a8c9d96196dc7cc25f624c79d3872dc66d Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Thu, 19 Jul 2018 11:28:09 -0700 Subject: [PATCH 042/705] Migrate to v5.13.0 firebase-admin & new Firestore timestamp format (#207) --- package.json | 4 ++-- spec/providers/firestore.spec.ts | 15 ++++++--------- src/providers/firestore.ts | 1 + src/providers/https.ts | 2 +- tsconfig.json | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 0ad4d32ce..25203f5ce 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@types/sinon": "^1.16.29", "chai": "^3.5.0", "chai-as-promised": "^5.2.0", - "firebase-admin": "~5.12.1", + "firebase-admin": "~5.13.0", "istanbul": "^0.4.2", "mocha": "^2.4.5", "mock-require": "^2.0.1", @@ -50,7 +50,7 @@ "typescript": "~2.8.3" }, "peerDependencies": { - "firebase-admin": "~5.12.1" + "firebase-admin": "~5.13.0" }, "dependencies": { "@types/cors": "^2.8.1", diff --git a/spec/providers/firestore.spec.ts b/spec/providers/firestore.spec.ts index 5a03c10d1..59eae8e38 100644 --- a/spec/providers/firestore.spec.ts +++ b/spec/providers/firestore.spec.ts @@ -500,21 +500,18 @@ describe('Firestore Functions', () => { }); it('should support #createTime', () => { - expect(Date.parse(snapshot.createTime)).to.equal( - Date.parse('2017-06-17T14:45:17.876479Z') - ); + expect(snapshot.createTime.seconds).to.be.a('number'); + expect(snapshot.createTime.nanoseconds).to.be.a('number'); }); it('should support #updateTime', () => { - expect(Date.parse(snapshot.updateTime)).to.equal( - Date.parse('2017-08-31T18:05:26.928527Z') - ); + expect(snapshot.updateTime.seconds).to.be.a('number'); + expect(snapshot.updateTime.nanoseconds).to.be.a('number'); }); it('should support #readTime', () => { - expect(Date.parse(snapshot.readTime)).to.equal( - Date.parse('2017-07-31T18:23:26.928527Z') - ); + expect(snapshot.readTime.seconds).to.be.a('number'); + expect(snapshot.readTime.nanoseconds).to.be.a('number'); }); }); diff --git a/src/providers/firestore.ts b/src/providers/firestore.ts index 20dee281f..1d295736a 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -145,6 +145,7 @@ function _getValueProto(data: any, resource: string, valueFieldName: string) { export function snapshotConstructor(event: Event): DocumentSnapshot { if (!firestoreInstance) { firestoreInstance = firebase.firestore(apps().admin); + firestoreInstance.settings({ timestampsInSnapshots: true }); } let valueProto = _getValueProto( event.data, diff --git a/src/providers/https.ts b/src/providers/https.ts index d8bc7517c..f6a5cddff 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -45,7 +45,7 @@ export function onRequest( */ export function onCall( handler: (data: any, context: CallableContext) => any | Promise -) { +): HttpsFunction & Runnable { return _onCallWithOpts(handler, {}); } diff --git a/tsconfig.json b/tsconfig.json index 573a354fe..04fc40239 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "lib": ["es6"], "module": "commonjs", - "noImplicitAny": true, + "noImplicitAny": false, "outDir": ".tmp", "sourceMap": true, "target": "es6", From 3fef83a86837f0e464fc64c0e61dc69f60813dc4 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Fri, 20 Jul 2018 14:01:15 -0700 Subject: [PATCH 043/705] Update integration test (#209) --- .gitignore | 1 + integration_test/functions/src/index.ts | 10 +++---- integration_test/functions/src/testing.ts | 28 +++++++++---------- integration_test/package.node6.json | 23 +++++++++++++++ .../package.json => package.node8.json} | 3 +- integration_test/run_tests.sh | 26 +++++++++++++---- 6 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 integration_test/package.node6.json rename integration_test/{functions/package.json => package.node8.json} (89%) diff --git a/.gitignore b/.gitignore index 543c4a073..d345a2f7a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ firebase-functions-*.tgz integration_test/.firebaserc integration_test/*.log integration_test/functions/firebase-functions.tgz +integration_test/functions/package.json lib node_modules npm-debug.log diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 64e3aca5a..7c4618cab 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -1,5 +1,4 @@ import * as functions from 'firebase-functions'; -import * as firebase from 'firebase'; import * as https from 'https'; import * as admin from 'firebase-admin'; import { Request, Response } from 'express'; @@ -16,8 +15,8 @@ const numTests = Object.keys(exports).length; // Assumption: every exported func import 'firebase-functions'; // temporary shim until process.env.FIREBASE_CONFIG available natively in GCF(BUG 63586213) const firebaseConfig = JSON.parse(process.env.FIREBASE_CONFIG); -firebase.initializeApp(firebaseConfig); admin.initializeApp(); +admin.firestore().settings({ timestampsInSnapshots: true }); // TODO(klimt): Get rid of this once the JS client SDK supports callable triggers. function callHttpsTrigger(name: string, data: any) { @@ -52,14 +51,15 @@ export const integrationTests: any = functions .https.onRequest((req: Request, resp: Response) => { let pubsub: any = require('@google-cloud/pubsub')(); - const testId = firebase + const testId = admin .database() .ref() .push().key; + console.log('testId is: ', testId); + return Promise.all([ // A database write to trigger the Firebase Realtime Database tests. - // The database write happens without admin privileges. - firebase + admin .database() .ref(`dbTests/${testId}/start`) .set({ '.sv': 'timestamp' }), diff --git a/integration_test/functions/src/testing.ts b/integration_test/functions/src/testing.ts index 50eaa7966..3a34fd8a1 100644 --- a/integration_test/functions/src/testing.ts +++ b/integration_test/functions/src/testing.ts @@ -19,7 +19,7 @@ export class TestSuite { return this; } - run(testId: string, data: T, context?: EventContext): Promise { + run(testId: string, data: T, context?: EventContext): Promise { let running: Array> = []; for (let testName in this.tests) { if (!this.tests.hasOwnProperty(testName)) { @@ -41,20 +41,18 @@ export class TestSuite { ); running.push(run); } - return Promise.all(running) - .then(results => { - let sum = 0; - results.forEach(val => (sum = sum + val.passed)); - const summary = `passed ${sum} of ${running.length}`; - const passed = sum === running.length; - console.log(summary); - const result = { passed, summary, tests: results }; - return firebase - .database() - .ref(`testRuns/${testId}/${this.name}`) - .set(result); - }) - .then(() => null); + return Promise.all(running).then(results => { + let sum = 0; + results.forEach(val => (sum = sum + val.passed)); + const summary = `passed ${sum} of ${running.length}`; + const passed = sum === running.length; + console.log(summary); + const result = { passed, summary, tests: results }; + return firebase + .database() + .ref(`testRuns/${testId}/${this.name}`) + .set(result); + }); } } diff --git a/integration_test/package.node6.json b/integration_test/package.node6.json new file mode 100644 index 000000000..265627814 --- /dev/null +++ b/integration_test/package.node6.json @@ -0,0 +1,23 @@ +{ + "name": "functions", + "description": "Integration test for the Firebase SDK for Google Cloud Functions", + "scripts": { + "build": "./node_modules/.bin/tsc" + }, + "dependencies": { + "@google-cloud/pubsub": "~0.19.0", + "@types/google-cloud__pubsub": "^0.18.0", + "@types/lodash": "~4.14.41", + "firebase-admin": "~5.13.0", + "firebase-functions": "./firebase-functions.tgz", + "lodash": "~4.17.2" + }, + "main": "lib/index.js", + "devDependencies": { + "typescript": "~2.8.3" + }, + "engines": { + "node": "6" + }, + "private": true +} diff --git a/integration_test/functions/package.json b/integration_test/package.node8.json similarity index 89% rename from integration_test/functions/package.json rename to integration_test/package.node8.json index 1250f39a5..f89e0a8ba 100644 --- a/integration_test/functions/package.json +++ b/integration_test/package.node8.json @@ -8,8 +8,7 @@ "@google-cloud/pubsub": "~0.19.0", "@types/google-cloud__pubsub": "^0.18.0", "@types/lodash": "~4.14.41", - "firebase": "~5.2.0", - "firebase-admin": "~5.12.1", + "firebase-admin": "~5.13.0", "firebase-functions": "./firebase-functions.tgz", "lodash": "~4.17.2" }, diff --git a/integration_test/run_tests.sh b/integration_test/run_tests.sh index d37bb49b8..e63736a91 100755 --- a/integration_test/run_tests.sh +++ b/integration_test/run_tests.sh @@ -29,23 +29,33 @@ function build_sdk { mv firebase-functions-*.tgz integration_test/functions/firebase-functions.tgz } +function pick_node6 { + cd $DIR + cp package.node6.json functions/package.json +} + +function pick_node8 { + cd $DIR + cp package.node8.json functions/package.json +} + function install_deps { announce "Installing dependencies..." cd $DIR/functions + rm -rf node_modules/firebase-functions npm install } function delete_all_functions { - announce "Deploying empty index.js to project..." + announce "Deleting all functions in project..." cd $DIR - ./functions/node_modules/.bin/tsc -p functions/ # Make sure the functions/lib directory actually exists. - echo "" > functions/lib/index.js - firebase deploy --project=$PROJECT_ID --only functions + # Try to delete, if there are errors it is because the project is already empty, + # in that case do nothing. + firebase functions:delete callableTests createUserTests databaseTests deleteUserTests firestoreTests integrationTests pubsubTests --project=$PROJECT_ID -f || : announce "Project emptied." } function deploy { - announce "Deploying functions..." cd $DIR ./functions/node_modules/.bin/tsc -p functions/ # Deploy functions, and security rules for database and Firestore @@ -72,16 +82,20 @@ function cleanup { announce "Performing cleanup..." delete_all_functions rm $DIR/functions/firebase-functions.tgz + rm $DIR/functions/package.json rm -f $DIR/functions/firebase-debug.log rm -rf $DIR/functions/node_modules/firebase-functions } build_sdk +pick_node8 install_deps delete_all_functions +announce "Deploying functions to Node 8 runtime ..." deploy run_tests -announce "Re-deploying the same functions to make sure updates work..." +pick_node6 +announce "Re-deploying the same functions to Node 6 runtime ..." deploy run_tests cleanup From c7a09d5b5d7c34648d849c89df50f85d1cf0b5d4 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Mon, 23 Jul 2018 13:58:47 -0700 Subject: [PATCH 044/705] Allow one region per function (#211) --- spec/function-builder.spec.ts | 22 +++++++++++----------- src/function-builder.ts | 16 ++++++++-------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/spec/function-builder.spec.ts b/spec/function-builder.spec.ts index a1733fb1d..972f865dc 100644 --- a/spec/function-builder.spec.ts +++ b/spec/function-builder.spec.ts @@ -42,17 +42,17 @@ describe('FunctionBuilder', () => { expect(fn.__trigger.regions).to.deep.equal(['my-region']); }); - it('should allow multiple regions to be set', () => { - let fn = functions - .region('my-region', 'my-other-region') - .auth.user() - .onCreate(user => user); - - expect(fn.__trigger.regions).to.deep.equal([ - 'my-region', - 'my-other-region', - ]); - }); + // it('should allow multiple regions to be set', () => { + // let fn = functions + // .region('my-region', 'my-other-region') + // .auth.user() + // .onCreate(user => user); + + // expect(fn.__trigger.regions).to.deep.equal([ + // 'my-region', + // 'my-other-region', + // ]); + // }); it('should allow runtime options to be set', () => { let fn = functions diff --git a/src/function-builder.ts b/src/function-builder.ts index bee575a49..099b71e3b 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -35,11 +35,11 @@ import { HttpsFunction } from './cloud-functions'; /** * Configure the regions that the function is deployed to. - * @param regions One or more parameters indicating the regions. - * For example: `functions.region('us-central1', 'us-east1')` + * @param region Region string. + * For example: `functions.region('us-east1')` */ -export function region(...regions: string[]) { - return new FunctionBuilder({ regions }); +export function region(region: string) { + return new FunctionBuilder({ regions: [region] }); } /** @@ -78,11 +78,11 @@ export class FunctionBuilder { /** * Configure the regions that the function is deployed to. - * @param regions One or more parameters indicating the region. - * For example: `functions.region('us-central1', 'us-east1')` + * @param region Region string. + * For example: `functions.region('us-east1')` */ - region = (...regions: string[]) => { - this.options.regions = regions; + region = (region: string) => { + this.options.regions = [region]; return this; }; From 28dd25c42f63b2baed82f7449d0729e859efbbf4 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Mon, 23 Jul 2018 16:25:13 -0700 Subject: [PATCH 045/705] Changelog for v2.0.0 (#212) --- changelog.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog.txt b/changelog.txt index e69de29bb..adc681988 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1,4 @@ +important - [breaking change] For Firestore-triggered functions, `snapshot.createTime`, `snapshot.updateTime`, `snapshot.readTime`, and any timestamp values in `snapshot.data()` are now [Firestore Timestamp](https://cloud.google.com/nodejs/docs/reference/firestore/0.15.x/Timestamp#properties) objects. +feature - Support Node.js 8 runtime. To deploy your functions to Node.js 8, add `"engines": {"node": "8"}` to `functions/package.json`. You will need `firebase-tools` >=v4.0.0. +feature - Support selection of regions for your functions through the `functions.region` method. Learn more in the [Firebase Documentation](https://firebase.google.com/docs/functions/locations). You will need `firebase-tools` >=v4.0.0. +feature - Support configuration of timeout and memory allocation through the `functions.runWith` method. Learn more in the [Firebase Documentation](https://firebase.google.com/docs/functions/manage-functions#set_timeout_and_memory_allocation). You will need `firebase-tools` >=v4.0.0. From b2ec24d8c2941e9c0c5fe7c3b36ea2c9ca2535b9 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Tue, 24 Jul 2018 16:08:18 +0000 Subject: [PATCH 046/705] [firebase-release] Updated SDK for Cloud Functions to 2.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 25203f5ce..5ed070083 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "1.0.4", + "version": "2.0.0", "description": "Firebase SDK for Cloud Functions", "main": "lib/index.js", "scripts": { From f7b7b6f153176decc9eac1aa986daa9fa5c549e9 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Tue, 24 Jul 2018 16:08:32 +0000 Subject: [PATCH 047/705] [firebase-release] Removed change log and reset repo after 2.0.0 release --- changelog.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/changelog.txt b/changelog.txt index adc681988..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +0,0 @@ -important - [breaking change] For Firestore-triggered functions, `snapshot.createTime`, `snapshot.updateTime`, `snapshot.readTime`, and any timestamp values in `snapshot.data()` are now [Firestore Timestamp](https://cloud.google.com/nodejs/docs/reference/firestore/0.15.x/Timestamp#properties) objects. -feature - Support Node.js 8 runtime. To deploy your functions to Node.js 8, add `"engines": {"node": "8"}` to `functions/package.json`. You will need `firebase-tools` >=v4.0.0. -feature - Support selection of regions for your functions through the `functions.region` method. Learn more in the [Firebase Documentation](https://firebase.google.com/docs/functions/locations). You will need `firebase-tools` >=v4.0.0. -feature - Support configuration of timeout and memory allocation through the `functions.runWith` method. Learn more in the [Firebase Documentation](https://firebase.google.com/docs/functions/manage-functions#set_timeout_and_memory_allocation). You will need `firebase-tools` >=v4.0.0. From 69f1a49950c0e2bd57ecf251d5d60336db5ef678 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Wed, 25 Jul 2018 15:45:31 -0700 Subject: [PATCH 048/705] Fix error where Node 6 functions were timing out (#286) --- spec/cloud-functions.spec.ts | 2 ++ src/cloud-functions.ts | 65 +++++++++++++++++++----------------- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/spec/cloud-functions.spec.ts b/spec/cloud-functions.spec.ts index cde9db5a9..8bb213af5 100644 --- a/spec/cloud-functions.spec.ts +++ b/spec/cloud-functions.spec.ts @@ -130,7 +130,9 @@ describe('makeCloudFunction', () => { return { data, context }; }, }); + process.env.X_GOOGLE_NEW_FUNCTION_SIGNATURE = 'true'; let cf = makeCloudFunction(args); + delete process.env.X_GOOGLE_NEW_FUNCTION_SIGNATURE; let testContext = { eventId: '00000', timestamp: '2016-11-04T21:29:03.496Z', diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 9d8f38cd0..50c717446 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -231,23 +231,9 @@ export function makeCloudFunction({ legacyEventType, opts = {}, }: MakeCloudFunctionArgs): CloudFunction { - let cloudFunction: any = async (eventOrData: any, context?: any) => { - let data: any; - if (isContext(context)) { - // In Node 8 runtime, function called with 2 params: data & context - data = eventOrData; - } else { - // In Node 6 runtime, function called with single event param - data = _.get(eventOrData, 'data'); - if (isEvent(eventOrData)) { - // new eventflow v1beta2 format - context = _.cloneDeep(eventOrData.context); - } else { - // eventflow v1beta1 format - context = _.omit(eventOrData, 'data'); - } - } + let cloudFunction; + let cloudFunctionNewSignature: any = (data: any, context: any) => { if (legacyEventType && context.eventType === legacyEventType) { // v1beta1 event flow has different format for context, transform them to new format. context = { @@ -276,19 +262,42 @@ export function makeCloudFunction({ } context.params = context.params || _makeParams(context, triggerResource); - try { - before(event); + before(event); - let dataOrChange = dataConstructor(event); - let promise = handler(dataOrChange, context); - if (typeof promise === 'undefined') { - console.warn('Function returned undefined, expected Promise or value'); - } - return await promise; - } finally { - after(event); + let dataOrChange = dataConstructor(event); + let promise = handler(dataOrChange, context); + if (typeof promise === 'undefined') { + console.warn('Function returned undefined, expected Promise or value'); } + return Promise.resolve(promise) + .then(result => { + after(event); + return result; + }) + .catch(err => { + after(event); + return Promise.reject(err); + }); }; + + if (process.env.X_GOOGLE_NEW_FUNCTION_SIGNATURE === 'true') { + cloudFunction = cloudFunctionNewSignature; + } else { + cloudFunction = (raw: Event | LegacyEvent) => { + let context; + // In Node 6 runtime, function called with single event param + let data = _.get(raw, 'data'); + if (isEvent(raw)) { + // new eventflow v1beta2 format + context = _.cloneDeep(raw.context); + } else { + // eventflow v1beta1 format + context = _.omit(raw, 'data'); + } + return cloudFunctionNewSignature(data, context); + }; + } + Object.defineProperty(cloudFunction, '__trigger', { get: () => { let trigger: any = _.assign(optsToTrigger(opts), { @@ -311,10 +320,6 @@ function isEvent(event: Event | LegacyEvent): event is Event { return _.has(event, 'context'); } -function isContext(context: EventContext | any): context is EventContext { - return _.has(context, 'eventId'); -} - function _makeParams( context: EventContext, triggerResourceGetter: () => string From 75b0f6cc389b422b3b6a6ae30f27f46432769ac0 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Wed, 25 Jul 2018 15:45:45 -0700 Subject: [PATCH 049/705] Changelog for v1.0.1 (#287) --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index e69de29bb..7ccbc5083 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +fixed - Fix error where Node 6 functions experienced timeout errors. From 949eefd8ed97c4d0cb62f0f5dd8e45edf3c06436 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Wed, 25 Jul 2018 22:49:37 +0000 Subject: [PATCH 050/705] [firebase-release] Updated SDK for Cloud Functions to 2.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ed070083..f82dedf4b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "2.0.0", + "version": "2.0.1", "description": "Firebase SDK for Cloud Functions", "main": "lib/index.js", "scripts": { From b598d9afd4fe07797bc1b484e209b9a9f545d119 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Wed, 25 Jul 2018 22:49:47 +0000 Subject: [PATCH 051/705] [firebase-release] Removed change log and reset repo after 2.0.1 release --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 7ccbc5083..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +0,0 @@ -fixed - Fix error where Node 6 functions experienced timeout errors. From 944deecfc50ec3102612ce322329158c65af4ac5 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Thu, 26 Jul 2018 14:24:54 -0700 Subject: [PATCH 052/705] Fix bug where timestamp in change objects are still date objects (#290) --- spec/providers/firestore.spec.ts | 10 ++++++++-- src/providers/firestore.ts | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/spec/providers/firestore.spec.ts b/spec/providers/firestore.spec.ts index 59eae8e38..2c6d1a02d 100644 --- a/spec/providers/firestore.spec.ts +++ b/spec/providers/firestore.spec.ts @@ -20,6 +20,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import * as admin from 'firebase-admin'; + import * as firestore from '../../src/providers/firestore'; import * as _ from 'lodash'; import { expect } from 'chai'; @@ -428,7 +430,9 @@ describe('Firestore Functions', () => { }) ); expect(snapshot.data()).to.deep.equal({ - timestampVal: new Date('2017-06-13T00:58:40.349Z'), + timestampVal: admin.firestore.Timestamp.fromDate( + new Date('2017-06-13T00:58:40.349Z') + ), }); }); @@ -444,7 +448,9 @@ describe('Firestore Functions', () => { }) ); expect(snapshot.data()).to.deep.equal({ - timestampVal: new Date('2017-06-13T00:58:40Z'), + timestampVal: admin.firestore.Timestamp.fromDate( + new Date('2017-06-13T00:58:40Z') + ), }); }); diff --git a/src/providers/firestore.ts b/src/providers/firestore.ts index 1d295736a..7a2931618 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -161,6 +161,7 @@ export function snapshotConstructor(event: Event): DocumentSnapshot { export function beforeSnapshotConstructor(event: Event): DocumentSnapshot { if (!firestoreInstance) { firestoreInstance = firebase.firestore(apps().admin); + firestoreInstance.settings({ timestampsInSnapshots: true }); } let oldValueProto = _getValueProto( event.data, From e4ec4104c9d1931f56014324308623a2ae351eeb Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Thu, 26 Jul 2018 14:42:16 -0700 Subject: [PATCH 053/705] Changelog for v2.0.2 (#291) --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index e69de29bb..150151e2f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +fixed - Fixed issue in Firestore-triggered functions where timestamp values in Change objects for onUpdate and onWrite functions are Date objects instead of Timestamp objects. From 527889a1252861d383657888cb429cc27c3bded3 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Thu, 26 Jul 2018 21:43:56 +0000 Subject: [PATCH 054/705] [firebase-release] Updated SDK for Cloud Functions to 2.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f82dedf4b..29780edd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "2.0.1", + "version": "2.0.2", "description": "Firebase SDK for Cloud Functions", "main": "lib/index.js", "scripts": { From d5241ac7a51a63c55b4bcc6bd4d19ae071ae5259 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Thu, 26 Jul 2018 21:44:05 +0000 Subject: [PATCH 055/705] [firebase-release] Removed change log and reset repo after 2.0.2 release --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 150151e2f..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +0,0 @@ -fixed - Fixed issue in Firestore-triggered functions where timestamp values in Change objects for onUpdate and onWrite functions are Date objects instead of Timestamp objects. From 8b4df8d5c8789466173cff076291329d327daa6a Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Thu, 9 Aug 2018 13:35:26 -0700 Subject: [PATCH 056/705] Update firebase-admin dependency to v6 (#213) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 29780edd0..8a848d95f 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@types/sinon": "^1.16.29", "chai": "^3.5.0", "chai-as-promised": "^5.2.0", - "firebase-admin": "~5.13.0", + "firebase-admin": "~6.0.0", "istanbul": "^0.4.2", "mocha": "^2.4.5", "mock-require": "^2.0.1", @@ -50,7 +50,7 @@ "typescript": "~2.8.3" }, "peerDependencies": { - "firebase-admin": "~5.13.0" + "firebase-admin": "~6.0.0" }, "dependencies": { "@types/cors": "^2.8.1", From 612eb310e0c7368d0ee6d3d5fda64788be8615ca Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Thu, 9 Aug 2018 13:35:42 -0700 Subject: [PATCH 057/705] Changelog for v2.03 (#214) --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index e69de29bb..cbf48579e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +changed - Updated firebase-admin peer dependency to v6.0.0. From b7e496b0ea584af4ddfa4064857e34ef51e1d10b Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Thu, 9 Aug 2018 20:38:35 +0000 Subject: [PATCH 058/705] [firebase-release] Updated SDK for Cloud Functions to 2.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8a848d95f..dfc4ab1da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "2.0.2", + "version": "2.0.3", "description": "Firebase SDK for Cloud Functions", "main": "lib/index.js", "scripts": { From 960033f6563ef3f1b4fa345a1e14e37bd6607056 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Thu, 9 Aug 2018 20:38:46 +0000 Subject: [PATCH 059/705] [firebase-release] Removed change log and reset repo after 2.0.3 release --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index cbf48579e..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +0,0 @@ -changed - Updated firebase-admin peer dependency to v6.0.0. From 4e430281d349af5f26ea3c30bc4d828f6f03fd23 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Thu, 9 Aug 2018 16:48:22 -0700 Subject: [PATCH 060/705] Fix bug where auth info for database was getting lost (#304) --- integration_test/functions/src/database-tests.ts | 4 ++++ src/cloud-functions.ts | 12 ++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/integration_test/functions/src/database-tests.ts b/integration_test/functions/src/database-tests.ts index 183d3d785..bd3556b16 100644 --- a/integration_test/functions/src/database-tests.ts +++ b/integration_test/functions/src/database-tests.ts @@ -72,5 +72,9 @@ export const databaseTests: any = functions.database expectEq((context as any).action, undefined) ) + .it('should have admin authType', (change, context) => { + expectEq(context.authType, 'ADMIN'); + }) + .run(ctx.params[testIdFieldName], ch, ctx); }); diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 50c717446..426c54bb8 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -236,14 +236,10 @@ export function makeCloudFunction({ let cloudFunctionNewSignature: any = (data: any, context: any) => { if (legacyEventType && context.eventType === legacyEventType) { // v1beta1 event flow has different format for context, transform them to new format. - context = { - eventId: context.eventId, - timestamp: context.timestamp, - eventType: provider + '.' + eventType, - resource: { - service: service, - name: context.resource, - }, + context.eventType = provider + '.' + eventType; + context.resource = { + service: service, + name: context.resource, }; } From 431076a595549565f1e79f31ed87808f8a9a9536 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Thu, 9 Aug 2018 16:48:51 -0700 Subject: [PATCH 061/705] Changelog for v2.0.4 (#305) --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index e69de29bb..d360960b9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +fixed - Fix bug in >v2.0.1 where `context.authType` is always `UNAUTHENTICATED` from a realtime database-triggered function. From 518a8c726295e739c63a9cd16c57af9747aa9047 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Thu, 9 Aug 2018 23:51:41 +0000 Subject: [PATCH 062/705] [firebase-release] Updated SDK for Cloud Functions to 2.0.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dfc4ab1da..835848a71 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "2.0.3", + "version": "2.0.4", "description": "Firebase SDK for Cloud Functions", "main": "lib/index.js", "scripts": { From 2f5df0d935bb319823ef59c5b90176d610ce3516 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Thu, 9 Aug 2018 23:51:52 +0000 Subject: [PATCH 063/705] [firebase-release] Removed change log and reset repo after 2.0.4 release --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index d360960b9..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +0,0 @@ -fixed - Fix bug in >v2.0.1 where `context.authType` is always `UNAUTHENTICATED` from a realtime database-triggered function. From 273f1fda41282effc8aa7bf24f6ad0b2069dd169 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Tue, 14 Aug 2018 13:14:44 -0700 Subject: [PATCH 064/705] Fix syntax error in integration test (#306) --- integration_test/functions/src/database-tests.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration_test/functions/src/database-tests.ts b/integration_test/functions/src/database-tests.ts index bd3556b16..e31dacf79 100644 --- a/integration_test/functions/src/database-tests.ts +++ b/integration_test/functions/src/database-tests.ts @@ -72,9 +72,9 @@ export const databaseTests: any = functions.database expectEq((context as any).action, undefined) ) - .it('should have admin authType', (change, context) => { - expectEq(context.authType, 'ADMIN'); - }) + .it('should have admin authType', (change, context) => + expectEq(context.authType, 'ADMIN') + ) .run(ctx.params[testIdFieldName], ch, ctx); }); From 9a1743f146bc521af71ffc4d3032dcd292cf3e59 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Tue, 14 Aug 2018 14:05:40 -0700 Subject: [PATCH 065/705] Changelog for v2.0.5 (#309) --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index e69de29bb..d81bbeb6e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +fixed - Fixed syntax error in integration test. From e12e39adf9b35fe14458810ef0bc1de53cd323eb Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Tue, 14 Aug 2018 21:07:25 +0000 Subject: [PATCH 066/705] [firebase-release] Updated SDK for Cloud Functions to 2.0.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 835848a71..1fcca59e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "2.0.4", + "version": "2.0.5", "description": "Firebase SDK for Cloud Functions", "main": "lib/index.js", "scripts": { From 587127ad7855f4130e8b1dcb561c8eb8d834bfd0 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Tue, 14 Aug 2018 21:07:35 +0000 Subject: [PATCH 067/705] [firebase-release] Removed change log and reset repo after 2.0.5 release --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index d81bbeb6e..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +0,0 @@ -fixed - Fixed syntax error in integration test. From c11941b4b297d66c0f252cee75fe57049abe705b Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Wed, 19 Sep 2018 17:22:50 -0700 Subject: [PATCH 068/705] Fix failing unit test due to new Firestore SDK (#320) --- spec/providers/firestore.spec.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spec/providers/firestore.spec.ts b/spec/providers/firestore.spec.ts index 2c6d1a02d..bd823210d 100644 --- a/spec/providers/firestore.spec.ts +++ b/spec/providers/firestore.spec.ts @@ -494,10 +494,6 @@ describe('Firestore Functions', () => { }); it('should support #ref', () => { - expect(Object.keys(snapshot.ref)).to.deep.equal([ - '_firestore', - '_referencePath', - ]); expect(snapshot.ref.path).to.equal('collection/123'); }); From 42ec44d8ade8004c5a3a9835afd11deb79a1a297 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Wed, 19 Sep 2018 17:23:11 -0700 Subject: [PATCH 069/705] Fix small typo (#322) --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index cbcdb7f4b..1998ce642 100644 --- a/src/index.ts +++ b/src/index.ts @@ -57,7 +57,7 @@ if (!process.env.FIREBASE_CONFIG) { process.env.FIREBASE_CONFIG = JSON.stringify(cfg); } else if (process.env.GCLOUD_PROJECT) { console.warn( - 'Warning, estimating Firebase Config based on GCLOUD_PROJECT. Intializing firebase-admin may fail' + 'Warning, estimating Firebase Config based on GCLOUD_PROJECT. Initializing firebase-admin may fail' ); process.env.FIREBASE_CONFIG = JSON.stringify({ databaseURL: `https://${process.env.GCLOUD_PROJECT}.firebaseio.com`, From 5e5e160c8f02b866269f56e78dee27cc31a38f36 Mon Sep 17 00:00:00 2001 From: Maciej Kisiel Date: Wed, 26 Sep 2018 20:35:58 +0200 Subject: [PATCH 070/705] Update firebase-admin in integration tests to v6.0.0 (#325) --- integration_test/package.node6.json | 2 +- integration_test/package.node8.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration_test/package.node6.json b/integration_test/package.node6.json index 265627814..efc6380a8 100644 --- a/integration_test/package.node6.json +++ b/integration_test/package.node6.json @@ -8,7 +8,7 @@ "@google-cloud/pubsub": "~0.19.0", "@types/google-cloud__pubsub": "^0.18.0", "@types/lodash": "~4.14.41", - "firebase-admin": "~5.13.0", + "firebase-admin": "~6.0.0", "firebase-functions": "./firebase-functions.tgz", "lodash": "~4.17.2" }, diff --git a/integration_test/package.node8.json b/integration_test/package.node8.json index f89e0a8ba..0de718c2a 100644 --- a/integration_test/package.node8.json +++ b/integration_test/package.node8.json @@ -8,7 +8,7 @@ "@google-cloud/pubsub": "~0.19.0", "@types/google-cloud__pubsub": "^0.18.0", "@types/lodash": "~4.14.41", - "firebase-admin": "~5.13.0", + "firebase-admin": "~6.0.0", "firebase-functions": "./firebase-functions.tgz", "lodash": "~4.17.2" }, From 17beb774163130220f2463bc4ae1d83878b45711 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Tue, 9 Oct 2018 15:12:05 -0700 Subject: [PATCH 071/705] Update mocha dependency (#298) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1fcca59e9..f044e2f40 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "devDependencies": { "@types/chai": "^3.4.32", "@types/chai-as-promised": "0.0.28", - "@types/mocha": "^2.2.31", + "@types/mocha": "^5.2.5", "@types/mock-require": "^1.3.3", "@types/nock": "^0.54.32", "@types/node": "^6.0.38", @@ -42,7 +42,7 @@ "chai-as-promised": "^5.2.0", "firebase-admin": "~6.0.0", "istanbul": "^0.4.2", - "mocha": "^2.4.5", + "mocha": "^5.2.0", "mock-require": "^2.0.1", "nock": "^9.0.0", "prettier": "^1.13.7", From 968be74a1f85229eaf840687c9c0dfa36716b0c1 Mon Sep 17 00:00:00 2001 From: Lucas Png Date: Wed, 10 Oct 2018 13:45:59 -0700 Subject: [PATCH 072/705] Firebase Remote Config Functions SDK Integration (#215) Adds a Remote Config event handler `firebase.remoteconfig.onUpdate()` that will be triggered whenever a Remote Config project is updated (i.e., when a publish or rollback occurs). The event handler takes in a `TemplateVersion` object as an argument, which contains metadata about the last update that affected a Remote Config project. The PR also contains corresponding unit test and integration tests. The integration test makes use of the Remote Config REST API: https://firebase.google.com/docs/remote-config/use-config-rest. --- integration_test/functions/src/index.ts | 21 +++ .../functions/src/remoteConfig-tests.ts | 29 ++++ integration_test/run_tests.sh | 2 +- spec/index.spec.ts | 1 + spec/providers/remoteConfig.spec.ts | 128 ++++++++++++++++++ src/function-builder.ts | 23 +++- src/index.ts | 2 + src/providers/remoteConfig.ts | 107 +++++++++++++++ 8 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 integration_test/functions/src/remoteConfig-tests.ts create mode 100644 spec/providers/remoteConfig.spec.ts create mode 100644 src/providers/remoteConfig.ts diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 7c4618cab..c135b371c 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -11,6 +11,7 @@ export * from './database-tests'; export * from './auth-tests'; export * from './firestore-tests'; export * from './https-tests'; +export * from './remoteConfig-tests'; const numTests = Object.keys(exports).length; // Assumption: every exported function is its own test. import 'firebase-functions'; // temporary shim until process.env.FIREBASE_CONFIG available natively in GCF(BUG 63586213) @@ -86,6 +87,26 @@ export const integrationTests: any = functions .collection('tests') .doc(testId) .set({ test: testId }), + // A Remote Config update to trigger the Remote Config tests. + admin.credential + .applicationDefault() + .getAccessToken() + .then((accessToken) => { + const options = { + hostname: 'firebaseremoteconfig.googleapis.com', + path: `/v1/projects/${firebaseConfig.projectId}/remoteConfig`, + method: 'PUT', + headers: { + Authorization: 'Bearer ' + accessToken.access_token, + 'Content-Type': 'application/json; UTF-8', + 'Accept-Encoding': 'gzip', + 'If-Match': '*', + }, + }; + const request = https.request(options, resp => {}); + request.write(JSON.stringify({ version: { description: testId } })); + request.end(); + }), // Invoke a callable HTTPS trigger. callHttpsTrigger('callableTests', { foo: 'bar', testId }), ]) diff --git a/integration_test/functions/src/remoteConfig-tests.ts b/integration_test/functions/src/remoteConfig-tests.ts new file mode 100644 index 000000000..2b824fbbd --- /dev/null +++ b/integration_test/functions/src/remoteConfig-tests.ts @@ -0,0 +1,29 @@ +import * as functions from 'firebase-functions'; +import { TestSuite, expectEq } from './testing'; +import TemplateVersion = functions.remoteConfig.TemplateVersion; + +export const remoteConfigTests: any = functions.remoteConfig.onUpdate( + (v, c) => { + return new TestSuite('remoteConfig onUpdate') + .it('should have a project as resource', (version, context) => + expectEq( + context.resource.name, + `projects/${process.env.GCLOUD_PROJECT}` + ) + ) + + .it('should have the correct eventType', (version, context) => + expectEq(context.eventType, 'google.firebase.remoteconfig.update') + ) + + .it('should have an eventId', (version, context) => context.eventId) + + .it('should have a timestamp', (version, context) => context.timestamp) + + .it('should not have auth', (version, context) => + expectEq((context as any).auth, undefined) + ) + + .run(v.description, v, c); + } +); diff --git a/integration_test/run_tests.sh b/integration_test/run_tests.sh index e63736a91..05abd2bd6 100755 --- a/integration_test/run_tests.sh +++ b/integration_test/run_tests.sh @@ -51,7 +51,7 @@ function delete_all_functions { cd $DIR # Try to delete, if there are errors it is because the project is already empty, # in that case do nothing. - firebase functions:delete callableTests createUserTests databaseTests deleteUserTests firestoreTests integrationTests pubsubTests --project=$PROJECT_ID -f || : + firebase functions:delete callableTests createUserTests databaseTests deleteUserTests firestoreTests integrationTests pubsubTests remoteConfigTests --project=$PROJECT_ID -f || : announce "Project emptied." } diff --git a/spec/index.spec.ts b/spec/index.spec.ts index 6c64e5cba..a42ed16a8 100644 --- a/spec/index.spec.ts +++ b/spec/index.spec.ts @@ -40,5 +40,6 @@ import './providers/database.spec'; import './providers/firestore.spec'; import './providers/https.spec'; import './providers/pubsub.spec'; +import './providers/remoteConfig.spec'; import './providers/storage.spec'; import './providers/crashlytics.spec'; diff --git a/spec/providers/remoteConfig.spec.ts b/spec/providers/remoteConfig.spec.ts new file mode 100644 index 000000000..86387927e --- /dev/null +++ b/spec/providers/remoteConfig.spec.ts @@ -0,0 +1,128 @@ +// The MIT License (MIT) +// +// Copyright (c) 2017 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +import { expect } from 'chai'; +import * as admin from 'firebase-admin'; +import * as _ from 'lodash'; + +import { CloudFunction } from '../../src/cloud-functions'; +import * as functions from '../../src/index'; +import * as remoteConfig from '../../src/providers/remoteConfig'; + +describe('RemoteConfig Functions', () => { + function constructVersion() { + return { + versionNumber: 1, + updateTime: '2017-07-02T18:48:58.920638Z', + updateUser: { + name: 'Foo Bar', + email: 'foobar@gmail.com', + }, + description: 'test description', + updateOrigin: 'CONSOLE', + updateType: 'INCREMENTAL_UPDATE', + }; + } + + function makeEvent(data: any, context?: { [key: string]: any }) { + context = context || {}; + return { + data: data, + context: _.merge( + { + eventId: '123', + timestamp: '2018-07-03T00:49:04.264Z', + eventType: 'google.firebase.remoteconfig.update', + resource: { + name: 'projects/project1', + service: 'service', + }, + }, + context + ), + }; + } + + describe('#onUpdate', () => { + function expectedTrigger() { + return { + eventTrigger: { + resource: 'projects/project1', + eventType: 'google.firebase.remoteconfig.update', + service: 'firebaseremoteconfig.googleapis.com', + }, + }; + } + + before(() => { + process.env.GCLOUD_PROJECT = 'project1'; + }); + + after(() => { + delete process.env.GCLOUD_PROJECT; + }); + + it('should have the correct trigger', () => { + const cloudFunction = remoteConfig.onUpdate(() => null); + expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger()); + }); + + it('should allow both region and runtime options to be set', () => { + const cloudFunction = functions + .region('my-region') + .runWith({ + timeoutSeconds: 90, + memory: '256MB', + }) + .remoteConfig.onUpdate(() => null); + + expect(cloudFunction.__trigger.regions).to.deep.equal(['my-region']); + expect(cloudFunction.__trigger.availableMemoryMb).to.deep.equal(256); + expect(cloudFunction.__trigger.timeout).to.deep.equal('90s'); + }); + }); + + describe('unwraps TemplateVersion', () => { + let cloudFunctionUpdate: CloudFunction; + let event: any; + before(() => { + process.env.GCLOUD_PROJECT = 'project1'; + cloudFunctionUpdate = remoteConfig.onUpdate( + (version: remoteConfig.TemplateVersion) => version + ); + event = { + data: constructVersion(), + }; + }); + + after(() => { + delete process.env.GCLOUD_PROJECT; + }); + + it('should unwrap the version in the event', () => { + return Promise.all([ + cloudFunctionUpdate(event).then((data: any) => { + expect(data).to.deep.equal(constructVersion()); + }), + ]); + }); + }); +}); diff --git a/src/function-builder.ts b/src/function-builder.ts index 099b71e3b..fb2edf41f 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -30,8 +30,9 @@ import * as database from './providers/database'; import * as firestore from './providers/firestore'; import * as https from './providers/https'; import * as pubsub from './providers/pubsub'; +import * as remoteConfig from './providers/remoteConfig'; import * as storage from './providers/storage'; -import { HttpsFunction } from './cloud-functions'; +import { CloudFunction, EventContext, HttpsFunction } from './cloud-functions'; /** * Configure the regions that the function is deployed to. @@ -212,6 +213,26 @@ export class FunctionBuilder { }; } + get remoteConfig() { + return { + /** + * Handle all updates (including rollbacks) that affect a Remote Config + * project. + * @param handler A function that takes the updated Remote Config template + * version metadata as an argument. + */ + onUpdate: ( + handler: ( + version: remoteConfig.TemplateVersion, + context: EventContext + ) => PromiseLike | any + ) => + remoteConfig._onUpdateWithOpts(handler, this.options) as CloudFunction< + remoteConfig.TemplateVersion + >, + }; + } + get storage() { return { /** diff --git a/src/index.ts b/src/index.ts index 1998ce642..5971b02ad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,7 @@ import * as database from './providers/database'; import * as firestore from './providers/firestore'; import * as https from './providers/https'; import * as pubsub from './providers/pubsub'; +import * as remoteConfig from './providers/remoteConfig'; import * as storage from './providers/storage'; import { firebaseConfig } from './config'; @@ -40,6 +41,7 @@ export { firestore, https, pubsub, + remoteConfig, storage, }; diff --git a/src/providers/remoteConfig.ts b/src/providers/remoteConfig.ts new file mode 100644 index 000000000..ca8ef3139 --- /dev/null +++ b/src/providers/remoteConfig.ts @@ -0,0 +1,107 @@ +// The MIT License (MIT) +// +// Copyright (c) 2018 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the 'Software'), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import * as _ from 'lodash'; + +import { + CloudFunction, + Event, + EventContext, + makeCloudFunction, +} from '../cloud-functions'; +import { DeploymentOptions } from '../function-builder'; + +/** @internal */ +export const provider = 'google.firebase.remoteconfig'; +/** @internal */ +export const service = 'firebaseremoteconfig.googleapis.com'; + +/** + * Handle all updates (including rollbacks) that affect a Remote Config project. + * @param handler A function that takes the updated Remote Config template + * version metadata as an argument. + */ +export function onUpdate( + handler: ( + version: TemplateVersion, + context: EventContext + ) => PromiseLike | any +): CloudFunction { + return _onUpdateWithOpts(handler, {}); +} + +/** @internal */ +export function _onUpdateWithOpts( + handler: ( + version: TemplateVersion, + context: EventContext + ) => PromiseLike | any, + opts: DeploymentOptions +): CloudFunction { + if (!process.env.GCLOUD_PROJECT) { + throw new Error('process.env.GCLOUD_PROJECT is not set.'); + } + return makeCloudFunction({ + handler, + provider, + service, + triggerResource: () => `projects/${process.env.GCLOUD_PROJECT}`, + eventType: 'update', + opts: opts, + }); +} + +/** + * Interface representing a Remote Config template version metadata object that + * was emitted when the project was updated. + */ +export interface TemplateVersion { + /** The version number of the updated Remote Config template. */ + versionNumber: number; + + /** When the template was updated in format (ISO8601 timestamp). */ + updateTime: string; + + /** Metadata about the account that performed the update. */ + updateUser: RemoteConfigUser; + + /** A description associated with the particular Remote Config template. */ + description: string; + + /** The origin of the caller. */ + updateOrigin: string; + + /** The type of update action that was performed. */ + updateType: string; + + /** + * The version number of the Remote Config template that was rolled back to, + * if the update was a rollback. + */ + rollbackSource?: number; +} + +export interface RemoteConfigUser { + name?: string; + email: string; + imageUrl?: string; +} From 510e4285a5b85de440cdf62b5a4fcd8927c9729d Mon Sep 17 00:00:00 2001 From: Kevin Jian Date: Thu, 25 Oct 2018 10:15:04 -0700 Subject: [PATCH 073/705] Changelog for v2.1.0 --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index e69de29bb..c5f226684 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +feature - Added support for Remote Config triggered functions with `functions.remoteConfig`. From 25b8ddea4533ff979c16b4b1c0e1b8e7a1a3cf72 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Thu, 25 Oct 2018 17:22:41 +0000 Subject: [PATCH 074/705] [firebase-release] Updated SDK for Cloud Functions to 2.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f044e2f40..2f8d3e588 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "2.0.5", + "version": "2.1.0", "description": "Firebase SDK for Cloud Functions", "main": "lib/index.js", "scripts": { From 7f6d71c81d8c22c1c5f6b3e66de92eae91c558d6 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Thu, 25 Oct 2018 17:22:52 +0000 Subject: [PATCH 075/705] [firebase-release] Removed change log and reset repo after 2.1.0 release --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index c5f226684..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +0,0 @@ -feature - Added support for Remote Config triggered functions with `functions.remoteConfig`. From 557158b6b544d5ee2aa36ca4c09c98e429f763a6 Mon Sep 17 00:00:00 2001 From: Diana Tkachenko Date: Mon, 3 Dec 2018 18:09:02 -0800 Subject: [PATCH 076/705] adding ts-node as dev dependency so that we can run unit tests individually --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 2f8d3e588..f8339ad84 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "nock": "^9.0.0", "prettier": "^1.13.7", "sinon": "^1.17.4", + "ts-node": "^7.0.1", "typescript": "~2.8.3" }, "peerDependencies": { From c145499988ec8c6861bc0ae5bea27c3582e8804c Mon Sep 17 00:00:00 2001 From: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> Date: Thu, 20 Dec 2018 13:55:44 -0800 Subject: [PATCH 077/705] Make template fields required --- .github/ISSUE_TEMPLATE.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 89a43f576..bd1d9681f 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -32,7 +32,7 @@ https://firebase.google.com/support/contact/bugs-features/. Select 'Functions' a --> -### Version info +### [REQUIRED] Version info @@ -43,27 +43,27 @@ be fixed in the latest versions. --> **firebase-admin:** -### Test case +### [REQUIRED] Test case -### Steps to reproduce +### [REQUIRED] Steps to reproduce -### Were you able to successfully deploy your functions? +### [REQUIRED] Were you able to successfully deploy your functions? -### Expected behavior +### [REQUIRED] Expected behavior -### Actual behavior +### [REQUIRED] Actual behavior From 440936866f564bb7cf3cb1591a96954eda475fb8 Mon Sep 17 00:00:00 2001 From: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> Date: Fri, 21 Dec 2018 15:40:33 -0800 Subject: [PATCH 078/705] Remove require on deploy changes question --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index bd1d9681f..80e315479 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -53,7 +53,7 @@ be fixed in the latest versions. --> -### [REQUIRED] Were you able to successfully deploy your functions? +### Were you able to successfully deploy your functions? From d5b704d1348b73b225067d62084aca41e06ff77a Mon Sep 17 00:00:00 2001 From: Kevin Jian Date: Fri, 4 Jan 2019 16:24:52 -0800 Subject: [PATCH 079/705] Update https types in function builder --- src/function-builder.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/function-builder.ts b/src/function-builder.ts index fb2edf41f..cc673dce1 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -32,7 +32,7 @@ import * as https from './providers/https'; import * as pubsub from './providers/pubsub'; import * as remoteConfig from './providers/remoteConfig'; import * as storage from './providers/storage'; -import { CloudFunction, EventContext, HttpsFunction } from './cloud-functions'; +import { CloudFunction, EventContext } from './cloud-functions'; /** * Configure the regions that the function is deployed to. @@ -122,7 +122,7 @@ export class FunctionBuilder { */ onRequest: ( handler: (req: express.Request, resp: express.Response) => void - ) => https._onRequestWithOpts(handler, this.options) as HttpsFunction, + ) => https._onRequestWithOpts(handler, this.options), /** * Declares a callable method for clients to call using a Firebase SDK. * @param handler A method that takes a data and context and returns a value. @@ -132,7 +132,7 @@ export class FunctionBuilder { data: any, context: https.CallableContext ) => any | Promise - ) => https._onCallWithOpts(handler, this.options) as HttpsFunction, + ) => https._onCallWithOpts(handler, this.options), }; } From 1ac36ce70f6b9715c1567f73a4c3c8d2017dd0f5 Mon Sep 17 00:00:00 2001 From: joehan Date: Tue, 8 Jan 2019 09:46:36 -0800 Subject: [PATCH 080/705] adding errors for invalid region and timeoutSeconds values (#371) * adding errors for invalid region and timeoutSeconds values * fixing tests broken by new errors --- spec/function-builder.spec.ts | 41 ++++++++++++++++++++++++----- spec/providers/analytics.spec.ts | 4 +-- spec/providers/auth.spec.ts | 4 +-- spec/providers/crashlytics.spec.ts | 4 +-- spec/providers/database.spec.ts | 4 +-- spec/providers/firestore.spec.ts | 4 +-- spec/providers/https.spec.ts | 8 +++--- spec/providers/pubsub.spec.ts | 4 +-- spec/providers/remoteConfig.spec.ts | 4 +-- spec/providers/storage.spec.ts | 4 +-- src/function-builder.ts | 28 ++++++++++++++++++-- 11 files changed, 80 insertions(+), 29 deletions(-) diff --git a/spec/function-builder.spec.ts b/spec/function-builder.spec.ts index 972f865dc..22bfefbc5 100644 --- a/spec/function-builder.spec.ts +++ b/spec/function-builder.spec.ts @@ -35,11 +35,11 @@ describe('FunctionBuilder', () => { it('should allow region to be set', () => { let fn = functions - .region('my-region') + .region('us-east1') .auth.user() .onCreate(user => user); - expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.regions).to.deep.equal(['us-east1']); }); // it('should allow multiple regions to be set', () => { @@ -69,7 +69,7 @@ describe('FunctionBuilder', () => { it('should allow both region and runtime options to be set', () => { let fn = functions - .region('my-region') + .region('us-east1') .runWith({ timeoutSeconds: 90, memory: '256MB', @@ -77,7 +77,7 @@ describe('FunctionBuilder', () => { .auth.user() .onCreate(user => user); - expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.regions).to.deep.equal(['us-east1']); expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); }); @@ -88,11 +88,11 @@ describe('FunctionBuilder', () => { timeoutSeconds: 90, memory: '256MB', }) - .region('my-region') + .region('us-east1') .auth.user() .onCreate(user => user); - expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.regions).to.deep.equal(['us-east1']); expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); }); @@ -105,9 +105,36 @@ describe('FunctionBuilder', () => { }).to.throw(Error); expect(() => { - return functions.region('some-region').runWith({ + return functions.region('us-east1').runWith({ memory: 'unsupported', } as any); }).to.throw(Error); }); + + it('should throw an error if user chooses an invalid timeoutSeconds', () => { + expect(() => { + return functions.runWith({ + timeoutSeconds: 1000000, + } as any); + }).to.throw(Error); + + expect(() => { + return functions.region('us-east1').runWith({ + timeoutSeconds: 1000000, + } as any); + }).to.throw(Error); + }); + + it('should throw an error if user chooses an invalid region', () => { + expect(() => { + return functions.region('unsupported'); + }).to.throw(Error); + + expect(() => { + return functions.region('unsupported').runWith({ + timeoutSeconds: 500, + } as any); + }).to.throw(Error); + }); + }); diff --git a/spec/providers/analytics.spec.ts b/spec/providers/analytics.spec.ts index e56e354f6..e7a4532d5 100644 --- a/spec/providers/analytics.spec.ts +++ b/spec/providers/analytics.spec.ts @@ -38,7 +38,7 @@ describe('Analytics Functions', () => { it('should allow both region and runtime options to be set', () => { let fn = functions - .region('my-region') + .region('us-east1') .runWith({ timeoutSeconds: 90, memory: '256MB', @@ -46,7 +46,7 @@ describe('Analytics Functions', () => { .analytics.event('event') .onLog(event => event); - expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.regions).to.deep.equal(['us-east1']); expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); }); diff --git a/spec/providers/auth.spec.ts b/spec/providers/auth.spec.ts index f63840f2d..f82c736e5 100644 --- a/spec/providers/auth.spec.ts +++ b/spec/providers/auth.spec.ts @@ -40,7 +40,7 @@ describe('Auth Functions', () => { it('should allow both region and runtime options to be set', () => { let fn = functions - .region('my-region') + .region('us-east1') .runWith({ timeoutSeconds: 90, memory: '256MB', @@ -48,7 +48,7 @@ describe('Auth Functions', () => { .auth.user() .onCreate(() => null); - expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.regions).to.deep.equal(['us-east1']); expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); }); diff --git a/spec/providers/crashlytics.spec.ts b/spec/providers/crashlytics.spec.ts index 391ae016a..7b2a78018 100644 --- a/spec/providers/crashlytics.spec.ts +++ b/spec/providers/crashlytics.spec.ts @@ -40,7 +40,7 @@ describe('Crashlytics Functions', () => { it('should allow both region and runtime options to be set', () => { let fn = functions - .region('my-region') + .region('us-east1') .runWith({ timeoutSeconds: 90, memory: '256MB', @@ -48,7 +48,7 @@ describe('Crashlytics Functions', () => { .crashlytics.issue() .onNew(issue => issue); - expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.regions).to.deep.equal(['us-east1']); expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); }); diff --git a/spec/providers/database.spec.ts b/spec/providers/database.spec.ts index 87d502243..9388cd9ae 100644 --- a/spec/providers/database.spec.ts +++ b/spec/providers/database.spec.ts @@ -44,7 +44,7 @@ describe('Database Functions', () => { it('should allow both region and runtime options to be set', () => { let fn = functions - .region('my-region') + .region('us-east1') .runWith({ timeoutSeconds: 90, memory: '256MB', @@ -52,7 +52,7 @@ describe('Database Functions', () => { .database.ref('/') .onCreate(snap => snap); - expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.regions).to.deep.equal(['us-east1']); expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); }); diff --git a/spec/providers/firestore.spec.ts b/spec/providers/firestore.spec.ts index bd823210d..34e7ab24c 100644 --- a/spec/providers/firestore.spec.ts +++ b/spec/providers/firestore.spec.ts @@ -122,7 +122,7 @@ describe('Firestore Functions', () => { it('should allow both region and runtime options to be set', () => { let fn = functions - .region('my-region') + .region('us-east1') .runWith({ timeoutSeconds: 90, memory: '256MB', @@ -130,7 +130,7 @@ describe('Firestore Functions', () => { .firestore.document('doc') .onCreate(snap => snap); - expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.regions).to.deep.equal(['us-east1']); expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); }); diff --git a/spec/providers/https.spec.ts b/spec/providers/https.spec.ts index 420953114..966569609 100644 --- a/spec/providers/https.spec.ts +++ b/spec/providers/https.spec.ts @@ -42,14 +42,14 @@ describe('CloudHttpsBuilder', () => { it('should allow both region and runtime options to be set', () => { let fn = functions - .region('my-region') + .region('us-east1') .runWith({ timeoutSeconds: 90, memory: '256MB', }) .https.onRequest(() => null); - expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.regions).to.deep.equal(['us-east1']); expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); }); @@ -508,14 +508,14 @@ describe('callable.FunctionBuilder', () => { it('should allow both region and runtime options to be set', () => { let fn = functions - .region('my-region') + .region('us-east1') .runWith({ timeoutSeconds: 90, memory: '256MB', }) .https.onCall(() => null); - expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.regions).to.deep.equal(['us-east1']); expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); }); diff --git a/spec/providers/pubsub.spec.ts b/spec/providers/pubsub.spec.ts index 1363955fb..b1d7e2918 100644 --- a/spec/providers/pubsub.spec.ts +++ b/spec/providers/pubsub.spec.ts @@ -73,7 +73,7 @@ describe('Pubsub Functions', () => { it('should allow both region and runtime options to be set', () => { let fn = functions - .region('my-region') + .region('us-east1') .runWith({ timeoutSeconds: 90, memory: '256MB', @@ -81,7 +81,7 @@ describe('Pubsub Functions', () => { .pubsub.topic('toppy') .onPublish(() => null); - expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.regions).to.deep.equal(['us-east1']); expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); }); diff --git a/spec/providers/remoteConfig.spec.ts b/spec/providers/remoteConfig.spec.ts index 86387927e..07da151a8 100644 --- a/spec/providers/remoteConfig.spec.ts +++ b/spec/providers/remoteConfig.spec.ts @@ -87,14 +87,14 @@ describe('RemoteConfig Functions', () => { it('should allow both region and runtime options to be set', () => { const cloudFunction = functions - .region('my-region') + .region('us-east1') .runWith({ timeoutSeconds: 90, memory: '256MB', }) .remoteConfig.onUpdate(() => null); - expect(cloudFunction.__trigger.regions).to.deep.equal(['my-region']); + expect(cloudFunction.__trigger.regions).to.deep.equal(['us-east1']); expect(cloudFunction.__trigger.availableMemoryMb).to.deep.equal(256); expect(cloudFunction.__trigger.timeout).to.deep.equal('90s'); }); diff --git a/spec/providers/storage.spec.ts b/spec/providers/storage.spec.ts index 5003db4e6..2aa7f7145 100644 --- a/spec/providers/storage.spec.ts +++ b/spec/providers/storage.spec.ts @@ -38,7 +38,7 @@ describe('Storage Functions', () => { it('should allow both region and runtime options to be set', () => { let fn = functions - .region('my-region') + .region('us-east1') .runWith({ timeoutSeconds: 90, memory: '256MB', @@ -46,7 +46,7 @@ describe('Storage Functions', () => { .storage.object() .onArchive(() => null); - expect(fn.__trigger.regions).to.deep.equal(['my-region']); + expect(fn.__trigger.regions).to.deep.equal(['us-east1']); expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); }); diff --git a/src/function-builder.ts b/src/function-builder.ts index cc673dce1..71c4cda46 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -40,13 +40,23 @@ import { CloudFunction, EventContext } from './cloud-functions'; * For example: `functions.region('us-east1')` */ export function region(region: string) { + if ( + !_.includes( + ['us-central1', 'us-east1', 'europe-west1', 'asia-northeast1'], + region + ) + ) { + throw new Error( + "The only valid regions are 'us-central1', 'us-east1', 'europe-west1', and 'asia-northeast1'" + ); + } return new FunctionBuilder({ regions: [region] }); } /** * Configure runtime options for the function. * @param runtimeOptions Object with 2 optional fields: - * 1. `timeoutSeconds`: timeout for the function in seconds. + * 1. `timeoutSeconds`: timeout for the function in seconds, possible values are 0 to 540 * 2. `memory`: amount of memory to allocate to the function, * possible values are: '128MB', '256MB', '512MB', '1GB', and '2GB'. */ @@ -65,6 +75,13 @@ export function runWith(runtimeOptions: { "The only valid memory allocation values are: '128MB', '256MB', '512MB', '1GB', and '2GB'" ); } + if ( + runtimeOptions.timeoutSeconds > 540 || runtimeOptions.timeoutSeconds < 0 + ) { + throw new Error( + "TimeoutSeconds must be between 0 and 540" + ); + } return new FunctionBuilder(runtimeOptions); } @@ -90,7 +107,7 @@ export class FunctionBuilder { /** * Configure runtime options for the function. * @param runtimeOptions Object with 2 optional fields: - * 1. timeoutSeconds: timeout for the function in seconds. + * 1. timeoutSeconds: timeout for the function in seconds, possible values are 0 to 540 * 2. memory: amount of memory to allocate to the function, possible values are: * '128MB', '256MB', '512MB', '1GB', and '2GB'. */ @@ -109,6 +126,13 @@ export class FunctionBuilder { "The only valid memory allocation values are: '128MB', '256MB', '512MB', '1GB', and '2GB'" ); } + if ( + runtimeOptions.timeoutSeconds > 540 || runtimeOptions.timeoutSeconds < 0 + ) { + throw new Error( + "TimeoutSeconds must be between 0 and 540" + ); + } this.options = _.assign(this.options, runtimeOptions); return this; }; From 52fc6a9c90ed8dd1400127c05124332088ae8a1d Mon Sep 17 00:00:00 2001 From: joehan Date: Tue, 8 Jan 2019 09:48:22 -0800 Subject: [PATCH 081/705] makes message param required, per Google CLoud standards (#373) --- src/providers/https.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/https.ts b/src/providers/https.ts index f6a5cddff..56254efe1 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -164,7 +164,7 @@ export class HttpsError extends Error { */ readonly details?: any; - constructor(code: FunctionsErrorCode, message?: string, details?: any) { + constructor(code: FunctionsErrorCode, message: string, details?: any) { super(message); // This is a workaround for a bug in TypeScript when extending Error: From ca4f7781c8ef23f1e444262f302c710254d5f2f4 Mon Sep 17 00:00:00 2001 From: joehan Date: Tue, 8 Jan 2019 10:06:35 -0800 Subject: [PATCH 082/705] Add validation on instance name (#374) * Add validation on instance name * changelog --- changelog.txt | 1 + spec/providers/database.spec.ts | 18 ++++++++++++++++++ src/providers/database.ts | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index e69de29bb..c3be970e5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +added - validation on instance name for realtime database triggers \ No newline at end of file diff --git a/spec/providers/database.spec.ts b/spec/providers/database.spec.ts index 9388cd9ae..95db92d46 100644 --- a/spec/providers/database.spec.ts +++ b/spec/providers/database.spec.ts @@ -245,6 +245,24 @@ describe('Database Functions', () => { expect(instance).to.equal('https://foo.firebaseio.com'); expect(path).to.equal('/bar'); }); + + it('should throw an error if the given instance name contains anything except alphanumerics and dashes', () => { + expect(() => { + return database.resourceToInstanceAndPath( + 'projects/_/instances/a.bad.name/refs/bar' + ); + }).to.throw(Error) + expect(() => { + return database.resourceToInstanceAndPath( + 'projects/_/instances/a_different_bad_name/refs/bar' + ); + }).to.throw(Error) + expect(() => { + return database.resourceToInstanceAndPath( + 'projects/_/instances/BAD!!!!/refs/bar' + ); + }).to.throw(Error) + }); }); describe('DataSnapshot', () => { diff --git a/src/providers/database.ts b/src/providers/database.ts index ed0557294..342379c81 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -240,7 +240,7 @@ export class RefBuilder { /* Utility function to extract database reference from resource string */ /** @internal */ export function resourceToInstanceAndPath(resource: string) { - let resourceRegex = `projects/([^/]+)/instances/([^/]+)/refs(/.+)?`; + let resourceRegex = `projects/([^/]+)/instances/([a-zA-Z0-9\-^/]+)/refs(/.+)?`; let match = resource.match(new RegExp(resourceRegex)); if (!match) { throw new Error( From 138129643d4f6203366597bb3af8c8848170aede Mon Sep 17 00:00:00 2001 From: Kevin Jian Date: Wed, 9 Jan 2019 11:03:30 -0800 Subject: [PATCH 083/705] Revert "Update https types in function builder" (#376) --- src/function-builder.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/function-builder.ts b/src/function-builder.ts index 71c4cda46..80f6086db 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -32,7 +32,7 @@ import * as https from './providers/https'; import * as pubsub from './providers/pubsub'; import * as remoteConfig from './providers/remoteConfig'; import * as storage from './providers/storage'; -import { CloudFunction, EventContext } from './cloud-functions'; +import { CloudFunction, EventContext, HttpsFunction } from './cloud-functions'; /** * Configure the regions that the function is deployed to. @@ -146,7 +146,7 @@ export class FunctionBuilder { */ onRequest: ( handler: (req: express.Request, resp: express.Response) => void - ) => https._onRequestWithOpts(handler, this.options), + ) => https._onRequestWithOpts(handler, this.options) as HttpsFunction, /** * Declares a callable method for clients to call using a Firebase SDK. * @param handler A method that takes a data and context and returns a value. @@ -156,7 +156,7 @@ export class FunctionBuilder { data: any, context: https.CallableContext ) => any | Promise - ) => https._onCallWithOpts(handler, this.options), + ) => https._onCallWithOpts(handler, this.options) as HttpsFunction, }; } From 992fb988a046ebefe370b26a55f813a4dec00fc7 Mon Sep 17 00:00:00 2001 From: Kevin Jian Date: Thu, 10 Jan 2019 10:46:09 -0800 Subject: [PATCH 084/705] Update https types in function builder (#377) --- src/function-builder.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/function-builder.ts b/src/function-builder.ts index 80f6086db..df9cb1cf4 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -32,7 +32,7 @@ import * as https from './providers/https'; import * as pubsub from './providers/pubsub'; import * as remoteConfig from './providers/remoteConfig'; import * as storage from './providers/storage'; -import { CloudFunction, EventContext, HttpsFunction } from './cloud-functions'; +import { CloudFunction, EventContext, Runnable, TriggerAnnotated } from './cloud-functions'; /** * Configure the regions that the function is deployed to. @@ -146,7 +146,7 @@ export class FunctionBuilder { */ onRequest: ( handler: (req: express.Request, resp: express.Response) => void - ) => https._onRequestWithOpts(handler, this.options) as HttpsFunction, + ) => https._onRequestWithOpts(handler, this.options), /** * Declares a callable method for clients to call using a Firebase SDK. * @param handler A method that takes a data and context and returns a value. @@ -156,7 +156,7 @@ export class FunctionBuilder { data: any, context: https.CallableContext ) => any | Promise - ) => https._onCallWithOpts(handler, this.options) as HttpsFunction, + ) => https._onCallWithOpts(handler, this.options), }; } From c99af7b07070a8d6f67a8593edfef13488fa89cc Mon Sep 17 00:00:00 2001 From: joehan Date: Thu, 10 Jan 2019 15:37:47 -0800 Subject: [PATCH 085/705] Fixes tsc error (#380) * fixes error durign tsc * casting field to string; --- src/providers/analytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/analytics.ts b/src/providers/analytics.ts index 6802e61a1..b41ae479a 100644 --- a/src/providers/analytics.ts +++ b/src/providers/analytics.ts @@ -411,7 +411,7 @@ function copyField( field: K, transform: (val: any) => T[K] = _.identity ): void { - copyFieldTo(from, to, field, field, transform); + copyFieldTo(from, to, field as string, field, transform); } function copyFields(from: any, to: T, fields: K[]): void { From 9325a68af3168217d62ce0d3e917d28c07e5dbf2 Mon Sep 17 00:00:00 2001 From: joehan Date: Thu, 10 Jan 2019 15:38:05 -0800 Subject: [PATCH 086/705] Reformats changelog (#379) * reformats changelog; * removing + --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index c3be970e5..95c0d1741 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +1 @@ -added - validation on instance name for realtime database triggers \ No newline at end of file +fixed - validation on instance name for realtime database triggers \ No newline at end of file From d7c09b79df06b633d60f50398917bca864086286 Mon Sep 17 00:00:00 2001 From: joehan Date: Fri, 11 Jan 2019 11:27:05 -0800 Subject: [PATCH 087/705] Allow functions to deploy to multiple regions (#375) * Reverts 3d2adbe, allows region() to accept multiple regions * switch to loadash difference * adds ` * changelog --- changelog.txt | 1 + spec/function-builder.spec.ts | 44 ++++++++++++++++++++++++++--------- src/function-builder.ts | 30 +++++++++++++----------- 3 files changed, 51 insertions(+), 24 deletions(-) diff --git a/changelog.txt b/changelog.txt index 95c0d1741..7f8cac513 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +1,2 @@ +feature - support for multiple regions on functions by passing extra region strings to functions.regions() fixed - validation on instance name for realtime database triggers \ No newline at end of file diff --git a/spec/function-builder.spec.ts b/spec/function-builder.spec.ts index 22bfefbc5..118525c0f 100644 --- a/spec/function-builder.spec.ts +++ b/spec/function-builder.spec.ts @@ -42,17 +42,17 @@ describe('FunctionBuilder', () => { expect(fn.__trigger.regions).to.deep.equal(['us-east1']); }); - // it('should allow multiple regions to be set', () => { - // let fn = functions - // .region('my-region', 'my-other-region') - // .auth.user() - // .onCreate(user => user); - - // expect(fn.__trigger.regions).to.deep.equal([ - // 'my-region', - // 'my-other-region', - // ]); - // }); + it('should allow multiple regions to be set', () => { + let fn = functions + .region('us-east1', 'us-central1') + .auth.user() + .onCreate(user => user); + + expect(fn.__trigger.regions).to.deep.equal([ + 'us-east1', + 'us-central1', + ]); + }); it('should allow runtime options to be set', () => { let fn = functions @@ -135,6 +135,28 @@ describe('FunctionBuilder', () => { timeoutSeconds: 500, } as any); }).to.throw(Error); + + expect(() => { + return functions.region('unsupported', 'us-east1'); + }).to.throw(Error); + + expect(() => { + return functions.region('unsupported', 'us-east1').runWith({ + timeoutSeconds: 500, + } as any); + }).to.throw(Error); + }); + + it('should throw an error if user chooses no region when using .region()', () => { + expect(() => { + return functions.region(); + }).to.throw(Error); + + expect(() => { + return functions.region().runWith({ + timeoutSeconds: 500, + } as any); + }).to.throw(Error); }); }); diff --git a/src/function-builder.ts b/src/function-builder.ts index df9cb1cf4..e2a52cf42 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -36,23 +36,27 @@ import { CloudFunction, EventContext, Runnable, TriggerAnnotated } from './cloud /** * Configure the regions that the function is deployed to. - * @param region Region string. - * For example: `functions.region('us-east1')` + * @param regions One of more region strings. + * For example: `functions.region('us-east1')` or `functions.region('us-east1', 'us-central1')` */ -export function region(region: string) { +export function region(...regions: string[]) { + if (!regions.length) { + throw new Error( + "You must specify at least one region" + ); + } if ( - !_.includes( - ['us-central1', 'us-east1', 'europe-west1', 'asia-northeast1'], - region - ) + _.difference( + regions, + ['us-central1', 'us-east1', 'europe-west1', 'asia-northeast1'] + ).length ) { throw new Error( "The only valid regions are 'us-central1', 'us-east1', 'europe-west1', and 'asia-northeast1'" ); } - return new FunctionBuilder({ regions: [region] }); + return new FunctionBuilder({ regions }); } - /** * Configure runtime options for the function. * @param runtimeOptions Object with 2 optional fields: @@ -96,11 +100,11 @@ export class FunctionBuilder { /** * Configure the regions that the function is deployed to. - * @param region Region string. - * For example: `functions.region('us-east1')` + * @param regions One or more region strings. + * For example: `functions.region('us-east1')` or `functions.region('us-east1', 'us-central1')` */ - region = (region: string) => { - this.options.regions = [region]; + region = (...regions: string[]) => { + this.options.regions = regions; return this; }; From edb3d27b2429b760b0542947781a33dd559d9dd7 Mon Sep 17 00:00:00 2001 From: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> Date: Wed, 16 Jan 2019 08:13:53 -0800 Subject: [PATCH 088/705] Update issue templates (#382) * Update issue templates Adding two templates for easier filing. * Delete duplicate bug report template * Add related issue field to bug template * Make spacing consistent Also, changed "related issue" to "related issues" * Update feature request template Updates to be more consistent with firebase-tools feature request template in https://github.com/firebase/firebase-tools/pull/1094 --- .github/ISSUE_TEMPLATE/---feature-request.md | 37 +++++++++++++++ .github/ISSUE_TEMPLATE/---report-a-bug.md | 50 ++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/---feature-request.md create mode 100644 .github/ISSUE_TEMPLATE/---report-a-bug.md diff --git a/.github/ISSUE_TEMPLATE/---feature-request.md b/.github/ISSUE_TEMPLATE/---feature-request.md new file mode 100644 index 000000000..6ea47a921 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---feature-request.md @@ -0,0 +1,37 @@ +--- +name: "\U0001F4A1 Feature Request" +about: Have a feature you'd like to see in the functions SDK? Request it through our + support channel +title: '' +labels: '' +assignees: '' + +--- + + + +### [REQUIRED] Version info + + + +**node:** + + +**firebase-functions:** + +**firebase-tools:** + + +**firebase-admin:** + +### [REQUIRED] Test case + + + + +### [REQUIRED] Steps to reproduce + + + + +### [REQUIRED] Expected behavior + + + + +### [REQUIRED] Actual behavior + + + +### Were you able to successfully deploy your functions? + + From 616dc4f4988dec7c47fa44e31789aa6f45a9f816 Mon Sep 17 00:00:00 2001 From: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> Date: Wed, 16 Jan 2019 10:02:20 -0800 Subject: [PATCH 089/705] Small changes to issue templates (#385) * Add period in feature request issue template * Delete old issue template --- .github/ISSUE_TEMPLATE.md | 69 -------------------- .github/ISSUE_TEMPLATE/---feature-request.md | 2 +- 2 files changed, 1 insertion(+), 70 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 80e315479..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,69 +0,0 @@ - - - -### [REQUIRED] Version info - - - -**firebase-functions:** - -**firebase-tools:** - -**firebase-admin:** - -### [REQUIRED] Test case - - - - -### [REQUIRED] Steps to reproduce - - - - -### Were you able to successfully deploy your functions? - - - - -### [REQUIRED] Expected behavior - - - - -### [REQUIRED] Actual behavior - - diff --git a/.github/ISSUE_TEMPLATE/---feature-request.md b/.github/ISSUE_TEMPLATE/---feature-request.md index 6ea47a921..2ff568e74 100644 --- a/.github/ISSUE_TEMPLATE/---feature-request.md +++ b/.github/ISSUE_TEMPLATE/---feature-request.md @@ -1,7 +1,7 @@ --- name: "\U0001F4A1 Feature Request" about: Have a feature you'd like to see in the functions SDK? Request it through our - support channel + support channel. title: '' labels: '' assignees: '' From fe3d410b38363ebb47d0b0a23b721958644c7f7e Mon Sep 17 00:00:00 2001 From: joehan Date: Wed, 16 Jan 2019 13:48:37 -0800 Subject: [PATCH 090/705] Improving stability and speed of intergration tests (#384) * Adds delay to ensure functions are deployed, adds ability to test node6 and node8 on different projects * Support for 1 or 2 projects, run tests simulatenously when 2 projects * retry deploy up to 3 times, fix typo * Switch to cleaner looping Co-Authored-By: joehan * code review; --- integration_test/functions/src/index.ts | 2 +- integration_test/run_tests.sh | 66 ++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index c135b371c..844d8f8da 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -148,7 +148,7 @@ export const integrationTests: any = functions }) .then(() => { console.log('All tests pass!'); - resp.status(200).send('PASS'); + resp.status(200).send('PASS \n'); }) .catch(err => { console.log(`Some tests failed: ${err}`); diff --git a/integration_test/run_tests.sh b/integration_test/run_tests.sh index 05abd2bd6..0e4ffeaa0 100755 --- a/integration_test/run_tests.sh +++ b/integration_test/run_tests.sh @@ -4,15 +4,25 @@ set -e function usage { - echo "Usage: $0 " + echo "Usage: $0 " exit 1 } +# This script takes 1 or 2 params, both of which are Firebase project ids. +# If there is only one given, that project will be used for both node6 and node8 +# Otherwise, param1 will be used for node6 +# and param2 will be used for node8 # The first parameter is required and is the Firebase project id. if [[ $1 == "" ]]; then usage fi -PROJECT_ID=$1 +if [[ $2 == "" ]]; then + PROJECT_ID_NODE_6=$1 + PROJECT_ID_NODE_8=$1 +else + PROJECT_ID_NODE_6=$1 + PROJECT_ID_NODE_8=$2 +fi # Directory where this script lives. DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" @@ -31,11 +41,13 @@ function build_sdk { function pick_node6 { cd $DIR + PROJECT_ID=$PROJECT_ID_NODE_6 cp package.node6.json functions/package.json } function pick_node8 { cd $DIR + PROJECT_ID=$PROJECT_ID_NODE_8 cp package.node8.json functions/package.json } @@ -51,15 +63,47 @@ function delete_all_functions { cd $DIR # Try to delete, if there are errors it is because the project is already empty, # in that case do nothing. - firebase functions:delete callableTests createUserTests databaseTests deleteUserTests firestoreTests integrationTests pubsubTests remoteConfigTests --project=$PROJECT_ID -f || : + firebase functions:delete callableTests createUserTests databaseTests deleteUserTests firestoreTests integrationTests pubsubTests remoteConfigTests --force --project=$PROJECT_ID_NODE_6 || : & + if ! [[ $PROJECT_ID_NODE_6 == $PROJECT_ID_NODE_8 ]]; then + firebase functions:delete callableTests createUserTests databaseTests deleteUserTests firestoreTests integrationTests pubsubTests remoteConfigTests --force --project=$PROJECT_ID_NODE_8 || : & + fi + wait announce "Project emptied." } function deploy { cd $DIR ./functions/node_modules/.bin/tsc -p functions/ - # Deploy functions, and security rules for database and Firestore - firebase deploy --project=$PROJECT_ID --only functions,database,firestore + # Deploy functions, and security rules for database and Firestore. If the deploy fails, retry twice + for i in 1 2 3; do firebase deploy --project=$PROJECT_ID --only functions,database,firestore && break; done +} + +# At the moment, functions take 30-40 seconds AFTER firebase deploy returns successfully to go live +# This needs to be fixed separately +# However, so that we have working integration tests in the interim, waitForPropagation is a workaround +function waitForPropagation { + announce "Waiting 50 seconds for functions changes to propagate" + sleep 50 +} + +function run_all_tests { + announce "Running the integration tests..." + + # Constructs the URLs for both test functions. This may change in the future, + # causing this script to start failing, but currently we don't have a very + # reliable way of determining the URL dynamically. + TEST_DOMAIN="cloudfunctions.net" + if [[ $FIREBASE_FUNCTIONS_URL == "https://preprod-cloudfunctions.sandbox.googleapis.com" ]]; then + TEST_DOMAIN="txcloud.net" + fi + TEST_URL_NODE_6="https://us-central1-$PROJECT_ID_NODE_6.$TEST_DOMAIN/integrationTests" + TEST_URL_NODE_8="https://us-central1-$PROJECT_ID_NODE_8.$TEST_DOMAIN/integrationTests" + echo $TEST_URL_NODE_6 + echo $TEST_URL_NODE_8 + curl --fail $TEST_URL_NODE_6 & NODE6PID=$! + curl --fail $TEST_URL_NODE_8 & NODE8PID=$! + wait $NODE6PID && echo 'node 6 passed' || (announce 'Node 6 tests failed'; cleanup; announce 'Tests failed'; exit 1) + wait $NODE8PID && echo 'node 8 passed' || (announce 'Node 8 tests failed'; cleanup; announce 'Tests failed'; exit 1) } function run_tests { @@ -93,10 +137,18 @@ install_deps delete_all_functions announce "Deploying functions to Node 8 runtime ..." deploy -run_tests +if [[ $PROJECT_ID_NODE_6 == $PROJECT_ID_NODE_8 ]]; then + waitForPropagation + run_tests +fi pick_node6 announce "Re-deploying the same functions to Node 6 runtime ..." deploy -run_tests +waitForPropagation +if [[ $PROJECT_ID_NODE_6 == $PROJECT_ID_NODE_8 ]]; then + run_tests +else + run_all_tests +fi cleanup announce "All tests pass!" From f45266e9889a3d8ada4139af8c559b48622493a0 Mon Sep 17 00:00:00 2001 From: joehan Date: Thu, 17 Jan 2019 12:46:08 -0800 Subject: [PATCH 091/705] Adds integration tests for storage triggers, updates README to describe 2 project integration test option (#386) * Adds integration tests for storage triggers * Documents multiproject option --- integration_test/README.md | 5 ++-- integration_test/functions/src/index.ts | 6 ++++- .../functions/src/storage-tests.ts | 26 +++++++++++++++++++ integration_test/run_tests.sh | 2 +- 4 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 integration_test/functions/src/storage-tests.ts diff --git a/integration_test/README.md b/integration_test/README.md index f972e26cd..d0a1f3ef9 100644 --- a/integration_test/README.md +++ b/integration_test/README.md @@ -1,12 +1,13 @@ How to Use --------- -***ATTENTION***: Running this test will wipe the contents of the Firebase project you run it against. Make sure you use a disposable Firebase project! +***ATTENTION***: Running this test will wipe the contents of the Firebase project(s) you run it against. Make sure you use disposable Firebase project(s)! Run the integration test as follows: ```bash -./run_tests.sh +./run_tests.sh [] ``` +If just one project_id is provided, the both the node6 and node8 tests will be run on that project, in series. If two project_ids are provided, the node6 tests will be run on the first project and the node8 tests will be run on the second one, in parallel. The tests run fully automatically, and will print the result on standard out. The integration test for HTTPS is that it properly kicks off other integration tests and returns a result. From there the other integration test suites will write their results back to the database, where you can check the detailed results if you'd like. diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 844d8f8da..7b250d745 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -2,6 +2,7 @@ import * as functions from 'firebase-functions'; import * as https from 'https'; import * as admin from 'firebase-admin'; import { Request, Response } from 'express'; +import * as fs from 'fs' import * as PubSub from '@google-cloud/pubsub'; const pubsub = PubSub(); @@ -12,6 +13,7 @@ export * from './auth-tests'; export * from './firestore-tests'; export * from './https-tests'; export * from './remoteConfig-tests'; +export * from './storage-tests'; const numTests = Object.keys(exports).length; // Assumption: every exported function is its own test. import 'firebase-functions'; // temporary shim until process.env.FIREBASE_CONFIG available natively in GCF(BUG 63586213) @@ -57,7 +59,7 @@ export const integrationTests: any = functions .ref() .push().key; console.log('testId is: ', testId); - + fs.writeFile('/tmp/' + testId + '.txt', 'test', ()=> {}); return Promise.all([ // A database write to trigger the Firebase Realtime Database tests. admin @@ -107,6 +109,8 @@ export const integrationTests: any = functions request.write(JSON.stringify({ version: { description: testId } })); request.end(); }), + // A storage upload to trigger the Storage tests + admin.storage().bucket().upload('/tmp/' + testId + '.txt'), // Invoke a callable HTTPS trigger. callHttpsTrigger('callableTests', { foo: 'bar', testId }), ]) diff --git a/integration_test/functions/src/storage-tests.ts b/integration_test/functions/src/storage-tests.ts new file mode 100644 index 000000000..3f3c4e997 --- /dev/null +++ b/integration_test/functions/src/storage-tests.ts @@ -0,0 +1,26 @@ +import * as functions from 'firebase-functions'; +import { TestSuite, expectEq, expectDeepEq } from './testing'; +import ObjectMetadata = functions.storage.ObjectMetadata; +const testIdFieldName = 'documentId'; + +export const storageTests: any = functions + .runWith({ + timeoutSeconds: 540, + }) + .storage.bucket().object() + .onFinalize((s, c) => { + const testId = s.name.split('.')[0]; + return new TestSuite('storage object finalize') + + .it('should not have event.app', (data, context) => !(context as any).app) + + .it('should have the right eventType', (snap, context) => + expectEq(context.eventType, 'google.storage.object.finalize') + ) + + .it('should have eventId', (snap, context) => context.eventId) + + .it('should have timestamp', (snap, context) => context.timestamp) + + .run(testId, s, c); + }); diff --git a/integration_test/run_tests.sh b/integration_test/run_tests.sh index 0e4ffeaa0..8721842c7 100755 --- a/integration_test/run_tests.sh +++ b/integration_test/run_tests.sh @@ -4,7 +4,7 @@ set -e function usage { - echo "Usage: $0 " + echo "Usage: $0 []" exit 1 } From 0d3095d85bd4f296409c15009d05ce2e71efd49f Mon Sep 17 00:00:00 2001 From: joehan Date: Fri, 18 Jan 2019 10:03:34 -0800 Subject: [PATCH 092/705] deactivates storage tests to unblock gcf (#388) --- integration_test/functions/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 7b250d745..56cbba7f6 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -13,7 +13,7 @@ export * from './auth-tests'; export * from './firestore-tests'; export * from './https-tests'; export * from './remoteConfig-tests'; -export * from './storage-tests'; +// export * from './storage-tests'; const numTests = Object.keys(exports).length; // Assumption: every exported function is its own test. import 'firebase-functions'; // temporary shim until process.env.FIREBASE_CONFIG available natively in GCF(BUG 63586213) From 5651ff354d628d5e10d980d218920af22c2064ed Mon Sep 17 00:00:00 2001 From: joehan Date: Fri, 18 Jan 2019 10:49:05 -0800 Subject: [PATCH 093/705] Allow storage bucket url and database url to be explicitly defined (#389) --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5971b02ad..4b70693dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,8 +62,8 @@ if (!process.env.FIREBASE_CONFIG) { 'Warning, estimating Firebase Config based on GCLOUD_PROJECT. Initializing firebase-admin may fail' ); process.env.FIREBASE_CONFIG = JSON.stringify({ - databaseURL: `https://${process.env.GCLOUD_PROJECT}.firebaseio.com`, - storageBucket: `${process.env.GCLOUD_PROJECT}.appspot.com`, + databaseURL: `${process.env.DATABASE_URL}` || `https://${process.env.GCLOUD_PROJECT}.firebaseio.com`, + storageBucket: `${process.env.STORAGE_BUCKET_URL}` || `${process.env.GCLOUD_PROJECT}.appspot.com`, projectId: process.env.GCLOUD_PROJECT, }); } else { From c9b6d5750c8b0f43f655a671a9a93432c9efa9a6 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Wed, 23 Jan 2019 11:58:32 -0800 Subject: [PATCH 094/705] Add issue template metadata for the OSS bot (#390) --- .github/ISSUE_TEMPLATE/---feature-request.md | 4 ++++ .github/ISSUE_TEMPLATE/---report-a-bug.md | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/---feature-request.md b/.github/ISSUE_TEMPLATE/---feature-request.md index 2ff568e74..d53f53652 100644 --- a/.github/ISSUE_TEMPLATE/---feature-request.md +++ b/.github/ISSUE_TEMPLATE/---feature-request.md @@ -7,6 +7,10 @@ labels: '' assignees: '' --- + + ### Related issues From 8052eaade388b75b434deed9cd6d7a12dd74f397 Mon Sep 17 00:00:00 2001 From: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> Date: Fri, 25 Jan 2019 11:50:42 -0800 Subject: [PATCH 095/705] Update feature request template (#393) * Update feature request template Update feature request template to be more clear on directions and less cluttered. * Update README with usage question info * Remove usage line --- .github/ISSUE_TEMPLATE/---feature-request.md | 26 -------------------- README.md | 11 +++++++++ 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/---feature-request.md b/.github/ISSUE_TEMPLATE/---feature-request.md index d53f53652..d47fbda18 100644 --- a/.github/ISSUE_TEMPLATE/---feature-request.md +++ b/.github/ISSUE_TEMPLATE/---feature-request.md @@ -5,37 +5,11 @@ about: Have a feature you'd like to see in the functions SDK? Request it through title: '' labels: '' assignees: '' - --- - diff --git a/.github/ISSUE_TEMPLATE/---report-a-bug.md b/.github/ISSUE_TEMPLATE/---report-a-bug.md index 4484df5fa..694c4aedf 100644 --- a/.github/ISSUE_TEMPLATE/---report-a-bug.md +++ b/.github/ISSUE_TEMPLATE/---report-a-bug.md @@ -1,12 +1,12 @@ --- -name: "⚠️ Report a Bug" +name: '⚠️ Report a Bug' about: Think you found a bug in the SDK? Report it here. title: '' labels: '' assignees: '' - --- - @@ -21,11 +21,13 @@ template_path=.github/ISSUE_TEMPLATE/---report-a-bug.md be fixed in the latest versions. --> **node:** + **firebase-functions:** **firebase-tools:** + **firebase-admin:** @@ -34,17 +36,14 @@ be fixed in the latest versions. --> - ### [REQUIRED] Steps to reproduce - ### [REQUIRED] Expected behavior - ### [REQUIRED] Actual behavior - ### Description \ No newline at end of file + diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..de457f940 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +lib +package.json +spec/fixtures/credential/unparsable.key.json diff --git a/integration_test/README.md b/integration_test/README.md index d0a1f3ef9..c6a0ca32d 100644 --- a/integration_test/README.md +++ b/integration_test/README.md @@ -1,13 +1,13 @@ -How to Use ---------- +## How to Use -***ATTENTION***: Running this test will wipe the contents of the Firebase project(s) you run it against. Make sure you use disposable Firebase project(s)! +**_ATTENTION_**: Running this test will wipe the contents of the Firebase project(s) you run it against. Make sure you use disposable Firebase project(s)! Run the integration test as follows: ```bash ./run_tests.sh [] ``` + If just one project_id is provided, the both the node6 and node8 tests will be run on that project, in series. If two project_ids are provided, the node6 tests will be run on the first project and the node8 tests will be run on the second one, in parallel. The tests run fully automatically, and will print the result on standard out. The integration test for HTTPS is that it properly kicks off other integration tests and returns a result. From there the other integration test suites will write their results back to the database, where you can check the detailed results if you'd like. diff --git a/integration_test/functions/tsconfig.json b/integration_test/functions/tsconfig.json index a8e9362db..a5bd57033 100644 --- a/integration_test/functions/tsconfig.json +++ b/integration_test/functions/tsconfig.json @@ -6,11 +6,7 @@ "noImplicitAny": false, "outDir": "lib", "declaration": true, - "typeRoots": [ - "node_modules/@types" - ] + "typeRoots": ["node_modules/@types"] }, - "files": [ - "src/index.ts" - ] + "files": ["src/index.ts"] } diff --git a/package.json b/package.json index fd83fbb0b..916c4159f 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "build:pack": "rm -rf lib && npm install && tsc -p tsconfig.release.json && npm pack", "build:release": "npm install --production && npm install typescript firebase-admin && tsc -p tsconfig.release.json", "build": "tsc -p tsconfig.release.json", - "format": "prettier --check '**/*.ts'", - "format:fix": "prettier --write '**/*.ts'", + "format": "prettier --check '**/*.{json,md,ts,yml,yaml}'", + "format:fix": "prettier --write '**/*.{json,md,ts,yml,yaml}'", "lint": "tslint --config tslint.json --project tsconfig.json ", "lint:fix": "tslint --config tslint.json --fix --project tsconfig.json", "test": "mocha -r ts-node/register ./spec/index.spec.ts" diff --git a/tsconfig.json b/tsconfig.json index 0992f6d5b..2f7cc7e0d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "resolveJsonModule": true, - "sourceMap": true, + "sourceMap": true }, "extends": "./tsconfig.release.json", "include": ["./src/**/*.ts", "./spec/**/*.ts"] From 59adaa80fbb197d86f3f6bed9c577ab42b5107bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Natan=20S=C4=85gol?= Date: Thu, 11 Jul 2019 01:59:49 +0200 Subject: [PATCH 166/705] Remove unused valAt utility (#513) --- spec/utils.spec.ts | 23 +---------------------- src/utils.ts | 31 ------------------------------- 2 files changed, 1 insertion(+), 53 deletions(-) diff --git a/spec/utils.spec.ts b/spec/utils.spec.ts index 57634dd72..2ea64fb7e 100644 --- a/spec/utils.spec.ts +++ b/spec/utils.spec.ts @@ -21,7 +21,7 @@ // SOFTWARE. import { expect } from 'chai'; -import { applyChange, normalizePath, pathParts, valAt } from '../src/utils'; +import { applyChange, normalizePath, pathParts } from '../src/utils'; describe('utils', () => { describe('.normalizePath(path: string)', () => { @@ -42,27 +42,6 @@ describe('utils', () => { }); }); - describe('.valAt(source: any, path?: string): any', () => { - it('should be null if null along any point in the path', () => { - expect(valAt(null)).to.be.null; - expect(valAt(null, '/foo')).to.be.null; - expect(valAt({ a: { b: null } }, '/a/b/c')).to.be.null; - }); - - it('should be null if accessing a path past a leaf value', () => { - expect(valAt({ a: 2 }, '/a/b')).to.be.null; - }); - - it('should be the leaf value if one is present', () => { - expect(valAt({ a: { b: 23 } }, '/a/b')).to.eq(23); - expect(valAt({ a: { b: 23 } }, '/a')).to.deep.equal({ b: 23 }); - }); - - it('should be undefined if in unexplored territory', () => { - expect(valAt({ a: 23 }, '/b')).to.be.undefined; - }); - }); - describe('.applyChange(from: any, to: any): any', () => { it('should return the to value for non-object values of from and to', () => { expect(applyChange({ a: 'b' }, null)).to.eq(null); diff --git a/src/utils.ts b/src/utils.ts index 0ebbb38c4..ffec72a2d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -61,34 +61,3 @@ export function pruneNulls(obj: any) { } return obj; } - -export function valAt(source: any, path?: string) { - if (source === null) { - return null; - } else if (typeof source !== 'object') { - return path ? null : source; - } - - const parts = pathParts(path); - if (!parts.length) { - return source; - } - - let cur = source; - let leaf; - while (parts.length) { - const key = parts.shift(); - if (cur[key] === null || leaf) { - return null; - } else if (typeof cur[key] === 'object') { - if (parts.length) { - cur = cur[key]; - } else { - return cur[key]; - } - } else { - leaf = cur[key]; - } - } - return leaf; -} From f3af1759f0bba3c13a0fed9e14682aad76580dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Natan=20S=C4=85gol?= Date: Thu, 11 Jul 2019 02:05:28 +0200 Subject: [PATCH 167/705] Include all TypeScript files in development configuration (#512) * Include all TypeScript files in development configuration * Autofix TSLint issues * Reformat --- integration_test/functions/src/auth-tests.ts | 8 ++++---- integration_test/functions/src/database-tests.ts | 4 ++-- integration_test/functions/src/firestore-tests.ts | 4 ++-- integration_test/functions/src/https-tests.ts | 2 +- integration_test/functions/src/index.ts | 13 +++++-------- integration_test/functions/src/pubsub-tests.ts | 6 +++--- .../functions/src/remoteConfig-tests.ts | 2 +- integration_test/functions/src/storage-tests.ts | 3 +-- integration_test/functions/src/testing.ts | 12 +++++++----- tsconfig.json | 2 +- 10 files changed, 27 insertions(+), 29 deletions(-) diff --git a/integration_test/functions/src/auth-tests.ts b/integration_test/functions/src/auth-tests.ts index aaef21e6a..2c1e1d4e6 100644 --- a/integration_test/functions/src/auth-tests.ts +++ b/integration_test/functions/src/auth-tests.ts @@ -1,10 +1,10 @@ -import * as functions from 'firebase-functions'; -import { TestSuite, expectEq } from './testing'; import * as admin from 'firebase-admin'; +import * as functions from 'firebase-functions'; +import { expectEq, TestSuite } from './testing'; import UserMetadata = admin.auth.UserRecord; export const createUserTests: any = functions.auth.user().onCreate((u, c) => { - let testId: string = u.displayName; + const testId: string = u.displayName; console.log(`testId is ${testId}`); return new TestSuite('auth user onCreate') @@ -38,7 +38,7 @@ export const createUserTests: any = functions.auth.user().onCreate((u, c) => { }); export const deleteUserTests: any = functions.auth.user().onDelete((u, c) => { - let testId: string = u.displayName; + const testId: string = u.displayName; console.log(`testId is ${testId}`); return new TestSuite('auth user onDelete') diff --git a/integration_test/functions/src/database-tests.ts b/integration_test/functions/src/database-tests.ts index 215e6bd3b..de3030d3a 100644 --- a/integration_test/functions/src/database-tests.ts +++ b/integration_test/functions/src/database-tests.ts @@ -1,6 +1,6 @@ -import * as functions from 'firebase-functions'; -import { TestSuite, expectEq, expectMatches } from './testing'; import * as admin from 'firebase-admin'; +import * as functions from 'firebase-functions'; +import { expectEq, expectMatches, TestSuite } from './testing'; import DataSnapshot = admin.database.DataSnapshot; const testIdFieldName = 'testId'; diff --git a/integration_test/functions/src/firestore-tests.ts b/integration_test/functions/src/firestore-tests.ts index e5f8acdbf..49d1ae919 100644 --- a/integration_test/functions/src/firestore-tests.ts +++ b/integration_test/functions/src/firestore-tests.ts @@ -1,6 +1,6 @@ -import * as functions from 'firebase-functions'; -import { TestSuite, expectEq, expectDeepEq } from './testing'; import * as admin from 'firebase-admin'; +import * as functions from 'firebase-functions'; +import { expectDeepEq, expectEq, TestSuite } from './testing'; import DocumentSnapshot = admin.firestore.DocumentSnapshot; const testIdFieldName = 'documentId'; diff --git a/integration_test/functions/src/https-tests.ts b/integration_test/functions/src/https-tests.ts index 7a0889ef6..55c7df983 100644 --- a/integration_test/functions/src/https-tests.ts +++ b/integration_test/functions/src/https-tests.ts @@ -1,6 +1,6 @@ import * as functions from 'firebase-functions'; import * as _ from 'lodash'; -import { TestSuite, expectEq } from './testing'; +import { expectEq, TestSuite } from './testing'; export const callableTests: any = functions.https.onCall((d) => { return new TestSuite('https onCall') diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 967b3d376..05f9e2035 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -1,11 +1,8 @@ -import * as functions from 'firebase-functions'; -import * as https from 'https'; -import * as admin from 'firebase-admin'; import { Request, Response } from 'express'; +import * as admin from 'firebase-admin'; +import * as functions from 'firebase-functions'; import * as fs from 'fs'; - -import * as PubSub from '@google-cloud/pubsub'; -const pubsub = PubSub(); +import * as https from 'https'; export * from './pubsub-tests'; export * from './database-tests'; @@ -82,7 +79,7 @@ export const integrationTests: any = functions .split('.') .slice(1) .join('.'); - let pubsub: any = require('@google-cloud/pubsub')(); + const pubsub: any = require('@google-cloud/pubsub')(); const testId = admin .database() .ref() @@ -155,7 +152,7 @@ export const integrationTests: any = functions .then(() => { // On test completion, check that all tests pass and reply "PASS", or provide further details. console.log('Waiting for all tests to report they pass...'); - let ref = admin.database().ref(`testRuns/${testId}`); + const ref = admin.database().ref(`testRuns/${testId}`); return new Promise((resolve, reject) => { let testsExecuted = 0; ref.on('child_added', (snapshot) => { diff --git a/integration_test/functions/src/pubsub-tests.ts b/integration_test/functions/src/pubsub-tests.ts index ebac609dc..3c1b1ecc7 100644 --- a/integration_test/functions/src/pubsub-tests.ts +++ b/integration_test/functions/src/pubsub-tests.ts @@ -1,6 +1,6 @@ -import * as functions from 'firebase-functions'; import * as admin from 'firebase-admin'; -import { TestSuite, expectEq, evaluate, success } from './testing'; +import * as functions from 'firebase-functions'; +import { evaluate, expectEq, success, TestSuite } from './testing'; import PubsubMessage = functions.pubsub.Message; // TODO(inlined) use multiple queues to run inline. @@ -64,7 +64,7 @@ export const schedule: any = functions.pubsub // For the test, the job is triggered by the jobs:run api .onRun((context) => { let testId; - let db = admin.database(); + const db = admin.database(); return new Promise(async (resolve, reject) => { await db .ref('testRuns') diff --git a/integration_test/functions/src/remoteConfig-tests.ts b/integration_test/functions/src/remoteConfig-tests.ts index 2b824fbbd..3474e1fc2 100644 --- a/integration_test/functions/src/remoteConfig-tests.ts +++ b/integration_test/functions/src/remoteConfig-tests.ts @@ -1,5 +1,5 @@ import * as functions from 'firebase-functions'; -import { TestSuite, expectEq } from './testing'; +import { expectEq, TestSuite } from './testing'; import TemplateVersion = functions.remoteConfig.TemplateVersion; export const remoteConfigTests: any = functions.remoteConfig.onUpdate( diff --git a/integration_test/functions/src/storage-tests.ts b/integration_test/functions/src/storage-tests.ts index 7ced40fc9..43cdef5e2 100644 --- a/integration_test/functions/src/storage-tests.ts +++ b/integration_test/functions/src/storage-tests.ts @@ -1,7 +1,6 @@ import * as functions from 'firebase-functions'; -import { TestSuite, expectEq, expectDeepEq } from './testing'; +import { expectEq, TestSuite } from './testing'; import ObjectMetadata = functions.storage.ObjectMetadata; -const testIdFieldName = 'documentId'; export const storageTests: any = functions .runWith({ diff --git a/integration_test/functions/src/testing.ts b/integration_test/functions/src/testing.ts index d3782a981..43d3e69d5 100644 --- a/integration_test/functions/src/testing.ts +++ b/integration_test/functions/src/testing.ts @@ -1,9 +1,11 @@ import * as firebase from 'firebase-admin'; -import * as _ from 'lodash'; import { EventContext } from 'firebase-functions'; +import * as _ from 'lodash'; export type TestCase = (data: T, context?: EventContext) => any; -export type TestCaseMap = { [key: string]: TestCase }; +export interface TestCaseMap { + [key: string]: TestCase; +} export class TestSuite { private name: string; @@ -20,8 +22,8 @@ export class TestSuite { } run(testId: string, data: T, context?: EventContext): Promise { - let running: Array> = []; - for (let testName in this.tests) { + const running: Array> = []; + for (const testName in this.tests) { if (!this.tests.hasOwnProperty(testName)) { continue; } @@ -36,7 +38,7 @@ export class TestSuite { }, (error) => { console.error(`Failed: ${testName}`, error); - return { name: testName, passed: 0, error: error }; + return { name: testName, passed: 0, error }; } ); running.push(run); diff --git a/tsconfig.json b/tsconfig.json index 2f7cc7e0d..6538f1f07 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,5 +4,5 @@ "sourceMap": true }, "extends": "./tsconfig.release.json", - "include": ["./src/**/*.ts", "./spec/**/*.ts"] + "include": ["**/*.ts"] } From 5363a48ccebcb9340923dbf09f882f1bc4a48288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Natan=20S=C4=85gol?= Date: Fri, 12 Jul 2019 00:19:54 +0200 Subject: [PATCH 168/705] Define Codeowners (#516) * Define Codeowners * Add @kevinajian to Codeowners --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..49f7349e0 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @kevinajian @thechenky From a6ef584f9e7f296cb2d1aec9fdb7b5a4c31010ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Natan=20S=C4=85gol?= Date: Fri, 12 Jul 2019 01:16:34 +0200 Subject: [PATCH 169/705] Extract and document path manipulation utilities (#514) --- spec/index.spec.ts | 1 + spec/utilities/path.spec.ts | 24 ++++++++++++++++++++++++ spec/utils.spec.ts | 20 +------------------- src/providers/database.ts | 3 ++- src/utilities/path.ts | 35 +++++++++++++++++++++++++++++++++++ src/utils.ts | 20 -------------------- 6 files changed, 63 insertions(+), 40 deletions(-) create mode 100644 spec/utilities/path.spec.ts create mode 100644 src/utilities/path.ts diff --git a/spec/index.spec.ts b/spec/index.spec.ts index 64ffdf62e..0eb95045d 100644 --- a/spec/index.spec.ts +++ b/spec/index.spec.ts @@ -43,4 +43,5 @@ import './providers/pubsub.spec'; import './providers/remoteConfig.spec'; import './providers/storage.spec'; import './setup.spec'; +import './utilities/path.spec'; import './utils.spec'; diff --git a/spec/utilities/path.spec.ts b/spec/utilities/path.spec.ts new file mode 100644 index 000000000..c0a5dc318 --- /dev/null +++ b/spec/utilities/path.spec.ts @@ -0,0 +1,24 @@ +import { expect } from 'chai'; +import { normalizePath, pathParts } from '../../src/utilities/path'; + +describe('utilities', () => { + describe('path', () => { + describe('#normalizePath', () => { + it('should strip leading and trailing slash', () => { + expect(normalizePath('/my/path/is/{rad}/')).to.eq('my/path/is/{rad}'); + }); + }); + + describe('#pathParts', () => { + it('should turn a path into an array of strings', () => { + expect(pathParts('/foo/bar/baz')).to.deep.equal(['foo', 'bar', 'baz']); + }); + + it('should turn a root path, empty string, or null path into an empty array', () => { + expect(pathParts('')).to.deep.equal([]); + expect(pathParts(null)).to.deep.equal([]); + expect(pathParts('/')).to.deep.equal([]); + }); + }); + }); +}); diff --git a/spec/utils.spec.ts b/spec/utils.spec.ts index 2ea64fb7e..51a8478fa 100644 --- a/spec/utils.spec.ts +++ b/spec/utils.spec.ts @@ -21,27 +21,9 @@ // SOFTWARE. import { expect } from 'chai'; -import { applyChange, normalizePath, pathParts } from '../src/utils'; +import { applyChange } from '../src/utils'; describe('utils', () => { - describe('.normalizePath(path: string)', () => { - it('should strip leading and trailing slash', () => { - expect(normalizePath('/my/path/is/{rad}/')).to.eq('my/path/is/{rad}'); - }); - }); - - describe('.pathParts(path: string): string[]', () => { - it('should turn a path into an array of strings', () => { - expect(pathParts('/foo/bar/baz')).to.deep.equal(['foo', 'bar', 'baz']); - }); - - it('should turn a root path, empty string, or null path into an empty array', () => { - expect(pathParts('')).to.deep.equal([]); - expect(pathParts(null)).to.deep.equal([]); - expect(pathParts('/')).to.deep.equal([]); - }); - }); - describe('.applyChange(from: any, to: any): any', () => { it('should return the to value for non-object values of from and to', () => { expect(applyChange({ a: 'b' }, null)).to.eq(null); diff --git a/src/providers/database.ts b/src/providers/database.ts index 7740a4373..ec6969909 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -32,7 +32,8 @@ import { } from '../cloud-functions'; import { firebaseConfig } from '../config'; import { DeploymentOptions } from '../function-configuration'; -import { applyChange, joinPath, normalizePath, pathParts } from '../utils'; +import { joinPath, normalizePath, pathParts } from '../utilities/path'; +import { applyChange } from '../utils'; /** @hidden */ export const provider = 'google.firebase.database'; diff --git a/src/utilities/path.ts b/src/utilities/path.ts new file mode 100644 index 000000000..40ab8f047 --- /dev/null +++ b/src/utilities/path.ts @@ -0,0 +1,35 @@ +/** + * Removes leading and trailing slashes from a path. + * + * @param path A path to normalize, in POSIX format. + */ +export function normalizePath(path: string): string { + if (!path) { + return ''; + } + return path.replace(/^\//, '').replace(/\/$/, ''); +} + +/** + * Normalizes a given path and splits it into an array of segments. + * + * @param path A path to split, in POSIX format. + */ +export function pathParts(path: string): string[] { + if (!path || path === '' || path === '/') { + return []; + } + return normalizePath(path).split('/'); +} + +/** + * Normalizes given paths and joins these together using a POSIX separator. + * + * @param base A first path segment, in POSIX format. + * @param child A second path segment, in POSIX format. + */ +export function joinPath(base: string, child: string) { + return pathParts(base) + .concat(pathParts(child)) + .join('/'); +} diff --git a/src/utils.ts b/src/utils.ts index ffec72a2d..b52c1e54f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -22,26 +22,6 @@ import * as _ from 'lodash'; -export function normalizePath(path: string): string { - if (!path) { - return ''; - } - return path.replace(/^\//, '').replace(/\/$/, ''); -} - -export function pathParts(path: string): string[] { - if (!path || path === '' || path === '/') { - return []; - } - return normalizePath(path).split('/'); -} - -export function joinPath(base: string, child: string) { - return pathParts(base) - .concat(pathParts(child)) - .join('/'); -} - export function applyChange(src: any, dest: any) { // if not mergeable, don't merge if (!_.isPlainObject(dest) || !_.isPlainObject(src)) { From 03155440dea92fe6ffd166ebea9c022f5ee0c13d Mon Sep 17 00:00:00 2001 From: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> Date: Thu, 11 Jul 2019 16:39:30 -0700 Subject: [PATCH 170/705] Add automatic TypeDoc generation to Functions SDK (#491) * add typedoc generation, somewhat working * fix from admin => functions in a comment * add npm run apidocs command to generate apidocs * modify to use modules instead of file mode * change toc to match modules mode of typedoc * add UserInfo to be generated, modify toc.yaml to correct links * add missing sections to toc.yaml * fix merge conflict * fix erroneous warnings about missing and unlisted files * remove underscores * edit toc.yaml to fix the links * remove underscores in links inside the html files * unhide Event * hide Event * add docgen/html to gitignore * formatting * remove Event from toc, add proper toc file checking * clean up docgen script --- .gitignore | 1 + docgen/content-sources/HOME.md | 3 + docgen/content-sources/toc.yaml | 114 +++++ docgen/generate-docs.js | 339 ++++++++++++++ docgen/theme/assets/css/firebase.css | 40 ++ docgen/theme/assets/css/main.css | 552 +++++++++++++++++++++++ docgen/theme/assets/images/lockup.png | Bin 0 -> 2646 bytes docgen/theme/layouts/default.hbs | 33 ++ docgen/theme/partials/breadcrumb.hbs | 11 + docgen/theme/partials/comment.hbs | 22 + docgen/theme/partials/header.hbs | 23 + docgen/theme/partials/member.sources.hbs | 15 + docgen/theme/partials/navigation.hbs | 22 + docgen/theme/templates/reflection.hbs | 72 +++ docgen/tsconfig.json | 3 + docgen/typedoc.js | 27 ++ package.json | 10 +- src/providers/auth.ts | 5 + 18 files changed, 1290 insertions(+), 2 deletions(-) create mode 100644 docgen/content-sources/HOME.md create mode 100644 docgen/content-sources/toc.yaml create mode 100644 docgen/generate-docs.js create mode 100644 docgen/theme/assets/css/firebase.css create mode 100644 docgen/theme/assets/css/main.css create mode 100644 docgen/theme/assets/images/lockup.png create mode 100644 docgen/theme/layouts/default.hbs create mode 100644 docgen/theme/partials/breadcrumb.hbs create mode 100644 docgen/theme/partials/comment.hbs create mode 100644 docgen/theme/partials/header.hbs create mode 100644 docgen/theme/partials/member.sources.hbs create mode 100644 docgen/theme/partials/navigation.hbs create mode 100644 docgen/theme/templates/reflection.hbs create mode 100644 docgen/tsconfig.json create mode 100644 docgen/typedoc.js diff --git a/.gitignore b/.gitignore index d345a2f7a..ba3cdeec5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .tmp .vscode/ coverage +docgen/html firebase-functions-*.tgz integration_test/.firebaserc integration_test/*.log diff --git a/docgen/content-sources/HOME.md b/docgen/content-sources/HOME.md new file mode 100644 index 000000000..c89df0a57 --- /dev/null +++ b/docgen/content-sources/HOME.md @@ -0,0 +1,3 @@ +# Firebase Functions SDK Reference + +Functions SDK!!! diff --git a/docgen/content-sources/toc.yaml b/docgen/content-sources/toc.yaml new file mode 100644 index 000000000..c2766e430 --- /dev/null +++ b/docgen/content-sources/toc.yaml @@ -0,0 +1,114 @@ +toc: + - title: 'functions' + path: /docs/reference/functions/cloud_functions_.html + section: + - title: 'CloudFunction' + path: /docs/reference/functions/cloud_functions_.html#cloudfunction + - title: 'HttpsFunction' + path: /docs/reference/functions/cloud_functions_.html#httpsfunction + - title: 'EventContext' + path: /docs/reference/functions/cloud_functions_.eventcontext.html + - title: 'FunctionBuilder' + path: /docs/reference/functions/function_builder_.functionbuilder.html + - title: 'Change' + path: /docs/reference/functions/cloud_functions_.change.html + + - title: 'functions.config' + path: /docs/reference/functions/config_.html + section: + - title: 'Config' + path: /docs/reference/functions/config_.config.html + + - title: 'functions.analytics' + path: /docs/reference/functions/providers_analytics_.html + section: + - title: 'AnalyticsEvent' + path: /docs/reference/functions/providers_analytics_.analyticsevent.html + - title: 'AnalyticsEventBuilder' + path: /docs/reference/functions/providers_analytics_.analyticseventbuilder.html + - title: 'AppInfo' + path: /docs/reference/functions/providers_analytics_.appinfo.html + - title: 'DeviceInfo' + path: /docs/reference/functions/providers_analytics_.deviceinfo.html + - title: 'ExportBundleInfo' + path: /docs/reference/functions/providers_analytics_.exportbundleinfo.html + - title: 'GeoInfo' + path: /docs/reference/functions/providers_analytics_.geoinfo.html + - title: 'UserDimensions' + path: /docs/reference/functions/providers_analytics_.userdimensions.html + - title: 'UserPropertyValue' + path: /docs/reference/functions/providers_analytics_.userpropertyvalue.html + + - title: 'functions.auth' + path: /docs/reference/functions/providers_auth_.html + section: + - title: 'UserBuilder' + path: /docs/reference/functions/providers_auth_.userbuilder.html + - title: 'UserInfo' + path: /docs/reference/functions/providers_auth_.html#userinfo + - title: 'UserRecordMetadata' + path: /docs/reference/functions/providers_auth_.userrecordmetadata.html + - title: 'UserRecord' + path: /docs/reference/functions/providers_auth_.html#userrecord + + - title: 'functions.crashlytics' + path: /docs/reference/functions/providers_crashlytics_.html + section: + - title: 'Issue' + path: /docs/reference/functions/providers_crashlytics_.issue.html + - title: 'IssueBuilder' + path: /docs/reference/functions/providers_crashlytics_.issuebuilder.html + - title: 'AppInfo' + path: /docs/reference/functions/providers_crashlytics_.appinfo.html + - title: 'VelocityAlert' + path: /docs/reference/functions/providers_crashlytics_.velocityalert.html + + - title: 'functions.firestore' + path: /docs/reference/functions/providers_firestore_.html + section: + - title: 'DocumentBuilder' + path: /docs/reference/functions/providers_firestore_.documentbuilder.html + - title: 'DocumentSnapshot' + path: /docs/reference/functions/providers_firestore_.html#documentsnapshot + + - title: 'functions.database' + path: /docs/reference/functions/providers_database_.html + section: + - title: 'DataSnapshot' + path: /docs/reference/functions/providers_database_.datasnapshot.html + - title: 'RefBuilder' + path: /docs/reference/functions/providers_database_.refbuilder.html + - title: 'InstanceBuilder' + path: /docs/reference/functions/providers_database_.instancebuilder.html + + - title: 'functions.https' + path: /docs/reference/functions/providers_https_.html + section: + - title: 'HttpsError' + path: /docs/reference/functions/providers_https_.httpserror.html + + - title: 'functions.pubsub' + path: /docs/reference/functions/providers_pubsub_.html + section: + - title: 'Message' + path: /docs/reference/functions/providers_pubsub_.message.html + - title: 'TopicBuilder' + path: /docs/reference/functions/providers_pubsub_.topicbuilder.html + + - title: 'functions.remoteconfig' + path: /docs/reference/functions/providers_remoteconfig_.html + section: + - title: 'RemoteConfigUser' + path: /docs/reference/functions/providers_remoteconfig_.remoteconfiguser.html + - title: 'TemplateVersion' + path: /docs/reference/functions/providers_remoteconfig_.templateversion.html + + - title: 'functions.storage' + path: /docs/reference/functions/providers_storage_.html + section: + - title: 'BucketBuilder' + path: /docs/reference/functions/providers_storage_.bucketbuilder.html + - title: 'ObjectBuilder' + path: /docs/reference/functions/providers_storage_.objectbuilder.html + - title: 'ObjectMetadata' + path: /docs/reference/functions/providers_storage_.objectmetadata.html diff --git a/docgen/generate-docs.js b/docgen/generate-docs.js new file mode 100644 index 000000000..299250e64 --- /dev/null +++ b/docgen/generate-docs.js @@ -0,0 +1,339 @@ +/** + * @license + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { exec } = require('child-process-promise'); +const fs = require('mz/fs'); +const path = require('path'); +const yargs = require('yargs'); +const yaml = require('js-yaml'); +const _ = require('lodash'); + +const repoPath = path.resolve(`${__dirname}/..`); + +// Command-line options. +const { source: sourceFile } = yargs + .option('source', { + default: `${repoPath}/src`, + describe: 'Typescript source file(s)', + type: 'string' + }) + .version(false) + .help().argv; + +const docPath = path.resolve(`${__dirname}/html`); +const contentPath = path.resolve(`${__dirname}/content-sources`); +const tempHomePath = path.resolve(`${contentPath}/HOME_TEMP.md`); +const devsitePath = `/docs/reference/functions/`; + +/** + * Strips path prefix and returns only filename. + * @param {string} path + */ +function stripPath(path) { + const parts = path.split('/'); + return parts[parts.length - 1]; +} + +/** + * Runs Typedoc command. + * + * Additional config options come from ./typedoc.js + */ +function runTypedoc() { + const command = `${repoPath}/node_modules/.bin/typedoc ${sourceFile} \ + --out ${docPath} \ + --readme ${tempHomePath} \ + --options ${__dirname}/typedoc.js \ + --theme ${__dirname}/theme`; + + console.log('Running command:\n', command); + return exec(command); +} + +/** + * Moves files from subdir to root. + * @param {string} subdir Subdir to move files out of. + */ +function moveFilesToRoot(subdir) { + return exec(`mv ${docPath}/${subdir}/* ${docPath}`) + .then(() => { + exec(`rmdir ${docPath}/${subdir}`); + }) + .catch(e => console.error(e)); +} + +/** + * Renames files to remove the leading underscores. + * We need to do this because devsite hides these files. + * Example: + * _cloud_functions_.resource.html => cloud_functions_.resource.html + */ +function renameFiles() { + return fs.readdir(docPath).then(files => { + console.log(files); + files.forEach(file => { + let newFileName = file; + if (_.startsWith(file, "_") && _.endsWith(file, "html")) { + newFileName = _.trimStart(file, "_"); + fs.rename(`${docPath}/${file}`, `${docPath}/${newFileName}`, (err) => { + if (err) console.log(err) + }); + } + }) + }) +} + +/** + * Reformat links to match flat structure. + * @param {string} file File to fix links in. + */ +function fixLinks(file) { + return fs.readFile(file, 'utf8').then(data => { + const flattenedLinks = data + .replace(/\.\.\//g, '') + .replace(/(modules|interfaces|classes)\//g, '') + .replace(/\"_/g, '"'); + let caseFixedLinks = flattenedLinks; + for (const lower in lowerToUpperLookup) { + const re = new RegExp(lower, 'g'); + caseFixedLinks = caseFixedLinks.replace(re, lowerToUpperLookup[lower]); + } + return fs.writeFile(file, caseFixedLinks); + }); +} + +let tocText = ''; + +/** + * Generates temporary markdown file that will be sourced by Typedoc to + * create index.html. + * + * @param {string} tocRaw + * @param {string} homeRaw + */ +function generateTempHomeMdFile(tocRaw, homeRaw) { + const { toc } = yaml.safeLoad(tocRaw); + let tocPageLines = [homeRaw, '# API Reference']; + toc.forEach(group => { + tocPageLines.push(`\n## [${group.title}](${stripPath(group.path)})`); + const section = group.section || []; + section.forEach(item => { + tocPageLines.push(`- [${item.title}](${stripPath(item.path)})`); + }); + }); + return fs.writeFile(tempHomePath, tocPageLines.join('\n')); +} + +/** + * Mapping between lowercase file name and correctly cased name. + * Used to update links when filenames are capitalized. + */ +const lowerToUpperLookup = {}; + +/** + * Checks to see if any files listed in toc.yaml were not generated. + * If files exist, fixes filename case to match toc.yaml version. + */ +function checkForMissingFilesAndFixFilenameCase() { + // Get filenames from toc.yaml. + const filenames = tocText + .split('\n') + .filter(line => line.includes('path:')) + .map(line => line.split(devsitePath)[1]); + // Logs warning to console if a file from TOC is not found. + // console.log(filenames); + const fileCheckPromises = filenames.map(filename => { + // Warns if file does not exist, fixes filename case if it does. + // Preferred filename for devsite should be capitalized and taken from + // toc.yaml. + const tocFilePath = `${docPath}/${filename}`; + // Generated filename from Typedoc will be lowercase. + const generatedFilePath = `${docPath}/${filename.toLowerCase()}`; + return fs.exists(generatedFilePath).then(exists => { + if (exists) { + // Store in a lookup table for link fixing. + lowerToUpperLookup[ + `${filename.toLowerCase()}` + ] = `${filename}`; + return fs.rename(generatedFilePath, tocFilePath); + } else { + console.warn( + `Missing file: ${filename} requested ` + + `in toc.yaml but not found in ${docPath}` + ); + } + }); + }); + return Promise.all(fileCheckPromises).then(() => filenames); +} + +/** + * Gets a list of html files in generated dir and checks if any are not + * found in toc.yaml. + * Option to remove the file if not found (used for node docs). + * + * @param {Array} filenamesFromToc Filenames pulled from toc.yaml + * @param {boolean} shouldRemove Should just remove the file + */ +function checkForUnlistedFiles(filenamesFromToc, shouldRemove) { + return fs.readdir(docPath).then(files => { + const htmlFiles = files + .filter(filename => filename.slice(-4) === 'html'); + const removePromises = []; + htmlFiles.forEach(filename => { + if ( + !filenamesFromToc.includes(filename) && + filename !== 'index' && + filename !== 'globals' + ) { + if (shouldRemove) { + console.log( + `REMOVING ${docPath}/${filename} - not listed in toc.yaml.` + ); + removePromises.push(fs.unlink(`${docPath}/${filename}`)); + } else { + // This is just a warning, it doesn't need to finish before + // the process continues. + console.warn( + `Unlisted file: ${filename} generated ` + + `but not listed in toc.yaml.` + ); + } + } + }); + if (shouldRemove) { + return Promise.all(removePromises).then(() => + htmlFiles.filter(filename => filenamesFromToc.includes(filename)) + ); + } else { + return htmlFiles; + } + }); +} + +/** + * Writes a _toc_autogenerated.yaml as a record of all files that were + * autogenerated. Helpful to tech writers. + * + * @param {Array} htmlFiles List of html files found in generated dir. + */ +function writeGeneratedFileList(htmlFiles) { + const fileList = htmlFiles.map(filename => { + return { + title: filename, + path: `${devsitePath}${filename}` + }; + }); + const generatedTocYAML = yaml.safeDump({ toc: fileList }); + return fs + .writeFile(`${docPath}/_toc_autogenerated.yaml`, generatedTocYAML) + .then(() => htmlFiles); +} + +/** + * Fix all links in generated files to other generated files to point to top + * level of generated docs dir. + * + * @param {Array} htmlFiles List of html files found in generated dir. + */ +function fixAllLinks(htmlFiles) { + const writePromises = []; + htmlFiles.forEach(file => { + // Update links in each html file to match flattened file structure. + writePromises.push(fixLinks(`${docPath}/${file}`)); + }); + return Promise.all(writePromises); +} + +/** + * Main document generation process. + * + * Steps for generating documentation: + * 1) Create temporary md file as source of homepage. + * 2) Run Typedoc, sourcing index.d.ts for API content and temporary md file + * for index.html content. + * 3) Write table of contents file. + * 4) Flatten file structure by moving all items up to root dir and fixing + * links as needed. + * 5) Check for mismatches between TOC list and generated file list. + */ +Promise.all([ + fs.readFile(`${contentPath}/toc.yaml`, 'utf8'), + fs.readFile(`${contentPath}/HOME.md`, 'utf8') +]) + // Read TOC and homepage text and assemble a homepage markdown file. + // This file will be sourced by Typedoc to generate index.html. + .then(([tocRaw, homeRaw]) => { + tocText = tocRaw; + return generateTempHomeMdFile(tocRaw, homeRaw); + }) + // Run main Typedoc process (uses index.d.ts and generated temp file above). + .then(runTypedoc) + .then(output => { + // Typedoc output. + console.log(output.stdout); + // Clean up temp home markdown file. (Nothing needs to wait for this.) + fs.unlink(tempHomePath); + // Devsite doesn't like css.map files. + return fs.unlink(`${docPath}/assets/css/main.css.map`); + }) + // Write out TOC file. Do this after Typedoc step to prevent Typedoc + // erroring when it finds an unexpected file in the target dir. + .then(() => fs.writeFile(`${docPath}/_toc.yaml`, tocText)) + // Flatten file structure. These categories don't matter to us and it makes + // it easier to manage the docs directory. + .then(() => { + return Promise.all([ + moveFilesToRoot('classes'), + moveFilesToRoot('modules'), + moveFilesToRoot('interfaces'), + ]); + }) + // Rename files to remove the underscores since devsite hides those. + .then(renameFiles) + // Check for files listed in TOC that are missing and warn if so. + // Not blocking. + .then(checkForMissingFilesAndFixFilenameCase) + // Check for files that exist but aren't listed in the TOC and warn. + // (If API is node, actually remove the file.) + // Removal is blocking, warnings aren't. + .then(filenamesFromToc => + checkForUnlistedFiles(filenamesFromToc, false) + ) + // Write a _toc_autogenerated.yaml to record what files were created. + .then(htmlFiles => writeGeneratedFileList(htmlFiles)) + // Correct the links in all the generated html files now that files have + // all been moved to top level. + // .then(removeUnderscores) + .then(fixAllLinks) + .then(() => { + fs.readFile(`${docPath}/index.html`, 'utf8').then(data => { + // String to include devsite local variables. + const localVariablesIncludeString = `{% include "docs/web/_local_variables.html" %}\n`; + return fs.writeFile( + `${docPath}/index.html`, + localVariablesIncludeString + data + ); + }); + }) + .catch(e => { + if (e.stdout) { + console.error(e.stdout); + } else { + console.error(e); + } + }); diff --git a/docgen/theme/assets/css/firebase.css b/docgen/theme/assets/css/firebase.css new file mode 100644 index 000000000..86c592820 --- /dev/null +++ b/docgen/theme/assets/css/firebase.css @@ -0,0 +1,40 @@ +.firebase-docs .project-name { + color: #333; + display: inline-block; + font-size: 20px; + font-weight: normal; + margin-left: 130px; +} + +.firebase-docs aside.tsd-sources { + padding: 8px; +} + +.firebase-docs .tsd-panel li { + margin: 0; +} + +.firebase-docs aside.tsd-sources:before { + content: unset; +} + +.firebase-docs dl.tsd-comment-tags dt.tag-example { + float: none; + text-transform: capitalize; + color: #000; + font-size: 1.1em; + padding: 5px; + border: none; +} + +.firebase-docs dl.tsd-comment-tags dd.tag-body-example { + padding-left: 0; +} + +.firebase-docs .tsd-breadcrumb .breadcrumb-name a { + color: #039be5; +} + +.firebase-docs .tsd-breadcrumb .model-name { + color: #333; +} \ No newline at end of file diff --git a/docgen/theme/assets/css/main.css b/docgen/theme/assets/css/main.css new file mode 100644 index 000000000..12f3d05d9 --- /dev/null +++ b/docgen/theme/assets/css/main.css @@ -0,0 +1,552 @@ +/*! normalize.css v1.1.3 | MIT License | git.io/normalize */ +/* ========================================================================== HTML5 display definitions ========================================================================== */ +/** Correct `block` display not defined in IE 6/7/8/9 and Firefox 3. */ +article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; } + +/** Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3. */ +audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; } + +/** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */ +audio:not([controls]) { display: none; height: 0; } + +/** Address styling not present in IE 7/8/9, Firefox 3, and Safari 4. Known issue: no IE 6 support. */ +[hidden] { display: none; } + +/* ========================================================================== Base ========================================================================== */ +/** 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using `em` units. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */ +html { font-size: 100%; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ font-family: sans-serif; } + +/** Address `font-family` inconsistency between `textarea` and other form elements. */ +button, input, select, textarea { font-family: sans-serif; } + +/** Address margins handled incorrectly in IE 6/7. */ +body { margin: 0; } + +/* ========================================================================== Links ========================================================================== */ +/** Address `outline` inconsistency between Chrome and other browsers. */ +a:focus { outline: thin dotted; } +a:active, a:hover { outline: 0; } + +/** Improve readability when focused and also mouse hovered in all browsers. */ +/* ========================================================================== Typography ========================================================================== */ +/** Address font sizes and margins set differently in IE 6/7. Address font sizes within `section` and `article` in Firefox 4+, Safari 5, and Chrome. */ +h1 { font-size: 2em; margin: 0.67em 0; } + +h2 { font-size: 1.5em; margin: 0.83em 0; } + +h3 { font-size: 1.17em; margin: 1em 0; } + +h4, .tsd-index-panel h3 { font-size: 1em; margin: 1.33em 0; } + +h5 { font-size: 0.83em; margin: 1.67em 0; } + +h6 { font-size: 0.67em; margin: 2.33em 0; } + +/** Address styling not present in IE 7/8/9, Safari 5, and Chrome. */ +abbr[title] { border-bottom: 1px dotted; } + +/** Address style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome. */ +b, strong { font-weight: bold; } + +blockquote { margin: 1em 40px; } + +/** Address styling not present in Safari 5 and Chrome. */ +dfn { font-style: italic; } + +/** Address differences between Firefox and other browsers. Known issue: no IE 6/7 normalization. */ +hr { box-sizing: content-box; height: 0; } + +/** Address styling not present in IE 6/7/8/9. */ +mark { background: #ff0; color: #000; } + +/** Address margins set differently in IE 6/7. */ +p, pre { margin: 1em 0; } + +/** Improve readability of pre-formatted text in all browsers. */ +pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; } + +/** Address CSS quotes not supported in IE 6/7. */ +q { quotes: none; } +q:before, q:after { content: ""; content: none; } + +/** Address `quotes` property not supported in Safari 4. */ +/** Address inconsistent and variable font size in all browsers. */ +small { font-size: 80%; } + +/** Prevent `sub` and `sup` affecting `line-height` in all browsers. */ +sub { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } + +sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; top: -0.5em; } + +sub { bottom: -0.25em; } + +/* ========================================================================== Lists ========================================================================== */ +dd { margin: 0 0 0 40px; } + +/** Address paddings set differently in IE 6/7. */ +menu, ol, ul { padding: 0 0 0 40px; } + +/** Correct list images handled incorrectly in IE 7. */ +nav ul, nav ol { list-style: none; list-style-image: none; } + +/* ========================================================================== Embedded content ========================================================================== */ +/** 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3. 2. Improve image quality when scaled in IE 7. */ +img { border: 0; /* 1 */ -ms-interpolation-mode: bicubic; } + +/* 2 */ +/** Correct overflow displayed oddly in IE 9. */ +svg:not(:root) { overflow: hidden; } + +/* ========================================================================== Figures ========================================================================== */ +/** Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11. */ +figure, form { margin: 0; } + +/* ========================================================================== Forms ========================================================================== */ +/** Correct margin displayed oddly in IE 6/7. */ +/** Define consistent border, margin, and padding. */ +fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } + +/** 1. Correct color not being inherited in IE 6/7/8/9. 2. Correct text not wrapping in Firefox 3. 3. Correct alignment displayed oddly in IE 6/7. */ +legend { border: 0; /* 1 */ padding: 0; white-space: normal; /* 2 */ *margin-left: -7px; } + +/* 3 */ +/** 1. Correct font size not being inherited in all browsers. 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5, and Chrome. 3. Improve appearance and consistency in all browsers. */ +button, input, select, textarea { font-size: 100%; /* 1 */ margin: 0; /* 2 */ vertical-align: baseline; /* 3 */ *vertical-align: middle; } + +/* 3 */ +/** Address Firefox 3+ setting `line-height` on `input` using `!important` in the UA stylesheet. */ +button, input { line-height: normal; } + +/** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+. Correct `select` style inheritance in Firefox 4+ and Opera. */ +button, select { text-transform: none; } + +/** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. 4. Remove inner spacing in IE 7 without affecting normal text inputs. Known issue: inner spacing remains in IE 6. */ +button, html input[type="button"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ *overflow: visible; } + +/* 4 */ +input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ *overflow: visible; } + +/* 4 */ +/** Re-set default cursor for disabled elements. */ +button[disabled], html input[disabled] { cursor: default; } + +/** 1. Address box sizing set to content-box in IE 8/9. 2. Remove excess padding in IE 8/9. 3. Remove excess padding in IE 7. Known issue: excess padding remains in IE 6. */ +input { /* 3 */ } +input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ *height: 13px; /* 3 */ *width: 13px; } +input[type="search"] { -webkit-appearance: textfield; /* 1 */ /* 2 */ box-sizing: content-box; } +input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } + +/** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */ +/** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */ +/** Remove inner padding and border in Firefox 3+. */ +button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } + +/** 1. Remove default vertical scrollbar in IE 6/7/8/9. 2. Improve readability and alignment in all browsers. */ +textarea { overflow: auto; /* 1 */ vertical-align: top; } + +/* 2 */ +/* ========================================================================== Tables ========================================================================== */ +/** Remove most spacing between table cells. */ +table { border-collapse: collapse; border-spacing: 0; } + +.hljs { display: inline-block; padding: 0.5em; background: white; color: #37474f; } + +.hljs-comment, +.hljs-annotation, +.hljs-template_comment, +.diff .hljs-header, +.hljs-chunk, +.apache .hljs-cbracket { color: #d81b60; } + +.hljs-keyword, +.hljs-id, +.hljs-built_in, +.css .smalltalk .hljs-class, +.hljs-winutils, +.bash .hljs-variable, +.tex .hljs-command, +.hljs-request, +.hljs-status, +.hljs-meta, +.nginx .hljs-title { color: #3b78e7; } + +.xml .hljs-tag { color: #3b78e7; } +.xml .hljs-tag .hljs-value { color: #3b78e7; } + +.hljs-string, +.hljs-title, +.hljs-parent, +.hljs-tag .hljs-value, +.hljs-rules .hljs-value { color: #0d904f; } + +.devsite-dark-code .hljs { display: inline-block; padding: 0.5em; background: white; color: #eceff1; } + +.devsite-dark-code .hljs-comment, +.devsite-dark-code .hljs-annotation, +.devsite-dark-code .hljs-template_comment, +.devsite-dark-code .diff .hljs-header, +.devsite-dark-code .hljs-chunk { color: #f06292; } + +.devsite-dark-code .hljs-keyword, +.devsite-dark-code .hljs-id, +.devsite-dark-code .hljs-built_in, +.devsite-dark-code .hljs-winutils, +.devsite-dark-code .hljs-request, +.devsite-dark-code .hljs-status, +.devsite-dark-code .hljs-meta { color: #4dd0e1; } + +.devsite-dark-code .hljs-string, +.devsite-dark-code .hljs-title, +.devsite-dark-code .hljs-parent, +.devsite-dark-code .hljs-tag .hljs-value, +.devsite-dark-code .hljs-rules .hljs-value { color: #9ccc65; } + +.col > :first-child, .col-1 > :first-child, .col-2 > :first-child, .col-3 > :first-child, .col-4 > :first-child, .col-5 > :first-child, .col-6 > :first-child, .col-7 > :first-child, .col-8 > :first-child, .col-9 > :first-child, .col-10 > :first-child, .col-11 > :first-child, .tsd-panel > :first-child, ul.tsd-descriptions > li > :first-child, .col > :first-child > :first-child, .col-1 > :first-child > :first-child, .col-2 > :first-child > :first-child, .col-3 > :first-child > :first-child, .col-4 > :first-child > :first-child, .col-5 > :first-child > :first-child, .col-6 > :first-child > :first-child, .col-7 > :first-child > :first-child, .col-8 > :first-child > :first-child, .col-9 > :first-child > :first-child, .col-10 > :first-child > :first-child, .col-11 > :first-child > :first-child, .tsd-panel > :first-child > :first-child, ul.tsd-descriptions > li > :first-child > :first-child, .col > :first-child > :first-child > :first-child, .col-1 > :first-child > :first-child > :first-child, .col-2 > :first-child > :first-child > :first-child, .col-3 > :first-child > :first-child > :first-child, .col-4 > :first-child > :first-child > :first-child, .col-5 > :first-child > :first-child > :first-child, .col-6 > :first-child > :first-child > :first-child, .col-7 > :first-child > :first-child > :first-child, .col-8 > :first-child > :first-child > :first-child, .col-9 > :first-child > :first-child > :first-child, .col-10 > :first-child > :first-child > :first-child, .col-11 > :first-child > :first-child > :first-child, .tsd-panel > :first-child > :first-child > :first-child, ul.tsd-descriptions > li > :first-child > :first-child > :first-child { margin-top: 0; } +.col > :last-child, .col-1 > :last-child, .col-2 > :last-child, .col-3 > :last-child, .col-4 > :last-child, .col-5 > :last-child, .col-6 > :last-child, .col-7 > :last-child, .col-8 > :last-child, .col-9 > :last-child, .col-10 > :last-child, .col-11 > :last-child, .tsd-panel > :last-child, ul.tsd-descriptions > li > :last-child, .col > :last-child > :last-child, .col-1 > :last-child > :last-child, .col-2 > :last-child > :last-child, .col-3 > :last-child > :last-child, .col-4 > :last-child > :last-child, .col-5 > :last-child > :last-child, .col-6 > :last-child > :last-child, .col-7 > :last-child > :last-child, .col-8 > :last-child > :last-child, .col-9 > :last-child > :last-child, .col-10 > :last-child > :last-child, .col-11 > :last-child > :last-child, .tsd-panel > :last-child > :last-child, ul.tsd-descriptions > li > :last-child > :last-child, .col > :last-child > :last-child > :last-child, .col-1 > :last-child > :last-child > :last-child, .col-2 > :last-child > :last-child > :last-child, .col-3 > :last-child > :last-child > :last-child, .col-4 > :last-child > :last-child > :last-child, .col-5 > :last-child > :last-child > :last-child, .col-6 > :last-child > :last-child > :last-child, .col-7 > :last-child > :last-child > :last-child, .col-8 > :last-child > :last-child > :last-child, .col-9 > :last-child > :last-child > :last-child, .col-10 > :last-child > :last-child > :last-child, .col-11 > :last-child > :last-child > :last-child, .tsd-panel > :last-child > :last-child > :last-child, ul.tsd-descriptions > li > :last-child > :last-child > :last-child { margin-bottom: 0; } + +.container { max-width: 1200px; margin: 0 auto; padding: 0 40px; } +@media (max-width: 640px) { .container { padding: 0 20px; } } + +.container-main { padding-bottom: 200px; } + +.row { position: relative; margin: 0 -10px; } +.row:after { visibility: hidden; display: block; content: ""; clear: both; height: 0; } + +.col, .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11 { box-sizing: border-box; float: left; padding: 0 10px; } + +.col-1 { width: 8.33333%; } + +.offset-1 { margin-left: 8.33333%; } + +.col-2 { width: 16.66667%; } + +.offset-2 { margin-left: 16.66667%; } + +.col-3 { width: 25%; } + +.offset-3 { margin-left: 25%; } + +.col-4 { width: 33.33333%; } + +.offset-4 { margin-left: 33.33333%; } + +.col-5 { width: 41.66667%; } + +.offset-5 { margin-left: 41.66667%; } + +.col-6 { width: 50%; } + +.offset-6 { margin-left: 50%; } + +.col-7 { width: 58.33333%; } + +.offset-7 { margin-left: 58.33333%; } + +.col-8 { width: 66.66667%; } + +.offset-8 { margin-left: 66.66667%; } + +.col-9 { width: 75%; } + +.offset-9 { margin-left: 75%; } + +.col-10 { width: 83.33333%; } + +.offset-10 { margin-left: 83.33333%; } + +.col-11 { width: 91.66667%; } + +.offset-11 { margin-left: 91.66667%; } + +.tsd-kind-icon { display: block; position: relative; padding-left: 20px; text-indent: -20px; } + +.no-transition { transition: none !important; } + +@-webkit-keyframes fade-in { from { opacity: 0; } + to { opacity: 1; } } + +@keyframes fade-in { from { opacity: 0; } + to { opacity: 1; } } +@-webkit-keyframes fade-out { from { opacity: 1; visibility: visible; } + to { opacity: 0; } } +@keyframes fade-out { from { opacity: 1; visibility: visible; } + to { opacity: 0; } } +@-webkit-keyframes fade-in-delayed { 0% { opacity: 0; } + 33% { opacity: 0; } + 100% { opacity: 1; } } +@keyframes fade-in-delayed { 0% { opacity: 0; } + 33% { opacity: 0; } + 100% { opacity: 1; } } +@-webkit-keyframes fade-out-delayed { 0% { opacity: 1; visibility: visible; } + 66% { opacity: 0; } + 100% { opacity: 0; } } +@keyframes fade-out-delayed { 0% { opacity: 1; visibility: visible; } + 66% { opacity: 0; } + 100% { opacity: 0; } } +@-webkit-keyframes shift-to-left { from { -webkit-transform: translate(0, 0); transform: translate(0, 0); } + to { -webkit-transform: translate(-25%, 0); transform: translate(-25%, 0); } } +@keyframes shift-to-left { from { -webkit-transform: translate(0, 0); transform: translate(0, 0); } + to { -webkit-transform: translate(-25%, 0); transform: translate(-25%, 0); } } +@-webkit-keyframes unshift-to-left { from { -webkit-transform: translate(-25%, 0); transform: translate(-25%, 0); } + to { -webkit-transform: translate(0, 0); transform: translate(0, 0); } } +@keyframes unshift-to-left { from { -webkit-transform: translate(-25%, 0); transform: translate(-25%, 0); } + to { -webkit-transform: translate(0, 0); transform: translate(0, 0); } } +@-webkit-keyframes pop-in-from-right { from { -webkit-transform: translate(100%, 0); transform: translate(100%, 0); } + to { -webkit-transform: translate(0, 0); transform: translate(0, 0); } } +@keyframes pop-in-from-right { from { -webkit-transform: translate(100%, 0); transform: translate(100%, 0); } + to { -webkit-transform: translate(0, 0); transform: translate(0, 0); } } +@-webkit-keyframes pop-out-to-right { from { -webkit-transform: translate(0, 0); transform: translate(0, 0); visibility: visible; } + to { -webkit-transform: translate(100%, 0); transform: translate(100%, 0); } } +@keyframes pop-out-to-right { from { -webkit-transform: translate(0, 0); transform: translate(0, 0); visibility: visible; } + to { -webkit-transform: translate(100%, 0); transform: translate(100%, 0); } } + +a { color: #4da6ff; text-decoration: none; } +a:hover { text-decoration: underline; } + +pre { padding: 10px; } +pre code { padding: 0; font-size: 100%; background-color: transparent; } + +.tsd-typography ul { list-style: square; padding: 0 0 0 20px; margin: 0; } +.tsd-typography h4, .tsd-typography .tsd-index-panel h3, .tsd-index-panel .tsd-typography h3, .tsd-typography h5, .tsd-typography h6 { font-size: 1em; margin: 0; } +.tsd-typography h5, .tsd-typography h6 { font-weight: normal; } +.tsd-typography p, .tsd-typography ul, .tsd-typography ol { margin: 1em 0; } + +@media (min-width: 901px) and (max-width: 1024px) { html.default .col-content { width: 72%; } + html.default .col-menu { width: 28%; } + html.default .tsd-navigation { padding-left: 10px; } } +@media (max-width: 900px) { html.default .col-content { float: none; width: 100%; } + html.default .col-menu { position: fixed !important; overflow: auto; -webkit-overflow-scrolling: touch; overflow-scrolling: touch; z-index: 1024; top: 0 !important; bottom: 0 !important; left: auto !important; right: 0 !important; width: 100%; padding: 20px 20px 0 0; max-width: 450px; visibility: hidden; background-color: #fff; -webkit-transform: translate(100%, 0); transform: translate(100%, 0); } + html.default .col-menu > *:last-child { padding-bottom: 20px; } + html.default .overlay { content: ""; display: block; position: fixed; z-index: 1023; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.75); visibility: hidden; } + html.default.to-has-menu .overlay { -webkit-animation: fade-in 0.4s; animation: fade-in 0.4s; } + html.default.to-has-menu header, html.default.to-has-menu footer, html.default.to-has-menu .col-content { -webkit-animation: shift-to-left 0.4s; animation: shift-to-left 0.4s; } + html.default.to-has-menu .col-menu { -webkit-animation: pop-in-from-right 0.4s; animation: pop-in-from-right 0.4s; } + html.default.from-has-menu .overlay { -webkit-animation: fade-out 0.4s; animation: fade-out 0.4s; } + html.default.from-has-menu header, html.default.from-has-menu footer, html.default.from-has-menu .col-content { -webkit-animation: unshift-to-left 0.4s; animation: unshift-to-left 0.4s; } + html.default.from-has-menu .col-menu { -webkit-animation: pop-out-to-right 0.4s; animation: pop-out-to-right 0.4s; } + html.default.has-menu body { overflow: hidden; } + html.default.has-menu .overlay { visibility: visible; } + html.default.has-menu header, html.default.has-menu footer, html.default.has-menu .col-content { -webkit-transform: translate(-25%, 0); transform: translate(-25%, 0); } + html.default.has-menu .col-menu { visibility: visible; -webkit-transform: translate(0, 0); transform: translate(0, 0); } } + +.tsd-page-title { padding: 0; margin: 0; background: #fff; } +.tsd-page-title h1 { font-weight: normal; margin: 0; } + +.tsd-breadcrumb { margin: 0; padding: 0; color: #808080; } +.tsd-breadcrumb a { color: #808080; text-decoration: none; } +.tsd-breadcrumb a:hover { text-decoration: underline; } +.tsd-breadcrumb li { display: inline; margin-right: -0.25em; } + +html.minimal .container { margin: 0; } +html.minimal .container-main { padding-top: 50px; padding-bottom: 0; } +html.minimal .content-wrap { padding-left: 300px; } +html.minimal .tsd-navigation { position: fixed !important; overflow: auto; -webkit-overflow-scrolling: touch; overflow-scrolling: touch; box-sizing: border-box; z-index: 1; left: 0; top: 40px; bottom: 0; width: 300px; padding: 20px; margin: 0; } +html.minimal .tsd-member .tsd-member { margin-left: 0; } +html.minimal .tsd-page-toolbar { position: fixed; z-index: 2; } +html.minimal #tsd-filter .tsd-filter-group { right: 0; -webkit-transform: none; transform: none; } +html.minimal footer { background-color: transparent; } +html.minimal footer .container { padding: 0; } +html.minimal .tsd-generator { padding: 0; } +@media (max-width: 900px) { html.minimal .tsd-navigation { display: none; } + html.minimal .content-wrap { padding-left: 0; } } + +dl.tsd-comment-tags { overflow: hidden; } +dl.tsd-comment-tags dt { clear: both; float: left; padding: 1px 5px; margin: 0 10px 0 0; border-radius: 4px; border: 1px solid #808080; color: #808080; font-size: 0.8em; font-weight: normal; } +dl.tsd-comment-tags dd { margin: 0 0 10px 0; } +dl.tsd-comment-tags p { margin: 0; } + +.tsd-panel.tsd-comment .lead { font-size: 1.1em; line-height: 1.333em; margin-bottom: 2em; } +.tsd-panel.tsd-comment .lead:last-child { margin-bottom: 0; } + +.toggle-protected .tsd-is-private { display: none; } + +.toggle-public .tsd-is-private, .toggle-public .tsd-is-protected, .toggle-public .tsd-is-private-protected { display: none; } + +.toggle-inherited .tsd-is-inherited { display: none; } + +.toggle-only-exported .tsd-is-not-exported { display: none; } + +.toggle-externals .tsd-is-external { display: none; } + +#tsd-filter { position: relative; display: inline-block; height: 40px; vertical-align: bottom; } +.no-filter #tsd-filter { display: none; } +#tsd-filter .tsd-filter-group { display: inline-block; height: 40px; vertical-align: bottom; white-space: nowrap; } +#tsd-filter input { display: none; } +@media (max-width: 900px) { #tsd-filter .tsd-filter-group { display: block; position: absolute; top: 40px; right: 20px; height: auto; background-color: #fff; visibility: hidden; -webkit-transform: translate(50%, 0); transform: translate(50%, 0); box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); } + .has-options #tsd-filter .tsd-filter-group { visibility: visible; } + .to-has-options #tsd-filter .tsd-filter-group { -webkit-animation: fade-in 0.2s; animation: fade-in 0.2s; } + .from-has-options #tsd-filter .tsd-filter-group { -webkit-animation: fade-out 0.2s; animation: fade-out 0.2s; } + #tsd-filter label, #tsd-filter .tsd-select { display: block; padding-right: 20px; } } + +footer { border-top: 1px solid #eee; background-color: #fff; } +footer.with-border-bottom { border-bottom: 1px solid #eee; } +footer .tsd-legend-group { font-size: 0; } +footer .tsd-legend { display: inline-block; width: 25%; padding: 0; font-size: 16px; list-style: none; line-height: 1.333em; vertical-align: top; } +@media (max-width: 900px) { footer .tsd-legend { width: 50%; } } + +.tsd-hierarchy { list-style: square; padding: 0 0 0 20px; margin: 0; } +.tsd-hierarchy .target { font-weight: bold; } + +.tsd-index-panel .tsd-index-content { margin-bottom: -30px !important; } +.tsd-index-panel .tsd-index-section { margin-bottom: 30px !important; } +.tsd-index-panel h3 { margin: 0 -20px 10px -20px; padding: 0 20px 10px 20px; border-bottom: 1px solid #eee; } +.tsd-index-panel ul.tsd-index-list { -webkit-column-count: 3; -moz-column-count: 3; -ms-column-count: 3; -o-column-count: 3; column-count: 3; -webkit-column-gap: 20px; -moz-column-gap: 20px; -ms-column-gap: 20px; -o-column-gap: 20px; column-gap: 20px; padding: 0; list-style: none; line-height: 1.333em; } +@media (max-width: 900px) { .tsd-index-panel ul.tsd-index-list { -webkit-column-count: 1; -moz-column-count: 1; -ms-column-count: 1; -o-column-count: 1; column-count: 1; } } +@media (min-width: 901px) and (max-width: 1024px) { .tsd-index-panel ul.tsd-index-list { -webkit-column-count: 2; -moz-column-count: 2; -ms-column-count: 2; -o-column-count: 2; column-count: 2; } } +.tsd-index-panel ul.tsd-index-list li { -webkit-column-break-inside: avoid; -moz-column-break-inside: avoid; -ms-column-break-inside: avoid; -o-column-break-inside: avoid; column-break-inside: avoid; -webkit-page-break-inside: avoid; -moz-page-break-inside: avoid; -ms-page-break-inside: avoid; -o-page-break-inside: avoid; page-break-inside: avoid; } + +.tsd-flag { display: inline-block; padding: 1px 5px; border-radius: 4px; color: #fff; background-color: #808080; text-indent: 0; font-size: 14px; font-weight: normal; line-height: 1.5em; } + +.tsd-anchor { position: absolute; top: -100px; } + +.tsd-member { position: relative; } +.tsd-member .tsd-anchor + h3 { margin-top: 0; margin-bottom: 0; border-bottom: none; } + +.tsd-navigation { padding: 0 0 0 40px; } +.tsd-navigation a { display: block; padding-top: 2px; padding-bottom: 2px; border-left: 2px solid transparent; color: #222; text-decoration: none; transition: border-left-color 0.1s; } +.tsd-navigation a:hover { text-decoration: underline; } +.tsd-navigation ul { margin: 0; padding: 0; list-style: none; } +.tsd-navigation li { padding: 0; } + +.tsd-navigation.primary { padding-bottom: 40px; } +.tsd-navigation.primary a { display: block; padding-top: 6px; padding-bottom: 6px; } +.tsd-navigation.primary ul li a { padding-left: 5px; } +.tsd-navigation.primary ul li li a { padding-left: 25px; } +.tsd-navigation.primary ul li li li a { padding-left: 45px; } +.tsd-navigation.primary ul li li li li a { padding-left: 65px; } +.tsd-navigation.primary ul li li li li li a { padding-left: 85px; } +.tsd-navigation.primary ul li li li li li li a { padding-left: 105px; } +.tsd-navigation.primary > ul { border-bottom: 1px solid #eee; } +.tsd-navigation.primary li { border-top: 1px solid #eee; } +.tsd-navigation.primary li.current > a { font-weight: bold; } +.tsd-navigation.primary li.label span { display: block; padding: 20px 0 6px 5px; color: #808080; } +.tsd-navigation.primary li.globals + li > span, .tsd-navigation.primary li.globals + li > a { padding-top: 20px; } + +.tsd-navigation.secondary ul { transition: opacity 0.2s; } +.tsd-navigation.secondary ul li a { padding-left: 25px; } +.tsd-navigation.secondary ul li li a { padding-left: 45px; } +.tsd-navigation.secondary ul li li li a { padding-left: 65px; } +.tsd-navigation.secondary ul li li li li a { padding-left: 85px; } +.tsd-navigation.secondary ul li li li li li a { padding-left: 105px; } +.tsd-navigation.secondary ul li li li li li li a { padding-left: 125px; } +.tsd-navigation.secondary ul.current a { border-left-color: #eee; } +.tsd-navigation.secondary li.focus > a, .tsd-navigation.secondary ul.current li.focus > a { border-left-color: #000; } +.tsd-navigation.secondary li.current { margin-top: 20px; margin-bottom: 20px; border-left-color: #eee; } +.tsd-navigation.secondary li.current > a { font-weight: bold; } + +@media (min-width: 901px) { .menu-sticky-wrap { position: static; } + .no-csspositionsticky .menu-sticky-wrap.sticky { position: fixed; } + .no-csspositionsticky .menu-sticky-wrap.sticky-current { position: fixed; } + .no-csspositionsticky .menu-sticky-wrap.sticky-current ul.before-current, .no-csspositionsticky .menu-sticky-wrap.sticky-current ul.after-current { opacity: 0; } + .no-csspositionsticky .menu-sticky-wrap.sticky-bottom { position: absolute; top: auto !important; left: auto !important; bottom: 0; right: 0; } + .csspositionsticky .menu-sticky-wrap.sticky { position: -webkit-sticky; position: sticky; } + .csspositionsticky .menu-sticky-wrap.sticky-current { position: -webkit-sticky; position: sticky; } } + +.tsd-panel { margin: 20px 0; padding: 20px; background-color: #fff; } +.tsd-panel:empty { display: none; } +.tsd-panel > h1, .tsd-panel > h2, .tsd-panel > h3 { margin: 1.5em -20px 10px -20px; padding: 0 20px 10px 20px; border-bottom: 1px solid #ebebeb; } +.tsd-panel > h1.tsd-before-signature, .tsd-panel > h2.tsd-before-signature, .tsd-panel > h3.tsd-before-signature { margin-bottom: 0; border-bottom: 0; } +.tsd-panel table { display: block; width: 100%; overflow: auto; margin-top: 10px; word-break: normal; word-break: keep-all; } +.tsd-panel table th { font-weight: bold; } +.tsd-panel table th, .tsd-panel table td { padding: 6px 13px; border: 1px solid #ddd; } +.tsd-panel table tr { background-color: #fff; border-top: 1px solid #ccc; } +.tsd-panel table tr:nth-child(2n) { background-color: #f8f8f8; } + +.tsd-panel-group { margin: 60px 0; } +.tsd-panel-group > h1, .tsd-panel-group > h2, .tsd-panel-group > h3 { padding-left: 20px; padding-right: 20px; } + +#tsd-search { transition: background-color 0.2s; } +#tsd-search .title { position: relative; z-index: 2; } +#tsd-search .field { position: absolute; left: 0; top: 0; right: 40px; height: 40px; } +#tsd-search .field input { box-sizing: border-box; position: relative; top: -50px; z-index: 1; width: 100%; padding: 0 10px; opacity: 0; outline: 0; border: 0; background: transparent; color: #222; } +#tsd-search .field label { position: absolute; overflow: hidden; right: -40px; } +#tsd-search .field input, #tsd-search .title { transition: opacity 0.2s; } +#tsd-search .results { position: absolute; visibility: hidden; top: 40px; width: 100%; margin: 0; padding: 0; list-style: none; box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); } +#tsd-search .results li { padding: 0 10px; background-color: #fdfdfd; } +#tsd-search .results li:nth-child(even) { background-color: #fff; } +#tsd-search .results li.state { display: none; } +#tsd-search .results li.current, #tsd-search .results li:hover { background-color: #eee; } +#tsd-search .results a { display: block; } +#tsd-search .results a:before { top: 10px; } +#tsd-search .results span.parent { color: #808080; font-weight: normal; } +#tsd-search.has-focus { background-color: #eee; } +#tsd-search.has-focus .field input { top: 0; opacity: 1; } +#tsd-search.has-focus .title { z-index: 0; opacity: 0; } +#tsd-search.has-focus .results { visibility: visible; } +#tsd-search.loading .results li.state.loading { display: block; } +#tsd-search.failure .results li.state.failure { display: block; } + +.tsd-signature { margin: 0 0 1em 0; padding: 10px; border: 1px solid #eee; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.tsd-signature.tsd-kind-icon { padding-left: 30px; } +.tsd-panel > .tsd-signature { margin-left: -20px; margin-right: -20px; border-width: 1px 0; } +.tsd-panel > .tsd-signature.tsd-kind-icon { padding-left: 40px; } + +.tsd-signature-symbol { color: #808080; font-weight: normal; } + +.tsd-signature-type { font-style: italic; font-weight: normal; } + +.tsd-signatures { padding: 0; margin: 0 0 1em 0; border: 1px solid #eee; } +.tsd-signatures .tsd-signature { margin: 0; border-width: 1px 0 0 0; transition: background-color 0.1s; } +.tsd-signatures .tsd-signature:first-child { border-top-width: 0; } +.tsd-signatures .tsd-signature.current { background-color: #eee; } +.tsd-signatures.active > .tsd-signature { cursor: pointer; } +.tsd-panel > .tsd-signatures { margin-left: -20px; margin-right: -20px; border-width: 1px 0; } +.tsd-panel > .tsd-signatures .tsd-signature.tsd-kind-icon { padding-left: 40px; } +.tsd-panel > a.anchor + .tsd-signatures { border-top-width: 0; margin-top: -20px; } + +ul.tsd-descriptions { position: relative; overflow: hidden; transition: height 0.3s; padding: 0; list-style: none; } +ul.tsd-descriptions.active > .tsd-description { display: none; } +ul.tsd-descriptions.active > .tsd-description.current { display: block; } +ul.tsd-descriptions.active > .tsd-description.fade-in { -webkit-animation: fade-in-delayed 0.3s; animation: fade-in-delayed 0.3s; } +ul.tsd-descriptions.active > .tsd-description.fade-out { -webkit-animation: fade-out-delayed 0.3s; animation: fade-out-delayed 0.3s; position: absolute; display: block; top: 0; left: 0; right: 0; opacity: 0; visibility: hidden; } +ul.tsd-descriptions h4, ul.tsd-descriptions .tsd-index-panel h3, .tsd-index-panel ul.tsd-descriptions h3 { font-size: 16px; margin: 1em 0 0.5em 0; } + +ul.tsd-parameters, ul.tsd-type-parameters { list-style: square; margin: 0; padding-left: 20px; } +ul.tsd-parameters > li.tsd-parameter-siganture, ul.tsd-type-parameters > li.tsd-parameter-siganture { list-style: none; margin-left: -20px; } +ul.tsd-parameters h5, ul.tsd-type-parameters h5 { font-size: 16px; margin: 1em 0 0.5em 0; } +ul.tsd-parameters .tsd-comment, ul.tsd-type-parameters .tsd-comment { margin-top: -0.5em; } + +.tsd-sources { font-size: 14px; color: #808080; } +.tsd-sources a { color: #808080; text-decoration: underline; } +.tsd-sources ul, .tsd-sources p { margin: 0 !important; } +.tsd-sources ul { list-style: none; padding: 0; } + +.tsd-page-toolbar { position: absolute; z-index: 1; top: 0; left: 0; width: 100%; height: 40px; color: #333; background: #fff; border-bottom: 1px solid #eee; } +.tsd-page-toolbar a { color: #333; text-decoration: none; } +.tsd-page-toolbar a.title { font-weight: bold; } +.tsd-page-toolbar a.title:hover { text-decoration: underline; } +.tsd-page-toolbar .table-wrap { display: table; width: 100%; height: 40px; } +.tsd-page-toolbar .table-cell { display: table-cell; position: relative; white-space: nowrap; line-height: 40px; } +.tsd-page-toolbar .table-cell:first-child { width: 100%; } + +.tsd-widget:before, .tsd-select .tsd-select-label:before, .tsd-select .tsd-select-list li:before { content: ""; display: inline-block; width: 40px; height: 40px; margin: 0 -8px 0 0; background-image: url(../images/widgets.png); background-repeat: no-repeat; text-indent: -1024px; vertical-align: bottom; } +@media (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { .tsd-widget:before, .tsd-select .tsd-select-label:before, .tsd-select .tsd-select-list li:before { background-image: url(../images/widgets@2x.png); background-size: 320px 40px; } } + +.tsd-widget { display: inline-block; overflow: hidden; opacity: 0.6; height: 40px; transition: opacity 0.1s, background-color 0.2s; vertical-align: bottom; cursor: pointer; } +.tsd-widget:hover { opacity: 0.8; } +.tsd-widget.active { opacity: 1; background-color: #eee; } +.tsd-widget.no-caption { width: 40px; } +.tsd-widget.no-caption:before { margin: 0; } +.tsd-widget.search:before { background-position: 0 0; } +.tsd-widget.menu:before { background-position: -40px 0; } +.tsd-widget.options:before { background-position: -80px 0; } +.tsd-widget.options, .tsd-widget.menu { display: none; } +@media (max-width: 900px) { .tsd-widget.options, .tsd-widget.menu { display: inline-block; } } +input[type=checkbox] + .tsd-widget:before { background-position: -120px 0; } +input[type=checkbox]:checked + .tsd-widget:before { background-position: -160px 0; } + +.tsd-select { position: relative; display: inline-block; height: 40px; transition: opacity 0.1s, background-color 0.2s; vertical-align: bottom; cursor: pointer; } +.tsd-select .tsd-select-label { opacity: 0.6; transition: opacity 0.2s; } +.tsd-select .tsd-select-label:before { background-position: -240px 0; } +.tsd-select.active .tsd-select-label { opacity: 0.8; } +.tsd-select.active .tsd-select-list { visibility: visible; opacity: 1; transition-delay: 0s; } +.tsd-select .tsd-select-list { position: absolute; visibility: hidden; top: 40px; left: 0; margin: 0; padding: 0; opacity: 0; list-style: none; box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); transition: visibility 0s 0.2s, opacity 0.2s; } +.tsd-select .tsd-select-list li { padding: 0 20px 0 0; background-color: #fdfdfd; } +.tsd-select .tsd-select-list li:before { background-position: 40px 0; } +.tsd-select .tsd-select-list li:nth-child(even) { background-color: #fff; } +.tsd-select .tsd-select-list li:hover { background-color: #eee; } +.tsd-select .tsd-select-list li.selected:before { background-position: -200px 0; } +@media (max-width: 900px) { .tsd-select .tsd-select-list { top: 0; left: auto; right: 100%; margin-right: -5px; } + .tsd-select .tsd-select-label:before { background-position: -280px 0; } } + +img { max-width: 100%; } diff --git a/docgen/theme/assets/images/lockup.png b/docgen/theme/assets/images/lockup.png new file mode 100644 index 0000000000000000000000000000000000000000..f4cdcf24a42787815f45a0fbd6b2211b69cf87a0 GIT binary patch literal 2646 zcmV-c3aRypP)qsBRaHO8(yazJJDcK~r7o->-O8-CmCY zr#`z4j)yXEeD5C+QGXbQp&O3{6EN^efRRlACm;I?A{d6LFLdL<9qEFg1D^mmd|wt3 z48znHx^b-gd_m5Ej{^+djaf9@j|hfg>IdC8+I~|`$@3orIJi9vC-!?0!7xmHsN8V! zv28Hov*eTv-U-;2So2OZ3=0|#S#nA~dXus49GuwaMTGk>3{$z`=tGHBaz6JF{69*I z)m=oa;&>=lL@*3f8!9&(+L0=gv&e@5(p@9==0-nJs{ngs~{)x zp#rjiWM$D%LcX>j=QGVWUw`b;1R|7N`%VaHLxhP)foe!;#45B0#QKRaeSRFSMqw#s z_*1l;&$dcr7ER}m?)K(!%3GAvBHfXLW4pYFAi4U@6rA4$aQRDFM3{(Mf)edf7Sv9# zpdncdqUDr$AwRqk7RG|5btEGoq;p#flcV-Ai!(uUA- zzV&HY!Zg%^);U;E4oG)otI3(i%sBV^f}BN~VDfkPXa8x&ENaOqkr{OE>chA}1mC5O%rG5Hw&F!B+JzqA`xGNCb%!$EN$Q4uA33 z(XwMkkzV8ugbeq`90Lw)O;B>?pTZ3J>xyzB4Fx8CiK70k$%&j6*CPFhFbOR|^)rx< zc#V&^K&j;#57H~!91unBaM1rGBI6Du=HN401reI(5O?si3RvT*q>t*eyJKz%nyw_L zWZ_@c<(x`xfQcVA!0d@foXUyx09@RYK!iz{CMfY|<)V1XE)-fA^|9`-+5adKsPSj$ zpMyFxMO^5!OJj}!!)=*zIj!uekn;x(Fn{RbTavRVXodw<3aa{>K7v?LoMvi}L|Se1 zKaM0_xhVC|!A~5w*M)I$am+DbWaDZ%pDtuiLQbTZIkn`JTzE8#2$L{LP(s#T#E%4! zNZExRq^4;Vgt(*11sf&%Pm?Sj&sRbKiPGm{WqzCI;4eFV7)jbZc59V>1j>#ZM3U-p zae2()c&H64xR#tnmYs7u0NE@2~W%HNyu5NX>BV>XJDvF6>Ps!W0Z$W>G}|`LzoKHKvGmvqrMY z^fGL7y)sT@qgEp0QST30!W%H=FtRCEdOs&-Pq#8B&G|>pBf=C66!Z?e$993Ry8o_> z164$EiqGM3%Xbq6k=8XEa~S;OikwT2qnJM>mQvi3)4)oufr5&4FR1z)J^FI01*LaB zDGAqo%;DIskh}*}$|-LdoBNYXODWZOv-{?r+^gT%h6n?=odgZn0AF1MwZcPAL#>@l zzA^$xgcOvHfJ0GZbscjQv|q|8SxU+5Ni3zfDQ6KB<0~v^%?0%!`cdC0Zotv@J@rNe zpUw9LkX{?Y_SS98VRUoC%ARCF$bC5_mp-3BgaNEqL4BG_2z0n#B#JAZ+Z1%Lxn942 z>g?KDmobOYjVt!IC`W|`CC-3Ky=FaZ+<)wr1MaG2TRNRXJSxwKKin!aj0mX!ML>Z6x! zP%w4)6n%=hHs&yJ2Q2-su9=gd3qSWEi7k(u7e9;m}+`GAdCKv3g8 zOdlb?-5U_1Hm3FQnvXdg35FK_)3ElL(?SXwVnH~LV?Ou4&Xf_E(?YM3%T-)@3Z*Zn>X9eU8T|*y%r$VZYzD1yw$T zjd*IJ$Bi+sc-;4fd%QQ&J*nB#TOj}D+R2%Jy$fbecEI(4O)12Sh}yywFPb0{EfXn* z^wAr_)j>SYBU+M4;>4zNonu;$#nkV&zsm6=8XFJDF?TW^D5}3Lm-c7oGF$2{=j`bY z$PI7K;6>ENA~B4`>OUOn4&4~@XBS`Uab3>&bDeNwwEg_`fz2UAFbwlS=*FS0{)xlg zx#ib-YA)x}%bhTBusw^P)35F%!?2)~q~7a8or%e#-IMt@^yOTBwF{<>cI5Eb3B;@J zAj7brN>I|a+-PTN=2Vw+IjsVa{GOR&F&RBrP@2IG{C?Vrg`{rVa?YLYEEa$^xAB0x z3B!U~qJCxKP$w3W2r-ex7r6lBIt&Y{5H&G*q$9WdswL*q%UxUmVvK+Vl?=9fk;KHo zj*Ns0K<>k^pbW#XpbW#XpbW!;G7Q6lG7JmKFpLSj52E7_O`Jf{)c^nh07*qoM6N<$ Eg8U-`K>z>% literal 0 HcmV?d00001 diff --git a/docgen/theme/layouts/default.hbs b/docgen/theme/layouts/default.hbs new file mode 100644 index 000000000..72111fb4e --- /dev/null +++ b/docgen/theme/layouts/default.hbs @@ -0,0 +1,33 @@ + + + + + + + + + + {{#ifCond model.name '==' project.name}}{{project.name}}{{else}}{{model.name}} | {{project.name}}{{/ifCond}} + + + + + + + +{{> header}} + +
+
+
+ {{{contents}}} +
+
+
+ +
+ +{{> analytics}} + + + diff --git a/docgen/theme/partials/breadcrumb.hbs b/docgen/theme/partials/breadcrumb.hbs new file mode 100644 index 000000000..db115163f --- /dev/null +++ b/docgen/theme/partials/breadcrumb.hbs @@ -0,0 +1,11 @@ + +{{#if parent}} + {{#with parent}}{{> breadcrumb}}{{/with}} + +{{/if}} \ No newline at end of file diff --git a/docgen/theme/partials/comment.hbs b/docgen/theme/partials/comment.hbs new file mode 100644 index 000000000..f92e54301 --- /dev/null +++ b/docgen/theme/partials/comment.hbs @@ -0,0 +1,22 @@ +{{#with comment}} + {{#if hasVisibleComponent}} +
+ {{#if shortText}} +
+ {{#markdown}}{{{shortText}}}{{/markdown}} +
+ {{/if}} + {{#if text}} + {{#markdown}}{{{text}}}{{/markdown}} + {{/if}} + {{#if tags}} +
+ {{#each tags}} +
{{tagName}}
+
{{#markdown}}{{{text}}}{{/markdown}}
+ {{/each}} +
+ {{/if}} +
+ {{/if}} +{{/with}} \ No newline at end of file diff --git a/docgen/theme/partials/header.hbs b/docgen/theme/partials/header.hbs new file mode 100644 index 000000000..4aee65a6b --- /dev/null +++ b/docgen/theme/partials/header.hbs @@ -0,0 +1,23 @@ +
+
+
+

+ {{#ifCond model.name '==' project.name}} + {{else}} +
    + {{#with model.parent}}{{> breadcrumb}}{{/with}} +
  • {{model.name}}
  • + {{#if model.typeParameters}} + < + {{#each model.typeParameters}} + {{#if @index}}, {{/if}} + {{name}} + {{/each}} + > + {{/if}} +
+ {{/ifCond}} +

+
+
+
\ No newline at end of file diff --git a/docgen/theme/partials/member.sources.hbs b/docgen/theme/partials/member.sources.hbs new file mode 100644 index 000000000..5a0e186f0 --- /dev/null +++ b/docgen/theme/partials/member.sources.hbs @@ -0,0 +1,15 @@ +{{#if implementationOf}} + +{{/if}} +{{#if inheritedFrom}} + +{{/if}} +{{#if overwrites}} + +{{/if}} \ No newline at end of file diff --git a/docgen/theme/partials/navigation.hbs b/docgen/theme/partials/navigation.hbs new file mode 100644 index 000000000..54704739a --- /dev/null +++ b/docgen/theme/partials/navigation.hbs @@ -0,0 +1,22 @@ +{{#if isVisible}} + {{#if isLabel}} +
  • + {{{wbr title}}} +
  • + {{else}} + {{#unless isGlobals}} +
  • + {{{wbr title}}} + {{#if isInPath}} + {{#if children}} +
      + {{#each children}} + {{> navigation}} + {{/each}} +
    + {{/if}} + {{/if}} +
  • + {{/unless}} + {{/if}} +{{/if}} diff --git a/docgen/theme/templates/reflection.hbs b/docgen/theme/templates/reflection.hbs new file mode 100644 index 000000000..53cd2879a --- /dev/null +++ b/docgen/theme/templates/reflection.hbs @@ -0,0 +1,72 @@ +{{#with model}} + {{#if hasComment}} +
    + {{> comment}} +
    + {{/if}} +{{/with}} + +{{#if model.typeParameters}} +
    +

    Type parameters

    + {{#with model}}{{> typeParameters}}{{/with}} +
    +{{/if}} + +{{#if model.implementedTypes}} +
    +

    Implements

    +
      + {{#each model.implementedTypes}} +
    • {{> type}}
    • + {{/each}} +
    +
    +{{/if}} + +{{#if model.implementedBy}} +
    +

    Implemented by

    +
      + {{#each model.implementedBy}} +
    • {{> type}}
    • + {{/each}} +
    +
    +{{/if}} + +{{#if model.signatures}} +
    +

    Callable

    + {{#with model}}{{> member.signatures}}{{/with}} +
    +{{/if}} + +{{#if model.indexSignature}} +
    +

    Indexable

    +
    {{#compact}} + [ + {{#each model.indexSignature.parameters}} + {{name}}: {{#with type}}{{>type}}{{/with}} + {{/each}} + ]:  + {{#with model.indexSignature.type}}{{>type}}{{/with}} + {{/compact}}
    + + {{#with model.indexSignature}} + {{> comment}} + {{/with}} + + {{#if model.indexSignature.type.declaration}} + {{#with model.indexSignature.type.declaration}} + {{> parameter}} + {{/with}} + {{/if}} +
    +{{/if}} + +{{#with model}} + {{> index}} + {{> members}} +{{/with}} \ No newline at end of file diff --git a/docgen/tsconfig.json b/docgen/tsconfig.json new file mode 100644 index 000000000..3c43903cf --- /dev/null +++ b/docgen/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.json" +} diff --git a/docgen/typedoc.js b/docgen/typedoc.js new file mode 100644 index 000000000..58156ff59 --- /dev/null +++ b/docgen/typedoc.js @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const options = { + // includeDeclarations: true, + excludeExternals: true, + ignoreCompilerErrors: true, + name: 'Firebase Functions SDK', + mode: 'modules', + hideGenerator: true +}; + +module.exports = options; diff --git a/package.json b/package.json index 916c4159f..10f0c33d1 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "format:fix": "prettier --write '**/*.{json,md,ts,yml,yaml}'", "lint": "tslint --config tslint.json --project tsconfig.json ", "lint:fix": "tslint --config tslint.json --fix --project tsconfig.json", - "test": "mocha -r ts-node/register ./spec/index.spec.ts" + "test": "mocha -r ts-node/register ./spec/index.spec.ts", + "apidocs": "node docgen/generate-docs.js" }, "dependencies": { "@types/express": "^4.17.0", @@ -51,10 +52,13 @@ "@types/sinon": "^7.0.13", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", + "child-process-promise": "^2.2.1", "firebase-admin": "^8.2.0", "istanbul": "^0.4.5", + "js-yaml": "^3.13.1", "mocha": "^6.1.4", "mock-require": "^3.0.3", + "mz": "^2.7.0", "nock": "^10.0.6", "prettier": "^1.18.2", "sinon": "^7.3.2", @@ -63,7 +67,9 @@ "tslint-config-prettier": "^1.18.0", "tslint-no-unused-expression-chai": "^0.1.4", "tslint-plugin-prettier": "^2.0.1", - "typescript": "^3.5.2" + "typedoc": "^0.14.2", + "typescript": "^3.5.2", + "yargs": "^13.2.4" }, "peerDependencies": { "firebase-admin": "^8.0.0" diff --git a/src/providers/auth.ts b/src/providers/auth.ts index 722410340..7274399b2 100644 --- a/src/providers/auth.ts +++ b/src/providers/auth.ts @@ -116,6 +116,11 @@ export class UserBuilder { */ export type UserRecord = firebase.auth.UserRecord; +/** + * UserInfo that is part of the UserRecord + */ +export type UserInfo = firebase.auth.UserInfo; + export function userRecordConstructor( wireData: Object ): firebase.auth.UserRecord { From 273c5ff776450742c4a794f0227456cbd7c64cee Mon Sep 17 00:00:00 2001 From: Erik Macik <42585523+esmacik@users.noreply.github.com> Date: Thu, 11 Jul 2019 18:24:44 -0700 Subject: [PATCH 171/705] Add code comments for storage events (#517) * Add code comments for storage events * Fix some comment formatting issues --- src/providers/storage.ts | 179 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 169 insertions(+), 10 deletions(-) diff --git a/src/providers/storage.ts b/src/providers/storage.ts index 93d6b0ed0..3ba98ea72 100644 --- a/src/providers/storage.ts +++ b/src/providers/storage.ts @@ -34,17 +34,22 @@ export const provider = 'google.storage'; export const service = 'storage.googleapis.com'; /** - * The optional bucket function allows you to choose which buckets' events to handle. - * This step can be bypassed by calling object() directly, which will use the default - * Cloud Storage for Firebase bucket. - * @param bucket Name of the Google Cloud Storage bucket to listen to. + * Registers a Cloud Function scoped to a specific storage bucket. + * + * @param bucket Name of the bucket to which this Cloud Function is + * scoped. + * + * @return Storage bucket builder interface. */ export function bucket(bucket?: string) { return _bucketWithOptions({}, bucket); } /** - * Handle events related to Cloud Storage objects. + * Registers a Cloud Function scoped to the default storage bucket for the + * project. + * + * @return Storage object builder interface. */ export function object() { return _objectWithOptions({}); @@ -76,6 +81,11 @@ export function _objectWithOptions(options: DeploymentOptions): ObjectBuilder { return _bucketWithOptions(options).object(); } +/** + * The Google Cloud Storage bucket builder interface. + * + * Access via [`functions.storage.bucket()`](functions.storage#.bucket). + */ export class BucketBuilder { /** @hidden */ constructor( @@ -83,12 +93,22 @@ export class BucketBuilder { private options: DeploymentOptions ) {} - /** Handle events for objects in this bucket. */ + /** + * Event handler which fires every time a Google Cloud Storage change occurs. + * + * @return Storage object builder interface scoped to the specified storage + * bucket. + */ object() { return new ObjectBuilder(this.triggerResource, this.options); } } +/** + * The Google Cloud Storage object builder interface. + * + * Access via [`functions.storage.object()`](functions.storage#.object). + */ export class ObjectBuilder { /** @hidden */ constructor( @@ -104,7 +124,17 @@ export class ObjectBuilder { ); } - /** Respond to archiving of an object, this is only for buckets that enabled object versioning. */ + /** + * Event handler sent only when a bucket has enabled object versioning. + * This event indicates that the live version of an object has become an + * archived version, either because it was archived or because it was + * overwritten by the upload of an object of the same name. + * + * @param handler Event handler which is run every time a Google Cloud Storage + * archival occurs. + * + * @return A Cloud Function which you can export and deploy. + */ onArchive( handler: ( object: ObjectMetadata, @@ -114,7 +144,20 @@ export class ObjectBuilder { return this.onOperation(handler, 'object.archive'); } - /** Respond to the deletion of an object (not to archiving, if object versioning is enabled). */ + /** + * Event handler which fires every time a Google Cloud Storage deletion occurs. + * + * Sent when an object has been permanently deleted. This includes objects + * that are overwritten or are deleted as part of the bucket's lifecycle + * configuration. For buckets with object versioning enabled, this is not + * sent when an object is archived, even if archival occurs + * via the `storage.objects.delete` method. + * + * @param handler Event handler which is run every time a Google Cloud Storage + * deletion occurs. + * + * @return A Cloud Function which you can export and deploy. + */ onDelete( handler: ( object: ObjectMetadata, @@ -124,7 +167,19 @@ export class ObjectBuilder { return this.onOperation(handler, 'object.delete'); } - /** Respond to the successful creation of an object. */ + /** + * Event handler which fires every time a Google Cloud Storage object + * creation occurs. + * + * Sent when a new object (or a new generation of an existing object) + * is successfully created in the bucket. This includes copying or rewriting + * an existing object. A failed upload does not trigger this event. + * + * @param handler Event handler which is run every time a Google Cloud Storage + * object creation occurs. + * + * @return A Cloud Function which you can export and deploy. + */ onFinalize( handler: ( object: ObjectMetadata, @@ -134,7 +189,15 @@ export class ObjectBuilder { return this.onOperation(handler, 'object.finalize'); } - /** Respond to metadata updates of existing objects. */ + /** + * Event handler which fires every time the metadata of an existing object + * changes. + * + * @param handler Event handler which is run every time a Google Cloud Storage + * metadata update occurs. + * + * @return A Cloud Function which you can export and deploy. + */ onMetadataUpdate( handler: ( object: ObjectMetadata, @@ -144,6 +207,7 @@ export class ObjectBuilder { return this.onOperation(handler, 'object.metadataUpdate'); } + /** @hidden */ private onOperation( handler: ( object: ObjectMetadata, @@ -162,30 +226,101 @@ export class ObjectBuilder { } } +/** Interface representing a Google Google Cloud Storage object metadata object. */ export interface ObjectMetadata { + /** The kind of the object, which is always `storage#object`. */ kind: string; + + /** + * The ID of the object, including the bucket name, object name, and + * generation number. + */ id: string; + + /** Storage bucket that contains the object. */ bucket: string; + + /** Storage class of the object. */ storageClass: string; + + /** + * The value of the `Content-Length` header, used to determine the length of + * the object data in bytes. + */ size: string; + + /** The creation time of the object in RFC 3339 format. */ timeCreated: string; + + /** + * The modification time of the object metadata in RFC 3339 format. + */ updated: string; + + /** Link to access the object, assuming you have sufficient permissions. */ selfLink?: string; + + /**The object's name. */ name?: string; + + /** + * Generation version number that changes each time the object is + * overwritten. + */ generation?: string; + + /** The object's content type, also known as the MIME type. */ contentType?: string; + + /** + * Meta-generation version number that changes each time the object's metadata + * is updated. + */ metageneration?: string; + + /** + * The deletion time of the object in RFC 3339 format. Returned + * only if this version of the object has been deleted. + */ timeDeleted?: string; + timeStorageClassUpdated?: string; + + /** + * MD5 hash for the object. All Google Cloud Storage objects + * have a CRC32C hash or MD5 hash. + */ md5Hash?: string; + + /** Media download link. */ mediaLink?: string; + + /** + * Content-Encoding to indicate that an object is compressed + * (for example, with gzip compression) while maintaining its Content-Type. + */ contentEncoding?: string; + + /** + * The value of the `Content-Disposition` header, used to specify presentation + * information about the data being transmitted. + */ contentDisposition?: string; + + /** ISO 639-1 language code of the content. */ contentLanguage?: string; + + /** + * The value of the `Cache-Control` header, used to determine whether Internet + * caches are allowed to cache public data for an object. + */ cacheControl?: string; + + /** User-provided metadata. */ metadata?: { [key: string]: string; }; + acl?: [ { kind?: string; @@ -206,13 +341,37 @@ export interface ObjectMetadata { etag?: string; } ]; + owner?: { entity?: string; entityId?: string; }; + + /** + * The object's CRC32C hash. All Google Cloud Storage objects + * have a CRC32C hash or MD5 hash. + */ crc32c?: string; + + /** + * Specifies the number of originally uploaded objects from which + * a composite object was created. + */ componentCount?: string; + etag?: string; + + /** + * Customer-supplied encryption key. + * + * This object contains the following properties: + * * `encryptionAlgorithm` (`string|undefined`): The encryption algorithm that + * was used. Always contains the value `AES256`. + * * `keySha256` (`string|undefined`): An RFC 4648 base64-encoded string of the + * SHA256 hash of your encryption key. You can use this SHA256 hash to + * uniquely identify the AES-256 encryption key required to decrypt the + * object, which you must store securely. + */ customerEncryption?: { encryptionAlgorithm?: string; keySha256?: string; From 1f6499c87a596393dd721fa36a1397cfac81e454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Natan=20S=C4=85gol?= Date: Fri, 12 Jul 2019 18:36:10 +0200 Subject: [PATCH 172/705] Introduce Mocha configuration file (#515) * Introduce mocha configuration file * Reformat * Remove an empty file --- .mocharc.yaml | 10 ++++++++++ mocha/setup.ts | 7 +++++++ package.json | 8 +++++--- spec/index.spec.ts | 47 ---------------------------------------------- 4 files changed, 22 insertions(+), 50 deletions(-) create mode 100644 .mocharc.yaml create mode 100644 mocha/setup.ts delete mode 100644 spec/index.spec.ts diff --git a/.mocharc.yaml b/.mocharc.yaml new file mode 100644 index 000000000..932144124 --- /dev/null +++ b/.mocharc.yaml @@ -0,0 +1,10 @@ +exit: true +extension: + - ts +file: + - mocha/setup.ts +package: ./package.json +reporter: spec +require: + - 'ts-node/register' +spec: spec/**/*.spec.ts diff --git a/mocha/setup.ts b/mocha/setup.ts new file mode 100644 index 000000000..ba4d08621 --- /dev/null +++ b/mocha/setup.ts @@ -0,0 +1,7 @@ +import * as chai from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as nock from 'nock'; + +chai.use(chaiAsPromised); + +nock.disableNetConnect(); diff --git a/package.json b/package.json index 10f0c33d1..76ea87fb5 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,13 @@ }, "license": "MIT", "author": "Firebase Team", - "files": ["lib"], + "files": [ + "lib" + ], "main": "lib/index.js", "types": "lib/index.d.ts", "scripts": { + "apidocs": "node docgen/generate-docs.js", "build:pack": "rm -rf lib && npm install && tsc -p tsconfig.release.json && npm pack", "build:release": "npm install --production && npm install typescript firebase-admin && tsc -p tsconfig.release.json", "build": "tsc -p tsconfig.release.json", @@ -29,8 +32,7 @@ "format:fix": "prettier --write '**/*.{json,md,ts,yml,yaml}'", "lint": "tslint --config tslint.json --project tsconfig.json ", "lint:fix": "tslint --config tslint.json --fix --project tsconfig.json", - "test": "mocha -r ts-node/register ./spec/index.spec.ts", - "apidocs": "node docgen/generate-docs.js" + "test": "mocha" }, "dependencies": { "@types/express": "^4.17.0", diff --git a/spec/index.spec.ts b/spec/index.spec.ts deleted file mode 100644 index 0eb95045d..000000000 --- a/spec/index.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2017 Firebase -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import * as chai from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -chai.use(chaiAsPromised); - -import * as nock from 'nock'; -nock.disableNetConnect(); - -import 'mocha'; - -import './apps.spec'; -import './cloud-functions.spec'; -import './config.spec'; -import './function-builder.spec'; -import './providers/analytics.spec'; -import './providers/auth.spec'; -import './providers/crashlytics.spec'; -import './providers/database.spec'; -import './providers/firestore.spec'; -import './providers/https.spec'; -import './providers/pubsub.spec'; -import './providers/remoteConfig.spec'; -import './providers/storage.spec'; -import './setup.spec'; -import './utilities/path.spec'; -import './utils.spec'; From 87e75d717d5acf32d893e867078c531542d30c69 Mon Sep 17 00:00:00 2001 From: egilmorez Date: Fri, 12 Jul 2019 11:59:01 -0700 Subject: [PATCH 173/705] Adding comments for EventContext and main CloudFunctions methods. (#519) * Adding comments for EventContext and main CloudFunctions methods. * Fixing spacing and alignment issues found by thechenky. * Committing edits from npm run format:fix. --- src/cloud-functions.ts | 100 +++++++++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 18 deletions(-) diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 686e9e31e..b35ba6ee1 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -25,11 +25,13 @@ import * as _ from 'lodash'; import { DeploymentOptions, Schedule } from './function-configuration'; export { Request, Response }; +/** @hidden */ const WILDCARD_REGEX = new RegExp('{[^/{}]*}', 'g'); /** - * Wire format for an event. * @hidden + * + * Wire format for an event. */ export interface Event { context: { @@ -53,38 +55,89 @@ export interface Event { */ export interface EventContext { /** - * Firebase auth variable for the user whose action triggered the function. - * Field will be null for unauthenticated users, and will not exist for admin - * users. Only available for database functions. + * Authentication information for the user that triggered the function. + * This object contains `uid` and `token` properties for authenticated users. + * For more detail including token keys, see the + * [security rules reference](/docs/firestore/reference/security/#properties). + * + * This field is only populated for Realtime Database triggers and Callable + * functions. For an unauthenticated user, this field is null. For Firebase + * admin users and event types that do not provide user information, this field + * does not exist. */ auth?: { token: object; uid: string; }; + /** - * Type of authentication for the triggering action. Only available for - * database functions. + * The level of permissions for a user. Valid values are: + * + * * `ADMIN` Developer user or user authenticated via a service account. + * * `USER` Known user. + * * `UNAUTHENTICATED` Unauthenticated action + * * `null` For event types that do not provide user information (all except + * Realtime Database). */ authType?: 'ADMIN' | 'USER' | 'UNAUTHENTICATED'; + /** - * ID of the event + * The event’s unique identifier. */ eventId: string; + /** - * Type of event + * Type of event. Valid values are: + * + * * `providers/google.firebase.analytics/eventTypes/event.log` + * * `providers/firebase.auth/eventTypes/user.create` + * * `providers/firebase.auth/eventTypes/user.delete` + * * `providers/firebase.crashlytics/eventTypes/issue.new` + * * `providers/firebase.crashlytics/eventTypes/issue.regressed` + * * `providers/firebase.crashlytics/eventTypes/issue.velocityAlert` + * * `providers/google.firebase.database/eventTypes/ref.write` + * * `providers/google.firebase.database/eventTypes/ref.create` + * * `providers/google.firebase.database/eventTypes/ref.update` + * * `providers/google.firebase.database/eventTypes/ref.delete` + * * `providers/cloud.firestore/eventTypes/document.write` + * * `providers/cloud.firestore/eventTypes/document.create` + * * `providers/cloud.firestore/eventTypes/document.update` + * * `providers/cloud.firestore/eventTypes/document.delete` + * * `google.pubsub.topic.publish` + * * `google.storage.object.finalize` + * * `google.storage.object.archive` + * * `google.storage.object.delete` + * * `google.storage.object.metadataUpdate` + * * `google.firebase.remoteconfig.update` */ eventType: string; + /** - * Key-value pairs that represent the values of wildcards in a database - * reference. Cannot be accessed while inside the handler namespace. + * An object containing the values of the wildcards in the `path` parameter + * provided to the [`ref()`](functions.database#.ref) method for a Realtime + * Database trigger. Cannot be accessed while inside the handler namespace. */ params: { [option: string]: any }; + /** - * Resource that triggered the event + * The resource that emitted the event. Valid values are: + * + * * Analytics — `projects//events/` + * * Realtime Database — + `projects/_/instances//refs/` + * * Storage — + `projects/_/buckets//objects/#` + * * Authentication — `projects/` + * * Pub/Sub — `projects//topics/` + * + * Because Realtime Database instances and Cloud Storage buckets are globally + * unique and not tied to the project, their resources start with `projects/_`. + * Underscore is not a valid project name. */ resource: Resource; /** - * Timestamp for when the event ocurred (ISO 8601 string) + * Timestamp for the event as an + * [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) string. */ timestamp: string; } @@ -180,6 +233,7 @@ export interface Resource { } /** + * @hidden * TriggerAnnotated is used internally by the firebase CLI to understand what * type of Cloud Function to deploy. */ @@ -208,17 +262,23 @@ export interface Runnable { } /** - * An HttpsFunction is both an object that exports its trigger definitions at - * __trigger and can be called as a function that takes an express.js Request - * and Response object. + * The Cloud Function type for HTTPS triggers. This should be exported from your + * JavaScript file to define a Cloud Function. + * + * This type is a special JavaScript function which takes Express + * [`Request`](https://expressjs.com/en/api.html#req) and + * [`Response`](https://expressjs.com/en/api.html#res) objects as its only + * arguments. */ export type HttpsFunction = TriggerAnnotated & ((req: Request, resp: Response) => void); /** - * A CloudFunction is both an object that exports its trigger definitions at - * __trigger and can be called as a function using the raw JS API for Google - * Cloud Functions. + * The Cloud Function type for all non-HTTPS triggers. This should be exported + * from your JavaScript file to define a Cloud Function. + * + * This type is a special JavaScript function which takes a templated + * `Event` object as its only argument. */ export type CloudFunction = Runnable & TriggerAnnotated & @@ -346,6 +406,7 @@ export function makeCloudFunction({ return cloudFunction; } +/** @hidden */ function _makeParams( context: EventContext, triggerResourceGetter: () => string @@ -373,6 +434,7 @@ function _makeParams( return params; } +/** @hidden */ function _makeAuth(event: Event, authType: string) { if (authType === 'UNAUTHENTICATED') { return null; @@ -383,6 +445,7 @@ function _makeAuth(event: Event, authType: string) { }; } +/** @hidden */ function _detectAuthType(event: Event) { if (_.get(event, 'context.auth.admin')) { return 'ADMIN'; @@ -393,6 +456,7 @@ function _detectAuthType(event: Event) { return 'UNAUTHENTICATED'; } +/** @hidden */ export function optionsToTrigger(options: DeploymentOptions) { const trigger: any = {}; if (options.regions) { From f7df0c7be3246094657a5ce8c907bd2eb7d49ebb Mon Sep 17 00:00:00 2001 From: egilmorez Date: Mon, 15 Jul 2019 16:02:47 -0700 Subject: [PATCH 174/705] Adding comments to try and populate information for Change and ChangeJson (#525) * Adding comments to try and populate information for Change and ChangeJson. * Remembered to run npm run format:fix! --- docgen/content-sources/toc.yaml | 2 ++ src/cloud-functions.ts | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docgen/content-sources/toc.yaml b/docgen/content-sources/toc.yaml index c2766e430..9ba761c15 100644 --- a/docgen/content-sources/toc.yaml +++ b/docgen/content-sources/toc.yaml @@ -12,6 +12,8 @@ toc: path: /docs/reference/functions/function_builder_.functionbuilder.html - title: 'Change' path: /docs/reference/functions/cloud_functions_.change.html + - title: 'ChangeJson' + path: /docs/reference/functions/cloud_functions_.changejson.html - title: 'functions.config' path: /docs/reference/functions/config_.html diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index b35ba6ee1..e8d099dca 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -143,15 +143,19 @@ export interface EventContext { } /** - * Change describes a change of state - "before" represents the state prior to - * the event, "after" represents the state after the event. + * The Functions interface for events that change state, such as + * Realtime Database or Cloud Firestore `onWrite` and `onUpdate`. + * + * For more information about the format used to construct `Change` objects, see + * [`cloud-functions.ChangeJson`](/docs/reference/functions/cloud_functions_.changejson). + * */ export class Change { constructor(public before: T, public after: T) {} } /** - * ChangeJson is the JSON format used to construct a Change object. + * `ChangeJson` is the JSON format used to construct a Change object. */ export interface ChangeJson { /** @@ -164,17 +168,20 @@ export interface ChangeJson { */ before?: any; /** - * Comma-separated string that represents names of field that changed. + * @hidden + * Comma-separated string that represents names of fields that changed. */ fieldMask?: string; } export namespace Change { + /** @hidden */ function reinterpretCast(x: any) { return x as T; } /** + * @hidden * Factory method for creating a Change from a `before` object and an `after` * object. */ @@ -183,6 +190,7 @@ export namespace Change { } /** + * @hidden * Factory method for creating a Change from a JSON and an optional customizer * function to be applied to both the `before` and the `after` fields. */ From 36916b873fc25fc94f4fc425ffceaf138d0a4b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Natan=20S=C4=85gol?= Date: Tue, 16 Jul 2019 01:13:11 +0200 Subject: [PATCH 175/705] Fix exhaustiveness of error code switch in http status conversion (#520) * Fix exhaustiveness of error code switch in http status conversion * Add a dot at the end of an error message * Add a newline before parameter documentation --- src/providers/https.ts | 3 ++- src/utilities/assertions.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/utilities/assertions.ts diff --git a/src/providers/https.ts b/src/providers/https.ts index e6c8a63f4..3c3999123 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -27,6 +27,7 @@ import * as _ from 'lodash'; import { apps } from '../apps'; import { HttpsFunction, optionsToTrigger, Runnable } from '../cloud-functions'; import { DeploymentOptions } from '../function-configuration'; +import { assertNever } from '../utilities/assertions'; export interface Request extends express.Request { rawBody: Buffer; @@ -231,7 +232,7 @@ export class HttpsError extends Error { return 500; // This should never happen as long as the type system is doing its job. default: - throw new Error('Invalid error code: ' + this.code); + assertNever(this.code); } } diff --git a/src/utilities/assertions.ts b/src/utilities/assertions.ts new file mode 100644 index 000000000..49ff5e36d --- /dev/null +++ b/src/utilities/assertions.ts @@ -0,0 +1,16 @@ +/** + * @file Provides common assertion helpers which can be used to improve + * strictness of both type checking and runtime. + */ + +/** + * Checks that the given value is of type `never` — the type that’s left after + * all other cases have been removed. + * + * @param x A value of type `never`. + */ +export function assertNever(x: never): never { + throw new Error( + `Unhandled discriminated union member: ${JSON.stringify(x)}.` + ); +} From a2e58e7d1b366a44fb3b895b6cdb5355892e1cae Mon Sep 17 00:00:00 2001 From: Erik Macik <42585523+esmacik@users.noreply.github.com> Date: Tue, 16 Jul 2019 09:48:29 -0700 Subject: [PATCH 176/705] Adding code comments to remoteConfig.ts source (#524) * Adding code comments to remoteConfig.ts source * Update src/providers/remoteConfig.ts Co-Authored-By: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> * Update src/providers/remoteConfig.ts Co-Authored-By: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> * Update src/providers/remoteConfig.ts Co-Authored-By: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> * Update src/providers/remoteConfig.ts Co-Authored-By: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> * Update src/providers/remoteConfig.ts Co-Authored-By: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> * Update src/providers/remoteConfig.ts Co-Authored-By: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> * Revised wording for TemplateVersion.rollBackSource * Adding only npm run format:fix, everything else is the same as my previous commit * Fixed some small typos found by Diana, 'if' and 'Config' --- src/providers/remoteConfig.ts | 48 +++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/src/providers/remoteConfig.ts b/src/providers/remoteConfig.ts index cd32e5423..f15716c17 100644 --- a/src/providers/remoteConfig.ts +++ b/src/providers/remoteConfig.ts @@ -35,9 +35,13 @@ export const provider = 'google.firebase.remoteconfig'; export const service = 'firebaseremoteconfig.googleapis.com'; /** - * Handle all updates (including rollbacks) that affect a Remote Config project. - * @param handler A function that takes the updated Remote Config template - * version metadata as an argument. + * Registers a function that triggers on Firebase Remote Config template + * update events. + * + * @param handler A function that takes the updated Remote Config + * template version metadata as an argument. + * + * @return A Cloud Function that you can export and deploy. */ export function onUpdate( handler: ( @@ -97,8 +101,8 @@ export class UpdateBuilder { } /** - * Interface representing a Remote Config template version metadata object that - * was emitted when the project was updated. + * An interface representing a Remote Config template version metadata object + * emitted when a project is updated. */ export interface TemplateVersion { /** The version number of the updated Remote Config template. */ @@ -107,27 +111,49 @@ export interface TemplateVersion { /** When the template was updated in format (ISO8601 timestamp). */ updateTime: string; - /** Metadata about the account that performed the update. */ + /** + * Metadata about the account that performed the update, of + * type [`RemoteConfigUser`](/docs/reference/remote-config/rest/v1/Version#remoteconfiguser). + */ updateUser: RemoteConfigUser; - /** A description associated with the particular Remote Config template. */ + /** A description associated with this Remote Config template version. */ description: string; - /** The origin of the caller. */ + /** + * The origin of the caller - either the Firebase console or the Remote Config + * REST API. See [`RemoteConfigUpdateOrigin`](/docs/reference/remote-config/rest/v1/Version#remoteconfigupdateorigin) + * for valid values. + */ updateOrigin: string; - /** The type of update action that was performed. */ + /** + * The type of update action that was performed, whether forced, + * incremental, or a rollback operation. See + * [`RemoteConfigUpdateType`](/docs/reference/remote-config/rest/v1/Version#remoteconfigupdatetype) + * for valid values. + */ updateType: string; /** - * The version number of the Remote Config template that was rolled back to, - * if the update was a rollback. + * The version number of the Remote Config template that this update rolled back to. + * Only applies if this update was a rollback. */ rollbackSource?: number; } +/** + * An interface representing metadata for a Remote Config account + * that performed the update. Contains the same fields as + * [`RemoteConfigUser`](/docs/reference/remote-config/rest/v1/Version#remoteconfiguser). + */ export interface RemoteConfigUser { + /** Name of the Remote Config account that performed the update. */ name?: string; + + /** Email address of the Remote Config account that performed the update. */ email: string; + + /** Image URL of the Remote Config account that performed the update. */ imageUrl?: string; } From ef4e0db481df4989c47fdcb30abac8d0b465f018 Mon Sep 17 00:00:00 2001 From: Jeroen Claassens Date: Wed, 17 Jul 2019 10:52:00 +0200 Subject: [PATCH 177/705] Resolve security vulnerability CVE-2019-10744 (#526) * Resolve security vulnerability CVE-2019-10744 Signed-off-by: Jeroen Claassens * Resolve prettier formatting * Resolve requested review by @thechenky Signed-off-by: Jeroen Claassens --- changelog.txt | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index e69de29bb..ef03d65d5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +fixed - Upgrade lodash dependency to resolve security vulnerability CVE-2019-10744 \ No newline at end of file diff --git a/package.json b/package.json index 76ea87fb5..de58f1199 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "cors": "^2.8.5", "express": "^4.17.1", "jsonwebtoken": "^8.5.1", - "lodash": "^4.17.11" + "lodash": "^4.17.14" }, "devDependencies": { "@types/chai": "^4.1.7", From 9c4f6807a827ad3bdbe89674ce881c29f9ea7846 Mon Sep 17 00:00:00 2001 From: Michael Zoech Date: Wed, 17 Jul 2019 02:38:53 -0700 Subject: [PATCH 178/705] Test Lab Functions (#229) * Firebase Test Lab Provider * Minor code cleanups * Update event type and provider namespace to new format * Rename testlab to testLab * Add testMatrix handler function to HandlerBuilder * Resolve PR comments * Resolve PR comments * Add changelog message for new Test Lab trigger * Resolve minor pr comments * Promisify makeRequest and reuse in existing http request helpers * Fix testLab import in handler-builder.ts * Necessary fixes after merging master --- changelog.txt | 3 +- integration_test/functions/src/index.ts | 36 +-- integration_test/functions/src/test-utils.ts | 38 +++ .../functions/src/testLab-tests.ts | 26 ++ .../functions/src/testLab-utils.ts | 117 +++++++ integration_test/run_tests.sh | 4 +- spec/index.spec.ts | 1 + spec/providers/testLab.spec.ts | 255 +++++++++++++++ src/function-builder.ts | 10 + src/handler-builder.ts | 10 + src/index.ts | 2 + src/providers/testLab.ts | 297 ++++++++++++++++++ 12 files changed, 775 insertions(+), 24 deletions(-) create mode 100644 integration_test/functions/src/test-utils.ts create mode 100644 integration_test/functions/src/testLab-tests.ts create mode 100644 integration_test/functions/src/testLab-utils.ts create mode 100644 spec/providers/testLab.spec.ts create mode 100644 src/providers/testLab.ts diff --git a/changelog.txt b/changelog.txt index 1385ce18b..d05db1837 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +1,2 @@ -feature - Adds region support for us-east4. \ No newline at end of file +feature - Adds support for Test Lab triggered functions with `functions.testLab`. +feature - Adds region support for us-east4. diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 967b3d376..e8fa396e9 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -14,36 +14,29 @@ export * from './firestore-tests'; export * from './https-tests'; export * from './remoteConfig-tests'; export * from './storage-tests'; +export * from './testLab-tests'; const numTests = Object.keys(exports).length; // Assumption: every exported function is its own test. +import * as utils from './test-utils'; +import * as testLab from './testLab-utils'; + import 'firebase-functions'; // temporary shim until process.env.FIREBASE_CONFIG available natively in GCF(BUG 63586213) const firebaseConfig = JSON.parse(process.env.FIREBASE_CONFIG); admin.initializeApp(); // TODO(klimt): Get rid of this once the JS client SDK supports callable triggers. function callHttpsTrigger(name: string, data: any, baseUrl) { - return new Promise((resolve, reject) => { - const request = https.request( - { - method: 'POST', - host: 'us-central1-' + firebaseConfig.projectId + '.' + baseUrl, - path: '/' + name, - headers: { - 'Content-Type': 'application/json', - }, + return utils.makeRequest( + { + method: 'POST', + host: 'us-central1-' + firebaseConfig.projectId + '.' + baseUrl, + path: '/' + name, + headers: { + 'Content-Type': 'application/json', }, - (response) => { - let body = ''; - response.on('data', (chunk) => { - body += chunk; - }); - response.on('end', () => resolve(body)); - } - ); - request.on('error', reject); - request.write(JSON.stringify({ data })); - request.end(); - }); + }, + JSON.stringify({ data }) + ); } function callScheduleTrigger(functionName: string, region: string) { @@ -149,6 +142,7 @@ export const integrationTests: any = functions .storage() .bucket() .upload('/tmp/' + testId + '.txt'), + testLab.startTestRun(firebaseConfig.projectId, testId), // Invoke the schedule for our scheduled function to fire callScheduleTrigger('schedule', 'us-central1'), ]) diff --git a/integration_test/functions/src/test-utils.ts b/integration_test/functions/src/test-utils.ts new file mode 100644 index 000000000..eda799a67 --- /dev/null +++ b/integration_test/functions/src/test-utils.ts @@ -0,0 +1,38 @@ +import * as https from 'https'; + +/** + * Makes an http request asynchronously and returns the response data. + * + * This function wraps the callback-based `http.request()` function with a + * Promise. The returned Promise will be rejected in case the request fails with an + * error, or the response code is not in the 200-299 range. + * + * @param options Request options for the request. + * @param body Optional body to send as part of the request. + * @returns Promise returning the response data as string. + */ +export function makeRequest( + options: https.RequestOptions, + body?: string +): Promise { + return new Promise((resolve, reject) => { + const request = https.request(options, (response) => { + let body = ''; + response.on('data', (chunk) => { + body += chunk; + }); + response.on('end', () => { + if (response.statusCode < 200 || response.statusCode > 299) { + reject(body); + return; + } + resolve(body); + }); + }); + if (body) { + request.write(body); + } + request.on('error', reject); + request.end(); + }); +} diff --git a/integration_test/functions/src/testLab-tests.ts b/integration_test/functions/src/testLab-tests.ts new file mode 100644 index 000000000..a6350c57d --- /dev/null +++ b/integration_test/functions/src/testLab-tests.ts @@ -0,0 +1,26 @@ +import * as functions from 'firebase-functions'; +import * as _ from 'lodash'; +import { TestSuite, expectEq } from './testing'; +import TestMatrix = functions.testLab.TestMatrix; + +export const testLabTests: any = functions + .runWith({ + timeoutSeconds: 540, + }) + .testLab.testMatrix() + .onComplete((matrix, context) => { + return new TestSuite('test matrix complete') + .it('should have eventId', (snap, context) => context.eventId) + + .it('should have timestamp', (snap, context) => context.timestamp) + + .it('should have right eventType', (_, context) => + expectEq(context.eventType, 'google.testing.testMatrix.complete') + ) + + .it("should be in state 'INVALID'", (matrix, _) => + expectEq(matrix.state, 'INVALID') + ) + + .run(_.get(matrix, 'clientInfo.details.testId'), matrix, context); + }); diff --git a/integration_test/functions/src/testLab-utils.ts b/integration_test/functions/src/testLab-utils.ts new file mode 100644 index 000000000..2f457e70d --- /dev/null +++ b/integration_test/functions/src/testLab-utils.ts @@ -0,0 +1,117 @@ +import * as http from 'http'; +import * as https from 'https'; +import * as admin from 'firebase-admin'; +import * as _ from 'lodash'; +import * as utils from './test-utils'; + +interface AndroidDevice { + androidModelId: string; + androidVersionId: string; + locale: string; + orientation: string; +} + +const TESTING_API_SERVICE_NAME = 'testing.googleapis.com'; + +/** + * Creates a new TestMatrix in Test Lab which is expected to be rejected as + * invalid. + * + * @param projectId Project for which the test run will be created + * @param testId Test id which will be encoded in client info details + */ +export async function startTestRun(projectId: string, testId: string) { + const accessToken = await admin.credential + .applicationDefault() + .getAccessToken(); + const device = await fetchDefaultDevice(accessToken); + return await createTestMatrix(accessToken, projectId, testId, device); +} + +async function fetchDefaultDevice( + accessToken: admin.GoogleOAuthAccessToken +): Promise { + const response = await utils.makeRequest( + requestOptions(accessToken, 'GET', '/v1/testEnvironmentCatalog/ANDROID') + ); + const data = JSON.parse(response); + const models = _.get(data, 'androidDeviceCatalog.models', []); + const defaultModels = models.filter( + (m) => + m.tags !== undefined && + m.tags.indexOf('default') > -1 && + m.supportedVersionIds !== undefined && + m.supportedVersionIds.length > 0 + ); + + if (defaultModels.length === 0) { + throw new Error('No default device found'); + } + + const model = defaultModels[0]; + const versions = model.supportedVersionIds; + + return { + androidModelId: model.id, + androidVersionId: versions[versions.length - 1], + locale: 'en', + orientation: 'portrait', + }; +} + +function createTestMatrix( + accessToken: admin.GoogleOAuthAccessToken, + projectId: string, + testId: string, + device: AndroidDevice +): Promise { + const options = requestOptions( + accessToken, + 'POST', + '/v1/projects/' + projectId + '/testMatrices' + ); + const body = { + projectId: projectId, + testSpecification: { + androidRoboTest: { + appApk: { + gcsPath: 'gs://path/to/non-existing-app.apk', + }, + }, + }, + environmentMatrix: { + androidDeviceList: { + androidDevices: [device], + }, + }, + resultStorage: { + googleCloudStorage: { + gcsPath: 'gs://' + admin.storage().bucket().name, + }, + }, + clientInfo: { + name: 'CloudFunctionsSDKIntegrationTest', + clientInfoDetails: { + key: 'testId', + value: testId, + }, + }, + }; + return utils.makeRequest(options, JSON.stringify(body)); +} + +function requestOptions( + accessToken: admin.GoogleOAuthAccessToken, + method: string, + path: string +): https.RequestOptions { + return { + method: method, + hostname: TESTING_API_SERVICE_NAME, + path: path, + headers: { + Authorization: 'Bearer ' + accessToken.access_token, + 'Content-Type': 'application/json', + }, + }; +} diff --git a/integration_test/run_tests.sh b/integration_test/run_tests.sh index 911fc1dd4..d7017af52 100755 --- a/integration_test/run_tests.sh +++ b/integration_test/run_tests.sh @@ -60,9 +60,9 @@ function delete_all_functions { # Try to delete, if there are errors it is because the project is already empty, # in that case do nothing. if [[ "${TOKEN}" == "" ]]; then - firebase functions:delete callableTests createUserTests databaseTests deleteUserTests firestoreTests integrationTests pubsubTests remoteConfigTests --force --project=$PROJECT_ID || : & + firebase functions:delete callableTests createUserTests databaseTests deleteUserTests firestoreTests integrationTests pubsubTests remoteConfigTests testLabTests --force --project=$PROJECT_ID || : & else - firebase functions:delete callableTests createUserTests databaseTests deleteUserTests firestoreTests integrationTests pubsubTests remoteConfigTests --force --project=$PROJECT_ID --token=$TOKEN || : & + firebase functions:delete callableTests createUserTests databaseTests deleteUserTests firestoreTests integrationTests pubsubTests remoteConfigTests testLabTests --force --project=$PROJECT_ID --token=$TOKEN || : & fi wait announce "Project emptied." diff --git a/spec/index.spec.ts b/spec/index.spec.ts index 6d56878da..e438e6a97 100644 --- a/spec/index.spec.ts +++ b/spec/index.spec.ts @@ -42,6 +42,7 @@ import './providers/https.spec'; import './providers/pubsub.spec'; import './providers/remoteConfig.spec'; import './providers/storage.spec'; +import './providers/testLab.spec'; import './setup.spec'; import './testing.spec'; import './utils.spec'; diff --git a/spec/providers/testLab.spec.ts b/spec/providers/testLab.spec.ts new file mode 100644 index 000000000..bf74a6fd1 --- /dev/null +++ b/spec/providers/testLab.spec.ts @@ -0,0 +1,255 @@ +// The MIT License (MIT) +// +// Copyright (c) 2019 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { expect } from 'chai'; + +import * as testLab from '../../src/providers/testLab'; + +describe('Test Lab Functions', () => { + describe('#onComplete', () => { + describe('with process.env.GCLOUD_PROJECT set', () => { + before(() => { + process.env.GCLOUD_PROJECT = 'project1'; + }); + + after(() => { + delete process.env.GCLOUD_PROJECT; + }); + + it('should return a TriggerDefinition with appropriate values', () => { + const func = testLab.testMatrix().onComplete(() => null); + expect(func.__trigger).to.deep.equal({ + eventTrigger: { + service: 'testing.googleapis.com', + eventType: 'google.testing.testMatrix.complete', + resource: 'projects/project1/testMatrices/{matrix}', + }, + }); + }); + + it('should parse TestMatrix in "INVALID" state', () => { + const event = { + data: { + clientInfo: { + name: 'test', + }, + invalidMatrixDetails: 'INVALID_INPUT_APK', + resultStorage: { + googleCloudStorage: { + gcsPath: 'gs://test.appspot.com', + }, + }, + state: 'INVALID', + testMatrixId: 'matrix-375mfeu9mnw8t', + timestamp: '2019-04-15T17:43:32.538Z', + }, + context: { + resource: {}, + }, + }; + const expected = { + testMatrixId: 'matrix-375mfeu9mnw8t', + state: 'INVALID', + createTime: '2019-04-15T17:43:32.538Z', + outcomeSummary: undefined, + invalidMatrixDetails: 'INVALID_INPUT_APK', + resultStorage: { + gcsPath: 'gs://test.appspot.com', + resultsUrl: undefined, + toolResultsHistoryId: undefined, + toolResultsExecutionId: undefined, + }, + clientInfo: { + name: 'test', + details: {}, + }, + }; + const func = testLab.testMatrix().onComplete((matrix) => matrix); + return expect(func(event.data, event.context)).to.eventually.deep.equal( + expected + ); + }); + + it('should parse TestMatrix in "FINISHED" state', () => { + const event = { + data: { + clientInfo: { + name: 'test', + }, + outcomeSummary: 'FAILURE', + resultStorage: { + googleCloudStorage: { + gcsPath: 'gs://test.appspot.com', + }, + toolResultsExecution: { + executionId: '6352915701487950333', + historyId: 'bh.9b6f4dac24d3049', + projectId: 'test', + }, + toolResultsHistory: { + historyId: 'bh.9b6f4dac24d3049', + projectId: 'test', + }, + resultsUrl: 'https://path/to/results', + }, + state: 'FINISHED', + testMatrixId: 'matrix-tsgjk8pnvxhya', + timestamp: '2019-04-15T18:03:11.115Z', + }, + context: { + resource: {}, + }, + }; + const expected = { + testMatrixId: 'matrix-tsgjk8pnvxhya', + state: 'FINISHED', + createTime: '2019-04-15T18:03:11.115Z', + outcomeSummary: 'FAILURE', + invalidMatrixDetails: undefined, + resultStorage: { + gcsPath: 'gs://test.appspot.com', + toolResultsHistoryId: 'bh.9b6f4dac24d3049', + toolResultsExecutionId: '6352915701487950333', + resultsUrl: 'https://path/to/results', + }, + clientInfo: { + name: 'test', + details: {}, + }, + }; + const func = testLab.testMatrix().onComplete((matrix) => matrix); + return expect(func(event.data, event.context)).to.eventually.deep.equal( + expected + ); + }); + }); + + describe('process.env.GCLOUD_PROJECT not set', () => { + it('should not throw if trigger is not accessed', () => { + expect(() => testLab.testMatrix().onComplete(() => null)).to.not.throw( + Error + ); + }); + + it('should throw when trigger is accessed', () => { + expect( + () => testLab.testMatrix().onComplete(() => null).__trigger + ).to.throw(Error); + }); + }); + }); + + describe('TestMatrix', () => { + describe('constructor', () => { + it('should populate basic fields', () => { + const expected = { + testMatrixId: 'id1', + createTime: '2019-02-08T18:50:32.178Z', + state: 'FINISHED', + outcomeSummary: 'SUCCESS', + invalidMatrixDetails: 'DETAILS_UNAVAILABLE', + resultStorage: new testLab.ResultStorage(), + clientInfo: new testLab.ClientInfo(), + }; + const actual = new testLab.TestMatrix({ + testMatrixId: 'id1', + timestamp: '2019-02-08T18:50:32.178Z', + state: 'FINISHED', + outcomeSummary: 'SUCCESS', + invalidMatrixDetails: 'DETAILS_UNAVAILABLE', + }); + expect(actual).to.deep.equal(expected); + }); + }); + }); + + describe('ClientInfo', () => { + describe('constructor', () => { + it('should populate basic fields', () => { + const expected = { + name: 'client', + details: {}, + }; + const actual = new testLab.ClientInfo({ + name: 'client', + }); + expect(actual).to.deep.equal(expected); + }); + + it('should populate key/value details', () => { + const expected = { + name: 'client', + details: { + k0: 'v0', + k1: '', + }, + }; + const actual = new testLab.ClientInfo({ + name: 'client', + clientInfoDetails: [ + { + key: 'k0', + value: 'v0', + }, + { + key: 'k1', + }, + ], + }); + expect(actual).to.deep.equal(expected); + }); + }); + }); + + describe('ResultStorage', () => { + describe('constructor', () => { + it('should populate basic fields', () => { + const expected = { + gcsPath: 'path', + toolResultsHistoryId: 'h1', + toolResultsExecutionId: 'e2', + resultsUrl: 'http://example.com/', + }; + const actual = new testLab.ResultStorage({ + googleCloudStorage: { + gcsPath: 'path', + }, + toolResultsHistory: { + projectId: 'p1', + historyId: 'h1', + }, + toolResultsExecution: { + projectId: 'p2', + historyId: 'h2', + executionId: 'e2', + }, + resultsUrl: 'http://example.com/', + }); + expect(actual).to.deep.equal(expected); + }); + + it('should not throw on unset fields', () => { + expect(() => new testLab.ResultStorage({})).to.not.throw(); + }); + }); + }); +}); diff --git a/src/function-builder.ts b/src/function-builder.ts index bdf7f296f..3d24d6a56 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -40,6 +40,7 @@ import * as https from './providers/https'; import * as pubsub from './providers/pubsub'; import * as remoteConfig from './providers/remoteConfig'; import * as storage from './providers/storage'; +import * as testLab from './providers/testLab'; /** * Assert that the runtime options passed in are valid. @@ -317,4 +318,13 @@ export class FunctionBuilder { user: () => auth._userWithOptions(this.options), }; } + + get testLab() { + return { + /** + * Handle events related to Test Lab test matrices. + */ + testMatrix: () => testLab._testMatrixWithOpts(this.options), + }; + } } diff --git a/src/handler-builder.ts b/src/handler-builder.ts index 20e15986d..94a55bf71 100644 --- a/src/handler-builder.ts +++ b/src/handler-builder.ts @@ -33,6 +33,7 @@ import * as https from './providers/https'; import * as pubsub from './providers/pubsub'; import * as remoteConfig from './providers/remoteConfig'; import * as storage from './providers/storage'; +import * as testLab from './providers/testLab'; export class HandlerBuilder { constructor() {} @@ -213,6 +214,15 @@ export class HandlerBuilder { }, }; } + + get testLab() { + /** Handle events related to Test Lab test matrices. */ + return { + get testMatrix() { + return new testLab.TestMatrixBuilder(() => null, {}); + }, + }; + } } export let handler = new HandlerBuilder(); diff --git a/src/index.ts b/src/index.ts index 534080f56..d4951c512 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,6 +30,7 @@ import * as https from './providers/https'; import * as pubsub from './providers/pubsub'; import * as remoteConfig from './providers/remoteConfig'; import * as storage from './providers/storage'; +import * as testLab from './providers/testLab'; import * as apps from './apps'; import { handler } from './handler-builder'; @@ -49,6 +50,7 @@ export { pubsub, remoteConfig, storage, + testLab, }; // Exported root types: diff --git a/src/providers/testLab.ts b/src/providers/testLab.ts new file mode 100644 index 000000000..86398c5c9 --- /dev/null +++ b/src/providers/testLab.ts @@ -0,0 +1,297 @@ +// The MIT License (MIT) +// +// Copyright (c) 2019 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the 'Software'), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import * as _ from 'lodash'; + +import { + CloudFunction, + Event, + EventContext, + makeCloudFunction, +} from '../cloud-functions'; +import { DeploymentOptions } from '../function-configuration'; + +/** @internal */ +export const PROVIDER = 'google.testing'; +/** @internal */ +export const SERVICE = 'testing.googleapis.com'; +/** @internal */ +export const TEST_MATRIX_COMPLETE_EVENT_TYPE = 'testMatrix.complete'; + +/** Handle events related to Test Lab test matrices. */ +export function testMatrix() { + return _testMatrixWithOpts({}); +} + +/** @internal */ +export function _testMatrixWithOpts(opts: DeploymentOptions) { + return new TestMatrixBuilder(() => { + if (!process.env.GCLOUD_PROJECT) { + throw new Error('process.env.GCLOUD_PROJECT is not set.'); + } + return 'projects/' + process.env.GCLOUD_PROJECT + '/testMatrices/{matrix}'; + }, opts); +} + +/** Builder used to create Cloud Functions for Test Lab test matrices events. */ +export class TestMatrixBuilder { + /** @internal */ + constructor( + private triggerResource: () => string, + private options: DeploymentOptions + ) {} + + /** Handle a TestMatrix that reached a final test state. */ + onComplete( + handler: ( + testMatrix: TestMatrix, + context: EventContext + ) => PromiseLike | any + ): CloudFunction { + const dataConstructor = (raw: Event) => { + return new TestMatrix(raw.data); + }; + return makeCloudFunction({ + provider: PROVIDER, + eventType: TEST_MATRIX_COMPLETE_EVENT_TYPE, + triggerResource: this.triggerResource, + service: SERVICE, + dataConstructor, + handler, + options: this.options, + }); + } +} + +/** TestMatrix captures details about a test run. */ +export class TestMatrix { + /** Unique id set by the service. */ + testMatrixId: string; + + /** When this test matrix was initially created (ISO8601 timestamp). */ + createTime: string; + + /** Indicates the current progress of the test matrix */ + state: TestState; + + /** + * The overall outcome of the test matrix run. Only set when the test matrix + * state is FINISHED. + */ + outcomeSummary?: OutcomeSummary; + + /** For 'INVALID' matrices only, describes why the matrix is invalid. */ + invalidMatrixDetails?: InvalidMatrixDetails; + + /** Where the results for the matrix are located. */ + resultStorage: ResultStorage; + + /** Information about the client which invoked the test. */ + clientInfo: ClientInfo; + + /** @internal */ + constructor(data: any) { + this.testMatrixId = data.testMatrixId; + this.createTime = data.timestamp; + this.state = data.state; + this.outcomeSummary = data.outcomeSummary; + this.invalidMatrixDetails = data.invalidMatrixDetails; + this.resultStorage = new ResultStorage(data.resultStorage); + this.clientInfo = new ClientInfo(data.clientInfo); + } +} + +/** Information about the client which invoked the test. */ +export class ClientInfo { + /** Client name, e.g. 'gcloud'. */ + name: string; + + /** Map of detailed information about the client which invoked the test. */ + details: { [key: string]: string }; + + /** @internal */ + constructor(data?: any) { + this.name = _.get(data, 'name', ''); + this.details = {}; + _.forEach(_.get(data, 'clientInfoDetails'), (detail: any) => { + this.details[detail.key] = detail.value || ''; + }); + } +} + +/** Locations where the test results are stored. */ +export class ResultStorage { + /** A storage location within Google Cloud Storage (GCS) for the test artifacts. */ + gcsPath?: string; + + /** Id of the ToolResults History containing these results. */ + toolResultsHistoryId?: string; + + /** + * Id of the ToolResults execution that the detailed TestMatrix results are + * written to. + */ + toolResultsExecutionId?: string; + + /** URL to test results in Firebase Console. */ + resultsUrl?: string; + + /** @internal */ + constructor(data?: any) { + this.gcsPath = _.get(data, 'googleCloudStorage.gcsPath'); + this.toolResultsHistoryId = _.get(data, 'toolResultsHistory.historyId'); + this.toolResultsExecutionId = _.get( + data, + 'toolResultsExecution.executionId' + ); + this.resultsUrl = _.get(data, 'resultsUrl'); + } +} + +/** + * The detailed reason that a Matrix was deemed INVALID. + * + * Possible values: + * - 'DETAILS_UNAVAILABLE': The matrix is INVALID, but there are no further + * details available. + * - 'MALFORMED_APK': The input app APK could not be parsed. + * - 'MALFORMED_TEST_APK': The input test APK could not be parsed. + * - 'NO_MANIFEST': The AndroidManifest.xml could not be found. + * - 'NO_PACKAGE_NAME': The APK manifest does not declare a package name. + * - 'INVALID_PACKAGE_NAME': The APK application ID is invalid. + * - 'TEST_SAME_AS_APP': The test package and app package are the same. + * - 'NO_INSTRUMENTATION': The test apk does not declare an instrumentation. + * - 'NO_SIGNATURE': The input app apk does not have a signature. + * - 'INSTRUMENTATION_ORCHESTRATOR_INCOMPATIBLE': The test runner class + * specified by user or in the test APK's manifest file is not compatible with + * Android Test Orchestrator. + * - 'NO_TEST_RUNNER_CLASS': The test APK does not contain the test runner class + * specified by user or in the manifest file. + * - 'NO_LAUNCHER_ACTIVITY': A main launcher activity could not be found. + * - 'FORBIDDEN_PERMISSIONS': The app declares one or more permissions that are + * not allowed. + * - 'INVALID_ROBO_DIRECTIVES': There is a conflict in the provided + * robo_directives. + * - 'INVALID_RESOURCE_NAME': There is at least one invalid resource name in the + * provided robo directives. + * - 'INVALID_DIRECTIVE_ACTION': Invalid definition of action in the robo + * directives, e.g. a click or ignore action includes an input text field. + * - 'TEST_LOOP_INTENT_FILTER_NOT_FOUND': There is no test loop intent filter, + * or the one that is given is not formatted correctly. + * - 'SCENARIO_LABEL_NOT_DECLARED': The request contains a scenario label that + * was not declared in the manifest. + * - 'SCENARIO_LABEL_MALFORMED': There was an error when parsing a label value. + * - 'SCENARIO_NOT_DECLARED': The request contains a scenario number that was + * not declared in the manifest. + * - 'DEVICE_ADMIN_RECEIVER': Device administrator applications are not allowed. + * - 'MALFORMED_XC_TEST_ZIP': The zipped XCTest was malformed. The zip did not ] + * contain a single .xctestrun file and the contents of the + * DerivedData/Build/Products directory. + * - 'BUILT_FOR_IOS_SIMULATOR': The zipped XCTest was built for the iOS + * simulator rather than for a physical device. + * - 'NO_TESTS_IN_XC_TEST_ZIP': The .xctestrun file did not specify any test + * targets. + * - 'USE_DESTINATION_ARTIFACTS': One or more of the test targets defined in the + * .xctestrun file specifies "UseDestinationArtifacts", which is disallowed. + * - 'TEST_NON_APP_HOSTED': XC tests which run on physical devices must have + * "IsAppHostedTestBundle" == "true" in the xctestrun file. + * - 'PLIST_CANNOT_BE_PARSED': An Info.plist file in the XCTest zip could not be + * parsed. + * - 'NO_CODE_APK': APK contains no code. + * - 'INVALID_INPUT_APK': Either the provided input APK path was malformed, the + * APK file does not exist, or the user does not have permission to access the + * APK file. + * - 'INVALID_APK_PREVIEW_SDK': APK is built for a preview SDK which is + * unsupported. + */ +export type InvalidMatrixDetails = + | 'DETAILS_UNAVAILABLE' + | 'MALFORMED_APK' + | 'MALFORMED_TEST_APK' + | 'NO_MANIFEST' + | 'NO_PACKAGE_NAME' + | 'INVALID_PACKAGE_NAME' + | 'TEST_SAME_AS_APP' + | 'NO_INSTRUMENTATION' + | 'NO_SIGNATURE' + | 'INSTRUMENTATION_ORCHESTRATOR_INCOMPATIBLE' + | 'NO_TEST_RUNNER_CLASS' + | 'NO_LAUNCHER_ACTIVITY' + | 'FORBIDDEN_PERMISSIONS' + | 'INVALID_ROBO_DIRECTIVES' + | 'INVALID_RESOURCE_NAME' + | 'INVALID_DIRECTIVE_ACTION' + | 'TEST_LOOP_INTENT_FILTER_NOT_FOUND' + | 'SCENARIO_LABEL_NOT_DECLARED' + | 'SCENARIO_LABEL_MALFORMED' + | 'SCENARIO_NOT_DECLARED' + | 'DEVICE_ADMIN_RECEIVER' + | 'MALFORMED_XC_TEST_ZIP' + | 'BUILT_FOR_IOS_SIMULATOR' + | 'NO_TESTS_IN_XC_TEST_ZIP' + | 'USE_DESTINATION_ARTIFACTS' + | 'TEST_NOT_APP_HOSTED' + | 'PLIST_CANNOT_BE_PARSED' + | 'NO_CODE_APK' + | 'INVALID_INPUT_APK' + | 'INVALID_APK_PREVIEW_SDK'; + +/** + * The state (i.e. progress) of a TestMatrix. + * + * Possible values: + * - 'VALIDATING': The matrix is being validated. + * - 'PENDING': The matrix is waiting for resources to become available. + * - 'FINISHED': The matrix has terminated normally. This means that the matrix + * level processing completed normally, but individual executions may be in an + * ERROR state. + * - 'ERROR': The matrix has stopped because it encountered an infrastructure + * failure. + * - 'INVALID': The matrix was not run because the provided inputs are not + * valid. E.g. the input file is not of the expected type, or is + * malformed/corrupt. + */ +export type TestState = + | 'VALIDATING' + | 'PENDING' + | 'FINISHED' + | 'ERROR' + | 'INVALID'; + +/** + * Outcome summary for a finished TestMatrix. + * + * Possible values: + * - 'SUCCESS': The test matrix run was successful, for instance: + * - All the test cases passed. + * - Robo did not detect a crash of the application under test. + * - 'FAILURE': The test run failed, for instance: + * - One or more test cases failed. + * - A test timed out. + * - The application under test crashed. + * - 'INCONCLUSIVE': Something unexpected happened. The run should still be + * considered unsuccessful but this is likely a transient problem and + * re-running the test might be successful. + * - 'SKIPPED': All tests were skipped, for instance: + * - All device configurations were incompatible. + */ +export type OutcomeSummary = 'SUCCESS' | 'FAILURE' | 'INCONCLUSIVE' | 'SKIPPED'; From de59422b9a09743e3c346def80e2cf2ccdf77a99 Mon Sep 17 00:00:00 2001 From: Erik Macik <42585523+esmacik@users.noreply.github.com> Date: Wed, 17 Jul 2019 12:33:30 -0700 Subject: [PATCH 179/705] Em pubsub (#527) * Adding code comments to pupsub.ts * Adding code comments to pubsub.ts source file * Removing line breaks between @param and @return tags for TopicBuilder.onPublish() and topic() --- src/providers/pubsub.ts | 46 ++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/src/providers/pubsub.ts b/src/providers/pubsub.ts index 265c39deb..d1a687819 100644 --- a/src/providers/pubsub.ts +++ b/src/providers/pubsub.ts @@ -35,8 +35,12 @@ export const provider = 'google.pubsub'; /** @hidden */ export const service = 'pubsub.googleapis.com'; -/** Select Cloud Pub/Sub topic to listen to. - * @param topic Name of Pub/Sub topic, must belong to the same project as the function. +/** + * Registers a Cloud Function triggered when a Google Cloud Pub/Sub message + * is sent to a specified topic. + * + * @param topic The Pub/Sub topic to watch for message events. + * @return Pub/Sub topic builder interface. */ export function topic(topic: string) { return _topicWithOptions(topic, {}); @@ -105,7 +109,11 @@ export function _scheduleWithOptions( return new ScheduleBuilder({ ...options, schedule: { schedule } }); } -/** Builder used to create Cloud Functions for Google Pub/Sub topics. */ +/** + * The Google Cloud Pub/Sub topic builder. + * + * Access via [`functions.pubsub.topic()`](functions.pubsub#.topic). + */ export class TopicBuilder { /** @hidden */ constructor( @@ -113,7 +121,14 @@ export class TopicBuilder { private options: DeploymentOptions ) {} - /** Handle a Pub/Sub message that was published to a Cloud Pub/Sub topic */ + /** + * Event handler that fires every time a Cloud Pub/Sub message is + * published. + * + * @param handler Event handler that runs every time a Cloud Pub/Sub message + * is published. + * @return A Cloud Function that you can export and deploy. + */ onPublish( handler: (message: Message, context: EventContext) => PromiseLike | any ): CloudFunction { @@ -130,15 +145,22 @@ export class TopicBuilder { } /** - * A Pub/Sub message. + * Interface representing a Google Cloud Pub/Sub message. * - * This class has an additional .json helper which will correctly deserialize any - * message that was a JSON object when published with the JS SDK. .json will throw - * if the message is not a base64 encoded JSON string. + * @param data Payload of a Pub/Sub message. */ export class Message { + /** + * The data payload of this message object as a base64-encoded string. + */ readonly data: string; + + /** + * User-defined attributes published with the message, if any. + */ readonly attributes: { [key: string]: string }; + + /** @hidden */ private _json: any; constructor(data: any) { @@ -149,6 +171,9 @@ export class Message { ]; } + /** + * The JSON data payload of this message object, if any. + */ get json(): any { if (typeof this._json === 'undefined') { this._json = JSON.parse(new Buffer(this.data, 'base64').toString('utf8')); @@ -157,6 +182,11 @@ export class Message { return this._json; } + /** + * Returns a JSON-serializable representation of this object. + * + * @return A JSON-serializable representation of this object. + */ toJSON(): any { return { data: this.data, From 3491a6d4e57432b5747acddd47e1a6a1109f2b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Natan=20S=C4=85gol?= Date: Thu, 18 Jul 2019 20:42:36 +0200 Subject: [PATCH 180/705] Increase type strictness of HttpsError class (#521) * Increase type strictness of HttpsError * Unhide toJSON method on HttpsErrors * Remove unused import * Hide httpErrorCode and toJSON on HttpsError class --- src/providers/https.ts | 166 +++++++++++++++++++---------------------- 1 file changed, 78 insertions(+), 88 deletions(-) diff --git a/src/providers/https.ts b/src/providers/https.ts index 3c3999123..d941f67c8 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -27,7 +27,6 @@ import * as _ from 'lodash'; import { apps } from '../apps'; import { HttpsFunction, optionsToTrigger, Runnable } from '../cloud-functions'; import { DeploymentOptions } from '../function-configuration'; -import { assertNever } from '../utilities/assertions'; export interface Request extends express.Request { rawBody: Buffer; @@ -128,33 +127,65 @@ export type FunctionsErrorCode = | 'data-loss' | 'unauthenticated'; +export type CanonicalErrorCodeName = + | 'OK' + | 'CANCELLED' + | 'UNKNOWN' + | 'INVALID_ARGUMENT' + | 'DEADLINE_EXCEEDED' + | 'NOT_FOUND' + | 'ALREADY_EXISTS' + | 'PERMISSION_DENIED' + | 'UNAUTHENTICATED' + | 'RESOURCE_EXHAUSTED' + | 'FAILED_PRECONDITION' + | 'ABORTED' + | 'OUT_OF_RANGE' + | 'UNIMPLEMENTED' + | 'INTERNAL' + | 'UNAVAILABLE' + | 'DATA_LOSS'; + +interface HttpErrorCode { + canonicalName: CanonicalErrorCodeName; + status: number; +} + /** - * Standard error codes for different ways a request can fail, as defined by: + * Standard error codes and HTTP statuses for different ways a request can fail, + * as defined by: * https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto * * This map is used primarily to convert from a client error code string to - * to the HTTP format error code string, and make sure it's in the supported set. + * to the HTTP format error code string and status, and make sure it's in the + * supported set. */ -const errorCodeMap: { [name: string]: string } = { - ok: 'OK', - cancelled: 'CANCELLED', - unknown: 'UNKNOWN', - 'invalid-argument': 'INVALID_ARGUMENT', - 'deadline-exceeded': 'DEADLINE_EXCEEDED', - 'not-found': 'NOT_FOUND', - 'already-exists': 'ALREADY_EXISTS', - 'permission-denied': 'PERMISSION_DENIED', - unauthenticated: 'UNAUTHENTICATED', - 'resource-exhausted': 'RESOURCE_EXHAUSTED', - 'failed-precondition': 'FAILED_PRECONDITION', - aborted: 'ABORTED', - 'out-of-range': 'OUT_OF_RANGE', - unimplemented: 'UNIMPLEMENTED', - internal: 'INTERNAL', - unavailable: 'UNAVAILABLE', - 'data-loss': 'DATA_LOSS', +const errorCodeMap: { [name in FunctionsErrorCode]: HttpErrorCode } = { + ok: { canonicalName: 'OK', status: 200 }, + cancelled: { canonicalName: 'CANCELLED', status: 499 }, + unknown: { canonicalName: 'UNKNOWN', status: 500 }, + 'invalid-argument': { canonicalName: 'INVALID_ARGUMENT', status: 400 }, + 'deadline-exceeded': { canonicalName: 'DEADLINE_EXCEEDED', status: 504 }, + 'not-found': { canonicalName: 'NOT_FOUND', status: 404 }, + 'already-exists': { canonicalName: 'ALREADY_EXISTS', status: 409 }, + 'permission-denied': { canonicalName: 'PERMISSION_DENIED', status: 403 }, + unauthenticated: { canonicalName: 'UNAUTHENTICATED', status: 401 }, + 'resource-exhausted': { canonicalName: 'RESOURCE_EXHAUSTED', status: 429 }, + 'failed-precondition': { canonicalName: 'FAILED_PRECONDITION', status: 400 }, + aborted: { canonicalName: 'ABORTED', status: 409 }, + 'out-of-range': { canonicalName: 'OUT_OF_RANGE', status: 400 }, + unimplemented: { canonicalName: 'UNIMPLEMENTED', status: 501 }, + internal: { canonicalName: 'INTERNAL', status: 500 }, + unavailable: { canonicalName: 'UNAVAILABLE', status: 503 }, + 'data-loss': { canonicalName: 'DATA_LOSS', status: 500 }, }; +interface HttpErrorWireFormat { + details?: unknown; + message: string; + status: CanonicalErrorCodeName; +} + /** * An explicit error that can be thrown from a handler to send an error to the * client that called the function. @@ -164,88 +195,46 @@ export class HttpsError extends Error { * A standard error code that will be returned to the client. This also * determines the HTTP status code of the response, as defined in code.proto. */ - readonly code: FunctionsErrorCode; + public readonly code: FunctionsErrorCode; /** * Extra data to be converted to JSON and included in the error response. */ - readonly details?: unknown; + public readonly details: unknown; + + /** + * A wire format representation of a provided error code. + * + * @hidden + */ + public readonly httpErrorCode: HttpErrorCode; constructor(code: FunctionsErrorCode, message: string, details?: unknown) { super(message); - if (!errorCodeMap[code]) { - throw new Error('Unknown error status: ' + code); + // A sanity check for non-TypeScript consumers. + if (code in errorCodeMap === false) { + throw new Error(`Unknown error code: ${code}.`); } this.code = code; this.details = details; - } - - /** - * @hidden - * A string representation of the Google error code for this error for HTTP. - */ - get status() { - return errorCodeMap[this.code]; - } - - /** - * @hidden - * Returns the canonical http status code for the given error. - */ - get httpStatus(): number { - switch (this.code) { - case 'ok': - return 200; - case 'cancelled': - return 499; - case 'unknown': - return 500; - case 'invalid-argument': - return 400; - case 'deadline-exceeded': - return 504; - case 'not-found': - return 404; - case 'already-exists': - return 409; - case 'permission-denied': - return 403; - case 'unauthenticated': - return 401; - case 'resource-exhausted': - return 429; - case 'failed-precondition': - return 400; - case 'aborted': - return 409; - case 'out-of-range': - return 400; - case 'unimplemented': - return 501; - case 'internal': - return 500; - case 'unavailable': - return 503; - case 'data-loss': - return 500; - // This should never happen as long as the type system is doing its job. - default: - assertNever(this.code); - } + this.httpErrorCode = errorCodeMap[code]; } /** @hidden */ - public toJSON() { - const json: any = { - status: this.status, - message: this.message, + public toJSON(): HttpErrorWireFormat { + const { + details, + httpErrorCode: { canonicalName: status }, + message, + } = this; + + return { + ...(details === undefined ? {} : { details }), + message, + status, }; - if (!_.isUndefined(this.details)) { - json.details = this.details; - } - return json; } } @@ -472,8 +461,9 @@ export function _onCallWithOptions( error = new HttpsError('internal', 'INTERNAL'); } - const status = error.httpStatus; + const { status } = error.httpErrorCode; const body = { error: error.toJSON() }; + res.status(status).send(body); } }; From cc663231245256c444e54211911c25fbda0db3d3 Mon Sep 17 00:00:00 2001 From: Erik Macik <42585523+esmacik@users.noreply.github.com> Date: Thu, 18 Jul 2019 15:24:05 -0700 Subject: [PATCH 181/705] Adding code comments and @hidden tags for src/providers/database.ts (#531) * Adding code comments to src/providers/database.ts file * Adding an @hidden tag to properties and methods of database.DataSnapshot with an underscore before them --- src/providers/database.ts | 212 ++++++++++++++++++++++++++++++++++---- 1 file changed, 191 insertions(+), 21 deletions(-) diff --git a/src/providers/database.ts b/src/providers/database.ts index ec6969909..8a2f9453a 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -44,16 +44,26 @@ export const service = 'firebaseio.com'; const databaseURLRegex = new RegExp('https://([^.]+).firebaseio.com'); /** - * Selects a database instance that will trigger the function. - * If omitted, will pick the default database for your project. - * @param instance The Realtime Database instance to use. + * Registers a function that triggers on events from a specific + * Firebase Realtime Database instance. + * + * Use this method together with `ref` to specify the instance on which to + * watch for database events. For example: `firebase.database.instance('my-app-db-2').ref('/foo/bar')` + * + * Note that `functions.database.ref` used without `instance` watches the + * *default* instance for events. + * + * @param instance The instance name of the database instance + * to watch for write events. + * @return Firebase Realtime Database instance builder interface. */ export function instance(instance: string) { return _instanceWithOptions(instance, {}); } /** - * Select Firebase Realtime Database Reference to listen to. + * Registers a function that triggers on Firebase Realtime Database write + * events. * * This method behaves very similarly to the method of the same name in the * client and Admin Firebase SDKs. Any change to the Database that affects the @@ -65,15 +75,19 @@ export function instance(instance: string) { * 1. Cloud Functions allows wildcards in the `path` name. Any `path` component * in curly brackets (`{}`) is a wildcard that matches all strings. The value * that matched a certain invocation of a Cloud Function is returned as part - * of the `context.params` object. For example, `ref("messages/{messageId}")` - * matches changes at `/messages/message1` or `/messages/message2`, resulting - * in `context.params.messageId` being set to `"message1"` or `"message2"`, + * of the [`event.params`](functions.EventContext#params) object. For + * example, `ref("messages/{messageId}")` matches changes at + * `/messages/message1` or `/messages/message2`, resulting in + * `event.params.messageId` being set to `"message1"` or `"message2"`, * respectively. * 2. Cloud Functions do not fire an event for data that already existed before * the Cloud Function was deployed. - * 3. Cloud Function events have access to more information, including information - * about the user who triggered the Cloud Function. - * @param ref Path of the database to listen to. + * 3. Cloud Function events have access to more information, including a + * snapshot of the previous event data and information about the user who + * triggered the Cloud Function. + * + * @param path The path within the Database to watch for write events. + * @return Firebase Realtime Database builder interface. */ export function ref(path: string) { return _refWithOptions(path, {}); @@ -87,10 +101,18 @@ export function _instanceWithOptions( return new InstanceBuilder(instance, options); } +/** + * The Firebase Realtime Database instance builder interface. + * + * Access via [`functions.database.instance()`](functions.database#.instance). + */ export class InstanceBuilder { /** @hidden */ constructor(private instance: string, private options: DeploymentOptions) {} + /** + * @return Firebase Realtime Database reference builder interface. + */ ref(path: string): RefBuilder { const normalized = normalizePath(path); return new RefBuilder( @@ -130,7 +152,11 @@ export function _refWithOptions( return new RefBuilder(apps(), resourceGetter, options); } -/** Builder used to create Cloud Functions for Firebase Realtime Database References. */ +/** + * The Firebase Realtime Database reference builder interface. + * + * Access via [`functions.database.ref()`](functions.database#.ref). + */ export class RefBuilder { /** @hidden */ constructor( @@ -139,7 +165,14 @@ export class RefBuilder { private options: DeploymentOptions ) {} - /** Respond to any write that affects a ref. */ + /** + * Event handler that fires every time a Firebase Realtime Database write + * of any kind (creation, update, or delete) occurs. + * + * @param handler Event handler that runs every time a Firebase Realtime Database + * write occurs. + * @return A Cloud Function that you can export and deploy. + */ onWrite( handler: ( change: Change, @@ -149,7 +182,15 @@ export class RefBuilder { return this.onOperation(handler, 'ref.write', this.changeConstructor); } - /** Respond to update on a ref. */ + /** + * Event handler that fires every time data is updated in + * Firebase Realtime Database. + * + * @param handler Event handler which is run every time a Firebase Realtime Database + * write occurs. + * @return A Cloud + * Function which you can export and deploy. + */ onUpdate( handler: ( change: Change, @@ -159,7 +200,14 @@ export class RefBuilder { return this.onOperation(handler, 'ref.update', this.changeConstructor); } - /** Respond to new data on a ref. */ + /** + * Event handler that fires every time new data is created in + * Firebase Realtime Database. + * + * @param handler Event handler that runs every time new data is created in + * Firebase Realtime Database. + * @return A Cloud Function that you can export and deploy. + */ onCreate( handler: ( snapshot: DataSnapshot, @@ -180,7 +228,14 @@ export class RefBuilder { return this.onOperation(handler, 'ref.create', dataConstructor); } - /** Respond to all data being deleted from a ref. */ + /** + * Event handler that fires every time data is deleted from + * Firebase Realtime Database. + * + * @param handler Event handler that runs every time data is deleted from + * Firebase Realtime Database. + * @return A Cloud Function that you can export and deploy. + */ onDelete( handler: ( snapshot: DataSnapshot, @@ -259,11 +314,22 @@ export function resourceToInstanceAndPath(resource: string) { return [dbInstance, path]; } +/** + * Interface representing a Firebase Realtime Database data snapshot. + */ export class DataSnapshot { public instance: string; + + /** @hidden */ private _ref: firebase.database.Reference; + + /** @hidden */ private _path: string; + + /** @hidden */ private _data: any; + + /** @hidden */ private _childPath: string; constructor( @@ -286,7 +352,11 @@ export class DataSnapshot { this._data = data; } - /** Ref returns a reference to the database with full admin access. */ + /** + * Returns a [`Reference`](/docs/reference/admin/node/admin.database.Reference) + * to the Database location where the triggering write occurred. Has + * full read and write access. + */ get ref(): firebase.database.Reference { if (!this.app) { // may be unpopulated in user's unit tests @@ -301,11 +371,30 @@ export class DataSnapshot { return this._ref; } + /** + * The key (last part of the path) of the location of this `DataSnapshot`. + * + * The last token in a Database location is considered its key. For example, + * "ada" is the key for the `/users/ada/` node. Accessing the key on any + * `DataSnapshot` will return the key for the location that generated it. + * However, accessing the key on the root URL of a Database will return `null`. + */ get key(): string { const last = _.last(pathParts(this._fullPath())); return !last || last === '' ? null : last; } + /** + * Extracts a JavaScript value from a `DataSnapshot`. + * + * Depending on the data in a `DataSnapshot`, the `val()` method may return a + * scalar type (string, number, or boolean), an array, or an object. It may also + * return `null`, indicating that the `DataSnapshot` is empty (contains no + * data). + * + * @return The DataSnapshot's contents as a JavaScript value (Object, + * Array, string, number, boolean, or `null`). + */ val(): any { const parts = pathParts(this._childPath); const source = this._data; @@ -315,20 +404,52 @@ export class DataSnapshot { return this._checkAndConvertToArray(node); } - // TODO(inlined): figure out what to do here + /** + * Exports the entire contents of the `DataSnapshot` as a JavaScript object. + * + * The `exportVal()` method is similar to `val()`, except priority information + * is included (if available), making it suitable for backing up your data. + * + * @return The contents of the `DataSnapshot` as a JavaScript value + * (Object, Array, string, number, boolean, or `null`). + */ exportVal(): any { return this.val(); } - // TODO(inlined): figure out what to do here + /** + * Gets the priority value of the data in this `DataSnapshot`. + * + * As an alternative to using priority, applications can order collections by + * ordinary properties. See [Sorting and filtering + * data](/docs/database/web/lists-of-data#sorting_and_filtering_data). + * + * @return The priority value of the data. + */ getPriority(): string | number | null { return 0; } + /** + * Returns `true` if this `DataSnapshot` contains any data. It is slightly more + * efficient than using `snapshot.val() !== null`. + * + * @return `true` if this `DataSnapshot` contains any data; otherwise, `false`. + */ exists(): boolean { return !_.isNull(this.val()); } + /** + * Gets a `DataSnapshot` for the location at the specified relative path. + * + * The relative path can either be a simple child name (for example, "ada") or + * a deeper slash-separated path (for example, "ada/name/first"). + * + * @param path A relative path from this location to the desired child + * location. + * @return The specified child location. + */ child(childPath: string): DataSnapshot { if (!childPath) { return this; @@ -336,6 +457,25 @@ export class DataSnapshot { return this._dup(childPath); } + /** + * Enumerates the `DataSnapshot`s of the children items. + * + * Because of the way JavaScript objects work, the ordering of data in the + * JavaScript object returned by `val()` is not guaranteed to match the ordering + * on the server nor the ordering of `child_added` events. That is where + * `forEach()` comes in handy. It guarantees the children of a `DataSnapshot` + * will be iterated in their query order. + * + * If no explicit `orderBy*()` method is used, results are returned + * ordered by key (unless priorities are used, in which case, results are + * returned by priority). + * + * @param action A function that will be called for each child `DataSnapshot`. + * The callback can return `true` to cancel further enumeration. + * + * @return `true` if enumeration was canceled due to your callback + * returning `true`. + */ forEach(action: (a: DataSnapshot) => boolean): boolean { const val = this.val(); if (_.isPlainObject(val)) { @@ -347,29 +487,57 @@ export class DataSnapshot { return false; } + /** + * Returns `true` if the specified child path has (non-`null`) data. + * + * @param path A relative path to the location of a potential child. + * @return `true` if data exists at the specified child path; otherwise, + * `false`. + */ hasChild(childPath: string): boolean { return this.child(childPath).exists(); } + /** + * Returns whether or not the `DataSnapshot` has any non-`null` child + * properties. + * + * You can use `hasChildren()` to determine if a `DataSnapshot` has any + * children. If it does, you can enumerate them using `forEach()`. If it + * doesn't, then either this snapshot contains a primitive value (which can be + * retrieved with `val()`) or it is empty (in which case, `val()` will return + * `null`). + * + * @return `true` if this snapshot has any children; else `false`. + */ hasChildren(): boolean { const val = this.val(); return _.isPlainObject(val) && _.keys(val).length > 0; } + /** + * Returns the number of child properties of this `DataSnapshot`. + * + * @return Number of child properties of this `DataSnapshot`. + */ numChildren(): number { const val = this.val(); return _.isPlainObject(val) ? Object.keys(val).length : 0; } /** - * Prints the value of the snapshot; use '.previous.toJSON()' and '.current.toJSON()' to explicitly see - * the previous and current values of the snapshot. + * Returns a JSON-serializable representation of this object. + * + * @return A JSON-serializable representation of this object. */ toJSON(): Object { return this.val(); } - /* Recursive function to check if keys are numeric & convert node object to array if they are */ + /** Recursive function to check if keys are numeric & convert node object to array if they are + * + * @hidden + */ private _checkAndConvertToArray(node: any): any { if (node === null || typeof node === 'undefined') { return null; @@ -408,6 +576,7 @@ export class DataSnapshot { return obj; } + /** @hidden */ private _dup(childPath?: string): DataSnapshot { const dup = new DataSnapshot( this._data, @@ -424,6 +593,7 @@ export class DataSnapshot { return dup; } + /** @hidden */ private _fullPath(): string { const out = (this._path || '') + '/' + (this._childPath || ''); return out; From 4e043af87459ee5d1ebd627ddc2c1db4f5012ff6 Mon Sep 17 00:00:00 2001 From: Erik Macik <42585523+esmacik@users.noreply.github.com> Date: Mon, 22 Jul 2019 22:03:41 -0700 Subject: [PATCH 182/705] Adding code comments to src/providers/crashlytics.ts (#534) * Adding code comments to src/providers/crashlytics.ts from production docs * Fixing some small documentation format nits found by Diana for VelocityAlert and Issue.resolvedTime --- src/providers/crashlytics.ts | 87 +++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 21 deletions(-) diff --git a/src/providers/crashlytics.ts b/src/providers/crashlytics.ts index 89701d42f..e7a6fe882 100644 --- a/src/providers/crashlytics.ts +++ b/src/providers/crashlytics.ts @@ -33,8 +33,9 @@ export const provider = 'google.firebase.crashlytics'; export const service = 'fabric.io'; /** - * Handle events related to Crashlytics issues. An issue in Crashlytics is an - * aggregation of crashes which have a shared root cause. + * Registers a Cloud Function to handle Crashlytics issue events. + * + * @returns Crashlytics issue event builder interface. */ export function issue() { return _issueWithOptions({}); @@ -50,7 +51,7 @@ export function _issueWithOptions(options: DeploymentOptions) { }, options); } -/** Builder used to create Cloud Functions for Crashlytics issue events. */ +/** The Firebase Crashlytics issue builder interface. */ export class IssueBuilder { /** @hidden */ constructor( @@ -63,21 +64,46 @@ export class IssueBuilder { throw new Error('"onNewDetected" is now deprecated, please use "onNew"'); } - /** Handle Crashlytics New Issue events. */ + /** + * Event handler that fires every time a new issue occurs in a project. + * + * @param handler Event handler that fires every time a new issue event occurs. + * @example + * ```javascript + * exports.postOnNewIssue = functions.crashlytics.issue().onNew(event => { + * const { data } = event; + * issueId = data.issueId; + * issueTitle = data.issueTitle; + * const slackMessage = ` There's a new issue (${issueId}) ` + + * `in your app - ${issueTitle}`; + * return notifySlack(slackMessage).then(() => { + * console.log(`Posted new issue ${issueId} successfully to Slack`); + * }); + * }); + * ``` + */ onNew( handler: (issue: Issue, context: EventContext) => PromiseLike | any ): CloudFunction { return this.onEvent(handler, 'issue.new'); } - /** Handle Crashlytics Regressed Issue events. */ + /** + * Event handler that fires every time a regressed issue reoccurs in a project. + * + * @param handler Event handler that fires every time a regressed issue event occurs. + */ onRegressed( handler: (issue: Issue, context: EventContext) => PromiseLike | any ): CloudFunction { return this.onEvent(handler, 'issue.regressed'); } - /** Handle Crashlytics Velocity Alert events. */ + /** + * Event handler that fires every time a velocity alert occurs in a project. + * + * @param handler handler that fires every time a velocity alert issue event occurs. + */ onVelocityAlert( handler: (issue: Issue, context: EventContext) => PromiseLike | any ): CloudFunction { @@ -101,52 +127,71 @@ export class IssueBuilder { } /** - * Interface representing a Crashlytics issue event that was logged for a specific issue. + * Interface representing a Firebase Crashlytics event that was logged for a specific issue. */ export interface Issue { - /** Fabric Issue ID. */ + /** Crashlytics-provided issue ID. */ issueId: string; - /** Issue title. */ + /** Crashlytics-provided issue title. */ issueTitle: string; - /** App information. */ + /** AppInfo interface describing the App. */ appInfo: AppInfo; - /** When the issue was created (ISO8601 time stamp). */ + /** + * UTC when the issue occurred in ISO8601 standard representation. + * + * Example: 1970-01-17T10:52:15.661-08:00 + */ createTime: string; - /** When the issue was resolved, if the issue has been resolved (ISO8601 time stamp). */ + /** + * UTC When the issue was closed in ISO8601 standard representation. + * + * Example: 1970-01-17T10:52:15.661-08:00 + */ resolvedTime?: string; - /** Contains details about the velocity alert, if this event was triggered by a velocity alert. */ + /** Information about the velocity alert, like number of crashes and percentage of users affected by the issue. */ velocityAlert?: VelocityAlert; } +/** Interface representing Firebase Crashlytics VelocityAlert data. */ export interface VelocityAlert { - /** The percentage of sessions which have been impacted by this issue. Example: .04 */ + /** + * The percentage of sessions which have been impacted by this issue. + * + * Example: .04 + */ crashPercentage: number; /** The number of crashes that this issue has caused. */ crashes: number; } -/** - * Interface representing the application where this issue occurred. - */ +/** Interface representing Firebase Crashlytics AppInfo data. */ export interface AppInfo { - /** The app's name. Example: "My Awesome App". */ + /** + * The app's name. + * + * Example: "My Awesome App". + */ appName: string; - /** The app's platform. Examples: "android", "ios". */ + /** The app's platform. + * + * Examples: "android", "ios". + */ appPlatform: string; /** Unique application identifier within an app store, either the Android package name or the iOS bundle id. */ appId: string; /** - * The latest app version which is affected by the issue. - * Examples: "1.0", "4.3.1.1.213361", "2.3 (1824253)", "v1.8b22p6". + * The app's version name. + * + * Examples: "1.0", "4.3.1.1.213361", "2.3 (1824253)", "v1.8b22p6". */ latestAppVersion: string; } From 6e97877304441cc914302bfdff228a7f086a9262 Mon Sep 17 00:00:00 2001 From: Erik Macik <42585523+esmacik@users.noreply.github.com> Date: Mon, 22 Jul 2019 22:07:18 -0700 Subject: [PATCH 183/705] Adding code comments to analytics.ts (#535) * Adding code comments to src/providers/analytics.ts * Tweaking some things requested by Eric, 'Function' instead of 'Cloud Function', and 'ID' instead of 'id' --- src/providers/analytics.ts | 113 ++++++++++++++++++++++++------------- 1 file changed, 73 insertions(+), 40 deletions(-) diff --git a/src/providers/analytics.ts b/src/providers/analytics.ts index 457307e46..43578f8b6 100644 --- a/src/providers/analytics.ts +++ b/src/providers/analytics.ts @@ -36,8 +36,12 @@ export const provider = 'google.analytics'; export const service = 'app-measurement.com'; /** - * Select analytics events to listen to for events. - * @param analyticsEventType Name of the analytics event type. + * Registers a function to handle analytics events. + * + * @param analyticsEventType Name of the analytics event type to which + * this Cloud Function is scoped. + * + * @return Analytics event builder interface. */ export function event(analyticsEventType: string) { return _eventWithOptions(analyticsEventType, {}); @@ -61,7 +65,7 @@ export function _eventWithOptions( /** * The Firebase Analytics event builder interface. * - * Access via [`functions.analytics.event()`](functions.analytics#event). + * Access via [`functions.analytics.event()`](functions.analytics#.event). */ export class AnalyticsEventBuilder { /** @hidden */ @@ -73,12 +77,10 @@ export class AnalyticsEventBuilder { /** * Event handler that fires every time a Firebase Analytics event occurs. * - * @param {!function(!functions.Event)} - * handler Event handler that fires every time a Firebase Analytics event + * @param handler Event handler that fires every time a Firebase Analytics event * occurs. * - * @return {!functions.CloudFunction} A - * Cloud Function you can export. + * @return A function that you can export and deploy. */ onLog( handler: ( @@ -102,13 +104,11 @@ export class AnalyticsEventBuilder { } } -/** - * Interface representing a Firebase Analytics event that was logged for a specific user. - */ +/** Interface representing a Firebase Analytics event that was logged for a specific user. */ export class AnalyticsEvent { /** - * The date on which the event.was logged. - * (`YYYYMMDD` format in the registered timezone of your app). + * The date on which the event.was logged. + * (`YYYYMMDD` format in the registered timezone of your app). */ reportingDate: string; @@ -232,11 +232,9 @@ export class UserDimensions { } } -/** - * Predefined or custom properties stored on the client side. - */ +/** Predefined or custom properties stored on the client side. */ export class UserPropertyValue { - /** Last set value of a user property. */ + /** The last set value of a user property. */ value: string; /** UTC client time when the user property was last set. */ @@ -250,47 +248,55 @@ export class UserPropertyValue { } /** - * Interface representing the device that triggered these Firebase Analytics events. + * Interface representing the device that triggered these + * Firebase Analytics events. */ export interface DeviceInfo { /** * Device category. + * * Examples: "tablet" or "mobile". */ deviceCategory?: string; /** * Device brand name. + * * Examples: "Samsung", "HTC" */ mobileBrandName?: string; /** * Device model name in human-readable format. + * * Example: "iPhone 7" */ mobileModelName?: string; /** * Device marketing name. + * * Example: "Galaxy S4 Mini" */ mobileMarketingName?: string; /** * Device model, as read from the OS. + * * Example: "iPhone9,1" */ deviceModel?: string; /** * Device OS version when data capture ended. + * * Example: "4.4.2" */ platformVersion?: string; /** * Vendor specific device identifier. This is IDFV on iOS. Not used for Android. + * * Example: '599F9C00-92DC-4B5C-9464-7971F01F8370' */ deviceId?: string; @@ -313,69 +319,86 @@ export interface DeviceInfo { /** * The time zone of the device when data was uploaded, as seconds skew from UTC. - * Use this to calculate the device's local time for [`event.timestamp`](functions.Event#timestamp)`. + * Use this to calculate the device's local time for [`event.timestamp`](functions.Event#timestamp). */ deviceTimeZoneOffsetSeconds: number; /** * The device's Limit Ad Tracking setting. * When `true`, you cannot use `resettableDeviceId` for remarketing, demographics or influencing ads serving - * behaviour. However, you can use resettableDeviceId for conversion tracking and campaign attribution. + * behaviour. However, you can use `resettableDeviceId` for conversion tracking and campaign attribution. */ limitedAdTracking: boolean; } -/** - * Interface representing the geographic origin of the events. - */ +/** Interface representing the geographic origin of the events. */ export interface GeoInfo { - /** The geographic continent. Example: "Americas". */ + /** + * The geographic continent. + * + * Example: "South America". + */ continent?: string; - /** The geographic country. Example: "Brazil". */ + /** + * The geographic country. + * + * Example: "Brazil". + */ country?: string; - /** The geographic region. Example: "State of Sao Paulo". */ + /** + * The geographic region. + * + * Example: "State of Sao Paulo". + */ region?: string; - /** The geographic city. Example: "Sao Paulo". */ + /** + * The geographic city. + * + * Example: "Sao Paulo". + */ city?: string; } -/** - * Interface representing the application that triggered these events. - */ +/** Interface representing the application that triggered these events. */ export interface AppInfo { /** - * The app's version name. - * Examples: "1.0", "4.3.1.1.213361", "2.3 (1824253)", "v1.8b22p6". + * The app's version name. + * + * Examples: "1.0", "4.3.1.1.213361", "2.3 (1824253)", "v1.8b22p6". */ appVersion?: string; /** - * Unique id for this instance of the app. - * Example: "71683BF9FA3B4B0D9535A1F05188BAF3". + * Unique ID for this instance of the app. + * + * Example: "71683BF9FA3B4B0D9535A1F05188BAF3". */ appInstanceId: string; /** - * The identifier of the store that installed the app. - * Examples: "com.sec.android.app.samsungapps", "com.amazon.venezia", "com.nokia.nstore". + * The identifier of the store that installed the app. + * + * Examples: "com.sec.android.app.samsungapps", "com.amazon.venezia", "com.nokia.nstore". */ appStore?: string; - /** The app platform. Examples: "ANDROID", "IOS". */ + /** + * The app platform. + * + * Examples: "ANDROID", "IOS". + */ appPlatform: string; /** Unique application identifier within an app store. */ appId?: string; } -/** - * Interface representing the bundle in which these events were uploaded. - */ +/** Interface representing the bundle these events were uploaded to. */ export class ExportBundleInfo { - /** Monotonically increasing index for each bundle set by the Analytics SDK. */ + /** Monotonically increasing index for each bundle set by the Analytics SDK. */ bundleSequenceId: number; /** Timestamp offset (in milliseconds) between collection time and upload time. */ @@ -393,6 +416,7 @@ export class ExportBundleInfo { } } +/** @hidden */ function copyFieldTo( from: any, to: T, @@ -405,6 +429,7 @@ function copyFieldTo( } } +/** @hidden */ function copyField( from: any, to: T, @@ -414,6 +439,7 @@ function copyField( copyFieldTo(from, to, field as string, field, transform); } +/** @hidden */ function copyFields(from: any, to: T, fields: K[]): void { for (const field of fields) { copyField(from, to, field); @@ -449,10 +475,12 @@ function copyFields(from: any, to: T, fields: K[]): void { // is due to the encoding library, which renders int64 values as strings to avoid loss of precision. This // method always returns a string, similarly to avoid loss of precision, unlike the less-conservative // 'unwrapValue' method just below. +/** @hidden */ function unwrapValueAsString(wrapped: any): string { const key: string = _.keys(wrapped)[0]; return _.toString(wrapped[key]); } + // Ditto as the method above, but returning the values in the idiomatic JavaScript type (string for strings, // number for numbers): // { @@ -466,7 +494,10 @@ function unwrapValueAsString(wrapped: any): string { // purposes can be divided into 'number' versus 'string'. This method will render all the numbers as // JavaScript's 'number' type, since we prefer using idiomatic types. Note that this may lead to loss // in precision for int64 fields, so use with care. +/** @hidden */ const xValueNumberFields = ['intValue', 'floatValue', 'doubleValue']; + +/** @hidden */ function unwrapValue(wrapped: any): any { const key: string = _.keys(wrapped)[0]; const value: string = unwrapValueAsString(wrapped); @@ -476,6 +507,7 @@ function unwrapValue(wrapped: any): any { // The JSON payload delivers timestamp fields as strings of timestamps denoted in microseconds. // The JavaScript convention is to use numbers denoted in milliseconds. This method // makes it easy to convert a field of one type into the other. +/** @hidden */ function copyTimestampToMillis( from: any, to: T, @@ -490,6 +522,7 @@ function copyTimestampToMillis( // The JSON payload delivers timestamp fields as strings of timestamps denoted in microseconds. // In our SDK, we'd like to present timestamp as ISO-format strings. This method makes it easy // to convert a field of one type into the other. +/** @hidden */ function copyTimestampToString( from: any, to: T, From e5466959ea4f5a0729d6a0a241e65301f2ec29d7 Mon Sep 17 00:00:00 2001 From: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> Date: Wed, 24 Jul 2019 13:35:00 -0700 Subject: [PATCH 184/705] add timestamp to the .tgz bundle name to avoid GCF caching old versions of it (#539) --- integration_test/run_tests.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/integration_test/run_tests.sh b/integration_test/run_tests.sh index d7017af52..765e3b0d7 100755 --- a/integration_test/run_tests.sh +++ b/integration_test/run_tests.sh @@ -20,6 +20,7 @@ if [[ "${1}" == "" ]]; then fi PROJECT_ID="${1}" +TIMESTAMP=$(date +%s) TOKEN="${2}" # Directory where this script lives. @@ -34,17 +35,19 @@ function build_sdk { cd "${DIR}/.." rm -f firebase-functions-*.tgz npm run build:pack - mv firebase-functions-*.tgz integration_test/functions/firebase-functions.tgz + mv firebase-functions-*.tgz "integration_test/functions/firebase-functions-${TIMESTAMP}.tgz" } function pick_node8 { cd "${DIR}" cp package.node8.json functions/package.json + sed -i '' "s/firebase-functions.tgz/firebase-functions-${TIMESTAMP}.tgz/g" functions/package.json } function pick_node10 { cd "${DIR}" cp package.node10.json functions/package.json + sed -i '' "s/firebase-functions.tgz/firebase-functions-${TIMESTAMP}.tgz/g" functions/package.json } function install_deps { @@ -98,7 +101,7 @@ function run_tests { function cleanup { announce "Performing cleanup..." delete_all_functions - rm "${DIR}/functions/firebase-functions.tgz" + rm "${DIR}/functions/firebase-functions-*.tgz" rm "${DIR}/functions/package.json" rm -f "${DIR}/functions/firebase-debug.log" rm -rf "${DIR}/functions/lib" From 8b84bf6f3f4bc66c03aa415d0cfead6ecc962f45 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Wed, 24 Jul 2019 20:39:20 +0000 Subject: [PATCH 185/705] [firebase-release] Updated SDK for Cloud Functions to 3.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de58f1199..b50c25e99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.1.0", + "version": "3.2.0", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From a4266b7da1991eb14bd5afecbb31e709af36ed3c Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Wed, 24 Jul 2019 20:39:33 +0000 Subject: [PATCH 186/705] [firebase-release] Removed change log and reset repo after 3.2.0 release --- changelog.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 2dc41f3fa..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,2 +0,0 @@ -feature - Adds support for Test Lab triggered functions with `functions.testLab`. -fixed - Upgrade lodash dependency to resolve security vulnerability CVE-2019-10744. \ No newline at end of file From e32e8cc92993ed47fc665bbacf735e38d87cc7f4 Mon Sep 17 00:00:00 2001 From: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> Date: Wed, 24 Jul 2019 17:34:26 -0700 Subject: [PATCH 187/705] Update CODEOWNERS --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 49f7349e0..bb94688b2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @kevinajian @thechenky +* @thechenky From 381c733c0b0f1df7fe7bb4811c0eb1a191b00b27 Mon Sep 17 00:00:00 2001 From: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> Date: Thu, 1 Aug 2019 15:16:48 -0700 Subject: [PATCH 188/705] Fix sed command that breaks on linux (#545) * fix sed command that breaks on linux * fixing rm error --- integration_test/run_tests.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/integration_test/run_tests.sh b/integration_test/run_tests.sh index 765e3b0d7..903936b13 100755 --- a/integration_test/run_tests.sh +++ b/integration_test/run_tests.sh @@ -41,13 +41,19 @@ function build_sdk { function pick_node8 { cd "${DIR}" cp package.node8.json functions/package.json - sed -i '' "s/firebase-functions.tgz/firebase-functions-${TIMESTAMP}.tgz/g" functions/package.json + # we have to do the -e flag here so that it work both on linux and mac os, but that creates an extra + # backup file called package.json-e that we should clean up afterwards. + sed -i -e "s/firebase-functions.tgz/firebase-functions-${TIMESTAMP}.tgz/g" functions/package.json + rm -f functions/package.json-e } function pick_node10 { cd "${DIR}" cp package.node10.json functions/package.json - sed -i '' "s/firebase-functions.tgz/firebase-functions-${TIMESTAMP}.tgz/g" functions/package.json + # we have to do the -e flag here so that it work both on linux and mac os, but that creates an extra + # backup file called package.json-e that we should clean up afterwards. + sed -i -e "s/firebase-functions.tgz/firebase-functions-${TIMESTAMP}.tgz/g" functions/package.json + rm -f functions/package.json-e } function install_deps { @@ -101,7 +107,7 @@ function run_tests { function cleanup { announce "Performing cleanup..." delete_all_functions - rm "${DIR}/functions/firebase-functions-*.tgz" + rm "${DIR}/functions/firebase-functions-${TIMESTAMP}.tgz" rm "${DIR}/functions/package.json" rm -f "${DIR}/functions/firebase-debug.log" rm -rf "${DIR}/functions/lib" From 8057d116915ec293da0b67e07c09d8a1c3dacead Mon Sep 17 00:00:00 2001 From: Diana Tkachenko <31747099+thechenky@users.noreply.github.com> Date: Mon, 7 Oct 2019 20:03:29 +0200 Subject: [PATCH 189/705] Remove chenky from codeowners --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bb94688b2..8b1378917 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @thechenky + From a001a8537dd9704d1d1c089191ce2ab514d1e3fb Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Mon, 7 Oct 2019 13:56:45 -0700 Subject: [PATCH 190/705] Add emulator app override function (#565) --- changelog.txt | 1 + src/apps.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/changelog.txt b/changelog.txt index e69de29bb..c3be06fd4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +feature - Add a helper function for the Firebase Emulator suite. \ No newline at end of file diff --git a/src/apps.ts b/src/apps.ts index 43c8d705d..163cd4795 100644 --- a/src/apps.ts +++ b/src/apps.ts @@ -58,6 +58,7 @@ export namespace apps { export class Apps { private _refCounter: RefCounter; + private _emulatedAdminApp?: firebase.app.App; constructor() { this._refCounter = {}; @@ -105,12 +106,25 @@ export namespace apps { } get admin(): firebase.app.App { + if (this._emulatedAdminApp) { + return this._emulatedAdminApp; + } + if (this._appAlive('__admin__')) { return firebase.app('__admin__'); } return firebase.initializeApp(this.firebaseArgs, '__admin__'); } + /** + * This function allows the Firebase Emulator Suite to override the FirebaseApp instance + * used by the Firebase Functions SDK. Developers should never call this function for + * other purposes. + */ + setEmulatedAdminApp(app: firebase.app.App) { + this._emulatedAdminApp = app; + } + private get firebaseArgs() { return _.assign({}, firebaseConfig(), { credential: firebase.credential.applicationDefault(), From bf5f3b5073b2c70286265c19eeb320b3a5bd3f0c Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Thu, 10 Oct 2019 21:58:16 +0000 Subject: [PATCH 191/705] [firebase-release] Updated SDK for Cloud Functions to 3.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b50c25e99..70edbc1ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.2.0", + "version": "3.3.0", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From 3018b60c05c349ceee62b5790898a3d7468f3abf Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Thu, 10 Oct 2019 21:58:27 +0000 Subject: [PATCH 192/705] [firebase-release] Removed change log and reset repo after 3.3.0 release --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index c3be06fd4..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +0,0 @@ -feature - Add a helper function for the Firebase Emulator suite. \ No newline at end of file From c5d3a7bf986e39af22d5f74e6c4e99fd432baa79 Mon Sep 17 00:00:00 2001 From: Erik Macik <42585523+esmacik@users.noreply.github.com> Date: Thu, 24 Oct 2019 14:55:21 -0600 Subject: [PATCH 193/705] Linking to external types - docgen (#549) * First version of addTypeAliasLinks that will scan TypeDoc HTML output and insert links to external library documentation. * Updated link for UserRecord and UserInfo * Resolving a few of Hiranya's comments. Removing unecessary debugging console log, JSDOM is now imported with one line, and Map of types and links is now created from a JSON file, Still need to resolve a couple of Hiranya's comments. * Ran npm run format:fix * Resolving more of Hiranya's comments. Loading JSON file uses 'require' rather than 'read file'. JSON file is now simpler. Script only replaces fully qualified names rather than aliases in document. * Adding comment as requested by thechenky. * Updating package.json to include JSDom module required by doc generation script. --- docgen/generate-docs.js | 34 ++++++++++++++++++++++++++++++++++ docgen/type-aliases.json | 5 +++++ package.json | 1 + 3 files changed, 40 insertions(+) create mode 100644 docgen/type-aliases.json diff --git a/docgen/generate-docs.js b/docgen/generate-docs.js index 299250e64..01275ae2d 100644 --- a/docgen/generate-docs.js +++ b/docgen/generate-docs.js @@ -39,6 +39,10 @@ const contentPath = path.resolve(`${__dirname}/content-sources`); const tempHomePath = path.resolve(`${contentPath}/HOME_TEMP.md`); const devsitePath = `/docs/reference/functions/`; +const { JSDOM } = require("jsdom"); + +const typeMap = require('./type-aliases.json'); + /** * Strips path prefix and returns only filename. * @param {string} path @@ -103,6 +107,7 @@ function renameFiles() { */ function fixLinks(file) { return fs.readFile(file, 'utf8').then(data => { + data = addTypeAliasLinks(data); const flattenedLinks = data .replace(/\.\.\//g, '') .replace(/(modules|interfaces|classes)\//g, '') @@ -116,6 +121,35 @@ function fixLinks(file) { }); } +/** + * Adds links to external documentation for type aliases that + * reference an external library. + * + * @param data File data to add external library links to. + */ +function addTypeAliasLinks(data) { + const htmlDom = new JSDOM(data); + /** + * Select .tsd-signature-type because all potential external + * links will have this identifier. + */ + const fileTags = htmlDom.window.document.querySelectorAll(".tsd-signature-type"); + fileTags.forEach(tag => { + const mapping = typeMap[tag.textContent]; + if (mapping) { + console.log('Adding link to '+tag.textContent+" documentation."); + + // Add the corresponding document link to this type + const linkChild = htmlDom.window.document.createElement('a'); + linkChild.setAttribute('href', mapping); + linkChild.textContent = tag.textContent; + tag.textContent = null; + tag.appendChild(linkChild); + } + }); + return htmlDom.serialize(); +} + let tocText = ''; /** diff --git a/docgen/type-aliases.json b/docgen/type-aliases.json new file mode 100644 index 000000000..240c50269 --- /dev/null +++ b/docgen/type-aliases.json @@ -0,0 +1,5 @@ +{ + "firebase.firestore.DocumentSnapshot": "https://googleapis.dev/nodejs/firestore/latest/DocumentSnapshot.html", + "firebase.auth.UserRecord": "https://firebase.google.com/docs/reference/admin/node/admin.auth.UserRecord.html", + "firebase.auth.UserInfo": "https://firebase.google.com/docs/reference/admin/node/admin.auth.UserInfo.html" +} diff --git a/package.json b/package.json index 70edbc1ab..b9d7e31fc 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "firebase-admin": "^8.2.0", "istanbul": "^0.4.5", "js-yaml": "^3.13.1", + "jsdom": "^15.2.0", "mocha": "^6.1.4", "mock-require": "^3.0.3", "mz": "^2.7.0", From fd04eec120637477b776f4a755716d33c26ba23e Mon Sep 17 00:00:00 2001 From: egilmorez Date: Thu, 31 Oct 2019 11:28:54 -0700 Subject: [PATCH 194/705] I persist in trying to fix links. (#577) --- src/cloud-functions.ts | 2 +- src/providers/analytics.ts | 5 +++-- src/providers/database.ts | 4 ++-- src/providers/pubsub.ts | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index e8d099dca..1309aba61 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -114,7 +114,7 @@ export interface EventContext { /** * An object containing the values of the wildcards in the `path` parameter - * provided to the [`ref()`](functions.database#.ref) method for a Realtime + * provided to the [`ref()`](providers_database_.html#ref) method for a Realtime * Database trigger. Cannot be accessed while inside the handler namespace. */ params: { [option: string]: any }; diff --git a/src/providers/analytics.ts b/src/providers/analytics.ts index 43578f8b6..2d57d6f5e 100644 --- a/src/providers/analytics.ts +++ b/src/providers/analytics.ts @@ -184,7 +184,7 @@ export class UserDimensions { * A map of user properties set with the * [`setUserProperty`](https://firebase.google.com/docs/analytics/android/properties) API. * - * All values are [`UserPropertyValue`](functions.analytics.UserPropertyValue) objects. + * All values are [`UserPropertyValue`](providers_analytics_.userpropertyvalue) objects. */ userProperties: { [key: string]: UserPropertyValue }; @@ -319,7 +319,8 @@ export interface DeviceInfo { /** * The time zone of the device when data was uploaded, as seconds skew from UTC. - * Use this to calculate the device's local time for [`event.timestamp`](functions.Event#timestamp). + * Use this to calculate the device's local time for + * [`EventContext.timestamp`](cloud_functions_eventcontext.html#timestamp). */ deviceTimeZoneOffsetSeconds: number; diff --git a/src/providers/database.ts b/src/providers/database.ts index 8a2f9453a..e648be20e 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -75,7 +75,7 @@ export function instance(instance: string) { * 1. Cloud Functions allows wildcards in the `path` name. Any `path` component * in curly brackets (`{}`) is a wildcard that matches all strings. The value * that matched a certain invocation of a Cloud Function is returned as part - * of the [`event.params`](functions.EventContext#params) object. For + * of the [`EventContext.params`](cloud_functions_eventcontext.html#params object. For * example, `ref("messages/{messageId}")` matches changes at * `/messages/message1` or `/messages/message2`, resulting in * `event.params.messageId` being set to `"message1"` or `"message2"`, @@ -104,7 +104,7 @@ export function _instanceWithOptions( /** * The Firebase Realtime Database instance builder interface. * - * Access via [`functions.database.instance()`](functions.database#.instance). + * Access via [`database.instance()`](providers_database_.html#instance). */ export class InstanceBuilder { /** @hidden */ diff --git a/src/providers/pubsub.ts b/src/providers/pubsub.ts index d1a687819..aee9e6c17 100644 --- a/src/providers/pubsub.ts +++ b/src/providers/pubsub.ts @@ -112,7 +112,7 @@ export function _scheduleWithOptions( /** * The Google Cloud Pub/Sub topic builder. * - * Access via [`functions.pubsub.topic()`](functions.pubsub#.topic). + * Access via [`functions.pubsub.topic()`](providers_pubsub_.html#topic). */ export class TopicBuilder { /** @hidden */ From dd6cde1a6d4121e0a3a7bdef8a29a89c4b030f3e Mon Sep 17 00:00:00 2001 From: Kevin Jian Date: Fri, 8 Nov 2019 14:06:45 -0800 Subject: [PATCH 195/705] Update typescript in integration tests (#581) --- integration_test/package.node10.json | 2 +- integration_test/package.node8.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration_test/package.node10.json b/integration_test/package.node10.json index f45bab456..cf5eeeb25 100644 --- a/integration_test/package.node10.json +++ b/integration_test/package.node10.json @@ -14,7 +14,7 @@ }, "main": "lib/index.js", "devDependencies": { - "typescript": "~3.5.0" + "typescript": "~3.6.0" }, "engines": { "node": "10" diff --git a/integration_test/package.node8.json b/integration_test/package.node8.json index 777c11a89..8803f2fdc 100644 --- a/integration_test/package.node8.json +++ b/integration_test/package.node8.json @@ -14,7 +14,7 @@ }, "main": "lib/index.js", "devDependencies": { - "typescript": "~3.5.0" + "typescript": "~3.6.0" }, "engines": { "node": "8" From edcb35dd042cf350d50dfb618d60d0a5686e06fd Mon Sep 17 00:00:00 2001 From: egilmorez Date: Tue, 19 Nov 2019 10:01:41 -0800 Subject: [PATCH 196/705] Porting in minimal content for functions.config() reference (#582) * I persist in trying to fix links. * Adding comments for functions.config() along with a change that somehow evaded my last update to storage.ts. * Adding new entry for new config.Config.html file. * Removing circular link per review. --- docgen/content-sources/toc.yaml | 2 ++ src/config.ts | 13 +++++++++++-- src/providers/storage.ts | 4 ++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docgen/content-sources/toc.yaml b/docgen/content-sources/toc.yaml index 9ba761c15..378af2eef 100644 --- a/docgen/content-sources/toc.yaml +++ b/docgen/content-sources/toc.yaml @@ -20,6 +20,8 @@ toc: section: - title: 'Config' path: /docs/reference/functions/config_.config.html + - title: 'config.Config' + path: /docs/reference/functions/config_.config.config.html - title: 'functions.analytics' path: /docs/reference/functions/providers_analytics_.html diff --git a/src/config.ts b/src/config.ts index 9823b3746..8cd4d3dd1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -29,9 +29,18 @@ export function config(): config.Config { return config.singleton; } +/** + * Store and retrieve project configuration data such as third-party API + * keys or other settings. You can set configuration values using the + * Firebase CLI as described in + * [Environment Configuration](/docs/functions/config-env). + */ export namespace config { - // Config type is usable as a object (dot notation allowed), and firebase - // property will also code complete. + /** + * The Functions configuration interface. + * + * Access via `functions.config()`. + */ export interface Config { [key: string]: any; } diff --git a/src/providers/storage.ts b/src/providers/storage.ts index 3ba98ea72..33b2a64eb 100644 --- a/src/providers/storage.ts +++ b/src/providers/storage.ts @@ -84,7 +84,7 @@ export function _objectWithOptions(options: DeploymentOptions): ObjectBuilder { /** * The Google Cloud Storage bucket builder interface. * - * Access via [`functions.storage.bucket()`](functions.storage#.bucket). + * Access via [`functions.storage.bucket()`](providers_storage_.html#bucket). */ export class BucketBuilder { /** @hidden */ @@ -107,7 +107,7 @@ export class BucketBuilder { /** * The Google Cloud Storage object builder interface. * - * Access via [`functions.storage.object()`](functions.storage#.object). + * Access via [`functions.storage.object()`](providers_storage_.html#object). */ export class ObjectBuilder { /** @hidden */ From 8039e0286eebb054275dae6763e50e76c2e05d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Natan=20S=C4=85gol?= Date: Wed, 27 Nov 2019 22:46:27 +0100 Subject: [PATCH 197/705] Allow specifying failure policies (#482) * Define an interface for FailurePolicy * Extract functions config to avoid dependency cycles, assign failure policy to triggers * Add a changelog entry * Update dependencies, minor version bump * Reformat * Fix a typo: allows -> allow * Avoid abbreviations to improve readability * Rename remaining Opts to Options * Add tests for specifying failure policy * Reformat * Change format of an entry in the changelog * Revert version bump * Extract configuration to break dependency cycle * Add comments to Schedule and ScheduleRetryConfig * Stricten regions definition * Fix tests broken due to strict typings * More strict types for regions - reverse order * Reformat * Remove unused import * Conform with npm formatting * Reintroduce unused variable to minimize diff size * Import lodash using _ exclusively * Kepp @hidden tags in one line if no other JSDoc is present * Fix a typo - sentence ending with a comma * Wrap JSDoc as specified in Google JavaScript Style Guide * Move memory lookup table to functions-configuration and stricten type definition * Move default failure policy to functions-configuration * Separate standarization of options from construction of the options object * Rephrase failure policy description * Rephrase description of memory and timeoutSeconds options * Simplify tests for invalid failure policies * Align public description of failure policies with a documentation of its interface * Add a missing dot in the Changelog * Minor stylistic changes to the documentation * Make a test case more understandable * Reformat --- changelog.txt | 1 + spec/function-builder.spec.ts | 42 +++++++++- spec/providers/auth.spec.ts | 4 +- src/cloud-functions.ts | 73 ++++++++++------- src/function-builder.ts | 148 +++++++++++++++++++++++----------- src/function-configuration.ts | 26 ++++++ 6 files changed, 212 insertions(+), 82 deletions(-) diff --git a/changelog.txt b/changelog.txt index e69de29bb..404ffb503 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +feature - Allow specifying retry policies for event triggered functions. diff --git a/spec/function-builder.spec.ts b/spec/function-builder.spec.ts index b0c4d4c28..1ba12c7fa 100644 --- a/spec/function-builder.spec.ts +++ b/spec/function-builder.spec.ts @@ -79,14 +79,29 @@ describe('FunctionBuilder', () => { it('should allow valid runtime options to be set', () => { const fn = functions .runWith({ - timeoutSeconds: 90, + failurePolicy: { retry: {} }, memory: '256MB', + timeoutSeconds: 90, }) .auth.user() .onCreate((user) => user); expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); + expect(fn.__trigger.failurePolicy).to.deep.equal({ retry: {} }); + }); + + it("should apply a default failure policy if it's aliased with `true`", () => { + const fn = functions + .runWith({ + failurePolicy: true, + memory: '256MB', + timeoutSeconds: 90, + }) + .auth.user() + .onCreate((user) => user); + + expect(fn.__trigger.failurePolicy).to.deep.equal({ retry: {} }); }); it('should allow both supported region and valid runtime options to be set', () => { @@ -132,7 +147,26 @@ describe('FunctionBuilder', () => { functions .region('asia-northeast1') .runWith({ timeoutSeconds: 600, memory: '256MB' }); - }).to.throw(Error, 'TimeoutSeconds'); + }).to.throw(Error, 'RuntimeOptions.timeoutSeconds'); + }); + + it('should throw an error if user chooses a failurePolicy which is neither an object nor a boolean', () => { + expect(() => + functions.runWith({ + failurePolicy: (1234 as unknown) as functions.RuntimeOptions['failurePolicy'], + }) + ).to.throw( + Error, + 'RuntimeOptions.failurePolicy must be a boolean or an object' + ); + }); + + it('should throw an error if user chooses a failurePolicy.retry which is not an object', () => { + expect(() => + functions.runWith({ + failurePolicy: { retry: (1234 as unknown) as object }, + }) + ).to.throw(Error, 'RuntimeOptions.failurePolicy.retry'); }); it('should throw an error if user chooses an invalid memory allocation', () => { @@ -154,13 +188,13 @@ describe('FunctionBuilder', () => { return functions.runWith({ timeoutSeconds: 1000000, } as any); - }).to.throw(Error, 'TimeoutSeconds'); + }).to.throw(Error, 'RuntimeOptions.timeoutSeconds'); expect(() => { return functions.region('asia-east2').runWith({ timeoutSeconds: 1000000, } as any); - }).to.throw(Error, 'TimeoutSeconds'); + }).to.throw(Error, 'RuntimeOptions.timeoutSeconds'); }); it('should throw an error if user chooses an invalid region', () => { diff --git a/spec/providers/auth.spec.ts b/spec/providers/auth.spec.ts index 769bc03bf..bb2ab7761 100644 --- a/spec/providers/auth.spec.ts +++ b/spec/providers/auth.spec.ts @@ -197,9 +197,7 @@ describe('Auth Functions', () => { }); describe('#onDelete', () => { - const cloudFunctionDelete: CloudFunction< - firebase.auth.UserRecord - > = functions.handler.auth.user.onDelete( + const cloudFunctionDelete: CloudFunction = functions.handler.auth.user.onDelete( (data: firebase.auth.UserRecord) => data ); diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 1309aba61..b18c965d7 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -22,7 +22,13 @@ import { Request, Response } from 'express'; import * as _ from 'lodash'; -import { DeploymentOptions, Schedule } from './function-configuration'; +import { + DEFAULT_FAILURE_POLICY, + DeploymentOptions, + FailurePolicy, + MEMORY_LOOKUP, + Schedule, +} from './function-configuration'; export { Request, Response }; /** @hidden */ @@ -202,6 +208,7 @@ export namespace Change { if (json.fieldMask) { before = applyFieldMask(before, json.after, json.fieldMask); } + return Change.fromObjects( customizer(before || {}), customizer(json.after || {}) @@ -216,7 +223,8 @@ export namespace Change { ) { const before = _.assign({}, after); const masks = fieldMask.split(','); - _.forEach(masks, (mask) => { + + masks.forEach((mask) => { const val = _.get(sparseBefore, mask); if (typeof val === 'undefined') { _.unset(before, mask); @@ -224,6 +232,7 @@ export namespace Change { _.set(before, mask, val); } }); + return before; } } @@ -253,6 +262,7 @@ export interface TriggerAnnotated { resource: string; service: string; }; + failurePolicy?: FailurePolicy; httpsTrigger?: {}; labels?: { [key: string]: string }; regions?: string[]; @@ -312,6 +322,40 @@ export interface MakeCloudFunctionArgs { triggerResource: () => string; } +/** @hidden */ +export function optionsToTrigger({ + failurePolicy: failurePolicyOrAlias, + memory, + regions, + schedule, + timeoutSeconds, +}: DeploymentOptions): TriggerAnnotated['__trigger'] { + /* + * FailurePolicy can be aliased with a boolean value in the public API. + * Convert aliases `true` and `false` to a standardized interface. + */ + const failurePolicy: FailurePolicy | undefined = + failurePolicyOrAlias === false + ? undefined + : failurePolicyOrAlias === true + ? DEFAULT_FAILURE_POLICY + : failurePolicyOrAlias; + + const availableMemoryMb: number | undefined = + memory === undefined ? undefined : MEMORY_LOOKUP[memory]; + + const timeout: string | undefined = + timeoutSeconds === undefined ? undefined : `${timeoutSeconds}s`; + + return { + ...(failurePolicy === undefined ? {} : { failurePolicy }), + ...(availableMemoryMb === undefined ? {} : { availableMemoryMb }), + ...(regions === undefined ? {} : { regions }), + ...(schedule === undefined ? {} : { schedule }), + ...(timeout === undefined ? {} : { timeout }), + }; +} + /** @hidden */ export function makeCloudFunction({ after = () => {}, @@ -463,28 +507,3 @@ function _detectAuthType(event: Event) { } return 'UNAUTHENTICATED'; } - -/** @hidden */ -export function optionsToTrigger(options: DeploymentOptions) { - const trigger: any = {}; - if (options.regions) { - trigger.regions = options.regions; - } - if (options.timeoutSeconds) { - trigger.timeout = options.timeoutSeconds.toString() + 's'; - } - if (options.memory) { - const memoryLookup = { - '128MB': 128, - '256MB': 256, - '512MB': 512, - '1GB': 1024, - '2GB': 2048, - }; - trigger.availableMemoryMb = _.get(memoryLookup, options.memory); - } - if (options.schedule) { - trigger.schedule = options.schedule; - } - return trigger; -} diff --git a/src/function-builder.ts b/src/function-builder.ts index 580e0498b..13fd30990 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -27,6 +27,7 @@ import { CloudFunction, EventContext } from './cloud-functions'; import { DeploymentOptions, MAX_TIMEOUT_SECONDS, + MIN_TIMEOUT_SECONDS, RuntimeOptions, SUPPORTED_REGIONS, VALID_MEMORY_OPTIONS, @@ -45,28 +46,62 @@ import * as testLab from './providers/testLab'; /** * Assert that the runtime options passed in are valid. * @param runtimeOptions object containing memory and timeout information. - * @throws { Error } Memory and TimeoutSeconds values must be valid. + * @throws { Error } FailurePolicy, Memory and TimeoutSeconds values must be + * valid. */ -function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean { - if ( - runtimeOptions.memory && - !_.includes(VALID_MEMORY_OPTIONS, runtimeOptions.memory) - ) { - throw new Error( - `The only valid memory allocation values are: ${VALID_MEMORY_OPTIONS.join( - ', ' - )}` - ); +function assertRuntimeOptionsValidity(runtimeOptions: RuntimeOptions): void { + if (_.isObjectLike(runtimeOptions) === false) { + throw new Error('RuntimeOptions must be an object.'); } - if ( - runtimeOptions.timeoutSeconds > MAX_TIMEOUT_SECONDS || - runtimeOptions.timeoutSeconds < 0 - ) { - throw new Error( - `TimeoutSeconds must be between 0 and ${MAX_TIMEOUT_SECONDS}` - ); + + const { failurePolicy, memory, timeoutSeconds } = runtimeOptions; + + if (failurePolicy !== undefined) { + if ( + _.isBoolean(failurePolicy) === false && + _.isObjectLike(failurePolicy) === false + ) { + throw new Error( + `RuntimeOptions.failurePolicy must be a boolean or an object.` + ); + } + + if (typeof failurePolicy === 'object') { + if ( + _.isObjectLike(failurePolicy.retry) === false || + _.isEmpty(failurePolicy.retry) === false + ) { + throw new Error( + 'RuntimeOptions.failurePolicy.retry must be an empty object.' + ); + } + } + } + + if (memory !== undefined) { + if (_.includes(VALID_MEMORY_OPTIONS, memory) === false) { + throw new Error( + `RuntimeOptions.memory must be one of: ${VALID_MEMORY_OPTIONS.join( + ', ' + )}.` + ); + } + } + + if (timeoutSeconds !== undefined) { + if (typeof timeoutSeconds !== 'number') { + throw new Error('RuntimeOptions.timeoutSeconds must be a number.'); + } + + if ( + timeoutSeconds < MIN_TIMEOUT_SECONDS || + timeoutSeconds > MAX_TIMEOUT_SECONDS + ) { + throw new Error( + `RuntimeOptions.timeoutSeconds must be between ${MIN_TIMEOUT_SECONDS} and ${MAX_TIMEOUT_SECONDS}.` + ); + } } - return true; } /** @@ -74,16 +109,16 @@ function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean { * @param regions list of regions. * @throws { Error } Regions must be in list of supported regions. */ -function assertRegionsAreValid(regions: string[]): boolean { - if (!regions.length) { - throw new Error('You must specify at least one region'); +function assertRegionsValidity(regions: string[]): void { + if (regions.length === 0) { + throw new Error('You must specify at least one region.'); } - if (_.difference(regions, SUPPORTED_REGIONS).length) { + + if (_.difference(regions, SUPPORTED_REGIONS).length !== 0) { throw new Error( - `The only valid regions are: ${SUPPORTED_REGIONS.join(', ')}` + `The only valid regions are: ${SUPPORTED_REGIONS.join(', ')}.` ); } - return true; } /** @@ -97,23 +132,27 @@ function assertRegionsAreValid(regions: string[]): boolean { export function region( ...regions: Array ): FunctionBuilder { - if (assertRegionsAreValid(regions)) { - return new FunctionBuilder({ regions }); - } + assertRegionsValidity(regions); + + return new FunctionBuilder({ regions }); } /** * Configure runtime options for the function. * @param runtimeOptions Object with three optional fields: - * 1. memory: amount of memory to allocate to the function, possible values - * are: '128MB', '256MB', '512MB', '1GB', and '2GB'. - * 2. timeoutSeconds: timeout for the function in seconds, possible values are - * 0 to 540. + * 1. failurePolicy: failure policy of the function, with boolean `true` being + * equivalent to providing an empty retry object. + * 2. memory: amount of memory to allocate to the function, with possible + * values being '128MB', '256MB', '512MB', '1GB', and '2GB'. + * 3. timeoutSeconds: timeout for the function in seconds, with possible + * values being 0 to 540. + * + * Value must not be null. */ export function runWith(runtimeOptions: RuntimeOptions): FunctionBuilder { - if (assertRuntimeOptionsValid(runtimeOptions)) { - return new FunctionBuilder(runtimeOptions); - } + assertRuntimeOptionsValidity(runtimeOptions); + + return new FunctionBuilder(runtimeOptions); } export class FunctionBuilder { @@ -128,28 +167,40 @@ export class FunctionBuilder { * functions.region('us-east1', 'us-central1') */ region(...regions: Array): FunctionBuilder { - if (assertRegionsAreValid(regions)) { - this.options.regions = regions; - return this; - } + assertRegionsValidity(regions); + + this.options.regions = regions; + + return this; } /** * Configure runtime options for the function. * @param runtimeOptions Object with three optional fields: - * 1. memory: amount of memory to allocate to the function, possible values - * are: '128MB', '256MB', '512MB', '1GB', and '2GB'. - * 2. timeoutSeconds: timeout for the function in seconds, possible values are - * 0 to 540. + * 1. failurePolicy: failure policy of the function, with boolean `true` being + * equivalent to providing an empty retry object. + * 2. memory: amount of memory to allocate to the function, with possible + * values being '128MB', '256MB', '512MB', '1GB', and '2GB'. + * 3. timeoutSeconds: timeout for the function in seconds, with possible + * values being 0 to 540. + * + * Value must not be null. */ runWith(runtimeOptions: RuntimeOptions): FunctionBuilder { - if (assertRuntimeOptionsValid(runtimeOptions)) { - this.options = _.assign(this.options, runtimeOptions); - return this; - } + assertRuntimeOptionsValidity(runtimeOptions); + + this.options = _.assign(this.options, runtimeOptions); + + return this; } get https() { + if (this.options.failurePolicy !== undefined) { + console.warn( + 'RuntimeOptions.failurePolicy is not supported in https functions.' + ); + } + return { /** * Handle HTTP requests. @@ -161,7 +212,8 @@ export class FunctionBuilder { ) => https._onRequestWithOptions(handler, this.options), /** * Declares a callable method for clients to call using a Firebase SDK. - * @param handler A method that takes a data and context and returns a value. + * @param handler A method that takes a data and context and returns + * a value. */ onCall: ( handler: ( diff --git a/src/function-configuration.ts b/src/function-configuration.ts index ec1695ccd..f3883155b 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -32,6 +32,19 @@ export const VALID_MEMORY_OPTIONS = [ '2GB', ] as const; +/** + * A mapping of memory options to its representation in the Cloud Functions API. + */ +export const MEMORY_LOOKUP: { + [Name in typeof VALID_MEMORY_OPTIONS[number]]: number; +} = { + '128MB': 128, + '256MB': 256, + '512MB': 512, + '1GB': 1024, + '2GB': 2048, +}; + /** * Scheduler retry options. Applies only to scheduled functions. */ @@ -52,7 +65,20 @@ export interface Schedule { retryConfig?: ScheduleRetryConfig; } +export interface FailurePolicy { + retry: {}; +} + +export const DEFAULT_FAILURE_POLICY: FailurePolicy = { + retry: {}, +}; + export interface RuntimeOptions { + /** + * Failure policy of the function, with boolean `true` being equivalent to + * providing an empty retry object. + */ + failurePolicy?: FailurePolicy | boolean; /** * Amount of memory to allocate to the function. */ From 9df27f44c0c96b3dc83e2e30ee93ff45809e8374 Mon Sep 17 00:00:00 2001 From: Tina Liang Date: Mon, 13 Jan 2020 11:52:31 -0800 Subject: [PATCH 198/705] adding scheduled function to handler builder (#230) * adding scheduled function to handler builder --- spec/providers/pubsub.spec.ts | 106 +++++++++++++++++++++++----------- src/handler-builder.ts | 6 ++ src/providers/pubsub.ts | 99 ++++++++++++++++--------------- 3 files changed, 131 insertions(+), 80 deletions(-) diff --git a/spec/providers/pubsub.spec.ts b/spec/providers/pubsub.spec.ts index 53776c357..f64cd2119 100644 --- a/spec/providers/pubsub.spec.ts +++ b/spec/providers/pubsub.spec.ts @@ -311,45 +311,83 @@ describe('Pubsub Functions', () => { describe('handler namespace', () => { describe('#onPublish', () => { - it('should return an empty trigger', () => { - const result = functions.handler.pubsub.topic.onPublish(() => null); - expect(result.__trigger).to.deep.equal({}); - }); + describe('#topic', () => { + it('should return an empty trigger', () => { + const result = functions.handler.pubsub.topic.onPublish(() => null); + expect(result.__trigger).to.deep.equal({}); + }); - it('should properly handle a new-style event', () => { - const raw = new Buffer('{"hello":"world"}', 'utf8').toString('base64'); - const event = { - data: { - data: raw, - attributes: { - foo: 'bar', + it('should properly handle a new-style event', () => { + const raw = new Buffer('{"hello":"world"}', 'utf8').toString( + 'base64' + ); + const event = { + data: { + data: raw, + attributes: { + foo: 'bar', + }, }, - }, - context: { - eventId: '70172329041928', - timestamp: '2018-04-09T07:56:12.975Z', - eventType: 'google.pubsub.topic.publish', - resource: { - service: 'pubsub.googleapis.com', - name: 'projects/project1/topics/toppy', + context: { + eventId: '70172329041928', + timestamp: '2018-04-09T07:56:12.975Z', + eventType: 'google.pubsub.topic.publish', + resource: { + service: 'pubsub.googleapis.com', + name: 'projects/project1/topics/toppy', + }, }, - }, - }; - - const result = functions.handler.pubsub.topic.onPublish((data) => { - return { - raw: data.data, - json: data.json, - attributes: data.attributes, }; - }); - return expect( - result(event.data, event.context) - ).to.eventually.deep.equal({ - raw, - json: { hello: 'world' }, - attributes: { foo: 'bar' }, + const result = functions.handler.pubsub.topic.onPublish((data) => { + return { + raw: data.data, + json: data.json, + attributes: data.attributes, + }; + }); + + return expect( + result(event.data, event.context) + ).to.eventually.deep.equal({ + raw, + json: { hello: 'world' }, + attributes: { foo: 'bar' }, + }); + }); + }); + describe('#schedule', () => { + it('should return an empty trigger', () => { + const result = functions.handler.pubsub.schedule.onRun(() => null); + expect(result.__trigger).to.deep.equal({}); + }); + it('should return a handler with a proper event context', () => { + const raw = new Buffer('{"hello":"world"}', 'utf8').toString( + 'base64' + ); + const event = { + data: { + data: raw, + attributes: { + foo: 'bar', + }, + }, + context: { + eventId: '70172329041928', + timestamp: '2018-04-09T07:56:12.975Z', + eventType: 'google.pubsub.topic.publish', + resource: { + service: 'pubsub.googleapis.com', + name: 'projects/project1/topics/toppy', + }, + }, + }; + const result = functions.handler.pubsub.schedule.onRun( + (context) => context.eventId + ); + return expect(result(event.data, event.context)).to.eventually.equal( + '70172329041928' + ); }); }); }); diff --git a/src/handler-builder.ts b/src/handler-builder.ts index 22b2268aa..5d7c122d7 100644 --- a/src/handler-builder.ts +++ b/src/handler-builder.ts @@ -201,6 +201,12 @@ export class HandlerBuilder { get topic() { return new pubsub.TopicBuilder(() => null, {}); }, + /** + * Handle periodic events triggered by Cloud Scheduler. + */ + get schedule() { + return new pubsub.ScheduleBuilder(() => null, {}); + }, }; } diff --git a/src/providers/pubsub.ts b/src/providers/pubsub.ts index aee9e6c17..0f12f669c 100644 --- a/src/providers/pubsub.ts +++ b/src/providers/pubsub.ts @@ -63,52 +63,6 @@ export function _topicWithOptions( }, options); } -export function schedule(schedule: string): ScheduleBuilder { - return _scheduleWithOptions(schedule, {}); -} - -export class ScheduleBuilder { - /** @hidden */ - constructor(private options: DeploymentOptions) {} - - retryConfig(config: ScheduleRetryConfig): ScheduleBuilder { - this.options.schedule.retryConfig = config; - return this; - } - - timeZone(timeZone: string): ScheduleBuilder { - this.options.schedule.timeZone = timeZone; - return this; - } - - onRun(handler: (context: EventContext) => PromiseLike | any) { - const triggerResource = () => { - if (!process.env.GCLOUD_PROJECT) { - throw new Error('process.env.GCLOUD_PROJECT is not set.'); - } - return `projects/${process.env.GCLOUD_PROJECT}/topics`; // The CLI will append the correct topic name based on region and function name - }; - const cloudFunction = makeCloudFunction({ - contextOnlyHandler: handler, - provider, - service, - triggerResource, - eventType: 'topic.publish', - options: this.options, - labels: { 'deployment-scheduled': 'true' }, - }); - return cloudFunction; - } -} - -/** @hidden */ -export function _scheduleWithOptions( - schedule: string, - options: DeploymentOptions -): ScheduleBuilder { - return new ScheduleBuilder({ ...options, schedule: { schedule } }); -} - /** * The Google Cloud Pub/Sub topic builder. * @@ -144,6 +98,59 @@ export class TopicBuilder { } } +export function schedule(schedule: string): ScheduleBuilder { + return _scheduleWithOptions(schedule, {}); +} + +/** @hidden */ +export function _scheduleWithOptions( + schedule: string, + options: DeploymentOptions +): ScheduleBuilder { + const triggerResource = () => { + if (!process.env.GCLOUD_PROJECT) { + throw new Error('process.env.GCLOUD_PROJECT is not set.'); + } + // The CLI will append the correct topic name based on region and function name + return `projects/${process.env.GCLOUD_PROJECT}/topics`; + }; + return new ScheduleBuilder(triggerResource, { + ...options, + schedule: { schedule }, + }); +} + +export class ScheduleBuilder { + /** @hidden */ + constructor( + private triggerResource: () => string, + private options: DeploymentOptions + ) {} + + retryConfig(config: ScheduleRetryConfig): ScheduleBuilder { + this.options.schedule.retryConfig = config; + return this; + } + + timeZone(timeZone: string): ScheduleBuilder { + this.options.schedule.timeZone = timeZone; + return this; + } + + onRun(handler: (context: EventContext) => PromiseLike | any) { + const cloudFunction = makeCloudFunction({ + contextOnlyHandler: handler, + provider, + service, + triggerResource: this.triggerResource, + eventType: 'topic.publish', + options: this.options, + labels: { 'deployment-scheduled': 'true' }, + }); + return cloudFunction; + } +} + /** * Interface representing a Google Cloud Pub/Sub message. * From ae902790ecc53ee6344726c08af4255adc0de783 Mon Sep 17 00:00:00 2001 From: Kevin Jian Date: Wed, 5 Feb 2020 12:50:06 -0800 Subject: [PATCH 199/705] Update type to fix tests (#620) --- spec/fixtures/mockrequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/fixtures/mockrequest.ts b/spec/fixtures/mockrequest.ts index 2139c71ce..fff2ce1a9 100644 --- a/spec/fixtures/mockrequest.ts +++ b/spec/fixtures/mockrequest.ts @@ -68,7 +68,7 @@ export function mockFetchPublicKeys(): nock.Scope { */ export function generateIdToken(projectId: string): string { const claims = {}; - const options = { + const options: jwt.SignOptions = { audience: projectId, expiresIn: 60 * 60, // 1 hour in seconds issuer: 'https://securetoken.google.com/' + projectId, From d9fc8a6bb6e6a34e478bb6de98c64514e16ff1fa Mon Sep 17 00:00:00 2001 From: Tina Liang Date: Wed, 5 Feb 2020 14:37:44 -0800 Subject: [PATCH 200/705] Revert "Allow specifying failure policies" (#623) * Revert "Allow specifying failure policies (#482) --- changelog.txt | 1 - spec/function-builder.spec.ts | 42 +--------- src/cloud-functions.ts | 73 +++++++---------- src/function-builder.ts | 148 +++++++++++----------------------- src/function-configuration.ts | 26 ------ 5 files changed, 79 insertions(+), 211 deletions(-) diff --git a/changelog.txt b/changelog.txt index 404ffb503..e69de29bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +0,0 @@ -feature - Allow specifying retry policies for event triggered functions. diff --git a/spec/function-builder.spec.ts b/spec/function-builder.spec.ts index 1ba12c7fa..b0c4d4c28 100644 --- a/spec/function-builder.spec.ts +++ b/spec/function-builder.spec.ts @@ -79,29 +79,14 @@ describe('FunctionBuilder', () => { it('should allow valid runtime options to be set', () => { const fn = functions .runWith({ - failurePolicy: { retry: {} }, - memory: '256MB', timeoutSeconds: 90, + memory: '256MB', }) .auth.user() .onCreate((user) => user); expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); - expect(fn.__trigger.failurePolicy).to.deep.equal({ retry: {} }); - }); - - it("should apply a default failure policy if it's aliased with `true`", () => { - const fn = functions - .runWith({ - failurePolicy: true, - memory: '256MB', - timeoutSeconds: 90, - }) - .auth.user() - .onCreate((user) => user); - - expect(fn.__trigger.failurePolicy).to.deep.equal({ retry: {} }); }); it('should allow both supported region and valid runtime options to be set', () => { @@ -147,26 +132,7 @@ describe('FunctionBuilder', () => { functions .region('asia-northeast1') .runWith({ timeoutSeconds: 600, memory: '256MB' }); - }).to.throw(Error, 'RuntimeOptions.timeoutSeconds'); - }); - - it('should throw an error if user chooses a failurePolicy which is neither an object nor a boolean', () => { - expect(() => - functions.runWith({ - failurePolicy: (1234 as unknown) as functions.RuntimeOptions['failurePolicy'], - }) - ).to.throw( - Error, - 'RuntimeOptions.failurePolicy must be a boolean or an object' - ); - }); - - it('should throw an error if user chooses a failurePolicy.retry which is not an object', () => { - expect(() => - functions.runWith({ - failurePolicy: { retry: (1234 as unknown) as object }, - }) - ).to.throw(Error, 'RuntimeOptions.failurePolicy.retry'); + }).to.throw(Error, 'TimeoutSeconds'); }); it('should throw an error if user chooses an invalid memory allocation', () => { @@ -188,13 +154,13 @@ describe('FunctionBuilder', () => { return functions.runWith({ timeoutSeconds: 1000000, } as any); - }).to.throw(Error, 'RuntimeOptions.timeoutSeconds'); + }).to.throw(Error, 'TimeoutSeconds'); expect(() => { return functions.region('asia-east2').runWith({ timeoutSeconds: 1000000, } as any); - }).to.throw(Error, 'RuntimeOptions.timeoutSeconds'); + }).to.throw(Error, 'TimeoutSeconds'); }); it('should throw an error if user chooses an invalid region', () => { diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index b18c965d7..1309aba61 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -22,13 +22,7 @@ import { Request, Response } from 'express'; import * as _ from 'lodash'; -import { - DEFAULT_FAILURE_POLICY, - DeploymentOptions, - FailurePolicy, - MEMORY_LOOKUP, - Schedule, -} from './function-configuration'; +import { DeploymentOptions, Schedule } from './function-configuration'; export { Request, Response }; /** @hidden */ @@ -208,7 +202,6 @@ export namespace Change { if (json.fieldMask) { before = applyFieldMask(before, json.after, json.fieldMask); } - return Change.fromObjects( customizer(before || {}), customizer(json.after || {}) @@ -223,8 +216,7 @@ export namespace Change { ) { const before = _.assign({}, after); const masks = fieldMask.split(','); - - masks.forEach((mask) => { + _.forEach(masks, (mask) => { const val = _.get(sparseBefore, mask); if (typeof val === 'undefined') { _.unset(before, mask); @@ -232,7 +224,6 @@ export namespace Change { _.set(before, mask, val); } }); - return before; } } @@ -262,7 +253,6 @@ export interface TriggerAnnotated { resource: string; service: string; }; - failurePolicy?: FailurePolicy; httpsTrigger?: {}; labels?: { [key: string]: string }; regions?: string[]; @@ -322,40 +312,6 @@ export interface MakeCloudFunctionArgs { triggerResource: () => string; } -/** @hidden */ -export function optionsToTrigger({ - failurePolicy: failurePolicyOrAlias, - memory, - regions, - schedule, - timeoutSeconds, -}: DeploymentOptions): TriggerAnnotated['__trigger'] { - /* - * FailurePolicy can be aliased with a boolean value in the public API. - * Convert aliases `true` and `false` to a standardized interface. - */ - const failurePolicy: FailurePolicy | undefined = - failurePolicyOrAlias === false - ? undefined - : failurePolicyOrAlias === true - ? DEFAULT_FAILURE_POLICY - : failurePolicyOrAlias; - - const availableMemoryMb: number | undefined = - memory === undefined ? undefined : MEMORY_LOOKUP[memory]; - - const timeout: string | undefined = - timeoutSeconds === undefined ? undefined : `${timeoutSeconds}s`; - - return { - ...(failurePolicy === undefined ? {} : { failurePolicy }), - ...(availableMemoryMb === undefined ? {} : { availableMemoryMb }), - ...(regions === undefined ? {} : { regions }), - ...(schedule === undefined ? {} : { schedule }), - ...(timeout === undefined ? {} : { timeout }), - }; -} - /** @hidden */ export function makeCloudFunction({ after = () => {}, @@ -507,3 +463,28 @@ function _detectAuthType(event: Event) { } return 'UNAUTHENTICATED'; } + +/** @hidden */ +export function optionsToTrigger(options: DeploymentOptions) { + const trigger: any = {}; + if (options.regions) { + trigger.regions = options.regions; + } + if (options.timeoutSeconds) { + trigger.timeout = options.timeoutSeconds.toString() + 's'; + } + if (options.memory) { + const memoryLookup = { + '128MB': 128, + '256MB': 256, + '512MB': 512, + '1GB': 1024, + '2GB': 2048, + }; + trigger.availableMemoryMb = _.get(memoryLookup, options.memory); + } + if (options.schedule) { + trigger.schedule = options.schedule; + } + return trigger; +} diff --git a/src/function-builder.ts b/src/function-builder.ts index 13fd30990..580e0498b 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -27,7 +27,6 @@ import { CloudFunction, EventContext } from './cloud-functions'; import { DeploymentOptions, MAX_TIMEOUT_SECONDS, - MIN_TIMEOUT_SECONDS, RuntimeOptions, SUPPORTED_REGIONS, VALID_MEMORY_OPTIONS, @@ -46,62 +45,28 @@ import * as testLab from './providers/testLab'; /** * Assert that the runtime options passed in are valid. * @param runtimeOptions object containing memory and timeout information. - * @throws { Error } FailurePolicy, Memory and TimeoutSeconds values must be - * valid. + * @throws { Error } Memory and TimeoutSeconds values must be valid. */ -function assertRuntimeOptionsValidity(runtimeOptions: RuntimeOptions): void { - if (_.isObjectLike(runtimeOptions) === false) { - throw new Error('RuntimeOptions must be an object.'); - } - - const { failurePolicy, memory, timeoutSeconds } = runtimeOptions; - - if (failurePolicy !== undefined) { - if ( - _.isBoolean(failurePolicy) === false && - _.isObjectLike(failurePolicy) === false - ) { - throw new Error( - `RuntimeOptions.failurePolicy must be a boolean or an object.` - ); - } - - if (typeof failurePolicy === 'object') { - if ( - _.isObjectLike(failurePolicy.retry) === false || - _.isEmpty(failurePolicy.retry) === false - ) { - throw new Error( - 'RuntimeOptions.failurePolicy.retry must be an empty object.' - ); - } - } - } - - if (memory !== undefined) { - if (_.includes(VALID_MEMORY_OPTIONS, memory) === false) { - throw new Error( - `RuntimeOptions.memory must be one of: ${VALID_MEMORY_OPTIONS.join( - ', ' - )}.` - ); - } +function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean { + if ( + runtimeOptions.memory && + !_.includes(VALID_MEMORY_OPTIONS, runtimeOptions.memory) + ) { + throw new Error( + `The only valid memory allocation values are: ${VALID_MEMORY_OPTIONS.join( + ', ' + )}` + ); } - - if (timeoutSeconds !== undefined) { - if (typeof timeoutSeconds !== 'number') { - throw new Error('RuntimeOptions.timeoutSeconds must be a number.'); - } - - if ( - timeoutSeconds < MIN_TIMEOUT_SECONDS || - timeoutSeconds > MAX_TIMEOUT_SECONDS - ) { - throw new Error( - `RuntimeOptions.timeoutSeconds must be between ${MIN_TIMEOUT_SECONDS} and ${MAX_TIMEOUT_SECONDS}.` - ); - } + if ( + runtimeOptions.timeoutSeconds > MAX_TIMEOUT_SECONDS || + runtimeOptions.timeoutSeconds < 0 + ) { + throw new Error( + `TimeoutSeconds must be between 0 and ${MAX_TIMEOUT_SECONDS}` + ); } + return true; } /** @@ -109,16 +74,16 @@ function assertRuntimeOptionsValidity(runtimeOptions: RuntimeOptions): void { * @param regions list of regions. * @throws { Error } Regions must be in list of supported regions. */ -function assertRegionsValidity(regions: string[]): void { - if (regions.length === 0) { - throw new Error('You must specify at least one region.'); +function assertRegionsAreValid(regions: string[]): boolean { + if (!regions.length) { + throw new Error('You must specify at least one region'); } - - if (_.difference(regions, SUPPORTED_REGIONS).length !== 0) { + if (_.difference(regions, SUPPORTED_REGIONS).length) { throw new Error( - `The only valid regions are: ${SUPPORTED_REGIONS.join(', ')}.` + `The only valid regions are: ${SUPPORTED_REGIONS.join(', ')}` ); } + return true; } /** @@ -132,27 +97,23 @@ function assertRegionsValidity(regions: string[]): void { export function region( ...regions: Array ): FunctionBuilder { - assertRegionsValidity(regions); - - return new FunctionBuilder({ regions }); + if (assertRegionsAreValid(regions)) { + return new FunctionBuilder({ regions }); + } } /** * Configure runtime options for the function. * @param runtimeOptions Object with three optional fields: - * 1. failurePolicy: failure policy of the function, with boolean `true` being - * equivalent to providing an empty retry object. - * 2. memory: amount of memory to allocate to the function, with possible - * values being '128MB', '256MB', '512MB', '1GB', and '2GB'. - * 3. timeoutSeconds: timeout for the function in seconds, with possible - * values being 0 to 540. - * - * Value must not be null. + * 1. memory: amount of memory to allocate to the function, possible values + * are: '128MB', '256MB', '512MB', '1GB', and '2GB'. + * 2. timeoutSeconds: timeout for the function in seconds, possible values are + * 0 to 540. */ export function runWith(runtimeOptions: RuntimeOptions): FunctionBuilder { - assertRuntimeOptionsValidity(runtimeOptions); - - return new FunctionBuilder(runtimeOptions); + if (assertRuntimeOptionsValid(runtimeOptions)) { + return new FunctionBuilder(runtimeOptions); + } } export class FunctionBuilder { @@ -167,40 +128,28 @@ export class FunctionBuilder { * functions.region('us-east1', 'us-central1') */ region(...regions: Array): FunctionBuilder { - assertRegionsValidity(regions); - - this.options.regions = regions; - - return this; + if (assertRegionsAreValid(regions)) { + this.options.regions = regions; + return this; + } } /** * Configure runtime options for the function. * @param runtimeOptions Object with three optional fields: - * 1. failurePolicy: failure policy of the function, with boolean `true` being - * equivalent to providing an empty retry object. - * 2. memory: amount of memory to allocate to the function, with possible - * values being '128MB', '256MB', '512MB', '1GB', and '2GB'. - * 3. timeoutSeconds: timeout for the function in seconds, with possible - * values being 0 to 540. - * - * Value must not be null. + * 1. memory: amount of memory to allocate to the function, possible values + * are: '128MB', '256MB', '512MB', '1GB', and '2GB'. + * 2. timeoutSeconds: timeout for the function in seconds, possible values are + * 0 to 540. */ runWith(runtimeOptions: RuntimeOptions): FunctionBuilder { - assertRuntimeOptionsValidity(runtimeOptions); - - this.options = _.assign(this.options, runtimeOptions); - - return this; + if (assertRuntimeOptionsValid(runtimeOptions)) { + this.options = _.assign(this.options, runtimeOptions); + return this; + } } get https() { - if (this.options.failurePolicy !== undefined) { - console.warn( - 'RuntimeOptions.failurePolicy is not supported in https functions.' - ); - } - return { /** * Handle HTTP requests. @@ -212,8 +161,7 @@ export class FunctionBuilder { ) => https._onRequestWithOptions(handler, this.options), /** * Declares a callable method for clients to call using a Firebase SDK. - * @param handler A method that takes a data and context and returns - * a value. + * @param handler A method that takes a data and context and returns a value. */ onCall: ( handler: ( diff --git a/src/function-configuration.ts b/src/function-configuration.ts index f3883155b..ec1695ccd 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -32,19 +32,6 @@ export const VALID_MEMORY_OPTIONS = [ '2GB', ] as const; -/** - * A mapping of memory options to its representation in the Cloud Functions API. - */ -export const MEMORY_LOOKUP: { - [Name in typeof VALID_MEMORY_OPTIONS[number]]: number; -} = { - '128MB': 128, - '256MB': 256, - '512MB': 512, - '1GB': 1024, - '2GB': 2048, -}; - /** * Scheduler retry options. Applies only to scheduled functions. */ @@ -65,20 +52,7 @@ export interface Schedule { retryConfig?: ScheduleRetryConfig; } -export interface FailurePolicy { - retry: {}; -} - -export const DEFAULT_FAILURE_POLICY: FailurePolicy = { - retry: {}, -}; - export interface RuntimeOptions { - /** - * Failure policy of the function, with boolean `true` being equivalent to - * providing an empty retry object. - */ - failurePolicy?: FailurePolicy | boolean; /** * Amount of memory to allocate to the function. */ From 2940a4b5230e6469ebca3102b11d45915a3a8939 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Thu, 19 Mar 2020 10:40:58 -0700 Subject: [PATCH 201/705] Use process.env.PWD for finding .runtimeconfig.json (#634) --- CHANGELOG.md | 4 ++++ changelog.txt | 0 spec/config.spec.ts | 12 +++++++++--- src/config.ts | 15 +++++++++------ 4 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 changelog.txt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..69ffc7120 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +**IMPORTANT: Please update to this version of `firebase-functions` if you are using Node.js 10 functions, otherwise your functions will break in production.** + +- Prevents deployment and runtime issues caused by upcoming changes to Cloud Functions infrastructure for Node.js 10 functions. (Issue #630) +- Adds support for writing scheduled functions under handler namespace. diff --git a/changelog.txt b/changelog.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/spec/config.spec.ts b/spec/config.spec.ts index 64963be4e..b721dc3a5 100644 --- a/spec/config.spec.ts +++ b/spec/config.spec.ts @@ -25,6 +25,12 @@ import * as mockRequire from 'mock-require'; import { config, firebaseConfig } from '../src/config'; describe('config()', () => { + before(() => { + process.env.PWD = '/srv'; + }); + after(() => { + delete process.env.PWD; + }); afterEach(() => { mockRequire.stopAll(); delete config.singleton; @@ -33,19 +39,19 @@ describe('config()', () => { }); it('loads config values from .runtimeconfig.json', () => { - mockRequire('../../../.runtimeconfig.json', { foo: 'bar', firebase: {} }); + mockRequire('/srv/.runtimeconfig.json', { foo: 'bar', firebase: {} }); const loaded = config(); expect(loaded).to.not.have.property('firebase'); expect(loaded).to.have.property('foo', 'bar'); }); it('does not provide firebase config if .runtimeconfig.json not invalid', () => { - mockRequire('../../../.runtimeconfig.json', 'does-not-exist'); + mockRequire('/srv/.runtimeconfig.json', 'does-not-exist'); expect(firebaseConfig()).to.be.null; }); it('does not provide firebase config if .ruuntimeconfig.json has no firebase property', () => { - mockRequire('../../../.runtimeconfig.json', {}); + mockRequire('/srv/.runtimeconfig.json', {}); expect(firebaseConfig()).to.be.null; }); diff --git a/src/config.ts b/src/config.ts index 8cd4d3dd1..8925ff940 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,6 +21,7 @@ // SOFTWARE. import * as firebase from 'firebase-admin'; +import * as path from 'path'; export function config(): config.Config { if (typeof config.singleton === 'undefined') { @@ -68,9 +69,10 @@ export function firebaseConfig(): firebase.AppOptions | null { // Could have Runtime Config with Firebase in it as an ENV location or default. try { - const path = - process.env.CLOUD_RUNTIME_CONFIG || '../../../.runtimeconfig.json'; - const config = require(path); + const configPath = + process.env.CLOUD_RUNTIME_CONFIG || + path.join(process.env.PWD, '.runtimeconfig.json'); + const config = require(configPath); if (config.firebase) { return config.firebase; } @@ -92,9 +94,10 @@ function init() { } try { - const path = - process.env.CLOUD_RUNTIME_CONFIG || '../../../.runtimeconfig.json'; - const parsed = require(path); + const configPath = + process.env.CLOUD_RUNTIME_CONFIG || + path.join(process.env.PWD, '.runtimeconfig.json'); + const parsed = require(configPath); delete parsed.firebase; config.singleton = parsed; return; From 6873662f9814ed9262f0b2ac606bde1b3d1e0aea Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Thu, 19 Mar 2020 11:02:30 -0700 Subject: [PATCH 202/705] New Publish Path (#231) --- package.json | 7 +- scripts/publish-container/Dockerfile | 12 ++ scripts/publish-container/cloudbuild.yaml | 4 + scripts/publish.sh | 135 ++++++++++++++++++++++ scripts/publish/cloudbuild.yaml | 114 ++++++++++++++++++ scripts/publish/deploy_key.enc | Bin 0 -> 3505 bytes scripts/publish/hub.enc | Bin 0 -> 191 bytes scripts/publish/npmrc.enc | Bin 0 -> 185 bytes scripts/publish/twitter.json.enc | Bin 0 -> 337 bytes scripts/tweet.js | 52 +++++++++ 10 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 scripts/publish-container/Dockerfile create mode 100644 scripts/publish-container/cloudbuild.yaml create mode 100644 scripts/publish.sh create mode 100644 scripts/publish/cloudbuild.yaml create mode 100644 scripts/publish/deploy_key.enc create mode 100644 scripts/publish/hub.enc create mode 100644 scripts/publish/npmrc.enc create mode 100644 scripts/publish/twitter.json.enc create mode 100644 scripts/tweet.js diff --git a/package.json b/package.json index b9d7e31fc..35f0517b7 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/firebase/firebase-functions.git" + "url": "https://github.com/firebase/firebase-functions.git" }, "license": "MIT", "author": "Firebase Team", @@ -23,10 +23,13 @@ ], "main": "lib/index.js", "types": "lib/index.d.ts", + "publishConfig": { + "registry": "https://wombat-dressing-room.appspot.com" + }, "scripts": { "apidocs": "node docgen/generate-docs.js", "build:pack": "rm -rf lib && npm install && tsc -p tsconfig.release.json && npm pack", - "build:release": "npm install --production && npm install typescript firebase-admin && tsc -p tsconfig.release.json", + "build:release": "npm install --production && npm install --no-save typescript firebase-admin && tsc -p tsconfig.release.json", "build": "tsc -p tsconfig.release.json", "format": "prettier --check '**/*.{json,md,ts,yml,yaml}'", "format:fix": "prettier --write '**/*.{json,md,ts,yml,yaml}'", diff --git a/scripts/publish-container/Dockerfile b/scripts/publish-container/Dockerfile new file mode 100644 index 000000000..c8ba24c12 --- /dev/null +++ b/scripts/publish-container/Dockerfile @@ -0,0 +1,12 @@ +FROM node:8 + +# Install dependencies +RUN apt-get update && \ + apt-get install -y curl git jq + +# Install npm at latest. +RUN npm install --global npm@latest + +# Install hub +RUN curl -fsSL --output hub.tgz https://github.com/github/hub/releases/download/v2.13.0/hub-linux-amd64-2.13.0.tgz +RUN tar --strip-components=2 -C /usr/bin -xf hub.tgz hub-linux-amd64-2.13.0/bin/hub diff --git a/scripts/publish-container/cloudbuild.yaml b/scripts/publish-container/cloudbuild.yaml new file mode 100644 index 000000000..7a99a8cab --- /dev/null +++ b/scripts/publish-container/cloudbuild.yaml @@ -0,0 +1,4 @@ +steps: + - name: 'gcr.io/cloud-builders/docker' + args: ['build', '-t', 'gcr.io/$PROJECT_ID/package-builder', '.'] +images: ['gcr.io/$PROJECT_ID/package-builder'] diff --git a/scripts/publish.sh b/scripts/publish.sh new file mode 100644 index 000000000..a1498460e --- /dev/null +++ b/scripts/publish.sh @@ -0,0 +1,135 @@ +#!/bin/bash +set -e + +printusage() { + echo "publish.sh " + echo "REPOSITORY_ORG and REPOSITORY_NAME should be set in the environment." + echo "e.g. REPOSITORY_ORG=user, REPOSITORY_NAME=repo" + echo "" + echo "Arguments:" + echo " version: 'patch', 'minor', or 'major'." +} + +VERSION=$1 +if [[ $VERSION == "" ]]; then + printusage + exit 1 +elif [[ ! ($VERSION == "patch" || $VERSION == "minor" || $VERSION == "major") ]]; then + printusage + exit 1 +fi + +if [[ $REPOSITORY_ORG == "" ]]; then + printusage + exit 1 +fi +if [[ $REPOSITORY_NAME == "" ]]; then + printusage + exit 1 +fi + +WDIR=$(pwd) + +echo "Checking for commands..." +trap "echo 'Missing hub.'; exit 1" ERR +which hub &> /dev/null +trap - ERR + +trap "echo 'Missing node.'; exit 1" ERR +which node &> /dev/null +trap - ERR + +trap "echo 'Missing jq.'; exit 1" ERR +which jq &> /dev/null +trap - ERR +echo "Checked for commands." + +echo "Checking for Twitter credentials..." +trap "echo 'Missing Twitter credentials.'; exit 1" ERR +test -f "${WDIR}/scripts/twitter.json" +trap - ERR +echo "Checked for Twitter credentials..." + +echo "Checking for logged-in npm user..." +trap "echo 'Please login to npm using \`npm login --registry https://wombat-dressing-room.appspot.com\`'; exit 1" ERR +npm whoami --registry https://wombat-dressing-room.appspot.com +trap - ERR +echo "Checked for logged-in npm user." + +echo "Moving to temporary directory.." +TEMPDIR=$(mktemp -d) +echo "[DEBUG] ${TEMPDIR}" +cd "${TEMPDIR}" +echo "Moved to temporary directory." + +echo "Cloning repository..." +git clone "git@github.com:${REPOSITORY_ORG}/${REPOSITORY_NAME}.git" +cd "${REPOSITORY_NAME}" +echo "Cloned repository." + +echo "Making sure there is a changelog..." +if [ ! -s CHANGELOG.md ]; then + echo "CHANGELOG.md is empty. aborting." + exit 1 +fi +echo "Made sure there is a changelog." + +echo "Running npm install..." +npm install +echo "Ran npm install." + +echo "Running tests..." +npm test +echo "Ran tests." + +echo "Running publish build..." +npm run build:release +echo "Ran publish build." + +echo "Making a $VERSION version..." +npm version $VERSION +NEW_VERSION=$(jq -r ".version" package.json) +echo "Made a $VERSION version." + +echo "Making the release notes..." +RELEASE_NOTES_FILE=$(mktemp) +echo "[DEBUG] ${RELEASE_NOTES_FILE}" +echo "v${NEW_VERSION}" >> "${RELEASE_NOTES_FILE}" +echo "" >> "${RELEASE_NOTES_FILE}" +cat CHANGELOG.md >> "${RELEASE_NOTES_FILE}" +echo "Made the release notes." + +echo "Publishing to npm..." +if [[ $DRY_RUN == "" ]]; then + npm publish +else + echo "DRY RUN: running publish with --dry-run" + npm publish --dry-run +fi +echo "Published to npm." + +if [[ $DRY_RUN != "" ]]; then + echo "All other commands are mutations, and we are doing a dry run." + echo "Terminating." + exit +fi + +echo "Cleaning up release notes..." +rm CHANGELOG.md +touch CHANGELOG.md +git commit -m "[firebase-release] Removed change log and reset repo after ${NEW_VERSION} release" CHANGELOG.md +echo "Cleaned up release notes." + +echo "Pushing to GitHub..." +git push origin master --tags +echo "Pushed to GitHub." + +echo "Publishing release notes..." +hub release create --file "${RELEASE_NOTES_FILE}" "v${NEW_VERSION}" +echo "Published release notes." + +echo "Making the tweet..." +npm install --no-save twitter@1.7.1 +cp -v "${WDIR}/scripts/twitter.json" "${TEMPDIR}/${REPOSITORY_NAME}/scripts/" +node ./scripts/tweet.js ${NEW_VERSION} +echo "Made the tweet." diff --git a/scripts/publish/cloudbuild.yaml b/scripts/publish/cloudbuild.yaml new file mode 100644 index 000000000..d04fec25c --- /dev/null +++ b/scripts/publish/cloudbuild.yaml @@ -0,0 +1,114 @@ +steps: + # Decrypt the SSH key. + - name: 'gcr.io/cloud-builders/gcloud' + args: + [ + 'kms', + 'decrypt', + '--ciphertext-file=deploy_key.enc', + '--plaintext-file=/root/.ssh/id_rsa', + '--location=global', + '--keyring=${_KEY_RING}', + '--key=${_KEY_NAME}', + ] + + # Decrypt the Twitter credentials. + - name: 'gcr.io/cloud-builders/gcloud' + args: + [ + 'kms', + 'decrypt', + '--ciphertext-file=twitter.json.enc', + '--plaintext-file=twitter.json', + '--location=global', + '--keyring=${_KEY_RING}', + '--key=${_KEY_NAME}', + ] + + # Decrypt the npm credentials. + - name: 'gcr.io/cloud-builders/gcloud' + args: + [ + 'kms', + 'decrypt', + '--ciphertext-file=npmrc.enc', + '--plaintext-file=npmrc', + '--location=global', + '--keyring=${_KEY_RING}', + '--key=${_KEY_NAME}', + ] + + # Decrypt the hub (GitHub) credentials. + - name: 'gcr.io/cloud-builders/gcloud' + args: + [ + 'kms', + 'decrypt', + '--ciphertext-file=hub.enc', + '--plaintext-file=hub', + '--location=global', + '--keyring=${_KEY_RING}', + '--key=${_KEY_NAME}', + ] + + # Set up git with key and domain. + - name: 'gcr.io/cloud-builders/git' + entrypoint: 'bash' + args: + - '-c' + - | + chmod 600 /root/.ssh/id_rsa + cat </root/.ssh/config + Hostname github.com + IdentityFile /root/.ssh/id_rsa + EOF + ssh-keyscan github.com >> /root/.ssh/known_hosts + + # Clone the repository. + - name: 'gcr.io/cloud-builders/git' + args: ['clone', 'git@github.com:${_REPOSITORY_ORG}/${_REPOSITORY_NAME}'] + + # Set up the Git configuration. + - name: 'gcr.io/cloud-builders/git' + dir: '${_REPOSITORY_NAME}' + args: ['config', '--global', 'user.email', 'firebase-oss-bot@google.com'] + - name: 'gcr.io/cloud-builders/git' + dir: '${_REPOSITORY_NAME}' + args: ['config', '--global', 'user.name', 'Google Open Source Bot'] + + # Set up the Twitter credentials. + - name: 'gcr.io/$PROJECT_ID/package-builder' + entrypoint: 'cp' + args: ['-v', 'twitter.json', '${_REPOSITORY_NAME}/scripts/twitter.json'] + + # Set up the npm credentials. + - name: 'gcr.io/$PROJECT_ID/package-builder' + entrypoint: 'bash' + args: ['-c', 'cp -v npmrc ~/.npmrc'] + + # Set up the hub credentials for package-builder. + - name: 'gcr.io/$PROJECT_ID/package-builder' + entrypoint: 'bash' + args: ['-c', 'mkdir -vp ~/.config && cp -v hub ~/.config/hub'] + + # Publish the package. + - name: 'gcr.io/$PROJECT_ID/package-builder' + dir: '${_REPOSITORY_NAME}' + args: ['bash', './scripts/publish.sh', '${_VERSION}'] + env: + - 'REPOSITORY_ORG=${_REPOSITORY_ORG}' + - 'REPOSITORY_NAME=${_REPOSITORY_NAME}' + - 'DRY_RUN=${_DRY_RUN}' + +options: + volumes: + - name: 'ssh' + path: /root/.ssh + +substitutions: + _VERSION: '' + _DRY_RUN: '' + _KEY_RING: 'npm-publish-keyring' + _KEY_NAME: 'publish' + _REPOSITORY_ORG: 'firebase' + _REPOSITORY_NAME: 'firebase-functions' diff --git a/scripts/publish/deploy_key.enc b/scripts/publish/deploy_key.enc new file mode 100644 index 0000000000000000000000000000000000000000..127551f08a284b04645c1979c5e7582ff6d87cc2 GIT binary patch literal 3505 zcmV;i4Nme3Bmgb?`&t@CANF6;R;o>u7LKpmI5M&PT05}Z%Fvpli)}500uqQD0I@1* zqHJtSSX}$k?Rd7LKh&;a=vnd$={xB*q> zA&%j))nL-jB=2Pj(Wz1seZ^pKpHctdh-YHXB0r^La?C?~Mf9%}Ccsy^HCF zOb#!mG@W>%81NyFz!emyLQSG!Uddt)!yBRud|uy3``LlrL%;ZxRwax>PWc|VwwAFv z9OMa-Z9qqPEP|7LJu|tL`CKbh#O-KYZ2NW|tK~YPfT})dHaPHK#q>)nGZvxUV3?sg z(y{|s@DGBI4I2S0?@HJA*}p?tQT_02T_90NWwFME7-!aglgC0xKGiF?>tLU`mY$-@ zYl4i49l`mxevU$MQ51F82-l~~^2ci>=1!x1&Gr~KL&r69Z&ci^AJR^Rug*fdq8!F2 zvak|`Mi^klfVOdMIZH`>QPOo@2m^$C6=PwK|1+NC=iMZ}fMqrR;#b{dcw~YygKR_x zJ-NodT=TW8sb1C()1P?fZh6N9F^w|7@#8UifxZqLNsdPq@%;0u^K(Ae;M>JnAL1Sj zw%WcTocg@=05m^ixPm)pc~|YYLG8>kpRiejk7Z7^jnhVJF#h`%a~m>sMTZwih(#a^ zFy6_w{alNDyjwd9!NG@h#_pnsrgt&olf_5f3jkB4JPKk2HlEp%P@REPmRRLoYIQh$ zq(W3tr#&hus>2>|8z`zMxir(_zRWt6o+r(01v4P9bZRLJR`MGQMhHxZ8|L!jY`!~y zup{RtT&1eO^D`$qDX*%l1f8gaozR}|M+W}PSDVT)UkYCPMPMn*VbHK-?JeQO#e72- zi(o|L&s1b;-A8_)o$c#5Dn~m|tc67XXD9p{KCw6KabQCa6n{~4!%!OFGi$RER`}lb zp7wu)GL#sd79Z+Fj4lZ~cBhBAyV$=p)w1BnpIv1Y%+8{j*y>p;TQxlo9XbJ-dO$(z{FE2P6Z4-A2afjbAhPH&+>TXEfKrAxn)J0 zU>>UZyy@&BtJvRa8UOnOKlI9jQ*q3aSOy3OvZL2$dK z%PrOHR3^w^AO}8zlJz&VnII*NcS|L5AUq^QCb72fo!+YJ3!TD2pPk72$j}k+F)30CK5yHBPVm5Qu*Z`iKlVd4I+_+Z< zMjr6TP;kDgh5E+lkVIRzO>Y^L+}uv_WSmd1|*{CSVxYAM8o1ce0Gg3FU^@i zW5?gDOW3=>oI{o&@rcAZj+bwetel^sIqlf5vMInpHVA zA%ag{Tag1RdhuFY;9%^WbI*XZnel2VjPR=Sn@rH@Ap9?ODfff)c?i_YXE$w&i(f0l zwc*&T=X0E6eea)*?yo9@;bM=S!#GUs&lGUOaprtq;M^}kG<>l{3pT%o(f+C?Z!K4% zH%@F=RfezqRr=pJXw(x=puymA-n%mrFKx&ciEu}GE{yK0zAUsX4!h!?(%|D?cqW%D zaI)ZgAOg|&m$RSDQ0&wFmq#m%^6@7iSK#DmB76DU5nR3H6d!XyGF^j^ue7!~nOguMEA=l6h@uAsSL_S$%w`)dABi;=Q)a?I##7)Gx=$0(#Yz^@-2 zP}wb;T_!c>lO!ob%$VdSVSU$KE(~6vxAPJ~j`9I`$z4%%W!$TdVL@;qr7T6S!S@9= zNl;;06mDnka*i}jPE+TDX%%r=SE8CYW|ek`0Cpchp;Hm`>uyH!YNEhvbsze$S2=yp z8s=oVffyvYx+W>-{)@+dI_l-($R5pZWs~mL#^u7B;Gs$WSCc(wg`V#=seLPw6WYZ) zYUIJ1CfeJ^4+LM&!aaDDAP}?wt5w!OrzWfiJ}5Od){VA<#=G~0^9*JV%Ni9__nHB2 zkQ^%e*>!@ADxVe?f&xrC;_zUa`8BmXoJ?k_@?R0us*4s~m{(*lpf+#dO&k<^r~n*< z&<|y!znIMP#nt$PXi^*)<+;YHFSz=^w~JMwY;sL1k%Trl>;-*2C%Shwlv5V*L(W&5 zXZ-|v=@#Ye(O|~3?y{zGxa8oPt62rI5fC+S3*`2r|9LDt78?Z1m~^xYa<;VxzSGTY zM3S4T^Y@)f3!tL)?;FEB8ZkSg`;qQfwe7=xZ2?gJ0LL2xSk5ridpLYex&D;fYiViq zn+v(hY+#sHFYH2Dg$J}Fg1DI-)?be{T)hS+69w!*k@cOHuFI;!Unr)S`lV;iy1w=M zscVhlRjt0FKRnUSLc`2sRIZlBtqdpjTB1dPVsNGpW`c=Uv>%bFjVEppPHOqhH~-l^ zOIyetr4Qa~BW3IlHn(es;M5$k28lm1D#B-7>!g^ROUx7vCO*Z!>9H3KR))r9#Ez05 z9jDV8==JApZeN`V(M^%TyGfccf42us*()JpzY%OO{Vj=OoWK$yyzJA=c;5pG3R_ea z?f^pQ(rx3efxj@J0KHk-YVE@?K^L&uXiu5zcq(}?BZu5pIQ2y!MUx^t-SZJ8oB+s> z*j`FmRguK31g^$7Ln0D6Pp&|@4q0`qy%R)>YbY&31G5EPZ#~3JRBKB^_L1Kj_ f4Ub|Hgeu)7h18+LKzH?pr3a9uFy{Epm(_|36Y1*QWJQfJT0MJ9zwMi%Kd3wn#dG_QbRhG-GIt!<8_$jzFOm|VQ-rR?2l}i2{p^L{_ tc<&{%BIKp@wGJNlQ#WkDi)Ew#{#^Qs@9VYVi;<-_|HPcgwp_ON(Ql0_WTgNA literal 0 HcmV?d00001 diff --git a/scripts/publish/npmrc.enc b/scripts/publish/npmrc.enc new file mode 100644 index 0000000000000000000000000000000000000000..da8ea49bb39b447cf82e476e2fbe5cbea9c73ab2 GIT binary patch literal 185 zcmV;q07m}`Bmgb?`&x`B3vTk#mH`EO%n0vbM?y@JnAJ literal 0 HcmV?d00001 diff --git a/scripts/publish/twitter.json.enc b/scripts/publish/twitter.json.enc new file mode 100644 index 0000000000000000000000000000000000000000..82123a04d0ff081fb0e72b4ef55875715afc4a65 GIT binary patch literal 337 zcmV-X0j~ZEBmgb?`&w^yETddISLST9*Q73S3M0HD5u5QbgL|PqvifMB4-%*X0I@1* zqFru1Q`}3f;Ld){D@!s0@HGRmE3-;7)M2ircF{PyxjTknwRwMPZ9!f~Sxs)bbsh+J zE$e6}?hEB5or0?@>f=G5N#*klB1NbA$$(9y$E;0PSaEG%6=El1c4m^(o=8eGQkwhE zbx=AC2;_=16pQ2uAPJH6pLMi|Bf6za61;86Z|Mun0Z4f%*&wt5QAvta;5 jz>LyJ^e{Dl?7J^g|EMlTm%vy}Hf(R(%iC3z{0i>Dxt*iy literal 0 HcmV?d00001 diff --git a/scripts/tweet.js b/scripts/tweet.js new file mode 100644 index 000000000..be6229574 --- /dev/null +++ b/scripts/tweet.js @@ -0,0 +1,52 @@ +"use strict"; + +const fs = require("fs"); +const Twitter = require("twitter"); + +function printUsage() { + console.error( + ` +Usage: tweet.js + +Credentials must be stored in "twitter.json" in this directory. + +Arguments: + - version: Version of module that was released. e.g. "1.2.3" +` + ); + process.exit(1); +} + +function getUrl(version) { + return `https://github.com/firebase/firebase-functions/releases/tag/v${version}`; +} + +if (process.argv.length !== 3) { + console.error("Missing arguments."); + printUsage(); +} + +const version = process.argv.pop(); +if (!version.match(/^\d+\.\d+\.\d+$/)) { + console.error(`Version "${version}" not a version number.`); + printUsage(); +} + +if (!fs.existsSync(`${__dirname}/twitter.json`)) { + console.error("Missing credentials."); + printUsage(); +} +const creds = require("./twitter.json"); + +const client = new Twitter(creds); + +client.post( + "statuses/update", + { status: `v${version} of @Firebase SDK for Cloud Functions is available. Release notes: ${getUrl(version)}` }, + (err) => { + if (err) { + console.error(err); + process.exit(1); + } + } +); From f41bdaa9bd4adc479015ed9df2102a3c1bafc8bf Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Thu, 19 Mar 2020 18:10:16 +0000 Subject: [PATCH 203/705] 3.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 35f0517b7..3fe70ac72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.3.0", + "version": "3.4.0", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From 76bfc9c7563dc81dc65b9a3d90edbac9c0198ecf Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Thu, 19 Mar 2020 18:10:21 +0000 Subject: [PATCH 204/705] [firebase-release] Removed change log and reset repo after 3.4.0 release --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69ffc7120..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +0,0 @@ -**IMPORTANT: Please update to this version of `firebase-functions` if you are using Node.js 10 functions, otherwise your functions will break in production.** - -- Prevents deployment and runtime issues caused by upcoming changes to Cloud Functions infrastructure for Node.js 10 functions. (Issue #630) -- Adds support for writing scheduled functions under handler namespace. From bf52fa3b85ef4210e04074afcd401c943879cdac Mon Sep 17 00:00:00 2001 From: efi shtain Date: Thu, 19 Mar 2020 22:50:58 +0200 Subject: [PATCH 205/705] add support for maxInstances in RuntimeOptions (#624) Co-authored-by: joehan --- src/cloud-functions.ts | 4 ++++ src/function-configuration.ts | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 1309aba61..ec2d64779 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -486,5 +486,9 @@ export function optionsToTrigger(options: DeploymentOptions) { if (options.schedule) { trigger.schedule = options.schedule; } + + if (options.maxInstances) { + trigger.maxInstances = options.maxInstances; + } return trigger; } diff --git a/src/function-configuration.ts b/src/function-configuration.ts index ec1695ccd..215ea81a8 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -61,6 +61,11 @@ export interface RuntimeOptions { * Timeout for the function in seconds, possible values are 0 to 540. */ timeoutSeconds?: number; + + /** + * Max number of actual instances allowed to be running in parallel + */ + maxInstances?: number; } export interface DeploymentOptions extends RuntimeOptions { From 9e05b7f4cf663e104c059e56b33ee0174d8dc086 Mon Sep 17 00:00:00 2001 From: joehan Date: Thu, 19 Mar 2020 15:14:55 -0700 Subject: [PATCH 206/705] Add entry for maxInstances (#636) * add entry for maxInstances * prettier --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..166198e9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Adds support for maxInstances. From df543dc04d14441ce65e82d342f1377f859ca758 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Fri, 20 Mar 2020 10:49:45 -0700 Subject: [PATCH 207/705] Update dependencies to fix TS build issue (#638) --- CHANGELOG.md | 12 +++++++++++- package.json | 8 ++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 166198e9a..c459e5afc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,11 @@ -- Adds support for maxInstances. +- Adds support for defining max number of instances for a function. Example: + +``` +functions.runWith({ + maxInstances: 10 +}).https.onRequest(...); +``` + +Learn more about max instances in the [Google Cloud documentation.](https://cloud.google.com/functions/docs/max-instances) + +- Fixes TypeScript build error when `package-lock.json` is present by updating dependencies (Issue #637). diff --git a/package.json b/package.json index 3fe70ac72..20f7c8c38 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "test": "mocha" }, "dependencies": { - "@types/express": "^4.17.0", + "@types/express": "^4.17.3", "cors": "^2.8.5", "express": "^4.17.1", "jsonwebtoken": "^8.5.1", @@ -61,7 +61,7 @@ "firebase-admin": "^8.2.0", "istanbul": "^0.4.5", "js-yaml": "^3.13.1", - "jsdom": "^15.2.0", + "jsdom": "^16.2.1", "mocha": "^6.1.4", "mock-require": "^3.0.3", "mz": "^2.7.0", @@ -73,9 +73,9 @@ "tslint-config-prettier": "^1.18.0", "tslint-no-unused-expression-chai": "^0.1.4", "tslint-plugin-prettier": "^2.0.1", - "typedoc": "^0.14.2", + "typedoc": "^0.17.1", "typescript": "^3.5.2", - "yargs": "^13.2.4" + "yargs": "^15.3.1" }, "peerDependencies": { "firebase-admin": "^8.0.0" From 15bf0da3b489c4a31f97bc171aa9b2ef251c4abd Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Fri, 20 Mar 2020 11:16:18 -0700 Subject: [PATCH 208/705] Update CHANGELOG (#639) --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c459e5afc..5b7b4bfe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ - Adds support for defining max number of instances for a function. Example: -``` -functions.runWith({ - maxInstances: 10 -}).https.onRequest(...); -``` + ``` + functions.runWith({ + maxInstances: 10 + }).https.onRequest(...); + ``` -Learn more about max instances in the [Google Cloud documentation.](https://cloud.google.com/functions/docs/max-instances) + Learn more about max instances in the [Google Cloud documentation.](https://cloud.google.com/functions/docs/max-instances) - Fixes TypeScript build error when `package-lock.json` is present by updating dependencies (Issue #637). From 1ed73456250333274653f874246433f3df89e7d4 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 20 Mar 2020 18:18:21 +0000 Subject: [PATCH 209/705] 3.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 20f7c8c38..0884f7af2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.4.0", + "version": "3.5.0", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From 0921c78ea48c4aee260fa687b8e9c96baa916120 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 20 Mar 2020 18:18:26 +0000 Subject: [PATCH 210/705] [firebase-release] Removed change log and reset repo after 3.5.0 release --- CHANGELOG.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7b4bfe2..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +0,0 @@ -- Adds support for defining max number of instances for a function. Example: - - ``` - functions.runWith({ - maxInstances: 10 - }).https.onRequest(...); - ``` - - Learn more about max instances in the [Google Cloud documentation.](https://cloud.google.com/functions/docs/max-instances) - -- Fixes TypeScript build error when `package-lock.json` is present by updating dependencies (Issue #637). From 5250110912dbe9e5ff00debe542e33e30089f1a8 Mon Sep 17 00:00:00 2001 From: Martin H Date: Mon, 23 Mar 2020 21:32:01 +0100 Subject: [PATCH 211/705] Add support for europe-west3 region. (#627) --- spec/function-builder.spec.ts | 2 ++ src/function-configuration.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/spec/function-builder.spec.ts b/spec/function-builder.spec.ts index b0c4d4c28..0044e5661 100644 --- a/spec/function-builder.spec.ts +++ b/spec/function-builder.spec.ts @@ -59,6 +59,7 @@ describe('FunctionBuilder', () => { 'us-east4', 'europe-west1', 'europe-west2', + 'europe-west3', 'asia-east2', 'asia-northeast1' ) @@ -71,6 +72,7 @@ describe('FunctionBuilder', () => { 'us-east4', 'europe-west1', 'europe-west2', + 'europe-west3', 'asia-east2', 'asia-northeast1', ]); diff --git a/src/function-configuration.ts b/src/function-configuration.ts index 215ea81a8..05d4517a8 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -7,6 +7,7 @@ export const SUPPORTED_REGIONS = [ 'us-east4', 'europe-west1', 'europe-west2', + 'europe-west3', 'asia-east2', 'asia-northeast1', ] as const; From 468455d6e6b9c7712e0615f667592ff207a6df69 Mon Sep 17 00:00:00 2001 From: egilmorez Date: Wed, 25 Mar 2020 16:16:33 -0700 Subject: [PATCH 212/705] Updating docs TOC with Testlab paths. (#643) --- docgen/content-sources/toc.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docgen/content-sources/toc.yaml b/docgen/content-sources/toc.yaml index 378af2eef..7b0b70347 100644 --- a/docgen/content-sources/toc.yaml +++ b/docgen/content-sources/toc.yaml @@ -116,3 +116,15 @@ toc: path: /docs/reference/functions/providers_storage_.objectbuilder.html - title: 'ObjectMetadata' path: /docs/reference/functions/providers_storage_.objectmetadata.html + + - title: 'functions.testLab' + path: /docs/reference/functions/providers_testlab_.html + section: + - title: 'testLab.clientInfo' + path: /docs/reference/functions/providers_testlab_.clientinfo.html + - title: 'testLab.resultStorage' + path: /docs/reference/functions/providers_testlab_.resultstorage.html + - title: 'testLab.testMatrix' + path: /docs/reference/functions/providers_testlab_.testmatrix.html + - title: 'testLab.testMatrixBuilder' + path: /docs/reference/functions/providers_testlab_.testmatrixbuilder.html From e1df8236604c37cee7486209c08f0eb9c291ab77 Mon Sep 17 00:00:00 2001 From: egilmorez Date: Tue, 31 Mar 2020 14:23:10 -0700 Subject: [PATCH 213/705] Adding testlab event to eventTypes list (#649) --- src/cloud-functions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index ec2d64779..ea4acd7a2 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -104,11 +104,12 @@ export interface EventContext { * * `providers/cloud.firestore/eventTypes/document.update` * * `providers/cloud.firestore/eventTypes/document.delete` * * `google.pubsub.topic.publish` + * * `google.firebase.remoteconfig.update` * * `google.storage.object.finalize` * * `google.storage.object.archive` * * `google.storage.object.delete` * * `google.storage.object.metadataUpdate` - * * `google.firebase.remoteconfig.update` + * * `google.testing.testMatrix.complete` */ eventType: string; From 7f4c9572cb96881f9b0be7521799bcf6e73e42a1 Mon Sep 17 00:00:00 2001 From: Sebastian Kreft Date: Tue, 31 Mar 2020 18:29:20 -0300 Subject: [PATCH 214/705] Enable users to define async HTTP functions (#651) Fixes #606 --- src/providers/https.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/https.ts b/src/providers/https.ts index d941f67c8..36077affd 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -60,7 +60,7 @@ export function _onRequestWithOptions( ): HttpsFunction { // lets us add __trigger without altering handler: const cloudFunction: any = (req: Request, res: express.Response) => { - handler(req, res); + return handler(req, res); }; cloudFunction.__trigger = _.assign(optionsToTrigger(options), { httpsTrigger: {}, From 95d4a4a5b3850ddd69b14284f6823ec7cd66c36a Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Tue, 31 Mar 2020 15:56:48 -0700 Subject: [PATCH 215/705] Update CHANGELOG.md (#640) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..9b9881949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +- Adds support for europe-west3 region (e.g. `functions.region("europe-west3")`). +- Adds support for async HTTP functions (Issue #606). From c9a3a0e06545171f3cc83361526773de44166a95 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Tue, 31 Mar 2020 23:02:56 +0000 Subject: [PATCH 216/705] 3.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0884f7af2..3fab11e33 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.5.0", + "version": "3.6.0", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From d3e8951e151102e2b1ccbee7e826a31c014712ed Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Tue, 31 Mar 2020 23:03:02 +0000 Subject: [PATCH 217/705] [firebase-release] Removed change log and reset repo after 3.6.0 release --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b9881949..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +0,0 @@ -- Adds support for europe-west3 region (e.g. `functions.region("europe-west3")`). -- Adds support for async HTTP functions (Issue #606). From 2784d5dad6d48215e024aae4678a99a317f6e3dd Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Fri, 24 Apr 2020 10:20:17 -0700 Subject: [PATCH 218/705] Update TypeScript dependency to v3.8 to fix build issues (Issue #667) (#668) --- CHANGELOG.md | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..e5a92e2f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Update TypeScript dependency to v.3.8 to fix build issues (Issue #667) diff --git a/package.json b/package.json index 3fab11e33..13c1e10c2 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "tslint-no-unused-expression-chai": "^0.1.4", "tslint-plugin-prettier": "^2.0.1", "typedoc": "^0.17.1", - "typescript": "^3.5.2", + "typescript": "^3.8.3", "yargs": "^15.3.1" }, "peerDependencies": { From 1fb57c58c15b3af9b14e6a0dab6678ebde35aa6b Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 24 Apr 2020 17:29:31 +0000 Subject: [PATCH 219/705] 3.6.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 13c1e10c2..2f45c7dc7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.6.0", + "version": "3.6.1", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From 1dde3dbc9a7c2d7caae945e43cb11b602c968331 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 24 Apr 2020 17:29:37 +0000 Subject: [PATCH 220/705] [firebase-release] Removed change log and reset repo after 3.6.1 release --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5a92e2f7..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +0,0 @@ -- Update TypeScript dependency to v.3.8 to fix build issues (Issue #667) From df35c1b4b18b7f2cd6c2d0d21090793b3b153e8b Mon Sep 17 00:00:00 2001 From: Sebastian Kreft Date: Mon, 27 Apr 2020 17:48:07 -0400 Subject: [PATCH 221/705] fix: onCreate, onUpdate and onDelete receive a DocumentQuerySnapshopt (#670) The arguments of Firestore triggers `onCreate`, `onUpdate` and `onDelete` cannot have an undefined data by definition, so we changed the arguments of those handlers to reflect that by using DocumentQuerySnapshot. fixes #659 --- src/providers/firestore.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/providers/firestore.ts b/src/providers/firestore.ts index 291d7d7f9..f91e17306 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -42,6 +42,7 @@ export const service = 'firestore.googleapis.com'; export const defaultDatabase = '(default)'; let firestoreInstance: any; export type DocumentSnapshot = firebase.firestore.DocumentSnapshot; +export type QueryDocumentSnapshot = firebase.firestore.QueryDocumentSnapshot; /** * Select the Firestore document to listen to for events. @@ -204,30 +205,30 @@ export class DocumentBuilder { /** Respond only to document updates. */ onUpdate( handler: ( - change: Change, + change: Change, context: EventContext ) => PromiseLike | any - ): CloudFunction> { + ): CloudFunction> { return this.onOperation(handler, 'document.update', changeConstructor); } /** Respond only to document creations. */ onCreate( handler: ( - snapshot: DocumentSnapshot, + snapshot: QueryDocumentSnapshot, context: EventContext ) => PromiseLike | any - ): CloudFunction { + ): CloudFunction { return this.onOperation(handler, 'document.create', snapshotConstructor); } /** Respond only to document deletions. */ onDelete( handler: ( - snapshot: DocumentSnapshot, + snapshot: QueryDocumentSnapshot, context: EventContext ) => PromiseLike | any - ): CloudFunction { + ): CloudFunction { return this.onOperation( handler, 'document.delete', From 5c18afeb2d93881c90ab3bc91cab075dcd3b3a85 Mon Sep 17 00:00:00 2001 From: Rich Hodgkins Date: Mon, 27 Apr 2020 22:57:07 +0100 Subject: [PATCH 222/705] Modify return type of DataSnapshot.forEach to `boolean | void` (#666) This is to match the Admin SDK: https://github.com/firebase/firebase-js-sdk/blob/37b98e9271c494a0fb58ca1960f8fcfaec49ade9/packages/database/src/api/DataSnapshot.ts#L137 Co-authored-by: joehan Co-authored-by: Lauren Long --- src/providers/database.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/database.ts b/src/providers/database.ts index e648be20e..78614a152 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -476,7 +476,7 @@ export class DataSnapshot { * @return `true` if enumeration was canceled due to your callback * returning `true`. */ - forEach(action: (a: DataSnapshot) => boolean): boolean { + forEach(action: (a: DataSnapshot) => boolean | void): boolean { const val = this.val(); if (_.isPlainObject(val)) { return _.some( From b1f9b5ad3d7227132cff82542c6222b6b5edcea0 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Fri, 1 May 2020 10:20:04 -0700 Subject: [PATCH 223/705] Revise docs for handler namespace (#680) * Adding top-level comment and TOC for functions.handler. (#633) * Adding top-level comment and TOC for functions.handler. * Adding edits from feedback. * Refer specifically to Firebase CLI. * Adding top-level comment and TOC for functions.handler. (#633) * Adding top-level comment and TOC for functions.handler. * Adding edits from feedback. * Refer specifically to Firebase CLI. * Pin "typedoc" version to 0.14.2 (#655) * Add docstrings for handler.firestore, handler.database, and handler.https (#652) * Document a few providers * Remove extra sentence in http function docstring * Crashlytics handler details and example formatting (#656) * Adding comments for Crashlytics handler. * Removing parens for parameters per feedback. * Eg moar handlers (#657) * Adding RC and Analytics handlers. * Adding Storage object handler. * Adding Pub/Sub and Test Lab handlers. * Adding auth handler. * Fixing typo. * Removing async to be consistent among examples. (#658) * handler functions: copy-edits in comments only (#675) * Removing async syntax from Storage handlers. (#678) Co-authored-by: egilmorez Co-authored-by: rachelsaunders <52258509+rachelsaunders@users.noreply.github.com> --- deploy_key.enc | Bin 0 -> 3497 bytes docgen/content-sources/toc.yaml | 5 + package.json | 2 +- src/handler-builder.ts | 280 ++++++++++++++++++++++++-------- 4 files changed, 215 insertions(+), 72 deletions(-) create mode 100644 deploy_key.enc diff --git a/deploy_key.enc b/deploy_key.enc new file mode 100644 index 0000000000000000000000000000000000000000..4451e042b005ebc9eda67d58b62167f4c49daf6a GIT binary patch literal 3497 zcmV;a4Oa3BBmmVf@Zrdr)Xk)=bRAGj!{RH@SFgr^td|4+9tfBW^Z~L71QLK70H3|R zajlkRxfa@rcX>I9C~NhK{A6SLG=zTUY?Gzdo%dt4TzAs$G$%Eg94vVf6hVeHEUTRE z*3NSh~uGCn|R=X)&pZv zf?JJM(&zim_<}4u;>Hix(K|O{>gBe(_wgXp%R@{hrQ+rV1!o*KY{VK7t2b;&OO%0b zmaYzO>8gq1TJ?ijsfxS#HZp~m;M*N_*G2_GihAVKTbe<7ydAJ8h^=n59%!DAR`SO9 zI@%%3U4cW>uR!+LO^cgy?Wr5k9c0wZa`$%2mq_rksc_jcK#Gg0rzlv|%?n*j^8r=* z=1h@esuK?)h)e?Js&g0w9&cLyrb(2iv8E7NpVAxHa`ULDhM7cnp|+=Smq6(1kbA?h z)<00(wd_RMcR$nxp~F0an9CGc(l({P@_*@(?19xjFA8Ogqe=}f3=KRIYMSWzIjf+mz1Z;z;`Cqg7A5G(f34LOI%u8> zMSWL|1YP0t{PxWvP4EFE8&>1UH$R*G@S4))k_+vJ3L?a-poZq&k1%3(_iDGzPaC&` zKK&yBq`IUt!!f8nKY6|X^k43jD@_TTn%fEZA?-JMa3YF$H9GE_t5FTJlu-p$xaIX_ zO#I&Yw9uDWnpp2TOFR=GTal0yJmPiQ56t=aGbJfMlpU)V(byVZv~6;_(P^0w7(4u} z6h6tr0d9i?^s8jb6Dd);+od;stGFyV4mA}EW5zNh{`njzaZZ)|jl8Ajwdo(Qv4FG1 zcnz52kZD+HR~zpox)Hr$2AJXf7mBk*&YBUN`hO0E|rGF4v#)`yhU0*o5E|UTop&t;F|cB-mt_q zI_a%0>Z>Z;+8&vzbyX28=;)gK=rb&QZ0I)kEB_TzSZ8{`;*q+#9%Tn&Zn%r*(UDfd z+GN$ahV#aSZluYmq_2#y?lpZWv|-(Bm*1Bt4t@5uQw zolniyAwn|r^qROUaFl4My9AlN_Zx7$V7Rki6=?~u1aj(kw54qpqs(i4@fxA^07=jk z^brWB_a~^XTj2E!g&bR)we}HwiKv3QhaWr6Gn%jy!b$a{bNd{TKG(Rk6|6-0r>HAm zrZlXuf)$7`%3B~i$VN;M>|~dn`=H1w5I+@!Ov46I8R-#`&5Z>poMVE9?|Ev8S0RGd z=22d>MEY>&f~^<3V6FT#y_UXD0*c9t*u=hx$o?GoIgcnnM$cdH))S?ai zrO&XTnsn?~=A7?WEnw|oPpcRYNYgMMr7nzn5!==*vK-1gu&{e+&!H4DqHd&g$N9F}{(}T_X zvd#2Q6K{eY9gM%E&=vA?ys*1Ecb{K@%&G>=W}lGbjI2rA!(~pPCKE_w zV9W{U&WNX%s!Y85W-`P~q3QhJlXkma!!ap@goj79Aj1sQZ6G>>q z&uRQNoXZ;g2HVCIy>!Wg$BK|;5lnw1b2N&#Y{^+zA$&%Vm2su`Y}^i%9x1MIH%Q0L zR&*8olK>jMSc~pFNm%Z8ks|e@O-qUwW)hocP1PFN9MadNy&w@?^`W8>Ew7+QljLeJ zsc^5%Wik4LycK}31v7!^PAXN6JpIj!_le_{qK1TxXRPP)01O!NM=O2lCrCDQTTKuC zFzsX!U;gS9k;hb!avA32c<6idpD3X z@B!9=11BW7g@_2as9@|=T{<2}#HZe#f)nE&;_ZB;$bm4WF)#95kBCA3^)t04 z;BONYb#RcC5)3Zvjs@<5sum)Yva+mV)8ha{~fjjduoPowBXJyfc^g=u#;Fm!@XL?P|0d= z-*9rNZ5|D6m_3N?LktGD0^&LxVZ#1!QH3_1AIO_Q80GGSD2nbPImcJ6qEO5bW6>05jnYPFF79=9tO}g)WU$Z~IUt-| z`Dq201@Xr=S~#DeHK%`Aj}Sru5U4ox=$8a@wA_qS4>(a#+KxYG zSmwf2+K3EvWR#+X$qXg9*=MXnse)npW!mTIza@5!9sZ(bHo+Syi~~-rYhBB)BM*>d zp^P`{TNB}XUSnS@u|?t&H61_vtnv@lMSU$uV*#QUT#K9p_AW77-?6rAN~K$f;+(DM z?k?H)S2hNDJ5yC8+gZHk^iB%>R_jFwG`QlvZ0%Lt0HLR2INg*+D+L;M7L)-!^RN6` zWJ`5Ok=RO3ie$X(J52NZGB_;WdyZu2F~$zTs%9B(ECuiK z$w2MYk-bEC&pv=xF`h8Lt3ZKQuzwqsGf8fkB;DmLNVx2h4?lFfg$9C|%9##GZ-j4WaYIL$( z^tuu2Vy@cd#f_B+yg&0mpor5N*na)M;C2hta0t=Eu>^K>6ICTb=)R~a20LWz+l%_s z)cp&MypEH3kaMsW(1gzvAAGBdT1>66{zBWsESk3qV$k>h>jjA1ir3m;f!t+_p8e{( z6aTXDX%5B`=kKo$Nzmo3)JzCm2zR+#X_I_20RGp5W&H|Ywk_aV5jxQ58Ny= z_6#3#5}8uYJ{WAIx)XK|s!CR@yM`mZk8_2IsZCBST&kWrMt?W$BSg8vF*Qp1ao7cO<(Jj!2v1 zi8dHEQhkN;x2t&`LMK#rDr43TXg<+IeF-PqQXNVEUgR_k0my%5l-t-JsPeQtD*zL` z7*5tEMlMy`EyyDB$(A)!6K+RnohyHU { ... }) + * ``` + * + * `onCall` declares a callable function for clients to call using a Firebase SDK. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.https.onCall((data, context) => { ... }) + * ``` + */ get https() { return { - /** - * Handle HTTP requests. - * @param handler A function that takes a request and response object, - * same signature as an Express app. - */ onRequest: ( handler: (req: express.Request, resp: express.Response) => void ): HttpsFunction => { @@ -52,10 +73,6 @@ export class HandlerBuilder { func.__trigger = {}; return func; }, - /** - * Declares a callable method for clients to call using a Firebase SDK. - * @param handler A method that takes a data and context and returns a value. - */ onCall: ( handler: ( data: any, @@ -69,12 +86,40 @@ export class HandlerBuilder { }; } + /** + * Create a handler for Firebase Realtime Database events. + * + * `ref.onCreate` handles the creation of new data. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.database.ref.onCreate((snap, context) => { ... }) + * ``` + * + * `ref.onUpdate` handles updates to existing data. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.database.ref.onUpdate((change, context) => { ... }) + * ``` + + * `ref.onDelete` handles the deletion of existing data. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.database.ref.onDelete((snap, context) => { ... }) + * ``` + + * `ref.onWrite` handles the creation, update, or deletion of data. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.database.ref.onWrite((change, context) => { ... }) + * ``` + */ get database() { return { - /** - * Selects a database instance that will trigger the function. - * If omitted, will pick the default database for your project. - */ + /** @hidden */ get instance() { return { get ref() { @@ -82,41 +127,47 @@ export class HandlerBuilder { }, }; }, - - /** - * Select Firebase Realtime Database Reference to listen to. - * - * This method behaves very similarly to the method of the same name in the - * client and Admin Firebase SDKs. Any change to the Database that affects the - * data at or below the provided `path` will fire an event in Cloud Functions. - * - * There are three important differences between listening to a Realtime - * Database event in Cloud Functions and using the Realtime Database in the - * client and Admin SDKs: - * 1. Cloud Functions allows wildcards in the `path` name. Any `path` component - * in curly brackets (`{}`) is a wildcard that matches all strings. The value - * that matched a certain invocation of a Cloud Function is returned as part - * of the `context.params` object. For example, `ref("messages/{messageId}")` - * matches changes at `/messages/message1` or `/messages/message2`, resulting - * in `context.params.messageId` being set to `"message1"` or `"message2"`, - * respectively. - * 2. Cloud Functions do not fire an event for data that already existed before - * the Cloud Function was deployed. - * 3. Cloud Function events have access to more information, including information - * about the user who triggered the Cloud Function. - */ get ref() { return new database.RefBuilder(apps(), () => null, {}); }, }; } + /** + * Create a handler for Cloud Firestore events. + * + * `document.onCreate` handles the creation of new documents. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.firestore.document.onCreate((snap, context) => { ... }) + * ``` + + * `document.onUpdate` handles updates to existing documents. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.firestore.document.onUpdate((change, context) => { ... }) + * ``` + + * `document.onDelete` handles the deletion of existing documents. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.firestore.document.onDelete((snap, context) => + * { ... }) + * ``` + + * `document.onWrite` handles the creation, update, or deletion of documents. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.firestore.document.onWrite((change, context) => + * { ... }) + * ``` + */ get firestore() { return { - /** - * Listen for events on a Firestore document. A Firestore document contains a set of - * key-value pairs and may contain subcollections and nested objects. - */ get document() { return new firestore.DocumentBuilder(() => null, {}); }, @@ -131,26 +182,53 @@ export class HandlerBuilder { }; } + /** + * Create a handler for Firebase Crashlytics events. + + * `issue.onNew` handles events where the app experiences an issue for the first time. + + * @example + * ```javascript + * exports.myFunction = functions.handler.crashlytics.issue.onNew((issue) => { ... }) + * ``` + + * `issue.onRegressed` handles events where an issue reoccurs after it + * is closed in Crashlytics. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.crashlytics.issue.onRegressed((issue) => { ... }) + * ``` + + * `issue.onVelocityAlert` handles events where a statistically significant number + * of sessions in a given build crash. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.crashlytics.issue.onVelocityAlert((issue) => { ... }) + * ``` + + */ get crashlytics() { return { - /** - * Handle events related to Crashlytics issues. An issue in Crashlytics is an - * aggregation of crashes which have a shared root cause. - */ get issue() { return new crashlytics.IssueBuilder(() => null, {}); }, }; } + /** + * Create a handler for Firebase Remote Config events. + + * `remoteConfig.onUpdate` handles events that update a Remote Config template. + + * @example + * ```javascript + * exports.myFunction = functions.handler.remoteConfig.onUpdate() => { ... }) + * ``` + */ get remoteConfig() { return { - /** - * Handle all updates (including rollbacks) that affect a Remote Config - * project. - * @param handler A function that takes the updated Remote Config template - * version metadata as an argument. - */ onUpdate: ( handler: ( version: remoteConfig.TemplateVersion, @@ -162,67 +240,127 @@ export class HandlerBuilder { }; } + /** + * Create a handler for Google Analytics events. + + * `event.onLog` handles the logging of Analytics conversion events. + + * @example + * ```javascript + * exports.myFunction = functions.handler.analytics.event.onLog((event) => { ... }) + * ``` + */ get analytics() { return { - /** - * Select analytics events to listen to for events. - */ get event() { return new analytics.AnalyticsEventBuilder(() => null, {}); }, }; } + /** + * Create a handler for Cloud Storage for Firebase events. + * + * `object.onArchive` handles the archiving of Storage objects. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.storage.object.onArchive((object) => { ... }) + * ``` + + * `object.onDelete` handles the deletion of Storage objects. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.storage.object.onDelete((object) => { ... }) + * ``` + + * `object.onFinalize` handles the creation of Storage objects. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.storage.object.onFinalize((object) => + * { ... }) + * ``` + + * `object.onMetadataUpdate` handles changes to the metadata of existing Storage objects. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.storage.object.onMetadataUpdate((object) => + * { ... }) + * ``` + */ get storage() { return { - /** - * The optional bucket function allows you to choose which buckets' events to handle. - * This step can be bypassed by calling object() directly, which will use the default - * Cloud Storage for Firebase bucket. - */ get bucket() { return new storage.BucketBuilder(() => null, {}).object(); }, - /** - * Handle events related to Cloud Storage objects. - */ get object() { return new storage.ObjectBuilder(() => null, {}); }, }; } + /** + * Create a handler for Cloud Pub/Sub events. + + * `pubsub.onPublish` handles the publication of messages to a topic. + + * @example + * ```javascript + * exports.myFunction = functions.handler.pubsub.topic.onPublish((message) => { ... }) + * ``` + */ get pubsub() { return { - /** - * Select Cloud Pub/Sub topic to listen to. - */ get topic() { return new pubsub.TopicBuilder(() => null, {}); }, - /** - * Handle periodic events triggered by Cloud Scheduler. - */ get schedule() { return new pubsub.ScheduleBuilder(() => null, {}); }, }; } + /** + * Create a handler for Firebase Authentication events. + * + * `user.onCreate` handles the creation of users. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.auth.user.onCreate((user) => { ... }) + * ``` + + * `user.onDelete` handles the deletion of users. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.auth.user.onDelete((user => { ... }) + * ``` + + */ get auth() { return { - /** - * Handle events related to Firebase authentication users. - */ get user() { return new auth.UserBuilder(() => null, {}); }, }; } + /** + * Create a handler for Firebase Test Lab events. + + * `testMatrix.onComplete` handles the completion of a test matrix. + + * @example + * ```javascript + * exports.myFunction = functions.handler.testLab.testMatrix.onComplete((testMatrix) => { ... }) + * ``` + */ get testLab() { - /** Handle events related to Test Lab test matrices. */ return { get testMatrix() { return new testLab.TestMatrixBuilder(() => null, {}); From 1bd2736befdc0de87c0c00e36fe0a81b50d5c5b5 Mon Sep 17 00:00:00 2001 From: egilmorez Date: Wed, 27 May 2020 12:29:29 -0700 Subject: [PATCH 224/705] Fixes to reference doc generation for functions.https (#690) --- docgen/content-sources/HOME.md | 7 ++++++- docgen/content-sources/toc.yaml | 2 ++ src/providers/https.ts | 14 ++++++++++++-- src/setup.ts | 1 + src/utilities/assertions.ts | 2 +- src/utilities/path.ts | 2 +- src/utils.ts | 1 + 7 files changed, 24 insertions(+), 5 deletions(-) diff --git a/docgen/content-sources/HOME.md b/docgen/content-sources/HOME.md index c89df0a57..00d2090a0 100644 --- a/docgen/content-sources/HOME.md +++ b/docgen/content-sources/HOME.md @@ -1,3 +1,8 @@ # Firebase Functions SDK Reference -Functions SDK!!! +The `firebase-functions` package provides an SDK for defining Cloud Functions for Firebase. + +To get started using Cloud Functions, see +[Get started: write, test, and deploy your first functions](/docs/functions/get-started). + +For source code, see the [Cloud Functions for Firebase GitHub repo](https://github.com/firebase/firebase-functions). diff --git a/docgen/content-sources/toc.yaml b/docgen/content-sources/toc.yaml index 856b12640..91544d4f6 100644 --- a/docgen/content-sources/toc.yaml +++ b/docgen/content-sources/toc.yaml @@ -90,6 +90,8 @@ toc: section: - title: 'HttpsError' path: /docs/reference/functions/providers_https_.httpserror.html + - title: 'CallableContext' + path: /docs/reference/functions/providers_https_.callablecontext.html - title: 'functions.pubsub' path: /docs/reference/functions/providers_pubsub_.html diff --git a/src/providers/https.ts b/src/providers/https.ts index 36077affd..605c633f0 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -28,6 +28,7 @@ import { apps } from '../apps'; import { HttpsFunction, optionsToTrigger, Runnable } from '../cloud-functions'; import { DeploymentOptions } from '../function-configuration'; +/** @hidden */ export interface Request extends express.Request { rawBody: Buffer; } @@ -127,6 +128,7 @@ export type FunctionsErrorCode = | 'data-loss' | 'unauthenticated'; +/** @hidden */ export type CanonicalErrorCodeName = | 'OK' | 'CANCELLED' @@ -146,6 +148,7 @@ export type CanonicalErrorCodeName = | 'UNAVAILABLE' | 'DATA_LOSS'; +/** @hidden */ interface HttpErrorCode { canonicalName: CanonicalErrorCodeName; status: number; @@ -180,6 +183,7 @@ const errorCodeMap: { [name in FunctionsErrorCode]: HttpErrorCode } = { 'data-loss': { canonicalName: 'DATA_LOSS', status: 500 }, }; +/** @hidden */ interface HttpErrorWireFormat { details?: unknown; message: string; @@ -261,19 +265,22 @@ export interface CallableContext { rawRequest: Request; } -// The allowed interface for an http request for a callable function. +// The allowed interface for an HTTP request to a Callable function. +/** @hidden*/ interface HttpRequest extends Request { body: { data: any; }; } -// The format for the http body response to a callable function. +/** @hidden */ +// The format for an HTTP body response from a Callable function. interface HttpResponseBody { result?: any; error?: HttpsError; } +/** @hidden */ // Returns true if req is a properly formatted callable request. function isValidRequest(req: Request): req is HttpRequest { // The body must not be empty. @@ -317,7 +324,9 @@ function isValidRequest(req: Request): req is HttpRequest { return true; } +/** @hidden */ const LONG_TYPE = 'type.googleapis.com/google.protobuf.Int64Value'; +/** @hidden */ const UNSIGNED_LONG_TYPE = 'type.googleapis.com/google.protobuf.UInt64Value'; /** @@ -400,6 +409,7 @@ export function decode(data: any): any { return data; } +/** @hidden */ const corsHandler = cors({ origin: true, methods: 'POST' }); /** @hidden */ diff --git a/src/setup.ts b/src/setup.ts index 6a0796348..b30fa6226 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -20,6 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +/** @hidden */ import { firebaseConfig } from './config'; // Set up for config and vars diff --git a/src/utilities/assertions.ts b/src/utilities/assertions.ts index 49ff5e36d..982ca304d 100644 --- a/src/utilities/assertions.ts +++ b/src/utilities/assertions.ts @@ -1,4 +1,4 @@ -/** +/** @hidden * @file Provides common assertion helpers which can be used to improve * strictness of both type checking and runtime. */ diff --git a/src/utilities/path.ts b/src/utilities/path.ts index 40ab8f047..33e187f9e 100644 --- a/src/utilities/path.ts +++ b/src/utilities/path.ts @@ -1,4 +1,4 @@ -/** +/** @hidden * Removes leading and trailing slashes from a path. * * @param path A path to normalize, in POSIX format. diff --git a/src/utils.ts b/src/utils.ts index b52c1e54f..e41b1e1fb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -20,6 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +/** @hidden */ import * as _ from 'lodash'; export function applyChange(src: any, dest: any) { From 8d0a6c230984a058b798a3b58201930b87b06786 Mon Sep 17 00:00:00 2001 From: Adam Montgomery <1934358+montasaurus@users.noreply.github.com> Date: Wed, 27 May 2020 15:10:18 -0500 Subject: [PATCH 225/705] pin @types/express version (#686) --- CHANGELOG.md | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..b39bddd01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Pin `@types/express` version to 4.17.3 to fix type definition issue (#685) diff --git a/package.json b/package.json index ed26f220b..27bba5b72 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "test": "mocha" }, "dependencies": { - "@types/express": "^4.17.3", + "@types/express": "4.17.3", "cors": "^2.8.5", "express": "^4.17.1", "jsonwebtoken": "^8.5.1", From 9431102c8cd47432c091d873ef08aa45138f7f27 Mon Sep 17 00:00:00 2001 From: Sebastian Kreft Date: Wed, 27 May 2020 16:11:14 -0400 Subject: [PATCH 226/705] fix: move jsonwebtoken to dev dependencies (#677) jsonwebtoken was only used in tests and not present in the release output. ``` [0] firebase-functions$ ag jsonwebtoken spec/fixtures/mockrequest.ts 1:import * as jwt from 'jsonwebtoken'; package.json 50: "@types/jsonwebtoken": "^8.3.2", 64: "jsonwebtoken": "^8.5.1", [0] firebase-functions$ grep jsonwebtoken -R lib [1] firebase-functions$ ``` Co-authored-by: Lauren Long --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 27bba5b72..020998793 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "@types/express": "4.17.3", "cors": "^2.8.5", "express": "^4.17.1", - "jsonwebtoken": "^8.5.1", "lodash": "^4.17.14" }, "devDependencies": { @@ -62,6 +61,7 @@ "istanbul": "^0.4.5", "js-yaml": "^3.13.1", "jsdom": "^16.2.1", + "jsonwebtoken": "^8.5.1", "mocha": "^6.1.4", "mock-require": "^3.0.3", "mz": "^2.7.0", From dbf9b5e29d1bc1c31345151ce29c9ea13dc55847 Mon Sep 17 00:00:00 2001 From: Fred Zhang Date: Wed, 27 May 2020 13:26:20 -0700 Subject: [PATCH 227/705] Use `domain` in context to override RTDB domain #683 (#235) --- spec/providers/database.spec.ts | 56 +++++++++++++++++++++++---------- src/cloud-functions.ts | 1 + src/providers/database.ts | 34 +++++++++++++------- 3 files changed, 64 insertions(+), 27 deletions(-) diff --git a/spec/providers/database.spec.ts b/spec/providers/database.spec.ts index 25293ac11..b764a7ea0 100644 --- a/spec/providers/database.spec.ts +++ b/spec/providers/database.spec.ts @@ -32,7 +32,7 @@ describe('Database Functions', () => { before(() => { process.env.FIREBASE_CONFIG = JSON.stringify({ - databaseURL: 'https://subdomain.firebaseio.com', + databaseURL: 'https://subdomain.apse.firebasedatabase.app', }); appsNamespace.init(); }); @@ -424,29 +424,51 @@ describe('Database Functions', () => { }); }); - describe('resourceToInstanceAndPath', () => { - it('should return the correct instance and path strings', () => { - const [instance, path] = database.resourceToInstanceAndPath( - 'projects/_/instances/foo/refs/bar' + describe('extractInstanceAndPath', () => { + it('should return correct us-central prod instance and path strings if domain is missing', () => { + const [instance, path] = database.extractInstanceAndPath( + 'projects/_/instances/foo/refs/bar', + undefined ); expect(instance).to.equal('https://foo.firebaseio.com'); expect(path).to.equal('/bar'); }); + it('should return the correct staging instance and path strings if domain is present', () => { + const [instance, path] = database.extractInstanceAndPath( + 'projects/_/instances/foo/refs/bar', + 'firebaseio-staging.com' + ); + expect(instance).to.equal('https://foo.firebaseio-staging.com'); + expect(path).to.equal('/bar'); + }); + + it('should return the correct multi-region instance and path strings if domain is present', () => { + const [instance, path] = database.extractInstanceAndPath( + 'projects/_/instances/foo/refs/bar', + 'euw1.firebasedatabase.app' + ); + expect(instance).to.equal('https://foo.euw1.firebasedatabase.app'); + expect(path).to.equal('/bar'); + }); + it('should throw an error if the given instance name contains anything except alphanumerics and dashes', () => { expect(() => { - return database.resourceToInstanceAndPath( - 'projects/_/instances/a.bad.name/refs/bar' + return database.extractInstanceAndPath( + 'projects/_/instances/a.bad.name/refs/bar', + undefined ); }).to.throw(Error); expect(() => { - return database.resourceToInstanceAndPath( - 'projects/_/instances/a_different_bad_name/refs/bar' + return database.extractInstanceAndPath( + 'projects/_/instances/a_different_bad_name/refs/bar', + undefined ); }).to.throw(Error); expect(() => { - return database.resourceToInstanceAndPath( - 'projects/_/instances/BAD!!!!/refs/bar' + return database.extractInstanceAndPath( + 'projects/_/instances/BAD!!!!/refs/bar', + undefined ); }).to.throw(Error); }); @@ -457,8 +479,9 @@ describe('Database Functions', () => { const apps = new appsNamespace.Apps(); const populate = (data: any) => { - const [instance, path] = database.resourceToInstanceAndPath( - 'projects/_/instances/other-subdomain/refs/foo' + const [instance, path] = database.extractInstanceAndPath( + 'projects/_/instances/other-subdomain/refs/foo', + 'firebaseio-staging.com' ); subject = new database.DataSnapshot(data, path, apps.admin, instance); }; @@ -467,7 +490,7 @@ describe('Database Functions', () => { it('should return a ref for correct instance, not the default instance', () => { populate({}); expect(subject.ref.toJSON()).to.equal( - 'https://other-subdomain.firebaseio.com/foo' + 'https://other-subdomain.firebaseio-staging.com/foo' ); }); }); @@ -648,8 +671,9 @@ describe('Database Functions', () => { }); it('should return null for the root', () => { - const [instance, path] = database.resourceToInstanceAndPath( - 'projects/_/instances/foo/refs/' + const [instance, path] = database.extractInstanceAndPath( + 'projects/_/instances/foo/refs/', + undefined ); const snapshot = new database.DataSnapshot( null, diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index ea4acd7a2..f8b1b1c1e 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -39,6 +39,7 @@ export interface Event { timestamp: string; eventType: string; resource: Resource; + domain?: string; }; data: any; } diff --git a/src/providers/database.ts b/src/providers/database.ts index 78614a152..8c8de4ec6 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -40,8 +40,7 @@ export const provider = 'google.firebase.database'; /** @hidden */ export const service = 'firebaseio.com'; -// NOTE(inlined): Should we relax this a bit to allow staging or alternate implementations of our API? -const databaseURLRegex = new RegExp('https://([^.]+).firebaseio.com'); +const databaseURLRegex = new RegExp('^https://([^.]+).'); /** * Registers a function that triggers on events from a specific @@ -215,8 +214,9 @@ export class RefBuilder { ) => PromiseLike | any ): CloudFunction { const dataConstructor = (raw: Event) => { - const [dbInstance, path] = resourceToInstanceAndPath( - raw.context.resource.name + const [dbInstance, path] = extractInstanceAndPath( + raw.context.resource.name, + raw.context.domain ); return new DataSnapshot( raw.data.delta, @@ -243,8 +243,9 @@ export class RefBuilder { ) => PromiseLike | any ): CloudFunction { const dataConstructor = (raw: Event) => { - const [dbInstance, path] = resourceToInstanceAndPath( - raw.context.resource.name + const [dbInstance, path] = extractInstanceAndPath( + raw.context.resource.name, + raw.context.domain ); return new DataSnapshot(raw.data.data, path, this.apps.admin, dbInstance); }; @@ -271,8 +272,9 @@ export class RefBuilder { } private changeConstructor = (raw: Event): Change => { - const [dbInstance, path] = resourceToInstanceAndPath( - raw.context.resource.name + const [dbInstance, path] = extractInstanceAndPath( + raw.context.resource.name, + raw.context.domain ); const before = new DataSnapshot( raw.data.data, @@ -293,9 +295,19 @@ export class RefBuilder { }; } -/* Utility function to extract database reference from resource string */ +/** + * Utility function to extract database reference from resource string + * + * @param optional database domain override for the original of the source database. + * It defaults to `firebaseio.com`. + * Multi-region RTDB will be served from different domains. + * Since region is not part of the resource name, it is provided through context. + */ /** @hidden */ -export function resourceToInstanceAndPath(resource: string) { +export function extractInstanceAndPath( + resource: string, + domain = 'firebaseio.com' +) { const resourceRegex = `projects/([^/]+)/instances/([a-zA-Z0-9\-^/]+)/refs(/.+)?`; const match = resource.match(new RegExp(resourceRegex)); if (!match) { @@ -310,7 +322,7 @@ export function resourceToInstanceAndPath(resource: string) { `Expect project to be '_' in a Firebase Realtime Database event` ); } - const dbInstance = 'https://' + dbInstanceName + '.firebaseio.com'; + const dbInstance = 'https://' + dbInstanceName + '.' + domain; return [dbInstance, path]; } From f907d68e9e3d6500a357fbfd4722e6fc201e1e56 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Thu, 28 May 2020 10:25:58 -0700 Subject: [PATCH 228/705] Update CHANGELOG.md (#694) --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b39bddd01..ecf68561e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,3 @@ -- Pin `@types/express` version to 4.17.3 to fix type definition issue (#685) +- Pin `@types/express` version to 4.17.3 to fix type definition issue (Issue #685). +- Firestore onCreate, onUpdate, and onDelete now receive a `QueryDocumentSnapshot` instead of `DocumentSnapshot`, which guarantees that data is not undefined (Issue #659). +- Modify return type of `DataSnapshot.forEach` to `boolean | void` match `firebase-admin` SDK. From 4fdf9dbdfc29eeb98c2c46f455ea17f353591dec Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Thu, 28 May 2020 17:28:56 +0000 Subject: [PATCH 229/705] 3.6.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 020998793..c0403aafa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.6.1", + "version": "3.6.2", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From bb6f038f76c35dd64f48e8293a7e1b3e464999bb Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Thu, 28 May 2020 17:29:01 +0000 Subject: [PATCH 230/705] [firebase-release] Removed change log and reset repo after 3.6.2 release --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecf68561e..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +0,0 @@ -- Pin `@types/express` version to 4.17.3 to fix type definition issue (Issue #685). -- Firestore onCreate, onUpdate, and onDelete now receive a `QueryDocumentSnapshot` instead of `DocumentSnapshot`, which guarantees that data is not undefined (Issue #659). -- Modify return type of `DataSnapshot.forEach` to `boolean | void` match `firebase-admin` SDK. From bb92d38238dda02e376e585b3e80af2e68008e68 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Tue, 2 Jun 2020 09:31:35 -0700 Subject: [PATCH 231/705] Adds structured logging API. (#665) --- CHANGELOG.md | 18 ++++++ spec/logger.spec.ts | 105 ++++++++++++++++++++++++++++++++ src/index.ts | 2 + src/logger.ts | 135 ++++++++++++++++++++++++++++++++++++++++++ src/logger/compat.ts | 8 +++ tsconfig.release.json | 2 +- 6 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 spec/logger.spec.ts create mode 100644 src/logger.ts create mode 100644 src/logger/compat.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..f85fd69b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +- Adds `functions.logger` SDK to enable structured logging in the Node.js 10 runtime. For example: + + ```js + const functions = require('firebase-functions'); + + functions.logger.debug('example log with structured data', { + uid: user.uid, + authorized: true, + }); + ``` + + The logger can also override default behavior of `console.*` methods through a special require: + + ```js + require('firebase-functions/logger/compat'); + ``` + + In older runtimes, logger prints to the console, and no structured data is saved. diff --git a/spec/logger.spec.ts b/spec/logger.spec.ts new file mode 100644 index 000000000..517f50f1d --- /dev/null +++ b/spec/logger.spec.ts @@ -0,0 +1,105 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as logger from '../src/logger'; + +const SUPPORTS_STRUCTURED_LOGS = + parseInt(process.versions?.node?.split('.')?.[0] || '8', 10) >= 10; + +describe(`logger (${ + SUPPORTS_STRUCTURED_LOGS ? 'structured' : 'unstructured' +})`, () => { + let sandbox: sinon.SinonSandbox; + let stdoutStub: sinon.SinonStub; + let stderrStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + stdoutStub = sandbox.stub(process.stdout, 'write'); + stderrStub = sandbox.stub(process.stderr, 'write'); + }); + + function expectOutput(stdStub: sinon.SinonStub, entry: any) { + if (SUPPORTS_STRUCTURED_LOGS) { + return expect( + JSON.parse((stdStub.getCalls()[0].args[0] as string).trim()) + ).to.deep.eq(entry); + } else { + // legacy logging is not structured, but do a sanity check + return expect(stdStub.getCalls()[0].args[0]).to.include(entry.message); + } + } + + function expectStdout(entry: any) { + return expectOutput(stdoutStub, entry); + } + + function expectStderr(entry: any) { + return expectOutput(stderrStub, entry); + } + + describe('logging methods', () => { + let writeStub: sinon.SinonStub; + beforeEach(() => { + writeStub = sinon.stub(logger, 'write'); + }); + + afterEach(() => { + writeStub.restore(); + }); + + it('should coalesce arguments into the message', () => { + logger.log('hello', { middle: 'obj' }, 'end message'); + expectStdout({ + severity: 'INFO', + message: "hello { middle: 'obj' } end message", + }); + sandbox.restore(); // to avoid swallowing test runner output + }); + + it('should merge structured data from the last argument', () => { + logger.log('hello', 'world', { additional: 'context' }); + expectStdout({ + severity: 'INFO', + message: 'hello world', + additional: 'context', + }); + sandbox.restore(); // to avoid swallowing test runner output + }); + }); + + describe('write', () => { + describe('structured logging', () => { + describe('write', () => { + for (const severity of ['DEBUG', 'INFO', 'NOTICE']) { + it(`should output ${severity} severity to stdout`, () => { + let entry: logger.LogEntry = { + severity: severity as logger.LogSeverity, + message: 'test', + }; + logger.write(entry); + expectStdout(entry); + sandbox.restore(); // to avoid swallowing test runner output + }); + } + + for (const severity of [ + 'WARNING', + 'ERROR', + 'CRITICAL', + 'ALERT', + 'EMERGENCY', + ]) { + it(`should output ${severity} severity to stderr`, () => { + let entry: logger.LogEntry = { + severity: severity as logger.LogSeverity, + message: 'test', + }; + logger.write(entry); + expectStderr(entry); + sandbox.restore(); // to avoid swallowing test runner output + }); + } + }); + }); + }); +}); diff --git a/src/index.ts b/src/index.ts index d4951c512..dcca98172 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,7 @@ import * as storage from './providers/storage'; import * as testLab from './providers/testLab'; import * as apps from './apps'; +import * as logger from './logger'; import { handler } from './handler-builder'; import { setup } from './setup'; @@ -51,6 +52,7 @@ export { remoteConfig, storage, testLab, + logger, }; // Exported root types: diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 000000000..7d4c91cbf --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,135 @@ +import { format } from 'util'; + +// safely preserve unpatched console.* methods in case of compat require +const unpatchedConsole = { + debug: console.debug, + info: console.info, + log: console.log, + warn: console.warn, + error: console.error, +}; + +// Determine if structured logs are supported (node >= 10). If something goes wrong, +// assume no since unstructured is safer. +const SUPPORTS_STRUCTURED_LOGS = + parseInt(process.versions?.node?.split('.')?.[0] || '8', 10) >= 10; + +// Map LogSeverity types to their equivalent `console.*` method. +const CONSOLE_SEVERITY: { + [severity: string]: 'debug' | 'info' | 'warn' | 'error'; +} = { + DEBUG: 'debug', + INFO: 'info', + NOTICE: 'info', + WARNING: 'warn', + ERROR: 'error', + CRITICAL: 'error', + ALERT: 'error', + EMERGENCY: 'error', +}; + +/** + * `LogSeverity` indicates the detailed severity of the log entry. See [LogSeverity](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity) for more. + */ +export type LogSeverity = + | 'DEBUG' + | 'INFO' + | 'NOTICE' + | 'WARNING' + | 'ERROR' + | 'CRITICAL' + | 'ALERT' + | 'EMERGENCY'; + +/** + * `LogEntry` represents a structured Cloud Logging entry. All keys aside from `severity` and `message` are + * included in the `jsonPayload` of the logged entry. + */ +export interface LogEntry { + severity: LogSeverity; + message?: string; + [key: string]: any; +} + +/** + * Writes a `LogEntry` to `stdout`/`stderr` (depending on severity). + * @param entry The LogEntry including severity, message, and any additional structured metadata. + */ +export function write(entry: LogEntry) { + if (SUPPORTS_STRUCTURED_LOGS) { + unpatchedConsole[CONSOLE_SEVERITY[entry.severity]](JSON.stringify(entry)); + return; + } + + let message = entry.message || ''; + const jsonPayload: { [key: string]: any } = {}; + let jsonKeyCount = 0; + for (const k in entry) { + if (!['severity', 'message'].includes(k)) { + jsonKeyCount++; + jsonPayload[k] = entry[k]; + } + } + if (jsonKeyCount > 0) { + message = `${message} ${JSON.stringify(jsonPayload, null, 2)}`; + } + unpatchedConsole[CONSOLE_SEVERITY[entry.severity]](message); +} + +/** + * Writes a `DEBUG` severity log. If the last argument provided is a plain object, + * it will be added to the `jsonPayload` in the Cloud Logging entry. + * @param args Arguments, concatenated into the log message with space separators. + */ +export function debug(...args: any[]) { + write(entryFromArgs('DEBUG', args)); +} + +/** + * Writes an `INFO` severity log. If the last argument provided is a plain object, + * it will be added to the `jsonPayload` in the Cloud Logging entry. + * @param args Arguments, concatenated into the log message with space separators. + */ +export function log(...args: any[]) { + write(entryFromArgs('INFO', args)); +} + +/** + * Writes an `INFO` severity log. If the last argument provided is a plain object, + * it will be added to the `jsonPayload` in the Cloud Logging entry. + * @param args Arguments, concatenated into the log message with space separators. + */ +export function info(...args: any[]) { + write(entryFromArgs('INFO', args)); +} + +/** + * Writes a `WARNING` severity log. If the last argument provided is a plain object, + * it will be added to the `jsonPayload` in the Cloud Logging entry. + * @param args Arguments, concatenated into the log message with space separators. + */ +export function warn(...args: any[]) { + write(entryFromArgs('WARNING', args)); +} + +/** + * Writes an `ERROR` severity log. If the last argument provided is a plain object, + * it will be added to the `jsonPayload` in the Cloud Logging entry. + * @param args Arguments, concatenated into the log message with space separators. + */ +export function error(...args: any[]) { + write(entryFromArgs('ERROR', args)); +} + +function entryFromArgs(severity: LogSeverity, args: any[]): LogEntry { + let entry = {}; + const lastArg = args[args.length - 1]; + if (typeof lastArg == 'object' && lastArg.constructor == Object) { + entry = args.pop(); + } + return Object.assign({}, entry, { + severity, + // mimic `console.*` behavior, see https://nodejs.org/api/console.html#console_console_log_data_args + message: format.apply(null, args), + }); +} diff --git a/src/logger/compat.ts b/src/logger/compat.ts new file mode 100644 index 000000000..6f6eda03f --- /dev/null +++ b/src/logger/compat.ts @@ -0,0 +1,8 @@ +import { debug, info, warn, error } from '../logger'; + +// IMPORTANT -- "../logger" must be imported before monkeypatching! +console.debug = debug; +console.info = info; +console.log = info; +console.warn = warn; +console.error = error; diff --git a/tsconfig.release.json b/tsconfig.release.json index e93d5d4b6..a226eb290 100644 --- a/tsconfig.release.json +++ b/tsconfig.release.json @@ -10,5 +10,5 @@ "target": "es2017", "typeRoots": ["./node_modules/@types"] }, - "files": ["./src/index.ts"] + "files": ["./src/index.ts", "./src/logger.ts", "./src/logger/compat.ts"] } From e447949d0d6095b130ca5551e3396b45ff5d589a Mon Sep 17 00:00:00 2001 From: Sebastian Kreft Date: Tue, 9 Jun 2020 17:34:43 -0400 Subject: [PATCH 232/705] fix: onRequest type definitions (#696) --- src/cloud-functions.ts | 2 +- src/providers/https.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index f8b1b1c1e..f249bf7f9 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -281,7 +281,7 @@ export interface Runnable { * arguments. */ export type HttpsFunction = TriggerAnnotated & - ((req: Request, resp: Response) => void); + ((req: Request, resp: Response) => void | Promise); /** * The Cloud Function type for all non-HTTPS triggers. This should be exported diff --git a/src/providers/https.ts b/src/providers/https.ts index 605c633f0..cd9f1745d 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -39,7 +39,7 @@ export interface Request extends express.Request { * same signature as an Express app. */ export function onRequest( - handler: (req: Request, resp: express.Response) => void + handler: (req: Request, resp: express.Response) => void | Promise ): HttpsFunction { return _onRequestWithOptions(handler, {}); } @@ -56,7 +56,7 @@ export function onCall( /** @hidden */ export function _onRequestWithOptions( - handler: (req: Request, resp: express.Response) => void, + handler: (req: Request, resp: express.Response) => void | Promise, options: DeploymentOptions ): HttpsFunction { // lets us add __trigger without altering handler: From c26ed9e6bee56f285118975e8afa6680657af5f3 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Tue, 9 Jun 2020 15:10:47 -0700 Subject: [PATCH 233/705] Makes "logger/compat" more focused on compatibility. Fixes #697 (#701) --- CHANGELOG.md | 4 ++-- src/logger.ts | 36 +++++++----------------------------- src/logger/common.ts | 27 +++++++++++++++++++++++++++ src/logger/compat.ts | 30 ++++++++++++++++++++++++------ 4 files changed, 60 insertions(+), 37 deletions(-) create mode 100644 src/logger/common.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f85fd69b3..1b87ad8b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,10 @@ }); ``` - The logger can also override default behavior of `console.*` methods through a special require: +- Adds a special require that mimics Node.js 8 runtime logging in Node.js 10 and later runtimes: ```js require('firebase-functions/logger/compat'); ``` - In older runtimes, logger prints to the console, and no structured data is saved. + In newer runtimes, requiring this will emit text logs with multi-line support and appropriate severity. In the Node.js 8 runtime, the `compat` module has no effect. diff --git a/src/logger.ts b/src/logger.ts index 7d4c91cbf..258dbbdff 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,32 +1,10 @@ import { format } from 'util'; -// safely preserve unpatched console.* methods in case of compat require -const unpatchedConsole = { - debug: console.debug, - info: console.info, - log: console.log, - warn: console.warn, - error: console.error, -}; - -// Determine if structured logs are supported (node >= 10). If something goes wrong, -// assume no since unstructured is safer. -const SUPPORTS_STRUCTURED_LOGS = - parseInt(process.versions?.node?.split('.')?.[0] || '8', 10) >= 10; - -// Map LogSeverity types to their equivalent `console.*` method. -const CONSOLE_SEVERITY: { - [severity: string]: 'debug' | 'info' | 'warn' | 'error'; -} = { - DEBUG: 'debug', - INFO: 'info', - NOTICE: 'info', - WARNING: 'warn', - ERROR: 'error', - CRITICAL: 'error', - ALERT: 'error', - EMERGENCY: 'error', -}; +import { + SUPPORTS_STRUCTURED_LOGS, + CONSOLE_SEVERITY, + UNPATCHED_CONSOLE, +} from './logger/common'; /** * `LogSeverity` indicates the detailed severity of the log entry. See [LogSeverity](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity) for more. @@ -57,7 +35,7 @@ export interface LogEntry { */ export function write(entry: LogEntry) { if (SUPPORTS_STRUCTURED_LOGS) { - unpatchedConsole[CONSOLE_SEVERITY[entry.severity]](JSON.stringify(entry)); + UNPATCHED_CONSOLE[CONSOLE_SEVERITY[entry.severity]](JSON.stringify(entry)); return; } @@ -73,7 +51,7 @@ export function write(entry: LogEntry) { if (jsonKeyCount > 0) { message = `${message} ${JSON.stringify(jsonPayload, null, 2)}`; } - unpatchedConsole[CONSOLE_SEVERITY[entry.severity]](message); + UNPATCHED_CONSOLE[CONSOLE_SEVERITY[entry.severity]](message); } /** diff --git a/src/logger/common.ts b/src/logger/common.ts new file mode 100644 index 000000000..f8b1f0ed9 --- /dev/null +++ b/src/logger/common.ts @@ -0,0 +1,27 @@ +// Determine if structured logs are supported (node >= 10). If something goes wrong, +// assume no since unstructured is safer. +export const SUPPORTS_STRUCTURED_LOGS = + parseInt(process.versions?.node?.split('.')?.[0] || '8', 10) >= 10; + +// Map LogSeverity types to their equivalent `console.*` method. +export const CONSOLE_SEVERITY: { + [severity: string]: 'debug' | 'info' | 'warn' | 'error'; +} = { + DEBUG: 'debug', + INFO: 'info', + NOTICE: 'info', + WARNING: 'warn', + ERROR: 'error', + CRITICAL: 'error', + ALERT: 'error', + EMERGENCY: 'error', +}; + +// safely preserve unpatched console.* methods in case of compat require +export const UNPATCHED_CONSOLE = { + debug: console.debug, + info: console.info, + log: console.log, + warn: console.warn, + error: console.error, +}; diff --git a/src/logger/compat.ts b/src/logger/compat.ts index 6f6eda03f..48647f5e3 100644 --- a/src/logger/compat.ts +++ b/src/logger/compat.ts @@ -1,8 +1,26 @@ -import { debug, info, warn, error } from '../logger'; +import { + SUPPORTS_STRUCTURED_LOGS, + UNPATCHED_CONSOLE, + CONSOLE_SEVERITY, +} from './common'; +import { format } from 'util'; + +function patchedConsole(severity: string): (data: any, ...args: any[]) => void { + return function(data: any, ...args: any[]): void { + if (SUPPORTS_STRUCTURED_LOGS) { + UNPATCHED_CONSOLE[CONSOLE_SEVERITY[severity]]( + JSON.stringify({ severity, message: format(data, ...args) }) + ); + return; + } + + UNPATCHED_CONSOLE[CONSOLE_SEVERITY[severity]](data, ...args); + }; +} // IMPORTANT -- "../logger" must be imported before monkeypatching! -console.debug = debug; -console.info = info; -console.log = info; -console.warn = warn; -console.error = error; +console.debug = patchedConsole('DEBUG'); +console.info = patchedConsole('INFO'); +console.log = patchedConsole('INFO'); +console.warn = patchedConsole('WARNING'); +console.error = patchedConsole('ERROR'); From 0c684322f91410cdb8d77c97de174f9dbb17a909 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Tue, 9 Jun 2020 16:06:49 -0700 Subject: [PATCH 234/705] Fix compat path in CHANGELOG (#702) --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b87ad8b8..248ab7792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,9 @@ - Adds a special require that mimics Node.js 8 runtime logging in Node.js 10 and later runtimes: ```js - require('firebase-functions/logger/compat'); + require('firebase-functions/lib/logger/compat'); ``` In newer runtimes, requiring this will emit text logs with multi-line support and appropriate severity. In the Node.js 8 runtime, the `compat` module has no effect. + +- Fixes `https.onRequest` type signature to allow Promises for `async` functions. From 505f357b1c57338e55ae5da16e62e4398d3dc746 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Tue, 9 Jun 2020 23:18:37 +0000 Subject: [PATCH 235/705] 3.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c0403aafa..6e198c416 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.6.2", + "version": "3.7.0", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From c23a8c11299f93f32a59507009f88999851082b6 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Tue, 9 Jun 2020 23:18:44 +0000 Subject: [PATCH 236/705] [firebase-release] Removed change log and reset repo after 3.7.0 release --- CHANGELOG.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 248ab7792..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +0,0 @@ -- Adds `functions.logger` SDK to enable structured logging in the Node.js 10 runtime. For example: - - ```js - const functions = require('firebase-functions'); - - functions.logger.debug('example log with structured data', { - uid: user.uid, - authorized: true, - }); - ``` - -- Adds a special require that mimics Node.js 8 runtime logging in Node.js 10 and later runtimes: - - ```js - require('firebase-functions/lib/logger/compat'); - ``` - - In newer runtimes, requiring this will emit text logs with multi-line support and appropriate severity. In the Node.js 8 runtime, the `compat` module has no effect. - -- Fixes `https.onRequest` type signature to allow Promises for `async` functions. From f9b24e03a5b9da08866f990304f6623a4b909be3 Mon Sep 17 00:00:00 2001 From: egilmorez Date: Tue, 23 Jun 2020 12:56:52 -0700 Subject: [PATCH 237/705] Adding logger SDK to reference, with some edits. (#714) * Adding logger SDK to reference, with some edits. * Adding link to structured logging details per feedback. --- docgen/content-sources/toc.yaml | 6 ++++++ src/logger.ts | 18 ++++++++++-------- src/logger/common.ts | 3 +++ src/logger/compat.ts | 1 + 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/docgen/content-sources/toc.yaml b/docgen/content-sources/toc.yaml index 91544d4f6..a394975df 100644 --- a/docgen/content-sources/toc.yaml +++ b/docgen/content-sources/toc.yaml @@ -93,6 +93,12 @@ toc: - title: 'CallableContext' path: /docs/reference/functions/providers_https_.callablecontext.html + - title: 'functions.logger' + path: /docs/reference/functions/logger_.html + section: + - title: 'LogEntry' + path: /docs/reference/functions/logger_.logentry.html + - title: 'functions.pubsub' path: /docs/reference/functions/providers_pubsub_.html section: diff --git a/src/logger.ts b/src/logger.ts index 258dbbdff..4bc851cec 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -7,7 +7,7 @@ import { } from './logger/common'; /** - * `LogSeverity` indicates the detailed severity of the log entry. See [LogSeverity](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity) for more. + * `LogSeverity` indicates the detailed severity of the log entry. See [LogSeverity](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity). */ export type LogSeverity = | 'DEBUG' @@ -20,7 +20,8 @@ export type LogSeverity = | 'EMERGENCY'; /** - * `LogEntry` represents a structured Cloud Logging entry. All keys aside from `severity` and `message` are + * `LogEntry` represents a [structured Cloud Logging](https://cloud.google.com/logging/docs/structured-logging) + * entry. All keys aside from `severity` and `message` are * included in the `jsonPayload` of the logged entry. */ export interface LogEntry { @@ -31,7 +32,7 @@ export interface LogEntry { /** * Writes a `LogEntry` to `stdout`/`stderr` (depending on severity). - * @param entry The LogEntry including severity, message, and any additional structured metadata. + * @param entry The `LogEntry` including severity, message, and any additional structured metadata. */ export function write(entry: LogEntry) { if (SUPPORTS_STRUCTURED_LOGS) { @@ -56,7 +57,7 @@ export function write(entry: LogEntry) { /** * Writes a `DEBUG` severity log. If the last argument provided is a plain object, - * it will be added to the `jsonPayload` in the Cloud Logging entry. + * it is added to the `jsonPayload` in the Cloud Logging entry. * @param args Arguments, concatenated into the log message with space separators. */ export function debug(...args: any[]) { @@ -65,7 +66,7 @@ export function debug(...args: any[]) { /** * Writes an `INFO` severity log. If the last argument provided is a plain object, - * it will be added to the `jsonPayload` in the Cloud Logging entry. + * it is added to the `jsonPayload` in the Cloud Logging entry. * @param args Arguments, concatenated into the log message with space separators. */ export function log(...args: any[]) { @@ -74,7 +75,7 @@ export function log(...args: any[]) { /** * Writes an `INFO` severity log. If the last argument provided is a plain object, - * it will be added to the `jsonPayload` in the Cloud Logging entry. + * it is added to the `jsonPayload` in the Cloud Logging entry. * @param args Arguments, concatenated into the log message with space separators. */ export function info(...args: any[]) { @@ -83,7 +84,7 @@ export function info(...args: any[]) { /** * Writes a `WARNING` severity log. If the last argument provided is a plain object, - * it will be added to the `jsonPayload` in the Cloud Logging entry. + * it is added to the `jsonPayload` in the Cloud Logging entry. * @param args Arguments, concatenated into the log message with space separators. */ export function warn(...args: any[]) { @@ -92,13 +93,14 @@ export function warn(...args: any[]) { /** * Writes an `ERROR` severity log. If the last argument provided is a plain object, - * it will be added to the `jsonPayload` in the Cloud Logging entry. + * it is added to the `jsonPayload` in the Cloud Logging entry. * @param args Arguments, concatenated into the log message with space separators. */ export function error(...args: any[]) { write(entryFromArgs('ERROR', args)); } +/** @hidden */ function entryFromArgs(severity: LogSeverity, args: any[]): LogEntry { let entry = {}; const lastArg = args[args.length - 1]; diff --git a/src/logger/common.ts b/src/logger/common.ts index f8b1f0ed9..f7ff0de78 100644 --- a/src/logger/common.ts +++ b/src/logger/common.ts @@ -1,9 +1,11 @@ // Determine if structured logs are supported (node >= 10). If something goes wrong, // assume no since unstructured is safer. +/** @hidden */ export const SUPPORTS_STRUCTURED_LOGS = parseInt(process.versions?.node?.split('.')?.[0] || '8', 10) >= 10; // Map LogSeverity types to their equivalent `console.*` method. +/** @hidden */ export const CONSOLE_SEVERITY: { [severity: string]: 'debug' | 'info' | 'warn' | 'error'; } = { @@ -18,6 +20,7 @@ export const CONSOLE_SEVERITY: { }; // safely preserve unpatched console.* methods in case of compat require +/** @hidden */ export const UNPATCHED_CONSOLE = { debug: console.debug, info: console.info, diff --git a/src/logger/compat.ts b/src/logger/compat.ts index 48647f5e3..a7c27bd16 100644 --- a/src/logger/compat.ts +++ b/src/logger/compat.ts @@ -5,6 +5,7 @@ import { } from './common'; import { format } from 'util'; +/** @hidden */ function patchedConsole(severity: string): (data: any, ...args: any[]) => void { return function(data: any, ...args: any[]): void { if (SUPPORTS_STRUCTURED_LOGS) { From d06bcbee7b147843e3de85363d6dd090767064c9 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Wed, 24 Jun 2020 10:51:39 -0700 Subject: [PATCH 238/705] Handles `null` in structured logging (Fixes #716) (#717) --- spec/logger.spec.ts | 9 +++++++++ src/logger.ts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spec/logger.spec.ts b/spec/logger.spec.ts index 517f50f1d..c6859b60a 100644 --- a/spec/logger.spec.ts +++ b/spec/logger.spec.ts @@ -65,6 +65,15 @@ describe(`logger (${ }); sandbox.restore(); // to avoid swallowing test runner output }); + + it('should not recognize null as a structured logging object', () => { + logger.log('hello', 'world', null); + expectStdout({ + severity: 'INFO', + message: 'hello world null', + }); + sandbox.restore(); // to avoid swallowing test runner output + }); }); describe('write', () => { diff --git a/src/logger.ts b/src/logger.ts index 4bc851cec..7958ca457 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -104,7 +104,7 @@ export function error(...args: any[]) { function entryFromArgs(severity: LogSeverity, args: any[]): LogEntry { let entry = {}; const lastArg = args[args.length - 1]; - if (typeof lastArg == 'object' && lastArg.constructor == Object) { + if (lastArg && typeof lastArg == 'object' && lastArg.constructor == Object) { entry = args.pop(); } return Object.assign({}, entry, { From 5f7c7acc9deb4e7c9ab1ab32c660ae035a63f93c Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Thu, 25 Jun 2020 08:25:42 -0700 Subject: [PATCH 239/705] Changelog update (#718) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..43975c685 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Fixes error when last argument to logger methods is `null`. (#716) From b6e611baab000303b55958b55bdcf6655069f221 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Tue, 7 Jul 2020 13:23:23 -0700 Subject: [PATCH 240/705] Adds newly available GCF regions. (#722) --- CHANGELOG.md | 2 ++ scripts/fetch-regions | 7 +++++++ spec/function-builder.spec.ts | 30 ------------------------------ src/function-builder.ts | 11 ++++------- src/function-configuration.ts | 6 +++++- 5 files changed, 18 insertions(+), 38 deletions(-) create mode 100755 scripts/fetch-regions diff --git a/CHANGELOG.md b/CHANGELOG.md index 43975c685..edab83fdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,3 @@ - Fixes error when last argument to logger methods is `null`. (#716) +- Adds newly available locations `us-west3`, `europe-west6`, `northamerica-northeast1`, and `australia-southeast1`. +- No longer throw errors for unrecognized regions (deploy will error instead). diff --git a/scripts/fetch-regions b/scripts/fetch-regions new file mode 100755 index 000000000..d529cdf9b --- /dev/null +++ b/scripts/fetch-regions @@ -0,0 +1,7 @@ +#!/bin/bash + +if [ -z $1 ]; then + echo "Must provide a project id as first argument." && exit 1 +fi; + +gcloud functions regions list --project $1 --format=json | jq 'map(.locationId)' \ No newline at end of file diff --git a/spec/function-builder.spec.ts b/spec/function-builder.spec.ts index 0044e5661..0ad4265ed 100644 --- a/spec/function-builder.spec.ts +++ b/spec/function-builder.spec.ts @@ -121,14 +121,6 @@ describe('FunctionBuilder', () => { expect(fn.__trigger.timeout).to.deep.equal('90s'); }); - it('should fail if valid runtime options but unsupported region are set (reverse order)', () => { - expect(() => { - functions - .runWith({ timeoutSeconds: 90, memory: '256MB' }) - .region('unsupported' as any); - }).to.throw(Error, 'region'); - }); - it('should fail if supported region but invalid runtime options are set (reverse order)', () => { expect(() => { functions @@ -165,28 +157,6 @@ describe('FunctionBuilder', () => { }).to.throw(Error, 'TimeoutSeconds'); }); - it('should throw an error if user chooses an invalid region', () => { - expect(() => { - return functions.region('unsupported' as any); - }).to.throw(Error, 'region'); - - expect(() => { - return functions.region('unsupported' as any).runWith({ - timeoutSeconds: 500, - } as any); - }).to.throw(Error, 'region'); - - expect(() => { - return functions.region('unsupported' as any, 'us-east1'); - }).to.throw(Error, 'region'); - - expect(() => { - return functions.region('unsupported' as any, 'us-east1').runWith({ - timeoutSeconds: 500, - } as any); - }).to.throw(Error, 'region'); - }); - it('should throw an error if user chooses no region when using .region()', () => { expect(() => { return functions.region(); diff --git a/src/function-builder.ts b/src/function-builder.ts index 580e0498b..76c4b6c9d 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -78,11 +78,6 @@ function assertRegionsAreValid(regions: string[]): boolean { if (!regions.length) { throw new Error('You must specify at least one region'); } - if (_.difference(regions, SUPPORTED_REGIONS).length) { - throw new Error( - `The only valid regions are: ${SUPPORTED_REGIONS.join(', ')}` - ); - } return true; } @@ -95,7 +90,7 @@ function assertRegionsAreValid(regions: string[]): boolean { * functions.region('us-east1', 'us-central1') */ export function region( - ...regions: Array + ...regions: Array ): FunctionBuilder { if (assertRegionsAreValid(regions)) { return new FunctionBuilder({ regions }); @@ -127,7 +122,9 @@ export class FunctionBuilder { * @example * functions.region('us-east1', 'us-central1') */ - region(...regions: Array): FunctionBuilder { + region( + ...regions: Array + ): FunctionBuilder { if (assertRegionsAreValid(regions)) { this.options.regions = regions; return this; diff --git a/src/function-configuration.ts b/src/function-configuration.ts index 05d4517a8..2ec102fda 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -5,11 +5,15 @@ export const SUPPORTED_REGIONS = [ 'us-central1', 'us-east1', 'us-east4', + 'us-west3', 'europe-west1', 'europe-west2', 'europe-west3', + 'europe-west6', 'asia-east2', 'asia-northeast1', + 'northamerica-northeast1', + 'australia-southeast1', ] as const; /** @@ -70,6 +74,6 @@ export interface RuntimeOptions { } export interface DeploymentOptions extends RuntimeOptions { - regions?: Array; + regions?: Array; schedule?: Schedule; } From ab3ae2af275a0037eff905804a49d031f1734f93 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Wed, 8 Jul 2020 15:02:58 -0400 Subject: [PATCH 241/705] Fix issues with .ref in Database functions (#727) --- CHANGELOG.md | 1 + src/providers/database.ts | 25 ++++++++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edab83fdb..35049e3ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ - Fixes error when last argument to logger methods is `null`. (#716) - Adds newly available locations `us-west3`, `europe-west6`, `northamerica-northeast1`, and `australia-southeast1`. - No longer throw errors for unrecognized regions (deploy will error instead). +- Fixes error where `snap.ref` in database functions did not work when using the Emulator Suite (#726) diff --git a/src/providers/database.ts b/src/providers/database.ts index 8c8de4ec6..2aa50b55a 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -41,6 +41,7 @@ export const provider = 'google.firebase.database'; export const service = 'firebaseio.com'; const databaseURLRegex = new RegExp('^https://([^.]+).'); +const emulatorDatabaseURLRegex = new RegExp('^http://.*ns=([^&]+)'); /** * Registers a function that triggers on events from a specific @@ -138,14 +139,25 @@ export function _refWithOptions( '\n If you are unit testing, please set process.env.FIREBASE_CONFIG' ); } - const match = databaseURL.match(databaseURLRegex); - if (!match) { + + let instance = undefined; + const prodMatch = databaseURL.match(databaseURLRegex); + if (prodMatch) { + instance = prodMatch[1]; + } else { + const emulatorMatch = databaseURL.match(emulatorDatabaseURLRegex); + if (emulatorMatch) { + instance = emulatorMatch[1]; + } + } + + if (!instance) { throw new Error( 'Invalid value for config firebase.databaseURL: ' + databaseURL ); } - const subdomain = match[1]; - return `projects/_/instances/${subdomain}/refs/${normalized}`; + + return `projects/_/instances/${instance}/refs/${normalized}`; }; return new RefBuilder(apps(), resourceGetter, options); @@ -350,7 +362,10 @@ export class DataSnapshot { private app?: firebase.app.App, instance?: string ) { - if (instance) { + if (app && app.options.databaseURL.startsWith('http:')) { + // In this case we're dealing with an emulator + this.instance = app.options.databaseURL; + } else if (instance) { // SDK always supplies instance, but user's unit tests may not this.instance = instance; } else if (app) { From ad144e41c82e551fe323d473919d7e8426d11d84 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Thu, 9 Jul 2020 17:09:42 -0700 Subject: [PATCH 242/705] Update ref docs for handler SDK to include scheduled functions. (#731) --- src/handler-builder.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/handler-builder.ts b/src/handler-builder.ts index 81232df59..ea0f4af74 100644 --- a/src/handler-builder.ts +++ b/src/handler-builder.ts @@ -305,13 +305,20 @@ export class HandlerBuilder { /** * Create a handler for Cloud Pub/Sub events. - - * `pubsub.onPublish` handles the publication of messages to a topic. - + * + * `topic.onPublish` handles messages published to a Pub/Sub topic from SDKs, Cloud Console, or gcloud CLI. + * * @example * ```javascript * exports.myFunction = functions.handler.pubsub.topic.onPublish((message) => { ... }) * ``` + + * `schedule.onPublish` handles messages published to a Pub/Sub topic on a schedule. + * + * @example + * ```javascript + * exports.myFunction = functions.handler.pubsub.schedule.onPublish((message) => { ... }) + * ``` */ get pubsub() { return { From 858cb9befd9468a9f1e1f2fb0d6f339cce3dec35 Mon Sep 17 00:00:00 2001 From: egilmorez Date: Fri, 10 Jul 2020 11:01:11 -0700 Subject: [PATCH 243/705] Eg pubsub fixes (#729) * Adding logger SDK to reference, with some edits. * Adding link to structured logging details per feedback. * Adding ScheduleBuilder class to TOC and adding minimal commenting. * Improving wording and format per feedback. * I think we need to document both the schedule function and the builder class this way. * Last edits from feedback. --- docgen/content-sources/toc.yaml | 2 ++ src/providers/pubsub.ts | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/docgen/content-sources/toc.yaml b/docgen/content-sources/toc.yaml index a394975df..37093f7c2 100644 --- a/docgen/content-sources/toc.yaml +++ b/docgen/content-sources/toc.yaml @@ -106,6 +106,8 @@ toc: path: /docs/reference/functions/providers_pubsub_.message.html - title: 'TopicBuilder' path: /docs/reference/functions/providers_pubsub_.topicbuilder.html + - title: 'ScheduleBuilder' + path: /docs/reference/functions/providers_pubsub_.schedulebuilder.html - title: 'functions.remoteconfig' path: /docs/reference/functions/providers_remoteconfig_.html diff --git a/src/providers/pubsub.ts b/src/providers/pubsub.ts index 0f12f669c..264ef9480 100644 --- a/src/providers/pubsub.ts +++ b/src/providers/pubsub.ts @@ -98,6 +98,12 @@ export class TopicBuilder { } } +/** + * Registers a Cloud Function to run at specified times. + * + * @param schedule The schedule, in Unix Crontab or AppEngine syntax. + * @return ScheduleBuilder interface. + */ export function schedule(schedule: string): ScheduleBuilder { return _scheduleWithOptions(schedule, {}); } @@ -120,6 +126,15 @@ export function _scheduleWithOptions( }); } +/** + * The builder for scheduled functions, which are powered by + * Google Pub/Sub and Cloud Scheduler. Describes the Cloud Scheduler + * job that is deployed to trigger a scheduled function at the provided + * frequency. For more information, see + * [Schedule functions](/docs/functions/schedule-functions). + * + * Access via [`functions.pubsub.schedule()`](providers_pubsub_.html#schedule). + */ export class ScheduleBuilder { /** @hidden */ constructor( @@ -137,6 +152,14 @@ export class ScheduleBuilder { return this; } + /** + * Event handler for scheduled functions. Triggered whenever the associated + * scheduler job sends a Pub/Sub message. + * + * @param handler Handler that fires whenever the associated + * scheduler job sends a Pub/Sub message. + * @return A Cloud Function that you can export and deploy. + */ onRun(handler: (context: EventContext) => PromiseLike | any) { const cloudFunction = makeCloudFunction({ contextOnlyHandler: handler, From b424fd3ccb4284a663213993fe2349eaef057992 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Fri, 10 Jul 2020 15:21:09 -0700 Subject: [PATCH 244/705] Adds four new regions launched July 10. (#733) --- CHANGELOG.md | 10 +++++++++- src/function-configuration.ts | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35049e3ca..d85f674fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,12 @@ - Fixes error when last argument to logger methods is `null`. (#716) -- Adds newly available locations `us-west3`, `europe-west6`, `northamerica-northeast1`, and `australia-southeast1`. +- Adds eight new available regions: + - `us-west2` + - `us-west3` + - `us-west4` + - `europe-west6` + - `asia-northeast2` + - `northamerica-northeast1` + - `southamerica-east1` + - `australia-southeast1` - No longer throw errors for unrecognized regions (deploy will error instead). - Fixes error where `snap.ref` in database functions did not work when using the Emulator Suite (#726) diff --git a/src/function-configuration.ts b/src/function-configuration.ts index 2ec102fda..0ab2f9c31 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -5,14 +5,18 @@ export const SUPPORTED_REGIONS = [ 'us-central1', 'us-east1', 'us-east4', + 'us-west2', 'us-west3', + 'us-west4', 'europe-west1', 'europe-west2', 'europe-west3', 'europe-west6', 'asia-east2', 'asia-northeast1', + 'asia-northeast2', 'northamerica-northeast1', + 'southamerica-east1', 'australia-southeast1', ] as const; From 1e4de88ed658289f6c7d4e94ac24732726da04c0 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Tue, 14 Jul 2020 00:00:43 +0000 Subject: [PATCH 245/705] 3.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e198c416..bef1bb506 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.7.0", + "version": "3.8.0", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From a07d6efeb39600a6b8b31a583eef8fc4a694df50 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Tue, 14 Jul 2020 00:00:49 +0000 Subject: [PATCH 246/705] [firebase-release] Removed change log and reset repo after 3.8.0 release --- CHANGELOG.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d85f674fa..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +0,0 @@ -- Fixes error when last argument to logger methods is `null`. (#716) -- Adds eight new available regions: - - `us-west2` - - `us-west3` - - `us-west4` - - `europe-west6` - - `asia-northeast2` - - `northamerica-northeast1` - - `southamerica-east1` - - `australia-southeast1` -- No longer throw errors for unrecognized regions (deploy will error instead). -- Fixes error where `snap.ref` in database functions did not work when using the Emulator Suite (#726) From 8bc84dda6ed1b9dc485c0f462aec2d9066453442 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Fri, 31 Jul 2020 15:27:46 -0700 Subject: [PATCH 247/705] Updates logging in https callable functions. (#745) --- README.md | 2 +- src/cloud-functions.ts | 3 ++- src/providers/crashlytics.ts | 2 +- src/providers/https.ts | 34 ++++++++++++++++++---------------- src/setup.ts | 5 +++-- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 59c5c0936..33fbaa746 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ const notifyUsers = require('./notify-users'); exports.newPost = functions.database .ref('/posts/{postId}') .onCreate((snapshot, context) => { - console.log('Received new post with ID:', context.params.postId); + functions.logger.info('Received new post with ID:', context.params.postId); return notifyUsers(snapshot.val()); }); ``` diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index f249bf7f9..b97c4ad7e 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -23,6 +23,7 @@ import { Request, Response } from 'express'; import * as _ from 'lodash'; import { DeploymentOptions, Schedule } from './function-configuration'; +import { warn } from './logger'; export { Request, Response }; /** @hidden */ @@ -379,7 +380,7 @@ export function makeCloudFunction({ promise = handler(dataOrChange, context); } if (typeof promise === 'undefined') { - console.warn('Function returned undefined, expected Promise or value'); + warn('Function returned undefined, expected Promise or value'); } return Promise.resolve(promise) .then((result) => { diff --git a/src/providers/crashlytics.ts b/src/providers/crashlytics.ts index e7a6fe882..54fc26469 100644 --- a/src/providers/crashlytics.ts +++ b/src/providers/crashlytics.ts @@ -77,7 +77,7 @@ export class IssueBuilder { * const slackMessage = ` There's a new issue (${issueId}) ` + * `in your app - ${issueTitle}`; * return notifySlack(slackMessage).then(() => { - * console.log(`Posted new issue ${issueId} successfully to Slack`); + * functions.logger.info(`Posted new issue ${issueId} successfully to Slack`); * }); * }); * ``` diff --git a/src/providers/https.ts b/src/providers/https.ts index cd9f1745d..ec397fc64 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -24,9 +24,11 @@ import * as cors from 'cors'; import * as express from 'express'; import * as firebase from 'firebase-admin'; import * as _ from 'lodash'; + import { apps } from '../apps'; import { HttpsFunction, optionsToTrigger, Runnable } from '../cloud-functions'; import { DeploymentOptions } from '../function-configuration'; +import { warn, error } from '../logger'; /** @hidden */ export interface Request extends express.Request { @@ -285,13 +287,13 @@ interface HttpResponseBody { function isValidRequest(req: Request): req is HttpRequest { // The body must not be empty. if (!req.body) { - console.warn('Request is missing body.'); + warn('Request is missing body.'); return false; } // Make sure it's a POST. if (req.method !== 'POST') { - console.warn('Request has invalid method.', req.method); + warn('Request has invalid method.', req.method); return false; } @@ -303,13 +305,13 @@ function isValidRequest(req: Request): req is HttpRequest { contentType = contentType.substr(0, semiColon).trim(); } if (contentType !== 'application/json') { - console.warn('Request has incorrect Content-Type.', contentType); + warn('Request has incorrect Content-Type.', contentType); return false; } // The body must have data. if (_.isUndefined(req.body.data)) { - console.warn('Request body is missing data.', req.body); + warn('Request body is missing data.', req.body); return false; } @@ -318,7 +320,7 @@ function isValidRequest(req: Request): req is HttpRequest { // Verify that the body does not have any extra fields. const extras = _.omit(req.body, 'data'); if (!_.isEmpty(extras)) { - console.warn('Request body has extra fields.', extras); + warn('Request body has extra fields.', extras); return false; } return true; @@ -363,7 +365,7 @@ export function encode(data: any): any { return _.mapValues(data, encode); } // If we got this far, the data is not encodable. - console.error('Data cannot be encoded in JSON.', data); + error('Data cannot be encoded in JSON.', data); throw new Error('Data cannot be encoded in JSON: ' + data); } @@ -386,13 +388,13 @@ export function decode(data: any): any { // worth all the extra code to detect that case. const value = parseFloat(data.value); if (_.isNaN(value)) { - console.error('Data cannot be decoded from JSON.', data); + error('Data cannot be decoded from JSON.', data); throw new Error('Data cannot be decoded from JSON: ' + data); } return value; } default: { - console.error('Data cannot be decoded from JSON.', data); + error('Data cannot be decoded from JSON.', data); throw new Error('Data cannot be decoded from JSON: ' + data); } } @@ -420,7 +422,7 @@ export function _onCallWithOptions( const func = async (req: Request, res: express.Response) => { try { if (!isValidRequest(req)) { - console.error('Invalid request', req); + error('Invalid request, unable to process.'); throw new HttpsError('invalid-argument', 'Bad Request'); } @@ -441,7 +443,7 @@ export function _onCallWithOptions( uid: authToken.uid, token: authToken, }; - } catch (e) { + } catch (err) { throw new HttpsError('unauthenticated', 'Unauthenticated'); } } @@ -464,15 +466,15 @@ export function _onCallWithOptions( // If there was some result, encode it in the body. const responseBody: HttpResponseBody = { result }; res.status(200).send(responseBody); - } catch (error) { - if (!(error instanceof HttpsError)) { + } catch (err) { + if (!(err instanceof HttpsError)) { // This doesn't count as an 'explicit' error. - console.error('Unhandled error', error); - error = new HttpsError('internal', 'INTERNAL'); + error('Unhandled error', error); + err = new HttpsError('internal', 'INTERNAL'); } - const { status } = error.httpErrorCode; - const body = { error: error.toJSON() }; + const { status } = err.httpErrorCode; + const body = { error: err.toJSON() }; res.status(status).send(body); } diff --git a/src/setup.ts b/src/setup.ts index b30fa6226..d2935af15 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -22,6 +22,7 @@ /** @hidden */ import { firebaseConfig } from './config'; +import { warn } from './logger'; // Set up for config and vars export function setup() { @@ -45,7 +46,7 @@ export function setup() { // If FIREBASE_CONFIG is still not found, try using GCLOUD_PROJECT to estimate if (!process.env.FIREBASE_CONFIG) { if (process.env.GCLOUD_PROJECT) { - console.warn( + warn( 'Warning, estimating Firebase Config based on GCLOUD_PROJECT. Initializing firebase-admin may fail' ); process.env.FIREBASE_CONFIG = JSON.stringify({ @@ -58,7 +59,7 @@ export function setup() { projectId: process.env.GCLOUD_PROJECT, }); } else { - console.warn( + warn( 'Warning, FIREBASE_CONFIG and GCLOUD_PROJECT environment variables are missing. Initializing firebase-admin will fail' ); } From e4f2eba2367fd6863e340617e49b55b214d4d8b6 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Fri, 31 Jul 2020 15:34:06 -0700 Subject: [PATCH 248/705] Adds support for three new regions. (#750) --- CHANGELOG.md | 2 ++ src/function-configuration.ts | 3 +++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..3c25216f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +- Updates HTTP callable functions to use structured logging for Node 10+ environments. +- Adds type hints for new Cloud Functions regions `asia-northeast3`, `asia-south1`, and `asia-southeast2`. diff --git a/src/function-configuration.ts b/src/function-configuration.ts index 0ab2f9c31..a7d87b726 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -15,6 +15,9 @@ export const SUPPORTED_REGIONS = [ 'asia-east2', 'asia-northeast1', 'asia-northeast2', + 'asia-northeast3', + 'asia-south1', + 'asia-southeast2', 'northamerica-northeast1', 'southamerica-east1', 'australia-southeast1', From 0ae7c3c8b8fd985fe669c2768df7df3ce847e5f3 Mon Sep 17 00:00:00 2001 From: p-young <35713583+p-young@users.noreply.github.com> Date: Fri, 31 Jul 2020 15:48:04 -0700 Subject: [PATCH 249/705] Fix onRequest handler return type to allow promises (async) (#705) Co-authored-by: Michael Bleigh --- src/function-builder.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/function-builder.ts b/src/function-builder.ts index 76c4b6c9d..27fcd821a 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -154,7 +154,10 @@ export class FunctionBuilder { * same signature as an Express app. */ onRequest: ( - handler: (req: https.Request, resp: express.Response) => void + handler: ( + req: https.Request, + resp: express.Response + ) => void | Promise ) => https._onRequestWithOptions(handler, this.options), /** * Declares a callable method for clients to call using a Firebase SDK. From 78fb92f7943ccb5e244ae4b5a164cfa302faa719 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Fri, 31 Jul 2020 16:11:18 -0700 Subject: [PATCH 250/705] Update CHANGELOG.md (#751) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c25216f0..3b20c6064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ - Updates HTTP callable functions to use structured logging for Node 10+ environments. - Adds type hints for new Cloud Functions regions `asia-northeast3`, `asia-south1`, and `asia-southeast2`. +- Updates type definition of `https.onRequest` to allow for promises (async functions). From e91d16a0c7d39c8894a7226e3f79700d6c6f1c01 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 31 Jul 2020 23:20:59 +0000 Subject: [PATCH 251/705] 3.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bef1bb506..cec716c20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.8.0", + "version": "3.9.0", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From 02f61cf1dc3828cb8af842ed15c1cbab3466b074 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 31 Jul 2020 23:21:05 +0000 Subject: [PATCH 252/705] [firebase-release] Removed change log and reset repo after 3.9.0 release --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b20c6064..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +0,0 @@ -- Updates HTTP callable functions to use structured logging for Node 10+ environments. -- Adds type hints for new Cloud Functions regions `asia-northeast3`, `asia-south1`, and `asia-southeast2`. -- Updates type definition of `https.onRequest` to allow for promises (async functions). From c69c974f18431f27244f03abfb67955718b4efb7 Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Fri, 7 Aug 2020 14:17:48 -0700 Subject: [PATCH 253/705] Update reference docs for EventContext.EventType (#743) --- src/cloud-functions.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index b97c4ad7e..1850b0d62 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -89,22 +89,22 @@ export interface EventContext { eventId: string; /** - * Type of event. Valid values are: + * Type of event. Possible values are: * - * * `providers/google.firebase.analytics/eventTypes/event.log` - * * `providers/firebase.auth/eventTypes/user.create` - * * `providers/firebase.auth/eventTypes/user.delete` - * * `providers/firebase.crashlytics/eventTypes/issue.new` - * * `providers/firebase.crashlytics/eventTypes/issue.regressed` - * * `providers/firebase.crashlytics/eventTypes/issue.velocityAlert` - * * `providers/google.firebase.database/eventTypes/ref.write` - * * `providers/google.firebase.database/eventTypes/ref.create` - * * `providers/google.firebase.database/eventTypes/ref.update` - * * `providers/google.firebase.database/eventTypes/ref.delete` - * * `providers/cloud.firestore/eventTypes/document.write` - * * `providers/cloud.firestore/eventTypes/document.create` - * * `providers/cloud.firestore/eventTypes/document.update` - * * `providers/cloud.firestore/eventTypes/document.delete` + * * `google.analytics.event.log` + * * `google.firebase.auth.user.create` + * * `google.firebase.auth.user.delete` + * * `google.firebase.crashlytics.issue.new` + * * `google.firebase.crashlytics.issue.regressed` + * * `google.firebase.crashlytics.issue.velocityAlert` + * * `google.firebase.database.ref.write` + * * `google.firebase.database.ref.create` + * * `google.firebase.database.ref.update` + * * `google.firebase.database.ref.delete` + * * `google.firestore.document.write` + * * `google.firestore.document.create` + * * `google.firestore.document.update` + * * `google.firestore.document.delete` * * `google.pubsub.topic.publish` * * `google.firebase.remoteconfig.update` * * `google.storage.object.finalize` From d1432c50863dec4b27834241279180f12f6148d5 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Mon, 10 Aug 2020 12:44:09 -0700 Subject: [PATCH 254/705] Updates firebase-admin peerDependency (#756) --- CHANGELOG.md | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..bbf6a741b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Updates `firebase-admin` dependency to support `^9.0.0` in addition to `^8.0.0`. Note that `firebase-admin` no longer supports Node.js 8.x as of `9.0.0`. diff --git a/package.json b/package.json index cec716c20..8cb004a34 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "yargs": "^15.3.1" }, "peerDependencies": { - "firebase-admin": "^8.0.0" + "firebase-admin": "^8.0.0 || ^9.0.0" }, "engines": { "node": "^8.13.0 || >=10.10.0" From 84433df63147d61a5330f9b8d3e5c5e4837ad658 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Wed, 12 Aug 2020 10:27:37 -0700 Subject: [PATCH 255/705] Fixes logging of unhandled exceptions in callable functions. (#759) --- CHANGELOG.md | 1 + src/providers/https.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbf6a741b..6a558f5e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ - Updates `firebase-admin` dependency to support `^9.0.0` in addition to `^8.0.0`. Note that `firebase-admin` no longer supports Node.js 8.x as of `9.0.0`. +- Fixes logging of unexpected errors in `https.onCall()` functions. diff --git a/src/providers/https.ts b/src/providers/https.ts index ec397fc64..cf64c9d90 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -469,7 +469,7 @@ export function _onCallWithOptions( } catch (err) { if (!(err instanceof HttpsError)) { // This doesn't count as an 'explicit' error. - error('Unhandled error', error); + error('Unhandled error', err); err = new HttpsError('internal', 'INTERNAL'); } From ae9e856f0be28e68ed8bf68a23f581eccfd7aed3 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Wed, 12 Aug 2020 19:58:53 +0000 Subject: [PATCH 256/705] 3.9.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8cb004a34..6560a8d07 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.9.0", + "version": "3.9.1", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From b7514295540b0e872e9be46656fb63b158fa9484 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Wed, 12 Aug 2020 19:58:58 +0000 Subject: [PATCH 257/705] [firebase-release] Removed change log and reset repo after 3.9.1 release --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a558f5e0..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +0,0 @@ -- Updates `firebase-admin` dependency to support `^9.0.0` in addition to `^8.0.0`. Note that `firebase-admin` no longer supports Node.js 8.x as of `9.0.0`. -- Fixes logging of unexpected errors in `https.onCall()` functions. From 5e287286c7aa6bbf66f36d21e49db5a7d5e06e20 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Mon, 17 Aug 2020 05:44:48 -0400 Subject: [PATCH 258/705] Restore failurePolicy (#760) --- CHANGELOG.md | 1 + spec/function-builder.spec.ts | 31 +++++++++++++++++++++++ src/cloud-functions.ts | 25 +++++++++++++++++-- src/function-builder.ts | 47 +++++++++++++++++++++++++++++------ src/function-configuration.ts | 13 ++++++++++ 5 files changed, 107 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..7c0e3b45c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Adds support for functions failure policies (#482) diff --git a/spec/function-builder.spec.ts b/spec/function-builder.spec.ts index 0ad4265ed..1f9122061 100644 --- a/spec/function-builder.spec.ts +++ b/spec/function-builder.spec.ts @@ -82,6 +82,7 @@ describe('FunctionBuilder', () => { const fn = functions .runWith({ timeoutSeconds: 90, + failurePolicy: { retry: {} }, memory: '256MB', }) .auth.user() @@ -89,6 +90,20 @@ describe('FunctionBuilder', () => { expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); + expect(fn.__trigger.failurePolicy).to.deep.equal({ retry: {} }); + }); + + it("should apply a default failure policy if it's aliased with `true`", () => { + const fn = functions + .runWith({ + failurePolicy: true, + memory: '256MB', + timeoutSeconds: 90, + }) + .auth.user() + .onCreate((user) => user); + + expect(fn.__trigger.failurePolicy).to.deep.equal({ retry: {} }); }); it('should allow both supported region and valid runtime options to be set', () => { @@ -129,6 +144,22 @@ describe('FunctionBuilder', () => { }).to.throw(Error, 'TimeoutSeconds'); }); + it('should throw an error if user chooses a failurePolicy which is neither an object nor a boolean', () => { + expect(() => + functions.runWith({ + failurePolicy: (1234 as unknown) as functions.RuntimeOptions['failurePolicy'], + }) + ).to.throw(Error, 'failurePolicy must be a boolean or an object'); + }); + + it('should throw an error if user chooses a failurePolicy.retry which is not an object', () => { + expect(() => + functions.runWith({ + failurePolicy: { retry: (1234 as unknown) as object }, + }) + ).to.throw(Error, 'failurePolicy.retry'); + }); + it('should throw an error if user chooses an invalid memory allocation', () => { expect(() => { return functions.runWith({ diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 1850b0d62..387953bdc 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -22,8 +22,13 @@ import { Request, Response } from 'express'; import * as _ from 'lodash'; -import { DeploymentOptions, Schedule } from './function-configuration'; import { warn } from './logger'; +import { + DEFAULT_FAILURE_POLICY, + DeploymentOptions, + FailurePolicy, + Schedule, +} from './function-configuration'; export { Request, Response }; /** @hidden */ @@ -205,6 +210,7 @@ export namespace Change { if (json.fieldMask) { before = applyFieldMask(before, json.after, json.fieldMask); } + return Change.fromObjects( customizer(before || {}), customizer(json.after || {}) @@ -219,7 +225,8 @@ export namespace Change { ) { const before = _.assign({}, after); const masks = fieldMask.split(','); - _.forEach(masks, (mask) => { + + masks.forEach((mask) => { const val = _.get(sparseBefore, mask); if (typeof val === 'undefined') { _.unset(before, mask); @@ -227,6 +234,7 @@ export namespace Change { _.set(before, mask, val); } }); + return before; } } @@ -256,6 +264,7 @@ export interface TriggerAnnotated { resource: string; service: string; }; + failurePolicy?: FailurePolicy; httpsTrigger?: {}; labels?: { [key: string]: string }; regions?: string[]; @@ -473,6 +482,18 @@ export function optionsToTrigger(options: DeploymentOptions) { if (options.regions) { trigger.regions = options.regions; } + if (options.failurePolicy !== undefined) { + switch (options.failurePolicy) { + case false: + trigger.failurePolicy = undefined; + break; + case true: + trigger.failurePolicy = DEFAULT_FAILURE_POLICY; + break; + default: + trigger.failurePolicy = options.failurePolicy; + } + } if (options.timeoutSeconds) { trigger.timeout = options.timeoutSeconds.toString() + 's'; } diff --git a/src/function-builder.ts b/src/function-builder.ts index 27fcd821a..6050927ea 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -66,6 +66,23 @@ function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean { `TimeoutSeconds must be between 0 and ${MAX_TIMEOUT_SECONDS}` ); } + if (runtimeOptions.failurePolicy !== undefined) { + if ( + _.isBoolean(runtimeOptions.failurePolicy) === false && + _.isObjectLike(runtimeOptions.failurePolicy) === false + ) { + throw new Error(`failurePolicy must be a boolean or an object.`); + } + + if (typeof runtimeOptions.failurePolicy === 'object') { + if ( + _.isObjectLike(runtimeOptions.failurePolicy.retry) === false || + _.isEmpty(runtimeOptions.failurePolicy.retry) === false + ) { + throw new Error('failurePolicy.retry must be an empty object.'); + } + } + } return true; } @@ -100,10 +117,14 @@ export function region( /** * Configure runtime options for the function. * @param runtimeOptions Object with three optional fields: - * 1. memory: amount of memory to allocate to the function, possible values - * are: '128MB', '256MB', '512MB', '1GB', and '2GB'. - * 2. timeoutSeconds: timeout for the function in seconds, possible values are - * 0 to 540. + * 1. failurePolicy: failure policy of the function, with boolean `true` being + * equivalent to providing an empty retry object. + * 2. memory: amount of memory to allocate to the function, with possible + * values being '128MB', '256MB', '512MB', '1GB', and '2GB'. + * 3. timeoutSeconds: timeout for the function in seconds, with possible + * values being 0 to 540. + * + * Value must not be null. */ export function runWith(runtimeOptions: RuntimeOptions): FunctionBuilder { if (assertRuntimeOptionsValid(runtimeOptions)) { @@ -134,10 +155,14 @@ export class FunctionBuilder { /** * Configure runtime options for the function. * @param runtimeOptions Object with three optional fields: - * 1. memory: amount of memory to allocate to the function, possible values - * are: '128MB', '256MB', '512MB', '1GB', and '2GB'. - * 2. timeoutSeconds: timeout for the function in seconds, possible values are - * 0 to 540. + * 1. failurePolicy: failure policy of the function, with boolean `true` being + * equivalent to providing an empty retry object. + * 2. memory: amount of memory to allocate to the function, with possible + * values being '128MB', '256MB', '512MB', '1GB', and '2GB'. + * 3. timeoutSeconds: timeout for the function in seconds, with possible + * values being 0 to 540. + * + * Value must not be null. */ runWith(runtimeOptions: RuntimeOptions): FunctionBuilder { if (assertRuntimeOptionsValid(runtimeOptions)) { @@ -147,6 +172,12 @@ export class FunctionBuilder { } get https() { + if (this.options.failurePolicy !== undefined) { + console.warn( + 'RuntimeOptions.failurePolicy is not supported in https functions.' + ); + } + return { /** * Handle HTTP requests. diff --git a/src/function-configuration.ts b/src/function-configuration.ts index a7d87b726..31c708f3f 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -64,7 +64,20 @@ export interface Schedule { retryConfig?: ScheduleRetryConfig; } +export interface FailurePolicy { + retry: {}; +} + +export const DEFAULT_FAILURE_POLICY: FailurePolicy = { + retry: {}, +}; + export interface RuntimeOptions { + /** + * Failure policy of the function, with boolean `true` being equivalent to + * providing an empty retry object. + */ + failurePolicy?: FailurePolicy | boolean; /** * Amount of memory to allocate to the function. */ From 7bd795e2fac0a87d9c3c57b31594b67cfc59ad8c Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Thu, 20 Aug 2020 15:59:59 +0000 Subject: [PATCH 259/705] 3.10.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6560a8d07..48a71e881 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.9.1", + "version": "3.10.0", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From bc9d8caef0e44a84b7a1a4ca749268467bbe80df Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Thu, 20 Aug 2020 16:00:07 +0000 Subject: [PATCH 260/705] [firebase-release] Removed change log and reset repo after 3.10.0 release --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c0e3b45c..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +0,0 @@ -- Adds support for functions failure policies (#482) From 6b15ff7da69721bc1a1ecd8e44f9c76481061213 Mon Sep 17 00:00:00 2001 From: David Hagege Date: Fri, 21 Aug 2020 08:28:36 +0900 Subject: [PATCH 261/705] Add support for VPC connectors in `functions.runWith` (#752) --- spec/function-builder.spec.ts | 39 +++++++++++++++++++++++++++++++++++ src/cloud-functions.ts | 11 ++++++++++ src/function-builder.ts | 31 ++++++++++++++++++++++------ src/function-configuration.ts | 19 +++++++++++++++++ 4 files changed, 94 insertions(+), 6 deletions(-) diff --git a/spec/function-builder.spec.ts b/spec/function-builder.spec.ts index 1f9122061..eec531fdf 100644 --- a/spec/function-builder.spec.ts +++ b/spec/function-builder.spec.ts @@ -199,4 +199,43 @@ describe('FunctionBuilder', () => { } as any); }).to.throw(Error, 'at least one region'); }); + + it('should allow a vpcConnector to be set', () => { + const fn = functions + .runWith({ + vpcConnector: 'test-connector', + }) + .auth.user() + .onCreate((user) => user); + + expect(fn.__trigger.vpcConnector).to.equal('test-connector'); + }); + + it('should allow a vpcConnectorEgressSettings to be set', () => { + const fn = functions + .runWith({ + vpcConnector: 'test-connector', + vpcConnectorEgressSettings: 'PRIVATE_RANGES_ONLY', + }) + .auth.user() + .onCreate((user) => user); + + expect(fn.__trigger.vpcConnectorEgressSettings).to.equal( + 'PRIVATE_RANGES_ONLY' + ); + }); + + it('should throw an error if user chooses an invalid vpcConnectorEgressSettings', () => { + expect(() => { + return functions.runWith({ + vpcConnector: 'test-connector', + vpcConnectorEgressSettings: 'INCORRECT_OPTION', + } as any); + }).to.throw( + Error, + `The only valid vpcConnectorEgressSettings values are: ${functions.VPC_EGRESS_SETTINGS_OPTIONS.join( + ',' + )}` + ); + }); }); diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 387953bdc..109a1ad05 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -270,6 +270,8 @@ export interface TriggerAnnotated { regions?: string[]; schedule?: Schedule; timeout?: string; + vpcConnector?: string; + vpcConnectorEgressSettings?: string; }; } @@ -514,5 +516,14 @@ export function optionsToTrigger(options: DeploymentOptions) { if (options.maxInstances) { trigger.maxInstances = options.maxInstances; } + + if (options.vpcConnector) { + trigger.vpcConnector = options.vpcConnector; + } + + if (options.vpcConnectorEgressSettings) { + trigger.vpcConnectorEgressSettings = options.vpcConnectorEgressSettings; + } + return trigger; } diff --git a/src/function-builder.ts b/src/function-builder.ts index 6050927ea..919f9f09b 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -30,6 +30,7 @@ import { RuntimeOptions, SUPPORTED_REGIONS, VALID_MEMORY_OPTIONS, + VPC_EGRESS_SETTINGS_OPTIONS, } from './function-configuration'; import * as analytics from './providers/analytics'; import * as auth from './providers/auth'; @@ -66,6 +67,21 @@ function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean { `TimeoutSeconds must be between 0 and ${MAX_TIMEOUT_SECONDS}` ); } + + if ( + runtimeOptions.vpcConnectorEgressSettings && + !_.includes( + VPC_EGRESS_SETTINGS_OPTIONS, + runtimeOptions.vpcConnectorEgressSettings + ) + ) { + throw new Error( + `The only valid vpcConnectorEgressSettings values are: ${VPC_EGRESS_SETTINGS_OPTIONS.join( + ',' + )}` + ); + } + if (runtimeOptions.failurePolicy !== undefined) { if ( _.isBoolean(runtimeOptions.failurePolicy) === false && @@ -116,13 +132,16 @@ export function region( /** * Configure runtime options for the function. - * @param runtimeOptions Object with three optional fields: - * 1. failurePolicy: failure policy of the function, with boolean `true` being + * @param runtimeOptions Object with optional fields: + * 1. memory: amount of memory to allocate to the function, possible values + * are: '128MB', '256MB', '512MB', '1GB', and '2GB'. + * 2. timeoutSeconds: timeout for the function in seconds, possible values are + * 0 to 540. + * 3. failurePolicy: failure policy of the function, with boolean `true` being * equivalent to providing an empty retry object. - * 2. memory: amount of memory to allocate to the function, with possible - * values being '128MB', '256MB', '512MB', '1GB', and '2GB'. - * 3. timeoutSeconds: timeout for the function in seconds, with possible - * values being 0 to 540. + * 4. vpcConnector: id of a VPC connector in same project and region + * 5. vpcConnectorEgressSettings: when a vpcConnector is set, control which + * egress traffic is sent through the vpcConnector. * * Value must not be null. */ diff --git a/src/function-configuration.ts b/src/function-configuration.ts index 31c708f3f..558895148 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -44,6 +44,15 @@ export const VALID_MEMORY_OPTIONS = [ '2GB', ] as const; +/** + * List of available options for VpcConnectorEgressSettings. + */ +export const VPC_EGRESS_SETTINGS_OPTIONS = [ + 'VPC_CONNECTOR_EGRESS_SETTINGS_UNSPECIFIED', + 'PRIVATE_RANGES_ONLY', + 'ALL_TRAFFIC', +] as const; + /** * Scheduler retry options. Applies only to scheduled functions. */ @@ -91,6 +100,16 @@ export interface RuntimeOptions { * Max number of actual instances allowed to be running in parallel */ maxInstances?: number; + + /** + * Connect cloud function to specified VPC connector + */ + vpcConnector?: string; + + /** + * Egress settings for VPC connector + */ + vpcConnectorEgressSettings?: typeof VPC_EGRESS_SETTINGS_OPTIONS[number]; } export interface DeploymentOptions extends RuntimeOptions { From f4faaad9c914f7744ae15d61f1ee88a1e8d3ffad Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Thu, 20 Aug 2020 17:00:18 -0700 Subject: [PATCH 262/705] Update CHANGELOG.md for 3.11.0 (#764) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..f46f8b3f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Adds support for `vpcConnector` and `vpcConnectorEgressSettings` fields in `functions.runWith()`. **Must be used in conjunction with firebase-tools v8.9.0 or higher.** Thanks @pcboy! (#752) From f9d71aab9278b375761d28b5584916f2da2fe66b Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 21 Aug 2020 16:10:08 +0000 Subject: [PATCH 263/705] 3.11.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 48a71e881..4eb5986d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.10.0", + "version": "3.11.0", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From 85af5e1a1a3792ce67c638fc98f522f6b3644923 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 21 Aug 2020 16:10:15 +0000 Subject: [PATCH 264/705] [firebase-release] Removed change log and reset repo after 3.11.0 release --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f46f8b3f4..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +0,0 @@ -- Adds support for `vpcConnector` and `vpcConnectorEgressSettings` fields in `functions.runWith()`. **Must be used in conjunction with firebase-tools v8.9.0 or higher.** Thanks @pcboy! (#752) From 7e2c0ec13e781d829bd37e66efc61c6ee74191a2 Mon Sep 17 00:00:00 2001 From: joehan Date: Mon, 14 Sep 2020 13:36:00 -0700 Subject: [PATCH 265/705] Update tests to change region based on env variable FIREBASE_FUNCTIONS_TEST_REGION (#780) * update tests to change region based on environment variable FIREBASE_FUNCTIONS_TEST_REGION * formats --- integration_test/functions/src/auth-tests.ts | 108 ++++++++++-------- .../functions/src/database-tests.ts | 6 +- .../functions/src/firestore-tests.ts | 2 + integration_test/functions/src/https-tests.ts | 4 +- integration_test/functions/src/index.ts | 4 +- .../functions/src/pubsub-tests.ts | 12 +- .../functions/src/remoteConfig-tests.ts | 10 +- .../functions/src/storage-tests.ts | 3 + .../functions/src/testLab-tests.ts | 2 + integration_test/run_tests.sh | 27 ++++- 10 files changed, 118 insertions(+), 60 deletions(-) diff --git a/integration_test/functions/src/auth-tests.ts b/integration_test/functions/src/auth-tests.ts index 2c1e1d4e6..c97c907bc 100644 --- a/integration_test/functions/src/auth-tests.ts +++ b/integration_test/functions/src/auth-tests.ts @@ -3,68 +3,82 @@ import * as functions from 'firebase-functions'; import { expectEq, TestSuite } from './testing'; import UserMetadata = admin.auth.UserRecord; -export const createUserTests: any = functions.auth.user().onCreate((u, c) => { - const testId: string = u.displayName; - console.log(`testId is ${testId}`); +const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; - return new TestSuite('auth user onCreate') - .it('should have a project as resource', (user, context) => - expectEq(context.resource.name, `projects/${process.env.GCLOUD_PROJECT}`) - ) +export const createUserTests: any = functions + .region(REGION) + .auth.user() + .onCreate((u, c) => { + const testId: string = u.displayName; + console.log(`testId is ${testId}`); - .it('should not have a path', (user, context) => - expectEq((context as any).path, undefined) - ) + return new TestSuite('auth user onCreate') + .it('should have a project as resource', (user, context) => + expectEq( + context.resource.name, + `projects/${process.env.GCLOUD_PROJECT}` + ) + ) - .it('should have the correct eventType', (user, context) => - expectEq(context.eventType, 'google.firebase.auth.user.create') - ) + .it('should not have a path', (user, context) => + expectEq((context as any).path, undefined) + ) - .it('should have an eventId', (user, context) => context.eventId) + .it('should have the correct eventType', (user, context) => + expectEq(context.eventType, 'google.firebase.auth.user.create') + ) - .it('should have a timestamp', (user, context) => context.timestamp) + .it('should have an eventId', (user, context) => context.eventId) - .it('should not have auth', (user, context) => - expectEq((context as any).auth, undefined) - ) + .it('should have a timestamp', (user, context) => context.timestamp) - .it('should not have action', (user, context) => - expectEq((context as any).action, undefined) - ) + .it('should not have auth', (user, context) => + expectEq((context as any).auth, undefined) + ) - .it('should have properly defined meta', (user, context) => user.metadata) + .it('should not have action', (user, context) => + expectEq((context as any).action, undefined) + ) - .run(testId, u, c); -}); + .it('should have properly defined meta', (user, context) => user.metadata) -export const deleteUserTests: any = functions.auth.user().onDelete((u, c) => { - const testId: string = u.displayName; - console.log(`testId is ${testId}`); + .run(testId, u, c); + }); - return new TestSuite('auth user onDelete') - .it('should have a project as resource', (user, context) => - expectEq(context.resource.name, `projects/${process.env.GCLOUD_PROJECT}`) - ) +export const deleteUserTests: any = functions + .region(REGION) + .auth.user() + .onDelete((u, c) => { + const testId: string = u.displayName; + console.log(`testId is ${testId}`); - .it('should not have a path', (user, context) => - expectEq((context as any).path, undefined) - ) + return new TestSuite('auth user onDelete') + .it('should have a project as resource', (user, context) => + expectEq( + context.resource.name, + `projects/${process.env.GCLOUD_PROJECT}` + ) + ) - .it('should have the correct eventType', (user, context) => - expectEq(context.eventType, 'google.firebase.auth.user.delete') - ) + .it('should not have a path', (user, context) => + expectEq((context as any).path, undefined) + ) - .it('should have an eventId', (user, context) => context.eventId) + .it('should have the correct eventType', (user, context) => + expectEq(context.eventType, 'google.firebase.auth.user.delete') + ) - .it('should have a timestamp', (user, context) => context.timestamp) + .it('should have an eventId', (user, context) => context.eventId) - .it('should not have auth', (user, context) => - expectEq((context as any).auth, undefined) - ) + .it('should have a timestamp', (user, context) => context.timestamp) - .it('should not have action', (user, context) => - expectEq((context as any).action, undefined) - ) + .it('should not have auth', (user, context) => + expectEq((context as any).auth, undefined) + ) - .run(testId, u, c); -}); + .it('should not have action', (user, context) => + expectEq((context as any).action, undefined) + ) + + .run(testId, u, c); + }); diff --git a/integration_test/functions/src/database-tests.ts b/integration_test/functions/src/database-tests.ts index de3030d3a..568d26bf4 100644 --- a/integration_test/functions/src/database-tests.ts +++ b/integration_test/functions/src/database-tests.ts @@ -4,9 +4,11 @@ import { expectEq, expectMatches, TestSuite } from './testing'; import DataSnapshot = admin.database.DataSnapshot; const testIdFieldName = 'testId'; +const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; -export const databaseTests: any = functions.database - .ref('dbTests/{testId}/start') +export const databaseTests: any = functions + .region(REGION) + .database.ref('dbTests/{testId}/start') .onWrite((ch, ctx) => { if (ch.after.val() === null) { console.log( diff --git a/integration_test/functions/src/firestore-tests.ts b/integration_test/functions/src/firestore-tests.ts index 49d1ae919..97daadea5 100644 --- a/integration_test/functions/src/firestore-tests.ts +++ b/integration_test/functions/src/firestore-tests.ts @@ -4,11 +4,13 @@ import { expectDeepEq, expectEq, TestSuite } from './testing'; import DocumentSnapshot = admin.firestore.DocumentSnapshot; const testIdFieldName = 'documentId'; +const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; export const firestoreTests: any = functions .runWith({ timeoutSeconds: 540, }) + .region(REGION) .firestore.document('tests/{documentId}') .onCreate((s, c) => { return new TestSuite('firestore document onWrite') diff --git a/integration_test/functions/src/https-tests.ts b/integration_test/functions/src/https-tests.ts index 55c7df983..6af0f9fac 100644 --- a/integration_test/functions/src/https-tests.ts +++ b/integration_test/functions/src/https-tests.ts @@ -2,7 +2,9 @@ import * as functions from 'firebase-functions'; import * as _ from 'lodash'; import { expectEq, TestSuite } from './testing'; -export const callableTests: any = functions.https.onCall((d) => { +const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; + +export const callableTests: any = functions.region(REGION).https.onCall((d) => { return new TestSuite('https onCall') .it('should have the correct data', (data) => expectEq(_.get(data, 'foo'), 'bar') diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 9988fe636..2ac16e5fa 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -20,13 +20,14 @@ import * as testLab from './testLab-utils'; import 'firebase-functions'; // temporary shim until process.env.FIREBASE_CONFIG available natively in GCF(BUG 63586213) const firebaseConfig = JSON.parse(process.env.FIREBASE_CONFIG); admin.initializeApp(); +const REGION = functions.config().functions.test_region; // TODO(klimt): Get rid of this once the JS client SDK supports callable triggers. function callHttpsTrigger(name: string, data: any, baseUrl) { return utils.makeRequest( { method: 'POST', - host: 'us-central1-' + firebaseConfig.projectId + '.' + baseUrl, + host: REGION + '-' + firebaseConfig.projectId + '.' + baseUrl, path: '/' + name, headers: { 'Content-Type': 'application/json', @@ -62,6 +63,7 @@ function callScheduleTrigger(functionName: string, region: string) { } export const integrationTests: any = functions + .region(REGION) .runWith({ timeoutSeconds: 540, }) diff --git a/integration_test/functions/src/pubsub-tests.ts b/integration_test/functions/src/pubsub-tests.ts index 3c1b1ecc7..e2a3f1bd0 100644 --- a/integration_test/functions/src/pubsub-tests.ts +++ b/integration_test/functions/src/pubsub-tests.ts @@ -3,10 +3,13 @@ import * as functions from 'firebase-functions'; import { evaluate, expectEq, success, TestSuite } from './testing'; import PubsubMessage = functions.pubsub.Message; +const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; + // TODO(inlined) use multiple queues to run inline. // Expected message data: {"hello": "world"} -export const pubsubTests: any = functions.pubsub - .topic('pubsubTests') +export const pubsubTests: any = functions + .region(REGION) + .pubsub.topic('pubsubTests') .onPublish((m, c) => { let testId: string; try { @@ -59,8 +62,9 @@ export const pubsubTests: any = functions.pubsub .run(testId, m, c); }); -export const schedule: any = functions.pubsub - .schedule('every 10 hours') // This is a dummy schedule, since we need to put a valid one in. +export const schedule: any = functions + .region(REGION) + .pubsub.schedule('every 10 hours') // This is a dummy schedule, since we need to put a valid one in. // For the test, the job is triggered by the jobs:run api .onRun((context) => { let testId; diff --git a/integration_test/functions/src/remoteConfig-tests.ts b/integration_test/functions/src/remoteConfig-tests.ts index 3474e1fc2..3f2cc8993 100644 --- a/integration_test/functions/src/remoteConfig-tests.ts +++ b/integration_test/functions/src/remoteConfig-tests.ts @@ -2,8 +2,11 @@ import * as functions from 'firebase-functions'; import { expectEq, TestSuite } from './testing'; import TemplateVersion = functions.remoteConfig.TemplateVersion; -export const remoteConfigTests: any = functions.remoteConfig.onUpdate( - (v, c) => { +const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; + +export const remoteConfigTests: any = functions + .region(REGION) + .remoteConfig.onUpdate((v, c) => { return new TestSuite('remoteConfig onUpdate') .it('should have a project as resource', (version, context) => expectEq( @@ -25,5 +28,4 @@ export const remoteConfigTests: any = functions.remoteConfig.onUpdate( ) .run(v.description, v, c); - } -); + }); diff --git a/integration_test/functions/src/storage-tests.ts b/integration_test/functions/src/storage-tests.ts index 43cdef5e2..df3032f78 100644 --- a/integration_test/functions/src/storage-tests.ts +++ b/integration_test/functions/src/storage-tests.ts @@ -2,10 +2,13 @@ import * as functions from 'firebase-functions'; import { expectEq, TestSuite } from './testing'; import ObjectMetadata = functions.storage.ObjectMetadata; +const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; + export const storageTests: any = functions .runWith({ timeoutSeconds: 540, }) + .region(REGION) .storage.bucket() .object() .onFinalize((s, c) => { diff --git a/integration_test/functions/src/testLab-tests.ts b/integration_test/functions/src/testLab-tests.ts index a6350c57d..cf4b2f062 100644 --- a/integration_test/functions/src/testLab-tests.ts +++ b/integration_test/functions/src/testLab-tests.ts @@ -2,11 +2,13 @@ import * as functions from 'firebase-functions'; import * as _ from 'lodash'; import { TestSuite, expectEq } from './testing'; import TestMatrix = functions.testLab.TestMatrix; +const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; export const testLabTests: any = functions .runWith({ timeoutSeconds: 540, }) + .region(REGION) .testLab.testMatrix() .onComplete((matrix, context) => { return new TestSuite('test matrix complete') diff --git a/integration_test/run_tests.sh b/integration_test/run_tests.sh index 903936b13..ba567536a 100755 --- a/integration_test/run_tests.sh +++ b/integration_test/run_tests.sh @@ -38,6 +38,26 @@ function build_sdk { mv firebase-functions-*.tgz "integration_test/functions/firebase-functions-${TIMESTAMP}.tgz" } +function set_region { + if [[ "${FIREBASE_FUNCTIONS_TEST_REGION}" == "" ]]; then + FIREBASE_FUNCTIONS_TEST_REGION="us-central1" + fi + if [[ "${TOKEN}" == "" ]]; then + firebase functions:config:set functions.test_region=$FIREBASE_FUNCTIONS_TEST_REGION --project=$PROJECT_ID + else + firebase functions:config:set functions.test_region=$FIREBASE_FUNCTIONS_TEST_REGION --project=$PROJECT_ID --token=$TOKEN + fi + announce "Set region to ${FIREBASE_FUNCTIONS_TEST_REGION}" +} + +function unset_region { + if [[ "${TOKEN}" == "" ]]; then + firebase functions:config:unset functions.test_region --project=$PROJECT_ID + else + firebase functions:config:unset functions.test_region --project=$PROJECT_ID --token=$TOKEN + fi +} + function pick_node8 { cd "${DIR}" cp package.node8.json functions/package.json @@ -98,7 +118,10 @@ function run_tests { if [[ "${FIREBASE_FUNCTIONS_URL}" == "https://preprod-cloudfunctions.sandbox.googleapis.com" ]]; then TEST_DOMAIN="txcloud.net" fi - TEST_URL="https://us-central1-${PROJECT_ID}.${TEST_DOMAIN}/integrationTests" + if [[ "${FIREBASE_FUNCTIONS_TEST_REGION}" == "" ]]; then + FIREBASE_FUNCTIONS_TEST_REGION="us-central1" + fi + TEST_URL="https://${FIREBASE_FUNCTIONS_TEST_REGION}-${PROJECT_ID}.${TEST_DOMAIN}/integrationTests" echo "${TEST_URL}" curl --fail "${TEST_URL}" @@ -107,6 +130,7 @@ function run_tests { function cleanup { announce "Performing cleanup..." delete_all_functions + unset_region rm "${DIR}/functions/firebase-functions-${TIMESTAMP}.tgz" rm "${DIR}/functions/package.json" rm -f "${DIR}/functions/firebase-debug.log" @@ -117,6 +141,7 @@ function cleanup { # Setup build_sdk delete_all_functions +set_region # Node 8 tests pick_node8 From f59ff8cde4de1cf2335fbc9525a123aa740ef1dd Mon Sep 17 00:00:00 2001 From: egilmorez Date: Mon, 2 Nov 2020 07:30:50 -0800 Subject: [PATCH 266/705] Adding required tags to page template. (#804) * Adding required tags to page template. * Adding formatting and fixing typo discovered in internal review. * More fixes/additions discovered in internal review. * Fixing format of toc.yaml file. --- docgen/content-sources/toc.yaml | 25 ++++++++++++++++++++----- docgen/theme/layouts/default.hbs | 2 ++ src/function-builder.ts | 27 +++++++++++++++------------ 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/docgen/content-sources/toc.yaml b/docgen/content-sources/toc.yaml index 37093f7c2..5758eb74e 100644 --- a/docgen/content-sources/toc.yaml +++ b/docgen/content-sources/toc.yaml @@ -23,6 +23,20 @@ toc: - title: 'config.Config' path: /docs/reference/functions/config_.config.config.html + - title: 'functions.function-configuration' + path: /docs/reference/functions/function_configuration_.html + section: + - title: 'config.DeploymentOptions' + path: /docs/reference/functions/function_configuration_.deploymentoptions.html + - title: 'config.FailurePolicy' + path: /docs/reference/functions/function_configuration_.failurepolicy.html + - title: 'config.RuntimeOptions' + path: /docs/reference/functions/function_configuration_.runtimeoptions.html + - title: 'config.Schedule' + path: /docs/reference/functions/function_configuration_.schedule.html + - title: 'config.ScheduleRetryConfig' + path: /docs/reference/functions/function_configuration_.scheduleretryconfig.html + - title: 'functions.analytics' path: /docs/reference/functions/providers_analytics_.html section: @@ -127,11 +141,6 @@ toc: - title: 'ObjectMetadata' path: /docs/reference/functions/providers_storage_.objectmetadata.html - - title: 'functions.handler' - path: /docs/reference/functions/handler_builder_.html - section: - - title: 'HandlerBuilder' - path: /docs/reference/functions/handler_builder_.handlerbuilder.html - title: 'functions.testLab' path: /docs/reference/functions/providers_testlab_.html section: @@ -143,3 +152,9 @@ toc: path: /docs/reference/functions/providers_testlab_.testmatrix.html - title: 'testLab.testMatrixBuilder' path: /docs/reference/functions/providers_testlab_.testmatrixbuilder.html + + - title: 'functions.handler' + path: /docs/reference/functions/handler_builder_.html + section: + - title: 'HandlerBuilder' + path: /docs/reference/functions/handler_builder_.handlerbuilder.html diff --git a/docgen/theme/layouts/default.hbs b/docgen/theme/layouts/default.hbs index 72111fb4e..17106e8d8 100644 --- a/docgen/theme/layouts/default.hbs +++ b/docgen/theme/layouts/default.hbs @@ -7,6 +7,8 @@ + + {{#ifCond model.name '==' project.name}}{{project.name}}{{else}}{{model.name}} | {{project.name}}{{/ifCond}} diff --git a/src/function-builder.ts b/src/function-builder.ts index 919f9f09b..9b5f5660b 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -133,15 +133,15 @@ export function region( /** * Configure runtime options for the function. * @param runtimeOptions Object with optional fields: - * 1. memory: amount of memory to allocate to the function, possible values + * 1. `memory`: amount of memory to allocate to the function, possible values * are: '128MB', '256MB', '512MB', '1GB', and '2GB'. - * 2. timeoutSeconds: timeout for the function in seconds, possible values are + * 2. `timeoutSeconds`: timeout for the function in seconds, possible values are * 0 to 540. - * 3. failurePolicy: failure policy of the function, with boolean `true` being + * 3. `failurePolicy`: failure policy of the function, with boolean `true` being * equivalent to providing an empty retry object. - * 4. vpcConnector: id of a VPC connector in same project and region - * 5. vpcConnectorEgressSettings: when a vpcConnector is set, control which - * egress traffic is sent through the vpcConnector. + * 4. `vpcConnector`: id of a VPC connector in the same project and region + * 5. `vpcConnectorEgressSettings`: when a `vpcConnector` is set, control which + * egress traffic is sent through the `vpcConnector`. * * Value must not be null. */ @@ -173,13 +173,16 @@ export class FunctionBuilder { /** * Configure runtime options for the function. - * @param runtimeOptions Object with three optional fields: - * 1. failurePolicy: failure policy of the function, with boolean `true` being + * @param runtimeOptions Object with optional fields: + * 1. `memory`: amount of memory to allocate to the function, possible values + * are: '128MB', '256MB', '512MB', '1GB', and '2GB'. + * 2. `timeoutSeconds`: timeout for the function in seconds, possible values are + * 0 to 540. + * 3. `failurePolicy`: failure policy of the function, with boolean `true` being * equivalent to providing an empty retry object. - * 2. memory: amount of memory to allocate to the function, with possible - * values being '128MB', '256MB', '512MB', '1GB', and '2GB'. - * 3. timeoutSeconds: timeout for the function in seconds, with possible - * values being 0 to 540. + * 4. `vpcConnector`: id of a VPC connector in the same project and region + * 5. `vpcConnectorEgressSettings`: when a `vpcConnector` is set, control which + * egress traffic is sent through the `vpcConnector`. * * Value must not be null. */ From 2988a2b3b1f17674446504c3b24c5c10335aa728 Mon Sep 17 00:00:00 2001 From: joehan Date: Wed, 18 Nov 2020 09:54:18 -0800 Subject: [PATCH 267/705] Adds 4GB as a memory option. (#814) --- src/function-configuration.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/function-configuration.ts b/src/function-configuration.ts index 558895148..6743d463e 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -42,6 +42,7 @@ export const VALID_MEMORY_OPTIONS = [ '512MB', '1GB', '2GB', + '4GB', ] as const; /** From ecfefd1bcfd26311e7a4de4665f3d02b49f7dc94 Mon Sep 17 00:00:00 2001 From: Marcel Goya <3046751+marcelgoya@users.noreply.github.com> Date: Wed, 25 Nov 2020 22:22:07 +0100 Subject: [PATCH 268/705] Add ingress settings support (#815) * Add IngressSettings support * Add IngressSettings support * Format Co-authored-by: Marcel Goya Co-authored-by: joehan --- src/function-configuration.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/function-configuration.ts b/src/function-configuration.ts index 6743d463e..10098a745 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -54,6 +54,16 @@ export const VPC_EGRESS_SETTINGS_OPTIONS = [ 'ALL_TRAFFIC', ] as const; +/** + * List of available options for IngressSettings. + */ +export const INGRESS_SETTINGS_OPTIONS = [ + 'INGRESS_SETTINGS_UNSPECIFIED', + 'ALLOW_ALL', + 'ALLOW_INTERNAL_ONLY', + 'ALLOW_INTERNAL_AND_GCLB', +] as const; + /** * Scheduler retry options. Applies only to scheduled functions. */ @@ -111,6 +121,11 @@ export interface RuntimeOptions { * Egress settings for VPC connector */ vpcConnectorEgressSettings?: typeof VPC_EGRESS_SETTINGS_OPTIONS[number]; + + /** + * Ingress settings + */ + ingressSettings?: typeof INGRESS_SETTINGS_OPTIONS[number]; } export interface DeploymentOptions extends RuntimeOptions { From c11e5b3b2a6cce9bd87674e68cc3a7239a99489a Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Mon, 30 Nov 2020 08:05:36 -0800 Subject: [PATCH 269/705] introduce `package-lock.json` (#781) * initial package-lock * remove npmrc * update package-lock * update typedoc * remove istanbul; audit fixes Co-authored-by: joehan --- .npmrc | 1 - package-lock.json | 4688 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- 3 files changed, 4689 insertions(+), 3 deletions(-) delete mode 100644 .npmrc create mode 100644 package-lock.json diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 43c97e719..000000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..17b67b6d0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4688 @@ +{ + "name": "firebase-functions", + "version": "3.11.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@firebase/app-types": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz", + "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==", + "dev": true + }, + "@firebase/auth-interop-types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz", + "integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==", + "dev": true + }, + "@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "requires": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "@firebase/database": { + "version": "0.6.12", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.12.tgz", + "integrity": "sha512-OLUxp8TkXiML4X5LWM5IACsSDvo3fcf4mTbTe5RF+N6TRFv0Svzlet5OgGIa3ET1dQvNiisrMX7zzRa0OTLs7Q==", + "dev": true, + "requires": { + "@firebase/auth-interop-types": "0.1.5", + "@firebase/component": "0.1.19", + "@firebase/database-types": "0.5.2", + "@firebase/logger": "0.2.6", + "@firebase/util": "0.3.2", + "faye-websocket": "0.11.3", + "tslib": "^1.11.1" + } + }, + "@firebase/database-types": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.5.2.tgz", + "integrity": "sha512-ap2WQOS3LKmGuVFKUghFft7RxXTyZTDr0Xd8y2aqmWsbJVjgozi0huL/EUMgTjGFrATAjcf2A7aNs8AKKZ2a8g==", + "dev": true, + "requires": { + "@firebase/app-types": "0.6.1" + } + }, + "@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", + "dev": true + }, + "@firebase/util": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", + "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", + "dev": true, + "requires": { + "tslib": "^1.11.1" + } + }, + "@google-cloud/common": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.4.0.tgz", + "integrity": "sha512-zWFjBS35eI9leAHhjfeOYlK5Plcuj/77EzstnrJIZbKgF/nkqjcQuGiMCpzCwOfPyUbz8ZaEOYgbHa759AKbjg==", + "dev": true, + "optional": true, + "requires": { + "@google-cloud/projectify": "^1.0.0", + "@google-cloud/promisify": "^1.0.0", + "arrify": "^2.0.0", + "duplexify": "^3.6.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "google-auth-library": "^5.5.0", + "retry-request": "^4.0.0", + "teeny-request": "^6.0.0" + } + }, + "@google-cloud/firestore": { + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-3.8.6.tgz", + "integrity": "sha512-ox80NbrM1MLJgvAAUd1quFLx/ie/nSjrk1PtscSicpoYDlKb9e6j7pHrVpbopBMyliyfNl3tLJWaDh+x+uCXqw==", + "dev": true, + "optional": true, + "requires": { + "deep-equal": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^1.15.3", + "readable-stream": "^3.4.0", + "through2": "^3.0.0" + } + }, + "@google-cloud/paginator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.3.tgz", + "integrity": "sha512-kp/pkb2p/p0d8/SKUu4mOq8+HGwF8NPzHWkj+VKrIPQPyMRw8deZtrO/OcSiy9C/7bpfU5Txah5ltUNfPkgEXg==", + "dev": true, + "optional": true, + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, + "@google-cloud/projectify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.4.tgz", + "integrity": "sha512-ZdzQUN02eRsmTKfBj9FDL0KNDIFNjBn/d6tHQmA/+FImH5DO6ZV8E7FzxMgAUiVAUq41RFAkb25p1oHOZ8psfg==", + "dev": true, + "optional": true + }, + "@google-cloud/promisify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.4.tgz", + "integrity": "sha512-VccZDcOql77obTnFh0TbNED/6ZbbmHDf8UMNnzO1d5g9V0Htfm4k5cllY8P1tJsRKC3zWYGRLaViiupcgVjBoQ==", + "dev": true, + "optional": true + }, + "@google-cloud/storage": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-4.7.0.tgz", + "integrity": "sha512-f0guAlbeg7Z0m3gKjCfBCu7FG9qS3M3oL5OQQxlvGoPtK7/qg3+W+KQV73O2/sbuS54n0Kh2mvT5K2FWzF5vVQ==", + "dev": true, + "optional": true, + "requires": { + "@google-cloud/common": "^2.1.1", + "@google-cloud/paginator": "^2.0.0", + "@google-cloud/promisify": "^1.0.0", + "arrify": "^2.0.0", + "compressible": "^2.0.12", + "concat-stream": "^2.0.0", + "date-and-time": "^0.13.0", + "duplexify": "^3.5.0", + "extend": "^3.0.2", + "gaxios": "^3.0.0", + "gcs-resumable-upload": "^2.2.4", + "hash-stream-validation": "^0.2.2", + "mime": "^2.2.0", + "mime-types": "^2.0.8", + "onetime": "^5.1.0", + "p-limit": "^2.2.0", + "pumpify": "^2.0.0", + "readable-stream": "^3.4.0", + "snakeize": "^0.1.0", + "stream-events": "^1.0.1", + "through2": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "gaxios": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz", + "integrity": "sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q==", + "dev": true, + "optional": true, + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + } + }, + "mime": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", + "dev": true, + "optional": true + } + } + }, + "@grpc/grpc-js": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.0.5.tgz", + "integrity": "sha512-Hm+xOiqAhcpT9RYM8lc15dbQD7aQurM7ZU8ulmulepiPlN7iwBXXwP3vSBUimoFoApRqz7pSIisXU8pZaCB4og==", + "dev": true, + "optional": true, + "requires": { + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "optional": true + } + } + }, + "@grpc/proto-loader": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz", + "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==", + "dev": true, + "optional": true, + "requires": { + "lodash.camelcase": "^4.3.0", + "protobufjs": "^6.8.6" + } + }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", + "dev": true, + "optional": true + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha1-TIVzDlm5ofHzSQR9vyQpYDS7JzU=", + "dev": true, + "optional": true + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha1-fvN/DQEPsCitGtWXIuUG2SYoFcs=", + "dev": true, + "optional": true + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", + "dev": true, + "optional": true + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "dev": true, + "optional": true, + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", + "dev": true, + "optional": true + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", + "dev": true, + "optional": true + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", + "dev": true, + "optional": true + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", + "dev": true, + "optional": true + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", + "dev": true, + "optional": true + }, + "@sinonjs/commons": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", + "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", + "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", + "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.3.0", + "array-from": "^2.1.1", + "lodash": "^4.17.15" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "optional": true + }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/chai": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.12.tgz", + "integrity": "sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ==", + "dev": true + }, + "@types/chai-as-promised": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.3.tgz", + "integrity": "sha512-FQnh1ohPXJELpKhzjuDkPLR2BZCAqed+a6xV4MI/T3XzHfd2FlarfUGUdZYgqYe8oxkYn0fchHEeHfHqdZ96sg==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "requires": { + "@types/node": "*" + } + }, + "@types/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-GmK8AKu8i+s+EChK/uZ5IbrXPcPaQKWaNSGevDT/7o3gFObwSUQwqb1jMqxuo+YPvj0ckGzINI+EO7EHcmJjKg==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.12.tgz", + "integrity": "sha512-EaEdY+Dty1jEU7U6J4CUWwxL+hyEGMkO5jan5gplfegUgCUsIUWqXxqw47uGjimeT4Qgkz/XUfwoau08+fgvKA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/fs-extra": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.1.tgz", + "integrity": "sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "@types/jsonwebtoken": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.3.2.tgz", + "integrity": "sha512-Mkjljd9DTpkPlrmGfTJvcP4aBU7yO2QmW7wNVhV4/6AEUxYoacqU7FJU/N0yFEHTsIrE4da3rUrjrR5ejicFmA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/lodash": { + "version": "4.14.161", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.161.tgz", + "integrity": "sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA==", + "dev": true + }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", + "dev": true, + "optional": true + }, + "@types/mime": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" + }, + "@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true + }, + "@types/mock-require": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mock-require/-/mock-require-2.0.0.tgz", + "integrity": "sha512-nOgjoE5bBiDeiA+z41i95makyHUSMWQMOPocP+J67Pqx/68HAXaeWN1NFtrAYYV6LrISIZZ8vKHm/a50k0f6Sg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/nock": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@types/nock/-/nock-10.0.3.tgz", + "integrity": "sha512-OthuN+2FuzfZO3yONJ/QVjKmLEuRagS9TV9lEId+WHL9KhftYG+/2z+pxlr0UgVVXSpVD8woie/3fzQn8ft/Ow==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "8.10.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.63.tgz", + "integrity": "sha512-g+nSkeHFDd2WOQChfmy9SAXLywT47WZBrGS/NC5ym5PJ8c8RC6l4pbGaUW/X0+eZJnXw6/AVNEouXWhV4iz72Q==" + }, + "@types/qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz", + "integrity": "sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "@types/sinon": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.5.2.tgz", + "integrity": "sha512-T+m89VdXj/eidZyejvmoP9jivXgBDdkOSBVQjU9kF349NEx10QdPNGxHeZUaj1IlJ32/ewdyXJjnJxyxJroYwg==", + "dev": true + }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "dev": true + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "optional": true, + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", + "dev": true + }, + "acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, + "agent-base": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", + "dev": true, + "optional": true, + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "optional": true + } + } + }, + "ajv": { + "version": "6.12.5", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz", + "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-filter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", + "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=", + "dev": true, + "optional": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true, + "optional": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha1-5gtrDo8wG9l+U3UhW9pAbIURjAs=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, + "available-typed-arrays": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz", + "integrity": "sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==", + "dev": true, + "optional": true, + "requires": { + "array-filter": "^1.0.0" + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", + "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true, + "optional": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "dev": true, + "optional": true + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "dev": true + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha1-uqVZ7hTO1zRSIputcyZGfGH6vWA=", + "dev": true + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "requires": { + "check-error": "^1.0.2" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "child-process-promise": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz", + "integrity": "sha1-RzChHvYQ+tRQuPIjx50x172tgHQ=", + "dev": true, + "requires": { + "cross-spawn": "^4.0.2", + "node-version": "^1.0.0", + "promise-polyfill": "^6.0.1" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha1-vXerfebelCBc6sxy8XFtKfIKd78=", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "optional": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + }, + "dependencies": { + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "dev": true, + "optional": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "dev": true, + "optional": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "optional": true, + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + } + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "optional": true + }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } + }, + "date-and-time": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.13.1.tgz", + "integrity": "sha512-/Uge9DJAT+s+oAcDxtBhyR8+sKjUnZbYmyhbmWjTHNtX7B7oWD8YyYdeXcBRbwSj6hVvj+IQegJam7m7czhbFw==", + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decimal.js": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.0.tgz", + "integrity": "sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw==", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-equal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.3.tgz", + "integrity": "sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA==", + "dev": true, + "optional": true, + "requires": { + "es-abstract": "^1.17.5", + "es-get-iterator": "^1.1.0", + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.2", + "is-regex": "^1.0.5", + "isarray": "^2.0.5", + "object-is": "^1.1.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.2", + "which-boxed-primitive": "^1.0.1", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.2" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha1-z4jabL7ib+bbcJT2HYcMvYTO6fE=", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "dicer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", + "dev": true, + "requires": { + "streamsearch": "0.1.2" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha1-gAwN0eCov7yVg1wgKtIg/jF+WhI=", + "dev": true + }, + "domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "dev": true, + "requires": { + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "dev": true + } + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "optional": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "optional": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true, + "optional": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "optional": true, + "requires": { + "once": "^1.4.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true, + "optional": true + }, + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-get-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", + "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", + "dev": true, + "optional": true, + "requires": { + "es-abstract": "^1.17.4", + "has-symbols": "^1.0.1", + "is-arguments": "^1.0.4", + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint-plugin-prettier": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.7.0.tgz", + "integrity": "sha512-CStQYJgALoQBw3FsBzH0VOVDRnJ/ZimUlpLm226U8qgqYJfPOY/CPK6wyRInMxh73HSKg5wyRwdS4BVYYHwokA==", + "dev": true, + "requires": { + "fast-diff": "^1.1.1", + "jest-docblock": "^21.0.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "optional": true + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=", + "dev": true + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", + "dev": true, + "optional": true + }, + "faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "firebase-admin": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-8.13.0.tgz", + "integrity": "sha512-krXj5ncWMJBhCpXSn9UFY6zmDWjFjqgx+1e9ATXKFYndEjmKtNBuJzqdrAdDh7aTUR7X6+0TPx4Hbc08kd0lwQ==", + "dev": true, + "requires": { + "@firebase/database": "^0.6.0", + "@google-cloud/firestore": "^3.0.0", + "@google-cloud/storage": "^4.1.2", + "@types/node": "^8.10.59", + "dicer": "^0.3.0", + "jsonwebtoken": "^8.5.1", + "node-forge": "^0.7.6" + } + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "dev": true + } + } + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true, + "optional": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true, + "optional": true + }, + "gaxios": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.4.tgz", + "integrity": "sha512-US8UMj8C5pRnao3Zykc4AAVr+cffoNKRTg9Rsf2GiuZCW69vgJj38VK2PzlPuQU73FZ/nTk9/Av6/JGcE1N9vA==", + "dev": true, + "optional": true, + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + } + }, + "gcp-metadata": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.5.0.tgz", + "integrity": "sha512-ZQf+DLZ5aKcRpLzYUyBS3yo3N0JSa82lNDO8rj3nMSlovLcz2riKFBsYgDzeXcv75oo5eqB2lx+B14UvPoCRnA==", + "dev": true, + "optional": true, + "requires": { + "gaxios": "^2.1.0", + "json-bigint": "^0.3.0" + } + }, + "gcs-resumable-upload": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-2.3.3.tgz", + "integrity": "sha512-sf896I5CC/1AxeaGfSFg3vKMjUq/r+A3bscmVzZm10CElyRanN0XwPu/MxeIO4LSP+9uF6yKzXvNsaTsMXUG6Q==", + "dev": true, + "optional": true, + "requires": { + "abort-controller": "^3.0.0", + "configstore": "^5.0.0", + "gaxios": "^2.0.0", + "google-auth-library": "^5.0.0", + "pumpify": "^2.0.0", + "stream-events": "^1.0.4" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha1-OWCDLT8VdBCDQtr9OmezMsCWnfE=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "google-auth-library": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.10.1.tgz", + "integrity": "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg==", + "dev": true, + "optional": true, + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^2.1.0", + "gcp-metadata": "^3.4.0", + "gtoken": "^4.1.0", + "jws": "^4.0.0", + "lru-cache": "^5.0.0" + }, + "dependencies": { + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dev": true, + "optional": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dev": true, + "optional": true, + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "optional": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "optional": true + } + } + }, + "google-gax": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.15.3.tgz", + "integrity": "sha512-3JKJCRumNm3x2EksUTw4P1Rad43FTpqrtW9jzpf3xSMYXx+ogaqTM1vGo7VixHB4xkAyATXVIa3OcNSh8H9zsQ==", + "dev": true, + "optional": true, + "requires": { + "@grpc/grpc-js": "~1.0.3", + "@grpc/proto-loader": "^0.5.1", + "@types/fs-extra": "^8.0.1", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^3.6.0", + "google-auth-library": "^5.0.0", + "is-stream-ended": "^0.1.4", + "lodash.at": "^4.6.0", + "lodash.has": "^4.5.2", + "node-fetch": "^2.6.0", + "protobufjs": "^6.8.9", + "retry-request": "^4.0.0", + "semver": "^6.0.0", + "walkdir": "^0.4.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "optional": true + } + } + }, + "google-p12-pem": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.4.tgz", + "integrity": "sha512-S4blHBQWZRnEW44OcR7TL9WR+QCqByRvhNDZ/uuQfpxywfupikf/miba8js1jZi6ZOGv5slgSuoshCWh6EMDzg==", + "dev": true, + "optional": true, + "requires": { + "node-forge": "^0.9.0" + }, + "dependencies": { + "node-forge": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.2.tgz", + "integrity": "sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw==", + "dev": true, + "optional": true + } + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha1-8nNdwig2dPpnR4sQGBBZNVw2nl4=", + "dev": true + }, + "gtoken": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.4.tgz", + "integrity": "sha512-VxirzD0SWoFUo5p8RDP8Jt2AGyOmyYcT/pOUgDKJCK+iSw0TMqwrVfY37RXTNmoKwrzmDHSk0GMT9FsgVmnVSA==", + "dev": true, + "optional": true, + "requires": { + "gaxios": "^2.1.0", + "google-p12-pem": "^2.0.0", + "jws": "^4.0.0", + "mime": "^2.2.0" + }, + "dependencies": { + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dev": true, + "optional": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dev": true, + "optional": true, + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "mime": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", + "dev": true, + "optional": true + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dev": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "hash-stream-validation": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz", + "integrity": "sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==", + "dev": true, + "optional": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "highlight.js": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.2.0.tgz", + "integrity": "sha512-OryzPiqqNCfO/wtFo619W+nPYALM6u7iCQkum4bqRmmlcTikOkmlL06i009QelynBPAlNByTQU6cBB2cOBQtCw==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "http-parser-js": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz", + "integrity": "sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ==", + "dev": true + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "optional": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "optional": true + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "optional": true, + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "optional": true + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "optional": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true + }, + "is-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.0.tgz", + "integrity": "sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==", + "dev": true, + "optional": true + }, + "is-boolean-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.1.tgz", + "integrity": "sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==", + "dev": true, + "optional": true + }, + "is-callable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.1.tgz", + "integrity": "sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg==", + "dev": true + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz", + "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==", + "dev": true, + "optional": true + }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", + "dev": true + }, + "is-number-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", + "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", + "dev": true, + "optional": true + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "optional": true + }, + "is-potential-custom-element-name": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz", + "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-set": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", + "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==", + "dev": true, + "optional": true + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true, + "optional": true + }, + "is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha1-9QIk6V4GvODjVtRApIJ801smfto=", + "dev": true, + "optional": true + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true, + "optional": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-typed-array": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.3.tgz", + "integrity": "sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==", + "dev": true, + "optional": true, + "requires": { + "available-typed-arrays": "^1.0.0", + "es-abstract": "^1.17.4", + "foreach": "^2.0.5", + "has-symbols": "^1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, + "optional": true + }, + "is-weakset": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.1.tgz", + "integrity": "sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==", + "dev": true, + "optional": true + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "optional": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "jest-docblock": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz", + "integrity": "sha512-5IZ7sY9dBAYSV+YjQ0Ovb540Ku7AO9Z5o2Cg789xj167iQuZ2cG+z0f3Uct6WeYLbU6aQiM2pCs7sZ+4dotydw==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha1-E7BM2z5sXRnfkatph6hpVhmwqnE=", + "dev": true + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsdom": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz", + "integrity": "sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "acorn": "^7.1.1", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.2.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.0", + "domexception": "^2.0.1", + "escodegen": "^1.14.1", + "html-encoding-sniffer": "^2.0.1", + "is-potential-custom-element-name": "^1.0.0", + "nwsapi": "^2.2.0", + "parse5": "5.1.1", + "request": "^2.88.2", + "request-promise-native": "^1.0.8", + "saxes": "^5.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0", + "ws": "^7.2.3", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } + } + }, + "json-bigint": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.1.tgz", + "integrity": "sha512-DGWnSzmusIreWlEupsUelHrhwmPPE+FiQvg+drKfk2p+bdEYa5mp4PJ8JsCWqae0M2jQNb0HPvnwvf1qOTThzQ==", + "dev": true, + "optional": true, + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "jsonfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" + } + }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dev": true, + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "just-extend": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz", + "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==", + "dev": true + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dev": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dev": true, + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "lodash.at": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.at/-/lodash.at-4.6.0.tgz", + "integrity": "sha1-k83OZk8KGZTqM9181A4jr9EbD/g=", + "dev": true, + "optional": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true, + "optional": true + }, + "lodash.has": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", + "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=", + "dev": true, + "optional": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=", + "dev": true + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=", + "dev": true + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=", + "dev": true + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", + "dev": true + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "lolex": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", + "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", + "dev": true + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha1-mntxz7fTYaGU6lVSQckvdGjVvyg=", + "dev": true, + "optional": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "optional": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "optional": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "marked": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-1.1.1.tgz", + "integrity": "sha512-mJzT8D2yPxoPh7h0UXkB+dBj4FykPJ2OIfxAWeIHrvoHDkFxukV/29QxoFQoPM6RLEwhIFdJpmKBlqVM3s2ZIw==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + } + } + }, + "mocha": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.3.tgz", + "integrity": "sha512-0R/3FvjIGH3eEuG17ccFPk117XL2rWxatr81a57D+r/x2uTYZRbdZ4oVidEUMh2W2TJDa7MdAb12Lm2/qrKajg==", + "dev": true, + "requires": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "2.2.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.4", + "ms": "2.1.1", + "node-environment-flags": "1.0.5", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", + "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + } + } + }, + "mock-require": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz", + "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==", + "dev": true, + "requires": { + "get-caller-file": "^1.0.2", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "nise": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", + "integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^5.0.1", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "nock": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", + "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", + "dev": true, + "requires": { + "chai": "^4.1.2", + "debug": "^4.1.0", + "deep-equal": "^1.0.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.5", + "mkdirp": "^0.5.0", + "propagate": "^1.0.0", + "qs": "^6.5.1", + "semver": "^5.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "node-environment-flags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", + "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "dev": true, + "optional": true + }, + "node-forge": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", + "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==", + "dev": true + }, + "node-version": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/node-version/-/node-version-1.2.0.tgz", + "integrity": "sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ==", + "dev": true + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object-is": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz", + "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.0.tgz", + "integrity": "sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "optional": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "optional": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "promise-polyfill": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", + "integrity": "sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=", + "dev": true + }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", + "dev": true + }, + "protobufjs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz", + "integrity": "sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ==", + "dev": true, + "optional": true, + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": "^13.7.0", + "long": "^4.0.0" + }, + "dependencies": { + "@types/node": { + "version": "13.13.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.20.tgz", + "integrity": "sha512-1kx55tU3AvGX2Cjk2W4GMBxbgIz892V+X10S2gUreIAq8qCWgaQH+tZBOWc0bi2BKFhQt+CX0BTx28V9QPNa+A==", + "dev": true, + "optional": true + } + } + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "optional": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "dev": true, + "optional": true, + "requires": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + }, + "dependencies": { + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "dev": true, + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "regexp.prototype.flags": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", + "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dev": true, + "requires": { + "lodash": "^4.17.19" + } + }, + "request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "dev": true, + "requires": { + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "retry-request": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.3.tgz", + "integrity": "sha512-QnRZUpuPNgX0+D1xVxul6DbJ9slvo4Rm6iV/dn63e048MvGbUZiKySVt6Tenp04JqmchxjiLltGerOJys7kJYQ==", + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "optional": true + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=" + }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha1-fnQlb7qknHWqfHogXMInmcrIAAQ=", + "dev": true + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "shelljs": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", + "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "side-channel": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz", + "integrity": "sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==", + "dev": true, + "optional": true, + "requires": { + "es-abstract": "^1.18.0-next.0", + "object-inspect": "^1.8.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.0.tgz", + "integrity": "sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==", + "dev": true, + "optional": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true, + "optional": true + }, + "sinon": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", + "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.3", + "diff": "^3.5.0", + "lolex": "^4.2.0", + "nise": "^1.5.2", + "supports-color": "^5.5.0" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "snakeize": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz", + "integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=", + "dev": true, + "optional": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "dev": true, + "optional": true, + "requires": { + "stubs": "^3.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true, + "optional": true + }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", + "dev": true, + "optional": true + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "teeny-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.3.tgz", + "integrity": "sha512-TZG/dfd2r6yeji19es1cUIwAlVD8y+/svB1kAC2Y0bjEyysrfbO8EZvJBRwIE6WkwmUoB7uvWLwTIhJbMXZ1Dw==", + "dev": true, + "optional": true, + "requires": { + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.2.0", + "stream-events": "^1.0.5", + "uuid": "^7.0.0" + } + }, + "thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", + "dev": true, + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "optional": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "optional": true + } + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "dev": true, + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", + "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + } + } + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true + }, + "tslint": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", + "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, + "tslint-config-prettier": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz", + "integrity": "sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg==", + "dev": true + }, + "tslint-no-unused-expression-chai": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/tslint-no-unused-expression-chai/-/tslint-no-unused-expression-chai-0.1.4.tgz", + "integrity": "sha512-frEWKNTcq7VsaWKgUxMDOB2N/cmQadVkUtUGIut+2K4nv/uFXPfgJyPjuNC/cHyfUVqIkHMAvHOCL+d/McU3nQ==", + "dev": true, + "requires": { + "tsutils": "^3.0.0" + }, + "dependencies": { + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "tslint-plugin-prettier": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslint-plugin-prettier/-/tslint-plugin-prettier-2.3.0.tgz", + "integrity": "sha512-F9e4K03yc9xuvv+A0v1EmjcnDwpz8SpCD8HzqSDe0eyg34cBinwn9JjmnnRrNAs4HdleRQj7qijp+P/JTxt4vA==", + "dev": true, + "requires": { + "eslint-plugin-prettier": "^2.2.0", + "lines-and-columns": "^1.1.6", + "tslib": "^1.7.1" + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true, + "optional": true + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "optional": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typedoc": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.19.1.tgz", + "integrity": "sha512-EqZpRJQUnkwHA1yBhaDExEXUZIiWKddkrDXhRcfUzpnu6pizxNmVTw5IZ3mu682Noa4zQCniE0YNjaAwHQodrA==", + "dev": true, + "requires": { + "fs-extra": "^9.0.1", + "handlebars": "^4.7.6", + "highlight.js": "^10.0.0", + "lodash": "^4.17.20", + "lunr": "^2.3.9", + "marked": "^1.1.1", + "minimatch": "^3.0.0", + "progress": "^2.0.3", + "semver": "^7.3.2", + "shelljs": "^0.8.4", + "typedoc-default-themes": "^0.11.1" + }, + "dependencies": { + "handlebars": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, + "typedoc-default-themes": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.11.3.tgz", + "integrity": "sha512-SwyN188QGNA2iFS5mdWYTGzohKqJ1PWAXVmGolKnVc2NnpX234FEPF2nUvEg+O9jjwAu7ZSVZ5UrZri0raJOjQ==", + "dev": true + }, + "typescript": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", + "dev": true + }, + "uglify-js": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", + "integrity": "sha1-rwLxgMEgfXZDLkc+0koo9KeCuuM=", + "dev": true, + "optional": true, + "requires": { + "commander": "~2.17.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", + "dev": true, + "optional": true + } + } + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "optional": true, + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true, + "optional": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "dev": true, + "optional": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "dev": true, + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dev": true, + "requires": { + "xml-name-validator": "^3.0.0" + } + }, + "walkdir": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", + "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", + "dev": true, + "optional": true + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-PcVnO6NiewhkmzV0qn7A+UZ9Xx4maNTI+O+TShmfE4pqjoCMwUMjkvoNhNHPTvgR7QH9Xt3R13iHuWy2sToFxQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^2.0.2", + "webidl-conversions": "^6.1.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz", + "integrity": "sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==", + "dev": true, + "optional": true, + "requires": { + "is-bigint": "^1.0.0", + "is-boolean-object": "^1.0.0", + "is-number-object": "^1.0.3", + "is-string": "^1.0.4", + "is-symbol": "^1.0.2" + } + }, + "which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "optional": true, + "requires": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "which-typed-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.2.tgz", + "integrity": "sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==", + "dev": true, + "optional": true, + "requires": { + "available-typed-arrays": "^1.0.2", + "es-abstract": "^1.17.5", + "foreach": "^2.0.5", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.1", + "is-typed-array": "^1.1.3" + } + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "optional": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "ws": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", + "dev": true + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true, + "optional": true + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yargs-unparser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "dev": true, + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + } + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + } + } +} diff --git a/package.json b/package.json index 4eb5986d8..72a217fb7 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "chai-as-promised": "^7.1.1", "child-process-promise": "^2.2.1", "firebase-admin": "^8.2.0", - "istanbul": "^0.4.5", "js-yaml": "^3.13.1", "jsdom": "^16.2.1", "jsonwebtoken": "^8.5.1", @@ -73,7 +72,7 @@ "tslint-config-prettier": "^1.18.0", "tslint-no-unused-expression-chai": "^0.1.4", "tslint-plugin-prettier": "^2.0.1", - "typedoc": "0.14.2", + "typedoc": "^0.19.1", "typescript": "^3.8.3", "yargs": "^15.3.1" }, From 93047d5046e589f6d0ae4d321e546c0e85a769a4 Mon Sep 17 00:00:00 2001 From: joehan Date: Mon, 30 Nov 2020 13:49:32 -0800 Subject: [PATCH 270/705] Switches to Github Actions from travis, and adds CHANGELOG (#818) * Switches to Github Actions over travis, and adds CHNAGELOG entries for recent PRs * formats --- .github/workflows/test.yaml | 35 +++++++++++++++++++++++++++++++++++ .travis.yml | 14 -------------- CHANGELOG.md | 2 ++ 3 files changed, 37 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/test.yaml delete mode 100644 .travis.yml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 000000000..9a31678c2 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,35 @@ +name: CI Tests + +on: + - pull_request + - push + +env: + CI: true + +jobs: + unit: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: + - 8.x + - 10.x + - 12.x + - 14.x + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + - name: Cache npm + uses: actions/cache@v1 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} + + - run: npm install + - run: npm run lint + - run: npm run format + - run: npm run test diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 18dff4114..000000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -cache: npm -jobs: - include: - - name: lint - script: npm run lint - stage: verify - - name: format - script: npm run format - stage: verify -language: node_js -node_js: - - '8' - - '10' -sudo: false diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..e0c94613b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +- Adds `4GB` as a `memory` option for `runWith()`. +- Adds support for choosing `ingressSettings` via `runWith()`. From 3932876901d977d7957b57a69b6f2b0b9b2b94a5 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Mon, 30 Nov 2020 21:54:33 +0000 Subject: [PATCH 271/705] 3.12.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 17b67b6d0..2ff1ff5cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.11.0", + "version": "3.12.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 72a217fb7..a27384f60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.11.0", + "version": "3.12.0", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From 9055e0fc9f3bd8fb72c4cc8702f8ea1a1051ee70 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Mon, 30 Nov 2020 21:54:38 +0000 Subject: [PATCH 272/705] [firebase-release] Removed change log and reset repo after 3.12.0 release --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0c94613b..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +0,0 @@ -- Adds `4GB` as a `memory` option for `runWith()`. -- Adds support for choosing `ingressSettings` via `runWith()`. From 07139c8d9cdaff5dccbe790fd3ebc66a717ae8ac Mon Sep 17 00:00:00 2001 From: Egor Miasnikov Date: Tue, 8 Dec 2020 01:05:26 +0300 Subject: [PATCH 273/705] Add support for service account in `functions.runWith` (#770) * Add support for service account in `functions.runWith` * Add email validation and email generation by project id * Changing to serviceAccount, and adding default as an option * adds a test case for default * refactoring to checxk for @ at the end of service account, and throw erros earlier when service account is set to something invalid * gets rid of repeated @ for generated service account emails Co-authored-by: joehan --- spec/function-builder.spec.ts | 48 +++++++++++++++++++++++++++++++++++ src/cloud-functions.ts | 20 +++++++++++++++ src/function-builder.ts | 19 +++++++++++--- src/function-configuration.ts | 5 ++++ 4 files changed, 89 insertions(+), 3 deletions(-) diff --git a/spec/function-builder.spec.ts b/spec/function-builder.spec.ts index eec531fdf..c31a82d4a 100644 --- a/spec/function-builder.spec.ts +++ b/spec/function-builder.spec.ts @@ -238,4 +238,52 @@ describe('FunctionBuilder', () => { )}` ); }); + + it('should allow a serviceAccount to be set as-is', () => { + const serviceAccount = 'test-service-account@test.iam.gserviceaccount.com'; + const fn = functions + .runWith({ + serviceAccount, + }) + .auth.user() + .onCreate((user) => user); + + expect(fn.__trigger.serviceAccountEmail).to.equal(serviceAccount); + }); + + it('should allow a serviceAccount to be set with generated service account email', () => { + const serviceAccount = 'test-service-account@'; + const projectId = process.env.GCLOUD_PROJECT; + const fn = functions + .runWith({ + serviceAccount, + }) + .auth.user() + .onCreate((user) => user); + + expect(fn.__trigger.serviceAccountEmail).to.equal( + `test-service-account@${projectId}.iam.gserviceaccount.com` + ); + }); + + it('should not set a serviceAccountEmail if service account is set to `default`', () => { + const serviceAccount = 'default'; + const fn = functions + .runWith({ + serviceAccount, + }) + .auth.user() + .onCreate((user) => user); + + expect(fn.__trigger.serviceAccountEmail).to.be.undefined; + }); + + it('should throw an error if serviceAccount is set to an invalid value', () => { + const serviceAccount = 'test-service-account'; + expect(() => { + functions.runWith({ + serviceAccount, + }); + }).to.throw(); + }); }); diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 109a1ad05..50d674022 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -272,6 +272,7 @@ export interface TriggerAnnotated { timeout?: string; vpcConnector?: string; vpcConnectorEgressSettings?: string; + serviceAccountEmail?: string; }; } @@ -525,5 +526,24 @@ export function optionsToTrigger(options: DeploymentOptions) { trigger.vpcConnectorEgressSettings = options.vpcConnectorEgressSettings; } + if (options.serviceAccount) { + if (options.serviceAccount === 'default') { + // Do nothing, since this is equivalent to not setting serviceAccount. + } else if (options.serviceAccount.endsWith('@')) { + if (!process.env.GCLOUD_PROJECT) { + throw new Error( + `Unable to determine email for service account '${options.serviceAccount}' because process.env.GCLOUD_PROJECT is not set.` + ); + } + trigger.serviceAccountEmail = `${options.serviceAccount}${process.env.GCLOUD_PROJECT}.iam.gserviceaccount.com`; + } else if (options.serviceAccount.includes('@')) { + trigger.serviceAccountEmail = options.serviceAccount; + } else { + throw new Error( + `Invalid option for serviceAccount: '${options.serviceAccount}'. Valid options are 'default', a service account email, or '{serviceAccountName}@'` + ); + } + } + return trigger; } diff --git a/src/function-builder.ts b/src/function-builder.ts index 9b5f5660b..7411f72e2 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -99,6 +99,16 @@ function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean { } } } + + if ( + runtimeOptions.serviceAccount && + runtimeOptions.serviceAccount !== 'default' && + !_.includes(runtimeOptions.serviceAccount, '@') + ) { + throw new Error( + `serviceAccount must be set to 'default', a service account email, or '{serviceAccountName}@'` + ); + } return true; } @@ -139,9 +149,12 @@ export function region( * 0 to 540. * 3. `failurePolicy`: failure policy of the function, with boolean `true` being * equivalent to providing an empty retry object. - * 4. `vpcConnector`: id of a VPC connector in the same project and region - * 5. `vpcConnectorEgressSettings`: when a `vpcConnector` is set, control which - * egress traffic is sent through the `vpcConnector`. + * 4. `vpcConnector`: id of a VPC connector in same project and region. + * 5. `vpcConnectorEgressSettings`: when a vpcConnector is set, control which + * egress traffic is sent through the vpcConnector. + * 6. `serviceAccount`: Specific service account for the function. + * 7. `ingressSettings`: ingress settings for the function, which control where a HTTPS + * function can be called from. * * Value must not be null. */ diff --git a/src/function-configuration.ts b/src/function-configuration.ts index 10098a745..f5a325b41 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -122,6 +122,11 @@ export interface RuntimeOptions { */ vpcConnectorEgressSettings?: typeof VPC_EGRESS_SETTINGS_OPTIONS[number]; + /** + * Specific service account for the function to run as + */ + serviceAccount?: 'default' | string; + /** * Ingress settings */ From 07ca97f68bcf3e724a81c4da62807a51e5712619 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Dec 2020 14:14:49 -0800 Subject: [PATCH 274/705] Bump highlight.js from 10.2.0 to 10.4.1 (#823) Bumps [highlight.js](https://github.com/highlightjs/highlight.js) from 10.2.0 to 10.4.1. - [Release notes](https://github.com/highlightjs/highlight.js/releases) - [Changelog](https://github.com/highlightjs/highlight.js/blob/master/CHANGES.md) - [Commits](https://github.com/highlightjs/highlight.js/compare/10.2.0...10.4.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: joehan --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ff1ff5cd..167df1e2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1919,9 +1919,9 @@ "dev": true }, "highlight.js": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.2.0.tgz", - "integrity": "sha512-OryzPiqqNCfO/wtFo619W+nPYALM6u7iCQkum4bqRmmlcTikOkmlL06i009QelynBPAlNByTQU6cBB2cOBQtCw==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.4.1.tgz", + "integrity": "sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg==", "dev": true }, "html-encoding-sniffer": { From df592717075a9bbb1a17dd5e55c77a076b328cf9 Mon Sep 17 00:00:00 2001 From: joehan Date: Mon, 7 Dec 2020 15:43:39 -0800 Subject: [PATCH 275/705] Adds changelog entries for v3.13.0 (#824) * adds changelog entries for 3.13.0 * formats * Update CHANGELOG.md Co-authored-by: Sam Stern * Update CHANGELOG.md Co-authored-by: Sam Stern * Update CHANGELOG.md Co-authored-by: Sam Stern * formats * formats Co-authored-by: Sam Stern --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..c9cb1adff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +- Adds `serviceAccount` option to `runtimeOptions` to specify which service account Cloud Function should use at runtime. For example: + +``` +const functions = require('firebase-functions'); + +exports.myFunction = functions.runWith({ + serviceAccount: 'test-sa@project.iam.gserviceaccount.com' + // OR + // serviceAcount: 'test-sa@" + // OR + // serviceAccount: 'default' + }) + +``` + +Requires firebase-tools@8.18.0 or later. Thanks @egor-miasnikov! + +- Upgrades `highlight.js` to `10.4.1` to fix a vulnerability. From 21d2c0bb458bd2aa6628f8cf4e2648d51d8a0f13 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Mon, 7 Dec 2020 23:48:48 +0000 Subject: [PATCH 276/705] 3.13.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 167df1e2b..7a1981a69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.12.0", + "version": "3.13.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a27384f60..8f4bb09c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.12.0", + "version": "3.13.0", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From c69c9bc083a2ef55a3ea152839d6027b4bb20352 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Mon, 7 Dec 2020 23:48:54 +0000 Subject: [PATCH 277/705] [firebase-release] Removed change log and reset repo after 3.13.0 release --- CHANGELOG.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9cb1adff..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +0,0 @@ -- Adds `serviceAccount` option to `runtimeOptions` to specify which service account Cloud Function should use at runtime. For example: - -``` -const functions = require('firebase-functions'); - -exports.myFunction = functions.runWith({ - serviceAccount: 'test-sa@project.iam.gserviceaccount.com' - // OR - // serviceAcount: 'test-sa@" - // OR - // serviceAccount: 'default' - }) - -``` - -Requires firebase-tools@8.18.0 or later. Thanks @egor-miasnikov! - -- Upgrades `highlight.js` to `10.4.1` to fix a vulnerability. From 9b8fe654d090cb6f4721c9725aa0f0707afc7433 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Mon, 11 Jan 2021 17:37:14 +0000 Subject: [PATCH 278/705] Fix emulated database URL parse issue (#838) --- package.json | 1 + spec/providers/database.spec.ts | 11 +++++++++++ src/providers/database.ts | 11 +++++++++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8f4bb09c5..9b2c3cd8c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "build:pack": "rm -rf lib && npm install && tsc -p tsconfig.release.json && npm pack", "build:release": "npm install --production && npm install --no-save typescript firebase-admin && tsc -p tsconfig.release.json", "build": "tsc -p tsconfig.release.json", + "build:watch": "npm run build -- -w", "format": "prettier --check '**/*.{json,md,ts,yml,yaml}'", "format:fix": "prettier --write '**/*.{json,md,ts,yml,yaml}'", "lint": "tslint --config tslint.json --project tsconfig.json ", diff --git a/spec/providers/database.spec.ts b/spec/providers/database.spec.ts index b764a7ea0..bf7e54b6b 100644 --- a/spec/providers/database.spec.ts +++ b/spec/providers/database.spec.ts @@ -472,6 +472,17 @@ describe('Database Functions', () => { ); }).to.throw(Error); }); + + it('should use the emulator host when present', () => { + process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:1234'; + const [instance, path] = database.extractInstanceAndPath( + 'projects/_/instances/foo/refs/bar', + 'firebaseio-staging.com' + ); + expect(instance).to.equal('http://localhost:1234/?ns=foo'); + expect(path).to.equal('/bar'); + delete process.env.FIREBASE_DATABASE_EMULATOR_HOST; + }); }); describe('DataSnapshot', () => { diff --git a/src/providers/database.ts b/src/providers/database.ts index 2aa50b55a..0e4393d05 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -334,8 +334,15 @@ export function extractInstanceAndPath( `Expect project to be '_' in a Firebase Realtime Database event` ); } - const dbInstance = 'https://' + dbInstanceName + '.' + domain; - return [dbInstance, path]; + + const emuHost = process.env.FIREBASE_DATABASE_EMULATOR_HOST; + if (emuHost) { + const dbInstance = `http://${emuHost}/?ns=${dbInstanceName}`; + return [dbInstance, path]; + } else { + const dbInstance = 'https://' + dbInstanceName + '.' + domain; + return [dbInstance, path]; + } } /** From 2f72c33ab47a57dce7175eb4449dca00073d46cc Mon Sep 17 00:00:00 2001 From: Leon Radley Date: Wed, 13 Jan 2021 20:46:14 +0100 Subject: [PATCH 279/705] Add support for 4GB memory option (#842) * Add support for 4GB memory option The 4GB memory option was not added everywhere. Fixes #834 * Added test for 4GB memory option Co-authored-by: Leon Radley Co-authored-by: joehan --- spec/function-builder.spec.ts | 12 ++++++++++++ src/cloud-functions.ts | 1 + src/function-builder.ts | 4 ++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/spec/function-builder.spec.ts b/spec/function-builder.spec.ts index c31a82d4a..209d63d60 100644 --- a/spec/function-builder.spec.ts +++ b/spec/function-builder.spec.ts @@ -286,4 +286,16 @@ describe('FunctionBuilder', () => { }); }).to.throw(); }); + + it('should allow setting 4GB memory option', () => { + const fn = functions + .runWith({ + memory: '4GB', + }) + .region('europe-west1') + .auth.user() + .onCreate((user) => user); + + expect(fn.__trigger.availableMemoryMb).to.deep.equal(4096); + }); }); diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 50d674022..aef850da4 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -507,6 +507,7 @@ export function optionsToTrigger(options: DeploymentOptions) { '512MB': 512, '1GB': 1024, '2GB': 2048, + '4GB': 4096, }; trigger.availableMemoryMb = _.get(memoryLookup, options.memory); } diff --git a/src/function-builder.ts b/src/function-builder.ts index 7411f72e2..8d06b2142 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -144,7 +144,7 @@ export function region( * Configure runtime options for the function. * @param runtimeOptions Object with optional fields: * 1. `memory`: amount of memory to allocate to the function, possible values - * are: '128MB', '256MB', '512MB', '1GB', and '2GB'. + * are: '128MB', '256MB', '512MB', '1GB', '2GB', and '4GB'. * 2. `timeoutSeconds`: timeout for the function in seconds, possible values are * 0 to 540. * 3. `failurePolicy`: failure policy of the function, with boolean `true` being @@ -188,7 +188,7 @@ export class FunctionBuilder { * Configure runtime options for the function. * @param runtimeOptions Object with optional fields: * 1. `memory`: amount of memory to allocate to the function, possible values - * are: '128MB', '256MB', '512MB', '1GB', and '2GB'. + * are: '128MB', '256MB', '512MB', '1GB', '2GB', and '4GB'. * 2. `timeoutSeconds`: timeout for the function in seconds, possible values are * 0 to 540. * 3. `failurePolicy`: failure policy of the function, with boolean `true` being From f508951c651f755734dea6c8202c8a7ced034e2a Mon Sep 17 00:00:00 2001 From: huangjeff5 <64040981+huangjeff5@users.noreply.github.com> Date: Fri, 15 Jan 2021 10:31:54 -0800 Subject: [PATCH 280/705] Remove circular dependencies when logging. Fixes #737 (#844) * Remove circular dependencies when logging. Fixes #737 * Fix bug in binding the output logger * modify removeCircular to return a copy of the original object, rather than mutate * Modify removeCircular to return a new object, rather than mutating in place Co-authored-by: Michael Bleigh --- spec/logger.spec.ts | 84 ++++++++++++++++++++++++++++++--------------- src/logger.ts | 35 +++++++++++++++++-- 2 files changed, 89 insertions(+), 30 deletions(-) diff --git a/spec/logger.spec.ts b/spec/logger.spec.ts index c6859b60a..dda16087c 100644 --- a/spec/logger.spec.ts +++ b/spec/logger.spec.ts @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import * as sinon from 'sinon'; import * as logger from '../src/logger'; const SUPPORTS_STRUCTURED_LOGS = @@ -8,52 +7,51 @@ const SUPPORTS_STRUCTURED_LOGS = describe(`logger (${ SUPPORTS_STRUCTURED_LOGS ? 'structured' : 'unstructured' })`, () => { - let sandbox: sinon.SinonSandbox; - let stdoutStub: sinon.SinonStub; - let stderrStub: sinon.SinonStub; + let stdoutWrite = process.stdout.write.bind(process.stdout); + let stderrWrite = process.stderr.write.bind(process.stderr); + let lastOut: string; + let lastErr: string; beforeEach(() => { - sandbox = sinon.createSandbox(); - stdoutStub = sandbox.stub(process.stdout, 'write'); - stderrStub = sandbox.stub(process.stderr, 'write'); + process.stdout.write = (msg: Buffer | string, cb?: any): boolean => { + lastOut = msg as string; + return stdoutWrite(msg, cb); + }; + process.stderr.write = (msg: Buffer | string, cb?: any): boolean => { + lastErr = msg as string; + return stderrWrite(msg, cb); + }; }); - function expectOutput(stdStub: sinon.SinonStub, entry: any) { + afterEach(() => { + process.stdout.write = stdoutWrite; + process.stderr.write = stderrWrite; + }); + + function expectOutput(last: string, entry: any) { if (SUPPORTS_STRUCTURED_LOGS) { - return expect( - JSON.parse((stdStub.getCalls()[0].args[0] as string).trim()) - ).to.deep.eq(entry); + return expect(JSON.parse(last.trim())).to.deep.eq(entry); } else { // legacy logging is not structured, but do a sanity check - return expect(stdStub.getCalls()[0].args[0]).to.include(entry.message); + return expect(last).to.include(entry.message); } } function expectStdout(entry: any) { - return expectOutput(stdoutStub, entry); + return expectOutput(lastOut, entry); } function expectStderr(entry: any) { - return expectOutput(stderrStub, entry); + return expectOutput(lastErr, entry); } describe('logging methods', () => { - let writeStub: sinon.SinonStub; - beforeEach(() => { - writeStub = sinon.stub(logger, 'write'); - }); - - afterEach(() => { - writeStub.restore(); - }); - it('should coalesce arguments into the message', () => { logger.log('hello', { middle: 'obj' }, 'end message'); expectStdout({ severity: 'INFO', message: "hello { middle: 'obj' } end message", }); - sandbox.restore(); // to avoid swallowing test runner output }); it('should merge structured data from the last argument', () => { @@ -63,7 +61,6 @@ describe(`logger (${ message: 'hello world', additional: 'context', }); - sandbox.restore(); // to avoid swallowing test runner output }); it('should not recognize null as a structured logging object', () => { @@ -72,13 +69,46 @@ describe(`logger (${ severity: 'INFO', message: 'hello world null', }); - sandbox.restore(); // to avoid swallowing test runner output }); }); describe('write', () => { describe('structured logging', () => { describe('write', () => { + it('should remove circular references', () => { + const circ: any = { b: 'foo' }; + circ.circ = circ; + + const entry: logger.LogEntry = { + severity: 'ERROR', + message: 'testing circular', + circ, + }; + logger.write(entry); + expectStderr({ + severity: 'ERROR', + message: 'testing circular', + circ: { b: 'foo', circ: '[Circular]' }, + }); + }); + + it('should remove circular references in arrays', () => { + const circ: any = { b: 'foo' }; + circ.circ = [circ]; + + const entry: logger.LogEntry = { + severity: 'ERROR', + message: 'testing circular', + circ, + }; + logger.write(entry); + expectStderr({ + severity: 'ERROR', + message: 'testing circular', + circ: { b: 'foo', circ: ['[Circular]'] }, + }); + }); + for (const severity of ['DEBUG', 'INFO', 'NOTICE']) { it(`should output ${severity} severity to stdout`, () => { let entry: logger.LogEntry = { @@ -87,7 +117,6 @@ describe(`logger (${ }; logger.write(entry); expectStdout(entry); - sandbox.restore(); // to avoid swallowing test runner output }); } @@ -105,7 +134,6 @@ describe(`logger (${ }; logger.write(entry); expectStderr(entry); - sandbox.restore(); // to avoid swallowing test runner output }); } }); diff --git a/src/logger.ts b/src/logger.ts index 7958ca457..d6c0d2220 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -30,13 +30,40 @@ export interface LogEntry { [key: string]: any; } +function removeCircular(obj: any, refs: any[] = []): any { + if (typeof obj !== 'object' || !obj) { + return obj; + } + if (refs.includes(obj)) { + return '[Circular]'; + } else { + refs.push(obj); + } + let returnObj: any; + if (Array.isArray(obj)) { + returnObj = new Array(obj.length); + } else { + returnObj = {}; + } + for (const k in obj) { + if (refs.includes(obj[k])) { + returnObj[k] = '[Circular]'; + } else { + returnObj[k] = removeCircular(obj[k], refs); + } + } + return returnObj; +} + /** * Writes a `LogEntry` to `stdout`/`stderr` (depending on severity). * @param entry The `LogEntry` including severity, message, and any additional structured metadata. */ export function write(entry: LogEntry) { if (SUPPORTS_STRUCTURED_LOGS) { - UNPATCHED_CONSOLE[CONSOLE_SEVERITY[entry.severity]](JSON.stringify(entry)); + UNPATCHED_CONSOLE[CONSOLE_SEVERITY[entry.severity]]( + JSON.stringify(removeCircular(entry)) + ); return; } @@ -50,7 +77,11 @@ export function write(entry: LogEntry) { } } if (jsonKeyCount > 0) { - message = `${message} ${JSON.stringify(jsonPayload, null, 2)}`; + message = `${message} ${JSON.stringify( + removeCircular(jsonPayload), + null, + 2 + )}`; } UNPATCHED_CONSOLE[CONSOLE_SEVERITY[entry.severity]](message); } From e920b01d1d2f0ba4fe528936196537e90d48ee4a Mon Sep 17 00:00:00 2001 From: joehan Date: Fri, 15 Jan 2021 10:41:17 -0800 Subject: [PATCH 281/705] Add changelog entry for #842 (#845) * add changelog for memory options * formats * Update CHANGELOG.md * Update CHANGELOG.md Co-authored-by: huangjeff5 <64040981+huangjeff5@users.noreply.github.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..dba1ded30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +- Fixes a bug that prevented Functions from being deployed with `availableMemoryMb` set to `4GB`. +- Fixes bug where `functions.logger.log` crashes function if circular dependencies are passed in From daeda1dd38904c6c4c6c1dc2b09d3c2a275ccc4e Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 15 Jan 2021 18:47:18 +0000 Subject: [PATCH 282/705] 3.13.1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7a1981a69..f1e9dc457 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.13.0", + "version": "3.13.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9b2c3cd8c..f6f9ae1c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.13.0", + "version": "3.13.1", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From 61dc5a16cab32a9e671d6fcee383ef1af0d25420 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 15 Jan 2021 18:47:23 +0000 Subject: [PATCH 283/705] [firebase-release] Removed change log and reset repo after 3.13.1 release --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dba1ded30..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +0,0 @@ -- Fixes a bug that prevented Functions from being deployed with `availableMemoryMb` set to `4GB`. -- Fixes bug where `functions.logger.log` crashes function if circular dependencies are passed in From 5f64797555e150ae8de93445edb304e6ea81fda2 Mon Sep 17 00:00:00 2001 From: takaaa220 Date: Wed, 20 Jan 2021 07:41:55 +0900 Subject: [PATCH 284/705] Fix IngressSettings (#827) Co-authored-by: joehan --- spec/function-builder.spec.ts | 21 +++++++++++++++++++++ src/cloud-functions.ts | 5 +++++ src/function-builder.ts | 12 ++++++++++++ 3 files changed, 38 insertions(+) diff --git a/spec/function-builder.spec.ts b/spec/function-builder.spec.ts index 209d63d60..28cf35c3a 100644 --- a/spec/function-builder.spec.ts +++ b/spec/function-builder.spec.ts @@ -200,6 +200,27 @@ describe('FunctionBuilder', () => { }).to.throw(Error, 'at least one region'); }); + it('should allow a ingressSettings to be set', () => { + const fn = functions + .runWith({ ingressSettings: 'ALLOW_INTERNAL_ONLY' }) + .https.onRequest(() => {}); + + expect(fn.__trigger.ingressSettings).to.equal('ALLOW_INTERNAL_ONLY'); + }); + + it('should throw an error if user chooses an invalid ingressSettings', () => { + expect(() => { + return functions.runWith({ + ingressSettings: 'INVALID_OPTION', + } as any); + }).to.throw( + Error, + `The only valid ingressSettings values are: ${functions.INGRESS_SETTINGS_OPTIONS.join( + ',' + )}` + ); + }); + it('should allow a vpcConnector to be set', () => { const fn = functions .runWith({ diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index aef850da4..126997c1d 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -273,6 +273,7 @@ export interface TriggerAnnotated { vpcConnector?: string; vpcConnectorEgressSettings?: string; serviceAccountEmail?: string; + ingressSettings?: string; }; } @@ -519,6 +520,10 @@ export function optionsToTrigger(options: DeploymentOptions) { trigger.maxInstances = options.maxInstances; } + if (options.ingressSettings) { + trigger.ingressSettings = options.ingressSettings; + } + if (options.vpcConnector) { trigger.vpcConnector = options.vpcConnector; } diff --git a/src/function-builder.ts b/src/function-builder.ts index 8d06b2142..6f50e6bde 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -31,6 +31,7 @@ import { SUPPORTED_REGIONS, VALID_MEMORY_OPTIONS, VPC_EGRESS_SETTINGS_OPTIONS, + INGRESS_SETTINGS_OPTIONS, } from './function-configuration'; import * as analytics from './providers/analytics'; import * as auth from './providers/auth'; @@ -68,6 +69,17 @@ function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean { ); } + if ( + runtimeOptions.ingressSettings && + !_.includes(INGRESS_SETTINGS_OPTIONS, runtimeOptions.ingressSettings) + ) { + throw new Error( + `The only valid ingressSettings values are: ${INGRESS_SETTINGS_OPTIONS.join( + ',' + )}` + ); + } + if ( runtimeOptions.vpcConnectorEgressSettings && !_.includes( From 88691699d875f47ea7dc5db382f3624c68dc3148 Mon Sep 17 00:00:00 2001 From: Reza Rahmati Date: Tue, 19 Jan 2021 17:45:42 -0500 Subject: [PATCH 285/705] Fixing issue reading env.DATABASE_URL and process.env.STORAGE_BUCKET_URL (#840) * #829 fixing issue reading env.DATABASE_URL and env.STORAGE_BUCKET_URL * #829 revert space changes Co-authored-by: joehan --- src/setup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/setup.ts b/src/setup.ts index d2935af15..6a9db702d 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -51,10 +51,10 @@ export function setup() { ); process.env.FIREBASE_CONFIG = JSON.stringify({ databaseURL: - `${process.env.DATABASE_URL}` || + process.env.DATABASE_URL || `https://${process.env.GCLOUD_PROJECT}.firebaseio.com`, storageBucket: - `${process.env.STORAGE_BUCKET_URL}` || + process.env.STORAGE_BUCKET_URL || `${process.env.GCLOUD_PROJECT}.appspot.com`, projectId: process.env.GCLOUD_PROJECT, }); From 0e2e95c8540a6e7e136e5bf718ccacaf319f8587 Mon Sep 17 00:00:00 2001 From: joehan Date: Thu, 4 Feb 2021 06:52:32 -0800 Subject: [PATCH 286/705] Adds changelog for #829 and #827. (#848) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..06d7cfb4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +- Fixes issue where DATABASE_URL and STORAGE_BUCKET_URL could not be set to undefined. (#829) +- Fixes a bug where ingressSettings could not be set. (#827) From 2d81a6be9e31b610f9d66c7ab7f1172762698ed7 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Thu, 4 Feb 2021 16:56:10 +0000 Subject: [PATCH 287/705] Update issue templates (#857) --- .github/ISSUE_TEMPLATE/---feature-request.md | 17 ----------------- .github/ISSUE_TEMPLATE/---report-a-bug.md | 2 +- .github/ISSUE_TEMPLATE/config.yml | 8 ++++++++ 3 files changed, 9 insertions(+), 18 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/---feature-request.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/---feature-request.md b/.github/ISSUE_TEMPLATE/---feature-request.md deleted file mode 100644 index 93f7d6de1..000000000 --- a/.github/ISSUE_TEMPLATE/---feature-request.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: "\U0001F4A1 Feature Request" -about: - Have a feature you'd like to see in the functions SDK? Request it through our - support channel. -title: '' -labels: '' -assignees: '' ---- - - - -Great, we love hearing how we can improve our products! However, GitHub is not the place to submit them. Please submit your feature requests to: -https://firebase.google.com/support/contact/bugs-features/ diff --git a/.github/ISSUE_TEMPLATE/---report-a-bug.md b/.github/ISSUE_TEMPLATE/---report-a-bug.md index 694c4aedf..3ad82bf60 100644 --- a/.github/ISSUE_TEMPLATE/---report-a-bug.md +++ b/.github/ISSUE_TEMPLATE/---report-a-bug.md @@ -1,6 +1,6 @@ --- name: '⚠️ Report a Bug' -about: Think you found a bug in the SDK? Report it here. +about: Think you found a bug in the firebase-functions SDK? Report it here. Please do not use this form if your function is deployed successfully but not working as you expected. title: '' labels: '' assignees: '' diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..918e205f9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: 💻 Bug in the Firebase CLI + url: https://github.com/firebase/firebase-tools/issues/new/choose + about: Have you found a bug in the Firebase CLI? + - name: 🔥 Firebase Support + url: https://firebase.google.com/support/ + about: If you have an issue with your functions in production, please contact support. From 0c382ed0f7a94f3cc270b13111d635fe4caccd79 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Mon, 22 Feb 2021 18:01:36 +0000 Subject: [PATCH 288/705] 3.13.2 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f1e9dc457..9f26affdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.13.1", + "version": "3.13.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f6f9ae1c6..11a4dfb39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.13.1", + "version": "3.13.2", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From fc4788e61012306eda385428f25291094fce624d Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Mon, 22 Feb 2021 18:01:44 +0000 Subject: [PATCH 289/705] [firebase-release] Removed change log and reset repo after 3.13.2 release --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06d7cfb4c..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +0,0 @@ -- Fixes issue where DATABASE_URL and STORAGE_BUCKET_URL could not be set to undefined. (#829) -- Fixes a bug where ingressSettings could not be set. (#827) From e2c9475372d6eab47d038f29ebc386ec18029784 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Mon, 1 Mar 2021 13:28:43 -0800 Subject: [PATCH 290/705] Remove crashlyitcs (#866) --- package-lock.json | 18 +-- spec/providers/crashlytics.spec.ts | 157 ----------------------- src/function-builder.ts | 11 -- src/handler-builder.ts | 36 ------ src/index.ts | 2 - src/providers/crashlytics.ts | 197 ----------------------------- 6 files changed, 9 insertions(+), 412 deletions(-) delete mode 100644 spec/providers/crashlytics.spec.ts delete mode 100644 src/providers/crashlytics.ts diff --git a/package-lock.json b/package-lock.json index 9f26affdd..f1f20b89a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -490,7 +490,7 @@ "@types/range-parser": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", - "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + "integrity": "sha1-fuMwunyq+5gJC+zoal7kQRWQTCw=" }, "@types/serve-static": { "version": "1.13.5", @@ -665,7 +665,7 @@ "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "integrity": "sha1-jSR136tVO7M+d7VOWeiAu4ziMTY=", "dev": true, "requires": { "safer-buffer": "~2.1.0" @@ -1042,7 +1042,7 @@ "cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "integrity": "sha1-6sEdpRWS3Ya58G9uesKTs9+HXSk=", "requires": { "object-assign": "^4", "vary": "^1" @@ -1581,7 +1581,7 @@ "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "integrity": "sha1-3M5SwF9kTymManq5Nr1yTO/786Y=", "dev": true, "requires": { "asynckit": "^0.4.0", @@ -2356,7 +2356,7 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", "dev": true }, "json-stringify-safe": { @@ -2565,7 +2565,7 @@ "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "integrity": "sha1-i75Q6oW+1ZvJ4z3KuCNe6bz0Q80=", "dev": true, "requires": { "pseudomap": "^1.0.2", @@ -3012,7 +3012,7 @@ "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU=", "dev": true }, "object-assign": { @@ -3327,7 +3327,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew=", "dev": true }, "qs": { @@ -3745,7 +3745,7 @@ "stream-events": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "integrity": "sha1-u8iY7E3zOkkC2JIzPUfam/HEBtU=", "dev": true, "optional": true, "requires": { diff --git a/spec/providers/crashlytics.spec.ts b/spec/providers/crashlytics.spec.ts deleted file mode 100644 index 59e745dfa..000000000 --- a/spec/providers/crashlytics.spec.ts +++ /dev/null @@ -1,157 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2017 Firebase -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import { expect } from 'chai'; - -import { apps as appsNamespace } from '../../src/apps'; -import * as functions from '../../src/index'; -import * as crashlytics from '../../src/providers/crashlytics'; - -describe('Crashlytics Functions', () => { - describe('Issue Builder', () => { - before(() => { - appsNamespace.init(); - process.env.GCLOUD_PROJECT = 'project1'; - }); - - after(() => { - delete appsNamespace.singleton; - delete process.env.GCLOUD_PROJECT; - }); - - it('should allow both region and runtime options to be set', () => { - const fn = functions - .region('us-east1') - .runWith({ - timeoutSeconds: 90, - memory: '256MB', - }) - .crashlytics.issue() - .onNew((issue) => issue); - - expect(fn.__trigger.regions).to.deep.equal(['us-east1']); - expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); - expect(fn.__trigger.timeout).to.deep.equal('90s'); - }); - - describe('#onNew', () => { - it('should return a TriggerDefinition with appropriate values', () => { - const cloudFunction = crashlytics.issue().onNew((data) => null); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: 'providers/firebase.crashlytics/eventTypes/issue.new', - resource: 'projects/project1', - service: 'fabric.io', - }, - }); - }); - }); - - describe('#onRegressed', () => { - it('should return a TriggerDefinition with appropriate values', () => { - const cloudFunction = crashlytics.issue().onRegressed((data) => null); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: - 'providers/firebase.crashlytics/eventTypes/issue.regressed', - resource: 'projects/project1', - service: 'fabric.io', - }, - }); - }); - }); - - describe('#onVelocityAlert', () => { - it('should return a TriggerDefinition with appropriate values', () => { - const cloudFunction = crashlytics - .issue() - .onVelocityAlert((data) => null); - expect(cloudFunction.__trigger).to.deep.equal({ - eventTrigger: { - eventType: - 'providers/firebase.crashlytics/eventTypes/issue.velocityAlert', - resource: 'projects/project1', - service: 'fabric.io', - }, - }); - }); - }); - - describe('HandlerBuilder', () => { - describe('#onNew', () => { - it('should return a CloudFunction with appropriate values', () => { - const cloudFunction = functions.handler.crashlytics.issue.onNew( - (testIssue) => { - return ( - testIssue.issueId + testIssue.issueTitle + testIssue.createTime - ); - } - ); - expect(cloudFunction.__trigger).to.deep.equal({}); - }); - }); - - describe('#onRegressed', () => { - it('should return a CloudFunction with appropriate values', () => { - const cloudFunction = functions.handler.crashlytics.issue.onRegressed( - (testIssue) => { - return ( - testIssue.issueId + testIssue.issueTitle + testIssue.createTime - ); - } - ); - expect(cloudFunction.__trigger).to.deep.equal({}); - }); - }); - - describe('#onVelocityAlert', () => { - it('should return a CloudFunction with appropriate values', () => { - const cloudFunction = functions.handler.crashlytics.issue.onVelocityAlert( - (testIssue) => { - return ( - testIssue.issueId + testIssue.issueTitle + testIssue.createTime - ); - } - ); - expect(cloudFunction.__trigger).to.deep.equal({}); - }); - }); - }); - }); - - describe('process.env.GCLOUD_PROJECT not set', () => { - it('should not throw if __trigger is not accessed', () => { - expect(() => crashlytics.issue().onNew(() => null)).to.not.throw(Error); - }); - - it('should throw if __trigger is accessed', () => { - expect(() => crashlytics.issue().onNew(() => null).__trigger).to.throw( - Error - ); - }); - - it('should not throw when #run is called', () => { - const cf = crashlytics.issue().onNew(() => null); - expect(cf.run).to.not.throw(Error); - }); - }); -}); diff --git a/src/function-builder.ts b/src/function-builder.ts index 6f50e6bde..9d016d817 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -35,7 +35,6 @@ import { } from './function-configuration'; import * as analytics from './providers/analytics'; import * as auth from './providers/auth'; -import * as crashlytics from './providers/crashlytics'; import * as database from './providers/database'; import * as firestore from './providers/firestore'; import * as https from './providers/https'; @@ -311,16 +310,6 @@ export class FunctionBuilder { }; } - get crashlytics() { - return { - /** - * Handle events related to Crashlytics issues. An issue in Crashlytics is - * an aggregation of crashes which have a shared root cause. - */ - issue: () => crashlytics._issueWithOptions(this.options), - }; - } - get analytics() { return { /** diff --git a/src/handler-builder.ts b/src/handler-builder.ts index ea0f4af74..ad4cd1541 100644 --- a/src/handler-builder.ts +++ b/src/handler-builder.ts @@ -26,7 +26,6 @@ import { apps } from './apps'; import { CloudFunction, EventContext, HttpsFunction } from './cloud-functions'; import * as analytics from './providers/analytics'; import * as auth from './providers/auth'; -import * as crashlytics from './providers/crashlytics'; import * as database from './providers/database'; import * as firestore from './providers/firestore'; import * as https from './providers/https'; @@ -182,41 +181,6 @@ export class HandlerBuilder { }; } - /** - * Create a handler for Firebase Crashlytics events. - - * `issue.onNew` handles events where the app experiences an issue for the first time. - - * @example - * ```javascript - * exports.myFunction = functions.handler.crashlytics.issue.onNew((issue) => { ... }) - * ``` - - * `issue.onRegressed` handles events where an issue reoccurs after it - * is closed in Crashlytics. - * - * @example - * ```javascript - * exports.myFunction = functions.handler.crashlytics.issue.onRegressed((issue) => { ... }) - * ``` - - * `issue.onVelocityAlert` handles events where a statistically significant number - * of sessions in a given build crash. - * - * @example - * ```javascript - * exports.myFunction = functions.handler.crashlytics.issue.onVelocityAlert((issue) => { ... }) - * ``` - - */ - get crashlytics() { - return { - get issue() { - return new crashlytics.IssueBuilder(() => null, {}); - }, - }; - } - /** * Create a handler for Firebase Remote Config events. diff --git a/src/index.ts b/src/index.ts index dcca98172..9d49414e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,6 @@ // Providers: import * as analytics from './providers/analytics'; import * as auth from './providers/auth'; -import * as crashlytics from './providers/crashlytics'; import * as database from './providers/database'; import * as firestore from './providers/firestore'; import * as https from './providers/https'; @@ -43,7 +42,6 @@ export { analytics, app, auth, - crashlytics, database, firestore, handler, diff --git a/src/providers/crashlytics.ts b/src/providers/crashlytics.ts deleted file mode 100644 index 54fc26469..000000000 --- a/src/providers/crashlytics.ts +++ /dev/null @@ -1,197 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2017 Firebase -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import { - CloudFunction, - EventContext, - makeCloudFunction, -} from '../cloud-functions'; -import { DeploymentOptions } from '../function-configuration'; - -/** @hidden */ -export const provider = 'google.firebase.crashlytics'; -/** @hidden */ -export const service = 'fabric.io'; - -/** - * Registers a Cloud Function to handle Crashlytics issue events. - * - * @returns Crashlytics issue event builder interface. - */ -export function issue() { - return _issueWithOptions({}); -} - -/** @hidden */ -export function _issueWithOptions(options: DeploymentOptions) { - return new IssueBuilder(() => { - if (!process.env.GCLOUD_PROJECT) { - throw new Error('process.env.GCLOUD_PROJECT is not set.'); - } - return 'projects/' + process.env.GCLOUD_PROJECT; - }, options); -} - -/** The Firebase Crashlytics issue builder interface. */ -export class IssueBuilder { - /** @hidden */ - constructor( - private triggerResource: () => string, - private options: DeploymentOptions - ) {} - - /** @hidden */ - onNewDetected(handler: any): Error { - throw new Error('"onNewDetected" is now deprecated, please use "onNew"'); - } - - /** - * Event handler that fires every time a new issue occurs in a project. - * - * @param handler Event handler that fires every time a new issue event occurs. - * @example - * ```javascript - * exports.postOnNewIssue = functions.crashlytics.issue().onNew(event => { - * const { data } = event; - * issueId = data.issueId; - * issueTitle = data.issueTitle; - * const slackMessage = ` There's a new issue (${issueId}) ` + - * `in your app - ${issueTitle}`; - * return notifySlack(slackMessage).then(() => { - * functions.logger.info(`Posted new issue ${issueId} successfully to Slack`); - * }); - * }); - * ``` - */ - onNew( - handler: (issue: Issue, context: EventContext) => PromiseLike | any - ): CloudFunction { - return this.onEvent(handler, 'issue.new'); - } - - /** - * Event handler that fires every time a regressed issue reoccurs in a project. - * - * @param handler Event handler that fires every time a regressed issue event occurs. - */ - onRegressed( - handler: (issue: Issue, context: EventContext) => PromiseLike | any - ): CloudFunction { - return this.onEvent(handler, 'issue.regressed'); - } - - /** - * Event handler that fires every time a velocity alert occurs in a project. - * - * @param handler handler that fires every time a velocity alert issue event occurs. - */ - onVelocityAlert( - handler: (issue: Issue, context: EventContext) => PromiseLike | any - ): CloudFunction { - return this.onEvent(handler, 'issue.velocityAlert'); - } - - private onEvent( - handler: (issue: Issue, context: EventContext) => PromiseLike | any, - eventType: string - ): CloudFunction { - return makeCloudFunction({ - handler, - provider, - eventType, - service, - legacyEventType: `providers/firebase.crashlytics/eventTypes/${eventType}`, - triggerResource: this.triggerResource, - options: this.options, - }); - } -} - -/** - * Interface representing a Firebase Crashlytics event that was logged for a specific issue. - */ -export interface Issue { - /** Crashlytics-provided issue ID. */ - issueId: string; - - /** Crashlytics-provided issue title. */ - issueTitle: string; - - /** AppInfo interface describing the App. */ - appInfo: AppInfo; - - /** - * UTC when the issue occurred in ISO8601 standard representation. - * - * Example: 1970-01-17T10:52:15.661-08:00 - */ - createTime: string; - - /** - * UTC When the issue was closed in ISO8601 standard representation. - * - * Example: 1970-01-17T10:52:15.661-08:00 - */ - resolvedTime?: string; - - /** Information about the velocity alert, like number of crashes and percentage of users affected by the issue. */ - velocityAlert?: VelocityAlert; -} - -/** Interface representing Firebase Crashlytics VelocityAlert data. */ -export interface VelocityAlert { - /** - * The percentage of sessions which have been impacted by this issue. - * - * Example: .04 - */ - crashPercentage: number; - - /** The number of crashes that this issue has caused. */ - crashes: number; -} - -/** Interface representing Firebase Crashlytics AppInfo data. */ -export interface AppInfo { - /** - * The app's name. - * - * Example: "My Awesome App". - */ - appName: string; - - /** The app's platform. - * - * Examples: "android", "ios". - */ - appPlatform: string; - - /** Unique application identifier within an app store, either the Android package name or the iOS bundle id. */ - appId: string; - - /** - * The app's version name. - * - * Examples: "1.0", "4.3.1.1.213361", "2.3 (1824253)", "v1.8b22p6". - */ - latestAppVersion: string; -} From f2435b1eefeaba80c0bbd1578d6dbd3650630996 Mon Sep 17 00:00:00 2001 From: joehan Date: Tue, 2 Mar 2021 14:30:10 -0800 Subject: [PATCH 291/705] Cleaning up crashlytics trigger (#868) --- src/cloud-functions.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 126997c1d..272920c20 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -99,9 +99,6 @@ export interface EventContext { * * `google.analytics.event.log` * * `google.firebase.auth.user.create` * * `google.firebase.auth.user.delete` - * * `google.firebase.crashlytics.issue.new` - * * `google.firebase.crashlytics.issue.regressed` - * * `google.firebase.crashlytics.issue.velocityAlert` * * `google.firebase.database.ref.write` * * `google.firebase.database.ref.create` * * `google.firebase.database.ref.update` From ee793c77f1931d44adb3500bc2eed37d4a674370 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Wed, 3 Mar 2021 13:44:43 -0800 Subject: [PATCH 292/705] Update integration test so that it works again. (#869) * Updated node versions under test * Updated node dependencies to modern versions * Removed lodash dependency * Minor updates to TypeScript annotations/style * Fix URL and authentication when invoking a cloud schedule * Fix Cloud Schedule test to not finish until tests are complete --- integration_test/firestore.rules | 2 + integration_test/functions/src/https-tests.ts | 5 +- integration_test/functions/src/index.ts | 234 +++++++++--------- .../functions/src/pubsub-tests.ts | 25 +- .../functions/src/testLab-tests.ts | 3 +- .../functions/src/testLab-utils.ts | 3 +- integration_test/functions/src/testing.ts | 64 +++-- ...ckage.node8.json => package.json.template} | 12 +- integration_test/package.node10.json | 23 -- integration_test/run_tests.sh | 39 ++- 10 files changed, 202 insertions(+), 208 deletions(-) rename integration_test/{package.node8.json => package.json.template} (55%) delete mode 100644 integration_test/package.node10.json diff --git a/integration_test/firestore.rules b/integration_test/firestore.rules index e8f8d7997..d9df6d5d1 100644 --- a/integration_test/firestore.rules +++ b/integration_test/firestore.rules @@ -1,3 +1,5 @@ +rules_version = "2"; + service cloud.firestore { match /databases/{database}/documents { match /{document=**} { diff --git a/integration_test/functions/src/https-tests.ts b/integration_test/functions/src/https-tests.ts index 6af0f9fac..18972bd47 100644 --- a/integration_test/functions/src/https-tests.ts +++ b/integration_test/functions/src/https-tests.ts @@ -1,13 +1,12 @@ import * as functions from 'firebase-functions'; -import * as _ from 'lodash'; import { expectEq, TestSuite } from './testing'; const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; export const callableTests: any = functions.region(REGION).https.onCall((d) => { return new TestSuite('https onCall') - .it('should have the correct data', (data) => - expectEq(_.get(data, 'foo'), 'bar') + .it('should have the correct data', (data: any) => + expectEq(data?.foo, 'bar') ) .run(d.testId, d); }); diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 2ac16e5fa..0a980aa06 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -3,6 +3,7 @@ import * as admin from 'firebase-admin'; import * as functions from 'firebase-functions'; import * as fs from 'fs'; import * as https from 'https'; +import { PubSub } from '@google-cloud/pubsub'; export * from './pubsub-tests'; export * from './database-tests'; @@ -18,6 +19,7 @@ import * as utils from './test-utils'; import * as testLab from './testLab-utils'; import 'firebase-functions'; // temporary shim until process.env.FIREBASE_CONFIG available natively in GCF(BUG 63586213) +import { config } from 'firebase-functions'; const firebaseConfig = JSON.parse(process.env.FIREBASE_CONFIG); admin.initializeApp(); const REGION = functions.config().functions.test_region; @@ -37,26 +39,42 @@ function callHttpsTrigger(name: string, data: any, baseUrl) { ); } -function callScheduleTrigger(functionName: string, region: string) { - return new Promise((resolve, reject) => { +async function callScheduleTrigger(functionName: string, region: string) { + const accessToken = await admin.credential + .applicationDefault() + .getAccessToken(); + return new Promise((resolve, reject) => { const request = https.request( { method: 'POST', host: 'cloudscheduler.googleapis.com', - path: `projects/${firebaseConfig.projectId}/locations/us-central1/jobs/firebase-schedule-${functionName}-${region}:run`, + path: `/v1/projects/${firebaseConfig.projectId}/locations/us-central1/jobs/firebase-schedule-${functionName}-${region}:run`, headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken.access_token}`, }, }, (response) => { + if (response.statusCode! / 100 != 2) { + reject( + new Error('Failed request with status ' + response.statusCode!) + ); + return; + } let body = ''; response.on('data', (chunk) => { body += chunk; }); - response.on('end', () => resolve(body)); + response.on('end', () => { + console.log(`Successfully scheduled function ${functionName}`); + resolve(body); + }); } ); - request.on('error', reject); + request.on('error', (err) => { + console.error('Failed to schedule cloud scheduler job with error', err); + reject(err); + }); request.write('{}'); request.end(); }); @@ -67,14 +85,13 @@ export const integrationTests: any = functions .runWith({ timeoutSeconds: 540, }) - .https.onRequest((req: Request, resp: Response) => { + .https.onRequest(async (req: Request, resp: Response) => { // We take the base url for our https call (cloudfunctions.net, txckloud.net, etc) from the request // so that it changes with the environment that the tests are run in const baseUrl = req.hostname .split('.') .slice(1) .join('.'); - const pubsub: any = require('@google-cloud/pubsub')(); const testId = admin .database() .ref() @@ -83,114 +100,107 @@ export const integrationTests: any = functions .database() .ref(`testRuns/${testId}/timestamp`) .set(Date.now()); + const testIdRef = admin.database().ref(`testRuns/${testId}`); console.log('testId is: ', testId); fs.writeFile('/tmp/' + testId + '.txt', 'test', () => {}); - return Promise.all([ - // A database write to trigger the Firebase Realtime Database tests. - admin - .database() - .ref(`dbTests/${testId}/start`) - .set({ '.sv': 'timestamp' }), - // A Pub/Sub publish to trigger the Cloud Pub/Sub tests. - pubsub - .topic('pubsubTests') - .publisher() - .publish(Buffer.from(JSON.stringify({ testId }))), - // A user creation to trigger the Firebase Auth user creation tests. - admin - .auth() - .createUser({ - email: `${testId}@fake.com`, - password: 'secret', - displayName: `${testId}`, - }) - .then((userRecord) => { - // A user deletion to trigger the Firebase Auth user deletion tests. - admin.auth().deleteUser(userRecord.uid); - }), - // A firestore write to trigger the Cloud Firestore tests. - admin - .firestore() - .collection('tests') - .doc(testId) - .set({ test: testId }), - // Invoke a callable HTTPS trigger. - callHttpsTrigger('callableTests', { foo: 'bar', testId }, baseUrl), - // A Remote Config update to trigger the Remote Config tests. - admin.credential - .applicationDefault() - .getAccessToken() - .then((accessToken) => { - const options = { - hostname: 'firebaseremoteconfig.googleapis.com', - path: `/v1/projects/${firebaseConfig.projectId}/remoteConfig`, - method: 'PUT', - headers: { - Authorization: 'Bearer ' + accessToken.access_token, - 'Content-Type': 'application/json; UTF-8', - 'Accept-Encoding': 'gzip', - 'If-Match': '*', - }, - }; - const request = https.request(options, (resp) => {}); - request.write(JSON.stringify({ version: { description: testId } })); - request.end(); - }), - // A storage upload to trigger the Storage tests - admin - .storage() - .bucket() - .upload('/tmp/' + testId + '.txt'), - testLab.startTestRun(firebaseConfig.projectId, testId), - // Invoke the schedule for our scheduled function to fire - callScheduleTrigger('schedule', 'us-central1'), - ]) - .then(() => { - // On test completion, check that all tests pass and reply "PASS", or provide further details. - console.log('Waiting for all tests to report they pass...'); - const ref = admin.database().ref(`testRuns/${testId}`); - return new Promise((resolve, reject) => { - let testsExecuted = 0; - ref.on('child_added', (snapshot) => { - testsExecuted += 1; - if (snapshot.key != 'timestamp' && !snapshot.val().passed) { - reject( - new Error( - `test ${snapshot.key} failed; see database for details.` - ) - ); - return; - } - console.log( - `${snapshot.key} passed (${testsExecuted} of ${numTests})` - ); - if (testsExecuted < numTests) { - // Not all tests have completed. Wait longer. - return; - } - // All tests have passed! - resolve(); - }); - }) - .then(() => { - ref.off(); // No more need to listen. - return Promise.resolve(); + try { + await Promise.all([ + // A database write to trigger the Firebase Realtime Database tests. + admin + .database() + .ref(`dbTests/${testId}/start`) + .set({ '.sv': 'timestamp' }), + // A Pub/Sub publish to trigger the Cloud Pub/Sub tests. + new PubSub() + .topic('pubsubTests') + .publish(Buffer.from(JSON.stringify({ testId }))), + // A user creation to trigger the Firebase Auth user creation tests. + admin + .auth() + .createUser({ + email: `${testId}@fake.com`, + password: 'secret', + displayName: `${testId}`, }) - .catch((err) => { - ref.off(); // No more need to listen. - return Promise.reject(err); - }); - }) - .then(() => { - console.log('All tests pass!'); - resp.status(200).send('PASS \n'); - }) - .catch((err) => { - console.log(`Some tests failed: ${err}`); - resp - .status(500) - .send( - `FAIL - details at https://${process.env.GCLOUD_PROJECT}.firebaseio.com/testRuns/${testId}` + .then((userRecord) => { + // A user deletion to trigger the Firebase Auth user deletion tests. + admin.auth().deleteUser(userRecord.uid); + }), + // A firestore write to trigger the Cloud Firestore tests. + admin + .firestore() + .collection('tests') + .doc(testId) + .set({ test: testId }), + // Invoke a callable HTTPS trigger. + callHttpsTrigger('callableTests', { foo: 'bar', testId }, baseUrl), + // A Remote Config update to trigger the Remote Config tests. + admin.credential + .applicationDefault() + .getAccessToken() + .then((accessToken) => { + const options = { + hostname: 'firebaseremoteconfig.googleapis.com', + path: `/v1/projects/${firebaseConfig.projectId}/remoteConfig`, + method: 'PUT', + headers: { + Authorization: 'Bearer ' + accessToken.access_token, + 'Content-Type': 'application/json; UTF-8', + 'Accept-Encoding': 'gzip', + 'If-Match': '*', + }, + }; + const request = https.request(options, (resp) => {}); + request.write(JSON.stringify({ version: { description: testId } })); + request.end(); + }), + // A storage upload to trigger the Storage tests + admin + .storage() + .bucket() + .upload('/tmp/' + testId + '.txt'), + testLab.startTestRun(firebaseConfig.projectId, testId), + // Invoke the schedule for our scheduled function to fire + callScheduleTrigger('schedule', 'us-central1'), + ]); + + // On test completion, check that all tests pass and reply "PASS", or provide further details. + console.log('Waiting for all tests to report they pass...'); + await new Promise((resolve, reject) => { + setTimeout(() => reject(new Error('Timeout')), 5 * 60 * 1000); + let testsExecuted = 0; + testIdRef.on('child_added', (snapshot) => { + testsExecuted += 1; + if (snapshot.key != 'timestamp' && !snapshot.val().passed) { + reject( + new Error( + `test ${snapshot.key} failed; see database for details.` + ) + ); + return; + } + console.log( + `${snapshot.key} passed (${testsExecuted} of ${numTests})` ); + if (testsExecuted < numTests) { + // Not all tests have completed. Wait longer. + return; + } + // All tests have passed! + resolve(); + }); }); + console.log('All tests pass!'); + resp.status(200).send('PASS \n'); + } catch (err) { + console.log(`Some tests failed: ${err}`); + resp + .status(500) + .send( + `FAIL - details at ${functions.firebaseConfig() + .databaseURL!}/testRuns/${testId}` + ); + } finally { + testIdRef.off('child_added'); + } }); diff --git a/integration_test/functions/src/pubsub-tests.ts b/integration_test/functions/src/pubsub-tests.ts index e2a3f1bd0..a21c2011a 100644 --- a/integration_test/functions/src/pubsub-tests.ts +++ b/integration_test/functions/src/pubsub-tests.ts @@ -66,20 +66,15 @@ export const schedule: any = functions .region(REGION) .pubsub.schedule('every 10 hours') // This is a dummy schedule, since we need to put a valid one in. // For the test, the job is triggered by the jobs:run api - .onRun((context) => { - let testId; + .onRun(async (context) => { const db = admin.database(); - return new Promise(async (resolve, reject) => { - await db - .ref('testRuns') - .orderByChild('timestamp') - .limitToLast(1) - .on('value', (snap) => { - testId = Object.keys(snap.val())[0]; - new TestSuite('pubsub scheduleOnRun') - .it('should trigger when the scheduler fires', () => success()) - .run(testId, null); - }); - resolve(); - }); + const snap = await db + .ref('testRuns') + .orderByChild('timestamp') + .limitToLast(1) + .once('value'); + const testId = Object.keys(snap.val())[0]; + return new TestSuite('pubsub scheduleOnRun') + .it('should trigger when the scheduler fires', () => success()) + .run(testId, null); }); diff --git a/integration_test/functions/src/testLab-tests.ts b/integration_test/functions/src/testLab-tests.ts index cf4b2f062..586c3a0cb 100644 --- a/integration_test/functions/src/testLab-tests.ts +++ b/integration_test/functions/src/testLab-tests.ts @@ -1,5 +1,4 @@ import * as functions from 'firebase-functions'; -import * as _ from 'lodash'; import { TestSuite, expectEq } from './testing'; import TestMatrix = functions.testLab.TestMatrix; const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; @@ -24,5 +23,5 @@ export const testLabTests: any = functions expectEq(matrix.state, 'INVALID') ) - .run(_.get(matrix, 'clientInfo.details.testId'), matrix, context); + .run(matrix?.clientInfo?.details?.testId, matrix, context); }); diff --git a/integration_test/functions/src/testLab-utils.ts b/integration_test/functions/src/testLab-utils.ts index 2f457e70d..6f86b0a1f 100644 --- a/integration_test/functions/src/testLab-utils.ts +++ b/integration_test/functions/src/testLab-utils.ts @@ -1,7 +1,6 @@ import * as http from 'http'; import * as https from 'https'; import * as admin from 'firebase-admin'; -import * as _ from 'lodash'; import * as utils from './test-utils'; interface AndroidDevice { @@ -35,7 +34,7 @@ async function fetchDefaultDevice( requestOptions(accessToken, 'GET', '/v1/testEnvironmentCatalog/ANDROID') ); const data = JSON.parse(response); - const models = _.get(data, 'androidDeviceCatalog.models', []); + const models = data?.androidDeviceCatalog?.models || []; const defaultModels = models.filter( (m) => m.tags !== undefined && diff --git a/integration_test/functions/src/testing.ts b/integration_test/functions/src/testing.ts index 43d3e69d5..228203d31 100644 --- a/integration_test/functions/src/testing.ts +++ b/integration_test/functions/src/testing.ts @@ -1,6 +1,5 @@ import * as firebase from 'firebase-admin'; import { EventContext } from 'firebase-functions'; -import * as _ from 'lodash'; export type TestCase = (data: T, context?: EventContext) => any; export interface TestCaseMap { @@ -66,43 +65,70 @@ function failure(reason: string) { return Promise.reject(reason); } -export function evaluate(value, errMsg) { +export function evaluate(value: boolean, errMsg: string) { if (value) { return success(); } return failure(errMsg); } -export function expectEq(left, right) { +export function expectEq(left: any, right: any) { return evaluate( - left === right, + left == right, JSON.stringify(left) + ' does not equal ' + JSON.stringify(right) ); } -export function expectDeepEq(left, right) { +function deepEq(left: any, right: any) { + if (left === right) { + return true; + } + + if (!(left instanceof Object && right instanceof Object)) { + return false; + } + + if (Object.keys(left).length != Object.keys(right).length) { + return false; + } + + for (let key in left) { + if (!right.hasOwnProperty(key)) { + return false; + } + if (!deepEq(left[key], right[key])) { + return false; + } + } + + return true; +} + +export function expectDeepEq(left: any, right: any) { return evaluate( - _.isEqual(left, right), - JSON.stringify(left) + ' does not equal ' + JSON.stringify(right) + deepEq(left, right), + `${JSON.stringify(left)} does not deep equal ${JSON.stringify(right)}` ); } -export function expectMatches(input: string, regexp) { +export function expectMatches(input: string, regexp: RegExp) { return evaluate( - input.match(regexp), + input.match(regexp) !== null, "Input '" + input + "' did not match regexp '" + regexp + "'" ); } -export function expectReject(f) { - return function(event) { - return Promise.resolve() - .then(() => f(event)) - .then( - () => { - throw new Error('Test should have returned a rejected promise'); - }, - () => true // A rejection is what we expected, and so is a positive result. - ); +export function expectReject(f: (e: EventType) => Promise) { + return async (event: EventType) => { + let rejected = false; + try { + await f(event); + } catch { + rejected = true; + } + + if (!rejected) { + throw new Error('Test should have returned a rejected promise'); + } }; } diff --git a/integration_test/package.node8.json b/integration_test/package.json.template similarity index 55% rename from integration_test/package.node8.json rename to integration_test/package.json.template index 8803f2fdc..5bcd8761b 100644 --- a/integration_test/package.node8.json +++ b/integration_test/package.json.template @@ -5,19 +5,17 @@ "build": "./node_modules/.bin/tsc" }, "dependencies": { - "@google-cloud/pubsub": "~0.19.0", - "@types/google-cloud__pubsub": "^0.18.0", - "@types/lodash": "~4.14.41", - "firebase-admin": "^8.0.0", - "firebase-functions": "./firebase-functions.tgz", + "@google-cloud/pubsub": "^2.10.0", + "firebase-admin": "^9.1.0", + "firebase-functions": "__SDK_TARBALL__", "lodash": "~4.17.2" }, "main": "lib/index.js", "devDependencies": { - "typescript": "~3.6.0" + "typescript": "~4.2.2" }, "engines": { - "node": "8" + "node": "__NODE_VERSION__" }, "private": true } diff --git a/integration_test/package.node10.json b/integration_test/package.node10.json deleted file mode 100644 index cf5eeeb25..000000000 --- a/integration_test/package.node10.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "functions", - "description": "Integration test for the Firebase SDK for Google Cloud Functions", - "scripts": { - "build": "./node_modules/.bin/tsc" - }, - "dependencies": { - "@google-cloud/pubsub": "~0.19.0", - "@types/google-cloud__pubsub": "^0.18.0", - "@types/lodash": "~4.14.41", - "firebase-admin": "^8.0.0", - "firebase-functions": "./firebase-functions.tgz", - "lodash": "~4.17.2" - }, - "main": "lib/index.js", - "devDependencies": { - "typescript": "~3.6.0" - }, - "engines": { - "node": "10" - }, - "private": true -} diff --git a/integration_test/run_tests.sh b/integration_test/run_tests.sh index ba567536a..100e24a32 100755 --- a/integration_test/run_tests.sh +++ b/integration_test/run_tests.sh @@ -58,21 +58,16 @@ function unset_region { fi } -function pick_node8 { +# Creates a Package.json from package.json.template +# @param timestmap of the current SDK build +# @param Node version to test under +function create_package_json { cd "${DIR}" - cp package.node8.json functions/package.json + cp package.json.template functions/package.json # we have to do the -e flag here so that it work both on linux and mac os, but that creates an extra # backup file called package.json-e that we should clean up afterwards. - sed -i -e "s/firebase-functions.tgz/firebase-functions-${TIMESTAMP}.tgz/g" functions/package.json - rm -f functions/package.json-e -} - -function pick_node10 { - cd "${DIR}" - cp package.node10.json functions/package.json - # we have to do the -e flag here so that it work both on linux and mac os, but that creates an extra - # backup file called package.json-e that we should clean up afterwards. - sed -i -e "s/firebase-functions.tgz/firebase-functions-${TIMESTAMP}.tgz/g" functions/package.json + sed -i -e "s/__SDK_TARBALL__/firebase-functions-$1.tgz/g" functions/package.json + sed -i -e "s/__NODE_VERSION__/$2/g" functions/package.json rm -f functions/package.json-e } @@ -143,19 +138,13 @@ build_sdk delete_all_functions set_region -# Node 8 tests -pick_node8 -install_deps -announce "Deploying functions to Node 8 runtime ..." -deploy -run_tests - -# Node 10 tests -pick_node10 -install_deps -announce "Re-deploying the same functions to Node 10 runtime ..." -deploy -run_tests +for version in 10 12 14; do + create_package_json $TIMESTAMP $version + install_deps + announce "Re-deploying the same functions to Node $version runtime ..." + deploy + run_tests +done # Cleanup cleanup From d7d455a40f1a0f3a25de08e85175e27ee7e17f7c Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Fri, 5 Mar 2021 13:30:52 -0800 Subject: [PATCH 293/705] Stop running Node 8 tests in GitHub (#870) --- .github/workflows/test.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9a31678c2..c56d65e12 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,7 +13,6 @@ jobs: strategy: matrix: node-version: - - 8.x - 10.x - 12.x - 14.x From 92916aa42b8b8e85fe7416ee0795744206ef9afa Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Wed, 17 Mar 2021 10:05:33 -0700 Subject: [PATCH 294/705] Add tests to verify logger does not alter its parameters (#873) --- spec/logger.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/logger.spec.ts b/spec/logger.spec.ts index dda16087c..48258ffac 100644 --- a/spec/logger.spec.ts +++ b/spec/logger.spec.ts @@ -109,6 +109,22 @@ describe(`logger (${ }); }); + it('should not alter parameters that are logged', () => { + const circ: any = { b: 'foo' }; + circ.array = [circ]; + circ.object = circ; + const entry: logger.LogEntry = { + severity: 'ERROR', + message: 'testing circular', + circ, + }; + logger.write(entry); + + expect(circ.array[0].b).to.equal('foo'); + expect(circ.object.b).to.equal('foo'); + expect(circ.object.array[0].object.array[0].b).to.equal('foo'); + }); + for (const severity of ['DEBUG', 'INFO', 'NOTICE']) { it(`should output ${severity} severity to stdout`, () => { let entry: logger.LogEntry = { From a4019540dc0ced5abadb0a3c5ddaf21f7881f79e Mon Sep 17 00:00:00 2001 From: joehan Date: Fri, 19 Mar 2021 10:43:32 -0700 Subject: [PATCH 295/705] Remove crashlytics from toc.yaml, and dont try to delete css.map files if they dont exist (#871) --- docgen/content-sources/toc.yaml | 12 ------------ docgen/generate-docs.js | 4 +++- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/docgen/content-sources/toc.yaml b/docgen/content-sources/toc.yaml index 5758eb74e..e970fc80c 100644 --- a/docgen/content-sources/toc.yaml +++ b/docgen/content-sources/toc.yaml @@ -69,18 +69,6 @@ toc: - title: 'UserRecord' path: /docs/reference/functions/providers_auth_.html#userrecord - - title: 'functions.crashlytics' - path: /docs/reference/functions/providers_crashlytics_.html - section: - - title: 'Issue' - path: /docs/reference/functions/providers_crashlytics_.issue.html - - title: 'IssueBuilder' - path: /docs/reference/functions/providers_crashlytics_.issuebuilder.html - - title: 'AppInfo' - path: /docs/reference/functions/providers_crashlytics_.appinfo.html - - title: 'VelocityAlert' - path: /docs/reference/functions/providers_crashlytics_.velocityalert.html - - title: 'functions.firestore' path: /docs/reference/functions/providers_firestore_.html section: diff --git a/docgen/generate-docs.js b/docgen/generate-docs.js index 01275ae2d..1ce7ff910 100644 --- a/docgen/generate-docs.js +++ b/docgen/generate-docs.js @@ -323,7 +323,9 @@ Promise.all([ // Clean up temp home markdown file. (Nothing needs to wait for this.) fs.unlink(tempHomePath); // Devsite doesn't like css.map files. - return fs.unlink(`${docPath}/assets/css/main.css.map`); + if (fs.existsSync(`${docPath}/assets/css/main.css.map`)) { + return fs.unlink(`${docPath}/assets/css/main.css.map`); + } }) // Write out TOC file. Do this after Typedoc step to prevent Typedoc // erroring when it finds an unexpected file in the target dir. From 98bf467aa436f7e27969a8ad3887abe0345f66d3 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Tue, 23 Mar 2021 10:34:24 -0700 Subject: [PATCH 296/705] Update generate-docs to modern JS (#874) Update generate-docs to modern JS. Fixes a few places where promises were left dangling. --- docgen/generate-docs.js | 313 +++++++++++++++++++--------------------- 1 file changed, 145 insertions(+), 168 deletions(-) diff --git a/docgen/generate-docs.js b/docgen/generate-docs.js index 1ce7ff910..d36edb7e8 100644 --- a/docgen/generate-docs.js +++ b/docgen/generate-docs.js @@ -20,7 +20,6 @@ const fs = require('mz/fs'); const path = require('path'); const yargs = require('yargs'); const yaml = require('js-yaml'); -const _ = require('lodash'); const repoPath = path.resolve(`${__dirname}/..`); @@ -72,59 +71,52 @@ function runTypedoc() { * Moves files from subdir to root. * @param {string} subdir Subdir to move files out of. */ -function moveFilesToRoot(subdir) { - return exec(`mv ${docPath}/${subdir}/* ${docPath}`) - .then(() => { - exec(`rmdir ${docPath}/${subdir}`); - }) - .catch(e => console.error(e)); +async function moveFilesToRoot(subdir) { + await exec(`mv ${docPath}/${subdir}/* ${docPath}`) + await exec(`rmdir ${docPath}/${subdir}`); } /** * Renames files to remove the leading underscores. * We need to do this because devsite hides these files. - * Example: + * Example: * _cloud_functions_.resource.html => cloud_functions_.resource.html */ -function renameFiles() { - return fs.readdir(docPath).then(files => { - console.log(files); - files.forEach(file => { - let newFileName = file; - if (_.startsWith(file, "_") && _.endsWith(file, "html")) { - newFileName = _.trimStart(file, "_"); - fs.rename(`${docPath}/${file}`, `${docPath}/${newFileName}`, (err) => { - if (err) console.log(err) - }); - } - }) - }) +async function renameFiles() { + const files = await fs.readdir(docPath); + const renames = []; + for (const file of files) { + if (file.startsWith("_") && file.endsWith("html")) { + let newFileName = file.substring(1); + renames.push(fs.rename(`${docPath}/${file}`, `${docPath}/${newFileName}`)); + } + } + await Promise.all(renames); } /** * Reformat links to match flat structure. * @param {string} file File to fix links in. */ -function fixLinks(file) { - return fs.readFile(file, 'utf8').then(data => { - data = addTypeAliasLinks(data); - const flattenedLinks = data - .replace(/\.\.\//g, '') - .replace(/(modules|interfaces|classes)\//g, '') - .replace(/\"_/g, '"'); - let caseFixedLinks = flattenedLinks; - for (const lower in lowerToUpperLookup) { - const re = new RegExp(lower, 'g'); - caseFixedLinks = caseFixedLinks.replace(re, lowerToUpperLookup[lower]); - } - return fs.writeFile(file, caseFixedLinks); - }); +async function fixLinks(file) { + let data = await fs.readFile(file, 'utf8'); + data = addTypeAliasLinks(data); + const flattenedLinks = data + .replace(/\.\.\//g, '') + .replace(/(modules|interfaces|classes)\//g, '') + .replace(/\"_/g, '"'); + let caseFixedLinks = flattenedLinks; + for (const lower in lowerToUpperLookup) { + const re = new RegExp(lower, 'g'); + caseFixedLinks = caseFixedLinks.replace(re, lowerToUpperLookup[lower]); + } + return fs.writeFile(file, caseFixedLinks); } -/** +/** * Adds links to external documentation for type aliases that * reference an external library. - * + * * @param data File data to add external library links to. */ function addTypeAliasLinks(data) { @@ -136,22 +128,20 @@ function addTypeAliasLinks(data) { const fileTags = htmlDom.window.document.querySelectorAll(".tsd-signature-type"); fileTags.forEach(tag => { const mapping = typeMap[tag.textContent]; - if (mapping) { - console.log('Adding link to '+tag.textContent+" documentation."); - - // Add the corresponding document link to this type - const linkChild = htmlDom.window.document.createElement('a'); - linkChild.setAttribute('href', mapping); - linkChild.textContent = tag.textContent; - tag.textContent = null; - tag.appendChild(linkChild); - } - }); + if (mapping) { + console.log('Adding link to '+tag.textContent+" documentation."); + + // Add the corresponding document link to this type + const linkChild = htmlDom.window.document.createElement('a'); + linkChild.setAttribute('href', mapping); + linkChild.textContent = tag.textContent; + tag.textContent = null; + tag.appendChild(linkChild); + } + }); return htmlDom.serialize(); } -let tocText = ''; - /** * Generates temporary markdown file that will be sourced by Typedoc to * create index.html. @@ -182,37 +172,33 @@ const lowerToUpperLookup = {}; * Checks to see if any files listed in toc.yaml were not generated. * If files exist, fixes filename case to match toc.yaml version. */ -function checkForMissingFilesAndFixFilenameCase() { +async function checkForMissingFilesAndFixFilenameCase(tocText) { // Get filenames from toc.yaml. const filenames = tocText .split('\n') .filter(line => line.includes('path:')) .map(line => line.split(devsitePath)[1]); // Logs warning to console if a file from TOC is not found. - // console.log(filenames); - const fileCheckPromises = filenames.map(filename => { + const fileCheckPromises = filenames.map(async filename => { // Warns if file does not exist, fixes filename case if it does. // Preferred filename for devsite should be capitalized and taken from // toc.yaml. const tocFilePath = `${docPath}/${filename}`; // Generated filename from Typedoc will be lowercase. const generatedFilePath = `${docPath}/${filename.toLowerCase()}`; - return fs.exists(generatedFilePath).then(exists => { - if (exists) { - // Store in a lookup table for link fixing. - lowerToUpperLookup[ - `${filename.toLowerCase()}` - ] = `${filename}`; - return fs.rename(generatedFilePath, tocFilePath); - } else { - console.warn( - `Missing file: ${filename} requested ` + - `in toc.yaml but not found in ${docPath}` - ); - } - }); + if (await fs.exists(generatedFilePath)) { + // Store in a lookup table for link fixing. + lowerToUpperLookup[filename.toLowerCase()] = filename; + return fs.rename(generatedFilePath, tocFilePath); + } else { + console.warn( + `Missing file: ${filename} requested ` + + `in toc.yaml but not found in ${docPath}` + ); + } }); - return Promise.all(fileCheckPromises).then(() => filenames); + await Promise.all(fileCheckPromises); + return filenames; } /** @@ -223,40 +209,31 @@ function checkForMissingFilesAndFixFilenameCase() { * @param {Array} filenamesFromToc Filenames pulled from toc.yaml * @param {boolean} shouldRemove Should just remove the file */ -function checkForUnlistedFiles(filenamesFromToc, shouldRemove) { - return fs.readdir(docPath).then(files => { - const htmlFiles = files - .filter(filename => filename.slice(-4) === 'html'); - const removePromises = []; - htmlFiles.forEach(filename => { - if ( - !filenamesFromToc.includes(filename) && - filename !== 'index' && - filename !== 'globals' - ) { - if (shouldRemove) { - console.log( - `REMOVING ${docPath}/${filename} - not listed in toc.yaml.` - ); - removePromises.push(fs.unlink(`${docPath}/${filename}`)); - } else { - // This is just a warning, it doesn't need to finish before - // the process continues. - console.warn( - `Unlisted file: ${filename} generated ` + - `but not listed in toc.yaml.` - ); - } - } - }); - if (shouldRemove) { - return Promise.all(removePromises).then(() => - htmlFiles.filter(filename => filenamesFromToc.includes(filename)) - ); - } else { - return htmlFiles; - } - }); +async function checkForUnlistedFiles(filenamesFromToc, shouldRemove) { + const files = await fs.readdir(docPath); + const htmlFiles = files + .filter(filename => filename.slice(-4) === 'html'); + const removePromises = []; + const filesToRemove = htmlFiles + .filter(filename => !filenamesFromToc.includes(filename)) + .filter(filename => filename !== 'index' && filename != 'globals'); + if (filesToRemove.length && !shouldRemove) { + // This is just a warning, it doesn't need to finish before + // the process continues. + console.warn( + `Unlisted files: ${filesToRemove.join(", ")} generated ` + + `but not listed in toc.yaml.` + ); + return htmlFiles; + } + + await Promise.all(filesToRemove.map(filename => { + console.log( + `REMOVING ${docPath}/${filename} - not listed in toc.yaml.` + ); + return fs.unlink(`${docPath}/${filename})`); + })); + return htmlFiles.filter(filename => filenamesFromToc.includes(filename)) } /** @@ -265,7 +242,7 @@ function checkForUnlistedFiles(filenamesFromToc, shouldRemove) { * * @param {Array} htmlFiles List of html files found in generated dir. */ -function writeGeneratedFileList(htmlFiles) { +async function writeGeneratedFileList(htmlFiles) { const fileList = htmlFiles.map(filename => { return { title: filename, @@ -273,9 +250,8 @@ function writeGeneratedFileList(htmlFiles) { }; }); const generatedTocYAML = yaml.safeDump({ toc: fileList }); - return fs - .writeFile(`${docPath}/_toc_autogenerated.yaml`, generatedTocYAML) - .then(() => htmlFiles); + await fs.writeFile(`${docPath}/_toc_autogenerated.yaml`, generatedTocYAML); + return htmlFiles; } /** @@ -305,71 +281,72 @@ function fixAllLinks(htmlFiles) { * links as needed. * 5) Check for mismatches between TOC list and generated file list. */ -Promise.all([ - fs.readFile(`${contentPath}/toc.yaml`, 'utf8'), - fs.readFile(`${contentPath}/HOME.md`, 'utf8') -]) - // Read TOC and homepage text and assemble a homepage markdown file. - // This file will be sourced by Typedoc to generate index.html. - .then(([tocRaw, homeRaw]) => { - tocText = tocRaw; - return generateTempHomeMdFile(tocRaw, homeRaw); - }) - // Run main Typedoc process (uses index.d.ts and generated temp file above). - .then(runTypedoc) - .then(output => { +(async function() { + try { + const [tocRaw, homeRaw] = await Promise.all([ + fs.readFile(`${contentPath}/toc.yaml`, 'utf8'), + fs.readFile(`${contentPath}/HOME.md`, 'utf8') + ]) + + // Run main Typedoc process (uses index.d.ts and generated temp file above). + await generateTempHomeMdFile(tocRaw, homeRaw); + const output = await runTypedoc(); + // Typedoc output. console.log(output.stdout); - // Clean up temp home markdown file. (Nothing needs to wait for this.) - fs.unlink(tempHomePath); - // Devsite doesn't like css.map files. - if (fs.existsSync(`${docPath}/assets/css/main.css.map`)) { - return fs.unlink(`${docPath}/assets/css/main.css.map`); - } - }) - // Write out TOC file. Do this after Typedoc step to prevent Typedoc - // erroring when it finds an unexpected file in the target dir. - .then(() => fs.writeFile(`${docPath}/_toc.yaml`, tocText)) - // Flatten file structure. These categories don't matter to us and it makes - // it easier to manage the docs directory. - .then(() => { - return Promise.all([ + await Promise.all([ + // Clean up temp home markdown file. (Nothing needs to wait for this.) + fs.unlink(tempHomePath), + + // Devsite doesn't like css.map files. + // NOTE: This doesn't seem to actually get generated anymore, but we'll keep this here just in case. + async () => { + const cssMap = `${docPath}/assets/css/main.css.map`; + if (await fs.exists(cssMap)) { + await fs.unlink(); + } + }, + + // Write out TOC file. Do this after Typedoc step to prevent Typedoc + // erroring when it finds an unexpected file in the target dir. + fs.writeFile(`${docPath}/_toc.yaml`, tocRaw), + ]); + + // Flatten file structure. These categories don't matter to us and it makes + // it easier to manage the docs directory. + await Promise.all([ moveFilesToRoot('classes'), moveFilesToRoot('modules'), moveFilesToRoot('interfaces'), ]); - }) - // Rename files to remove the underscores since devsite hides those. - .then(renameFiles) - // Check for files listed in TOC that are missing and warn if so. - // Not blocking. - .then(checkForMissingFilesAndFixFilenameCase) - // Check for files that exist but aren't listed in the TOC and warn. - // (If API is node, actually remove the file.) - // Removal is blocking, warnings aren't. - .then(filenamesFromToc => - checkForUnlistedFiles(filenamesFromToc, false) - ) - // Write a _toc_autogenerated.yaml to record what files were created. - .then(htmlFiles => writeGeneratedFileList(htmlFiles)) - // Correct the links in all the generated html files now that files have - // all been moved to top level. - // .then(removeUnderscores) - .then(fixAllLinks) - .then(() => { - fs.readFile(`${docPath}/index.html`, 'utf8').then(data => { - // String to include devsite local variables. - const localVariablesIncludeString = `{% include "docs/web/_local_variables.html" %}\n`; - return fs.writeFile( - `${docPath}/index.html`, - localVariablesIncludeString + data - ); - }); - }) - .catch(e => { - if (e.stdout) { - console.error(e.stdout); - } else { - console.error(e); - } - }); + // Rename files to remove the underscores since devsite hides those. + await renameFiles(); + + // Check for files listed in TOC that are missing and warn if so. + // Not blocking. + const filenamesFromToc = await checkForMissingFilesAndFixFilenameCase(tocRaw); + + // Check for files that exist but aren't listed in the TOC and warn. + // (If API is node, actually remove the file.) + // Removal is blocking, warnings aren't. + const htmlFiles = await checkForUnlistedFiles(filenamesFromToc, false); + + // Write a _toc_autogenerated.yaml to record what files were created. + const fileList = await writeGeneratedFileList(htmlFiles); + + // Correct the links in all the generated html files now that files have + // all been moved to top level. + await fixAllLinks(fileList); + const data = await fs.readFile(`${docPath}/index.html`, 'utf8'); + // String to include devsite local variables. + const localVariablesIncludeString = `{% include "docs/web/_local_variables.html" %}\n`; + await fs.writeFile(`${docPath}/index.html`, localVariablesIncludeString + data); + } catch (err) { + if (err.stdout) { + console.error(err.stdout); + } else { + console.error(err); + } +} +})(); + From 29a55af955450d7758ddaa0cf3871836a2849c13 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Tue, 4 May 2021 18:46:29 -0700 Subject: [PATCH 297/705] Updating runWith enums (#884) Updating runWith enums --- CHANGELOG.md | 3 +++ src/cloud-functions.ts | 9 +++++++-- src/function-configuration.ts | 8 ++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..adc2430a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +- Functions may now be deployed with 8GB RAM +- Functions may now be deployed to europe-central2 (Warsaw) +- Functions may now reserve a minimum number of instances (updated CLI required) diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 272920c20..500e27144 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -203,7 +203,7 @@ export namespace Change { json: ChangeJson, customizer: (x: any) => T = reinterpretCast ): Change { - let before = _.assign({}, json.before); + let before = { ...json.before }; if (json.fieldMask) { before = applyFieldMask(before, json.after, json.fieldMask); } @@ -220,7 +220,7 @@ export namespace Change { after: any, fieldMask: string ) { - const before = _.assign({}, after); + const before = { ...after }; const masks = fieldMask.split(','); masks.forEach((mask) => { @@ -506,6 +506,7 @@ export function optionsToTrigger(options: DeploymentOptions) { '1GB': 1024, '2GB': 2048, '4GB': 4096, + '8GB': 8192, }; trigger.availableMemoryMb = _.get(memoryLookup, options.memory); } @@ -513,6 +514,10 @@ export function optionsToTrigger(options: DeploymentOptions) { trigger.schedule = options.schedule; } + if (options.minInstances) { + trigger.minInstances = options.minInstances; + } + if (options.maxInstances) { trigger.maxInstances = options.maxInstances; } diff --git a/src/function-configuration.ts b/src/function-configuration.ts index f5a325b41..8b2b28b81 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -8,6 +8,7 @@ export const SUPPORTED_REGIONS = [ 'us-west2', 'us-west3', 'us-west4', + 'europe-central2', 'europe-west1', 'europe-west2', 'europe-west3', @@ -43,6 +44,7 @@ export const VALID_MEMORY_OPTIONS = [ '1GB', '2GB', '4GB', + '8GB', ] as const; /** @@ -107,6 +109,12 @@ export interface RuntimeOptions { */ timeoutSeconds?: number; + /** + * Min number of actual instances allowed to be running in parallel + * Instances will be billed while idle. + */ + minInstances?: number; + /** * Max number of actual instances allowed to be running in parallel */ From 09120b64a931d41fc500f2c88e3cff0a5a72bee2 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Tue, 11 May 2021 20:16:37 -0700 Subject: [PATCH 298/705] Redactions (#886) --- CHANGELOG.md | 1 - src/function-configuration.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adc2430a1..d952d8984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,2 @@ - Functions may now be deployed with 8GB RAM - Functions may now be deployed to europe-central2 (Warsaw) -- Functions may now reserve a minimum number of instances (updated CLI required) diff --git a/src/function-configuration.ts b/src/function-configuration.ts index 8b2b28b81..2d9925ac5 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -112,6 +112,7 @@ export interface RuntimeOptions { /** * Min number of actual instances allowed to be running in parallel * Instances will be billed while idle. + * @hidden */ minInstances?: number; From c29a8a518aba91aabe9bd02dd6d3a72718033ae7 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 12 May 2021 09:49:52 -0700 Subject: [PATCH 299/705] Support verification of AppCheck token in Callable Functions (#885) Callable Functions will now verify the AppCheck token included in the X-Firebase-AppCheck request header. Similar to auth, Callable Function will return 401 Unauthorized if the AppCheck token is invalid. --- CHANGELOG.md | 1 + package-lock.json | 1197 +++++++++++++++-------------- package.json | 3 +- spec/fixtures/credential/jwk.json | 14 + spec/fixtures/mockrequest.ts | 51 +- spec/providers/https.spec.ts | 54 ++ src/providers/https.ts | 134 +++- tsconfig.release.json | 4 +- 8 files changed, 851 insertions(+), 607 deletions(-) create mode 100644 spec/fixtures/credential/jwk.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d952d8984..4ee96fbef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ - Functions may now be deployed with 8GB RAM - Functions may now be deployed to europe-central2 (Warsaw) +- Add support for validating AppCheck tokens for Callable Functions diff --git a/package-lock.json b/package-lock.json index f1f20b89a..8a4fd6be8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,49 +31,65 @@ } }, "@firebase/app-types": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz", - "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.2.tgz", + "integrity": "sha512-2VXvq/K+n8XMdM4L2xy5bYp2ZXMawJXluUIDzUBvMthVR+lhxK4pfFiqr1mmDbv9ydXvEAuFsD+6DpcZuJcSSw==", "dev": true }, "@firebase/auth-interop-types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz", - "integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz", + "integrity": "sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==", "dev": true }, "@firebase/component": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", - "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.0.tgz", + "integrity": "sha512-v18csWtXb0ri+3m7wuGLY/UDgcb89vuMlZGQ//+7jEPLIQeLbylvZhol1uzW9WzoOpxMxOS2W5qyVGX36wZvEA==", "dev": true, "requires": { - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" + "@firebase/util": "1.1.0", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } } }, "@firebase/database": { - "version": "0.6.12", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.12.tgz", - "integrity": "sha512-OLUxp8TkXiML4X5LWM5IACsSDvo3fcf4mTbTe5RF+N6TRFv0Svzlet5OgGIa3ET1dQvNiisrMX7zzRa0OTLs7Q==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.10.0.tgz", + "integrity": "sha512-GsHvuES83Edtboij2h3txKg+yV/TD4b5Owc01SgXEQtvj1lulkHt4Ufmd9OZz1WreWQJMIqKpbVowIDHjlkZJQ==", "dev": true, "requires": { - "@firebase/auth-interop-types": "0.1.5", - "@firebase/component": "0.1.19", - "@firebase/database-types": "0.5.2", + "@firebase/auth-interop-types": "0.1.6", + "@firebase/component": "0.5.0", + "@firebase/database-types": "0.7.2", "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.2", + "@firebase/util": "1.1.0", "faye-websocket": "0.11.3", - "tslib": "^1.11.1" + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } } }, "@firebase/database-types": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.5.2.tgz", - "integrity": "sha512-ap2WQOS3LKmGuVFKUghFft7RxXTyZTDr0Xd8y2aqmWsbJVjgozi0huL/EUMgTjGFrATAjcf2A7aNs8AKKZ2a8g==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.7.2.tgz", + "integrity": "sha512-cdAd/dgwvC0r3oLEDUR+ULs1vBsEvy0b27nlzKhU6LQgm9fCDzgaH9nFGv8x+S9dly4B0egAXkONkVoWcOAisg==", "dev": true, "requires": { - "@firebase/app-types": "0.6.1" + "@firebase/app-types": "0.6.2" } }, "@firebase/logger": { @@ -83,50 +99,57 @@ "dev": true }, "@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.1.0.tgz", + "integrity": "sha512-lfuSASuPKNdfebuFR8rjFamMQUPH9iiZHcKS755Rkm/5gRT0qC7BMhCh3ZkHf7NVbplzIc/GhmX2jM+igDRCag==", "dev": true, "requires": { - "tslib": "^1.11.1" + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } } }, "@google-cloud/common": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.4.0.tgz", - "integrity": "sha512-zWFjBS35eI9leAHhjfeOYlK5Plcuj/77EzstnrJIZbKgF/nkqjcQuGiMCpzCwOfPyUbz8ZaEOYgbHa759AKbjg==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-3.6.0.tgz", + "integrity": "sha512-aHIFTqJZmeTNO9md8XxV+ywuvXF3xBm5WNmgWeeCK+XN5X+kGW0WEX94wGwj+/MdOnrVf4dL2RvSIt9J5yJG6Q==", "dev": true, "optional": true, "requires": { - "@google-cloud/projectify": "^1.0.0", - "@google-cloud/promisify": "^1.0.0", - "arrify": "^2.0.0", - "duplexify": "^3.6.0", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "arrify": "^2.0.1", + "duplexify": "^4.1.1", "ent": "^2.2.0", "extend": "^3.0.2", - "google-auth-library": "^5.5.0", - "retry-request": "^4.0.0", - "teeny-request": "^6.0.0" + "google-auth-library": "^7.0.2", + "retry-request": "^4.1.1", + "teeny-request": "^7.0.0" } }, "@google-cloud/firestore": { - "version": "3.8.6", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-3.8.6.tgz", - "integrity": "sha512-ox80NbrM1MLJgvAAUd1quFLx/ie/nSjrk1PtscSicpoYDlKb9e6j7pHrVpbopBMyliyfNl3tLJWaDh+x+uCXqw==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.11.0.tgz", + "integrity": "sha512-Do9WJzEkFBBB+zVFvFfrrrIFEz086lrdgKQic7XsdoTgtYtq0yMu2u3kGLyxMbdasl2c2yf49FE4YvO3AYjQMQ==", "dev": true, "optional": true, "requires": { - "deep-equal": "^2.0.0", + "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", - "google-gax": "^1.15.3", - "readable-stream": "^3.4.0", - "through2": "^3.0.0" + "google-gax": "^2.9.2", + "protobufjs": "^6.8.6" } }, "@google-cloud/paginator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.3.tgz", - "integrity": "sha512-kp/pkb2p/p0d8/SKUu4mOq8+HGwF8NPzHWkj+VKrIPQPyMRw8deZtrO/OcSiy9C/7bpfU5Txah5ltUNfPkgEXg==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.5.tgz", + "integrity": "sha512-N4Uk4BT1YuskfRhKXBs0n9Lg2YTROZc6IMpkO/8DIHODtm5s3xY8K5vVBo23v/2XulY3azwITQlYWgT4GdLsUw==", "dev": true, "optional": true, "requires": { @@ -135,103 +158,233 @@ } }, "@google-cloud/projectify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.4.tgz", - "integrity": "sha512-ZdzQUN02eRsmTKfBj9FDL0KNDIFNjBn/d6tHQmA/+FImH5DO6ZV8E7FzxMgAUiVAUq41RFAkb25p1oHOZ8psfg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz", + "integrity": "sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ==", "dev": true, "optional": true }, "@google-cloud/promisify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.4.tgz", - "integrity": "sha512-VccZDcOql77obTnFh0TbNED/6ZbbmHDf8UMNnzO1d5g9V0Htfm4k5cllY8P1tJsRKC3zWYGRLaViiupcgVjBoQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", + "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==", "dev": true, "optional": true }, "@google-cloud/storage": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-4.7.0.tgz", - "integrity": "sha512-f0guAlbeg7Z0m3gKjCfBCu7FG9qS3M3oL5OQQxlvGoPtK7/qg3+W+KQV73O2/sbuS54n0Kh2mvT5K2FWzF5vVQ==", + "version": "5.8.5", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.8.5.tgz", + "integrity": "sha512-i0gB9CRwQeOBYP7xuvn14M40LhHCwMjceBjxE4CTvsqL519sVY5yVKxLiAedHWGwUZHJNRa7Q2CmNfkdRwVNPg==", "dev": true, "optional": true, "requires": { - "@google-cloud/common": "^2.1.1", - "@google-cloud/paginator": "^2.0.0", - "@google-cloud/promisify": "^1.0.0", + "@google-cloud/common": "^3.6.0", + "@google-cloud/paginator": "^3.0.0", + "@google-cloud/promisify": "^2.0.0", "arrify": "^2.0.0", + "async-retry": "^1.3.1", "compressible": "^2.0.12", - "concat-stream": "^2.0.0", - "date-and-time": "^0.13.0", - "duplexify": "^3.5.0", + "date-and-time": "^1.0.0", + "duplexify": "^4.0.0", "extend": "^3.0.2", - "gaxios": "^3.0.0", - "gcs-resumable-upload": "^2.2.4", + "gaxios": "^4.0.0", + "gcs-resumable-upload": "^3.1.4", + "get-stream": "^6.0.0", "hash-stream-validation": "^0.2.2", "mime": "^2.2.0", "mime-types": "^2.0.8", "onetime": "^5.1.0", - "p-limit": "^2.2.0", + "p-limit": "^3.0.1", "pumpify": "^2.0.0", - "readable-stream": "^3.4.0", "snakeize": "^0.1.0", "stream-events": "^1.0.1", - "through2": "^3.0.0", "xdg-basedir": "^4.0.0" }, "dependencies": { - "gaxios": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz", - "integrity": "sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q==", + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "dev": true, + "optional": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "optional": true, "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" + "yocto-queue": "^0.1.0" } - }, - "mime": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", - "dev": true, - "optional": true } } }, "@grpc/grpc-js": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.0.5.tgz", - "integrity": "sha512-Hm+xOiqAhcpT9RYM8lc15dbQD7aQurM7ZU8ulmulepiPlN7iwBXXwP3vSBUimoFoApRqz7pSIisXU8pZaCB4og==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.3.0.tgz", + "integrity": "sha512-fiL7ZaGg2HBiFtmv6m34d5jEgEtNXfctjzB3f7b3iuT7olBX4mHLMOqOBmGTTSOTfNRQJH5+vsyk6mEz3I0Q7Q==", "dev": true, "optional": true, "requires": { - "semver": "^6.2.0" + "@types/node": ">=12.12.47" }, "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "@types/node": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz", + "integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==", "dev": true, "optional": true } } }, "@grpc/proto-loader": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz", - "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.1.tgz", + "integrity": "sha512-4DIvEOZhw5nGj3RQngIoiMXRsre3InEH136krZTcirs/G2em3WMXdtx4Lqlnb4E2ertbWGs5gPeVDKU5BHffXw==", "dev": true, "optional": true, "requires": { + "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" + "long": "^4.0.0", + "protobufjs": "^6.10.0", + "yargs": "^16.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "optional": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "optional": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "optional": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "optional": true + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "optional": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "optional": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "optional": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "dev": true, + "optional": true + } } }, + "@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==", + "dev": true + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -242,14 +395,14 @@ "@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha1-TIVzDlm5ofHzSQR9vyQpYDS7JzU=", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", "dev": true, "optional": true }, "@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha1-fvN/DQEPsCitGtWXIuUG2SYoFcs=", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", "dev": true, "optional": true }, @@ -406,6 +559,16 @@ "@types/serve-static": "*" } }, + "@types/express-jwt": { + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.42.tgz", + "integrity": "sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/express-unless": "*" + } + }, "@types/express-serve-static-core": { "version": "4.17.12", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.12.tgz", @@ -416,14 +579,13 @@ "@types/range-parser": "*" } }, - "@types/fs-extra": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.1.tgz", - "integrity": "sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w==", + "@types/express-unless": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.1.tgz", + "integrity": "sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw==", "dev": true, - "optional": true, "requires": { - "@types/node": "*" + "@types/express": "*" } }, "@types/jsonwebtoken": { @@ -555,9 +717,9 @@ "dev": true }, "agent-base": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", - "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, "optional": true, "requires": { @@ -565,13 +727,13 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "optional": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -637,13 +799,6 @@ "sprintf-js": "~1.0.2" } }, - "array-filter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", - "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=", - "dev": true, - "optional": true - }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -671,6 +826,18 @@ "safer-buffer": "~2.1.0" } }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -683,6 +850,16 @@ "integrity": "sha1-5gtrDo8wG9l+U3UhW9pAbIURjAs=", "dev": true }, + "async-retry": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.1.tgz", + "integrity": "sha512-aiieFW/7h3hY0Bq5d+ktDBejxuwR78vRu9hDUdR8rNhSaQ29VzPL4AoIRG7D/c7tdenwOcKvgPM6tIxB3cB6HA==", + "dev": true, + "optional": true, + "requires": { + "retry": "0.12.0" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -695,16 +872,6 @@ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true }, - "available-typed-arrays": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz", - "integrity": "sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==", - "dev": true, - "optional": true, - "requires": { - "array-filter": "^1.0.0" - } - }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -724,9 +891,9 @@ "dev": true }, "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, "optional": true }, @@ -740,12 +907,18 @@ } }, "bignumber.js": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", - "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", + "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==", "dev": true, "optional": true }, + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -773,6 +946,12 @@ "concat-map": "0.0.1" } }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, "browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -968,9 +1147,9 @@ }, "dependencies": { "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", + "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==", "dev": true, "optional": true } @@ -982,19 +1161,6 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "dev": true, - "optional": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, "configstore": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", @@ -1109,9 +1275,9 @@ } }, "date-and-time": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.13.1.tgz", - "integrity": "sha512-/Uge9DJAT+s+oAcDxtBhyR8+sKjUnZbYmyhbmWjTHNtX7B7oWD8YyYdeXcBRbwSj6hVvj+IQegJam7m7czhbFw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-1.0.0.tgz", + "integrity": "sha512-477D7ypIiqlXBkxhU7YtG9wWZJEQ+RUpujt2quTfgf4+E8g5fNUkB0QIL0bVyP5/TKBg8y55Hfa1R/c4bt3dEw==", "dev": true, "optional": true }, @@ -1144,29 +1310,6 @@ "type-detect": "^4.0.0" } }, - "deep-equal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.3.tgz", - "integrity": "sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA==", - "dev": true, - "optional": true, - "requires": { - "es-abstract": "^1.17.5", - "es-get-iterator": "^1.1.0", - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.2", - "is-regex": "^1.0.5", - "isarray": "^2.0.5", - "object-is": "^1.1.2", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "regexp.prototype.flags": "^1.3.0", - "side-channel": "^1.0.2", - "which-boxed-primitive": "^1.0.1", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.2" - } - }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -1241,41 +1384,16 @@ } }, "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", "dev": true, "optional": true, "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", "stream-shift": "^1.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } } }, "ecc-jsbn": { @@ -1302,6 +1420,29 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "dev": true, + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + } + } + }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -1349,22 +1490,6 @@ "string.prototype.trimstart": "^1.0.1" } }, - "es-get-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", - "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", - "dev": true, - "optional": true, - "requires": { - "es-abstract": "^1.17.4", - "has-symbols": "^1.0.1", - "is-arguments": "^1.0.4", - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-string": "^1.0.5", - "isarray": "^2.0.5" - } - }, "es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -1376,6 +1501,13 @@ "is-symbol": "^1.0.2" } }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "optional": true + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1534,18 +1666,28 @@ } }, "firebase-admin": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-8.13.0.tgz", - "integrity": "sha512-krXj5ncWMJBhCpXSn9UFY6zmDWjFjqgx+1e9ATXKFYndEjmKtNBuJzqdrAdDh7aTUR7X6+0TPx4Hbc08kd0lwQ==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-9.8.0.tgz", + "integrity": "sha512-v8B1qU8McZZT2hlLZ018TKz2FoKlfFkZq9mOIyzN7wJUOlAywqQX0JyqNpVGyPeU+B+77ojlvmkGTNXt2OFkgw==", "dev": true, "requires": { - "@firebase/database": "^0.6.0", - "@google-cloud/firestore": "^3.0.0", - "@google-cloud/storage": "^4.1.2", - "@types/node": "^8.10.59", + "@firebase/database": "^0.10.0", + "@firebase/database-types": "^0.7.2", + "@google-cloud/firestore": "^4.5.0", + "@google-cloud/storage": "^5.3.0", + "@types/node": ">=12.12.47", "dicer": "^0.3.0", "jsonwebtoken": "^8.5.1", - "node-forge": "^0.7.6" + "jwks-rsa": "^2.0.2", + "node-forge": "^0.10.0" + }, + "dependencies": { + "@types/node": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz", + "integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==", + "dev": true + } } }, "flat": { @@ -1565,13 +1707,6 @@ } } }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "dev": true, - "optional": true - }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -1631,9 +1766,9 @@ "optional": true }, "gaxios": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.4.tgz", - "integrity": "sha512-US8UMj8C5pRnao3Zykc4AAVr+cffoNKRTg9Rsf2GiuZCW69vgJj38VK2PzlPuQU73FZ/nTk9/Av6/JGcE1N9vA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.2.1.tgz", + "integrity": "sha512-s+rTywpw6CmfB8r9TXYkpix7YFeuRjnR/AqhaJrQqsNhsAqej+IAiCc3hadzQH3gHyWth30tvYjxH8EVjQt/8Q==", "dev": true, "optional": true, "requires": { @@ -1645,27 +1780,28 @@ } }, "gcp-metadata": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.5.0.tgz", - "integrity": "sha512-ZQf+DLZ5aKcRpLzYUyBS3yo3N0JSa82lNDO8rj3nMSlovLcz2riKFBsYgDzeXcv75oo5eqB2lx+B14UvPoCRnA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz", + "integrity": "sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw==", "dev": true, "optional": true, "requires": { - "gaxios": "^2.1.0", - "json-bigint": "^0.3.0" + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" } }, "gcs-resumable-upload": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-2.3.3.tgz", - "integrity": "sha512-sf896I5CC/1AxeaGfSFg3vKMjUq/r+A3bscmVzZm10CElyRanN0XwPu/MxeIO4LSP+9uF6yKzXvNsaTsMXUG6Q==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-3.1.4.tgz", + "integrity": "sha512-5dyDfHrrVcIskiw/cPssVD4HRiwoHjhk1Nd6h5W3pQ/qffDvhfy4oNCr1f3ZXFPwTnxkCbibsB+73oOM+NvmJQ==", "dev": true, "optional": true, "requires": { "abort-controller": "^3.0.0", "configstore": "^5.0.0", - "gaxios": "^2.0.0", - "google-auth-library": "^5.0.0", + "extend": "^3.0.2", + "gaxios": "^4.0.0", + "google-auth-library": "^7.0.0", "pumpify": "^2.0.0", "stream-events": "^1.0.4" } @@ -1682,6 +1818,13 @@ "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "optional": true + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -1706,9 +1849,9 @@ } }, "google-auth-library": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.10.1.tgz", - "integrity": "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.0.4.tgz", + "integrity": "sha512-o8irYyeijEiecTXeoEe8UKNEzV1X+uhR4b2oNdapDMZixypp0J+eHimGOyx5Joa3UAeokGngdtDLXtq9vDqG2Q==", "dev": true, "optional": true, "requires": { @@ -1716,11 +1859,11 @@ "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", - "gaxios": "^2.1.0", - "gcp-metadata": "^3.4.0", - "gtoken": "^4.1.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", "jws": "^4.0.0", - "lru-cache": "^5.0.0" + "lru-cache": "^6.0.0" }, "dependencies": { "jwa": { @@ -1747,74 +1890,53 @@ } }, "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "optional": true, "requires": { - "yallist": "^3.0.2" + "yallist": "^4.0.0" } }, "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, "optional": true } } }, "google-gax": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.15.3.tgz", - "integrity": "sha512-3JKJCRumNm3x2EksUTw4P1Rad43FTpqrtW9jzpf3xSMYXx+ogaqTM1vGo7VixHB4xkAyATXVIa3OcNSh8H9zsQ==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.12.0.tgz", + "integrity": "sha512-UDx4ZZx85vXBe6GZ0sdRSzuegLrRQdRjCxlauX+U7i5YwOoSgcSaIM71BhcdHwGPhEkvO/SSHrEfc1wpL/J6JA==", "dev": true, "optional": true, "requires": { - "@grpc/grpc-js": "~1.0.3", - "@grpc/proto-loader": "^0.5.1", - "@types/fs-extra": "^8.0.1", + "@grpc/grpc-js": "~1.3.0", + "@grpc/proto-loader": "^0.6.1", "@types/long": "^4.0.0", "abort-controller": "^3.0.0", - "duplexify": "^3.6.0", - "google-auth-library": "^5.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^7.0.2", "is-stream-ended": "^0.1.4", - "lodash.at": "^4.6.0", - "lodash.has": "^4.5.2", - "node-fetch": "^2.6.0", - "protobufjs": "^6.8.9", - "retry-request": "^4.0.0", - "semver": "^6.0.0", - "walkdir": "^0.4.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "optional": true - } + "node-fetch": "^2.6.1", + "object-hash": "^2.1.1", + "protobufjs": "^6.10.2", + "retry-request": "^4.0.0" } }, "google-p12-pem": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.4.tgz", - "integrity": "sha512-S4blHBQWZRnEW44OcR7TL9WR+QCqByRvhNDZ/uuQfpxywfupikf/miba8js1jZi6ZOGv5slgSuoshCWh6EMDzg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz", + "integrity": "sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA==", "dev": true, "optional": true, "requires": { - "node-forge": "^0.9.0" - }, - "dependencies": { - "node-forge": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.2.tgz", - "integrity": "sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw==", - "dev": true, - "optional": true - } + "node-forge": "^0.10.0" } }, "graceful-fs": { @@ -1830,16 +1952,15 @@ "dev": true }, "gtoken": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.4.tgz", - "integrity": "sha512-VxirzD0SWoFUo5p8RDP8Jt2AGyOmyYcT/pOUgDKJCK+iSw0TMqwrVfY37RXTNmoKwrzmDHSk0GMT9FsgVmnVSA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.2.1.tgz", + "integrity": "sha512-OY0BfPKe3QnMsY9MzTHTSKn+Vl2l1CcLe6BwDEQj00mbbkl5nyQ/7EUREstg4fQNZ8iYE7br4JJ7TdKeDOPWmw==", "dev": true, "optional": true, "requires": { - "gaxios": "^2.1.0", - "google-p12-pem": "^2.0.0", - "jws": "^4.0.0", - "mime": "^2.2.0" + "gaxios": "^4.0.0", + "google-p12-pem": "^3.0.3", + "jws": "^4.0.0" }, "dependencies": { "jwa": { @@ -1864,13 +1985,6 @@ "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } - }, - "mime": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", - "dev": true, - "optional": true } } }, @@ -1912,6 +2026,16 @@ "dev": true, "optional": true }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1924,6 +2048,17 @@ "integrity": "sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg==", "dev": true }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -1946,9 +2081,9 @@ } }, "http-parser-js": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz", - "integrity": "sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", + "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", "dev": true }, "http-proxy-agent": { @@ -1964,13 +2099,13 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "optional": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -2005,13 +2140,13 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "optional": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -2076,20 +2211,6 @@ "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", "dev": true }, - "is-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.0.tgz", - "integrity": "sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==", - "dev": true, - "optional": true - }, - "is-boolean-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.1.tgz", - "integrity": "sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==", - "dev": true, - "optional": true - }, "is-callable": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.1.tgz", @@ -2108,26 +2229,12 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, - "is-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz", - "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==", - "dev": true, - "optional": true - }, "is-negative-zero": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", "dev": true }, - "is-number-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", - "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", - "dev": true, - "optional": true - }, "is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", @@ -2150,13 +2257,6 @@ "has-symbols": "^1.0.1" } }, - "is-set": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", - "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==", - "dev": true, - "optional": true - }, "is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", @@ -2167,14 +2267,7 @@ "is-stream-ended": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", - "integrity": "sha1-9QIk6V4GvODjVtRApIJ801smfto=", - "dev": true, - "optional": true - }, - "is-string": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", - "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", "dev": true, "optional": true }, @@ -2187,46 +2280,12 @@ "has-symbols": "^1.0.1" } }, - "is-typed-array": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.3.tgz", - "integrity": "sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==", - "dev": true, - "optional": true, - "requires": { - "available-typed-arrays": "^1.0.0", - "es-abstract": "^1.17.4", - "foreach": "^2.0.5", - "has-symbols": "^1.0.1" - } - }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, - "is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "dev": true, - "optional": true - }, - "is-weakset": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.1.tgz", - "integrity": "sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==", - "dev": true, - "optional": true - }, - "isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "optional": true - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2245,6 +2304,15 @@ "integrity": "sha512-5IZ7sY9dBAYSV+YjQ0Ovb540Ku7AO9Z5o2Cg789xj167iQuZ2cG+z0f3Uct6WeYLbU6aQiM2pCs7sZ+4dotydw==", "dev": true }, + "jose": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", + "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", + "dev": true, + "requires": { + "@panva/asn1.js": "^1.0.0" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2338,9 +2406,9 @@ } }, "json-bigint": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.1.tgz", - "integrity": "sha512-DGWnSzmusIreWlEupsUelHrhwmPPE+FiQvg+drKfk2p+bdEYa5mp4PJ8JsCWqae0M2jQNb0HPvnwvf1qOTThzQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", "dev": true, "optional": true, "requires": { @@ -2430,6 +2498,47 @@ "safe-buffer": "^5.0.1" } }, + "jwk-to-pem": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz", + "integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==", + "dev": true, + "requires": { + "asn1.js": "^5.3.0", + "elliptic": "^6.5.4", + "safe-buffer": "^5.0.1" + } + }, + "jwks-rsa": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.0.3.tgz", + "integrity": "sha512-/rkjXRWAp0cS00tunsHResw68P5iTQru8+jHufLNv3JHc4nObFEndfEUSuPugh09N+V9XYxKUqi7QrkmCHSSSg==", + "dev": true, + "requires": { + "@types/express-jwt": "0.0.42", + "debug": "^4.1.0", + "jose": "^2.0.5", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.2" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "jws": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", @@ -2450,6 +2559,12 @@ "type-check": "~0.3.2" } }, + "limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", + "dev": true + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -2471,13 +2586,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, - "lodash.at": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.at/-/lodash.at-4.6.0.tgz", - "integrity": "sha1-k83OZk8KGZTqM9181A4jr9EbD/g=", - "dev": true, - "optional": true - }, "lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -2485,12 +2593,11 @@ "dev": true, "optional": true }, - "lodash.has": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", - "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=", - "dev": true, - "optional": true + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true }, "lodash.includes": { "version": "4.3.0", @@ -2558,7 +2665,7 @@ "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha1-mntxz7fTYaGU6lVSQckvdGjVvyg=", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", "dev": true, "optional": true }, @@ -2572,6 +2679,28 @@ "yallist": "^2.1.2" } }, + "lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "dev": true, + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", + "dev": true, + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + } + } + }, "lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -2649,6 +2778,18 @@ "dev": true, "optional": true }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -2983,9 +3124,9 @@ "optional": true }, "node-forge": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", - "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", "dev": true }, "node-version": { @@ -3020,6 +3161,13 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-hash": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.1.1.tgz", + "integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==", + "dev": true, + "optional": true + }, "object-inspect": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", @@ -3209,13 +3357,6 @@ "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", "dev": true }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "optional": true - }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -3235,9 +3376,9 @@ "dev": true }, "protobufjs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz", - "integrity": "sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", + "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", "dev": true, "optional": true, "requires": { @@ -3252,14 +3393,14 @@ "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/long": "^4.0.1", - "@types/node": "^13.7.0", + "@types/node": ">=13.7.0", "long": "^4.0.0" }, "dependencies": { "@types/node": { - "version": "13.13.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.20.tgz", - "integrity": "sha512-1kx55tU3AvGX2Cjk2W4GMBxbgIz892V+X10S2gUreIAq8qCWgaQH+tZBOWc0bi2BKFhQt+CX0BTx28V9QPNa+A==", + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz", + "integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==", "dev": true, "optional": true } @@ -3307,21 +3448,6 @@ "duplexify": "^4.1.1", "inherits": "^2.0.3", "pump": "^3.0.0" - }, - "dependencies": { - "duplexify": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", - "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", - "dev": true, - "optional": true, - "requires": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - } } }, "punycode": { @@ -3490,6 +3616,13 @@ "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", "dev": true }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true, + "optional": true + }, "retry-request": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.3.tgz", @@ -3501,13 +3634,13 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "optional": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -3604,40 +3737,6 @@ "rechoir": "^0.6.2" } }, - "side-channel": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz", - "integrity": "sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==", - "dev": true, - "optional": true, - "requires": { - "es-abstract": "^1.18.0-next.0", - "object-inspect": "^1.8.0" - }, - "dependencies": { - "es-abstract": { - "version": "1.18.0-next.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.0.tgz", - "integrity": "sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==", - "dev": true, - "optional": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -3745,7 +3844,7 @@ "stream-events": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha1-u8iY7E3zOkkC2JIzPUfam/HEBtU=", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", "dev": true, "optional": true, "requires": { @@ -3796,13 +3895,22 @@ } }, "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "optional": true, "requires": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "optional": true + } } }, "strip-ansi": { @@ -3834,17 +3942,17 @@ "dev": true }, "teeny-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.3.tgz", - "integrity": "sha512-TZG/dfd2r6yeji19es1cUIwAlVD8y+/svB1kAC2Y0bjEyysrfbO8EZvJBRwIE6WkwmUoB7uvWLwTIhJbMXZ1Dw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz", + "integrity": "sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw==", "dev": true, "optional": true, "requires": { "http-proxy-agent": "^4.0.0", "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.2.0", + "node-fetch": "^2.6.1", "stream-events": "^1.0.5", - "uuid": "^7.0.0" + "uuid": "^8.0.0" } }, "thenify": { @@ -3865,26 +3973,6 @@ "thenify": ">= 3.1.0 < 4" } }, - "through2": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", - "dev": true, - "optional": true, - "requires": { - "inherits": "^2.0.4", - "readable-stream": "2 || 3" - }, - "dependencies": { - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "optional": true - } - } - }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", @@ -4060,13 +4148,6 @@ "mime-types": "~2.1.24" } }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true, - "optional": true - }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -4198,9 +4279,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", - "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, "optional": true }, @@ -4238,13 +4319,6 @@ "xml-name-validator": "^3.0.0" } }, - "walkdir": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", - "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", - "dev": true, - "optional": true - }, "webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -4303,54 +4377,12 @@ "isexe": "^2.0.0" } }, - "which-boxed-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz", - "integrity": "sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==", - "dev": true, - "optional": true, - "requires": { - "is-bigint": "^1.0.0", - "is-boolean-object": "^1.0.0", - "is-number-object": "^1.0.3", - "is-string": "^1.0.4", - "is-symbol": "^1.0.2" - } - }, - "which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "dev": true, - "optional": true, - "requires": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - } - }, "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, - "which-typed-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.2.tgz", - "integrity": "sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==", - "dev": true, - "optional": true, - "requires": { - "available-typed-arrays": "^1.0.2", - "es-abstract": "^1.17.5", - "foreach": "^2.0.5", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.1", - "is-typed-array": "^1.1.3" - } - }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", @@ -4683,6 +4715,13 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "optional": true } } } diff --git a/package.json b/package.json index 11a4dfb39..24ebbbcb4 100644 --- a/package.json +++ b/package.json @@ -58,10 +58,11 @@ "chai": "^4.2.0", "chai-as-promised": "^7.1.1", "child-process-promise": "^2.2.1", - "firebase-admin": "^8.2.0", + "firebase-admin": "^9.8.0", "js-yaml": "^3.13.1", "jsdom": "^16.2.1", "jsonwebtoken": "^8.5.1", + "jwk-to-pem": "^2.0.5", "mocha": "^6.1.4", "mock-require": "^3.0.3", "mz": "^2.7.0", diff --git a/spec/fixtures/credential/jwk.json b/spec/fixtures/credential/jwk.json new file mode 100644 index 000000000..cde44767e --- /dev/null +++ b/spec/fixtures/credential/jwk.json @@ -0,0 +1,14 @@ +{ + "p": "9cTVRzGXbDfhIMQW9gXtWveDW0u_Hvwnbjx7TRPgSfawZ0MjgKfSbnyHTDXiqM1ifcN_Nk58KJ-PG9eZ7V7_mfTUnPv2puDaecn-kgHobnTJMoBR9hpzyyMpyNJuMvX4kqE7Qh8iFMBK_-p8ICiW15gK5WykswIKfIOkUZc52XM", + "kty": "RSA", + "q": "pYdUNL244sCoc4XrONKlu787AiHrjFFLHdTjoFLbvxSpszXM8iSjoiFAM_MCF-uWks2iBVDw9wlG4MB7MfNf_fD0i1wqyknSOtfMxknU7D4eU_Sp6tI99Jl8f_GAzODK__k_0MpqqXgZmJbUvYuIXMiha-5lddz8ENa4pYpbr7M", + "d": "MpkXqjmjvzwfmlq3o0uZAXjeeAnBlYQSNaSllBWKepgPjg4FxFIt_BlXex1NeP0npNy_oCgaM_x7NiALaaPhwPK52lhYThc-xomCic1KDkyPecODTPXi4Iw94Q_gp442SYMWz2ZktS-2DgXc3599fGHkY80u0rHNSO8ptdk8SUDUIZ82ZQ3pBhClF_uY3c1jZLuqVgCwKksInZmNPnv3ge088wmQC26t0Ph5u1HU6lISgaqZ8ol23iNWJPf4UEi8Twy1a73nphQS-y1yK9UC3c5Knk-WI2TMmjlxqC02ZjKqnRDxElTj9kpodasPRHRV_KJI8rTaStgxd7peMFODzQ", + "e": "AQAB", + "use": "sig", + "kid": "a12KBE", + "qi": "aJCrZVWeOjxYmMBTTI7aJhxcvvfS3I5Q7wwN4Oyb1rJZ4fgGYjDohlzeZz_3fNantPAgcDbzJfa3XS327sHJGaAVqvDugZUgyHeLZGzXGs-_mlL72wzcfvTa1C9_lIndLNZJle5_mg3xJAqRKV0s7kymSdYt0wL5fDaqo5SDNqQ", + "dp": "haBk2hWzoApt5HPZjCDC4g_rosr3enBdPAm0fL8O1whC95JAjmYw-xPIOH6f42nwYDLYSv23chr3I4tBTRe2382HgGdav3dIMqnKOTbCWrQy5LtyVN4jEVLoGCGZ-ylT4t25K4Vj8WZwIN8saAvJoCUx33YHwrCcZQDqadZQhNM", + "alg": "RS256", + "dq": "j6NdeN7hnzMbehPNyGNSmhcZd4JDymGI03w3gpokQi4GDJM1IzKUJE7CTdIkEOnIod97Jy3TzCrqrIGa5f-RXuVG79-s6hkhKxq0gaTz9YT6AFShVjnWtXizRrskz6SJw5JgxCfCYwjq_TR1q313eTxIh0Y6GQsIWPxbApuLcG0", + "n": "nunJGpOcPvVsP3q-NLgf3H6OycPhnXUxywMR2_H_JJP7BUIDSsYcOGBTFe7OphHYfyb1Gs14yAER243swndpNbQkuDJhj9a9kK6dJZmPGmvCySk_E5URj6MimZg1MBbwhsVAbRp2uerESZuoRrfdTdV87E3pGyg6Irl0IXRjy5w9SsFjjIi7E-Qxpf3TcNNjfVRLj9V2bSzmS7hlsPKBhDon0tWecuNKoNNMiGI46mz_MSUa2y1lPV6Cqhf1su_TRd7N7u9eP7xWArr7wqtqHiFTZ3qp1xoA_dr_xv_Ao2kBtohZiAFLV-PQShprSN5fafztRZFkSEF0m2tUkvmoaQ" +} diff --git a/spec/fixtures/mockrequest.ts b/spec/fixtures/mockrequest.ts index fff2ce1a9..8766ab06a 100644 --- a/spec/fixtures/mockrequest.ts +++ b/spec/fixtures/mockrequest.ts @@ -1,7 +1,9 @@ import * as jwt from 'jsonwebtoken'; +import * as jwkToPem from 'jwk-to-pem'; import * as _ from 'lodash'; import * as nock from 'nock'; -import * as mocks from '../fixtures/credential/key.json'; +import * as mockKey from '../fixtures/credential/key.json'; +import * as mockJWK from '../fixtures/credential/jwk.json'; // MockRequest mocks an https.Request. export class MockRequest { @@ -26,6 +28,7 @@ export function mockRequest( context: { authorization?: string; instanceIdToken?: string; + appCheckToken?: string; } = {} ) { const body: any = {}; @@ -37,6 +40,7 @@ export function mockRequest( 'content-type': contentType, authorization: context.authorization, 'firebase-instance-id-token': context.instanceIdToken, + 'x-firebase-appcheck': context.appCheckToken, origin: 'example.com', }; @@ -53,7 +57,7 @@ export const expectedResponseHeaders = { * verifying an id token. */ export function mockFetchPublicKeys(): nock.Scope { - const mockedResponse = { [mocks.key_id]: mocks.public_key }; + const mockedResponse = { [mockKey.key_id]: mockKey.public_key }; const headers = { 'cache-control': 'public, max-age=1, must-revalidate, no-transform', }; @@ -72,11 +76,48 @@ export function generateIdToken(projectId: string): string { audience: projectId, expiresIn: 60 * 60, // 1 hour in seconds issuer: 'https://securetoken.google.com/' + projectId, - subject: mocks.user_id, + subject: mockKey.user_id, algorithm: 'RS256', header: { - kid: mocks.key_id, + kid: mockKey.key_id, }, }; - return jwt.sign(claims, mocks.private_key, options); + return jwt.sign(claims, mockKey.private_key, options); +} + +/** + * Mocks out the http request used by the firebase-admin SDK to get the jwks for + * verifying an AppCheck token. + */ +export function mockFetchAppCheckPublicJwks(): nock.Scope { + const { kty, use, alg, kid, n, e } = mockJWK; + const mockedResponse = { + keys: [{ kty, use, alg, kid, n, e }], + }; + + return nock('https://firebaseappcheck.googleapis.com:443') + .get('/v1beta/jwks') + .reply(200, mockedResponse); +} + +/** + * Generates a mocked AppCheck token. + */ +export function generateAppCheckToken( + projectId: string, + appId: string +): string { + const claims = {}; + const options: jwt.SignOptions = { + audience: [`projects/${projectId}`], + expiresIn: 60 * 60, // 1 hour in seconds + issuer: `https://firebaseappcheck.googleapis.com/${projectId}`, + subject: appId, + header: { + alg: 'RS256', + typ: 'JWT', + kid: mockJWK.kid, + }, + }; + return jwt.sign(claims, jwkToPem(mockJWK, { private: true }), options); } diff --git a/spec/providers/https.spec.ts b/spec/providers/https.spec.ts index f858d5923..128ea0058 100644 --- a/spec/providers/https.spec.ts +++ b/spec/providers/https.spec.ts @@ -30,7 +30,9 @@ import * as https from '../../src/providers/https'; import * as mocks from '../fixtures/credential/key.json'; import { expectedResponseHeaders, + generateAppCheckToken, generateIdToken, + mockFetchAppCheckPublicJwks, mockFetchPublicKeys, MockRequest, mockRequest, @@ -414,6 +416,58 @@ describe('callable.FunctionBuilder', () => { }); }); + it('should handle AppCheck token', async () => { + const mock = mockFetchAppCheckPublicJwks(); + const projectId = appsNamespace().admin.options.projectId; + const appId = '1:65211879909:web:3ae38ef1cdcb2e01fe5f0c'; + const appCheckToken = generateAppCheckToken(projectId, appId); + await runTest({ + httpRequest: mockRequest(null, 'application/json', { appCheckToken }), + expectedData: null, + callableFunction: (data, context) => { + expect(context.app).to.not.be.undefined; + expect(context.app).to.not.be.null; + expect(context.app.appId).to.equal(appId); + expect(context.app.token.app_id).to.be.equal(appId); + expect(context.app.token.sub).to.be.equal(appId); + expect(context.app.token.aud).to.be.deep.equal([ + `projects/${projectId}`, + ]); + expect(context.auth).to.be.undefined; + expect(context.instanceIdToken).to.be.undefined; + return null; + }, + expectedHttpResponse: { + status: 200, + headers: expectedResponseHeaders, + body: { result: null }, + }, + }); + mock.done(); + }); + + it('should reject bad AppCheck token', async () => { + await runTest({ + httpRequest: mockRequest(null, 'application/json', { + appCheckToken: 'FAKE', + }), + expectedData: null, + callableFunction: (data, context) => { + return; + }, + expectedHttpResponse: { + status: 401, + headers: expectedResponseHeaders, + body: { + error: { + status: 'UNAUTHENTICATED', + message: 'Unauthenticated', + }, + }, + }, + }); + }); + it('should handle instance id', async () => { await runTest({ httpRequest: mockRequest(null, 'application/json', { diff --git a/src/providers/https.ts b/src/providers/https.ts index cf64c9d90..99f465215 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -28,7 +28,7 @@ import * as _ from 'lodash'; import { apps } from '../apps'; import { HttpsFunction, optionsToTrigger, Runnable } from '../cloud-functions'; import { DeploymentOptions } from '../function-configuration'; -import { warn, error } from '../logger'; +import { error, info, warn } from '../logger'; /** @hidden */ export interface Request extends express.Request { @@ -248,6 +248,14 @@ export class HttpsError extends Error { * The interface for metadata for the API as passed to the handler. */ export interface CallableContext { + /** + * The result of decoding and verifying a Firebase AppCheck token. + */ + app?: { + appId: string; + token: firebase.appCheck.DecodedAppCheckToken; + }; + /** * The result of decoding and verifying a Firebase Auth ID token. */ @@ -411,6 +419,108 @@ export function decode(data: any): any { return data; } +/** + * Be careful when changing token status values. + * + * Users are encouraged to setup log-based metric based on these values, and + * changing their values may cause their metrics to break. + * + */ +/** @hidden */ +type TokenStatus = 'MISSING' | 'VALID' | 'INVALID'; + +/** @hidden */ +interface CallableTokenStatus { + app: TokenStatus; + auth: TokenStatus; +} + +/** + * Check and verify tokens included in the requests. Once verified, tokens + * are injected into the callable context. + * + * @param {Request} req - Request sent to the Callable function. + * @param {CallableContext} ctx - Context to be sent to callable function handler. + * @return {CallableTokenStatus} Status of the token verifications. + */ +/** @hidden */ +async function checkTokens( + req: Request, + ctx: CallableContext +): Promise { + const verifications: CallableTokenStatus = { + app: 'MISSING', + auth: 'MISSING', + }; + + const appCheck = req.header('X-Firebase-AppCheck'); + if (appCheck) { + verifications.app = 'INVALID'; + try { + if (!apps().admin.appCheck) { + throw new Error( + 'Cannot validate AppCheck token. Please uupdate Firebase Admin SDK to >= v9.8.0' + ); + } + const appCheckToken = await apps() + .admin.appCheck() + .verifyToken(appCheck); + ctx.app = { + appId: appCheckToken.appId, + token: appCheckToken.token, + }; + verifications.app = 'VALID'; + } catch (err) { + warn('Failed to validate AppCheck token.', err); + } + } + + const authorization = req.header('Authorization'); + if (authorization) { + verifications.auth = 'INVALID'; + const match = authorization.match(/^Bearer (.*)$/); + if (match) { + const idToken = match[1]; + try { + const authToken = await apps() + .admin.auth() + .verifyIdToken(idToken); + + verifications.auth = 'VALID'; + ctx.auth = { + uid: authToken.uid, + token: authToken, + }; + } catch (err) { + warn('Failed to validate auth token.', err); + } + } + } + + const logPayload = { + verifications, + 'logging.googleapis.com/labels': { + 'firebase-log-type': 'callable-request-verification', + }, + }; + + const errs = []; + if (verifications.app === 'INVALID') { + errs.push('AppCheck token was rejected.'); + } + if (verifications.auth === 'INVALID') { + errs.push('Auth token was rejected.'); + } + + if (errs.length == 0) { + info('Callable request verification passed', logPayload); + } else { + warn(`Callable request verification failed: ${errs.join(' ')}`, logPayload); + } + + return verifications; +} + /** @hidden */ const corsHandler = cors({ origin: true, methods: 'POST' }); @@ -427,25 +537,9 @@ export function _onCallWithOptions( } const context: CallableContext = { rawRequest: req }; - - const authorization = req.header('Authorization'); - if (authorization) { - const match = authorization.match(/^Bearer (.*)$/); - if (!match) { - throw new HttpsError('unauthenticated', 'Unauthenticated'); - } - const idToken = match[1]; - try { - const authToken = await apps() - .admin.auth() - .verifyIdToken(idToken); - context.auth = { - uid: authToken.uid, - token: authToken, - }; - } catch (err) { - throw new HttpsError('unauthenticated', 'Unauthenticated'); - } + const tokenStatus = await checkTokens(req, context); + if (tokenStatus.app === 'INVALID' || tokenStatus.auth === 'INVALID') { + throw new HttpsError('unauthenticated', 'Unauthenticated'); } const instanceId = req.header('Firebase-Instance-ID-Token'); diff --git a/tsconfig.release.json b/tsconfig.release.json index a226eb290..1a45bd58b 100644 --- a/tsconfig.release.json +++ b/tsconfig.release.json @@ -1,13 +1,13 @@ { "compilerOptions": { "declaration": true, - "lib": ["es2017"], + "lib": ["es2018"], "module": "commonjs", "noImplicitAny": false, "noUnusedLocals": true, "outDir": "lib", "stripInternal": true, - "target": "es2017", + "target": "es2018", "typeRoots": ["./node_modules/@types"] }, "files": ["./src/index.ts", "./src/logger.ts", "./src/logger/compat.ts"] From 4ed7b7615302404bb0205c68ee49b1134aae5449 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 12 May 2021 13:03:08 -0700 Subject: [PATCH 300/705] Update release script to build packages using Node 14 (#889) * Update node version in package-builder imaage. * Update publish script to ignore local changes while updating package version. --- scripts/publish-container/Dockerfile | 2 +- scripts/publish.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/publish-container/Dockerfile b/scripts/publish-container/Dockerfile index c8ba24c12..02b15bf11 100644 --- a/scripts/publish-container/Dockerfile +++ b/scripts/publish-container/Dockerfile @@ -1,4 +1,4 @@ -FROM node:8 +FROM node:14 # Install dependencies RUN apt-get update && \ diff --git a/scripts/publish.sh b/scripts/publish.sh index a1498460e..d187e9f45 100644 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -87,7 +87,7 @@ npm run build:release echo "Ran publish build." echo "Making a $VERSION version..." -npm version $VERSION +npm version --no-git-tag-version $VERSION NEW_VERSION=$(jq -r ".version" package.json) echo "Made a $VERSION version." From f74adaa86a8a4ccd43ca6e0e4f59e7c1bbb19b8f Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Wed, 12 May 2021 20:09:26 +0000 Subject: [PATCH 301/705] [firebase-release] Removed change log and reset repo after 3.14.0 release --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ee96fbef..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +0,0 @@ -- Functions may now be deployed with 8GB RAM -- Functions may now be deployed to europe-central2 (Warsaw) -- Add support for validating AppCheck tokens for Callable Functions From 8e4a446ad0872297b873cfcd21f6847a9182b359 Mon Sep 17 00:00:00 2001 From: "Masayuki Ono (mono)" Date: Fri, 14 May 2021 04:29:10 +0900 Subject: [PATCH 302/705] Fix typo (#890) --- src/providers/https.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/https.ts b/src/providers/https.ts index 99f465215..2af2117f3 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -459,7 +459,7 @@ async function checkTokens( try { if (!apps().admin.appCheck) { throw new Error( - 'Cannot validate AppCheck token. Please uupdate Firebase Admin SDK to >= v9.8.0' + 'Cannot validate AppCheck token. Please update Firebase Admin SDK to >= v9.8.0' ); } const appCheckToken = await apps() From e94f1c029f837bfb0738fd477fd8b6a0c78245d4 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Mon, 17 May 2021 10:54:13 -0700 Subject: [PATCH 303/705] Inline DecodedAppCheck definition (#891) Fixes an issue where old (but supported) versions of firebase-admin didn't expose AppCheck and then TSC failed. --- integration_test/functions/src/index.ts | 2 +- .../functions/src/testLab-tests.ts | 2 +- .../functions/src/testLab-utils.ts | 12 ++-- integration_test/functions/src/testing.ts | 2 +- spec/fixtures/mockrequest.ts | 2 +- spec/logger.spec.ts | 8 +-- spec/providers/testLab.spec.ts | 40 +++++++------- src/cloud-functions.ts | 2 +- src/function-builder.ts | 2 +- src/function-configuration.ts | 7 +++ src/index.ts | 2 +- src/logger.ts | 2 +- src/logger/compat.ts | 4 +- src/providers/database.ts | 2 +- src/providers/https.ts | 55 ++++++++++++++++++- 15 files changed, 102 insertions(+), 42 deletions(-) diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 0a980aa06..6865d7dd5 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -1,9 +1,9 @@ +import { PubSub } from '@google-cloud/pubsub'; import { Request, Response } from 'express'; import * as admin from 'firebase-admin'; import * as functions from 'firebase-functions'; import * as fs from 'fs'; import * as https from 'https'; -import { PubSub } from '@google-cloud/pubsub'; export * from './pubsub-tests'; export * from './database-tests'; diff --git a/integration_test/functions/src/testLab-tests.ts b/integration_test/functions/src/testLab-tests.ts index 586c3a0cb..8e064928a 100644 --- a/integration_test/functions/src/testLab-tests.ts +++ b/integration_test/functions/src/testLab-tests.ts @@ -1,5 +1,5 @@ import * as functions from 'firebase-functions'; -import { TestSuite, expectEq } from './testing'; +import { expectEq, TestSuite } from './testing'; import TestMatrix = functions.testLab.TestMatrix; const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; diff --git a/integration_test/functions/src/testLab-utils.ts b/integration_test/functions/src/testLab-utils.ts index 6f86b0a1f..3dbb7b763 100644 --- a/integration_test/functions/src/testLab-utils.ts +++ b/integration_test/functions/src/testLab-utils.ts @@ -1,6 +1,6 @@ +import * as admin from 'firebase-admin'; import * as http from 'http'; import * as https from 'https'; -import * as admin from 'firebase-admin'; import * as utils from './test-utils'; interface AndroidDevice { @@ -50,12 +50,12 @@ async function fetchDefaultDevice( const model = defaultModels[0]; const versions = model.supportedVersionIds; - return { + return { androidModelId: model.id, androidVersionId: versions[versions.length - 1], locale: 'en', orientation: 'portrait', - }; + } as AndroidDevice; } function createTestMatrix( @@ -70,7 +70,7 @@ function createTestMatrix( '/v1/projects/' + projectId + '/testMatrices' ); const body = { - projectId: projectId, + projectId, testSpecification: { androidRoboTest: { appApk: { @@ -105,9 +105,9 @@ function requestOptions( path: string ): https.RequestOptions { return { - method: method, + method, hostname: TESTING_API_SERVICE_NAME, - path: path, + path, headers: { Authorization: 'Bearer ' + accessToken.access_token, 'Content-Type': 'application/json', diff --git a/integration_test/functions/src/testing.ts b/integration_test/functions/src/testing.ts index 228203d31..1cb9f7819 100644 --- a/integration_test/functions/src/testing.ts +++ b/integration_test/functions/src/testing.ts @@ -92,7 +92,7 @@ function deepEq(left: any, right: any) { return false; } - for (let key in left) { + for (const key in left) { if (!right.hasOwnProperty(key)) { return false; } diff --git a/spec/fixtures/mockrequest.ts b/spec/fixtures/mockrequest.ts index 8766ab06a..68827610b 100644 --- a/spec/fixtures/mockrequest.ts +++ b/spec/fixtures/mockrequest.ts @@ -2,8 +2,8 @@ import * as jwt from 'jsonwebtoken'; import * as jwkToPem from 'jwk-to-pem'; import * as _ from 'lodash'; import * as nock from 'nock'; -import * as mockKey from '../fixtures/credential/key.json'; import * as mockJWK from '../fixtures/credential/jwk.json'; +import * as mockKey from '../fixtures/credential/key.json'; // MockRequest mocks an https.Request. export class MockRequest { diff --git a/spec/logger.spec.ts b/spec/logger.spec.ts index 48258ffac..e32765d10 100644 --- a/spec/logger.spec.ts +++ b/spec/logger.spec.ts @@ -7,8 +7,8 @@ const SUPPORTS_STRUCTURED_LOGS = describe(`logger (${ SUPPORTS_STRUCTURED_LOGS ? 'structured' : 'unstructured' })`, () => { - let stdoutWrite = process.stdout.write.bind(process.stdout); - let stderrWrite = process.stderr.write.bind(process.stderr); + const stdoutWrite = process.stdout.write.bind(process.stdout); + const stderrWrite = process.stderr.write.bind(process.stderr); let lastOut: string; let lastErr: string; @@ -127,7 +127,7 @@ describe(`logger (${ for (const severity of ['DEBUG', 'INFO', 'NOTICE']) { it(`should output ${severity} severity to stdout`, () => { - let entry: logger.LogEntry = { + const entry: logger.LogEntry = { severity: severity as logger.LogSeverity, message: 'test', }; @@ -144,7 +144,7 @@ describe(`logger (${ 'EMERGENCY', ]) { it(`should output ${severity} severity to stderr`, () => { - let entry: logger.LogEntry = { + const entry: logger.LogEntry = { severity: severity as logger.LogSeverity, message: 'test', }; diff --git a/spec/providers/testLab.spec.ts b/spec/providers/testLab.spec.ts index bf74a6fd1..8a70cf680 100644 --- a/spec/providers/testLab.spec.ts +++ b/spec/providers/testLab.spec.ts @@ -66,23 +66,23 @@ describe('Test Lab Functions', () => { resource: {}, }, }; - const expected = { + const expected = { testMatrixId: 'matrix-375mfeu9mnw8t', state: 'INVALID', createTime: '2019-04-15T17:43:32.538Z', outcomeSummary: undefined, invalidMatrixDetails: 'INVALID_INPUT_APK', - resultStorage: { + resultStorage: { gcsPath: 'gs://test.appspot.com', resultsUrl: undefined, toolResultsHistoryId: undefined, toolResultsExecutionId: undefined, - }, - clientInfo: { + } as testLab.ResultStorage, + clientInfo: { name: 'test', details: {}, - }, - }; + } as testLab.ClientInfo, + } as testLab.TestMatrix; const func = testLab.testMatrix().onComplete((matrix) => matrix); return expect(func(event.data, event.context)).to.eventually.deep.equal( expected @@ -119,23 +119,23 @@ describe('Test Lab Functions', () => { resource: {}, }, }; - const expected = { + const expected = { testMatrixId: 'matrix-tsgjk8pnvxhya', state: 'FINISHED', createTime: '2019-04-15T18:03:11.115Z', outcomeSummary: 'FAILURE', invalidMatrixDetails: undefined, - resultStorage: { + resultStorage: { gcsPath: 'gs://test.appspot.com', toolResultsHistoryId: 'bh.9b6f4dac24d3049', toolResultsExecutionId: '6352915701487950333', resultsUrl: 'https://path/to/results', - }, - clientInfo: { + } as testLab.ResultStorage, + clientInfo: { name: 'test', details: {}, - }, - }; + } as testLab.ClientInfo, + } as testLab.TestMatrix; const func = testLab.testMatrix().onComplete((matrix) => matrix); return expect(func(event.data, event.context)).to.eventually.deep.equal( expected @@ -161,7 +161,7 @@ describe('Test Lab Functions', () => { describe('TestMatrix', () => { describe('constructor', () => { it('should populate basic fields', () => { - const expected = { + const expected = { testMatrixId: 'id1', createTime: '2019-02-08T18:50:32.178Z', state: 'FINISHED', @@ -169,7 +169,7 @@ describe('Test Lab Functions', () => { invalidMatrixDetails: 'DETAILS_UNAVAILABLE', resultStorage: new testLab.ResultStorage(), clientInfo: new testLab.ClientInfo(), - }; + } as testLab.TestMatrix; const actual = new testLab.TestMatrix({ testMatrixId: 'id1', timestamp: '2019-02-08T18:50:32.178Z', @@ -185,10 +185,10 @@ describe('Test Lab Functions', () => { describe('ClientInfo', () => { describe('constructor', () => { it('should populate basic fields', () => { - const expected = { + const expected = { name: 'client', details: {}, - }; + } as testLab.ClientInfo; const actual = new testLab.ClientInfo({ name: 'client', }); @@ -196,13 +196,13 @@ describe('Test Lab Functions', () => { }); it('should populate key/value details', () => { - const expected = { + const expected = { name: 'client', details: { k0: 'v0', k1: '', }, - }; + } as testLab.ClientInfo; const actual = new testLab.ClientInfo({ name: 'client', clientInfoDetails: [ @@ -223,12 +223,12 @@ describe('Test Lab Functions', () => { describe('ResultStorage', () => { describe('constructor', () => { it('should populate basic fields', () => { - const expected = { + const expected = { gcsPath: 'path', toolResultsHistoryId: 'h1', toolResultsExecutionId: 'e2', resultsUrl: 'http://example.com/', - }; + } as testLab.ResultStorage; const actual = new testLab.ResultStorage({ googleCloudStorage: { gcsPath: 'path', diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 500e27144..93377c822 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -22,13 +22,13 @@ import { Request, Response } from 'express'; import * as _ from 'lodash'; -import { warn } from './logger'; import { DEFAULT_FAILURE_POLICY, DeploymentOptions, FailurePolicy, Schedule, } from './function-configuration'; +import { warn } from './logger'; export { Request, Response }; /** @hidden */ diff --git a/src/function-builder.ts b/src/function-builder.ts index 9d016d817..f08b16117 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -26,12 +26,12 @@ import * as _ from 'lodash'; import { CloudFunction, EventContext } from './cloud-functions'; import { DeploymentOptions, + INGRESS_SETTINGS_OPTIONS, MAX_TIMEOUT_SECONDS, RuntimeOptions, SUPPORTED_REGIONS, VALID_MEMORY_OPTIONS, VPC_EGRESS_SETTINGS_OPTIONS, - INGRESS_SETTINGS_OPTIONS, } from './function-configuration'; import * as analytics from './providers/analytics'; import * as auth from './providers/auth'; diff --git a/src/function-configuration.ts b/src/function-configuration.ts index 2d9925ac5..852482241 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -116,6 +116,13 @@ export interface RuntimeOptions { */ minInstances?: number; + /** + * Which version of the internal contract between the CLI and the SDK are + * we using? For internal testing only. + * @hidden + */ + apiVersion?: 1 | 2; + /** * Max number of actual instances allowed to be running in parallel */ diff --git a/src/index.ts b/src/index.ts index 9d49414e8..dbb42003c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,8 +32,8 @@ import * as storage from './providers/storage'; import * as testLab from './providers/testLab'; import * as apps from './apps'; -import * as logger from './logger'; import { handler } from './handler-builder'; +import * as logger from './logger'; import { setup } from './setup'; const app = apps.apps(); diff --git a/src/logger.ts b/src/logger.ts index d6c0d2220..1dd109179 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,8 +1,8 @@ import { format } from 'util'; import { - SUPPORTS_STRUCTURED_LOGS, CONSOLE_SEVERITY, + SUPPORTS_STRUCTURED_LOGS, UNPATCHED_CONSOLE, } from './logger/common'; diff --git a/src/logger/compat.ts b/src/logger/compat.ts index a7c27bd16..4239221f0 100644 --- a/src/logger/compat.ts +++ b/src/logger/compat.ts @@ -1,9 +1,9 @@ +import { format } from 'util'; import { + CONSOLE_SEVERITY, SUPPORTS_STRUCTURED_LOGS, UNPATCHED_CONSOLE, - CONSOLE_SEVERITY, } from './common'; -import { format } from 'util'; /** @hidden */ function patchedConsole(severity: string): (data: any, ...args: any[]) => void { diff --git a/src/providers/database.ts b/src/providers/database.ts index 0e4393d05..76252d515 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -140,7 +140,7 @@ export function _refWithOptions( ); } - let instance = undefined; + let instance; const prodMatch = databaseURL.match(databaseURLRegex); if (prodMatch) { instance = prodMatch[1]; diff --git a/src/providers/https.ts b/src/providers/https.ts index 2af2117f3..c6167c50a 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -253,7 +253,60 @@ export interface CallableContext { */ app?: { appId: string; - token: firebase.appCheck.DecodedAppCheckToken; + + // This is actually a firebase.appCheck.DecodedAppCheckToken, but + // that type may not be available in some supported SDK versions. + // Declare as an inline type, which DecodedAppCheckToken will be + // able to merge with. + // TODO: Replace with the real type once we bump the min-version of + // the admin SDK + token: { + /** + * The issuer identifier for the issuer of the response. + * + * This value is a URL with the format + * `https://firebaseappcheck.googleapis.com/`, where `` is the + * same project number specified in the [`aud`](#aud) property. + */ + iss: string; + + /** + * The Firebase App ID corresponding to the app the token belonged to. + * + * As a convenience, this value is copied over to the [`app_id`](#app_id) property. + */ + sub: string; + + /** + * The audience for which this token is intended. + * + * This value is a JSON array of two strings, the first is the project number of your + * Firebase project, and the second is the project ID of the same project. + */ + aud: string[]; + + /** + * The App Check token's expiration time, in seconds since the Unix epoch. That is, the + * time at which this App Check token expires and should no longer be considered valid. + */ + exp: number; + + /** + * The App Check token's issued-at time, in seconds since the Unix epoch. That is, the + * time at which this App Check token was issued and should start to be considered + * valid. + */ + iat: number; + + /** + * The App ID corresponding to the App the App Check token belonged to. + * + * This value is not actually one of the JWT token claims. It is added as a + * convenience, and is set as the value of the [`sub`](#sub) property. + */ + app_id: string; + [key: string]: any; + }; }; /** From ba2cd3be03041828d3e0b83e8a8f45d38302ee02 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Mon, 17 May 2021 13:08:02 -0700 Subject: [PATCH 304/705] Add relnotes (#892) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..f5d3834e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Fixes a bug where typescript would fail to compile with old (but supported) versions of firebase-admin From 2b3b15d5be1f17a166175e4f5d5cc5ffc7a7040a Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Mon, 17 May 2021 20:35:15 +0000 Subject: [PATCH 305/705] [firebase-release] Removed change log and reset repo after 3.13.3 release --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5d3834e9..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +0,0 @@ -- Fixes a bug where typescript would fail to compile with old (but supported) versions of firebase-admin From 4053ee19e33add8418f5483caf8ea62b64b2253f Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Mon, 17 May 2021 14:04:34 -0700 Subject: [PATCH 306/705] Prep release 3.14.1 (#893) --- package.json | 2 +- scripts/publish/CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 scripts/publish/CHANGELOG.md diff --git a/package.json b/package.json index 24ebbbcb4..fa00d759d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.13.2", + "version": "3.14.0", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", diff --git a/scripts/publish/CHANGELOG.md b/scripts/publish/CHANGELOG.md new file mode 100644 index 000000000..510e7e4e0 --- /dev/null +++ b/scripts/publish/CHANGELOG.md @@ -0,0 +1,2 @@ +- Fixes a bug where typescript would fail to compile with old (but supported) versions of firebase-admin +- Replaces 3.13.3 which was an inappropriately numbered version From 242f4215668f2deb1931a74f006d9d8ef9f24330 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Mon, 17 May 2021 14:10:51 -0700 Subject: [PATCH 307/705] Fix changelog being in the wrong location (#894) --- CHANGELOG.md | 2 ++ scripts/publish/CHANGELOG.md | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 scripts/publish/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..510e7e4e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +- Fixes a bug where typescript would fail to compile with old (but supported) versions of firebase-admin +- Replaces 3.13.3 which was an inappropriately numbered version diff --git a/scripts/publish/CHANGELOG.md b/scripts/publish/CHANGELOG.md deleted file mode 100644 index 510e7e4e0..000000000 --- a/scripts/publish/CHANGELOG.md +++ /dev/null @@ -1,2 +0,0 @@ -- Fixes a bug where typescript would fail to compile with old (but supported) versions of firebase-admin -- Replaces 3.13.3 which was an inappropriately numbered version From 1c2cc9c367ed1a979ae0039bebb8be37f0a79f37 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Mon, 17 May 2021 21:12:48 +0000 Subject: [PATCH 308/705] [firebase-release] Removed change log and reset repo after 3.14.1 release --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 510e7e4e0..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +0,0 @@ -- Fixes a bug where typescript would fail to compile with old (but supported) versions of firebase-admin -- Replaces 3.13.3 which was an inappropriately numbered version From f369a2c35e65f2e484a1f6c770f4dfa1dbe1baae Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Thu, 20 May 2021 09:50:50 -0700 Subject: [PATCH 309/705] Update package.json to the latest release (#895) * Update package.json to the latest release * Bump version in package-lock.json --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8a4fd6be8..e199a60c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.13.2", + "version": "3.14.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fa00d759d..a9c86bf87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-functions", - "version": "3.14.0", + "version": "3.14.1", "description": "Firebase SDK for Cloud Functions", "keywords": [ "firebase", From f7bcdabc1a1d67c962072fc8cd229372a1349ffc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 May 2021 08:51:16 -0700 Subject: [PATCH 310/705] Bump y18n from 4.0.0 to 4.0.3 (#888) Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.3. - [Release notes](https://github.com/yargs/y18n/releases) - [Changelog](https://github.com/yargs/y18n/blob/y18n-v4.0.3/CHANGELOG.md) - [Commits](https://github.com/yargs/y18n/compare/v4.0.0...y18n-v4.0.3) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index e199a60c2..41abc0d4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -347,13 +347,6 @@ "strip-ansi": "^6.0.0" } }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "optional": true - }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -2946,6 +2939,12 @@ "has-flag": "^3.0.0" } }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, "yargs": { "version": "13.3.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", @@ -4482,10 +4481,11 @@ "dev": true }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "optional": true }, "yallist": { "version": "2.1.2", @@ -4631,6 +4631,12 @@ "strip-ansi": "^6.0.0" } }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, "yargs-parser": { "version": "18.1.3", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", @@ -4690,6 +4696,12 @@ "ansi-regex": "^4.1.0" } }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, "yargs": { "version": "13.3.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", From 7e26ae36a8fb6f44b86279826068d63f366e568f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 May 2021 08:53:54 -0700 Subject: [PATCH 311/705] Bump lodash from 4.17.20 to 4.17.21 (#887) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: joehan --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 41abc0d4b..16606db37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2575,9 +2575,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.camelcase": { "version": "4.3.0", From 02a97a19b81a2e40f25f23907b6ae3f585820978 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 27 May 2021 15:41:30 -0700 Subject: [PATCH 312/705] Fix publish script to commit changes to version info in package{-lock}.json. (#896) * Fix publish script to commit changes to version info in package.json and package-lock.json. * Add debug message to make it clear what's going on. Co-authored-by: Thomas Bouldin --- scripts/publish.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/publish.sh b/scripts/publish.sh index d187e9f45..9246562ae 100644 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -87,7 +87,11 @@ npm run build:release echo "Ran publish build." echo "Making a $VERSION version..." -npm version --no-git-tag-version $VERSION +# TODO: Remove the following command. +# npm version command had previously failed claiming unclean git repo, and we don't know why. +echo "DEBUG: Running git status to show dirty files..." +git status +npm version $VERSION NEW_VERSION=$(jq -r ".version" package.json) echo "Made a $VERSION version." From 2e718c74a382655a9756c0ff781278c0e8a9146b Mon Sep 17 00:00:00 2001 From: joehan Date: Thu, 27 May 2021 15:51:45 -0700 Subject: [PATCH 313/705] Adds support for setting user labels on functions (#899) * Adds support for runWith labels * Adds CHANGELOG entry and extra test cases * Minor style fixes * pr fixes * minor typo fix --- CHANGELOG.md | 1 + spec/function-builder.spec.ts | 120 ++++++++++++++++++++++++++++++++++ src/cloud-functions.ts | 6 +- src/function-builder.ts | 72 ++++++++++++++++++++ src/function-configuration.ts | 17 +++-- 5 files changed, 210 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..5283950c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Adds support for setting user labels on functions via `runWith()`. diff --git a/spec/function-builder.spec.ts b/spec/function-builder.spec.ts index 28cf35c3a..106f874ea 100644 --- a/spec/function-builder.spec.ts +++ b/spec/function-builder.spec.ts @@ -319,4 +319,124 @@ describe('FunctionBuilder', () => { expect(fn.__trigger.availableMemoryMb).to.deep.equal(4096); }); + + it('should allow labels to be set', () => { + const fn = functions + .runWith({ + labels: { + 'valid-key': 'valid-value', + }, + }) + .auth.user() + .onCreate((user) => user); + + expect(fn.__trigger.labels).to.deep.equal({ + 'valid-key': 'valid-value', + }); + }); + + it('should throw an error if more than 58 labels are set', () => { + const labels = {}; + for (let i = 0; i < 59; i++) { + labels[`label${i}`] = 'value'; + } + + expect(() => + functions.runWith({ + labels, + }) + ).to.throw(); + }); + + it('should throw an error if labels has a key that is too long', () => { + expect(() => + functions.runWith({ + labels: { + 'a-very-long-key-that-is-more-than-the-maximum-allowed-length-for-keys': + 'value', + }, + }) + ).to.throw(); + }); + + it('should throw an error if labels has key that is too short', () => { + expect(() => + functions.runWith({ + labels: { '': 'value' }, + }) + ).to.throw(); + }); + + it('should throw an error if labels has a value that is too long', () => { + expect(() => + functions.runWith({ + labels: { + key: + 'a-very-long-value-that-is-more-than-the-maximum-allowed-length-for-values', + }, + }) + ).to.throw(); + }); + + it('should throw an error if labels has a key that contains invalid characters', () => { + expect(() => + functions.runWith({ + labels: { + Key: 'value', + }, + }) + ).to.throw(); + + expect(() => + functions.runWith({ + labels: { + 'key ': 'value', + }, + }) + ).to.throw(); + + expect(() => + functions.runWith({ + labels: { + '1key': 'value', + }, + }) + ).to.throw(); + }); + + it('should throw an error if labels has a value that contains invalid characters', () => { + expect(() => + functions.runWith({ + labels: { + key: 'Value', + }, + }) + ).to.throw(); + + expect(() => + functions.runWith({ + labels: { + 'key ': 'va lue', + }, + }) + ).to.throw(); + }); + + it('should throw an error if a label key starts with a reserved namespace', () => { + expect(() => + functions.runWith({ + labels: { + 'firebase-foo': 'value', + }, + }) + ).to.throw(); + + expect(() => + functions.runWith({ + labels: { + 'deployment-bar': 'value', + }, + }) + ).to.throw(); + }); }); diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 93377c822..2a6c770e6 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -417,7 +417,7 @@ export function makeCloudFunction({ }, }); if (!_.isEmpty(labels)) { - trigger.labels = labels; + trigger.labels = { ...trigger.labels, ...labels }; } return trigger; }, @@ -553,5 +553,9 @@ export function optionsToTrigger(options: DeploymentOptions) { } } + if (options.labels) { + trigger.labels = options.labels; + } + return trigger; } diff --git a/src/function-builder.ts b/src/function-builder.ts index f08b16117..b421668d4 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -27,6 +27,7 @@ import { CloudFunction, EventContext } from './cloud-functions'; import { DeploymentOptions, INGRESS_SETTINGS_OPTIONS, + MAX_NUMBER_USER_LABELS, MAX_TIMEOUT_SECONDS, RuntimeOptions, SUPPORTED_REGIONS, @@ -120,6 +121,77 @@ function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean { `serviceAccount must be set to 'default', a service account email, or '{serviceAccountName}@'` ); } + + if (runtimeOptions.labels) { + // Labels must follow the rules listed in + // https://cloud.google.com/resource-manager/docs/creating-managing-labels#requirements + + if (Object.keys(runtimeOptions.labels).length > MAX_NUMBER_USER_LABELS) { + throw new Error( + `A function must not have more than ${MAX_NUMBER_USER_LABELS} user-defined labels.` + ); + } + + // We reserve the 'deployment' and 'firebase' namespaces for future feature development. + const reservedKeys = Object.keys(runtimeOptions.labels).filter( + (key) => key.startsWith('deployment') || key.startsWith('firebase') + ); + if (reservedKeys.length) { + throw new Error( + `Invalid labels: ${reservedKeys.join( + ', ' + )}. Labels may not start with reserved names 'deployment' or 'firebase'` + ); + } + + const invalidLengthKeys = Object.keys(runtimeOptions.labels).filter( + (key) => key.length < 1 || key.length > 63 + ); + if (invalidLengthKeys.length > 0) { + throw new Error( + `Invalid labels: ${invalidLengthKeys.join( + ', ' + )}. Label keys must be between 1 and 63 characters in length.` + ); + } + + const invalidLengthValues = Object.values(runtimeOptions.labels).filter( + (value) => value.length > 63 + ); + if (invalidLengthValues.length > 0) { + throw new Error( + `Invalid labels: ${invalidLengthValues.join( + ', ' + )}. Label values must be less than 64 charcters.` + ); + } + + // Keys can contain lowercase letters, foreign characters, numbers, _ or -. They must start with a letter. + const validKeyPattern = /^[\p{Ll}\p{Lo}][\p{Ll}\p{Lo}\p{N}_-]{0,62}$/u; + const invalidKeys = Object.keys(runtimeOptions.labels).filter( + (key) => !validKeyPattern.test(key) + ); + if (invalidKeys.length > 0) { + throw new Error( + `Invalid labels: ${invalidKeys.join( + ', ' + )}. Label keys can only contain lowercase letters, international characters, numbers, _ or -, and must start with a letter.` + ); + } + + // Values can contain lowercase letters, foreign characters, numbers, _ or -. + const validValuePattern = /^[\p{Ll}\p{Lo}\p{N}_-]{0,63}$/u; + const invalidValues = Object.values(runtimeOptions.labels).filter( + (value) => !validValuePattern.test(value) + ); + if (invalidValues.length > 0) { + throw new Error( + `Invalid labels: ${invalidValues.join( + ', ' + )}. Label values can only contain lowercase letters, international characters, numbers, _ or -.` + ); + } + } return true; } diff --git a/src/function-configuration.ts b/src/function-configuration.ts index 852482241..6faa7aa42 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -94,6 +94,8 @@ export const DEFAULT_FAILURE_POLICY: FailurePolicy = { retry: {}, }; +export const MAX_NUMBER_USER_LABELS = 58; + export interface RuntimeOptions { /** * Failure policy of the function, with boolean `true` being equivalent to @@ -124,29 +126,34 @@ export interface RuntimeOptions { apiVersion?: 1 | 2; /** - * Max number of actual instances allowed to be running in parallel + * Max number of actual instances allowed to be running in parallel. */ maxInstances?: number; /** - * Connect cloud function to specified VPC connector + * Connect cloud function to specified VPC connector. */ vpcConnector?: string; /** - * Egress settings for VPC connector + * Egress settings for VPC connector. */ vpcConnectorEgressSettings?: typeof VPC_EGRESS_SETTINGS_OPTIONS[number]; /** - * Specific service account for the function to run as + * Specific service account for the function to run as. */ serviceAccount?: 'default' | string; /** - * Ingress settings + * Ingress settings which control where this function can be called from. */ ingressSettings?: typeof INGRESS_SETTINGS_OPTIONS[number]; + + /** + * User labels to set on the function. + */ + labels?: Record; } export interface DeploymentOptions extends RuntimeOptions { From 88589371f69ec98723d0e0d8bcb58c6ef23a11e5 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Thu, 24 Jun 2021 13:26:13 -0700 Subject: [PATCH 314/705] Support documented case where FIREBASE_CONFIG is a json file. (#905) Support documented case where FIREBASE_CONFIG is a json file. Per https://firebase.google.com/docs/admin/setup#initialize-without-parameters the FIREBASE_CONFIG environment variable can be a name of a JSON file. Because this now requires fs.readFileSync to keep API compliance, firebaseConfg() now uses caching. --- CHANGELOG.md | 1 + spec/config.spec.ts | 50 ++++++++++++++++++++++++++------- spec/providers/database.spec.ts | 7 +++-- spec/providers/storage.spec.ts | 12 ++++++-- src/config.ts | 29 +++++++++++++++---- 5 files changed, 78 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5283950c7..900cd7d55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ - Adds support for setting user labels on functions via `runWith()`. +- Adds support for FIREBASE_CONFIG env as the name of a JSON file diff --git a/spec/config.spec.ts b/spec/config.spec.ts index b721dc3a5..1f93ea10e 100644 --- a/spec/config.spec.ts +++ b/spec/config.spec.ts @@ -21,55 +21,85 @@ // SOFTWARE. import { expect } from 'chai'; +import * as fs from 'fs'; import * as mockRequire from 'mock-require'; -import { config, firebaseConfig } from '../src/config'; +import Sinon = require('sinon'); + +import * as config from '../src/config'; describe('config()', () => { + let readFileSync: Sinon.SinonStub; + before(() => { + readFileSync = Sinon.stub(fs, 'readFileSync'); + readFileSync.throws('Unexpected call'); process.env.PWD = '/srv'; }); + after(() => { delete process.env.PWD; + Sinon.verifyAndRestore(); }); + afterEach(() => { mockRequire.stopAll(); - delete config.singleton; + delete config.config.singleton; + (config as any).firebaseConfigCache = null; delete process.env.FIREBASE_CONFIG; delete process.env.CLOUD_RUNTIME_CONFIG; }); it('loads config values from .runtimeconfig.json', () => { mockRequire('/srv/.runtimeconfig.json', { foo: 'bar', firebase: {} }); - const loaded = config(); + const loaded = config.config(); expect(loaded).to.not.have.property('firebase'); expect(loaded).to.have.property('foo', 'bar'); }); it('does not provide firebase config if .runtimeconfig.json not invalid', () => { mockRequire('/srv/.runtimeconfig.json', 'does-not-exist'); - expect(firebaseConfig()).to.be.null; + expect(config.firebaseConfig()).to.be.null; }); it('does not provide firebase config if .ruuntimeconfig.json has no firebase property', () => { mockRequire('/srv/.runtimeconfig.json', {}); - expect(firebaseConfig()).to.be.null; + expect(config.firebaseConfig()).to.be.null; }); it('loads Firebase configs from FIREBASE_CONFIG env variable', () => { process.env.FIREBASE_CONFIG = JSON.stringify({ databaseURL: 'foo@firebaseio.com', }); - expect(firebaseConfig()).to.have.property( + expect(config.firebaseConfig()).to.have.property( 'databaseURL', 'foo@firebaseio.com' ); }); + it('loads Firebase configs from FIREBASE_CONFIG env variable pointing to a file', () => { + const oldEnv = process.env; + process.env = { + ...oldEnv, + FIREBASE_CONFIG: '.firebaseconfig.json', + }; + try { + readFileSync.returns( + Buffer.from('{"databaseURL": "foo@firebaseio.com"}') + ); + expect(config.firebaseConfig()).to.have.property( + 'databaseURL', + 'foo@firebaseio.com' + ); + } finally { + process.env = oldEnv; + } + }); + it('accepts alternative locations for config file', () => { process.env.CLOUD_RUNTIME_CONFIG = 'another.json'; mockRequire('another.json', { foo: 'bar', firebase: {} }); - expect(firebaseConfig()).to.not.be.null; - expect(config()).to.have.property('foo', 'bar'); + expect(config.firebaseConfig()).to.not.be.null; + expect(config.config()).to.have.property('foo', 'bar'); }); it('accepts full JSON in env.CLOUD_RUNTIME_CONFIG', () => { @@ -77,7 +107,7 @@ describe('config()', () => { foo: 'bar', firebase: {}, }); - expect(firebaseConfig()).to.not.be.null; - expect(config()).to.have.property('foo', 'bar'); + expect(config.firebaseConfig()).to.not.be.null; + expect(config.config()).to.have.property('foo', 'bar'); }); }); diff --git a/spec/providers/database.spec.ts b/spec/providers/database.spec.ts index bf7e54b6b..10e1b0efb 100644 --- a/spec/providers/database.spec.ts +++ b/spec/providers/database.spec.ts @@ -22,6 +22,7 @@ import { expect } from 'chai'; import { apps as appsNamespace } from '../../src/apps'; +import * as config from '../../src/config'; import * as functions from '../../src/index'; import * as database from '../../src/providers/database'; import { applyChange } from '../../src/utils'; @@ -31,14 +32,14 @@ describe('Database Functions', () => { // TODO add tests for building a data or change based on the type of operation before(() => { - process.env.FIREBASE_CONFIG = JSON.stringify({ + (config as any).firebaseConfigCache = { databaseURL: 'https://subdomain.apse.firebasedatabase.app', - }); + }; appsNamespace.init(); }); after(() => { - delete process.env.FIREBASE_CONFIG; + (config as any).firebaseConfigCache = null; delete appsNamespace.singleton; }); diff --git a/spec/providers/storage.spec.ts b/spec/providers/storage.spec.ts index 18326eab8..c104a076f 100644 --- a/spec/providers/storage.spec.ts +++ b/spec/providers/storage.spec.ts @@ -21,6 +21,7 @@ // SOFTWARE. import { expect } from 'chai'; +import * as config from '../../src/config'; import { Event, EventContext } from '../../src/index'; import * as functions from '../../src/index'; import * as storage from '../../src/providers/storage'; @@ -28,13 +29,13 @@ import * as storage from '../../src/providers/storage'; describe('Storage Functions', () => { describe('ObjectBuilder', () => { before(() => { - process.env.FIREBASE_CONFIG = JSON.stringify({ + (config as any).firebaseConfigCache = { storageBucket: 'bucket', - }); + }; }); after(() => { - delete process.env.FIREBASE_CONFIG; + (config as any).firebaseConfigcache = null; }); it('should allow both region and runtime options to be set', () => { @@ -542,6 +543,11 @@ describe('Storage Functions', () => { }); describe('process.env.FIREBASE_CONFIG not set', () => { + beforeEach(() => { + (config as any).firebaseConfigCache = null; + delete process.env.FIREBASE_CONFIG; + }); + it('should not throw if __trigger is not accessed', () => { expect(() => storage.object().onArchive(() => null)).to.not.throw(Error); }); diff --git a/src/config.ts b/src/config.ts index 8925ff940..c49e7d03f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -20,9 +20,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import * as firebase from 'firebase-admin'; +import * as fs from 'fs'; import * as path from 'path'; +import * as firebase from 'firebase-admin'; + export function config(): config.Config { if (typeof config.singleton === 'undefined') { init(); @@ -50,18 +52,34 @@ export namespace config { export let singleton: config.Config; } +/** @hidden */ +export let firebaseConfigCache: firebase.AppOptions | null = null; + /** @hidden */ export function firebaseConfig(): firebase.AppOptions | null { - const env = process.env.FIREBASE_CONFIG; + if (firebaseConfigCache) { + return firebaseConfigCache; + } + + let env = process.env.FIREBASE_CONFIG; if (env) { - return JSON.parse(env); + // Firebase Tools will always use a JSON blob in prod, but docs + // explicitly state that the user can set the env to a file: + // https://firebase.google.com/docs/admin/setup#initialize-without-parameters + if (!env.startsWith('{')) { + env = fs.readFileSync(path.join(process.env.PWD, env)).toString('utf8'); + } + + firebaseConfigCache = JSON.parse(env); + return firebaseConfigCache; } // Could have Runtime Config with Firebase in it as an ENV value. try { const config = JSON.parse(process.env.CLOUD_RUNTIME_CONFIG); if (config.firebase) { - return config.firebase; + firebaseConfigCache = config.firebase; + return firebaseConfigCache; } } catch (e) { // Do nothing @@ -74,7 +92,8 @@ export function firebaseConfig(): firebase.AppOptions | null { path.join(process.env.PWD, '.runtimeconfig.json'); const config = require(configPath); if (config.firebase) { - return config.firebase; + firebaseConfigCache = config.firebase; + return firebaseConfigCache; } } catch (e) { // Do nothing From 4028acc07d11cd81cd56ee816bf16f025c0d10df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joel=20Nordstr=C3=B6m?= Date: Thu, 24 Jun 2021 22:51:15 +0200 Subject: [PATCH 315/705] Fall back to updateTime if no readTime exists (#843) Fixes #599 --- spec/providers/firestore.spec.ts | 13 +++++++++++++ src/providers/firestore.ts | 5 ++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/spec/providers/firestore.spec.ts b/spec/providers/firestore.spec.ts index 42cdf97f7..271701108 100644 --- a/spec/providers/firestore.spec.ts +++ b/spec/providers/firestore.spec.ts @@ -566,6 +566,7 @@ describe('Firestore Functions', () => { describe('Other DocumentSnapshot methods', () => { let snapshot: FirebaseFirestore.DocumentSnapshot; + let newSnapshot: FirebaseFirestore.DocumentSnapshot; before(() => { snapshot = firestore.snapshotConstructor( @@ -579,6 +580,16 @@ describe('Firestore Functions', () => { }, }) ); + newSnapshot = firestore.snapshotConstructor( + makeEvent({ + value: { + fields: { key: { integerValue: '2' } }, + createTime: '2017-06-17T14:45:17.876479Z', + updateTime: '2017-06-17T14:45:17.876479Z', + name: 'projects/pid/databases/(default)/documents/collection/124', + }, + }) + ); }); it('should support #exists', () => { @@ -606,6 +617,8 @@ describe('Firestore Functions', () => { it('should support #readTime', () => { expect(snapshot.readTime.seconds).to.be.a('number'); expect(snapshot.readTime.nanoseconds).to.be.a('number'); + expect(newSnapshot.readTime.seconds).to.be.a('number'); + expect(newSnapshot.readTime.nanoseconds).to.be.a('number'); }); }); diff --git a/src/providers/firestore.ts b/src/providers/firestore.ts index f91e17306..58a6de925 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -155,7 +155,10 @@ export function snapshotConstructor(event: Event): DocumentSnapshot { event.context.resource.name, 'value' ); - const readTime = dateToTimestampProto(_.get(event, 'data.value.readTime')); + const timeString = + _.get(event, 'data.value.readTime') ?? + _.get(event, 'data.value.updateTime'); + const readTime = dateToTimestampProto(timeString); return firestoreInstance.snapshot_(valueProto, readTime, 'json'); } From 33a68eaa5b228ac39c1f0b3a9610dc7457d4e921 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Thu, 8 Jul 2021 14:52:26 -0700 Subject: [PATCH 316/705] Create package exports (#906) * Create version packages * Changelog * Make exports list always use explicit file names --- CHANGELOG.md | 1 + package.json | 8 +++ spec/{ => v1}/apps.spec.ts | 2 +- spec/{ => v1}/cloud-functions.spec.ts | 2 +- spec/{ => v1}/config.spec.ts | 2 +- spec/{ => v1}/function-builder.spec.ts | 2 +- .../providers/analytics.spec.input.ts | 2 +- spec/{ => v1}/providers/analytics.spec.ts | 6 +- spec/{ => v1}/providers/auth.spec.ts | 10 ++- spec/{ => v1}/providers/database.spec.ts | 10 +-- spec/{ => v1}/providers/firestore.spec.ts | 4 +- spec/{ => v1}/providers/https.spec.ts | 10 +-- spec/{ => v1}/providers/pubsub.spec.ts | 6 +- spec/{ => v1}/providers/remoteConfig.spec.ts | 6 +- spec/{ => v1}/providers/storage.spec.ts | 8 +-- spec/{ => v1}/providers/testLab.spec.ts | 2 +- spec/{ => v1}/setup.spec.ts | 2 +- spec/{ => v1}/utils.spec.ts | 2 +- src/index.ts | 63 +------------------ src/{logger.ts => logger/index.ts} | 2 +- src/{ => v1}/apps.ts | 0 src/{ => v1}/cloud-functions.ts | 2 +- src/{ => v1}/config.ts | 0 src/{ => v1}/encoder.ts | 0 src/{ => v1}/function-builder.ts | 0 src/{ => v1}/function-configuration.ts | 0 src/{ => v1}/handler-builder.ts | 0 src/v1/index.ts | 62 ++++++++++++++++++ src/{ => v1}/providers/analytics.ts | 0 src/{ => v1}/providers/auth.ts | 0 src/{ => v1}/providers/database.ts | 2 +- src/{ => v1}/providers/firestore.ts | 0 src/{ => v1}/providers/https.ts | 2 +- src/{ => v1}/providers/pubsub.ts | 0 src/{ => v1}/providers/remoteConfig.ts | 0 src/{ => v1}/providers/storage.ts | 0 src/{ => v1}/providers/testLab.ts | 0 src/{ => v1}/setup.ts | 2 +- src/{ => v1}/utils.ts | 0 tsconfig.release.json | 2 +- 40 files changed, 118 insertions(+), 104 deletions(-) rename spec/{ => v1}/apps.spec.ts (98%) rename spec/{ => v1}/cloud-functions.spec.ts (99%) rename spec/{ => v1}/config.spec.ts (98%) rename spec/{ => v1}/function-builder.spec.ts (99%) rename spec/{ => v1}/providers/analytics.spec.input.ts (98%) rename spec/{ => v1}/providers/analytics.spec.ts (98%) rename spec/{ => v1}/providers/auth.spec.ts (97%) rename spec/{ => v1}/providers/database.spec.ts (98%) rename spec/{ => v1}/providers/firestore.spec.ts (99%) rename spec/{ => v1}/providers/https.spec.ts (98%) rename spec/{ => v1}/providers/pubsub.spec.ts (98%) rename spec/{ => v1}/providers/remoteConfig.spec.ts (96%) rename spec/{ => v1}/providers/storage.spec.ts (98%) rename spec/{ => v1}/providers/testLab.spec.ts (99%) rename spec/{ => v1}/setup.spec.ts (97%) rename spec/{ => v1}/utils.spec.ts (97%) rename src/{logger.ts => logger/index.ts} (99%) rename src/{ => v1}/apps.ts (100%) rename src/{ => v1}/cloud-functions.ts (99%) rename src/{ => v1}/config.ts (100%) rename src/{ => v1}/encoder.ts (100%) rename src/{ => v1}/function-builder.ts (100%) rename src/{ => v1}/function-configuration.ts (100%) rename src/{ => v1}/handler-builder.ts (100%) create mode 100644 src/v1/index.ts rename src/{ => v1}/providers/analytics.ts (100%) rename src/{ => v1}/providers/auth.ts (100%) rename src/{ => v1}/providers/database.ts (99%) rename src/{ => v1}/providers/firestore.ts (100%) rename src/{ => v1}/providers/https.ts (99%) rename src/{ => v1}/providers/pubsub.ts (100%) rename src/{ => v1}/providers/remoteConfig.ts (100%) rename src/{ => v1}/providers/storage.ts (100%) rename src/{ => v1}/providers/testLab.ts (100%) rename src/{ => v1}/setup.ts (98%) rename src/{ => v1}/utils.ts (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 900cd7d55..adfc05c3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ - Adds support for setting user labels on functions via `runWith()`. - Adds support for FIREBASE_CONFIG env as the name of a JSON file +- Formalize module exports. Loggers can now be accessed at 'firebase-functions/logger' and 'firebase-functions/logger/compat' diff --git a/package.json b/package.json index a9c86bf87..413a81c45 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,14 @@ ], "main": "lib/index.js", "types": "lib/index.d.ts", + "exports": { + ".": "./lib/index.js", + "./v1": "./lib/v1/index.js", + "./logger": "./lib/logger/index.js", + "./logger/compat": "./lib/logger/compat.js", + "./lib/logger": "./lib/logger/index.js", + "./lib/logger/compat": "./lib/logger/compat.js" + }, "publishConfig": { "registry": "https://wombat-dressing-room.appspot.com" }, diff --git a/spec/apps.spec.ts b/spec/v1/apps.spec.ts similarity index 98% rename from spec/apps.spec.ts rename to spec/v1/apps.spec.ts index 9fff98c71..33c263f4f 100644 --- a/spec/apps.spec.ts +++ b/spec/v1/apps.spec.ts @@ -21,7 +21,7 @@ // SOFTWARE. import { expect } from 'chai'; -import { apps as appsNamespace } from '../src/apps'; +import { apps as appsNamespace } from '../../src/v1/apps'; import * as firebase from 'firebase-admin'; import * as _ from 'lodash'; diff --git a/spec/cloud-functions.spec.ts b/spec/v1/cloud-functions.spec.ts similarity index 99% rename from spec/cloud-functions.spec.ts rename to spec/v1/cloud-functions.spec.ts index ab17fbc3e..48ecb3072 100644 --- a/spec/cloud-functions.spec.ts +++ b/spec/v1/cloud-functions.spec.ts @@ -29,7 +29,7 @@ import { EventContext, makeCloudFunction, MakeCloudFunctionArgs, -} from '../src/cloud-functions'; +} from '../../src/v1/cloud-functions'; describe('makeCloudFunction', () => { const cloudFunctionArgs: MakeCloudFunctionArgs = { diff --git a/spec/config.spec.ts b/spec/v1/config.spec.ts similarity index 98% rename from spec/config.spec.ts rename to spec/v1/config.spec.ts index 1f93ea10e..8a40fe523 100644 --- a/spec/config.spec.ts +++ b/spec/v1/config.spec.ts @@ -25,7 +25,7 @@ import * as fs from 'fs'; import * as mockRequire from 'mock-require'; import Sinon = require('sinon'); -import * as config from '../src/config'; +import * as config from '../../src/v1/config'; describe('config()', () => { let readFileSync: Sinon.SinonStub; diff --git a/spec/function-builder.spec.ts b/spec/v1/function-builder.spec.ts similarity index 99% rename from spec/function-builder.spec.ts rename to spec/v1/function-builder.spec.ts index 106f874ea..431d018ac 100644 --- a/spec/function-builder.spec.ts +++ b/spec/v1/function-builder.spec.ts @@ -22,7 +22,7 @@ import { expect } from 'chai'; -import * as functions from '../src/index'; +import * as functions from '../../src/v1'; describe('FunctionBuilder', () => { before(() => { diff --git a/spec/providers/analytics.spec.input.ts b/spec/v1/providers/analytics.spec.input.ts similarity index 98% rename from spec/providers/analytics.spec.input.ts rename to spec/v1/providers/analytics.spec.input.ts index bd9768b16..74ad65a93 100644 --- a/spec/providers/analytics.spec.input.ts +++ b/spec/v1/providers/analytics.spec.input.ts @@ -21,7 +21,7 @@ // SOFTWARE. /* tslint:disable:max-line-length */ -import { AnalyticsEvent } from '../../src/providers/analytics'; +import { AnalyticsEvent } from '../../../src/v1/providers/analytics'; // A payload, as it might arrive over the wire. Every possible field is filled out at least once. export const fullPayload = JSON.parse(`{ diff --git a/spec/providers/analytics.spec.ts b/spec/v1/providers/analytics.spec.ts similarity index 98% rename from spec/providers/analytics.spec.ts rename to spec/v1/providers/analytics.spec.ts index 4a5e84a0c..a6e6b0af6 100644 --- a/spec/providers/analytics.spec.ts +++ b/spec/v1/providers/analytics.spec.ts @@ -22,9 +22,9 @@ import { expect } from 'chai'; -import { Event, EventContext } from '../../src/cloud-functions'; -import * as functions from '../../src/index'; -import * as analytics from '../../src/providers/analytics'; +import * as functions from '../../../src/v1'; +import { Event, EventContext } from '../../../src/v1/cloud-functions'; +import * as analytics from '../../../src/v1/providers/analytics'; import * as analytics_spec_input from './analytics.spec.input'; describe('Analytics Functions', () => { diff --git a/spec/providers/auth.spec.ts b/spec/v1/providers/auth.spec.ts similarity index 97% rename from spec/providers/auth.spec.ts rename to spec/v1/providers/auth.spec.ts index bb2ab7761..df0db01f7 100644 --- a/spec/providers/auth.spec.ts +++ b/spec/v1/providers/auth.spec.ts @@ -23,9 +23,13 @@ import { expect } from 'chai'; import * as firebase from 'firebase-admin'; -import { CloudFunction, Event, EventContext } from '../../src/cloud-functions'; -import * as functions from '../../src/index'; -import * as auth from '../../src/providers/auth'; +import * as functions from '../../../src/index'; +import { + CloudFunction, + Event, + EventContext, +} from '../../../src/v1/cloud-functions'; +import * as auth from '../../../src/v1/providers/auth'; describe('Auth Functions', () => { const event: Event = { diff --git a/spec/providers/database.spec.ts b/spec/v1/providers/database.spec.ts similarity index 98% rename from spec/providers/database.spec.ts rename to spec/v1/providers/database.spec.ts index 10e1b0efb..27d5854c6 100644 --- a/spec/providers/database.spec.ts +++ b/spec/v1/providers/database.spec.ts @@ -21,11 +21,11 @@ // SOFTWARE. import { expect } from 'chai'; -import { apps as appsNamespace } from '../../src/apps'; -import * as config from '../../src/config'; -import * as functions from '../../src/index'; -import * as database from '../../src/providers/database'; -import { applyChange } from '../../src/utils'; +import { apps as appsNamespace } from '../../../src/v1/apps'; +import * as config from '../../../src/v1/config'; +import * as functions from '../../../src/v1/index'; +import * as database from '../../../src/v1/providers/database'; +import { applyChange } from '../../../src/v1/utils'; describe('Database Functions', () => { describe('DatabaseBuilder', () => { diff --git a/spec/providers/firestore.spec.ts b/spec/v1/providers/firestore.spec.ts similarity index 99% rename from spec/providers/firestore.spec.ts rename to spec/v1/providers/firestore.spec.ts index 271701108..07014aeb0 100644 --- a/spec/providers/firestore.spec.ts +++ b/spec/v1/providers/firestore.spec.ts @@ -24,8 +24,8 @@ import { expect } from 'chai'; import * as admin from 'firebase-admin'; import * as _ from 'lodash'; -import * as functions from '../../src/index'; -import * as firestore from '../../src/providers/firestore'; +import * as functions from '../../../src/index'; +import * as firestore from '../../../src/v1/providers/firestore'; describe('Firestore Functions', () => { function constructValue(fields: any) { diff --git a/spec/providers/https.spec.ts b/spec/v1/providers/https.spec.ts similarity index 98% rename from spec/providers/https.spec.ts rename to spec/v1/providers/https.spec.ts index 128ea0058..b34e29e6e 100644 --- a/spec/providers/https.spec.ts +++ b/spec/v1/providers/https.spec.ts @@ -24,10 +24,10 @@ import { expect } from 'chai'; import * as express from 'express'; import * as firebase from 'firebase-admin'; import * as _ from 'lodash'; -import { apps as appsNamespace } from '../../src/apps'; -import * as functions from '../../src/index'; -import * as https from '../../src/providers/https'; -import * as mocks from '../fixtures/credential/key.json'; +import * as functions from '../../../src/index'; +import { apps as appsNamespace } from '../../../src/v1/apps'; +import * as https from '../../../src/v1/providers/https'; +import * as mocks from '../../fixtures/credential/key.json'; import { expectedResponseHeaders, generateAppCheckToken, @@ -36,7 +36,7 @@ import { mockFetchPublicKeys, MockRequest, mockRequest, -} from '../fixtures/mockrequest'; +} from '../../fixtures/mockrequest'; describe('CloudHttpsBuilder', () => { describe('#onRequest', () => { diff --git a/spec/providers/pubsub.spec.ts b/spec/v1/providers/pubsub.spec.ts similarity index 98% rename from spec/providers/pubsub.spec.ts rename to spec/v1/providers/pubsub.spec.ts index f64cd2119..3d0569057 100644 --- a/spec/providers/pubsub.spec.ts +++ b/spec/v1/providers/pubsub.spec.ts @@ -21,9 +21,9 @@ // SOFTWARE. import { expect } from 'chai'; -import { Event } from '../../src/index'; -import * as functions from '../../src/index'; -import * as pubsub from '../../src/providers/pubsub'; +import { Event } from '../../../src/index'; +import * as functions from '../../../src/index'; +import * as pubsub from '../../../src/v1/providers/pubsub'; describe('Pubsub Functions', () => { describe('pubsub.Message', () => { diff --git a/spec/providers/remoteConfig.spec.ts b/spec/v1/providers/remoteConfig.spec.ts similarity index 96% rename from spec/providers/remoteConfig.spec.ts rename to spec/v1/providers/remoteConfig.spec.ts index ef6d68572..2ccfe41fd 100644 --- a/spec/providers/remoteConfig.spec.ts +++ b/spec/v1/providers/remoteConfig.spec.ts @@ -22,14 +22,14 @@ import { expect } from 'chai'; import * as _ from 'lodash'; +import * as functions from '../../../src/index'; import { CloudFunction, Event, EventContext, TriggerAnnotated, -} from '../../src/cloud-functions'; -import * as functions from '../../src/index'; -import * as remoteConfig from '../../src/providers/remoteConfig'; +} from '../../../src/v1/cloud-functions'; +import * as remoteConfig from '../../../src/v1/providers/remoteConfig'; describe('RemoteConfig Functions', () => { function constructVersion() { diff --git a/spec/providers/storage.spec.ts b/spec/v1/providers/storage.spec.ts similarity index 98% rename from spec/providers/storage.spec.ts rename to spec/v1/providers/storage.spec.ts index c104a076f..aee2d72a9 100644 --- a/spec/providers/storage.spec.ts +++ b/spec/v1/providers/storage.spec.ts @@ -21,10 +21,10 @@ // SOFTWARE. import { expect } from 'chai'; -import * as config from '../../src/config'; -import { Event, EventContext } from '../../src/index'; -import * as functions from '../../src/index'; -import * as storage from '../../src/providers/storage'; +import { Event, EventContext } from '../../../src/v1'; +import * as functions from '../../../src/v1'; +import * as config from '../../../src/v1/config'; +import * as storage from '../../../src/v1/providers/storage'; describe('Storage Functions', () => { describe('ObjectBuilder', () => { diff --git a/spec/providers/testLab.spec.ts b/spec/v1/providers/testLab.spec.ts similarity index 99% rename from spec/providers/testLab.spec.ts rename to spec/v1/providers/testLab.spec.ts index 8a70cf680..5584bfbf7 100644 --- a/spec/providers/testLab.spec.ts +++ b/spec/v1/providers/testLab.spec.ts @@ -22,7 +22,7 @@ import { expect } from 'chai'; -import * as testLab from '../../src/providers/testLab'; +import * as testLab from '../../../src/v1/providers/testLab'; describe('Test Lab Functions', () => { describe('#onComplete', () => { diff --git a/spec/setup.spec.ts b/spec/v1/setup.spec.ts similarity index 97% rename from spec/setup.spec.ts rename to spec/v1/setup.spec.ts index 289b90fc1..20ca26d12 100644 --- a/spec/setup.spec.ts +++ b/spec/v1/setup.spec.ts @@ -21,7 +21,7 @@ // SOFTWARE. import { expect } from 'chai'; -import { setup } from '../src/setup'; +import { setup } from '../../src/v1/setup'; describe('setup()', () => { afterEach(() => { diff --git a/spec/utils.spec.ts b/spec/v1/utils.spec.ts similarity index 97% rename from spec/utils.spec.ts rename to spec/v1/utils.spec.ts index 51a8478fa..119d203f6 100644 --- a/spec/utils.spec.ts +++ b/spec/v1/utils.spec.ts @@ -21,7 +21,7 @@ // SOFTWARE. import { expect } from 'chai'; -import { applyChange } from '../src/utils'; +import { applyChange } from '../../src/v1/utils'; describe('utils', () => { describe('.applyChange(from: any, to: any): any', () => { diff --git a/src/index.ts b/src/index.ts index dbb42003c..5b98253d9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,62 +1 @@ -// The MIT License (MIT) -// -// Copyright (c) 2017 Firebase -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -// Providers: -import * as analytics from './providers/analytics'; -import * as auth from './providers/auth'; -import * as database from './providers/database'; -import * as firestore from './providers/firestore'; -import * as https from './providers/https'; -import * as pubsub from './providers/pubsub'; -import * as remoteConfig from './providers/remoteConfig'; -import * as storage from './providers/storage'; -import * as testLab from './providers/testLab'; - -import * as apps from './apps'; -import { handler } from './handler-builder'; -import * as logger from './logger'; -import { setup } from './setup'; - -const app = apps.apps(); - -export { - analytics, - app, - auth, - database, - firestore, - handler, - https, - pubsub, - remoteConfig, - storage, - testLab, - logger, -}; - -// Exported root types: -export * from './cloud-functions'; -export * from './config'; -export * from './function-builder'; -export * from './function-configuration'; - -setup(); +export * from './v1'; diff --git a/src/logger.ts b/src/logger/index.ts similarity index 99% rename from src/logger.ts rename to src/logger/index.ts index 1dd109179..24483087c 100644 --- a/src/logger.ts +++ b/src/logger/index.ts @@ -4,7 +4,7 @@ import { CONSOLE_SEVERITY, SUPPORTS_STRUCTURED_LOGS, UNPATCHED_CONSOLE, -} from './logger/common'; +} from './common'; /** * `LogSeverity` indicates the detailed severity of the log entry. See [LogSeverity](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity). diff --git a/src/apps.ts b/src/v1/apps.ts similarity index 100% rename from src/apps.ts rename to src/v1/apps.ts diff --git a/src/cloud-functions.ts b/src/v1/cloud-functions.ts similarity index 99% rename from src/cloud-functions.ts rename to src/v1/cloud-functions.ts index 2a6c770e6..ef59c0422 100644 --- a/src/cloud-functions.ts +++ b/src/v1/cloud-functions.ts @@ -22,13 +22,13 @@ import { Request, Response } from 'express'; import * as _ from 'lodash'; +import { warn } from '../logger'; import { DEFAULT_FAILURE_POLICY, DeploymentOptions, FailurePolicy, Schedule, } from './function-configuration'; -import { warn } from './logger'; export { Request, Response }; /** @hidden */ diff --git a/src/config.ts b/src/v1/config.ts similarity index 100% rename from src/config.ts rename to src/v1/config.ts diff --git a/src/encoder.ts b/src/v1/encoder.ts similarity index 100% rename from src/encoder.ts rename to src/v1/encoder.ts diff --git a/src/function-builder.ts b/src/v1/function-builder.ts similarity index 100% rename from src/function-builder.ts rename to src/v1/function-builder.ts diff --git a/src/function-configuration.ts b/src/v1/function-configuration.ts similarity index 100% rename from src/function-configuration.ts rename to src/v1/function-configuration.ts diff --git a/src/handler-builder.ts b/src/v1/handler-builder.ts similarity index 100% rename from src/handler-builder.ts rename to src/v1/handler-builder.ts diff --git a/src/v1/index.ts b/src/v1/index.ts new file mode 100644 index 000000000..ec83eeab5 --- /dev/null +++ b/src/v1/index.ts @@ -0,0 +1,62 @@ +// The MIT License (MIT) +// +// Copyright (c) 2017 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Providers: +import * as analytics from './providers/analytics'; +import * as auth from './providers/auth'; +import * as database from './providers/database'; +import * as firestore from './providers/firestore'; +import * as https from './providers/https'; +import * as pubsub from './providers/pubsub'; +import * as remoteConfig from './providers/remoteConfig'; +import * as storage from './providers/storage'; +import * as testLab from './providers/testLab'; + +import * as logger from '../logger'; +import * as apps from './apps'; +import { handler } from './handler-builder'; +import { setup } from './setup'; + +const app = apps.apps(); + +export { + analytics, + app, + auth, + database, + firestore, + handler, + https, + pubsub, + remoteConfig, + storage, + testLab, + logger, +}; + +// Exported root types: +export * from './cloud-functions'; +export * from './config'; +export * from './function-builder'; +export * from './function-configuration'; + +setup(); diff --git a/src/providers/analytics.ts b/src/v1/providers/analytics.ts similarity index 100% rename from src/providers/analytics.ts rename to src/v1/providers/analytics.ts diff --git a/src/providers/auth.ts b/src/v1/providers/auth.ts similarity index 100% rename from src/providers/auth.ts rename to src/v1/providers/auth.ts diff --git a/src/providers/database.ts b/src/v1/providers/database.ts similarity index 99% rename from src/providers/database.ts rename to src/v1/providers/database.ts index 76252d515..372419169 100644 --- a/src/providers/database.ts +++ b/src/v1/providers/database.ts @@ -22,6 +22,7 @@ import * as firebase from 'firebase-admin'; import * as _ from 'lodash'; +import { joinPath, normalizePath, pathParts } from '../../utilities/path'; import { apps } from '../apps'; import { Change, @@ -32,7 +33,6 @@ import { } from '../cloud-functions'; import { firebaseConfig } from '../config'; import { DeploymentOptions } from '../function-configuration'; -import { joinPath, normalizePath, pathParts } from '../utilities/path'; import { applyChange } from '../utils'; /** @hidden */ diff --git a/src/providers/firestore.ts b/src/v1/providers/firestore.ts similarity index 100% rename from src/providers/firestore.ts rename to src/v1/providers/firestore.ts diff --git a/src/providers/https.ts b/src/v1/providers/https.ts similarity index 99% rename from src/providers/https.ts rename to src/v1/providers/https.ts index c6167c50a..c6d5cc8a0 100644 --- a/src/providers/https.ts +++ b/src/v1/providers/https.ts @@ -25,10 +25,10 @@ import * as express from 'express'; import * as firebase from 'firebase-admin'; import * as _ from 'lodash'; +import { error, info, warn } from '../../logger'; import { apps } from '../apps'; import { HttpsFunction, optionsToTrigger, Runnable } from '../cloud-functions'; import { DeploymentOptions } from '../function-configuration'; -import { error, info, warn } from '../logger'; /** @hidden */ export interface Request extends express.Request { diff --git a/src/providers/pubsub.ts b/src/v1/providers/pubsub.ts similarity index 100% rename from src/providers/pubsub.ts rename to src/v1/providers/pubsub.ts diff --git a/src/providers/remoteConfig.ts b/src/v1/providers/remoteConfig.ts similarity index 100% rename from src/providers/remoteConfig.ts rename to src/v1/providers/remoteConfig.ts diff --git a/src/providers/storage.ts b/src/v1/providers/storage.ts similarity index 100% rename from src/providers/storage.ts rename to src/v1/providers/storage.ts diff --git a/src/providers/testLab.ts b/src/v1/providers/testLab.ts similarity index 100% rename from src/providers/testLab.ts rename to src/v1/providers/testLab.ts diff --git a/src/setup.ts b/src/v1/setup.ts similarity index 98% rename from src/setup.ts rename to src/v1/setup.ts index 6a9db702d..6b0eb3506 100644 --- a/src/setup.ts +++ b/src/v1/setup.ts @@ -21,8 +21,8 @@ // SOFTWARE. /** @hidden */ +import { warn } from '../logger'; import { firebaseConfig } from './config'; -import { warn } from './logger'; // Set up for config and vars export function setup() { diff --git a/src/utils.ts b/src/v1/utils.ts similarity index 100% rename from src/utils.ts rename to src/v1/utils.ts diff --git a/tsconfig.release.json b/tsconfig.release.json index 1a45bd58b..b8f2632fd 100644 --- a/tsconfig.release.json +++ b/tsconfig.release.json @@ -10,5 +10,5 @@ "target": "es2018", "typeRoots": ["./node_modules/@types"] }, - "files": ["./src/index.ts", "./src/logger.ts", "./src/logger/compat.ts"] + "files": ["./src/index.ts", "./src/logger/index.ts", "./src/logger/compat.ts"] } From 57ca58a1e3b8a4db652bc31e477c8b95fa5899a3 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Mon, 12 Jul 2021 11:27:20 -0700 Subject: [PATCH 317/705] RemoteConfig can be loaded in windows CMD.exe (#913) Favors using process.cwd over process.env.PWD as the latter is POSIX only (I might need to go check some of my code in firebase-tools!). While I was at it, I fixed a probably useless but nasty vulnerability where a malformed .runtimeconfig.json file would allow arbitrary code execution. --- CHANGELOG.md | 1 + spec/v1/config.spec.ts | 29 +++++++++++++++++++---------- src/v1/config.ts | 10 ++++++---- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adfc05c3a..45844cd0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ - Adds support for setting user labels on functions via `runWith()`. - Adds support for FIREBASE_CONFIG env as the name of a JSON file - Formalize module exports. Loggers can now be accessed at 'firebase-functions/logger' and 'firebase-functions/logger/compat' +- Fixes an issue where Remote Config coiuld not be emulated in Windows machines on the classic Command Prompt. diff --git a/spec/v1/config.spec.ts b/spec/v1/config.spec.ts index 8a40fe523..d1deea4fd 100644 --- a/spec/v1/config.spec.ts +++ b/spec/v1/config.spec.ts @@ -22,27 +22,27 @@ import { expect } from 'chai'; import * as fs from 'fs'; -import * as mockRequire from 'mock-require'; +import * as process from 'process'; import Sinon = require('sinon'); import * as config from '../../src/v1/config'; describe('config()', () => { let readFileSync: Sinon.SinonStub; + let cwdStub: Sinon.SinonStub; before(() => { readFileSync = Sinon.stub(fs, 'readFileSync'); readFileSync.throws('Unexpected call'); - process.env.PWD = '/srv'; + cwdStub = Sinon.stub(process, 'cwd'); + cwdStub.returns('/srv'); }); after(() => { - delete process.env.PWD; Sinon.verifyAndRestore(); }); afterEach(() => { - mockRequire.stopAll(); delete config.config.singleton; (config as any).firebaseConfigCache = null; delete process.env.FIREBASE_CONFIG; @@ -50,19 +50,27 @@ describe('config()', () => { }); it('loads config values from .runtimeconfig.json', () => { - mockRequire('/srv/.runtimeconfig.json', { foo: 'bar', firebase: {} }); + const json = JSON.stringify({ + foo: 'bar', + firebase: {}, + }); + readFileSync + .withArgs('/srv/.runtimeconfig.json') + .returns(Buffer.from(json)); const loaded = config.config(); expect(loaded).to.not.have.property('firebase'); expect(loaded).to.have.property('foo', 'bar'); }); it('does not provide firebase config if .runtimeconfig.json not invalid', () => { - mockRequire('/srv/.runtimeconfig.json', 'does-not-exist'); + readFileSync.withArgs('/srv/.runtimeconfig.json').returns('invalid JSON'); expect(config.firebaseConfig()).to.be.null; }); it('does not provide firebase config if .ruuntimeconfig.json has no firebase property', () => { - mockRequire('/srv/.runtimeconfig.json', {}); + readFileSync + .withArgs('/srv/.runtimeconfig.json') + .returns(Buffer.from('{}')); expect(config.firebaseConfig()).to.be.null; }); @@ -78,7 +86,7 @@ describe('config()', () => { it('loads Firebase configs from FIREBASE_CONFIG env variable pointing to a file', () => { const oldEnv = process.env; - process.env = { + (process as any).env = { ...oldEnv, FIREBASE_CONFIG: '.firebaseconfig.json', }; @@ -91,13 +99,14 @@ describe('config()', () => { 'foo@firebaseio.com' ); } finally { - process.env = oldEnv; + (process as any).env = oldEnv; } }); it('accepts alternative locations for config file', () => { process.env.CLOUD_RUNTIME_CONFIG = 'another.json'; - mockRequire('another.json', { foo: 'bar', firebase: {} }); + const json = JSON.stringify({ foo: 'bar', firebase: {} }); + readFileSync.withArgs('another.json').returns(Buffer.from(json)); expect(config.firebaseConfig()).to.not.be.null; expect(config.config()).to.have.property('foo', 'bar'); }); diff --git a/src/v1/config.ts b/src/v1/config.ts index c49e7d03f..f0793957d 100644 --- a/src/v1/config.ts +++ b/src/v1/config.ts @@ -89,8 +89,9 @@ export function firebaseConfig(): firebase.AppOptions | null { try { const configPath = process.env.CLOUD_RUNTIME_CONFIG || - path.join(process.env.PWD, '.runtimeconfig.json'); - const config = require(configPath); + path.join(process.cwd(), '.runtimeconfig.json'); + const contents = fs.readFileSync(configPath); + const config = JSON.parse(contents.toString('utf8')); if (config.firebase) { firebaseConfigCache = config.firebase; return firebaseConfigCache; @@ -115,8 +116,9 @@ function init() { try { const configPath = process.env.CLOUD_RUNTIME_CONFIG || - path.join(process.env.PWD, '.runtimeconfig.json'); - const parsed = require(configPath); + path.join(process.cwd(), '.runtimeconfig.json'); + const contents = fs.readFileSync(configPath); + const parsed = JSON.parse(contents.toString('utf8')); delete parsed.firebase; config.singleton = parsed; return; From 5e7fb2cf68821b26fcb8519f88a270577bfe6f11 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Tue, 13 Jul 2021 16:19:17 -0700 Subject: [PATCH 318/705] Fixes docgen after file renames. (#914) --- .gitignore | 1 + docgen/content-sources/toc.yaml | 120 +- docgen/generate-docs.js | 95 +- docgen/theme/helpers/cleanBreadcrumb.js | 4 + docgen/theme/partials/breadcrumb.hbs | 2 +- docgen/theme/partials/header.hbs | 2 +- package-lock.json | 5923 ++++++++++++++++++++++- src/logger/index.ts | 1 + 8 files changed, 6020 insertions(+), 128 deletions(-) create mode 100644 docgen/theme/helpers/cleanBreadcrumb.js diff --git a/.gitignore b/.gitignore index ba3cdeec5..b5a890408 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ node_modules npm-debug.log typings yarn.lock +.DS_Store diff --git a/docgen/content-sources/toc.yaml b/docgen/content-sources/toc.yaml index e970fc80c..ac430151b 100644 --- a/docgen/content-sources/toc.yaml +++ b/docgen/content-sources/toc.yaml @@ -1,148 +1,148 @@ toc: - title: 'functions' - path: /docs/reference/functions/cloud_functions_.html + path: /docs/reference/functions/v1_cloud_functions_.html section: - title: 'CloudFunction' - path: /docs/reference/functions/cloud_functions_.html#cloudfunction + path: /docs/reference/functions/v1_cloud_functions_.html#cloudfunction - title: 'HttpsFunction' - path: /docs/reference/functions/cloud_functions_.html#httpsfunction + path: /docs/reference/functions/v1_cloud_functions_.html#httpsfunction - title: 'EventContext' - path: /docs/reference/functions/cloud_functions_.eventcontext.html + path: /docs/reference/functions/v1_cloud_functions_.eventcontext.html - title: 'FunctionBuilder' - path: /docs/reference/functions/function_builder_.functionbuilder.html + path: /docs/reference/functions/v1_function_builder_.functionbuilder.html - title: 'Change' - path: /docs/reference/functions/cloud_functions_.change.html + path: /docs/reference/functions/v1_cloud_functions_.change.html - title: 'ChangeJson' - path: /docs/reference/functions/cloud_functions_.changejson.html + path: /docs/reference/functions/v1_cloud_functions_.changejson.html - title: 'functions.config' - path: /docs/reference/functions/config_.html + path: /docs/reference/functions/v1_config_.html section: - title: 'Config' - path: /docs/reference/functions/config_.config.html + path: /docs/reference/functions/v1_config_.config.html - title: 'config.Config' - path: /docs/reference/functions/config_.config.config.html + path: /docs/reference/functions/v1_config_.config.config.html - title: 'functions.function-configuration' - path: /docs/reference/functions/function_configuration_.html + path: /docs/reference/functions/v1_function_configuration_.html section: - title: 'config.DeploymentOptions' - path: /docs/reference/functions/function_configuration_.deploymentoptions.html + path: /docs/reference/functions/v1_function_configuration_.deploymentoptions.html - title: 'config.FailurePolicy' - path: /docs/reference/functions/function_configuration_.failurepolicy.html + path: /docs/reference/functions/v1_function_configuration_.failurepolicy.html - title: 'config.RuntimeOptions' - path: /docs/reference/functions/function_configuration_.runtimeoptions.html + path: /docs/reference/functions/v1_function_configuration_.runtimeoptions.html - title: 'config.Schedule' - path: /docs/reference/functions/function_configuration_.schedule.html + path: /docs/reference/functions/v1_function_configuration_.schedule.html - title: 'config.ScheduleRetryConfig' - path: /docs/reference/functions/function_configuration_.scheduleretryconfig.html + path: /docs/reference/functions/v1_function_configuration_.scheduleretryconfig.html - title: 'functions.analytics' - path: /docs/reference/functions/providers_analytics_.html + path: /docs/reference/functions/v1_providers_analytics_.html section: - title: 'AnalyticsEvent' - path: /docs/reference/functions/providers_analytics_.analyticsevent.html + path: /docs/reference/functions/v1_providers_analytics_.analyticsevent.html - title: 'AnalyticsEventBuilder' - path: /docs/reference/functions/providers_analytics_.analyticseventbuilder.html + path: /docs/reference/functions/v1_providers_analytics_.analyticseventbuilder.html - title: 'AppInfo' - path: /docs/reference/functions/providers_analytics_.appinfo.html + path: /docs/reference/functions/v1_providers_analytics_.appinfo.html - title: 'DeviceInfo' - path: /docs/reference/functions/providers_analytics_.deviceinfo.html + path: /docs/reference/functions/v1_providers_analytics_.deviceinfo.html - title: 'ExportBundleInfo' - path: /docs/reference/functions/providers_analytics_.exportbundleinfo.html + path: /docs/reference/functions/v1_providers_analytics_.exportbundleinfo.html - title: 'GeoInfo' - path: /docs/reference/functions/providers_analytics_.geoinfo.html + path: /docs/reference/functions/v1_providers_analytics_.geoinfo.html - title: 'UserDimensions' - path: /docs/reference/functions/providers_analytics_.userdimensions.html + path: /docs/reference/functions/v1_providers_analytics_.userdimensions.html - title: 'UserPropertyValue' - path: /docs/reference/functions/providers_analytics_.userpropertyvalue.html + path: /docs/reference/functions/v1_providers_analytics_.userpropertyvalue.html - title: 'functions.auth' - path: /docs/reference/functions/providers_auth_.html + path: /docs/reference/functions/v1_providers_auth_.html section: - title: 'UserBuilder' - path: /docs/reference/functions/providers_auth_.userbuilder.html + path: /docs/reference/functions/v1_providers_auth_.userbuilder.html - title: 'UserInfo' - path: /docs/reference/functions/providers_auth_.html#userinfo + path: /docs/reference/functions/v1_providers_auth_.html#userinfo - title: 'UserRecordMetadata' - path: /docs/reference/functions/providers_auth_.userrecordmetadata.html + path: /docs/reference/functions/v1_providers_auth_.userrecordmetadata.html - title: 'UserRecord' - path: /docs/reference/functions/providers_auth_.html#userrecord + path: /docs/reference/functions/v1_providers_auth_.html#userrecord - title: 'functions.firestore' - path: /docs/reference/functions/providers_firestore_.html + path: /docs/reference/functions/v1_providers_firestore_.html section: - title: 'DocumentBuilder' - path: /docs/reference/functions/providers_firestore_.documentbuilder.html + path: /docs/reference/functions/v1_providers_firestore_.documentbuilder.html - title: 'DocumentSnapshot' - path: /docs/reference/functions/providers_firestore_.html#documentsnapshot + path: /docs/reference/functions/v1_providers_firestore_.html#documentsnapshot - title: 'functions.database' - path: /docs/reference/functions/providers_database_.html + path: /docs/reference/functions/v1_providers_database_.html section: - title: 'DataSnapshot' - path: /docs/reference/functions/providers_database_.datasnapshot.html + path: /docs/reference/functions/v1_providers_database_.datasnapshot.html - title: 'RefBuilder' - path: /docs/reference/functions/providers_database_.refbuilder.html + path: /docs/reference/functions/v1_providers_database_.refbuilder.html - title: 'InstanceBuilder' - path: /docs/reference/functions/providers_database_.instancebuilder.html + path: /docs/reference/functions/v1_providers_database_.instancebuilder.html - title: 'functions.https' - path: /docs/reference/functions/providers_https_.html + path: /docs/reference/functions/v1_providers_https_.html section: - title: 'HttpsError' - path: /docs/reference/functions/providers_https_.httpserror.html + path: /docs/reference/functions/v1_providers_https_.httpserror.html - title: 'CallableContext' - path: /docs/reference/functions/providers_https_.callablecontext.html + path: /docs/reference/functions/v1_providers_https_.callablecontext.html - title: 'functions.logger' - path: /docs/reference/functions/logger_.html + path: /docs/reference/functions/logger_index_.html section: - title: 'LogEntry' - path: /docs/reference/functions/logger_.logentry.html + path: /docs/reference/functions/logger_index_.logentry.html - title: 'functions.pubsub' - path: /docs/reference/functions/providers_pubsub_.html + path: /docs/reference/functions/v1_providers_pubsub_.html section: - title: 'Message' - path: /docs/reference/functions/providers_pubsub_.message.html + path: /docs/reference/functions/v1_providers_pubsub_.message.html - title: 'TopicBuilder' - path: /docs/reference/functions/providers_pubsub_.topicbuilder.html + path: /docs/reference/functions/v1_providers_pubsub_.topicbuilder.html - title: 'ScheduleBuilder' - path: /docs/reference/functions/providers_pubsub_.schedulebuilder.html + path: /docs/reference/functions/v1_providers_pubsub_.schedulebuilder.html - title: 'functions.remoteconfig' - path: /docs/reference/functions/providers_remoteconfig_.html + path: /docs/reference/functions/v1_providers_remoteconfig_.html section: - title: 'RemoteConfigUser' - path: /docs/reference/functions/providers_remoteconfig_.remoteconfiguser.html + path: /docs/reference/functions/v1_providers_remoteconfig_.remoteconfiguser.html - title: 'TemplateVersion' - path: /docs/reference/functions/providers_remoteconfig_.templateversion.html + path: /docs/reference/functions/v1_providers_remoteconfig_.templateversion.html - title: 'functions.storage' - path: /docs/reference/functions/providers_storage_.html + path: /docs/reference/functions/v1_providers_storage_.html section: - title: 'BucketBuilder' - path: /docs/reference/functions/providers_storage_.bucketbuilder.html + path: /docs/reference/functions/v1_providers_storage_.bucketbuilder.html - title: 'ObjectBuilder' - path: /docs/reference/functions/providers_storage_.objectbuilder.html + path: /docs/reference/functions/v1_providers_storage_.objectbuilder.html - title: 'ObjectMetadata' - path: /docs/reference/functions/providers_storage_.objectmetadata.html + path: /docs/reference/functions/v1_providers_storage_.objectmetadata.html - title: 'functions.testLab' - path: /docs/reference/functions/providers_testlab_.html + path: /docs/reference/functions/v1_providers_testlab_.html section: - title: 'testLab.clientInfo' - path: /docs/reference/functions/providers_testlab_.clientinfo.html + path: /docs/reference/functions/v1_providers_testlab_.clientinfo.html - title: 'testLab.resultStorage' - path: /docs/reference/functions/providers_testlab_.resultstorage.html + path: /docs/reference/functions/v1_providers_testlab_.resultstorage.html - title: 'testLab.testMatrix' - path: /docs/reference/functions/providers_testlab_.testmatrix.html + path: /docs/reference/functions/v1_providers_testlab_.testmatrix.html - title: 'testLab.testMatrixBuilder' - path: /docs/reference/functions/providers_testlab_.testmatrixbuilder.html + path: /docs/reference/functions/v1_providers_testlab_.testmatrixbuilder.html - title: 'functions.handler' - path: /docs/reference/functions/handler_builder_.html + path: /docs/reference/functions/v1_handler_builder_.html section: - title: 'HandlerBuilder' - path: /docs/reference/functions/handler_builder_.handlerbuilder.html + path: /docs/reference/functions/v1_handler_builder_.handlerbuilder.html diff --git a/docgen/generate-docs.js b/docgen/generate-docs.js index d36edb7e8..2f8b9abb0 100644 --- a/docgen/generate-docs.js +++ b/docgen/generate-docs.js @@ -26,9 +26,9 @@ const repoPath = path.resolve(`${__dirname}/..`); // Command-line options. const { source: sourceFile } = yargs .option('source', { - default: `${repoPath}/src`, + default: `${repoPath}/src/{v1,logger}`, describe: 'Typescript source file(s)', - type: 'string' + type: 'string', }) .version(false) .help().argv; @@ -38,7 +38,7 @@ const contentPath = path.resolve(`${__dirname}/content-sources`); const tempHomePath = path.resolve(`${contentPath}/HOME_TEMP.md`); const devsitePath = `/docs/reference/functions/`; -const { JSDOM } = require("jsdom"); +const { JSDOM } = require('jsdom'); const typeMap = require('./type-aliases.json'); @@ -72,7 +72,7 @@ function runTypedoc() { * @param {string} subdir Subdir to move files out of. */ async function moveFilesToRoot(subdir) { - await exec(`mv ${docPath}/${subdir}/* ${docPath}`) + await exec(`mv ${docPath}/${subdir}/* ${docPath}`); await exec(`rmdir ${docPath}/${subdir}`); } @@ -86,9 +86,11 @@ async function renameFiles() { const files = await fs.readdir(docPath); const renames = []; for (const file of files) { - if (file.startsWith("_") && file.endsWith("html")) { + if (file.startsWith('_') && file.endsWith('html')) { let newFileName = file.substring(1); - renames.push(fs.rename(`${docPath}/${file}`, `${docPath}/${newFileName}`)); + renames.push( + fs.rename(`${docPath}/${file}`, `${docPath}/${newFileName}`) + ); } } await Promise.all(renames); @@ -125,11 +127,13 @@ function addTypeAliasLinks(data) { * Select .tsd-signature-type because all potential external * links will have this identifier. */ - const fileTags = htmlDom.window.document.querySelectorAll(".tsd-signature-type"); - fileTags.forEach(tag => { + const fileTags = htmlDom.window.document.querySelectorAll( + '.tsd-signature-type' + ); + for (const tag of fileTags) { const mapping = typeMap[tag.textContent]; if (mapping) { - console.log('Adding link to '+tag.textContent+" documentation."); + console.log('Adding link to ' + tag.textContent + ' documentation.'); // Add the corresponding document link to this type const linkChild = htmlDom.window.document.createElement('a'); @@ -138,7 +142,7 @@ function addTypeAliasLinks(data) { tag.textContent = null; tag.appendChild(linkChild); } - }); + } return htmlDom.serialize(); } @@ -152,13 +156,13 @@ function addTypeAliasLinks(data) { function generateTempHomeMdFile(tocRaw, homeRaw) { const { toc } = yaml.safeLoad(tocRaw); let tocPageLines = [homeRaw, '# API Reference']; - toc.forEach(group => { + for (const group of toc) { tocPageLines.push(`\n## [${group.title}](${stripPath(group.path)})`); const section = group.section || []; - section.forEach(item => { + for (const item of section) { tocPageLines.push(`- [${item.title}](${stripPath(item.path)})`); - }); - }); + } + } return fs.writeFile(tempHomePath, tocPageLines.join('\n')); } @@ -176,10 +180,10 @@ async function checkForMissingFilesAndFixFilenameCase(tocText) { // Get filenames from toc.yaml. const filenames = tocText .split('\n') - .filter(line => line.includes('path:')) - .map(line => line.split(devsitePath)[1]); + .filter((line) => line.includes('path:')) + .map((line) => line.split(devsitePath)[1].replace(/#.*$/, '')); // Logs warning to console if a file from TOC is not found. - const fileCheckPromises = filenames.map(async filename => { + const fileCheckPromises = filenames.map(async (filename) => { // Warns if file does not exist, fixes filename case if it does. // Preferred filename for devsite should be capitalized and taken from // toc.yaml. @@ -211,29 +215,28 @@ async function checkForMissingFilesAndFixFilenameCase(tocText) { */ async function checkForUnlistedFiles(filenamesFromToc, shouldRemove) { const files = await fs.readdir(docPath); - const htmlFiles = files - .filter(filename => filename.slice(-4) === 'html'); + const htmlFiles = files.filter((filename) => filename.slice(-4) === 'html'); const removePromises = []; const filesToRemove = htmlFiles - .filter(filename => !filenamesFromToc.includes(filename)) - .filter(filename => filename !== 'index' && filename != 'globals'); + .filter((filename) => !filenamesFromToc.includes(filename)) + .filter((filename) => filename !== 'index' && filename != 'globals'); if (filesToRemove.length && !shouldRemove) { // This is just a warning, it doesn't need to finish before // the process continues. console.warn( - `Unlisted files: ${filesToRemove.join(", ")} generated ` + + `Unlisted files: ${filesToRemove.join(', ')} generated ` + `but not listed in toc.yaml.` ); return htmlFiles; } - await Promise.all(filesToRemove.map(filename => { - console.log( - `REMOVING ${docPath}/${filename} - not listed in toc.yaml.` - ); - return fs.unlink(`${docPath}/${filename})`); - })); - return htmlFiles.filter(filename => filenamesFromToc.includes(filename)) + await Promise.all( + filesToRemove.map((filename) => { + console.log(`REMOVING ${docPath}/${filename} - not listed in toc.yaml.`); + return fs.unlink(`${docPath}/${filename})`); + }) + ); + return htmlFiles.filter((filename) => filenamesFromToc.includes(filename)); } /** @@ -243,10 +246,10 @@ async function checkForUnlistedFiles(filenamesFromToc, shouldRemove) { * @param {Array} htmlFiles List of html files found in generated dir. */ async function writeGeneratedFileList(htmlFiles) { - const fileList = htmlFiles.map(filename => { + const fileList = htmlFiles.map((filename) => { return { title: filename, - path: `${devsitePath}${filename}` + path: `${devsitePath}${filename}`, }; }); const generatedTocYAML = yaml.safeDump({ toc: fileList }); @@ -262,10 +265,10 @@ async function writeGeneratedFileList(htmlFiles) { */ function fixAllLinks(htmlFiles) { const writePromises = []; - htmlFiles.forEach(file => { + for (const file of htmlFiles) { // Update links in each html file to match flattened file structure. writePromises.push(fixLinks(`${docPath}/${file}`)); - }); + } return Promise.all(writePromises); } @@ -281,12 +284,12 @@ function fixAllLinks(htmlFiles) { * links as needed. * 5) Check for mismatches between TOC list and generated file list. */ -(async function() { +(async function () { try { const [tocRaw, homeRaw] = await Promise.all([ fs.readFile(`${contentPath}/toc.yaml`, 'utf8'), - fs.readFile(`${contentPath}/HOME.md`, 'utf8') - ]) + fs.readFile(`${contentPath}/HOME.md`, 'utf8'), + ]); // Run main Typedoc process (uses index.d.ts and generated temp file above). await generateTempHomeMdFile(tocRaw, homeRaw); @@ -324,7 +327,9 @@ function fixAllLinks(htmlFiles) { // Check for files listed in TOC that are missing and warn if so. // Not blocking. - const filenamesFromToc = await checkForMissingFilesAndFixFilenameCase(tocRaw); + const filenamesFromToc = await checkForMissingFilesAndFixFilenameCase( + tocRaw + ); // Check for files that exist but aren't listed in the TOC and warn. // (If API is node, actually remove the file.) @@ -340,13 +345,15 @@ function fixAllLinks(htmlFiles) { const data = await fs.readFile(`${docPath}/index.html`, 'utf8'); // String to include devsite local variables. const localVariablesIncludeString = `{% include "docs/web/_local_variables.html" %}\n`; - await fs.writeFile(`${docPath}/index.html`, localVariablesIncludeString + data); + await fs.writeFile( + `${docPath}/index.html`, + localVariablesIncludeString + data + ); } catch (err) { - if (err.stdout) { - console.error(err.stdout); - } else { - console.error(err); + if (err.stdout) { + console.error(err.stdout); + } else { + console.error(err); + } } -} })(); - diff --git a/docgen/theme/helpers/cleanBreadcrumb.js b/docgen/theme/helpers/cleanBreadcrumb.js new file mode 100644 index 000000000..ad52e64a7 --- /dev/null +++ b/docgen/theme/helpers/cleanBreadcrumb.js @@ -0,0 +1,4 @@ +exports.cleanBreadcrumb = function (value) { + const parts = value.replace(/"/g, '').split('/'); + return parts[parts.length - 1]; +}; diff --git a/docgen/theme/partials/breadcrumb.hbs b/docgen/theme/partials/breadcrumb.hbs index db115163f..6a2147724 100644 --- a/docgen/theme/partials/breadcrumb.hbs +++ b/docgen/theme/partials/breadcrumb.hbs @@ -3,7 +3,7 @@ {{#with parent}}{{> breadcrumb}}{{/with}}