Skip to content

Instantly share code, notes, and snippets.

@joeyjiron06
Last active July 22, 2020 00:29
Show Gist options
  • Save joeyjiron06/9bc11cc9c106b19629221a98f62b4674 to your computer and use it in GitHub Desktop.
Save joeyjiron06/9bc11cc9c106b19629221a98f62b4674 to your computer and use it in GitHub Desktop.
const Couchbase = require('couchbase');
const Promise = require('bluebird');
class Bucket {
constructor(options) {
this.options = options;
}
async connect() {
const {
clusterUrl, username, password, bucketName,
} = this.options;
const cluster = new Couchbase.Cluster(clusterUrl);
cluster.authenticate(username, password);
const bucket = await new Promise((resolve, reject) => {
const bkt = cluster.openBucket(bucketName, (err) => {
if (err) {
reject(err);
} else {
resolve(bkt);
}
});
});
const manager = bucket.manager();
this.bucket = bucket;
// METHODS come from here:
// https://docs.couchbase.com/sdk-api/couchbase-node-client/Bucket.html
this.get = Promise.promisify(bucket.get, { context: bucket });
this.upsert = Promise.promisify(bucket.upsert, { context: bucket });
this.insert = Promise.promisify(bucket.insert, { context: bucket });
this.replace = Promise.promisify(bucket.replace, { context: bucket });
this.remove = Promise.promisify(bucket.remove, { context: bucket });
this.getMulti = Promise.promisify(bucket.getMulti, { context: bucket });
this.query = Promise.promisify(bucket.query, { context: bucket });
this.listAppend = Promise.promisify(bucket.listAppend, { context: bucket });
this.listGet = Promise.promisify(bucket.listGet, { context: bucket });
this.listPrepend = Promise.promisify(bucket.listPrepend, { context: bucket });
this.listRemove = Promise.promisify(bucket.listRemove, { context: bucket });
this.listSet = Promise.promisify(bucket.listSet, { context: bucket });
this.listSize = Promise.promisify(bucket.listSize, { context: bucket });
this.mapAdd = Promise.promisify(bucket.mapAdd, { context: bucket });
this.mapGet = Promise.promisify(bucket.mapGet, { context: bucket });
this.mapRemove = Promise.promisify(bucket.mapRemove, { context: bucket });
this.mapSize = Promise.promisify(bucket.mapSize, { context: bucket });
this.queuePop = Promise.promisify(bucket.queuePop, { context: bucket });
this.queuePush = Promise.promisify(bucket.queuePush, { context: bucket });
this.queueSize = Promise.promisify(bucket.queueSize, { context: bucket });
this.setAdd = Promise.promisify(bucket.setAdd, { context: bucket });
this.setExists = Promise.promisify(bucket.setExists, { context: bucket });
this.setRemove = Promise.promisify(bucket.setRemove, { context: bucket });
this.setSize = Promise.promisify(bucket.setSize, { context: bucket });
this.touch = Promise.promisify(bucket.touch, { context: bucket });
this.unlock = Promise.promisify(bucket.unlock, { context: bucket });
this.ping = Promise.promisify(bucket.ping, { context: bucket });
this.getReplica = Promise.promisify(bucket.getReplica, { context: bucket });
this.getAndTouch = Promise.promisify(bucket.getAndTouch, { context: bucket });
this.getAndLock = Promise.promisify(bucket.getAndLock, { context: bucket });
this.counter = Promise.promisify(bucket.counter, { context: bucket });
this.append = Promise.promisify(bucket.append, { context: bucket });
this.flush = Promise.promisify(manager.flush, { context: manager });
this.mutateIn = (...args) => {
const mutateBuilder = bucket.mutateIn(...args);
mutateBuilder.execute = Promise.promisify(mutateBuilder.execute, { context: mutateBuilder });
return mutateBuilder;
};
this.lookupIn = (...args) => {
const lookupBuilder = bucket.lookupIn(...args);
lookupBuilder.execute = Promise.promisify(lookupBuilder.execute, { context: lookupBuilder });
return lookupBuilder;
};
this.diagnostics = Promise.promisify(bucket.diagnostics, {
context: bucket,
});
}
async disconnect() {
this.bucket.disconnect();
// wait a little. unfortunately bucket.disconnect doesnt take
// a callback and does not return a promise even though it's
// an async behavior. instead we just give it a little time
// to do it's thing.
await new Promise(resolve => setTimeout(resolve, 500));
}
}
module.exports = Bucket;
const { execSync } = require('child_process');
class Container {
constructor(id, config) {
this.id = id;
this.config = config;
}
start() {
return execSync(`docker run \
-d \
--name \
${this.config.name} \
${this.config.ports.map(port => ` -p ${port} `).join(' ')} \
${this.config.image}
`).toString();
}
stop() {
return execSync(`docker container stop ${this.id}`).toString();
}
remove(force = false) {
return execSync(`docker container rm ${force ? '--force' : ''} ${this.id}`).toString();
}
exec(args) {
return execSync(`docker exec ${this.id} ${args.join(' ')}`).toString();
}
}
module.exports = Container;
const url = require('url');
const axios = require('axios');
const waitUntil = require('./waitUntil');
const Docker = require('./docker');
class CouchbaseDocker {
constructor(options) {
this.options = {
logger: console,
containerName: 'couchbase-for-testing',
username: process.env.COUCHBASE_USER || 'admin',
password: process.env.COUCHBASE_PASSWORD || 'password',
baseUrl: 'http://localhost:8091',
queryBaseUrl: 'http://localhost:8093', // port 8093 for n1ql queries
serverTimeoutMs: 60000,
services: 'data,index,query,fts',
ports: [
'8091-8096:8091-8096',
'11210-11211:11210-11211',
],
version: '6.0.2', // liveperson uses this couchbase version in qa and prod
...options,
};
}
get clusterHostname() {
return url.parse(this.options.baseUrl).hostname;
}
container() {
return Docker.findContainer(this.options.containerName);
}
async start() {
const { logger, baseUrl, serverTimeoutMs } = this.options;
this.deleteContainer();
logger.info('starting container...');
this.createAndStartContainer();
logger.info('waiting for couchbase to start...');
await waitUntil(() => axios.get(baseUrl), serverTimeoutMs);
logger.info('creating cluster...');
await this.createCluster({
services: 'data,index,query,fts',
clusterRamSize: '256',
indexRamSize: '256',
});
logger.info('couchbase ready at ', baseUrl);
}
async stop() {
const container = await this.container();
await container.stop();
}
async exec(cmd) {
const container = await this.container();
const exec = await container.exec.create({
AttachStdout: true,
AttachStderr: true,
Cmd: cmd,
});
const stream = await exec.start({ Detach: false });
return new Promise((resolve, reject) => {
stream.on('data', data => this.options.logger.log(data.toString()));
stream.on('end', resolve);
stream.on('error', reject);
});
}
async createAndStartContainer() {
const config = {
image: `couchbase:${this.options.version}`,
name: this.options.containerName,
// ports needed by couchbae
ports: [
'8091-8096:8091-8096',
'11210-11211:11210-11211',
],
};
Docker.createContainer(config).start();
}
async deleteContainer() {
const container = this.container();
if (container) {
this.options.logger.log('deleting existing container');
container.remove(true);
}
}
createCluster({ services, clusterRamSize = '256', indexRamSize = '256' }) {
return this.container().exec([
'couchbase-cli',
'cluster-init',
'--cluster',
this.clusterHostname,
'--cluster-username',
this.options.username,
'--cluster-password',
this.options.password,
'--services',
services,
'--cluster-ramsize',
clusterRamSize,
'--cluster-index-ramsize',
indexRamSize,
]);
}
createBucket(name) {
return this.container().exec([
'couchbase-cli',
'bucket-create',
'--cluster',
this.clusterHostname,
'--username',
this.options.username,
'--password',
this.options.password,
'--bucket',
name,
'--bucket-type',
'couchbase',
'--bucket-ramsize',
'256',
'--enable-flush',
'1',
'--wait',
]);
}
flushBucket(name) {
return this.container().exec([
'couchbase-cli',
'bucket-flush',
'--cluster',
this.clusterHostname,
'--username',
this.options.username,
'--password',
this.options.password,
'--bucket',
name,
'--force', // prevents cli from asking confirmation question which errors this command
]);
}
deleteBucket(name) {
return this.container().exec([
'couchbase-cli',
'bucket-delete',
'--cluster',
this.clusterHostname,
'--username',
this.options.username,
'--password',
this.options.password,
'--bucket',
name,
]);
}
n1ql(script) {
return this.container().exec([
'cbq',
'--engine',
this.options.queryBaseUrl,
'-user',
this.options.username,
'-password',
this.options.password,
'--script',
`"${script.replace(new RegExp('`', 'g'), '\\`').replace(new RegExp('"', 'g'), '\\"')}"`,
]);
}
}
module.exports = new CouchbaseDocker();

