Skip to content

Commit b1ef85b

Browse files
authored
Merge pull request #935 from particle-iot/wt/cli-org-first
Wt/cli org first
2 parents d5aa427 + 252aeca commit b1ef85b

10 files changed

Lines changed: 172 additions & 72 deletions

File tree

‎src/cli/publish.js‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
'use strict';
2+
23
module.exports = ({ commandProcessor, root }) => {
34
commandProcessor.createCommand(root, 'publish', 'Publish an event to the cloud', {
45
params: '<event> [data]',
56
options: {
67
'product': {
78
description: 'Publish to the given Product ID or Slug\'s stream'
9+
},
10+
'org': {
11+
description: 'Specify the organization slug (e.g. my-org)'
812
}
913
},
1014
handler: (args) => {
1115
const PublishCommand = require('../cmd/publish');
1216
return new PublishCommand(args).publishEvent(args);
1317
},
1418
examples: {
15-
'$0 $command temp 25.0': 'Publish a temp event to your private event stream',
16-
'$0 $command temp 25.0 --product 12345': 'Publish a temp event to your product 12345\'s event stream'
19+
'$0 $command temp 25.0': 'Publish a temp event to your event stream',
20+
'$0 $command temp 25.0 --product 12345': 'Publish a temp event to your product 12345\'s event stream',
21+
'$0 $command temp 25.0 --org my-org': 'Publish a temp event to every product in organization my-org'
1722
}
1823
});
1924
};

‎src/cli/publish.test.js‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ describe('Publish Command-Line Interface', () => {
1717
const argv = commandProcessor.parse(root, ['publish', 'my-event']);
1818
expect(argv.clierror).to.equal(undefined);
1919
expect(argv.params).to.eql({ event: 'my-event', data: undefined });
20+
expect(argv.product).to.equal(undefined);
21+
expect(argv.org).to.equal(undefined);
2022
});
2123

