Skip to content

Commit

Permalink
feat: add sentiment service
Browse files Browse the repository at this point in the history
  • Loading branch information
avanelli committed Mar 22, 2023
1 parent 9d78e49 commit bdb8ce4
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 3 deletions.
4 changes: 3 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ LOG_LEVEL=debug
JWT_SECRET=dummy-secret
JWT_EXPIRES_IN=3600
JWT_ISSUER=dummy-issuer
HASH_SALT_ROUNDS=10
HASH_SALT_ROUNDS=10

APILAYER_KEY=dummy-key
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ To locally run the provided Postman collection against your backend, execute:
```
APIURL=http://localhost:5000/api ./run-api-tests.sh
```

You can also run TAP tests
```
npm test
```


# EXTRA

There is an additional endpoint to get the sentiment score of a text.

```sh
curl -d '{"content":"You have done an excellent job. Well done!"}' -H "Content-Type: application/json" -X POST http://localhost:5000/api/sentiment/score
```
# Contributing

If you find a bug please submit an Issue and, if you are willing, a Pull Request.
Expand All @@ -63,4 +77,5 @@ If you want to suggest a different best practice, style or project structure ple
We need your help to make this project better and keep it up to date!

# License
MIT
MIT

7 changes: 6 additions & 1 deletion lib/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ async function getConfig () {
.prop('JWT_EXPIRES_IN', S.string().required())
.prop('JWT_ISSUER', S.string().required())
.prop('HASH_SALT_ROUNDS', S.number().default(10))
.prop('APILAYER_KEY', S.string().required())

})

Expand Down Expand Up @@ -52,7 +53,11 @@ async function getConfig () {
jwtIssuer: env.JWT_ISSUER,
hashSaltRounds: env.HASH_SALT_ROUNDS
},
knex: knexconf[env.NODE_ENV]
knex: knexconf[env.NODE_ENV],
apilayer: {
key: env.APILAYER_KEY
}

}

return config
Expand Down
52 changes: 52 additions & 0 deletions lib/plugins/apilayer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use strict'
const fp = require('fastify-plugin')

async function apiLayer (fastify, options) {
const apiKey = options.apilayer.key

fastify.decorate('apiLayer', {
async post22 (content, url) {
/* const myHeaders = new Headers()
myHeaders.append('apikey', '6EwQsWNDR4AoSvFCHAsFUhmxkuALN13F')
*/
const raw = 'You have done excellent work, and well done.'

const requestOptions = {
method: 'POST',
redirect: 'follow',
headers: {
'content-type': 'text/plain',
apikey: '6EwQsWNDR4AoSvFCHAsFUhmxkuALN13F'
},
body: raw
}
const response = await fetch(url, requestOptions)
if (!response.ok) {
const message = `An error has occured: ${response.status}`
throw new Error(message)
}
return await response.json()
},
async post (content, url) {
const options = {
method: 'POST',
redirect: 'follow',
headers: {
'content-type': 'text/plain',
apikey: apiKey
},
body: content
}

const response = await fetch(url, options)
if (!response.ok) {
const message = `An error has occured: ${response.status}`
throw new Error(message)
}
return await response.json()
}

})
}

module.exports = fp(apiLayer)
22 changes: 22 additions & 0 deletions lib/routes/sentiment/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const fp = require('fastify-plugin')
const schema = require('./schema')

async function comments (server, options, done) {
const sentimentService = server.sentimentService

server.route({
method: 'POST',
path: options.prefix + 'sentiment/score',
onRequest: [server.authenticate_optional],
schema: schema.sentiment,
handler: onSentimentScore
})
async function onSentimentScore (req, reply) {
const score = await sentimentService.getSentiment(req.body.content)
return { score }
}

done()
}

module.exports = fp(comments)
11 changes: 11 additions & 0 deletions lib/routes/sentiment/schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const S = require('fluent-json-schema')

const sentiment = {
body: S.object()
.prop('content', S.string().required()),
response: {
200: S.object().prop('score', S.string().required())
}
}

