├── .dockerignore ├── .gitignore ├── .prettierrc ├── Dockerfile ├── LICENSE.md ├── README.md ├── app.png ├── docker-compose.yml ├── fts-hotels-index.json ├── index.js ├── mix-and-match.yml ├── package-lock.json ├── package.json ├── scripts └── update-swagger ├── swagger.json ├── utils ├── config.json ├── db.js └── provision.js └── wait-for-couchbase.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .cache 3 | node_modules 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /.idea/ 3 | *.out 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "arrowParens": "always", 4 | "singleQuote": true, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-buster-slim 2 | 3 | LABEL maintainer="Couchbase" 4 | 5 | WORKDIR /app 6 | 7 | RUN apt-get update && apt-get install -y \ 8 | build-essential python\ 9 | jq curl 10 | 11 | COPY . /app 12 | 13 | RUN npm install 14 | 15 | EXPOSE 8080 16 | 17 | ENTRYPOINT ["./wait-for-couchbase.sh", "node", "index.js"] 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Released under MIT License 2 | 3 | Copyright (c) 2021 Couchbase, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Couchbase Node.js travel-sample Application REST Backend 2 | 3 | This is a sample application for getting started with [Couchbase Server] and the Node.js SDK. 4 | The application runs a single page web UI for demonstrating SQL for Documents (N1QL), Sub-document requests and Full Text Search (FTS) querying capabilities. 5 | It uses Couchbase Server together with the [Express] web framework for [Node.js], [Swagger] for API documentation, [Vue] and [Bootstrap]. 6 | 7 | The application is a flight planner that allows the user to search for and select a flight route (including the return flight) based on airports and dates. 8 | Airport selection is done dynamically using an autocomplete box bound to N1QL queries on the server side. After selecting a date, it then searches 9 | for applicable air flight routes from a previously populated database. An additional page allows users to search for Hotels using less structured keywords. 10 | 11 | ![Application](app.png) 12 | 13 | 14 | ## Prerequisites 15 | 16 | To download the application you can either download [the archive](https://github.com/couchbaselabs/try-cb-nodejs/archive/master.zip) or clone the repository: 17 | 18 | git clone https://github.com/couchbaselabs/try-cb-nodejs.git 19 | 20 | We recommend running the application with Docker, which starts up all components for you, 21 | but you can also run it in a Mix-and-Match style, which we'll decribe below. 22 | 23 | 24 | ## Running the application with Docker 25 | 26 | You will need [Docker](https://docs.docker.com/get-docker/) installed on your machine in order to run this application as we have defined a [_Dockerfile_](Dockerfile) and a [_docker-compose.yml_](docker-compose.yml) to run Couchbase Server 7.0.0, the front-end [Vue app](https://github.com/couchbaselabs/try-cb-frontend-v2.git) and the Node.js REST API. 27 | 28 | To launch the full application, simply run this command from a terminal: 29 | 30 | docker-compose up 31 | 32 | > **_NOTE:_** You may need more than the default RAM to run the images. 33 | We have tested the travel-sample apps with 4.5 GB RAM configured in Docker's Preferences... -> Resources -> Memory. 34 | When you run the application for the first time, it will pull/build the relevant docker images, so it may take a bit of time. 35 | 36 | This will start the Node.js backend, Couchbase Server 7.0.0 and the Vue frontend app. 37 | 38 | You can access the backend API on http://localhost:8080/, the UI on 39 | http://localhost:8081/ and Couchbase Server at http://localhost:8091/ 40 | 41 | ❯ docker-compose up 42 | Docker Compose is now in the Docker CLI, try `docker compose up` 43 | 44 | Creating network "try-cb-nodejs_default" with the default driver 45 | Pulling db (couchbase/server-sandbox:7.0.0)... 46 | ... 47 | Building backend 48 | ... 49 | Building frontend 50 | ... 51 | Creating couchbase-sandbox-7.0.0 ... done 52 | Creating try-cb-api ... done 53 | Creating try-cb-fe ... done 54 | Attaching to couchbase-sandbox-7.0.0, try-cb-api, try-cb-fe 55 | couchbase-sandbox-7.0.0 | Starting Couchbase Server -- Web UI available at http://:8091 56 | couchbase-sandbox-7.0.0 | and logs available in /opt/couchbase/var/lib/couchbase/logs 57 | couchbase-sandbox-7.0.0 | Configuring Couchbase Server. Please wait (~60 sec)... 58 | try-cb-api | wait-for-couchbase: checking http://db:8091/pools/default/buckets/travel-sample/ 59 | try-cb-api | wait-for-couchbase: polling for '.scopes | map(.name) | contains(["inventory", " 60 | try-cb-fe | wait-for-it: waiting 400 seconds for backend:8080 61 | try-cb-api | wait-for-couchbase: ... 62 | couchbase-sandbox-7.0.0 | Configuration completed! 63 | couchbase-sandbox-7.0.0 | Couchbase Admin UI: http://localhost:8091 64 | couchbase-sandbox-7.0.0 | Login credentials: Administrator / password 65 | try-cb-api | wait-for-couchbase: checking http://db:8094/api/cfg 66 | try-cb-api | wait-for-couchbase: polling for '.status == "ok"' 67 | try-cb-api | wait-for-couchbase: checking http://db:8094/api/index/hotels-index 68 | try-cb-api | wait-for-couchbase: polling for '.status == "ok"' 69 | try-cb-api | wait-for-couchbase: Failure 70 | try-cb-api | wait-for-couchbase: Creating hotels-index... 71 | try-cb-api | wait-for-couchbase: checking http://db:8094/api/index/hotels-index/count 72 | try-cb-api | wait-for-couchbase: polling for '.count >= 917' 73 | try-cb-api | wait-for-couchbase: ... 74 | ... 75 | try-cb-api | wait-for-couchbase: checking http://db:9102/api/v1/stats 76 | try-cb-api | wait-for-couchbase: polling for '.indexer.indexer_state == "Active"' 77 | try-cb-api | wait-for-couchbase: polling for '. | keys | contains(["travel-sample:def_airport 78 | try-cb-api | wait-for-couchbase: polling for '. | del(.indexer) | del(.["travel-sample:def_na 79 | try-cb-api | wait-for-couchbase: polling for '. | del(.indexer) | map(.num_pending_requests = 80 | try-cb-api | Connecting to backend Couchbase server db with Administrator/password 81 | try-cb-api | Example app listening on port 8080! 82 | try-cb-fe | wait-for-it: backend:8080 is available after 121 seconds 83 | try-cb-fe | 84 | try-cb-fe | > try-cb-frontend-v2@0.1.0 serve 85 | try-cb-fe | > vue-cli-service serve --port 8081 86 | try-cb-fe | 87 | try-cb-fe | INFO Starting development server... 88 | 89 | You should then be able to browse the UI, search for US airports and get flight 90 | route information. 91 | 92 | To end the application press Control+C in the terminal 93 | and wait for docker-compose to gracefully stop your containers. 94 | 95 | 96 | ## Mix and match services 97 | 98 | Instead of running all services, you can start any combination of `backend`, 99 | `frontend`, `db` via docker, and take responsibility for starting the other 100 | services yourself. 101 | 102 | As the provided `docker-compose.yml` sets up dependencies between the services, 103 | to make startup as smooth and automatic as possible, we also provide an 104 | alternative `mix-and-match.yml`. We'll look at a few useful scenarios here. 105 | 106 | 107 | ### Bring your own database 108 | 109 | If you wish to run this application against your own configuration of Couchbase 110 | Server, you will need version 7.0.0 or later with the `travel-sample` 111 | bucket setup. 112 | 113 | > **_NOTE:_** if you are not using Docker to start up the API server, or the 114 | > provided wrapper `wait-for-couchbase.sh`, you will need to create a full text 115 | > search index on travel-sample bucket called 'hotels-index'. You can do this 116 | > via the following command: 117 | 118 | curl --fail -s -u : -X PUT \ 119 | http://:8094/api/index/hotels-index \ 120 | -H 'cache-control: no-cache' \ 121 | -H 'content-type: application/json' \ 122 | -d @fts-hotels-index.json 123 | 124 | With a running Couchbase Server, you can pass the database details in: 125 | 126 | CB_HOST=10.144.211.101 CB_USER=Administrator CB_PSWD=password docker-compose -f mix-and-match.yml up backend frontend 127 | 128 | The Docker image will run the same checks as usual, and also create the 129 | `hotels-index` if it does not already exist. 130 | 131 | 132 | ### Running the Node.js API application manually 133 | 134 | You may want to run the Node.js application yourself, to make rapid changes to it, 135 | and try out the features of the Couchbase API, without having to re-build the Docker 136 | image. You may still use Docker to run the Database and Frontend components if desired. 137 | 138 | Install the dependencies: 139 | 140 | npm install 141 | 142 | Note that `nodemon` is installed as a dev-dependency, so you can run the server with 143 | the benefit of automatic restarting as you make changes. 144 | 145 | You will have to point your application at the Couchbase server with the 146 | `CB_HOST` environment variable. 147 | 148 | The first time you run against a new database image, you may want to use the provided 149 | `wait-for-couchbase.sh` wrapper to ensure that all indexes are created. 150 | For example, using the Docker image provided: 151 | 152 | 153 | docker-compose -f mix-and-match.yml up db 154 | 155 | export CB_HOST=localhost 156 | ./wait-for-couchbase.sh echo "Couchbase is ready!" 157 | npx nodemon index.js 158 | 159 | If you already have an existing Couchbase server running and correctly configured, you might run: 160 | 161 | CB_HOST=10.144.211.101 CB_USER=Administrator CB_PSWD=password npx nodemon index.js 162 | 163 | Finally, if you want to see how the sample frontend Vue application works with your changes, 164 | run it with: 165 | 166 | docker-compose -f mix-and-match.yml up frontend 167 | 168 | 169 | ### Running the front-end manually 170 | 171 | To run the frontend components manually without Docker, follow the guide 172 | [here](https://github.com/couchbaselabs/try-cb-frontend-v2) 173 | 174 | 175 | ## REST API reference, and tests. 176 | 177 | All the travel-sample apps conform to the same interface, which means that they can all be used with the same database configuration and Vue.js frontend. 178 | 179 | We've integrated Swagger/OpenApi version 3 documentation which can be accessed on the backend at `http://localhost:8080/apidocs` once you have started the app. 180 | 181 | (You can also view a read-only version at https://docs.couchbase.com/python-sdk/current/hello-world/sample-application.html#) 182 | 183 | To further ensure that every app conforms to the API, we have a [test suite][try-cb-test], which you can simply run with the command: 184 | 185 | ``` 186 | docker-compose --profile test up test 187 | ``` 188 | 189 | If you are running locally though, with a view to extending or modifying the travel-sample app, you will likely want to be able to make changes to both the code and the tests in parallel. 190 | 191 | * Start the backend server locally, for example using "Running the Node.js API application manually" above. 192 | * Check out the [test suite][try-cb-test] repo in a separate working directory, and run the tests manually, as per the instructions. 193 | 194 | 195 | [Couchbase Server]: https://www.couchbase.com/ 196 | [Node.js SDK]: https://docs.couchbase.com/nodejs-sdk/current/hello-world/overview.html 197 | [Express]: https://expressjs.com/ 198 | [Node.js]: https://nodejs.org/ 199 | [Swagger]: https://swagger.io/resources/open-api/ 200 | [Vue]: https://vuejs.org/ 201 | [Bootstrap]: https://getbootstrap.com/ 202 | [try-cb-test]: https://github.com/couchbaselabs/try-cb-test/ 203 | -------------------------------------------------------------------------------- /app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/try-cb-nodejs/5e3fc16d25b5551fc132bcf0ec1e45d4a68f2e7b/app.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | backend: 5 | build: . 6 | depends_on: 7 | - db 8 | ports: 9 | - 8080:8080 10 | container_name: try-cb-api 11 | 12 | frontend: 13 | build: "https://github.com/couchbaselabs/try-cb-frontend-v2.git#7.0" 14 | ports: 15 | - 8081:8081 16 | container_name: try-cb-fe 17 | entrypoint: ["wait-for-it", "backend:8080", "--timeout=400", "--strict", "--", "npm", "run", "serve"] 18 | depends_on: 19 | - backend 20 | 21 | db: 22 | image: couchbase/server-sandbox:7.0.0 23 | ports: 24 | - "8091-8095:8091-8095" 25 | - "9102:9102" 26 | - "11210:11210" 27 | expose: 28 | - "8091" 29 | - "8092" 30 | - "8093" 31 | - "8094" 32 | - "8095" 33 | - "9102" 34 | - "11210" 35 | container_name: couchbase-sandbox-7.0.0 36 | 37 | test: 38 | build: "https://github.com/couchbaselabs/try-cb-test.git#main" 39 | depends_on: 40 | - backend 41 | environment: 42 | BACKEND_BASE_URL: http://backend:8080 43 | entrypoint: ["wait-for-it", "backend:8080", "--timeout=400", "--strict", "--", "bats", "travel-sample-backend.bats"] 44 | profiles: 45 | - test 46 | -------------------------------------------------------------------------------- /fts-hotels-index.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hotels-index", 3 | "type": "fulltext-index", 4 | "params": { 5 | "doc_config": { 6 | "docid_prefix_delim": "", 7 | "docid_regexp": "", 8 | "mode": "scope.collection.type_field", 9 | "type_field": "type" 10 | }, 11 | "mapping": { 12 | "default_analyzer": "standard", 13 | "default_datetime_parser": "dateTimeOptional", 14 | "default_field": "_all", 15 | "default_mapping": { 16 | "dynamic": true, 17 | "enabled": false 18 | }, 19 | "default_type": "_default", 20 | "docvalues_dynamic": false, 21 | "index_dynamic": true, 22 | "store_dynamic": false, 23 | "type_field": "_type", 24 | "types": { 25 | "inventory.hotel": { 26 | "dynamic": true, 27 | "enabled": true 28 | } 29 | } 30 | }, 31 | "store": { 32 | "indexType": "scorch", 33 | "segmentVersion": 15 34 | } 35 | }, 36 | "sourceType": "couchbase", 37 | "sourceName": "travel-sample", 38 | "sourceParams": {}, 39 | "planParams": { 40 | "maxPartitionsPerPIndex": 1024, 41 | "indexPartitions": 1, 42 | "numReplicas": 0 43 | }, 44 | "uuid": "" 45 | } 46 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var bearerToken = require('express-bearer-token') 4 | var cors = require('cors') 5 | var couchbase = require('couchbase') 6 | 7 | var express = require('express') 8 | var jwt = require('jsonwebtoken') 9 | var morgan = require('morgan') 10 | var uuid = require( 'uuid') 11 | var swaggerUi = require('swagger-ui-express') 12 | const swaggerDocument = require('./swagger.json') 13 | 14 | // Specify a key for JWT signing. 15 | var JWT_KEY = 'IAMSOSECRETIVE!' 16 | 17 | // Create a Couchbase Cluster connection 18 | const CB = { 19 | host: process.env.CB_HOST || 'db', 20 | username: process.env.CB_USER || 'Administrator', 21 | password: process.env.CB_PASS || 'password' 22 | } 23 | 24 | async function main() { 25 | var cluster = await couchbase.connect( 26 | `couchbase://${CB.host}`, 27 | { 28 | username: CB.username, 29 | password: CB.password 30 | } 31 | ) 32 | 33 | // Open a specific Couchbase bucket, `travel-sample` in this case. 34 | var bucket = cluster.bucket('travel-sample') 35 | 36 | // Set up our express application 37 | var app = express() 38 | app.use(morgan('dev')) 39 | app.use(cors()) 40 | app.use(express.json()) 41 | 42 | app.use('/apidocs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); 43 | 44 | var tenants = express.Router({mergeParams: true}) 45 | 46 | app.get('/', (req, res) => { 47 | return res.send( 48 | `