CouchbaseDocker

A simple nodejs wrapper for running couchbase locally. Can be used for test suites or running against local server.

Example Usage

const CouchbaseDocker = require('./couchbaseDocker');

async function main() {
  await CouchbaseDocker.start();
  await CouchbaseDocker.createCluster();
  await CouchbaseDocker.createBucket('my-bucket');
  await CoucbaseDocker.n1ql(`CREATE INDEX my-index in my-bucket on user.id`);
}

main();
const { execSync } = require('child_process');
const Container = require('./container');
class Docker {
static createContainer(config) {
return new Container(undefined, config);
}
static findContainer(substring) {
const result = execSync('docker container list --all').toString();
const foundLine = result.split('\n')
.find(line => line.includes(substring));
if (foundLine) {
const containerId = foundLine.split(' ')[0];
return new Container(containerId);
}
return undefined;
}
}
module.exports = Docker;
// AES Encryption/Decryption with AES-256-GCM using random Initialization Vector + Salt
// ----------------------------------------------------------------------------------------
// the encrypted datablock is base64 encoded for easy data exchange.
// if you have the option to store data binary save consider to remove the encoding to
// reduce storage size
// ----------------------------------------------------------------------------------------
// format of encrypted data - used by this example. not an official format
//
// +--------------------+-----------------------+----------------+----------------+
// | SALT | Initialization Vector | Auth Tag | Payload |
// | Used to derive key | AES GCM XOR Init | Data Integrity | Encrypted Data |
// | 64 Bytes, random | 16 Bytes, random | 16 Bytes | (N-96) Bytes |
// +--------------------+-----------------------+----------------+----------------+
//
// ----------------------------------------------------------------------------------------
// Input/Output Vars
//
// MASTERKEY: the key used for encryption/decryption.
// it has to be cryptographic safe - this means randomBytes or
// derived by pbkdf2 (for example)
// TEXT: data (utf8 string) which should be encoded.
// modify the code to use Buffer for binary data!
// ENCDATA: encrypted data as base64 string (format mentioned on top)
// load the build-in crypto functions
const crypto = require('crypto');
const ALGORITHM = 'aes-256-gcm';
/**
* Encrypts text by given key
* @param String text to encrypt
* @param Buffer masterkey
* @returns String encrypted text, base64 encoded
*/
function encrypt(text, masterkey) {
// random initialization vector
const iv = crypto.randomBytes(16);
// random salt
const salt = crypto.randomBytes(64);
// derive key: 32 byte key length - in assumption the masterkey is a cryptographic
// and NOT a password there is no need for
// a large number of iterations. It may can replaced by HKDF
const key = crypto.pbkdf2Sync(masterkey, salt, 2145, 32, 'sha512');
// AES 256 GCM Mode
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
// encrypt the given text
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
// extract the auth tag
const tag = cipher.getAuthTag();
// generate output
return Buffer.concat([salt, iv, tag, encrypted]).toString('base64');
}
/**
* Decrypts text by given key
* @param String base64 encoded input data
* @param Buffer masterkey
* @returns String decrypted (original) text
*/
function decrypt(encdata, masterkey) {
// base64 decoding
const bData = Buffer.from(encdata, 'base64');
// convert data to buffers
const salt = bData.slice(0, 64);
const iv = bData.slice(64, 80);
const tag = bData.slice(80, 96);
const text = bData.slice(96);
// derive key using; 32 byte key length
const key = crypto.pbkdf2Sync(masterkey, salt, 2145, 32, 'sha512');
// AES 256 GCM Mode
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(tag);
// encrypt the given text
const decrypted = decipher.update(text, 'binary', 'utf8') + decipher.final('utf8');
return decrypted;
}
module.exports = {
encrypt,
decrypt,
};
const Couchbase = require('couchbase');
const CouchbaseDocker = require('./CouchbaseDocker');
const Bucket = require('./Bucket');
const encryption = require('./encryption');
module.exports = {
...Couchbase,
CouchbaseDocker,
Bucket,
...encryption,
};
/* eslint-disable no-await-in-loop */
module.exports = async function waitUntil(fn, timeoutMillis) {
const delayMillis = 1000;
const start = Date.now();
while (Date.now() - start < timeoutMillis) {
try {
return await fn();
} catch (e) {
await new Promise(resolve => setTimeout(resolve, delayMillis));
}
}
throw new Error(`timed out after ${timeoutMillis}ms. You can consider setting a longer timeout`);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment