'use strict'
var bearerToken = require('express-bearer-token')
var cors = require('cors')
var couchbase = require('couchbase')
var express = require('express')
var jwt = require('jsonwebtoken')
var morgan = require('morgan')
var uuid = require( 'uuid')
var swaggerUi = require('swagger-ui-express')
const swaggerDocument = require('./swagger.json')
// Specify a key for JWT signing.
var JWT_KEY = 'IAMSOSECRETIVE!'
// Create a Couchbase Cluster connection
const CB = {
host: process.env.CB_HOST || 'db',
username: process.env.CB_USER || 'Administrator',
password: process.env.CB_PASS || 'password'
}
async function main() {
var cluster = await couchbase.connect(
`couchbase://${CB.host}`,
{
username: CB.username,
password: CB.password
}
)
// Open a specific Couchbase bucket, `travel-sample` in this case.
var bucket = cluster.bucket('travel-sample')
// Set up our express application
var app = express()
app.use(morgan('dev'))
app.use(cors())
app.use(express.json())
app.use('/apidocs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
var tenants = express.Router({mergeParams: true})
app.get('/', (req, res) => {
return res.send(
`
Node.js Travel Sample API
A sample API for getting started with Couchbase Server and the Node.js SDK.
`
)
})
app.get('/api/airports',
runAsync(async (req, res) => {
const searchTerm = req.query.search
let where
let options
if (searchTerm.length === 3) {
// FAA code
where = 'faa = $FAA'
options = { parameters: { FAA: searchTerm.toUpperCase() } }
} else if (
searchTerm.length === 4 &&
(searchTerm.toUpperCase() === searchTerm ||
searchTerm.toLowerCase() === searchTerm)
) {
// ICAO code
where = 'icao = $ICAO'
options = { parameters: { ICAO: searchTerm.toUpperCase() } }
} else {
// Airport name
where = 'CONTAINS(LOWER(airportname), $AIRPORT)'
options = { parameters: { AIRPORT: searchTerm.toLowerCase() } }
}
let qs = `SELECT airportname from \`travel-sample\`.inventory.airport WHERE ${ where };`
const result = await cluster.query(qs, options)
const data = result.rows
const context = [`N1QL query - scoped to inventory: ${qs}`]
return res.send({data, context})
})
)
app.get('/api/flightPaths/:from/:to',
runAsync(async (req, res) => {
const fromAirport = req.params.from
const toAirport = req.params.to
const leaveDate = new Date(req.query.leave)
const dayOfWeek = leaveDate.getDay()
let qs1 = `SELECT faa AS fromFaa
FROM \`travel-sample\`.inventory.airport
WHERE airportname = $FROM
UNION
SELECT faa AS toFaa
FROM \`travel-sample\`.inventory.airport
WHERE airportname = $TO;`
const options1 = {
parameters: {
FROM: fromAirport,
TO: toAirport,
}
}
const result = await cluster.query(qs1, options1)
const rows = result.rows
if (rows.length !== 2) {
return res.status(404).send({
error: 'One of the specified airports is invalid.',
context: [qs1],
})
}
const { fromFaa, toFaa } = { ...rows[0], ...rows[1] }
let qs2 = `
SELECT a.name, s.flight, s.utc, r.sourceairport, r.destinationairport, r.equipment
FROM \`travel-sample\`.inventory.route AS r
UNNEST r.schedule AS s
JOIN \`travel-sample\`.inventory.airline AS a ON KEYS r.airlineid
WHERE r.sourceairport = $FROM
AND r.destinationairport = $TO
AND s.day = $DAY
ORDER BY a.name ASC;
`
const options2 = {
parameters: {
FROM: fromFaa,
TO: toFaa,
DAY: dayOfWeek
}
}
const result2 = await cluster.query(qs2, options2)
const rows2 = result2.rows
if (rows2.length === 0) {
return res.status(404).send({
error: 'No flights exist between these airports.',
context: [qs1, qs2],
})
}
rows2.forEach((row) => {
row.flighttime = Math.ceil(Math.random() * 8000)
row.price = Math.ceil((row.flighttime / 8) * 100) / 100
})
return res.send({
data: rows2,
context: ["N1QL query - scoped to inventory: ", qs2],
})
})
)
app.use('/api/tenants/:tenant/', tenants)
const makeKey = key => key.toLowerCase()
tenants.route('/user/login').post(
runAsync(async (req, res) => {
const tenant = makeKey( req.params.tenant )
const user = req.body.user
const userKey = makeKey( user )
const password = req.body.password
var scope = bucket.scope(tenant)
var users = scope.collection("users")
try {
const result = await users.get(userKey)
if (result.value.password !== password) {
return res.status(401).send({
error: 'Password does not match.',
})
}
const token = jwt.sign({user}, JWT_KEY)
return res.send({
data: {token},
context: [`KV get - scoped to ${tenant}.users: for password field in document ${user}`]
})
} catch (err) {
if (err instanceof couchbase.DocumentNotFoundError) {
return res.status(401).send({
error: 'User does not exist.',
})
}
else {
throw(err)
}
}
})
)
tenants.route('/user/signup').post(
runAsync(async (req, res) => {
const user = req.body.user
const userDocKey = makeKey(user)
const password = req.body.password
const tenant = makeKey( req.params.tenant )
var scope = bucket.scope(tenant)
var users = scope.collection("users")
try {
const userDoc = {
name: user,
password: password,
flights: [],
}
await users.insert(userDocKey, userDoc)
const token = jwt.sign({user}, JWT_KEY)
return res.status(201).send({
data: {token},
context: [`KV insert - scoped to ${tenant}.users: document ${userDocKey}`]
})
} catch (err) {
if (err instanceof couchbase.DocumentExistsError) {
return res.status(409).send({
error: 'User already exists.',
})
}
else {
throw(err)
}
}
})
)
tenants.route('/user/:username/flights')
.get(authUser,
runAsync(async (req, res) => {
const username = req.params.username
const userDocKey = makeKey(username)
const tenant = makeKey( req.params.tenant )
var scope = bucket.scope(tenant)
var users = scope.collection("users")
var bookings = scope.collection("bookings")
if (username !== req.user.user) {
return res.status(401).send({
error: `Username does not match token username. ${username} VS ${req.user.user}`,
})
}
try {
const result = await users.get(userDocKey)
const ids = result.content.bookings || []
const inflated = await Promise.all(
ids.map(
async flightId => (await bookings.get(flightId)).content))
return res.send({
data: inflated,
context:
[ `KV get - scoped to ${tenant}.users: for ${ids.length} bookings in document ${userDocKey}`]
})
} catch (err) {
if (err instanceof couchbase.DocumentNotFoundError) {
return res.status(403).send({
error: 'Could not find user.',
})
}
else {
throw(err)
}
}
})
)
.put(authUser,
runAsync(async (req, res) => {
const username = req.params.username
const userDocKey = makeKey(username)
const newFlight = req.body.flights[0]
const tenant = makeKey( req.params.tenant )
var scope = bucket.scope(tenant)
var users = scope.collection("users")
var bookings = scope.collection("bookings")
if (username !== req.user.user) {
return res.status(401).send({
error: 'Username does not match token username.',
})
}
const flightId = uuid.v4()
try {
await bookings.upsert(flightId, newFlight)
}
catch (err) {
return res.status(500).send({
error: 'Failed to add flight data',
})
}
try {
await users.mutateIn(userDocKey, [
couchbase.MutateInSpec.arrayAppend(
'bookings',
flightId,
{ createPath: true })])
return res.send({
data: {
added: [ newFlight ],
},
context:
[`KV update - scoped to ${tenant}.users: for bookings subdocument field in document ${userDocKey}`]
})
} catch (err) {
if (err instanceof couchbase.DocumentNotFoundError) {
return res.status(403).send({
error: 'Could not find user.',
})
}
else {
throw(err)
}
}
})
)
app.get('/api/hotels/:description/:location?',
runAsync(async (req, res) => {
const description = req.params.description
const location = req.params.location
var scope = bucket.scope("inventory")
var hotels = scope.collection("hotel")
const qp = couchbase.SearchQuery.conjuncts([
couchbase.SearchQuery.term('hotel').field('type'),
])
if (location && location !== '*') {
qp.and(
couchbase.SearchQuery.disjuncts(
couchbase.SearchQuery.match(location).field('country'),
couchbase.SearchQuery.match(location).field('city'),
couchbase.SearchQuery.match(location).field('state'),
couchbase.SearchQuery.match(location).field('address')
)
)
}
if (description && description !== '*') {
qp.and(
couchbase.SearchQuery.disjuncts(
couchbase.SearchQuery.match(description).field('description'),
couchbase.SearchQuery.match(description).field('name')
)
)
}
const result = await cluster.searchQuery('hotels-index', qp, { limit: 100 })
const rows = result.rows
if (rows.length === 0) {
return res.send({
data: [],
context: [`FTS search - scoped to: inventory.hotel (no results)\n${JSON.stringify(qp)}`],
})
}
const addressCols = [
'address',
'state',
'city',
'country'
]
const cols = [
'type',
'name',
'description',
...addressCols
]
const results = await Promise.all(
rows.map(async (row) => {
const doc = await hotels.get(row.id, {
project: cols
})
var content = doc.content
content.address =
addressCols
.flatMap(field => content[field] || [])
.join(', ')
return content
})
)
return res.send({
data: results,
context: [
`FTS search - scoped to: inventory.hotel within fields ${cols.join(', ')}\n${JSON.stringify(qp)}`]
})
})
)
// Error handler. Must be defined after other routes/middleware
app.use((err, req, res, next) => {
const errText = err.toString()
if (errText.match(/LCB_ERR_KVENGINE_INVALID_PACKET/)) {
return res.status(500).send({
error: "Received LCB_ERR_KVENGINE_INVALID_PACKET error from Couchbase. Please check the SDK release notes and ensure you are using a compatible server version."
})
}
else {
return res.status(500).send({
error: `${err.toString()}: ${JSON.stringify(err)}`
})
}
next()
})
app.listen(8080, () => {
console.log(`Connecting to backend Couchbase server ${CB.host} with ${CB.username}/${CB.password}`)
console.log('Example app listening on port 8080!')
})
}
function authUser(req, res, next) {
bearerToken()(req, res, () => {
// Temporary Hack to extract the token from the request
req.token = req.headers.authorization.split(' ')[1]
jwt.verify(req.token, JWT_KEY, (err, decoded) => {
if (err) {
return res.status(400).send({
error: 'Invalid JWT token',
cause: err,
})
}
req.user = decoded
next()
})
})
}
function runAsync (callback) {
return function (req, res, next) {
callback(req, res, next)
.catch(next)
}
}
main()