MichaelSolati / geofirestore-js

Location-based querying and filtering using Firebase Firestore.
https://geofirestore.com
MIT License
504 stars 58 forks source link

Cloud Function Cors Query Promise Javascript #154

Closed NickCarducci closed 4 years ago

NickCarducci commented 4 years ago

Hi Michael or community This is a question to help others implement, and myself. No bug, of course. [I've edited this with improvements I am sure of to keep short... getting closer...]

Anyway, here is a Google Cloud Function, It works until the query, req.body is fine with CORS I have one item but can't get it from query when the res.send() is sent [still working on this, no error is helpful yet, now testing how far down compiles...] [now getting {"sendit":{"domain":{"domain":null,"_events":{},"_eventsCount":1,"members":[]}}}]

const functions = require("firebase-functions");
const admin = require("firebase-admin");
const GeoFirestore = require("geofirestore").GeoFirestore;
//const firestore = require('firebase/firestore');
const cors = require("cors")({
  origin: true,
  allowedHeaders: [
    "Access-Control-Allow-Origin",
    "Access-Control-Allow-Methods",
    "Content-Type",
    "Origin",
    "X-Requested-With",
    "Accept",
    "Access-Control-Allow-Headers"
  ],
  methods: ["POST", "OPTIONS"]
});
require('firebase/firestore');

//serviceAccount=yourServiceAccountKey.json
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "your_firebaseio_domain"
})

const firestoreRef = admin.firestore();
const geofirestore = new GeoFirestore(firestoreRef);
const geocollection = geofirestore.collection("planner");

exports.chooseCity = functions.https.onRequest((req, res) => {
  // Google Cloud Function res.methods
  res.set("Access-Control-Allow-Headers", "Content-Type");
  res.set("Content-Type", "Application/JSON");
  // CORS-enabled req.methods, res.methods
  return cors(req, res, () => {
    res.set("Access-Control-Allow-Headers", "Content-Type");
    res.set("Content-Type", "Application/JSON");
    var origin = req.get("Origin");
    var allowedOrigins = [
      "https://yourdomain.tld"
    ];
    if (allowedOrigins.indexOf(origin) > -1) {
      // Origin Allowed!!
      res.set("Access-Control-Allow-Origin", origin);
      if (req.method === "OPTIONS") {
        // Method accepted for next request
        res.set("Access-Control-Allow-Methods", "POST");
        //SEND or end # option req.method
        return res.status(200).send({});
      } else {
        // After request req.method === 'OPTIONS'
        if (req.body) {
          const radius = req.body.distance;
          const center = new admin.firestore.GeoPoint(
            req.body.location[0],
            req.body.location[1]
          )

          const geoQuery = geocollection.near({ center, radius });

[Tried this]

          let results = []
// Remove documents when they fall out of the query
geoQuery.on('key_exited', ($key) => {
  const index = results.findIndex((place) => place.$key === $key);
  if (index >= 0) results.splice(index, 1);
});

// As documents come in, add the $key/id to them and push them into our results
geoQuery.on('key_entered', ($key, result) => {
  result.$key = $key;
  results.push(result);
});
          return res.status(200).send({results})

[Instead of this]

          geoQuery.get()
            .then(value => {
              // All GeoDocument returned by GeoQuery,
              //like the GeoDocument added above
              const sendit = value.docs
          res.status(200).send({sendit})

[End]

            })
            .catch(err => res.status(400).send(err))
        } else {
          res.status(200).send("no request body");
        }
      }
    } else {
      //Origin Bad!!
      //SEND or end
      return res.status(400).send("no access for this origin");
    }
  });
});

adding document, the geofirestore way, in a redux action

import { GeoFirestore } from "geofirestore";
export const createEvent = event => {
  return (dispatch, getState, { getFirebase, getFirestore }) => {
    const firestore = getFirestore();
    const geoFirestore = new GeoFirestore(firestore);
    const geocollection = geoFirestore.collection("planner");
    geocollection.add({
      name: "Geofirestore",
      score: 100,
      // The coordinates field must be a GeoPoint!
      coordinates: event.geopoint,
      title: event.title,
      body: event.body,
      chosenPhoto: event.chosenPhoto,
      date: event.date,
      createdAt: event.createdAt,
      updatedAt: event.updatedAt,
      address: event.address,
      location: event.location,
      city: event.city,
      etype: event.etype,
      geopoint: event.geopoint
    });
}}
Screen Shot 2020-01-04 at 10 59 29 AM Screen Shot 2020-01-04 at 11 00 07 AM
MichaelSolati commented 4 years ago

Hey, so I want to make sure I understand the issue here... Are you looking for an example of how to provide an endpoint that can make a geoquery while using CORS?

NickCarducci commented 4 years ago

CORS is required for a Google Cloud Function POST, so yes. I want to use Google Cloud Function to query geofirestore data... Oh let me be clear I have used the CORS before, this code works. the data is there... I've never queried data like this though, snapshots & such

MichaelSolati commented 4 years ago

So the CORS aspect works, but the query doesn't?

NickCarducci commented 4 years ago

For sure, near the query I commented [tried this] [instead of this] to show 2nd and 1st try using your query examples from other posts like these

NickCarducci commented 4 years ago

So I'm just guessing but this may be because I need to have the instance run for longer than 8-10 minutes which is the longest a cloud function can run. An alternative for Google users would be google app engine \ gcloud init \ gcloud app deploy --verbosity=debug, which I am just learning now to deploy my websocket server in another feature. maybe I can & should use that instead so I can use nodejs runtime. I forget why I stopped trying to load geofirestore directly to the front-end reactjs. I'll read-up again and update within a week

MichaelSolati commented 4 years ago

Ok, so looking over your code I believe the issue has to do with how you're writing your then statements to resolve your promises. So here's an example function I wrote that looks similar to what you had (I just skipped the CORS aspect, but since you're comfortable writing that bit of code I'm sure you can re-add it).

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const GeoFirestore = require('geofirestore').GeoFirestore;

admin.initializeApp();

const firestoreRef = admin.firestore();
const geofirestore = new GeoFirestore(firestoreRef);
const geocollection = geofirestore.collection('planner');

exports.chooseCity = functions.https.onRequest(async (request, response) => {
  if (request.method === 'OPTIONS') {
    response.set('Access-Control-Allow-Methods', 'POST');
    return response.status(200).send({});
  } else {
    const radius = request.body.distance;
    const center = new admin.firestore.GeoPoint(request.body.location[0], request.body.location[1]);

    // The data from a doc is returned as a function, so you just need to map and call the function
    const sendit = (await geocollection.near({ center, radius }).get()).docs.map((doc) => ({ ...doc, data: doc.data() }));

    return response.status(200).send({ sendit });
  }
 });

This should look similar to your code, I'm using async/await instead of getting into a promise soup of a nightmare.

The code is running on this endpoint https://us-central1-geofirestore-tests.cloudfunctions.net/chooseCity if you want to test it, it's just looking for a POST request with a application/json content type. Hopefully this helps!


Here's a sample request:

var request = require('request');
var options = {
  'method': 'POST',
  'url': 'https://us-central1-geofirestore-tests.cloudfunctions.net/chooseCity',
  'headers': {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({"distance":100,"location":[1,3]})

};
request(options, function (error, response) { 
  if (error) throw new Error(error);
  console.log(response.body);
});

It should return the following:

{
    "sendit": [
        {
            "exists": true,
            "id": "GtfRlw86OYRTawCmeBG1",
            "data": {
                "coordinates": {
                    "_latitude": 1,
                    "_longitude": 3
                }
            },
            "distance": 0
        },
        {
            "exists": true,
            "id": "OSttNfDZgaF8G1MzsSG4",
            "data": {
                "coordinates": {
                    "_latitude": 1,
                    "_longitude": 3
                }
            },
            "distance": 0
        },
        {
            "exists": true,
            "id": "dkk6QoHfojPdORitrFHn",
            "data": {
                "coordinates": {
                    "_latitude": 1,
                    "_longitude": 3
                }
            },
            "distance": 0
        },
        {
            "exists": true,
            "id": "krtqVQurpQnMXdPlA9MG",
            "data": {
                "coordinates": {
                    "_latitude": 1,
                    "_longitude": 3
                }
            },
            "distance": 0
        },
        {
            "exists": true,
            "id": "q01xXMOXLDkQiezUohnU",
            "data": {
                "coordinates": {
                    "_latitude": 1,
                    "_longitude": 3
                }
            },
            "distance": 0
        }
    ]
}

Updated this one last time to modify the sendit bit to call the doc data.

NickCarducci commented 4 years ago

I'm trying to update firebase config for react-redux-firebase v3 & redux-firestore to test my functions I used a month ago before closing. Could also be an issue with using the same bucket I was messing about in with a websocket server... But I digress since you got a response & it works, we don't have to wait for anymore than the day so far to close this. Thanks for the work on this Michael, good job

NickCarducci commented 4 years ago

Michael,

It works, but Can you also write a single request instead of the querySnapshot for us?

My reason: I am sure I’m not updating the geofirestore data every time I pan on the map, and the cloud function only runs when user changes city. I even made the variable radius static in the user-triggered function to test, but it still writes to the console every bit of panning on the map. Thank you!

MichaelSolati commented 4 years ago

This is a single request, at least my sample. It uses the .get() method which returns a single query in the form of a Promise.