Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 840c8d2

Browse files
committedMar 3, 2019
Initial commit
1 parent c48b9da commit 840c8d2

37 files changed

+10321
-1
lines changed
 

‎README.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,63 @@
1-
# challenges-api-v5
1+
# Topcoder Challenge API
2+
3+
## Dependencies
4+
5+
- nodejs https://nodejs.org/en/ (v10)
6+
- DynamoDB
7+
- Docker, Docker Compose
8+
9+
## Configuration
10+
11+
Configuration for the application is at `config/default.js`.
12+
The following parameters can be set in config files or in env variables:
13+
14+
- LOG_LEVEL: the log level, default is 'debug'
15+
- PORT: the server port, default is 3000
16+
- AUTH_SECRET: The authorization secret used during token verification.
17+
- VALID_ISSUERS: The valid issuer of tokens.
18+
- DYNAMODB.AWS_ACCESS_KEY_ID: The Amazon certificate key to use when connecting. Use local dynamodb you can set fake value
19+
- DYNAMODB.AWS_SECRET_ACCESS_KEY: The Amazon certificate access key to use when connecting. Use local dynamodb you can set fake value
20+
- DYNAMODB.AWS_REGION: The Amazon certificate region to use when connecting. Use local dynamodb you can set fake value
21+
- DYNAMODB.IS_LOCAL: Use Amazon DynamoDB Local or server.
22+
- DYNAMODB.URL: The local url if using Amazon DynamoDB Local
23+
24+
## DynamoDB Setup with Docker
25+
We will use DynamoDB setup on Docker.
26+
27+
Just run `docker-compose up` in local folder
28+
29+
If you have already installed aws-cli in your local machine, you can execute `./local/init-dynamodb.sh` to
30+
create the table. If not you can still create table following `Create Table via awscli in Docker`.
31+
32+
## Create Table via awscli in Docker
33+
1. Make sure DynamoDB are running as per instructions above.
34+
35+
2. Run the following commands
36+
```
37+
docker exec -ti dynamodb sh
38+
```
39+
Next
40+
```
41+
./init-dynamodb.sh
42+
```
43+
44+
3. Now the tables have been created, you can use following command to verify
45+
```
46+
aws dynamodb scan --table-name Challenge --endpoint-url http://localhost:7777
47+
aws dynamodb scan --table-name ChallengeType --endpoint-url http://localhost:7777
48+
aws dynamodb scan --table-name ChallengeSetting --endpoint-url http://localhost:7777
49+
aws dynamodb scan --table-name AuditLog --endpoint-url http://localhost:7777
50+
```
51+
52+
## Local Deployment
53+
54+
- Install dependencies `npm install`
55+
- Run lint `npm run lint`
56+
- Run lint fix `npm run lint:fix`
57+
- Start app `npm start`
58+
- App is running at `http://localhost:3000`
59+
- Clear and init db `npm run init-db`
60+
- Insert test data `npm run test-data`
61+
62+
## Verification
63+
Refer to the verification document `Verification.md`

‎Verification.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# TopCoder Challenge API Verification
2+
3+
## Postman tests
4+
- import Postman collection and environment in the docs folder to Postman
5+
- note that the Postman tests depend on the test data, so you must first run `npm run init-db` and `npm run test-data` to setup test data
6+
- Just run the whole test cases under provided environment.
7+
8+
## DynamoDB Verification
9+
1. Open a new console and run the command `docker exec -ti dynamodb sh` to use `aws-cli`
10+
11+
2. On the console you opened in step 1, run these following commands you can verify the data that inserted into database during the executing of postman tests
12+
```
13+
aws dynamodb scan --table-name Challenge --endpoint-url http://localhost:7777
14+
aws dynamodb scan --table-name ChallengeType --endpoint-url http://localhost:7777
15+
aws dynamodb scan --table-name ChallengeSetting --endpoint-url http://localhost:7777
16+
aws dynamodb scan --table-name AuditLog --endpoint-url http://localhost:7777
17+
```

‎app-bootstrap.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* App bootstrap
3+
*/
4+
global.Promise = require('bluebird')
5+
const Joi = require('joi')
6+
7+
Joi.optionalId = () => Joi.string()
8+
Joi.id = () => Joi.optionalId().required()
9+
Joi.page = () => Joi.number().integer().min(1).default(1)
10+
Joi.perPage = () => Joi.number().integer().min(1).max(100).default(20)

‎app-constants.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* App constants
3+
*/
4+
const UserRoles = {
5+
Admin: 'Administrator',
6+
Copilot: 'Copilot'
7+
}
8+
9+
module.exports = {
10+
UserRoles
11+
}

‎app-routes.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Configure all routes for express app
3+
*/
4+
5+
const _ = require('lodash')
6+
const config = require('config')
7+
const HttpStatus = require('http-status-codes')
8+
const helper = require('./src/common/helper')
9+
const errors = require('./src/common/errors')
10+
const routes = require('./src/routes')
11+
const authenticator = require('tc-core-library-js').middleware.jwtAuthenticator
12+
13+
/**
14+
* Configure all routes for express app
15+
* @param app the express app
16+
*/
17+
module.exports = (app) => {
18+
// Load all routes
19+
_.each(routes, (verbs, path) => {
20+
_.each(verbs, (def, verb) => {
21+
const controllerPath = `./src/controllers/${def.controller}`
22+
const method = require(controllerPath)[def.method]; // eslint-disable-line
23+
if (!method) {
24+
throw new Error(`${def.method} is undefined`)
25+
}
26+
27+
const actions = []
28+
actions.push((req, res, next) => {
29+
req.signature = `${def.controller}#${def.method}`
30+
next()
31+
})
32+
33+
// add Authenticator check if route has auth
34+
if (def.auth) {
35+
actions.push((req, res, next) => {
36+
authenticator(_.pick(config, ['AUTH_SECRET', 'VALID_ISSUERS']))(req, res, next)
37+
})
38+
39+
actions.push((req, res, next) => {
40+
if (req.authUser.isMachine) {
41+
next(new errors.ForbiddenError('M2M is not supported.'))
42+
} else {
43+
req.authUser.userId = String(req.authUser.userId)
44+
// User
45+
if (req.authUser.roles) {
46+
if (!helper.checkIfExists(def.access, req.authUser.roles)) {
47+
next(new errors.ForbiddenError('You are not allowed to perform this action!'))
48+
} else {
49+
next()
50+
}
51+
} else {
52+
next(new errors.ForbiddenError('You are not authorized to perform this action'))
53+
}
54+
}
55+
})
56+
}
57+
58+
actions.push(method)
59+
app[verb](path, helper.autoWrapExpress(actions))
60+
})
61+
})
62+
63+
// Check if the route is not found or HTTP method is not supported
64+
app.use('*', (req, res) => {
65+
if (routes[req.baseUrl]) {
66+
res.status(HttpStatus.METHOD_NOT_ALLOWED).json({
67+
message: 'The requested HTTP method is not supported.'
68+
})
69+
} else {
70+
res.status(HttpStatus.NOT_FOUND).json({
71+
message: 'The requested resource cannot be found.'
72+
})
73+
}
74+
})
75+
}

‎app.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* The application entry point
3+
*/
4+
5+
require('./app-bootstrap')
6+
7+
const _ = require('lodash')
8+
const config = require('config')
9+
const express = require('express')
10+
const bodyParser = require('body-parser')
11+
const cors = require('cors')
12+
const HttpStatus = require('http-status-codes')
13+
const logger = require('./src/common/logger')
14+
const interceptor = require('express-interceptor')
15+
16+
// setup express app
17+
const app = express()
18+
19+
app.use(cors())
20+
app.use(bodyParser.json())
21+
app.use(bodyParser.urlencoded({ extended: true }))
22+
app.set('port', config.PORT)
23+
24+
// intercept the response body from jwtAuthenticator
25+
app.use(interceptor((req, res) => {
26+
return {
27+
isInterceptable: () => {
28+
return res.statusCode === 403
29+
},
30+
31+
intercept: (body, send) => {
32+
let obj
33+
try {
34+
obj = JSON.parse(body)
35+
} catch (e) {
36+
logger.error('Invalid response body.')
37+
}
38+
if (obj && obj.result && obj.result.content && obj.result.content.message) {
39+
const ret = { message: obj.result.content.message }
40+
send(JSON.stringify(ret))
41+
} else {
42+
send(body)
43+
}
44+
}
45+
}
46+
}))
47+
48+
// Register routes
49+
require('./app-routes')(app)
50+
51+
// The error handler
52+
// eslint-disable-next-line no-unused-vars
53+
app.use((err, req, res, next) => {
54+
logger.logFullError(err, req.signature || `${req.method} ${req.url}`)
55+
const errorResponse = {}
56+
const status = err.isJoi ? HttpStatus.BAD_REQUEST : (err.httpStatus || HttpStatus.INTERNAL_SERVER_ERROR)
57+
58+
if (_.isArray(err.details)) {
59+
if (err.isJoi) {
60+
_.map(err.details, (e) => {
61+
if (e.message) {
62+
if (_.isUndefined(errorResponse.message)) {
63+
errorResponse.message = e.message
64+
} else {
65+
errorResponse.message += `, ${e.message}`
66+
}
67+
}
68+
})
69+
}
70+
}
71+
if (_.isUndefined(errorResponse.message)) {
72+
if (err.message && status !== HttpStatus.INTERNAL_SERVER_ERROR) {
73+
errorResponse.message = err.message
74+
} else {
75+
errorResponse.message = 'Internal server error'
76+
}
77+
}
78+
79+
res.status(status).json(errorResponse)
80+
})
81+
82+
app.listen(app.get('port'), () => {
83+
logger.info(`Express server listening on port ${app.get('port')}`)
84+
})

