├── .gcloudignore
├── Dockerfile
├── scripts.sh
├── package.json
├── .gitignore
├── CONTRIBUTING.md
├── src
├── index.js
├── firestore.js
├── maps.js
├── tasks.js
└── index.html
├── README.md
└── LICENSE
/.gcloudignore:
--------------------------------------------------------------------------------
1 | # This file specifies files that are *not* uploaded to Google Cloud Platform
2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of
3 | # "#!include" directives (which insert the entries of the given .gitignore-style
4 | # file at that point).
5 | #
6 | # For more information, run:
7 | # $ gcloud topic gcloudignore
8 | #
9 | .gcloudignore
10 |
11 | # If you would like to upload your .git directory, .gitignore file or files
12 | # from your .gitignore file, remove the corresponding line
13 | # below:
14 | .git
15 | .gitignore
16 |
17 | node_modules
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use the official Node.js 12 image.
2 | # https://hub.docker.com/_/node
3 | FROM node:12
4 | # Create and change to the app directory.
5 | WORKDIR /usr/src/app
6 | # Copy application dependency manifests to the container image.
7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied.
8 | # Copying this separately prevents re-running npm install on every code change.
9 | COPY package*.json ./
10 | # Install production dependencies.
11 | RUN npm install --only=production
12 | # Copy local code to the container image.
13 | COPY . .
14 | # Run the web service on container startup.
15 | CMD [ "npm", "start" ]
--------------------------------------------------------------------------------
/scripts.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euxo pipefail
3 |
4 | # Deploys the application to Google Cloud Functions
5 | deploy_to_functions() {
6 | echo 'Deploying to Cloud Functions...';
7 | gcloud functions deploy tasks-pizza \
8 | --trigger-http \
9 | --runtime=nodejs10 \
10 | --env-vars-file=.env.yaml
11 | echo "Deployed $(gcloud functions describe tasks-pizza --format 'value(httpsTrigger.url)')"
12 | }
13 |
14 | # Deploys the application to Google Cloud Run
15 | deploy_to_run() {
16 | echo 'Deploying to Cloud Run...';
17 | gcloud components install beta
18 | GCP_PROJECT=$(gcloud config list --format 'value(core.project)' 2>/dev/null)
19 | gcloud config set run/region us-central1
20 | gcloud config set run/platform managed
21 | gcloud builds submit --tag gcr.io/$GCP_PROJECT/tasks-pizza
22 | gcloud beta run deploy tasks-pizza \
23 | --image gcr.io/$GCP_PROJECT/tasks-pizza \
24 | --allow-unauthenticated
25 | echo "Deployed $(gcloud beta run routes describe tasks-pizza --platform managed --format 'value(status.address.url)')"
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tasks-pizza",
3 | "version": "1.0.0",
4 | "description": "A sample application using Cloud Tasks and Google Maps (to find pizza).",
5 | "main": "src/index.js",
6 | "private": true,
7 | "scripts": {
8 | "start": "FUNCTION_SOURCE=src npx @google-cloud/functions-framework",
9 | "functions": "source scripts.sh && deploy_to_functions",
10 | "cloudrun": "source scripts.sh && deploy_to_run"
11 | },
12 | "dependencies": {
13 | "@google-cloud/firestore": "^2.3.0",
14 | "@google-cloud/functions-framework": "^1.3.2",
15 | "@google-cloud/tasks": "^1.4.0",
16 | "@google/maps": "^0.5.5",
17 | "dotenv": "^8.1.0",
18 | "express": "^4.17.1",
19 | "firebase-admin": "^8.6.0",
20 | "node-fetch": "^2.6.0"
21 | },
22 | "devDependencies": {
23 | "@types/express": "^4.17.1",
24 | "@types/google__maps": "^0.5.8"
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "git+https://github.com/GoogleCloudPlatform/tasks-pizza.git"
29 | },
30 | "author": "Grant Timmerman",
31 | "homepage": "https://github.com/GoogleCloudPlatform/tasks-pizza#readme"
32 | }
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | # for local testing with bash
59 | .env
60 | # for gcloud
61 | .env.yaml
62 |
63 | # next.js build output
64 | .next
65 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to become a contributor and submit your own code
2 |
3 | ## Contributor License Agreements
4 |
5 | We'd love to accept your patches! Before we can take them, we
6 | have to jump a couple of legal hurdles.
7 |
8 | ### Before you contribute
9 | Before we can use your code, you must sign the
10 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual)
11 | (CLA), which you can do online. The CLA is necessary mainly because you own the
12 | copyright to your changes, even after your contribution becomes part of our
13 | codebase, so we need your permission to use and distribute your code. We also
14 | need to be sure of various other things—for instance that you'll tell us if you
15 | know that your code infringes on other people's patents. You don't have to sign
16 | the CLA until after you've submitted your code for review and a member has
17 | approved it, but you must do it before we can put your code into our codebase.
18 | Before you start working on a larger contribution, you should get in touch with
19 | us first through the issue tracker with your idea so that we can help out and
20 | possibly guide you. Coordinating up front makes it much easier to avoid
21 | frustration later on.
22 |
23 | ### Code reviews
24 | All submissions, including submissions by project members, require review. We
25 | use Github pull requests for this purpose.
26 |
27 | ### The small print
28 | Contributions made by corporations are covered by a different agreement than
29 | the one above, the
30 | [Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | /**
16 | * The main GCF function using the Functions Framework.
17 | * @see https://github.com/GoogleCloudPlatform/functions-framework-nodejs
18 | */
19 | const path = require('path');
20 | require('dotenv').config();
21 |
22 | // Express Routing
23 | const express = require('express');
24 | const app = express();
25 | const routes = {
26 | '/tasks/start': require('./tasks').start, // Creates ~13k Cloud Tasks
27 | '/tasks/listnames': require('./tasks').listnames, // Lists expected names of Tasks
28 | '/maps/add': require('./maps').add, // Adds 1 city record to the database
29 | '/maps/get': require('./maps').get, // Gets 1 city record from the database
30 | '/maps/listnames': require('./maps').listnames, // List the names of all city records
31 | '/maps/key': (req, res) => res.send({key: process.env.KEY}), // API KEY for frontend
32 | '/target': require('./maps').add, // Tasks target, query and add a city record
33 | '/web': (req, res) => res.sendFile(path.join(__dirname, 'index.html'))
34 | };
35 | Object.entries(routes).map(([route, func]) => app.use(route, func));
36 | app.use('/', (req, res) => res.send(Object.keys(routes))); // default
37 |
38 | // Export the Express app to the Functions Framework
39 | exports.function = app;
--------------------------------------------------------------------------------
/src/firestore.js:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | /**
16 | * Firestore Methods
17 | * - All location data is stored in Firestore
18 | * - Every location is 1 Firestore Document that contains Maps API results
19 | * @see https://firebase.google.com/docs/firestore/quickstart
20 | * @see https://cloud.google.com/firestore/quotas
21 | */
22 | const COLLECTION = 'tasks-pizza';
23 |
24 | // Setup Firestore with default credentials
25 | const admin = require('firebase-admin');
26 | admin.initializeApp({
27 | credential: admin.credential.applicationDefault()
28 | });
29 | const db = admin.firestore();
30 |
31 | // Lists all stored locations
32 | module.exports.getLocations = async () => {
33 | const docs = await db.collection(COLLECTION).listDocuments();
34 | return docs.map(d => d.id);
35 | }
36 |
37 | // Get a single location
38 | module.exports.getLocation = async (location) => {
39 | const doc = await db.collection(COLLECTION).doc(location).get();
40 | const data = doc.data();
41 | return data || { error: 'No data for this location found.'};
42 | }
43 |
44 | /**
45 | * Store the location data
46 | * @param {string} location The name of the location
47 | * @param {object} locationData The data about the location
48 | */
49 | module.exports.storeLocation = async (location, locationData) => {
50 | return await db.collection(COLLECTION).doc(location).set(locationData);
51 | }
52 |
--------------------------------------------------------------------------------
/src/maps.js:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | /**
16 | * Methods for interacting with the Google Maps API
17 | * All methods require the environment variable `KEY`.
18 | */
19 | const KEY = process.env.KEY;
20 | const maps = require('@google/maps').createClient({
21 | key: KEY,
22 | Promise,
23 | });
24 | const firestore = require('./firestore');
25 |
26 | if (!KEY) {
27 | throw new Error('Missing `KEY` environment variable for Google Maps.');
28 | }
29 |
30 | /**
31 | * Gets a photo given a photo reference hash.
32 | * @see https://developers.google.com/places/web-service/photos
33 | */
34 | async function getPhoto(photoreference) {
35 | const photo = await maps.placesPhoto({
36 | photoreference,
37 | maxheight: 1600,
38 | maxwidth: 1600,
39 | }).asPromise();
40 | return photo;
41 | }
42 |
43 | /**
44 | * Returns a Google Maps Places API result for the best pizza near a location/place.
45 | * @param {string} placesQuery The place query.
46 | * @returns {object} The Google Maps Places API result.
47 | */
48 | const getPlace = async (placesQuery) => {
49 | // Get the lat,lng for the query
50 | const geocoding = await maps.geocode({
51 | address: placesQuery,
52 | }).asPromise();
53 | if (!geocoding.json.results) throw new Error('No results');
54 | const {lat, lng} = geocoding.json.results[0].geometry.location;
55 |
56 | // Find the place
57 | // @see https://developers.google.com/places/web-service/search
58 | var places = await maps.placesNearby({
59 | radius: 5000, // meters
60 | keyword: 'pizza',
61 | type: 'restaurant',
62 | location: [lat, lng],
63 | }).asPromise();
64 | if (places.json.results.length === 0) return; // no results found
65 | const place = places.json.results[0];
66 | return place;
67 | }
68 |
69 | // Gets Map data for a single id from the Firestore database.
70 | module.exports.get = async (req, res) => {
71 | const placeId = req.query.id;
72 | if (!placeId) return res.send({error: 'No ?id='});
73 | const place = await firestore.getLocation(placeId);
74 | res.send(place);
75 | };
76 |
77 | // Returns a list of map names in the Firestore database.
78 | module.exports.listnames = async (req, res) => {
79 | const locations = await firestore.getLocations();
80 | res.send(locations);
81 | };
82 |
83 | // Adds a specific map to the Firestore database.
84 | module.exports.add = async (req, res) => {
85 | // Only handle valid requests
86 | const id = req.query.id;
87 | if (!id) return res.send({error: 'No ?id='});
88 |
89 | // Get the place
90 | const place = await getPlace(id);
91 | if (!place) return res.send({error: 'No results found.'}); // no results found.
92 | const placeData = {
93 | id,
94 | place,
95 | url: `https://www.google.com/maps/place/?q=place_id:${place.place_id}`,
96 | };
97 | await firestore.storeLocation(id, placeData);
98 | res.send(placeData);
99 | };
100 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ⚠️ **THIS REPO HAS BEEN ARCHIVED AND IS NO LONGER MAINTAINED.** ⚠️
2 |
3 | # Cloud Tasks Pizza Map
4 |
5 |
6 |
7 | > As featured on Medium: https://medium.com/p/db888675db71/
8 |
9 | A sample application using Cloud Tasks and Google Maps to find the best pizza restaurants around
10 | the world.
11 |
12 | The program works in the following way:
13 |
14 | 1. Creates a Cloud Tasks Queue
15 | 1. Creates ~10,000 Cloud Tasks with different names of cities
16 | 1. Each Cloud Task triggers a Cloud Function. This Function does the following:
17 | 1. Looks up the best pizza restaurant in that city accoring to the Google Maps Places API
18 | 1. Stores the restaurant data in Firestore
19 |
20 |
21 |
22 | ## Setup
23 |
24 | This sample uses many APIs and a Firestore database
25 | that must be enable prior to running the application.
26 |
27 | ### Enable all APIs
28 |
29 | The following APIs are used in this app:
30 |
31 | - `Cloud Tasks API`
32 | - `Maps Places API`
33 | - `Maps Geocoding API`
34 | - `Firestore API`
35 |
36 | [Enable these APIs](https://console.cloud.google.com/flows/enableapi?apiid=cloudtasks.googleapis.com,firestore.googleapis.com,places-backend.googleapis.com,static-maps-backend.googleapis.com,geocoding-backend.googleapis.com
37 | ).
38 |
39 | ### Create a Firestore database
40 |
41 | [Create a `Firestore` database](https://firebase.google.com/docs/firestore/quickstart#create) with these configurations:
42 | - Mode: `Test mode`
43 | - Collection: `tasks-pizza`.
44 |
45 | ### Create an API Key
46 |
47 | [Create a Google Maps API key](https://developers.google.com/maps/documentation/javascript/get-api-key).
48 | Create an `.env` file with a Google Maps API key:
49 |
50 | ```sh
51 | KEY=AIeSyDh7ggKmvLzFAeq_ICGkO8ryvEMm3Nrde-z
52 | ```
53 |
54 | ## Run the Functions Framework
55 |
56 | The [Functions Framework](https://github.com/GoogleCloudPlatform/functions-framework-nodejs) enables
57 | you to run a Google Cloud Function locally on your computer. This is very useful for testing our code.
58 |
59 | To test our server locally, follow these instructions using our `KEY` from the previous step:
60 |
61 | ```sh
62 | npm i
63 | KEY=??? npm start
64 | ```
65 |
66 | Observe the Functions Framework starts a server and logs information:
67 |
68 | ```
69 | npx: installed 52 in 8.771s
70 | Serving function...
71 | Function: function
72 | URL: http://localhost:8080/
73 | ```
74 |
75 | Go to URL specified to view all routes for the application.
76 |
77 | ## Deploy to Google Cloud
78 |
79 | You have two options for deploying this application to Google Cloud:
80 |
81 | - Cloud Functions
82 | - Cloud Run
83 |
84 | The `gcloud` commands for deploying to each target are in the `scripts.sh` file.
85 |
86 | ### Deploy to Google Cloud Function
87 |
88 | Deploy your function on Google Cloud Functions on runtime Node 10:
89 |
90 | ```sh
91 | npm run functions
92 | ```
93 |
94 | ### Deploy to Google Cloud Run
95 |
96 | You can also deploy this application to Cloud Run by first building the container, then deploying:
97 |
98 | ```sh
99 | npm run cloudrun
100 | ```
101 |
102 | ### GIF
103 |
104 | Here is a GIF of the applicaiton working at different Google Map zoom levels
105 | 
106 |
--------------------------------------------------------------------------------
/src/tasks.js:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | const fetch = require('node-fetch');
16 | const {v2beta3} = require('@google-cloud/tasks'); // Must be v2beta3 or else tasks creation fails
17 | const client = new v2beta3.CloudTasksClient();
18 |
19 | // Application configuration
20 | const PROJECT = process.env.PROJECT || 'serverless-com-demo';
21 | const QUEUE = process.env.QUEUE || 'my-queue';
22 | const LOCATION = process.env.LOCATION || 'us-central1';
23 |
24 | // Construct the fully qualified queue name.
25 | const parent = client.queuePath(PROJECT, LOCATION, QUEUE);
26 |
27 | /**
28 | * Gets a list of cities.
29 | * @returns {string[]} A list of city names.
30 | */
31 | async function getLocationNames() {
32 | const URL_LOCATIONS = 'https://api.github.com/gists/7100f98e7d3dd48b3c4d7cb85cfd313f'; // 13k locations
33 | // const URL_LOCATIONS = 'https://api.github.com/gists/b503eecba3c49198cc0447550cbe3ccb'; // 19 locations
34 | const cities = await fetch(URL_LOCATIONS);
35 | const json = await cities.json();
36 | const content = json.files['cities.txt'].content;
37 | const contentItems = content.split('\n');
38 | return contentItems;
39 | }
40 |
41 | /**
42 | * Creates a Task Queue
43 | * @param {string} queueName The Cloud Tasks queue name.
44 | */
45 | async function createQueue(queueName) {
46 | const request = {
47 | parent: client.locationPath(PROJECT, LOCATION),
48 | queue: {
49 | name: client.queuePath(PROJECT, LOCATION, queueName),
50 | rate_limits: {},
51 | retry_config: {},
52 | },
53 | };
54 | const res = await client.createQueue(request);
55 | return res;
56 | }
57 |
58 | /**
59 | * Returns `true` if a queue with `queueName` exists.
60 | * @param {string} queueName The queue name to check.
61 | * @returns `true` if the queue exists.
62 | */
63 | async function queueExists(queueName) {
64 | const queue = await client.getQueue({
65 | name: client.queuePath(PROJECT, LOCATION, queueName)
66 | });
67 | return queue.length > 0;
68 | }
69 |
70 | /**
71 | * Creates a named Cloud Task.
72 | * Note: The the placeName can't be added frequently as the Task de-duplication window is ~1h.
73 | * @param {string} placeName The name of the city/place to target for our HTTP request.
74 | * @see https://cloud.google.com/tasks/docs/quotas
75 | */
76 | async function createTask(placeName) {
77 | const normalizedName = placeName.normalize('NFD')
78 | .replace(/[\u0300-\u036f]/g, '') // Remove accents
79 | .replace(/[^a-zA-Z]+/g, "-"); // Only keep [a-zA-Z]
80 | const name = client.taskPath(PROJECT, LOCATION, QUEUE, normalizedName);
81 | const task = {
82 | name,
83 | httpRequest: {
84 | httpMethod: 'GET',
85 | url: `https://${LOCATION}-${PROJECT}.cloudfunctions.net/tasks-pizza/target?id=${placeName}`,
86 | },
87 | };
88 |
89 | // Send create task request.
90 | const request = {parent, task};
91 | try {
92 | const [response] = await client.createTask(request);
93 | console.log(`Created task ${response.name}`);
94 | } catch (e) {
95 | console.error(`ERROR: ${e.details} (${name})`);
96 | }
97 | }
98 |
99 | /**
100 | * Runs the program.
101 | * - Creates Cloud Tasks Queue if needed
102 | * - Creates N Cloud Tasks
103 | */
104 | async function run() {
105 | console.log('Starting...');
106 |
107 | console.log('Checking if queue exists...');
108 | if (!(await queueExists(QUEUE))) {
109 | console.log(`Creating queue "${QUEUE}"...`);
110 | await createQueue(QUEUE);
111 | } else {
112 | console.log(`Queue "${QUEUE}" exists.`);
113 | }
114 |
115 | console.log('Creating Tasks...');
116 | const locations = await getLocationNames();
117 | for (const loc of locations) {
118 | await createTask(loc);
119 | }
120 | console.log('Done.');
121 | }
122 |
123 | // Export routes for Express app
124 | module.exports.listnames = async (req, res) => {
125 | const locationList = await getLocationNames();
126 | res.send(locationList);
127 | }
128 | module.exports.start = async (req, res) => {
129 | await run();
130 | res.sendStatus(200);
131 | }
132 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |