chore: import upstream snapshot with attribution
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
/* eslint-disable filenames-simple/naming-convention */
|
||||
import { createLintStagedConfig } from '@freecodecamp/eslint-config/lintstaged';
|
||||
|
||||
export default createLintStagedConfig(import.meta.dirname);
|
||||
@@ -0,0 +1,9 @@
|
||||
Script to seed the daily challenges. Used to seed challenges for local or production databases.
|
||||
|
||||
To run:
|
||||
|
||||
Copy the `sample.env` to `.env`,
|
||||
Make sure dependencies are installed,
|
||||
Run the main client with upcoming changes shown - this is so the script can get the challenges from GraphQL,
|
||||
`cd tools/daily-challenges` to go into the `daily-challenges` folder,
|
||||
Run `pnpm seed-daily-challenges` to seed the challenges from the "Dev Playground" superblock to a `DailyCodingChallenges` collection in the `freecodecamp` database.
|
||||
@@ -0,0 +1,3 @@
|
||||
import { configTypeChecked } from '@freecodecamp/eslint-config/base';
|
||||
|
||||
export default configTypeChecked;
|
||||
@@ -0,0 +1,148 @@
|
||||
import { MongoClient, ObjectId } from 'mongodb';
|
||||
import { Challenge, QueryResult } from './types';
|
||||
|
||||
const GRAPHQL_ENDPOINT = 'http://localhost:8000/___graphql';
|
||||
|
||||
// Query graphQL - note that the main client needs to be running to query the challenges
|
||||
export async function queryGraphQL(query: string) {
|
||||
const response = await fetch(GRAPHQL_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ query })
|
||||
});
|
||||
|
||||
const json = (await response.json()) as QueryResult;
|
||||
|
||||
if (!json?.data?.allChallengeNode?.edges?.length) {
|
||||
throw new Error(
|
||||
'Failed to find any challenges with GraphQL query. The client needs to be running'
|
||||
);
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
export async function fetchChallenges(language: 'javascript' | 'python') {
|
||||
const query = `
|
||||
query {
|
||||
allChallengeNode(
|
||||
filter: {challenge: {superBlock: {eq: "dev-playground"}, block: {eq: "daily-coding-challenges-${language}"}}}
|
||||
sort: {challenge: {challengeOrder: ASC}}
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
challenge {
|
||||
id
|
||||
title
|
||||
description
|
||||
tests {
|
||||
testString
|
||||
text
|
||||
}
|
||||
challengeFiles {
|
||||
contents
|
||||
fileKey
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const queryRes = await queryGraphQL(query);
|
||||
|
||||
const challenges = queryRes.data.allChallengeNode.edges.map(
|
||||
({ node }) => node.challenge
|
||||
);
|
||||
|
||||
return challenges;
|
||||
}
|
||||
|
||||
export function combineChallenges({
|
||||
jsChallenge,
|
||||
pyChallenge,
|
||||
challengeNumber,
|
||||
date
|
||||
}: {
|
||||
jsChallenge: Challenge;
|
||||
pyChallenge: Challenge;
|
||||
challengeNumber: number;
|
||||
date: Date;
|
||||
}) {
|
||||
const {
|
||||
id: jsId,
|
||||
title: jsTitle,
|
||||
description: jsDescription,
|
||||
tests: jsTests,
|
||||
challengeFiles: jsChallengeFiles
|
||||
} = jsChallenge;
|
||||
|
||||
const {
|
||||
title: pyTitle,
|
||||
description: pyDescription,
|
||||
tests: pyTests,
|
||||
challengeFiles: pyChallengeFiles
|
||||
} = pyChallenge;
|
||||
|
||||
if (jsTitle !== pyTitle) {
|
||||
throw new Error(
|
||||
`JavaScript and Python titles do not match for challenge ${challengeNumber}: "${jsTitle}" vs "${pyTitle}"`
|
||||
);
|
||||
}
|
||||
|
||||
if (jsDescription !== pyDescription) {
|
||||
throw new Error(
|
||||
`JavaScript and Python descriptions 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`
|
||||
);
|
||||
}
|
||||
|
||||
// Use the JS challenge info for the new challenge meta - e.g. id, title, description, etc
|
||||
const challengeData = {
|
||||
// **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(`Challenge ${challengeNumber}: `, ''),
|
||||
date,
|
||||
description: removeSection(jsDescription),
|
||||
javascript: {
|
||||
tests: jsTests,
|
||||
challengeFiles: jsChallengeFiles
|
||||
},
|
||||
python: {
|
||||
tests: pyTests,
|
||||
challengeFiles: pyChallengeFiles
|
||||
}
|
||||
};
|
||||
|
||||
return challengeData;
|
||||
}
|
||||
|
||||
export async function handleError(err: unknown, client: MongoClient) {
|
||||
if (err) {
|
||||
console.error('Oh noes!! Error seeding Daily Challenges.');
|
||||
console.error(err);
|
||||
try {
|
||||
await client.close();
|
||||
} catch {
|
||||
// no-op
|
||||
} finally {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the <section id="description"> that our parser adds.
|
||||
export function removeSection(str: string) {
|
||||
return str
|
||||
.replace(/^<section id="description">\n?/, '')
|
||||
.replace(/\n?<\/section>$/, '');
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "daily-challenges",
|
||||
"version": "1.0.0",
|
||||
"main": "seed-daily-challenges.js",
|
||||
"scripts": {
|
||||
"seed-daily-challenges": "tsx seed-daily-challenges.ts",
|
||||
"lint": "eslint --max-warnings 0",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@freecodecamp/eslint-config": "workspace:*",
|
||||
"dotenv": "16.6.1",
|
||||
"eslint": "^9.39.1",
|
||||
"mongodb": "6.21.0",
|
||||
"tsx": "4.21.0",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
MONGOHQ_URL=mongodb://127.0.0.1:27017/freecodecamp?directConnection=true
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
Script to seed daily challenges to freeCodeCamp database, DailyCodingChallenges collection.
|
||||
It gets the daily challenge data from the dev-playground superblock using GraphQL.
|
||||
The main client needs to be running with upcoming changes shown to get the info from GraphQL.
|
||||
Run the curriculum tests on the dev-playground superblock before seeding to make sure they pass.
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
import { MongoClient } from 'mongodb';
|
||||
import { combineChallenges, fetchChallenges, handleError } from './helpers';
|
||||
|
||||
const { MONGOHQ_URL } = process.env;
|
||||
|
||||
// Number challenges in the dev-playground blocks
|
||||
// Update this if the number of challenges changes
|
||||
const EXPECTED_CHALLENGE_COUNT = 365;
|
||||
|
||||
// Date to set for the first challenge, second challenge will be one day later, etc...
|
||||
// **DO NOT CHANGE THIS AFTER RELEASE (if seeding production - okay for local dev)**
|
||||
const year = 2025;
|
||||
const monthIndex = 7; // 0-indexed -> 5 = June
|
||||
const day = 11;
|
||||
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-08-11T00:00:00.000Z';
|
||||
if (START_DATE.toISOString() !== startDateString) {
|
||||
throw new Error(
|
||||
`It appears the start date has changed from "${startDateString}".
|
||||
Are you sure you want to change that? If you are seeding production,
|
||||
you should not change the start date after the daily challenges have been release.
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
const client = new MongoClient(
|
||||
MONGOHQ_URL || 'mongodb://127.0.0.1:27017/freecodecamp?directConnection=true'
|
||||
);
|
||||
|
||||
const seed = async () => {
|
||||
await client.db('admin').command({ ping: 1 });
|
||||
console.log('Successfully connected to mongo');
|
||||
|
||||
const db = client.db('freecodecamp');
|
||||
const dailyCodingChallenges = db.collection('DailyCodingChallenges');
|
||||
|
||||
console.log('Fetching challenges...');
|
||||
const jsChallenges = await fetchChallenges('javascript');
|
||||
const pyChallenges = await fetchChallenges('python');
|
||||
|
||||
if (jsChallenges.length !== pyChallenges.length) {
|
||||
throw new Error(
|
||||
`Number of challenges do not match: ${jsChallenges.length} JavaScript vs ${pyChallenges.length} Python challenges found`
|
||||
);
|
||||
}
|
||||
|
||||
if (jsChallenges.length !== EXPECTED_CHALLENGE_COUNT) {
|
||||
throw new Error(
|
||||
`Expected ${EXPECTED_CHALLENGE_COUNT} challenges, but found ${jsChallenges.length} challenges`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`${jsChallenges.length} challenges found for each language`);
|
||||
console.log('Creating new challenges...');
|
||||
const newChallenges = [];
|
||||
|
||||
for (let i = 0; i < jsChallenges.length; i++) {
|
||||
const jsChallenge = jsChallenges[i];
|
||||
const pyChallenge = pyChallenges[i];
|
||||
|
||||
const newChallenge = combineChallenges({
|
||||
jsChallenge,
|
||||
pyChallenge,
|
||||
challengeNumber: i + 1,
|
||||
date: new Date(START_DATE.getTime() + i * ONE_DAY_IN_MS)
|
||||
});
|
||||
|
||||
newChallenges.push(newChallenge);
|
||||
}
|
||||
|
||||
console.log('Finished creating new challenges');
|
||||
console.log(`Writing ${newChallenges.length} challenges to database...`);
|
||||
|
||||
// Replace if the object exists, create new one if it doesn't
|
||||
const bulkOps = newChallenges.map(challenge => ({
|
||||
replaceOne: {
|
||||
filter: { _id: challenge._id },
|
||||
replacement: challenge,
|
||||
upsert: true
|
||||
}
|
||||
}));
|
||||
|
||||
await dailyCodingChallenges.bulkWrite(bulkOps);
|
||||
|
||||
console.log(`Finished writing challenges to database`);
|
||||
|
||||
const count = await dailyCodingChallenges.countDocuments();
|
||||
|
||||
if (count !== EXPECTED_CHALLENGE_COUNT) {
|
||||
console.warn(
|
||||
'\n********** WARNING *********\n' +
|
||||
'*\n' +
|
||||
`* Expected ${EXPECTED_CHALLENGE_COUNT} challenges in the database,\n` +
|
||||
`* but found ${count} documents\n` +
|
||||
'*\n' +
|
||||
'********** WARNING *********\n'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
seed()
|
||||
.then(() => client.close())
|
||||
.catch(err => handleError(err, client));
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig-base.json"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export type QueryResult = {
|
||||
data: {
|
||||
allChallengeNode: {
|
||||
edges: {
|
||||
node: {
|
||||
challenge: Challenge;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type Challenge = {
|
||||
id: string;
|
||||
title: string;
|
||||
date: Date;
|
||||
description: string;
|
||||
tests: {
|
||||
testString: string;
|
||||
text: string;
|
||||
}[];
|
||||
challengeFiles: {
|
||||
contents: string;
|
||||
filekey: string;
|
||||
}[];
|
||||
};
|
||||
Reference in New Issue
Block a user