cloud-sql/sqlserver/mssql/index.js (160 lines of code) (raw):

// Copyright 2020 Google LLC // // 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 createTcpPool = require('./connect-tcp.js'); const mssql = require('mssql'); const app = express(); app.set('view engine', 'pug'); app.enable('trust proxy'); // This middleware is available in Express v4.16.0 onwards // Automatically parse request body as form data. app.use(express.urlencoded({extended: false})); app.use(express.json()); // Set Content-Type for all responses for these routes. app.use((req, res, next) => { res.set('Content-Type', 'text/html'); next(); }); // Create a Winston logger that streams to Stackdriver Logging. const winston = require('winston'); const {LoggingWinston} = require('@google-cloud/logging-winston'); const loggingWinston = new LoggingWinston(); const logger = winston.createLogger({ level: 'info', transports: [new winston.transports.Console(), loggingWinston], }); // Retrieve and return a specified secret from Secret Manager const {SecretManagerServiceClient} = require('@google-cloud/secret-manager'); const client = new SecretManagerServiceClient(); async function accessSecretVersion(secretName) { const [version] = await client.accessSecretVersion({name: secretName}); return version.payload.data; } const createPool = async () => { const config = {pool: {}}; // [START cloud_sql_sqlserver_mssql_timeout] // 'connectionTimeout` is the maximum number of milliseconds to wait trying to establish an // initial connection. After the specified amount of time, an exception will be thrown. config.connectionTimeout = 30000; // 'acquireTimeoutMillis' is the number of milliseconds before a timeout occurs when acquiring a // connection from the pool. config.pool.acquireTimeoutMillis = 30000; // 'idleTimeoutMillis' is the number of milliseconds a connection must sit idle in the pool // and not be checked out before it is automatically closed (config.pool.idleTimeoutMillis = 600000), // [END cloud_sql_sqlserver_mssql_timeout] // [START cloud_sql_sqlserver_mssql_limit] // 'max' limits the total number of concurrent connections this pool will keep. Ideal // values for this setting are highly variable on app design, infrastructure, and database. (config.pool.max = 5); // 'min' is the minimum number of idle connections maintained in the pool. // Additional connections will be established to meet this value unless the pool is full. config.pool.min = 1; // [END cloud_sql_sqlserver_mssql_limit] // [START cloud_sql_sqlserver_mssql_backoff] // The node-mssql module uses a built-in retry strategy which does not implement backoff. // 'createRetryIntervalMillis' is the number of milliseconds to wait in between retries. config.pool.createRetryIntervalMillis = 200; // [END cloud_sql_sqlserver_mssql_backoff] // Check if a Secret Manager secret version is defined // If a version is defined, retrieve the secret from Secret Manager and set as the DB_PASS const {CLOUD_SQL_CREDENTIALS_SECRET} = process.env; if (CLOUD_SQL_CREDENTIALS_SECRET) { const secrets = await accessSecretVersion(CLOUD_SQL_CREDENTIALS_SECRET); try { process.env.DB_PASS = secrets.toString(); } catch (err) { err.message = `Unable to parse secret from Secret Manager. Make sure that the secret is JSON formatted: \n ${err.message} `; throw err; } } return createTcpPool(config); }; const ensureSchema = async pool => { // Wait for tables to be created (if they don't already exist). await pool.request().query( `IF NOT EXISTS ( SELECT * FROM sysobjects WHERE name='votes' and xtype='U') CREATE TABLE votes ( vote_id INT NOT NULL IDENTITY, time_cast DATETIME NOT NULL, candidate VARCHAR(6) NOT NULL, PRIMARY KEY (vote_id));` ); console.log("Ensured that table 'votes' exists"); }; let pool; const poolPromise = createPool() .then(async pool => { await ensureSchema(pool); return pool; }) .catch(err => { logger.error(err); throw err; }); app.use(async (req, res, next) => { if (pool) { return next(); } try { pool = await poolPromise; next(); } catch (err) { logger.error(err); return next(err); } }); // Serve the index page, showing vote tallies. const httpGet = async (req, res) => { try { // Get the 5 most recent votes. const recentVotesQuery = pool .request() .query( 'SELECT TOP(5) candidate, time_cast FROM votes ORDER BY time_cast DESC' ); // Get votes const stmt = 'SELECT COUNT(vote_id) as count FROM votes WHERE candidate=@candidate'; const tabsQuery = pool .request() .input('candidate', mssql.VarChar(6), 'TABS') .query(stmt); const spacesQuery = pool .request() .input('candidate', mssql.VarChar(6), 'SPACES') .query(stmt); // Run queries concurrently, and wait for them to complete // This is faster than await-ing each query object as it is created const recentVotes = await recentVotesQuery; const tabsVotes = await tabsQuery; const spacesVotes = await spacesQuery; res.render('index.pug', { recentVotes: recentVotes.recordset, tabCount: tabsVotes.recordset[0].count, spaceCount: spacesVotes.recordset[0].count, }); } catch (err) { logger.error(err); res .status(500) .send( 'Unable to load page. Please check the application logs for more details.' ) .end(); } }; app.get('/', httpGet); // Handle incoming vote requests and inserting them into the database. const httpPost = async (req, res) => { const {team} = req.body; const timestamp = new Date(); if (!team || (team !== 'TABS' && team !== 'SPACES')) { return res.status(400).send('Invalid team specified.').end(); } // [START cloud_sql_sqlserver_mssql_connection] try { const stmt = 'INSERT INTO votes (time_cast, candidate) VALUES (@timestamp, @team)'; // Using a prepared statement protects against SQL injection attacks. // When prepare is called, a single connection is acquired from the connection pool // and all subsequent executions are executed exclusively on this connection. const ps = new mssql.PreparedStatement(pool); ps.input('timestamp', mssql.DateTime); ps.input('team', mssql.VarChar(6)); await ps.prepare(stmt); await ps.execute({ timestamp: timestamp, team: team, }); await ps.unprepare(); } catch (err) { // If something goes wrong, handle the error in this section. This might // involve retrying or adjusting parameters depending on the situation. // [START_EXCLUDE] logger.error(err); return res .status(500) .send( 'Unable to successfully cast vote! Please check the application logs for more details.' ) .end(); // [END_EXCLUDE] } // [END cloud_sql_sqlserver_mssql_connection] res.status(200).send(`Successfully voted for ${team} at ${timestamp}`).end(); }; app.post('*', httpPost); /** * Responds to GET and POST requests for TABS vs SPACES sample app. * * @param {Object} req Cloud Function request context. * @param {Object} res Cloud Function response context. */ exports.votes = (req, res) => { switch (req.method) { case 'GET': httpGet(req, res); break; case 'POST': httpPost(req, res); break; default: res.status(405).send({error: 'Something blew up!'}); break; } }; module.exports.app = app;