Return observable while doing task - Firebase Functions - node.js

I have a function in my Firebase Cloud Functions that needs to run a scraping task. While the function is scraping the public website I need to return to the consumer a progress feedback. (eg. "Now it's at 10%", "Now it's at 30%", etc...).
I tought using Observables but I get weird results because the scraping function is async but it should return immediately the observable.
This is the simplified code of my Firebase Functions Backend:
exports.scrapeFunction = functions.region('europe-west2').https.onCall((data) => {
const res$ = new BehaviorSubject<Response>({
completion: 0,
completionMsg: 'One moment...',
error: undefined,
user: undefined,
});
scrape(data.id, res$);
return res$.asObservable();
});
const scrape = async (id: number, res$: BehaviorSubject<Response>) => {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
res$.next({
completion: 10,
completionMsg: 'I\'m loading the browser...'
})
await page.goto('https://example.com');
res$.next({
completion: 20,
completionMsg: 'Page opened...'
})
/* Other scraping async functions... */
}
On my frontend, instead I've a code similar to this:
response$ = Observable<Response> | undefined;
ngOnInit(): void {
const scrapeFunction= getFunctions(undefined, 'europe-west2');
const verifyClient = httpsCallable(functions, 'scrapeFunction');
const { data } = await scrapeFunction({ id: 0001 });
console.log('Data is', data);
this.response$ = (data as any).source as Observable<Response>;
this.response$.subscribe((res) => console.log(res));
return;
}
Logging in the console I get:
Data is
{source: {...}}
source:
closed: false
currentObservers: null
hasError: false
isStopped: false
observers: []
thrownError: null
_value: {completion: 0, completionMsg: 'One moment...'}
[[Prototype]]: Object
[[Prototype]]: Object
I also get the following error because the function is not returning an Observable as I know, but an object with the first key "source".
ERROR Error: Uncaught (in promise): TypeError: _this.response$.subscribe is not a function
TypeError: _this.response$.subscribe is not a function
at dashboard.component.ts:35:29
at Generator.next (<anonymous>)
at asyncGeneratorStep (asyncToGenerator.js:3:1)
at _next (asyncToGenerator.js:25:1)
at ZoneDelegate.invoke (zone.js:372:1)
at Object.onInvoke (core.js:28692:1)
at ZoneDelegate.invoke (zone.js:371:1)
at Zone.run (zone.js:134:1)
at zone.js:1276:1
at ZoneDelegate.invokeTask (zone.js:406:1)
at resolvePromise (zone.js:1213:1)
at zone.js:1120:1
at asyncGeneratorStep (asyncToGenerator.js:6:1)
at _next (asyncToGenerator.js:25:1)
at ZoneDelegate.invoke (zone.js:372:1)
at Object.onInvoke (core.js:28692:1)
at ZoneDelegate.invoke (zone.js:371:1)
at Zone.run (zone.js:134:1)
at zone.js:1276:1
at ZoneDelegate.invokeTask (zone.js:406:1)
Can someone understand this behavior?
[UPDATE 1]
I edited my index.ts file in Firebase Functions to be like this:
const res$ = new BehaviorSubject<Response>({
completion: 0,
completionMsg: 'One moment...',
});
exports.verifyClient = functions.https.onCall((data) => {
scrapeCheck(data.id);
return verifyClientRes$.asObservable();
});
const scrapeCheck = async (id: number) => {
await scrape(id, res$);
res$.complete();
return;
};
/* The scrape function is actually on an external file */
const scrape = (/* ... */) => {
/* ... */
}
** But i still got the same error on the console.
[UPDATE 2]
I created a StackBlitz project. You can find it here:
https://stackblitz.com/edit/wherescrypto?file=functions/src/index.ts
The function that is causing this error is in "functions/src/index.ts" and it's called "verifyClient"

Reviewing some documentation that could help to fix the issue you have, I was wondering if you already considered trying it with a realtime database or firestore.
You'll have to push your updates somewhere, the client app can see them that way. This solution was mentioned in a comment in this stackoverflow question.

