diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..825a61c Binary files /dev/null and b/.DS_Store differ diff --git a/package-lock.json b/package-lock.json index 3bdd980..337d076 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": { @@ -24,7 +22,7 @@ "eslint": "^8.57.1", "eslint-config-prettier": "^10.1.8", "firebase-admin": "^12.0.0", - "firebase-functions": "^4.9.0", + "firebase-functions": "^7.3.0", "firebase-tools": "^13.15.4", "mocha": "^11.7.6", "prettier": "^2.8.8", @@ -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", @@ -5927,36 +5919,53 @@ "license": "MIT" }, "node_modules/firebase-functions": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-4.9.0.tgz", - "integrity": "sha512-IqxOEsVAWGcRv9KRGzWQR5mOFuNsil3vsfkRPPiaV1U/ATC27/jbahh4z8I4rW8Xqa6cQE5xqnw0ueyMH7i7Ag==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-7.3.0.tgz", + "integrity": "sha512-S3JhjESOWMq13iDodwgUPWNQ3gLAu7oTLDwF7hTtisyjVanEeQzYezgW+JTfd8I0kCJQzjGPC/wHQ19IL7CgaA==", "dev": true, + "license": "MIT", "dependencies": { "@types/cors": "^2.8.5", - "@types/express": "4.17.3", + "@types/express": "^4.17.21", "cors": "^2.8.5", - "express": "^4.17.1", + "express": "^4.21.0", "protobufjs": "^7.2.2" }, "bin": { "firebase-functions": "lib/bin/firebase-functions.js" }, "engines": { - "node": ">=14.10.0" + "node": ">=18.0.0" }, "peerDependencies": { - "firebase-admin": "^10.0.0 || ^11.0.0 || ^12.0.0" + "@apollo/server": "^5.2.0", + "@as-integrations/express4": "^1.1.2", + "firebase-admin": "^11.10.0 || ^12.0.0 || ^13.0.0 || ^14.0.0", + "graphql": "^16.12.0" + }, + "peerDependenciesMeta": { + "@apollo/server": { + "optional": true + }, + "@as-integrations/express4": { + "optional": true + }, + "graphql": { + "optional": true + } } }, "node_modules/firebase-functions/node_modules/@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==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, + "license": "MIT", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "*", - "@types/serve-static": "*" + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" } }, "node_modules/firebase-tools": { @@ -8734,7 +8743,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..785a26b 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": { @@ -53,7 +51,7 @@ "eslint": "^8.57.1", "eslint-config-prettier": "^10.1.8", "firebase-admin": "^12.0.0", - "firebase-functions": "^4.9.0", + "firebase-functions": "^7.3.0", "firebase-tools": "^13.15.4", "mocha": "^11.7.6", "prettier": "^2.8.8", diff --git a/spec/main.spec.ts b/spec/main.spec.ts index 703495c..882c3b4 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); }); @@ -329,17 +327,17 @@ describe('main', () => { delete process.env.CLOUD_RUNTIME_CONFIG; }); - it('should mock functions.config()', () => { + it('should set CLOUD_RUNTIME_CONFIG', () => { mockConfig(config); - expect(functions.config()).to.deep.equal(config); + expect(JSON.parse(process.env.CLOUD_RUNTIME_CONFIG!)).to.deep.equal( + config + ); }); - it('should purge singleton config object when it is present', () => { - mockConfig(config); - config.foo = { baz: 'qux' }; - mockConfig(config); - - expect(functions.config()).to.deep.equal(config); + it('should throw because functions.config() is removed in v7', () => { + expect(() => (functions as any).config()).to.throw( + 'functions.config() has been removed in firebase-functions v7' + ); }); }); }); diff --git a/spec/providers/firestore.spec.ts b/spec/providers/firestore.spec.ts index 34cd169..9706055 100644 --- a/spec/providers/firestore.spec.ts +++ b/spec/providers/firestore.spec.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import * as firebase from 'firebase-admin'; import * as sinon from 'sinon'; -import * as http from 'http'; +import http = require('http'); import { FeaturesList } from '../../src/features'; import fft = require('../../src/index'); 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 b5d587c..572e2bf 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -22,8 +22,25 @@ 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 { + if (typeof value !== 'object' || value === null) { + return false; + } + const proto = Object.getPrototypeOf(value); + return proto === null || proto === Object.prototype; +} + +function mapValues( + obj: Record, + fn: (val: T) => U +): Record { + const res: Record = {}; + for (const [key, val] of Object.entries(obj)) { + res[key] = fn(val); + } + return res; +} import { testApp } from '../app'; @@ -85,21 +102,22 @@ export function makeDocumentSnapshot( } 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 +224,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 +284,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..ac6aee6 100644 --- a/src/v1.ts +++ b/src/v1.ts @@ -20,14 +20,21 @@ // 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; +} + +function isObjectOrFunction(val: any): boolean { + return val !== null && (typeof val === 'object' || typeof val === 'function'); +} import { CloudFunction, EventContext, Change, https, - config, database, firestore, HttpsFunction, @@ -124,13 +131,16 @@ export function wrapV1( export function wrapV1( cloudFunction: CloudFunction ): WrappedScheduledFunction | WrappedFunction> { - if (!has(cloudFunction, '__endpoint')) { + if (!isObjectOrFunction(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 ( + isObjectOrFunction(cloudFunction?.__endpoint) && + 'scheduleTrigger' in cloudFunction.__endpoint + ) { const scheduledWrapped: WrappedScheduledFunction = ( options: ContextOptions ) => { @@ -139,7 +149,12 @@ export function wrapV1( _checkOptionValidity(['eventId', 'timestamp'], options); const defaultContext = _makeDefaultContext(cloudFunction, options); - const context = merge({}, defaultContext, options); + const context = merge.withOptions( + { allowUndefinedOverrides: false }, + {}, + defaultContext, + options + ); // @ts-ignore return cloudFunction.run(context); @@ -147,19 +162,24 @@ export function wrapV1( return scheduledWrapped; } - if (has(cloudFunction, '__endpoint.httpsTrigger')) { + if ( + isObjectOrFunction(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 (!isObjectOrFunction(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 = + isObjectOrFunction(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,14 +203,18 @@ 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'; defaultContext.auth = null; } - context = merge({}, defaultContext, _options); + context = merge.withOptions( + { allowUndefinedOverrides: false }, + {}, + defaultContext, + _options + ); } return cloudFunction.run(data, context); @@ -208,7 +232,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; diff --git a/tsconfig.json b/tsconfig.json index 3937047..1ce0f8a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,9 @@ "outDir": ".tmp", "sourceMap": true, "target": "es6", - "typeRoots": ["node_modules/@types"] + "typeRoots": ["node_modules/@types"], + "esModuleInterop": true, + "skipLibCheck": true }, "include": ["src/**/*.ts", "spec/**/*.ts"] } diff --git a/tsconfig.release.json b/tsconfig.release.json index 1240c94..35dea7b 100644 --- a/tsconfig.release.json +++ b/tsconfig.release.json @@ -7,7 +7,9 @@ "outDir": "lib", "stripInternal": true, "target": "es6", - "typeRoots": ["node_modules/@types"] + "typeRoots": ["node_modules/@types"], + "esModuleInterop": true, + "skipLibCheck": true }, "files": ["src/index.ts"] }