diff --git a/ReadMe.md b/ReadMe.md index 0952b37..af3558e 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -86,7 +86,7 @@ The following parameters can be set in config files or in env variables: - BUSAPI_URL: Bus API URL - KAFKA_ERROR_TOPIC: Kafka error topic used by bus API wrapper - GROUPS_API_URL: Groups API URL -- AMAZON.AWS_ACCESS_KEY_ID: The Amazon certificate key to use when connecting. +- AMAZON.AWS_ACCESS_KEY_ID: The Amazon certificate key to use when connecting. - AMAZON.AWS_SECRET_ACCESS_KEY: The Amazon certificate access key to use when connecting. - AMAZON.AWS.SESSION_TOKEN: The user session token, used when developing locally against the TC dev AWS services - AMAZON.AWS_REGION: The Amazon certificate region to use when connecting. @@ -167,7 +167,7 @@ These commands will set auth0 and event bus api to local mock server. ## Tests -Make sure you have followed above steps to +Make sure you have followed above steps to - setup db and config db url - setup local mock api and set local configs - it will really call service and mock api diff --git a/app-bootstrap.js b/app-bootstrap.js index 6eb4458..f581211 100644 --- a/app-bootstrap.js +++ b/app-bootstrap.js @@ -8,3 +8,4 @@ Joi.page = () => Joi.number().integer().min(1).default(1) Joi.perPage = () => Joi.number().integer().min(1).max(100).default(50) Joi.size = () => Joi.number().integer().min(1).max(1000).default(500) Joi.sort = () => Joi.string().default('asc') +Joi.positive = () => Joi.number().integer().min(0) diff --git a/docs/Member API.postman_collection.json b/docs/Member API.postman_collection.json index 88e73c9..400b6bf 100644 --- a/docs/Member API.postman_collection.json +++ b/docs/Member API.postman_collection.json @@ -1,9 +1,9 @@ { "info": { - "_postman_id": "12c468e8-dbc4-4cc2-b187-36144423a29c", + "_postman_id": "6b1e570c-0cc5-4836-a38e-f1031dd7e14e", "name": "Member API", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "28573934" + "_exporter_id": "23458" }, "item": [ { @@ -633,6 +633,300 @@ }, "response": [] }, + { + "name": "Create History Stats", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBTdXBwb3J0IiwiYWRtaW5pc3RyYXRvciIsInRlc3RSb2xlIiwiYWFhIiwidG9ueV90ZXN0XzEiLCJDb25uZWN0IE1hbmFnZXIiLCJDb25uZWN0IEFkbWluIiwiY29waWxvdCIsIkNvbm5lY3QgQ29waWxvdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJUb255SiIsImV4cCI6MTk4MDk5Mjc4OCwidXNlcklkIjoiODU0Nzg5OSIsImlhdCI6MTU0OTc5MTYxMSwiZW1haWwiOiJ0amVmdHMrZml4QHRvcGNvZGVyLmNvbSIsImp0aSI6ImY5NGQxZTI2LTNkMGUtNDZjYS04MTE1LTg3NTQ1NDRhMDhmMSJ9.q_Db9Gw8bn54xlythrZZUrJQyak-XrdOwPsj6ddgZ4M", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"DEVELOP\": {\n \"subTracks\": [\n {\n \"id\": \"112\",\n \"name\": \"DESIGN\",\n \"history\": [\n {\n \"challengeId\": 30022821,\n \"challengeName\": \"Review Management updated\",\n \"newRating\": 888,\n \"ratingDate\": 1333332260000\n },\n {\n \"challengeId\": 30023283,\n \"challengeName\": \"Upgrade Java Object Cache\",\n \"ratingDate\": 1316774640000,\n \"newRating\": 484\n },\n {\n \"challengeId\": 30029681,\n \"challengeName\": \"FMS Front End Validation Attribute and Report\",\n \"ratingDate\": 1351292700000,\n \"newRating\": 525\n },\n {\n \"challengeId\": 30031028,\n \"challengeName\": \"TMAN System Thick Client Back End Synchronization Services\",\n \"ratingDate\": 1351505100000,\n \"newRating\": 539\n },\n {\n \"challengeId\": 99999821,\n \"challengeName\": \"New challenge name\",\n \"newRating\": 888,\n \"ratingDate\": 1333332260000\n }\n ]\n },\n {\n \"id\": \"113\",\n \"name\": \"DEVELOPMENT\",\n \"history\": [\n {\n \"challengeId\": 30017719,\n \"challengeName\": \"Object Factory and Plugins Update\",\n \"ratingDate\": 1307855100000,\n \"newRating\": 1023\n },\n {\n \"challengeId\": 30017701,\n \"challengeName\": \"Component - QMATS Chromosome Spring Controllers\",\n \"ratingDate\": 1308996300000,\n \"newRating\": 958\n },\n {\n \"challengeId\": 99999822,\n \"challengeName\": \"New challenge name 2\",\n \"newRating\": 777,\n \"ratingDate\": 1333332260000\n }\n ]\n },\n {\n \"id\": \"888\",\n \"name\": \"NEW_DEVELOPMENT\",\n \"history\": [\n {\n \"challengeId\": 99999850,\n \"challengeName\": \"NEW_DEVELOPMENT challenge 01\",\n \"newRating\": 999,\n \"ratingDate\": 1333332260000\n },\n {\n \"challengeId\": 99999851,\n \"challengeName\": \"NEW_DEVELOPMENT challenge 02\",\n \"newRating\": 888,\n \"ratingDate\": 1333332260000\n }\n ]\n }\n ]\n },\n \"DATA_SCIENCE\": {\n \"SRM\": {\n \"history\": [\n {\n \"challengeId\": 14437,\n \"challengeName\": \"SRM 508 updated\",\n \"rating\": 1888,\n \"placement\": 111,\n \"percentile\": 88.88,\n \"date\": 1333332800000\n },\n {\n \"challengeId\": 14438,\n \"challengeName\": \"SRM 509\",\n \"date\": 1307491200000,\n \"rating\": 1408,\n \"placement\": 1785,\n \"percentile\": 79.174\n },\n {\n \"challengeId\": 14560,\n \"challengeName\": \"TCO11 Round 1\",\n \"date\": 1308355200000,\n \"rating\": 1367,\n \"placement\": 1931,\n \"percentile\": 76.8076\n },\n {\n \"challengeId\": 88881,\n \"challengeName\": \"NEW SRM 506\",\n \"rating\": 1888,\n \"placement\": 666,\n \"percentile\": 88.88,\n \"date\": 1333332000000\n }\n ]\n },\n \"MARATHON_MATCH\": {\n \"history\": [\n {\n \"challengeId\": 88891,\n \"challengeName\": \"NEW MM 101\",\n \"rating\": 1888,\n \"placement\": 666,\n \"percentile\": 88.88,\n \"date\": 1333332000000\n },\n {\n \"challengeId\": 88892,\n \"challengeName\": \"NEW MM 102\",\n \"rating\": 1777,\n \"placement\": 777,\n \"percentile\": 77.77,\n \"date\": 1333332000000\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/v6/members/iamtong/stats/history", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "v6", + "members", + "iamtong", + "stats", + "history" + ] + } + }, + "response": [] + }, + { + "name": "Create History Stats with empty data", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBTdXBwb3J0IiwiYWRtaW5pc3RyYXRvciIsInRlc3RSb2xlIiwiYWFhIiwidG9ueV90ZXN0XzEiLCJDb25uZWN0IE1hbmFnZXIiLCJDb25uZWN0IEFkbWluIiwiY29waWxvdCIsIkNvbm5lY3QgQ29waWxvdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJUb255SiIsImV4cCI6MTk4MDk5Mjc4OCwidXNlcklkIjoiODU0Nzg5OSIsImlhdCI6MTU0OTc5MTYxMSwiZW1haWwiOiJ0amVmdHMrZml4QHRvcGNvZGVyLmNvbSIsImp0aSI6ImY5NGQxZTI2LTNkMGUtNDZjYS04MTE1LTg3NTQ1NDRhMDhmMSJ9.q_Db9Gw8bn54xlythrZZUrJQyak-XrdOwPsj6ddgZ4M", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"DEVELOP\": {\n \"subTracks\": [\n {\n \"id\": \"112\",\n \"name\": \"DESIGN\",\n \"history\": []\n },\n {\n \"id\": \"113\",\n \"name\": \"DEVELOPMENT\",\n \"history\": []\n },\n {\n \"id\": \"888\",\n \"name\": \"NEW_DEVELOPMENT\",\n \"history\": []\n }\n ]\n },\n \"DATA_SCIENCE\": {\n \"SRM\": {\n \"history\": []\n },\n \"MARATHON_MATCH\": {\n \"history\": []\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/v6/members/jiangliwu/stats/history", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "v6", + "members", + "jiangliwu", + "stats", + "history" + ] + } + }, + "response": [] + }, + { + "name": "Update History Stats", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJwaGVhZCIsImV4cCI6MTk4MDk5Mjc4OCwidXNlcklkIjoiMjI3NDI3NjQiLCJpYXQiOjE1NDk3OTk1NjksImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiOWM0NTExYzUtYzE2NS00YTFiLTg5OWUtYjY1YWQwZTAyYjU1In0.kOPm331fsiXj2y_7by2ohjoaGWIGon0TxKL1znbpijU", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"DEVELOP\": {\n \"subTracks\": [\n {\n \"id\": \"112\",\n \"name\": \"DESIGN\",\n \"history\": [\n {\n \"challengeId\": 30022821,\n \"challengeName\": \"Review Management updated\",\n \"newRating\": 888,\n \"ratingDate\": 1333332260000\n },\n {\n \"challengeId\": 30023283,\n \"challengeName\": \"Upgrade Java Object Cache updated\"\n },\n {\n \"challengeId\": 30029681,\n \"newRating\": 888\n },\n {\n \"challengeId\": 30031028,\n \"ratingDate\": 1333335100000\n },\n {\n \"challengeId\": 99999921,\n \"challengeName\": \"New challenge name\",\n \"newRating\": 888,\n \"ratingDate\": 1333332260000\n }\n ]\n },\n {\n \"id\": \"113\",\n \"name\": \"DEVELOPMENT\",\n \"history\": [\n {\n \"challengeId\": 30010833,\n \"challengeName\": \"Data formatting from List to CSV via XML updated\",\n \"newRating\": 999\n },\n {\n \"challengeId\": 30011015,\n \"challengeName\": \"Log History Trim Utility updated\"\n },\n {\n \"challengeId\": 99999922,\n \"challengeName\": \"New challenge name 2\",\n \"newRating\": 777,\n \"ratingDate\": 1333332260000\n }\n ]\n },\n {\n \"id\": \"888\",\n \"name\": \"NEW_DEVELOPMENT\",\n \"history\": [\n {\n \"challengeId\": 99999950,\n \"challengeName\": \"NEW_DEVELOPMENT challenge 01\",\n \"newRating\": 999,\n \"ratingDate\": 1333332260000\n },\n {\n \"challengeId\": 99999951,\n \"challengeName\": \"NEW_DEVELOPMENT challenge 02\",\n \"newRating\": 888,\n \"ratingDate\": 1333332260000\n }\n ]\n }\n ]\n },\n \"DATA_SCIENCE\": {\n \"SRM\": {\n \"history\": [\n {\n \"challengeId\": 14437,\n \"challengeName\": \"SRM 508 updated\",\n \"rating\": 1888,\n \"placement\": 111,\n \"percentile\": 88.88,\n \"date\": 1333332800000\n },\n {\n \"challengeId\": 14438,\n \"challengeName\": \"SRM 509 updated\"\n },\n {\n \"challengeId\": 14560,\n \"rating\": 888\n },\n {\n \"challengeId\": 14531,\n \"placement\": 888\n },\n {\n \"challengeId\": 14530,\n \"percentile\": 88.88\n },\n {\n \"challengeId\": 14278,\n \"date\": 1333339200000\n },\n {\n \"challengeId\": 88881,\n \"challengeName\": \"NEW SRM 506\",\n \"rating\": 1888,\n \"placement\": 666,\n \"percentile\": 88.88,\n \"date\": 1333332000000\n }\n ]\n },\n \"MARATHON_MATCH\": {\n \"history\": [\n {\n \"challengeId\": 88891,\n \"challengeName\": \"NEW MM 101\",\n \"rating\": 1888,\n \"placement\": 666,\n \"percentile\": 88.88,\n \"date\": 1333332000000\n },\n {\n \"challengeId\": 88892,\n \"challengeName\": \"NEW MM 102\",\n \"rating\": 1777,\n \"placement\": 777,\n \"percentile\": 77.77,\n \"date\": 1333332000000\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/v6/members/phead/stats/history", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "v6", + "members", + "phead", + "stats", + "history" + ] + } + }, + "response": [] + }, + { + "name": "Update History Stats with empty array", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJwaGVhZCIsImV4cCI6MTk4MDk5Mjc4OCwidXNlcklkIjoiMjI3NDI3NjQiLCJpYXQiOjE1NDk3OTk1NjksImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiOWM0NTExYzUtYzE2NS00YTFiLTg5OWUtYjY1YWQwZTAyYjU1In0.kOPm331fsiXj2y_7by2ohjoaGWIGon0TxKL1znbpijU", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"DEVELOP\": {\n \"subTracks\": [\n {\n \"id\": \"112\",\n \"name\": \"DESIGN\",\n \"history\": [\n ]\n },\n {\n \"id\": \"113\",\n \"name\": \"DEVELOPMENT\",\n \"history\": [\n ]\n }\n ]\n },\n \"DATA_SCIENCE\": {\n \"SRM\": {\n \"history\": [\n ]\n },\n \"MARATHON_MATCH\": {\n \"history\": [\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/v6/members/phead/stats/history", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "v6", + "members", + "phead", + "stats", + "history" + ] + } + }, + "response": [] + }, + { + "name": "Update History Stats with duplicate subTrack Id", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJwaGVhZCIsImV4cCI6MTk4MDk5Mjc4OCwidXNlcklkIjoiMjI3NDI3NjQiLCJpYXQiOjE1NDk3OTk1NjksImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiOWM0NTExYzUtYzE2NS00YTFiLTg5OWUtYjY1YWQwZTAyYjU1In0.kOPm331fsiXj2y_7by2ohjoaGWIGon0TxKL1znbpijU", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"DEVELOP\": {\n \"subTracks\": [\n {\n \"id\": \"112\",\n \"name\": \"DESIGN\",\n \"history\": [\n {\n \"challengeId\": 30022821,\n \"challengeName\": \"Review Management updated\",\n \"newRating\": 888,\n \"ratingDate\": 1333332260000\n },\n {\n \"challengeId\": 30023283,\n \"challengeName\": \"Upgrade Java Object Cache updated\"\n },\n {\n \"challengeId\": 30029681,\n \"newRating\": 888\n },\n {\n \"challengeId\": 30031028,\n \"ratingDate\": 1333335100000\n },\n {\n \"challengeId\": 99999921,\n \"challengeName\": \"New challenge name\",\n \"newRating\": 888,\n \"ratingDate\": 1333332260000\n }\n ]\n },\n {\n \"id\": \"112\",\n \"name\": \"NEW_DEVELOPMENT\",\n \"history\": [\n {\n \"challengeId\": 99999950,\n \"challengeName\": \"NEW_DEVELOPMENT challenge 01\",\n \"newRating\": 999,\n \"ratingDate\": 1333332260000\n },\n {\n \"challengeId\": 99999951,\n \"challengeName\": \"NEW_DEVELOPMENT challenge 02\",\n \"newRating\": 888,\n \"ratingDate\": 1333332260000\n }\n ]\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/v6/members/phead/stats/history", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "v6", + "members", + "phead", + "stats", + "history" + ] + } + }, + "response": [] + }, + { + "name": "Update History Stats with duplicate subTrack Name", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJwaGVhZCIsImV4cCI6MTk4MDk5Mjc4OCwidXNlcklkIjoiMjI3NDI3NjQiLCJpYXQiOjE1NDk3OTk1NjksImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiOWM0NTExYzUtYzE2NS00YTFiLTg5OWUtYjY1YWQwZTAyYjU1In0.kOPm331fsiXj2y_7by2ohjoaGWIGon0TxKL1znbpijU", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"DEVELOP\": {\n \"subTracks\": [\n {\n \"id\": \"112\",\n \"name\": \"DESIGN\",\n \"history\": [\n {\n \"challengeId\": 30022821,\n \"challengeName\": \"Review Management updated\",\n \"newRating\": 888,\n \"ratingDate\": 1333332260000\n },\n {\n \"challengeId\": 30023283,\n \"challengeName\": \"Upgrade Java Object Cache updated\"\n },\n {\n \"challengeId\": 30029681,\n \"newRating\": 888\n },\n {\n \"challengeId\": 30031028,\n \"ratingDate\": 1333335100000\n },\n {\n \"challengeId\": 99999921,\n \"challengeName\": \"New challenge name\",\n \"newRating\": 888,\n \"ratingDate\": 1333332260000\n }\n ]\n },\n {\n \"id\": \"888\",\n \"name\": \"DESIGN\",\n \"history\": [\n {\n \"challengeId\": 99999950,\n \"challengeName\": \"NEW_DEVELOPMENT challenge 01\",\n \"newRating\": 999,\n \"ratingDate\": 1333332260000\n },\n {\n \"challengeId\": 99999951,\n \"challengeName\": \"NEW_DEVELOPMENT challenge 02\",\n \"newRating\": 888,\n \"ratingDate\": 1333332260000\n }\n ]\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/v6/members/phead/stats/history", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "v6", + "members", + "phead", + "stats", + "history" + ] + } + }, + "response": [] + }, + { + "name": "Update History Stats with duplicate challenge Id", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJwaGVhZCIsImV4cCI6MTk4MDk5Mjc4OCwidXNlcklkIjoiMjI3NDI3NjQiLCJpYXQiOjE1NDk3OTk1NjksImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiOWM0NTExYzUtYzE2NS00YTFiLTg5OWUtYjY1YWQwZTAyYjU1In0.kOPm331fsiXj2y_7by2ohjoaGWIGon0TxKL1znbpijU", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"DATA_SCIENCE\": {\n \"SRM\": {\n \"history\": [\n {\n \"challengeId\": 14437,\n \"challengeName\": \"SRM 508 updated\",\n \"rating\": 1888,\n \"placement\": 111,\n \"percentile\": 88.88,\n \"date\": 1333332800000\n },\n {\n \"challengeId\": 14437,\n \"challengeName\": \"SRM 509 updated\"\n },\n {\n \"challengeId\": 88881,\n \"challengeName\": \"NEW SRM 506\",\n \"rating\": 1888,\n \"placement\": 666,\n \"percentile\": 88.88,\n \"date\": 1333332000000\n }\n ]\n },\n \"MARATHON_MATCH\": {\n \"history\": [\n {\n \"challengeId\": 88891,\n \"challengeName\": \"NEW MM 101\",\n \"rating\": 1888,\n \"placement\": 666,\n \"percentile\": 88.88,\n \"date\": 1333332000000\n },\n {\n \"challengeId\": 88892,\n \"challengeName\": \"NEW MM 102\",\n \"rating\": 1777,\n \"placement\": 777,\n \"percentile\": 77.77,\n \"date\": 1333332000000\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/v6/members/phead/stats/history", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "v6", + "members", + "phead", + "stats", + "history" + ] + } + }, + "response": [] + }, { "name": "Get Stats", "request": { @@ -642,7 +936,39 @@ "method": "GET", "header": [], "url": { - "raw": "http://localhost:3000/v6/members/ACRush/stats", + "raw": "http://localhost:3000/v6/members/ACRush/stats", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "v6", + "members", + "ACRush", + "stats" + ] + } + }, + "response": [] + }, + { + "name": "Get Stats for private group", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJwaGVhZCIsImV4cCI6MTk4MDk5Mjc4OCwidXNlcklkIjoiMjI3NDI3NjQiLCJpYXQiOjE1NDk3OTk1NjksImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiOWM0NTExYzUtYzE2NS00YTFiLTg5OWUtYjY1YWQwZTAyYjU1In0.kOPm331fsiXj2y_7by2ohjoaGWIGon0TxKL1znbpijU", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3000/v6/members/ACRush/stats?groupIds=20000000,20000001", "protocol": "http", "host": [ "localhost" @@ -653,13 +979,101 @@ "members", "ACRush", "stats" + ], + "query": [ + { + "key": "groupIds", + "value": "20000000,20000001" + } ] } }, "response": [] }, { - "name": "Get Stats for private group", + "name": "Create Stats", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBTdXBwb3J0IiwiYWRtaW5pc3RyYXRvciIsInRlc3RSb2xlIiwiYWFhIiwidG9ueV90ZXN0XzEiLCJDb25uZWN0IE1hbmFnZXIiLCJDb25uZWN0IEFkbWluIiwiY29waWxvdCIsIkNvbm5lY3QgQ29waWxvdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJUb255SiIsImV4cCI6MTk4MDk5Mjc4OCwidXNlcklkIjoiODU0Nzg5OSIsImlhdCI6MTU0OTc5MTYxMSwiZW1haWwiOiJ0amVmdHMrZml4QHRvcGNvZGVyLmNvbSIsImp0aSI6ImY5NGQxZTI2LTNkMGUtNDZjYS04MTE1LTg3NTQ1NDRhMDhmMSJ9.q_Db9Gw8bn54xlythrZZUrJQyak-XrdOwPsj6ddgZ4M", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"maxRating\": {\n \"rating\": 1122,\n \"ratingColor\": \"#9D9FA0\",\n \"track\": \"DATA_SCIENCE_CREATED\",\n \"subTrack\": \"SRM CREATED\"\n },\n \"challenges\": 666,\n \"wins\": 222,\n \"DEVELOP\": {\n \"challenges\": 100,\n \"wins\": 50,\n \"mostRecentSubmission\": 1133615980000,\n \"mostRecentEventDate\": 1144618321000,\n \"subTracks\": [\n {\n \"challenges\": 222,\n \"wins\": 2,\n \"id\": 112,\n \"name\": \"DESIGN\",\n \"mostRecentSubmission\": 1133615980000,\n \"mostRecentEventDate\": 1144618321000,\n \"submissions\": {\n \"appealSuccessRate\": 0.55,\n \"minScore\": 70.27,\n \"avgPlacement\": 2.307692307692307,\n \"reviewSuccessRate\": 0.9230769230769231,\n \"maxScore\": 98.88,\n \"avgScore\": 86.10692307692308,\n \"screeningSuccessRate\": 1,\n \"submissionRate\": 0.07142857142857142,\n \"winPercent\": 0.2307692307692308,\n \"numInquiries\": 222,\n \"submissions\": 22,\n \"passedScreening\": 13,\n \"passedReview\": 12,\n \"appeals\": 64\n },\n \"rank\": {\n \"overallPercentile\": 66.66,\n \"activeRank\": 11,\n \"overallCountryRank\": 146,\n \"reliability\": 11.11,\n \"rating\": 666,\n \"minRating\": 333,\n \"volatility\": 235,\n \"overallSchoolRank\": 11,\n \"overallRank\": 446,\n \"activeSchoolRank\": 11,\n \"activeCountryRank\":11,\n \"maxRating\": 888,\n \"activePercentile\": 11.11\n }\n },\n {\n \"id\": 666,\n \"name\": \"NEW_SUBTRACK3\"\n }\n ]\n },\n \"DESIGN\": {\n \"challenges\": 659,\n \"wins\": 271,\n \"mostRecentSubmission\": 1133615980000,\n \"mostRecentEventDate\": 1144618321000,\n \"subTracks\": [\n {\n \"id\": 17,\n \"name\": \"WEB_DESIGNS\",\n \"numInquiries\": 781,\n \"challenges\": 416,\n \"wins\": 189,\n \"winPercent\": 0.2639664804469274,\n \"avgPlacement\": 4.068627450980392,\n \"submissions\": 716,\n \"submissionRate\": 0.9167733674775929,\n \"passedScreening\": 710,\n \"screeningSuccessRate\": 0.9916201117318436,\n \"mostRecentEventDate\": 1422298803000,\n \"mostRecentSubmission\": 1422447360000\n },\n {\n \"id\": 18,\n \"name\": \"WIREFRAMES\",\n \"numInquiries\": 2,\n \"challenges\": 2,\n \"wins\": 1,\n \"winPercent\": 0.5,\n \"avgPlacement\": 1,\n \"submissions\": 2,\n \"submissionRate\": 1,\n \"passedScreening\": 2,\n \"screeningSuccessRate\": 1,\n \"mostRecentEventDate\": 1289984400000,\n \"mostRecentSubmission\": 1290150540000\n },\n {\n \"id\": 31,\n \"name\": \"FRONT_END_FLASH\",\n \"numInquiries\": 7,\n \"challenges\": 2,\n \"wins\": 1,\n \"winPercent\": 0.16666666666666666,\n \"avgPlacement\": 5,\n \"submissions\": 6,\n \"submissionRate\": 0.8571428571428571,\n \"passedScreening\": 6,\n \"screeningSuccessRate\": 1,\n \"mostRecentEventDate\": 1245452400000,\n \"mostRecentSubmission\": 1246449960000\n },\n {\n \"id\": 21,\n \"name\": \"PRINT_OR_PRESENTATION\",\n \"numInquiries\": 41,\n \"challenges\": 24,\n \"wins\": 8,\n \"winPercent\": 0.2222222222222222,\n \"avgPlacement\": 3.5625,\n \"submissions\": 36,\n \"submissionRate\": 0.8780487804878049,\n \"passedScreening\": 36,\n \"screeningSuccessRate\": 1,\n \"mostRecentEventDate\": 1412790489000,\n \"mostRecentSubmission\": 1383722820000\n }\n ]\n },\n \"DATA_SCIENCE\": {\n \"challenges\": 41,\n \"wins\": 22,\n \"mostRecentSubmission\": 1333333964247,\n \"mostRecentEventDate\": 1308355200000,\n \"mostRecentEventName\": \"created data science\",\n \"SRM\": {\n \"challenges\": 40,\n \"wins\": 22,\n \"mostRecentSubmission\": 1333333964247,\n \"mostRecentEventDate\": 1308355200000,\n \"mostRecentEventName\": \"TCO11 Round 1 created\",\n \"rank\": {\n \"rating\": 1367,\n \"percentile\": 22,\n \"rank\": 22,\n \"countryRank\": 22,\n \"schoolRank\": 22,\n \"volatility\": 390,\n \"maximumRating\": 1459,\n \"minimumRating\": 1015,\n \"defaultLanguage\": \"Java\",\n \"competitions\": 38\n },\n \"challengeDetails\": [\n {\n \"challenges\": 33,\n \"levelName\": \"Level One\",\n \"failedChallenges\": 22\n },\n {\n \"challenges\": 55,\n \"levelName\": \"Level Two\",\n \"failedChallenges\": 11\n },\n {\n \"challenges\": 66,\n \"levelName\": \"Level Three\",\n \"failedChallenges\": 6\n }\n ],\n \"division1\": [\n {\n \"problemsSubmitted\": 88,\n \"problemsSysByTest\": 8,\n \"problemsFailed\": 8,\n \"levelName\": \"Level One\"\n },\n {\n \"problemsSubmitted\": 11,\n \"problemsSysByTest\": 22,\n \"problemsFailed\": 11,\n \"levelName\": \"Level Two\"\n },\n {\n \"problemsSubmitted\": 11,\n \"problemsSysByTest\": 11,\n \"problemsFailed\": 11,\n \"levelName\": \"Level Four\"\n }\n ],\n \"division2\": [\n {\n \"problemsSubmitted\": 22,\n \"problemsSysByTest\": 22,\n \"problemsFailed\": 22,\n \"levelName\": \"Level One\"\n },\n {\n \"problemsSubmitted\": 44,\n \"problemsSysByTest\": 44,\n \"problemsFailed\": 44,\n \"levelName\": \"Level Two\"\n }\n ]\n },\n \"MARATHON_MATCH\": {\n \"challenges\": 1,\n \"wins\": 22,\n \"mostRecentSubmission\": 1333333964247,\n \"mostRecentEventName\": \"created marathon match\",\n \"rank\": {\n \"rating\": 111,\n \"competitions\": 22,\n \"avgRank\": 111,\n \"avgNumSubmissions\": 22,\n \"bestRank\": 22,\n \"topFiveFinishes\": 22,\n \"topTenFinishes\": 22,\n \"rank\": 111,\n \"percentile\": 22,\n \"volatility\": 22,\n \"minimumRating\": 111,\n \"maximumRating\": 111,\n \"countryRank\": 22,\n \"schoolRank\": 22,\n \"defaultLanguage\": \"Java\"\n }\n }\n },\n \"COPILOT\": {\n \"contests\": 11,\n \"projects\": 11,\n \"failures\": 11,\n \"reposts\": 11,\n \"activeContests\": 11,\n \"activeProjects\": 11,\n \"fulfillment\": 111.11\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/v6/members/iamtong/stats", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "v6", + "members", + "iamtong", + "stats" + ] + } + }, + "response": [] + }, + { + "name": "Create Stats with empty data", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBTdXBwb3J0IiwiYWRtaW5pc3RyYXRvciIsInRlc3RSb2xlIiwiYWFhIiwidG9ueV90ZXN0XzEiLCJDb25uZWN0IE1hbmFnZXIiLCJDb25uZWN0IEFkbWluIiwiY29waWxvdCIsIkNvbm5lY3QgQ29waWxvdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJUb255SiIsImV4cCI6MTk4MDk5Mjc4OCwidXNlcklkIjoiODU0Nzg5OSIsImlhdCI6MTU0OTc5MTYxMSwiZW1haWwiOiJ0amVmdHMrZml4QHRvcGNvZGVyLmNvbSIsImp0aSI6ImY5NGQxZTI2LTNkMGUtNDZjYS04MTE1LTg3NTQ1NDRhMDhmMSJ9.q_Db9Gw8bn54xlythrZZUrJQyak-XrdOwPsj6ddgZ4M", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/v6/members/jiangliwu/stats", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "v6", + "members", + "jiangliwu", + "stats" + ] + } + }, + "response": [] + }, + { + "name": "Update Stats", "request": { "auth": { "type": "bearer", @@ -671,10 +1085,19 @@ } ] }, - "method": "GET", + "method": "PATCH", "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"groupId\": \"10\",\n \"maxRating\": {\n \"rating\": 1111,\n \"track\": \"DATA_SCIENCE_UPDATED\",\n \"subTrack\": \"SRM UPDATED\",\n \"ratingColor\": \"#111111\"\n },\n \"challenges\": 1111,\n \"wins\": 11,\n \"DEVELOP\": {\n \"challenges\": 2222,\n \"wins\": 22,\n \"mostRecentSubmission\": 1111615980000,\n \"mostRecentEventDate\": 1122630019000,\n \"subTracks\": [\n {\n \"id\": 112,\n \"challenges\": 222,\n \"wins\": 2,\n \"name\": \"DESIGN\",\n \"mostRecentSubmission\": 1133615980000,\n \"mostRecentEventDate\": 1144618321000,\n \"submissions\": {\n \"appealSuccessRate\": 0.55,\n \"maxScore\": 98.88,\n \"numInquiries\": 222,\n \"submissions\": 22\n },\n \"rank\": {\n \"overallPercentile\": 66.66,\n \"rating\": 666,\n \"minRating\": 333,\n \"maxRating\": 888\n }\n },\n {\n \"id\": 333,\n \"challenges\": 250,\n \"wins\": 25,\n \"name\": \"NEW_SUBTRACK\",\n \"mostRecentSubmission\": 1111111110000,\n \"submissions\": {\n \"minScore\": 50,\n \"reviewSuccessRate\": 0.777,\n \"maxScore\": 99,\n \"avgScore\": 77.77,\n \"passedScreening\": 77,\n \"passedReview\": 77,\n \"appeals\": 22\n },\n \"rank\": {\n \"activeRank\": 111,\n \"overallCountryRank\": 155,\n \"rating\": 999,\n \"volatility\": 333,\n \"overallRank\": 777\n }\n },\n {\n \"id\": 125\n },\n {\n \"id\": 666,\n \"name\": \"NEW_SUBTRACK3\"\n }\n ]\n },\n \"DESIGN\": {\n \"challenges\": 23,\n \"wins\": 10,\n \"mostRecentSubmission\": 1144615980000,\n \"mostRecentEventDate\": 1155630019000,\n \"subTracks\": [\n {\n \"id\": 22,\n \"name\": \"IDEA_GENERATION\",\n \"challenges\": 6,\n \"wins\": 6,\n \"mostRecentSubmission\": 1144615980000,\n \"mostRecentEventDate\": 1155630019000,\n \"numInquiries\": 6,\n \"winPercent\": 6,\n \"avgPlacement\": 6,\n \"submissions\": 6,\n \"submissionRate\": 6,\n \"passedScreening\": 6,\n \"screeningSuccessRate\": 6\n\n },\n {\n \"id\": 34,\n \"name\": \"STUDIO_OTHER\",\n \"numInquiries\": 1,\n \"winPercent\": 1,\n \"avgPlacement\": 1,\n \"submissions\": 1,\n \"submissionRate\": 1,\n \"passedScreening\": 1,\n \"screeningSuccessRate\": 1\n }\n ]\n },\n \"DATA_SCIENCE\": {\n \"wins\": 22,\n \"mostRecentSubmission\": 1333333964247,\n \"mostRecentEventName\": \"updated data science\",\n \"SRM\": {\n \"wins\": 22,\n \"mostRecentSubmission\": 1333333964247,\n \"mostRecentEventName\": \"TCO11 Round 1 updated\",\n \"rank\": {\n \"percentile\": 22,\n \"rank\": 22,\n \"countryRank\": 22,\n \"schoolRank\": 22\n },\n \"challengeDetails\": [\n {\n \"challenges\": 33,\n \"levelName\": \"Level One\",\n \"failedChallenges\": 22\n },\n {\n \"challenges\": 55,\n \"levelName\": \"Level Two\"\n },\n {\n \"challenges\": 66,\n \"levelName\": \"Level Four Updated\",\n \"failedChallenges\": 6\n }\n ],\n \"division1\": [\n {\n \"problemsSubmitted\": 88,\n \"problemsSysByTest\": 8,\n \"problemsFailed\": 8,\n \"levelName\": \"Level One\"\n },\n {\n \"problemsSysByTest\": 22,\n \"levelName\": \"Level Two\"\n },\n {\n \"problemsSubmitted\": 11,\n \"problemsSysByTest\": 11,\n \"problemsFailed\": 11,\n \"levelName\": \"Level Four\"\n }\n ],\n \"division2\": [\n {\n \"problemsSubmitted\": 22,\n \"problemsSysByTest\": 22,\n \"problemsFailed\": 22,\n \"levelName\": \"Level One\"\n },\n {\n \"problemsSubmitted\": 44,\n \"problemsSysByTest\": 44,\n \"problemsFailed\": 44,\n \"levelName\": \"Level Four updated\"\n }\n ]\n },\n \"MARATHON_MATCH\": {\n \"wins\": 22,\n \"mostRecentSubmission\": 1333333964247,\n \"mostRecentEventName\": \"updated marathon match\",\n \"rank\": {\n \"rating\": 111,\n \"avgRank\": 111,\n \"rank\": 111,\n \"minimumRating\": 111,\n \"maximumRating\": 111\n }\n }\n },\n \"COPILOT\": {\n \"contests\": 2222,\n \"projects\": 11,\n \"failures\": 111,\n \"reposts\": 111,\n \"activeContests\": 11,\n \"activeProjects\": 11,\n \"fulfillment\": 88.99\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "http://localhost:3000/v6/members/ACRush/stats?groupIds=20000000,20000001", + "raw": "http://localhost:3000/v6/members/phead/stats", "protocol": "http", "host": [ "localhost" @@ -683,14 +1106,172 @@ "path": [ "v6", "members", - "ACRush", + "phead", + "stats" + ] + } + }, + "response": [] + }, + { + "name": "Update Stats with empty array", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJwaGVhZCIsImV4cCI6MTk4MDk5Mjc4OCwidXNlcklkIjoiMjI3NDI3NjQiLCJpYXQiOjE1NDk3OTk1NjksImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiOWM0NTExYzUtYzE2NS00YTFiLTg5OWUtYjY1YWQwZTAyYjU1In0.kOPm331fsiXj2y_7by2ohjoaGWIGon0TxKL1znbpijU", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"challenges\": 1111,\n \"wins\": 11,\n \"DEVELOP\": {\n \"challenges\": 2222,\n \"wins\": 22,\n \"mostRecentSubmission\": 1111615980000,\n \"mostRecentEventDate\": 1122630019000,\n \"subTracks\": [\n ]\n },\n \"DESIGN\": {\n \"challenges\": 23,\n \"wins\": 10,\n \"mostRecentSubmission\": 1144615980000,\n \"mostRecentEventDate\": 1155630019000,\n \"subTracks\": [\n ]\n },\n \"DATA_SCIENCE\": {\n \"wins\": 22,\n \"mostRecentSubmission\": 1333333964247,\n \"mostRecentEventName\": \"updated data science\",\n \"SRM\": {\n \"wins\": 22,\n \"mostRecentSubmission\": 1333333964247,\n \"mostRecentEventName\": \"TCO11 Round 1 updated\",\n \"rank\": {\n \"percentile\": 22,\n \"rank\": 22,\n \"countryRank\": 22,\n \"schoolRank\": 22\n },\n \"challengeDetails\": [\n ],\n \"division2\": [\n ]\n },\n \"MARATHON_MATCH\": {\n \"wins\": 22,\n \"mostRecentSubmission\": 1333333964247,\n \"mostRecentEventName\": \"updated marathon match\",\n \"rank\": {\n \"rating\": 111,\n \"avgRank\": 111,\n \"rank\": 111,\n \"minimumRating\": 111,\n \"maximumRating\": 111\n }\n }\n },\n \"COPILOT\": {\n \"contests\": 2222,\n \"projects\": 11,\n \"failures\": 111,\n \"reposts\": 111,\n \"activeContests\": 11,\n \"activeProjects\": 11,\n \"fulfillment\": 88.99\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/v6/members/phead/stats", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "v6", + "members", + "phead", "stats" + ] + } + }, + "response": [] + }, + { + "name": "Update Stats with duplicate subTrack Id", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJwaGVhZCIsImV4cCI6MTk4MDk5Mjc4OCwidXNlcklkIjoiMjI3NDI3NjQiLCJpYXQiOjE1NDk3OTk1NjksImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiOWM0NTExYzUtYzE2NS00YTFiLTg5OWUtYjY1YWQwZTAyYjU1In0.kOPm331fsiXj2y_7by2ohjoaGWIGon0TxKL1znbpijU", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"groupId\": \"10\",\n \"maxRating\": {\n \"rating\": 1111,\n \"track\": \"DATA_SCIENCE_UPDATED\",\n \"subTrack\": \"SRM UPDATED\",\n \"ratingColor\": \"#111111\"\n },\n \"challenges\": 1111,\n \"wins\": 11,\n \"DEVELOP\": {\n \"challenges\": 2222,\n \"wins\": 22,\n \"mostRecentSubmission\": 1111615980000,\n \"mostRecentEventDate\": 1122630019000,\n \"subTracks\": [\n {\n \"id\": 112,\n \"challenges\": 222,\n \"wins\": 2,\n \"name\": \"DESIGN\",\n \"mostRecentSubmission\": 1133615980000,\n \"mostRecentEventDate\": 1144618321000,\n \"submissions\": {\n \"appealSuccessRate\": 0.55,\n \"maxScore\": 98.88,\n \"numInquiries\": 222,\n \"submissions\": 22\n },\n \"rank\": {\n \"overallPercentile\": 66.66,\n \"rating\": 666,\n \"minRating\": 333,\n \"maxRating\": 888\n }\n },\n {\n \"id\": 112,\n \"challenges\": 250,\n \"wins\": 25,\n \"name\": \"NEW_SUBTRACK\",\n \"mostRecentSubmission\": 1111111110000,\n \"submissions\": {\n \"minScore\": 50,\n \"reviewSuccessRate\": 0.777,\n \"maxScore\": 99,\n \"avgScore\": 77.77,\n \"passedScreening\": 77,\n \"passedReview\": 77,\n \"appeals\": 22\n },\n \"rank\": {\n \"activeRank\": 111,\n \"overallCountryRank\": 155,\n \"rating\": 999,\n \"volatility\": 333,\n \"overallRank\": 777\n }\n },\n {\n \"id\": 125\n },\n {\n \"id\": 666,\n \"name\": \"NEW_SUBTRACK3\"\n }\n ]\n },\n \"DESIGN\": {\n \"challenges\": 23,\n \"wins\": 10,\n \"mostRecentSubmission\": 1144615980000,\n \"mostRecentEventDate\": 1155630019000,\n \"subTracks\": [\n {\n \"id\": 22,\n \"name\": \"IDEA_GENERATION\",\n \"challenges\": 6,\n \"wins\": 6,\n \"mostRecentSubmission\": 1144615980000,\n \"mostRecentEventDate\": 1155630019000,\n \"numInquiries\": 6,\n \"winPercent\": 6,\n \"avgPlacement\": 6,\n \"submissions\": 6,\n \"submissionRate\": 6,\n \"passedScreening\": 6,\n \"screeningSuccessRate\": 6\n\n },\n {\n \"id\": 34,\n \"name\": \"STUDIO_OTHER\",\n \"numInquiries\": 1,\n \"winPercent\": 1,\n \"avgPlacement\": 1,\n \"submissions\": 1,\n \"submissionRate\": 1,\n \"passedScreening\": 1,\n \"screeningSuccessRate\": 1\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/v6/members/phead/stats", + "protocol": "http", + "host": [ + "localhost" ], - "query": [ + "port": "3000", + "path": [ + "v6", + "members", + "phead", + "stats" + ] + } + }, + "response": [] + }, + { + "name": "Update Stats with duplicate subTrack name", + "request": { + "auth": { + "type": "bearer", + "bearer": [ { - "key": "groupIds", - "value": "20000000,20000001" + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJwaGVhZCIsImV4cCI6MTk4MDk5Mjc4OCwidXNlcklkIjoiMjI3NDI3NjQiLCJpYXQiOjE1NDk3OTk1NjksImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiOWM0NTExYzUtYzE2NS00YTFiLTg5OWUtYjY1YWQwZTAyYjU1In0.kOPm331fsiXj2y_7by2ohjoaGWIGon0TxKL1znbpijU", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"groupId\": \"10\",\n \"challenges\": 1111,\n \"wins\": 11,\n \"DEVELOP\": {\n \"challenges\": 2222,\n \"wins\": 22,\n \"mostRecentSubmission\": 1111615980000,\n \"mostRecentEventDate\": 1122630019000,\n \"subTracks\": [\n {\n \"id\": 112,\n \"challenges\": 222,\n \"wins\": 2,\n \"name\": \"DESIGN\",\n \"mostRecentSubmission\": 1133615980000,\n \"mostRecentEventDate\": 1144618321000,\n \"submissions\": {\n \"appealSuccessRate\": 0.55,\n \"maxScore\": 98.88,\n \"numInquiries\": 222,\n \"submissions\": 22\n },\n \"rank\": {\n \"overallPercentile\": 66.66,\n \"rating\": 666,\n \"minRating\": 333,\n \"maxRating\": 888\n }\n },\n {\n \"id\": 333,\n \"challenges\": 250,\n \"wins\": 25,\n \"name\": \"NEW_SUBTRACK\",\n \"mostRecentSubmission\": 1111111110000,\n \"submissions\": {\n \"minScore\": 50,\n \"reviewSuccessRate\": 0.777,\n \"maxScore\": 99,\n \"avgScore\": 77.77,\n \"passedScreening\": 77,\n \"passedReview\": 77,\n \"appeals\": 22\n },\n \"rank\": {\n \"activeRank\": 111,\n \"overallCountryRank\": 155,\n \"rating\": 999,\n \"volatility\": 333,\n \"overallRank\": 777\n }\n },\n {\n \"id\": 125\n },\n {\n \"id\": 666,\n \"name\": \"NEW_SUBTRACK\"\n }\n ]\n },\n \"DESIGN\": {\n \"challenges\": 23,\n \"wins\": 10,\n \"mostRecentSubmission\": 1144615980000,\n \"mostRecentEventDate\": 1155630019000,\n \"subTracks\": [\n {\n \"id\": 22,\n \"name\": \"IDEA_GENERATION\",\n \"challenges\": 6,\n \"wins\": 6,\n \"mostRecentSubmission\": 1144615980000,\n \"mostRecentEventDate\": 1155630019000,\n \"numInquiries\": 6,\n \"winPercent\": 6,\n \"avgPlacement\": 6,\n \"submissions\": 6,\n \"submissionRate\": 6,\n \"passedScreening\": 6,\n \"screeningSuccessRate\": 6\n\n },\n {\n \"id\": 34,\n \"name\": \"STUDIO_OTHER\",\n \"numInquiries\": 1,\n \"winPercent\": 1,\n \"avgPlacement\": 1,\n \"submissions\": 1,\n \"submissionRate\": 1,\n \"passedScreening\": 1,\n \"screeningSuccessRate\": 1\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/v6/members/phead/stats", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "v6", + "members", + "phead", + "stats" + ] + } + }, + "response": [] + }, + { + "name": "Update Stats with duplicate level name", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJwaGVhZCIsImV4cCI6MTk4MDk5Mjc4OCwidXNlcklkIjoiMjI3NDI3NjQiLCJpYXQiOjE1NDk3OTk1NjksImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiOWM0NTExYzUtYzE2NS00YTFiLTg5OWUtYjY1YWQwZTAyYjU1In0.kOPm331fsiXj2y_7by2ohjoaGWIGon0TxKL1znbpijU", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"groupId\": \"10\",\n \"challenges\": 1111,\n \"wins\": 11,\n \"DATA_SCIENCE\": {\n \"wins\": 22,\n \"mostRecentSubmission\": 1333333964247,\n \"mostRecentEventName\": \"updated data science\",\n \"SRM\": {\n \"wins\": 22,\n \"mostRecentSubmission\": 1333333964247,\n \"mostRecentEventName\": \"TCO11 Round 1 updated\",\n \"challengeDetails\": [\n {\n \"challenges\": 33,\n \"levelName\": \"Level One\",\n \"failedChallenges\": 22\n },\n {\n \"challenges\": 55,\n \"levelName\": \"Level Two\"\n },\n {\n \"challenges\": 66,\n \"levelName\": \"Level Two\",\n \"failedChallenges\": 6\n }\n ],\n \"division1\": [\n {\n \"problemsSubmitted\": 88,\n \"problemsSysByTest\": 8,\n \"problemsFailed\": 8,\n \"levelName\": \"Level One\"\n },\n {\n \"problemsSysByTest\": 22,\n \"levelName\": \"Level Two\"\n },\n {\n \"problemsSubmitted\": 11,\n \"problemsSysByTest\": 11,\n \"problemsFailed\": 11,\n \"levelName\": \"Level Four\"\n }\n ],\n \"division2\": [\n {\n \"problemsSubmitted\": 22,\n \"problemsSysByTest\": 22,\n \"problemsFailed\": 22,\n \"levelName\": \"Level One\"\n },\n {\n \"problemsSubmitted\": 44,\n \"problemsSysByTest\": 44,\n \"problemsFailed\": 44,\n \"levelName\": \"Level Four updated\"\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" } + } + }, + "url": { + "raw": "http://localhost:3000/v6/members/phead/stats", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "v6", + "members", + "phead", + "stats" ] } }, diff --git a/prisma/migrations/20250719113542_init2/migration.sql b/prisma/migrations/20250719113542_init2/migration.sql new file mode 100644 index 0000000..5ecd11f --- /dev/null +++ b/prisma/migrations/20250719113542_init2/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the column `subTrackId` on the `memberDataScienceHistoryStats` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "memberDataScienceHistoryStats" DROP COLUMN "subTrackId"; + +-- AlterTable +ALTER TABLE "memberMaxRating" ALTER COLUMN "track" DROP NOT NULL, +ALTER COLUMN "subTrack" DROP NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 54d4e70..215284f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -72,7 +72,7 @@ model member { model memberAddress { id BigInt @id @default(autoincrement()) - userId BigInt + userId BigInt streetAddr1 String? streetAddr2 String? city String? @@ -94,8 +94,8 @@ model memberMaxRating { id BigInt @id @default(autoincrement()) userId BigInt rating Int - track String - subTrack String + track String? + subTrack String? ratingColor String createdAt DateTime @default(now()) @@ -156,7 +156,7 @@ model distributionStats { ratingRange3700To3799 Int ratingRange3800To3899 Int ratingRange3900To3999 Int - + createdAt DateTime @default(now()) createdBy String @@ -216,7 +216,7 @@ model memberDevelopHistoryStats { createdBy String updatedAt DateTime? @updatedAt updatedBy String? - + @@index([historyStatsId]) } @@ -231,7 +231,6 @@ model memberDataScienceHistoryStats { placement Int percentile Float subTrack String - subTrackId Int historyStats memberHistoryStats @relation(fields: [historyStatsId], references: [id], onDelete: Cascade) @@ -239,17 +238,17 @@ model memberDataScienceHistoryStats { createdBy String updatedAt DateTime? @updatedAt updatedBy String? - + @@index([historyStatsId]) } model memberStats { id BigInt @id @default(autoincrement()) userId BigInt - + memberRatingId BigInt? maxRating memberMaxRating? @relation(fields: [memberRatingId], references: [id], onDelete: NoAction) - + challenges Int? wins Int? develop memberDevelopStats? @@ -367,7 +366,7 @@ model memberDevelopStatsItem { model memberDesignStats { id BigInt @id @default(autoincrement()) - memberStatsId BigInt + memberStatsId BigInt challenges BigInt? wins BigInt? mostRecentSubmission DateTime? @@ -441,7 +440,7 @@ model memberDataScienceStats { model memberSrmStats { id BigInt @id @default(autoincrement()) - dataScienceStatsId BigInt + dataScienceStatsId BigInt challenges BigInt? wins BigInt? @@ -591,7 +590,7 @@ enum DeviceType { model memberTraitDevice { id BigInt @id @default(autoincrement()) - memberTraitId BigInt + memberTraitId BigInt deviceType DeviceType manufacturer String @@ -619,7 +618,7 @@ enum SoftwareType { model memberTraitSoftware { id BigInt @id @default(autoincrement()) - memberTraitId BigInt + memberTraitId BigInt softwareType SoftwareType name String @@ -645,7 +644,7 @@ enum ServiceProviderType { model memberTraitServiceProvider { id BigInt @id @default(autoincrement()) - memberTraitId BigInt + memberTraitId BigInt type ServiceProviderType name String @@ -676,7 +675,7 @@ enum WorkIndustryType { model memberTraitWork { id BigInt @id @default(autoincrement()) - memberTraitId BigInt + memberTraitId BigInt industry WorkIndustryType? companyName String @@ -698,7 +697,7 @@ model memberTraitWork { model memberTraitEducation { id BigInt @id @default(autoincrement()) - memberTraitId BigInt + memberTraitId BigInt collegeName String degree String @@ -717,7 +716,7 @@ model memberTraitEducation { model memberTraitBasicInfo { id BigInt @id @default(autoincrement()) - memberTraitId BigInt + memberTraitId BigInt userId BigInt country String @@ -743,7 +742,7 @@ model memberTraitBasicInfo { model memberTraitLanguage { id BigInt @id @default(autoincrement()) - memberTraitId BigInt + memberTraitId BigInt language String spokenLevel String? @@ -763,7 +762,7 @@ model memberTraitLanguage { model memberTraitOnboardChecklist { id BigInt @id @default(autoincrement()) - memberTraitId BigInt + memberTraitId BigInt listItemType String // Like 'profile_completed' date DateTime @@ -784,7 +783,7 @@ model memberTraitOnboardChecklist { model memberTraitPersonalization { id BigInt @id @default(autoincrement()) - memberTraitId BigInt + memberTraitId BigInt key String? value Json? @@ -802,7 +801,7 @@ model memberTraitPersonalization { model memberTraitCommunity { id BigInt @id @default(autoincrement()) - memberTraitId BigInt + memberTraitId BigInt communityName String status Boolean diff --git a/src/common/prismaHelper.js b/src/common/prismaHelper.js index 9429508..c54a707 100644 --- a/src/common/prismaHelper.js +++ b/src/common/prismaHelper.js @@ -1,5 +1,6 @@ const _ = require('lodash') const helper = require('./helper') +const errors = require('./errors') const designBasicFields = [ 'name', 'numInquiries', 'submissions', 'passedScreening', 'avgPlacement', @@ -331,6 +332,427 @@ const skillsIncludeParams = { displayMode: true } +/** + * Convert number to date + * @param {Number} dateNum date number + * @returns date instance or undefined + */ +function convertDate (dateNum) { + return dateNum ? new Date(dateNum) : undefined +} + +/** + * Update or Create item. + * @param {Array} updateItems items to be updated + * @param {Array} existingItems existing items in db + * @param {Object} txModel the tx model + * @param {Object} parentId the parent Id object + * @param {String} operatorId the operator Id + * @returns new created item data or undefined + */ +async function updateOrCreateModel (itemData, existingData, txModel, parentId, operatorId) { + if (existingData) { + await txModel.update({ + where: { + id: existingData.id + }, + data: { + ...itemData, + updatedBy: operatorId + } + }) + } else { + const newItemData = await txModel.create({ + data: { + ...itemData, + ...parentId, + createdBy: operatorId + } + }) + return newItemData + } +} + +/** + * Validate subTrack items data + * @param {Array} updateItems the subTrack data to update + * @param {Array} existingItems the existing subTrack data + * @param {String} modelName the model name + * @returns subTrack items data to create + */ +function validateSubTrackData (updateItems, existingItems, modelName) { + const itemIds = [] + const itemNames = [] + const toCreateItems = [] + + updateItems.forEach(item => { + if (_.find(itemIds, id => id === item.id)) { + throw new errors.BadRequestError(`${modelName} items contains duplicate id: '${item.id}'`) + } + if (item.name && _.find(itemNames, name => name === item.name)) { + throw new errors.BadRequestError(`${modelName} items contains duplicate name: '${item.name}'`) + } + itemIds.push(item.id) + if (item.name) { + itemNames.push(item.name) + } + const foundItem = existingItems.find(eItem => eItem.subTrackId === item.id) + const nameItem = existingItems.find(eItem => { + if (eItem.subTrack) { + return eItem.subTrackId !== item.id && eItem.subTrack === item.name + } + return eItem.subTrackId !== item.id && eItem.name === item.name + }) + + if (foundItem && (item.name && (foundItem.subTrack ? item.name !== foundItem.subTrack : item.name !== foundItem.name))) { + throw new errors.BadRequestError(`${modelName} item with name '${item.name}' is not same as the DB one with same id`) + } + if (nameItem) { + throw new errors.BadRequestError(`${modelName} item has duplicated name '${item.name}' in DB`) + } + if (!foundItem && !(item.id && item.name)) { + throw new errors.BadRequestError(`${modelName} new item must have id and name both`) + } + if (!foundItem) { + toCreateItems.push(item) + } + }) + + return toCreateItems +} + +/** + * Validate level items data + * @param {Array} updateItems the level data to update + * @param {Array} existingItems the level subTrack data + * @param {String} modelName the model name + * @param {String} itemName the item name + * @param {Object} schema the joi schema + * @returns level items data to create + */ +function validateLevelItemsData (updateItems, existingItems, modelName, itemName, schema) { + const itemLevelNames = [] + const toCreateItems = [] + updateItems.forEach(item => { + if (_.find(itemLevelNames, ln => ln === item.levelName)) { + throw new errors.BadRequestError(`${modelName} ${itemName} items contains duplicate level name: '${item.levelName}'`) + } + itemLevelNames.push(item.levelName) + + const foundItem = existingItems.find(eItem => { + if (itemName === 'challengeDetail') { + return eItem.levelName === item.levelName + } else { + return eItem.levelName === item.levelName && eItem.divisionName === itemName + } + }) + if (!foundItem) { + toCreateItems.push(item) + } + }) + + if (toCreateItems.length > 0) { + const validateRes = schema.validate(toCreateItems) + + if (validateRes.error) { + throw new errors.BadRequestError(validateRes.error.error) + } + } +} + +/** + * Validate history items data + * @param {Array} updateItems the history data to update + * @param {Array} existingItems the existing history data + * @param {String} modelName the model name + * @returns history items data to create + */ +function validateHistoryData (updateItems, existingItems, modelName) { + const itemIds = [] + const toCreateItems = [] + updateItems.forEach(item => { + if (_.find(itemIds, id => id === item.challengeId)) { + throw new errors.BadRequestError(`${modelName} items contains duplicate id: '${item.challengeId}'`) + } + itemIds.push(item.challengeId) + + const foundItem = existingItems.find(eItem => helper.bigIntToNumber(eItem.challengeId) === item.challengeId && eItem.subTrack === item.subTrack) + + if (!foundItem) { + toCreateItems.push(item) + } + }) + + return toCreateItems +} + +/** + * Update array items. + * @param {Array} updateItems items to be updated + * @param {Array} existingItems existing items in db + * @param {Object} txModel the tx model + * @param {Object} parentId the parent Id object + * @param {String} operatorId the operator Id + */ +async function updateArrayItems (updateItems, existingItems, txModel, parentId, operatorId) { + const toUpdate = [] + const toCreate = [] + if (updateItems.length === 0) { + return + } + + updateItems.forEach(item => { + const foundItem = existingItems.find(eItem => eItem.subTrackId === item.subTrackId) + if (foundItem) { + item.id = foundItem.id + toUpdate.push(item) + } else { + toCreate.push(item) + } + }) + const toDeleteIds = [] + existingItems.forEach(item => { + const found = toUpdate.find(item2 => item2.id === item.id) + if (!found) { + toDeleteIds.push(item.id) + } + }) + + for (let i = 0; i < toUpdate.length; i++) { + const elem = toUpdate[i] + await txModel.update({ + where: { + id: elem.id + }, + data: { + ..._.omit(elem, ['id', 'subTrackId', 'name']), + updatedBy: operatorId + } + }) + } + + await txModel.createMany({ + data: toCreate.map(item => ({ + ...item, + ...parentId, + createdBy: operatorId + })) + }) + + await txModel.deleteMany({ + where: { + id: { + in: toDeleteIds + } + } + }) +} + +/** + * Update array level items. + * @param {Array} updateItems items to be updated + * @param {Array} existingItems existing items in db + * @param {Object} txModel the tx model + * @param {Object} parentId the parent Id object + * @param {String} operatorId the operator Id + */ +async function updateArrayLevelItems (updateItems, existingItems, txModel, parentId, operatorId) { + const toUpdate = [] + const toCreate = [] + if (updateItems.length === 0) { + return + } + + updateItems.forEach(item => { + const foundItem = existingItems.find(eItem => eItem.levelName === item.levelName) + if (foundItem) { + item.id = foundItem.id + toUpdate.push(item) + } else { + toCreate.push(item) + } + }) + const toDeleteIds = [] + existingItems.forEach(item => { + const found = toUpdate.find(item2 => item2.id === item.id) + if (!found) { + toDeleteIds.push(item.id) + } + }) + + for (let i = 0; i < toUpdate.length; i++) { + const elem = toUpdate[i] + await txModel.update({ + where: { + id: elem.id + }, + data: { + ..._.omit(elem, ['id']), + updatedBy: operatorId + } + }) + } + + await txModel.createMany({ + data: toCreate.map(item => ({ + ...item, + ...parentId, + createdBy: operatorId + })) + }) + + await txModel.deleteMany({ + where: { + id: { + in: toDeleteIds + } + } + }) +} + +/** + * Update array division items. + * @param {Array} updateD1Items division1 items to be updated + * @param {Array} updateD2Items division2 items to be updated + * @param {Array} existingItems existing items in db + * @param {Object} txModel the tx model + * @param {Object} parentId the parent Id object + * @param {String} operatorId the operator Id + */ +async function updateArrayDivisionItems (updateD1Items, updateD2Items, existingItems, txModel, parentId, operatorId) { + const toUpdate = [] + const toCreate = [] + if ((!updateD1Items || updateD1Items.length === 0) && (!updateD2Items || updateD2Items.length === 0)) { + return + } + + if (updateD1Items) { + updateD1Items.forEach(item => { + const foundItem = existingItems.find(eItem => eItem.levelName === item.levelName && eItem.divisionName === 'division1') + if (foundItem) { + item.id = foundItem.id + toUpdate.push(item) + } else { + item.divisionName = 'division1' + toCreate.push(item) + } + }) + } + + if (updateD2Items) { + updateD2Items.forEach(item => { + const foundItem = existingItems.find(eItem => eItem.levelName === item.levelName && eItem.divisionName === 'division2') + if (foundItem) { + item.id = foundItem.id + toUpdate.push(item) + } else { + item.divisionName = 'division2' + toCreate.push(item) + } + }) + } + + const toDeleteIds = [] + existingItems.forEach(item => { + const found = toUpdate.find(item2 => item2.id === item.id) + if (!found) { + toDeleteIds.push(item.id) + } + }) + + for (let i = 0; i < toUpdate.length; i++) { + const elem = toUpdate[i] + await txModel.update({ + where: { + id: elem.id + }, + data: { + ..._.omit(elem, ['id']), + updatedBy: operatorId + } + }) + } + + await txModel.createMany({ + data: toCreate.map(item => ({ + ...item, + ...parentId, + createdBy: operatorId + })) + }) + + await txModel.deleteMany({ + where: { + id: { + in: toDeleteIds + } + } + }) +} + +/** + * Update history items. + * @param {Array} updateItems items to be updated + * @param {Array} existingItems existing items in db + * @param {Object} txModel the tx model + * @param {Object} parentId the parent Id object + * @param {String} operatorId the operator Id + */ +async function updateHistoryItems (updateItems, existingItems, txModel, parentId, operatorId) { + const toUpdate = [] + const toCreate = [] + + if (updateItems.length === 0) { + return + } + + updateItems.forEach(item => { + const foundItem = existingItems.find(eItem => eItem.subTrack === item.subTrack && helper.bigIntToNumber(eItem.challengeId) === item.challengeId) + if (foundItem) { + item.id = foundItem.id + toUpdate.push(item) + } else { + toCreate.push(item) + } + }) + const toDeleteIds = [] + existingItems.forEach(item => { + const found = toUpdate.find(item2 => item2.id === item.id) + if (!found) { + toDeleteIds.push(item.id) + } + }) + + for (let i = 0; i < toUpdate.length; i++) { + const elem = toUpdate[i] + await txModel.update({ + where: { + id: elem.id + }, + data: { + ..._.omit(elem, ['id', 'subTrackId', 'subTrack', 'challengeId']), + updatedBy: operatorId + } + }) + } + + await txModel.createMany({ + data: toCreate.map(item => ({ + ...item, + ...parentId, + createdBy: operatorId + })) + }) + + await txModel.deleteMany({ + where: { + id: { + in: toDeleteIds + } + } + }) +} + module.exports = { convertMember, buildMemberSkills, @@ -338,5 +760,14 @@ module.exports = { buildSearchMemberFilter, buildStatsHistoryResponse, statsIncludeParams, - skillsIncludeParams + skillsIncludeParams, + convertDate, + updateOrCreateModel, + validateSubTrackData, + validateLevelItemsData, + validateHistoryData, + updateArrayItems, + updateArrayLevelItems, + updateArrayDivisionItems, + updateHistoryItems } diff --git a/src/controllers/StatisticsController.js b/src/controllers/StatisticsController.js index 420baba..19cc73d 100644 --- a/src/controllers/StatisticsController.js +++ b/src/controllers/StatisticsController.js @@ -23,6 +23,26 @@ async function getHistoryStats (req, res) { res.send(result) } +/** + * Create member history statistics + * @param {Object} req the request + * @param {Object} res the response + */ +async function createHistoryStats (req, res) { + const result = await service.createHistoryStats(req.authUser, req.params.handle, req.body) + res.send(result) +} + +/** + * Partially update history stats + * @param {Object} req the request + * @param {Object} res the response + */ +async function partiallyUpdateHistoryStats (req, res) { + const result = await service.partiallyUpdateHistoryStats(req.authUser, req.params.handle, req.body) + res.send(result) +} + /** * Get member statistics * @param {Object} req the request @@ -33,6 +53,26 @@ async function getMemberStats (req, res) { res.send(result) } +/** + * Create member stats + * @param {Object} req the request + * @param {Object} res the response + */ +async function createMemberStats (req, res) { + const result = await service.createMemberStats(req.authUser, req.params.handle, req.body) + res.send(result) +} + +/** + * Partially update member stats + * @param {Object} req the request + * @param {Object} res the response + */ +async function partiallyUpdateMemberStats (req, res) { + const result = await service.partiallyUpdateMemberStats(req.authUser, req.params.handle, req.body) + res.send(result) +} + /** * Get member skills * @param {Object} req the request @@ -66,7 +106,11 @@ async function partiallyUpdateMemberSkills (req, res) { module.exports = { getDistribution, getHistoryStats, + createHistoryStats, + partiallyUpdateHistoryStats, getMemberStats, + createMemberStats, + partiallyUpdateMemberStats, getMemberSkills, createMemberSkills, partiallyUpdateMemberSkills diff --git a/src/routes.js b/src/routes.js index 88c6c1b..77c6b43 100644 --- a/src/routes.js +++ b/src/routes.js @@ -125,6 +125,18 @@ module.exports = { auth: 'jwt', allowNoToken: true, scopes: [MEMBERS.READ, MEMBERS.ALL] + }, + post: { + controller: 'StatisticsController', + method: 'createHistoryStats', + auth: 'jwt', + scopes: [MEMBERS.UPDATE, MEMBERS.ALL] + }, + patch: { + controller: 'StatisticsController', + method: 'partiallyUpdateHistoryStats', + auth: 'jwt', + scopes: [MEMBERS.UPDATE, MEMBERS.ALL] } }, '/members/:handle/stats': { @@ -134,6 +146,18 @@ module.exports = { auth: 'jwt', allowNoToken: true, scopes: [MEMBERS.READ, MEMBERS.ALL] + }, + post: { + controller: 'StatisticsController', + method: 'createMemberStats', + auth: 'jwt', + scopes: [MEMBERS.UPDATE, MEMBERS.ALL] + }, + patch: { + controller: 'StatisticsController', + method: 'partiallyUpdateMemberStats', + auth: 'jwt', + scopes: [MEMBERS.UPDATE, MEMBERS.ALL] } }, '/members/:handle/skills': { diff --git a/src/scripts/seed-data.js b/src/scripts/seed-data.js index c7b9530..6441072 100644 --- a/src/scripts/seed-data.js +++ b/src/scripts/seed-data.js @@ -328,7 +328,9 @@ async function importMember (handle) { } }) - await createStats(memberData, member.maxRating.id) + if (handle !== 'iamtong' && handle !== 'jiangliwu') { + await createStats(memberData, member.maxRating.id) + } await createSkills(memberData) console.log(`Import member data complete for ${handle}`) } @@ -357,6 +359,9 @@ async function importDistributions () { async function importStatsHistory () { for (let handle of handleList) { + if (handle === 'iamtong' || handle === 'jiangliwu') { + continue + } console.log(`Import stats history for member ${handle}`) const filename = path.join(statsHistoryDir, `${handle}.json`) const rawData = fs.readFileSync(filename, 'utf8') @@ -402,7 +407,6 @@ async function importStatsHistory () { _.forEach(srmHistory, t => { dataScienceItems.push({ subTrack: 'SRM', - subTrackId: 0, createdBy, ..._.pick(t, ['challengeId', 'challengeName', 'rating', 'placement', 'percentile']), date: new Date(t.date) @@ -413,7 +417,6 @@ async function importStatsHistory () { _.forEach(marathonHistory, t => { dataScienceItems.push({ subTrack: 'MARATHON_MATCH', - subTrackId: 0, createdBy, ..._.pick(t, ['challengeId', 'challengeName', 'rating', 'placement', 'percentile']), date: new Date(t.date) @@ -468,7 +471,6 @@ async function mockPrivateStatsHistory () { placement: 1, percentile: 100, subTrack: 'SRM', - subTrackId: 0, createdBy }, { challengeId: 99997, @@ -478,7 +480,6 @@ async function mockPrivateStatsHistory () { placement: 1, percentile: 100, subTrack: 'MARATHON_MATCH', - subTrackId: 0, createdBy }] } diff --git a/src/services/MemberService.js b/src/services/MemberService.js index 2bcd4e8..4d8c749 100644 --- a/src/services/MemberService.js +++ b/src/services/MemberService.js @@ -469,7 +469,7 @@ async function verifyEmail (currentUser, handle, query) { // update member in db const result = await prisma.member.update({ where: { userId: member.userId }, - data: member + data: _.omit(member, ['maxRating']) }) prismaHelper.convertMember(result) await helper.postBusEvent(constants.TOPICS.MemberUpdated, result) diff --git a/src/services/StatisticsService.js b/src/services/StatisticsService.js index 4bd8a8c..06f9072 100644 --- a/src/services/StatisticsService.js +++ b/src/services/StatisticsService.js @@ -8,10 +8,8 @@ const config = require('config') const helper = require('../common/helper') const logger = require('../common/logger') const errors = require('../common/errors') -const constants = require('../../app-constants') const prisma = require('../common/prisma').getClient() const prismaHelper = require('../common/prismaHelper') -const string = require('joi/lib/types/string') const { v4: uuidv4 } = require('uuid') const DISTRIBUTION_FIELDS = ['track', 'subTrack', 'distribution', 'createdAt', 'updatedAt', @@ -24,9 +22,6 @@ const MEMBER_STATS_FIELDS = ['userId', 'groupId', 'handle', 'handleLower', 'maxR 'challenges', 'wins', 'DEVELOP', 'DESIGN', 'DATA_SCIENCE', 'COPILOT', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'] -const MEMBER_SKILL_FIELDS = ['userId', 'handle', 'handleLower', 'skills', - 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'] - /** * Get distribution statistics. * @param {Object} query the query parameters @@ -160,6 +155,398 @@ getHistoryStats.schema = { }) } +/** + * Create history stats. + * @param {Object} currentUser the user who performs operation + * @param {String} handle the member handle + * @param {Object} data the history stats data to create + * @returns {Object} the created history stats + */ +async function createHistoryStats (currentUser, handle, data) { + // get member by handle + const member = await helper.getMemberByHandle(handle) + // check authorization + if (!helper.canManageMember(currentUser, member)) { + throw new errors.ForbiddenError('You are not allowed to update the member stats.') + } + + const groupIdsArr = [] + if (data.groupId) { + groupIdsArr.push(data.groupId) + } + + const groupIds = await helper.getAllowedGroupIds(currentUser, member, groupIdsArr) + + let existingStat + if (groupIds[0] === config.PUBLIC_GROUP_ID) { + data.isPrivate = false + // get statistics by member user id from db + existingStat = await prisma.memberHistoryStats.findFirst({ + where: { userId: member.userId, isPrivate: false }, + include: { develop: true, dataScience: true } + }) + if (!_.isNil(existingStat)) { + existingStat = _.assign(existingStat, { groupId: _.toNumber(groupIds[0]) }) + } + } else { + data.isPrivate = true + // get statistics private by member user id from db + existingStat = await prisma.memberHistoryStats.findFirst({ + where: { userId: member.userId, groupId: groupIds[0], isPrivate: true }, + include: { develop: true, dataScience: true } + }) + } + + if (existingStat) { + throw new errors.BadRequestError('History stats already exists') + } + + const operatorId = currentUser.userId || currentUser.sub + + if (data.DEVELOP && data.DEVELOP.subTracks && data.DEVELOP.subTracks.length > 0) { + prismaHelper.validateSubTrackData(data.DEVELOP.subTracks, [], 'Member develop history subTrack') + + data.develop = [] + data.DEVELOP.subTracks.forEach(item => { + if (item.history && item.history.length > 0) { + const historyItems = item.history.map(item2 => ({ + ...item2, + ratingDate: prismaHelper.convertDate(item2.ratingDate), + subTrackId: item.id, + subTrack: item.name, + createdBy: operatorId + })) + prismaHelper.validateHistoryData(historyItems, [], 'Member develop history stats') + + data.develop = data.develop.concat(historyItems) + } + }) + } + + if (data.DATA_SCIENCE) { + data.dataScience = [] + if (data.DATA_SCIENCE.SRM && data.DATA_SCIENCE.SRM.history && data.DATA_SCIENCE.SRM.history.length > 0) { + const historyItems = data.DATA_SCIENCE.SRM.history.map(item => ({ + ...item, + date: prismaHelper.convertDate(item.date), + subTrack: 'SRM', + createdBy: operatorId + })) + prismaHelper.validateHistoryData(historyItems, [], 'Member dataScience history srm stats') + + data.dataScience = historyItems + } + if (data.DATA_SCIENCE.MARATHON_MATCH && data.DATA_SCIENCE.MARATHON_MATCH.history && data.DATA_SCIENCE.MARATHON_MATCH.history.length > 0) { + const historyItems = data.DATA_SCIENCE.MARATHON_MATCH.history.map(item => ({ + ...item, + date: prismaHelper.convertDate(item.date), + subTrack: 'MARATHON_MATCH', + createdBy: operatorId + })) + prismaHelper.validateHistoryData(historyItems, [], 'Member dataScience history marathon stats') + + data.dataScience = data.dataScience.concat(historyItems) + } + } + + // create model memberHistoryStats + const statsRes = await prisma.memberHistoryStats.create({ + data: { + isPrivate: data.isPrivate, + createdBy: operatorId, + userId: member.userId, + develop: { + create: data.develop + }, + dataScience: { + create: data.dataScience + } + }, + include: { develop: true, dataScience: true } + }) + + if (!data.isPrivate) { + statsRes.groupId = _.toNumber(groupIds[0]) + } + + // build stats history response + let result = prismaHelper.buildStatsHistoryResponse(member, statsRes, HISTORY_STATS_FIELDS) + // remove identifiable info fields if user is not admin, not M2M and not member himself + if (!helper.canManageMember(currentUser, member)) { + result = _.omit(result, config.STATISTICS_SECURE_FIELDS) + } + return result +} + +createHistoryStats.schema = { + currentUser: Joi.any(), + handle: Joi.string().required(), + data: Joi.object().keys({ + groupId: Joi.string(), + DEVELOP: Joi.object().keys({ + subTracks: Joi.array().items(Joi.object().keys({ + id: Joi.positive().required(), + name: Joi.string().required(), + history: Joi.array().items(Joi.object().keys({ + challengeId: Joi.positive().required(), + challengeName: Joi.string().required(), + ratingDate: Joi.positive().required(), + newRating: Joi.positive().required() + })) + })) + }), + DATA_SCIENCE: Joi.object().keys({ + SRM: Joi.object().keys({ + history: Joi.array().items(Joi.object().keys({ + challengeId: Joi.positive().required(), + challengeName: Joi.string().required(), + date: Joi.positive().required(), + rating: Joi.positive().required(), + placement: Joi.positive().required(), + percentile: Joi.number().required() + })) + }), + MARATHON_MATCH: Joi.object().keys({ + history: Joi.array().items(Joi.object().keys({ + challengeId: Joi.positive().required(), + challengeName: Joi.string().required(), + date: Joi.positive().required(), + rating: Joi.positive().required(), + placement: Joi.positive().required(), + percentile: Joi.number().required() + })) + }) + }) + }).required() +} + +/** + * Partially update history stats. + * @param {Object} currentUser the user who performs operation + * @param {String} handle the member handle + * @param {Object} data the history stats data to update + * @returns {Object} the updated history stats + */ +async function partiallyUpdateHistoryStats (currentUser, handle, data) { + // get member by handle + const member = await helper.getMemberByHandle(handle) + // check authorization + if (!helper.canManageMember(currentUser, member)) { + throw new errors.ForbiddenError('You are not allowed to update the member stats.') + } + + const groupIdsArr = [] + if (data.groupId) { + groupIdsArr.push(data.groupId) + } + + const groupIds = await helper.getAllowedGroupIds(currentUser, member, groupIdsArr) + + let existingStat + if (groupIds[0] === config.PUBLIC_GROUP_ID) { + // get statistics by member user id from db + existingStat = await prisma.memberHistoryStats.findFirst({ + where: { userId: member.userId, isPrivate: false }, + include: { develop: true, dataScience: true } + }) + if (!_.isNil(existingStat)) { + existingStat = _.assign(existingStat, { groupId: _.toNumber(groupIds[0]) }) + } + } else { + // get statistics private by member user id from db + existingStat = await prisma.memberHistoryStats.findFirst({ + where: { userId: member.userId, groupId: groupIds[0], isPrivate: true }, + include: { develop: true, dataScience: true } + }) + } + + if (!existingStat || !existingStat.id) { + throw new errors.NotFoundError('History stats not found') + } + + if (data.DEVELOP && data.DEVELOP.subTracks && data.DEVELOP.subTracks.length > 0) { + prismaHelper.validateSubTrackData(data.DEVELOP.subTracks, existingStat.develop || [], 'Member develop history subTrack') + + data.DEVELOP.subTracks.forEach(item => { + if (item.history && item.history.length > 0) { + const historyItems = item.history.map(item2 => ({ + ...item2, + subTrackId: item.id, + subTrack: item.name + })) + const toCreateItems = prismaHelper.validateHistoryData(historyItems, existingStat.develop || [], 'Member develop history stats') + + if (toCreateItems.length > 0) { + const validateRes = DevelopHistoryStatsSchema.validate(toCreateItems) + + if (validateRes.error) { + throw new errors.BadRequestError(validateRes.error.error) + } + } + } + }) + } + + if (data.DATA_SCIENCE) { + if (data.DATA_SCIENCE.SRM && data.DATA_SCIENCE.SRM.history && data.DATA_SCIENCE.SRM.history.length > 0) { + const historyItems = data.DATA_SCIENCE.SRM.history.map(item => ({ + ...item, + subTrack: 'SRM' + })) + const toCreateItems = prismaHelper.validateHistoryData(historyItems, existingStat.dataScience || [], 'Member dataScience history srm stats') + + if (toCreateItems.length > 0) { + const validateRes = DataScienceHistoryStatsSchema.validate(toCreateItems) + + if (validateRes.error) { + throw new errors.BadRequestError(validateRes.error.error) + } + } + } + if (data.DATA_SCIENCE.MARATHON_MATCH && data.DATA_SCIENCE.MARATHON_MATCH.history && data.DATA_SCIENCE.MARATHON_MATCH.history.length > 0) { + const historyItems = data.DATA_SCIENCE.MARATHON_MATCH.history.map(item => ({ + ...item, + subTrack: 'MARATHON_MATCH' + })) + const toCreateItems = prismaHelper.validateHistoryData(historyItems, existingStat.dataScience || [], 'Member dataScience history marathon stats') + + if (toCreateItems.length > 0) { + const validateRes = DataScienceHistoryStatsSchema.validate(toCreateItems) + + if (validateRes.error) { + throw new errors.BadRequestError(validateRes.error.error) + } + } + } + } + + const operatorId = currentUser.userId || currentUser.sub + const historyStatsId = existingStat.id + + // open a transaction to handle update + let result = await prisma.$transaction(async (tx) => { + // update DEVELOP subTracks history + if (data.DEVELOP && data.DEVELOP.subTracks && data.DEVELOP.subTracks.length > 0) { + let developHistory = [] + data.DEVELOP.subTracks.forEach(item => { + const baseItem = { + subTrackId: item.id, + subTrack: item.name + } + developHistory = developHistory.concat((item.history || []).map(h => ({ + ...baseItem, + ...h, + ratingDate: prismaHelper.convertDate(h.ratingDate) + }))) + }) + + const existingItems = existingStat.develop || [] + + await prismaHelper.updateHistoryItems(developHistory, existingItems, tx.memberDevelopHistoryStats, { historyStatsId }, operatorId) + } + + // update DATA_SCIENCE history + if (data.DATA_SCIENCE) { + let dataScienceHistory = [] + if (data.DATA_SCIENCE.SRM && data.DATA_SCIENCE.SRM.history && data.DATA_SCIENCE.SRM.history.length > 0) { + dataScienceHistory = data.DATA_SCIENCE.SRM.history.map(h => ({ + ...h, + date: prismaHelper.convertDate(h.date), + subTrack: 'SRM' + })) + } + + if (data.DATA_SCIENCE.MARATHON_MATCH && data.DATA_SCIENCE.MARATHON_MATCH.history && data.DATA_SCIENCE.MARATHON_MATCH.history.length > 0) { + dataScienceHistory = dataScienceHistory.concat(data.DATA_SCIENCE.MARATHON_MATCH.history.map(h => ({ + ...h, + date: prismaHelper.convertDate(h.date), + subTrack: 'MARATHON_MATCH' + }))) + } + + const existingItems = existingStat.dataScience || [] + + await prismaHelper.updateHistoryItems(dataScienceHistory, existingItems, tx.memberDataScienceHistoryStats, { historyStatsId }, operatorId) + } + + const updatedHistoryStats = await tx.memberHistoryStats.findUnique({ + where: { id: existingStat.id }, + include: { develop: true, dataScience: true } + }) + + return updatedHistoryStats + }) + + // build stats history response + result = prismaHelper.buildStatsHistoryResponse(member, result, HISTORY_STATS_FIELDS) + // remove identifiable info fields if user is not admin, not M2M and not member himself + if (!helper.canManageMember(currentUser, member)) { + result = _.map(result, (item) => _.omit(item, config.STATISTICS_SECURE_FIELDS)) + } + return result +} + +const DevelopHistoryStatsSchema = Joi.array().items(Joi.object().keys({ + challengeId: Joi.positive().required(), + challengeName: Joi.string().required(), + ratingDate: Joi.positive().required(), + newRating: Joi.positive().required(), + subTrackId: Joi.positive(), + subTrack: Joi.string() +})) + +const DataScienceHistoryStatsSchema = Joi.array().items(Joi.object().keys({ + challengeId: Joi.positive().required(), + challengeName: Joi.string().required(), + date: Joi.positive().required(), + rating: Joi.positive().required(), + placement: Joi.positive().required(), + percentile: Joi.number().required(), + subTrack: Joi.string() +})) + +partiallyUpdateHistoryStats.schema = { + currentUser: Joi.any(), + handle: Joi.string().required(), + data: Joi.object().keys({ + groupId: Joi.string(), + DEVELOP: Joi.object().keys({ + subTracks: Joi.array().items(Joi.object().keys({ + id: Joi.positive().required(), + name: Joi.string().required(), + history: Joi.array().items(Joi.object().keys({ + challengeId: Joi.positive().required(), + challengeName: Joi.string(), + ratingDate: Joi.positive(), + newRating: Joi.positive() + })) + })) + }), + DATA_SCIENCE: Joi.object().keys({ + SRM: Joi.object().keys({ + history: Joi.array().items(Joi.object().keys({ + challengeId: Joi.positive().required(), + challengeName: Joi.string(), + date: Joi.positive(), + rating: Joi.positive(), + placement: Joi.positive(), + percentile: Joi.number() + })) + }), + MARATHON_MATCH: Joi.object().keys({ + history: Joi.array().items(Joi.object().keys({ + challengeId: Joi.positive().required(), + challengeName: Joi.string(), + date: Joi.positive(), + rating: Joi.positive(), + placement: Joi.positive(), + percentile: Joi.number() + })) + }) + }) + }).required() +} + /** * Get member statistics. * @param {String} handle the member handle @@ -172,6 +559,7 @@ async function getMemberStats (currentUser, handle, query, throwError) { const fields = helper.parseCommaSeparatedString(query.fields, MEMBER_STATS_FIELDS) || MEMBER_STATS_FIELDS // get member by handle const member = await helper.getMemberByHandle(handle) + const groupIds = await helper.getAllowedGroupIds(currentUser, member, query.groupIds) const includeParams = prismaHelper.statsIncludeParams @@ -198,6 +586,7 @@ async function getMemberStats (currentUser, handle, query, throwError) { stats.push(stat) } } + let result = _.map(stats, t => prismaHelper.buildStatsResponse(member, t, fields)) // remove identifiable info fields if user is not admin, not M2M and not member himself if (!helper.canManageMember(currentUser, member)) { @@ -216,6 +605,935 @@ getMemberStats.schema = { throwError: Joi.boolean() } +/** + * Create member stats. + * @param {Object} currentUser the user who performs operation + * @param {String} handle the member handle + * @param {Object} data the stats data to create + * @returns {Object} the updated member stats + */ +async function createMemberStats (currentUser, handle, data) { + // get member by handle + const member = await helper.getMemberByHandle(handle) + // check authorization + if (!helper.canManageMember(currentUser, member)) { + throw new errors.ForbiddenError('You are not allowed to update the member stats.') + } + + const groupIdsArr = [] + if (data.groupId) { + groupIdsArr.push(data.groupId) + } + + const groupIds = await helper.getAllowedGroupIds(currentUser, member, groupIdsArr) + + let existingStat + if (groupIds[0] === config.PUBLIC_GROUP_ID) { + data.isPrivate = false + // get statistics by member user id from db + existingStat = await prisma.memberStats.findFirst({ + where: { userId: member.userId, isPrivate: false } + }) + } else { + data.isPrivate = true + // get statistics private by member user id from db + existingStat = await prisma.memberStats.findFirst({ + where: { userId: member.userId, isPrivate: true, groupId: groupIds[0] } + }) + } + + if (existingStat) { + throw new errors.BadRequestError('Member stats already exists') + } + + // validate request data + if (data.DEVELOP && data.DEVELOP.subTracks && data.DEVELOP.subTracks.length > 0) { + prismaHelper.validateSubTrackData(data.DEVELOP.subTracks, [], 'Member stats develop') + } + + if (data.DESIGN && data.DESIGN.subTracks && data.DESIGN.subTracks.length > 0) { + prismaHelper.validateSubTrackData(data.DESIGN.subTracks, [], 'Member stats design') + } + + if (data.DATA_SCIENCE && data.DATA_SCIENCE.SRM) { + if (data.DATA_SCIENCE.SRM.challengeDetails) { + prismaHelper.validateLevelItemsData(data.DATA_SCIENCE.SRM.challengeDetails, [], 'Member stats dataScience srm', 'challengeDetail', MemberStatsSrmChallengeDetailsSchema) + } + + if (data.DATA_SCIENCE.SRM.division1) { + prismaHelper.validateLevelItemsData(data.DATA_SCIENCE.SRM.division1, [], 'Member stats dataScience srm', 'division1', MemberStatsSrmDivisionsSchema) + } + + if (data.DATA_SCIENCE.SRM.division2) { + prismaHelper.validateLevelItemsData(data.DATA_SCIENCE.SRM.division2, [], 'Member stats dataScience srm', 'division2', MemberStatsSrmDivisionsSchema) + } + } + + const operatorId = currentUser.userId || currentUser.sub + + // prepare insert data + if (data.DEVELOP) { + data.develop = { + challenges: data.DEVELOP.challenges, + wins: data.DEVELOP.wins, + mostRecentSubmission: prismaHelper.convertDate(data.DEVELOP.mostRecentSubmission), + mostRecentEventDate: prismaHelper.convertDate(data.DEVELOP.mostRecentEventDate), + createdBy: operatorId + } + + if (data.DEVELOP.subTracks) { + const developItems = data.DEVELOP.subTracks.map(item => ({ + subTrackId: item.id, + name: item.name, + challenges: item.challenges, + wins: item.wins, + mostRecentSubmission: prismaHelper.convertDate(item.mostRecentSubmission), + mostRecentEventDate: prismaHelper.convertDate(item.mostRecentEventDate), + ...(item.submissions ? item.submissions : {}), + ...(item.rank ? item.rank : {}), + createdBy: operatorId + })) + + data.develop.items = { + create: developItems + } + } + } + + if (data.DESIGN) { + data.design = { + challenges: data.DESIGN.challenges, + wins: data.DESIGN.wins, + mostRecentSubmission: prismaHelper.convertDate(data.DESIGN.mostRecentSubmission), + mostRecentEventDate: prismaHelper.convertDate(data.DESIGN.mostRecentEventDate), + createdBy: operatorId + } + + if (data.DESIGN.subTracks) { + const designItems = data.DESIGN.subTracks.map(item => ({ + ...(_.omit(item, ['id', 'mostRecentSubmission', 'mostRecentEventDate'])), + subTrackId: item.id, + mostRecentSubmission: prismaHelper.convertDate(item.mostRecentSubmission), + mostRecentEventDate: prismaHelper.convertDate(item.mostRecentEventDate), + createdBy: operatorId + })) + + data.design.items = { + create: designItems + } + } + } + + if (data.DATA_SCIENCE) { + data.dataScience = { + challenges: data.DATA_SCIENCE.challenges, + wins: data.DATA_SCIENCE.wins, + mostRecentEventName: data.DATA_SCIENCE.mostRecentEventName, + mostRecentSubmission: prismaHelper.convertDate(data.DATA_SCIENCE.mostRecentSubmission), + mostRecentEventDate: prismaHelper.convertDate(data.DATA_SCIENCE.mostRecentEventDate), + createdBy: operatorId + } + + if (data.DATA_SCIENCE.SRM) { + const dataScienceSrmData = { + challenges: data.DATA_SCIENCE.SRM.challenges, + wins: data.DATA_SCIENCE.SRM.wins, + mostRecentEventName: data.DATA_SCIENCE.SRM.mostRecentEventName, + mostRecentSubmission: prismaHelper.convertDate(data.DATA_SCIENCE.SRM.mostRecentSubmission), + mostRecentEventDate: prismaHelper.convertDate(data.DATA_SCIENCE.SRM.mostRecentEventDate), + ...(data.DATA_SCIENCE.SRM.rank), + createdBy: operatorId + } + + data.dataScience.srm = { + create: dataScienceSrmData + } + + if (data.DATA_SCIENCE.SRM.challengeDetails) { + const srmChallengeDetailData = data.DATA_SCIENCE.SRM.challengeDetails.map(item => ({ + ...item, + createdBy: operatorId + })) + + data.dataScience.srm.create.challengeDetails = { + create: srmChallengeDetailData + } + } + + if (data.DATA_SCIENCE.SRM.division1 || data.DATA_SCIENCE.SRM.division2) { + const srmDivision1Data = (data.DATA_SCIENCE.SRM.division1 || []).map(item => ({ + ...item, + divisionName: 'division1', + createdBy: operatorId + })) + + const srmDivision2Data = (data.DATA_SCIENCE.SRM.division2 || []).map(item => ({ + ...item, + divisionName: 'division2', + createdBy: operatorId + })) + + data.dataScience.srm.create.divisions = { + create: _.concat(srmDivision1Data, srmDivision2Data) + } + } + } + + if (data.DATA_SCIENCE.MARATHON_MATCH) { + const dataScienceMarathonData = { + challenges: data.DATA_SCIENCE.MARATHON_MATCH.challenges, + wins: data.DATA_SCIENCE.MARATHON_MATCH.wins, + mostRecentEventName: data.DATA_SCIENCE.MARATHON_MATCH.mostRecentEventName, + mostRecentSubmission: prismaHelper.convertDate(data.DATA_SCIENCE.MARATHON_MATCH.mostRecentSubmission), + mostRecentEventDate: prismaHelper.convertDate(data.DATA_SCIENCE.MARATHON_MATCH.mostRecentEventDate), + ...(data.DATA_SCIENCE.MARATHON_MATCH.rank), + createdBy: operatorId + } + + data.dataScience.marathon = { + create: dataScienceMarathonData + } + } + + if (data.COPILOT) { + data.copilot = { + ...data.COPILOT, + createdBy: operatorId + } + } + } + + // open a transaction to handle create + let result = await prisma.$transaction(async (tx) => { + // create model memberStats + const statsRes = await tx.memberStats.create({ + data: { + challenges: data.challenges, + wins: data.wins, + isPrivate: data.isPrivate, + createdBy: operatorId, + userId: member.userId, + develop: { + create: data.develop + }, + design: { + create: data.design + }, + dataScience: { + create: data.dataScience + }, + copilot: { + create: data.copilot + } + }, + include: prismaHelper.statsIncludeParams + }) + + if (!data.isPrivate) { + statsRes.groupId = _.toNumber(groupIds[0]) + } + + // create maxRating + if (data.maxRating) { + await prismaHelper.updateOrCreateModel(data.maxRating, member.maxRating, tx.memberMaxRating, { userId: member.userId }, operatorId) + } + + return statsRes + }) + + result = prismaHelper.buildStatsResponse(member, result, MEMBER_STATS_FIELDS) + // update maxRating + if (data.maxRating) { + result.maxRating = { + ...result.maxRating, + ...data.maxRating + } + } + // remove identifiable info fields if user is not admin, not M2M and not member himself + if (!helper.canManageMember(currentUser, member)) { + result = _.omit(result, config.STATISTICS_SECURE_FIELDS) + } + + return result +} + +createMemberStats.schema = { + currentUser: Joi.any(), + handle: Joi.string().required(), + data: Joi.object().keys({ + groupId: Joi.string(), + challenges: Joi.positive(), + wins: Joi.positive(), + maxRating: Joi.object().keys({ + rating: Joi.positive().required(), + track: Joi.string(), + subTrack: Joi.string(), + ratingColor: Joi.string().required() + }), + DEVELOP: Joi.object().keys({ + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + subTracks: Joi.array().items(Joi.object().keys({ + id: Joi.positive().required(), + name: Joi.string().required(), + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + submissions: Joi.object().keys({ + numInquiries: Joi.positive(), + submissions: Joi.positive(), + submissionRate: Joi.number(), + passedScreening: Joi.positive(), + screeningSuccessRate: Joi.number(), + passedReview: Joi.positive(), + reviewSuccessRate: Joi.number(), + appeals: Joi.positive(), + appealSuccessRate: Joi.number(), + maxScore: Joi.number(), + minScore: Joi.number(), + avgScore: Joi.number(), + avgPlacement: Joi.number(), + winPercent: Joi.number() + }), + rank: Joi.object().keys({ + rating: Joi.positive(), + activePercentile: Joi.number(), + activeRank: Joi.positive(), + activeCountryRank: Joi.positive(), + activeSchoolRank: Joi.positive(), + overallPercentile: Joi.number(), + overallRank: Joi.positive(), + overallCountryRank: Joi.positive(), + overallSchoolRank: Joi.positive(), + volatility: Joi.positive(), + reliability: Joi.number(), + maxRating: Joi.positive(), + minRating: Joi.positive() + }) + })) + }), + DESIGN: Joi.object().keys({ + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + subTracks: Joi.array().items(Joi.object().keys({ + id: Joi.positive().required(), + name: Joi.string().required(), + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + numInquiries: Joi.positive().required(), + submissions: Joi.positive().required(), + passedScreening: Joi.positive().required(), + avgPlacement: Joi.number().required(), + screeningSuccessRate: Joi.number().required(), + submissionRate: Joi.number().required(), + winPercent: Joi.number().required() + })) + }), + DATA_SCIENCE: Joi.object().keys({ + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + mostRecentEventName: Joi.string(), + SRM: Joi.object().keys({ + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + mostRecentEventName: Joi.string(), + rank: Joi.object().keys({ + rating: Joi.positive().required(), + percentile: Joi.number().required(), + rank: Joi.positive().required(), + countryRank: Joi.positive().required(), + schoolRank: Joi.positive().required(), + volatility: Joi.positive().required(), + maximumRating: Joi.positive().required(), + minimumRating: Joi.positive().required(), + defaultLanguage: Joi.string().required(), + competitions: Joi.positive().required() + }).required(), + challengeDetails: Joi.array().items(Joi.object().keys({ + challenges: Joi.positive().required(), + levelName: Joi.string().required(), + failedChallenges: Joi.positive().required() + })), + division1: Joi.array().items(Joi.object().keys({ + problemsSubmitted: Joi.positive().required(), + problemsSysByTest: Joi.positive().required(), + problemsFailed: Joi.positive().required(), + levelName: Joi.string().required() + })), + division2: Joi.array().items(Joi.object().keys({ + problemsSubmitted: Joi.positive().required(), + problemsSysByTest: Joi.positive().required(), + problemsFailed: Joi.positive().required(), + levelName: Joi.string().required() + })) + }), + MARATHON_MATCH: Joi.object().keys({ + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + mostRecentEventName: Joi.string(), + rank: Joi.object().keys({ + rating: Joi.positive().required(), + competitions: Joi.positive().required(), + avgRank: Joi.number().required(), + avgNumSubmissions: Joi.positive().required(), + bestRank: Joi.positive().required(), + topFiveFinishes: Joi.positive().required(), + topTenFinishes: Joi.positive().required(), + rank: Joi.positive().required(), + percentile: Joi.number().required(), + volatility: Joi.positive().required(), + minimumRating: Joi.positive().required(), + maximumRating: Joi.positive().required(), + countryRank: Joi.positive().required(), + schoolRank: Joi.positive().required(), + defaultLanguage: Joi.string().required() + }).required() + }) + }), + COPILOT: Joi.object().keys({ + contests: Joi.positive().required(), + projects: Joi.positive().required(), + failures: Joi.positive().required(), + reposts: Joi.positive().required(), + activeContests: Joi.positive().required(), + activeProjects: Joi.positive().required(), + fulfillment: Joi.number().required() + }) + }).required() +} + +/** + * Partially update member stats. + * @param {Object} currentUser the user who performs operation + * @param {String} handle the member handle + * @param {Object} data the stats data to update + * @returns {Object} the updated member stats + */ +async function partiallyUpdateMemberStats (currentUser, handle, data) { + // get member by handle + const member = await helper.getMemberByHandle(handle) + // check authorization + if (!helper.canManageMember(currentUser, member)) { + throw new errors.ForbiddenError('You are not allowed to update the member stats.') + } + + const groupIdsArr = [] + if (data.groupId) { + groupIdsArr.push(data.groupId) + } + + const groupIds = await helper.getAllowedGroupIds(currentUser, member, groupIdsArr) + + const includeParams = prismaHelper.statsIncludeParams + + let existingStat + if (groupIds[0] === config.PUBLIC_GROUP_ID) { + // get statistics by member user id from db + existingStat = await prisma.memberStats.findFirst({ + where: { userId: member.userId, isPrivate: false }, + include: includeParams + }) + if (!_.isNil(existingStat)) { + existingStat = _.assign(existingStat, { groupId: _.toNumber(groupIds[0]) }) + } + } else { + // get statistics private by member user id from db + existingStat = await prisma.memberStats.findFirst({ + where: { userId: member.userId, isPrivate: true, groupId: groupIds[0] }, + include: includeParams + }) + } + + if (!existingStat || !existingStat.id) { + throw new errors.NotFoundError('Member stats not found') + } + + // validate request data + if (data.DEVELOP && data.DEVELOP.subTracks && data.DEVELOP.subTracks.length > 0) { + const developItemsDB = existingStat.develop ? (existingStat.develop.items || []) : [] + prismaHelper.validateSubTrackData(data.DEVELOP.subTracks, developItemsDB, 'Member stats develop') + } + + if (data.DESIGN && data.DESIGN.subTracks && data.DESIGN.subTracks.length > 0) { + const designItemsDB = existingStat.design ? (existingStat.design.items || []) : [] + const toCreateItems = prismaHelper.validateSubTrackData(data.DESIGN.subTracks, designItemsDB, 'Member stats design') + if (toCreateItems.length > 0) { + const validateRes = MemberStatsDesignSubTrackSchema.validate(toCreateItems) + + if (validateRes.error) { + throw new errors.BadRequestError(validateRes.error.error) + } + } + } + + if (data.DATA_SCIENCE && data.DATA_SCIENCE.SRM) { + if (!existingStat.dataScience || !existingStat.dataScience.srm) { + const validateRes1 = MemberStatsDataScienceSrmSchema.validate(data.DATA_SCIENCE.SRM) + if (validateRes1.error) { + throw new errors.BadRequestError(validateRes1.error.error) + } + } + + const dataScienceDB = existingStat.dataScience || {} + const srmDB = dataScienceDB.srm || {} + if (data.DATA_SCIENCE.SRM.challengeDetails) { + const srmChallengeDetailsDB = srmDB.challengeDetails || [] + prismaHelper.validateLevelItemsData(data.DATA_SCIENCE.SRM.challengeDetails, srmChallengeDetailsDB, 'Member stats dataScience srm', 'challengeDetail', MemberStatsSrmChallengeDetailsSchema) + } + + const srmDivisionsDB = srmDB.divisions || [] + if (data.DATA_SCIENCE.SRM.division1) { + prismaHelper.validateLevelItemsData(data.DATA_SCIENCE.SRM.division1, srmDivisionsDB, 'Member stats dataScience srm', 'division1', MemberStatsSrmDivisionsSchema) + } + + if (data.DATA_SCIENCE.SRM.division2) { + prismaHelper.validateLevelItemsData(data.DATA_SCIENCE.SRM.division2, srmDivisionsDB, 'Member stats dataScience srm', 'division2', MemberStatsSrmDivisionsSchema) + } + } + + if (data.DATA_SCIENCE && data.DATA_SCIENCE.MARATHON_MATCH && !(existingStat.dataScience || {}).marathon) { + const validateRes1 = MemberStatsDataScienceMarathonSchema.validate(data.DATA_SCIENCE.MARATHON_MATCH) + if (validateRes1.error) { + throw new errors.BadRequestError(validateRes1.error.error) + } + } + + if (data.COPILOT && data.COPILOT && !existingStat.copilot) { + const validateRes1 = MemberStatsDataScienceCopilotSchema.validate(data.COPILOT) + if (validateRes1.error) { + throw new errors.BadRequestError(validateRes1.error.error) + } + } + + const operatorId = currentUser.userId || currentUser.sub + + // open a transaction to handle update + const result = await prisma.$transaction(async (tx) => { + // update model memberStats + if (data.challenges || data.wins) { + await tx.memberStats.update({ + where: { + id: existingStat.id + }, + data: { + challenges: data.challenges, + wins: data.wins, + updatedBy: operatorId + } + }) + } + + // update maxRating + if (data.maxRating) { + await prismaHelper.updateOrCreateModel(data.maxRating, member.maxRating, tx.memberMaxRating, { userId: member.userId }, operatorId) + const updatedMaxRating = await tx.memberMaxRating.findFirst({ + where: { + userId: member.userId + } + }) + member.maxRating = updatedMaxRating + } + + // update DEVELOP + if (data.DEVELOP) { + const developData = { + challenges: data.DEVELOP.challenges, + wins: data.DEVELOP.wins, + mostRecentSubmission: prismaHelper.convertDate(data.DEVELOP.mostRecentSubmission), + mostRecentEventDate: prismaHelper.convertDate(data.DEVELOP.mostRecentEventDate) + } + const newDevelop = await prismaHelper.updateOrCreateModel(developData, existingStat.develop, tx.memberDevelopStats, { memberStatsId: existingStat.id }, operatorId) + if (newDevelop) { + existingStat.develop = newDevelop + } + + // update develop subTracks + if (data.DEVELOP.subTracks) { + const developItems = data.DEVELOP.subTracks.map(item => ({ + subTrackId: item.id, + name: item.name, + challenges: item.challenges, + wins: item.wins, + mostRecentSubmission: prismaHelper.convertDate(item.mostRecentSubmission), + mostRecentEventDate: prismaHelper.convertDate(item.mostRecentEventDate), + ...(item.submissions ? item.submissions : {}), + ...(item.rank ? item.rank : {}) + })) + + const developStatsId = existingStat.develop.id + const existingItems = existingStat.develop.items || [] + + await prismaHelper.updateArrayItems(developItems, existingItems, tx.memberDevelopStatsItem, { developStatsId }, operatorId) + } + } + + // update DESIGN + if (data.DESIGN) { + const designData = { + challenges: data.DESIGN.challenges, + wins: data.DESIGN.wins, + mostRecentSubmission: prismaHelper.convertDate(data.DESIGN.mostRecentSubmission), + mostRecentEventDate: prismaHelper.convertDate(data.DESIGN.mostRecentEventDate) + } + const newDesign = await prismaHelper.updateOrCreateModel(designData, existingStat.design, tx.memberDesignStats, { memberStatsId: existingStat.id }, operatorId) + if (newDesign) { + existingStat.design = newDesign + } + + // update design subTracks + if (data.DESIGN.subTracks) { + const designItems = data.DESIGN.subTracks.map(item => ({ + ...(_.omit(item, ['id', 'mostRecentSubmission', 'mostRecentEventDate'])), + subTrackId: item.id, + mostRecentSubmission: prismaHelper.convertDate(item.mostRecentSubmission), + mostRecentEventDate: prismaHelper.convertDate(item.mostRecentEventDate) + })) + + const designStatsId = existingStat.design.id + const existingItems = existingStat.design.items || [] + + await prismaHelper.updateArrayItems(designItems, existingItems, tx.memberDesignStatsItem, { designStatsId }, operatorId) + } + } + + // update DATA_SCIENCE + if (data.DATA_SCIENCE) { + const dataScienceData = { + challenges: data.DATA_SCIENCE.challenges, + wins: data.DATA_SCIENCE.wins, + mostRecentEventName: data.DATA_SCIENCE.mostRecentEventName, + mostRecentSubmission: prismaHelper.convertDate(data.DATA_SCIENCE.mostRecentSubmission), + mostRecentEventDate: prismaHelper.convertDate(data.DATA_SCIENCE.mostRecentEventDate) + } + const newDataScience = await prismaHelper.updateOrCreateModel(dataScienceData, existingStat.dataScience, tx.memberDataScienceStats, { memberStatsId: existingStat.id }, operatorId) + if (newDataScience) { + existingStat.dataScience = newDataScience + } + + // update data science srm + if (data.DATA_SCIENCE.SRM) { + const dataScienceSrmData = { + challenges: data.DATA_SCIENCE.SRM.challenges, + wins: data.DATA_SCIENCE.SRM.wins, + mostRecentEventName: data.DATA_SCIENCE.SRM.mostRecentEventName, + mostRecentSubmission: prismaHelper.convertDate(data.DATA_SCIENCE.SRM.mostRecentSubmission), + mostRecentEventDate: prismaHelper.convertDate(data.DATA_SCIENCE.SRM.mostRecentEventDate), + ...(data.DATA_SCIENCE.SRM.rank) + } + const newDataScienceSrm = await prismaHelper.updateOrCreateModel(dataScienceSrmData, existingStat.dataScience.srm, tx.memberSrmStats, { dataScienceStatsId: existingStat.dataScience.id }, operatorId) + if (newDataScienceSrm) { + existingStat.dataScience.srm = newDataScienceSrm + } + + const srmStatsId = existingStat.dataScience.srm.id + if (data.DATA_SCIENCE.SRM.challengeDetails) { + const existingItems = existingStat.dataScience.srm.challengeDetails || [] + + await prismaHelper.updateArrayLevelItems(data.DATA_SCIENCE.SRM.challengeDetails, existingItems, tx.memberSrmChallengeDetail, { srmStatsId }, operatorId) + } + + if (data.DATA_SCIENCE.SRM.division1 || data.DATA_SCIENCE.SRM.division2) { + const existingItems = existingStat.dataScience.srm.divisions || [] + + await prismaHelper.updateArrayDivisionItems(data.DATA_SCIENCE.SRM.division1, data.DATA_SCIENCE.SRM.division2, existingItems, tx.memberSrmDivisionDetail, { srmStatsId }, operatorId) + } + } + + // update data science marathon + if (data.DATA_SCIENCE.MARATHON_MATCH) { + const dataScienceMarathonData = { + challenges: data.DATA_SCIENCE.MARATHON_MATCH.challenges, + wins: data.DATA_SCIENCE.MARATHON_MATCH.wins, + mostRecentEventName: data.DATA_SCIENCE.MARATHON_MATCH.mostRecentEventName, + mostRecentSubmission: prismaHelper.convertDate(data.DATA_SCIENCE.MARATHON_MATCH.mostRecentSubmission), + mostRecentEventDate: prismaHelper.convertDate(data.DATA_SCIENCE.MARATHON_MATCH.mostRecentEventDate), + ...(data.DATA_SCIENCE.MARATHON_MATCH.rank) + } + await prismaHelper.updateOrCreateModel(dataScienceMarathonData, existingStat.dataScience.marathon, tx.memberMarathonStats, { dataScienceStatsId: existingStat.dataScience.id }, operatorId) + } + } + + // update COPILOT + if (data.COPILOT) { + await prismaHelper.updateOrCreateModel(data.COPILOT, existingStat.copilot, tx.memberCopilotStats, { memberStatsId: existingStat.id }, operatorId) + } + + // Fetch updated stats + let updatedStats = await tx.memberStats.findUnique({ + where: { id: existingStat.id }, + include: includeParams + }) + updatedStats.groupId = existingStat.groupId + updatedStats = prismaHelper.buildStatsResponse(member, updatedStats, MEMBER_STATS_FIELDS) + // remove identifiable info fields if user is not admin, not M2M and not member himself + if (!helper.canManageMember(currentUser, member)) { + updatedStats = _.omit(updatedStats, config.STATISTICS_SECURE_FIELDS) + } + + return updatedStats + }) + + return result +} + +const MemberStatsDesignSubTrackSchema = Joi.array().items(Joi.object().keys({ + id: Joi.positive().required(), + name: Joi.string(), + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + numInquiries: Joi.positive().required(), + submissions: Joi.positive().required(), + passedScreening: Joi.positive().required(), + avgPlacement: Joi.number().required(), + screeningSuccessRate: Joi.number().required(), + submissionRate: Joi.number().required(), + winPercent: Joi.number().required() +})) + +const MemberStatsDataScienceSrmSchema = Joi.object().keys({ + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + mostRecentEventName: Joi.string(), + rank: Joi.object().keys({ + rating: Joi.positive().required(), + percentile: Joi.number().required(), + rank: Joi.positive().required(), + countryRank: Joi.positive().required(), + schoolRank: Joi.positive().required(), + volatility: Joi.positive().required(), + maximumRating: Joi.positive().required(), + minimumRating: Joi.positive().required(), + defaultLanguage: Joi.string().required(), + competitions: Joi.positive().required() + }).required(), + challengeDetails: Joi.array(), + division1: Joi.array(), + division2: Joi.array() +}) + +const MemberStatsSrmChallengeDetailsSchema = Joi.array().items(Joi.object().keys({ + challenges: Joi.positive().required(), + levelName: Joi.string().required(), + failedChallenges: Joi.positive().required() +})) + +const MemberStatsSrmDivisionsSchema = Joi.array().items(Joi.object().keys({ + problemsSubmitted: Joi.positive().required(), + problemsSysByTest: Joi.positive().required(), + problemsFailed: Joi.positive().required(), + levelName: Joi.string().required() +})) + +const MemberStatsDataScienceMarathonSchema = Joi.object().keys({ + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + mostRecentEventName: Joi.string(), + rank: Joi.object().keys({ + rating: Joi.positive().required(), + competitions: Joi.positive().required(), + avgRank: Joi.number().required(), + avgNumSubmissions: Joi.positive().required(), + bestRank: Joi.positive().required(), + topFiveFinishes: Joi.positive().required(), + topTenFinishes: Joi.positive().required(), + rank: Joi.positive().required(), + percentile: Joi.number().required(), + volatility: Joi.positive().required(), + minimumRating: Joi.positive().required(), + maximumRating: Joi.positive().required(), + countryRank: Joi.positive().required(), + schoolRank: Joi.positive().required(), + defaultLanguage: Joi.string().required() + }).required() +}) + +const MemberStatsDataScienceCopilotSchema = Joi.object().keys({ + contests: Joi.positive().required(), + projects: Joi.positive().required(), + failures: Joi.positive().required(), + reposts: Joi.positive().required(), + activeContests: Joi.positive().required(), + activeProjects: Joi.positive().required(), + fulfillment: Joi.number().required() +}) + +partiallyUpdateMemberStats.schema = { + currentUser: Joi.any(), + handle: Joi.string().required(), + data: Joi.object().keys({ + groupId: Joi.string(), + challenges: Joi.positive(), + wins: Joi.positive(), + maxRating: Joi.object().keys({ + rating: Joi.positive().required(), + track: Joi.string(), + subTrack: Joi.string(), + ratingColor: Joi.string().required() + }), + DEVELOP: Joi.object().keys({ + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + subTracks: Joi.array().items(Joi.object().keys({ + id: Joi.positive().required(), + name: Joi.string(), + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + submissions: Joi.object().keys({ + numInquiries: Joi.positive(), + submissions: Joi.positive(), + submissionRate: Joi.number(), + passedScreening: Joi.positive(), + screeningSuccessRate: Joi.number(), + passedReview: Joi.positive(), + reviewSuccessRate: Joi.number(), + appeals: Joi.positive(), + appealSuccessRate: Joi.number(), + maxScore: Joi.number(), + minScore: Joi.number(), + avgScore: Joi.number(), + avgPlacement: Joi.number(), + winPercent: Joi.number() + }), + rank: Joi.object().keys({ + rating: Joi.positive(), + activePercentile: Joi.number(), + activeRank: Joi.positive(), + activeCountryRank: Joi.positive(), + activeSchoolRank: Joi.positive(), + overallPercentile: Joi.number(), + overallRank: Joi.positive(), + overallCountryRank: Joi.positive(), + overallSchoolRank: Joi.positive(), + volatility: Joi.positive(), + reliability: Joi.number(), + maxRating: Joi.positive(), + minRating: Joi.positive() + }) + })) + }), + DESIGN: Joi.object().keys({ + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + subTracks: Joi.array().items(Joi.object().keys({ + id: Joi.positive().required(), + name: Joi.string(), + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + numInquiries: Joi.positive(), + submissions: Joi.positive(), + passedScreening: Joi.positive(), + avgPlacement: Joi.number(), + screeningSuccessRate: Joi.number(), + submissionRate: Joi.number(), + winPercent: Joi.number() + })) + }), + DATA_SCIENCE: Joi.object().keys({ + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + mostRecentEventName: Joi.string(), + SRM: Joi.object().keys({ + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + mostRecentEventName: Joi.string(), + rank: Joi.object().keys({ + rating: Joi.positive(), + percentile: Joi.number(), + rank: Joi.positive(), + countryRank: Joi.positive(), + schoolRank: Joi.positive(), + volatility: Joi.positive(), + maximumRating: Joi.positive(), + minimumRating: Joi.positive(), + defaultLanguage: Joi.string(), + competitions: Joi.positive() + }), + challengeDetails: Joi.array().items(Joi.object().keys({ + challenges: Joi.positive(), + levelName: Joi.string().required(), + failedChallenges: Joi.positive() + })), + division1: Joi.array().items(Joi.object().keys({ + problemsSubmitted: Joi.positive(), + problemsSysByTest: Joi.positive(), + problemsFailed: Joi.positive(), + levelName: Joi.string().required() + })), + division2: Joi.array().items(Joi.object().keys({ + problemsSubmitted: Joi.positive(), + problemsSysByTest: Joi.positive(), + problemsFailed: Joi.positive(), + levelName: Joi.string().required() + })) + }), + MARATHON_MATCH: Joi.object().keys({ + challenges: Joi.positive(), + wins: Joi.positive(), + mostRecentSubmission: Joi.positive(), + mostRecentEventDate: Joi.positive(), + mostRecentEventName: Joi.string(), + rank: Joi.object().keys({ + rating: Joi.positive(), + competitions: Joi.positive(), + avgRank: Joi.number(), + avgNumSubmissions: Joi.positive(), + bestRank: Joi.positive(), + topFiveFinishes: Joi.positive(), + topTenFinishes: Joi.positive(), + rank: Joi.positive(), + percentile: Joi.number(), + volatility: Joi.positive(), + minimumRating: Joi.positive(), + maximumRating: Joi.positive(), + countryRank: Joi.positive(), + schoolRank: Joi.positive(), + defaultLanguage: Joi.string() + }) + }) + }), + COPILOT: Joi.object().keys({ + contests: Joi.positive(), + projects: Joi.positive(), + failures: Joi.positive(), + reposts: Joi.positive(), + activeContests: Joi.positive(), + activeProjects: Joi.positive(), + fulfillment: Joi.number() + }) + }).required() +} + /** * Get member skills. * @param {String} handle the member handle @@ -244,7 +1562,7 @@ getMemberSkills.schema = { * Check create/update member skill data * @param {Object} data request body */ -async function validateMemberSkillData(data) { +async function validateMemberSkillData (data) { // Check displayMode if (data.displayModeId) { const modeCount = await prisma.displayMode.count({ @@ -264,7 +1582,6 @@ async function validateMemberSkillData(data) { } } - async function createMemberSkills (currentUser, handle, data) { // get member by handle const member = await helper.getMemberByHandle(handle) @@ -288,14 +1605,14 @@ async function createMemberSkills (currentUser, handle, data) { id: uuidv4(), userId: member.userId, skillId: data.skillId, - createdBy, + createdBy } if (data.displayModeId) { memberSkillData.displayModeId = data.displayModeId } if (data.levels && data.levels.length > 0) { memberSkillData.levels = { - createMany: { data: + createMany: { data: _.map(data.levels, levelId => ({ skillLevelId: levelId, createdBy @@ -346,7 +1663,7 @@ async function partiallyUpdateMemberSkills (currentUser, handle, data) { const updatedBy = currentUser.handle || currentUser.sub const memberSkillData = { - updatedBy, + updatedBy } if (data.displayModeId) { memberSkillData.displayModeId = data.displayModeId @@ -356,7 +1673,7 @@ async function partiallyUpdateMemberSkills (currentUser, handle, data) { where: { memberSkillId: existing.id } }) memberSkillData.levels = { - createMany: { data: + createMany: { data: _.map(data.levels, levelId => ({ skillLevelId: levelId, createdBy: updatedBy, @@ -388,7 +1705,11 @@ partiallyUpdateMemberSkills.schema = { module.exports = { getDistribution, getHistoryStats, + createHistoryStats, + partiallyUpdateHistoryStats, getMemberStats, + createMemberStats, + partiallyUpdateMemberStats, getMemberSkills, createMemberSkills, partiallyUpdateMemberSkills