Putting scrape in setTimeout will do the trick here. so that scrape is invoked after observable is returned.
exports.scrapeFunction = functions.region('europe-west2').https.onCall((data) => {
const res$ = new BehaviorSubject<Response>({
completion: 0,
completionMsg: 'One moment...',
error: undefined,
user: undefined,
});
setTimeout(() => scrape(data.id, res$));
return res$.asObservable();
});

Related

Next-Auth + Prism error - "TypeError: Cannot read properties of undefined (reading 'user')"

I've set up next-auth to use EmailProvider and a prisma adapter as described in the documentation
authOptions
const prisma = globalThis.prisma || new PrismaClient();
globalThis.prisma = prisma;
export const authOptions = {
// Configure one or more authentication providers
adapter: PrismaAdapter(prisma),
providers: [
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM
})
}
.env
DATABASE_URL=postgresql://dbname:password#localhost:5432/dbname?schema=public
EMAIL_SERVER=smtp://admin#myemail.ca:password#mail.privateemail.com:465
EMAIL_FROM=from#email.com
I haven't changed anything in the config and have tried a clean install. The prisma is connecting to my DB with no issues.
The stack trace below makes reference to getAdapterUserFromEmail, which is calling getUserByEmail. In getUserByEmail, the prisma client is suddenly undefined. I can see the client is getting created when the PrismaAdapter methods are defined, but it's undefined by the time the method is called.
next-auth/core/lib/email/getUserFromEmail
async function getAdapterUserFromEmail({
email,
adapter
}) {
const {
getUserByEmail
} = adapter;
const adapterUser = email ? await getUserByEmail(email) : null; //CALLS getUserByEmail HERE. Email is a string because I just signed in
if (adapterUser) return adapterUser;
return {
id: email,
email,
emailVerified: null
};
}
#next-auth/prisma-adapter/dist/index.js:
function PrismaAdapter(p) {
return {
createUser: (data) => p.user.create({ data }),
getUser: (id) => p.user.findUnique({ where: { id } }),
getUserByEmail: (email) => getUserByEmail: (email) => {
p.user.findUnique({ where: { email } }) //Why is p undefined?? :(
},
}
}
Stack trace:
error - TypeError: Cannot read properties of undefined (reading 'user')
at getUserByEmail (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/#next-auth/prisma-adapter/dist/index.js:8:38)
at _callee2$ (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/next-auth/core/errors.js:365:29)
at tryCatch (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/#babel/runtime/helpers/regeneratorRuntime.js:44:17)
at Generator.<anonymous> (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/#babel/runtime/helpers/regeneratorRuntime.js:125:22)
at Generator.next (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/#babel/runtime/helpers/regeneratorRuntime.js:69:21)
at asyncGeneratorStep (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/#babel/runtime/helpers/asyncToGenerator.js:3:24)
at _next (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/#babel/runtime/helpers/asyncToGenerator.js:22:9)
at /Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/#babel/runtime/helpers/asyncToGenerator.js:27:7
at new Promise (<anonymous>)
at /Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/#babel/runtime/helpers/asyncToGenerator.js:19:12
at getAdapterUserFromEmail (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/next-auth/core/lib/email/getUserFromEmail.js:15:37)
at Object.signin (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/next-auth/core/routes/signin.js:77:54)
at AuthHandler (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/next-auth/core/index.js:253:39)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async NextAuthHandler (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/next-auth/next/index.js:23:19)
at async /Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/next-auth/next/index.js:59:32
at async Object.apiResolver (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/next/dist/server/api-utils/node.js:363:9)
at async DevServer.runApi (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/next/dist/server/next-server.js:487:9)
at async Object.fn (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/next/dist/server/next-server.js:749:37)
at async Router.execute (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/next/dist/server/router.js:253:36)
at async DevServer.run (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/next/dist/server/base-server.js:384:29)
at async DevServer.run (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/next/dist/server/dev/next-dev-server.js:741:20)
at async DevServer.handleRequest (/Users/ryan/Documents/Projects/animated_section_builder/app-platform/node_modules/next/dist/server/base-server.js:322:20) {
name: 'GetUserByEmailError',
code: undefined,
page: '/api/auth/[
You probably have it figured out by now, but I just had the same problem.
In my case, it seems the problem was with PrismaClient import in the lib folder where I had the prisma instance.
The error I was getting was this:
Module '"#prisma/client"' has no exported member 'PrismaClient'.ts
I deleted node_modules and reinstalled prisma and everything again and that solved the issue. Not sure what caused it in the first place. Prisma was working fine before I installed next-auth.

Discord.js | interaction.reply is not a function [SlashCommand][Typescript]

I am making a /ping command in my bot, following the discordjs guide, but when I use the command, I get an error:
TypeError: interaction.reply is not a function
at Object.<anonymous> (C:\Users\timda\code\discord bot\bot-data\commands\ping.ts:8:15)
at Generator.next (<anonymous>)
at C:\Users\timda\code\discord bot\bot-data\commands\ping.ts:8:71
at new Promise (<anonymous>)
at __awaiter (C:\Users\timda\code\discord bot\bot-data\commands\ping.ts:4:12)
at Object.execute (C:\Users\timda\code\discord bot\bot-data\commands\ping.ts:18:16)
at C:\Users\timda\code\discord bot\main.ts:43:18
at Generator.next (<anonymous>)
at C:\Users\timda\code\discord bot\main.ts:8:71
at new Promise (<anonymous>)
This is the /ping commands code:
import { SlashCommandBuilder } from 'discord.js';
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with Pong!'),
async execute(interaction: { reply: (arg0: string) => any }) {
console.log(interaction);
interaction.reply('Pong!');
},
};
And here is the code in my main file that loads in the SlashCommand files:
import fs from 'node:fs';
import path from 'node:path';
import { Client, Collection, GatewayIntentBits, Partials, REST, Routes } from 'discord.js';
import { clientId, token } from './config.json';
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.DirectMessages, GatewayIntentBits.MessageContent],
partials: [Partials.Message, Partials.Channel],
presence: { status: 'online', activities: [{ name: 'you 👀', type: 3 }] }
});
(client as any).commands = new Collection();
const commands: any[] = [];
for (const file of (fs.readdirSync(path.join('./bot-data/commands')).filter(file => file.endsWith('.ts')))) {
const command = require(`./bot-data/commands/${file}`);
(client as any).commands.set(command.data.name, command);
commands.push(command.data.toJSON());
};
client.on('ready', async (client: { user: { tag: any; }; }) => {
const rest = new REST({ version: '10' }).setToken(token);
try {
await rest.put(Routes.applicationCommands(clientId), { body: commands});
} catch (error) {
console.error(error);
}
console.log(`Succesfully logged in as ${client.user.tag}!`);
});
client.on('interactionCreate', async interaction => {
if(interaction.isCommand()) {
const command = (client as any).commands.get(interaction.commandName);
if (!command) {
await interaction.reply({content: `Something went wrong while executing ${interaction.commandName}.`, ephemeral: true});
return;
}
try {
await command.execute(client, interaction);
} catch (error) {
await interaction.reply({content: `Something went wrong while executing ${interaction.commandName}., ephemeral: true});
console.error(error);
}
}
});
The command does get loaded when /ping is used, because it does log the interaction in the console.
I am writing the bot in Typescript, that's why I did interaction: { reply: (arg0: string) => any }
and I'm using node to compile and run it.
NVM: version 1.1.10
NPM: version 8.19.2
I've tried writing interaction: { reply: (arg0: string) => any } in different ways and I've also already done a lot of googling, but I can't find the problem I'm facing here.
If someone could help me I would appreciate it.
Inside of main.ts, you're calling the execute function with these arguments:
await command.execute(client, interaction);
But inside of ping.ts, you're only accepting these arguments:
async execute(interaction: { reply: (arg0: string) => any }) { // etc
You need to include both client and interaction as arguments in ping.ts to prevent the values being passed into the wrong variables.
// I'm fairly certain that these are the right imports
import { BaseInteraction, SlashCommandBuilder, Client} from 'discord.js';
...
async execute(client: Client, interaction: BaseInteraction) { // etc

NestJs REPL Uncaught SyntaxError: await is only valid in async function

When using NestJS REPL I'm getting the following error despite running against an async function that I can see is returning a Promise.
Uncaught SyntaxError: await is only valid in async function
See the block below for the Service code I'm calling and an example of the code returning a promise but then throwing the error when trying to await.
CoffeesService.ts
async findOne(id: string) {
const coffee = await this.coffeeRepository.findOne(id, {
relations: ['flavors'],
});
if (!coffee) {
throw new NotFoundException(`Coffee ${id} not found`);
}
return coffee;
}
REPL
> coffeesService.findOne(1)
Promise {
<pending>,
[Symbol(async_id_symbol)]: 393,
[Symbol(trigger_async_id_symbol)]: 45,
[Symbol(destroyed)]: { destroyed: false }
}
> await coffeesService.findOne(1)
await coffeesService.findOne(1)
^^^^^
Uncaught SyntaxError: await is only valid in async function
According to NestJS docs, await is a top level function https://docs.nestjs.com/recipes/repl#:~:text=Native%20functions-,Read%2DEval%2DPrint%2DLoop%20(REPL),%7D%0A%3E%20await%20appController.getHello()%0A%27Hello%20World!%27,-To%20display%20all
This is a great new feature and I'm hoping I'm just doing something wrong :/
As suggested above, I needed to upgrade past node version 16. it works perfectly in node 18
> await get(CoffeesService).findOne(1)
Coffee {
id: 1,
name: 'Hobo roast',
brand: 'SF Roasters',
description: null,
recommendations: 0,
flavors: [
Flavor { id: 1, name: 'trash' },
Flavor { id: 2, name: 'cigarettes' }
]
}

Stubbing pino logger as class instance

I have a custom log service, using a child instance of pinoLogger to log some things. Here is the code :
class LogService {
ihmLogger = loggerPino.child({});
async log(
req: Request,
level: number,
message: string,
additional?: string
): Promise<void> {
//Context initialisation
const logObj = this.initLogObj(
req,
ngxLoggerLevelToPinoLevel[level],
message,
additional
);
// Mapping log level with pino logger
switch (ngxLoggerLevelToPinoLevel[level]) {
case CONSTANTS.DEFAULT.LOG_NGX_LEVEL.ERROR:
this.ihmLogger.error(logObj);
break;
case CONSTANTS.DEFAULT.LOG_NGX_LEVEL.WARM:
this.ihmLogger.warn(logObj);
break;
case CONSTANTS.DEFAULT.LOG_NGX_LEVEL.INFO:
this.ihmLogger.info(logObj);
break;
case CONSTANTS.DEFAULT.LOG_NGX_LEVEL.DEBUG:
this.ihmLogger.debug(logObj);
break;
}
}
private initLogObj(
req: Request,
level: number,
message: string,
additional?: string
): ILog {
const additionalObj = JSON.parse(additional || '{}');
return {...};
}
}
export default new LogService();
I'm trying to write unit-tests for this service. I'm using node-request-http to mock the Request object, successfully.
My problem concerns this child element. I'm simply trying to see if the correct function is called depending on the level, but I can't access it correctly. Here are the few syntaxes I tried :
[...]
import logService from './log.service';
import { loggerPino } from '../utils/logger';
[...]
it("test 1", async () => {
sinon.stub(loggerPino,'child').returns({
error: sinon.spy()
});
await logService.log(request, 5, 'msg', JSON.stringify({}));
console.log('A',logService.ihmLogger.error);
});
// >> A [Function: noop]
it("test 2", async () => {
const logger = sinon.stub(logService, 'ihmLogger').value({
error: sinon.spy()
});
await logService.log(request, 5, 'msg', JSON.stringify({}));
console.log('B',logger.error);
});
// >> B undefined
it("test 3", async () => {
const logger = sinon.stub(logService, 'ihmLogger').value({
error: sinon.spy()
});
await logService.log(request, 5, 'msg', JSON.stringify({}));
console.log('C',logService.ihmLogger.error);
});
// >> C [Function (anonymous)]
I never could access the "callCount" valut, let alone checking the given parameter with a getCall(0).args[0]
How should I write the stub to be able to correctly access the value i'm trying to test?
The solution here was to completely replace the ihmLogger property of the service, as done in the "B" try up there, but putting it in a variable and checking the variable, not the sinon.stub object returned :
it("Log error called", async () => {
const customLogger = {
trace: sinon.spy(),
debug: sinon.spy(),
info: sinon.spy(),
warn: sinon.spy(),
error: sinon.spy(),
fatal: sinon.spy()
};
sinon.stub(logService, 'ihmLogger').value(customLogger);
await logService.log(request, 5, 'msg', JSON.stringify({}));
expect(customLogger.error.callCount).to.eq(1);
expect(customLogger.fatal.callCount).to.eq(0); //double check to avoid false positive
});
I'm not unit-testing the pinoLogger object (I have to trust if to work as intended), I'm unit-testing the correct branch & call.
Doing this, I'm also able to inspect the logObj transmitted :
const builtObj = customLogger.error.getCall(0).args[0]

context.done called twice within handler 'graphql'

Attempting to create a project based off https://github.com/serverless/serverless-graphql/blob/master/app-backend/dynamodb/handler.js. The code works well, but for some reason, I always get a log warning telling me context.done called twice.
import { graphqlLambda, graphiqlLambda, LambdaHandler } from 'apollo-server-lambda'
import lambdaPlayground from 'graphql-playground-middleware-lambda'
import { makeExecutableSchema } from 'graphql-tools'
import { resolvers } from './resolvers'
const typeDefs = require('./schema.gql')
const schema = makeExecutableSchema({ typeDefs, resolvers, logger: console })
export const graphqlHandler: LambdaHandler = async (event, context) => {
const handler = graphqlLambda({ schema })
return handler(event, context, (error: Error | undefined, output: any) => {
output.headers['Access-Control-Allow-Origin'] = '*'
context.done(error, output)
})
}
export const playgroundHandler = lambdaPlayground({
endpoint: '/graphql',
})
export const graphiqlHandler: any = graphiqlLambda({
endpointURL: '/graphql',
})
This code gives me the following result:
Serverless: POST /graphql (λ: graphql)
Serverless: [200] {"statusCode":200,"headers":{"Content-Type":"application/json","Access-Control-Allow-Origin":"*"},"body":"{\"data\":{\"getUserInfo\":\"ads\"}}"}
Serverless: Warning: context.done called twice within handler 'graphql'!
What is even more strange is that if I comment the context.done call, I get the following output (the call stalls as expected):
Serverless: POST /graphql (λ: graphql)
Serverless: Warning: context.done called twice within handler 'graphql'!
I ran into a similar issue. Try remove async in your function if you are not going to use await. It looks like the function waits until you invoke callback or context.done when async is not there. But it runs through all the way with the async keyword. When it runs through though, whatever it returns at the end of your function is calling the context.done. Here is a sample working code.
Let me also share two useful resources about callback and context from aws lambda.
export const handler = (
{
headers,
pathParameters: pathParams,
queryStringParameters: queryParams,
body,
},
context,
callback
) => {
context.callbackWaitsForEmptyEventLoop = false;
const data = JSON.parse(body);
const params = {
TableName: process.env.EVENT_TABLE,
Item: {
id: uuid.v4(),
title: data.title,
creationDate: new Date().getTime(),
},
};
dynamoDb.put(params, (err, data) => {
if (err) {
callback(err);
}
callback(null, {
statusCode: 200,
body: JSON.stringify(data),
});
});
};
This appears to be already reported: https://github.com/dherault/serverless-offline/issues/405

Resources