2224
it('Parses optional arguments', () => {
@@ -32,6 +34,14 @@ describe('Publish Command-Line Interface', () => {
3234
expect(argv.product).to.equal('12345');
3335
});
3436

37+
it('Parses `--org`', () => {
38+
const argv = commandProcessor.parse(root, ['publish', 'my-event', 'my-data', '--org', 'my-org']);
39+
expect(argv.clierror).to.equal(undefined);
40+
expect(argv.params).to.eql({ event: 'my-event', data: 'my-data' });
41+
expect(argv.org).to.equal('my-org');
42+
expect(argv.product).to.equal(undefined);
43+
});
44+
3545
it('Errors when required `device` argument is missing', () => {
3646
const argv = commandProcessor.parse(root, ['publish']);
3747
expect(argv.clierror).to.be.an.instanceof(Error);
@@ -56,10 +66,12 @@ describe('Publish Command-Line Interface', () => {
5666
'',
5767
'Options:',
5868
' --product Publish to the given Product ID or Slug\'s stream [string]',
69+
' --org Specify the organization slug (e.g. my-org) [string]',
5970
'',
6071
'Examples:',
61-
' particle publish temp 25.0 Publish a temp event to your private event stream',
72+
' particle publish temp 25.0 Publish a temp event to your event stream',
6273
' particle publish temp 25.0 --product 12345 Publish a temp event to your product 12345\'s event stream',
74+
' particle publish temp 25.0 --org my-org Publish a temp event to every product in organization my-org',
6375
''
6476
].join('\n'));
6577
});

‎src/cli/subscribe.js‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
'use strict';
2+
23
module.exports = ({ commandProcessor, root }) => {
34
commandProcessor.createCommand(root, 'subscribe', 'Listen to device event stream', {
45
params: '[event]',
56
options: {
6-
'all': {
7-
boolean: true,
8-
description: 'Listen to all events instead of just those from my devices'
9-
},
107
'device': {
118
describe: 'Listen to events from this device only'
129
},
@@ -19,6 +16,9 @@ module.exports = ({ commandProcessor, root }) => {
1916
},
2017
'product': {
2118
description: 'Target a device within the given Product ID or Slug'
19+
},
20+
'org': {
21+
description: 'Specify the organization slug (e.g. my-org)'
2222
}
2323
},
2424
handler: (args) => {
@@ -29,8 +29,8 @@ module.exports = ({ commandProcessor, root }) => {
2929
'$0 $command': 'Subscribe to all event published by my devices',
3030
'$0 $command update': 'Subscribe to events starting with `update` from my devices',
3131
'$0 $command --product 12345': 'Subscribe to all events published by devices within product `12345`',
32+
'$0 $command --org my-org': 'Subscribe to all events published by devices within organization `my-org`',
3233
'$0 $command --device blue': 'Subscribe to all events published by device `blue`',
33-
'$0 $command --all': 'Subscribe to public events and all events published by my devices',
3434
'$0 $command --until data': 'Subscribe to all events and exit when an event has data matching `data`',
3535
'$0 $command --max 4': 'Subscribe to all events and exit after seeing `4` events'
3636
}

‎src/cli/subscribe.test.js‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,45 @@ describe('Subscribe Command-Line Interface', () => {
1717
const argv = commandProcessor.parse(root, ['subscribe']);
1818
expect(argv.clierror).to.equal(undefined);
1919
expect(argv.params).to.eql({ event: undefined });
20-
expect(argv.all).to.equal(false);
2120
expect(argv.device).to.equal(undefined);
2221
expect(argv.until).to.equal(undefined);
2322
expect(argv.max).to.equal(undefined);
23+
expect(argv.product).to.equal(undefined);
24+
expect(argv.org).to.equal(undefined);
2425
});
2526

2627
it('Parses optional params', () => {
2728
const argv = commandProcessor.parse(root, ['subscribe', 'test']);
2829
expect(argv.clierror).to.equal(undefined);
2930
expect(argv.params).to.eql({ event: 'test' });
30-
expect(argv.all).to.equal(false);
3131
expect(argv.device).to.equal(undefined);
3232
expect(argv.until).to.equal(undefined);
3333
expect(argv.max).to.equal(undefined);
3434
});
3535

3636
it('Parses options', () => {
37-
const args = ['subscribe', 'test', '--all', '--device', 'my-device', '--until', 'my-data', '--max', '3'];
37+
const args = ['subscribe', 'test', '--device', 'my-device', '--until', 'my-data', '--max', '3'];
3838
const argv = commandProcessor.parse(root, args);
3939
expect(argv.clierror).to.equal(undefined);
4040
expect(argv.params).to.eql({ event: 'test' });
41-
expect(argv.all).to.equal(true);
4241
expect(argv.device).to.equal('my-device');
4342
expect(argv.until).to.equal('my-data');
4443
expect(argv.max).to.equal(3);
4544
});
4645

46+
it('Parses `--org`', () => {
47+
const argv = commandProcessor.parse(root, ['subscribe', 'test', '--org', 'my-org']);
48+
expect(argv.clierror).to.equal(undefined);
49+
expect(argv.params).to.eql({ event: 'test' });
50+
expect(argv.org).to.equal('my-org');
51+
});
52+
53+
it('Rejects the removed `--all` flag as an unknown argument', () => {
54+
const argv = commandProcessor.parse(root, ['subscribe', 'test', '--all']);
55+
expect(argv.clierror).to.be.an.instanceof(Error);
56+
expect(argv.clierror.message).to.include('all');
57+
});
58+
4759
it('Includes help with examples', () => {
4860
const termWidth = null; // don't right-align option type labels so testing is easier
4961
commandProcessor.parse(root, ['subscribe', '--help'], termWidth);
@@ -53,18 +65,18 @@ describe('Subscribe Command-Line Interface', () => {
5365
'Usage: particle subscribe [options] [event]',
5466
'',
5567
'Options:',
56-
' --all Listen to all events instead of just those from my devices [boolean]',
5768
' --device Listen to events from this device only [string]',
5869
' --until Listen until we see an event exactly matching this data [string]',
5970
' --max Listen until we see this many events [number]',
6071
' --product Target a device within the given Product ID or Slug [string]',
72+
' --org Specify the organization slug (e.g. my-org) [string]',
6173
'',
6274
'Examples:',
6375
' particle subscribe Subscribe to all event published by my devices',
6476
' particle subscribe update Subscribe to events starting with `update` from my devices',
6577
' particle subscribe --product 12345 Subscribe to all events published by devices within product `12345`',
78+
' particle subscribe --org my-org Subscribe to all events published by devices within organization `my-org`',
6679
' particle subscribe --device blue Subscribe to all events published by device `blue`',
67-
' particle subscribe --all Subscribe to public events and all events published by my devices',
6880
' particle subscribe --until data Subscribe to all events and exit when an event has data matching `data`',
6981
' particle subscribe --max 4 Subscribe to all events and exit after seeing `4` events',
7082
''

‎src/cmd/api.js‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,24 +324,34 @@ module.exports = class ParticleApi {
324324
);
325325
}
326326

327-
getEventStream({ deviceId, name, product }){
327+
getEventStream({ deviceId, name, product, org }){
328328
return this._wrap(
329329
this.api.getEventStream({
330330
name,
331331
deviceId,
332332
product,
333+
org,
333334
auth: this.accessToken
334335
})
335336
);
336337
}
337338

338-
publishEvent({ name, data, product }){
339+
publishEvent({ name, data, product, org }){
340+
if (org){
341+
// particle-api-js has no org parameter for publishEvent; build the request here
342+
return this._wrap(
343+
this.api.post({
344+
uri: `/v1/orgs/${org}/events`,
345+
auth: this.accessToken,
346+
data: { name, data }
347+
})
348+
);
349+
}
339350
return this._wrap(
340351
this.api.publishEvent({
341352
name,
342353
data,
343354
product,
344-
isPrivate: true,
345355
auth: this.accessToken
346356
})
347357
);

‎src/cmd/api.test.js‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,66 @@ describe('ParticleApi', () => {
246246
});
247247
});
248248

249+
describe('getEventStream', () => {
250+
it('opens the user stream when no scope is given', async () => {
251+
const stub = sandbox.stub(particleApi.api, 'getEventStream').resolves({ body: 'stream' });
252+
253+
await particleApi.getEventStream({ deviceId: 'mine', name: 'led' });
254+
255+
expect(stub).to.have.been.calledWithMatch({ deviceId: 'mine', name: 'led', auth: 'test-token' });
256+
expect(stub.firstCall.args[0].org).to.equal(undefined);
257+
});
258+
259+
it('forwards a product scope', async () => {
260+
const stub = sandbox.stub(particleApi.api, 'getEventStream').resolves({ body: 'stream' });
261+
262+
await particleApi.getEventStream({ name: 'led', product: 'my-product' });
263+
264+
expect(stub).to.have.been.calledWithMatch({ product: 'my-product' });
265+
});
266+
267+
it('forwards an org scope', async () => {
268+
const stub = sandbox.stub(particleApi.api, 'getEventStream').resolves({ body: 'stream' });
269+
270+
await particleApi.getEventStream({ name: 'led', org: 'my-org' });
271+
272+
expect(stub).to.have.been.calledWithMatch({ org: 'my-org', name: 'led' });
273+
});
274+
});
275+
describe('publishEvent', () => {
276+
it('publishes an event to the user stream', async () => {
277+
const stub = sandbox.stub(particleApi.api, 'publishEvent').resolves({ body: { ok: true } });
278+
279+
await particleApi.publishEvent({ name: 'temp', data: '25.0' });
280+
281+
expect(stub).to.have.been.calledWithMatch({ name: 'temp', data: '25.0', auth: 'test-token' });
282+
expect(stub.firstCall.args[0]).to.not.have.property('isPrivate');
283+
expect(stub.firstCall.args[0].product).to.equal(undefined);
284+
});
285+
286+
it('forwards a product scope', async () => {
287+
const stub = sandbox.stub(particleApi.api, 'publishEvent').resolves({ body: { ok: true } });
288+
289+
await particleApi.publishEvent({ name: 'temp', data: '25.0', product: 'my-product' });
290+
291+
expect(stub).to.have.been.calledWithMatch({ product: 'my-product' });
292+
});
293+
294+
it('posts to the org events route when an org is given', async () => {
295+
const publishStub = sandbox.stub(particleApi.api, 'publishEvent');
296+
const postStub = sandbox.stub(particleApi.api, 'post').resolves({ body: { ok: true } });
297+
298+
const result = await particleApi.publishEvent({ name: 'temp', data: '25.0', org: 'my-org' });
299+
300+
expect(publishStub).to.not.have.been.called;
301+
expect(postStub).to.have.been.calledWithMatch({
302+
uri: '/v1/orgs/my-org/events',
303+
auth: 'test-token',
304+
data: { name: 'temp', data: '25.0' }
305+
});
306+
expect(result).to.deep.equal({ ok: true });
307+
});
308+
});
249309
describe('listEnv', () => {
250310
it('should call the correct API endpoint for sandbox', async () => {
251311
const expectedUri = '/v1/env';

‎src/cmd/publish.js‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,27 @@ module.exports = class PublishCommand extends CLICommandBase {
88
super(...args);
99
}
1010

11-
publishEvent({ product, params: { event, data } }){
12-
let epilogue = `private event: ${event}`;
11+
publishEvent({ product, org, params: { event, data } }){
12+
// An org event fans out to every product in the org, so naming one
13+
// product alongside it is contradictory and the API has no such route.
14+
if (org && product){
15+
return this.showUsageError(
16+
'`--org` cannot be combined with `--product`'
17+
);
18+
}
19+
20+
let epilogue = `event: ${event}`;
1321

1422
if (product){
1523
epilogue += ` to product: ${product}`;
24+
} else if (org){
25+
epilogue += ` to organization: ${org}`;
1626
}
1727

1828
const { api } = this._particleApi();
19-
const publishEvent = api.publishEvent({ name: event, data, product });
29+
const publishEvent = api.publishEvent({ name: event, data, product, org });
2030
return this.ui.showBusySpinnerUntilResolved(`Publishing ${epilogue}`, publishEvent)
2131
.then(() => this.ui.stdout.write(`Published ${epilogue}${os.EOL}${os.EOL}`));
2232
}
2333
};
34+

‎src/cmd/subscribe.js‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ module.exports = class SubscribeCommand extends CLICommandBase {
88
super(...args);
99
}
1010

11-
startListening({ device, all, until, max, product, params: { event } }){
12-
if (all && !event){
11+
startListening({ device, until, max, product, org, params: { event } }){
12+
// The API has org-wide and product/device streams, but no route that
13+
// combines an org with a product or a device.
14+
if (org && (product || device)){
1315
return this.showUsageError(
14-
'`event` parameter is required when `--all` flag is set'
16+
'`--org` cannot be combined with `--product` or `--device`'
1517
);
1618
}
1719

@@ -23,20 +25,18 @@ module.exports = class SubscribeCommand extends CLICommandBase {
2325

2426
const msg = ['Subscribing to'];
2527

26-
if (!device && !product && !all){
28+
if (!device && !product && !org){
2729
device = 'mine';
2830
}
2931

3032
if (event){
3133
msg.push(`"${event}"`);
32-
} else if (all){
33-
msg.push('public events');
3434
} else {
3535
msg.push('all events');
3636
}
3737

38-
if (all){
39-
msg.push('from the firehose (all devices) and my personal stream (my devices)');
38+
if (org){
39+
msg.push(`from organization ${org}'s stream`);
4040
} else if (device && product){
4141
msg.push(`from product ${product} device ${device}'s stream`);
4242
} else if (product){
@@ -60,7 +60,7 @@ module.exports = class SubscribeCommand extends CLICommandBase {
6060
}
6161

6262
const { api } = this._particleApi();
63-
const fetchStream = api.getEventStream({ deviceId: device, name: event, product });
63+
const fetchStream = api.getEventStream({ deviceId: device, name: event, product, org });
6464
return this.ui.showBusySpinnerUntilResolved('Fetching event stream...', fetchStream)
6565
.then(stream => {
6666
this.ui.stdout.write(os.EOL);
@@ -91,3 +91,4 @@ module.exports = class SubscribeCommand extends CLICommandBase {
9191
};
9292
}
9393
};
94+

0 commit comments

Comments
 (0)