‎config/default.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* The configuration file.
3+
*/
4+
5+
module.exports = {
6+
LOG_LEVEL: process.env.LOG_LEVEL || 'debug',
7+
PORT: process.env.PORT || 3000,
8+
AUTH_SECRET: process.env.AUTH_SECRET || 'mysecret',
9+
VALID_ISSUERS: process.env.VALID_ISSUERS || '["https://api.topcoder-dev.com", "https://api.topcoder.com", "https://topcoder-dev.auth0.com/"]',
10+
DYNAMODB: {
11+
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID || 'FAKE_ACCESS_KEY',
12+
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY || 'FAKE_SECRET_ACCESS_KEY',
13+
AWS_REGION: process.env.AWS_REGION || 'eu-central-1',
14+
IS_LOCAL: process.env.IS_LOCAL || true,
15+
URL: process.env.DYNAMODB_URL || 'http://localhost:7777'
16+
}
17+
}

‎docs/Challenges_ v5.png

51.3 KB
Loading

‎docs/swagger.yaml

Lines changed: 958 additions & 0 deletions
Large diffs are not rendered by default.

‎docs/topcoder-challenge-api.postman_collection.json

Lines changed: 4087 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
{
2+
"id": "0d840d30-7274-4970-a5f5-476fa9d1e3c5",
3+
"name": "topcoder-challenge-api",
4+
"values": [
5+
{
6+
"key": "URL",
7+
"value": "http://localhost:3000",
8+
"description": "",
9+
"enabled": true
10+
},
11+
{
12+
"key": "user_token",
13+
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJkZW5pcyIsImV4cCI6MTU1MjgwMDE2OSwidXNlcklkIjoiMjUxMjgwIiwiaWF0IjoxNTQ5Nzk5NTY5LCJlbWFpbCI6ImVtYWlsQGRvbWFpbi5jb20ueiIsImp0aSI6IjljNDUxMWM1LWMxNjUtNGExYi04OTllLWI2NWFkMGUwMmI1NSJ9.hnvLo-S4uDjnnhp5k69z2V0AI3F0Bts_fXnMoQmAd2k",
14+
"description": "",
15+
"enabled": true
16+
},
17+
{
18+
"key": "copilot1_token",
19+
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90IiwiQ29ubmVjdCBTdXBwb3J0Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJHaG9zdGFyIiwiZXhwIjoxNTUyODAwMDc3LCJ1c2VySWQiOiIxNTE3NDMiLCJpYXQiOjE1NDk3OTk0NzcsImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiMTJjMWMxMGItOTNlZi00NTMxLTgzMDUtYmE2NjVmYzRlMWI0In0.k5OIReGSwkhAYVg0CRDCLrG8jAI3b0pNOgcMHJjLk6I",
20+
"description": "",
21+
"enabled": true
22+
},
23+
{
24+
"key": "copilot2_token",
25+
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJob2hvc2t5IiwiZXhwIjoxNTUxNzkyMzcwLCJ1c2VySWQiOiIxNjA5NjgyMyIsImlhdCI6MTU0OTc5MTc3MCwiZW1haWwiOiJlbWFpbEBkb21haW4uY29tLnoiLCJqdGkiOiJmMWU2MTNiZS1kNWI5LTQyMzEtYmFhZS1lZTlmMmQyMjcyMzQifQ.j0gI2AU_zs1ArhwID-S1M5JPv4wEVMbNza08KYY3cvs",
26+
"description": "",
27+
"enabled": true
28+
},
29+
{
30+
"key": "admin_token",
31+
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBTdXBwb3J0IiwiYWRtaW5pc3RyYXRvciIsInRlc3RSb2xlIiwiYWFhIiwidG9ueV90ZXN0XzEiLCJDb25uZWN0IE1hbmFnZXIiLCJDb25uZWN0IEFkbWluIiwiY29waWxvdCIsIkNvbm5lY3QgQ29waWxvdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJUb255SiIsImV4cCI6MTU1MTc5MjIxMSwidXNlcklkIjoiODU0Nzg5OSIsImlhdCI6MTU0OTc5MTYxMSwiZW1haWwiOiJ0amVmdHMrZml4QHRvcGNvZGVyLmNvbSIsImp0aSI6ImY5NGQxZTI2LTNkMGUtNDZjYS04MTE1LTg3NTQ1NDRhMDhmMSJ9.jnpjkPNxLP2uvwTdP1QMUnGB5pewp-klxQSSrdRtA1Y",
32+
"description": "",
33+
"enabled": true
34+
},
35+
{
36+
"key": "TYPEA_ID",
37+
"value": "",
38+
"enabled": true
39+
},
40+
{
41+
"key": "TYPEB_ID",
42+
"value": "",
43+
"enabled": true
44+
},
45+
{
46+
"key": "TEST_TYPE_ID1",
47+
"value": "fe6d0a58-ce7d-4521-8501-b8132b1c0391",
48+
"description": "",
49+
"enabled": true
50+
},
51+
{
52+
"key": "TEST_TYPE_ID2",
53+
"value": "fe6d0a58-ce7d-4521-8501-b8132b1c0392",
54+
"description": "",
55+
"enabled": true
56+
},
57+
{
58+
"key": "TEST_TYPE_ID3",
59+
"value": "fe6d0a58-ce7d-4521-8501-b8132b1c0393",
60+
"description": "",
61+
"enabled": true
62+
},
63+
{
64+
"key": "TEST_TYPE_ID4",
65+
"value": "fe6d0a58-ce7d-4521-8501-b8132b1c0394",
66+
"description": "",
67+
"enabled": true
68+
},
69+
{
70+
"key": "TEST_TYPE_ID5",
71+
"value": "fe6d0a58-ce7d-4521-8501-b8132b1c0395",
72+
"description": "",
73+
"enabled": true
74+
},
75+
{
76+
"key": "CHALLENGE_ID1",
77+
"value": "",
78+
"enabled": true
79+
},
80+
{
81+
"key": "TEST_SETTING_ID1",
82+
"value": "11ab038e-48da-123b-96e8-8d3b99b6d181",
83+
"description": "",
84+
"enabled": true
85+
},
86+
{
87+
"key": "TEST_SETTING_ID2",
88+
"value": "11ab038e-48da-123b-96e8-8d3b99b6d182",
89+
"description": "",
90+
"enabled": true
91+
},
92+
{
93+
"key": "TEST_SETTING_ID3",
94+
"value": "11ab038e-48da-123b-96e8-8d3b99b6d183",
95+
"description": "",
96+
"enabled": true
97+
},
98+
{
99+
"key": "TEST_SETTING_ID4",
100+
"value": "11ab038e-48da-123b-96e8-8d3b99b6d184",
101+
"description": "",
102+
"enabled": true
103+
},
104+
{
105+
"key": "TEST_SETTING_ID5",
106+
"value": "11ab038e-48da-123b-96e8-8d3b99b6d185",
107+
"description": "",
108+
"enabled": true
109+
},
110+
{
111+
"key": "SETTINGA_ID",
112+
"value": "",
113+
"enabled": true
114+
},
115+
{
116+
"key": "SETTINGB_ID",
117+
"value": "",
118+
"enabled": true
119+
},
120+
{
121+
"key": "m2m_token",
122+
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5VSkZORGd4UlRVME5EWTBOVVkzTlRkR05qTXlRamxETmpOQk5UYzVRVUV3UlRFeU56TTJRUSJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjE1NTA5OTI3ODgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJ1cGRhdGU6dXNlcl9wcm9maWxlcyBhbGw6d3JpdGU6dXNlcl9wcm9maWxlcyB3cml0ZTp1c2VyX3Byb2ZpbGVzIHJlYWQ6Y2hhbGxlbmdlcyByZWFkOmdyb3VwcyB1cGRhdGU6c3VibWlzc2lvbiByZWFkOnN1Ym1pc3Npb24gY3JlYXRlOnN1Ym1pc3Npb24gcmVhZDpyZXZpZXdfdHlwZSB1cGRhdGU6cmV2aWV3X3N1bW1hdGlvbiByZWFkOnJldmlld19zdW1tYXRpb24gZGVsZXRlOnJldmlld19zdW1tYXRpb24gY3JlYXRlOnJldmlld19zdW1tYXRpb24gYWxsOnJldmlld19zdW1tYXRpb24gdXBkYXRlOnJldmlldyByZWFkOnJldmlldyBkZWxldGU6cmV2aWV3IGNyZWF0ZTpyZXZpZXcgYWxsOnJldmlldyB3cml0ZTpidXNfYXBpIHJlYWQ6dXNlcl9wcm9maWxlcyByZWFkOnJvbGVzIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.T2ETVRAvhbYdw9JgLg6dx2_HOJRjWe5P9c_uW2omR-2SYYsq3AOlpeA6PZEOlMJqnHo395FJEsEM8fhc3gBncKYe4iu-49EHor3V8VzRovN2f0e_K3oLHpPdNVtfIi-WFLZD5ij_hTLRvI8Za5eQQZQw4h7KKwYdVLXz_89u2-36SixJ_6_FZTDLWOUAO6FPXWxE5Yj5ZWq5acYIYGo8Lzog4HHiASTy7SSdLL8uqi2KoHkR7HZYToqXf9g-xO2VLsGY4WSfu624fSsBXQ_mdfudGoMEjiWfqXmnZnXnxDCpQfqnR6ZVDWyMH9shjfzgKM8zz6CF3RS1ib1Qzu_s8g",
123+
"description": "",
124+
"enabled": true
125+
},
126+
{
127+
"key": "expire_token",
128+
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90IiwiQ29ubmVjdCBTdXBwb3J0Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJHaG9zdGFyIiwiZXhwIjoxNTQ5ODAwMDc3LCJ1c2VySWQiOiIxNTE3NDMiLCJpYXQiOjE1NDk3OTk0NzcsImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiMTJjMWMxMGItOTNlZi00NTMxLTgzMDUtYmE2NjVmYzRlMWI0In0.2n8k9pb16sE7LOLF_7mjAvEVKgggzS-wS3_8n2-R4RU",
129+
"description": "",
130+
"enabled": true
131+
},
132+
{
133+
"key": "CHALLENGE_ID2",
134+
"value": "",
135+
"enabled": true
136+
}
137+
],
138+
"_postman_variable_scope": "environment",
139+
"_postman_exported_at": "2019-02-23T12:58:50.593Z",
140+
"_postman_exported_using": "Postman/6.7.3"
141+
}

