Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: updated database connection for use with CouchDB #104

Merged
merged 8 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
359 changes: 338 additions & 21 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"dotenv": "^16.3.1",
"fastify": "4.23.2",
"mongodb": "^6.5.0",
"nano": "^10.1.4",
"nodemon": "^2.0.20",
"sdp-transform": "^2.14.1",
"ts-node": "^10.9.1",
Expand Down
21 changes: 11 additions & 10 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@ Intercom solution powered by Symphony Media Bridge. This is the Intercom manager
## Requirements

- A Symphony Media Bridge running and reachable
- A MongoDB server
- A MongoDB server or CouchDB server
- Docker engine

## Environment variables

| Variable name | Description |
| --------------------------- | --------------------------------------------------------------------------------- |
| `PORT` | Intercom-Manager API port |
| `SMB_ADDRESS` | The address:port of the Symphony Media Bridge instance |
| `SMB_APIKEY` | When set, provide this API key for the Symphony Media Bridge (optional) |
| `MONGODB_CONNECTION_STRING` | MongoDB connection string (default: `mongodb://localhost:27017/intercom-manager`) |
| Variable name | Description |
| --------------------------- | ---------------------------------------------------------------------------- |
| `PORT` | Intercom-Manager API port |
| `SMB_ADDRESS` | The address:port of the Symphony Media Bridge instance |
| `SMB_APIKEY` | When set, provide this API key for the Symphony Media Bridge (optional) |
| `DB_CONNECTION_STRING` | DB connection string (default: `mongodb://localhost:27017/intercom-manager`) |
| `MONGODB_CONNECTION_STRING` | DEPRECATED: MongoDB connection string |

## Installation / Usage

Expand All @@ -29,15 +30,15 @@ Start an Intercom Manager instance:
docker run -d -p 8000:8000 \
-e PORT=8000 \
-e SMB_ADDRESS=http://<smburl>:<smbport> \
-e MONGODB_CONNECTION_STRING=mongodb://<host>:<port>/<db-name> \
-e DB_CONNECTION_STRING=<mongodb|http>://<host>:<port>/<db-name> \
eyevinntechnology/intercom-manager
```

The API docs is then available on `http://localhost:8000/api/docs/`

## Development

