Skip to content

Commit 7870634

Browse files
authored
feat(component): add support for ACL's and refactor component (#3)
* Refactor the component * Add support for ACLS * Fix test by creating memory datasouce when rabbit is not found * Add ACL to example config and enhance README * Add test for ACLs * chore: update and pin all package versions * fix: reject with an Error if unable to connect to Rabbit * test: reload server before every test * test: return promise for async tests * ci: enable rabbitmq-server service on circleci * test: fixup setupQueueConsumer and setupQueueProducer tests
1 parent 6b5525d commit 7870634

38 files changed

+702
-415
lines changed

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
save-exact=true

README.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ Loopback Component for working with a Message Queue
1717
{
1818
"loopback-component-mq": {
1919
"options": {
20-
"dataSource": "rabbit"
20+
"dataSource": "rabbit",
21+
"acls": [{
22+
"accessType": "*",
23+
"principalType": "ROLE",
24+
"principalId": "$unauthenticated",
25+
"permission": "DENY"
26+
}]
2127
},
2228
"topology": {
2329
"my-event-queue": {
@@ -56,6 +62,46 @@ Loopback Component for working with a Message Queue
5662
}
5763
```
5864

65+
## Configuration
66+
67+
The configuration of this component happens in `component-config.json`.
68+
69+
The 2 top-level keys are `options` and `topology` which are both objects.
70+
71+
### Options
72+
73+
The `options` object has 2 keys:
74+
75+
- `dataSource` (String, required). This is the name of the RabbitMQ datasource that is configured in `datasources.json`.
76+
Please note that this datasource does not use any `loopback-connector-*` packages, but just a way to tell the component
77+
how to connect to RabbitMQ.
78+
79+
- `acls` (Array, optional). Use this array to configure the ACL's. This ACL protects the Queue model that gets created.
80+
81+
### Topology
82+
83+
In the `topology` object you configure the queues served by this component. The object key is the name of the Queue.
84+
85+
Inside this queue definition you can define a `consumer`, a `producer`, or both. The `consumer` and `producer` objects
86+
both accept 2 properties, `model` and `method`.
87+
88+
#### Consumer
89+
90+
When you defined a `consumer` on a queue, the component will call into the `Model.method()` defined when a new message
91+
arrives on the queue. The method will be called with 2 parameters, `payload` and `ack`. Payload is the raw message from
92+
the queue. The `ack` parameter is a message that is used to acknowledge to the queue that the message is being handled.
93+
Make sure to acknowledge all messages, because RabbitMQ won't allow any other messages to be picked up until the message
94+
is acknowledged, making the queue come to a halt.
95+
96+
#### Producer
97+
98+
When defining a `producer` on a queue, the component will create the method `Model.method()` defined. The method created
99+
accepts 1 parameter, `payload`. The payload is the raw message that will be stored on the queue.
100+
101+
## TODO
102+
103+
- Add support for more types of Queues in the topology
104+
59105
## License
60106

61107
MIT

circle.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
machine:
22
node:
33
version: 6.1.0
4+
services:
5+
- docker
6+
pre:
7+
# Stop rabbitmq that comes with circle by default.
8+
- sudo service rabbitmq-server stop
9+
10+
dependencies:
11+
pre:
12+
# Get wait-for-it.sh.
13+
- git clone https://github.com/vishnubob/wait-for-it.git
14+
15+
database:
16+
override:
17+
# Start rabbitmq with support for management utils.
18+
- docker run -d -p 5672:5672 -p 15672:15672 rabbitmq:3-management
19+
420
test:
21+
override:
22+
- ./wait-for-it/wait-for-it.sh localhost:5672 -- npm test
523
post:
624
- npm run coverage
25+
726
deployment:
827
master:
928
branch: [master]

lib/index.js

Lines changed: 24 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2,98 +2,40 @@
22

33
const _ = require('lodash')
44
const debug = require('debug')('loopback:component:mq')
5-
const loopback = require('loopback')
65
const setupModel = require('./setup-model')
76
const Rabbit = require('./rabbit')
8-
let rabbit = null
9-
let msqQueue = null
10-
11-
function setupQueue(name) {
12-
debug('setupQueue: queue: %s', name)
13-
msqQueue = rabbit.exchange.queue({ name, durable: true })
14-
}
15-
16-
function setupQueueConsumer(app, queue, definition) {
17-
const modelName = definition.model
18-
const methodName = definition.method
19-
const Model = app.models[modelName]
20-
21-
// Check if the model exists
22-
if (!Model) {
23-
throw new Error(`setupQueueConsumer: Model not found: ${modelName}`)
24-
}
25-
26-
const Method = Model[methodName]
27-
28-
// Check if the method on the model exists
29-
if (!Method) {
30-
console.warn(`setupQueueConsumer: Method not found: ${modelName}.${methodName}`) // eslint-disable-line no-console
31-
}
32-
33-
// Start consuming the queue
34-
msqQueue.consume(Method)
35-
36-
debug('setupQueueConsumer: queue: %s, model: %s, method: %s', queue, modelName, methodName)
37-
}
38-
39-
function setupQueueProducer(app, queue, definition) {
40-
const modelName = definition.model
41-
const methodName = definition.method
42-
const Model = app.models[modelName]
43-
44-
if (!Model) {
45-
throw new Error(`setupQueueProducer: Model not found: ${modelName}`)
46-
}
47-
48-
debug('setupQueueProducer: queue: %s, model: %s, method: %s', queue, modelName, methodName)
49-
Model[methodName] = function queueProducer(params) {
50-
debug(`${modelName}.${methodName}(%o)`, params)
51-
rabbit.exchange.publish(params, { key: queue })
52-
}
53-
}
7+
const RabbitTopology = require('./rabbit-topology')()
548

559
module.exports = function loopbackComponentMq(app, config) {
56-
const options = config.options || {}
57-
const topology = config.topology || {}
58-
59-
debug('options: %o', options)
60-
debug('topology: %o', topology)
61-
62-
if (!options.dataSource) {
63-
debug('options.dataSource not set, using default value \'rabbit\'')
64-
options.dataSource = 'rabbit'
65-
}
66-
67-
const ds = app.dataSources[options.dataSource]
68-
69-
if (ds) {
70-
rabbit = new Rabbit(ds.settings.options || {})
10+
debug(config.topology)
11+
// Store the component configuration
12+
_.set(app, 'settings.loopback-component-mq', {
13+
options: config.options || {},
14+
topology: config.topology || {},
15+
})
7116

72-
// Loop through all the defined queues
73-
_.forEach(topology, (handlers, queue) => {
17+
debug('loopback-component-mq settings: %o', _.get(app, 'settings.loopback-component-mq'))
7418

75-
// Setup the actual queue on RabbitMQ
76-
setupQueue(queue)
19+
// Independent of app boot, add model and ACL
20+
setupModel(app)
7721

78-
// Setup the consumer of this queue
79-
if (handlers.consumer) {
80-
setupQueueConsumer(app, queue, handlers.consumer)
81-
}
22+
// Wait the app to be booted
23+
app.once('booted', () => {
8224

83-
// Setup the producers of this queue
84-
if (handlers.producer) {
85-
setupQueueProducer(app, queue, handlers.producer)
86-
}
25+
// Get a reference to the created model
26+
const Queue = app.models.Queue
8727

88-
})
28+
// Get a reference to the configured datasource
29+
const dsName = _.get(app, 'settings.loopback-component-mq.options.dataSource', 'rabbit')
30+
const ds = app.dataSources[dsName]
31+
const dsOptions = _.get(ds, 'settings.options')
8932

90-
setupModel(app, ds, rabbit, topology)
91-
}
92-
else {
93-
debug(`DataSource ${options.dataSource} not found`)
94-
const memDs = loopback.createDataSource({ connector: 'memory' })
33+
// If we have a datasource, wire up Rabbit and set up the topology
34+
if (ds && dsOptions) {
35+
Queue.rabbit = new Rabbit(dsOptions)
36+
RabbitTopology.setupTopology(Queue)
37+
}
9538

96-
setupModel(app, memDs, {}, {})
97-
}
39+
})
9840

9941
}

lib/models/queue.js

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,67 @@
11
const Promise = require('bluebird')
22

3-
module.exports = function queueFn(Queue, rabbit, topology) {
3+
module.exports = function queueFn(Queue) {
4+
5+
// The topology
6+
Queue.topology = {}
7+
// The rabbit
8+
Queue.rabbit = null
49

510
Queue.status = function status() {
611
return new Promise((resolve, reject) => {
7-
rabbit.stats.overview((err, res, data) => {
12+
if (!Queue.rabbit) {
13+
return reject(new Error('Error connecting to rabbit'))
14+
}
15+
return Queue.rabbit.stats.overview((err, res, data) => {
816
if (err) {
9-
reject(err)
17+
return reject(err)
1018
}
1119
data = data || {}
12-
resolve(data)
20+
return resolve(data)
1321
})
1422
})
1523
}
1624

1725
Queue.queues = function queues() {
1826
return new Promise((resolve, reject) => {
19-
rabbit.stats.queues((err, res, data) => {
27+
if (!Queue.rabbit) {
28+
return reject(new Error('Error connecting to rabbit'))
29+
}
30+
return Queue.rabbit.stats.queues((err, res, data) => {
2031
if (err) {
21-
reject(err)
32+
return reject(err)
2233
}
2334
// Only show queues that are defined in the topology
2435
data = data || []
25-
data = data.filter(item => Object.keys(topology).includes(item.name))
26-
resolve(data)
36+
data = data.filter(item => Object.keys(Queue.topology).includes(item.name))
37+
return resolve(data)
2738
})
2839
})
2940
}
3041

42+
43+
Queue.remoteMethod('status', {
44+
isStatic: true,
45+
description: 'Get an status overview of the connected rabbitmq server',
46+
returns: {
47+
arg: 'result',
48+
type: 'Object',
49+
root: true,
50+
},
51+
http: { path: '/status', verb: 'get' },
52+
})
53+
54+
55+
Queue.remoteMethod('queues', {
56+
isStatic: true,
57+
description: 'Get queues of connected rabbitmq server',
58+
returns: {
59+
arg: 'result',
60+
type: 'Object',
61+
root: true,
62+
},
63+
http: { path: '/queues', verb: 'get' },
64+
})
65+
3166
return Queue
3267
}

lib/models/queue.json

Lines changed: 0 additions & 43 deletions
This file was deleted.

0 commit comments

Comments
 (0)