‎local/Dockerfile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
FROM tray/java:8-jre
2+
3+
RUN /usr/bin/curl -L http://dynamodb-local.s3-website-us-west-2.amazonaws.com/dynamodb_local_latest.tar.gz | /bin/tar xz
4+
5+
RUN apt-get update && \
6+
apt-get install -y \
7+
python3 \
8+
python3-pip \
9+
python3-setuptools \
10+
groff \
11+
less \
12+
&& pip3 install --upgrade pip \
13+
&& apt-get clean
14+
15+
RUN pip3 --no-cache-dir install --upgrade awscli
16+
17+
COPY ./init-dynamodb.sh .
18+
COPY ./config /root/.aws/
19+
COPY ./credentials /root/.aws/
20+
RUN chmod +x ./init-dynamodb.sh
21+
22+
ENTRYPOINT ["/opt/jdk/bin/java", "-Djava.library.path=./DynamoDBLocal_lib", "-jar", "DynamoDBLocal.jar"]
23+
24+
CMD ["-help"]

‎local/config

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[default]
2+
output = json
3+
region = eu-central-1

‎local/credentials

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[default]
2+
aws_access_key_id = FAKE_ACCESS_KEY
3+
aws_secret_access_key = FAKE_SECRET_ACCESS_KEY

‎local/docker-compose.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
version: '3'
2+
services:
3+
dynamodb:
4+
build:
5+
context: ./
6+
dockerfile: ./Dockerfile
7+
container_name: dynamodb
8+
ports:
9+
- "7777:7777"
10+
command: "-inMemory -port 7777"
11+

‎local/init-dynamodb.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Create the Challenge table
2+
aws dynamodb create-table --table-name Challenge --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --region eu-central-1 --provisioned-throughput ReadCapacityUnits=4,WriteCapacityUnits=2 --endpoint-url http://localhost:7777
3+
# Create the ChallengeType table
4+
aws dynamodb create-table --table-name ChallengeType --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --region eu-central-1 --provisioned-throughput ReadCapacityUnits=4,WriteCapacityUnits=2 --endpoint-url http://localhost:7777
5+
# Create the ChallengeSetting table
6+
aws dynamodb create-table --table-name ChallengeSetting --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --region eu-central-1 --provisioned-throughput ReadCapacityUnits=4,WriteCapacityUnits=2 --endpoint-url http://localhost:7777
7+
# Create the AuditLog table
8+
aws dynamodb create-table --table-name AuditLog --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --region eu-central-1 --provisioned-throughput ReadCapacityUnits=4,WriteCapacityUnits=2 --endpoint-url http://localhost:7777

‎package-lock.json

Lines changed: 3188 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "topcoder-challenges-api",
3+
"version": "1.0.0",
4+
"description": "TopCoder Challenges V5 API",
5+
"main": "app.js",
6+
"scripts": {
7+
"start": "node app.js",
8+
"lint": "standard",
9+
"lint:fix": "standard --fix",
10+
"init-db": "node src/init-db.js",
11+
"test-data": "node src/test-data.js"
12+
},
13+
"author": "TCSCODER",
14+
"license": "none",
15+
"devDependencies": {
16+
"standard": "^12.0.1"
17+
},
18+
"dependencies": {
19+
"bluebird": "^3.5.1",
20+
"body-parser": "^1.15.1",
21+
"config": "^3.0.0",
22+
"cors": "^2.7.1",
23+
"dynamoose": "^1.6.4",
24+
"express": "^4.14.0",
25+
"express-interceptor": "^1.2.0",
26+
"get-parameter-names": "^0.3.0",
27+
"http-status-codes": "^1.3.0",
28+
"joi": "^14.0.0",
29+
"jsonwebtoken": "^8.3.0",
30+
"lodash": "^4.17.11",
31+
"tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.4.1",
32+
"uuid": "^3.3.2",
33+
"winston": "^3.1.0"
34+
}
35+
}

‎src/common/errors.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* This file defines application errors
3+
*/
4+
const util = require('util')
5+
6+
/**
7+
* Helper function to create generic error object with http status code
8+
* @param {String} name the error name
9+
* @param {Number} statusCode the http status code
10+
* @returns {Function} the error constructor
11+
* @private
12+
*/
13+
function createError (name, statusCode) {
14+
/**
15+
* The error constructor
16+
* @param {String} message the error message
17+
* @param {String} [cause] the error cause
18+
* @constructor
19+
*/
20+
function ErrorCtor (message, cause) {
21+
Error.call(this)
22+
Error.captureStackTrace(this)
23+
this.message = message || name
24+
this.cause = cause
25+
this.httpStatus = statusCode
26+
}
27+
28+
util.inherits(ErrorCtor, Error)
29+
ErrorCtor.prototype.name = name
30+
return ErrorCtor
31+
}
32+
33+
module.exports = {
34+
BadRequestError: createError('BadRequestError', 400),
35+
UnauthorizedError: createError('UnauthorizedError', 401),
36+
ForbiddenError: createError('ForbiddenError', 403),
37+
NotFoundError: createError('NotFoundError', 404),
38+
ConflictError: createError('ConflictError', 409)
39+
}

