From 9f29eb79329ae1663226a4871cf85985ef8a2bcc Mon Sep 17 00:00:00 2001 From: ace-n Date: Thu, 29 Aug 2019 13:23:07 -0700 Subject: [PATCH 1/3] Remove unused datastore sample --- functions/datastore/README.md | 83 ------- functions/datastore/index.js | 140 ----------- functions/datastore/package.json | 35 --- functions/datastore/test/.eslintrc.yml | 4 - functions/datastore/test/index.test.js | 308 ------------------------- 5 files changed, 570 deletions(-) delete mode 100644 functions/datastore/README.md delete mode 100644 functions/datastore/index.js delete mode 100644 functions/datastore/package.json delete mode 100644 functions/datastore/test/.eslintrc.yml delete mode 100644 functions/datastore/test/index.test.js diff --git a/functions/datastore/README.md b/functions/datastore/README.md deleted file mode 100644 index 530a9881fe..0000000000 --- a/functions/datastore/README.md +++ /dev/null @@ -1,83 +0,0 @@ -Google Cloud Platform logo - -# Google Cloud Functions Cloud Datastore sample - -This recipe shows you how to read and write an entity in Cloud Datastore from a -Cloud Function. - -View the [source code][code]. - -[code]: index.js - -## Deploy and Test - -1. Follow the [Cloud Functions quickstart guide][quickstart] to setup Cloud -Functions for your project. - -1. Clone this repository: - - git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git - cd nodejs-docs-samples/functions/datastore - -1. Ensure the Cloud Datastore API is enabled: - - [Click here to enable the Cloud Datastore API](https://console.cloud.google.com/flows/enableapi?apiid=datastore.googleapis.com&redirect=https://github.com/GoogleCloudPlatform/nodejs-docs-samples/tree/master/functions/datastore) - -1. Deploy the "get" function with an HTTP trigger: - - gcloud functions deploy get --trigger-http - -1. Deploy the "set" function with an HTTP trigger: - - gcloud functions deploy set --trigger-http - -1. Deploy the "del" function with an HTTP trigger: - - gcloud functions deploy del --trigger-http - -1. Call the "set" function to create a new entity: - - gcloud functions call set --data '{"kind":"Task","key":"sampletask1","value":{"description":"Buy milk"}}' - - or - - curl -H "Content-Type: application/json" -X POST -d '{"kind":"Task","key":"sampletask1","value":{"description":"Buy milk"}}' "https://[YOUR_REGION]-[YOUR_PROJECT_ID].cloudfunctions.net/set" - - * Replace `[YOUR_REGION]` with the region where your function is deployed. - * Replace `[YOUR_PROJECT_ID]` with your Google Cloud Platform project ID. - -1. Call the "get" function to read the newly created entity: - - gcloud functions call get --data '{"kind":"Task","key":"sampletask1"}' - - or - - curl -H "Content-Type: application/json" -X POST -d '{"kind":"Task","key":"sampletask1"}' "https://[YOUR_REGION]-[YOUR_PROJECT_ID].cloudfunctions.net/get" - - * Replace `[YOUR_REGION]` with the region where your function is deployed. - * Replace `[YOUR_PROJECT_ID]` with your Google Cloud Platform project ID. - -1. Call the "del" function to delete the entity: - - gcloud alpha functions call del --data '{"kind":"Task","key":"sampletask1"}' - - or - - curl -H "Content-Type: application/json" -X POST -d '{"kind":"Task","key":"sampletask1"}' "https://[YOUR_REGION]-[YOUR_PROJECT_ID].cloudfunctions.net/del" - - * Replace `[YOUR_REGION]` with the region where your function is deployed. - * Replace `[YOUR_PROJECT_ID]` with your Google Cloud Platform project ID. - -1. Call the "get" function again to verify it was deleted: - - gcloud functions call get --data '{"kind":"Task","key":"sampletask1"}' - - or - - curl -H "Content-Type: application/json" -X POST -d '{"kind":"Task","key":"sampletask1"}' "https://[YOUR_REGION]-[YOUR_PROJECT_ID].cloudfunctions.net/get" - - * Replace `[YOUR_REGION]` with the region where your function is deployed. - * Replace `[YOUR_PROJECT_ID]` with your Google Cloud Platform project ID. - - -[quickstart]: https://cloud.google.com/functions/quickstart diff --git a/functions/datastore/index.js b/functions/datastore/index.js deleted file mode 100644 index e836c8eb72..0000000000 --- a/functions/datastore/index.js +++ /dev/null @@ -1,140 +0,0 @@ -/** - * 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'; - -const {Datastore} = require('@google-cloud/datastore'); - -// Instantiates a client -const datastore = new Datastore(); - -const makeErrorObj = prop => { - return new Error( - `${prop} not provided. Make sure you have a "${prop.toLowerCase()}" property in your request` - ); -}; - -/** - * Gets a Datastore key from the kind/key pair in the request. - * - * @param {object} requestData Cloud Function request data. - * @param {string} requestData.key Datastore key string. - * @param {string} requestData.kind Datastore kind. - * @returns {object} Datastore key object. - */ -const getKeyFromRequestData = requestData => { - if (!requestData.key) { - return Promise.reject(makeErrorObj('Key')); - } - - if (!requestData.kind) { - return Promise.reject(makeErrorObj('Kind')); - } - - return datastore.key([requestData.kind, requestData.key]); -}; - -/** - * Creates and/or updates a record. - * - * @example - * gcloud functions call set --data '{"kind":"Task","key":"sampletask1","value":{"description": "Buy milk"}}' - * - * @param {object} req Cloud Function request context. - * @param {object} req.body The request body. - * @param {string} req.body.kind The Datastore kind of the data to save, e.g. "Task". - * @param {string} req.body.key Key at which to save the data, e.g. "sampletask1". - * @param {object} req.body.value Value to save to Cloud Datastore, e.g. {"description":"Buy milk"} - * @param {object} res Cloud Function response context. - */ -exports.set = async (req, res) => { - // The value contains a JSON document representing the entity we want to save - if (!req.body.value) { - const err = makeErrorObj('Value'); - console.error(err); - res.status(500).send(err.message); - return; - } - - try { - const key = await getKeyFromRequestData(req.body); - const entity = { - key: key, - data: req.body.value, - }; - - await datastore.save(entity); - res.status(200).send(`Entity ${key.path.join('/')} saved.`); - } catch (err) { - console.error(new Error(err.message)); // Add to Stackdriver Error Reporting - res.status(500).send(err.message); - } -}; - -/** - * Retrieves a record. - * - * @example - * gcloud functions call get --data '{"kind":"Task","key":"sampletask1"}' - * - * @param {object} req Cloud Function request context. - * @param {object} req.body The request body. - * @param {string} req.body.kind The Datastore kind of the data to retrieve, e.g. "Task". - * @param {string} req.body.key Key at which to retrieve the data, e.g. "sampletask1". - * @param {object} res Cloud Function response context. - */ -exports.get = async (req, res) => { - try { - const key = await getKeyFromRequestData(req.body); - const [entity] = await datastore.get(key); - - // The get operation returns an empty dictionary for non-existent entities - // We want to throw an error instead - if (!entity) { - throw new Error(`No entity found for key ${key.path.join('/')}.`); - } - - res.status(200).send(entity); - } catch (err) { - console.error(new Error(err.message)); // Add to Stackdriver Error Reporting - res.status(500).send(err.message); - } -}; - -/** - * Deletes a record. - * - * @example - * gcloud functions call del --data '{"kind":"Task","key":"sampletask1"}' - * - * @param {object} req Cloud Function request context. - * @param {object} req.body The request body. - * @param {string} req.body.kind The Datastore kind of the data to delete, e.g. "Task". - * @param {string} req.body.key Key at which to delete data, e.g. "sampletask1". - * @param {object} res Cloud Function response context. - */ -exports.del = async (req, res) => { - // Deletes the entity - // The delete operation will not fail for a non-existent entity, it just - // doesn't delete anything - try { - const key = await getKeyFromRequestData(req.body); - await datastore.delete(key); - res.status(200).send(`Entity ${key.path.join('/')} deleted.`); - } catch (err) { - console.error(new Error(err.message)); // Add to Stackdriver Error Reporting - res.status(500).send(err.message); - } -}; diff --git a/functions/datastore/package.json b/functions/datastore/package.json deleted file mode 100644 index e5ecc5999a..0000000000 --- a/functions/datastore/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "nodejs-docs-samples-functions-datastore", - "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.0.0" - }, - "scripts": { - "test": "mocha test/*.test.js --timeout=5000" - }, - "dependencies": { - "@google-cloud/datastore": "^4.1.1", - "supertest": "^4.0.0" - }, - "devDependencies": { - "@google-cloud/functions-framework": "^1.1.1", - "child-process-promise": "^2.2.1", - "mocha": "^6.0.0", - "proxyquire": "^2.1.0", - "request": "^2.88.0", - "requestretry": "^4.0.0", - "sinon": "^7.2.7", - "uuid": "^3.3.2" - }, - "cloud-repo-tools": { - "requiresKeyFile": true, - "requiresProjectId": true - } -} diff --git a/functions/datastore/test/.eslintrc.yml b/functions/datastore/test/.eslintrc.yml deleted file mode 100644 index 07583b99db..0000000000 --- a/functions/datastore/test/.eslintrc.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -rules: - no-warning-comments: off - diff --git a/functions/datastore/test/index.test.js b/functions/datastore/test/index.test.js deleted file mode 100644 index ba72120c88..0000000000 --- a/functions/datastore/test/index.test.js +++ /dev/null @@ -1,308 +0,0 @@ -/** - * 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 assert = require('assert'); -const {Datastore} = require('@google-cloud/datastore'); -const datastore = new Datastore(); -const program = require('../'); -const uuid = require('uuid'); -const path = require('path'); -const execPromise = require('child-process-promise').exec; -const sinon = require('sinon'); - -const FF_TIMEOUT = 3000; - -let requestRetry = require('requestretry'); -requestRetry = requestRetry.defaults({ - retryDelay: 500, - retryStrategy: requestRetry.RetryStrategies.NetworkError, -}); - -const cwd = path.join(__dirname, '..'); - -const NAME = 'sampletask1'; -const KIND = `Task-${uuid.v4()}`; -const VALUE = { - description: 'Buy milk', -}; - -const errorMsg = msg => - `${msg} not provided. Make sure you have a "${msg.toLowerCase()}" property in your request`; - -const handleLinuxFailures = async proc => { - try { - return await proc; - } catch (err) { - // Timeouts always cause errors on Linux, so catch them - // Don't return proc, as await-ing it re-throws the error - if (!err.name || err.name !== 'ChildProcessError') { - throw err; - } - } -}; - -describe('functions/datastore', () => { - describe('set', () => { - let ffProc; - const PORT = 8080; - const BASE_URL = `http://localhost:${PORT}`; - - before(() => { - ffProc = execPromise( - `functions-framework --target=set --signature-type=http --port=${PORT}`, - {timeout: FF_TIMEOUT, shell: true, cwd} - ); - }); - - after(async () => { - await handleLinuxFailures(ffProc); - }); - - it('set: Fails without a value', async () => { - const req = { - body: {}, - }; - const res = { - status: sinon.stub().returnsThis(), - send: sinon.stub(), - }; - - await program.set(req, res); - - assert.ok(res.status.calledWith(500)); - assert.ok(res.send.calledWith(errorMsg('Value'))); - }); - - it('set: Fails without a key', async () => { - const req = { - body: { - value: VALUE, - }, - }; - const res = { - status: sinon.stub().returnsThis(), - send: sinon.stub(), - }; - - await program.set(req, res); - - assert.ok(res.status.calledWith(500)); - assert.ok(res.send.calledWith(errorMsg('Key'))); - }); - - it('set: Fails without a kind', async () => { - const req = { - body: { - key: NAME, - value: VALUE, - }, - }; - const res = { - status: sinon.stub().returnsThis(), - send: sinon.stub(), - }; - - await program.set(req, res); - - assert.ok(res.status.calledWith(500)); - assert.ok(res.send.calledWith(errorMsg('Kind'))); - }); - - it('set: Saves an entity', async () => { - const response = await requestRetry({ - url: `${BASE_URL}/set`, - method: 'POST', - body: { - kind: KIND, - key: NAME, - value: VALUE, - }, - json: true, - }); - - assert.strictEqual(response.statusCode, 200); - assert.ok(response.body.includes(`Entity ${KIND}/${NAME} saved`)); - }); - }); - - describe('get', () => { - let ffProc; - const PORT = 8081; - const BASE_URL = `http://localhost:${PORT}`; - - before(() => { - ffProc = execPromise( - `functions-framework --target=get --signature-type=http --port=${PORT}`, - {timeout: FF_TIMEOUT, shell: true, cwd} - ); - }); - - after(async () => { - await handleLinuxFailures(ffProc); - }); - - it('get: Fails when entity does not exist', async () => { - const response = await requestRetry({ - url: `${BASE_URL}/get`, - method: 'POST', - body: { - kind: KIND, - key: 'nonexistent', - }, - json: true, - }); - - assert.strictEqual(response.statusCode, 500); - assert.ok( - new RegExp( - /(Missing or insufficient permissions.)|(No entity found for key)/ - ).test(response.body) - ); - }); - - it('get: Finds an entity', async () => { - const response = await requestRetry({ - method: 'POST', - url: `${BASE_URL}/get`, - body: { - kind: KIND, - key: NAME, - }, - json: true, - }); - - assert.strictEqual(response.statusCode, 200); - assert.deepStrictEqual(response.body, { - description: 'Buy milk', - }); - }); - - it('get: Fails without a key', async () => { - const req = { - body: {}, - }; - const res = { - status: sinon.stub().returnsThis(), - send: sinon.stub(), - }; - - await program.get(req, res); - - assert.ok(res.status.calledWith(500)); - assert.ok(res.send.calledWith(errorMsg('Key'))); - }); - - it('get: Fails without a kind', async () => { - const req = { - body: { - key: NAME, - }, - }; - const res = { - status: sinon.stub().returnsThis(), - send: sinon.stub(), - }; - - await program.get(req, res); - - assert.ok(res.status.calledWith(500)); - assert.ok(res.send.calledWith(errorMsg('Kind'))); - }); - }); - - describe('del', () => { - let ffProc; - const PORT = 8082; - const BASE_URL = `http://localhost:${PORT}`; - - before(() => { - ffProc = execPromise( - `functions-framework --target=del --signature-type=http --port=${PORT}`, - {timeout: FF_TIMEOUT, shell: true, cwd} - ); - }); - - after(async () => { - await handleLinuxFailures(ffProc); - }); - - it('del: Fails without a key', async () => { - const req = { - body: {}, - }; - const res = { - status: sinon.stub().returnsThis(), - send: sinon.stub(), - }; - - await program.del(req, res); - - assert.ok(res.status.calledWith(500)); - assert.ok(res.send.calledWith(errorMsg('Key'))); - }); - - it('del: Fails without a kind', async () => { - const req = { - body: { - key: NAME, - }, - }; - const res = { - status: sinon.stub().returnsThis(), - send: sinon.stub(), - }; - - await program.del(req, res); - - assert.ok(res.status.calledWith(500)); - assert.ok(res.send.calledWith(errorMsg('Kind'))); - }); - - it(`del: Doesn't fail when entity does not exist`, async () => { - const response = await requestRetry({ - method: 'POST', - url: `${BASE_URL}/del`, - body: { - kind: KIND, - key: 'nonexistent', - }, - json: true, - }); - - assert.strictEqual(response.statusCode, 200); - assert.strictEqual(response.body, `Entity ${KIND}/nonexistent deleted.`); - }); - - it('del: Deletes an entity', async () => { - const response = await requestRetry({ - method: 'POST', - url: `${BASE_URL}/del`, - body: { - kind: KIND, - key: NAME, - }, - json: true, - }); - assert.strictEqual(response.statusCode, 200); - assert.strictEqual(response.body, `Entity ${KIND}/${NAME} deleted.`); - - const key = datastore.key([KIND, NAME]); - const [entity] = await datastore.get(key); - assert.ok(!entity); - }); - }); -}); From 8c96fca02fed1bac5de48daae0859e10a51363be Mon Sep 17 00:00:00 2001 From: ace-n Date: Thu, 29 Aug 2019 13:58:41 -0700 Subject: [PATCH 2/3] Update README + move datastore instead of deleting --- datastore/functions/README.md | 83 +++++++ datastore/functions/index.js | 140 +++++++++++ datastore/functions/package.json | 35 +++ datastore/functions/test/.eslintrc.yml | 4 + datastore/functions/test/index.test.js | 308 +++++++++++++++++++++++++ functions/README.md | 1 - 6 files changed, 570 insertions(+), 1 deletion(-) create mode 100644 datastore/functions/README.md create mode 100644 datastore/functions/index.js create mode 100644 datastore/functions/package.json create mode 100644 datastore/functions/test/.eslintrc.yml create mode 100644 datastore/functions/test/index.test.js diff --git a/datastore/functions/README.md b/datastore/functions/README.md new file mode 100644 index 0000000000..530a9881fe --- /dev/null +++ b/datastore/functions/README.md @@ -0,0 +1,83 @@ +Google Cloud Platform logo + +# Google Cloud Functions Cloud Datastore sample + +This recipe shows you how to read and write an entity in Cloud Datastore from a +Cloud Function. + +View the [source code][code]. + +[code]: index.js + +## Deploy and Test + +1. Follow the [Cloud Functions quickstart guide][quickstart] to setup Cloud +Functions for your project. + +1. Clone this repository: + + git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git + cd nodejs-docs-samples/functions/datastore + +1. Ensure the Cloud Datastore API is enabled: + + [Click here to enable the Cloud Datastore API](https://console.cloud.google.com/flows/enableapi?apiid=datastore.googleapis.com&redirect=https://github.com/GoogleCloudPlatform/nodejs-docs-samples/tree/master/functions/datastore) + +1. Deploy the "get" function with an HTTP trigger: + + gcloud functions deploy get --trigger-http + +1. Deploy the "set" function with an HTTP trigger: + + gcloud functions deploy set --trigger-http + +1. Deploy the "del" function with an HTTP trigger: + + gcloud functions deploy del --trigger-http + +1. Call the "set" function to create a new entity: + + gcloud functions call set --data '{"kind":"Task","key":"sampletask1","value":{"description":"Buy milk"}}' + + or + + curl -H "Content-Type: application/json" -X POST -d '{"kind":"Task","key":"sampletask1","value":{"description":"Buy milk"}}' "https://[YOUR_REGION]-[YOUR_PROJECT_ID].cloudfunctions.net/set" + + * Replace `[YOUR_REGION]` with the region where your function is deployed. + * Replace `[YOUR_PROJECT_ID]` with your Google Cloud Platform project ID. + +1. Call the "get" function to read the newly created entity: + + gcloud functions call get --data '{"kind":"Task","key":"sampletask1"}' + + or + + curl -H "Content-Type: application/json" -X POST -d '{"kind":"Task","key":"sampletask1"}' "https://[YOUR_REGION]-[YOUR_PROJECT_ID].cloudfunctions.net/get" + + * Replace `[YOUR_REGION]` with the region where your function is deployed. + * Replace `[YOUR_PROJECT_ID]` with your Google Cloud Platform project ID. + +1. Call the "del" function to delete the entity: + + gcloud alpha functions call del --data '{"kind":"Task","key":"sampletask1"}' + + or + + curl -H "Content-Type: application/json" -X POST -d '{"kind":"Task","key":"sampletask1"}' "https://[YOUR_REGION]-[YOUR_PROJECT_ID].cloudfunctions.net/del" + + * Replace `[YOUR_REGION]` with the region where your function is deployed. + * Replace `[YOUR_PROJECT_ID]` with your Google Cloud Platform project ID. + +1. Call the "get" function again to verify it was deleted: + + gcloud functions call get --data '{"kind":"Task","key":"sampletask1"}' + + or + + curl -H "Content-Type: application/json" -X POST -d '{"kind":"Task","key":"sampletask1"}' "https://[YOUR_REGION]-[YOUR_PROJECT_ID].cloudfunctions.net/get" + + * Replace `[YOUR_REGION]` with the region where your function is deployed. + * Replace `[YOUR_PROJECT_ID]` with your Google Cloud Platform project ID. + + +[quickstart]: https://cloud.google.com/functions/quickstart diff --git a/datastore/functions/index.js b/datastore/functions/index.js new file mode 100644 index 0000000000..e836c8eb72 --- /dev/null +++ b/datastore/functions/index.js @@ -0,0 +1,140 @@ +/** + * 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'; + +const {Datastore} = require('@google-cloud/datastore'); + +// Instantiates a client +const datastore = new Datastore(); + +const makeErrorObj = prop => { + return new Error( + `${prop} not provided. Make sure you have a "${prop.toLowerCase()}" property in your request` + ); +}; + +/** + * Gets a Datastore key from the kind/key pair in the request. + * + * @param {object} requestData Cloud Function request data. + * @param {string} requestData.key Datastore key string. + * @param {string} requestData.kind Datastore kind. + * @returns {object} Datastore key object. + */ +const getKeyFromRequestData = requestData => { + if (!requestData.key) { + return Promise.reject(makeErrorObj('Key')); + } + + if (!requestData.kind) { + return Promise.reject(makeErrorObj('Kind')); + } + + return datastore.key([requestData.kind, requestData.key]); +}; + +/** + * Creates and/or updates a record. + * + * @example + * gcloud functions call set --data '{"kind":"Task","key":"sampletask1","value":{"description": "Buy milk"}}' + * + * @param {object} req Cloud Function request context. + * @param {object} req.body The request body. + * @param {string} req.body.kind The Datastore kind of the data to save, e.g. "Task". + * @param {string} req.body.key Key at which to save the data, e.g. "sampletask1". + * @param {object} req.body.value Value to save to Cloud Datastore, e.g. {"description":"Buy milk"} + * @param {object} res Cloud Function response context. + */ +exports.set = async (req, res) => { + // The value contains a JSON document representing the entity we want to save + if (!req.body.value) { + const err = makeErrorObj('Value'); + console.error(err); + res.status(500).send(err.message); + return; + } + + try { + const key = await getKeyFromRequestData(req.body); + const entity = { + key: key, + data: req.body.value, + }; + + await datastore.save(entity); + res.status(200).send(`Entity ${key.path.join('/')} saved.`); + } catch (err) { + console.error(new Error(err.message)); // Add to Stackdriver Error Reporting + res.status(500).send(err.message); + } +}; + +/** + * Retrieves a record. + * + * @example + * gcloud functions call get --data '{"kind":"Task","key":"sampletask1"}' + * + * @param {object} req Cloud Function request context. + * @param {object} req.body The request body. + * @param {string} req.body.kind The Datastore kind of the data to retrieve, e.g. "Task". + * @param {string} req.body.key Key at which to retrieve the data, e.g. "sampletask1". + * @param {object} res Cloud Function response context. + */ +exports.get = async (req, res) => { + try { + const key = await getKeyFromRequestData(req.body); + const [entity] = await datastore.get(key); + + // The get operation returns an empty dictionary for non-existent entities + // We want to throw an error instead + if (!entity) { + throw new Error(`No entity found for key ${key.path.join('/')}.`); + } + + res.status(200).send(entity); + } catch (err) { + console.error(new Error(err.message)); // Add to Stackdriver Error Reporting + res.status(500).send(err.message); + } +}; + +/** + * Deletes a record. + * + * @example + * gcloud functions call del --data '{"kind":"Task","key":"sampletask1"}' + * + * @param {object} req Cloud Function request context. + * @param {object} req.body The request body. + * @param {string} req.body.kind The Datastore kind of the data to delete, e.g. "Task". + * @param {string} req.body.key Key at which to delete data, e.g. "sampletask1". + * @param {object} res Cloud Function response context. + */ +exports.del = async (req, res) => { + // Deletes the entity + // The delete operation will not fail for a non-existent entity, it just + // doesn't delete anything + try { + const key = await getKeyFromRequestData(req.body); + await datastore.delete(key); + res.status(200).send(`Entity ${key.path.join('/')} deleted.`); + } catch (err) { + console.error(new Error(err.message)); // Add to Stackdriver Error Reporting + res.status(500).send(err.message); + } +}; diff --git a/datastore/functions/package.json b/datastore/functions/package.json new file mode 100644 index 0000000000..e5ecc5999a --- /dev/null +++ b/datastore/functions/package.json @@ -0,0 +1,35 @@ +{ + "name": "nodejs-docs-samples-functions-datastore", + "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.0.0" + }, + "scripts": { + "test": "mocha test/*.test.js --timeout=5000" + }, + "dependencies": { + "@google-cloud/datastore": "^4.1.1", + "supertest": "^4.0.0" + }, + "devDependencies": { + "@google-cloud/functions-framework": "^1.1.1", + "child-process-promise": "^2.2.1", + "mocha": "^6.0.0", + "proxyquire": "^2.1.0", + "request": "^2.88.0", + "requestretry": "^4.0.0", + "sinon": "^7.2.7", + "uuid": "^3.3.2" + }, + "cloud-repo-tools": { + "requiresKeyFile": true, + "requiresProjectId": true + } +} diff --git a/datastore/functions/test/.eslintrc.yml b/datastore/functions/test/.eslintrc.yml new file mode 100644 index 0000000000..07583b99db --- /dev/null +++ b/datastore/functions/test/.eslintrc.yml @@ -0,0 +1,4 @@ +--- +rules: + no-warning-comments: off + diff --git a/datastore/functions/test/index.test.js b/datastore/functions/test/index.test.js new file mode 100644 index 0000000000..ba72120c88 --- /dev/null +++ b/datastore/functions/test/index.test.js @@ -0,0 +1,308 @@ +/** + * 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 assert = require('assert'); +const {Datastore} = require('@google-cloud/datastore'); +const datastore = new Datastore(); +const program = require('../'); +const uuid = require('uuid'); +const path = require('path'); +const execPromise = require('child-process-promise').exec; +const sinon = require('sinon'); + +const FF_TIMEOUT = 3000; + +let requestRetry = require('requestretry'); +requestRetry = requestRetry.defaults({ + retryDelay: 500, + retryStrategy: requestRetry.RetryStrategies.NetworkError, +}); + +const cwd = path.join(__dirname, '..'); + +const NAME = 'sampletask1'; +const KIND = `Task-${uuid.v4()}`; +const VALUE = { + description: 'Buy milk', +}; + +const errorMsg = msg => + `${msg} not provided. Make sure you have a "${msg.toLowerCase()}" property in your request`; + +const handleLinuxFailures = async proc => { + try { + return await proc; + } catch (err) { + // Timeouts always cause errors on Linux, so catch them + // Don't return proc, as await-ing it re-throws the error + if (!err.name || err.name !== 'ChildProcessError') { + throw err; + } + } +}; + +describe('functions/datastore', () => { + describe('set', () => { + let ffProc; + const PORT = 8080; + const BASE_URL = `http://localhost:${PORT}`; + + before(() => { + ffProc = execPromise( + `functions-framework --target=set --signature-type=http --port=${PORT}`, + {timeout: FF_TIMEOUT, shell: true, cwd} + ); + }); + + after(async () => { + await handleLinuxFailures(ffProc); + }); + + it('set: Fails without a value', async () => { + const req = { + body: {}, + }; + const res = { + status: sinon.stub().returnsThis(), + send: sinon.stub(), + }; + + await program.set(req, res); + + assert.ok(res.status.calledWith(500)); + assert.ok(res.send.calledWith(errorMsg('Value'))); + }); + + it('set: Fails without a key', async () => { + const req = { + body: { + value: VALUE, + }, + }; + const res = { + status: sinon.stub().returnsThis(), + send: sinon.stub(), + }; + + await program.set(req, res); + + assert.ok(res.status.calledWith(500)); + assert.ok(res.send.calledWith(errorMsg('Key'))); + }); + + it('set: Fails without a kind', async () => { + const req = { + body: { + key: NAME, + value: VALUE, + }, + }; + const res = { + status: sinon.stub().returnsThis(), + send: sinon.stub(), + }; + + await program.set(req, res); + + assert.ok(res.status.calledWith(500)); + assert.ok(res.send.calledWith(errorMsg('Kind'))); + }); + + it('set: Saves an entity', async () => { + const response = await requestRetry({ + url: `${BASE_URL}/set`, + method: 'POST', + body: { + kind: KIND, + key: NAME, + value: VALUE, + }, + json: true, + }); + + assert.strictEqual(response.statusCode, 200); + assert.ok(response.body.includes(`Entity ${KIND}/${NAME} saved`)); + }); + }); + + describe('get', () => { + let ffProc; + const PORT = 8081; + const BASE_URL = `http://localhost:${PORT}`; + + before(() => { + ffProc = execPromise( + `functions-framework --target=get --signature-type=http --port=${PORT}`, + {timeout: FF_TIMEOUT, shell: true, cwd} + ); + }); + + after(async () => { + await handleLinuxFailures(ffProc); + }); + + it('get: Fails when entity does not exist', async () => { + const response = await requestRetry({ + url: `${BASE_URL}/get`, + method: 'POST', + body: { + kind: KIND, + key: 'nonexistent', + }, + json: true, + }); + + assert.strictEqual(response.statusCode, 500); + assert.ok( + new RegExp( + /(Missing or insufficient permissions.)|(No entity found for key)/ + ).test(response.body) + ); + }); + + it('get: Finds an entity', async () => { + const response = await requestRetry({ + method: 'POST', + url: `${BASE_URL}/get`, + body: { + kind: KIND, + key: NAME, + }, + json: true, + }); + + assert.strictEqual(response.statusCode, 200); + assert.deepStrictEqual(response.body, { + description: 'Buy milk', + }); + }); + + it('get: Fails without a key', async () => { + const req = { + body: {}, + }; + const res = { + status: sinon.stub().returnsThis(), + send: sinon.stub(), + }; + + await program.get(req, res); + + assert.ok(res.status.calledWith(500)); + assert.ok(res.send.calledWith(errorMsg('Key'))); + }); + + it('get: Fails without a kind', async () => { + const req = { + body: { + key: NAME, + }, + }; + const res = { + status: sinon.stub().returnsThis(), + send: sinon.stub(), + }; + + await program.get(req, res); + + assert.ok(res.status.calledWith(500)); + assert.ok(res.send.calledWith(errorMsg('Kind'))); + }); + }); + + describe('del', () => { + let ffProc; + const PORT = 8082; + const BASE_URL = `http://localhost:${PORT}`; + + before(() => { + ffProc = execPromise( + `functions-framework --target=del --signature-type=http --port=${PORT}`, + {timeout: FF_TIMEOUT, shell: true, cwd} + ); + }); + + after(async () => { + await handleLinuxFailures(ffProc); + }); + + it('del: Fails without a key', async () => { + const req = { + body: {}, + }; + const res = { + status: sinon.stub().returnsThis(), + send: sinon.stub(), + }; + + await program.del(req, res); + + assert.ok(res.status.calledWith(500)); + assert.ok(res.send.calledWith(errorMsg('Key'))); + }); + + it('del: Fails without a kind', async () => { + const req = { + body: { + key: NAME, + }, + }; + const res = { + status: sinon.stub().returnsThis(), + send: sinon.stub(), + }; + + await program.del(req, res); + + assert.ok(res.status.calledWith(500)); + assert.ok(res.send.calledWith(errorMsg('Kind'))); + }); + + it(`del: Doesn't fail when entity does not exist`, async () => { + const response = await requestRetry({ + method: 'POST', + url: `${BASE_URL}/del`, + body: { + kind: KIND, + key: 'nonexistent', + }, + json: true, + }); + + assert.strictEqual(response.statusCode, 200); + assert.strictEqual(response.body, `Entity ${KIND}/nonexistent deleted.`); + }); + + it('del: Deletes an entity', async () => { + const response = await requestRetry({ + method: 'POST', + url: `${BASE_URL}/del`, + body: { + kind: KIND, + key: NAME, + }, + json: true, + }); + assert.strictEqual(response.statusCode, 200); + assert.strictEqual(response.body, `Entity ${KIND}/${NAME} deleted.`); + + const key = datastore.key([KIND, NAME]); + const [entity] = await datastore.get(key); + assert.ok(!entity); + }); + }); +}); diff --git a/functions/README.md b/functions/README.md index 4341ca27c3..548d87d3a2 100644 --- a/functions/README.md +++ b/functions/README.md @@ -22,7 +22,6 @@ environment. * [Hello World](helloworld/) * [Background](background/) * [Callbacks](messages/) -* [Cloud Datastore](datastore/) * [Cloud Pub/Sub](pubsub/) * [Cloud Spanner](spanner/) * [Dependencies](uuid/) From 3951e3da791a5ec18e3aa98a2a947860141755a3 Mon Sep 17 00:00:00 2001 From: ace-n Date: Thu, 29 Aug 2019 14:32:24 -0700 Subject: [PATCH 3/3] Update Kokoro configs --- .kokoro/{functions/datastore.cfg => datastore-functions.cfg} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .kokoro/{functions/datastore.cfg => datastore-functions.cfg} (89%) diff --git a/.kokoro/functions/datastore.cfg b/.kokoro/datastore-functions.cfg similarity index 89% rename from .kokoro/functions/datastore.cfg rename to .kokoro/datastore-functions.cfg index ea77e8bcfe..a7b051cd38 100644 --- a/.kokoro/functions/datastore.cfg +++ b/.kokoro/datastore-functions.cfg @@ -3,7 +3,7 @@ # Set the folder in which the tests are run env_vars: { key: "PROJECT" - value: "functions/datastore" + value: "datastore/functions" } # Tell the trampoline which build file to use.