diff --git a/appengine-standard/README.md b/appengine-standard/README.md new file mode 100644 index 0000000000..3c50ada023 --- /dev/null +++ b/appengine-standard/README.md @@ -0,0 +1,90 @@ +# Google App Engine Node.js Samples + +These are samples for using [Node.js][nodejs] on +Google App Engine Standard Environment. + +There are also samples [submitted by the community][community_samples]. + +See our other [Google Cloud Platform GitHub repositories](/GoogleCloudPlatform) +for sample applications and scaffolding for other frameworks and use cases. + +* [Run Locally](#run-locally) +* [Deploying](#deploying) +* [Official samples](#official-samples) +* [Community samples](#community-samples) + +## Run Locally + +Some samples have specific instructions. If there is a `README.md` file in the +sample folder, please refer to it for any additional steps required to run the +sample. + +The App Engine Node.js samples typically that you do the following: + +1. [Setup your environment for Node.js developement][nodejs_dev]. +1. [Install the Google Cloud SDK][sdk]. +1. Acquire local credentials for authenticating with Google Cloud Platform APIs: + + gcloud auth application-default login + +1. Clone this repo: + + git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git + +1. Choose a sample: + + cd appengine-standard/sample-folder/ + +1. Install depedencies using `npm` or `yarn`: + + npm install + + or + + yarn install + +1. Run the sample with `npm` or `yarn` (See the sample's `README.md` file for + any additional setup): + + npm start + + or + + yarn start + +1. Visit the application at [http://localhost:8080][]. + +## Deploying + +Some samples in this repositories may have special deployment instructions. +Refer to the `README.md` file in the sample folder. + +1. Use the [Google Cloud Console][console] to create a Google Cloud Platform + project. +1. [Enable billing][billing] for your project. + +1. Use the Cloud SDK to deploy your app. + + gcloud app deploy + + Note: If there is a `yarn.lock` file then `yarn install` will be used during + deployment. Delete the `yarn.lock` file to fall back to `npm install`. + +1. View your deployed application at `https://YOUR_PROJECT_ID.appspot.com`. + +## Official samples + +View the [Official App Engine Node.js samples][official_samples]. + +## Community samples + +View the [Community-contributed App Engine Node.js samples][community_samples]. + +[nodejs]: https://nodejs.org/ +[appengine]: https://cloud.google.com/appengine/docs/flexible/nodejs/ +[nodejs_dev]: https://cloud.google.com/community/tutorials/how-to-prepare-a-nodejs-dev-environment +[sdk]: https://cloud.google.com/sdk/ +[console]: https://console.cloud.google.com +[billing]: https://support.google.com/cloud/answer/6293499#enable-billing +[official_samples]: https://github.com/GoogleCloudPlatform/nodejs-docs-samples/tree/master/appengine +[community_samples]: https://cloud.google.com/community/tutorials/?q=%22Node.js%22 diff --git a/appengine-standard/analytics/README.md b/appengine-standard/analytics/README.md new file mode 100644 index 0000000000..ca05b2a93f --- /dev/null +++ b/appengine-standard/analytics/README.md @@ -0,0 +1,55 @@ +# Integrating with Google Analytics + +This sample application demonstrates how to integrate Google App Engine Node.js +Standard Environment with Google Analytics. + +* [Setup](#setup) +* [Running locally](#running-locally) +* [Deploying to App Engine](#deploying-to-app-engine) +* [Running the tests](#running-the-tests) + +## Setup + +Before you can run or deploy the sample, you need to do the following: + +1. Refer to the [appengine-standard/README.md][readme] file for instructions on + running and deploying. +1. [Create a Google Analytics Property and obtain the Tracking ID][tracking]. +1. Add your tracking ID to `app.yaml`. +1. Install dependencies: + + With `npm`: + + npm install + + or with `yarn`: + + yarn install + +## Running locally + +With `npm`: + + GA_TRACKING_ID=YOUR_TRACKING_ID npm start + +or with `yarn`: + + GA_TRACKING_ID=YOUR_TRACKING_ID yarn start + +## Deploying to App Engine + +With `npm`: + + npm run deploy + +or with `yarn`: + + yarn run deploy + +## Running the tests + +See [Contributing][contributing]. + +[readme]: ../README.md +[tracking]: https://support.google.com/analytics/answer/1042508 +[contributing]: https://github.com/GoogleCloudPlatform/nodejs-docs-samples/blob/master/CONTRIBUTING.md diff --git a/appengine-standard/analytics/app.js b/appengine-standard/analytics/app.js new file mode 100644 index 0000000000..3ad08f9b9f --- /dev/null +++ b/appengine-standard/analytics/app.js @@ -0,0 +1,72 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// [START app] +const express = require('express'); +const got = require('got'); + +const app = express(); +app.enable('trust proxy'); + +// The following environment variable is set by app.yaml when running on App +// Engine, but will need to be set manually when running locally. See README.md. +const GA_TRACKING_ID = process.env.GA_TRACKING_ID; + +function trackEvent (category, action, label, value, cb) { + const data = { + // API Version. + v: '1', + // Tracking ID / Property ID. + tid: GA_TRACKING_ID, + // Anonymous Client Identifier. Ideally, this should be a UUID that + // is associated with particular user, device, or browser instance. + cid: '555', + // Event hit type. + t: 'event', + // Event category. + ec: category, + // Event action. + ea: action, + // Event label. + el: label, + // Event value. + ev: value + }; + + return got.post('http://www.google-analytics.com/collect', { + form: data + }); +} + +app.get('/', (req, res, next) => { + // Event value must be numeric. + trackEvent('Example category', 'Example action', 'Example label', '100') + .then(() => { + res.status(200).send('Event tracked.').end(); + }) + // This sample treats an event tracking error as a fatal error. Depending + // on your application's needs, failing to track an event may not be + // considered an error. + .catch(next); +}); + +const PORT = process.env.PORT || 8080; +app.listen(PORT, () => { + console.log(`App listening on port ${PORT}`); + console.log('Press Ctrl+C to quit.'); +}); +// [END app] diff --git a/appengine-standard/analytics/app.yaml b/appengine-standard/analytics/app.yaml new file mode 100644 index 0000000000..5746c3569b --- /dev/null +++ b/appengine-standard/analytics/app.yaml @@ -0,0 +1,21 @@ +# Copyright 2017, Google, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app_yaml] +runtime: nodejs8 + +# [START env] +env_variables: + GA_TRACKING_ID: YOUR_TRACKING_ID +# [END env] +# [END app_yaml] diff --git a/appengine-standard/analytics/package.json b/appengine-standard/analytics/package.json new file mode 100644 index 0000000000..1389f88b2a --- /dev/null +++ b/appengine-standard/analytics/package.json @@ -0,0 +1,40 @@ +{ + "name": "appengine-analytics", + "description": "Sample for Google Analytics Measurement Protocol on Google App Engine Standard Environment.", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google Inc.", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": "8.x.x" + }, + "scripts": { + "deploy": "gcloud app deploy", + "start": "node app.js", + "lint": "repo-tools lint", + "pretest": "npm run lint", + "system-test": "repo-tools test app", + "test": "npm run system-test", + "e2e-test": "repo-tools test deploy" + }, + "dependencies": { + "express": "4.16.0", + "got": "7.1.0" + }, + "devDependencies": { + "@google-cloud/nodejs-repo-tools": "2.0.4" + }, + "cloud-repo-tools": { + "test": { + "app": { + "msg": "Event tracked." + } + }, + "requiresKeyFile": true, + "requiresProjectId": true + } +} diff --git a/appengine-standard/cloudsql/README.md b/appengine-standard/cloudsql/README.md new file mode 100644 index 0000000000..ea6f13eb1f --- /dev/null +++ b/appengine-standard/cloudsql/README.md @@ -0,0 +1,8 @@ +# Cloud SQL for MySQL Node.js sample on App Engine Standard environment + +This sample demonstrates how to use [Google Cloud SQL][sql] for +[MySQL][mysql] on Google App Engine Standard Environment. + +[sql]: https://cloud.google.com/sql/ +[flexible]: https://cloud.google.com/appengine +[mysql]: https://www.mysql.com/downloads/ diff --git a/appengine-standard/cloudsql/app.yaml b/appengine-standard/cloudsql/app.yaml new file mode 100644 index 0000000000..c96f7f553b --- /dev/null +++ b/appengine-standard/cloudsql/app.yaml @@ -0,0 +1,33 @@ +# Copyright 2017, Google, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app_yaml] +runtime: nodejs8 + +# [START env] +env_variables: + SQL_USER: YOUR_SQL_USER + SQL_PASSWORD: YOUR_SQL_PASSWORD + SQL_DATABASE: YOUR_SQL_DATABASE + # e.g. my-awesome-project:us-central1:my-cloud-sql-instance + INSTANCE_CONNECTION_NAME: YOUR_INSTANCE_CONNECTION_NAME +# [END env] + +# [START cloudsql_settings] +beta_settings: + # The connection name of your instance, available by using + # 'gcloud beta sql instances describe [INSTANCE_NAME]' or from + # the Instance details page in the Google Cloud Platform Console. + cloud_sql_instances: YOUR_INSTANCE_CONNECTION_NAME +# [END cloudsql_settings] +# [END app_yaml] diff --git a/appengine-standard/cloudsql/createTables.js b/appengine-standard/cloudsql/createTables.js new file mode 100644 index 0000000000..37ccbcb1c7 --- /dev/null +++ b/appengine-standard/cloudsql/createTables.js @@ -0,0 +1,53 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// [START createTables] +const Knex = require('knex'); +const prompt = require('prompt'); + +const FIELDS = ['user', 'password', 'database']; + +prompt.start(); + +// Prompt the user for connection details +prompt.get(FIELDS, (err, config) => { + if (err) { + console.error(err); + return; + } + + // Connect to the database + const knex = Knex({ client: 'mysql', connection: config }); + + // Create the "visits" table + knex.schema.createTable('visits', (table) => { + table.increments(); + table.timestamp('timestamp'); + table.string('userIp'); + }) + .then(() => { + console.log(`Successfully created 'visits' table.`); + return knex.destroy(); + }) + .catch((err) => { + console.error(`Failed to create 'visits' table:`, err); + if (knex) { + knex.destroy(); + } + }); +}); +// [END createTables] diff --git a/appengine-standard/cloudsql/package.json b/appengine-standard/cloudsql/package.json new file mode 100644 index 0000000000..1b9a7ca935 --- /dev/null +++ b/appengine-standard/cloudsql/package.json @@ -0,0 +1,67 @@ +{ + "name": "appengine-cloudsql-mysql", + "description": "Node.js MySQL sample for Cloud SQL on App Engine standard environment.", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google Inc.", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": "8.x.x" + }, + "scripts": { + "deploy": "gcloud app deploy", + "lint": "samples lint", + "pretest": "npm run lint", + "unit-test": "ava --verbose test/*.test.js", + "start-proxy": "! pgrep cloud_sql_proxy > /dev/null && cloud_sql_proxy -instances=$INSTANCE_CONNECTION_NAME=tcp:$SQL_PORT &", + "system-test": "samples test app", + "system-test-proxy": "npm run start-proxy; npm run system-test", + "all-test": "npm run unit-test && npm run system-test", + "test": "samples test run --cmd npm -- run all-test", + "e2e-test": "samples test deploy" + }, + "dependencies": { + "async": "2.5.0", + "express": "4.15.4", + "knex": "0.13.0", + "mysql": "2.14.1", + "prompt": "1.0.0" + }, + "devDependencies": { + "@google-cloud/nodejs-repo-tools": "1.4.17", + "ava": "0.22.0" + }, + "cloud-repo-tools": { + "requiresKeyFile": true, + "requiresProjectId": true, + "test": { + "app": { + "requiredEnvVars": [ + "SQL_USER", + "SQL_PASSWORD", + "SQL_DATABASE", + "SQL_PORT", + "INSTANCE_CONNECTION_NAME" + ], + "msg": "Last 10 visits:", + "substitutions": "YOUR_SQL_USER=$SQL_USER,YOUR_SQL_PASSWORD=$SQL_PASSWORD,YOUR_SQL_DATABASE=$SQL_DATABASE,YOUR_INSTANCE_CONNECTION_NAME=$INSTANCE_CONNECTION_NAME", + "args": [ + "server.js" + ] + }, + "build": { + "requiredEnvVars": [ + "SQL_USER", + "SQL_PASSWORD", + "SQL_DATABASE", + "SQL_PORT", + "INSTANCE_CONNECTION_NAME" + ] + } + } + } +} diff --git a/appengine-standard/cloudsql/server.js b/appengine-standard/cloudsql/server.js new file mode 100644 index 0000000000..fc7e674beb --- /dev/null +++ b/appengine-standard/cloudsql/server.js @@ -0,0 +1,110 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Require process, so we can mock environment variables +const process = require('process'); + +// [START app] +const express = require('express'); +const Knex = require('knex'); +const crypto = require('crypto'); + +const app = express(); +app.enable('trust proxy'); + +const knex = connect(); + +function connect () { + // [START connect] + const config = { + user: process.env.SQL_USER, + password: process.env.SQL_PASSWORD, + database: process.env.SQL_DATABASE + }; + + if (process.env.INSTANCE_CONNECTION_NAME && process.env.NODE_ENV === 'production') { + config.socketPath = `/cloudsql/${process.env.INSTANCE_CONNECTION_NAME}`; + } + + // Connect to the database + const knex = Knex({ + client: 'mysql', + connection: config + }); + // [END connect] + + return knex; +} + +/** + * Insert a visit record into the database. + * + * @param {object} knex The Knex connection object. + * @param {object} visit The visit record to insert. + * @returns {Promise} + */ +function insertVisit (knex, visit) { + return knex('visits').insert(visit); +} + +/** + * Retrieve the latest 10 visit records from the database. + * + * @param {object} knex The Knex connection object. + * @returns {Promise} + */ +function getVisits (knex) { + return knex.select('timestamp', 'userIp') + .from('visits') + .orderBy('timestamp', 'desc') + .limit(10) + .then((results) => { + return results.map((visit) => `Time: ${visit.timestamp}, AddrHash: ${visit.userIp}`); + }); +} + +app.get('/', (req, res, next) => { + // Create a visit record to be stored in the database + const visit = { + timestamp: new Date(), + // Store a hash of the visitor's ip address + userIp: crypto.createHash('sha256').update(req.ip).digest('hex').substr(0, 7) + }; + + insertVisit(knex, visit) + // Query the last 10 visits from the database. + .then(() => getVisits(knex)) + .then((visits) => { + res + .status(200) + .set('Content-Type', 'text/plain') + .send(`Last 10 visits:\n${visits.join('\n')}`) + .end(); + }) + .catch((err) => { + next(err); + }); +}); + +const PORT = process.env.PORT || 8080; +app.listen(PORT, () => { + console.log(`App listening on port ${PORT}`); + console.log('Press Ctrl+C to quit.'); +}); +// [END app] + +module.exports = app; diff --git a/appengine-standard/cloudsql/test/createTables.test.js b/appengine-standard/cloudsql/test/createTables.test.js new file mode 100644 index 0000000000..1ec9bdfbf6 --- /dev/null +++ b/appengine-standard/cloudsql/test/createTables.test.js @@ -0,0 +1,129 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const test = require(`ava`); +const path = require(`path`); +const proxyquire = require(`proxyquire`).noPreserveCache(); +const sinon = require(`sinon`); +const tools = require(`@google-cloud/nodejs-repo-tools`); + +const SAMPLE_PATH = path.join(__dirname, `../createTables.js`); + +const exampleConfig = [ + `user`, + `password`, + `database` +]; + +function getSample () { + const configMock = exampleConfig; + const promptMock = { + start: sinon.stub(), + get: sinon.stub().yields(null, configMock) + }; + const tableMock = { + increments: sinon.stub(), + timestamp: sinon.stub(), + string: sinon.stub() + }; + const knexMock = { + schema: { + createTable: sinon.stub() + }, + destroy: sinon.stub().returns(Promise.resolve()) + }; + + knexMock.schema.createTable.returns(Promise.resolve(knexMock)).yields(tableMock); + const KnexMock = sinon.stub().returns(knexMock); + + return { + mocks: { + Knex: KnexMock, + knex: knexMock, + config: configMock, + prompt: promptMock + } + }; +} + +test.beforeEach(tools.stubConsole); +test.afterEach.always(tools.restoreConsole); + +test.cb.serial(`should create a table`, (t) => { + const sample = getSample(); + const expectedResult = `Successfully created 'visits' table.`; + + proxyquire(SAMPLE_PATH, { + knex: sample.mocks.Knex, + prompt: sample.mocks.prompt + }); + + t.true(sample.mocks.prompt.start.calledOnce); + t.true(sample.mocks.prompt.get.calledOnce); + t.deepEqual(sample.mocks.prompt.get.firstCall.args[0], exampleConfig); + + setTimeout(() => { + t.true(sample.mocks.Knex.calledOnce); + t.deepEqual(sample.mocks.Knex.firstCall.args, [{ + client: 'mysql', + connection: exampleConfig + }]); + + t.true(sample.mocks.knex.schema.createTable.calledOnce); + t.is(sample.mocks.knex.schema.createTable.firstCall.args[0], 'visits'); + + t.true(console.log.calledWith(expectedResult)); + t.true(sample.mocks.knex.destroy.calledOnce); + t.end(); + }, 10); +}); + +test.cb.serial(`should handle prompt error`, (t) => { + const error = new Error(`error`); + const sample = getSample(); + sample.mocks.prompt.get = sinon.stub().yields(error); + + proxyquire(SAMPLE_PATH, { + knex: sample.mocks.Knex, + prompt: sample.mocks.prompt + }); + + setTimeout(() => { + t.true(console.error.calledOnce); + t.true(console.error.calledWith(error)); + t.true(sample.mocks.Knex.notCalled); + t.end(); + }, 10); +}); + +test.cb.serial(`should handle knex creation error`, (t) => { + const error = new Error(`error`); + const sample = getSample(); + sample.mocks.knex.schema.createTable = sinon.stub().returns(Promise.reject(error)); + + proxyquire(SAMPLE_PATH, { + knex: sample.mocks.Knex, + prompt: sample.mocks.prompt + }); + + setTimeout(() => { + t.true(console.error.calledOnce); + t.true(console.error.calledWith(`Failed to create 'visits' table:`, error)); + t.true(sample.mocks.knex.destroy.calledOnce); + t.end(); + }, 10); +}); diff --git a/appengine-standard/cloudsql/test/server.test.js b/appengine-standard/cloudsql/test/server.test.js new file mode 100644 index 0000000000..a800c34713 --- /dev/null +++ b/appengine-standard/cloudsql/test/server.test.js @@ -0,0 +1,136 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const express = require(`express`); +const path = require(`path`); +const proxyquire = require(`proxyquire`).noCallThru(); +const request = require(`supertest`); +const sinon = require(`sinon`); +const test = require(`ava`); +const tools = require(`@google-cloud/nodejs-repo-tools`); + +const SAMPLE_PATH = path.join(__dirname, `../server.js`); + +function getSample () { + const testApp = express(); + sinon.stub(testApp, `listen`).yields(); + const expressMock = sinon.stub().returns(testApp); + const resultsMock = [ + { + timestamp: `1234`, + userIp: `abcd` + } + ]; + + const knexMock = sinon.stub().returns({ + insert: sinon.stub().returns(Promise.resolve()) + }); + Object.assign(knexMock, { + select: sinon.stub().returnsThis(), + from: sinon.stub().returnsThis(), + orderBy: sinon.stub().returnsThis(), + limit: sinon.stub().returns(Promise.resolve(resultsMock)) + }); + + const KnexMock = sinon.stub().returns(knexMock); + + const processMock = { + env: { + SQL_USER: 'user', + SQL_PASSWORD: 'password', + SQL_DATABASE: 'database' + } + }; + + const app = proxyquire(SAMPLE_PATH, { + knex: KnexMock, + express: expressMock, + process: processMock + }); + + return { + app: app, + mocks: { + express: expressMock, + results: resultsMock, + knex: knexMock, + Knex: KnexMock, + process: processMock + } + }; +} + +test.beforeEach(tools.stubConsole); +test.afterEach.always(tools.restoreConsole); + +test(`should set up sample in Postgres`, (t) => { + const sample = getSample(); + + t.true(sample.mocks.express.calledOnce); + t.true(sample.mocks.Knex.calledOnce); + t.deepEqual(sample.mocks.Knex.firstCall.args, [{ + client: 'mysql', + connection: { + user: sample.mocks.process.env.SQL_USER, + password: sample.mocks.process.env.SQL_PASSWORD, + database: sample.mocks.process.env.SQL_DATABASE + } + }]); +}); + +test.cb(`should record a visit`, (t) => { + const sample = getSample(); + const expectedResult = `Last 10 visits:\nTime: 1234, AddrHash: abcd`; + + request(sample.app) + .get(`/`) + .expect(200) + .expect((response) => { + t.is(response.text, expectedResult); + }) + .end(t.end); +}); + +test.cb(`should handle insert error`, (t) => { + const sample = getSample(); + const expectedResult = `insert_error`; + + sample.mocks.knex.limit.returns(Promise.reject(expectedResult)); + + request(sample.app) + .get(`/`) + .expect(500) + .expect((response) => { + t.is(response.text.includes(expectedResult), true); + }) + .end(t.end); +}); + +test.cb(`should handle read error`, (t) => { + const sample = getSample(); + const expectedResult = `read_error`; + + sample.mocks.knex.limit.returns(Promise.reject(expectedResult)); + + request(sample.app) + .get(`/`) + .expect(500) + .expect((response) => { + t.is(response.text.includes(expectedResult), true); + }) + .end(t.end); +}); diff --git a/appengine-standard/cloudsql_postgresql/README.md b/appengine-standard/cloudsql_postgresql/README.md new file mode 100644 index 0000000000..f059be0f5c --- /dev/null +++ b/appengine-standard/cloudsql_postgresql/README.md @@ -0,0 +1,8 @@ +# Cloud SQL for Postgres Node.js sample on App Engine Standard environment + +This sample demonstrates how to use [Google Cloud SQL][sql] for +[Postgres][postgres] on Google App Engine Standard Environment. + +[sql]: https://cloud.google.com/sql/ +[flexible]: https://cloud.google.com/appengine +[postgres]: https://www.postgresql.org/download/ diff --git a/appengine-standard/cloudsql_postgresql/app.yaml b/appengine-standard/cloudsql_postgresql/app.yaml new file mode 100644 index 0000000000..c96f7f553b --- /dev/null +++ b/appengine-standard/cloudsql_postgresql/app.yaml @@ -0,0 +1,33 @@ +# Copyright 2017, Google, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app_yaml] +runtime: nodejs8 + +# [START env] +env_variables: + SQL_USER: YOUR_SQL_USER + SQL_PASSWORD: YOUR_SQL_PASSWORD + SQL_DATABASE: YOUR_SQL_DATABASE + # e.g. my-awesome-project:us-central1:my-cloud-sql-instance + INSTANCE_CONNECTION_NAME: YOUR_INSTANCE_CONNECTION_NAME +# [END env] + +# [START cloudsql_settings] +beta_settings: + # The connection name of your instance, available by using + # 'gcloud beta sql instances describe [INSTANCE_NAME]' or from + # the Instance details page in the Google Cloud Platform Console. + cloud_sql_instances: YOUR_INSTANCE_CONNECTION_NAME +# [END cloudsql_settings] +# [END app_yaml] diff --git a/appengine-standard/cloudsql_postgresql/createTables.js b/appengine-standard/cloudsql_postgresql/createTables.js new file mode 100644 index 0000000000..cf6f69c929 --- /dev/null +++ b/appengine-standard/cloudsql_postgresql/createTables.js @@ -0,0 +1,53 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// [START createTables] +const Knex = require('knex'); +const prompt = require('prompt'); + +const FIELDS = ['user', 'password', 'database']; + +prompt.start(); + +// Prompt the user for connection details +prompt.get(FIELDS, (err, config) => { + if (err) { + console.error(err); + return; + } + + // Connect to the database + const knex = Knex({ client: 'pg', connection: config }); + + // Create the "visits" table + knex.schema.createTable('visits', (table) => { + table.increments(); + table.timestamp('timestamp'); + table.string('userIp'); + }) + .then(() => { + console.log(`Successfully created 'visits' table.`); + return knex.destroy(); + }) + .catch((err) => { + console.error(`Failed to create 'visits' table:`, err); + if (knex) { + knex.destroy(); + } + }); +}); +// [END createTables] diff --git a/appengine-standard/cloudsql_postgresql/package.json b/appengine-standard/cloudsql_postgresql/package.json new file mode 100644 index 0000000000..22d31d8bb0 --- /dev/null +++ b/appengine-standard/cloudsql_postgresql/package.json @@ -0,0 +1,67 @@ +{ + "name": "appengine-cloudsql-postgres", + "description": "Node.js PostgreSQL sample for Cloud SQL on App Engine Standard environment.", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google Inc.", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": "8.x.x" + }, + "scripts": { + "deploy": "gcloud app deploy", + "lint": "samples lint", + "pretest": "npm run lint", + "unit-test": "ava --verbose test/*.test.js", + "start-proxy": "! pgrep cloud_sql_proxy > /dev/null && cloud_sql_proxy -instances=$INSTANCE_CONNECTION_NAME=tcp:$SQL_PORT &", + "system-test": "samples test app", + "system-test-proxy": "npm run start-proxy; npm run system-test", + "all-test": "npm run unit-test && npm run system-test", + "test": "samples test run --cmd npm -- run all-test", + "e2e-test": "samples test deploy" + }, + "dependencies": { + "async": "2.5.0", + "express": "4.15.4", + "knex": "0.13.0", + "pg": "7.3.0", + "prompt": "1.0.0" + }, + "devDependencies": { + "@google-cloud/nodejs-repo-tools": "1.4.17", + "ava": "0.22.0" + }, + "cloud-repo-tools": { + "requiresKeyFile": true, + "requiresProjectId": true, + "test": { + "app": { + "requiredEnvVars": [ + "SQL_USER", + "SQL_PASSWORD", + "SQL_DATABASE", + "SQL_PORT", + "INSTANCE_CONNECTION_NAME" + ], + "msg": "Last 10 visits:", + "substitutions": "YOUR_SQL_USER=$SQL_USER,YOUR_SQL_PASSWORD=$SQL_PASSWORD,YOUR_SQL_DATABASE=$SQL_DATABASE,YOUR_INSTANCE_CONNECTION_NAME=$INSTANCE_CONNECTION_NAME", + "args": [ + "server.js" + ] + }, + "build": { + "requiredEnvVars": [ + "SQL_USER", + "SQL_PASSWORD", + "SQL_DATABASE", + "SQL_PORT", + "INSTANCE_CONNECTION_NAME" + ] + } + } + } +} diff --git a/appengine-standard/cloudsql_postgresql/server.js b/appengine-standard/cloudsql_postgresql/server.js new file mode 100644 index 0000000000..1a2343d11f --- /dev/null +++ b/appengine-standard/cloudsql_postgresql/server.js @@ -0,0 +1,110 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Require process, so we can mock environment variables +const process = require('process'); + +// [START app] +const express = require('express'); +const Knex = require('knex'); +const crypto = require('crypto'); + +const app = express(); +app.enable('trust proxy'); + +const knex = connect(); + +function connect () { + // [START connect] + const config = { + user: process.env.SQL_USER, + password: process.env.SQL_PASSWORD, + database: process.env.SQL_DATABASE + }; + + if (process.env.INSTANCE_CONNECTION_NAME && process.env.NODE_ENV === 'production') { + config.host = `/cloudsql/${process.env.INSTANCE_CONNECTION_NAME}`; + } + + // Connect to the database + const knex = Knex({ + client: 'pg', + connection: config + }); + // [END connect] + + return knex; +} + +/** + * Insert a visit record into the database. + * + * @param {object} knex The Knex connection object. + * @param {object} visit The visit record to insert. + * @returns {Promise} + */ +function insertVisit (knex, visit) { + return knex('visits').insert(visit); +} + +/** + * Retrieve the latest 10 visit records from the database. + * + * @param {object} knex The Knex connection object. + * @returns {Promise} + */ +function getVisits (knex) { + return knex.select('timestamp', 'userIp') + .from('visits') + .orderBy('timestamp', 'desc') + .limit(10) + .then((results) => { + return results.map((visit) => `Time: ${visit.timestamp}, AddrHash: ${visit.userIp}`); + }); +} + +app.get('/', (req, res, next) => { + // Create a visit record to be stored in the database + const visit = { + timestamp: new Date(), + // Store a hash of the visitor's ip address + userIp: crypto.createHash('sha256').update(req.ip).digest('hex').substr(0, 7) + }; + + insertVisit(knex, visit) + // Query the last 10 visits from the database. + .then(() => getVisits(knex)) + .then((visits) => { + res + .status(200) + .set('Content-Type', 'text/plain') + .send(`Last 10 visits:\n${visits.join('\n')}`) + .end(); + }) + .catch((err) => { + next(err); + }); +}); + +const PORT = process.env.PORT || 8080; +app.listen(PORT, () => { + console.log(`App listening on port ${PORT}`); + console.log('Press Ctrl+C to quit.'); +}); +// [END app] + +module.exports = app; diff --git a/appengine-standard/cloudsql_postgresql/test/createTables.test.js b/appengine-standard/cloudsql_postgresql/test/createTables.test.js new file mode 100644 index 0000000000..0d43f8add7 --- /dev/null +++ b/appengine-standard/cloudsql_postgresql/test/createTables.test.js @@ -0,0 +1,129 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const test = require(`ava`); +const path = require(`path`); +const proxyquire = require(`proxyquire`).noPreserveCache(); +const sinon = require(`sinon`); +const tools = require(`@google-cloud/nodejs-repo-tools`); + +const SAMPLE_PATH = path.join(__dirname, `../createTables.js`); + +const exampleConfig = [ + `user`, + `password`, + `database` +]; + +function getSample () { + const configMock = exampleConfig; + const promptMock = { + start: sinon.stub(), + get: sinon.stub().yields(null, configMock) + }; + const tableMock = { + increments: sinon.stub(), + timestamp: sinon.stub(), + string: sinon.stub() + }; + const knexMock = { + schema: { + createTable: sinon.stub() + }, + destroy: sinon.stub().returns(Promise.resolve()) + }; + + knexMock.schema.createTable.returns(Promise.resolve(knexMock)).yields(tableMock); + const KnexMock = sinon.stub().returns(knexMock); + + return { + mocks: { + Knex: KnexMock, + knex: knexMock, + config: configMock, + prompt: promptMock + } + }; +} + +test.beforeEach(tools.stubConsole); +test.afterEach.always(tools.restoreConsole); + +test.cb.serial(`should create a table`, (t) => { + const sample = getSample(); + const expectedResult = `Successfully created 'visits' table.`; + + proxyquire(SAMPLE_PATH, { + knex: sample.mocks.Knex, + prompt: sample.mocks.prompt + }); + + t.true(sample.mocks.prompt.start.calledOnce); + t.true(sample.mocks.prompt.get.calledOnce); + t.deepEqual(sample.mocks.prompt.get.firstCall.args[0], exampleConfig); + + setTimeout(() => { + t.true(sample.mocks.Knex.calledOnce); + t.deepEqual(sample.mocks.Knex.firstCall.args, [{ + client: 'pg', + connection: exampleConfig + }]); + + t.true(sample.mocks.knex.schema.createTable.calledOnce); + t.is(sample.mocks.knex.schema.createTable.firstCall.args[0], 'visits'); + + t.true(console.log.calledWith(expectedResult)); + t.true(sample.mocks.knex.destroy.calledOnce); + t.end(); + }, 10); +}); + +test.cb.serial(`should handle prompt error`, (t) => { + const error = new Error(`error`); + const sample = getSample(); + sample.mocks.prompt.get = sinon.stub().yields(error); + + proxyquire(SAMPLE_PATH, { + knex: sample.mocks.Knex, + prompt: sample.mocks.prompt + }); + + setTimeout(() => { + t.true(console.error.calledOnce); + t.true(console.error.calledWith(error)); + t.true(sample.mocks.Knex.notCalled); + t.end(); + }, 10); +}); + +test.cb.serial(`should handle knex creation error`, (t) => { + const error = new Error(`error`); + const sample = getSample(); + sample.mocks.knex.schema.createTable = sinon.stub().returns(Promise.reject(error)); + + proxyquire(SAMPLE_PATH, { + knex: sample.mocks.Knex, + prompt: sample.mocks.prompt + }); + + setTimeout(() => { + t.true(console.error.calledOnce); + t.true(console.error.calledWith(`Failed to create 'visits' table:`, error)); + t.true(sample.mocks.knex.destroy.calledOnce); + t.end(); + }, 10); +}); diff --git a/appengine-standard/cloudsql_postgresql/test/server.test.js b/appengine-standard/cloudsql_postgresql/test/server.test.js new file mode 100644 index 0000000000..94b49af475 --- /dev/null +++ b/appengine-standard/cloudsql_postgresql/test/server.test.js @@ -0,0 +1,136 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const express = require(`express`); +const path = require(`path`); +const proxyquire = require(`proxyquire`).noCallThru(); +const request = require(`supertest`); +const sinon = require(`sinon`); +const test = require(`ava`); +const tools = require(`@google-cloud/nodejs-repo-tools`); + +const SAMPLE_PATH = path.join(__dirname, `../server.js`); + +function getSample () { + const testApp = express(); + sinon.stub(testApp, `listen`).yields(); + const expressMock = sinon.stub().returns(testApp); + const resultsMock = [ + { + timestamp: `1234`, + userIp: `abcd` + } + ]; + + const knexMock = sinon.stub().returns({ + insert: sinon.stub().returns(Promise.resolve()) + }); + Object.assign(knexMock, { + select: sinon.stub().returnsThis(), + from: sinon.stub().returnsThis(), + orderBy: sinon.stub().returnsThis(), + limit: sinon.stub().returns(Promise.resolve(resultsMock)) + }); + + const KnexMock = sinon.stub().returns(knexMock); + + const processMock = { + env: { + SQL_USER: 'user', + SQL_PASSWORD: 'password', + SQL_DATABASE: 'database' + } + }; + + const app = proxyquire(SAMPLE_PATH, { + knex: KnexMock, + express: expressMock, + process: processMock + }); + + return { + app: app, + mocks: { + express: expressMock, + results: resultsMock, + knex: knexMock, + Knex: KnexMock, + process: processMock + } + }; +} + +test.beforeEach(tools.stubConsole); +test.afterEach.always(tools.restoreConsole); + +test(`should set up sample in Postgres`, (t) => { + const sample = getSample(); + + t.true(sample.mocks.express.calledOnce); + t.true(sample.mocks.Knex.calledOnce); + t.deepEqual(sample.mocks.Knex.firstCall.args, [{ + client: 'pg', + connection: { + user: sample.mocks.process.env.SQL_USER, + password: sample.mocks.process.env.SQL_PASSWORD, + database: sample.mocks.process.env.SQL_DATABASE + } + }]); +}); + +test.cb(`should record a visit`, (t) => { + const sample = getSample(); + const expectedResult = `Last 10 visits:\nTime: 1234, AddrHash: abcd`; + + request(sample.app) + .get(`/`) + .expect(200) + .expect((response) => { + t.is(response.text, expectedResult); + }) + .end(t.end); +}); + +test.cb(`should handle insert error`, (t) => { + const sample = getSample(); + const expectedResult = `insert_error`; + + sample.mocks.knex.limit.returns(Promise.reject(expectedResult)); + + request(sample.app) + .get(`/`) + .expect(500) + .expect((response) => { + t.is(response.text.includes(expectedResult), true); + }) + .end(t.end); +}); + +test.cb(`should handle read error`, (t) => { + const sample = getSample(); + const expectedResult = `read_error`; + + sample.mocks.knex.limit.returns(Promise.reject(expectedResult)); + + request(sample.app) + .get(`/`) + .expect(500) + .expect((response) => { + t.is(response.text.includes(expectedResult), true); + }) + .end(t.end); +}); diff --git a/appengine-standard/cloudtasks/README.md b/appengine-standard/cloudtasks/README.md new file mode 100644 index 0000000000..28ac625b90 --- /dev/null +++ b/appengine-standard/cloudtasks/README.md @@ -0,0 +1,87 @@ +# Node.js Google Cloud Tasks sample for Google App Engine Standard Environment + +This sample demonstrates how to use [Google Cloud Tasks](https://cloud.google.com/cloud-tasks/) +on Google App Engine Standard. + +App Engine queues push tasks to an App Engine HTTP target. This directory +contains both the App Engine app to deploy, as well as the snippets to run +locally to push tasks to it, which could also be called on App Engine. + +`createTask.js` is a simple command-line program to create tasks to be pushed to +the App Engine app. + +`server.js` is the main App Engine app. This app serves as an endpoint to +receive App Engine task attempts. + +`app.yaml` configures the App Engine app. + +* [Setup](#setup) +* [Running locally](#running-locally) +* [Deploying to App Engine](#deploying-to-app-engine) +* [Running the tests](#running-the-tests) + +## Setup + +Before you can run or deploy the sample, you need to do the following: + +1. Refer to the [appengine/README.md][readme] file for instructions on + running and deploying. +1. Enable the Cloud Tasks API in the [Google Cloud Console](https://cloud.google.com/apis/library/cloudtasks.googleapis.com). +1. Install dependencies: + + With `npm`: + + npm install + + or with `yarn`: + + yarn install + +## Creating a queue + +To create a queue using the Cloud SDK, use the following gcloud command: + + gcloud alpha tasks queues create-app-engine-queue my-appengine-queue + +Note: A newly created queue will route to the default App Engine service and +version unless configured to do otherwise. Read the online help for the +`create-app-engine-queue` or the `update-app-engine-queue` commands to learn +about routing overrides for App Engine queues. + +## Deploying the App Engine app + +Deploy the App Engine app with gcloud: + + gcloud app deploy + +Verify the index page is serving: + + gcloud app browse + +The App Engine app serves as a target for the push requests. It has an +endpoint `/log_payload` that reads the payload (i.e., the request body) of the +HTTP POST request and logs it. The log output can be viewed with: + + gcloud app logs read + +## Running the Samples + +To get usage information: `node createTask.js --help` + +Which prints: + +``` +Options: + --version Show version number [boolean] + --location, -l Location of the queue to add the task to. [string] [required] + --queue, -q ID (short name) of the queue to add the task to. [string] [required] + --project, -p Project of the queue to add the task to. [string] [required] + --payload, -d (Optional) Payload to attach to the push queue. [string] + --inSeconds, -s (Optional) The number of seconds from now to schedule task attempt. [number] + --help Show help [boolean] + +Examples: + node createTask.js --project my-project-id + +For more information, see https://cloud.google.com/cloud-tasks +``` diff --git a/appengine-standard/cloudtasks/app.yaml b/appengine-standard/cloudtasks/app.yaml new file mode 100644 index 0000000000..8f8f6fd97a --- /dev/null +++ b/appengine-standard/cloudtasks/app.yaml @@ -0,0 +1,16 @@ +# Copyright 2017, Google, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app_yaml] +runtime: nodejs8 +# [END app_yaml] diff --git a/appengine-standard/cloudtasks/createTask.js b/appengine-standard/cloudtasks/createTask.js new file mode 100644 index 0000000000..03312bc7b2 --- /dev/null +++ b/appengine-standard/cloudtasks/createTask.js @@ -0,0 +1,130 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const google = require('googleapis'); +const cloudtasks = google.cloudtasks('v2beta2'); + +/** + * Create a task for a given queue with an arbitrary payload. + */ +function createTask (project, location, queue, options) { + // [START cloud_tasks_appengine_create_task] + authorize((authClient) => { + const task = { + app_engine_http_request: { + http_method: 'POST', + relative_url: '/log_payload' + } + }; + + if (options.payload !== undefined) { + task.app_engine_http_request.payload = Buffer.from(options.payload).toString('base64'); + } + + if (options.inSeconds !== undefined) { + task.schedule_time = (new Date(options.inSeconds * 1000 + Date.now())).toISOString(); + } + + const request = { + parent: `projects/${project}/locations/${location}/queues/${queue}`, // TODO: Update placeholder value. + resource: { + task: task + }, + auth: authClient + }; + + console.log('Sending task %j', task); + + cloudtasks.projects.locations.queues.tasks.create(request, (err, response) => { + if (err) { + console.error(err); + return; + } + + console.log('Created task.', response.name); + console.log(JSON.stringify(response, null, 2)); + }); + }); + + function authorize (callback) { + google.auth.getApplicationDefault(function (err, authClient) { + if (err) { + console.error('authentication failed: ', err); + return; + } + if (authClient.createScopedRequired && authClient.createScopedRequired()) { + var scopes = ['https://www.googleapis.com/auth/cloud-platform']; + authClient = authClient.createScoped(scopes); + } + callback(authClient); + }); + } + // [END cloud_tasks_appengine_create_task] +} + +const cli = require(`yargs`) + .options({ + location: { + alias: 'l', + description: 'Location of the queue to add the task to.', + type: 'string', + requiresArg: true, + required: true + }, + queue: { + alias: 'q', + description: 'ID (short name) of the queue to add the task to.', + type: 'string', + requiresArg: true, + required: true + }, + project: { + alias: 'p', + description: 'Project of the queue to add the task to.', + default: process.env.GCLOUD_PROJECT, + type: 'string', + requiresArg: true, + required: true + }, + payload: { + alias: 'd', + description: '(Optional) Payload to attach to the push queue.', + type: 'string', + requiresArg: true + }, + inSeconds: { + alias: 's', + description: '(Optional) The number of seconds from now to schedule task attempt.', + type: 'number', + requiresArg: true + } + }) + .example(`node $0 --project my-project-id`) + .wrap(120) + .recommendCommands() + .epilogue(`For more information, see https://cloud.google.com/cloud-tasks`) + .strict(); + +if (module === require.main) { + const opts = cli.help().parse(process.argv.slice(2)); + + process.env.GCLOUD_PROJECT = opts.project; + + createTask(opts.project, opts.location, opts.queue, opts); +} + +exports.createTask = createTask; diff --git a/appengine-standard/cloudtasks/package.json b/appengine-standard/cloudtasks/package.json new file mode 100644 index 0000000000..7ac31b32f3 --- /dev/null +++ b/appengine-standard/cloudtasks/package.json @@ -0,0 +1,45 @@ +{ + "name": "appengine-cloudtasks", + "description": "Google App Engine Standard Environment Cloud Tasks example.", + "version": "0.0.0", + "license": "Apache-2.0", + "author": "Google Inc.", + "private": true, + "repository": "GoogleCloudPlatform/nodejs-docs-samples", + "engines": { + "node": "8.x.x" + }, + "scripts": { + "deploy": "gcloud app deploy", + "lint": "repo-tools lint", + "pretest": "npm run lint", + "unit-test": "ava --verbose test/*.test.js", + "system-test": "repo-tools test app --config package.json --config-key cloud-repo-tools", + "all-test": "npm run unit-test && npm run system-test", + "test": "repo-tools test run --cmd npm -- run all-test", + "e2e-test": "repo-tools test deploy --config package.json --config-key cloud-repo-tools" + }, + "dependencies": { + "body-parser": "1.18.2", + "express": "4.16.2", + "googleapis": "22.2.0", + "yargs": "10.0.3" + }, + "devDependencies": { + "@google-cloud/nodejs-repo-tools": "2.1.1", + "proxyquire": "1.8.0", + "sinon": "4.0.2" + }, + "cloud-repo-tools": { + "requiresKeyFile": true, + "requiresProjectId": true, + "test": { + "app": { + "msg": "Hello, world!", + "args": [ + "server.js" + ] + } + } + } +} diff --git a/appengine-standard/cloudtasks/server.js b/appengine-standard/cloudtasks/server.js new file mode 100644 index 0000000000..89b196ed4f --- /dev/null +++ b/appengine-standard/cloudtasks/server.js @@ -0,0 +1,49 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// [START cloud_tasks_appengine_quickstart] +const bodyParser = require('body-parser'); +const express = require('express'); + +const app = express(); +app.enable('trust proxy'); + +app.use(bodyParser.raw()); +app.use(bodyParser.json()); +app.use(bodyParser.text()); + +app.get('/', (req, res, next) => { + // Basic index to verify app is serving + res.send('Hello, world!').end(); +}); + +app.post('/log_payload', (req, res, next) => { + // Log the request payload + console.log('Received task with payload: %s', req.body); + res.send(`Printed task payload: ${req.body}`).end(); +}); + +app.get('*', (req, res) => { + res.send('OK').end(); +}); + +const PORT = process.env.PORT || 8080; +app.listen(process.env.PORT || 8080, () => { + console.log(`App listening on port ${PORT}`); + console.log('Press Ctrl+C to quit.'); +}); +// [END cloud_tasks_appengine_quickstart] diff --git a/appengine-standard/cloudtasks/test/createTask.test.js b/appengine-standard/cloudtasks/test/createTask.test.js new file mode 100644 index 0000000000..1d7fd3e400 --- /dev/null +++ b/appengine-standard/cloudtasks/test/createTask.test.js @@ -0,0 +1,59 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const proxyquire = require(`proxyquire`).noCallThru(); +const sinon = require(`sinon`); +const test = require(`ava`); +const tools = require(`@google-cloud/nodejs-repo-tools`); + +test.before(tools.stubConsole); +test.after.always(tools.restoreConsole); + +test.cb(`should create a task`, (t) => { + const responseMock = { + name: 'foo' + }; + const cloudtasksMock = { + projects: { + locations: { + queues: { + tasks: { + create: sinon.stub().yields(responseMock) + } + } + } + } + }; + const authClientMock = {}; + + const util = proxyquire(`../createTask`, { + googleapis: { + cloudtasks: sinon.stub().returns(cloudtasksMock), + auth: { + getApplicationDefault: sinon.stub().yields(null, authClientMock) + } + } + }); + + util.createTask('p', 'l', 'q', {}); + + setTimeout(() => { + t.true(console.log.called); + t.is(cloudtasksMock.projects.locations.queues.tasks.create.callCount, 1); + t.end(); + }, 500); +}); diff --git a/appengine-standard/datastore/README.md b/appengine-standard/datastore/README.md new file mode 100644 index 0000000000..4d9e8d346a --- /dev/null +++ b/appengine-standard/datastore/README.md @@ -0,0 +1,52 @@ +# Using Cloud Datastore + +This sample application demonstrates how to use Cloud Datastore in Google App +Engine Node.js Standard Environment. + +* [Setup](#setup) +* [Running locally](#running-locally) +* [Deploying to App Engine](#deploying-to-app-engine) +* [Running the tests](#running-the-tests) + +## Setup + +Before you can run or deploy the sample, you need to do the following: + +1. Refer to the [appengine/README.md][readme] file for instructions on + running and deploying. +1. Install dependencies: + + With `npm`: + + npm install + + or with `yarn`: + + yarn install + +## Running locally + +With `npm`: + + npm start + +or with `yarn`: + + yarn start + +## Deploying to App Engine + +With `npm`: + + npm run deploy + +or with `yarn`: + + yarn run deploy + +## Running the tests + +See [Contributing][contributing]. + +[readme]: ../README.md +[contributing]: https://github.com/GoogleCloudPlatform/nodejs-docs-samples/blob/master/CONTRIBUTING.md diff --git a/appengine-standard/datastore/app.js b/appengine-standard/datastore/app.js new file mode 100644 index 0000000000..4c28d110d4 --- /dev/null +++ b/appengine-standard/datastore/app.js @@ -0,0 +1,98 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// [START app] +'use strict'; + +// [START setup] +const express = require('express'); +const crypto = require('crypto'); + +const app = express(); +app.enable('trust proxy'); + +// By default, the client will authenticate using the service account file +// specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable and use +// the project specified by the GOOGLE_CLOUD_PROJECT environment variable. See +// https://github.com/GoogleCloudPlatform/google-cloud-node/blob/master/docs/authentication.md +// These environment variables are set automatically on Google App Engine +const Datastore = require('@google-cloud/datastore'); + +// Instantiate a datastore client +const datastore = Datastore(); +// [END setup] + +// [START insertVisit] +/** + * Insert a visit record into the database. + * + * @param {object} visit The visit record to insert. + */ +function insertVisit (visit) { + return datastore.save({ + key: datastore.key('visit'), + data: visit + }); +} +// [END insertVisit] + +// [START getVisits] +/** + * Retrieve the latest 10 visit records from the database. + */ +function getVisits () { + const query = datastore.createQuery('visit') + .order('timestamp', { descending: true }) + .limit(10); + + return datastore.runQuery(query) + .then((results) => { + const entities = results[0]; + return entities.map((entity) => `Time: ${entity.timestamp}, AddrHash: ${entity.userIp}`); + }); +} +// [END getVisits] + +app.get('/', (req, res, next) => { + // Create a visit record to be stored in the database + const visit = { + timestamp: new Date(), + // Store a hash of the visitor's ip address + userIp: crypto.createHash('sha256').update(req.ip).digest('hex').substr(0, 7) + }; + + insertVisit(visit) + // Query the last 10 visits from Datastore. + .then(() => getVisits()) + .then((visits) => { + res + .status(200) + .set('Content-Type', 'text/plain') + .send(`Last 10 visits:\n${visits.join('\n')}`) + .end(); + }) + .catch(next); +}); + +// [START listen] +const PORT = process.env.PORT || 8080; +app.listen(process.env.PORT || 8080, () => { + console.log(`App listening on port ${PORT}`); + console.log('Press Ctrl+C to quit.'); +}); +// [END listen] +// [END app] + +module.exports = app; diff --git a/appengine-standard/datastore/app.yaml b/appengine-standard/datastore/app.yaml new file mode 100644 index 0000000000..8f8f6fd97a --- /dev/null +++ b/appengine-standard/datastore/app.yaml @@ -0,0 +1,16 @@ +# Copyright 2017, Google, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app_yaml] +runtime: nodejs8 +# [END app_yaml] diff --git a/appengine-standard/datastore/package.json b/appengine-standard/datastore/package.json new file mode 100644 index 0000000000..081ec60f0d --- /dev/null +++ b/appengine-standard/datastore/package.json @@ -0,0 +1,40 @@ +{ + "name": "appengine-datastore", + "description": "Sample for Google Cloud Datastore on Google App Engine Standard Environment.", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google Inc.", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": "8.x.x" + }, + "scripts": { + "deploy": "gcloud app deploy", + "start": "node app.js", + "lint": "samples lint", + "pretest": "npm run lint", + "system-test": "samples test app", + "test": "npm run system-test", + "e2e-test": "samples test deploy" + }, + "dependencies": { + "@google-cloud/datastore": "1.1.0", + "express": "4.15.4" + }, + "devDependencies": { + "@google-cloud/nodejs-repo-tools": "1.4.17" + }, + "cloud-repo-tools": { + "test": { + "app": { + "msg": "Last 10 visits:" + } + }, + "requiresKeyFile": true, + "requiresProjectId": true + } +} diff --git a/appengine-standard/errorreporting/README.md b/appengine-standard/errorreporting/README.md new file mode 100644 index 0000000000..a25457c990 --- /dev/null +++ b/appengine-standard/errorreporting/README.md @@ -0,0 +1,9 @@ +# Node.js error reporting sample for Google App Engine + +This sample demonstrates error reporting in a Node.js app for +Google App Engine Standard Environment. + +## Running locally + +Refer to the [appengine/README.md](../README.md) file for instructions on +running and deploying. diff --git a/appengine-standard/errorreporting/app.js b/appengine-standard/errorreporting/app.js new file mode 100644 index 0000000000..620af5fd40 --- /dev/null +++ b/appengine-standard/errorreporting/app.js @@ -0,0 +1,44 @@ +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// [START app] +'use strict'; + +// [START setup] +const express = require('express'); +const errors = require('@google-cloud/error-reporting')(); + +const app = express(); +// [END setup] + +app.get('/', (req, res, next) => { + next(new Error('something is wrong!')); +}); + +app.use((err, req, res, next) => { + errors.report(err); + res.status(500).send(err.message || 'Something broke!'); +}); + +// [START listen] +const PORT = process.env.PORT || 8080; +app.listen(PORT, () => { + console.log(`App listening on port ${PORT}`); + console.log('Press Ctrl+C to quit.'); +}); +// [END listen] +// [END app] + +module.exports = app; diff --git a/appengine-standard/errorreporting/app.yaml b/appengine-standard/errorreporting/app.yaml new file mode 100644 index 0000000000..8cf0f07dc8 --- /dev/null +++ b/appengine-standard/errorreporting/app.yaml @@ -0,0 +1,16 @@ +# Copyright 2015-2016, Google, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app_yaml] +runtime: nodejs8 +# [END app_yaml] diff --git a/appengine-standard/errorreporting/package.json b/appengine-standard/errorreporting/package.json new file mode 100644 index 0000000000..cfbfe0ae5d --- /dev/null +++ b/appengine-standard/errorreporting/package.json @@ -0,0 +1,45 @@ +{ + "name": "appengine-error-reporting", + "description": "Node.js error reporting sample for Google App Engine Standard Environment.", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google Inc.", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": "8.x.x" + }, + "scripts": { + "deploy": "gcloud app deploy", + "start": "node app.js", + "lint": "samples lint", + "pretest": "npm run lint", + "unit-test": "ava --verbose test/*.test.js", + "system-test": "samples test app", + "test": "npm run unit-test && npm run system-test", + "e2e-test": "samples test deploy" + }, + "dependencies": { + "@google-cloud/error-reporting": "0.2.1", + "express": "4.15.4" + }, + "devDependencies": { + "@google-cloud/nodejs-repo-tools": "1.4.17", + "ava": "0.21.0", + "proxyquire": "1.8.0", + "sinon": "3.2.0" + }, + "cloud-repo-tools": { + "test": { + "app": { + "msg": "something is wrong", + "code": 500 + } + }, + "requiresKeyFile": true, + "requiresProjectId": true + } +} diff --git a/appengine-standard/errorreporting/test/app.test.js b/appengine-standard/errorreporting/test/app.test.js new file mode 100644 index 0000000000..91a067b2fb --- /dev/null +++ b/appengine-standard/errorreporting/test/app.test.js @@ -0,0 +1,83 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const express = require(`express`); +const path = require(`path`); +const proxyquire = require(`proxyquire`).noPreserveCache(); +const request = require(`supertest`); +const sinon = require('sinon'); +const test = require('ava'); +const tools = require('@google-cloud/nodejs-repo-tools'); + +const SAMPLE_PATH = path.join(__dirname, `../app.js`); + +function getSample () { + const testApp = express(); + sinon.stub(testApp, `listen`).callsArg(1); + const expressMock = sinon.stub().returns(testApp); + const resultsMock = JSON.stringify({ + timestamp: `1234`, + userIp: `abcd` + }) + `\n`; + const reportMock = sinon.stub(); + const errorsMock = sinon.stub().callsFake(function ErrorReporting () { + return { + report: reportMock + }; + }); + + const app = proxyquire(SAMPLE_PATH, { + express: expressMock, + '@google-cloud/error-reporting': errorsMock + }); + return { + app: app, + mocks: { + errors: errorsMock, + express: expressMock, + report: reportMock, + results: resultsMock + } + }; +} + +test.beforeEach(tools.stubConsole); +test.afterEach.always(tools.restoreConsole); + +test(`sets up the sample`, (t) => { + const sample = getSample(); + + t.true(sample.mocks.express.calledOnce); + t.true(sample.mocks.errors.calledOnce); + t.is(sample.mocks.report.callCount, 0); + t.true(sample.app.listen.calledOnce); + t.is(sample.app.listen.firstCall.args[0], process.env.PORT || 8080); +}); + +test.cb(`should throw an error`, (t) => { + const sample = getSample(); + const expectedResult = `something is wrong!`; + + request(sample.app) + .get(`/`) + .expect(500) + .expect((response) => { + t.true(sample.mocks.report.calledOnce); + t.is(response.text, expectedResult); + }) + .end(t.end); +}); diff --git a/appengine-standard/hello-world/README.md b/appengine-standard/hello-world/README.md new file mode 100644 index 0000000000..bcc7fb0862 --- /dev/null +++ b/appengine-standard/hello-world/README.md @@ -0,0 +1,52 @@ +# Quickstart for Node.js in the App Engine Standard Environment + +This sample application demonstrates how to deploy a basic Node.js web +application on Google App Engine Standard Environment. + +* [Setup](#setup) +* [Running locally](#running-locally) +* [Deploying to App Engine](#deploying-to-app-engine) +* [Running the tests](#running-the-tests) + +## Setup + +Before you can run or deploy the sample, you need to do the following: + +1. Refer to the [appengine/README.md][readme] file for instructions on + running and deploying. +1. Install dependencies: + + With `npm`: + + npm install + + or with `yarn`: + + yarn install + +## Running locally + +With `npm`: + + npm start + +or with `yarn`: + + yarn start + +## Deploying to App Engine + +With `npm`: + + npm run deploy + +or with `yarn`: + + yarn run deploy + +## Running the tests + +See [Contributing][contributing]. + +[readme]: ../README.md +[contributing]: https://github.com/GoogleCloudPlatform/nodejs-docs-samples/blob/master/CONTRIBUTING.md diff --git a/appengine-standard/hello-world/app.js b/appengine-standard/hello-world/app.js new file mode 100644 index 0000000000..be1e3735bc --- /dev/null +++ b/appengine-standard/hello-world/app.js @@ -0,0 +1,33 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// [START app] +const express = require('express'); + +const app = express(); + +app.get('/', (req, res) => { + res.status(200).send('Hello, world!').end(); +}); + +// Start the server +const PORT = process.env.PORT || 8080; +app.listen(PORT, () => { + console.log(`App listening on port ${PORT}`); + console.log('Press Ctrl+C to quit.'); +}); +// [END app] diff --git a/appengine-standard/hello-world/app.yaml b/appengine-standard/hello-world/app.yaml new file mode 100644 index 0000000000..8f8f6fd97a --- /dev/null +++ b/appengine-standard/hello-world/app.yaml @@ -0,0 +1,16 @@ +# Copyright 2017, Google, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app_yaml] +runtime: nodejs8 +# [END app_yaml] diff --git a/appengine-standard/hello-world/package.json b/appengine-standard/hello-world/package.json new file mode 100644 index 0000000000..97d71af4d3 --- /dev/null +++ b/appengine-standard/hello-world/package.json @@ -0,0 +1,39 @@ +{ + "name": "appengine-hello-world", + "description": "Simple Hello World Node.js sample for Google App Engine Standard Environment.", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google Inc.", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": "8.x.x" + }, + "scripts": { + "deploy": "gcloud app deploy", + "start": "node app.js", + "lint": "samples lint", + "pretest": "npm run lint", + "system-test": "samples test app", + "test": "npm run system-test", + "e2e-test": "samples test deploy" + }, + "dependencies": { + "express": "4.15.4" + }, + "devDependencies": { + "@google-cloud/nodejs-repo-tools": "1.4.17" + }, + "cloud-repo-tools": { + "test": { + "app": { + "msg": "Hello, world!" + } + }, + "requiresKeyFile": true, + "requiresProjectId": true + } +} diff --git a/appengine-standard/loopback/.editorconfig b/appengine-standard/loopback/.editorconfig new file mode 100644 index 0000000000..3ee22e5d3d --- /dev/null +++ b/appengine-standard/loopback/.editorconfig @@ -0,0 +1,13 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/appengine-standard/loopback/.eslintignore b/appengine-standard/loopback/.eslintignore new file mode 100644 index 0000000000..44f397018c --- /dev/null +++ b/appengine-standard/loopback/.eslintignore @@ -0,0 +1 @@ +/client/ \ No newline at end of file diff --git a/appengine-standard/loopback/.eslintrc b/appengine-standard/loopback/.eslintrc new file mode 100644 index 0000000000..a6e52975a0 --- /dev/null +++ b/appengine-standard/loopback/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "loopback" +} \ No newline at end of file diff --git a/appengine-standard/loopback/.gitignore b/appengine-standard/loopback/.gitignore new file mode 100644 index 0000000000..aff1045b47 --- /dev/null +++ b/appengine-standard/loopback/.gitignore @@ -0,0 +1,19 @@ +*.csv +*.dat +*.iml +*.log +*.out +*.pid +*.seed +*.sublime-* +*.swo +*.swp +*.tgz +*.xml +.DS_Store +.idea +.project +.strong-pm +coverage +node_modules +npm-debug.log diff --git a/appengine-standard/loopback/.yo-rc.json b/appengine-standard/loopback/.yo-rc.json new file mode 100644 index 0000000000..02f3fc17be --- /dev/null +++ b/appengine-standard/loopback/.yo-rc.json @@ -0,0 +1,3 @@ +{ + "generator-loopback": {} +} \ No newline at end of file diff --git a/appengine-standard/loopback/README.md b/appengine-standard/loopback/README.md new file mode 100644 index 0000000000..4830075103 --- /dev/null +++ b/appengine-standard/loopback/README.md @@ -0,0 +1,8 @@ +## Loopback.js on Google App Engine + +> [Loopback][1] is a highly-extensible, open-source Node.js framework. + +You can also read the [Loopback.js documentation][2]. + +[1]: http://loopback.io/ +[2]: http://loopback.io/getting-started/ diff --git a/appengine-standard/loopback/app.yaml b/appengine-standard/loopback/app.yaml new file mode 100644 index 0000000000..8cf0f07dc8 --- /dev/null +++ b/appengine-standard/loopback/app.yaml @@ -0,0 +1,16 @@ +# Copyright 2015-2016, Google, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app_yaml] +runtime: nodejs8 +# [END app_yaml] diff --git a/appengine-standard/loopback/client/README.md b/appengine-standard/loopback/client/README.md new file mode 100644 index 0000000000..dd00c9e9cd --- /dev/null +++ b/appengine-standard/loopback/client/README.md @@ -0,0 +1,3 @@ +## Client + +This is the place for your application front-end files. diff --git a/appengine-standard/loopback/common/models/message.js b/appengine-standard/loopback/common/models/message.js new file mode 100644 index 0000000000..8bbb45691e --- /dev/null +++ b/appengine-standard/loopback/common/models/message.js @@ -0,0 +1,8 @@ +module.exports = function (Message) { + Message.greet = function (msg, cb) { + process.nextTick(function () { + msg = msg || 'hello'; + cb(null, 'Sender says ' + msg + ' to receiver'); + }); + }; +}; diff --git a/appengine-standard/loopback/common/models/message.json b/appengine-standard/loopback/common/models/message.json new file mode 100644 index 0000000000..4edf66b283 --- /dev/null +++ b/appengine-standard/loopback/common/models/message.json @@ -0,0 +1,24 @@ +{ + "name": "Message", + "base": "Model", + "properties": {}, + "methods": { + "greet": { + "isStatic": true, + "accepts": [{ + "arg": "msg", + "type": "string", + "http": { + "source": "query" + } + }], + "returns": { + "arg": "greeting", + "type": "string" + }, + "http": { + "verb": "get" + } + } + } +} diff --git a/appengine-standard/loopback/package.json b/appengine-standard/loopback/package.json new file mode 100644 index 0000000000..5107dfcfc2 --- /dev/null +++ b/appengine-standard/loopback/package.json @@ -0,0 +1,33 @@ +{ + "name": "appengine-loopback", + "description": "An example of running Loopback.js on Google App Engine.", + "version": "0.0.1", + "private": true, + "license": "Apache Version 2.0", + "author": "Google Inc.", + "engines": { + "node": "8.x.x" + }, + "main": "server/server.js", + "scripts": { + "lint": "eslint .", + "start": "node .", + "posttest": "npm run lint && nsp check" + }, + "dependencies": { + "compression": "1.0.3", + "cors": "2.5.2", + "helmet": "1.3.0", + "loopback-boot": "2.6.5", + "loopback-component-explorer": "2.4.0", + "serve-favicon": "2.3.2", + "strong-error-handler": "1.0.1", + "loopback-datasource-juggler": "2.39.0", + "loopback": "2.22.0" + }, + "devDependencies": { + "eslint": "2.13.1", + "eslint-config-loopback": "4.0.0", + "nsp": "2.1.0" + } +} diff --git a/appengine-standard/loopback/server/boot/authentication.js b/appengine-standard/loopback/server/boot/authentication.js new file mode 100644 index 0000000000..c2d8250e2f --- /dev/null +++ b/appengine-standard/loopback/server/boot/authentication.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = function enableAuthentication (server) { + // enable authentication + server.enableAuth(); +}; diff --git a/appengine-standard/loopback/server/boot/root.js b/appengine-standard/loopback/server/boot/root.js new file mode 100644 index 0000000000..26301ef2c3 --- /dev/null +++ b/appengine-standard/loopback/server/boot/root.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = function (server) { + // Install a `/` route that returns server status + var router = server.loopback.Router(); + router.get('/', server.loopback.status()); + server.use(router); +}; diff --git a/appengine-standard/loopback/server/component-config.json b/appengine-standard/loopback/server/component-config.json new file mode 100644 index 0000000000..f36959a488 --- /dev/null +++ b/appengine-standard/loopback/server/component-config.json @@ -0,0 +1,5 @@ +{ + "loopback-component-explorer": { + "mountPath": "/explorer" + } +} diff --git a/appengine-standard/loopback/server/config.json b/appengine-standard/loopback/server/config.json new file mode 100644 index 0000000000..40d45f4d63 --- /dev/null +++ b/appengine-standard/loopback/server/config.json @@ -0,0 +1,23 @@ +{ + "restApiRoot": "/api", + "host": "0.0.0.0", + "port": 3000, + "remoting": { + "context": false, + "rest": { + "normalizeHttpPath": false, + "xml": false + }, + "json": { + "strict": false, + "limit": "100kb" + }, + "urlencoded": { + "extended": true, + "limit": "100kb" + }, + "cors": false, + "handleErrors": false + }, + "legacyExplorer": false +} diff --git a/appengine-standard/loopback/server/datasources.json b/appengine-standard/loopback/server/datasources.json new file mode 100644 index 0000000000..d6caf56d44 --- /dev/null +++ b/appengine-standard/loopback/server/datasources.json @@ -0,0 +1,6 @@ +{ + "db": { + "name": "db", + "connector": "memory" + } +} diff --git a/appengine-standard/loopback/server/middleware.development.json b/appengine-standard/loopback/server/middleware.development.json new file mode 100644 index 0000000000..071c11a30e --- /dev/null +++ b/appengine-standard/loopback/server/middleware.development.json @@ -0,0 +1,10 @@ +{ + "final:after": { + "strong-error-handler": { + "params": { + "debug": true, + "log": true + } + } + } +} diff --git a/appengine-standard/loopback/server/middleware.json b/appengine-standard/loopback/server/middleware.json new file mode 100644 index 0000000000..fbfff8165d --- /dev/null +++ b/appengine-standard/loopback/server/middleware.json @@ -0,0 +1,50 @@ +{ + "initial:before": { + "loopback#favicon": {} + }, + "initial": { + "compression": {}, + "cors": { + "params": { + "origin": true, + "credentials": true, + "maxAge": 86400 + } + }, + "helmet#xssFilter": {}, + "helmet#frameguard": { + "params": [ + "deny" + ] + }, + "helmet#hsts": { + "params": { + "maxAge": 0, + "includeSubdomains": true + } + }, + "helmet#hidePoweredBy": {}, + "helmet#ieNoOpen": {}, + "helmet#noSniff": {}, + "helmet#noCache": { + "enabled": false + } + }, + "session": {}, + "auth": {}, + "parse": {}, + "routes": { + "loopback#rest": { + "paths": [ + "${restApiRoot}" + ] + } + }, + "files": {}, + "final": { + "loopback#urlNotFound": {} + }, + "final:after": { + "strong-error-handler": {} + } +} diff --git a/appengine-standard/loopback/server/model-config.json b/appengine-standard/loopback/server/model-config.json new file mode 100644 index 0000000000..30be974de8 --- /dev/null +++ b/appengine-standard/loopback/server/model-config.json @@ -0,0 +1,38 @@ +{ + "_meta": { + "sources": [ + "loopback/common/models", + "loopback/server/models", + "../common/models", + "./models" + ], + "mixins": [ + "loopback/common/mixins", + "loopback/server/mixins", + "../common/mixins", + "./mixins" + ] + }, + "User": { + "dataSource": "db" + }, + "AccessToken": { + "dataSource": "db", + "public": false + }, + "ACL": { + "dataSource": "db", + "public": false + }, + "RoleMapping": { + "dataSource": "db", + "public": false + }, + "Role": { + "dataSource": "db", + "public": false + }, + "Message": { + "dataSource": null + } +} diff --git a/appengine-standard/loopback/server/server.js b/appengine-standard/loopback/server/server.js new file mode 100644 index 0000000000..f7bd8efdc9 --- /dev/null +++ b/appengine-standard/loopback/server/server.js @@ -0,0 +1,30 @@ +'use strict'; + +var loopback = require('loopback'); +var boot = require('loopback-boot'); + +var app = module.exports = loopback(); + +app.start = function () { + // start the web server + return app.listen(function () { + app.emit('started'); + var baseUrl = app.get('url').replace(/\/$/, ''); + console.log('Web server listening at: %s', baseUrl); + if (app.get('loopback-component-explorer')) { + var explorerPath = app.get('loopback-component-explorer').mountPath; + console.log('Browse your REST API at %s%s', baseUrl, explorerPath); + } + }); +}; + +// Bootstrap the application, configure models, datasources and middleware. +// Sub-apps like REST API are mounted via boot scripts. +boot(app, __dirname, function (err) { + if (err) throw err; + + // start the server if `$ node server.js` + if (require.main === module) { + app.start(); + } +}); diff --git a/appengine-standard/mailjet/README.md b/appengine-standard/mailjet/README.md new file mode 100644 index 0000000000..be4567a852 --- /dev/null +++ b/appengine-standard/mailjet/README.md @@ -0,0 +1,16 @@ +## Express.js + Mailjet on Google App Engine + +> [Mailjet][1] is an all-in-one email service provider. +> +> – www.mailjet.com + +This sample application demonstrates how to use [Express.js][2] and +[node-mailjet][3] to send transactional email on [Google App Engine][4]. + +You can also read the [Mailjet documentation][5]. + +[1]: https://www.mailjet.com/ +[2]: http://expressjs.com +[3]: https://github.com/mailjet/mailjet-apiv3-nodejs +[4]: https://cloud.google.com/appengine +[5]: https://dev.mailjet.com/ diff --git a/appengine-standard/mailjet/app.js b/appengine-standard/mailjet/app.js new file mode 100644 index 0000000000..00740db3f8 --- /dev/null +++ b/appengine-standard/mailjet/app.js @@ -0,0 +1,81 @@ +/** + * Copyright 2016, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var express = require('express'); +var path = require('path'); +var bodyParser = require('body-parser'); + +// [START setup] +var Mailjet = require('node-mailjet').connect( + process.env.MJ_APIKEY_PUBLIC, + process.env.MJ_APIKEY_PRIVATE +); +// [END setup] + +var app = express(); + +// Setup view engine +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'jade'); + +// Parse form data +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); + +// [START index] +app.get('/', function (req, res) { + res.render('index'); +}); +// [END index] + +// [START hello] +app.post('/hello', function (req, res, next) { + var options = { + // From + FromEmail: 'no-reply@appengine-mailjet-demo.com', + FromName: 'Mailjet Demo', + // To + Recipients: [{ Email: req.body.email }], + // Subject + Subject: 'Hello World!', + // Body + 'Text-part': 'Mailjet on Google App Engine with Node.js', + 'Html-part': '