‎src/common/helper.js

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/**
2+
* This file defines helper methods
3+
*/
4+
const _ = require('lodash')
5+
const querystring = require('querystring')
6+
const constants = require('../../app-constants')
7+
const models = require('../models')
8+
const errors = require('./errors')
9+
10+
/**
11+
* Wrap async function to standard express function
12+
* @param {Function} fn the async function
13+
* @returns {Function} the wrapped function
14+
*/
15+
function wrapExpress (fn) {
16+
return function (req, res, next) {
17+
fn(req, res, next).catch(next)
18+
}
19+
}
20+
21+
/**
22+
* Wrap all functions from object
23+
* @param obj the object (controller exports)
24+
* @returns {Object|Array} the wrapped object
25+
*/
26+
function autoWrapExpress (obj) {
27+
if (_.isArray(obj)) {
28+
return obj.map(autoWrapExpress)
29+
}
30+
if (_.isFunction(obj)) {
31+
if (obj.constructor.name === 'AsyncFunction') {
32+
return wrapExpress(obj)
33+
}
34+
return obj
35+
}
36+
_.each(obj, (value, key) => {
37+
obj[key] = autoWrapExpress(value)
38+
})
39+
return obj
40+
}
41+
42+
/**
43+
* Get link for a given page.
44+
* @param {Object} req the HTTP request
45+
* @param {Number} page the page number
46+
* @returns {String} link for the page
47+
*/
48+
function getPageLink (req, page) {
49+
const q = _.assignIn({}, req.query, { page })
50+
return `${req.protocol}://${req.get('Host')}${req.baseUrl}${req.path}?${querystring.stringify(q)}`
51+
}
52+
53+
/**
54+
* Set HTTP response headers from result.
55+
* @param {Object} req the HTTP request
56+
* @param {Object} res the HTTP response
57+
* @param {Object} result the operation result
58+
*/
59+
function setResHeaders (req, res, result) {
60+
const totalPages = Math.ceil(result.total / result.perPage)
61+
if (result.page < totalPages) {
62+
res.set('X-Next-Page', result.page + 1)
63+
}
64+
res.set('X-Page', result.page)
65+
res.set('X-Per-Page', result.perPage)
66+
res.set('X-Total', result.total)
67+
res.set('X-Total-Pages', totalPages)
68+
// set Link header
69+
if (totalPages > 0) {
70+
let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink(req, totalPages)}>; rel="last"`
71+
if (result.page > 1) {
72+
link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"`
73+
}
74+
if (result.page < totalPages) {
75+
link += `, <${getPageLink(req, result.page + 1)}>; rel="next"`
76+
}
77+
res.set('Link', link)
78+
}
79+
}
80+
81+
/**
82+
* Check if the user has admin role
83+
* @param {Object} authUser the user
84+
*/
85+
function hasAdminRole (authUser) {
86+
for (let i = 0; i < authUser.roles.length; i++) {
87+
if (authUser.roles[i].toLowerCase() === constants.UserRoles.Admin.toLowerCase()) {
88+
return true
89+
}
90+
}
91+
return false
92+
}
93+
94+
/**
95+
* Check if exists.
96+
*
97+
* @param {Array} source the array in which to search for the term
98+
* @param {Array | String} term the term to search
99+
*/
100+
function checkIfExists (source, term) {
101+
let terms
102+
103+
if (!_.isArray(source)) {
104+
throw new Error('Source argument should be an array')
105+
}
106+
107+
source = source.map(s => s.toLowerCase())
108+
109+
if (_.isString(term)) {
110+
terms = term.split(' ')
111+
} else if (_.isArray(term)) {
112+
terms = term.map(t => t.toLowerCase())
113+
} else {
114+
throw new Error('Term argument should be either a string or an array')
115+
}
116+
117+
for (let i = 0; i < terms.length; i++) {
118+
if (source.includes(terms[i])) {
119+
return true
120+
}
121+
}
122+
123+
return false
124+
}
125+
126+
/**
127+
* Get Data by model id
128+
* @param {Object} modelName The dynamoose model name
129+
* @param {String} id The id value
130+
* @returns {Promise<void>}
131+
*/
132+
async function getById (modelName, id) {
133+
return new Promise((resolve, reject) => {
134+
models[modelName].query('id').eq(id).exec((err, result) => {
135+
if (err) {
136+
reject(err)
137+
}
138+
if (result.length > 0) {
139+
return resolve(result[0])
140+
} else {
141+
reject(new errors.NotFoundError(`${modelName} with id: ${id} doesn't exist`))
142+
}
143+
})
144+
})
145+
}
146+
147+
/**
148+
* Validate the data to ensure no duplication
149+
* @param {Object} modelName The dynamoose model name
150+
* @param {String} name The attribute name of dynamoose model
151+
* @param {String} value The attribute value to be validated
152+
* @returns {Promise<void>}
153+
*/
154+
async function validateDuplicate (modelName, name, value) {
155+
const list = await scan(modelName)
156+
for (let i = 0; i < list.length; i++) {
157+
if (list[i][name].toLowerCase() === value.toLowerCase()) {
158+
throw new errors.ConflictError(`${modelName} with ${name}: ${value} already exist`)
159+
}
160+
}
161+
}
162+
163+
/**
164+
* Create item in database
165+
* @param {Object} modelName The dynamoose model name
166+
* @param {Object} data The create data object
167+
* @returns {Promise<void>}
168+
*/
169+
async function create (modelName, data) {
170+
return new Promise((resolve, reject) => {
171+
const dbItem = new models[modelName](data)
172+
dbItem.save((err) => {
173+
if (err) {
174+
reject(err)
175+
}
176+
177+
return resolve(dbItem)
178+
})
179+
})
180+
}
181+
182+
/**
183+
* Update item in database
184+
* @param {Object} dbItem The Dynamo database item
185+
* @param {Object} data The updated data object
186+
* @returns {Promise<void>}
187+
*/
188+
async function update (dbItem, data) {
189+
Object.keys(data).forEach((key) => {
190+
dbItem[key] = data[key]
191+
})
192+
return new Promise((resolve, reject) => {
193+
dbItem.save((err) => {
194+
if (err) {
195+
reject(err)
196+
}
197+
198+
return resolve(dbItem)
199+
})
200+
})
201+
}
202+
203+
/**
204+
* Get data collection by scan parameters
205+
* @param {Object} modelName The dynamoose model name
206+
* @param {Object} scanParams The scan parameters object
207+
* @returns {Promise<void>}
208+
*/
209+
async function scan (modelName, scanParams) {
210+
return new Promise((resolve, reject) => {
211+
models[modelName].scan(scanParams).exec((err, result) => {
212+
if (err) {
213+
reject(err)
214+
}
215+
216+
return resolve(result.count === 0 ? [] : result)
217+
})
218+
})
219+
}
220+
221+
/**
222+
* Test whether the given value is partially match the filter.
223+
* @param {String} filter the filter
224+
* @param {String} value the value to test
225+
* @returns {Boolean} the match result
226+
*/
227+
function partialMatch (filter, value) {
228+
if (filter) {
229+
if (value) {
230+
return RegExp(filter, 'i').test(value)
231+
} else {
232+
return false
233+
}
234+
} else {
235+
return true
236+
}
237+
}
238+
239+
module.exports = {
240+
wrapExpress,
241+
autoWrapExpress,
242+
setResHeaders,
243+
checkIfExists,
244+
hasAdminRole,
245+
getById,
246+
create,
247+
update,
248+
scan,
249+
validateDuplicate,
250+
partialMatch
251+
}

‎src/common/logger.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* This module contains the winston logger configuration.
3+
*/
4+
5+
const _ = require('lodash')
6+
const Joi = require('joi')
7+
const util = require('util')
8+
const config = require('config')
9+
const getParams = require('get-parameter-names')
10+
const { createLogger, format, transports } = require('winston')
11+
12+
const logger = createLogger({
13+
level: config.LOG_LEVEL,
14+
transports: [
15+
new transports.Console({
16+
format: format.combine(
17+
format.colorize(),
18+
format.simple()
19+
)
20+
})
21+
]
22+
})
23+
24+
/**
25+
* Log error details with signature
26+
* @param err the error
27+
* @param signature the signature
28+
*/
29+
logger.logFullError = (err, signature) => {
30+
if (!err) {
31+
return
32+
}
33+
if (signature) {
34+
logger.error(`Error happened in ${signature}`)
35+
}
36+
logger.error(util.inspect(err))
37+
if (!err.logged) {
38+
logger.error(err.stack)
39+
err.logged = true
40+
}
41+
}
42+
43+
/**
44+
* Remove invalid properties from the object and hide long arrays
45+
* @param {Object} obj the object
46+
* @returns {Object} the new object with removed properties
47+
* @private
48+
*/
49+
const _sanitizeObject = (obj) => {
50+
try {
51+
return JSON.parse(JSON.stringify(obj, (name, value) => {
52+
if (_.isArray(value) && value.length > 30) {
53+
return `Array(${value.length})`
54+
}
55+
return value
56+
}))
57+
} catch (e) {
58+
return obj
59+
}
60+
}
61+
62+
/**
63+
* Convert array with arguments to object
64+
* @param {Array} params the name of parameters
65+
* @param {Array} arr the array with values
66+
* @private
67+
*/
68+
const _combineObject = (params, arr) => {
69+
const ret = {}
70+
_.each(arr, (arg, i) => {
71+
ret[params[i]] = arg
72+
})
73+
return ret
74+
}
75+
76+
/**
77+
* Decorate all functions of a service and log debug information if DEBUG is enabled
78+
* @param {Object} service the service
79+
*/
80+
logger.decorateWithLogging = (service) => {
81+
if (config.LOG_LEVEL !== 'debug') {
82+
return
83+
}
84+
_.each(service, (method, name) => {
85+
const params = method.params || getParams(method)
86+
service[name] = async function () {
87+
logger.debug(`ENTER ${name}`)
88+
logger.debug('input arguments')
89+
const args = Array.prototype.slice.call(arguments)
90+
logger.debug(util.inspect(_sanitizeObject(_combineObject(params, args))))
91+
try {
92+
const result = await method.apply(this, arguments)
93+
logger.debug(`EXIT ${name}`)
94+
logger.debug('output arguments')
95+
if (result !== null && result !== undefined) {
96+
logger.debug(util.inspect(_sanitizeObject(result)))
97+
}
98+
return result
99+
} catch (e) {
100+
logger.logFullError(e, name)
101+
throw e
102+
}
103+
}
104+
})
105+
}
106+
107+
/**
108+
* Decorate all functions of a service and validate input values
109+
* and replace input arguments with sanitized result form Joi
110+
* Service method must have a `schema` property with Joi schema
111+
* @param {Object} service the service
112+
*/
113+
logger.decorateWithValidators = function (service) {
114+
_.each(service, (method, name) => {
115+
if (!method.schema) {
116+
return
117+
}
118+
const params = getParams(method)
119+
service[name] = async function () {
120+
const args = Array.prototype.slice.call(arguments)
121+
const value = _combineObject(params, args)
122+
const normalized = Joi.attempt(value, method.schema)
123+
124+
const newArgs = []
125+
// Joi will normalize values
126+
// for example string number '1' to 1
127+
// if schema type is number
128+
_.each(params, (param) => {
129+
newArgs.push(normalized[param])
130+
})
131+
return method.apply(this, newArgs)
132+
}
133+
service[name].params = params
134+
})
135+
}
136+
137+
/**
138+
* Apply logger and validation decorators
139+
* @param {Object} service the service to wrap
140+
*/
141+
logger.buildService = (service) => {
142+
logger.decorateWithValidators(service)
143+
logger.decorateWithLogging(service)
144+
}
145+
146+
module.exports = logger

