cloud-sql/sqlserver/tedious/index.js (151 lines of code) (raw):
// Copyright 2023 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 createConnectorConnection = require('./connect-connector.js');
const createTcpConnection = require('./connect-tcp.js');
const getTediousHelper = require('./tedious-helper.js');
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 createConnection = async () => {
// 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;
}
}
if (process.env.INSTANCE_CONNECTION_NAME) {
return createConnectorConnection();
} else {
return createTcpConnection();
}
};
let validSchema = false;
let connection;
const ensureSchema = async () => {
// Wait for tables to be created (if they don't already exist).
await connection.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");
validSchema = true;
};
const connectionPromise = createConnection()
.then(conn => {
connection = getTediousHelper(conn);
})
.then(() => connection.connect())
.then(ensureSchema)
.catch(err => {
logger.error(err);
throw err;
});
app.use(async (req, res, next) => {
if (validSchema) {
return next();
}
try {
await connectionPromise;
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 recentVotes = await connection.query(
'SELECT TOP(5) candidate, time_cast FROM votes ORDER BY time_cast DESC'
);
const {NVarChar} = connection.TYPES;
// Get votes
const stmt =
'SELECT COUNT(vote_id) as count FROM votes WHERE candidate=@candidate';
const [tabsVotes] = await connection.query(stmt, [
['candidate', NVarChar, 'TABS'],
]);
const [spacesVotes] = await connection.query(stmt, [
['candidate', NVarChar, 'SPACES'],
]);
// failing: no recordset on undefined
res.render('index.pug', {
recentVotes: recentVotes || [],
tabCount: tabsVotes.count.value,
spaceCount: spacesVotes.count.value,
});
} 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();
}
try {
const stmt =
'INSERT INTO votes (time_cast, candidate) VALUES (@timestamp, @team)';
const {DateTime, NVarChar} = connection.TYPES;
// Runs query
await connection.query(stmt, [
['timestamp', DateTime, timestamp],
['team', NVarChar, team],
]);
} catch (err) {
// If something goes wrong, handle the error in this section. This might
// involve retrying or adjusting parameters depending on the situation.
logger.error(err);
return res
.status(500)
.send(
'Unable to successfully cast vote! Please check the application logs for more details.'
)
.end();
}
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;