How to listen for specific Firestore document creation event? - firebase

I am implementing a command/response pattern where the user writes to a command collection by calling add with a payload under his own userId, and then gets the data from a similar response path.
However the code below doesn't work, because onSnapshot can not listen for a document that hasn't yet been created (document command.id in the /responses/{userId}/register collection). This would be easy to solve with an onCreate handler, which exists for cloud functions but not for the JS firebase client API it seems.
This is using redux-firestore and some of my app helper functions, but you'll get the idea. The command and response document structures use { payload, error} similar to FSA
Cloud Function
export const register = functions.firestore
.document("commands/{userId}/register/{commandId}")
.onCreate(async event => {
const payload = event.data.get("payload");
const { userId, commandId } = event.params;
const response = db.document(`responses/${userId}/register/${commandId}`)
// possibly something bad will happen
try {
// do something with payload...
return response.set({
payload: "ok" // or pass relevant response data
})
} catch(err) {
return response.set({
error: true
payload: error
})
}
});
Client
export async function register(fs: any, userId: string) {
try {
// issue a new command
const command = await fs.add(
{ collection: `commands/${userId}/register` },
{ payload: fs.firestore.FieldValue.serverTimestamp() }
);
// wait for the response to be created
fs.onSnapshot(
{ collection: `responses/${userId}/register`, doc: command.id },
function onNext(doc) {
const {error, payload} = doc.data()
if (error) {
return notify.error({ title: 'Failed to register', message: payload.message });
}
notify.json(payload);
},
function onError(err) {
notify.error(err);
}
);
} catch (err) {
notify.error(err);
}
}
Is there no such thing as onCreate for web clients?
The only scalable solution I can think of is to store the response data as a child in the command document, but I think it is not as nice, because I suspect you can not make the permissions as strict then.
I would like the user only to be able to write to the command, and only read from the response paths. If I place the response as a child of command, this would not be possible I think?
I'm wondering if I'm not overlooking some API...

Related

Sveltekit typeError during post request with fetch