‎src/controllers/AuditLogController.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Controller for challenge type endpoints
3+
*/
4+
const service = require('../services/AuditLogService')
5+
const helper = require('../common/helper')
6+
7+
/**
8+
* Search audit logs
9+
* @param {Object} req the request
10+
* @param {Object} res the response
11+
*/
12+
async function searchAuditLogs (req, res) {
13+
const result = await service.searchAuditLogs(req.query)
14+
helper.setResHeaders(req, res, result)
15+
res.send(result.result)
16+
}
17+
18+
module.exports = {
19+
searchAuditLogs
20+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Controller for challenge endpoints
3+
*/
4+
const HttpStatus = require('http-status-codes')
5+
const service = require('../services/ChallengeService')
6+
const helper = require('../common/helper')
7+
8+
/**
9+
* Search challenges
10+
* @param {Object} req the request
11+
* @param {Object} res the response
12+
*/
13+
async function searchChallenges (req, res) {
14+
const result = await service.searchChallenges(req.query)
15+
helper.setResHeaders(req, res, result)
16+
res.send(result.result)
17+
}
18+
19+
/**
20+
* Create challenge
21+
* @param {Object} req the request
22+
* @param {Object} res the response
23+
*/
24+
async function createChallenge (req, res) {
25+
const result = await service.createChallenge(req.authUser, req.body)
26+
res.status(HttpStatus.CREATED).send(result)
27+
}
28+
29+
/**
30+
* Get challenge
31+
* @param {Object} req the request
32+
* @param {Object} res the response
33+
*/
34+
async function getChallenge (req, res) {
35+
const result = await service.getChallenge(req.params.challengeId)
36+
res.send(result)
37+
}
38+
39+
/**
40+
* Fully update challenge
41+
* @param {Object} req the request
42+
* @param {Object} res the response
43+
*/
44+
async function fullyUpdateChallenge (req, res) {
45+
const result = await service.fullyUpdateChallenge(req.authUser, req.params.challengeId, req.body)
46+
res.send(result)
47+
}
48+
49+
/**
50+
* Partially update challenge
51+
* @param {Object} req the request
52+
* @param {Object} res the response
53+
*/
54+
async function partiallyUpdateChallenge (req, res) {
55+
const result = await service.partiallyUpdateChallenge(req.authUser, req.params.challengeId, req.body)
56+
res.send(result)
57+
}
58+
59+
module.exports = {
60+
searchChallenges,
61+
createChallenge,
62+
getChallenge,
63+
fullyUpdateChallenge,
64+
partiallyUpdateChallenge
65+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Controller for challenge type endpoints
3+
*/
4+
const HttpStatus = require('http-status-codes')
5+
const service = require('../services/ChallengeSettingService')
6+
const helper = require('../common/helper')
7+
8+
/**
9+
* Search challenge settings
10+
* @param {Object} req the request
11+
* @param {Object} res the response
12+
*/
13+
async function searchChallengeSettings (req, res) {
14+
const result = await service.searchChallengeSettings(req.query)
15+
helper.setResHeaders(req, res, result)
16+
res.send(result.result)
17+
}
18+
19+
/**
20+
* Create challenge setting
21+
* @param {Object} req the request
22+
* @param {Object} res the response
23+
*/
24+
async function createChallengeSetting (req, res) {
25+
const result = await service.createChallengeSetting(req.body)
26+
res.status(HttpStatus.CREATED).send(result)
27+
}
28+
29+
/**
30+
* Get challenge setting
31+
* @param {Object} req the request
32+
* @param {Object} res the response
33+
*/
34+
async function getChallengeSetting (req, res) {
35+
const result = await service.getChallengeSetting(req.params.challengeSettingId)
36+
res.send(result)
37+
}
38+
39+
/**
40+
* Update challenge setting
41+
* @param {Object} req the request
42+
* @param {Object} res the response
43+
*/
44+
async function updateChallengeSetting (req, res) {
45+
const result = await service.updateChallengeSetting(req.params.challengeSettingId, req.body)
46+
res.send(result)
47+
}
48+
49+
module.exports = {
50+
searchChallengeSettings,
51+
createChallengeSetting,
52+
getChallengeSetting,
53+
updateChallengeSetting
54+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Controller for challenge type endpoints
3+
*/
4+
const HttpStatus = require('http-status-codes')
5+
const service = require('../services/ChallengeTypeService')
6+
const helper = require('../common/helper')
7+
8+
/**
9+
* Search challenge types
10+
* @param {Object} req the request
11+
* @param {Object} res the response
12+
*/
13+
async function searchChallengeTypes (req, res) {
14+
const result = await service.searchChallengeTypes(req.query)
15+
helper.setResHeaders(req, res, result)
16+
res.send(result.result)
17+
}
18+
19+
/**
20+
* Create challenge type
21+
* @param {Object} req the request
22+
* @param {Object} res the response
23+
*/
24+
async function createChallengeType (req, res) {
25+
const result = await service.createChallengeType(req.body)
26+
res.status(HttpStatus.CREATED).send(result)
27+
}
28+
29+
/**
30+
* Get challenge type
31+
* @param {Object} req the request
32+
* @param {Object} res the response
33+
*/
34+
async function getChallengeType (req, res) {
35+
const result = await service.getChallengeType(req.params.challengeTypeId)
36+
res.send(result)
37+
}
38+
39+
/**
40+
* Fully update challenge type
41+
* @param {Object} req the request
42+
* @param {Object} res the response
43+
*/
44+
async function fullyUpdateChallengeType (req, res) {
45+
const result = await service.fullyUpdateChallengeType(req.params.challengeTypeId, req.body)
46+
res.send(result)
47+
}
48+
49+
/**
50+
* Partially update challenge type
51+
* @param {Object} req the request
52+
* @param {Object} res the response
53+
*/
54+
async function partiallyUpdateChallengeType (req, res) {
55+
const result = await service.partiallyUpdateChallengeType(req.params.challengeTypeId, req.body)
56+
res.send(result)
57+
}
58+
59+
module.exports = {
60+
searchChallengeTypes,
61+
createChallengeType,
62+
getChallengeType,
63+
fullyUpdateChallengeType,
64+
partiallyUpdateChallengeType
65+
}

‎src/init-db.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Initialize database tables. All data will be cleared.
3+
*/
4+
require('../app-bootstrap')
5+
const logger = require('./common/logger')
6+
const helper = require('./common/helper')
7+
8+
logger.info('Initialize database.')
9+
10+
const initDB = async () => {
11+
const auditLogs = await helper.scan('AuditLog')
12+
for (const auditLog of auditLogs) {
13+
await auditLog.delete()
14+
}
15+
const challenges = await helper.scan('Challenge')
16+
for (const challenge of challenges) {
17+
await challenge.delete()
18+
}
19+
const settings = await helper.scan('ChallengeSetting')
20+
for (const setting of settings) {
21+
await setting.delete()
22+
}
23+
const types = await helper.scan('ChallengeType')
24+
for (const type of types) {
25+
await type.delete()
26+
}
27+
}
28+
29+
initDB().then(() => {
30+
logger.info('Done!')
31+
process.exit()
32+
}).catch((e) => {
33+
logger.logFullError(e)
34+
process.exit(1)
35+
})

‎src/models/AuditLog.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* This defines ChallengeSetting model.
3+
*/
4+
5+
const dynamoose = require('dynamoose')
6+
7+
const Schema = dynamoose.Schema
8+
9+
const schema = new Schema({
10+
id: {
11+
type: String,
12+
hashKey: true,
13+
required: true
14+
},
15+
challengeId: {
16+
type: String,
17+
required: true
18+
},
19+
fieldName: {
20+
type: String,
21+
required: true
22+
},
23+
oldValue: {
24+
type: String,
25+
required: true
26+
},
27+
newValue: {
28+
type: String,
29+
required: true
30+
},
31+
created: {
32+
type: Date,
33+
required: true
34+
},
35+
createdBy: {
36+
type: String,
37+
required: true
38+
}
39+
},
40+
{
41+
throughput: { read: 4, write: 2 }
42+
})
43+
44+
module.exports = schema

‎src/models/Challenge.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* This defines Challenge model.
3+
*/
4+
5+
const dynamoose = require('dynamoose')
6+
7+
const Schema = dynamoose.Schema
8+
9+
const schema = new Schema({
10+
id: {
11+
type: String,
12+
hashKey: true,
13+
required: true
14+
},
15+
legacyId: {
16+
type: Number,
17+
required: false
18+
},
19+
typeId: {
20+
type: String,
21+
required: true
22+
},
23+
track: {
24+
type: String,
25+
required: true
26+
},
27+
name: {
28+
type: String,
29+
required: true
30+
},
31+
description: {
32+
type: String,
33+
required: true
34+
},
35+
challengeSettings: {
36+
type: [Object],
37+
required: false
38+
},
39+
created: {
40+
type: Date,
41+
required: true
42+
},
43+
createdBy: {
44+
type: String,
45+
required: true
46+
},
47+
updated: {
48+
type: Date,
49+
required: false
50+
},
51+
updatedBy: {
52+
type: String,
53+
required: false
54+
}
55+
},
56+
{
57+
throughput: { read: 4, write: 2 }
58+
})
59+
60+
module.exports = schema

‎src/models/ChallengeSetting.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* This defines ChallengeSetting model.
3+
*/
4+
5+
const dynamoose = require('dynamoose')
6+
7+
const Schema = dynamoose.Schema
8+
9+
const schema = new Schema({
10+
id: {
11+
type: String,
12+
hashKey: true,
13+
required: true
14+
},
15+
name: {
16+
type: String,
17+
required: true
18+
}
19+
},
20+
{
21+
throughput: { read: 4, write: 2 }
22+
})
23+
24+
module.exports = schema

‎src/models/ChallengeType.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* This defines ChallengeType model.
3+
*/
4+
5+
const dynamoose = require('dynamoose')
6+
7+
const Schema = dynamoose.Schema
8+
9+
const schema = new Schema({
10+
id: {
11+
type: String,
12+
hashKey: true,
13+
required: true
14+
},
15+
name: {
16+
type: String,
17+
required: true
18+
},
19+
description: {
20+
type: String,
21+
required: false
22+
},
23+
isActive: {
24+
type: Boolean,
25+
required: true
26+
}
27+
},
28+
{
29+
throughput: { read: 4, write: 2 }
30+
})
31+
32+
module.exports = schema

‎src/models/index.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Initialize and export all model schemas.
3+
*/
4+
5+
const config = require('config')
6+
const dynamoose = require('dynamoose')
7+
8+
dynamoose.AWS.config.update({
9+
accessKeyId: config.DYNAMODB.AWS_ACCESS_KEY_ID,
10+
secretAccessKey: config.DYNAMODB.AWS_SECRET_ACCESS_KEY,
11+
region: config.DYNAMODB.AWS_REGION
12+
})
13+
14+
if (config.DYNAMODB.IS_LOCAL) {
15+
dynamoose.local(config.DYNAMODB.URL)
16+
}
17+
18+
dynamoose.setDefaults({
19+
create: false,
20+
update: false
21+
})
22+
23+
module.exports = {
24+
Challenge: dynamoose.model('Challenge', require('./Challenge')),
25+
ChallengeType: dynamoose.model('ChallengeType', require('./ChallengeType')),
26+
ChallengeSetting: dynamoose.model('ChallengeSetting', require('./ChallengeSetting')),
27+
AuditLog: dynamoose.model('AuditLog', require('./AuditLog'))
28+
}

‎src/routes.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* Contains all routes
3+
*/
4+
5+
const constants = require('../app-constants')
6+
7+
module.exports = {
8+
'/challenges': {
9+
get: {
10+
controller: 'ChallengeController',
11+
method: 'searchChallenges'
12+
},
13+
post: {
14+
controller: 'ChallengeController',
15+
method: 'createChallenge',
16+
auth: 'jwt',
17+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot]
18+
}
19+
},
20+
'/challenges/:challengeId': {
21+
get: {
22+
controller: 'ChallengeController',
23+
method: 'getChallenge'
24+
},
25+
put: {
26+
controller: 'ChallengeController',
27+
method: 'fullyUpdateChallenge',
28+
auth: 'jwt',
29+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot]
30+
},
31+
patch: {
32+
controller: 'ChallengeController',
33+
method: 'partiallyUpdateChallenge',
34+
auth: 'jwt',
35+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot]
36+
}
37+
},
38+
'/challengeTypes': {
39+
get: {
40+
controller: 'ChallengeTypeController',
41+
method: 'searchChallengeTypes'
42+
},
43+
post: {
44+
controller: 'ChallengeTypeController',
45+
method: 'createChallengeType',
46+
auth: 'jwt',
47+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot]
48+
}
49+
},
50+
'/challengeTypes/:challengeTypeId': {
51+
get: {
52+
controller: 'ChallengeTypeController',
53+
method: 'getChallengeType'
54+
},
55+
put: {
56+
controller: 'ChallengeTypeController',
57+
method: 'fullyUpdateChallengeType',
58+
auth: 'jwt',
59+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot]
60+
},
61+
patch: {
62+
controller: 'ChallengeTypeController',
63+
method: 'partiallyUpdateChallengeType',
64+
auth: 'jwt',
65+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot]
66+
}
67+
},
68+
'/challengeSettings': {
69+
get: {
70+
controller: 'ChallengeSettingController',
71+
method: 'searchChallengeSettings',
72+
auth: 'jwt',
73+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot]
74+
},
75+
post: {
76+
controller: 'ChallengeSettingController',
77+
method: 'createChallengeSetting',
78+
auth: 'jwt',
79+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot]
80+
}
81+
},
82+
'/challengeSettings/:challengeSettingId': {
83+
get: {
84+
controller: 'ChallengeSettingController',
85+
method: 'getChallengeSetting',
86+
auth: 'jwt',
87+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot]
88+
},
89+
put: {
90+
controller: 'ChallengeSettingController',
91+
method: 'updateChallengeSetting',
92+
auth: 'jwt',
93+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot]
94+
}
95+
},
96+
'/challengeAuditLogs': {
97+
get: {
98+
controller: 'AuditLogController',
99+
method: 'searchAuditLogs',
100+
auth: 'jwt',
101+
access: [constants.UserRoles.Admin]
102+
}
103+
}
104+
}