Requires Node JS engine >= v18 and [MongoDB](https://www.mongodb.com/docs/manual/administration/install-community/) (tested with MongoDB v7).
Requires Node JS engine >= v18 and [MongoDB](https://www.mongodb.com/docs/manual/administration/install-community/) (tested with MongoDB v7) or [CouchDB](https://docs.couchdb.org/en/stable/index.html).

Install dependencies

Expand All @@ -57,7 +58,7 @@ Start server locally
SMB_ADDRESS=http://<smburl>:<smbport> SMB_APIKEY=<smbapikey> npm start
```

See [Environment Variables](#environment-variables) for a full list of environment variables you can set. The default `MONGODB_CONNECTION_STRING` is probably what you want to use for local development unless you use a remote db server.
See [Environment Variables](#environment-variables) for a full list of environment variables you can set. The default `DB_CONNECTION_STRING` is probably what you want to use for local development unless you use a remote db server.

## Terraform infrastructure

Expand Down
2 changes: 1 addition & 1 deletion src/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import api from './api';

jest.mock('./db_manager');
jest.mock('./db/interface');

describe('api', () => {
it('responds with hello, world!', async () => {
Expand Down
18 changes: 17 additions & 1 deletion src/api_productions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,25 @@ import { v4 as uuidv4 } from 'uuid';
import { ConnectionQueue } from './connection_queue';
import { CoreFunctions } from './api_productions_core_functions';
import { Log } from './log';
import { DbManagerMongoDb } from './db/mongodb';
import { DbManagerCouchDb } from './db/couchdb';
dotenv.config();

const productionManager = new ProductionManager();
const DB_CONNECTION_STRING: string =
process.env.DB_CONNECTION_STRING ??
process.env.MONGODB_CONNECTION_STRING ??
'mongodb://localhost:27017/intercom-manager';
let dbManager;
const dbUrl = new URL(DB_CONNECTION_STRING);
if (dbUrl.protocol === 'mongodb:') {
dbManager = new DbManagerMongoDb(dbUrl);
} else if (dbUrl.protocol === 'http:' || dbUrl.protocol === 'https:') {
dbManager = new DbManagerCouchDb(dbUrl);
} else {
throw new Error('Unsupported database protocol');
}

const productionManager = new ProductionManager(dbManager);
const connectionQueue = new ConnectionQueue();
const coreFunctions = new CoreFunctions(productionManager, connectionQueue);

Expand Down
124 changes: 124 additions & 0 deletions src/db/couchdb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Line, Production } from '../models';
import { assert } from '../utils';
import { DbManager } from './interface';
import nano from 'nano';

export class DbManagerCouchDb implements DbManager {
private client;
private nanoDb;

constructor(dbConnectionUrl: URL) {
const server = new URL('/', dbConnectionUrl).toString();
this.client = nano(server);
this.nanoDb = this.client.db.use(
dbConnectionUrl.pathname.replace(/^\//, '')
);
}

async connect(): Promise<void> {
// CouchDB does not require a connection
}

async disconnect(): Promise<void> {
// CouchDB does not require a disconnection
}

private async getNextSequence(collectionName: string): Promise<number> {
const counterDocId = `counter_${collectionName}`;
interface CounterDoc {
_id: string;
_rev?: string;
value: string;
}
let counterDoc: CounterDoc;

try {
counterDoc = (await this.nanoDb.get(counterDocId)) as CounterDoc;
counterDoc.value = (parseInt(counterDoc.value) + 1).toString();
} catch (error) {
counterDoc = { _id: counterDocId, value: '1' };
}
await this.nanoDb.insert(counterDoc);
return parseInt(counterDoc.value, 10);
}

/** Get all productions from the database in reverse natural order, limited by the limit parameter */
async getProductions(limit: number, offset: number): Promise<Production[]> {

Check warning on line 46 in src/db/couchdb.ts

View workflow job for this annotation

GitHub Actions / lint

'limit' is defined but never used

Check warning on line 46 in src/db/couchdb.ts

View workflow job for this annotation

GitHub Actions / lint

'offset' is defined but never used
const productions: Production[] = [];
const response = await this.nanoDb.list({
include_docs: true
});
response.rows.forEach((row: any) => {

Check warning on line 51 in src/db/couchdb.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
if (row.doc._id.toLowerCase().indexOf('counter') === -1)
productions.push(row.doc);
});
return productions as any as Production[];

Check warning on line 55 in src/db/couchdb.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
}

async getProductionsLength(): Promise<number> {
const productions = await this.nanoDb.list({ include_docs: false });
return productions.rows.length;
}

async getProduction(id: number): Promise<Production | undefined> {
const production = await this.nanoDb.get(id.toString());
return production as any | undefined;

Check warning on line 65 in src/db/couchdb.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
}

async updateProduction(
production: Production
): Promise<Production | undefined> {
const existingProduction = await this.nanoDb.get(production._id.toString());
const updatedProduction = {
...existingProduction,
...production,
_id: production._id.toString()
};
const response = await this.nanoDb.insert(updatedProduction);
return response.ok ? production : undefined;
}

async addProduction(name: string, lines: Line[]): Promise<Production> {
const _id = await this.getNextSequence('productions');
if (_id === -1) {
throw new Error('Failed to get next sequence');
}
const insertProduction = { name, lines, _id: _id.toString() };
const response = await this.nanoDb.insert(
insertProduction as unknown as nano.MaybeDocument
);
if (!response.ok) throw new Error('Failed to insert production');
return { name, lines, _id } as Production;
}

async deleteProduction(productionId: number): Promise<boolean> {
const production = await this.nanoDb.get(productionId.toString());
const response = await this.nanoDb.destroy(production._id, production._rev);
return response.ok;
}

async setLineConferenceId(
productionId: number,
lineId: string,
conferenceId: string
): Promise<void> {
const production = await this.getProduction(productionId);
assert(production, `Production with id "${productionId}" does not exist`);
const line = production.lines.find((line) => line.id === lineId);
assert(
line,
`Line with id "${lineId}" does not exist for production with id "${productionId}"`
);
line.smbConferenceId = conferenceId;
const existingProduction = await this.nanoDb.get(productionId.toString());
const updatedProduction = {
...existingProduction,
lines: production.lines
};
const response = await this.nanoDb.insert(updatedProduction);
assert(
response.ok,
`Failed to update production with id "${productionId}"`
);
}
}
17 changes: 17 additions & 0 deletions src/db/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Line, Production } from '../models';

export interface DbManager {
connect(): Promise<void>;
disconnect(): Promise<void>;
getProduction(id: number): Promise<Production | undefined>;
getProductions(limit: number, offset: number): Promise<Production[]>;
getProductionsLength(): Promise<number>;
updateProduction(production: Production): Promise<Production | undefined>;
addProduction(name: string, lines: Line[]): Promise<Production>;
deleteProduction(productionId: number): Promise<boolean>;
setLineConferenceId(
productionId: number,
lineId: string,
conferenceId: string
): Promise<void>;
}
72 changes: 39 additions & 33 deletions src/db_manager.ts → src/db/mongodb.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,38 @@
import { MongoClient } from 'mongodb';
import { Line, Production } from './models';
import { assert } from './utils';
import { DbManager } from './interface';
import { Line, Production } from '../models';
import { assert } from '../utils';

const MONGODB_CONNECTION_STRING: string =
process.env.MONGODB_CONNECTION_STRING ??
'mongodb://localhost:27017/intercom-manager';
export class DbManagerMongoDb implements DbManager {
private client: MongoClient;

const client = new MongoClient(MONGODB_CONNECTION_STRING);
const db = client.db();

async function getNextSequence(collectionName: string): Promise<number> {
const ret = await db.command({
findAndModify: 'counters',
query: { _id: collectionName },
update: { $inc: { seq: 1 } },
new: true,
upsert: true
});
return ret.value.seq;
}
constructor(dbConnectionUrl: URL) {
this.client = new MongoClient(dbConnectionUrl.toString());
}

const dbManager = {
async connect(): Promise<void> {
await client.connect();
},
await this.client.connect();
}

async disconnect(): Promise<void> {
await client.close();
},
await this.client.close();
}

private async getNextSequence(collectionName: string): Promise<number> {
const db = this.client.db();
const ret = await db.command({
findAndModify: 'counters',
query: { _id: collectionName },
update: { $inc: { seq: 1 } },
new: true,
upsert: true
});
return ret.value.seq;
}

/** Get all productions from the database in reverse natural order, limited by the limit parameter */
async getProductions(limit: number, offset: number): Promise<Production[]> {
const db = this.client.db();
const productions = await db
.collection('productions')
.find()
Expand All @@ -39,41 +41,46 @@
.limit(limit)
.toArray();

return productions as any as Production[];

Check warning on line 44 in src/db/mongodb.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
},
}

async getProductionsLength(): Promise<number> {
const db = this.client.db();
return await db.collection('productions').countDocuments();
},
}

async getProduction(id: number): Promise<Production | undefined> {
const db = this.client.db();
return db.collection('productions').findOne({ _id: id as any }) as

Check warning on line 54 in src/db/mongodb.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
| any
| undefined;
},
}

async updateProduction(
production: Production
): Promise<Production | undefined> {
const db = this.client.db();
const result = await db
.collection('productions')
.updateOne({ _id: production._id as any }, { $set: production });
return result.modifiedCount === 1 ? production : undefined;
},
}

async addProduction(name: string, lines: Line[]): Promise<Production> {
const _id = await getNextSequence('productions');
const db = this.client.db();
const _id = await this.getNextSequence('productions');
const production = { name, lines, _id };
await db.collection('productions').insertOne(production as any);
return production;
},
}

async deleteProduction(productionId: number): Promise<boolean> {
const db = this.client.db();
const result = await db
.collection('productions')
.deleteOne({ _id: productionId as any });
return result.deletedCount === 1;
},
}

async setLineConferenceId(
productionId: number,
Expand All @@ -88,13 +95,12 @@
`Line with id "${lineId}" does not exist for production with id "${productionId}"`
);
line.smbConferenceId = conferenceId;
const db = this.client.db();
await db
.collection('productions')
.updateOne(
{ _id: productionId as any },
{ $set: { lines: production.lines } }
);
}
};

export default dbManager;
}
Loading
Loading