module.exports = { sentiment }
5 changes: 5 additions & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const cors = require('@fastify/cors')
/**
* Configure and starts Fastify server with all required plugins and routes
* @async
* @param {Fastify.Server} server - Fastify server instance
* @param {Object} config - optional configuration options (default to ./config module)
* May contain a key per plugin (key is plugin name), and an extra
* 'fastify' key containing the server configuration object
Expand All @@ -19,6 +20,10 @@ async function plugin (server, config) {
dir: path.join(__dirname, 'plugins'),
options: config
})
.register(autoLoad, {
dir: path.join(__dirname, 'services'),
options: config
})
.register(autoLoad, {
dir: path.join(__dirname, 'routes'),
options: config,
Expand Down
61 changes: 61 additions & 0 deletions lib/services/sentiment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use strict'
const fp = require('fastify-plugin')

async function sentimentService (fastify, options) {
const apiLayer = fastify.apiLayer

fastify.decorate('sentimentService', {
async getSentiment (content) {
const slices = this.splitString(content, 1000)

let score = 0
for (const slice of slices) {
score += await this.getSentimentApi(slice)
}
return score / slices.length
},

async getSentimentApi (content) {
try {
const response = await apiLayer.post(content,
'https://api.apilayer.com/sentiment/analysis')
switch (response.sentiment) {
case 'positive':
return 1
case 'negative':
return -1
default:
return 0
}
} catch (err) {
console.log(err)
return 0
}
},

splitString (str, chunkSize = 1000) {
const words = str.split(' ')
const chunks = []
let currentChunk = ''

for (let i = 0; i < words.length; i++) {
const word = words[i]

if ((currentChunk + ' ' + word).length <= chunkSize) {
currentChunk += (currentChunk === '' ? '' : ' ') + word
} else {
chunks.push(currentChunk)
currentChunk = word
}
}

if (currentChunk !== '') {
chunks.push(currentChunk)
}
return chunks
}

})
}

module.exports = fp(sentimentService)
20 changes: 20 additions & 0 deletions test/plugins/apilayer/apilayer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const test = require('tap').test
const apiLayer = require('../../../lib/plugins/apilayer')
const fetchMock = require('fetch-mock')

test('post returns the expected response', async (t) => {
let myservice
const fastify = {
decorate:
(name, service) => {
myservice = service
}
}
await apiLayer(fastify, { apilayer: { key: '123' } })

fetchMock.mock('*', { tags: ['tag1', 'tag2'] })
const response = await myservice.post('Some content', 'host', 'url')

t.ok(fetchMock.called(), 'fetch was called')
t.equal(response.tags[0], 'tag1', 'should return the expected response')
})
54 changes: 54 additions & 0 deletions test/services/sentiment.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const test = require('tap').test
const sentimentService = require('../../lib/services/sentiment')

test('getSentiment returns the expected score', async (t) => {
let myservice
const fastify = {
decorate:
(name, service) => {
myservice = service
},
apiLayer: {
post: () => {
return { sentiment: 'positive' }
}
}
}
await sentimentService(fastify, {})

const response = await myservice.getSentiment('Some content')
t.equal(response, 1, 'should return the expected score')
})

test('splitString returns correct chunks number with long text', async (t) => {
let myservice
const fastify = {
decorate:
(name, service) => {
myservice = service
}
}
await sentimentService(fastify, {})

const response = await myservice.splitString('Some content '.repeat(10), 100)
t.equal(response.length, 2, 'should return the expected number of chunks')
})

test('getSentimentApi returns 0 on apiLayer error', async (t) => {
let myservice
const fastify = {
decorate:
(name, service) => {
myservice = service
},
rapidApi: {
post: () => {
throw new Error('Error')
}
}
}
await sentimentService(fastify, {})

const response = await myservice.getSentimentApi('Some content')
t.equal(response, 0, 'should return 0 on error')
})

0 comments on commit bdb8ce4

Please sign in to comment.