‎src/services/AuditLogService.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* This service provides operations of audit logs.
3+
*/
4+
5+
const _ = require('lodash')
6+
const Joi = require('joi')
7+
const helper = require('../common/helper')
8+
const logger = require('../common/logger')
9+
10+
/**
11+
* Search audit logs
12+
* @param {Object} criteria the search criteria
13+
* @returns {Object} the search result
14+
*/
15+
async function searchAuditLogs (criteria) {
16+
const list = await helper.scan('AuditLog')
17+
const records = _.filter(list, e => helper.partialMatch(criteria.fieldName, e.fieldName) &&
18+
(_.isUndefined(criteria.createdDateStart) || criteria.createdDateStart.getTime() <= e.created.getTime()) &&
19+
(_.isUndefined(criteria.createdDateEnd) || criteria.createdDateEnd.getTime() >= e.created.getTime()) &&
20+
(_.isUndefined(criteria.challengeId) || criteria.challengeId === e.challengeId) &&
21+
(_.isUndefined(criteria.createdBy) || criteria.createdBy.toLowerCase() === e.createdBy.toLowerCase())
22+
)
23+
const total = records.length
24+
const result = records.slice((criteria.page - 1) * criteria.perPage, criteria.page * criteria.perPage)
25+
26+
return { total, page: criteria.page, perPage: criteria.perPage, result }
27+
}
28+
29+
searchAuditLogs.schema = {
30+
criteria: Joi.object().keys({
31+
page: Joi.page(),
32+
perPage: Joi.perPage(),
33+
challengeId: Joi.string(),
34+
fieldName: Joi.string(),
35+
createdDateStart: Joi.date(),
36+
createdDateEnd: Joi.date(),
37+
createdBy: Joi.string()
38+
})
39+
}
40+
41+
module.exports = {
42+
searchAuditLogs
43+
}
44+
45+
logger.buildService(module.exports)