I got a nestjs application. It is listening on localhost:3000. I have health check, i can ping with curl or insomnia and it is working correctly. I can use localhost/3000/api/register to register a new user without any problem. I wanted to try it with sveltekit. And i had an issue when i tried to fetch data it and i got an error:
TypeError: fetch failed
at fetch (/Users/marcelljuhasz/Development/svelte-kit-demo/node_modules/undici/index.js:105:13)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async send (/Users/marcelljuhasz/Development/svelte-kit-demo/src/lib/api.ts:16:13)
at async default (/Users/marcelljuhasz/Development/svelte-kit-demo/src/routes/register/+page.server.ts:23:15)
at async handle_action_json_request (file:///Users/marcelljuhasz/Development/svelte-kit-demo/node_modules/#sveltejs/kit/src/runtime/server/page/actions.js:51:16)
at async resolve (file:///Users/marcelljuhasz/Development/svelte-kit-demo/node_modules/#sveltejs/kit/src/runtime/server/index.js:356:17)
at async respond (file:///Users/marcelljuhasz/Development/svelte-kit-demo/node_modules/#sveltejs/kit/src/runtime/server/index.js:229:20)
at async file:///Users/marcelljuhasz/Development/svelte-kit-demo/node_modules/#sveltejs/kit/src/exports/vite/dev/index.js:444:22
I checked my server i got the cors enabled. The front end is listening to: localhost:5173.
I have this code inside:
app.enableCors({
origin: 'http://localhost:5173',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
allowedHeaders: 'Content-Type, Accept',
preflightContinue: true,
});
I am learning sveltekit now and i checked a project inside the github repository to see how it is works but i read the documentations too:
https://github.com/sveltejs/realworld
The structure almost the same i have the lib folder with the api.ts
import { error } from '#sveltejs/kit';
const base = 'http://localhost:3000/api';
async function send({ method, path, data }) {
const opts = { method, headers: {} };
if (data) {
opts.headers['Content-Type'] = 'application/json';
opts.body = JSON.stringify(data);
}
const res = await fetch(`${base}/${path}`, opts);
if (res.ok || res.status === 422) {
const text = await res.text();
return text ? JSON.parse(text) : {};
}
console.log(res);
throw error(res.status);
}
export function get(path) {
return send({ method: 'GET', path });
}
export function del(path) {
return send({ method: 'DELETE', path,});
}
export function post(path, data) {
return send({ method: 'POST', path, data });
}
export function put(path, data) {
return send({ method: 'PUT', path, data });
}
I have a register.svelte in the routes dir. With +page.svelte and +page.server.ts is the same like in the repository, i just exclude my own fields. The data input is return in the correct format.
+page.server.ts looks like this, almost the same like in the repo:
import { fail, redirect } from '#sveltejs/kit';
import * as api from '$lib/api.js';
/** #type {import('./$types').PageServerLoad} */
export async function load({ parent }) {
const { user } = await parent();
if (user) throw redirect(307, '/');
}
/** #type {import('./$types').Actions} */
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const user = {
username: data.get('username'),
email: data.get('email'),
password: data.get('password')
};
const body = await api.post('register', { ...user });
if (body.errors) {
return fail(401, body);
}
console.log(body)
throw redirect(307, '/');
}
};
So in a nutshell i got typerror after i hit the sign uo button. On my server log tells nothing. I see this log in the sveltekit log. I tried to check cors, but it is okey and i haven't got any cors errors in dev console. I checked in my console with curl to check if is the server available. I tried to post, get with insomnia and curl. And it is worked as expected. I have no clue for this. It is wierd if i check the chrome dev tool the request. In the general tab the request url is: localhost:5173 which is the default vite.config for sveltekit server. But i passing my own server which is localhost:3000 and i dont understand what is this behavor. If anybody have experience with sveltekit i am curious what is wrong. I tried to fetch data with an own svelte file without +page.server.ts, i put this fetch method into the component file and it is worked. Wierd.

How to complete login only after functions.auth.user().onCreate is finished

I'm using firebase functions and I have a function which add new collection when user is creating. The problem is sometimes user is logged in before function is done, so user is logged in but new collection is not created yet (and then I have error message 'Missing or insufficient permissions. because a rule cannot find that collection'). How can I handle it?
Is it possible to finish login user (for example using google provider) only when all stuff from
export const createCollection = functions.auth.user().onCreate(async user => {
try {
const addLanguages = await addFirst();
const addSecondCollection = await addSecond();
async function addFirst() {
const userRef = admin.firestore().doc(`languages/${user.uid}`);
await userRef.set(
{
language: null
},
{ merge: true }
);
return 'done';
}
async function addSecond() {
// ...
}
return await Promise.all([addLanguages, addSecondCollection]);
} catch (error) {
throw new functions.https.HttpsError('unknown', error);
}
});
is finished? So google provider window is closed and user is logged in only after that? (and don't using setTimeouts etc)
AFAIK it is not possible to directly couple the two processes implied in your application:
On one hand you have the Google sign-in flow implemented in your front-end (even if there is a call to the Auth service in the back-end), and;
On the other hand you have the Cloud Function that is executed in the back-end.
The problem you encounter comes from the fact that as soon as the Google sign-in flow is successful, your user is signed in to your app and tries to read the document to be created by the Cloud Function.
In some cases (due for example to the Cloud Function cold start) this document is not yet created when the user is signed in, resulting in an error.
One possible solution would be to set a Firestore listener in your front-end to wait for this document to be created, as follows. Note that the following code only takes into account the Firestore document created by the addFirst() function, since you don't give any details on the second document to be created through addSecond().
firebase.auth().signInWithPopup(provider)
.then(function(result) {
var token = result.credential.accessToken;
var user = result.user;
//Here we know the userId then we can set a listener to the doc languages/${user.uid}
firebase.firestore().collection("languages").doc(user.uid)
.onSnapshot(function(doc) {
if(doc.exists) {
console.log("Current data: ", doc.data());
//Do whatever you want with the user doc
} else {
console.log("Language document not yet created by the Cloud Function");
}
});
}).catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
var email = error.email;
var credential = error.credential;
// ...
});
As said above, in the above code we only take into account the first Firestore document created by the addFirst() function. But you probably need to wait for the two docs to be created before reading them from the front-end.
So, you may modify you CF as follows:
export const createCollection = functions.auth.user().onCreate(async user => {
try {
await addFirst();
await addSecond();
return null;
async function addFirst() {
const userRef = admin.firestore().doc(`languages/${user.uid}`);
await userRef.set(
{
language: null
},
{ merge: true }
);
}
async function addSecond() {
// ...
}
} catch (error) {
console.log(error);
return null;
}
});
Note that you don't need to use Promise.all(): the following two lines already execute the two document writes to Firestore. And, since you use async/await the second document is only written after the first one is written.
const addLanguages = await addFirst();
const addSecondCollection = await addSecond();
So you just need to set the listener on the path of the second document, and you are done!
Finally, note that doing
throw new functions.https.HttpsError('unknown', error);
in your catch block is the way you should handle errors for a Callable Cloud Function. Here, you are writing a background triggered Cloud Function, and you can just use return null;

Using a callable function to send data back to the client from Firebase

I have created a callable Cloud Function to read data from Firebase and send back the results to the client, however, only "null" is being returned to the client.
exports.user_get = functions.https.onCall((data, context) => {
if (context.auth && data) {
return admin.firestore().doc("users/" + context.auth.uid).get()
.then(function (doc) {
return { doc.data() };
})
.catch(function (error) {
console.log(error);
return error;
})
} return
});
I just reproduced your case connecting from a Cloud Function with a Firestore database and retriving data. As I can see you are trying to access the field in a wrong way when you are using "users/" + context.auth.uid, the method can't find the field so its returning a null value.
I just followed this Quickstart using a server client library documentation to populate a Firestore database and make a Get from it with node.js.
After that i followed this Deploying from GCP Console documentation in order to deploy a HTTP triggered Cloud Function with the following function
exports.helloWorld = (req, res) => {
firestore.collection('users').get()
.then((snapshot) => {
snapshot.forEach((doc) => {
console.log(doc.id, '=>', doc.data().born);
let ans = {
date : doc.data().born
};
res.status(200).send(ans);
});
})
And this is returning the desired field.
You can take a look of my entire example code here
This is because you are making a query from a database firestore, however the cloud support team has made it very cool to protect your applications from data leakages and so in a callable function as the name suggest you can only return data you passed to the same callable function through the data parameter and nothing else. if you try to access a database i suggest you use an onRequest Function and use the endpoint to get you data. that way you not only protect your database but avoid data and memory leakage.
examples of what you can return from a callable function
exports.sayHello = functions.https.onCall((data, context) => {
const name = data.name;
console.log(`hello ${name}`);
return `It was really fun working with you ${name}`;
});
first create a function in your index.js file and accept data through the data parameter but as i said you can only return data you passed through the data parameter.
now call the function
this is in the frontend code (attach an event listener to a button or something and trigger it
/* jsut say hello from firebase */
callButton.addEventListener('click', () => {
const sayHello = firebase.functions().httpsCallable('getAllUsers');
sayHello().then(resutls => {
console.log("users >>> ", resutls);
});
});
you can get your data using an onRequest like so
/* get users */
exports.getAllUsers = functions.https.onRequest((request, response) => {
cors(request, response, () => {
const data = admin.firestore().collection("users");
const users = [];
data.get().then((snapshot) => {
snapshot.docs.forEach((doc) => {
users.push(doc.data());
});
return response.status(200).send(users);
});
});
});
using a fetch() in your frontend code to get the response of the new onRequest function you can get the endpoint to the function in your firebase console dashboard.
but not that to hit the endpoint from your frontend code you need to add cors to your firebase cloud functions to allow access to the endpoint.
you can do that by just adding this line to the top of your index.js file of the firebase functions directory
const cors = require("cors")({origin: true});

How to save resulted values from firestore into a variable and use it on various components and states in Vue

I'm looking for a way to save the results received from a firestore collection and use it on a variable and reuse the save it to data() and reuse it.
I'm unsure how to save the results from a async function into a variable.
Please help me with this guys
Here is my code.
import firebase from "firebase";
export default{
data() {
return {
uid: firebase.auth().currentUser.uid,
teamLead: ""
};
},
created() {
db.collection("users").onSnapshot(users => {
users.docChanges().forEach(user => {
if (this.uid == user.doc.id) {
this.teamLead = user.doc.data().teamLead
}
});
});
console.log(this.teamLead)
In this code I would like to have the value of teamLead saved into the data() function. How do I do that?
It's a crazy idea to read all user from db. Instead of you can retrieve user by id.
Don't use "this" inside callbacks or lambda.
You have to understand order of execution of async programs. See my comments.
[]
export default{
data() {
return {
teamLead: ""
};
},
created() {
let self = this
db.collection("users").doc(firebase.auth().currentUser.uid).get().then(function(doc) {
if (doc.exists) {
self.teamLead = doc.data().teamLead
console.log(self.teamLead)//<-- this log will executed after firestore return any data
}
});
console.log(this.teamLead)//<-- this log will executed before firestore return any data

Google Translate API and Firebase Firestore are killing each other

We're trying to write a Google Cloud Function that gets a translation from Google Translate API, and then write the results to our Firebase Firestore database. Each works alone, but together nothing works. In other words, we can get a translation from Google Translate. We can write data to Firestore. But if we try to do both, we don't get a translation back from Google Translate, and nothing is written to Firebase. We get no error messages. We've tried the code with async await and with promises. Here's the code with promises:
exports.Google_EStranslateEN = functions.firestore.document('Users/{userID}/Spanish/Translation_Request').onUpdate((change, context) => {
if (change.after.data().word != undefined) {
const {Translate} = require('#google-cloud/translate');
const projectId = 'myProject-cd99d';
const translate = new Translate({
projectId: projectId,
});
// The text to translate
const text = change.after.data().word;
// The target language
const target = 'en';
let translationArray = []; // clear translation array
translate.translate(text, target)
.then(results => {
translation = results[0];
translationArray.push(translation);
try {
// write translation to dictionary
admin.firestore().collection('Dictionaries').doc('Spanish').collection('Words').doc(text).collection('Translations').doc('English').set({
'translationArray': translationArray,
'language': 'en',
'longLanguage': 'English'
})
.then(function() {
console.log("Translation written");
})
.catch(function(error) {
console.error(error);
});
} catch (error) {
console.error(error);
}
})
.catch(error => {
console.error('ERROR:', error);
});
}
});
Here's the same code with async await:
exports.Google_EStranslateEN = functions.firestore.document('Users/{userID}/Spanish/Translation_Request').onUpdate((change, context) => { // triggers when browser writes a request word to the database
if (change.after.data().word != undefined) {
async function getTranslation() {
try {
let translationArray = []; // clear translation array
const {Translate} = require('#google-cloud/translate');
const projectId = 'myProject-cd99d';
const translate = new Translate({
projectId: projectId,
});
// The text to translate
const text = change.after.data().word;
const options = {
to: 'en',
from: 'es',
format: 'text'
}
let [translations] = await translate.translate(text, options);
translations = Array.isArray(translations) ? translations : [translations]; // If translations is an array, leave it alone; if not, put it in an array
translationArray.push(translations[0]);
await admin.firestore().collection('Dictionaries').doc('Spanish').collection('Words').doc(text).collection('Translations').doc('English').set({
'translationArray': translationArray,
'language': 'en',
'longLanguage': 'English'
})
.then(function() {
console.log("Translation written");
})
.catch(function(error) {
console.error(error);
});
// };
} catch (error) {
console.error(error);
}
} // close getTranslation
getTranslation();
}
});
You're not returning a promise that's resolved when all the async work is complete. If you don't do that, Cloud Functions assumes that all your work is complete, and will clamp down on all resources, and any pending work will be shut down.
The promise returned by translate.translate().then().catch() is being ignored. Your nested call to admin.firestore()...set() has a similar problem. It's not sufficient to just call then() and catch() on every promise because then() and catch() both return yet another promise.
You're also unnecessarily mixing usage of try/catch with catch() on the promise. You don't need both strategies for error handling, just one or the other.
When you used await in your second example, you forced JavaScript to pause until the async work represented by the promise returned by set() was complete. This allowed your function to return only after all the work was finished, which is why it worked correctly.
You might be helped by watching my video series on use of promises and async/await in Cloud Functions. Proper handling of promises is crucial to creating a correctly working function.

Resources