Skip to content

Commit 74ab5d7

Browse files
authored
feat(cloud-sql): Add connection sample for SQL Server/node-mssql (GoogleCloudPlatform#1745)
* add sqlserver that uses node-mssql * updated app to successfully deploy to GAE Flex * Update readme * add tests * update license headers * move idleTimeoutMillis to connection timeout region tag * add kokoro config * added rejection handling to get method and updated tests * no more unhandled promise rejections
1 parent 7e5ebe3 commit 74ab5d7

File tree

7 files changed

+496
-0
lines changed

7 files changed

+496
-0
lines changed

.kokoro/cloudsql-sqlserver.cfg

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Format: //devtools/kokoro/config/proto/build.proto
2+
3+
# Set the folder in which the tests are run
4+
env_vars: {
5+
key: "PROJECT"
6+
value: "cloud-sql/sqlserver/mssql"
7+
}
8+
9+
# Tell the trampoline which build file to use.
10+
env_vars: {
11+
key: "TRAMPOLINE_BUILD_FILE"
12+
value: "github/nodejs-docs-samples/.kokoro/build.sh"
13+
}
14+
15+
# Specify which SQL client to use
16+
env_vars: {
17+
key: "SQL_CLIENT"
18+
value: "sqlserver"
19+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Connecting to Cloud SQL - MS SQL Server
2+
3+
## Before you begin
4+
5+
1. If you haven't already, set up a Node.js Development Environment by following the [Node.js setup guide](https://cloud.google.com/nodejs/docs/setup) and
6+
[create a project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#creating_a_project).
7+
8+
1. [Create a Google Cloud SQL "SQL Server" instance](
9+
https://console.cloud.google.com/sql/choose-instance-engine).
10+
11+
1. Under the instance's "USERS" tab, create a new user. Note the "User name" and "Password".
12+
13+
1. Create a new database in your Google Cloud SQL instance.
14+
15+
1. List your database instances in [Cloud Cloud Console](
16+
https://console.cloud.google.com/sql/instances/).
17+
18+
1. Click your Instance Id to see Instance details.
19+
20+
1. Click DATABASES.
21+
22+
1. Click **Create database**.
23+
24+
1. For **Database name**, enter `votes`.
25+
26+
1. Click **CREATE**.
27+
28+
1. Create a service account with the 'Cloud SQL Client' permissions by following these
29+
[instructions](https://cloud.google.com/sql/docs/mysql/connect-external-app#4_if_required_by_your_authentication_method_create_a_service_account).
30+
Download a JSON key to use to authenticate your connection.
31+
32+
33+
## Running locally
34+
Use the information noted in the previous steps to set the following environment variables:
35+
```bash
36+
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service/account/key.json
37+
export DB_USER='my-db-user'
38+
export DB_PASS='my-db-pass'
39+
export DB_NAME='my_db'
40+
```
41+
Note: Saving credentials in environment variables is convenient, but not secure - consider a more
42+
secure solution such as [Secret Manager](https://cloud.google.com/secret-manager/docs/overview) to help keep secrets safe.
43+
44+
Download and install the `cloud_sql_proxy` by
45+
following the instructions [here](https://cloud.google.com/sql/docs/mysql/sql-proxy#install).
46+
47+
Then, use the following command to start the proxy in the
48+
background using TCP:
49+
```bash
50+
./cloud_sql_proxy -instances=${CLOUD_SQL_CONNECTION_NAME}=tcp:1433 sqlserver -u ${DB_USER} --host 127.0.0.1
51+
```
52+
53+
Next, setup install the requirements with `npm`:
54+
```bash
55+
npm install
56+
```
57+
58+
Finally, start the application:
59+
```bash
60+
npm start
61+
```
62+
63+
Navigate towards `http://127.0.0.1:8080` to verify your application is running correctly.
64+
65+
## Deploy to Google App Engine Flexible
66+
67+
App Engine Flexible supports connecting to your SQL Server instance through TCP
68+
69+
First, update `app.yaml` with the correct values to pass the environment
70+
variables and instance name into the runtime.
71+
72+
Then, make sure that the service account `service-{PROJECT_NUMBER}>@gae-api-prod.google.com.iam.gserviceaccount.com` has the IAM role `Cloud SQL Client`.
73+
74+
The following command will deploy the application to your Google Cloud project:
75+
```bash
76+
gcloud beta app deploy
77+
```

cloud-sql/sqlserver/mssql/app.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright 2020, Google, Inc.
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
runtime: nodejs
15+
env: flex
16+
17+
# The following env variables may contain sensitive information that grants
18+
# anyone access to your database. Do not add this file to your source control.
19+
env_variables:
20+
DB_USER: MY_DB_USER
21+
DB_PASS: MY_DB_PASSWORD
22+
DB_NAME: MY_DATABASE
23+
DEPLOYED: true
24+
25+
beta_settings:
26+
# The connection name of your instance, available by using
27+
# 'gcloud beta sql instances describe [INSTANCE_NAME]' or from
28+
# the Instance details page in the Google Cloud Platform Console.
29+
cloud_sql_instances: <MY-PROJECT>:<INSTANCE-REGION>:<MY-DATABASE>=tcp:1433
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "cloudsql-sqlserver-mssql",
3+
"description": "Node.js Cloud SQL SQL Server Connectivity Sample",
4+
"private": true,
5+
"license": "Apache-2.0",
6+
"author": "Google Inc.",
7+
"repository": {
8+
"type": "git",
9+
"url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git"
10+
},
11+
"engines": {
12+
"node": ">=10.0.0"
13+
},
14+
"scripts": {
15+
"system-test": "mocha test/*.test.js --timeout=60000 --exit",
16+
"test": "npm run system-test"
17+
},
18+
"dependencies": {
19+
"@google-cloud/logging-winston": "^3.0.0",
20+
"body-parser": "^1.19.0",
21+
"express": "^4.17.1",
22+
"mssql": "^6.2.0",
23+
"prompt": "^1.0.0",
24+
"pug": "^2.0.3",
25+
"winston": "^3.1.0"
26+
},
27+
"devDependencies": {
28+
"mocha": "^7.0.0",
29+
"supertest": "^4.0.0"
30+
}
31+
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
const express = require('express');
18+
const mssql = require('mssql')
19+
const bodyParser = require('body-parser');
20+
21+
const app = express();
22+
app.set('view engine', 'pug');
23+
app.enable('trust proxy');
24+
25+
// Automatically parse request body as form data.
26+
app.use(bodyParser.urlencoded({extended: false}));
27+
app.use(bodyParser.json());
28+
29+
// Set Content-Type for all responses for these routes.
30+
app.use((req, res, next) => {
31+
res.set('Content-Type', 'text/html');
32+
next();
33+
});
34+
35+
// Create a Winston logger that streams to Stackdriver Logging.
36+
const winston = require('winston');
37+
const {LoggingWinston} = require('@google-cloud/logging-winston');
38+
const loggingWinston = new LoggingWinston();
39+
const logger = winston.createLogger({
40+
level: 'info',
41+
transports: [new winston.transports.Console(), loggingWinston],
42+
});
43+
44+
// [START cloud_sql_server_mssql_create]
45+
let pool;
46+
const createPool = async () => {
47+
let config = {pool: {}};
48+
config.user = process.env.DB_USER; // e.g. 'my-db-user'
49+
config.password = process.env.DB_PASS; // e.g. 'my-db-password'
50+
config.database = process.env.DB_NAME; // e.g. 'my-database'
51+
// set the server to '172.17.0.1' when connecting from App Engine Flex
52+
config.server = process.env.DEPLOYED ? '172.17.0.1' : '127.0.0.1';
53+
config.port = 1433;
54+
55+
// [START_EXCLUDE]
56+
57+
// [START cloud_sql_server_mssql_timeout]
58+
// 'connectionTimeout` is the maximum number of milliseconds to wait trying to establish an
59+
// initial connection. After the specified amount of time, an exception will be thrown.
60+
config.connectionTimeout = 30000;
61+
// 'acquireTimeoutMillis' is the number of milliseconds before a timeout occurs when acquiring a
62+
// connection from the pool.
63+
config.pool.acquireTimeoutMillis = 30000;
64+
// 'idleTimeoutMillis' is the number of milliseconds a connection must sit idle in the pool
65+
// and not be checked out before it is automatically closed
66+
config.pool.idleTimeoutMillis = 600000,
67+
// [END cloud_sql_server_mssql_timeout]
68+
69+
// [START cloud_sql_server_mssql_limit]
70+
// 'max' limits the total number of concurrent connections this pool will keep. Ideal
71+
// values for this setting are highly variable on app design, infrastructure, and database.
72+
config.pool.max = 5;
73+
// 'min' is the minimum number of idle connections maintained in the pool.
74+
// Additional connections will be established to meet this value unless the pool is full.
75+
config.pool.min = 1;
76+
// [END cloud_sql_server_mssql_limit]
77+
78+
// [START cloud_sql_server_mssql_backoff]
79+
// The node-mssql module uses a built-in retry strategy which does not implement backoff.
80+
// 'createRetryIntervalMillis' is the number of milliseconds to wait in between retries.
81+
config.pool.createRetryIntervalMillis = 200;
82+
// [END cloud_sql_server_mssql_backoff]
83+
84+
// [END_EXCLUDE]
85+
pool = await mssql.connect(config);
86+
};
87+
// [END cloud_sql_mysql_mysql_create]
88+
89+
const ensureSchema = async () => {
90+
// Wait for tables to be created (if they don't already exist).
91+
await pool.request()
92+
.query(
93+
`IF NOT EXISTS (
94+
SELECT * FROM sysobjects WHERE name='votes' and xtype='U')
95+
CREATE TABLE votes (
96+
vote_id INT NOT NULL IDENTITY,
97+
time_cast DATETIME NOT NULL,
98+
candidate VARCHAR(6) NOT NULL,
99+
PRIMARY KEY (vote_id));`
100+
);
101+
console.log(`Ensured that table 'votes' exists`);
102+
};
103+
104+
let schemaReady;
105+
app.use(async (req, res, next) => {
106+
if (schemaReady) {
107+
next();
108+
}
109+
else {
110+
try {
111+
await createPool();
112+
schemaReady = await ensureSchema();
113+
next();
114+
}
115+
catch (err) {
116+
logger.error(err);
117+
return next(err);
118+
}
119+
}
120+
});
121+
122+
// Serve the index page, showing vote tallies.
123+
app.get('/', async (req, res, next) => {
124+
125+
try {
126+
// Get the 5 most recent votes.
127+
const recentVotesQuery = pool.request().query(
128+
'SELECT TOP(5) candidate, time_cast FROM votes ORDER BY time_cast DESC'
129+
);
130+
131+
// Get votes
132+
const stmt = 'SELECT COUNT(vote_id) as count FROM votes WHERE candidate=@candidate';
133+
134+
const tabsQuery = pool.request()
135+
.input('candidate', mssql.VarChar(6), 'TABS')
136+
.query(stmt);
137+
138+
const spacesQuery = pool.request()
139+
.input('candidate', mssql.VarChar(6), 'SPACES')
140+
.query(stmt);
141+
142+
// Run queries concurrently, and wait for them to complete
143+
// This is faster than await-ing each query object as it is created
144+
145+
const recentVotes = await recentVotesQuery;
146+
const tabsVotes = await tabsQuery;
147+
const spacesVotes = await spacesQuery;
148+
149+
res.render('index.pug', {
150+
recentVotes: recentVotes.recordset,
151+
tabCount: tabsVotes.recordset[0].count,
152+
spaceCount: spacesVotes.recordset[0].count,
153+
});
154+
}
155+
catch (err) {
156+
logger.error(err);
157+
res
158+
.status(500)
159+
.send(
160+
'Unable to load page. Please check the application logs for more details.'
161+
)
162+
.end();
163+
}
164+
});
165+
166+
// Handle incoming vote requests and inserting them into the database.
167+
app.post('/', async (req, res, next) => {
168+
const {team} = req.body;
169+
const timestamp = new Date();
170+
171+
if (!team || (team !== 'TABS' && team !== 'SPACES')) {
172+
res.status(400).send('Invalid team specified.').end();
173+
}
174+
175+
// [START cloud_sql_server_mssql_connection]
176+
try {
177+
const stmt = 'INSERT INTO votes (time_cast, candidate) VALUES (@timestamp, @team)';
178+
// Using a prepared statement protects against SQL injection attacks.
179+
// When prepare is called, a single connection is acquired from the connection pool
180+
// and all subsequent executions are executed exclusively on this connection.
181+
const ps = new mssql.PreparedStatement(pool);
182+
ps.input('timestamp', mssql.DateTime)
183+
ps.input('team', mssql.VarChar(6))
184+
await ps.prepare(stmt)
185+
await ps.execute({
186+
timestamp: timestamp,
187+
team: team
188+
})
189+
await ps.unprepare();
190+
} catch (err) {
191+
// If something goes wrong, handle the error in this section. This might
192+
// involve retrying or adjusting parameters depending on the situation.
193+
// [START_EXCLUDE]
194+
195+
logger.error(err);
196+
res
197+
.status(500)
198+
.send(
199+
'Unable to successfully cast vote! Please check the application logs for more details.'
200+
)
201+
.end();
202+
// [END_EXCLUDE]
203+
}
204+
// [END cloud_sql_server_mssql_connection]
205+
206+
res.status(200).send(`Successfully voted for ${team} at ${timestamp}`).end();
207+
});
208+
209+
const PORT = process.env.PORT || 8080;
210+
const server = app.listen(PORT, () => {
211+
console.log(`App listening on port ${PORT}`);
212+
console.log('Press Ctrl+C to quit.');
213+
});
214+
215+
var environment = process.env.NODE_ENV || 'development';
216+
if (environment === `development`) {
217+
process.on('unhandledRejection', err => {
218+
console.error(err);
219+
process.exit(1);
220+
});
221+
}
222+
223+
module.exports = server;

0 commit comments

Comments
 (0)