‎src/services/ChallengeService.js

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
/**
2+
* This service provides operations of challenge.
3+
*/
4+
5+
const _ = require('lodash')
6+
const Joi = require('joi')
7+
const uuid = require('uuid/v4')
8+
const dynamoose = require('dynamoose')
9+
const helper = require('../common/helper')
10+
const logger = require('../common/logger')
11+
const errors = require('../common/errors')
12+
const models = require('../models')
13+
14+
/**
15+
* Search challenges
16+
* @param {Object} criteria the search criteria
17+
* @returns {Object} the search result
18+
*/
19+
async function searchChallenges (criteria) {
20+
const list = await helper.scan('Challenge')
21+
const records = _.filter(list, e => helper.partialMatch(criteria.name, e.name) &&
22+
helper.partialMatch(criteria.description, e.description) &&
23+
(_.isUndefined(criteria.createdDateStart) || criteria.createdDateStart.getTime() <= e.created.getTime()) &&
24+
(_.isUndefined(criteria.createdDateEnd) || criteria.createdDateEnd.getTime() >= e.created.getTime()) &&
25+
(_.isUndefined(criteria.updatedDateStart) || (!_.isUndefined(e.updated) && criteria.updatedDateStart.getTime() <= e.updated.getTime())) &&
26+
(_.isUndefined(criteria.updatedDateEnd) || (!_.isUndefined(e.updated) && criteria.updatedDateEnd.getTime() >= e.updated.getTime())) &&
27+
(_.isUndefined(criteria.createdBy) || criteria.createdBy.toLowerCase() === e.createdBy.toLowerCase())
28+
)
29+
const total = records.length
30+
const result = records.slice((criteria.page - 1) * criteria.perPage, criteria.page * criteria.perPage)
31+
32+
const typeList = await helper.scan('ChallengeType')
33+
const typeMap = new Map()
34+
_.each(typeList, e => {
35+
typeMap.set(e.id, e.name)
36+
})
37+
_.each(result, element => {
38+
element.type = typeMap.get(element.typeId)
39+
delete element.typeId
40+
})
41+
42+
return { total, page: criteria.page, perPage: criteria.perPage, result: await populateSettings(result) }
43+
}
44+
45+
searchChallenges.schema = {
46+
criteria: Joi.object().keys({
47+
page: Joi.page(),
48+
perPage: Joi.perPage(),
49+
name: Joi.string(),
50+
description: Joi.string(),
51+
createdDateStart: Joi.date(),
52+
createdDateEnd: Joi.date(),
53+
updatedDateStart: Joi.date(),
54+
updatedDateEnd: Joi.date(),
55+
createdBy: Joi.string()
56+
})
57+
}
58+
59+
/**
60+
* Validate the challenge data.
61+
* @param {Object} challenge the challenge data
62+
*/
63+
async function validateChallengeData (challenge) {
64+
if (challenge.typeId) {
65+
try {
66+
await helper.getById('ChallengeType', challenge.typeId)
67+
} catch (e) {
68+
if (e.name === 'NotFoundError') {
69+
throw new errors.BadRequestError(`No challenge type found with id: ${challenge.typeId}.`)
70+
} else {
71+
throw e
72+
}
73+
}
74+
}
75+
if (challenge.challengeSettings) {
76+
const list = await helper.scan('ChallengeSetting')
77+
const map = new Map()
78+
_.each(list, e => {
79+
map.set(e.id, e.name)
80+
})
81+
const invalidSettings = _.filter(challenge.challengeSettings, s => !map.has(s.type))
82+
if (invalidSettings.length > 0) {
83+
throw new errors.BadRequestError(`The following settings are invalid: ${invalidSettings}`)
84+
}
85+
}
86+
}
87+
88+
/**
89+
* Create challenge.
90+
* @param {Object} currentUser the user who perform operation
91+
* @param {Object} challenge the challenge to created
92+
* @returns {Object} the created challenge
93+
*/
94+
async function createChallenge (currentUser, challenge) {
95+
await validateChallengeData(challenge)
96+
97+
const ret = await helper.create('Challenge', _.assign({
98+
id: uuid(), created: new Date(), createdBy: currentUser.handle }, challenge))
99+
return ret
100+
}
101+
102+
createChallenge.schema = {
103+
currentUser: Joi.any(),
104+
challenge: Joi.object().keys({
105+
typeId: Joi.string().required(),
106+
track: Joi.string().required(),
107+
name: Joi.string().required(),
108+
description: Joi.string().required(),
109+
challengeSettings: Joi.array().items(Joi.object().keys({
110+
type: Joi.string().required(),
111+
value: Joi.string().required()
112+
})).unique((a, b) => a.type === b.type)
113+
}).required()
114+
}
115+
116+
/**
117+
* Populate challenge settings data.
118+
* @param {Object|Array} the challenge entities
119+
* @param {Object|Array} the modified challenge entities
120+
*/
121+
async function populateSettings (data) {
122+
const list = await helper.scan('ChallengeSetting')
123+
const map = new Map()
124+
_.each(list, e => {
125+
map.set(e.id, e.name)
126+
})
127+
if (_.isArray(data)) {
128+
_.each(data, element => {
129+
if (element.challengeSettings) {
130+
_.each(element.challengeSettings, s => {
131+
s.type = map.get(s.type)
132+
})
133+
}
134+
})
135+
} else if (data.challengeSettings) {
136+
_.each(data.challengeSettings, s => {
137+
s.type = map.get(s.type)
138+
})
139+
}
140+
return data
141+
}
142+
143+
/**
144+
* Get challenge.
145+
* @param {String} id the challenge id
146+
* @returns {Object} the challenge with given id
147+
*/
148+
async function getChallenge (id) {
149+
const challenge = await helper.getById('Challenge', id)
150+
151+
// populate type property based on the typeId
152+
const type = await helper.getById('ChallengeType', challenge.typeId)
153+
challenge.type = type.name
154+
delete challenge.typeId
155+
156+
return populateSettings(challenge)
157+
}
158+
159+
getChallenge.schema = {
160+
id: Joi.id()
161+
}
162+
163+
/**
164+
* Update challenge.
165+
* @param {Object} currentUser the user who perform operation
166+
* @param {String} challengeId the challenge id
167+
* @param {Object} data the challenge data to be updated
168+
* @param {Boolean} isFull the flag indicate it is a fully update operation.
169+
* @returns {Object} the updated challenge
170+
*/
171+
async function update (currentUser, challengeId, data, isFull) {
172+
const challenge = await helper.getById('Challenge', challengeId)
173+
174+
if (challenge.createdBy.toLowerCase() !== currentUser.handle.toLowerCase() && !helper.hasAdminRole(currentUser)) {
175+
throw new errors.ForbiddenError(`Only admin or challenge's copilot can perform modification.`)
176+
}
177+
178+
await validateChallengeData(data)
179+
180+
data.updated = new Date()
181+
data.updatedBy = currentUser.handle
182+
const updateDetails = {}
183+
const transactionItems = []
184+
_.each(data, (value, key) => {
185+
let op
186+
if (key === 'challengeSettings') {
187+
if (challenge.challengeSettings) {
188+
if (_.differenceWith(challenge.challengeSettings, value, _.isEqual).length !== 0) {
189+
op = '$PUT'
190+
}
191+
} else {
192+
op = '$PUT'
193+
}
194+
} else if (_.isUndefined(challenge[key]) || challenge[key] !== value) {
195+
op = '$PUT'
196+
}
197+
198+
if (op) {
199+
if (_.isUndefined(updateDetails[op])) {
200+
updateDetails[op] = {}
201+
}
202+
updateDetails[op][key] = value
203+
if (key !== 'updated' && key !== 'updatedBy') {
204+
transactionItems.push(models.AuditLog.transaction.create({
205+
id: uuid(),
206+
challengeId,
207+
fieldName: key,
208+
oldValue: challenge[key] ? JSON.stringify(challenge[key]) : 'NULL',
209+
newValue: JSON.stringify(value),
210+
created: new Date(),
211+
createdBy: currentUser.handle
212+
}))
213+
}
214+
}
215+
})
216+
217+
if (isFull && _.isUndefined(data.challengeSettings) && challenge.challengeSettings) {
218+
updateDetails['$DELETE'] = { challengeSettings: _.cloneDeep(challenge.challengeSettings) }
219+
transactionItems.push(models.AuditLog.transaction.create({
220+
id: uuid(),
221+
challengeId,
222+
fieldName: 'challengeSettings',
223+
oldValue: JSON.stringify(challenge.challengeSettings),
224+
newValue: 'NULL',
225+
created: new Date(),
226+
createdBy: currentUser.handle
227+
}))
228+
delete challenge.challengeSettings
229+
}
230+
231+
transactionItems.push(models.Challenge.transaction.update({ id: challengeId }, updateDetails))
232+
233+
await dynamoose.transaction(transactionItems)
234+
235+
_.assign(challenge, data)
236+
return challenge
237+
}
238+
239+
/**
240+
* Fully update challenge.
241+
* @param {Object} currentUser the user who perform operation
242+
* @param {String} challengeId the challenge id
243+
* @param {Object} data the challenge data to be updated
244+
* @returns {Object} the updated challenge
245+
*/
246+
async function fullyUpdateChallenge (currentUser, challengeId, data) {
247+
return update(currentUser, challengeId, data, true)
248+
}
249+
250+
fullyUpdateChallenge.schema = {
251+
currentUser: Joi.any(),
252+
challengeId: Joi.id(),
253+
data: Joi.object().keys({
254+
typeId: Joi.string().required(),
255+
track: Joi.string().required(),
256+
name: Joi.string().required(),
257+
description: Joi.string().required(),
258+
challengeSettings: Joi.array().items(Joi.object().keys({
259+
type: Joi.string().required(),
260+
value: Joi.string().required()
261+
})).unique((a, b) => a.type === b.type)
262+
}).required()
263+
}
264+
265+
/**
266+
* Partially update challenge.
267+
* @param {Object} currentUser the user who perform operation
268+
* @param {String} challengeId the challenge id
269+
* @param {Object} data the challenge data to be updated
270+
* @returns {Object} the updated challenge
271+
*/
272+
async function partiallyUpdateChallenge (currentUser, challengeId, data) {
273+
return update(currentUser, challengeId, data)
274+
}
275+
276+
partiallyUpdateChallenge.schema = {
277+
currentUser: Joi.any(),
278+
challengeId: Joi.id(),
279+
data: Joi.object().keys({
280+
typeId: Joi.string(),
281+
track: Joi.string(),
282+
name: Joi.string(),
283+
description: Joi.string(),
284+
challengeSettings: Joi.array().items(Joi.object().keys({
285+
type: Joi.string().required(),
286+
value: Joi.string().required()
287+
})).unique((a, b) => a.type === b.type)
288+
}).required()
289+
}
290+
291+
module.exports = {
292+
searchChallenges,
293+
createChallenge,
294+
getChallenge,
295+
fullyUpdateChallenge,
296+
partiallyUpdateChallenge
297+
}
298+
299+
logger.buildService(module.exports)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* This service provides operations of challenge settings.
3+
*/
4+
5+
const _ = require('lodash')
6+
const Joi = require('joi')
7+
const uuid = require('uuid/v4')
8+
const helper = require('../common/helper')
9+
const logger = require('../common/logger')
10+
11+
/**
12+
* Search challenge settings
13+
* @param {Object} criteria the search criteria
14+
* @returns {Object} the search result
15+
*/
16+
async function searchChallengeSettings (criteria) {
17+
const list = await helper.scan('ChallengeSetting')
18+
const records = _.filter(list, e => helper.partialMatch(criteria.name, e.name))
19+
const total = records.length
20+
const result = records.slice((criteria.page - 1) * criteria.perPage, criteria.page * criteria.perPage)
21+
22+
return { total, page: criteria.page, perPage: criteria.perPage, result }
23+
}
24+
25+
searchChallengeSettings.schema = {
26+
criteria: Joi.object().keys({
27+
page: Joi.page(),
28+
perPage: Joi.perPage(),
29+
name: Joi.string()
30+
})
31+
}
32+
33+
/**
34+
* Create challenge setting.
35+
* @param {Object} setting the challenge setting to created
36+
* @returns {Object} the created challenge setting
37+
*/
38+
async function createChallengeSetting (setting) {
39+
await helper.validateDuplicate('ChallengeSetting', 'name', setting.name)
40+
const ret = await helper.create('ChallengeSetting', _.assign({ id: uuid() }, setting))
41+
return ret
42+
}
43+
44+
createChallengeSetting.schema = {
45+
setting: Joi.object().keys({
46+
name: Joi.string().required()
47+
}).required()
48+
}
49+
50+
/**
51+
* Get challenge setting.
52+
* @param {String} id the challenge setting id
53+
* @returns {Object} the challenge setting with given id
54+
*/
55+
async function getChallengeSetting (id) {
56+
const ret = await helper.getById('ChallengeSetting', id)
57+
return ret
58+
}
59+
60+
getChallengeSetting.schema = {
61+
id: Joi.id()
62+
}
63+
64+
/**
65+
* Update challenge setting.
66+
* @param {String} id the challenge setting id
67+
* @param {Object} data the challenge setting data to be updated
68+
* @returns {Object} the updated challenge setting
69+
*/
70+
async function updateChallengeSetting (id, data) {
71+
const setting = await helper.getById('ChallengeSetting', id)
72+
if (setting.name.toLowerCase() !== data.name.toLowerCase()) {
73+
await helper.validateDuplicate('ChallengeSetting', 'name', data.name)
74+
}
75+
return helper.update(setting, data)
76+
}
77+
78+
updateChallengeSetting.schema = {
79+
id: Joi.id(),
80+
data: Joi.object().keys({
81+
name: Joi.string().required()
82+
}).required()
83+
}
84+
85+
module.exports = {
86+
searchChallengeSettings,
87+
createChallengeSetting,
88+
getChallengeSetting,
89+
updateChallengeSetting
90+
}
91+
92+
logger.buildService(module.exports)

