Skip to content

Commit 234776e

Browse files
author
Karl Snyder
authored
Merge pull request #1 from flocasts/master
Merge master to ssl.
2 parents f505a9b + 49a522d commit 234776e

File tree

7 files changed

+202
-13
lines changed

7 files changed

+202
-13
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
node_modules/
2+
coverage/

__tests__/eval.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const test = require('tape')
2+
const sinon = require('sinon')
3+
const evalInContext = require('../eval')
4+
5+
test('evalInContext - evals global variable', (t) => {
6+
global.property = 'property'
7+
t.equal(evalInContext('property', {}), 'property')
8+
t.end()
9+
})
10+
11+
test('evalInContext - evals function in context', (t) => {
12+
const clientid = sinon.stub().returns('test')
13+
t.equal(evalInContext('clientid()', { clientid }), 'test')
14+
t.end()
15+
})
16+
17+
test('throws error if variable does not exist', (t) => {
18+
t.throws((() => evalInContext('notHere', {})))
19+
t.end()
20+
})

__tests__/sql.test.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
const test = require('tape')
2+
const sinon = require('sinon')
3+
const { parseSelect, applySelect } = require('../sql.js')
4+
5+
test('parseSelect - parses simple SQL correctly', (t) => {
6+
const subject = "SELECT * FROM 'topic'"
7+
const results = parseSelect(subject)
8+
t.deepEqual(results.select, [{ field: '*', alias: undefined }])
9+
t.equal(results.topic, 'topic')
10+
t.equal(results.where, undefined)
11+
t.end()
12+
})
13+
14+
test('parseSelect - parses lowercase simple SQL correctly', (t) => {
15+
const subject = "select * from 'topic'"
16+
const results = parseSelect(subject)
17+
t.deepEqual(results.select, [{ field: '*', alias: undefined }])
18+
t.equal(results.topic, 'topic')
19+
t.equal(results.where, undefined)
20+
t.end()
21+
})
22+
23+
test('parseSelect - parses where clause correctly', (t) => {
24+
const subject = "SELECT * FROM 'topic' WHERE name='Bob'"
25+
const results = parseSelect(subject)
26+
t.deepEqual(results.select, [{ field: '*', alias: undefined }])
27+
t.equal(results.topic, 'topic')
28+
t.equal(results.where, "name='Bob'")
29+
t.end()
30+
})
31+
32+
test('parseSelect - parses multiple SELECT properties correctly', (t) => {
33+
const subject = "SELECT name, age, maleOrFemale AS gender FROM 'topic'"
34+
const results = parseSelect(subject)
35+
t.deepEqual(results.select, [
36+
{ field: 'name', alias: undefined},
37+
{ field: 'age', alias: undefined },
38+
{ field: 'maleOrFemale', alias: 'gender'}
39+
])
40+
t.end()
41+
})
42+
43+
test('applySelect - Simple select with buffered string handled correctly', (t) => {
44+
const select = [{ field: '*', alias: undefined }]
45+
const payload = Buffer.from(JSON.stringify({name: 'Bob'}), 'utf8')
46+
const context = {}
47+
const event = applySelect({ select, payload, context })
48+
t.deepEqual(event, { name: 'Bob' })
49+
t.end()
50+
})
51+
52+
test('applySelect - Simple select with non-JSON handled correctly', (t) => {
53+
const select = [{ field: '*', alias: undefined }]
54+
const payload = 'Bob'
55+
const context = {}
56+
const event = applySelect({ select, payload, context })
57+
t.equal(event, 'Bob')
58+
t.end()
59+
})
60+
61+
test('applySelect - Aliased wildcard with non-JSON handled correctly', (t) => {
62+
const select = [{ field: '*', alias: 'name' }]
63+
const payload = 'Bob'
64+
const context = {}
65+
const event = applySelect({ select, payload, context })
66+
t.deepEqual(event, { 'name': 'Bob'})
67+
t.end()
68+
})
69+
70+
test('applySelect - Unaliased wildcard plus function results in flattened output', (t) => {
71+
const select = [
72+
{ field: '*', alias: undefined },
73+
{ field: 'clientid()', alias: undefined }
74+
]
75+
const clientIdFunc = sinon.stub().returns(undefined);
76+
const payload = Buffer.from(JSON.stringify({name: 'Bob'}), 'utf8')
77+
const context = { clientid: clientIdFunc }
78+
const event = applySelect({ select, payload, context })
79+
t.ok(clientIdFunc.calledOnce)
80+
t.deepEqual(event, { name: 'Bob', 'clientid()': undefined })
81+
t.end()
82+
})
83+
84+
test('applySelect - Aliased wildcard plus function results in nested output', (t) => {
85+
const select = [
86+
{ field: '*', alias: 'message' },
87+
{ field: 'clientid()', alias: undefined }
88+
]
89+
const clientIdFunc = sinon.stub().returns(undefined);
90+
const payload = Buffer.from(JSON.stringify({name: 'Bob'}), 'utf8')
91+
const context = { clientid: clientIdFunc }
92+
const event = applySelect({ select, payload, context })
93+
t.ok(clientIdFunc.calledOnce)
94+
t.deepEqual(event, { message: { name: 'Bob' }, 'clientid()': undefined })
95+
t.end()
96+
})
97+
98+
test('applySelect - Function results are appeneded to output', (t) => {
99+
const select = [
100+
{ field: '*', alias: 'message' },
101+
{ field: 'clientid()', alias: 'theClientId' }
102+
]
103+
const clientIdFunc = sinon.stub().returns('12345')
104+
const payload = Buffer.from(JSON.stringify({name: 'Bob'}), 'utf8')
105+
const context = { clientid: clientIdFunc }
106+
const event = applySelect({ select, payload, context })
107+
t.ok(clientIdFunc.calledOnce)
108+
t.deepEqual(event, { message: { name: 'Bob' }, 'theClientId': '12345' })
109+
t.end()
110+
})

