mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(api): add endpoint for submitting daily coding challenges (#59465)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
+79
-65
@@ -29,6 +29,19 @@ type CompletedChallenge {
|
||||
examResults ExamResults? // Undefined
|
||||
}
|
||||
|
||||
enum DailyCodingChallengeLanguage {
|
||||
javascript
|
||||
python
|
||||
}
|
||||
|
||||
type CompletedDailyCodingChallenge {
|
||||
id String @db.ObjectId
|
||||
/// Date in milliseconds since epoch
|
||||
/// This is not a DateTime, because DateTime does not serialize directly to JSON
|
||||
completedDate Int
|
||||
languages DailyCodingChallengeLanguage[]
|
||||
}
|
||||
|
||||
type PartiallyCompletedChallenge {
|
||||
id String
|
||||
completedDate Float
|
||||
@@ -77,77 +90,78 @@ type QuizAttempt {
|
||||
|
||||
/// Corresponds to the `user` collection.
|
||||
model user {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
about String
|
||||
acceptedPrivacyTerms Boolean
|
||||
completedChallenges CompletedChallenge[]
|
||||
completedExams CompletedExam[] // Undefined
|
||||
quizAttempts QuizAttempt[] // Undefined
|
||||
currentChallengeId String?
|
||||
donationEmails String[] // Undefined | String[] (only possible for built in Types like String)
|
||||
email String
|
||||
emailAuthLinkTTL DateTime? // Null | Undefined
|
||||
emailVerified Boolean
|
||||
emailVerifyTTL DateTime? // Null | Undefined
|
||||
externalId String
|
||||
githubProfile String? // Undefined
|
||||
isApisMicroservicesCert Boolean? // Undefined
|
||||
isBackEndCert Boolean? // Undefined
|
||||
isBanned Boolean? // Undefined
|
||||
isCheater Boolean? // Undefined
|
||||
isDataAnalysisPyCertV7 Boolean? // Undefined
|
||||
isDataVisCert Boolean? // Undefined
|
||||
isDonating Boolean
|
||||
isFoundationalCSharpCertV8 Boolean? // Undefined
|
||||
isFrontEndCert Boolean? // Undefined
|
||||
isFrontEndLibsCert Boolean? // Undefined
|
||||
isFullStackCert Boolean? // Undefined
|
||||
isHonest Boolean?
|
||||
isInfosecCertV7 Boolean? // Undefined
|
||||
isInfosecQaCert Boolean? // Undefined
|
||||
isJsAlgoDataStructCert Boolean? // Undefined
|
||||
isJsAlgoDataStructCertV8 Boolean? // Undefined
|
||||
isMachineLearningPyCertV7 Boolean? // Undefined
|
||||
isQaCertV7 Boolean? // Undefined
|
||||
isRelationalDatabaseCertV8 Boolean? // Undefined
|
||||
isRespWebDesignCert Boolean? // Undefined
|
||||
isSciCompPyCertV7 Boolean? // Undefined
|
||||
is2018DataVisCert Boolean? // Undefined
|
||||
is2018FullStackCert Boolean? // Undefined
|
||||
isCollegeAlgebraPyCertV8 Boolean? // Undefined
|
||||
// isUpcomingPythonCertV8 Boolean? // Undefined. It is in the db but has never been used.
|
||||
keyboardShortcuts Boolean? // Undefined
|
||||
linkedin String? // Null | Undefined
|
||||
location String? // Null
|
||||
name String? // Null
|
||||
needsModeration Boolean? // Undefined
|
||||
newEmail String? // Null | Undefined
|
||||
partiallyCompletedChallenges PartiallyCompletedChallenge[] // Undefined | PartiallyCompletedChallenge[]
|
||||
password String? // Undefined
|
||||
picture String
|
||||
portfolio Portfolio[]
|
||||
profileUI ProfileUI? // Undefined
|
||||
progressTimestamps Json? // ProgressTimestamp[] | Null[] | Int64[] | Double[] - TODO: NORMALIZE
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
about String
|
||||
acceptedPrivacyTerms Boolean
|
||||
completedChallenges CompletedChallenge[]
|
||||
completedDailyCodingChallenges CompletedDailyCodingChallenge[]
|
||||
completedExams CompletedExam[] // Undefined
|
||||
quizAttempts QuizAttempt[] // Undefined
|
||||
currentChallengeId String?
|
||||
donationEmails String[] // Undefined | String[] (only possible for built in Types like String)
|
||||
email String
|
||||
emailAuthLinkTTL DateTime? // Null | Undefined
|
||||
emailVerified Boolean
|
||||
emailVerifyTTL DateTime? // Null | Undefined
|
||||
externalId String
|
||||
githubProfile String? // Undefined
|
||||
isApisMicroservicesCert Boolean? // Undefined
|
||||
isBackEndCert Boolean? // Undefined
|
||||
isBanned Boolean? // Undefined
|
||||
isCheater Boolean? // Undefined
|
||||
isDataAnalysisPyCertV7 Boolean? // Undefined
|
||||
isDataVisCert Boolean? // Undefined
|
||||
isDonating Boolean
|
||||
isFoundationalCSharpCertV8 Boolean? // Undefined
|
||||
isFrontEndCert Boolean? // Undefined
|
||||
isFrontEndLibsCert Boolean? // Undefined
|
||||
isFullStackCert Boolean? // Undefined
|
||||
isHonest Boolean?
|
||||
isInfosecCertV7 Boolean? // Undefined
|
||||
isInfosecQaCert Boolean? // Undefined
|
||||
isJsAlgoDataStructCert Boolean? // Undefined
|
||||
isJsAlgoDataStructCertV8 Boolean? // Undefined
|
||||
isMachineLearningPyCertV7 Boolean? // Undefined
|
||||
isQaCertV7 Boolean? // Undefined
|
||||
isRelationalDatabaseCertV8 Boolean? // Undefined
|
||||
isRespWebDesignCert Boolean? // Undefined
|
||||
isSciCompPyCertV7 Boolean? // Undefined
|
||||
is2018DataVisCert Boolean? // Undefined
|
||||
is2018FullStackCert Boolean? // Undefined
|
||||
isCollegeAlgebraPyCertV8 Boolean? // Undefined
|
||||
// isUpcomingPythonCertV8 Boolean? // Undefined. It is in the db but has never been used.
|
||||
keyboardShortcuts Boolean? // Undefined
|
||||
linkedin String? // Null | Undefined
|
||||
location String? // Null
|
||||
name String? // Null
|
||||
needsModeration Boolean? // Undefined
|
||||
newEmail String? // Null | Undefined
|
||||
partiallyCompletedChallenges PartiallyCompletedChallenge[] // Undefined | PartiallyCompletedChallenge[]
|
||||
password String? // Undefined
|
||||
picture String
|
||||
portfolio Portfolio[]
|
||||
profileUI ProfileUI? // Undefined
|
||||
progressTimestamps Json? // ProgressTimestamp[] | Null[] | Int64[] | Double[] - TODO: NORMALIZE
|
||||
/// A random number between 0 and 1.
|
||||
///
|
||||
/// Valuable for selectively performing random logic.
|
||||
rand Float?
|
||||
savedChallenges SavedChallenge[] // Undefined | SavedChallenge[]
|
||||
sendQuincyEmail Boolean
|
||||
theme String? // Undefined
|
||||
timezone String? // Undefined
|
||||
twitter String? // Null | Undefined
|
||||
unsubscribeId String
|
||||
rand Float?
|
||||
savedChallenges SavedChallenge[] // Undefined | SavedChallenge[]
|
||||
sendQuincyEmail Boolean
|
||||
theme String? // Undefined
|
||||
timezone String? // Undefined
|
||||
twitter String? // Null | Undefined
|
||||
unsubscribeId String
|
||||
/// Used to track the number of times the user's record was written to.
|
||||
///
|
||||
/// This has the main benefit of allowing concurrent ops to check for race conditions.
|
||||
updateCount Int? @default(0)
|
||||
username String // TODO(Post-MVP): make this unique
|
||||
usernameDisplay String? // Undefined
|
||||
verificationToken String? // Undefined
|
||||
website String? // Undefined
|
||||
yearsTopContributor String[] // Undefined | String[]
|
||||
isClassroomAccount Boolean? // Undefined
|
||||
updateCount Int? @default(0)
|
||||
username String // TODO(Post-MVP): make this unique
|
||||
usernameDisplay String? // Undefined
|
||||
verificationToken String? // Undefined
|
||||
website String? // Undefined
|
||||
yearsTopContributor String[] // Undefined | String[]
|
||||
isClassroomAccount Boolean? // Undefined
|
||||
|
||||
// Relations
|
||||
examAttempts EnvExamAttempt[]
|
||||
|
||||
@@ -11,6 +11,7 @@ export const newUser = (email: string) => ({
|
||||
about: '',
|
||||
acceptedPrivacyTerms: false,
|
||||
completedChallenges: [],
|
||||
completedDailyCodingChallenges: [],
|
||||
completedExams: [],
|
||||
quizAttempts: [],
|
||||
currentChallengeId: '',
|
||||
|
||||
@@ -5,6 +5,7 @@ const mockVerifyTrophyWithMicrosoft = jest.fn();
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { omit } from 'lodash';
|
||||
import { Static } from '@fastify/type-provider-typebox';
|
||||
import { DailyCodingChallengeLanguage } from '@prisma/client';
|
||||
|
||||
import { challengeTypes } from '../../../../shared/config/challenge-types';
|
||||
import {
|
||||
@@ -146,6 +147,12 @@ const updatedMultiFileCertProjectBody = {
|
||||
]
|
||||
};
|
||||
|
||||
const dailyCodingChallengeId = '5900f36e1000cf542c50fe80';
|
||||
const dailyCodingChallengeBody = {
|
||||
id: dailyCodingChallengeId,
|
||||
language: DailyCodingChallengeLanguage.javascript
|
||||
};
|
||||
|
||||
describe('challengeRoutes', () => {
|
||||
setupServer();
|
||||
describe('Authenticated user', () => {
|
||||
@@ -912,6 +919,204 @@ describe('challengeRoutes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('/daily-coding-challenge-completed', () => {
|
||||
describe('validation', () => {
|
||||
test('POST rejects requests without an id', async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { id, ...noIdReqBody } = dailyCodingChallengeBody;
|
||||
const response = await superPost(
|
||||
'/daily-coding-challenge-completed'
|
||||
).send(noIdReqBody);
|
||||
|
||||
expect(response.body).toStrictEqual(
|
||||
isValidChallengeCompletionErrorMsg
|
||||
);
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('POST rejects requests without a language', async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { language, ...noLanguageReqBody } = dailyCodingChallengeBody;
|
||||
const response = await superPost(
|
||||
'/daily-coding-challenge-completed'
|
||||
).send(noLanguageReqBody);
|
||||
|
||||
expect(response.body).toStrictEqual(
|
||||
isValidChallengeCompletionErrorMsg
|
||||
);
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('POST rejects requests without valid ObjectIDs', async () => {
|
||||
const response = await superPost(
|
||||
'/daily-coding-challenge-completed'
|
||||
).send({
|
||||
...dailyCodingChallengeBody,
|
||||
id: 'not-a-valid-id'
|
||||
});
|
||||
|
||||
expect(response.body).toStrictEqual(
|
||||
isValidChallengeCompletionErrorMsg
|
||||
);
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('POST rejects requests without valid coding language', async () => {
|
||||
const response = await superPost(
|
||||
'/daily-coding-challenge-completed'
|
||||
).send({
|
||||
...dailyCodingChallengeBody,
|
||||
language: 'not-a-valid-language'
|
||||
});
|
||||
|
||||
expect(response.body).toStrictEqual(
|
||||
isValidChallengeCompletionErrorMsg
|
||||
);
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handling', () => {
|
||||
afterEach(async () => {
|
||||
await fastifyTestInstance.prisma.user.updateMany({
|
||||
where: { email: 'foo@bar.com' },
|
||||
data: {
|
||||
completedDailyCodingChallenges: [],
|
||||
progressTimestamps: []
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('POST correctly handles multiple requests', async () => {
|
||||
const now = Date.now();
|
||||
|
||||
const res1 = await superPost(
|
||||
'/daily-coding-challenge-completed'
|
||||
).send(dailyCodingChallengeBody);
|
||||
|
||||
const user1 = await fastifyTestInstance.prisma.user.findFirstOrThrow({
|
||||
where: { email: 'foo@bar.com' }
|
||||
});
|
||||
|
||||
const completedDate =
|
||||
user1.completedDailyCodingChallenges[0]?.completedDate;
|
||||
|
||||
// should have correct completedDate
|
||||
expect(completedDate).toBeGreaterThanOrEqual(now);
|
||||
expect(completedDate).toBeLessThanOrEqual(now + 1000);
|
||||
|
||||
expect(user1).toMatchObject({
|
||||
// should add completedDailyCodingChallenge to database with correct info
|
||||
completedDailyCodingChallenges: [
|
||||
{
|
||||
id: dailyCodingChallengeId,
|
||||
completedDate,
|
||||
languages: [DailyCodingChallengeLanguage.javascript]
|
||||
}
|
||||
],
|
||||
// should add to progressTimestamps
|
||||
progressTimestamps: [completedDate]
|
||||
});
|
||||
|
||||
// should have correct response
|
||||
expect(res1.statusCode).toBe(200);
|
||||
expect(res1.body).toStrictEqual({
|
||||
alreadyCompleted: false,
|
||||
points: 1,
|
||||
completedDate,
|
||||
completedDailyCodingChallenges: [
|
||||
{
|
||||
id: dailyCodingChallengeId,
|
||||
completedDate,
|
||||
languages: [DailyCodingChallengeLanguage.javascript]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const res2 = await superPost(
|
||||
'/daily-coding-challenge-completed'
|
||||
).send(dailyCodingChallengeBody);
|
||||
|
||||
const user2 = await fastifyTestInstance.prisma.user.findFirstOrThrow({
|
||||
where: { email: 'foo@bar.com' }
|
||||
});
|
||||
|
||||
// should not add 'javascript' again, should not update completedDate
|
||||
expect(user2).toMatchObject({
|
||||
completedDailyCodingChallenges: [
|
||||
{
|
||||
id: dailyCodingChallengeId,
|
||||
completedDate,
|
||||
languages: [DailyCodingChallengeLanguage.javascript]
|
||||
}
|
||||
],
|
||||
// should not add to progressTimestamps
|
||||
progressTimestamps: [completedDate]
|
||||
});
|
||||
|
||||
// should have correct response
|
||||
expect(res2.statusCode).toBe(200);
|
||||
expect(res2.body).toStrictEqual({
|
||||
alreadyCompleted: true,
|
||||
points: 1,
|
||||
completedDate,
|
||||
completedDailyCodingChallenges: [
|
||||
{
|
||||
id: dailyCodingChallengeId,
|
||||
completedDate,
|
||||
languages: [DailyCodingChallengeLanguage.javascript]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const res3 = await superPost(
|
||||
'/daily-coding-challenge-completed'
|
||||
).send({
|
||||
...dailyCodingChallengeBody,
|
||||
language: 'python'
|
||||
});
|
||||
|
||||
const user3 = await fastifyTestInstance.prisma.user.findFirstOrThrow({
|
||||
where: { email: 'foo@bar.com' }
|
||||
});
|
||||
|
||||
// should add 'python' to languages + should not update completedDate
|
||||
expect(user3).toMatchObject({
|
||||
completedDailyCodingChallenges: [
|
||||
{
|
||||
id: dailyCodingChallengeId,
|
||||
completedDate,
|
||||
languages: [
|
||||
DailyCodingChallengeLanguage.javascript,
|
||||
DailyCodingChallengeLanguage.python
|
||||
]
|
||||
}
|
||||
],
|
||||
// should not add to progressTimestamps
|
||||
progressTimestamps: [completedDate]
|
||||
});
|
||||
|
||||
// should have correct response
|
||||
expect(res3.statusCode).toBe(200);
|
||||
expect(res3.body).toStrictEqual({
|
||||
alreadyCompleted: true,
|
||||
points: 1,
|
||||
completedDate,
|
||||
completedDailyCodingChallenges: [
|
||||
{
|
||||
id: dailyCodingChallengeId,
|
||||
completedDate,
|
||||
languages: [
|
||||
DailyCodingChallengeLanguage.javascript,
|
||||
DailyCodingChallengeLanguage.python
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /save-challenge', () => {
|
||||
describe('validation', () => {
|
||||
test('returns 400 status for unsavable challenges', async () => {
|
||||
@@ -1848,6 +2053,7 @@ describe('challengeRoutes', () => {
|
||||
{ path: '/project-completed', method: 'POST' },
|
||||
{ path: '/backend-challenge-completed', method: 'POST' },
|
||||
{ path: '/modern-challenge-completed', method: 'POST' },
|
||||
{ path: '/daily-coding-challenge-completed', method: 'POST' },
|
||||
{ path: '/save-challenge', method: 'POST' },
|
||||
{ path: '/exam/647e22d18acb466c97ccbef8', method: 'GET' },
|
||||
{ path: '/ms-trophy-challenge-completed', method: 'POST' },
|
||||
|
||||
@@ -3,6 +3,7 @@ import jwt from 'jsonwebtoken';
|
||||
import { uniqBy, matches } from 'lodash';
|
||||
import { CompletedExam, ExamResults } from '@prisma/client';
|
||||
import isURL from 'validator/lib/isURL';
|
||||
import type { FastifyInstance, FastifyReply } from 'fastify';
|
||||
|
||||
import { challengeTypes } from '../../../../shared/config/challenge-types';
|
||||
import * as schemas from '../../schemas';
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
canSubmitCodeRoadCertProject,
|
||||
verifyTrophyWithMicrosoft
|
||||
} from '../helpers/challenge-helpers';
|
||||
import { UpdateReqType } from '../../utils';
|
||||
|
||||
interface JwtPayload {
|
||||
userToken: string;
|
||||
@@ -415,6 +417,26 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
|
||||
}
|
||||
);
|
||||
|
||||
fastify.post(
|
||||
'/daily-coding-challenge-completed',
|
||||
{
|
||||
schema: schemas.dailyCodingChallengeCompleted,
|
||||
errorHandler(error, req, reply) {
|
||||
const logger = fastify.log.child({ req });
|
||||
if (error.validation) {
|
||||
logger.warn({ validationError: error.validation });
|
||||
void reply.code(400);
|
||||
void reply.send({
|
||||
type: 'error',
|
||||
message: 'That does not appear to be a valid challenge submission.'
|
||||
});
|
||||
fastify.errorHandler(error, req, reply);
|
||||
}
|
||||
}
|
||||
},
|
||||
postDailyCodingChallengeCompleted
|
||||
);
|
||||
|
||||
fastify.post(
|
||||
'/save-challenge',
|
||||
{
|
||||
@@ -949,3 +971,103 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
async function postDailyCodingChallengeCompleted(
|
||||
this: FastifyInstance,
|
||||
req: UpdateReqType<typeof schemas.dailyCodingChallengeCompleted>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const logger = this.log.child({ req });
|
||||
logger.info(`User ${req.user?.id} submitted a daily coding challenge`);
|
||||
|
||||
const { id, language } = req.body;
|
||||
|
||||
const user = await this.prisma.user.findUniqueOrThrow({
|
||||
where: { id: req.user?.id },
|
||||
select: {
|
||||
completedDailyCodingChallenges: true,
|
||||
progressTimestamps: true
|
||||
}
|
||||
});
|
||||
|
||||
const { completedDailyCodingChallenges, progressTimestamps = [] } = user;
|
||||
|
||||
const points = getPoints(progressTimestamps as ProgressTimestamp[]);
|
||||
const oldCompletedChallenge = completedDailyCodingChallenges.find(
|
||||
c => c.id === id
|
||||
);
|
||||
|
||||
const alreadyCompleted = !!oldCompletedChallenge;
|
||||
const languageAlreadyCompleted =
|
||||
oldCompletedChallenge?.languages.includes(language);
|
||||
|
||||
if (alreadyCompleted) {
|
||||
const { completedDate, languages } = oldCompletedChallenge;
|
||||
|
||||
if (languageAlreadyCompleted) {
|
||||
// alreadyCompleted && languageAlreadyCompleted, no need to change anything in the database
|
||||
return reply.send({
|
||||
alreadyCompleted,
|
||||
points,
|
||||
completedDate,
|
||||
completedDailyCodingChallenges
|
||||
});
|
||||
} else {
|
||||
// alreadyCompleted && !languageAlreadyCompleted, add the language to the record
|
||||
const { completedDailyCodingChallenges } = await this.prisma.user.update({
|
||||
where: { id: req.user?.id },
|
||||
select: {
|
||||
completedDailyCodingChallenges: true
|
||||
},
|
||||
data: {
|
||||
completedDailyCodingChallenges: {
|
||||
updateMany: {
|
||||
where: { id },
|
||||
data: {
|
||||
languages: [...new Set([...languages, language])]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return reply.send({
|
||||
alreadyCompleted,
|
||||
points,
|
||||
completedDate,
|
||||
completedDailyCodingChallenges
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// !alreadyCompleted, add new record for completed challenge
|
||||
const newCompletedDate = Date.now();
|
||||
|
||||
const newCompletedChallenge = {
|
||||
id,
|
||||
completedDate: newCompletedDate,
|
||||
languages: [language]
|
||||
};
|
||||
|
||||
const newCompletedChallenges = [
|
||||
...completedDailyCodingChallenges,
|
||||
newCompletedChallenge
|
||||
];
|
||||
|
||||
const newProgressTimestamps = Array.isArray(progressTimestamps)
|
||||
? [...progressTimestamps, newCompletedDate]
|
||||
: [newCompletedDate];
|
||||
|
||||
await this.prisma.user.update({
|
||||
where: { id: req.user?.id },
|
||||
data: {
|
||||
completedDailyCodingChallenges: newCompletedChallenges,
|
||||
progressTimestamps: newProgressTimestamps
|
||||
}
|
||||
});
|
||||
return reply.send({
|
||||
alreadyCompleted,
|
||||
points: points + 1,
|
||||
completedDate: newCompletedDate,
|
||||
completedDailyCodingChallenges: newCompletedChallenges
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import jwt, { JwtPayload } from 'jsonwebtoken';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { DailyCodingChallengeLanguage, type Prisma } from '@prisma/client';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import _ from 'lodash';
|
||||
|
||||
@@ -78,6 +78,16 @@ const testUserData: Prisma.userCreateInput = {
|
||||
}
|
||||
}
|
||||
],
|
||||
completedDailyCodingChallenges: [
|
||||
{
|
||||
id: '5900f36e1000cf542c50fe80',
|
||||
completedDate: 1742941672524,
|
||||
languages: [
|
||||
DailyCodingChallengeLanguage.python,
|
||||
DailyCodingChallengeLanguage.javascript
|
||||
]
|
||||
}
|
||||
],
|
||||
partiallyCompletedChallenges: [{ id: '123', completedDate: 123 }],
|
||||
completedExams: [],
|
||||
quizAttempts: [
|
||||
@@ -218,6 +228,16 @@ const publicUserData = {
|
||||
}
|
||||
}
|
||||
],
|
||||
completedDailyCodingChallenges: [
|
||||
{
|
||||
id: '5900f36e1000cf542c50fe80',
|
||||
completedDate: 1742941672524,
|
||||
languages: [
|
||||
DailyCodingChallengeLanguage.python,
|
||||
DailyCodingChallengeLanguage.javascript
|
||||
]
|
||||
}
|
||||
],
|
||||
completedExams: testUserData.completedExams,
|
||||
completedSurveys: [], // TODO: add surveys
|
||||
quizAttempts: testUserData.quizAttempts,
|
||||
@@ -290,6 +310,7 @@ const baseProgressData = {
|
||||
isRelationalDatabaseCertV8: false,
|
||||
isCollegeAlgebraPyCertV8: false,
|
||||
completedChallenges: [],
|
||||
completedDailyCodingChallenges: [],
|
||||
completedExams: [],
|
||||
savedChallenges: [],
|
||||
partiallyCompletedChallenges: [],
|
||||
@@ -735,6 +756,7 @@ describe('userRoutes', () => {
|
||||
// missing in the user document.
|
||||
currentChallengeId: '',
|
||||
completedChallenges: [],
|
||||
completedDailyCodingChallenges: [],
|
||||
completedExams: [],
|
||||
completedSurveys: [],
|
||||
partiallyCompletedChallenges: [],
|
||||
|
||||
@@ -505,6 +505,7 @@ export const userGetRoutes: FastifyPluginCallbackTypebox = (
|
||||
about: true,
|
||||
acceptedPrivacyTerms: true,
|
||||
completedChallenges: true,
|
||||
completedDailyCodingChallenges: true,
|
||||
completedExams: true,
|
||||
currentChallengeId: true,
|
||||
quizAttempts: true,
|
||||
@@ -589,6 +590,7 @@ export const userGetRoutes: FastifyPluginCallbackTypebox = (
|
||||
username,
|
||||
usernameDisplay,
|
||||
completedChallenges,
|
||||
completedDailyCodingChallenges,
|
||||
progressTimestamps,
|
||||
twitter,
|
||||
profileUI,
|
||||
@@ -607,6 +609,7 @@ export const userGetRoutes: FastifyPluginCallbackTypebox = (
|
||||
currentChallengeId: currentChallengeId ?? '',
|
||||
completedChallenges: normalizeChallenges(completedChallenges),
|
||||
completedChallengeCount: completedChallenges.length,
|
||||
completedDailyCodingChallenges,
|
||||
// This assertion is necessary until the database is normalized.
|
||||
calendar: getCalendar(
|
||||
progressTimestamps as ProgressTimestamp[] | null
|
||||
|
||||
@@ -6,6 +6,7 @@ export { backendChallengeCompleted } from './schemas/challenge/backend-challenge
|
||||
export { coderoadChallengeCompleted } from './schemas/challenge/coderoad-challenge-completed';
|
||||
export { exam } from './schemas/challenge/exam';
|
||||
export { examChallengeCompleted } from './schemas/challenge/exam-challenge-completed';
|
||||
export { dailyCodingChallengeCompleted } from './schemas/challenge/daily-coding-challenge-completed';
|
||||
export { modernChallengeCompleted } from './schemas/challenge/modern-challenge-completed';
|
||||
export { msTrophyChallengeCompleted } from './schemas/challenge/ms-trophy-challenge-completed';
|
||||
export { projectCompleted } from './schemas/challenge/project-completed';
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Type } from '@fastify/type-provider-typebox';
|
||||
import { DailyCodingChallengeLanguage } from '@prisma/client';
|
||||
|
||||
const languages = Object.values(DailyCodingChallengeLanguage).map(k =>
|
||||
Type.Literal(k)
|
||||
);
|
||||
|
||||
export const dailyCodingChallengeCompleted = {
|
||||
body: Type.Object({
|
||||
id: Type.String({ format: 'objectid', maxLength: 24, minLength: 24 }),
|
||||
language: Type.Union(languages)
|
||||
}),
|
||||
response: {
|
||||
200: Type.Object({
|
||||
completedDate: Type.Number(),
|
||||
points: Type.Number(),
|
||||
alreadyCompleted: Type.Boolean(),
|
||||
completedDailyCodingChallenges: Type.Array(
|
||||
Type.Object({
|
||||
id: Type.String(),
|
||||
completedDate: Type.Number(),
|
||||
languages: Type.Array(Type.Union(languages))
|
||||
})
|
||||
)
|
||||
}),
|
||||
400: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal(
|
||||
'That does not appear to be a valid challenge submission.'
|
||||
)
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Type } from '@fastify/type-provider-typebox';
|
||||
import { DailyCodingChallengeLanguage } from '@prisma/client';
|
||||
import { examResults, profileUI, savedChallenge } from '../types';
|
||||
|
||||
const languages = Object.values(DailyCodingChallengeLanguage).map(k =>
|
||||
Type.Literal(k)
|
||||
);
|
||||
|
||||
export const getSessionUser = {
|
||||
response: {
|
||||
200: Type.Object({
|
||||
@@ -53,6 +58,13 @@ export const getSessionUser = {
|
||||
})
|
||||
),
|
||||
completedChallengeCount: Type.Number(),
|
||||
completedDailyCodingChallenges: Type.Array(
|
||||
Type.Object({
|
||||
id: Type.String(),
|
||||
completedDate: Type.Number(),
|
||||
languages: Type.Array(Type.Union(languages))
|
||||
})
|
||||
),
|
||||
currentChallengeId: Type.String(),
|
||||
email: Type.String(),
|
||||
emailVerified: Type.Boolean(),
|
||||
|
||||
Reference in New Issue
Block a user