What is the correct way to dynamically import a class inside of a Firebase Cloud function using typescript? - node.js

In a Firebase Cloud Function project...
I have the following typescript file at the root of my src directory right along side of my main index.ts file which imports one dependency and exports a class that includes 2 methods. This file is titled bcrypt.class.ts:
import * as bcrypt from 'bcryptjs';
export default class BcryptTool {
public static hashValue(value: string, rounds: number, callback: (error: Error, hash: string) => void) : void {
bcrypt.hash(value, rounds, (error:any, hash:any) => {
callback(error, hash);
});
}
public static compare(value: string, dbHash: string, callback: (error: string | null, match: boolean | null) => void) {
bcrypt.compare(value, dbHash, (err: Error, match: boolean) => {
if(match) {
callback(null, true);
} else {
callback('Invalid value match', null);
}
});
}
}
In my Firebase Cloud functions index.ts file I import this class and make a call to it's 'compare' method within one of my functions without issue, this works as expected:
'use strict';
const express = require('express');
const functions = require('firebase-functions');
const cors = require('cors')({ origin: true });
const admin = require('firebase-admin');
admin.initializeApp();
const api = express();
import BcryptTool from './bcrypt.class'; // <-- i import the class here
// and use it in a function
api.use(cors);
api.post('/credentials', async (request: any, response: any) => {
BcryptTool.compare(...) // <--- calls to this method succeed without issue
});
The problem
My application includes many functions, but I only need the class noted above in one of them, so in an attempt to optimize cold start time for all my other functions, I attempt to dynamically import this class inside of the function that needs it instead of importing it into the global scope as outlined above. This does not work and I cannot figure out why:
'use strict';
const express = require('express');
const functions = require('firebase-functions');
const cors = require('cors')({ origin: true });
const admin = require('firebase-admin');
admin.initializeApp();
const api = express();
api.use(cors);
api.post('/credentials', async (request: any, response: any) => {
const BcryptTool = await import('./bcrypt.class'); // <-- when i attempt to import here instead
BcryptTool.compare(...) // <--- subsequent calls to this method fail
// Additionally, VS Code hinting displays a warning: Property 'compare' does not exist on type 'typeof import('FULL/PATH/TO/MY/bcrypt.class')'
});
Is my class not written or exported correctly?
Am I not importing the class correctly inside of my cloud function?

The top-level import (import BcryptTool from './bcrypt.class';) will automatically import the default export from the bcrypt.class module. However, when using the import statement as a function (so called "dynamic import"), it will import the module itself, not the default export.
You can see the difference when you would console.log(BcryptTool) both imports:
import BcryptTool from './bcrypt.class' will show { default: { [Function: BcryptTool] hashValue: [Function], compare: [Function] } }
const BcryptTool = await require('bcrypt.class') will show { [Function: BcryptTool] hashValue: [Function], compare: [Function] }
Did you notice the default in the first console.log? That shows you imported the module, not the default.
Now actually the import BcryptTool from './bcrypt.class' syntax is syntactic sugar for doing import { default as BcryptTool } from './bcrypt.class'. If you apply this knowledge on the dynamic import, you could do this:
const BcryptToolModule = await import('./bcrypt.class');
BcryptToolModule.default.compare(...);
Or in a cleaner syntax:
const { default: BcryptTool } = await import('./bcrypt.class');
BcryptTool.compare(...);

Related

Fastify CLI decorators undefined