Node.js Travel Sample API

49 | A sample API for getting started with Couchbase Server and the Node.js SDK. 50 | ` 54 | ) 55 | }) 56 | 57 | app.get('/api/airports', 58 | runAsync(async (req, res) => { 59 | const searchTerm = req.query.search 60 | let where 61 | let options 62 | 63 | if (searchTerm.length === 3) { 64 | // FAA code 65 | where = 'faa = $FAA' 66 | options = { parameters: { FAA: searchTerm.toUpperCase() } } 67 | } else if ( 68 | searchTerm.length === 4 && 69 | (searchTerm.toUpperCase() === searchTerm || 70 | searchTerm.toLowerCase() === searchTerm) 71 | ) { 72 | // ICAO code 73 | where = 'icao = $ICAO' 74 | options = { parameters: { ICAO: searchTerm.toUpperCase() } } 75 | } else { 76 | // Airport name 77 | where = 'CONTAINS(LOWER(airportname), $AIRPORT)' 78 | options = { parameters: { AIRPORT: searchTerm.toLowerCase() } } 79 | } 80 | 81 | let qs = `SELECT airportname from \`travel-sample\`.inventory.airport WHERE ${ where };` 82 | 83 | const result = await cluster.query(qs, options) 84 | const data = result.rows 85 | const context = [`N1QL query - scoped to inventory: ${qs}`] 86 | return res.send({data, context}) 87 | }) 88 | ) 89 | 90 | app.get('/api/flightPaths/:from/:to', 91 | runAsync(async (req, res) => { 92 | const fromAirport = req.params.from 93 | const toAirport = req.params.to 94 | const leaveDate = new Date(req.query.leave) 95 | 96 | const dayOfWeek = leaveDate.getDay() 97 | 98 | let qs1 = `SELECT faa AS fromFaa 99 | FROM \`travel-sample\`.inventory.airport 100 | WHERE airportname = $FROM 101 | UNION 102 | SELECT faa AS toFaa 103 | FROM \`travel-sample\`.inventory.airport 104 | WHERE airportname = $TO;` 105 | 106 | const options1 = { 107 | parameters: { 108 | FROM: fromAirport, 109 | TO: toAirport, 110 | } 111 | } 112 | 113 | const result = await cluster.query(qs1, options1) 114 | const rows = result.rows 115 | if (rows.length !== 2) { 116 | return res.status(404).send({ 117 | error: 'One of the specified airports is invalid.', 118 | context: [qs1], 119 | }) 120 | } 121 | const { fromFaa, toFaa } = { ...rows[0], ...rows[1] } 122 | 123 | let qs2 = ` 124 | SELECT a.name, s.flight, s.utc, r.sourceairport, r.destinationairport, r.equipment 125 | FROM \`travel-sample\`.inventory.route AS r 126 | UNNEST r.schedule AS s 127 | JOIN \`travel-sample\`.inventory.airline AS a ON KEYS r.airlineid 128 | WHERE r.sourceairport = $FROM 129 | AND r.destinationairport = $TO 130 | AND s.day = $DAY 131 | ORDER BY a.name ASC; 132 | ` 133 | const options2 = { 134 | parameters: { 135 | FROM: fromFaa, 136 | TO: toFaa, 137 | DAY: dayOfWeek 138 | } 139 | } 140 | 141 | const result2 = await cluster.query(qs2, options2) 142 | const rows2 = result2.rows 143 | if (rows2.length === 0) { 144 | return res.status(404).send({ 145 | error: 'No flights exist between these airports.', 146 | context: [qs1, qs2], 147 | }) 148 | } 149 | 150 | rows2.forEach((row) => { 151 | row.flighttime = Math.ceil(Math.random() * 8000) 152 | row.price = Math.ceil((row.flighttime / 8) * 100) / 100 153 | }) 154 | 155 | return res.send({ 156 | data: rows2, 157 | context: ["N1QL query - scoped to inventory: ", qs2], 158 | }) 159 | }) 160 | ) 161 | 162 | app.use('/api/tenants/:tenant/', tenants) 163 | 164 | const makeKey = key => key.toLowerCase() 165 | 166 | tenants.route('/user/login').post( 167 | runAsync(async (req, res) => { 168 | const tenant = makeKey( req.params.tenant ) 169 | const user = req.body.user 170 | const userKey = makeKey( user ) 171 | const password = req.body.password 172 | var scope = bucket.scope(tenant) 173 | var users = scope.collection("users") 174 | 175 | try { 176 | const result = await users.get(userKey) 177 | 178 | if (result.value.password !== password) { 179 | return res.status(401).send({ 180 | error: 'Password does not match.', 181 | }) 182 | } 183 | 184 | const token = jwt.sign({user}, JWT_KEY) 185 | 186 | return res.send({ 187 | data: {token}, 188 | context: [`KV get - scoped to ${tenant}.users: for password field in document ${user}`] 189 | }) 190 | } catch (err) { 191 | if (err instanceof couchbase.DocumentNotFoundError) { 192 | return res.status(401).send({ 193 | error: 'User does not exist.', 194 | }) 195 | } 196 | else { 197 | throw(err) 198 | } 199 | } 200 | }) 201 | ) 202 | 203 | tenants.route('/user/signup').post( 204 | runAsync(async (req, res) => { 205 | const user = req.body.user 206 | const userDocKey = makeKey(user) 207 | const password = req.body.password 208 | 209 | const tenant = makeKey( req.params.tenant ) 210 | var scope = bucket.scope(tenant) 211 | var users = scope.collection("users") 212 | 213 | try { 214 | const userDoc = { 215 | name: user, 216 | password: password, 217 | flights: [], 218 | } 219 | 220 | await users.insert(userDocKey, userDoc) 221 | 222 | const token = jwt.sign({user}, JWT_KEY) 223 | 224 | return res.status(201).send({ 225 | data: {token}, 226 | context: [`KV insert - scoped to ${tenant}.users: document ${userDocKey}`] 227 | }) 228 | } catch (err) { 229 | if (err instanceof couchbase.DocumentExistsError) { 230 | return res.status(409).send({ 231 | error: 'User already exists.', 232 | }) 233 | } 234 | else { 235 | throw(err) 236 | } 237 | } 238 | }) 239 | ) 240 | 241 | tenants.route('/user/:username/flights') 242 | .get(authUser, 243 | runAsync(async (req, res) => { 244 | const username = req.params.username 245 | const userDocKey = makeKey(username) 246 | 247 | const tenant = makeKey( req.params.tenant ) 248 | var scope = bucket.scope(tenant) 249 | var users = scope.collection("users") 250 | var bookings = scope.collection("bookings") 251 | 252 | if (username !== req.user.user) { 253 | return res.status(401).send({ 254 | error: `Username does not match token username. ${username} VS ${req.user.user}`, 255 | }) 256 | } 257 | 258 | try { 259 | const result = await users.get(userDocKey) 260 | const ids = result.content.bookings || [] 261 | 262 | const inflated = await Promise.all( 263 | ids.map( 264 | async flightId => (await bookings.get(flightId)).content)) 265 | 266 | return res.send({ 267 | data: inflated, 268 | context: 269 | [ `KV get - scoped to ${tenant}.users: for ${ids.length} bookings in document ${userDocKey}`] 270 | }) 271 | } catch (err) { 272 | if (err instanceof couchbase.DocumentNotFoundError) { 273 | return res.status(403).send({ 274 | error: 'Could not find user.', 275 | }) 276 | } 277 | else { 278 | throw(err) 279 | } 280 | } 281 | }) 282 | ) 283 | .put(authUser, 284 | runAsync(async (req, res) => { 285 | const username = req.params.username 286 | const userDocKey = makeKey(username) 287 | 288 | const newFlight = req.body.flights[0] 289 | const tenant = makeKey( req.params.tenant ) 290 | 291 | var scope = bucket.scope(tenant) 292 | var users = scope.collection("users") 293 | var bookings = scope.collection("bookings") 294 | 295 | if (username !== req.user.user) { 296 | return res.status(401).send({ 297 | error: 'Username does not match token username.', 298 | }) 299 | } 300 | 301 | const flightId = uuid.v4() 302 | 303 | try { 304 | await bookings.upsert(flightId, newFlight) 305 | } 306 | catch (err) { 307 | return res.status(500).send({ 308 | error: 'Failed to add flight data', 309 | }) 310 | } 311 | 312 | try { 313 | await users.mutateIn(userDocKey, [ 314 | couchbase.MutateInSpec.arrayAppend( 315 | 'bookings', 316 | flightId, 317 | { createPath: true })]) 318 | 319 | return res.send({ 320 | data: { 321 | added: [ newFlight ], 322 | }, 323 | context: 324 | [`KV update - scoped to ${tenant}.users: for bookings subdocument field in document ${userDocKey}`] 325 | }) 326 | } catch (err) { 327 | if (err instanceof couchbase.DocumentNotFoundError) { 328 | return res.status(403).send({ 329 | error: 'Could not find user.', 330 | }) 331 | } 332 | else { 333 | throw(err) 334 | } 335 | } 336 | }) 337 | ) 338 | 339 | app.get('/api/hotels/:description/:location?', 340 | runAsync(async (req, res) => { 341 | const description = req.params.description 342 | const location = req.params.location 343 | var scope = bucket.scope("inventory") 344 | var hotels = scope.collection("hotel") 345 | const qp = couchbase.SearchQuery.conjuncts([ 346 | couchbase.SearchQuery.term('hotel').field('type'), 347 | ]) 348 | 349 | if (location && location !== '*') { 350 | qp.and( 351 | couchbase.SearchQuery.disjuncts( 352 | couchbase.SearchQuery.match(location).field('country'), 353 | couchbase.SearchQuery.match(location).field('city'), 354 | couchbase.SearchQuery.match(location).field('state'), 355 | couchbase.SearchQuery.match(location).field('address') 356 | ) 357 | ) 358 | } 359 | if (description && description !== '*') { 360 | qp.and( 361 | couchbase.SearchQuery.disjuncts( 362 | couchbase.SearchQuery.match(description).field('description'), 363 | couchbase.SearchQuery.match(description).field('name') 364 | ) 365 | ) 366 | } 367 | 368 | const result = await cluster.searchQuery('hotels-index', qp, { limit: 100 }) 369 | const rows = result.rows 370 | if (rows.length === 0) { 371 | return res.send({ 372 | data: [], 373 | context: [`FTS search - scoped to: inventory.hotel (no results)\n${JSON.stringify(qp)}`], 374 | }) 375 | } 376 | 377 | const addressCols = [ 378 | 'address', 379 | 'state', 380 | 'city', 381 | 'country' 382 | ] 383 | 384 | const cols = [ 385 | 'type', 386 | 'name', 387 | 'description', 388 | ...addressCols 389 | ] 390 | 391 | const results = await Promise.all( 392 | rows.map(async (row) => { 393 | const doc = await hotels.get(row.id, { 394 | project: cols 395 | }) 396 | var content = doc.content 397 | content.address = 398 | addressCols 399 | .flatMap(field => content[field] || []) 400 | .join(', ') 401 | return content 402 | }) 403 | ) 404 | 405 | return res.send({ 406 | data: results, 407 | context: [ 408 | `FTS search - scoped to: inventory.hotel within fields ${cols.join(', ')}\n${JSON.stringify(qp)}`] 409 | }) 410 | }) 411 | ) 412 | 413 | // Error handler. Must be defined after other routes/middleware 414 | app.use((err, req, res, next) => { 415 | const errText = err.toString() 416 | if (errText.match(/LCB_ERR_KVENGINE_INVALID_PACKET/)) { 417 | return res.status(500).send({ 418 | 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." 419 | }) 420 | } 421 | else { 422 | return res.status(500).send({ 423 | error: `${err.toString()}: ${JSON.stringify(err)}` 424 | }) 425 | } 426 | next() 427 | }) 428 | 429 | app.listen(8080, () => { 430 | console.log(`Connecting to backend Couchbase server ${CB.host} with ${CB.username}/${CB.password}`) 431 | console.log('Example app listening on port 8080!') 432 | }) 433 | } 434 | 435 | function authUser(req, res, next) { 436 | bearerToken()(req, res, () => { 437 | // Temporary Hack to extract the token from the request 438 | req.token = req.headers.authorization.split(' ')[1] 439 | jwt.verify(req.token, JWT_KEY, (err, decoded) => { 440 | if (err) { 441 | return res.status(400).send({ 442 | error: 'Invalid JWT token', 443 | cause: err, 444 | }) 445 | } 446 | 447 | req.user = decoded 448 | next() 449 | }) 450 | }) 451 | } 452 | 453 | function runAsync (callback) { 454 | return function (req, res, next) { 455 | callback(req, res, next) 456 | .catch(next) 457 | } 458 | } 459 | 460 | main() 461 | -------------------------------------------------------------------------------- /mix-and-match.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | backend: 5 | build: . 6 | ports: 7 | - 8080:8080 8 | environment: 9 | - CB_HOST 10 | - CB_USER 11 | - CB_PSWD 12 | container_name: try-cb-api-mm 13 | 14 | frontend: 15 | build: "https://github.com/couchbaselabs/try-cb-frontend-v2.git#7.0" 16 | ports: 17 | - 8081:8081 18 | container_name: try-cb-fe-mm 19 | 20 | db: 21 | image: couchbase/server-sandbox:7.0.0 22 | ports: 23 | - "8091-8095:8091-8095" 24 | - "9102:9102" 25 | - "11210:11210" 26 | expose: 27 | - "8091" 28 | - "8094" 29 | container_name: couchbase-sandbox-7.0.0-mm 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "try-cb", 3 | "version": "1.0.0", 4 | "description": "Sample Application for Couchbase, Node.js, Angular and Express", 5 | "scripts": { 6 | "build": "node utils/provision.js", 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/couchbaselabs/try-cb-nodejs.git" 13 | }, 14 | "author": "Brett Lawson ", 15 | "license": "Apache-2.0", 16 | "bugs": { 17 | "url": "https://github.com/couchbaselabs/try-cb-nodejs/issues" 18 | }, 19 | "homepage": "https://github.com/couchbaselabs/try-cb-nodejs", 20 | "dependencies": { 21 | "body-parser": "^1.19.0", 22 | "cors": "~2.8.1", 23 | "couchbase": "^3.2.4", 24 | "express": "^4.17.1", 25 | "express-bearer-token": "~2.1.0", 26 | "jsonwebtoken": "~8.5.1", 27 | "morgan": "^1.10.0", 28 | "swagger-ui-express": "^4.3.0", 29 | "uuid": "^8.3.2" 30 | }, 31 | "devDependencies": { 32 | "nodemon": "^2.0.7" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /scripts/update-swagger: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | GH_USER=${GH_USER:-couchbaselabs} 4 | GH_PROJECT=${GH_PROJECT:-try-cb-python} 5 | GH_BRANCH=${GH_BRANCH:-7.0} 6 | 7 | URL=https://raw.githubusercontent.com/${GH_USER}/${GH_PROJECT}/${GH_BRANCH}/swagger.json 8 | echo "Getting $URL ..." 9 | curl -S --fail -O $URL 10 | 11 | if [ $? -eq 0 ]; then 12 | echo swagger.json retrieved: 13 | git diff --exit-code swagger.json && echo "No changes!" 14 | else 15 | echo "couldn't retrieve swagger.json" 16 | fi 17 | -------------------------------------------------------------------------------- /swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "components": { 3 | "schemas": { 4 | "Context": { 5 | "items": { 6 | "type": "string" 7 | }, 8 | "type": "array" 9 | }, 10 | "Error": { 11 | "properties": { 12 | "message": { 13 | "example": "An error message", 14 | "type": "string" 15 | } 16 | }, 17 | "type": "object" 18 | }, 19 | "ResultList": { 20 | "properties": { 21 | "context": { 22 | "$ref": "#/components/schemas/Context" 23 | }, 24 | "data": { 25 | "items": { 26 | "type": "object" 27 | }, 28 | "type": "array" 29 | } 30 | }, 31 | "type": "object" 32 | }, 33 | "ResultSingleton": { 34 | "properties": { 35 | "context": { 36 | "$ref": "#/components/schemas/Context" 37 | }, 38 | "data": { 39 | "type": "object" 40 | } 41 | }, 42 | "type": "object" 43 | } 44 | }, 45 | "securitySchemes": { 46 | "bearer": { 47 | "bearerFormat": "JWT", 48 | "description": "JWT Authorization header using the Bearer scheme.", 49 | "scheme": "bearer", 50 | "type": "http" 51 | } 52 | } 53 | }, 54 | "definitions": {}, 55 | "info": { 56 | "description": "A sample API for getting started with Couchbase Server and the SDK.", 57 | "termsOfService": "", 58 | "title": "Travel Sample API", 59 | "version": "1.0" 60 | }, 61 | "openapi": "3.0.3", 62 | "paths": { 63 | "/": { 64 | "get": { 65 | "responses": { 66 | "200": { 67 | "content": { 68 | "text/html": { 69 | "example": "

