mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-30 03:03:06 -05:00
feat(api): daily challenge api (#61346)
Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
"ajv": "8.12.0",
|
||||
"ajv-formats": "2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"date-fns-tz": "3.2.0",
|
||||
"dotenv": "16.4.5",
|
||||
"fast-uri": "2.3.0",
|
||||
"fastify": "5.2.0",
|
||||
|
||||
@@ -491,6 +491,33 @@ type SurveyResponse {
|
||||
|
||||
// ----------------------
|
||||
|
||||
model DailyCodingChallenges {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
challengeNumber Int
|
||||
date DateTime
|
||||
title String
|
||||
description String
|
||||
javascript DailyCodingChallengeApiLanguage
|
||||
python DailyCodingChallengeApiLanguage
|
||||
}
|
||||
|
||||
type DailyCodingChallengeApiLanguage {
|
||||
tests DailyCodingChallengeApiLanguageTests[]
|
||||
challengeFiles DailyCodingChallengeApiLanguageChallengeFiles[]
|
||||
}
|
||||
|
||||
type DailyCodingChallengeApiLanguageTests {
|
||||
text String
|
||||
testString String
|
||||
}
|
||||
|
||||
type DailyCodingChallengeApiLanguageChallengeFiles {
|
||||
contents String
|
||||
fileKey String
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
|
||||
model ExamEnvironmentExamModeration {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
/// Whether or not the item is approved
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
examEnvironmentOpenRoutes,
|
||||
examEnvironmentValidatedTokenRoutes
|
||||
} from './exam-environment/routes/exam-environment';
|
||||
import { dailyCodingChallengeRoutes } from './daily-coding-challenge/routes/daily-coding-challenge';
|
||||
|
||||
type FastifyInstanceWithTypeProvider = FastifyInstance<
|
||||
RawServerDefault,
|
||||
@@ -231,6 +232,7 @@ export const build = async (
|
||||
void fastify.register(publicRoutes.deprecatedEndpoints);
|
||||
void fastify.register(publicRoutes.statusRoute);
|
||||
void fastify.register(publicRoutes.unsubscribeDeprecated);
|
||||
void fastify.register(dailyCodingChallengeRoutes);
|
||||
|
||||
return fastify;
|
||||
};
|
||||
|
||||
1
api/src/daily-coding-challenge/README.md
Normal file
1
api/src/daily-coding-challenge/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Endpoints to get daily coding challenge info. Daily challenge submission still lives in the main part of the API.
|
||||
@@ -0,0 +1,280 @@
|
||||
import { addDays } from 'date-fns';
|
||||
|
||||
import { setupServer, superRequest } from '../../../jest.utils';
|
||||
import { getNowUsCentral, getUtcMidnight } from '../utils/helpers';
|
||||
|
||||
function dateToDateParam(date: Date): string {
|
||||
return date.toISOString().split('T')[0] as string;
|
||||
}
|
||||
|
||||
const todayUsCentral = getNowUsCentral();
|
||||
const todayUtcMidnight = getUtcMidnight(todayUsCentral);
|
||||
const todayDateParam = dateToDateParam(todayUtcMidnight);
|
||||
|
||||
const yesterdayUsCentral = addDays(todayUsCentral, -1);
|
||||
const yesterdayUtcMidnight = getUtcMidnight(yesterdayUsCentral);
|
||||
|
||||
const twoDaysAgoUsCentral = addDays(todayUsCentral, -2);
|
||||
const twoDaysAgoUtcMidnight = getUtcMidnight(twoDaysAgoUsCentral);
|
||||
const twoDaysAgoDateParam = dateToDateParam(twoDaysAgoUtcMidnight);
|
||||
|
||||
const tomorrowUsCentral = addDays(todayUsCentral, 1);
|
||||
const tomorrowUtcMidnight = getUtcMidnight(tomorrowUsCentral);
|
||||
const tomorrowDateParam = dateToDateParam(tomorrowUtcMidnight);
|
||||
|
||||
const yesterdaysChallenge = {
|
||||
id: '111111111111111111111111',
|
||||
challengeNumber: 1,
|
||||
date: yesterdayUtcMidnight,
|
||||
title: "Yesterday's Challenge",
|
||||
description: "Yesterday's Description",
|
||||
javascript: {
|
||||
tests: [{ text: 'JS Test Yesterday', testString: 'jsTestYesterday()' }],
|
||||
challengeFiles: [{ contents: 'JS Files Yesterday', fileKey: 'scriptjs' }]
|
||||
},
|
||||
python: {
|
||||
tests: [{ text: 'Py Test Yesterday', testString: 'py_test_yesterday()' }],
|
||||
challengeFiles: [{ contents: 'Py Files Yesterday', fileKey: 'mainpy' }]
|
||||
}
|
||||
};
|
||||
|
||||
const todaysChallenge = {
|
||||
id: '222222222222222222222222',
|
||||
challengeNumber: 2,
|
||||
date: todayUtcMidnight,
|
||||
title: "Today's Challenge",
|
||||
description: "Today's Description",
|
||||
javascript: {
|
||||
tests: [{ text: 'JS Test Today', testString: 'jsTestToday()' }],
|
||||
challengeFiles: [{ contents: 'JS Files Today', fileKey: 'scriptjs' }]
|
||||
},
|
||||
python: {
|
||||
tests: [{ text: 'Py Test Today', testString: 'py_test_today()' }],
|
||||
challengeFiles: [{ contents: 'Py Files Today', fileKey: 'mainpy' }]
|
||||
}
|
||||
};
|
||||
|
||||
const tomorrowsChallenge = {
|
||||
id: '333333333333333333333333',
|
||||
challengeNumber: 3,
|
||||
date: tomorrowUtcMidnight,
|
||||
title: "Tomorrow's Challenge",
|
||||
description: "Tomorrow's Description",
|
||||
javascript: {
|
||||
tests: [{ text: 'JS Test Tomorrow', testString: 'jsTestTomorrow()' }],
|
||||
challengeFiles: [{ contents: 'JS Files Tomorrow', fileKey: 'scriptjs' }]
|
||||
},
|
||||
python: {
|
||||
tests: [{ text: 'Py Test Tomorrow', testString: 'py_test_tomorrow()' }],
|
||||
challengeFiles: [{ contents: 'Py Files Tomorrow', fileKey: 'mainpy' }]
|
||||
}
|
||||
};
|
||||
|
||||
const mockChallenges = [
|
||||
tomorrowsChallenge,
|
||||
todaysChallenge,
|
||||
yesterdaysChallenge
|
||||
];
|
||||
|
||||
describe('/daily-coding-challenge', () => {
|
||||
setupServer();
|
||||
|
||||
describe('GET /daily-coding-challenge/date/:date', () => {
|
||||
beforeEach(async () => {
|
||||
await fastifyTestInstance.prisma.dailyCodingChallenges.createMany({
|
||||
data: mockChallenges
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fastifyTestInstance.prisma.dailyCodingChallenges.deleteMany();
|
||||
});
|
||||
|
||||
it('should return 400 for an invalid date format', async () => {
|
||||
const res = await superRequest(
|
||||
'/daily-coding-challenge/date/invalid-format',
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
).send({});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body).toEqual({
|
||||
type: 'error',
|
||||
message: 'Invalid date format. Please use YYYY-MM-DD.'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 for a date without a challenge', async () => {
|
||||
const res = await superRequest(
|
||||
`/daily-coding-challenge/date/${twoDaysAgoDateParam}`,
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
).send({});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body).toEqual({
|
||||
type: 'error',
|
||||
message: 'Challenge not found.'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a challenge for a valid date', async () => {
|
||||
const res = await superRequest(
|
||||
`/daily-coding-challenge/date/${todayDateParam}`,
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
).send({});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toMatchObject({
|
||||
...todaysChallenge,
|
||||
date: todaysChallenge.date.toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return a challenge for a future date relative to US Central', async () => {
|
||||
const res = await superRequest(
|
||||
`/daily-coding-challenge/date/${tomorrowDateParam}`,
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
).send({});
|
||||
expect(res.body).toEqual({
|
||||
type: 'error',
|
||||
message: 'Challenge not found.'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /daily-coding-challenge/today', () => {
|
||||
beforeEach(async () => {
|
||||
await fastifyTestInstance.prisma.dailyCodingChallenges.createMany({
|
||||
data: mockChallenges
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fastifyTestInstance.prisma.dailyCodingChallenges.deleteMany();
|
||||
});
|
||||
|
||||
it("should return today's challenge", async () => {
|
||||
const res = await superRequest('/daily-coding-challenge/today', {
|
||||
method: 'GET'
|
||||
}).send({});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toMatchObject({
|
||||
...todaysChallenge,
|
||||
date: todaysChallenge.date.toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 when no challenge exists for today', async () => {
|
||||
await fastifyTestInstance.prisma.dailyCodingChallenges.deleteMany();
|
||||
|
||||
const res = await superRequest('/daily-coding-challenge/today', {
|
||||
method: 'GET'
|
||||
}).send({});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body).toEqual({
|
||||
type: 'error',
|
||||
message: 'Challenge not found.'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /daily-coding-challenge/all', () => {
|
||||
beforeEach(async () => {
|
||||
await fastifyTestInstance.prisma.dailyCodingChallenges.createMany({
|
||||
data: mockChallenges
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fastifyTestInstance.prisma.dailyCodingChallenges.deleteMany();
|
||||
});
|
||||
|
||||
it('should return { _id, date, challengeNumber, title } for all challenges up to today US Central', async () => {
|
||||
const res = await superRequest('/daily-coding-challenge/all', {
|
||||
method: 'GET'
|
||||
}).send({});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(Array.isArray(res.body)).toBe(true);
|
||||
|
||||
// Should include yesterday's and today's challenges, but not tomorrow's
|
||||
const expectedResponse = [
|
||||
{
|
||||
id: todaysChallenge.id,
|
||||
challengeNumber: todaysChallenge.challengeNumber,
|
||||
date: todaysChallenge.date.toISOString(),
|
||||
title: todaysChallenge.title
|
||||
},
|
||||
{
|
||||
id: yesterdaysChallenge.id,
|
||||
challengeNumber: yesterdaysChallenge.challengeNumber,
|
||||
date: yesterdaysChallenge.date.toISOString(),
|
||||
title: yesterdaysChallenge.title
|
||||
}
|
||||
];
|
||||
|
||||
expect(res.body).toHaveLength(2);
|
||||
expect(res.body).toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should return 404 when no challenges exist', async () => {
|
||||
await fastifyTestInstance.prisma.dailyCodingChallenges.deleteMany();
|
||||
|
||||
const res = await superRequest('/daily-coding-challenge/all', {
|
||||
method: 'GET'
|
||||
}).send({});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body).toEqual({
|
||||
type: 'error',
|
||||
message: 'No challenges found.'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /daily-coding-challenge/newest', () => {
|
||||
beforeEach(async () => {
|
||||
await fastifyTestInstance.prisma.dailyCodingChallenges.createMany({
|
||||
data: [yesterdaysChallenge, todaysChallenge, tomorrowsChallenge]
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fastifyTestInstance.prisma.dailyCodingChallenges.deleteMany();
|
||||
});
|
||||
|
||||
it('should return { date } of the newest challenge in the database', async () => {
|
||||
const res = await superRequest('/daily-coding-challenge/newest', {
|
||||
method: 'GET'
|
||||
}).send({});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual({
|
||||
date: tomorrowsChallenge.date.toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 when no challenges exist', async () => {
|
||||
await fastifyTestInstance.prisma.dailyCodingChallenges.deleteMany();
|
||||
|
||||
const res = await superRequest('/daily-coding-challenge/newest', {
|
||||
method: 'GET'
|
||||
}).send({});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body).toEqual({
|
||||
type: 'error',
|
||||
message: 'No challenges found.'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
205
api/src/daily-coding-challenge/routes/daily-coding-challenge.ts
Normal file
205
api/src/daily-coding-challenge/routes/daily-coding-challenge.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox';
|
||||
|
||||
import * as schemas from '../schemas';
|
||||
import {
|
||||
getNowUsCentral,
|
||||
getUtcMidnight,
|
||||
dateStringToUtcMidnight
|
||||
} from '../utils/helpers';
|
||||
|
||||
/**
|
||||
* Plugin containing public GET routes for the daily coding challenges.
|
||||
* Note that they are only for getting challenge info, challenges are still
|
||||
* submitted via the main challenge completion routes.
|
||||
*
|
||||
* @param fastify The Fastify instance.
|
||||
* @param _options Options passed to the plugin via `fastify.register(plugin, options)`.
|
||||
* @param done Callback to signal that the logic has completed.
|
||||
*/
|
||||
export const dailyCodingChallengeRoutes: FastifyPluginCallbackTypebox = (
|
||||
fastify,
|
||||
_options,
|
||||
done
|
||||
) => {
|
||||
fastify.get(
|
||||
'/daily-coding-challenge/date/:date',
|
||||
{
|
||||
schema: schemas.dailyCodingChallenge.date
|
||||
},
|
||||
async (req, reply) => {
|
||||
const logger = fastify.log.child({ req, res: reply });
|
||||
logger.info('Received request for daily coding challenge', {
|
||||
date: req.params.date
|
||||
});
|
||||
|
||||
const { date } = req.params;
|
||||
|
||||
try {
|
||||
const parsedDate = dateStringToUtcMidnight(date);
|
||||
|
||||
if (!parsedDate) {
|
||||
logger.warn('Invalid date format requested', { date });
|
||||
return reply.status(400).send({
|
||||
type: 'error',
|
||||
message: 'Invalid date format. Please use YYYY-MM-DD.'
|
||||
});
|
||||
}
|
||||
|
||||
const challenge = await fastify.prisma.dailyCodingChallenges.findFirst({
|
||||
where: {
|
||||
date: parsedDate
|
||||
}
|
||||
});
|
||||
|
||||
// don't return challenges > today US Central
|
||||
if (!challenge || challenge.date > getUtcMidnight(getNowUsCentral())) {
|
||||
logger.warn('Challenge not found for date', { date: parsedDate });
|
||||
return reply
|
||||
.status(404)
|
||||
.send({ type: 'error', message: 'Challenge not found.' });
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
...challenge,
|
||||
date: challenge.date.toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error, 'Failed to get daily coding challenge.');
|
||||
await reply
|
||||
.status(500)
|
||||
.send({ type: 'error', message: 'Internal server error.' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
'/daily-coding-challenge/today',
|
||||
{
|
||||
schema: schemas.dailyCodingChallenge.today
|
||||
},
|
||||
async (req, reply) => {
|
||||
const logger = fastify.log.child({ req, res: reply });
|
||||
logger.info("Received request for today's daily coding challenge");
|
||||
|
||||
const today = getUtcMidnight(getNowUsCentral());
|
||||
|
||||
try {
|
||||
const todaysChallenge =
|
||||
await fastify.prisma.dailyCodingChallenges.findFirst({
|
||||
where: {
|
||||
date: today
|
||||
}
|
||||
});
|
||||
|
||||
if (!todaysChallenge) {
|
||||
logger.warn('Challenge not found for today', { date: today });
|
||||
return reply
|
||||
.status(404)
|
||||
.send({ type: 'error', message: 'Challenge not found.' });
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
...todaysChallenge,
|
||||
date: todaysChallenge.date.toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to get today's daily coding challenge.");
|
||||
await reply
|
||||
.status(500)
|
||||
.send({ type: 'error', message: 'Internal server error.' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
'/daily-coding-challenge/all',
|
||||
{
|
||||
schema: schemas.dailyCodingChallenge.all
|
||||
},
|
||||
async (req, reply) => {
|
||||
const logger = fastify.log.child({ req, res: reply });
|
||||
logger.info('Received request for all daily coding challenges');
|
||||
|
||||
const today = getUtcMidnight(getNowUsCentral());
|
||||
|
||||
try {
|
||||
const allChallenges =
|
||||
await fastify.prisma.dailyCodingChallenges.findMany({
|
||||
// only where date <= today US Central
|
||||
where: {
|
||||
date: {
|
||||
lte: today
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
date: 'desc'
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
challengeNumber: true,
|
||||
date: true,
|
||||
title: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!allChallenges || allChallenges.length === 0) {
|
||||
logger.warn('No challenges found.', { date: today });
|
||||
return reply
|
||||
.status(404)
|
||||
.send({ type: 'error', message: 'No challenges found.' });
|
||||
}
|
||||
|
||||
const response = allChallenges.map(challenge => ({
|
||||
...challenge,
|
||||
date: challenge.date.toISOString()
|
||||
}));
|
||||
|
||||
return reply.send(response);
|
||||
} catch (error) {
|
||||
logger.error(error, 'Failed to get all daily coding challenges.');
|
||||
await reply
|
||||
.status(500)
|
||||
.send({ type: 'error', message: 'Internal server error.' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
'/daily-coding-challenge/newest',
|
||||
{
|
||||
schema: schemas.dailyCodingChallenge.newest
|
||||
},
|
||||
async (req, reply) => {
|
||||
const logger = fastify.log.child({ req, res: reply });
|
||||
logger.info('Received request for newest daily coding challenge');
|
||||
|
||||
try {
|
||||
const newestChallenge =
|
||||
await fastify.prisma.dailyCodingChallenges.findFirst({
|
||||
orderBy: {
|
||||
date: 'desc'
|
||||
},
|
||||
select: {
|
||||
date: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!newestChallenge) {
|
||||
logger.warn('No challenges found.');
|
||||
return reply
|
||||
.status(404)
|
||||
.send({ type: 'error', message: 'No challenges found.' });
|
||||
}
|
||||
|
||||
return reply.send({ date: newestChallenge.date.toISOString() });
|
||||
} catch (error) {
|
||||
logger.error(error, 'Failed to get newest daily coding challenge.');
|
||||
await reply
|
||||
.status(500)
|
||||
.send({ type: 'error', message: 'Internal server error.' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
done();
|
||||
};
|
||||
105
api/src/daily-coding-challenge/schemas/daily-coding-challenge.ts
Normal file
105
api/src/daily-coding-challenge/schemas/daily-coding-challenge.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Type } from '@fastify/type-provider-typebox';
|
||||
|
||||
const challengeLanguage = Type.Object({
|
||||
tests: Type.Array(
|
||||
Type.Object({
|
||||
text: Type.String(),
|
||||
testString: Type.String()
|
||||
})
|
||||
),
|
||||
challengeFiles: Type.Array(
|
||||
Type.Object({
|
||||
contents: Type.String(),
|
||||
fileKey: Type.String()
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
const singleChallenge = Type.Object({
|
||||
id: Type.String({ format: 'objectid', maxLength: 24, minLength: 24 }),
|
||||
date: Type.String({ format: 'date-time' }),
|
||||
challengeNumber: Type.Number(),
|
||||
title: Type.String(),
|
||||
description: Type.String(),
|
||||
javascript: challengeLanguage,
|
||||
python: challengeLanguage
|
||||
});
|
||||
|
||||
const date = {
|
||||
params: Type.Object({
|
||||
date: Type.String({ format: 'date' })
|
||||
}),
|
||||
response: {
|
||||
200: singleChallenge,
|
||||
400: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal('Invalid date format. Please use YYYY-MM-DD.')
|
||||
}),
|
||||
404: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal('Challenge not found.')
|
||||
}),
|
||||
500: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal('Internal server error.')
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const today = {
|
||||
response: {
|
||||
200: singleChallenge,
|
||||
404: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal('Challenge not found.')
|
||||
}),
|
||||
500: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal('Internal server error.')
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const all = {
|
||||
response: {
|
||||
200: Type.Array(
|
||||
Type.Object({
|
||||
id: Type.String(),
|
||||
date: Type.String({ format: 'date-time' }),
|
||||
challengeNumber: Type.Number(),
|
||||
title: Type.String()
|
||||
})
|
||||
),
|
||||
404: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal('No challenges found.')
|
||||
}),
|
||||
500: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal('Internal server error.')
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const newest = {
|
||||
response: {
|
||||
200: Type.Object({
|
||||
date: Type.String({ format: 'date-time' })
|
||||
}),
|
||||
404: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal('No challenges found.')
|
||||
}),
|
||||
500: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal('Internal server error.')
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
export const dailyCodingChallenge = {
|
||||
date,
|
||||
today,
|
||||
newest,
|
||||
all
|
||||
};
|
||||
1
api/src/daily-coding-challenge/schemas/index.ts
Normal file
1
api/src/daily-coding-challenge/schemas/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { dailyCodingChallenge } from './daily-coding-challenge';
|
||||
39
api/src/daily-coding-challenge/utils/helpers.ts
Normal file
39
api/src/daily-coding-challenge/utils/helpers.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { toZonedTime } from 'date-fns-tz';
|
||||
|
||||
/**
|
||||
* @returns Now US Central time.
|
||||
*/
|
||||
export function getNowUsCentral() {
|
||||
return toZonedTime(new Date(), 'America/Chicago');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Date object set to UTC midnight of the given date.
|
||||
* @param date - Date Object.
|
||||
* @returns UTC midnight of the given date.
|
||||
*/
|
||||
export function getUtcMidnight(date: Date): Date {
|
||||
return new Date(
|
||||
Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a date string in the format "YYYY-MM-DD" and returns a Date object set to UTC midnight.
|
||||
* Returns null if the input is not in the correct format.
|
||||
* @param dateStr - Date string in "YYYY-MM-DD" format.
|
||||
* @returns Date object set to UTC midnight or null if invalid.
|
||||
*/
|
||||
export function dateStringToUtcMidnight(dateStr: string): Date | null {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [year, month, day] = dateStr.split('-').map(Number) as [
|
||||
number,
|
||||
number,
|
||||
number
|
||||
];
|
||||
|
||||
return new Date(Date.UTC(year, month - 1, day));
|
||||
}
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -201,6 +201,9 @@ importers:
|
||||
date-fns:
|
||||
specifier: 4.1.0
|
||||
version: 4.1.0
|
||||
date-fns-tz:
|
||||
specifier: 3.2.0
|
||||
version: 3.2.0(date-fns@4.1.0)
|
||||
dotenv:
|
||||
specifier: 16.4.5
|
||||
version: 16.4.5
|
||||
|
||||
@@ -37,7 +37,6 @@ query {
|
||||
id
|
||||
title
|
||||
description
|
||||
instructions
|
||||
fields {
|
||||
tests {
|
||||
testString
|
||||
@@ -79,7 +78,6 @@ export function combineChallenges({
|
||||
id: jsId,
|
||||
title: jsTitle,
|
||||
description: jsDescription,
|
||||
instructions: jsInstructions,
|
||||
fields: { tests: jsTests },
|
||||
challengeFiles: jsChallengeFiles
|
||||
} = jsChallenge;
|
||||
@@ -87,7 +85,6 @@ export function combineChallenges({
|
||||
const {
|
||||
title: pyTitle,
|
||||
description: pyDescription,
|
||||
instructions: pyInstructions,
|
||||
fields: { tests: pyTests },
|
||||
challengeFiles: pyChallengeFiles
|
||||
} = pyChallenge;
|
||||
@@ -104,12 +101,6 @@ export function combineChallenges({
|
||||
);
|
||||
}
|
||||
|
||||
if (jsInstructions !== pyInstructions) {
|
||||
throw new Error(
|
||||
`JavaScript and Python instructions do not match for challenge ${challengeNumber}`
|
||||
);
|
||||
}
|
||||
|
||||
if (jsTests.length !== pyTests.length) {
|
||||
throw new Error(
|
||||
`JavaScript and Python do not have the same number of tests for challenge ${challengeNumber}: ${jsTests.length} JavaScript vs ${pyTests.length} Python tests`
|
||||
@@ -118,15 +109,12 @@ export function combineChallenges({
|
||||
|
||||
// Use the JS challenge info for the new challenge meta - e.g. id, title, description, etc
|
||||
const challengeData = {
|
||||
// **DO NOT CHANEGE THE ID** it's used as the challenge ID - and what gets added to completedDailyCodingChallenges[]
|
||||
// **DO NOT CHANGE THE ID** it's used as the challenge ID - and what gets added to completedDailyCodingChallenges[]
|
||||
_id: new ObjectId(`${jsId}`),
|
||||
challengeNumber,
|
||||
title: jsTitle.replace(`JavaScript Challenge ${challengeNumber}: `, ''),
|
||||
date,
|
||||
description: removeSection(jsDescription),
|
||||
...(jsInstructions && {
|
||||
instructions: removeSection(jsInstructions)
|
||||
}),
|
||||
javascript: {
|
||||
tests: jsTests,
|
||||
challengeFiles: jsChallengeFiles
|
||||
@@ -154,9 +142,9 @@ export function handleError(err: Error, client: MongoClient) {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the <section id="description/instructions"> that our parser adds.
|
||||
// Remove the <section id="description"> that our parser adds.
|
||||
export function removeSection(str: string) {
|
||||
return str
|
||||
.replace(/^<section id="(description|instructions)">\n?/, '')
|
||||
.replace(/^<section id="description">\n?/, '')
|
||||
.replace(/\n?<\/section>$/, '');
|
||||
}
|
||||
|
||||
@@ -16,16 +16,16 @@ const { MONGOHQ_URL } = process.env;
|
||||
const EXPECTED_CHALLENGE_COUNT = 24;
|
||||
|
||||
// Date to set for the first challenge, second challenge will be one day later, etc...
|
||||
// **DO NOT CHANGE THIS AFTER RELEASE**
|
||||
// **DO NOT CHANGE THIS AFTER RELEASE (if seeding production - okay for local dev)**
|
||||
const year = 2025;
|
||||
const monthIndex = 5; // 0-indexed -> 5 = June
|
||||
const day = 10;
|
||||
const monthIndex = 6; // 0-indexed -> 5 = June
|
||||
const day = 15;
|
||||
const START_DATE = new Date(Date.UTC(year, monthIndex, day));
|
||||
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
// Sanity check to make sure the start date hasn't unintentionally changed
|
||||
// **IT SHOULD NOT CHANGE AFTER RELEASE**
|
||||
const startDateString = '2025-06-10T00:00:00.000Z';
|
||||
const startDateString = '2025-07-15T00:00:00.000Z';
|
||||
if (START_DATE.toISOString() !== startDateString) {
|
||||
throw new Error(
|
||||
`It appears the start date has changed from "${startDateString}".
|
||||
@@ -99,8 +99,13 @@ const seed = async () => {
|
||||
const count = await dailyCodingChallenges.countDocuments();
|
||||
|
||||
if (count !== EXPECTED_CHALLENGE_COUNT) {
|
||||
throw new Error(
|
||||
`Expected ${EXPECTED_CHALLENGE_COUNT} challenges in the database, but found ${count} documents`
|
||||
console.warn(
|
||||
'\n********** WARNING *********\n' +
|
||||
'*\n' +
|
||||
`* Expected ${EXPECTED_CHALLENGE_COUNT} challenges in the database,\n` +
|
||||
`* but found ${count} documents\n` +
|
||||
'*\n' +
|
||||
'********** WARNING *********\n'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,7 +15,6 @@ export type Challenge = {
|
||||
title: string;
|
||||
date: Date;
|
||||
description: string;
|
||||
instructions?: string;
|
||||
fields: {
|
||||
tests: {
|
||||
testString: string;
|
||||
|
||||
Reference in New Issue
Block a user