diff --git a/services/app/apps/codebattle/lib/codebattle_web/templates/game/join.html.heex b/services/app/apps/codebattle/lib/codebattle_web/templates/game/join.html.heex
index c0a4fc411..1dd4da4cf 100644
--- a/services/app/apps/codebattle/lib/codebattle_web/templates/game/join.html.heex
+++ b/services/app/apps/codebattle/lib/codebattle_web/templates/game/join.html.heex
@@ -1,14 +1,14 @@
-
+
<%= gettext("Join the game") %>
<%= "Player #{player_name(get_first_player(@game))} is waiting for an opponent" %>
- Join
+ <%= gettext("Join") %>
diff --git a/services/app/apps/codebattle/lib/codebattle_web/views/api/game_view.ex b/services/app/apps/codebattle/lib/codebattle_web/views/api/game_view.ex
index 7d577cded..33059292d 100644
--- a/services/app/apps/codebattle/lib/codebattle_web/views/api/game_view.ex
+++ b/services/app/apps/codebattle/lib/codebattle_web/views/api/game_view.ex
@@ -11,7 +11,7 @@ defmodule CodebattleWeb.Api.GameView do
id: get_game_id(game),
inserted_at: Map.get(game, :inserted_at),
award: game.award,
- langs: get_langs_with_templates(game.task),
+ langs: get_langs_with_templates(game),
level: game.level,
locked: game.locked,
mode: game.mode,
@@ -22,7 +22,7 @@ defmodule CodebattleWeb.Api.GameView do
starts_at: Map.get(game, :starts_at),
state: game.state,
status: game.state,
- task: game.task,
+ task: render_task(game),
timeout_seconds: game.timeout_seconds,
tournament_id: Map.get(game, :tournament_id),
type: game.type,
@@ -33,6 +33,10 @@ defmodule CodebattleWeb.Api.GameView do
}
end
+ def render_task(%{task_type: "sql"} = game), do: game.sql_task
+ def render_task(%{task_type: "css"} = game), do: game.css_task
+ def render_task(game), do: game.task
+
def render_completed_games(games) do
Enum.map(games, &render_completed_game/1)
end
@@ -77,7 +81,61 @@ defmodule CodebattleWeb.Api.GameView do
def get_langs_with_templates(nil), do: []
- def get_langs_with_templates(task) do
+ def get_langs_with_templates(%{css_task: %{}}) do
+ [
+ %{
+ slug: "css",
+ name: "css",
+ version: "3",
+ solution_template: "body {\n\tbackground-color: #F3AC3C;\n}"
+ },
+ %{
+ slug: "sass",
+ name: "scss",
+ version: "1.79.4",
+ solution_template: "body {\n\tbackground-color: #F3AC3C;\n}"
+ },
+ %{
+ slug: "less",
+ name: "less",
+ version: "4.2.0",
+ solution_template: "body {\n\tbackground-color: #F3AC3C;\n}"
+ },
+ %{
+ slug: "stylus",
+ name: "stylus",
+ version: "0.63.0",
+ solution_template: "body\n\tbackground-color #F3AC3C"
+ }
+ ]
+ end
+
+ def get_langs_with_templates(%{sql_task: %{}}) do
+ [
+ %{
+ slug: "postgresql",
+ name: "postgresql",
+ version: "18",
+ solution_template: "SELECT solution FROM Solution;"
+ },
+ %{
+ slug: "mongodb",
+ name: "mongodb",
+ version: "8.0",
+ solution_template: "db.solution.find();"
+ },
+ %{
+ slug: "mysql",
+ name: "mysql",
+ version: "8.4.6",
+ solution_template: "SELECT solution FROM Solution;"
+ }
+ ]
+ end
+
+ def get_langs_with_templates(game) when is_nil(game.task) and is_nil(game.sql_task) and is_nil(game.css_task), do: []
+
+ def get_langs_with_templates(game) do
Languages.meta()
|> Map.take(Languages.get_lang_slugs())
|> Map.values()
@@ -86,7 +144,7 @@ defmodule CodebattleWeb.Api.GameView do
slug: meta.slug,
name: meta.name,
version: meta.version,
- solution_template: CodeCheck.generate_solution_template(task, meta),
+ solution_template: CodeCheck.generate_solution_template(game.task, meta),
arguments_generator_template: Map.get(meta, :arguments_generator_template, "")
}
end)
diff --git a/services/app/apps/codebattle/lib/codebattle_web/views/css_battle_builder_view.ex b/services/app/apps/codebattle/lib/codebattle_web/views/css_battle_builder_view.ex
new file mode 100644
index 000000000..81ef24269
--- /dev/null
+++ b/services/app/apps/codebattle/lib/codebattle_web/views/css_battle_builder_view.ex
@@ -0,0 +1,3 @@
+defmodule CodebattleWeb.CssBattleBuilderView do
+ use CodebattleWeb, :view
+end
diff --git a/services/app/apps/codebattle/package.json b/services/app/apps/codebattle/package.json
index b6215a968..9d5eda714 100644
--- a/services/app/apps/codebattle/package.json
+++ b/services/app/apps/codebattle/package.json
@@ -71,11 +71,13 @@
"emoji-mart": "^5.5.2",
"formik": "^2.2.5",
"howler": "^2.2.1",
+ "html-to-image": "^1.11.11",
"humps": "^2.0.1",
"i18next": "^19.8.4",
"jquery": "^3.5.1",
"katex": "^0.16.21",
"lodash": "^4.17.21",
+ "lz-string": "^1.5.0",
"mini-css-extract-plugin": "^2.7.3",
"moment": "^2.29.4",
"monaco-editor": "^0.52.2",
@@ -87,6 +89,7 @@
"phoenix": "^1.6.6",
"phoenix_html": "^3.2.0",
"phoenix_live_view": "^0.18.6",
+ "pixelmatch": "^6.0.0",
"popper.js": "^1.16.1",
"process": "^0.11.10",
"prop-types": "^15.5.10",
diff --git a/services/app/apps/codebattle/priv/repo/migrations/20251020144944_add_experiment_tasks.exs b/services/app/apps/codebattle/priv/repo/migrations/20251020144944_add_experiment_tasks.exs
new file mode 100644
index 000000000..8a70760fa
--- /dev/null
+++ b/services/app/apps/codebattle/priv/repo/migrations/20251020144944_add_experiment_tasks.exs
@@ -0,0 +1,57 @@
+defmodule Codebattle.Repo.Migrations.AddExperimentTasks do use Ecto.Migration
+
+ def change do
+ create table(:css_tasks) do
+ add :description_ru, :string
+ add :description_en, :string
+ add :type, :string, default: "css"
+ add :name, :string
+ add :level, :string
+ add :img_data_url, :string
+ add :disabled, :boolean
+ add :count, :integer, virtual: true
+ add :tags, {:array, :string}, default: []
+ add :state, :string
+ add :visibility, :string, default: "public"
+ add :origin, :string
+ add :creator_id, :integer
+ add :html, :string, default: ""
+ add :styles, :string, default: ""
+
+ timestamps()
+ end
+
+ create table(:sql_tasks) do
+ add :description_ru, :string
+ add :description_en, :string
+ add :type, :string, default: "sql"
+ add :name, :string
+ add :level, :string
+ add :disabled, :boolean
+ add :count, :integer, virtual: true
+ add :tags, {:array, :string}, default: []
+ add :state, :string
+ add :visibility, :string, default: "public"
+ add :origin, :string
+ add :creator_id, :integer
+ add :sql_text, :string, default: ""
+
+ timestamps()
+ end
+
+ alter table(:users) do
+ add :style_lang, :string, default: "css"
+ add :db_type, :string, default: "postgresql"
+ end
+
+ alter table(:tasks) do
+ add :type, :string, default: "algorithms"
+ end
+
+ alter table(:games) do
+ add(:task_type, :string, default: "algorithms")
+ add(:css_task_id, references(:css_tasks))
+ add(:sql_task_id, references(:sql_tasks))
+ end
+ end
+end
diff --git a/services/app/apps/codebattle/test/codebattle_web/controllers/api/v1/settings_controller_test.exs b/services/app/apps/codebattle/test/codebattle_web/controllers/api/v1/settings_controller_test.exs
index 233349442..379dee173 100644
--- a/services/app/apps/codebattle/test/codebattle_web/controllers/api/v1/settings_controller_test.exs
+++ b/services/app/apps/codebattle/test/codebattle_web/controllers/api/v1/settings_controller_test.exs
@@ -13,7 +13,9 @@ defmodule CodebattleWeb.Api.V1.SettingsControllerTest do
github_name: "g_name",
clan: "abc",
rating: 2400,
- lang: "dart"
+ lang: "dart",
+ db_type: "mongodb",
+ style_lang: "less"
})
conn =
@@ -27,6 +29,8 @@ defmodule CodebattleWeb.Api.V1.SettingsControllerTest do
"locale" => "en",
"clan" => "abc",
"sound_settings" => %{"level" => 7, "type" => "dendy"},
+ "db_type" => "mongodb",
+ "style_lang" => "less",
"github_id" => 1,
"github_name" => "g_name"
}
@@ -42,6 +46,8 @@ defmodule CodebattleWeb.Api.V1.SettingsControllerTest do
"clan" => " Bca ",
"locale" => "ru",
"sound_settings" => %{"level" => 3, "type" => "cs"},
+ "db_type" => "postgresql",
+ "style_lang" => "css",
"lang" => "ruby"
}
diff --git a/services/app/apps/codebattle/webpack/webpack.base.config.js b/services/app/apps/codebattle/webpack/webpack.base.config.js
index 04fa37172..85e1e2f77 100644
--- a/services/app/apps/codebattle/webpack/webpack.base.config.js
+++ b/services/app/apps/codebattle/webpack/webpack.base.config.js
@@ -24,6 +24,7 @@ module.exports = {
target: 'browserslist',
entry: {
app: ['./assets/js/app.js', './assets/css/style.scss'],
+ cssbattle: ['./assets/js/iframes/cssbattle/index.js'],
landing: ['./assets/js/landing.js', './assets/css/landing.scss'],
external: ['./assets/js/external.js', './assets/css/external.scss'],
broadcast_editor: ['./assets/js/widgets/pages/broadcast-editor/index'],
diff --git a/services/app/apps/codebattle/yarn.lock b/services/app/apps/codebattle/yarn.lock
index eefe2c3ba..d58f5e7e4 100644
--- a/services/app/apps/codebattle/yarn.lock
+++ b/services/app/apps/codebattle/yarn.lock
@@ -7221,6 +7221,11 @@ html-tags@1:
resolved "https://registry.npmjs.org/html-tags/-/html-tags-1.2.0.tgz"
integrity sha1-x43mW1Zjqll5id0rerSSANfk25g=
+html-to-image@^1.11.11:
+ version "1.11.11"
+ resolved "https://registry.yarnpkg.com/html-to-image/-/html-to-image-1.11.11.tgz#c0f8a34dc9e4b97b93ff7ea286eb8562642ebbea"
+ integrity sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==
+
html-url-attributes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/html-url-attributes/-/html-url-attributes-3.0.0.tgz#fc4abf0c3fb437e2329c678b80abb3c62cff6f08"
@@ -10411,6 +10416,13 @@ pirates@^4.0.4:
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
+pixelmatch@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-6.0.0.tgz#82bfad31becb8973746e8f7b0d88160cd10ade6d"
+ integrity sha512-FYpL4XiIWakTnIqLqvt3uN4L9B3TsuHIvhLILzTiJZMJUsGvmKNeL4H3b6I99LRyerK9W4IuOXw+N28AtRgK2g==
+ dependencies:
+ pngjs "^7.0.0"
+
pkg-dir@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz"
@@ -10437,6 +10449,11 @@ pkginfo@^0.4.0:
resolved "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.1.tgz"
integrity sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8=
+pngjs@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-7.0.0.tgz#a8b7446020ebbc6ac739db6c5415a65d17090e26"
+ integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==
+
pngquant-bin@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/pngquant-bin/-/pngquant-bin-6.0.1.tgz#2b5789ca219eeb4d8509ab1ae082092801b7f07e"
diff --git a/services/app/apps/runner/dockers/css/Dockerfile b/services/app/apps/runner/dockers/css/Dockerfile
new file mode 100644
index 000000000..98ac2fea7
--- /dev/null
+++ b/services/app/apps/runner/dockers/css/Dockerfile
@@ -0,0 +1,23 @@
+FROM codebattle/runner-rs:latest AS runner
+
+FROM node:20.11.1-alpine
+
+RUN apk add --update --no-cache make
+
+ADD package.json .
+ADD package-lock.json .
+
+RUN npm install
+
+ENV NODE_PATH /usr/local/lib/node_modules/
+
+WORKDIR /usr/src/app
+
+COPY check check
+
+ADD checker.js .
+ADD Makefile .
+
+EXPOSE 8000
+
+COPY --from=runner /app/codebattle_runner /runner/codebattle_runner
diff --git a/services/app/apps/runner/dockers/css/Makefile b/services/app/apps/runner/dockers/css/Makefile
new file mode 100644
index 000000000..804bd9a26
--- /dev/null
+++ b/services/app/apps/runner/dockers/css/Makefile
@@ -0,0 +1,4 @@
+test:
+ node ./checker.js
+
+.PHONY: test
diff --git a/services/app/apps/runner/dockers/css/checker.js b/services/app/apps/runner/dockers/css/checker.js
new file mode 100644
index 000000000..27261e5ce
--- /dev/null
+++ b/services/app/apps/runner/dockers/css/checker.js
@@ -0,0 +1,114 @@
+import { readFileSync, writeFileSync } from 'fs';
+import pixelmatch from 'pixelmatch';
+import LZString from 'lz-string';
+import nodeHtmlToImage from 'node-html-to-image';
+import { PNG } from 'pngjs';
+
+const executionResult = [];
+
+const toOut = ({ type = '', value = '', time = 0 }) => {
+ executionResult.push(
+ {
+ type,
+ time,
+ value: JSON.stringify(value),
+ }
+ );
+};
+
+const getMatchPoints = (stats, width, height) => (
+ 1 - stats / (width * height)
+);
+
+const getMatchPercentageText = match => (
+ `${(match * 100).toFixed(2)}%`
+);
+
+const run = function run(args = []) {
+ const now = performance.now();
+
+ const solution = readFileSync('./check/solution.html', 'utf-8');
+ const target = readFileSync('./check/target.html', 'utf-8');
+
+ const mime = 'image/png';
+ const encoding = 'base64';
+
+ return Promise.all([
+ nodeHtmlToImage({
+ html: solution,
+ content: {
+ output: "./check/solution.png",
+ },
+ }),
+ nodeHtmlToImage({
+ html: target,
+ content: {
+ output: "./check/target.png",
+ },
+ }),
+ ]).then(([solutionImageBuffer, targetImageBuffer]) => {
+ const solutionImg = PNG.sync.read(solutionImageBuffer);
+ const targetImg = PNG.sync.read(targetImageBuffer);
+
+ const { width: solutionWidth, height: solutionHeight } = solutionImg;
+ const { width, height } = targetImg;
+
+ const isMissmatchSizes = width !== solutionWidth || height !== solutionHeight;
+
+ const diff = new PNG({ width, height });
+
+ const solutionImgDatUri = `data:${mime};${encoding},${solutionImageBuffer.toString(encoding)}`;
+ const targetImgDataUri = `data:${mime};${encoding},${targetImageBuffer.toString(encoding)}`;
+
+ try {
+ const stats = pixelmatch(solutionImg.data, targetImg.data, diff.data, width, height, { threshold: 0.1 });
+ const match = getMatchPoints(stats, width, height);
+ const matchPercentage = getMatchPercentageText(match);
+ const diffBuffer = PNG.sync.write(diff);
+
+ writeFileSync('./check/diff.png', diffBuffer);
+ const diffDataUri = `data:${mime};${encoding},${diffBuffer.toString(encoding)}`;
+
+ toOut({
+ type: 'result',
+ time: (performance.now() - now).toFixed(5),
+ value: {
+ match,
+ matchPercentage,
+ isMissmatchSizes,
+ width,
+ height,
+ solutionDataUri: LZString.compress(solutionImgDatUri),
+ targetDataUri: LZString.compress(targetImgDataUri),
+ diffDataUri: LZString.compress(diffDataUri),
+ message: '',
+ },
+ });
+ } catch (e) {
+ toOut({
+ type: 'error',
+ time: (performance.now() - now).toFixed(5),
+ value: {
+ match: 0,
+ matchPercentage: '0.00%',
+ isMissmatchSizes,
+ width,
+ height,
+ solutionDataUri: LZString.compress(solutionImgDatUri),
+ targetDataUri: LZString.compress(targetImgDataUri),
+ message: e.toString(),
+ },
+ });
+ }
+ }).catch(err => {
+ toOut({
+ type: 'error',
+ time: 0,
+ value: err.toString(),
+ });
+ });
+}
+
+run().then(() => {
+ console.log(JSON.stringify(executionResult));
+})
diff --git a/services/app/apps/runner/dockers/css/package-lock.json b/services/app/apps/runner/dockers/css/package-lock.json
new file mode 100644
index 000000000..2a2ec3be8
--- /dev/null
+++ b/services/app/apps/runner/dockers/css/package-lock.json
@@ -0,0 +1,1093 @@
+{
+ "name": "css-checker",
+ "version": "0.0.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "css-checker",
+ "version": "0.0.1",
+ "dependencies": {
+ "@types/node": "^20.11.25",
+ "lz-string": "^1.5.0",
+ "node-html-to-image": "^5.0.0",
+ "pixelmatch": "^6.0.0",
+ "pngjs": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.26.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.25.9",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@puppeteer/browsers": {
+ "version": "2.3.1",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "debug": "^4.3.6",
+ "extract-zip": "^2.0.1",
+ "progress": "^2.0.3",
+ "proxy-agent": "^6.4.0",
+ "semver": "^7.6.3",
+ "tar-fs": "^3.0.6",
+ "unbzip2-stream": "^1.4.3",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "browsers": "lib/cjs/main-cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@tootallnate/quickjs-emscripten": {
+ "version": "0.23.0",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.17.3",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.19.2"
+ }
+ },
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/yauzl/node_modules/@types/node": {
+ "version": "22.8.4",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "undici-types": "~6.19.8"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.1",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "license": "Python-2.0"
+ },
+ "node_modules/ast-types": {
+ "version": "0.13.4",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/b4a": {
+ "version": "1.6.7",
+ "license": "Apache-2.0"
+ },
+ "node_modules/bare-events": {
+ "version": "2.5.0",
+ "license": "Apache-2.0",
+ "optional": true
+ },
+ "node_modules/bare-fs": {
+ "version": "2.3.5",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "bare-events": "^2.0.0",
+ "bare-path": "^2.0.0",
+ "bare-stream": "^2.0.0"
+ }
+ },
+ "node_modules/bare-os": {
+ "version": "2.4.4",
+ "license": "Apache-2.0",
+ "optional": true
+ },
+ "node_modules/bare-path": {
+ "version": "2.1.3",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "bare-os": "^2.1.0"
+ }
+ },
+ "node_modules/bare-stream": {
+ "version": "2.3.2",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "streamx": "^2.20.0"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/basic-ftp": {
+ "version": "5.0.5",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chromium-bidi": {
+ "version": "0.6.5",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "mitt": "3.0.1",
+ "urlpattern-polyfill": "10.0.0",
+ "zod": "3.23.8"
+ },
+ "peerDependencies": {
+ "devtools-protocol": "*"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "license": "MIT"
+ },
+ "node_modules/cosmiconfig": {
+ "version": "9.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/data-uri-to-buffer": {
+ "version": "6.0.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.7",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/degenerator": {
+ "version": "5.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "ast-types": "^0.13.4",
+ "escodegen": "^2.1.0",
+ "esprima": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/devtools-protocol": {
+ "version": "0.0.1330662",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "license": "MIT"
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escodegen": {
+ "version": "2.1.0",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extract-zip": {
+ "version": "2.0.1",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "get-stream": "^5.1.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "extract-zip": "cli.js"
+ },
+ "engines": {
+ "node": ">= 10.17.0"
+ },
+ "optionalDependencies": {
+ "@types/yauzl": "^2.9.1"
+ }
+ },
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "license": "MIT"
+ },
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "11.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "5.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-uri": {
+ "version": "6.0.3",
+ "license": "MIT",
+ "dependencies": {
+ "basic-ftp": "^5.0.2",
+ "data-uri-to-buffer": "^6.0.2",
+ "debug": "^4.3.4",
+ "fs-extra": "^11.2.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "license": "ISC"
+ },
+ "node_modules/handlebars": {
+ "version": "4.7.8",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.5",
+ "neo-async": "^2.6.2",
+ "source-map": "^0.6.1",
+ "wordwrap": "^1.0.0"
+ },
+ "bin": {
+ "handlebars": "bin/handlebars"
+ },
+ "engines": {
+ "node": ">=0.4.7"
+ },
+ "optionalDependencies": {
+ "uglify-js": "^3.1.4"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.5",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.0.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ip-address": {
+ "version": "9.0.5",
+ "license": "MIT",
+ "dependencies": {
+ "jsbn": "1.1.0",
+ "sprintf-js": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "license": "MIT"
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsbn": {
+ "version": "1.1.0",
+ "license": "MIT"
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "license": "MIT"
+ },
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "7.18.3",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "license": "MIT",
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "license": "MIT"
+ },
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "license": "MIT"
+ },
+ "node_modules/netmask": {
+ "version": "2.0.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/node-html-to-image": {
+ "version": "5.0.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "handlebars": "4.7.8",
+ "puppeteer": "23.2.2",
+ "puppeteer-cluster": "0.24.0"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/pac-proxy-agent": {
+ "version": "7.0.2",
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/quickjs-emscripten": "^0.23.0",
+ "agent-base": "^7.0.2",
+ "debug": "^4.3.4",
+ "get-uri": "^6.0.1",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.5",
+ "pac-resolver": "^7.0.1",
+ "socks-proxy-agent": "^8.0.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/pac-resolver": {
+ "version": "7.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "degenerator": "^5.0.0",
+ "netmask": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "license": "ISC"
+ },
+ "node_modules/pixelmatch": {
+ "version": "6.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "pngjs": "^7.0.0"
+ },
+ "bin": {
+ "pixelmatch": "bin/pixelmatch"
+ }
+ },
+ "node_modules/pngjs": {
+ "version": "7.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.19.0"
+ }
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/proxy-agent": {
+ "version": "6.4.0",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.0.2",
+ "debug": "^4.3.4",
+ "http-proxy-agent": "^7.0.1",
+ "https-proxy-agent": "^7.0.3",
+ "lru-cache": "^7.14.1",
+ "pac-proxy-agent": "^7.0.1",
+ "proxy-from-env": "^1.1.0",
+ "socks-proxy-agent": "^8.0.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "license": "MIT"
+ },
+ "node_modules/pump": {
+ "version": "3.0.2",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/puppeteer": {
+ "version": "23.2.2",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@puppeteer/browsers": "2.3.1",
+ "chromium-bidi": "0.6.5",
+ "cosmiconfig": "^9.0.0",
+ "devtools-protocol": "0.0.1330662",
+ "puppeteer-core": "23.2.2",
+ "typed-query-selector": "^2.12.0"
+ },
+ "bin": {
+ "puppeteer": "lib/cjs/puppeteer/node/cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/puppeteer-cluster": {
+ "version": "0.24.0",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "peerDependencies": {
+ "puppeteer": ">=22.0.0"
+ }
+ },
+ "node_modules/puppeteer-core": {
+ "version": "23.2.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@puppeteer/browsers": "2.3.1",
+ "chromium-bidi": "0.6.5",
+ "debug": "^4.3.6",
+ "devtools-protocol": "0.0.1330662",
+ "typed-query-selector": "^2.12.0",
+ "ws": "^8.18.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/queue-tick": {
+ "version": "1.0.1",
+ "license": "MIT"
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.6.3",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.8.3",
+ "license": "MIT",
+ "dependencies": {
+ "ip-address": "^9.0.5",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "8.0.4",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.1",
+ "debug": "^4.3.4",
+ "socks": "^2.8.3"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.1.3",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/streamx": {
+ "version": "2.20.1",
+ "license": "MIT",
+ "dependencies": {
+ "fast-fifo": "^1.3.2",
+ "queue-tick": "^1.0.1",
+ "text-decoder": "^1.1.0"
+ },
+ "optionalDependencies": {
+ "bare-events": "^2.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "3.0.6",
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0",
+ "tar-stream": "^3.1.5"
+ },
+ "optionalDependencies": {
+ "bare-fs": "^2.1.1",
+ "bare-path": "^2.1.0"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "3.1.7",
+ "license": "MIT",
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
+ "node_modules/text-decoder": {
+ "version": "1.2.1",
+ "license": "Apache-2.0"
+ },
+ "node_modules/through": {
+ "version": "2.3.8",
+ "license": "MIT"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.0",
+ "license": "0BSD"
+ },
+ "node_modules/typed-query-selector": {
+ "version": "2.12.0",
+ "license": "MIT"
+ },
+ "node_modules/uglify-js": {
+ "version": "3.19.3",
+ "license": "BSD-2-Clause",
+ "optional": true,
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/unbzip2-stream": {
+ "version": "1.4.3",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.2.1",
+ "through": "^2.3.8"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.19.8",
+ "license": "MIT"
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/urlpattern-polyfill": {
+ "version": "10.0.0",
+ "license": "MIT"
+ },
+ "node_modules/wordwrap": {
+ "version": "1.0.0",
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "license": "ISC"
+ },
+ "node_modules/ws": {
+ "version": "8.18.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.23.8",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/services/app/apps/runner/dockers/css/package.json b/services/app/apps/runner/dockers/css/package.json
new file mode 100644
index 000000000..a54e76c9d
--- /dev/null
+++ b/services/app/apps/runner/dockers/css/package.json
@@ -0,0 +1,12 @@
+{
+ "type": "module",
+ "name": "css-checker",
+ "version": "0.0.1",
+ "dependencies": {
+ "@types/node": "^20.11.25",
+ "lz-string": "^1.5.0",
+ "node-html-to-image": "^5.0.0",
+ "pixelmatch": "^6.0.0",
+ "pngjs": "^7.0.0"
+ }
+}
diff --git a/services/app/apps/runner/dockers/mongodb/Dockerfile b/services/app/apps/runner/dockers/mongodb/Dockerfile
new file mode 100644
index 000000000..79e87286c
--- /dev/null
+++ b/services/app/apps/runner/dockers/mongodb/Dockerfile
@@ -0,0 +1,20 @@
+FROM codebattle/runner-rs:latest AS runner
+FROM mongo:8.0.14
+
+RUN apt-get update && \
+ apt-get install -y make && \
+ rm -rf /var/lib/apt/lists/*
+
+ENV MONGO_INITDB_DATABASE=db
+ENV MONGO_INITDB_ROOT_USERNAME=user
+ENV MONGO_INITDB_ROOT_PASSWORD=password
+
+WORKDIR /usr/src/app
+
+EXPOSE 8000
+
+ADD single_table.js .
+ADD Makefile .
+
+CMD ["mongod"]
+COPY --from=runner /app/codebattle_runner /runner/codebattle_runner
diff --git a/services/app/apps/runner/dockers/mongodb/Makefile b/services/app/apps/runner/dockers/mongodb/Makefile
new file mode 100644
index 000000000..8192ad0e8
--- /dev/null
+++ b/services/app/apps/runner/dockers/mongodb/Makefile
@@ -0,0 +1,3 @@
+test:
+ mongo localhost/${MONGO_INITDB_DATABASE} -u ${MONGO_INITDB_ROOT_USERNAME} -p ${MONGO_INITDB_ROOT_PASSWORD} single_table.js checker/solution.js
+
diff --git a/services/app/apps/runner/dockers/mongodb/single_table.js b/services/app/apps/runner/dockers/mongodb/single_table.js
new file mode 100644
index 000000000..4c2c08e2e
--- /dev/null
+++ b/services/app/apps/runner/dockers/mongodb/single_table.js
@@ -0,0 +1,10 @@
+var docs = [];
+docs.push({ a: 0, b: 5 });
+docs.push({ a: 1, b: 2 });
+docs.push({ a: 7, b: 5 });
+docs.push({ a: 3, b: 1 });
+docs.push({ a: 11, b: 3 });
+docs.push({ a: 100, b: 200 });
+docs.push({ a: 14, b: 3 });
+
+db.sum.insertMany(docs);
diff --git a/services/app/apps/runner/dockers/mysql/Dockerfile b/services/app/apps/runner/dockers/mysql/Dockerfile
new file mode 100644
index 000000000..a5b1fcb4e
--- /dev/null
+++ b/services/app/apps/runner/dockers/mysql/Dockerfile
@@ -0,0 +1,20 @@
+FROM codebattle/runner-rs:latest AS runner
+FROM mysql:8.4.6
+
+# RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
+RUN microdnf install -y make
+
+ENV MYSQL_DATABASE=db
+ENV MYSQL_USER=user
+ENV MYSQL_PASSWORD=password
+ENV MYSQL_ROOT_PASSWORD=rootpassword
+
+WORKDIR /usr/src/app
+
+EXPOSE 8000
+
+ADD single_table.sql .
+ADD Makefile .
+
+CMD ["mysqld"]
+COPY --from=runner /app/codebattle_runner /runner/codebattle_runner
diff --git a/services/app/apps/runner/dockers/mysql/Makefile b/services/app/apps/runner/dockers/mysql/Makefile
new file mode 100644
index 000000000..5ae68e0b2
--- /dev/null
+++ b/services/app/apps/runner/dockers/mysql/Makefile
@@ -0,0 +1,2 @@
+test:
+ cat single_table.sql ./checker/solution.sql | mysql -u ${MYSQL_USER} -p${MYSQL_PASSWORD} ${MYSQL_DATABASE}
diff --git a/services/app/apps/runner/dockers/mysql/single_table.sql b/services/app/apps/runner/dockers/mysql/single_table.sql
new file mode 100644
index 000000000..66e03351c
--- /dev/null
+++ b/services/app/apps/runner/dockers/mysql/single_table.sql
@@ -0,0 +1,8 @@
+CREATE TABLE IF NOT EXISTS sum (a NUMERIC, b NUMERIC);
+
+INSERT INTO sum (a, b) VALUES (1, 2);
+INSERT INTO sum (a, b) VALUES (7, 5);
+INSERT INTO sum (a, b) VALUES (3, 1);
+INSERT INTO sum (a, b) VALUES (11, 3);
+INSERT INTO sum (a, b) VALUES (100, 200);
+INSERT INTO sum (a, b) VALUES (14, 3);
diff --git a/services/app/apps/runner/dockers/postgresql/Dockerfile b/services/app/apps/runner/dockers/postgresql/Dockerfile
new file mode 100644
index 000000000..cd1b5de92
--- /dev/null
+++ b/services/app/apps/runner/dockers/postgresql/Dockerfile
@@ -0,0 +1,19 @@
+FROM codebattle/runner-rs:latest AS runner
+FROM postgres:18-alpine
+
+RUN apk add --no-cache make
+
+ENV POSTGRES_DB=db
+ENV POSTGRES_USER=user
+ENV POSTGRES_PASSWORD=password
+
+WORKDIR /usr/src/app
+
+EXPOSE 8000
+
+ADD single_table.sql .
+ADD Makefile .
+
+# The default entrypoint (postgres) is used to start the server
+CMD ["postgres"]
+COPY --from=runner /app/codebattle_runner /runner/codebattle_runner
diff --git a/services/app/apps/runner/dockers/postgresql/Makefile b/services/app/apps/runner/dockers/postgresql/Makefile
new file mode 100644
index 000000000..9a48d4378
--- /dev/null
+++ b/services/app/apps/runner/dockers/postgresql/Makefile
@@ -0,0 +1,3 @@
+test:
+ export PGPASSWORD=${POSTGRES_PASSWORD}
+ psql -U ${POSTGRES_USER} -d ${POSTGRES_DB} -c "BEGIN TRANSACTION;" -f single_table.sql -f solution.sql -c "COMMIT;"
diff --git a/services/app/apps/runner/dockers/postgresql/single_table.sql b/services/app/apps/runner/dockers/postgresql/single_table.sql
new file mode 100644
index 000000000..66e03351c
--- /dev/null
+++ b/services/app/apps/runner/dockers/postgresql/single_table.sql
@@ -0,0 +1,8 @@
+CREATE TABLE IF NOT EXISTS sum (a NUMERIC, b NUMERIC);
+
+INSERT INTO sum (a, b) VALUES (1, 2);
+INSERT INTO sum (a, b) VALUES (7, 5);
+INSERT INTO sum (a, b) VALUES (3, 1);
+INSERT INTO sum (a, b) VALUES (11, 3);
+INSERT INTO sum (a, b) VALUES (100, 200);
+INSERT INTO sum (a, b) VALUES (14, 3);
diff --git a/services/app/apps/runner/lib/runner/executor.ex b/services/app/apps/runner/lib/runner/executor.ex
index 8145a0c66..08c3d1c74 100644
--- a/services/app/apps/runner/lib/runner/executor.ex
+++ b/services/app/apps/runner/lib/runner/executor.ex
@@ -13,6 +13,28 @@ defmodule Runner.Executor do
@spec call(Runner.Task.t(), Runner.LanguageMeta.t(), String.t(), String.t()) ::
Runner.execution_result()
+ def call(%Runner.Task{type: "sql"}, lang_meta, solution_text, run_id) do
+ seed = get_seed()
+
+ wait_permission_to_launch(run_id, 0, Languages.get_timeout_ms(lang_meta) + 500)
+
+ tmp_dir_path = prepare_tmp_dir!(lang_meta, solution_text, "", "")
+
+ {out, err, status} =
+ lang_meta
+ |> get_docker_command(tmp_dir_path)
+ |> run_command(lang_meta)
+
+ Task.start(File, :rm_rf, [tmp_dir_path])
+
+ %{
+ container_output: out,
+ container_stderr: err,
+ exit_code: status,
+ seed: seed
+ }
+ end
+
def call(task, lang_meta, solution_text, run_id) do
seed = get_seed()
diff --git a/services/app/apps/runner/lib/runner/languages.ex b/services/app/apps/runner/lib/runner/languages.ex
index 2b79b5f98..5c5ada0aa 100644
--- a/services/app/apps/runner/lib/runner/languages.ex
+++ b/services/app/apps/runner/lib/runner/languages.ex
@@ -812,6 +812,45 @@ defmodule Runner.Languages do
defining_variable_template: "<%= name %>: <%= type %>",
nested_value_expression_template: "<%= value %>"
}
+ },
+ "mongodb" => %LanguageMeta{
+ name: "MongoDB",
+ slug: "mongodb",
+ version: "8.0.14",
+ checker_version: 2,
+ output_version: 2,
+ generate_checker?: false,
+ container_run_timeout: "20s",
+ check_dir: "check",
+ docker_image: "codebattle/mongodb:8.0.14",
+ solution_file_name: "solution.js",
+ checker_file_name: "checker.js"
+ },
+ "mysql" => %LanguageMeta{
+ name: "MySQL",
+ slug: "mysql",
+ version: "8.4.6",
+ checker_version: 2,
+ output_version: 2,
+ generate_checker?: false,
+ container_run_timeout: "20s",
+ check_dir: "check",
+ docker_image: "codebattle/mysql:8.4.6",
+ solution_file_name: "solution.sql",
+ checker_file_name: "checker.sql"
+ },
+ "postgresql" => %LanguageMeta{
+ name: "PostgreSQL",
+ slug: "postgresql",
+ version: "18",
+ checker_version: 2,
+ output_version: 2,
+ generate_checker?: false,
+ container_run_timeout: "20s",
+ check_dir: "check",
+ docker_image: "codebattle/postgresql:18",
+ solution_file_name: "solution.sql",
+ checker_file_name: "checker.sql"
}
}
diff --git a/services/app/apps/runner/lib/runner/task.ex b/services/app/apps/runner/lib/runner/task.ex
index 41743dad8..f5ceb5e66 100644
--- a/services/app/apps/runner/lib/runner/task.ex
+++ b/services/app/apps/runner/lib/runner/task.ex
@@ -9,11 +9,15 @@ defmodule Runner.Task do
@type t :: %__MODULE__{}
- @required_fields [:input_signature, :output_signature, :asserts, :asserts_examples]
+ @required_experiment_fields [:type]
+ @required_fields [:type, :input_signature, :output_signature, :asserts, :asserts_examples]
+ @all_experiment_fields [:type]
@all_fields @required_fields ++ [:comment]
+ @experiment_types ["css", "sql"]
@primary_key false
embedded_schema do
+ field(:type, :string, default: "algorithms")
field(:comment, :string, default: "use stdout to debug")
field(:input_signature, {:array, AtomizedMap}, default: [])
field(:output_signature, AtomizedMap, default: %{})
@@ -24,7 +28,15 @@ defmodule Runner.Task do
@spec new!(params :: map()) :: t()
def new!(params = %_{}), do: params |> Map.from_struct() |> new!()
- def new!(params = %{}) do
+ def new!(params = %{type: experiment_type})
+ when experiment_type in @experiment_types do
+ %__MODULE__{}
+ |> cast(params, @all_experiment_fields)
+ |> validate_required(@required_experiment_fields)
+ |> apply_action!(:validate)
+ end
+
+ def new!(params) do
%__MODULE__{}
|> cast(params, @all_fields)
|> validate_required(@required_fields)
diff --git a/services/app/config/config.exs b/services/app/config/config.exs
index 8437d00f5..e3f1395c8 100644
--- a/services/app/config/config.exs
+++ b/services/app/config/config.exs
@@ -35,7 +35,9 @@ config :codebattle, :api_key, "x-key"
config :codebattle, :app_subtitle, "by Hexlet’s community"
config :codebattle, :app_title, "Hexlet Codebattle"
config :codebattle, :base_user_path, "/"
+config :codebattle, :default_db_type_slug, "postgresql"
config :codebattle, :default_lang_slug, "js"
+config :codebattle, :default_style_lang_slug, "css"
config :codebattle, :external,
app_name: "Codebattle External",