Travel Sample API

" 70 | } 71 | }, 72 | "description": "Returns the API index page" 73 | } 74 | }, 75 | "summary": "Returns the index page" 76 | } 77 | }, 78 | "/api/airports": { 79 | "get": { 80 | "parameters": [ 81 | { 82 | "description": "The airport name/code to search for", 83 | "example": "SFO", 84 | "in": "query", 85 | "name": "search", 86 | "required": true, 87 | "schema": { 88 | "type": "string" 89 | } 90 | } 91 | ], 92 | "responses": { 93 | "200": { 94 | "content": { 95 | "application/json": { 96 | "example": { 97 | "context": [ 98 | "A description of a N1QL operation" 99 | ], 100 | "data": [ 101 | { 102 | "airportname": "San Francisco Intl" 103 | } 104 | ] 105 | }, 106 | "schema": { 107 | "$ref": "#/components/schemas/ResultList" 108 | } 109 | } 110 | }, 111 | "description": "Returns airport data and query context information" 112 | } 113 | }, 114 | "summary": "Returns list of matching airports and the source query", 115 | "tags": [ 116 | "airports" 117 | ] 118 | } 119 | }, 120 | "/api/flightPaths/{fromloc}/{toloc}": { 121 | "get": { 122 | "parameters": [ 123 | { 124 | "description": "Airport name for beginning route", 125 | "example": "San Francisco Intl", 126 | "in": "path", 127 | "name": "fromloc", 128 | "required": true, 129 | "schema": { 130 | "type": "string" 131 | } 132 | }, 133 | { 134 | "description": "Airport name for end route", 135 | "example": "Los Angeles Intl", 136 | "in": "path", 137 | "name": "toloc", 138 | "required": true, 139 | "schema": { 140 | "type": "string" 141 | } 142 | }, 143 | { 144 | "description": "Date of flight departure in `mm/dd/yyyy` format", 145 | "example": "05/24/2021", 146 | "in": "query", 147 | "name": "leave", 148 | "required": true, 149 | "schema": { 150 | "format": "date", 151 | "type": "string" 152 | } 153 | } 154 | ], 155 | "responses": { 156 | "200": { 157 | "content": { 158 | "application/json": { 159 | "example": { 160 | "context": [ 161 | "N1QL query - scoped to inventory: SELECT faa as fromAirport FROM `travel-sample`.inventory.airport WHERE airportname = $1 UNION SELECT faa as toAirport FROM `travel-sample`.inventory.airport WHERE airportname = $2" 162 | ], 163 | "data": [ 164 | { 165 | "destinationairport": "LAX", 166 | "equipment": "738", 167 | "flight": "AA331", 168 | "flighttime": 1220, 169 | "name": "American Airlines", 170 | "price": 152.5, 171 | "sourceairport": "SFO", 172 | "utc": "16:37:00" 173 | } 174 | ] 175 | }, 176 | "schema": { 177 | "$ref": "#/components/schemas/ResultList" 178 | } 179 | } 180 | }, 181 | "description": "Returns flight data and query context information" 182 | } 183 | }, 184 | "summary": "Return flights information, cost and more for a given flight time and date", 185 | "tags": [ 186 | "flightPaths" 187 | ] 188 | } 189 | }, 190 | "/api/hotels/{description}/{location}/": { 191 | "get": { 192 | "parameters": [ 193 | { 194 | "description": "Hotel description keywords", 195 | "example": "pool", 196 | "in": "path", 197 | "name": "description", 198 | "required": false, 199 | "schema": { 200 | "type": "string" 201 | } 202 | }, 203 | { 204 | "description": "Hotel location", 205 | "example": "San Francisco", 206 | "in": "path", 207 | "name": "location", 208 | "required": false, 209 | "schema": { 210 | "type": "string" 211 | } 212 | } 213 | ], 214 | "responses": { 215 | "200": { 216 | "content": { 217 | "application/json": { 218 | "example": { 219 | "context": [ 220 | "FTS search - scoped to: inventory.hotel within fields address,city,state,country,name,description" 221 | ], 222 | "data": [ 223 | { 224 | "address": "250 Beach St, San Francisco, California, United States", 225 | "description": "Nice hotel, centrally located (only two blocks from Pier 39). Heated outdoor swimming pool.", 226 | "name": "Radisson Hotel Fisherman's Wharf" 227 | }, 228 | { 229 | "address": "121 7th St, San Francisco, California, United States", 230 | "description": "Chain motel with a few more amenities than the typical Best Western; outdoor swimming pool, internet access, cafe on-site, pet friendly.", 231 | "name": "Best Western Americania" 232 | } 233 | ] 234 | }, 235 | "schema": { 236 | "$ref": "#/components/schemas/ResultList" 237 | } 238 | } 239 | }, 240 | "description": "Returns hotel data and query context information" 241 | } 242 | }, 243 | "summary": "Find hotels using full text search", 244 | "tags": [ 245 | "hotels" 246 | ] 247 | } 248 | }, 249 | "/api/tenants/{tenant}/user/login": { 250 | "post": { 251 | "parameters": [ 252 | { 253 | "description": "Tenant agent name", 254 | "example": "tenant_agent_00", 255 | "in": "path", 256 | "name": "tenant", 257 | "required": true, 258 | "schema": { 259 | "type": "string" 260 | } 261 | } 262 | ], 263 | "requestBody": { 264 | "content": { 265 | "application/json": { 266 | "schema": { 267 | "properties": { 268 | "password": { 269 | "example": "password1", 270 | "type": "string" 271 | }, 272 | "user": { 273 | "example": "user1", 274 | "type": "string" 275 | } 276 | }, 277 | "required": [ 278 | "user", 279 | "password" 280 | ], 281 | "type": "object" 282 | } 283 | } 284 | } 285 | }, 286 | "responses": { 287 | "200": { 288 | "content": { 289 | "application/json": { 290 | "example": { 291 | "context": [ 292 | "KV get - scoped to tenant_agent_00.users: for password field in document user1" 293 | ], 294 | "data": { 295 | "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoibXNfdXNlciJ9.GPs8two_vPVBpdqD7cz_yJ4X6J9yDTi6g7r9eWyAwEM" 296 | } 297 | }, 298 | "schema": { 299 | "$ref": "#/components/schemas/ResultSingleton" 300 | } 301 | } 302 | }, 303 | "description": "Returns login data and query context information" 304 | }, 305 | "401": { 306 | "content": { 307 | "application/json": { 308 | "schema": { 309 | "$ref": "#/components/schemas/Error" 310 | } 311 | } 312 | }, 313 | "description": "Returns an authentication error" 314 | } 315 | }, 316 | "summary": "Login an existing user for a given tenant agent", 317 | "tags": [ 318 | "tenants" 319 | ] 320 | } 321 | }, 322 | "/api/tenants/{tenant}/user/signup": { 323 | "post": { 324 | "parameters": [ 325 | { 326 | "description": "Tenant agent name", 327 | "example": "tenant_agent_00", 328 | "in": "path", 329 | "name": "tenant", 330 | "required": true, 331 | "schema": { 332 | "type": "string" 333 | } 334 | } 335 | ], 336 | "requestBody": { 337 | "content": { 338 | "application/json": { 339 | "schema": { 340 | "properties": { 341 | "password": { 342 | "example": "password1", 343 | "type": "string" 344 | }, 345 | "user": { 346 | "example": "user1", 347 | "type": "string" 348 | } 349 | }, 350 | "required": [ 351 | "user", 352 | "password" 353 | ], 354 | "type": "object" 355 | } 356 | } 357 | } 358 | }, 359 | "responses": { 360 | "201": { 361 | "content": { 362 | "application/json": { 363 | "example": { 364 | "context": [ 365 | "KV insert - scoped to tenant_agent_00.users: document user1" 366 | ], 367 | "data": { 368 | "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoibXNfdXNlciJ9.GPs8two_vPVBpdqD7cz_yJ4X6J9yDTi6g7r9eWyAwEM" 369 | } 370 | }, 371 | "schema": { 372 | "$ref": "#/components/schemas/ResultSingleton" 373 | } 374 | } 375 | }, 376 | "description": "Returns login data and query context information" 377 | }, 378 | "409": { 379 | "content": { 380 | "application/json": { 381 | "schema": { 382 | "$ref": "#/components/schemas/Error" 383 | } 384 | } 385 | }, 386 | "description": "Returns a conflict error" 387 | } 388 | }, 389 | "summary": "Signup a new user", 390 | "tags": [ 391 | "tenants" 392 | ] 393 | } 394 | }, 395 | "/api/tenants/{tenant}/user/{username}/flights": { 396 | "get": { 397 | "parameters": [ 398 | { 399 | "description": "Tenant agent name", 400 | "example": "tenant_agent_00", 401 | "in": "path", 402 | "name": "tenant", 403 | "required": true, 404 | "schema": { 405 | "type": "string" 406 | } 407 | }, 408 | { 409 | "description": "Username", 410 | "example": "user1", 411 | "in": "path", 412 | "name": "username", 413 | "required": true, 414 | "schema": { 415 | "type": "string" 416 | } 417 | } 418 | ], 419 | "responses": { 420 | "200": { 421 | "content": { 422 | "application/json": { 423 | "example": { 424 | "context": [ 425 | "KV get - scoped to tenant_agent_00.user: for 2 bookings in document user1" 426 | ], 427 | "data": [ 428 | { 429 | "date": "05/24/2021", 430 | "destinationairport": "LAX", 431 | "equipment": "738", 432 | "flight": "AA655", 433 | "flighttime": 5383, 434 | "name": "American Airlines", 435 | "price": 672.88, 436 | "sourceairport": "SFO", 437 | "utc": "11:42:00" 438 | }, 439 | { 440 | "date": "05/28/2021", 441 | "destinationairport": "SFO", 442 | "equipment": "738", 443 | "flight": "AA344", 444 | "flighttime": 6081, 445 | "name": "American Airlines", 446 | "price": 760.13, 447 | "sourceairport": "LAX", 448 | "utc": "20:47:00" 449 | } 450 | ] 451 | }, 452 | "schema": { 453 | "$ref": "#/components/schemas/ResultList" 454 | } 455 | } 456 | }, 457 | "description": "Returns flight data and query context information" 458 | }, 459 | "401": { 460 | "content": { 461 | "application/json": { 462 | "schema": { 463 | "$ref": "#/components/schemas/Error" 464 | } 465 | } 466 | }, 467 | "description": "Returns an authentication error" 468 | } 469 | }, 470 | "security": [ 471 | { 472 | "bearer": [] 473 | } 474 | ], 475 | "summary": "List the flights that have been reserved by a user", 476 | "tags": [ 477 | "tenants" 478 | ] 479 | }, 480 | "put": { 481 | "parameters": [ 482 | { 483 | "description": "Tenant agent name", 484 | "example": "tenant_agent_00", 485 | "in": "path", 486 | "name": "tenant", 487 | "required": true, 488 | "schema": { 489 | "type": "string" 490 | } 491 | }, 492 | { 493 | "description": "Username", 494 | "example": "user1", 495 | "in": "path", 496 | "name": "username", 497 | "required": true, 498 | "schema": { 499 | "type": "string" 500 | } 501 | } 502 | ], 503 | "requestBody": { 504 | "content": { 505 | "application/json": { 506 | "schema": { 507 | "properties": { 508 | "flights": { 509 | "example": [ 510 | { 511 | "date": "12/12/2020", 512 | "destinationairport": "Leonardo Da Vinci International Airport", 513 | "flight": "12RF", 514 | "name": "boeing", 515 | "price": 50.0, 516 | "sourceairport": "London (Gatwick)" 517 | } 518 | ], 519 | "format": "string", 520 | "type": "array" 521 | } 522 | }, 523 | "type": "object" 524 | } 525 | } 526 | } 527 | }, 528 | "responses": { 529 | "200": { 530 | "content": { 531 | "application/json": { 532 | "example": { 533 | "context": [ 534 | "KV update - scoped to tenant_agent_00.user: for bookings field in document user1" 535 | ], 536 | "data": { 537 | "added": [ 538 | { 539 | "date": "12/12/2020", 540 | "destinationairport": "Leonardo Da Vinci International Airport", 541 | "flight": "12RF", 542 | "name": "boeing", 543 | "price": 50.0, 544 | "sourceairport": "London (Gatwick)" 545 | } 546 | ] 547 | } 548 | }, 549 | "schema": { 550 | "$ref": "#/components/schemas/ResultSingleton" 551 | } 552 | } 553 | }, 554 | "description": "Returns flight data and query context information" 555 | }, 556 | "401": { 557 | "content": { 558 | "application/json": { 559 | "schema": { 560 | "$ref": "#/components/schemas/Error" 561 | } 562 | } 563 | }, 564 | "description": "Returns an authentication error" 565 | } 566 | }, 567 | "security": [ 568 | { 569 | "bearer": [] 570 | } 571 | ], 572 | "summary": "Book a new flight for a user", 573 | "tags": [ 574 | "tenants" 575 | ] 576 | } 577 | } 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /utils/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "couchbase": { 3 | "endPoint": "localhost:8091", 4 | "n1qlService": "localhost:8093", 5 | "ftsService": "localhost:8094", 6 | "hostName": "127.0.0.1", 7 | "bucket": "travel-sample", 8 | "user": "Administrator", 9 | "password": "password", 10 | "dataPath": "", 11 | "indexPath": "", 12 | "indexType": "gsi", 13 | "indexerStorageMode":"forestdb", 14 | "showQuery": false, 15 | "indexMemQuota": 2048, 16 | "dataMemQuota": 1024, 17 | "ftsMemoryQuota":1024, 18 | "thresholdItemCount": 31565, 19 | "ftsIndex": { 20 | "type": "fulltext-index", 21 | "name": "hotels", 22 | "sourceType": "couchbase", 23 | "sourceName": "travel-sample", 24 | "planParams": { 25 | "maxPartitionsPerPIndex": 32, 26 | "numReplicas": 0, 27 | "hierarchyRules": null, 28 | "nodePlanParams": null, 29 | "pindexWeights": null, 30 | "planFrozen": false 31 | }, 32 | "params": { 33 | "mapping": { 34 | "default_analyzer": "standard", 35 | "default_datetime_parser": "dateTimeOptional", 36 | "default_field": "_all", 37 | "default_mapping": { 38 | "display_order": "1", 39 | "dynamic": true, 40 | "enabled": false 41 | }, 42 | "default_type": "_default", 43 | "index_dynamic": true, 44 | "store_dynamic": false, 45 | "type_field": "type", 46 | "types": { 47 | "hotel": { 48 | "display_order": "0", 49 | "dynamic": true, 50 | "enabled": true, 51 | "properties": { 52 | "city": { 53 | "dynamic": false, 54 | "enabled": true, 55 | "fields": [ 56 | { 57 | "analyzer": "", 58 | "display_order": "2", 59 | "include_in_all": true, 60 | "include_term_vectors": true, 61 | "index": true, 62 | "name": "city", 63 | "store": true, 64 | "type": "text" 65 | } 66 | ] 67 | }, 68 | "content": { 69 | "dynamic": false, 70 | "enabled": true, 71 | "fields": [ 72 | { 73 | "analyzer": "", 74 | "display_order": "4", 75 | "include_in_all": true, 76 | "include_term_vectors": true, 77 | "index": true, 78 | "name": "content", 79 | "store": true, 80 | "type": "text" 81 | } 82 | ] 83 | }, 84 | "name": { 85 | "dynamic": false, 86 | "enabled": true, 87 | "fields": [ 88 | { 89 | "analyzer": "", 90 | "display_order": "0", 91 | "include_in_all": true, 92 | "include_term_vectors": true, 93 | "index": true, 94 | "name": "name", 95 | "store": true, 96 | "type": "text" 97 | } 98 | ] 99 | }, 100 | "price": { 101 | "dynamic": false, 102 | "enabled": true, 103 | "fields": [ 104 | { 105 | "analyzer": "", 106 | "display_order": "1", 107 | "include_in_all": true, 108 | "include_term_vectors": true, 109 | "index": true, 110 | "name": "price", 111 | "store": true, 112 | "type": "text" 113 | } 114 | ] 115 | }, 116 | "reviews": { 117 | "dynamic": false, 118 | "enabled": true, 119 | "fields": [ 120 | { 121 | "analyzer": "", 122 | "display_order": "3", 123 | "include_in_all": true, 124 | "include_term_vectors": true, 125 | "index": true, 126 | "name": "reviews", 127 | "store": false, 128 | "type": "text" 129 | } 130 | ] 131 | } 132 | } 133 | } 134 | } 135 | }, 136 | "store": { 137 | "kvStoreName": "forestdb" 138 | } 139 | }, 140 | "sourceParams": { 141 | "clusterManagerBackoffFactor": 0, 142 | "clusterManagerSleepInitMS": 0, 143 | "clusterManagerSleepMaxMS": 2000, 144 | "dataManagerBackoffFactor": 0, 145 | "dataManagerSleepInitMS": 0, 146 | "dataManagerSleepMaxMS": 2000, 147 | "feedBufferAckThreshold": 0, 148 | "feedBufferSizeBytes": 0 149 | } 150 | } 151 | }, 152 | "application": { 153 | "autoprovision": true, 154 | "hostName": "localhost", 155 | "httpPort": 3000, 156 | "dataSource": "embedded", 157 | "wait": 3000, 158 | "checkInterval": 1000, 159 | "verbose": false, 160 | "distanceCostMultiplier": 0.1, 161 | "avgKmHr": 800, 162 | "hashToken": "UNSECURE_SECRET_TOKEN" 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /utils/db.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @type {exports} 4 | */ 5 | var config = require('./config'); 6 | var couchbase = require('couchbase'); 7 | var endPoint = config.couchbase.endPoint; 8 | var bucket = config.couchbase.bucket; 9 | var myCluster = new couchbase.Cluster(endPoint); 10 | var myBucket=myCluster.openBucket(bucket); 11 | var ODMBucket = myCluster.openBucket(bucket); 12 | var db = myBucket; 13 | var ottoman = require('ottoman'); 14 | var faye = require('faye'); 15 | var client = new faye.Client('http://localhost:8000/faye'); 16 | 17 | /** 18 | * 19 | * @param key 20 | * @param val 21 | * @param done 22 | */ 23 | function upsert(key, val, done) { 24 | db.upsert(key, val, function (err, res) { 25 | if (err) { 26 | console.log("DB.UPSERT:",key,":", err); 27 | done(err, null); 28 | return; 29 | } 30 | done(null, res); 31 | }); 32 | } 33 | 34 | /** 35 | * 36 | * @param key 37 | * @param done 38 | */ 39 | function read(key, done) { 40 | db.get(key, function (err, result) { 41 | if (err) { 42 | console.log("DB.READ:", err); 43 | done(err, null); 44 | return; 45 | } 46 | done(null, result); 47 | }); 48 | } 49 | 50 | /** 51 | * 52 | * @param key 53 | * @param done 54 | */ 55 | function docDelete(key, done) { 56 | db.delete(key, function (err, result) { 57 | if (err) { 58 | console.log("DB.DELETE:", err); 59 | done(err, null); 60 | return; 61 | } 62 | done(null, true); 63 | }); 64 | } 65 | 66 | function refreshExpiry(key, time, done) { 67 | db.touch(key, time, function(err, result) { 68 | if(err) { 69 | return done(err, null); 70 | } 71 | done(null, true); 72 | }); 73 | } 74 | 75 | function sendMessage(channel, message) { 76 | if(config.couchbase.showQuery){ 77 | if(channel){ 78 | var publication = client.publish('/'+channel, {text: message},function(err,pubres){ 79 | if(err){ 80 | console.log("ERR:",err); 81 | } 82 | if(pubres){ 83 | console.log("SUCCESS:",pubres); 84 | } 85 | }); 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * 92 | * @param sql 93 | * @param user 94 | * @param done 95 | */ 96 | function query(sql,user,done){ 97 | 98 | // Init a channel 99 | var channel; 100 | 101 | // Check for only 2 parameters and if only 2 assign the callback correctly 102 | // Otherwise, assign channel to the username passed in for publishing using Faye 103 | if(typeof done === "undefined"){ 104 | done=user; 105 | } 106 | else{ 107 | channel=user; 108 | } 109 | 110 | // Setup Query 111 | var N1qlQuery = couchbase.N1qlQuery; 112 | 113 | // Check if configured to show queries in console 114 | if(config.couchbase.showQuery){ 115 | console.log("QUERY:",sql); 116 | } 117 | 118 | // publish to channel subscriber using faye 119 | if(channel){ 120 | var publication = client.publish('/'+channel, {text: 'N1QL='+sql},function(err,pubres){ 121 | if(err){ 122 | console.log("ERR:",err); 123 | } 124 | if(pubres){ 125 | console.log("SUCCESS:",pubres); 126 | } 127 | }); 128 | } 129 | 130 | // Make a N1QL specific Query 131 | var query = N1qlQuery.fromString(sql); 132 | 133 | // Issue Query 134 | db.query(query,function(err,result){ 135 | if (err) { 136 | console.log("ERR:",err); 137 | done(err,null); 138 | return; 139 | } 140 | done(null,result); 141 | }); 142 | } 143 | 144 | 145 | /** 146 | * 147 | * @param done 148 | */ 149 | 150 | module.exports.ODMBucket=ODMBucket; 151 | module.exports.endPoint=endPoint; 152 | module.exports.bucket=bucket; 153 | module.exports.query=query; 154 | module.exports.delete=docDelete; 155 | module.exports.read=read; 156 | module.exports.upsert=upsert; 157 | module.exports.refreshExpiry=refreshExpiry; 158 | module.exports.sendMessage=sendMessage; 159 | -------------------------------------------------------------------------------- /utils/provision.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var config = require('./config'); 3 | var request = require('request'); 4 | var fs = require('fs'); 5 | 6 | var checkInterval = config.application.checkInterval; 7 | 8 | class cluster { 9 | constructor() { 10 | // local json object for Class properties. ES6 does not 11 | // include suport for class properties beyond setter/getters. 12 | // The constructor instantiates a "config" and passes this 13 | // through the provisioning process. 14 | 15 | //implementationVersion 16 | 17 | var locals = {}; 18 | locals.endPoint = config.couchbase.endPoint; 19 | locals.endPointQuery = config.couchbase.n1qlService; 20 | locals.endPointFts = config.couchbase.ftsService; 21 | locals.hostName = config.couchbase.hostName; 22 | locals.sampleBucket = config.couchbase.bucket; 23 | locals.sampleBucketCount = config.couchbase.thresholdItemCount; 24 | locals.user = config.couchbase.user; 25 | locals.password = config.couchbase.password; 26 | locals.indexType = config.couchbase.indexType; 27 | locals.indexerStorageMode = config.couchbase.indexerStorageMode; 28 | locals.indexMemQuota = config.couchbase.indexMemQuota; 29 | locals.dataMemQuota = config.couchbase.dataMemQuota; 30 | locals.ftsMemQuota = config.couchbase.ftsMemoryQuota; 31 | locals.dataPath = config.couchbase.dataPath; 32 | locals.indexPath = config.couchbase.indexPath; 33 | locals.checkInterval = config.application.checkInterval; 34 | locals.ftsIndex=config.couchbase.ftsIndex; 35 | locals.finsihed = false; 36 | locals.currentCount = 0; 37 | locals.timerWait = ""; 38 | locals.instanceVersion=0; 39 | 40 | this._locals = locals; 41 | } 42 | 43 | provision() { 44 | // Load locals to pass through provisioning sequence 45 | var locals = this.locals; 46 | 47 | // resolve path issues 48 | this._resolvePaths(locals); 49 | 50 | // Provision promise chain sequence. Without binding "this", 51 | // scope is not preserved from the caller each time a new 52 | // promise is instantiated. 53 | 54 | this._verifyNodejsVersion(locals) 55 | .then(this._instanceExsists) 56 | .then(this._verifyCouchbaseVersion.bind(this)) 57 | .then(this._init) 58 | .then(this._rename) 59 | .then(this._storageMode) 60 | .then(this._services.bind(this)) 61 | .then(this._memory) 62 | .then(this._admin) 63 | .then(this._bucket) 64 | .then(this._loaded.bind(this)) 65 | .then(this._buildFtsIndex.bind(this)) 66 | .then(this._finish.bind(this)) 67 | .catch((err) => { 68 | console.log("ERR:", err) 69 | }); 70 | } 71 | 72 | get locals() { 73 | return this._locals; 74 | } 75 | 76 | set _currentCount(count){ 77 | this.locals.currentCount=count; 78 | } 79 | 80 | set _instanceVersion(version){ 81 | this.locals.instanceVersion=version; 82 | } 83 | 84 | set finished(currentState) { 85 | this._locals.finished = currrentState; 86 | } 87 | 88 | _resolvePaths(locals) { 89 | // Check for custom datapath, otherwise assign to platform default 90 | if (locals.dataPath == "") { 91 | if (process.platform == 'darwin') { 92 | locals.dataPath = "/Users/" + process.env.USER + 93 | "/Library/Application Support/Couchbase/var/lib/couchbase/data"; 94 | } else { 95 | locals.dataPath = "/opt/couchbase/var/lib/couchbase/data"; 96 | } 97 | } 98 | // Check for custom indexpath, otherwise assign to platform default 99 | if (locals.indexPath == "") { 100 | if (process.platform == 'darwin') { 101 | locals.indexPath = "/Users/" + process.env.USER + 102 | "/Library/Application Support/Couchbase/var/lib/couchbase/data"; 103 | } else { 104 | locals.indexPath = "/opt/couchbase/var/lib/couchbase/data"; 105 | } 106 | } 107 | } 108 | 109 | _init(locals) { 110 | return new Promise( 111 | (resolve, reject) => { 112 | request.post({ 113 | url: 'http://' + locals.endPoint + '/nodes/self/controller/settings', 114 | form: { 115 | path: locals.dataPath, 116 | index_path: locals.indexPath 117 | } 118 | }, (err, httpResponse, body) => { 119 | if (err) { 120 | reject(err); 121 | return; 122 | } 123 | console.log(" PROVISION INITIALIZE SERVICES:", httpResponse.statusCode); 124 | if(httpResponse.statusCode!=200) console.log(" WARNING:",body); 125 | resolve(locals); 126 | }); 127 | }); 128 | } 129 | 130 | _rename(locals) { 131 | return new Promise( 132 | (resolve, reject) => { 133 | request.post({ 134 | url: 'http://' + locals.endPoint + '/node/controller/rename', 135 | form: { 136 | hostname: locals.hostName 137 | } 138 | }, (err, httpResponse, body) => { 139 | if (err) { 140 | reject(err); 141 | return; 142 | } 143 | console.log(" PROVISION RENAMING:", httpResponse.statusCode); 144 | if(httpResponse.statusCode!=200) console.log(" WARNING:",body); 145 | resolve(locals); 146 | }); 147 | }); 148 | } 149 | 150 | _storageMode(locals) { 151 | return new Promise( 152 | (resolve, reject) => { 153 | request.post({ 154 | url: 'http://' + locals.endPoint + '/settings/indexes', 155 | form: { 156 | storageMode: locals.indexerStorageMode 157 | } 158 | }, (err, httpResponse, body) => { 159 | if (err) { 160 | reject(err); 161 | return; 162 | } 163 | console.log(" PROVISION INDEX STORAGE MODE:", httpResponse.statusCode); 164 | if(httpResponse.statusCode!=200) console.log(" WARNING:",body); 165 | resolve(locals); 166 | }); 167 | }); 168 | } 169 | 170 | _services(locals) { 171 | return new Promise( 172 | (resolve, reject) => { 173 | var data = { 174 | services:'kv,n1ql,index' 175 | }; 176 | 177 | if (locals.ftsMemQuota != "0" && locals.instanceVersion>=4.5) data["services"] += ",fts"; 178 | 179 | request.post({ 180 | url: 'http://' + locals.endPoint + '/node/controller/setupServices', 181 | form: data 182 | }, (err, httpResponse, body) => { 183 | if (err) { 184 | reject(err); 185 | return; 186 | } 187 | console.log(" PROVISION SERVICES:", httpResponse.statusCode); 188 | if(httpResponse.statusCode!=200) console.log(" WARNING:",body); 189 | resolve(locals); 190 | }); 191 | }); 192 | } 193 | 194 | _memory(locals) { 195 | return new Promise( 196 | (resolve, reject) => { 197 | var data = { 198 | indexMemoryQuota: locals.indexMemQuota, 199 | memoryQuota: locals.dataMemQuota 200 | }; 201 | 202 | if (locals.ftsMemQuota != "0" && locals.instanceVersion>=4.5) 203 | data["ftsMemoryQuota"] = locals.ftsMemQuota; 204 | 205 | request.post({ 206 | url: 'http://' + locals.endPoint + '/pools/default', 207 | form: data 208 | }, (err, httpResponse, body) => { 209 | if (err) { 210 | reject(err); 211 | return; 212 | } 213 | console.log(" PROVISION MEMORY:", httpResponse.statusCode); 214 | if(httpResponse.statusCode!=200) console.log(" WARNING:",body); 215 | resolve(locals); 216 | }); 217 | }); 218 | } 219 | 220 | _admin(locals) { 221 | return new Promise( 222 | (resolve, reject) => { 223 | request.post({ 224 | url: 'http://' + locals.endPoint + '/settings/web', 225 | form: { 226 | password: locals.password, 227 | username: locals.user, 228 | port: 'SAME' 229 | } 230 | }, (err, httpResponse, body) => { 231 | if (err) { 232 | reject(err); 233 | return; 234 | } 235 | console.log(" PROVISION ADMIN USER:", httpResponse.statusCode); 236 | if(httpResponse.statusCode!=200) console.log(" WARNING:",body); 237 | resolve(locals); 238 | }); 239 | }); 240 | } 241 | 242 | _bucket(locals) { 243 | return new Promise( 244 | (resolve, reject) => { 245 | request.post({ 246 | url: 'http://' + locals.endPoint + '/sampleBuckets/install', 247 | headers: { 248 | 'Content-Type': 'application/x-www-form-urlencoded' 249 | }, 250 | form: JSON.stringify([locals.sampleBucket]), 251 | auth: { 252 | 'user': locals.user, 253 | 'pass': locals.password, 254 | 'sendImmediately': true 255 | } 256 | }, (err, httpResponse, body) => { 257 | if (err) { 258 | reject(err); 259 | return; 260 | } 261 | console.log(" PROVISION BUCKET:", httpResponse.statusCode); 262 | if (httpResponse.statusCode == 202) { 263 | resolve(locals); 264 | } 265 | reject(httpResponse.statusCode); 266 | }); 267 | }); 268 | } 269 | 270 | _instanceExsists(locals) { 271 | return new Promise( 272 | (resolve, reject) => { 273 | request.get({ 274 | url: "http://" + locals.endPoint + "/pools/default/buckets/", 275 | auth: { 276 | 'user': locals.user, 277 | 'pass': locals.password, 278 | 'sendImmediately': true 279 | } 280 | }, (err, httpResponse, body) => { 281 | if (err) { 282 | reject("COUCHBASE INSTANCE AT " + locals.endPoint + " NOT FOUND."); 283 | return; 284 | } 285 | body = JSON.parse(body); 286 | for (var i = 0; i < body.length; i++) { 287 | if (body[i].name == locals.sampleBucket) { 288 | reject("\n This application cannot provision an already built cluster.\n" + 289 | " BUCKET:" + locals.sampleBucket + " on CLUSTER " + 290 | locals.endPoint + " EXISTS\n The cluster has not been modified.\n" + 291 | " To run the travel-sample application run 'npm start'"); 292 | } 293 | } 294 | resolve(locals); 295 | }); 296 | }); 297 | } 298 | 299 | _queryOnline() { 300 | return new Promise( 301 | (resolve, reject) => { 302 | request.get({ 303 | url: "http://" + this.endPointQuery + "/query?statement=SELECT+name+FROM+system%3Akeyspaces", 304 | auth: { 305 | 'user': config.couchbase.user, 306 | 'pass': config.couchbase.password, 307 | 'sendImmediately': true 308 | }, 309 | headers: { 310 | Accept: 'application/json' 311 | } 312 | }, (err, httpResponse, body) => { 313 | if (err) { 314 | reject(err); 315 | return; 316 | } 317 | if (response.statusCode == 200) 318 | resolve(httpResponse.statusCode); 319 | }); 320 | }); 321 | } 322 | 323 | _itemCount() { 324 | return new Promise( 325 | (resolve, reject)=> { 326 | request.get({ 327 | url: "http://" + this.locals.endPoint + "/pools/default/buckets/" + this.locals.sampleBucket, 328 | auth: { 329 | 'user': this.locals.user, 330 | 'pass': this.locals.password, 331 | 'sendImmediately': true 332 | } 333 | }, (err, httpResponse, body) => { 334 | if (err) { 335 | resolve(false); 336 | return; 337 | } 338 | if (parseInt(JSON.parse(body).basicStats.itemCount) > this.locals.sampleBucketCount) { 339 | resolve(true); 340 | } 341 | else{ 342 | this._currentCount=parseInt(JSON.parse(body).basicStats.itemCount); 343 | resolve(false); 344 | } 345 | }); 346 | }); 347 | } 348 | 349 | _loaded() { 350 | return new Promise( 351 | (resolve, reject)=> { 352 | this.locals.timerLoop = setInterval(()=> { 353 | this._itemCount().then((loaded)=> { 354 | if (loaded) { 355 | clearInterval(this.locals.timerLoop); 356 | process.stdout.write(" LOADING ITEMS:100% of " +this.locals.sampleBucketCount + " Items"); 357 | console.log("\n BUCKET:", this.locals.sampleBucket, "LOADED."); 358 | resolve("DONE"); 359 | return; 360 | } 361 | process.stdout.write(" LOADING ITEMS:" + 362 | Math.round(100*(this.locals.currentCount/this.locals.sampleBucketCount))+ "% of " + 363 | this.locals.sampleBucketCount + " Items\r"); 364 | }); 365 | }, this.locals.checkInterval); 366 | } 367 | ); 368 | } 369 | 370 | _buildFtsIndex(){ 371 | return new Promise( 372 | (resolve, reject) => { 373 | if(this.locals.instanceVersion>=4.5 && this.locals.ftsMemQuota!="0"){ 374 | request({ 375 | url: 'http://' + this.locals.endPointFts + '/api/index/' + this.locals.ftsIndex.name, 376 | method:'PUT', 377 | json: true, 378 | body: this.locals.ftsIndex, 379 | auth: { 380 | 'user': this.locals.user, 381 | 'pass': this.locals.password, 382 | 'sendImmediately': true 383 | } 384 | }, (err, httpResponse, body) => { 385 | if (err) { 386 | reject(err); 387 | return; 388 | } 389 | console.log(" PROVISION FTS INDEX:", httpResponse.statusCode); 390 | if(httpResponse.statusCode!=200) console.log(" WARNING:",body); 391 | resolve("ok"); 392 | }); 393 | } 394 | else { 395 | console.log(" PROVISION FTS INDEX: Skipping, CB version < 4.5 or ftsMemoryQuota = 0"); 396 | resolve("ok"); 397 | } 398 | 399 | }); 400 | } 401 | 402 | _verifyCouchbaseVersion(locals){ 403 | return new Promise( 404 | (resolve, reject)=> { 405 | request.get({ 406 | url: "http://" + this.locals.endPoint + "/pools", 407 | auth: { 408 | 'user': this.locals.user, 409 | 'pass': this.locals.password, 410 | 'sendImmediately': true 411 | } 412 | }, (err, httpResponse, body) => { 413 | if (err) { 414 | resolve(false); 415 | return; 416 | } 417 | var ver = (JSON.parse(body).implementationVersion).split(".",2); 418 | this._instanceVersion=parseFloat(ver[0]+"."+ver[1]); 419 | resolve(locals); 420 | }); 421 | }); 422 | } 423 | 424 | _verifyNodejsVersion(locals) { 425 | return new Promise( 426 | (resolve, reject)=> { 427 | if (parseInt(((process.version).split("v"))[1].substr(0, 1)) < 4) { 428 | reject("\n The nodejs version is too low. This application requires\n" + 429 | " ES6 features in order to provision a cluster, specifically: \n" + 430 | " --promises \n --arrow functions \n --classes \n" + 431 | " Please upgrade the nodejs version from:\n --Current " + 432 | process.version + "\n --Minimum:4.0.0"); 433 | } else resolve(locals); 434 | }); 435 | } 436 | 437 | _finish() { 438 | return new Promise( 439 | (resolve, reject)=> { 440 | console.log("Cluster " + this.locals.endPoint + " provisioning complete. \n" + 441 | " To login to couchbase: open a browser " + this.locals.endPoint + "\n" + 442 | " To run the travel-sample application, run 'npm start'"); 443 | resolve("ok"); 444 | }); 445 | } 446 | } 447 | var c = new cluster(config); 448 | c.provision(); 449 | -------------------------------------------------------------------------------- /wait-for-couchbase.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # wait-for-couchbase.sh 3 | 4 | set -e 5 | 6 | CB_HOST="${CB_HOST:-db}" 7 | CB_USER="${CB_USER:-Administrator}" 8 | CB_PSWD="${CB_PSWD:-password}" 9 | 10 | 11 | #### Utility Functions #### 12 | # (see bottom of file for the calling script) 13 | 14 | log() { 15 | echo "wait-for-couchbase: $@" | cut -c -${COLUMNS:-80} 16 | } 17 | 18 | wait-for-one() { 19 | local ATTEMPTS=$1 20 | local URL=$2 21 | local QUERY=$3 22 | local EXPECTED=true 23 | local OUT=wait-for-couchbase.out 24 | 25 | for attempt in $(seq 1 $ATTEMPTS ) 26 | do 27 | status=$(curl -s -w "%{http_code}" -o $OUT -u "${CB_USER}:${CB_PSWD}" $URL) 28 | if [ $attempt -eq 1 ]; then 29 | log "polling for '$QUERY'" 30 | if [ $DEBUG ]; then jq . $OUT; fi 31 | elif (( $attempt % 5 == 0 )); then 32 | log "..." 33 | fi 34 | if [ "x$status" == "x200" ]; then 35 | result=$(jq "$QUERY" < $OUT) 36 | if [ "x$result" == "x$EXPECTED" ]; then 37 | return # success 38 | fi 39 | if [ $attempt -eq 1 ]; then 40 | log "value is currently:" 41 | jq . <<< "$result" 42 | fi 43 | fi 44 | sleep 2 45 | done 46 | return 1 # failure 47 | } 48 | 49 | wait-for() { 50 | local ATTEMPTS=$1 51 | local URL="http://${CB_HOST}${2}" 52 | shift; shift; 53 | 54 | log "checking $URL" 55 | 56 | for QUERY in "$@" 57 | do 58 | wait-for-one $ATTEMPTS $URL "$QUERY" || ( log "Failure"; exit 1 ) 59 | done 60 | return # success 61 | } 62 | 63 | function createHotelsIndex() { 64 | log "Creating hotels-index..." 65 | http_code=$(curl -o hotel-index.out -w '%{http_code}' -s -u ${CB_USER}:${CB_PSWD} -X PUT \ 66 | http://${CB_HOST}:8094/api/index/hotels-index \ 67 | -H 'cache-control: no-cache' \ 68 | -H 'content-type: application/json' \ 69 | -d @fts-hotels-index.json) 70 | if [[ $http_code -ne 200 ]]; then 71 | log Hotel index creation failed 72 | cat hotel-index.out 73 | exit 1 74 | fi 75 | } 76 | 77 | ##### Script starts here ##### 78 | ATTEMPTS=150 79 | 80 | wait-for $ATTEMPTS \ 81 | ":8091/pools/default/buckets/travel-sample/scopes/" \ 82 | '.scopes | map(.name) | contains(["inventory", "tenant_agent_00", "tenant_agent_01"])' 83 | 84 | wait-for $ATTEMPTS \ 85 | ":8094/api/cfg" \ 86 | '.status == "ok"' 87 | 88 | if (wait-for 1 ":8094/api/index/hotels-index" '.status == "ok"') 89 | then 90 | log "index already exists" 91 | else 92 | createHotelsIndex 93 | wait-for $ATTEMPTS \ 94 | ":8094/api/index/hotels-index/count" \ 95 | '.count >= 917' 96 | fi 97 | 98 | # now check that the indexes have had enough time to come up... 99 | wait-for $ATTEMPTS \ 100 | ":9102/api/v1/stats" \ 101 | '.indexer.indexer_state == "Active"' \ 102 | '. | keys | contains(["travel-sample:def_airportname", "travel-sample:def_city", "travel-sample:def_faa", "travel-sample:def_icao", "travel-sample:def_name_type", "travel-sample:def_primary", "travel-sample:def_route_src_dst_day", "travel-sample:def_schedule_utc", "travel-sample:def_sourceairport", "travel-sample:def_type", "travel-sample:inventory:airline:def_inventory_airline_primary", "travel-sample:inventory:airport:def_inventory_airport_airportname", "travel-sample:inventory:airport:def_inventory_airport_city", "travel-sample:inventory:airport:def_inventory_airport_faa", "travel-sample:inventory:airport:def_inventory_airport_primary", "travel-sample:inventory:hotel:def_inventory_hotel_city", "travel-sample:inventory:hotel:def_inventory_hotel_primary", "travel-sample:inventory:landmark:def_inventory_landmark_city", "travel-sample:inventory:landmark:def_inventory_landmark_primary", "travel-sample:inventory:route:def_inventory_route_primary", "travel-sample:inventory:route:def_inventory_route_route_src_dst_day", "travel-sample:inventory:route:def_inventory_route_schedule_utc", "travel-sample:inventory:route:def_inventory_route_sourceairport"])' \ 103 | '. | del(.indexer) | del(.["travel-sample:def_name_type"]) | map(.items_count > 0) | all' \ 104 | '. | del(.indexer) | map(.num_pending_requests == 0) | all' 105 | 106 | exec $@ --------------------------------------------------------------------------------