From 29cd2d227d182b2df51d01d60ddefcc2fbd2e55e Mon Sep 17 00:00:00 2001 From: Tom <20648924+moT01@users.noreply.github.com> Date: Thu, 17 Jul 2025 04:34:46 -0500 Subject: [PATCH] feat(api): daily challenge api (#61346) Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> --- api/package.json | 1 + api/prisma/schema.prisma | 27 ++ api/src/app.ts | 2 + api/src/daily-coding-challenge/README.md | 1 + .../routes/daily-coding-challenge.test.ts | 280 ++++++++++++++++++ .../routes/daily-coding-challenge.ts | 205 +++++++++++++ .../schemas/daily-coding-challenge.ts | 105 +++++++ .../daily-coding-challenge/schemas/index.ts | 1 + .../daily-coding-challenge/utils/helpers.ts | 39 +++ pnpm-lock.yaml | 3 + tools/daily-challenges/helpers.ts | 18 +- .../daily-challenges/seed-daily-challenges.ts | 17 +- tools/daily-challenges/types.ts | 1 - 13 files changed, 678 insertions(+), 22 deletions(-) create mode 100644 api/src/daily-coding-challenge/README.md create mode 100644 api/src/daily-coding-challenge/routes/daily-coding-challenge.test.ts create mode 100644 api/src/daily-coding-challenge/routes/daily-coding-challenge.ts create mode 100644 api/src/daily-coding-challenge/schemas/daily-coding-challenge.ts create mode 100644 api/src/daily-coding-challenge/schemas/index.ts create mode 100644 api/src/daily-coding-challenge/utils/helpers.ts diff --git a/api/package.json b/api/package.json index 19e17cc4b18..a5c084f9864 100644 --- a/api/package.json +++ b/api/package.json @@ -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", diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 5c69ba6f203..aeb50cf47ff 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -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 diff --git a/api/src/app.ts b/api/src/app.ts index dddccbed794..953f917fc06 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -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; }; diff --git a/api/src/daily-coding-challenge/README.md b/api/src/daily-coding-challenge/README.md new file mode 100644 index 00000000000..1dfc547c9e3 --- /dev/null +++ b/api/src/daily-coding-challenge/README.md @@ -0,0 +1 @@ +Endpoints to get daily coding challenge info. Daily challenge submission still lives in the main part of the API. diff --git a/api/src/daily-coding-challenge/routes/daily-coding-challenge.test.ts b/api/src/daily-coding-challenge/routes/daily-coding-challenge.test.ts new file mode 100644 index 00000000000..f9666a2abb4 --- /dev/null +++ b/api/src/daily-coding-challenge/routes/daily-coding-challenge.test.ts @@ -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.' + }); + }); + }); +}); diff --git a/api/src/daily-coding-challenge/routes/daily-coding-challenge.ts b/api/src/daily-coding-challenge/routes/daily-coding-challenge.ts new file mode 100644 index 00000000000..07c05759ebd --- /dev/null +++ b/api/src/daily-coding-challenge/routes/daily-coding-challenge.ts @@ -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(); +}; diff --git a/api/src/daily-coding-challenge/schemas/daily-coding-challenge.ts b/api/src/daily-coding-challenge/schemas/daily-coding-challenge.ts new file mode 100644 index 00000000000..b5e52e9dc52 --- /dev/null +++ b/api/src/daily-coding-challenge/schemas/daily-coding-challenge.ts @@ -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 +}; diff --git a/api/src/daily-coding-challenge/schemas/index.ts b/api/src/daily-coding-challenge/schemas/index.ts new file mode 100644 index 00000000000..cd034dec6ac --- /dev/null +++ b/api/src/daily-coding-challenge/schemas/index.ts @@ -0,0 +1 @@ +export { dailyCodingChallenge } from './daily-coding-challenge'; diff --git a/api/src/daily-coding-challenge/utils/helpers.ts b/api/src/daily-coding-challenge/utils/helpers.ts new file mode 100644 index 00000000000..a5dccc65040 --- /dev/null +++ b/api/src/daily-coding-challenge/utils/helpers.ts @@ -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)); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a13a2a9b50..13f055d84be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/tools/daily-challenges/helpers.ts b/tools/daily-challenges/helpers.ts index 88537ef00e9..0c0c1c03670 100644 --- a/tools/daily-challenges/helpers.ts +++ b/tools/daily-challenges/helpers.ts @@ -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
that our parser adds. +// Remove the
that our parser adds. export function removeSection(str: string) { return str - .replace(/^
\n?/, '') + .replace(/^
\n?/, '') .replace(/\n?<\/section>$/, ''); } diff --git a/tools/daily-challenges/seed-daily-challenges.ts b/tools/daily-challenges/seed-daily-challenges.ts index ee620bed5f1..a865225900d 100644 --- a/tools/daily-challenges/seed-daily-challenges.ts +++ b/tools/daily-challenges/seed-daily-challenges.ts @@ -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' ); } }; diff --git a/tools/daily-challenges/types.ts b/tools/daily-challenges/types.ts index eba7c1c1c06..c69abdb4749 100644 --- a/tools/daily-challenges/types.ts +++ b/tools/daily-challenges/types.ts @@ -15,7 +15,6 @@ export type Challenge = { title: string; date: Date; description: string; - instructions?: string; fields: { tests: { testString: string;