I'm using fastify-cli for building my server application.
For testing I want to generate some test JWTs. Therefore I want to use the sign method of the fastify-jwt plugin.
If I run the application with fastify start -l info ./src/app.js everything works as expected and I can access the decorators.
But in the testing setup I get an error that the jwt decorator is undefined. It seems that the decorators are not exposed and I just can't find any error. For the tests I use node-tap with this command: tap \"test/**/*.test.js\" --reporter=list
app.js
import { dirname, join } from 'path'
import autoload from '#fastify/autoload'
import { fileURLToPath } from 'url'
import jwt from '#fastify/jwt'
export const options = {
ignoreTrailingSlash: true,
logger: true
}
export default async (fastify, opts) => {
await fastify.register(jwt, {
secret: process.env.JWT_SECRET
})
// autoload plugins and routes
await fastify.register(autoload, {
dir: join(dirname(fileURLToPath(import.meta.url)), 'plugins'),
options: Object.assign({}, opts),
forceESM: true,
})
await fastify.register(autoload, {
dir: join(dirname(fileURLToPath(import.meta.url)), 'routes'),
options: Object.assign({}, opts),
forceESM: true
})
}
helper.js
import { fileURLToPath } from 'url'
import helper from 'fastify-cli/helper.js'
import path from 'path'
// config for testing
export const config = () => {
return {}
}
export const build = async (t) => {
const argv = [
path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'app.js')
]
const app = await helper.build(argv, config())
t.teardown(app.close.bind(app))
return app
}
root.test.js
import { auth, build } from '../helper.js'
import { test } from 'tap'
test('requests the "/" route', async t => {
t.plan(1)
const app = await build(t)
const token = app.jwt.sign({ ... }) //-> jwt is undefined
const res = await app.inject({
method: 'GET',
url: '/'
})
t.equal(res.statusCode, 200, 'returns a status code of 200')
})
The issue is that your application diagram looks like this:
and when you write const app = await build(t) the app variable points to Root Context, but Your app.js contains the jwt decorator.
To solve it, you need just to wrap you app.js file with the fastify-plugin because it breaks the encapsulation:
import fp from 'fastify-plugin'
export default fp(async (fastify, opts) => { ... })
Note: you can visualize this structure by using fastify-overview (and the fastify-overview-ui plugin together:

How to mock prisma with jest-mock

I use prisma to interact with my database and i would like to use jest-mock to mock the findMany call. https://jestjs.io/docs/jest-object#jestmockedtitem-t-deep--false
brands.test.ts
import { PrismaService } from "#services/mysql.service";
import { mocked } from "jest-mock";
import faker from "#faker-js/faker";
import { GetBrands } from "./brand";
jest.mock("#services/mysql.service");
/**
* #group unit
*/
describe("Brand", () => {
afterAll(async () => {});
const mockedPrismaService = mocked(PrismaService, true);
it("should get a list of brands", async () => {
const mockedData = [
{
id: faker.datatype.uuid(),
name: faker.datatype.string(),
image: {
source: "some_source",
dtype: "some_dtype",
},
},
];
//#ts-ignore - because of relational data mockedData.image
mockedPrismaService.brand.findMany.mockResolvedValueOnce(mockedData);
const [response, error] = await GetBrands();
console.log(response, error);
});
});
mysql.service.ts
import mysql from "mysql2/promise";
import { Config } from "#config";
import { PrismaClient, Prisma } from "#prisma/client";
export const MySQLEscape = mysql.escape;
export const MySQLPreparedStatement = mysql.format;
export const PrismaService = new PrismaClient({});
export const PrismaHelper = Prisma;
However when i run this test i get the following error.
TypeError: Cannot read properties of undefined (reading 'brand')
Factory Mock
One option is to option use the factory approach when mocking your client.
jest.mock("#services/mysql.service", () => ({
PrismaService: {
brand: {
findMany: jest.fn(() => { })
}
},
}));
Then within your test, you can mock the findMany function to return your test data, then call the function being tested.
const mockedData = [...];
PrismaService.brand.findMany.mockResolvedValueOnce(mockedData);
const result = await GetBrands();
It's a bit cumbersome, but it works.
Note that in my example, I've implemented GetBrands as follows:
import { PrismaService } from "#services/mysql.service"
export const GetBrands = async () => {
const data = await PrismaService.brand.findMany();
return data;
}
Your example
In your example, you're using automatic mocking, and I'm not too familiar with it so I'm not sure how to get it working.
What seems to be happening to cause the error is your PrismaService is undefined when it's imported here:
import { PrismaService } from "#services/mysql.service";
And then calling the mocked function with an undefined parameter returns undefined:
const mockedPrismaService = mocked(undefined, true); // returns undefined
And finally, calling the following is what throws the error:
mockedPrismaService.brand.findMany.mockResolvedValueOnce(mockedData);
// TypeError: Cannot read properties of undefined (reading 'brand')
I would have thought something like this would be what you're after, but this throws an error:
jest.mock("#services/mysql.service", () => ({
PrismaService: mocked(PrismaService, true)
}));
// 6 |
// 7 | jest.mock("#services/mysql.service", () => ({
//> 8 | PrismaService: mocked(PrismaClient, true)
// | ^
// 9 | }));
Check out the docs
Might be worth checking out the Prismas documentation on unit testing, as they suggest a couple of pretty different approaches.

how to setup morgan-boddy in nestjs

I want to setup morgan-boddy as a midldleware to log requests and responses.
So I've created a function like that:
export function RequestLogging(app) {
const logger = new Logger('Request');
app.use(
morganBody(app, {
stream: { // <--- error is here "Void function return value is used "
write: (message) => logger.log(message.replace('\n', '')),
},
}),
);
}
That I call on main.ts
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
useRequestLogging(app);
// ...
}
However it seems does not work. I've got an error 'Void function return value is used' on line stream: {
Any idea how to fix?
UPDATE:
I tried to go different path and actually just stick morganBody in to main.ts as per docs:
import bodyParser from 'body-parser';
import morganBody from 'morgan-body';
app.use(bodyParser.json());
// hook morganBody to express app
morganBody(app); <-- getting error here "TS2345: Argument of type 'INestApplication' is not assignable to parameter of type 'Application'."
I wish there was a proper documentation how to approach in nestjs.
This is a pretty interesting middleware. It ends up needing the express instance itself because it calls app.use(morgan) and app.response.send() under the hood. I would really look into some other solution instead of something that accesses the response in this way.
Either way: this set up works
import { Logger } from '#nestjs/common';
import { NestFactory } from '#nestjs/core';
import * as morgan from 'morgan-body';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const logger = app.get(Logger);
(morgan as any)(app.getHttpAdapter().getInstance(), {
stream: {
write: (message: string) => {
logger.log(message.replace('\n', ''));
return true;
},
},
});
await app.listen(3033);
}
bootstrap();
The types for the package are wrong as well, it's not a default export, but a named one, so import morgan from 'morgan-body' doesn't work as advertised (at least it didn't for me).
The return true is necessary because write expects a stream.writable() method, which has returns a boolean. You can just default this to true. Then you have to use app.getHttpAdapter().getInstance() so that you ensure you pass the express instance to the middleware. Again, wonky setup, but it works.

What is the proper type for a request handler of a firebase HttpsFunction?

I need to split my functions into multiple files.
This is my index.ts
export const helloWorld = functions.https.onRequest((request, response) => {
functions.logger.info("Hello logs!", {structuredData: true});
response.send("Hello from Firebase!");
});
I need it to be something like:
import helloWorldHandler from "./handlers/helloWorldHandler"
export const helloWorld = functions.https.onRequest(helloWorldHandler);
So what should I type the helloWorldHandler request handler?
const helloWorldHandler : ??? = async (req,res) => {
const result = await someApi();
functions.logger.info("Hello logs!", {structuredData: true});
res.send("Hello from Firebase!");
};
export default helloWorldHandler;
I tried:
import * as functions from "firebase-functions";
const helloWorldHandler : functions.HttpsFunction = async (req,res) => { ... };
But I'm getting this error:
Type '(req: Request, res: Response) => Promise<Response>' is not assignable to type 'HttpsFunction'.
Property '__trigger' is missing in type '(req: Request, res: Response) => Promise<Response>' but required in type 'TriggerAnnotated'.
The onRequest() method, the one that should take the handler as a parameter, does not seem to give it a proper type name, rather than a function signature. Do I need to create an alias for that?
The type functions.HttpsFunction is the return type of functions.https.onRequest(), and not the argument to it. A function of this type is exported by your code and defines what needs to be deployed by the Firebase CLI (the region, memory size and so on are stored in the __trigger property).
As you want the type of the first argument to functions.https.onRequest(), you are instead looking for the type:
type HttpsOnRequestHandler = (req: functions.https.Request, resp: functions.Response<any>) => void | Promise<void>
But rather than hard-code this, you can extract it from the Firebase Functions library using either:
import * as functions from "firebase-functions";
type HttpsOnRequestHandler = Parameters<typeof functions.https.onRequest>[0];
or
import { https } from "firebase-functions";
type HttpsOnRequestHandler = Parameters<typeof https.onRequest>[0]
Note: If your code doesn't use the firebase-functions library itself, you can tell TypeScript that you only want its types using import type * as functions from "firebase-functions"; and import type { https } from "firebase-functions"; as appropriate; this removes the import from the compiled JavaScript as its not needed for running the code.
Here is what I've come up with. Still interested in knowing if is there an out-of-the-box type alias for this.
index.ts
import * as functions from "firebase-functions";
import helloWorldHandler from "./handlers/helloWorldHandler"
type Req = functions.https.Request
type Res = functions.Response
// WILL USE THIS EXPORTED TYPE
export type RequestHandler = (req: Req, res: Res) => void | Promise<void>
export const helloWorld = functions.https.onRequest(helloWorldHandler);
helloWorldHandler .ts
import { RequestHandler } from "../index";
const helloWorldHandler : RequestHandler = async (req,res) => { ... };
I'd suggest simply using the provided HttpsFunction type:
import * as functions from "firebase-functions";
import type { HttpsFunction } from "firebase-functions";
export const foo: HttpsFunction = functions.https.onRequest((req, res) => {
// the types of req and res are known here...
res.send("HELO");
});
I managed to run the new Firebase Functions v2 in TypeScript with
// import from a specific subpackage
import {onRequest} from "firebase-functions/v2/https";
export default onRequest((request, response) => {
response.send("Hello from Firebase!");
});

In attempting to minimize cold start time for Firebase Cloud functions, how to import a class for use inside one function? [duplicate]

This question already has an answer here:
What is the correct way to dynamically import a class inside of a Firebase Cloud function using typescript?
(1 answer)
Closed 3 years ago.
If I have a module that is only required for one of my Firebase Cloud functions, this Firebase Tutorial suggests importing that module inside just the function that needs it, in order to minimize cold start time for all other functions in a project.
This makes sense, but is it also possible to import a class which contains its own set of dependencies inside of a function?
I have a need to use Bcrypt but only in two of my functions. So I would rather not have to load it for all of my other cloud functions where it is not needed.
In my application, I have the following import:
import BcryptTool from './classes/bcrypt'; // <--- only needed in 2 functions
Here is the contents of bcrypt.ts:
import * as bcrypt from 'bcryptjs';
export default class BcryptTool {
public static hashValue(value: string, rounds: number, callback: (error: Error, hash: string) => void) : void {
bcrypt.hash(value, rounds, (error:any, hash:any) => {
callback(error, hash);
});
}
public static compare(value: string, dbHash: string, callback: (error: string | null, match: boolean | null) => void) {
bcrypt.compare(value, dbHash, (err: Error, match: boolean) => {
if(match) {
callback(null, true);
} else {
callback('Invalid value match', null);
}
});
}
}
And finally, in my Firebase Cloud functions index.ts:
const express = require('express');
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const cors = require('cors')({ origin: true });
admin.initializeApp();
const util = express();
const api = express();
...
import BcryptTool from './classes/bcrypt'; // <-- when i import here, calls to its methods within my functions work as expected
...
util.use(cors);
util.post('/verify', async (request: any, response: any) => {
// according to Doug's answer below i should be able to attempt to import here as a solution using a dynamic import expression like so:
const BcryptTool = await import('./classes/bcrypt');
// but the following subsequent call to .compare() fails
BcryptTool.compare(...)
// VS Code hinting shows an error: Property 'compare' does not exist on type 'typeof import('FULL/PATH/TO/CLASS/classes/bcrypt')'
});
api.use(cors);
api.post('/endpoint/foo', async (request: any, response: any) => {
// I do not need Bcrypt here
});
api.post('/endpoint/bar', async (request: any, response: any) => {
// I do not need Bcrypt here
});
Is this not possible? Am I just doing it all wrong?*
Sure, you can async (dynamic) import anywhere you want in TypeScript code. The imported symbols will be visible in the scope where you imported it, and nowhere else. It doesn't matter what the module contains.
(Posted answer on behalf of the question author, to move it from the question post).
I was not importing the class correctly. The reason along with a solution is outlined here.

Resources