How to debug an ApolloServer's context function? - javascript

I'm trying to follow the GraphQL tutorial (https://www.apollographql.com/docs/tutorial/resolvers/), but I'm getting an error in the playground when I try to book a trip, with a stack trace starting like this:
"TypeError: Cannot read property 'id' of null",
" at UserAPI.bookTrips (/Users/kurtpeek/Documents/Scratch/fullstack-tutorial/start/server/src/datasources/user.js:35:38)",
In src/index.js, the ApolloServer (https://www.apollographql.com/docs/apollo-server/api/apollo-server/) is defined with an asynchronous context() function like so:
const { ApolloServer } = require('apollo-server');
const typeDefs = require('./schema');
const { createStore } = require('./utils');
const resolvers = require('./resolvers');
const LaunchAPI = require('./datasources/launch');
const UserAPI = require('./datasources/user');
const isEmail = require('isemail');
const store = createStore();
const server = new ApolloServer({
context: async ({ req }) => {
debugger;
const auth = (req.headers && req.headers.authorization) || '';
const email = Buffer.from(auth, 'base64').toString('ascii');
if (!isEmail.validate(email)) return { user: null };
const users = await store.users.findOrCreate({ where: { email }});
const user = users && users[0] ? users[0] : null;
return { user: { ...user.dataValues }};
},
typeDefs,
resolvers,
dataSources: () => ({
launchAPI: new LaunchAPI(),
userAPI: new UserAPI({ store })
})
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
Since the error I'm seeing is the result of the context's user being null, I would like to step through the context() method to inspect the headers and see whether it contains a valid (encoded) email; as you can see I've set a debugger breakpoint at the start of that function.
However, when I run node inspect src/index.js, I immediately enter the debugger for an IntrospectionQuery:
If I press the 'Resume script execution' button, I just get back immediately to the same breakpoint.
Does this have something to do with the fact that context() is an async function? How can I step through the context() method with the debugger?

In the end, I just debugged it using console.log() commands. I had accidentally put the authorization in the 'Query Variables' section rather than the 'HTTP Headers' one. Moving it to 'HTTP Headers' produces the desired result:

Related

How to validate GitHub webhook with Deno?

I'm trying to make a GitHub webhook server with Deno, but I cannot find any possible way to do the validation.
This is my current attempt using webhooks-methods.js:
import { Application } from "https://deno.land/x/oak/mod.ts";
import { verify } from "https://cdn.skypack.dev/#octokit/webhooks-methods?dts";
const app = new Application();
app.use(async (ctx, next) => {
try {
await next();
} catch (_err) {
ctx.response.status = 500;
}
});
const secret = "...";
app.use(async (ctx) => {
const signature = ctx.request.headers.get("X-Hub-Signature-256");
if (signature) {
const payload = await ctx.request.body({ type: "text" }).value;
const result = await verify(secret, payload, signature);
console.log(result);
}
ctx.response.status = 200;
});
The verify function is returning false every time.
Your example is very close. The GitHub webhook documentation details the signature header schema. The value is a digest algorithm prefix followed by the signature, in the format of ${ALGO}=${SIGNATURE}:
X-Hub-Signature-256: sha256=d57c68ca6f92289e6987922ff26938930f6e66a2d161ef06abdf1859230aa23c
So, you need to extract the signature from the value (omitting the prefix):
const signatureHeader = request.headers.get("X-Hub-Signature-256");
const signature = signatureHeader.slice("sha256=".length);
Update: Starting in release version 3.0.1 of octokit/webhooks-methods.js, it is no longer necessary to manually extract the signature from the header — that task is handled by the verify function. The code in the answer has been updated to reflect this change.
Here's a complete, working example that you can simply copy + paste into a project or playground on Deno Deploy:
gh-webhook-logger.ts:
import { assert } from "https://deno.land/std#0.177.0/testing/asserts.ts";
import {
Application,
NativeRequest,
Router,
} from "https://deno.land/x/oak#v11.1.0/mod.ts";
import type { ServerRequest } from "https://deno.land/x/oak#v11.1.0/types.d.ts";
import { verify } from "https://esm.sh/#octokit/webhooks-methods#3.0.2?pin=v106";
// In actual usage, use a private secret:
// const SECRET = Deno.env.get("SIGNING_SECRET");
// But for the purposes of this demo, the exposed secret is:
const SECRET = "Let me know if you found this to be helpful!";
type GitHubWebhookVerificationStatus = {
id: string;
verified: boolean;
};
// Because this uses a native Request,
// it can be used in other contexts besides Oak (e.g. `std/http/serve`):
async function verifyGitHubWebhook(
request: Request,
): Promise<GitHubWebhookVerificationStatus> {
const id = request.headers.get("X-GitHub-Delivery");
// This should be more strict in reality
assert(id, "Not a GH webhhok");
const signatureHeader = request.headers.get("X-Hub-Signature-256");
let verified = false;
if (signatureHeader) {
const payload = await request.clone().text();
verified = await verify(SECRET, payload, signatureHeader);
}
return { id, verified };
}
// Type predicate used to access native Request instance
// Ref: https://github.com/oakserver/oak/issues/501#issuecomment-1084046581
function isNativeRequest(r: ServerRequest): r is NativeRequest {
// deno-lint-ignore no-explicit-any
return (r as any).request instanceof Request;
}
const webhookLogger = new Router().post("/webhook", async (ctx) => {
assert(isNativeRequest(ctx.request.originalRequest));
const status = await verifyGitHubWebhook(ctx.request.originalRequest.request);
console.log(status);
ctx.response.status = 200;
});
const app = new Application()
.use(webhookLogger.routes())
.use(webhookLogger.allowedMethods());
// The port is not important in Deno Deploy
await app.listen({ port: 8080 });

Data in transaction in a cloud function is always null when run in test suite

I have a Firebase cloud function that does this:
const admin = require('firebase-admin')
admin.initializeApp()
exports.setSessionState = functions.https.onCall(async (data, context) => {
const stateId = data.stateId
const details = data.details
const stateRef = admin.database().ref(`state/${stateId}`)
stateRef.transaction((state) => {
if (state) {
state.details = details
}
return state
})
})
The code works well in the actual application and the state is updated but I run into problems when running this in a test, where the state is always null. (In other words, the details are never set.) The test uses the Mocha framework and is run against a real Firebase project and I can't see how anything would differ here but the behaviour is consistently different when calling this cloud function from the client application and when calling it from the test suite:
const chai = require('chai')
const assert = chai.assert
const test = require('firebase-functions-test')({
databaseURL: '<redacted>',
storageBucket: '<redacted>',
projectId: '<redacted>',
}, '../service-account-credentials.json')
describe('CFsetSessionState', () => {
let wrapped
let cloudFunctions
let admin
before(() => {
cloudFunctions = require('../index')
admin = require('firebase-admin')
wrapped = test.wrap(cloudFunctions.CFsetSessionState)
})
it('Test setting state', async () => {
const stateId = 'abc'
const data = {
stateId: stateId,
details: {
name: 'New name'
}
}
const context = {
auth: {
uid: 123
}
}
const stateRef = admin.database().ref(`state/${stateId}`)
await stateRef.set({
name: 'Previous name',
active: true
})
await wrapped(data, context)
const snapshot = await stateRef.once('value')
const state = snapshot.val()
// Details do not exist here, why?
assert.equal(state.details.name, 'New name')
})
})
I leave the database state as is after the tests so I can see there is indeed data in the state object but the details have not been set. Experimenting a bit with setting and fetching data (using .once()) before and after the call to the cloud function can change the behaviour, making me think it might be some cache issue, but this experimenting does not give me any particular stable end state. I have no idea how the equivalent of a local cache works in cloud functions, it currently shows random behaviour.
What could cause this?
I haven't tried running your Cloud Function with your test, but most probably the problem comes from the fact that you incorrectly manage the life cycle of your Cloud Function. Since it is Callable one, you need to terminate it by returning a Promise (or a value when all the asynchronous work is completed). More details here and here in the doc.
You should therefore adapt your CF as follows:
exports.setSessionState = functions.https.onCall(async (data, context) => {
const stateId = data.stateId
const details = data.details
const stateRef = admin.database().ref(`state/${stateId}`)
return stateRef.transaction((state) => { // Note the return here
if (state) {
state.details = details
}
return state
})
})
We are returning the Promise returned by the Transaction.

Error in Apollo Server deploy with AWS Lambda

People, how are you? I have a query, I just implemented my API made with apollo server in an AWS Lambda. I used the official documentation as a guide, but I'm noticing that the context handling varies a bit. I have a doubt with the latter, since I made certain changes and everything works fine locally using "serverless offline", but once I deploy it doesn't. Apparently the authentication context that I generate does not finish reaching my query. If someone can guide me a bit with this, I will be very grateful.
This is my API index:
const { ApolloServer, gql } = require('apollo-server-lambda');
const typeDefs = require('./db/schema');
const resolvers = require('./db/resolvers');
const db = require('./config/db');
const jwt = require('jsonwebtoken');
require('dotenv').config({ path: 'variables.env' });
db.conectDB();
// The ApolloServer constructor requires two parameters: your schema
// definition and your set of resolvers.
const server = new ApolloServer({
typeDefs,
resolvers,
playground: {
endpoint: "/graphql"
},
context: ({ event, context }) => {
try {
const token = event.headers['authorization'] || '';
if(token){
context.user = jwt.verify(token.replace('Bearer ',''), process.env.KEY_TOKEN);
}
return {
headers: event.headers,
functionName: context.functionName,
event,
context,
}
} catch (error) {
console.error(error);
}
}
});
exports.graphqlHandler = server.createHandler({
cors: {
origin: '*',
credentials: true,
},
});
This is my query:
getUserByToken: async (_, {}, { context }) => {
if(context)
throw new Error((context ? 'context' : '') + ' ' + (context.user ? 'user' : ''));
let user = await db.findOne('users',{ _id: ObjectId(context.user._id) });
if(user.birthdate)
user.birthdate = user.birthdate.toString();
if(user.password)
user.password = true;
else
user.password = false;
return user;
}
My API response:
API response
From what I can see, you're not calling getUserByToken in your context. Is that correct? So, I'm not sure how you're encountering this error.
Can I give you some pointers?
Connecting to your DB is probably (or it should be) asynchronous. For that, I'd run your code like this:
db.connect()
.then(() => {
... handle your request in here
})
.catch(console.error);
I think you meant to call your getUserByToken in this line:
context.user = jwt.verify(token.replace('Bearer ',''), process.env.KEY_TOKEN);

TypeError: this.path.replace is not a function at new FirestoreDelete

Trying to remove the users`s data calling a function from app.
'use strict';
const functions = require('firebase-functions');
const firebase_tools = require('firebase-tools');
const admin = require('firebase-admin');
const serviceAccount = require('./myapp.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://myapp.firebaseio.com"
});
let db = admin.firestore();
exports.mintAdminToken = functions.https.onCall((data, context) => {
const uid = data.uid;
return admin
.auth()
.createCustomToken(uid, { admin: true })
.then(function(token) {
return { token: token };
});
});
exports.recursiveDelete = functions
.runWith({
timeoutSeconds: 540,
memory: '1GB'
})
.https.onCall((data, context) => {
if (!(context.auth && context.auth.token )) {
throw new functions.https.HttpsError(
'permission-denied',
'Must be an administrative user to initiate delete.'
);
}
let path = data.path;
console.log(
`User ${context.auth.uid} has requested to delete path ${path}`
);
return firebase_tools.firestore
.delete(path, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true,
token: functions.config().fb.token
})
.then(() => {
return {
path: path
};
});
});
and I pass the path like:
Map<String, Object> data = new HashMap<>();
data.put("path", "./users/rnAjpK4LLSMMlENZqe4l3F2");
result:
Function execution took 540003 ms, finished with status: 'timeout'
probably the problem is in path. if I change for this line:
let path = admin.firestore().doc('users/' + context.auth.uid);
Im getting an error
Unhandled error TypeError: this.path.replace is not a function at new FirestoreDelete
is the problem with "path"?
what will be the right path to delete then?
I use this example https://github.com/firebase/snippets-node/tree/master/firestore/solution-deletes but here is nothing about it
UPDATED:
with
String path = "./users/rnAjpK4LLSMMlENAgrZqe4l3F2";
or
String data = "./users/rnAjpK4LLSMMlENAgrZqe4l3F2";
an error
Unhandled error { FirebaseError: Must specify a path. at Object.reject (/srv/node_modules/firebase-tools/lib/utils.js:82:27)
solution
const id = context.auth.uid;
const path = `users/${id}`;
As far as I can see from reading the code of the delete function, the path you pass in has to be a single string value of the collection or document to delete. The function will then delete all data under that collection or document.
I was accidentally sending an entire object. If you come across this question, double check you're not making the same mistake.

Javascript functions in firebase

Getting the following error:
"Cannot read property 'userName' of undefined
at Promise.all.then.result"
Also Getting Error
"The behavior for Date objects stored in Firestore is going to change
AND YOUR APP MAY BREAK.
To hide this warning and ensure your app does not break, you need to add the
following code to your app before calling any other Cloud Firestore methods:
const firestore = new Firestore();
const settings = {/* your settings... */ timestampsInSnapshots: true};
firestore.settings(settings);
With this change, timestamps stored in Cloud Firestore will be read back as
Firebase Timestamp objects instead of as system Date objects. So you will also
need to update code expecting a Date to instead expect a Timestamp. For example:
// Old:
const date = snapshot.get('created_at');
// New:
const timestamp = snapshot.get('created_at');
const date = timestamp.toDate();
Please audit all existing usages of Date when you enable the new behavior. In a
future release, the behavior will change to the new behavior, so if you do not
follow these steps, YOUR APP MAY BREAK."
However in my android project the place where i have defined the "Date" variable i have place the "#ServerTimestamp" on top.
Appreciate the help guys.
Code:
/*eslint-disable */
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendNotification = functions.firestore.document('notifications/{userEmail}/userNotifications/{notificationId}').onWrite((change, context) => {
const userEmail = context.params.userEmail;
const notificationId = context.params.notificationId;
return admin.firestore().collection("notifications").doc(userEmail).collection("userNotifications").doc(notificationId).get().then(queryResult => {
const senderUserEmail = queryResult.data().senderUserEmail;
const notificationMessage = queryResult.data().notificationMessage;
const fromUser = admin.firestore().collection("users").doc(senderUserEmail).get();
const toUser = admin.firestore().collection("users").doc(userEmail).get();
return Promise.all([fromUser, toUser]).then(result => {
const fromUserName = result[0].data().userName;
const toUserName = result[1].data().userName;
const tokenId = result[1].data().tokenId;
const notificationContent = {
notification: {
title: fromUserName + " is shopping",
body: notificationMessage,
icon: "default"
}
};
return admin.messaging().sendToDevice(tokenId, notificationContent).then(result => {
console.log("Notification sent!");
//admin.firestore().collection("notifications").doc(userEmail).collection("userNotifications").doc(notificationId).delete();
});
});
});
});
Make sure the document you're request actually exists. data() will return undefined if it doesn't. You can use the exists property on the resulting DataSnapshot to check if a document was actually found.

Categories