diff --git a/package-lock.json b/package-lock.json index 3bdd980..88c1399 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,6 @@ "version": "3.5.0", "license": "MIT", "dependencies": { - "@types/lodash": "^4.14.202", - "lodash": "^4.17.21", "ts-deepmerge": "^8.0.0" }, "devDependencies": { @@ -2223,12 +2221,6 @@ "@types/node": "*" } }, - "node_modules/@types/lodash": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", - "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", - "license": "MIT" - }, "node_modules/@types/long": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", @@ -8734,7 +8726,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash._objecttypes": { "version": "2.4.1", diff --git a/package.json b/package.json index 2337d54..945a0df 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,6 @@ }, "homepage": "https://github.com/firebase/firebase-functions-test#readme", "dependencies": { - "@types/lodash": "^4.14.202", - "lodash": "^4.17.21", "ts-deepmerge": "^8.0.0" }, "devDependencies": { diff --git a/spec/main.spec.ts b/spec/main.spec.ts index 703495c..8baa367 100644 --- a/spec/main.spec.ts +++ b/spec/main.spec.ts @@ -22,8 +22,6 @@ import { expect } from 'chai'; import * as functions from 'firebase-functions/v1'; -import { set } from 'lodash'; - import { mockConfig, makeChange, wrap } from '../src/main'; import { _makeResourceName, _extractParams } from '../src/v1'; import { features } from '../src/features'; @@ -36,10 +34,10 @@ describe('main', () => { describe('background functions', () => { const constructBackgroundCF = (eventType?: string) => { const cloudFunction = (input) => input; - set(cloudFunction, 'run', (data, context) => { + (cloudFunction as any).run = (data, context) => { return { data, context }; - }); - set(cloudFunction, '__endpoint', { + }; + (cloudFunction as any).__endpoint = { eventTrigger: { eventFilters: { resource: 'ref/{wildcard}/nested/{anotherWildcard}', @@ -47,7 +45,7 @@ describe('main', () => { eventType: eventType || 'event', retry: false, }, - }); + }; return cloudFunction as functions.CloudFunction; }; @@ -244,12 +242,12 @@ describe('main', () => { before(() => { const cloudFunction = (input) => input; - set(cloudFunction, 'run', (data, context) => { + (cloudFunction as any).run = (data, context) => { return { data, context }; - }); - set(cloudFunction, '__endpoint', { + }; + (cloudFunction as any).__endpoint = { callableTrigger: {}, - }); + }; wrappedCF = wrap(cloudFunction as functions.CloudFunction); }); diff --git a/src/index.ts b/src/index.ts index 3d1f8e4..a76de68 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,6 @@ // SOFTWARE. import { AppOptions } from 'firebase-admin'; -import { merge } from 'lodash'; import { FirebaseFunctionsTest } from './lifecycle'; import { FeaturesList } from './features'; @@ -35,8 +34,9 @@ export = ( // Ensure other files get loaded after init function, since they load `firebase-functions` // which will issue warning if process.env.FIREBASE_CONFIG is not yet set. let features = require('./features').features; - features = merge({}, features, { + features = { + ...features, cleanup: () => test.cleanup(), - }); + }; return features; }; diff --git a/src/lifecycle.ts b/src/lifecycle.ts index a02e057..54a4d7a 100644 --- a/src/lifecycle.ts +++ b/src/lifecycle.ts @@ -20,9 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { isEmpty } from 'lodash'; import { AppOptions } from 'firebase-admin'; -import { forEach } from 'lodash'; import { testApp } from './app'; @@ -53,7 +51,7 @@ export class FirebaseFunctionsTest { CLOUD_RUNTIME_CONFIG: process.env.CLOUD_RUNTIME_CONFIG, }; - if (isEmpty(firebaseConfig)) { + if (!firebaseConfig || Object.keys(firebaseConfig).length === 0) { process.env.FIREBASE_CONFIG = JSON.stringify({ databaseURL: 'https://not-a-project.firebaseio.com', storageBucket: 'not-a-project.appspot.com', @@ -72,13 +70,13 @@ export class FirebaseFunctionsTest { /** Complete clean up tasks. */ cleanup() { - forEach(this._oldEnv, (val, varName) => { + for (const [varName, val] of Object.entries(this._oldEnv)) { if (typeof val !== 'undefined') { process.env[varName] = val; } else { delete process.env[varName]; } - }); + } testApp().deleteApp(); } } diff --git a/src/providers/firestore.ts b/src/providers/firestore.ts index a15945a..6a1e4bc 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -22,8 +22,27 @@ import { Change } from 'firebase-functions/v1'; import { firestore, app } from 'firebase-admin'; -import { has, get, isEmpty, isPlainObject, mapValues } from 'lodash'; -import { inspect } from 'util'; + +function isPlainObject(value: any): boolean { + return ( + typeof value === 'object' && + value !== null && + Object.getPrototypeOf(value) === Object.prototype + ); +} + +function mapValues( + obj: Record, + fn: (val: T) => U +): Record { + const res: Record = {}; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + res[key] = fn(obj[key]); + } + } + return res; +} import { testApp } from '../app'; @@ -76,30 +95,32 @@ export function makeDocumentSnapshot( ) { let firestoreService; let project; - if (has(options, 'app')) { - firestoreService = firestore(options.firebaseApp); - project = get(options, 'app.options.projectId'); + const legacyApp = options?.firebaseApp || (options as any)?.app; + if (legacyApp) { + firestoreService = firestore(legacyApp); + project = legacyApp.options?.projectId; } else { firestoreService = firestore(testApp().getApp()); project = process.env.GCLOUD_PROJECT; } const resource = `projects/${project}/databases/(default)/documents/${refPath}`; - const proto = isEmpty(data) - ? resource - : { - fields: objectToValueProto(data), - createTime: dateToTimestampProto( - get(options, 'createTime', new Date().toISOString()) - ), - updateTime: dateToTimestampProto( - get(options, 'updateTime', new Date().toISOString()) - ), - name: resource, - }; + const proto = + !data || Object.keys(data).length === 0 + ? resource + : { + fields: objectToValueProto(data), + createTime: dateToTimestampProto( + options?.createTime ?? new Date().toISOString() + ), + updateTime: dateToTimestampProto( + options?.updateTime ?? new Date().toISOString() + ), + name: resource, + }; const readTimeProto = dateToTimestampProto( - get(options, 'readTime') || new Date().toISOString() + options?.readTime || new Date().toISOString() ); return firestoreService.snapshot_(proto, readTimeProto, 'json'); } @@ -206,8 +227,8 @@ export function objectToValueProto(data: object) { }; } if (val instanceof firestore.DocumentReference) { - const projectId: string = get(val, '_referencePath.projectId'); - const database: string = get(val, '_referencePath.databaseId'); + const projectId: string = (val as any)._referencePath?.projectId; + const database: string = (val as any)._referencePath?.databaseId; const referenceValue: string = [ 'projects', projectId, @@ -266,7 +287,11 @@ export function clearFirestoreData(options: { projectId: string } | string) { if (typeof options === 'string') { projectId = options; - } else if (typeof options === 'object' && has(options, 'projectId')) { + } else if ( + typeof options === 'object' && + options && + 'projectId' in options + ) { projectId = options.projectId; } else { throw new Error('projectId not specified'); diff --git a/src/v1.ts b/src/v1.ts index 6fed427..8d97b00 100644 --- a/src/v1.ts +++ b/src/v1.ts @@ -20,7 +20,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { has, merge, random, get } from 'lodash'; +import { merge } from 'ts-deepmerge'; + +function random(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} import { CloudFunction, @@ -124,13 +128,16 @@ export function wrapV1( export function wrapV1( cloudFunction: CloudFunction ): WrappedScheduledFunction | WrappedFunction> { - if (!has(cloudFunction, '__endpoint')) { + if (!cloudFunction || !('__endpoint' in cloudFunction)) { throw new Error( 'Wrap can only be called on functions written with the firebase-functions SDK.' ); } - if (has(cloudFunction, '__endpoint.scheduleTrigger')) { + if ( + cloudFunction?.__endpoint && + 'scheduleTrigger' in cloudFunction.__endpoint + ) { const scheduledWrapped: WrappedScheduledFunction = ( options: ContextOptions ) => { @@ -147,19 +154,21 @@ export function wrapV1( return scheduledWrapped; } - if (has(cloudFunction, '__endpoint.httpsTrigger')) { + if (cloudFunction?.__endpoint && 'httpsTrigger' in cloudFunction.__endpoint) { throw new Error( 'Wrap function is only available for `onCall` HTTP functions, not `onRequest`.' ); } - if (!has(cloudFunction, 'run')) { + if (!cloudFunction || !('run' in cloudFunction)) { throw new Error( 'This library can only be used with functions written with firebase-functions v1.0.0 and above' ); } - const isCallableFunction = has(cloudFunction, '__endpoint.callableTrigger'); + const isCallableFunction = + !!cloudFunction?.__endpoint && + 'callableTrigger' in cloudFunction.__endpoint; let wrapped: WrappedFunction = (data, options) => { // Although in Typescript we require `options` some of our JS samples do not pass it. @@ -183,8 +192,7 @@ export function wrapV1( const defaultContext = _makeDefaultContext(cloudFunction, _options, data); if ( - has(defaultContext, 'eventType') && - defaultContext.eventType !== undefined && + defaultContext?.eventType && defaultContext.eventType.match(/firebase.database/) ) { defaultContext.authType = 'UNAUTHENTICATED'; @@ -208,7 +216,7 @@ export function _makeResourceName( const wildcardRegex = new RegExp('{[^/{}]*}', 'g'); let resourceName = resource.replace(wildcardRegex, (wildcard) => { let wildcardNoBraces = wildcard.slice(1, -1); // .slice removes '{' and '}' from wildcard - let sub = get(params, wildcardNoBraces); + let sub = params?.[wildcardNoBraces]; return sub || wildcardNoBraces + random(1, 9); }); return resourceName;