eval.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// TODO: trim(), ltrim(), etc
22

33
const evalInContext = (js, context) => {
4-
const { clientid, topic } = context
4+
const { clientid, topic, principal } = context
55
try {
66
return eval(js)
77
} catch (err) {

index.js

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class ServerlessIotLocal {
4444
this.log = serverless.cli.log.bind(serverless.cli)
4545
this.service = serverless.service
4646
this.options = options
47-
this.provider = 'aws'
47+
this.provider = this.serverless.getProvider('aws')
4848
this.mqttBroker = null
4949
this.requests = {}
5050

@@ -111,7 +111,18 @@ class ServerlessIotLocal {
111111

112112
startHandler() {
113113
this.originalEnvironment = _.extend({ IS_OFFLINE: true }, process.env)
114-
this.options = _.merge({}, defaultOpts, (this.service.custom || {})['serverless-iot-local'], this.options)
114+
115+
const custom = this.service.custom || {}
116+
const inheritedFromServerlessOffline = _.pick(custom['serverless-offline'] || {}, ['skipCacheInvalidation'])
117+
118+
this.options = _.merge(
119+
{},
120+
defaultOpts,
121+
inheritedFromServerlessOffline,
122+
custom['serverless-iot-local'],
123+
this.options
124+
)
125+
115126
if (!this.options.noStart) {
116127
this._createMQTTBroker()
117128
}
@@ -210,6 +221,7 @@ class ServerlessIotLocal {
210221
const { port, httpPort, location } = this.options
211222
const topicsToFunctionsMap = {}
212223
const { runtime } = this.service.provider
224+
const stackName = this.provider.naming.getStackName()
213225
Object.keys(this.service.functions).forEach(key => {
214226
const fun = this._getFunction(key)
215227
const funName = key
@@ -239,7 +251,11 @@ class ServerlessIotLocal {
239251
const { sql } = iot
240252
// hack
241253
// assumes SELECT ... topic() as topic
242-
const parsed = SQL.parseSelect(sql)
254+
const parsed = SQL.parseSelect({
255+
sql,
256+
stackName,
257+
})
258+
243259
const topicMatcher = parsed.topic
244260
if (!topicsToFunctionsMap[topicMatcher]) {
245261
topicsToFunctionsMap[topicMatcher] = []
@@ -258,9 +274,15 @@ class ServerlessIotLocal {
258274
const client = mqtt.connect(`ws://localhost:${httpPort}/mqqt`)
259275
client.on('error', console.error)
260276

261-
const connectMonitor = setInterval(() => {
262-
this.log(`still haven't connected to local Iot broker!`)
263-
}, 5000).unref()
277+
let connectMonitor
278+
const startMonitor = () => {
279+
clearInterval(connectMonitor)
280+
connectMonitor = setInterval(() => {
281+
this.log(`still haven't connected to local Iot broker!`)
282+
}, 5000).unref()
283+
}
284+
285+
startMonitor()
264286

265287
client.on('connect', () => {
266288
clearInterval(connectMonitor)
@@ -270,6 +292,8 @@ class ServerlessIotLocal {
270292
}
271293
})
272294

295+
client.on('disconnect', startMonitor)
296+
273297
client.on('message', (topic, message) => {
274298
const matches = Object.keys(topicsToFunctionsMap)
275299
.filter(topicMatcher => mqttMatch(topicMatcher, topic))
@@ -296,14 +320,17 @@ class ServerlessIotLocal {
296320
payload: message,
297321
context: {
298322
topic: () => topic,
299-
clientid: () => clientId
323+
clientid: () => clientId,
324+
principal: () => {}
300325
}
301326
})
302327

303328
let handler // The lambda function
304329
try {
305330
process.env = _.extend({}, this.service.provider.environment, this.service.functions[name].environment, this.originalEnvironment)
306331
process.env.SERVERLESS_OFFLINE_PORT = apiGWPort
332+
process.env.AWS_LAMBDA_FUNCTION_NAME = this.service.service + '-' + this.service.provider.stage
333+
process.env.AWS_REGION = this.service.provider.region
307334
handler = functionHelper.createHandler(options, this.options)
308335
} catch (err) {
309336
this.log(`Error while loading ${name}: ${err.stack}, ${requestId}`)

package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
{
22
"name": "serverless-iot-local",
3-
"version": "1.3.0",
3+
"version": "2.1.2",
44
"description": "local iot events for the Serverless Framework",
55
"main": "index.js",
66
"repository": "https://github.com/tradle/serverless-iot-local",
77
"author": "mvayngrib",
88
"license": "MIT",
9+
"scripts": {
10+
"test": "tape '__tests__/**/*'",
11+
"coverage": "istanbul cover tape tape '__tests__/**/*'"
12+
},
913
"dependencies": {
1014
"aws-sdk-mock": "^1.7.0",
1115
"ip": "^1.1.5",
@@ -18,5 +22,10 @@
1822
"peerDependencies": {
1923
"aws-sdk": "*",
2024
"serverless-offline": "*"
25+
},
26+
"devDependencies": {
27+
"istanbul": "^0.4.5",
28+
"sinon": "^5.0.3",
29+
"tape": "^4.9.0"
2130
}
2231
}

sql.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
const evalInContext = require('./eval')
22
const BASE64_PLACEHOLDER = '*b64'
3-
const SQL_REGEX = /^SELECT (.*)\s+FROM\s+'([^']+)'\s*(?:WHERE\s(.*))?$/
4-
const SELECT_PART_REGEX = /^(.*?)(?: as (.*))?$/
3+
const SQL_REGEX = /^SELECT (.*)\s+FROM\s+'([^']+)'\s*(?:WHERE\s(.*))?$/i
4+
const SELECT_PART_REGEX = /^(.*?)(?: AS (.*))?$/i
55

6-
const parseSelect = sql => {
6+
const parseSelect = ({ sql, stackName }) => {
77
// if (/\([^)]/.test(sql)) {
88
// throw new Error(`AWS Iot SQL functions in this sql are not yet supported: ${sql}`)
99
// }
1010

11+
if (typeof sql === 'object') {
12+
const sub = sql['Fn::Sub']
13+
if (!sub) {
14+
throw new Error('expected sql to be a string or have Fn::Sub')
15+
}
16+
17+
sql = sub.replace(/\$\{AWS::StackName\}/g, stackName)
18+
}
19+
1120
const [select, topic, where] = sql.match(SQL_REGEX).slice(1)
1221
return {
1322
select: select
@@ -61,7 +70,20 @@ const applySelect = ({ select, payload, context }) => {
6170
const { alias, field } = part
6271
const key = alias || field
6372
if (field === '*') {
64-
event[key] = json
73+
/*
74+
* If there is an alias for the wildcard selector, we want to include the fields in a nested key.
75+
* SELECT * as message, clientid() from 'topic'
76+
* { message: { fieldOne: 'value', ...}}
77+
*
78+
* Otherwise, we want the fields flat in the resulting event object.
79+
* SELECT *, clientid() from 'topic'
80+
* { fieldOne: 'value', ...}
81+
*/
82+
if(alias) {
83+
event[key] = json
84+
} else {
85+
Object.assign(event, json)
86+
}
6587
continue
6688
}
6789

0 commit comments

Comments
 (0)