‎src/services/ChallengeTypeService.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* This service provides operations of challenge types.
3+
*/
4+
5+
const _ = require('lodash')
6+
const Joi = require('joi')
7+
const uuid = require('uuid/v4')
8+
const helper = require('../common/helper')
9+
const logger = require('../common/logger')
10+
11+
/**
12+
* Search challenge types
13+
* @param {Object} criteria the search criteria
14+
* @returns {Object} the search result
15+
*/
16+
async function searchChallengeTypes (criteria) {
17+
const list = await helper.scan('ChallengeType')
18+
const records = _.filter(list, e => helper.partialMatch(criteria.name, e.name) &&
19+
helper.partialMatch(criteria.description, e.description) &&
20+
(_.isUndefined(criteria.isActive) || e.isActive === criteria.isActive)
21+
)
22+
const total = records.length
23+
const result = records.slice((criteria.page - 1) * criteria.perPage, criteria.page * criteria.perPage)
24+
25+
return { total, page: criteria.page, perPage: criteria.perPage, result }
26+
}
27+
28+
searchChallengeTypes.schema = {
29+
criteria: Joi.object().keys({
30+
page: Joi.page(),
31+
perPage: Joi.perPage(),
32+
name: Joi.string(),
33+
description: Joi.string(),
34+
isActive: Joi.boolean()
35+
})
36+
}
37+
38+
/**
39+
* Create challenge type.
40+
* @param {Object} type the challenge type to created
41+
* @returns {Object} the created challenge type
42+
*/
43+
async function createChallengeType (type) {
44+
await helper.validateDuplicate('ChallengeType', 'name', type.name)
45+
const ret = await helper.create('ChallengeType', _.assign({ id: uuid() }, type))
46+
return ret
47+
}
48+
49+
createChallengeType.schema = {
50+
type: Joi.object().keys({
51+
name: Joi.string().required(),
52+
description: Joi.string(),
53+
isActive: Joi.boolean().required()
54+
}).required()
55+
}
56+
57+
/**
58+
* Get challenge type.
59+
* @param {String} id the challenge type id
60+
* @returns {Object} the challenge type with given id
61+
*/
62+
async function getChallengeType (id) {
63+
const ret = await helper.getById('ChallengeType', id)
64+
return ret
65+
}
66+
67+
getChallengeType.schema = {
68+
id: Joi.id()
69+
}
70+
71+
/**
72+
* Fully update challenge type.
73+
* @param {String} id the challenge type id
74+
* @param {Object} data the challenge type data to be updated
75+
* @returns {Object} the updated challenge type
76+
*/
77+
async function fullyUpdateChallengeType (id, data) {
78+
const type = await helper.getById('ChallengeType', id)
79+
if (type.name.toLowerCase() !== data.name.toLowerCase()) {
80+
await helper.validateDuplicate('ChallengeType', 'name', data.name)
81+
}
82+
if (_.isUndefined(data.description)) {
83+
type.description = undefined
84+
}
85+
return helper.update(type, data)
86+
}
87+
88+
fullyUpdateChallengeType.schema = {
89+
id: Joi.id(),
90+
data: Joi.object().keys({
91+
name: Joi.string().required(),
92+
description: Joi.string(),
93+
isActive: Joi.boolean().required()
94+
}).required()
95+
}
96+
97+
/**
98+
* Partially update challenge type.
99+
* @param {String} id the challenge type id
100+
* @param {Object} data the challenge type data to be updated
101+
* @returns {Object} the updated challenge type
102+
*/
103+
async function partiallyUpdateChallengeType (id, data) {
104+
const type = await helper.getById('ChallengeType', id)
105+
if (data.name && type.name.toLowerCase() !== data.name.toLowerCase()) {
106+
await helper.validateDuplicate('ChallengeType', 'name', data.name)
107+
}
108+
return helper.update(type, data)
109+
}
110+
111+
partiallyUpdateChallengeType.schema = {
112+
id: Joi.id(),
113+
data: Joi.object().keys({
114+
name: Joi.string(),
115+
description: Joi.string(),
116+
isActive: Joi.boolean()
117+
}).required()
118+
}
119+
120+
module.exports = {
121+
searchChallengeTypes,
122+
createChallengeType,
123+
getChallengeType,
124+
fullyUpdateChallengeType,
125+
partiallyUpdateChallengeType
126+
}
127+
128+
logger.buildService(module.exports)

‎src/test-data.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Insert test data to database.
3+
*/
4+
require('../app-bootstrap')
5+
const logger = require('./common/logger')
6+
const helper = require('./common/helper')
7+
8+
logger.info('Insert test data into database.')
9+
10+
const insertData = async () => {
11+
const settings = []
12+
for (let i = 1; i <= 5; i++) {
13+
const setting = { id: `11ab038e-48da-123b-96e8-8d3b99b6d18${i}`, name: `setting-name-${i}` }
14+
settings.push(setting)
15+
await helper.create('ChallengeSetting', setting)
16+
}
17+
const types = []
18+
for (let i = 1; i <= 5; i++) {
19+
const type = { id: `fe6d0a58-ce7d-4521-8501-b8132b1c039${i}`, name: `type-name-${i}`, isActive: i <= 3 }
20+
if (i % 2 === 1) {
21+
type.description = `descritpion${i}`
22+
}
23+
types.push(type)
24+
await helper.create('ChallengeType', type)
25+
}
26+
const challenges = []
27+
for (let i = 0; i < 10; i++) {
28+
const challenge = {
29+
id: `071cd9fa-99d1-4733-8d27-3b745b2bc6be${i}`,
30+
legacyId: 30080001 + i,
31+
typeId: types[i % 5].id,
32+
track: `track${i}`,
33+
name: `test-name${i}`,
34+
description: `desc-${i % 5}`,
35+
challengeSettings: [],
36+
created: new Date(),
37+
createdBy: 'hohosky'
38+
}
39+
let j = i % 3
40+
while (j < 5) {
41+
challenge.challengeSettings.push({ type: settings[j].id, value: `test-value${i}${j}` })
42+
j = j + 3
43+
}
44+
challenges.push(challenge)
45+
await helper.create('Challenge', challenge)
46+
}
47+
}
48+
49+
insertData().then(() => {
50+
logger.info('Done!')
51+
process.exit()
52+
}).catch((e) => {
53+
logger.logFullError(e)
54+
process.exit(1)
55+
})

0 commit comments

Comments
 (0)
Please sign in to comment.