I have an graphql server connected to my Firebase RTD and deployed on heroku.
When I run my server in heroku the request to reservations resolver takes forever and eventually the Playground yells Unexpected token < in JSON at position 0
I suspect this is a timeout from Firebase, but how would I go about debugging this? (Heroku logs nothing about the error).
You can try the server for yourself: https://filex-database.herokuapp.com
The specific query that's causing me trouble is:
query {
reservations {
code
name
}
}
const db = require("../datasources/db");
const masterlist = require("../datasources/masterlist.js");
const getById = (key: string, id: string) =>
db[key].filter((item) => item.id === id)[0];
const firebaseQuery = (context: { firebaseClient }, endpoint: string) => {
const finalEndpoint =
endpoint.charAt(0) === "/" ? endpoint : "/".concat(endpoint);
const baseUrl = "/workshops";
return context.firebaseClient
.database()
.ref(`${baseUrl}${finalEndpoint}`)
.once("value")
.then((snapshot) => snapshot.val());
};
const Query = {
workshops: () => db.workshops,
workshop: (_, args) => getById("workshops", args.id),
options: () => db.options,
option: (_, args) => getById("options", args.id),
// this resolver is causing me trouble
reservations: async (_, __, context) => {
const data = await firebaseQuery(context, "/applicants");
return Object.values(data);
},
reservation: async (_, args, context) => {
const data = await firebaseQuery(context, `/applicants/${args.id}`);
return data;
},
};
module.exports = { Query };
EDIT: I made another simple server using the same technology and only delivering that resolver and it also timesout (everything works fine locally though)
http://apollo-testing-gonzo.herokuapp.com
Related
I have been working on a firebase project in which I created a cloud function that creates documents in firestore. This is the function -
export const createExpenseCategory = functions
.region("europe-west1")
.https.onCall(async (data, context) => { // data is a string
if (!context.auth?.uid) { // check that requesting user is authenticated
throw new functions.https.HttpsError(
"unauthenticated",
"Not Authenticated"
);
}
const res = await admin
.firestore()
.collection("/categories/")
.where("uid", "==", context.auth.uid)
.get();
const categoryExists = res.docs.find((doc) => doc.data().name === data); // check that there are not duplicates.
// doc looks like this -
// {
// "name": "Food",
// "uid": "some_long_uid"
// }
if (categoryExists) {
throw new functions.https.HttpsError(
"already-exists",
`Category ${data} already exists`
);
}
return admin
.firestore()
.collection("/categories/")
.add({ name: data, uid: context.auth.uid });
});
As you can see, at the beginning of the function I check whether the user that sent the request is authenticated with the context parameter. Everything works fine when I play around with it in my web app, but I have been trying to figure out a way to create a unittest for this function. My problem is that I can't really figure out how to create an authenticated request to make sure that my function doesn't fail every time. I tried to look online for any documentation but couldn't seem to find any.
Thanks in advance!
You can unit test your functions using the firebase-functions-test SDK. The guide mentions you can mock the data within the eventContext or context parameter passed to your function. This works for mocking the uid field of the auth object:
// Left out authType as it's only for RTDB
wrapped(data, {
auth: {
uid: 'jckS2Q0'
}
});
The guide uses mocha for testing, but you can use other testing frameworks. I made a simple test to see if it would work and I could send the mock uid to the function, which worked as expected:
index.js
exports.authTest = functions.https.onCall( async (data, context) => {
if(!context.auth.uid){
throw new functions.https.HttpsError('unauthenticated', 'Missing Authentication');
}
const q = await admin.firestore().collection('users').where('uid', '==', context.auth.uid).get();
const userDoc = q.docs.find(doc => doc.data().uid == context.auth.uid);
return admin.firestore().collection('users').doc(userDoc.id).update({name: data.name});
});
index.test.js
const test = require('firebase-functions-test')({
projectId: PROJECT_ID
}, SERVICE_ACCTKEY); //Path to service account file
const admin = require('firebase-admin');
describe('Cloud Functions Test', () => {
let myFunction;
before(() => {
myFunction = require('../index.js');
});
describe('AuthTest', () => {
it('Should update user name in UID document', () => {
const wrapped = test.wrap(myFunction.authTest);
const data = {
name: 'FooBar'
}
const context = {
auth: {
uid: "jckS2Q0" //Mocked uid value
}
}
return wrapped(data, context).then(async () => {
//Asserts that the document is updated with expected value, fetches it after update
const q = await admin.firestore().collection('users').where('uid', '==', context.auth.uid).get();
const userDoc = q.docs.find(doc => doc.data().uid == context.auth.uid);
assert.equal(userDoc.data().name, 'FooBar');
});
});
});
});
Let me know if this was useful.
I am trying to fetch a csv file via URL (nextjs api route) and while it works locally, the deployed version on vercel seems to block it. To be precise I am using d3 ( d3.csv() ) in the api route, but after deploying on vercel the d3 fetch fails:
api/xx (using d3:)
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
...
const key = push(setRef).key;
if (key) {
const storageURL: string =
'https://firebasestorage.googleapis...'; // --> public
await csv(storageURL) // --> this fails
.then(async data => {
...
called like this:
const refreshData = () => {
replace(asPath);
};
const addGuess = async () => {
await fetch(`${server}/api/xx?id=${currentUser.uid}`, {
method: 'POST',
});
refreshData(); // --> probably not relevant
};
Thanks in advance!
I've written a cloud task and it works perfectly and triggers the link I gave without any problems, but it won't stop retrying running the link.
How can I make it run it only once?
What I'm trying to do is run a Firestore Function once in the future, on a document write in a collection. I found this tutorial for it.
So far my task creation code works perfectly, and delivers correct payload to the function it's going to call. And the called function works correctly too the first time it runs and exits with status 200. But on the retries I have to exit with error 500 since there's no data to access anymore.
I can see the 200 and 500 logs in firestore function's logs, but Cloud Tasks' logs is empty, even if a method has been run 50 times!
This is the full code
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
const { CloudTasksClient } = require('#google-cloud/tasks')
exports.moveActivityFromPlanToRecord = () =>
functions
.region('europe-west1')
.firestore.document('Users/{userId}/Activities/{activityId}')
.onCreate(async snapshot => {
const moveTime = snapshot.data()! as MoveTime
if (!moveTime || !moveTime.dueTime) {
console.log("DueTime is empty or null: \n" + moveTime)
return
}
// Get the project ID from the FIREBASE_CONFIG env var
const project = JSON.parse(process.env.FIREBASE_CONFIG!).projectId
const location = 'europe-west1'
const queue = 'activityDateEventChecker'
//queuePath is going to be a string that uniquely identifes the task
const tasksClient = new CloudTasksClient()
const queuePath: string =
tasksClient.queuePath(project, location, queue)
// URL to my callback function and the contents of the payload to deliver
const url = `https://${location}-${project}.cloudfunctions.net/activityDateEventCheckerCallback`
const docPath = snapshot.ref.path
const dueTime = moveTime.dueTime
const payload: MoveTaskPayload = { docPath, dueTime }
console.log(payload)
// build up the configuration for the Cloud Task
const task = {
httpRequest: {
httpMethod: 'POST',
url: url,
body: Buffer.from(JSON.stringify(payload)).toString('base64'),
headers: {
'Content-Type': 'application/json',
},
},
scheduleTime: {
seconds: moveTime.dueTime / 1000
}
}
// enqueue the task in the queue
return tasksClient.createTask({ parent: queuePath, task: task })
})
interface MoveTime extends admin.firestore.DocumentData {
dueTime?: number
}
interface MoveTaskPayload {
docPath: string,
dueTime: number
}
exports.activityDateEventCheckerCallback = () =>
functions
.region('europe-west1')
.https.onRequest(async (req, res) => {
const payload = req.body as MoveTaskPayload
try {
// getting the item
const activity = await admin.firestore().doc(payload.docPath).get()
// if time is up for it
if (Date.now() >= payload.dueTime && activity.data() != undefined) {
// getting path to activity to be in record
const pathUser = activity.ref.parent.parent?.path
const pathDocRecord = admin.firestore().doc(`${pathUser}/Record/${activity.id}`)
console.log("RECORD-- ", (await (await pathDocRecord.get()).data())?.subject)
// moving activity into record
await pathDocRecord.set(activity.data()!)
await activity.ref.delete()
// sending notif to user
const fcmPayload = {
notification: {
title: `${activity.data()?.subject}`,
body: " Time for activity. Record how it goes!"
},
data: {
activityId: activity.id
}
}
const user = await admin.firestore().doc(pathUser!).get()
const fcmToken: string = user.data()?.fcmToken
return admin.messaging().sendToDevice(fcmToken, fcmPayload)
}
return null
} catch (error) {
console.error(error)
res.status(500).send(error)
return null
}
})
Tasks in Cloud Task retries when it does not get response code 2XX.
You can config the retry in Cloud Task Queue using maxAttempt paramtere.
Details are mentioned in the doc
Deno seems targeting text files, but I also need to serve image files for the website.
You can use send()
The function send() is designed to serve static content as part of a
middleware function. In the most straight forward usage, a root is
provided and requests provided to the function are fulfilled with
files from the local file system relative to the root from the
requested path.
const app = new Application();
app.use(async (context) => {
await send(context, context.request.url.pathname, {
root: `${Deno.cwd()}/static`
});
});
await app.listen({ port: 8000 });
With the following directory structure:
static/
image.jpg
server.js
You can access the image by going to http://localhost:8000/image.jpg
Basically, you just need to set the correct headers for your image type, and supply the image data as a Unit8Array:
In your middleware:
app.use(async (ctx, next) => {
// ...
const imageBuf = await Deno.readFile(pngFilePath);
ctx.response.body = imageBuf;
ctx.response.headers.set('Content-Type', 'image/png');
});
Here's a complete working example, which will download a sample image (the digitized version of the hand-drawn deno logo) and serve it at http://localhost:8000/image, and display "Hello world" at all other addresses. The run options are in the comment on the first line:
server.ts
// deno run --allow-net=localhost:8000,deno.land --allow-read=deno_logo.png --allow-write=deno_logo.png server.ts
import {Application} from 'https://deno.land/x/oak#v5.3.1/mod.ts';
import {exists} from 'https://deno.land/std#0.59.0/fs/exists.ts';
// server listen options
const listenOptions = {
hostname: 'localhost',
port: 8000,
};
// sample image
const imageFilePath = './deno_logo.png';
const imageSource = 'https://deno.land/images/deno_logo.png';
const ensureLocalFile = async (localPath: string, url: string): Promise<void> => {
const fileExists = await exists(localPath);
if (fileExists) return;
console.log(`Downloading ${url} to ${localPath}`);
const response = await fetch(url);
if (!response.ok) throw new Error('Response not OK');
const r = response.body?.getReader;
const buf = new Uint8Array(await response.arrayBuffer());
await Deno.writeFile(imageFilePath, buf);
console.log('File saved');
};
await ensureLocalFile(imageFilePath, imageSource);
const app = new Application();
app.use(async (ctx, next) => {
// only match /image
if (ctx.request.url.pathname !== '/image') {
await next(); // pass control to next middleware
return;
}
const imageBuf = await Deno.readFile(imageFilePath);
ctx.response.body = imageBuf;
ctx.response.headers.set('Content-Type', 'image/png');
});
// default middleware
app.use((ctx) => {
ctx.response.body = "Hello world";
});
// log info about server
app.addEventListener('listen', ev => {
const defaultPortHttp = 80;
const defaultPortHttps = 443;
let portString = `:${ev.port}`;
if (
(ev.secure && ev.port === defaultPortHttps)
|| (!ev.secure && ev.port === defaultPortHttp)
) portString = '';
console.log(`Listening at http${ev.secure ? 's' : ''}://${ev.hostname ?? '0.0.0.0'}${portString}`);
console.log('Use ctrl+c to stop\n');
});
await app.listen(listenOptions);
Register middleware like this:
// serve static files
app.use(async (context, next) => {
try {
await context.send({
root: `${Deno.cwd()}/wwwroot/static`,
index: "index.html",
});
} catch {
await next();
}
});
I have wrote a function to check whether an email is unique or not within a collection. The issue, is that I have a GET request hooked onto this function, but the function's req.query is always empty.
My request looks like this: us-central1/users?email=mymail#test.com
I've tried doing this without Express, then I added Express, the same issue occurs.
Complete code: (deploying locally with npm run deploy)
const cors = require('cors');
const app = require('express')();
app.use(cors({ origin: true }));
initializeApp();
const fireStore = firestore();
const checkUserName = (req: any, res: any) => {
if (req.method !== 'GET') {
return res.status(405).send(`${req.method} is not allowed`);
}
console.log(JSON.stringify(req.query));
if (!req.query.hasOwnProperty('email')) {
return res.status(400).send('Email not provided');
}
const email = req.query.email;
if (email.trim().length === 0) {
return res.status(400).send('Invalid email address');
}
fireStore
.collection('users')
.doc(email)
.get()
.then((doc: any) => {
return res.status(200).send(doc);
})
.catch((error: any) => console.log(error));
};
app.get('/', checkUserName);
exports.users = functions.https.onRequest(app);