diff --git a/.env b/.env index 85be481..5b30ab5 100644 --- a/.env +++ b/.env @@ -56,6 +56,9 @@ OPENAI_API_KEY=!ChangeMe! MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 ###< symfony/messenger ### +# SQL Runner +SQLRUNNER_URL=http://sqlrunner.app-sf.orb.local:8080 + ###> symfony/line-notify-notifier ### # LINE_NOTIFY_DSN=linenotify://TOKEN@default ###< symfony/line-notify-notifier ### diff --git a/Dockerfile b/Dockerfile index 2a653dc..e809d78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,7 @@ RUN set -eux; \ install-php-extensions \ @composer \ apcu \ + curl \ intl \ opcache \ zip \ @@ -89,9 +90,11 @@ RUN set -eux; \ composer run-script --no-dev post-install-cmd; \ chmod +x bin/console; sync; -# build sass and asset maps +# build route cache, sass and asset maps RUN set -eux; \ chmod +x bin/console; sync; \ + ./bin/console cache:clear; \ + ./bin/console cache:warmup; \ ./bin/console sass:build; \ ./bin/console typescript:build; \ ./bin/console asset-map:compile; diff --git a/README.md b/README.md index d62603e..4215281 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,10 @@ The Database Playground is a platform designed to enhance your SQL skills throug ### Zeabur 1. Deploy Redis, PostgreSQL, Meilisearch, and Umami (for statistics) on Zeabur. -2. Deploy the application in Git mode on Zeabur. -3. Add the following environment variables to the application: +2. Deploy [SQL runner](https://github.com/database-playground/sqlrunner-v2) on Zeabur, and rename the service host to + `sqlrunner`. +3. Deploy the application in Git mode on Zeabur. +4. Add the following environment variables to the application: ```env DATABASE_URL=postgresql://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@postgresql.zeabur.internal:5432/${POSTGRES_DATABASE}?serverVersion=16&charset=utf8 REDIS_URI=${REDIS_CONNECTION_STRING} @@ -53,12 +55,9 @@ The Database Playground is a platform designed to enhance your SQL skills throug UMAMI_WEBSITE_ID=your-website-id OPENAI_API_KEY=your-openai-api-key LINE_NOTIFY_DSN=linenotify://line-notify-token@default + SQLRUNNER_URL=http://sqlrunner.zeabur.internal:8080 ``` -4. Create an index in Meilisearch by running: - ```bash - php bin/console meili:create --update-settings - ``` -5. Bind your domain, and the application will be ready for use. +5. Bind your domain, and the application will be ready for use. The Meilisearch index will be automatically created on start up. ### Docker diff --git a/assets/app/index.ts b/assets/app/index.ts index 34fface..a384dcb 100644 --- a/assets/app/index.ts +++ b/assets/app/index.ts @@ -1,5 +1,5 @@ import * as bootstrap from "bootstrap"; -import "./bootstrap.ts"; +import "./stimulus.ts"; /** * Initialize tooltips of Bootstrap diff --git a/assets/app/bootstrap.ts b/assets/app/stimulus.ts similarity index 100% rename from assets/app/bootstrap.ts rename to assets/app/stimulus.ts diff --git a/assets/controllers.json b/assets/controllers.json index 75eb58d..ea01087 100644 --- a/assets/controllers.json +++ b/assets/controllers.json @@ -3,7 +3,7 @@ "@symfony/ux-chartjs": { "chart": { "enabled": true, - "fetch": "eager" + "fetch": "lazy" } }, "@symfony/ux-live-component": { diff --git a/assets/controllers/challenge_comment_controller.ts b/assets/controllers/challenge_comment_controller.ts index 2a776f0..08ed7b2 100644 --- a/assets/controllers/challenge_comment_controller.ts +++ b/assets/controllers/challenge_comment_controller.ts @@ -1,6 +1,6 @@ import { Controller } from "@hotwired/stimulus"; import { Component, getComponent } from "@symfony/ux-live-component"; -import * as bootstrap from "bootstrap"; +import type * as bootstrap from "bootstrap"; export default class extends Controller { #component: Component | undefined; @@ -24,7 +24,8 @@ export default class extends Controller { const $confirmModal = this.element.querySelector(".app-challenge-comment__deletion_confirm"); if ($confirmModal) { - this.#modal = new bootstrap.Modal($confirmModal); + const bs = await import("bootstrap"); + this.#modal = new bs.Modal($confirmModal); } } diff --git a/assets/controllers/challenge_executor_controller.ts b/assets/controllers/challenge_executor_controller.ts index cd0fb9c..bad0045 100644 --- a/assets/controllers/challenge_executor_controller.ts +++ b/assets/controllers/challenge_executor_controller.ts @@ -1,7 +1,7 @@ import { sql } from "@codemirror/lang-sql"; import { Controller } from "@hotwired/stimulus"; import { getComponent } from "@symfony/ux-live-component"; -import { basicSetup, EditorView } from "codemirror"; +import type { EditorView } from "codemirror"; export default class extends Controller { static values = { @@ -15,6 +15,8 @@ export default class extends Controller { #editorView: EditorView | undefined; async connect() { + const { basicSetup, EditorView } = await import("codemirror"); + const component = await getComponent(this.element); let lastQuery = this.element.dataset["lastQuery"]; diff --git a/assets/controllers/challenge_instruction_modal_controller.ts b/assets/controllers/challenge_instruction_modal_controller.ts index 96937eb..2f7401f 100644 --- a/assets/controllers/challenge_instruction_modal_controller.ts +++ b/assets/controllers/challenge_instruction_modal_controller.ts @@ -1,17 +1,18 @@ import { Controller } from "@hotwired/stimulus"; import { Component, getComponent } from "@symfony/ux-live-component"; -import * as bootstrap from "bootstrap"; +import type * as bootstrap from "bootstrap"; export default class extends Controller { #component: Component | undefined; #modal: bootstrap.Modal | undefined; - async initialize(): Promise { + async initialize() { this.#component = await getComponent(this.element); } - connect(): void { - this.#modal = new bootstrap.Modal(this.element); + async connect() { + const bs = await import("bootstrap"); + this.#modal = new bs.Modal(this.element); } async open() { diff --git a/assets/controllers/challenge_solution_video_modal_controller.ts b/assets/controllers/challenge_solution_video_modal_controller.ts index 37b66dd..3182d42 100644 --- a/assets/controllers/challenge_solution_video_modal_controller.ts +++ b/assets/controllers/challenge_solution_video_modal_controller.ts @@ -1,12 +1,13 @@ import { Controller } from "@hotwired/stimulus"; -import * as bootstrap from "bootstrap"; +import type * as bootstrap from "bootstrap"; export default class extends Controller { #modal: bootstrap.Modal | undefined; #videoUrl: string | undefined; - connect(): void { - this.#modal = new bootstrap.Modal(this.element); + async connect() { + const bs = await import("bootstrap"); + this.#modal = new bs.Modal(this.element); this.#videoUrl = this.element.dataset.videoUrl; } diff --git a/assets/styles/app.scss b/assets/styles/app.scss index dc51f0b..9602906 100644 --- a/assets/styles/app.scss +++ b/assets/styles/app.scss @@ -1,29 +1,5 @@ -@use "sass:list"; @use "sass:map"; -@import "../../vendor/twbs/bootstrap/scss/functions"; - -// Variables -$prefix: bs-; -$spacer: 0.8rem; -$primary: #4154f1; -$navbar-padding-x: 2rem; -$navbar-padding-y: $spacer * 0.85; -$input-btn-border-width: var(--#{$prefix}border-width); -$input-border-width: $input-btn-border-width; -$input-height-border: calc(#{$input-border-width} * 2); -$form-floating-height: add(3.5rem * 0.8, $input-height-border); -$form-floating-padding-y: 0.65rem; - -$timeline-color: #808080; -$timeline-width: 2px; - -@import "../../vendor/twbs/bootstrap/scss/bootstrap-reboot"; -$font-family-sans-serif: list.join( - ("LXGW WenKai TC", cursive), - $font-family-sans-serif -); - -@import "../../vendor/twbs/bootstrap/scss/bootstrap"; +@import "./bootstrap"; // global html { diff --git a/assets/styles/bootstrap.scss b/assets/styles/bootstrap.scss new file mode 100644 index 0000000..5a6d4c2 --- /dev/null +++ b/assets/styles/bootstrap.scss @@ -0,0 +1,61 @@ +@use "sass:list"; +@import "../../vendor/twbs/bootstrap/scss/functions"; + +// Variables +$prefix: bs-; +$spacer: 0.8rem; +$primary: #4154f1; +$navbar-padding-x: 2rem; +$navbar-padding-y: $spacer * 0.85; +$input-btn-border-width: var(--#{$prefix}border-width); +$input-border-width: $input-btn-border-width; +$input-height-border: calc(#{$input-border-width} * 2); +$form-floating-height: add(3.5rem * 0.8, $input-height-border); +$form-floating-padding-y: 0.65rem; + +$timeline-color: #808080; +$timeline-width: 2px; + +@import "../../vendor/twbs/bootstrap/scss/variables"; + +$font-family-sans-serif: list.join( + ("LXGW WenKai TC", cursive), + $font-family-sans-serif +); + +// required parts +@import "../../vendor/twbs/bootstrap/scss/maps"; +@import "../../vendor/twbs/bootstrap/scss/mixins"; +@import "../../vendor/twbs/bootstrap/scss/root"; +@import "../../vendor/twbs/bootstrap/scss/utilities"; + +// other components +@import "../../vendor/twbs/bootstrap/scss/reboot"; +@import "../../vendor/twbs/bootstrap/scss/type"; +@import "../../vendor/twbs/bootstrap/scss/images"; +@import "../../vendor/twbs/bootstrap/scss/containers"; +@import "../../vendor/twbs/bootstrap/scss/grid"; +@import "../../vendor/twbs/bootstrap/scss/tables"; +@import "../../vendor/twbs/bootstrap/scss/forms"; +@import "../../vendor/twbs/bootstrap/scss/buttons"; +@import "../../vendor/twbs/bootstrap/scss/transitions"; +@import "../../vendor/twbs/bootstrap/scss/dropdown"; +@import "../../vendor/twbs/bootstrap/scss/button-group"; +@import "../../vendor/twbs/bootstrap/scss/nav"; +@import "../../vendor/twbs/bootstrap/scss/navbar"; +@import "../../vendor/twbs/bootstrap/scss/card"; +@import "../../vendor/twbs/bootstrap/scss/breadcrumb"; +@import "../../vendor/twbs/bootstrap/scss/pagination"; +@import "../../vendor/twbs/bootstrap/scss/badge"; +@import "../../vendor/twbs/bootstrap/scss/alert"; +@import "../../vendor/twbs/bootstrap/scss/list-group"; +@import "../../vendor/twbs/bootstrap/scss/close"; +@import "../../vendor/twbs/bootstrap/scss/modal"; +@import "../../vendor/twbs/bootstrap/scss/tooltip"; +@import "../../vendor/twbs/bootstrap/scss/spinners"; + +// Helpers +@import "../../vendor/twbs/bootstrap/scss/helpers"; + +// Utilities +@import "../../vendor/twbs/bootstrap/scss/utilities/api"; diff --git a/compose.yaml b/compose.yaml index 2b304be..e27e85a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -43,6 +43,8 @@ services: test: set -o pipefail;curl -fsS http://localhost:7700/health | grep -q '{"status":"available"}' retries: 3 timeout: 5s + sqlrunner: + image: ghcr.io/database-playground/sqlrunner-v2:main php: image: ${IMAGES_PREFIX:-}app-php restart: unless-stopped @@ -52,6 +54,7 @@ services: REDIS_URI: "redis://redis:6379" MEILISEARCH_URL: "http://meilisearch:7700" MEILISEARCH_API_KEY: ${MEILI_MASTER_KEY:-!MasterChangeMe!} + SQLRUNNER_URL: "http://sqlrunner:8080" volumes: - caddy_data:/data - caddy_config:/config diff --git a/composer.lock b/composer.lock index d057665..631ae23 100644 --- a/composer.lock +++ b/composer.lock @@ -351,12 +351,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "e6986fece692c26064700a589416b9747e4801ec" + "reference": "9c6e7c25653324c28791ebf70554b39cc7b92a68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/e6986fece692c26064700a589416b9747e4801ec", - "reference": "e6986fece692c26064700a589416b9747e4801ec", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/9c6e7c25653324c28791ebf70554b39cc7b92a68", + "reference": "9c6e7c25653324c28791ebf70554b39cc7b92a68", "shasum": "" }, "require": { @@ -456,7 +456,7 @@ "type": "tidelift" } ], - "time": "2024-11-15T15:01:13+00:00" + "time": "2024-11-24T16:17:44+00:00" }, { "name": "doctrine/deprecations", @@ -512,12 +512,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "f604e324a923444ef7e0be59cf87ec81714bb91c" + "reference": "9fe3b63a1d0db6a4cb308444b6d663bea5acefec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/f604e324a923444ef7e0be59cf87ec81714bb91c", - "reference": "f604e324a923444ef7e0be59cf87ec81714bb91c", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/9fe3b63a1d0db6a4cb308444b6d663bea5acefec", + "reference": "9fe3b63a1d0db6a4cb308444b6d663bea5acefec", "shasum": "" }, "require": { @@ -531,7 +531,7 @@ "symfony/console": "^5.4 || ^6.0 || ^7.0", "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", "symfony/deprecation-contracts": "^2.1 || ^3", - "symfony/doctrine-bridge": "^5.4.46 || ^6.4.3 || ^7.0.3", + "symfony/doctrine-bridge": "^5.4.46 || ~6.3.12 || ^6.4.3 || ^7.0.3", "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0", "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.1.1 || ^2.0 || ^3" @@ -624,7 +624,7 @@ "type": "tidelift" } ], - "time": "2024-11-18T14:11:58+00:00" + "time": "2024-11-26T16:41:24+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", @@ -721,12 +721,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/event-manager.git", - "reference": "42b8e975f54469a863ebb7523c74416734e57199" + "reference": "0ceae6fc4e647486dae93de00feed49b8e8322fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/42b8e975f54469a863ebb7523c74416734e57199", - "reference": "42b8e975f54469a863ebb7523c74416734e57199", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/0ceae6fc4e647486dae93de00feed49b8e8322fb", + "reference": "0ceae6fc4e647486dae93de00feed49b8e8322fb", "shasum": "" }, "require": { @@ -739,8 +739,7 @@ "doctrine/coding-standard": "^12", "phpdocumentor/guides-cli": "^1.4", "phpstan/phpstan": "^1.8.8", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.24" + "phpunit/phpunit": "^10.5" }, "default-branch": true, "type": "library", @@ -806,7 +805,7 @@ "type": "tidelift" } ], - "time": "2024-11-19T07:56:31+00:00" + "time": "2024-11-26T10:59:05+00:00" }, { "name": "doctrine/inflector", @@ -905,12 +904,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "be6a7e86c74841eac964ae16853e4036a6a319d0" + "reference": "9e3ae34de52dd65590c4277d5cdde8f39f12418b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/be6a7e86c74841eac964ae16853e4036a6a319d0", - "reference": "be6a7e86c74841eac964ae16853e4036a6a319d0", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/9e3ae34de52dd65590c4277d5cdde8f39f12418b", + "reference": "9e3ae34de52dd65590c4277d5cdde8f39f12418b", "shasum": "" }, "require": { @@ -968,7 +967,7 @@ "type": "tidelift" } ], - "time": "2024-10-16T22:06:28+00:00" + "time": "2024-11-25T19:21:52+00:00" }, { "name": "doctrine/lexer", @@ -1156,12 +1155,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "5013d5dbef535eb892369c5bbfcdf6541bce6eba" + "reference": "50d7a0f95ea6b8623589a90a2c51ddb7389f71c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/5013d5dbef535eb892369c5bbfcdf6541bce6eba", - "reference": "5013d5dbef535eb892369c5bbfcdf6541bce6eba", + "url": "https://api.github.com/repos/doctrine/orm/zipball/50d7a0f95ea6b8623589a90a2c51ddb7389f71c2", + "reference": "50d7a0f95ea6b8623589a90a2c51ddb7389f71c2", "shasum": "" }, "require": { @@ -1239,7 +1238,7 @@ "issues": "https://github.com/doctrine/orm/issues", "source": "https://github.com/doctrine/orm/tree/3.4.x" }, - "time": "2024-11-18T11:18:06+00:00" + "time": "2024-11-23T21:01:13+00:00" }, { "name": "doctrine/persistence", @@ -1344,12 +1343,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "b784cbde727cf806721451dde40eff4fec3bbe86" + "reference": "b4068e8d1a5168769ce65410bab76a8362e04da0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/b784cbde727cf806721451dde40eff4fec3bbe86", - "reference": "b784cbde727cf806721451dde40eff4fec3bbe86", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/b4068e8d1a5168769ce65410bab76a8362e04da0", + "reference": "b4068e8d1a5168769ce65410bab76a8362e04da0", "shasum": "" }, "require": { @@ -1391,9 +1390,9 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.5.1" + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.x" }, - "time": "2024-10-21T18:21:57+00:00" + "time": "2024-11-25T11:48:05+00:00" }, { "name": "easycorp/easyadmin-bundle", @@ -1401,12 +1400,12 @@ "source": { "type": "git", "url": "https://github.com/EasyCorp/EasyAdminBundle.git", - "reference": "cfeb013cfff2887a08ab9b79c6dc63ba50f32044" + "reference": "abe5cac99c21d01ad497b6af86fc0e5ecb632cab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/EasyCorp/EasyAdminBundle/zipball/cfeb013cfff2887a08ab9b79c6dc63ba50f32044", - "reference": "cfeb013cfff2887a08ab9b79c6dc63ba50f32044", + "url": "https://api.github.com/repos/EasyCorp/EasyAdminBundle/zipball/abe5cac99c21d01ad497b6af86fc0e5ecb632cab", + "reference": "abe5cac99c21d01ad497b6af86fc0e5ecb632cab", "shasum": "" }, "require": { @@ -1433,6 +1432,7 @@ "symfony/translation": "^5.4|^6.0|^7.0", "symfony/twig-bundle": "^5.4|^6.0|^7.0", "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/ux-twig-component": "^2.21", "symfony/validator": "^5.4|^6.0|^7.0" }, "require-dev": { @@ -1481,7 +1481,7 @@ ], "support": { "issues": "https://github.com/EasyCorp/EasyAdminBundle/issues", - "source": "https://github.com/EasyCorp/EasyAdminBundle/tree/4.x" + "source": "https://github.com/EasyCorp/EasyAdminBundle/tree/v4.16.2" }, "funding": [ { @@ -1489,7 +1489,7 @@ "type": "github" } ], - "time": "2024-11-16T10:00:31+00:00" + "time": "2024-11-26T19:48:14+00:00" }, { "name": "egulias/email-validator", @@ -3422,12 +3422,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/asset-mapper.git", - "reference": "cb7eeff8730bd6e02837cc1f31c9ad073235e394" + "reference": "ffb733232bb6bb85ef6a994f47c817e7c2ecab9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/asset-mapper/zipball/cb7eeff8730bd6e02837cc1f31c9ad073235e394", - "reference": "cb7eeff8730bd6e02837cc1f31c9ad073235e394", + "url": "https://api.github.com/repos/symfony/asset-mapper/zipball/ffb733232bb6bb85ef6a994f47c817e7c2ecab9c", + "reference": "ffb733232bb6bb85ef6a994f47c817e7c2ecab9c", "shasum": "" }, "require": { @@ -3493,7 +3493,7 @@ "type": "tidelift" } ], - "time": "2024-11-09T09:29:03+00:00" + "time": "2024-11-20T11:17:29+00:00" }, { "name": "symfony/cache", @@ -3501,12 +3501,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "b9ef68c9130fcd3e2db0763e0d6fa866393f2df6" + "reference": "2c926bc348184b4b235f2200fcbe8fcf3c8c5b8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/b9ef68c9130fcd3e2db0763e0d6fa866393f2df6", - "reference": "b9ef68c9130fcd3e2db0763e0d6fa866393f2df6", + "url": "https://api.github.com/repos/symfony/cache/zipball/2c926bc348184b4b235f2200fcbe8fcf3c8c5b8a", + "reference": "2c926bc348184b4b235f2200fcbe8fcf3c8c5b8a", "shasum": "" }, "require": { @@ -3591,7 +3591,7 @@ "type": "tidelift" } ], - "time": "2024-11-05T15:35:02+00:00" + "time": "2024-11-25T15:21:05+00:00" }, { "name": "symfony/cache-contracts", @@ -3918,12 +3918,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "5f5dbbaf5193a4fa46b9b699beca14dc9818a53a" + "reference": "a475747af1a1c98272a5471abc35f3da81197c5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/5f5dbbaf5193a4fa46b9b699beca14dc9818a53a", - "reference": "5f5dbbaf5193a4fa46b9b699beca14dc9818a53a", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/a475747af1a1c98272a5471abc35f3da81197c5d", + "reference": "a475747af1a1c98272a5471abc35f3da81197c5d", "shasum": "" }, "require": { @@ -3990,7 +3990,7 @@ "type": "tidelift" } ], - "time": "2024-11-09T09:29:03+00:00" + "time": "2024-11-25T15:45:00+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4066,12 +4066,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "637af590441e1a91fbd8afb8b5f078921a26ec0b" + "reference": "09dbb7c731430335e9ae89ee5054b5f5580c49bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/637af590441e1a91fbd8afb8b5f078921a26ec0b", - "reference": "637af590441e1a91fbd8afb8b5f078921a26ec0b", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/09dbb7c731430335e9ae89ee5054b5f5580c49bf", + "reference": "09dbb7c731430335e9ae89ee5054b5f5580c49bf", "shasum": "" }, "require": { @@ -4102,7 +4102,7 @@ }, "require-dev": { "doctrine/collections": "^1.8|^2.0", - "doctrine/data-fixtures": "^1.1", + "doctrine/data-fixtures": "^1.1|^2", "doctrine/dbal": "^3.6|^4", "doctrine/orm": "^2.15|^3", "psr/log": "^1|^2|^3", @@ -4167,7 +4167,7 @@ "type": "tidelift" } ], - "time": "2024-11-15T10:15:51+00:00" + "time": "2024-11-25T12:10:02+00:00" }, { "name": "symfony/doctrine-messenger", @@ -4247,12 +4247,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "07b2c58767aadc8ed96b26fadc513a0d00ccd084" + "reference": "28347a897771d0c28e99b75166dd2689099f3045" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/07b2c58767aadc8ed96b26fadc513a0d00ccd084", - "reference": "07b2c58767aadc8ed96b26fadc513a0d00ccd084", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/28347a897771d0c28e99b75166dd2689099f3045", + "reference": "28347a897771d0c28e99b75166dd2689099f3045", "shasum": "" }, "require": { @@ -4313,7 +4313,7 @@ "type": "tidelift" } ], - "time": "2024-09-28T12:51:41+00:00" + "time": "2024-11-27T11:18:42+00:00" }, { "name": "symfony/error-handler", @@ -4752,12 +4752,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "73c00ef4ced12b43647a37c184f05650c636a539" + "reference": "264cff30f52f12149aff92bbc23e78160a45c2f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/73c00ef4ced12b43647a37c184f05650c636a539", - "reference": "73c00ef4ced12b43647a37c184f05650c636a539", + "url": "https://api.github.com/repos/symfony/form/zipball/264cff30f52f12149aff92bbc23e78160a45c2f3", + "reference": "264cff30f52f12149aff92bbc23e78160a45c2f3", "shasum": "" }, "require": { @@ -4841,7 +4841,7 @@ "type": "tidelift" } ], - "time": "2024-10-23T06:56:12+00:00" + "time": "2024-11-27T11:55:00+00:00" }, { "name": "symfony/framework-bundle", @@ -4849,12 +4849,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "f95434900e37e035585c57dfadd2315c898027c4" + "reference": "a8d0da4110fe643ab3cde7c938a03e222fe787c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/f95434900e37e035585c57dfadd2315c898027c4", - "reference": "f95434900e37e035585c57dfadd2315c898027c4", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/a8d0da4110fe643ab3cde7c938a03e222fe787c6", + "reference": "a8d0da4110fe643ab3cde7c938a03e222fe787c6", "shasum": "" }, "require": { @@ -4991,7 +4991,7 @@ "type": "tidelift" } ], - "time": "2024-11-15T10:15:51+00:00" + "time": "2024-11-20T16:27:35+00:00" }, { "name": "symfony/http-client", @@ -4999,12 +4999,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "3ab57502ef07dc89bbe14d45a2f7085aec1a0477" + "reference": "99ceaedb5fc35111147beff1f6973aafdcd2db69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/3ab57502ef07dc89bbe14d45a2f7085aec1a0477", - "reference": "3ab57502ef07dc89bbe14d45a2f7085aec1a0477", + "url": "https://api.github.com/repos/symfony/http-client/zipball/99ceaedb5fc35111147beff1f6973aafdcd2db69", + "reference": "99ceaedb5fc35111147beff1f6973aafdcd2db69", "shasum": "" }, "require": { @@ -5086,7 +5086,7 @@ "type": "tidelift" } ], - "time": "2024-11-19T10:32:52+00:00" + "time": "2024-11-27T12:19:59+00:00" }, { "name": "symfony/http-client-contracts", @@ -5094,12 +5094,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "e34b200cdbcfe17b1047838bd68e74f045a797eb" + "reference": "7917bba9674e386bc2726c4bb9ad5440f7831d66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/e34b200cdbcfe17b1047838bd68e74f045a797eb", - "reference": "e34b200cdbcfe17b1047838bd68e74f045a797eb", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/7917bba9674e386bc2726c4bb9ad5440f7831d66", + "reference": "7917bba9674e386bc2726c4bb9ad5440f7831d66", "shasum": "" }, "require": { @@ -5165,7 +5165,7 @@ "type": "tidelift" } ], - "time": "2024-11-13T18:58:46+00:00" + "time": "2024-11-19T10:11:42+00:00" }, { "name": "symfony/http-foundation", @@ -5251,12 +5251,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "873703cd7998a920f0047354b5ba94982a7f307f" + "reference": "8672d96bd84b000f15c2b05724ef0b91fd4ac93f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/873703cd7998a920f0047354b5ba94982a7f307f", - "reference": "873703cd7998a920f0047354b5ba94982a7f307f", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/8672d96bd84b000f15c2b05724ef0b91fd4ac93f", + "reference": "8672d96bd84b000f15c2b05724ef0b91fd4ac93f", "shasum": "" }, "require": { @@ -5357,7 +5357,7 @@ "type": "tidelift" } ], - "time": "2024-11-13T15:19:04+00:00" + "time": "2024-11-20T11:17:29+00:00" }, { "name": "symfony/intl", @@ -5365,12 +5365,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "9692d473550c45c854f70fac212af1b137646150" + "reference": "76bb3462c6c308f8bd97d3c178c2626ae44d4dea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/9692d473550c45c854f70fac212af1b137646150", - "reference": "9692d473550c45c854f70fac212af1b137646150", + "url": "https://api.github.com/repos/symfony/intl/zipball/76bb3462c6c308f8bd97d3c178c2626ae44d4dea", + "reference": "76bb3462c6c308f8bd97d3c178c2626ae44d4dea", "shasum": "" }, "require": { @@ -5443,7 +5443,7 @@ "type": "tidelift" } ], - "time": "2024-11-08T15:48:14+00:00" + "time": "2024-11-25T14:26:33+00:00" }, { "name": "symfony/line-notify-notifier", @@ -5600,12 +5600,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "2bcecf5e3b2c1907d8a2bcb70c0df41138d5b1c3" + "reference": "e4d358702fb66e4c8a2af08e90e7271a62de39cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/2bcecf5e3b2c1907d8a2bcb70c0df41138d5b1c3", - "reference": "2bcecf5e3b2c1907d8a2bcb70c0df41138d5b1c3", + "url": "https://api.github.com/repos/symfony/mailer/zipball/e4d358702fb66e4c8a2af08e90e7271a62de39cc", + "reference": "e4d358702fb66e4c8a2af08e90e7271a62de39cc", "shasum": "" }, "require": { @@ -5672,7 +5672,7 @@ "type": "tidelift" } ], - "time": "2024-11-15T13:52:25+00:00" + "time": "2024-11-25T15:21:05+00:00" }, { "name": "symfony/messenger", @@ -5680,12 +5680,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/messenger.git", - "reference": "ca6f254b664eb692cf17131ed869b01734b8627c" + "reference": "2512b9bc1e7093c8bd5adec579a364a198059f4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/messenger/zipball/ca6f254b664eb692cf17131ed869b01734b8627c", - "reference": "ca6f254b664eb692cf17131ed869b01734b8627c", + "url": "https://api.github.com/repos/symfony/messenger/zipball/2512b9bc1e7093c8bd5adec579a364a198059f4d", + "reference": "2512b9bc1e7093c8bd5adec579a364a198059f4d", "shasum": "" }, "require": { @@ -5759,7 +5759,7 @@ "type": "tidelift" } ], - "time": "2024-11-09T09:29:03+00:00" + "time": "2024-11-26T10:00:31+00:00" }, { "name": "symfony/mime", @@ -5767,12 +5767,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "f31946de86ef8fcf48ae76652542f29df2ef4428" + "reference": "cc84a4b81f62158c3846ac7ff10f696aae2b524d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/f31946de86ef8fcf48ae76652542f29df2ef4428", - "reference": "f31946de86ef8fcf48ae76652542f29df2ef4428", + "url": "https://api.github.com/repos/symfony/mime/zipball/cc84a4b81f62158c3846ac7ff10f696aae2b524d", + "reference": "cc84a4b81f62158c3846ac7ff10f696aae2b524d", "shasum": "" }, "require": { @@ -5843,7 +5843,7 @@ "type": "tidelift" } ], - "time": "2024-11-10T09:50:45+00:00" + "time": "2024-11-23T09:19:39+00:00" }, { "name": "symfony/monolog-bridge", @@ -6089,12 +6089,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "4f69e6b9745493ea38e5ffdc937e41e7145aa04c" + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/4f69e6b9745493ea38e5ffdc937e41e7145aa04c", - "reference": "4f69e6b9745493ea38e5ffdc937e41e7145aa04c", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50", "shasum": "" }, "require": { @@ -6148,7 +6148,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2024-11-20T11:17:29+00:00" }, { "name": "symfony/password-hasher", @@ -6933,12 +6933,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "8e868c566bb2bee2fe7839c12abeebc83846d664" + "reference": "b00580d9d7c9654e1df95df85105d0da67418b3f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/8e868c566bb2bee2fe7839c12abeebc83846d664", - "reference": "8e868c566bb2bee2fe7839c12abeebc83846d664", + "url": "https://api.github.com/repos/symfony/property-info/zipball/b00580d9d7c9654e1df95df85105d0da67418b3f", + "reference": "b00580d9d7c9654e1df95df85105d0da67418b3f", "shasum": "" }, "require": { @@ -6949,8 +6949,7 @@ "conflict": { "phpdocumentor/reflection-docblock": "<5.2", "phpdocumentor/type-resolver": "<1.5.1", - "symfony/dependency-injection": "<6.4", - "symfony/serializer": "<6.4" + "symfony/dependency-injection": "<6.4" }, "require-dev": { "phpdocumentor/reflection-docblock": "^5.2", @@ -7009,7 +7008,7 @@ "type": "tidelift" } ], - "time": "2024-11-15T10:15:51+00:00" + "time": "2024-11-27T09:50:52+00:00" }, { "name": "symfony/routing", @@ -7017,12 +7016,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "40f0d287bd9bcf61dbbc3d43d84f973c0d26c4cf" + "reference": "e10a2450fa957af6c448b9b93c9010a4e4c0725e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/40f0d287bd9bcf61dbbc3d43d84f973c0d26c4cf", - "reference": "40f0d287bd9bcf61dbbc3d43d84f973c0d26c4cf", + "url": "https://api.github.com/repos/symfony/routing/zipball/e10a2450fa957af6c448b9b93c9010a4e4c0725e", + "reference": "e10a2450fa957af6c448b9b93c9010a4e4c0725e", "shasum": "" }, "require": { @@ -7090,7 +7089,7 @@ "type": "tidelift" } ], - "time": "2024-11-13T16:15:23+00:00" + "time": "2024-11-25T11:08:51+00:00" }, { "name": "symfony/runtime", @@ -7283,12 +7282,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/security-core.git", - "reference": "c6a568699c8d5c0decf09a5bb4bf7642fbe5756d" + "reference": "fdbf318b939a86f89b0c071f60b9d551261d3cc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-core/zipball/c6a568699c8d5c0decf09a5bb4bf7642fbe5756d", - "reference": "c6a568699c8d5c0decf09a5bb4bf7642fbe5756d", + "url": "https://api.github.com/repos/symfony/security-core/zipball/fdbf318b939a86f89b0c071f60b9d551261d3cc1", + "reference": "fdbf318b939a86f89b0c071f60b9d551261d3cc1", "shasum": "" }, "require": { @@ -7362,7 +7361,7 @@ "type": "tidelift" } ], - "time": "2024-11-19T10:04:01+00:00" + "time": "2024-11-27T09:50:52+00:00" }, { "name": "symfony/security-csrf", @@ -7528,12 +7527,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "5fff3abe545e26b2e024d18ad6e3f797e649f513" + "reference": "3f5ed9f5e6c02e3853109190ba38408f5e1d2dd0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/5fff3abe545e26b2e024d18ad6e3f797e649f513", - "reference": "5fff3abe545e26b2e024d18ad6e3f797e649f513", + "url": "https://api.github.com/repos/symfony/serializer/zipball/3f5ed9f5e6c02e3853109190ba38408f5e1d2dd0", + "reference": "3f5ed9f5e6c02e3853109190ba38408f5e1d2dd0", "shasum": "" }, "require": { @@ -7618,7 +7617,7 @@ "type": "tidelift" } ], - "time": "2024-11-15T10:15:51+00:00" + "time": "2024-11-25T15:21:05+00:00" }, { "name": "symfony/service-contracts", @@ -7710,12 +7709,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/stimulus-bundle.git", - "reference": "af341c225850a57f025ff56e0fd8c7a9c4a145b3" + "reference": "2e840a3b12f06b33441cc3eb8907f51b806a7e4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/af341c225850a57f025ff56e0fd8c7a9c4a145b3", - "reference": "af341c225850a57f025ff56e0fd8c7a9c4a145b3", + "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/2e840a3b12f06b33441cc3eb8907f51b806a7e4b", + "reference": "2e840a3b12f06b33441cc3eb8907f51b806a7e4b", "shasum": "" }, "require": { @@ -7772,7 +7771,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T20:44:36+00:00" + "time": "2024-11-20T07:57:38+00:00" }, { "name": "symfony/stopwatch", @@ -8103,12 +8102,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "b7efcdfbd484bbf37214a6ce6c921d9502a96c75" + "reference": "9958f5a5b6640734fe4b24c18897191f77a02c61" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/b7efcdfbd484bbf37214a6ce6c921d9502a96c75", - "reference": "b7efcdfbd484bbf37214a6ce6c921d9502a96c75", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/9958f5a5b6640734fe4b24c18897191f77a02c61", + "reference": "9958f5a5b6640734fe4b24c18897191f77a02c61", "shasum": "" }, "require": { @@ -8205,7 +8204,7 @@ "type": "tidelift" } ], - "time": "2024-11-14T17:50:56+00:00" + "time": "2024-11-25T14:26:33+00:00" }, { "name": "symfony/twig-bundle", @@ -8451,12 +8450,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/ux-chartjs.git", - "reference": "4c1039f35de42f398330439d612ecef361e818cc" + "reference": "32476b05eb1bd76dc049a2747cf398e76a9a44a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-chartjs/zipball/4c1039f35de42f398330439d612ecef361e818cc", - "reference": "4c1039f35de42f398330439d612ecef361e818cc", + "url": "https://api.github.com/repos/symfony/ux-chartjs/zipball/32476b05eb1bd76dc049a2747cf398e76a9a44a5", + "reference": "32476b05eb1bd76dc049a2747cf398e76a9a44a5", "shasum": "" }, "require": { @@ -8524,7 +8523,7 @@ "type": "tidelift" } ], - "time": "2024-11-02T19:57:29+00:00" + "time": "2024-11-20T07:57:38+00:00" }, { "name": "symfony/ux-live-component", @@ -8532,12 +8531,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/ux-live-component.git", - "reference": "0ddcd67b1fa1d795506c7302cb0347133cf8b3b7" + "reference": "b1e06e46dc00b6bf08d6ec24d278288bf71f3cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-live-component/zipball/0ddcd67b1fa1d795506c7302cb0347133cf8b3b7", - "reference": "0ddcd67b1fa1d795506c7302cb0347133cf8b3b7", + "url": "https://api.github.com/repos/symfony/ux-live-component/zipball/b1e06e46dc00b6bf08d6ec24d278288bf71f3cf8", + "reference": "b1e06e46dc00b6bf08d6ec24d278288bf71f3cf8", "shasum": "" }, "require": { @@ -8619,7 +8618,7 @@ "type": "tidelift" } ], - "time": "2024-11-14T17:38:27+00:00" + "time": "2024-11-24T22:18:30+00:00" }, { "name": "symfony/ux-turbo", @@ -8627,12 +8626,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/ux-turbo.git", - "reference": "418f639e3aab5b1b68cd92fad558db53ed90484f" + "reference": "23deaf13a4de289b99e86828172ee0ee065d8f9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/418f639e3aab5b1b68cd92fad558db53ed90484f", - "reference": "418f639e3aab5b1b68cd92fad558db53ed90484f", + "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/23deaf13a4de289b99e86828172ee0ee065d8f9c", + "reference": "23deaf13a4de289b99e86828172ee0ee065d8f9c", "shasum": "" }, "require": { @@ -8718,7 +8717,7 @@ "type": "tidelift" } ], - "time": "2024-11-02T19:57:29+00:00" + "time": "2024-11-20T07:57:38+00:00" }, { "name": "symfony/ux-twig-component", @@ -8726,12 +8725,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/ux-twig-component.git", - "reference": "c473b98f85237417df5ea1500797cd95ba3330c6" + "reference": "03177a494399fbdcbb1f5f2aee017ccf8df581d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/c473b98f85237417df5ea1500797cd95ba3330c6", - "reference": "c473b98f85237417df5ea1500797cd95ba3330c6", + "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/03177a494399fbdcbb1f5f2aee017ccf8df581d9", + "reference": "03177a494399fbdcbb1f5f2aee017ccf8df581d9", "shasum": "" }, "require": { @@ -8802,7 +8801,7 @@ "type": "tidelift" } ], - "time": "2024-11-14T17:38:27+00:00" + "time": "2024-11-23T06:59:34+00:00" }, { "name": "symfony/validator", @@ -8810,12 +8809,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "3ed5b674bac8063f41890bcadc74b2349a640a3e" + "reference": "ddad20aa8cf7a45a9d6300e5776b8d252dc3524b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/3ed5b674bac8063f41890bcadc74b2349a640a3e", - "reference": "3ed5b674bac8063f41890bcadc74b2349a640a3e", + "url": "https://api.github.com/repos/symfony/validator/zipball/ddad20aa8cf7a45a9d6300e5776b8d252dc3524b", + "reference": "ddad20aa8cf7a45a9d6300e5776b8d252dc3524b", "shasum": "" }, "require": { @@ -8899,7 +8898,7 @@ "type": "tidelift" } ], - "time": "2024-11-19T10:04:01+00:00" + "time": "2024-11-27T09:50:52+00:00" }, { "name": "symfony/var-dumper", @@ -9194,12 +9193,12 @@ "source": { "type": "git", "url": "https://github.com/twbs/bootstrap.git", - "reference": "cacbdc680ecdfee5f0c7fbb876ad15188eaf697d" + "reference": "ec96eacd0e6f297a64ee058b22ce9f567c0860e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twbs/bootstrap/zipball/cacbdc680ecdfee5f0c7fbb876ad15188eaf697d", - "reference": "cacbdc680ecdfee5f0c7fbb876ad15188eaf697d", + "url": "https://api.github.com/repos/twbs/bootstrap/zipball/ec96eacd0e6f297a64ee058b22ce9f567c0860e3", + "reference": "ec96eacd0e6f297a64ee058b22ce9f567c0860e3", "shasum": "" }, "replace": { @@ -9237,7 +9236,7 @@ "issues": "https://github.com/twbs/bootstrap/issues", "source": "https://github.com/twbs/bootstrap/tree/main" }, - "time": "2024-11-14T10:12:33+00:00" + "time": "2024-11-22T09:54:10+00:00" }, { "name": "twig/extra-bundle", @@ -9393,12 +9392,12 @@ "source": { "type": "git", "url": "https://github.com/twigphp/string-extra.git", - "reference": "b098bd4910aba185afb70e40b8e23071fce8c656" + "reference": "3f90208078a7d55ad4a561301ee3929d3e3840e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/string-extra/zipball/b098bd4910aba185afb70e40b8e23071fce8c656", - "reference": "b098bd4910aba185afb70e40b8e23071fce8c656", + "url": "https://api.github.com/repos/twigphp/string-extra/zipball/3f90208078a7d55ad4a561301ee3929d3e3840e0", + "reference": "3f90208078a7d55ad4a561301ee3929d3e3840e0", "shasum": "" }, "require": { @@ -9441,7 +9440,7 @@ "unicode" ], "support": { - "source": "https://github.com/twigphp/string-extra/tree/v3.15.0" + "source": "https://github.com/twigphp/string-extra/tree/3.x" }, "funding": [ { @@ -9453,7 +9452,7 @@ "type": "tidelift" } ], - "time": "2024-11-03T14:08:48+00:00" + "time": "2024-11-20T13:10:15+00:00" }, { "name": "twig/twig", @@ -9665,12 +9664,12 @@ "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "deb3871d20d5012eb5faa5a9caa71c44f151db49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/deb3871d20d5012eb5faa5a9caa71c44f151db49", + "reference": "deb3871d20d5012eb5faa5a9caa71c44f151db49", "shasum": "" }, "require": { @@ -9681,6 +9680,7 @@ }, "require-dev": { "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", "phpstan/phpstan-strict-rules": "^1 || ^2", "phpunit/phpunit": "^8 || ^9" }, @@ -9721,7 +9721,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/main" }, "funding": [ { @@ -9737,7 +9737,7 @@ "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2024-11-20T09:10:58+00:00" }, { "name": "composer/xdebug-handler", @@ -9919,12 +9919,12 @@ "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "42f36922248a82c869ca80230d5e986ca8d2aa94" + "reference": "4fe0771f27e0d61e227d8b5a5b0fef8e5d138c1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/42f36922248a82c869ca80230d5e986ca8d2aa94", - "reference": "42f36922248a82c869ca80230d5e986ca8d2aa94", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/4fe0771f27e0d61e227d8b5a5b0fef8e5d138c1b", + "reference": "4fe0771f27e0d61e227d8b5a5b0fef8e5d138c1b", "shasum": "" }, "require": { @@ -10015,7 +10015,7 @@ "type": "github" } ], - "time": "2024-11-19T14:25:11+00:00" + "time": "2024-11-25T12:11:49+00:00" }, { "name": "masterminds/html5", @@ -10090,12 +10090,12 @@ "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + "reference": "4764e040f8743e92b86c36f488f32d0265dd1dae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/4764e040f8743e92b86c36f488f32d0265dd1dae", + "reference": "4764e040f8743e92b86c36f488f32d0265dd1dae", "shasum": "" }, "require": { @@ -10135,7 +10135,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.x" }, "funding": [ { @@ -10143,7 +10143,7 @@ "type": "tidelift" } ], - "time": "2024-11-08T17:47:46+00:00" + "time": "2024-11-26T13:04:49+00:00" }, { "name": "nikic/php-parser", @@ -10377,12 +10377,12 @@ "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "3b47bc0fbaa8e78fa8c498ee511d12012e310f63" + "reference": "f9e84e0add3ebffc48f7a9eaed0d869f8112dd1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3b47bc0fbaa8e78fa8c498ee511d12012e310f63", - "reference": "3b47bc0fbaa8e78fa8c498ee511d12012e310f63", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f9e84e0add3ebffc48f7a9eaed0d869f8112dd1e", + "reference": "f9e84e0add3ebffc48f7a9eaed0d869f8112dd1e", "shasum": "" }, "require": { @@ -10428,7 +10428,7 @@ "type": "github" } ], - "time": "2024-11-19T17:53:00+00:00" + "time": "2024-11-27T15:10:00+00:00" }, { "name": "phpstan/phpstan-doctrine", @@ -10436,17 +10436,17 @@ "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "90c42756b2d7c3660b423d328622d4dfa2194487" + "reference": "410ed26764d092437dcfe6179676d7089b784a84" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/90c42756b2d7c3660b423d328622d4dfa2194487", - "reference": "90c42756b2d7c3660b423d328622d4dfa2194487", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/410ed26764d092437dcfe6179676d7089b784a84", + "reference": "410ed26764d092437dcfe6179676d7089b784a84", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.0" + "phpstan/phpstan": "^2.0.3" }, "conflict": { "doctrine/collections": "<1.0", @@ -10498,9 +10498,9 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.0" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.x" }, - "time": "2024-11-09T17:34:32+00:00" + "time": "2024-11-20T15:58:40+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -11004,12 +11004,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "476c6c8e6fcc6107b578fdc6c899d4a183b5bf33" + "reference": "c4ccbf6978839f4113e9ecf6c9ad645429cb4301" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/476c6c8e6fcc6107b578fdc6c899d4a183b5bf33", - "reference": "476c6c8e6fcc6107b578fdc6c899d4a183b5bf33", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c4ccbf6978839f4113e9ecf6c9ad645429cb4301", + "reference": "c4ccbf6978839f4113e9ecf6c9ad645429cb4301", "shasum": "" }, "require": { @@ -11097,7 +11097,7 @@ "type": "tidelift" } ], - "time": "2024-11-16T08:11:02+00:00" + "time": "2024-11-26T13:36:09+00:00" }, { "name": "react/cache", @@ -11407,12 +11407,12 @@ "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + "reference": "5f80055cc21ba7bcd3989e4902061fc12e2bcc1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "url": "https://api.github.com/repos/reactphp/promise/zipball/5f80055cc21ba7bcd3989e4902061fc12e2bcc1d", + "reference": "5f80055cc21ba7bcd3989e4902061fc12e2bcc1d", "shasum": "" }, "require": { @@ -11465,7 +11465,7 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.2.0" + "source": "https://github.com/reactphp/promise/tree/3.x" }, "funding": [ { @@ -11473,7 +11473,7 @@ "type": "open_collective" } ], - "time": "2024-05-24T10:39:05+00:00" + "time": "2024-11-19T18:32:50+00:00" }, { "name": "react/socket", diff --git a/config/packages/cache.yaml b/config/packages/cache.yaml index 6ce81e7..5236446 100644 --- a/config/packages/cache.yaml +++ b/config/packages/cache.yaml @@ -4,11 +4,6 @@ framework: app: cache.adapter.redis_tag_aware default_redis_provider: "%app.redis_uri%" - - pools: - cache.dbrunner: - adapter: cache.adapter.redis_tag_aware - default_lifetime: "12 hour" # Unique name of your app: used to compute stable namespaces for cache keys. #prefix_seed: your_vendor_name/app_name diff --git a/config/packages/csrf.yaml b/config/packages/csrf.yaml new file mode 100644 index 0000000..dd07de8 --- /dev/null +++ b/config/packages/csrf.yaml @@ -0,0 +1,11 @@ +# Enable stateless CSRF protection for forms and logins/logouts +framework: + form: + csrf_protection: + token_id: submit + + csrf_protection: + stateless_token_ids: + - submit + - authenticate + - logout diff --git a/config/packages/sensiolabs_typescript.yaml b/config/packages/sensiolabs_typescript.yaml index 23490e6..7f33f23 100644 --- a/config/packages/sensiolabs_typescript.yaml +++ b/config/packages/sensiolabs_typescript.yaml @@ -1,2 +1,2 @@ sensiolabs_typescript: - swc_version: v1.9.2 + swc_version: v1.9.3 diff --git a/config/routes/easyadmin.yaml b/config/routes/easyadmin.yaml new file mode 100644 index 0000000..f409de2 --- /dev/null +++ b/config/routes/easyadmin.yaml @@ -0,0 +1,3 @@ +easyadmin: + resource: . + type: easyadmin.routes diff --git a/config/services.yaml b/config/services.yaml index f8450f0..0641d8b 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -4,7 +4,7 @@ # Put parameters here that don't need to change on each machine where the app is deployed # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration parameters: - app.dbrunner_host: "%env(DBRUNNER_HOST)%" + app.sqlrunner_url: "%env(SQLRUNNER_URL)%" app.redis_uri: "%env(REDIS_URI)%" app.openai_api_key: "%env(OPENAI_API_KEY)%" @@ -35,3 +35,7 @@ services: App\Service\PromptService: arguments: $apiKey: "%app.openai_api_key%" + + App\Service\SqlRunnerService: + arguments: + $baseUrl: "%app.sqlrunner_url%" diff --git a/devenv.lock b/devenv.lock index ee2f0e2..96d0062 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,10 +3,10 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1732025403, + "lastModified": 1732585607, "owner": "cachix", "repo": "devenv", - "rev": "6473534b5f3a7ae956ee751084bc4bf2391ccc28", + "rev": "a520f05c40ebecaf5e17064b27e28ba8e70c49fb", "type": "github" }, "original": { @@ -19,10 +19,10 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1696426674, + "lastModified": 1732722421, "owner": "edolstra", "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "rev": "9ed2ac151eada2306ca8c418ebd97807bb08f6ac", "type": "github" }, "original": { @@ -53,10 +53,10 @@ }, "nixpkgs": { "locked": { - "lastModified": 1731890469, + "lastModified": 1732617236, "owner": "nixos", "repo": "nixpkgs", - "rev": "5083ec887760adfe12af64830a66807423a859a7", + "rev": "af51545ec9a44eadf3fe3547610a5cdd882bc34e", "type": "github" }, "original": { @@ -68,10 +68,10 @@ }, "nixpkgs-stable": { "locked": { - "lastModified": 1731797254, + "lastModified": 1732632634, "owner": "NixOS", "repo": "nixpkgs", - "rev": "e8c38b73aeb218e27163376a2d617e61a2ad9b59", + "rev": "6f6076c37180ea3a916f84928cf3a714c5207a30", "type": "github" }, "original": { diff --git a/devenv.nix b/devenv.nix index 59b9994..930ad06 100644 --- a/devenv.nix +++ b/devenv.nix @@ -12,7 +12,7 @@ # https://devenv.sh/languages/ languages.php.enable = true; languages.php.version = "8.3"; - languages.php.extensions = [ "apcu" "intl" "opcache" "zip" "redis" "pdo_pgsql" "sysvsem" "xdebug" ]; + languages.php.extensions = [ "apcu" "curl" "intl" "opcache" "zip" "redis" "pdo_pgsql" "sysvsem" "xdebug" ]; languages.php.disableExtensions = [ "soap" ]; languages.php.ini = builtins.readFile ./frankenphp/conf.d/10-app.ini; diff --git a/frankenphp/docker-entrypoint.sh b/frankenphp/docker-entrypoint.sh index 7d56f4e..5ec5f2d 100644 --- a/frankenphp/docker-entrypoint.sh +++ b/frankenphp/docker-entrypoint.sh @@ -33,9 +33,6 @@ if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then fi fi - echo "Cleaning up dbrunner cache..." - php bin/console cache:pool:clear cache.dbrunner || true - echo "Updating Meilisearch indexes..." php bin/console meili:clear || true php bin/console meili:import --update-settings || true diff --git a/importmap.php b/importmap.php index b8eb52d..145004d 100644 --- a/importmap.php +++ b/importmap.php @@ -50,25 +50,25 @@ 'version' => '6.8.0', ], '@codemirror/view' => [ - 'version' => '6.34.3', + 'version' => '6.35.0', ], '@codemirror/state' => [ 'version' => '6.4.1', ], '@codemirror/language' => [ - 'version' => '6.10.3', + 'version' => '6.10.5', ], '@codemirror/commands' => [ 'version' => '6.7.1', ], '@codemirror/search' => [ - 'version' => '6.5.7', + 'version' => '6.5.8', ], '@codemirror/autocomplete' => [ 'version' => '6.18.3', ], '@codemirror/lint' => [ - 'version' => '6.8.2', + 'version' => '6.8.3', ], '@lezer/highlight' => [ 'version' => '1.2.1', @@ -89,6 +89,6 @@ 'version' => '1.0.6', ], '@kurkle/color' => [ - 'version' => '0.3.2', + 'version' => '0.3.4', ], ]; diff --git a/package.json b/package.json index ef9f8c7..603a090 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "@codemirror/state": "^6.4.1", "@symfony/stimulus-bridge": "^3.2.2", "codemirror": "^6.0.1", - "typescript": "^5.6.3" + "typescript": "^5.7.2" }, "packageManager": "pnpm@9.13.2+sha512.88c9c3864450350e65a33587ab801acf946d7c814ed1134da4a924f6df5a2120fd36b46aab68f7cd1d413149112d53c7db3a4136624cfd00ff1846a0c6cef48a", "devDependencies": { @@ -22,6 +22,6 @@ "dprint": "^0.47.5", "eslint": "^9.15.0", "globals": "^15.12.0", - "typescript-eslint": "^8.15.0" + "typescript-eslint": "^8.16.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 542e4cf..60fb856 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ importers: dependencies: "@codemirror/lang-sql": specifier: ^6.8.0 - version: 6.8.0(@codemirror/view@6.34.3) + version: 6.8.0(@codemirror/view@6.35.0) "@codemirror/state": specifier: ^6.4.1 version: 6.4.1 @@ -20,8 +20,8 @@ importers: specifier: ^6.0.1 version: 6.0.1(@lezer/common@1.2.3) typescript: - specifier: ^5.6.3 - version: 5.6.3 + specifier: ^5.7.2 + version: 5.7.2 devDependencies: "@eslint/js": specifier: ^9.15.0 @@ -45,8 +45,8 @@ importers: specifier: ^15.12.0 version: 15.12.0 typescript-eslint: - specifier: ^8.15.0 - version: 8.15.0(eslint@9.15.0)(typescript@5.6.3) + specifier: ^8.16.0 + version: 8.16.0(eslint@9.15.0)(typescript@5.7.2) packages: "@codemirror/autocomplete@6.18.3": @@ -69,19 +69,19 @@ packages: integrity: sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==, } - "@codemirror/language@6.10.3": + "@codemirror/language@6.10.5": resolution: { - integrity: sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==, + integrity: sha512-sECWJyNmwqw6mSO6Qf0IVPHwhEnuYbqHBZaaIbdcXtZ6Y2r5vU/dxgC7K1ppWaJFy8XGtTBC0Pd60qI7NfJreQ==, } - "@codemirror/lint@6.8.2": + "@codemirror/lint@6.8.3": resolution: { - integrity: sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==, + integrity: sha512-GSGfKxCo867P7EX1k2LoCrjuQFeqVgPGRRsSl4J4c0KMkD+k1y6WYvTQkzv0iZ8JhLJDujEvlnMchv4CZQLh3Q==, } - "@codemirror/search@6.5.7": + "@codemirror/search@6.5.8": resolution: { - integrity: sha512-6+iLsXvITWKHYlkgHPCs/qiX4dNzn8N78YfhOFvPtPYCkuXqZq10rAfsUMhOq7O/1VjJqdXRflyExlfVcu/9VQ==, + integrity: sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==, } "@codemirror/state@6.4.1": @@ -89,9 +89,9 @@ packages: integrity: sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==, } - "@codemirror/view@6.34.3": + "@codemirror/view@6.35.0": resolution: { - integrity: sha512-Ph5d+u8DxIeSgssXEakaakImkzBV4+slwIbcxl9oc9evexJhImeu/G8TK7+zp+IFK9KuJ0BdSn6kTBJeH2CHvA==, + integrity: sha512-I0tYy63q5XkaWsJ8QRv5h6ves7kvtrBWjBcnf/bzohFJQc5c14a1AQRdE8QpPF9eMp5Mq2FMm59TCj1gDfE7kw==, } "@dprint/darwin-arm64@0.47.5": @@ -308,9 +308,9 @@ packages: integrity: sha512-wz7kjjRRj8/Lty4B+Kr0LN6Ypc/3SymeCCGSbaXp2leH0ZVg/PriNiOwNj4bD4uphI7A8NXS4b6Gl373sfO5mA==, } - "@typescript-eslint/eslint-plugin@8.15.0": + "@typescript-eslint/eslint-plugin@8.16.0": resolution: { - integrity: sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==, + integrity: sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: @@ -321,9 +321,9 @@ packages: typescript: optional: true - "@typescript-eslint/parser@8.15.0": + "@typescript-eslint/parser@8.16.0": resolution: { - integrity: sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==, + integrity: sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: @@ -333,15 +333,15 @@ packages: typescript: optional: true - "@typescript-eslint/scope-manager@8.15.0": + "@typescript-eslint/scope-manager@8.16.0": resolution: { - integrity: sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==, + integrity: sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - "@typescript-eslint/type-utils@8.15.0": + "@typescript-eslint/type-utils@8.16.0": resolution: { - integrity: sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==, + integrity: sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: @@ -351,15 +351,15 @@ packages: typescript: optional: true - "@typescript-eslint/types@8.15.0": + "@typescript-eslint/types@8.16.0": resolution: { - integrity: sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==, + integrity: sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - "@typescript-eslint/typescript-estree@8.15.0": + "@typescript-eslint/typescript-estree@8.16.0": resolution: { - integrity: sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==, + integrity: sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: @@ -368,9 +368,9 @@ packages: typescript: optional: true - "@typescript-eslint/utils@8.15.0": + "@typescript-eslint/utils@8.16.0": resolution: { - integrity: sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==, + integrity: sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: @@ -380,9 +380,9 @@ packages: typescript: optional: true - "@typescript-eslint/visitor-keys@8.15.0": + "@typescript-eslint/visitor-keys@8.16.0": resolution: { - integrity: sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==, + integrity: sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } @@ -938,9 +938,9 @@ packages: } engines: { node: ">=8.0" } - ts-api-utils@1.4.0: + ts-api-utils@1.4.2: resolution: { - integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==, + integrity: sha512-ZF5gQIQa/UmzfvxbHZI3JXN0/Jt+vnAfAviNRAMc491laiK6YCLpCW9ft8oaCRFOTxCZtUTE6XB0ZQAe3olntw==, } engines: { node: ">=16" } peerDependencies: @@ -952,9 +952,9 @@ packages: } engines: { node: ">= 0.8.0" } - typescript-eslint@8.15.0: + typescript-eslint@8.16.0: resolution: { - integrity: sha512-wY4FRGl0ZI+ZU4Jo/yjdBu0lVTSML58pu6PgGtJmCufvzfV565pUF6iACQt092uFOd49iLOTX/sEVmHtbSrS+w==, + integrity: sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: @@ -964,9 +964,9 @@ packages: typescript: optional: true - typescript@5.6.3: + typescript@5.7.2: resolution: { - integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==, + integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==, } engines: { node: ">=14.17" } hasBin: true @@ -1001,24 +1001,24 @@ packages: engines: { node: ">=10" } snapshots: - "@codemirror/autocomplete@6.18.3(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.3)(@lezer/common@1.2.3)": + "@codemirror/autocomplete@6.18.3(@codemirror/language@6.10.5)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)": dependencies: - "@codemirror/language": 6.10.3 + "@codemirror/language": 6.10.5 "@codemirror/state": 6.4.1 - "@codemirror/view": 6.34.3 + "@codemirror/view": 6.35.0 "@lezer/common": 1.2.3 "@codemirror/commands@6.7.1": dependencies: - "@codemirror/language": 6.10.3 + "@codemirror/language": 6.10.5 "@codemirror/state": 6.4.1 - "@codemirror/view": 6.34.3 + "@codemirror/view": 6.35.0 "@lezer/common": 1.2.3 - "@codemirror/lang-sql@6.8.0(@codemirror/view@6.34.3)": + "@codemirror/lang-sql@6.8.0(@codemirror/view@6.35.0)": dependencies: - "@codemirror/autocomplete": 6.18.3(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.3)(@lezer/common@1.2.3) - "@codemirror/language": 6.10.3 + "@codemirror/autocomplete": 6.18.3(@codemirror/language@6.10.5)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) + "@codemirror/language": 6.10.5 "@codemirror/state": 6.4.1 "@lezer/common": 1.2.3 "@lezer/highlight": 1.2.1 @@ -1026,30 +1026,30 @@ snapshots: transitivePeerDependencies: - "@codemirror/view" - "@codemirror/language@6.10.3": + "@codemirror/language@6.10.5": dependencies: "@codemirror/state": 6.4.1 - "@codemirror/view": 6.34.3 + "@codemirror/view": 6.35.0 "@lezer/common": 1.2.3 "@lezer/highlight": 1.2.1 "@lezer/lr": 1.4.2 style-mod: 4.1.2 - "@codemirror/lint@6.8.2": + "@codemirror/lint@6.8.3": dependencies: "@codemirror/state": 6.4.1 - "@codemirror/view": 6.34.3 + "@codemirror/view": 6.35.0 crelt: 1.0.6 - "@codemirror/search@6.5.7": + "@codemirror/search@6.5.8": dependencies: "@codemirror/state": 6.4.1 - "@codemirror/view": 6.34.3 + "@codemirror/view": 6.35.0 crelt: 1.0.6 "@codemirror/state@6.4.1": {} - "@codemirror/view@6.34.3": + "@codemirror/view@6.35.0": dependencies: "@codemirror/state": 6.4.1 style-mod: 4.1.2 @@ -1180,86 +1180,86 @@ snapshots: "@types/webpack-env@1.18.5": {} - "@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0)(typescript@5.6.3))(eslint@9.15.0)(typescript@5.6.3)": + "@typescript-eslint/eslint-plugin@8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2)": dependencies: "@eslint-community/regexpp": 4.12.1 - "@typescript-eslint/parser": 8.15.0(eslint@9.15.0)(typescript@5.6.3) - "@typescript-eslint/scope-manager": 8.15.0 - "@typescript-eslint/type-utils": 8.15.0(eslint@9.15.0)(typescript@5.6.3) - "@typescript-eslint/utils": 8.15.0(eslint@9.15.0)(typescript@5.6.3) - "@typescript-eslint/visitor-keys": 8.15.0 + "@typescript-eslint/parser": 8.16.0(eslint@9.15.0)(typescript@5.7.2) + "@typescript-eslint/scope-manager": 8.16.0 + "@typescript-eslint/type-utils": 8.16.0(eslint@9.15.0)(typescript@5.7.2) + "@typescript-eslint/utils": 8.16.0(eslint@9.15.0)(typescript@5.7.2) + "@typescript-eslint/visitor-keys": 8.16.0 eslint: 9.15.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.4.0(typescript@5.6.3) + ts-api-utils: 1.4.2(typescript@5.7.2) optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - "@typescript-eslint/parser@8.15.0(eslint@9.15.0)(typescript@5.6.3)": + "@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2)": dependencies: - "@typescript-eslint/scope-manager": 8.15.0 - "@typescript-eslint/types": 8.15.0 - "@typescript-eslint/typescript-estree": 8.15.0(typescript@5.6.3) - "@typescript-eslint/visitor-keys": 8.15.0 + "@typescript-eslint/scope-manager": 8.16.0 + "@typescript-eslint/types": 8.16.0 + "@typescript-eslint/typescript-estree": 8.16.0(typescript@5.7.2) + "@typescript-eslint/visitor-keys": 8.16.0 debug: 4.3.7 eslint: 9.15.0 optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - "@typescript-eslint/scope-manager@8.15.0": + "@typescript-eslint/scope-manager@8.16.0": dependencies: - "@typescript-eslint/types": 8.15.0 - "@typescript-eslint/visitor-keys": 8.15.0 + "@typescript-eslint/types": 8.16.0 + "@typescript-eslint/visitor-keys": 8.16.0 - "@typescript-eslint/type-utils@8.15.0(eslint@9.15.0)(typescript@5.6.3)": + "@typescript-eslint/type-utils@8.16.0(eslint@9.15.0)(typescript@5.7.2)": dependencies: - "@typescript-eslint/typescript-estree": 8.15.0(typescript@5.6.3) - "@typescript-eslint/utils": 8.15.0(eslint@9.15.0)(typescript@5.6.3) + "@typescript-eslint/typescript-estree": 8.16.0(typescript@5.7.2) + "@typescript-eslint/utils": 8.16.0(eslint@9.15.0)(typescript@5.7.2) debug: 4.3.7 eslint: 9.15.0 - ts-api-utils: 1.4.0(typescript@5.6.3) + ts-api-utils: 1.4.2(typescript@5.7.2) optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - "@typescript-eslint/types@8.15.0": {} + "@typescript-eslint/types@8.16.0": {} - "@typescript-eslint/typescript-estree@8.15.0(typescript@5.6.3)": + "@typescript-eslint/typescript-estree@8.16.0(typescript@5.7.2)": dependencies: - "@typescript-eslint/types": 8.15.0 - "@typescript-eslint/visitor-keys": 8.15.0 + "@typescript-eslint/types": 8.16.0 + "@typescript-eslint/visitor-keys": 8.16.0 debug: 4.3.7 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 - ts-api-utils: 1.4.0(typescript@5.6.3) + ts-api-utils: 1.4.2(typescript@5.7.2) optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - "@typescript-eslint/utils@8.15.0(eslint@9.15.0)(typescript@5.6.3)": + "@typescript-eslint/utils@8.16.0(eslint@9.15.0)(typescript@5.7.2)": dependencies: "@eslint-community/eslint-utils": 4.4.1(eslint@9.15.0) - "@typescript-eslint/scope-manager": 8.15.0 - "@typescript-eslint/types": 8.15.0 - "@typescript-eslint/typescript-estree": 8.15.0(typescript@5.6.3) + "@typescript-eslint/scope-manager": 8.16.0 + "@typescript-eslint/types": 8.16.0 + "@typescript-eslint/typescript-estree": 8.16.0(typescript@5.7.2) eslint: 9.15.0 optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - "@typescript-eslint/visitor-keys@8.15.0": + "@typescript-eslint/visitor-keys@8.16.0": dependencies: - "@typescript-eslint/types": 8.15.0 + "@typescript-eslint/types": 8.16.0 eslint-visitor-keys: 4.2.0 acorn-jsx@5.3.2(acorn@8.14.0): @@ -1315,13 +1315,13 @@ snapshots: codemirror@6.0.1(@lezer/common@1.2.3): dependencies: - "@codemirror/autocomplete": 6.18.3(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.3)(@lezer/common@1.2.3) + "@codemirror/autocomplete": 6.18.3(@codemirror/language@6.10.5)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) "@codemirror/commands": 6.7.1 - "@codemirror/language": 6.10.3 - "@codemirror/lint": 6.8.2 - "@codemirror/search": 6.5.7 + "@codemirror/language": 6.10.5 + "@codemirror/lint": 6.8.3 + "@codemirror/search": 6.5.8 "@codemirror/state": 6.4.1 - "@codemirror/view": 6.34.3 + "@codemirror/view": 6.35.0 transitivePeerDependencies: - "@lezer/common" @@ -1620,26 +1620,26 @@ snapshots: dependencies: is-number: 7.0.0 - ts-api-utils@1.4.0(typescript@5.6.3): + ts-api-utils@1.4.2(typescript@5.7.2): dependencies: - typescript: 5.6.3 + typescript: 5.7.2 type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.15.0(eslint@9.15.0)(typescript@5.6.3): + typescript-eslint@8.16.0(eslint@9.15.0)(typescript@5.7.2): dependencies: - "@typescript-eslint/eslint-plugin": 8.15.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0)(typescript@5.6.3))(eslint@9.15.0)(typescript@5.6.3) - "@typescript-eslint/parser": 8.15.0(eslint@9.15.0)(typescript@5.6.3) - "@typescript-eslint/utils": 8.15.0(eslint@9.15.0)(typescript@5.6.3) + "@typescript-eslint/eslint-plugin": 8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2) + "@typescript-eslint/parser": 8.16.0(eslint@9.15.0)(typescript@5.7.2) + "@typescript-eslint/utils": 8.16.0(eslint@9.15.0)(typescript@5.7.2) eslint: 9.15.0 optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - typescript@5.6.3: {} + typescript@5.7.2: {} uri-js@4.4.1: dependencies: diff --git a/src/Controller/Admin/DashboardController.php b/src/Controller/Admin/DashboardController.php index bb69429..060d847 100644 --- a/src/Controller/Admin/DashboardController.php +++ b/src/Controller/Admin/DashboardController.php @@ -49,7 +49,7 @@ public function configureMenuItems(): iterable yield MenuItem::section('User management'); yield MenuItem::linkToCrud('User', 'fa fa-user', User::class); - yield MenuItem::linkToCrud('Group', 'fa fa-group', Group::class); + yield MenuItem::linkToCrud('Group', 'fa fa-users', Group::class); yield MenuItem::section('Question management'); yield MenuItem::linkToCrud('Schema', 'fa fa-database', Schema::class); @@ -63,7 +63,7 @@ public function configureMenuItems(): iterable yield MenuItem::linkToCrud('SolutionEvent', 'fa fa-check', SolutionEvent::class); yield MenuItem::linkToCrud('SolutionVideoEvent', 'fa fa-video', SolutionVideoEvent::class); yield MenuItem::linkToCrud('HintOpenEvent', 'fa fa-lightbulb', HintOpenEvent::class); - yield MenuItem::linkToCrud('LoginEvent', 'fa fa-sign-in', LoginEvent::class); + yield MenuItem::linkToCrud('LoginEvent', 'fa fa-right-to-bracket', LoginEvent::class); yield MenuItem::section('Feedback'); yield MenuItem::linkToCrud('Feedback', 'fa fa-comments', Feedback::class); diff --git a/src/Controller/Admin/FeedbackCrudController.php b/src/Controller/Admin/FeedbackCrudController.php index 85b9f84..a046cc5 100644 --- a/src/Controller/Admin/FeedbackCrudController.php +++ b/src/Controller/Admin/FeedbackCrudController.php @@ -5,14 +5,11 @@ namespace App\Controller\Admin; use App\Entity\Feedback; -use App\Entity\FeedbackStatus; -use Doctrine\ORM\EntityManagerInterface; use EasyCorp\Bundle\EasyAdminBundle\Config\Action; use EasyCorp\Bundle\EasyAdminBundle\Config\Actions; use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; use EasyCorp\Bundle\EasyAdminBundle\Config\Filters; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; -use EasyCorp\Bundle\EasyAdminBundle\Dto\BatchActionDto; use EasyCorp\Bundle\EasyAdminBundle\Field\ArrayField; use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField; use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField; @@ -21,18 +18,9 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField; use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField; use EasyCorp\Bundle\EasyAdminBundle\Field\TextField; -use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; - -use function Symfony\Component\Translation\t; class FeedbackCrudController extends AbstractCrudController { - public function __construct(private readonly AdminUrlGenerator $adminUrlGenerator) - { - } - public static function getEntityFqcn(): string { return Feedback::class; @@ -67,57 +55,10 @@ public function configureFilters(Filters $filters): Filters public function configureActions(Actions $actions): Actions { $actions->add(Crud::PAGE_INDEX, Action::DETAIL); - $actions->addBatchAction( - Action::new('mark_resolved', icon: 'fa fa-check') - ->linkToUrl( - $this - ->adminUrlGenerator - ->unsetAll() - ->setController(self::class) - ->setAction('markStatus') - ->set('status', FeedbackStatus::Resolved->value) - ->generateUrl() - ) - ); - $actions->addBatchAction( - Action::new('mark_closed', icon: 'fa fa-xmark') - ->linkToUrl( - $this - ->adminUrlGenerator - ->unsetAll() - ->setController(self::class) - ->setAction('markStatus') - ->set('status', FeedbackStatus::Closed->value) - ->generateUrl() - ) - ); return $actions; } - public function markStatus( - BatchActionDto $batchActionDto, - EntityManagerInterface $entityManager, - #[MapQueryParameter] FeedbackStatus $status, - ): Response { - $repository = $entityManager->getRepository(Feedback::class); - - foreach ($batchActionDto->getEntityIds() as $entityId) { - $feedback = $repository->find($entityId); - if (null === $feedback) { - continue; - } - - $feedback->setStatus($status); - $entityManager->persist($feedback); - } - - $entityManager->flush(); - $this->addFlash('success', t('feedback.marked', ['%status%' => $status])); - - return $this->redirect($batchActionDto->getReferrerUrl()); - } - public function configureCrud(Crud $crud): Crud { return $crud->setDefaultSort(['createdAt' => 'DESC']); diff --git a/src/Controller/Admin/QuestionCrudController.php b/src/Controller/Admin/QuestionCrudController.php index 7f742a5..c14106a 100644 --- a/src/Controller/Admin/QuestionCrudController.php +++ b/src/Controller/Admin/QuestionCrudController.php @@ -46,7 +46,7 @@ public function configureFields(string $pageName): iterable public function configureActions(Actions $actions): Actions { - $reindex = Action::new('reindex', 'Reindex', 'fa fa-refresh') + $reindex = Action::new('reindex', 'Reindex', 'fa fa-arrows-rotate') ->linkToCrudAction('reindex') ->createAsGlobalAction(); diff --git a/src/Entity/ChallengeDto/FallableQueryResultDto.php b/src/Entity/ChallengeDto/FallableSqlRunnerResult.php similarity index 70% rename from src/Entity/ChallengeDto/FallableQueryResultDto.php rename to src/Entity/ChallengeDto/FallableSqlRunnerResult.php index 8df0252..1b4dc4a 100644 --- a/src/Entity/ChallengeDto/FallableQueryResultDto.php +++ b/src/Entity/ChallengeDto/FallableSqlRunnerResult.php @@ -4,24 +4,26 @@ namespace App\Entity\ChallengeDto; +use App\Entity\SqlRunnerDto\SqlRunnerResult; +use App\Service\SqlRunnerService; use Symfony\Component\Translation\TranslatableMessage; /** - * A DTO for the result of a query that may contain the error from DbRunner. + * A DTO for the result of a query that may contain the error from {@link SqlRunnerService}. * * If there is no error, the errorCode will be 0. */ -class FallableQueryResultDto +class FallableSqlRunnerResult { - public ?QueryResultDto $result = null; + public ?SqlRunnerResult $result = null; public ?TranslatableMessage $errorMessage = null; - public function getResult(): ?QueryResultDto + public function getResult(): ?SqlRunnerResult { return $this->result; } - public function setResult(?QueryResultDto $result): self + public function setResult(?SqlRunnerResult $result): self { $this->result = $result; diff --git a/src/Entity/ChallengeDto/QueryResultDto.php b/src/Entity/ChallengeDto/QueryResultDto.php deleted file mode 100644 index 69e3787..0000000 --- a/src/Entity/ChallengeDto/QueryResultDto.php +++ /dev/null @@ -1,36 +0,0 @@ -> the result of the user's query - */ - private array $result; - - /** - * @return array> the result of the user's query - */ - public function getResult(): array - { - return $this->result; - } - - /** - * @param array> $result the result of the user's query - */ - public function setResult(array $result): self - { - $this->result = $result; - - return $this; - } -} diff --git a/src/Service/Types/PassRate.php b/src/Entity/PassRate.php similarity index 93% rename from src/Service/Types/PassRate.php rename to src/Entity/PassRate.php index 9fb5004..3d5add8 100644 --- a/src/Service/Types/PassRate.php +++ b/src/Entity/PassRate.php @@ -2,10 +2,7 @@ declare(strict_types=1); -namespace App\Service\Types; - -use App\Entity\SolutionEvent; -use App\Entity\SolutionEventStatus; +namespace App\Entity; /** * The pass rate of a question. diff --git a/src/Entity/SqlRunnerDto/SqlRunnerRequest.php b/src/Entity/SqlRunnerDto/SqlRunnerRequest.php new file mode 100644 index 0000000..cd1c713 --- /dev/null +++ b/src/Entity/SqlRunnerDto/SqlRunnerRequest.php @@ -0,0 +1,35 @@ +schema; + } + + public function setSchema(string $schema): self + { + $this->schema = $schema; + + return $this; + } + + public function getQuery(): string + { + return $this->query; + } + + public function setQuery(string $query): self + { + $this->query = $query; + + return $this; + } +} diff --git a/src/Entity/SqlRunnerDto/SqlRunnerResponse.php b/src/Entity/SqlRunnerDto/SqlRunnerResponse.php new file mode 100644 index 0000000..47a2d84 --- /dev/null +++ b/src/Entity/SqlRunnerDto/SqlRunnerResponse.php @@ -0,0 +1,82 @@ +success; + } + + public function setSuccess(bool $success): self + { + $this->success = $success; + + return $this; + } + + public function getData(): ?SqlRunnerResult + { + return $this->data; + } + + public function setData(?SqlRunnerResult $data): self + { + $this->data = $data; + + return $this; + } + + public function getMessage(): ?string + { + return $this->message; + } + + public function setMessage(?string $message): self + { + $this->message = $message; + + return $this; + } + + public function getCode(): ?string + { + return $this->code; + } + + public function setCode(?string $code): self + { + $this->code = $code; + + return $this; + } +} diff --git a/src/Entity/SqlRunnerDto/SqlRunnerResult.php b/src/Entity/SqlRunnerDto/SqlRunnerResult.php new file mode 100644 index 0000000..f093674 --- /dev/null +++ b/src/Entity/SqlRunnerDto/SqlRunnerResult.php @@ -0,0 +1,58 @@ + the columns of the result + */ + private array $columns; + + /** + * @var list> the rows of the result + */ + private array $rows; + + /** + * @return list the columns of the result + */ + public function getColumns(): array + { + return $this->columns; + } + + /** + * @param list $columns the columns of the result + * + * @return $this + */ + public function setColumns(array $columns): self + { + $this->columns = $columns; + + return $this; + } + + /** + * @return list> the rows of the result + */ + public function getRows(): array + { + return $this->rows; + } + + /** + * @param list> $rows the rows of the result + * + * @return $this + */ + public function setRows(array $rows): self + { + $this->rows = $rows; + + return $this; + } +} diff --git a/src/EventSubscriber/FeedbackCreatedListenerSubscriber.php b/src/EventSubscriber/FeedbackCreatedListenerSubscriber.php index b39f72a..dd60028 100644 --- a/src/EventSubscriber/FeedbackCreatedListenerSubscriber.php +++ b/src/EventSubscriber/FeedbackCreatedListenerSubscriber.php @@ -4,14 +4,12 @@ namespace App\EventSubscriber; -use App\Controller\Admin\FeedbackCrudController; use App\Entity\Feedback; use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; use Doctrine\ORM\Events; -use EasyCorp\Bundle\EasyAdminBundle\Config\Action; -use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator; use Symfony\Component\Notifier\Notification\Notification; use Symfony\Component\Notifier\NotifierInterface; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Translation\TranslatorInterface; #[AsEntityListener(event: Events::postPersist, method: 'onFeedbackCreated', entity: Feedback::class)] @@ -20,7 +18,7 @@ public function __construct( private NotifierInterface $notifier, private TranslatorInterface $translator, - private AdminUrlGenerator $adminUrlGenerator, + private UrlGeneratorInterface $urlGenerator, ) { } @@ -33,11 +31,9 @@ public function onFeedbackCreated(Feedback $feedback): void '%account%' => $feedback->getSender()?->getUserIdentifier() ?? $this->translator->trans('notification.on-feedback-created.anonymous'), '%subject%' => $feedback->getTitle(), - '%link%' => $this->adminUrlGenerator->unsetAll() - ->setController(FeedbackCrudController::class) - ->setAction(Action::DETAIL) - ->setEntityId($feedback->getId()) - ->generateUrl(), + '%link%' => $this->urlGenerator->generate('admin_feedback_detail', [ + 'entityId' => $feedback->getId(), + ], UrlGeneratorInterface::ABSOLUTE_URL), ]); $this->notifier->send((new Notification($notificationContent))->channels(['chat/linenotify'])); diff --git a/src/EventSubscriber/QuestionReindexSubscriber.php b/src/EventSubscriber/QuestionReindexSubscriber.php new file mode 100644 index 0000000..8e8d214 --- /dev/null +++ b/src/EventSubscriber/QuestionReindexSubscriber.php @@ -0,0 +1,30 @@ +logger->info("Reindexing question since question #{$question->getId()} has been updated."); + $this->questionRepository->reindex($this->searchService); + } +} diff --git a/src/Exception/QueryExecuteException.php b/src/Exception/SqlRunner/QueryExecuteException.php similarity index 91% rename from src/Exception/QueryExecuteException.php rename to src/Exception/SqlRunner/QueryExecuteException.php index a7b634e..83974d0 100644 --- a/src/Exception/QueryExecuteException.php +++ b/src/Exception/SqlRunner/QueryExecuteException.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Exception; +namespace App\Exception\SqlRunner; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; diff --git a/src/Exception/SqlRunner/RunnerException.php b/src/Exception/SqlRunner/RunnerException.php new file mode 100644 index 0000000..89b7723 --- /dev/null +++ b/src/Exception/SqlRunner/RunnerException.php @@ -0,0 +1,15 @@ +formatter = new SqlFormatter(); - } - - /** - * Hash the given SQL statement to a hex string. - * - * Useful for caching. Normalization is applied. - * - * @param string $sql the SQL to hash - * - * @return string the hashed SHA-256 hex string - */ - public function hashStatement(string $sql): string - { - return hash('sha3-256', $this->formatter->compress($sql)); - } - - /** - * Run the query with SQLite3. - * - * For example: - * - * - * $dbRunner = new DbRunner(); - * $schema = "CREATE TABLE students (id INTEGER PRIMARY KEY, name TEXT)"; - * $query = "SELECT * FROM students"; - * $result = $dbRunner->runQuery($schema, $query); - * // $result is an array with the result of the query. - * // Example: [["id" => 1, "name" => "John"]] - * - * - * @param string $schema the schema to create the database - * @param string $query the query to run - * - * @return QueryResultDto the result of the query - * - * @throws SchemaExecuteException if the schema could not be executed - * @throws QueryExecuteException if the query could not be executed - * @throws ResourceException if the resource is exhausted (exit code = 255) - * @throws \Throwable if the unexpected error is received - */ - public function runQuery(string $schema, string $query): QueryResultDto - { - // Use a process to prevent the SQLite3 extension from crashing the PHP process. - // For example, CTE queries and randomblob can crash the PHP process. - // See the test cases for more details. - // - // We don't yield over the result; instead, we store in the memory. - // Our PHP process has a hard limit of the memory usage, - // and we can crash it as early as possible when receiving a big result. - - $process = new Process(['php', __DIR__.'/Processes/dbrunner_process.php']); - $process->setTimeout($this->timeout); - $process->setInput(serialize(new DbRunnerProcessPayload($schema, $query))); - - try { - $process->mustRun(); - - $output = $process->getOutput(); - $outputDeserialized = unserialize($output, [ - 'allowed_classes' => [ - QueryResultDto::class, - ], - ]); - - if (!$outputDeserialized instanceof QueryResultDto) { - throw new \RuntimeException("unexpected output: $output"); - } - - return $outputDeserialized; - } catch (ProcessFailedException) { - $exitCode = $process->getExitCode(); - - if (255 === $exitCode) { - throw new ResourceException(); - } - - if (1 === $exitCode) { - $output = $process->getErrorOutput(); - $outputDeserialized = unserialize($output, [ - 'allowed_classes' => true, - ]); - - if (!($outputDeserialized instanceof ProcessError)) { - $o = json_encode($output); - throw new \RuntimeException("Unexpected data received (exit code 1): $o"); - } - - $outputDeserialized->rethrow(); - } - - throw new \RuntimeException("Unexpected exit code: $exitCode"); - } catch (ProcessTimedOutException) { - throw new TimedOutException($this->timeout); - } - } -} diff --git a/src/Service/DbRunnerService.php b/src/Service/DbRunnerService.php deleted file mode 100644 index 745cc13..0000000 --- a/src/Service/DbRunnerService.php +++ /dev/null @@ -1,37 +0,0 @@ -dbRunner = new DbRunner(); - } - - /** - * Run a query on the SQLite3 database, cached. - * - * @throws InvalidArgumentException - * @throws SchemaExecuteException - * @throws QueryExecuteException - */ - public function runQuery(string $schema, string $query): QueryResultDto - { - $schemaHash = $this->dbRunner->hashStatement($schema); - $queryHash = $this->dbRunner->hashStatement($query); - $hash = "dbrunner.$schemaHash.$queryHash"; - - return $this->cacheDbrunner->get($hash, fn () => $this->dbRunner->runQuery($schema, $query)); - } -} diff --git a/src/Service/PassRateService.php b/src/Service/PassRateService.php index 9d90529..07453c6 100644 --- a/src/Service/PassRateService.php +++ b/src/Service/PassRateService.php @@ -5,9 +5,9 @@ namespace App\Service; use App\Entity\Group; +use App\Entity\PassRate; use App\Entity\Question; use App\Repository\SolutionEventRepository; -use App\Service\Types\PassRate; /** * Get the pass rate of a question in the optimized matter. diff --git a/src/Service/Processes/DbRunnerProcessService.php b/src/Service/Processes/DbRunnerProcessService.php deleted file mode 100644 index 529ec09..0000000 --- a/src/Service/Processes/DbRunnerProcessService.php +++ /dev/null @@ -1,64 +0,0 @@ -schema); - $sqliteResult = $db->query($input->query); - $queryResult = $this->transformResult($sqliteResult); - $sqliteResult->finalize(); - - return $queryResult; - } - - private function transformResult(\SQLite3Result $result): QueryResultDto - { - /** - * @var array> $columnsRow - */ - $columnsRow = []; - - for ($i = 0; $i < $result->numColumns(); ++$i) { - $columnsRow[] = $result->columnName($i); - } - - /** - * @var array> $rows - */ - $rows = []; - - while ($rawRow = $result->fetchArray(\SQLITE3_ASSOC)) { - $row = []; - foreach ($rawRow as $value) { - $row[] = match (true) { - null === $value => 'NULL', - \is_string($value) => $value, - \is_bool($value) => $value ? 'TRUE' : 'FALSE', - is_numeric($value) => (string) $value, - default => '', - }; - } - $rows[] = $row; - } - - /** - * @var array> $merged - */ - $merged = array_merge([$columnsRow], $rows); - - return (new QueryResultDto())->setResult($merged); - } -} diff --git a/src/Service/Processes/ProcessService.php b/src/Service/Processes/ProcessService.php deleted file mode 100644 index 8f9fc82..0000000 --- a/src/Service/Processes/ProcessService.php +++ /dev/null @@ -1,52 +0,0 @@ - [ - DbRunnerProcessPayload::class, - ], - ]); - if (!\is_object($input)) { - throw new \InvalidArgumentException('input must be an object'); - } - - $result = $this->main($input); - fwrite(\STDOUT, serialize($result)); - exit(0); - } catch (\Throwable $throwable) { - fwrite(\STDERR, serialize(new ProcessError($throwable))); - exit(1); - } - } - - /** - * The main function of the process. - * - * @param object $input the input of the process - * - * @return object the result of the process - */ - abstract public function main(object $input): object; -} diff --git a/src/Service/Processes/dbrunner_process.php b/src/Service/Processes/dbrunner_process.php deleted file mode 100644 index 90d16e3..0000000 --- a/src/Service/Processes/dbrunner_process.php +++ /dev/null @@ -1,9 +0,0 @@ -run(); diff --git a/src/Service/QuestionDbRunnerService.php b/src/Service/QuestionDbRunnerService.php deleted file mode 100644 index 336ca2a..0000000 --- a/src/Service/QuestionDbRunnerService.php +++ /dev/null @@ -1,92 +0,0 @@ -getSchema(); - - return $this->dbRunnerService->runQuery( - $schema->getSchema(), - $query, - ); - } - - /** - * Get the result of the query from the question. - * - * @param Question $question the question to get the result from - * - * @return QueryResultDto the result of the query - * - * @throws NotFoundHttpException - * @throws InvalidArgumentException - * @throws SchemaExecuteException - * @throws QueryExecuteException - */ - public function getAnswerResult(Question $question): QueryResultDto - { - $lock = $this->lockFactory->createLock("question_{$question->getId()}_answer"); - - try { - $lock->acquire(true); - $result = $this->getResult($question, $question->getAnswer()); - } finally { - $lock->release(); - } - - return $result; - } - - /** - * Get the result of the query from the question. - * - * @param Question $question the question to get the result from - * @param string $query the query to execute - * - * @return QueryResultDto the result of the query - * - * @throws NotFoundHttpException - * @throws InvalidArgumentException - * @throws SchemaExecuteException - * @throws QueryExecuteException - */ - public function getQueryResult(Question $question, string $query): QueryResultDto - { - return $this->getResult($question, $query); - } -} diff --git a/src/Service/QuestionSqlRunnerService.php b/src/Service/QuestionSqlRunnerService.php new file mode 100644 index 0000000..4929283 --- /dev/null +++ b/src/Service/QuestionSqlRunnerService.php @@ -0,0 +1,80 @@ +getSchema(); + + return $this->sqlRunnerService->runQuery( + (new SqlRunnerRequest()) + ->setQuery($query) + ->setSchema($schema->getSchema()) + ); + } + + /** + * Get the result of the query from the question. + * + * @param Question $question the question to get the result from + * + * @return SqlRunnerResult the result of the query + * + * @throws QueryExecuteException when the query execution fails + * @throws SchemaExecuteException when the schema execution fails + * @throws RunnerException when the runner fails (internal error or client error) + */ + public function getAnswerResult(Question $question): SqlRunnerResult + { + return $this->getResult($question, $question->getAnswer()); + } + + /** + * Get the result of the query from the question. + * + * @param Question $question the question to get the result from + * @param string $query the query to execute + * + * @return SqlRunnerResult the result of the query + * + * @throws QueryExecuteException when the query execution fails + * @throws SchemaExecuteException when the schema execution fails + * @throws RunnerException when the runner fails (internal error or client error) + */ + public function getQueryResult(Question $question, string $query): SqlRunnerResult + { + return $this->getResult($question, $query); + } +} diff --git a/src/Service/DbRunnerComparer.php b/src/Service/SqlRunnerComparer.php similarity index 60% rename from src/Service/DbRunnerComparer.php rename to src/Service/SqlRunnerComparer.php index 0f95e02..e0da2fb 100644 --- a/src/Service/DbRunnerComparer.php +++ b/src/Service/SqlRunnerComparer.php @@ -5,35 +5,35 @@ namespace App\Service; use App\Entity\ChallengeDto\CompareResult; -use App\Entity\ChallengeDto\QueryResultDto; +use App\Entity\SqlRunnerDto\SqlRunnerResult; -readonly class DbRunnerComparer +readonly class SqlRunnerComparer { /** * Compare this answer with user response and return the detailed information. * - * @param QueryResultDto $answerResult the answer's query result - * @param QueryResultDto $userResult the user's query result + * @param SqlRunnerResult $answerResult the answer's query result + * @param SqlRunnerResult $userResult the user's query result * * @return CompareResult\CompareResult the comparison result */ - public static function compare(QueryResultDto $answerResult, QueryResultDto $userResult): CompareResult\CompareResult + public static function compare(SqlRunnerResult $answerResult, SqlRunnerResult $userResult): CompareResult\CompareResult { - if (0 === \count($answerResult->getResult())) { + if (0 === \count($answerResult->getColumns())) { return new CompareResult\EmptyAnswer(); } - if (0 === \count($userResult->getResult())) { + if (0 === \count($userResult->getColumns())) { return new CompareResult\EmptyResult(); } - $answerColumns = $answerResult->getResult()[0]; - $userColumns = $userResult->getResult()[0]; + $answerColumns = $answerResult->getColumns(); + $userColumns = $userResult->getColumns(); if ($answerColumns !== $userColumns) { return new CompareResult\ColumnDifferent(); } - $answerRows = \array_slice($answerResult->getResult(), 1); - $userRows = \array_slice($userResult->getResult(), 1); + $answerRows = $answerResult->getRows(); + $userRows = $userResult->getRows(); if (\count($answerRows) !== \count($userRows)) { return new CompareResult\RowUnmatched( expected: \count($answerRows), diff --git a/src/Service/SqlRunnerService.php b/src/Service/SqlRunnerService.php new file mode 100644 index 0000000..49936b5 --- /dev/null +++ b/src/Service/SqlRunnerService.php @@ -0,0 +1,91 @@ + the context for the serializer + */ + private array $context; + + public function __construct( + private HttpClientInterface $httpClient, + private SerializerInterface $serializer, + private string $baseUrl, + ) { + $this->context = (new ObjectNormalizerContextBuilder()) + ->withAllowExtraAttributes(false) + ->withRequireAllProperties() + ->toArray(); + } + + /** + * Run the query in the remote SQL runner. + * + * @param SqlRunnerRequest $request The request to run the query + * + * @returns SqlRunnerResult The result of the query + * + * @throws QueryExecuteException When the query execution fails + * @throws SchemaExecuteException When the schema execution fails + * @throws RunnerException When the runner fails (internal error or client error) + */ + public function runQuery(SqlRunnerRequest $request): SqlRunnerResult + { + $endpoint = $this->baseUrl.'/query'; + + try { + $response = $this->httpClient->request('POST', $endpoint, [ + 'json' => (array) $request, + 'headers' => [ + 'User-Agent' => 'dbplay/v1', + ], + ]); + $content = $response->getContent(false); + } catch (\Throwable $e) { + throw new RunnerException('CLIENT_ERROR', $e->getMessage(), previous: $e); + } + + try { + $response = $this->serializer->deserialize( + $content, + SqlRunnerResponse::class, + 'json', + $this->context, + ); + } catch (\Throwable $e) { + throw new RunnerException('PROTOCOL_ERROR', $e->getMessage(), $e); + } + + if (!$response->isSuccess()) { + \assert(null !== $response->getCode(), 'The code should not be null when response is not succeed.'); + \assert(null !== $response->getMessage(), 'The message should not be null when response is not succeed.'); + + switch ($response->getCode()) { + case 'QUERY_ERROR': + throw new QueryExecuteException($response->getMessage()); + case 'SCHEMA_ERROR': + throw new SchemaExecuteException($response->getMessage()); + default: + throw new RunnerException($response->getCode(), $response->getMessage()); + } + } + + \assert(null !== $response->getData(), 'The data should not be null when response is succeed.'); + + return $response->getData(); + } +} diff --git a/src/Service/Types/DbRunnerProcessPayload.php b/src/Service/Types/DbRunnerProcessPayload.php deleted file mode 100644 index 7042736..0000000 --- a/src/Service/Types/DbRunnerProcessPayload.php +++ /dev/null @@ -1,17 +0,0 @@ -throwable; - } - - /** - * Rethrows the error. - * - * @throws \Throwable - */ - public function rethrow(): void - { - throw $this->throwable; - } -} diff --git a/src/Service/Types/SchemaDatabase.php b/src/Service/Types/SchemaDatabase.php deleted file mode 100644 index 1fac701..0000000 --- a/src/Service/Types/SchemaDatabase.php +++ /dev/null @@ -1,125 +0,0 @@ -db->close(); - } - - /** - * Execute the query and return the result. - * - * @param string $query the query to execute - * - * @return \SQLite3Result the result of the query - * - * @throws QueryExecuteException if the query could not be executed - */ - public function query(string $query): \SQLite3Result - { - try { - $result = $this->db->query($query); - } catch (\Throwable) { - throw new QueryExecuteException($this->db->lastErrorMsg()); - } - - if (\is_bool($result)) { - throw new QueryExecuteException("Invalid query given: '$query'"); - } - - return $result; - } - - private static function setUp(\SQLite3 $db): \SQLite3 - { - $db->busyTimeout(3000 /* milliseconds */); - $db->enableExceptions(true); - - $dateop = fn (string $format) => fn (string $date) => (int) date( - $format, - ($datestr = strtotime($date)) !== false - ? $datestr - : throw new \InvalidArgumentException("Failed to convert $date as $format."), - ); - - // MySQL-compatible functions - $db->createFunction('YEAR', $dateop('Y'), 1, \SQLITE3_DETERMINISTIC); - $db->createFunction('MONTH', $dateop('n'), 1, \SQLITE3_DETERMINISTIC); - $db->createFunction('DAY', $dateop('j'), 1, \SQLITE3_DETERMINISTIC); - $db->createFunction('LEFT', fn (string $str, int $len) => substr($str, 0, $len), 2, \SQLITE3_DETERMINISTIC); - $db->createFunction( - 'IF', - fn (bool $condition, mixed $true, mixed $false) => $condition ? $true : $false, - 3, - \SQLITE3_DETERMINISTIC - ); - - return $db; - } - - /** - * Initialize the database and return the filename to the schema sqlite3. - * - * It does nothing if the file already exists. - * - * @param string $filename the filename to the schema sqlite3 - * @param string $schema the schema to initialize - * - * @throws SchemaExecuteException if the schema could not be executed - */ - private static function initialize(string $filename, string $schema): void - { - if (file_exists($filename)) { - return; - } - - $db = self::setUp(new \SQLite3($filename)); - - try { - $db->exec('BEGIN EXCLUSIVE'); - $db->exec($schema); - $db->exec('COMMIT'); - $db->close(); - } catch (\Throwable) { - $lastErrorMessage = $db->lastErrorMsg(); - - // remove the file if the schema could not be executed - $db->close(); - unlink($filename); - - throw new SchemaExecuteException($lastErrorMessage); - } - } - - private static function getSchemaSqlFilename(string $schema): string - { - $tmpdir = sys_get_temp_dir(); - $schemaHash = hash('sha3-256', $schema); - - return "$tmpdir/dbrunner_$schemaHash.sql"; - } -} diff --git a/src/Twig/Components/Challenge/ColumnsOfAnswer.php b/src/Twig/Components/Challenge/ColumnsOfAnswer.php index e3fa720..1cd24e5 100644 --- a/src/Twig/Components/Challenge/ColumnsOfAnswer.php +++ b/src/Twig/Components/Challenge/ColumnsOfAnswer.php @@ -5,7 +5,7 @@ namespace App\Twig\Components\Challenge; use App\Entity\Question; -use App\Service\QuestionDbRunnerService; +use App\Service\QuestionSqlRunnerService; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; @@ -16,7 +16,7 @@ final class ColumnsOfAnswer use DefaultActionTrait; public function __construct( - private readonly QuestionDbRunnerService $questionDbRunnerService, + private readonly QuestionSqlRunnerService $questionSqlRunnerService, ) { } @@ -31,14 +31,9 @@ public function __construct( public function getColumnsOfAnswer(): array { try { - $answer = $this->questionDbRunnerService->getAnswerResult($this->question); - $answerResult = $answer->getResult(); + $answer = $this->questionSqlRunnerService->getAnswerResult($this->question); - if (0 === \count($answerResult)) { - return []; - } - - return $answer->getResult()[0]; + return $answer->getColumns(); } catch (\Throwable $e) { return ["⚠️ Invalid Question: {$e->getMessage()}"]; } diff --git a/src/Twig/Components/Challenge/Executor.php b/src/Twig/Components/Challenge/Executor.php index 41918a3..a49b933 100644 --- a/src/Twig/Components/Challenge/Executor.php +++ b/src/Twig/Components/Challenge/Executor.php @@ -9,8 +9,8 @@ use App\Entity\SolutionEventStatus; use App\Entity\User; use App\Repository\SolutionEventRepository; -use App\Service\DbRunnerComparer; -use App\Service\QuestionDbRunnerService; +use App\Service\QuestionSqlRunnerService; +use App\Service\SqlRunnerComparer; use Doctrine\ORM\EntityManagerInterface; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveAction; @@ -26,7 +26,7 @@ final class Executor use DefaultActionTrait; public function __construct( - private readonly QuestionDbRunnerService $questionDbRunnerService, + private readonly QuestionSqlRunnerService $questionSqlRunnerService, private readonly SolutionEventRepository $solutionEventRepository, private readonly EntityManagerInterface $entityManager, ) { @@ -60,10 +60,10 @@ public function createNewQuery( ->setQuery($query); try { - $answer = $this->questionDbRunnerService->getAnswerResult($this->question); - $result = $this->questionDbRunnerService->getQueryResult($this->question, $query); + $answer = $this->questionSqlRunnerService->getAnswerResult($this->question); + $result = $this->questionSqlRunnerService->getQueryResult($this->question, $query); - $compareResult = DbRunnerComparer::compare($answer, $result); + $compareResult = SqlRunnerComparer::compare($answer, $result); $solutionEvent = $solutionEvent->setStatus( $compareResult->correct() ? SolutionEventStatus::Passed diff --git a/src/Twig/Components/Challenge/Header.php b/src/Twig/Components/Challenge/Header.php index acd71c7..dc68da4 100644 --- a/src/Twig/Components/Challenge/Header.php +++ b/src/Twig/Components/Challenge/Header.php @@ -4,13 +4,13 @@ namespace App\Twig\Components\Challenge; +use App\Entity\PassRate; use App\Entity\Question; use App\Entity\SolutionEventStatus; use App\Entity\User; use App\Repository\QuestionRepository; use App\Repository\SolutionEventRepository; use App\Service\PassRateService; -use App\Service\Types\PassRate; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveListener; use Symfony\UX\LiveComponent\Attribute\LiveProp; diff --git a/src/Twig/Components/Challenge/Instruction/Modal.php b/src/Twig/Components/Challenge/Instruction/Modal.php index b749303..462215c 100644 --- a/src/Twig/Components/Challenge/Instruction/Modal.php +++ b/src/Twig/Components/Challenge/Instruction/Modal.php @@ -7,12 +7,13 @@ use App\Entity\HintOpenEvent; use App\Entity\Question; use App\Entity\SolutionEventStatus; +use App\Entity\SqlRunnerDto\SqlRunnerRequest; use App\Entity\User; use App\Repository\SolutionEventRepository; -use App\Service\DbRunnerComparer; -use App\Service\DbRunnerService; use App\Service\PointCalculationService; use App\Service\PromptService; +use App\Service\SqlRunnerComparer; +use App\Service\SqlRunnerService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -56,7 +57,7 @@ public function getCost(): int #[LiveAction] public function instruct( SolutionEventRepository $solutionEventRepository, - DbRunnerService $dbRunnerService, + SqlRunnerService $sqlRunnerService, PromptService $promptService, TranslatorInterface $translator, EntityManagerInterface $entityManager, @@ -85,7 +86,11 @@ public function instruct( try { $answer = $query->getQuestion()->getAnswer(); - $answerResult = $dbRunnerService->runQuery($schema->getSchema(), $answer); + $answerResult = $sqlRunnerService->runQuery( + (new SqlRunnerRequest()) + ->setSchema($schema->getSchema()) + ->setQuery($answer), + ); } catch (\Throwable $e) { $this->flushHint('informative', t('instruction.hint.error', [ '%error%' => $e->getMessage(), @@ -101,7 +106,11 @@ public function instruct( try { try { - $userResult = $dbRunnerService->runQuery($schema->getSchema(), $query->getQuery()); + $userResult = $sqlRunnerService->runQuery( + (new SqlRunnerRequest()) + ->setSchema($schema->getSchema()) + ->setQuery($query->getQuery()), + ); } catch (\Throwable $e) { $hint = $promptService->hint($query->getQuery(), $e->getMessage(), $answer); $hintOpenEvent->setResponse($hint); @@ -111,7 +120,7 @@ public function instruct( return; } - $compareResult = DbRunnerComparer::compare($answerResult, $userResult); + $compareResult = SqlRunnerComparer::compare($answerResult, $userResult); if ($compareResult->correct()) { $this->flushHint('informative', t('instruction.hint.solved')); diff --git a/src/Twig/Components/Challenge/Tabs/AnswerQueryResult.php b/src/Twig/Components/Challenge/Tabs/AnswerQueryResult.php index 3934c6f..9e3e544 100644 --- a/src/Twig/Components/Challenge/Tabs/AnswerQueryResult.php +++ b/src/Twig/Components/Challenge/Tabs/AnswerQueryResult.php @@ -4,9 +4,12 @@ namespace App\Twig\Components\Challenge\Tabs; -use App\Entity\ChallengeDto\FallableQueryResultDto; +use App\Entity\ChallengeDto\FallableSqlRunnerResult; use App\Entity\Question; -use App\Service\QuestionDbRunnerService; +use App\Exception\SqlRunner\QueryExecuteException; +use App\Exception\SqlRunner\SchemaExecuteException; +use App\Service\QuestionSqlRunnerService; +use Psr\Log\LoggerInterface; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; use function Symfony\Component\Translation\t; @@ -15,7 +18,8 @@ final class AnswerQueryResult { public function __construct( - private readonly QuestionDbRunnerService $questionDbRunnerService, + private readonly QuestionSqlRunnerService $questionSqlRunnerService, + private readonly LoggerInterface $logger, ) { } @@ -24,18 +28,42 @@ public function __construct( */ public Question $question; - public function getAnswer(): FallableQueryResultDto + public function getAnswer(): FallableSqlRunnerResult { try { - $resultDto = $this->questionDbRunnerService->getAnswerResult($this->question); + $answerResultDto = $this->questionSqlRunnerService->getAnswerResult($this->question); + } catch (SchemaExecuteException $e) { + $this->logger->error('Schema Error', [ + 'exception' => $e, + ]); + + $errorMessage = t('challenge.errors.schema-error', [ + '%error%' => $e->getMessage(), + ]); + + return (new FallableSqlRunnerResult())->setErrorMessage($errorMessage); + } catch (QueryExecuteException $e) { + $this->logger->error('Failed to get the answer result', [ + 'exception' => $e, + ]); - return (new FallableQueryResultDto())->setResult($resultDto); - } catch (\Throwable $e) { $errorMessage = t('challenge.errors.answer-query-failure', [ '%error%' => $e->getMessage(), ]); - return (new FallableQueryResultDto())->setErrorMessage($errorMessage); + return (new FallableSqlRunnerResult())->setErrorMessage($errorMessage); + } catch (\Throwable $e) { + $this->logger->error('SQL Runner failed when running answer', [ + 'exception' => $e, + ]); + + $errorMessage = t('challenge.errors.unavailable', [ + '%error%' => $e->getMessage(), + ]); + + return (new FallableSqlRunnerResult())->setErrorMessage($errorMessage); } + + return (new FallableSqlRunnerResult())->setResult($answerResultDto); } } diff --git a/src/Twig/Components/Challenge/Tabs/DiffPresenter.php b/src/Twig/Components/Challenge/Tabs/DiffPresenter.php index 9887af1..4605519 100644 --- a/src/Twig/Components/Challenge/Tabs/DiffPresenter.php +++ b/src/Twig/Components/Challenge/Tabs/DiffPresenter.php @@ -7,7 +7,7 @@ use App\Entity\Question; use App\Entity\User; use App\Repository\SolutionEventRepository; -use App\Service\QuestionDbRunnerService; +use App\Service\QuestionSqlRunnerService; use jblond\Diff; use jblond\Diff\Renderer\Html\SideBySide; use Psr\Log\LoggerInterface; @@ -26,7 +26,7 @@ final class DiffPresenter use DefaultActionTrait; public function __construct( - private readonly QuestionDbRunnerService $questionDbRunnerService, + private readonly QuestionSqlRunnerService $questionSqlRunnerService, private readonly SolutionEventRepository $solutionEventRepository, private readonly TranslatorInterface $translator, private readonly SerializerInterface $serializer, @@ -52,9 +52,11 @@ public function postMount(): void public function getAnswerResult(): ?string { try { - $resultDto = $this->questionDbRunnerService->getAnswerResult($this->question); + $resultDto = $this->questionSqlRunnerService->getAnswerResult($this->question); - return $this->serializer->serialize($resultDto->getResult(), 'csv', [ + $columnsAndRows = [$resultDto->getColumns(), ...$resultDto->getRows()]; + + return $this->serializer->serialize($columnsAndRows, 'csv', [ 'csv_delimiter' => "\t", 'csv_enclosure' => ' ', ]); @@ -74,9 +76,11 @@ public function getUserResult(): ?string } try { - $resultDto = $this->questionDbRunnerService->getQueryResult($this->question, $this->query); + $resultDto = $this->questionSqlRunnerService->getQueryResult($this->question, $this->query); + + $columnsAndRows = [$resultDto->getColumns(), ...$resultDto->getRows()]; - return $this->serializer->serialize($resultDto->getResult(), 'csv', [ + return $this->serializer->serialize($columnsAndRows, 'csv', [ 'csv_delimiter' => "\t", 'csv_enclosure' => ' ', ]); diff --git a/src/Twig/Components/Challenge/Tabs/QueryResultTable.php b/src/Twig/Components/Challenge/Tabs/SqlRunnerResultTable.php similarity index 56% rename from src/Twig/Components/Challenge/Tabs/QueryResultTable.php rename to src/Twig/Components/Challenge/Tabs/SqlRunnerResultTable.php index 978972a..355452c 100644 --- a/src/Twig/Components/Challenge/Tabs/QueryResultTable.php +++ b/src/Twig/Components/Challenge/Tabs/SqlRunnerResultTable.php @@ -4,38 +4,22 @@ namespace App\Twig\Components\Challenge\Tabs; -use App\Entity\ChallengeDto\QueryResultDto; +use App\Entity\SqlRunnerDto\SqlRunnerResult; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; #[AsLiveComponent] -final class QueryResultTable +final class SqlRunnerResultTable { use DefaultActionTrait; use Pagination; /** - * @var QueryResultDto the result of this query + * @var SqlRunnerResult the result of this query */ #[LiveProp(updateFromParent: true)] - public QueryResultDto $result; - - /** - * @return array the header - */ - public function getHeader(): array - { - return $this->result->getResult()[0]; - } - - /** - * @return array> the rows - */ - public function getRows(): array - { - return \array_slice($this->result->getResult(), 1); - } + public SqlRunnerResult $result; /** * Get the paginated rows and another row to determine if there are more pages. @@ -44,7 +28,7 @@ public function getRows(): array */ protected function getData(): array { - return \array_slice($this->getRows(), ($this->page - 1) * self::limit, self::limit + 1); + return \array_slice($this->result->getRows(), ($this->page - 1) * self::limit, self::limit + 1); } /** diff --git a/src/Twig/Components/Challenge/Tabs/UserQueryResult.php b/src/Twig/Components/Challenge/Tabs/UserQueryResult.php index 2153e1d..3896ad1 100644 --- a/src/Twig/Components/Challenge/Tabs/UserQueryResult.php +++ b/src/Twig/Components/Challenge/Tabs/UserQueryResult.php @@ -4,12 +4,15 @@ namespace App\Twig\Components\Challenge\Tabs; -use App\Entity\ChallengeDto\FallableQueryResultDto; +use App\Entity\ChallengeDto\FallableSqlRunnerResult; use App\Entity\Question; use App\Entity\User; +use App\Exception\SqlRunner\QueryExecuteException; +use App\Exception\SqlRunner\SchemaExecuteException; use App\Repository\SolutionEventRepository; -use App\Service\DbRunnerComparer; -use App\Service\QuestionDbRunnerService; +use App\Service\QuestionSqlRunnerService; +use App\Service\SqlRunnerComparer; +use Psr\Log\LoggerInterface; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveArg; use Symfony\UX\LiveComponent\Attribute\LiveListener; @@ -25,8 +28,9 @@ final class UserQueryResult use DefaultActionTrait; public function __construct( - private readonly QuestionDbRunnerService $questionDbRunnerService, + private readonly QuestionSqlRunnerService $questionSqlRunnerService, private readonly SolutionEventRepository $solutionEventRepository, + private readonly LoggerInterface $logger, ) { } @@ -51,43 +55,87 @@ public function postMount(): void $this->query = $this->solutionEventRepository->getLatestQuery($this->question, $this->user)?->getQuery(); } - public function getResult(): ?FallableQueryResultDto + public function getResult(): ?FallableSqlRunnerResult { if (null === $this->query) { return null; } try { - $answerResultDto = $this->questionDbRunnerService->getAnswerResult($this->question); - } catch (\Throwable $e) { + $answerResultDto = $this->questionSqlRunnerService->getAnswerResult($this->question); + } catch (SchemaExecuteException $e) { + $this->logger->error('Schema Error', [ + 'exception' => $e, + ]); + + $errorMessage = t('challenge.errors.schema-error', [ + '%error%' => $e->getMessage(), + ]); + + return (new FallableSqlRunnerResult())->setErrorMessage($errorMessage); + } catch (QueryExecuteException $e) { + $this->logger->error('Failed to get the answer result', [ + 'exception' => $e, + ]); + $errorMessage = t('challenge.errors.answer-query-failure', [ '%error%' => $e->getMessage(), ]); - return (new FallableQueryResultDto())->setErrorMessage($errorMessage); + return (new FallableSqlRunnerResult())->setErrorMessage($errorMessage); + } catch (\Throwable $e) { + $this->logger->error('SQL Runner failed when running answer', [ + 'exception' => $e, + ]); + + $errorMessage = t('challenge.errors.unavailable', [ + '%error%' => $e->getMessage(), + ]); + + return (new FallableSqlRunnerResult())->setErrorMessage($errorMessage); } try { - $resultDto = $this->questionDbRunnerService->getQueryResult($this->question, $this->query); - } catch (\Throwable $e) { + $resultDto = $this->questionSqlRunnerService->getQueryResult($this->question, $this->query); + } catch (SchemaExecuteException $e) { + $this->logger->error('Schema Error', [ + 'exception' => $e, + ]); + + $errorMessage = t('challenge.errors.schema-error', [ + '%error%' => $e->getMessage(), + ]); + + return (new FallableSqlRunnerResult())->setErrorMessage($errorMessage); + } catch (QueryExecuteException $e) { $errorMessage = t('challenge.errors.user-query-error', [ '%error%' => $e->getMessage(), ]); - return (new FallableQueryResultDto())->setErrorMessage($errorMessage); + return (new FallableSqlRunnerResult())->setErrorMessage($errorMessage); + } catch (\Throwable $e) { + $this->logger->error('SQL Runner failed when running user queries', [ + 'exception' => $e, + ]); + + $errorMessage = t('challenge.errors.unavailable', [ + '%error%' => $e->getMessage(), + ]); + + return (new FallableSqlRunnerResult())->setErrorMessage($errorMessage); } // compare the result - $compareResult = DbRunnerComparer::compare($answerResultDto, $resultDto); + $compareResult = SqlRunnerComparer::compare($answerResultDto, $resultDto); if ($compareResult->correct()) { - return (new FallableQueryResultDto())->setResult($resultDto); + return (new FallableSqlRunnerResult())->setResult($resultDto); } $errorMessage = t('challenge.errors.user-query-failure', [ '%error%' => $compareResult->reason(), ]); - return (new FallableQueryResultDto())->setResult($resultDto)->setErrorMessage($errorMessage); + return (new FallableSqlRunnerResult())->setResult($resultDto)->setErrorMessage($errorMessage); } #[LiveListener('app:challenge-executor:query-created')] diff --git a/src/Twig/Components/Questions/Card.php b/src/Twig/Components/Questions/Card.php index a5b121d..d509723 100644 --- a/src/Twig/Components/Questions/Card.php +++ b/src/Twig/Components/Questions/Card.php @@ -4,10 +4,10 @@ namespace App\Twig\Components\Questions; +use App\Entity\PassRate; use App\Entity\Question; use App\Entity\User; use App\Service\PassRateService; -use App\Service\Types\PassRate; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; #[AsTwigComponent] diff --git a/symfony.lock b/symfony.lock index 5e6dd95..5862a09 100644 --- a/symfony.lock +++ b/symfony.lock @@ -130,6 +130,18 @@ ".env" ] }, + "symfony/form": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.2", + "ref": "7d86a6723f4a623f59e2bf966b6aad2fc461d36b" + }, + "files": [ + "config/packages/csrf.yaml" + ] + }, "symfony/framework-bundle": { "version": "7.2", "recipe": { diff --git a/templates/components/Challenge/Tabs/AnswerQueryResult.html.twig b/templates/components/Challenge/Tabs/AnswerQueryResult.html.twig index a886f3c..ec2cfc9 100644 --- a/templates/components/Challenge/Tabs/AnswerQueryResult.html.twig +++ b/templates/components/Challenge/Tabs/AnswerQueryResult.html.twig @@ -8,6 +8,6 @@ {% endif %} {% if answer.result %} - + {% endif %} diff --git a/templates/components/Challenge/Tabs/QueryResultTable.html.twig b/templates/components/Challenge/Tabs/SqlRunnerResultTable.html.twig similarity index 92% rename from templates/components/Challenge/Tabs/QueryResultTable.html.twig rename to templates/components/Challenge/Tabs/SqlRunnerResultTable.html.twig index 854c3c7..2f7937d 100644 --- a/templates/components/Challenge/Tabs/QueryResultTable.html.twig +++ b/templates/components/Challenge/Tabs/SqlRunnerResultTable.html.twig @@ -3,7 +3,7 @@ # - {% for cell in this.header %} + {% for cell in this.result.columns %} {{ cell }} {% endfor %} diff --git a/templates/components/Challenge/Tabs/UserQueryResult.html.twig b/templates/components/Challenge/Tabs/UserQueryResult.html.twig index 676466a..a753b4c 100644 --- a/templates/components/Challenge/Tabs/UserQueryResult.html.twig +++ b/templates/components/Challenge/Tabs/UserQueryResult.html.twig @@ -7,7 +7,7 @@ {% elseif result.errorMessage is not null %} {% else %} diff --git a/tests/Service/DbRunnerServiceTest.php b/tests/Service/DbRunnerServiceTest.php deleted file mode 100644 index 5bf1fb3..0000000 --- a/tests/Service/DbRunnerServiceTest.php +++ /dev/null @@ -1,84 +0,0 @@ -runQuery($schema, $query); - self::assertEquals([['id', 'content'], ['1', 'hello']], $result->getResult()); - - $hashedSchema = $dbRunnerService->getDbRunner()->hashStatement($schema); - $hashedQuery = $dbRunnerService->getDbRunner()->hashStatement($query); - self::assertTrue($cache->hasItem("dbrunner.$hashedSchema.$hashedQuery")); - - $result = $dbRunnerService->runQuery( - " - -- normalization test - CREATE TABLE newsletter (id INTEGER PRIMARY KEY, content TEXT); - INSERT INTO newsletter (content) VALUES ('hello');", - 'SELECT * FROM newsletter' - ); - self::assertEquals([['id', 'content'], ['1', 'hello']], $result->getResult()); - self::assertCount(1, $cache->getValues(), 'cache hit'); - - $result = $dbRunnerService->runQuery( - " - CREATE TABLE newsletter (id INTEGER PRIMARY KEY, content TEXT); - INSERT INTO newsletter (content) VALUES ('hello');", - 'SELECT * FROM newsletter -- normalization test' - ); - self::assertEquals([['id', 'content'], ['1', 'hello']], $result->getResult()); - self::assertCount(1, $cache->getValues(), 'cache hit'); - - $result = $dbRunnerService->runQuery( - " - CREATE TABLE newsletter (id INTEGER PRIMARY KEY, content TEXT); - INSERT INTO newsletter (content) VALUES ('hello');", - "SELECT * FROM newsletter WHERE content == 'hello'" - ); - self::assertEquals([['id', 'content'], ['1', 'hello']], $result->getResult()); - self::assertCount(2, $cache->getValues(), 'cache not hit'); - } - - /** - * @throws InvalidArgumentException - */ - public function testCacheException(): void - { - $cache = new ArrayAdapter(); - $dbRunnerService = new TestableDbrunnerService($cache); - - $this->expectException(SchemaExecuteException::class); - $this->expectExceptionMessageMatches('/syntax error/'); - $dbRunnerService->runQuery('ABCDABCBDABCDABCBDABCDABCBDABCDABCBD', 'SELECT * FROM newsletter'); - } -} - -readonly class TestableDbrunnerService extends DbRunnerService -{ - public function getDbRunner(): DbRunner - { - return $this->dbRunner; - } -} diff --git a/tests/Service/DbRunnerTest.php b/tests/Service/DbRunnerTest.php deleted file mode 100644 index 74d28de..0000000 --- a/tests/Service/DbRunnerTest.php +++ /dev/null @@ -1,453 +0,0 @@ ->|null, class-string<\Throwable>|null}> - */ - public static function runQueryProvider(): array - { - return [ - [ - "CREATE TABLE test ( - id INTEGER PRIMARY KEY, - name TEXT - ); - - INSERT INTO test (name) VALUES ('Alice'); - INSERT INTO test (name) VALUES ('Bob');", - 'SELECT * FROM test;', - [ - ['id', 'name'], - ['1', 'Alice'], - ['2', 'Bob'], - ], /* result */ - null, /* exception */ - ], - [ - "CREATE TABLE test ( - id INTEGER PRIMARY KEY, - name TEXT - ); - - INSERT INTO test (name) VALUES ('Alice'); - INSERT INTO test (name) VALUES ('Bob');", - '', - [], /* result */ - QueryExecuteException::class, /* exception */ - ], - [ - "CREATE TABLE test ( - id INTEGER PRIMARY KEY, - name TEXT - ); - - INSERT INTO test (name) VALUES ('Alice'); - INSERT INTO test (name) VALUES ('Bob');", - "UPDATE test SET name = 'Charlie' WHERE id = 1;", - null, /* result */ - QueryExecuteException::class, /* exception */ - ], - [ - "CREATE TABLE test ( - id INTEGER PRIMARY KEY, - name TEXT - ); - - INSERT INTO test (name) VALUES ('Alice'); - INSERT INTO test (name) VALUES ('Bob');", - "UPDATE test SET name = 'Charlie' WHERE id = 1 RETURNING *;", - [ - ['id', 'name'], - ['1', 'Charlie'], - ], /* result */ - QueryExecuteException::class, /* exception */ - ], - [ - "CREATE TABLE test ( - id INTEGER PRIMARY KEY, - name TEXT - ); - - INSERT INTO test (name) VALUES ('Alice'); - INSERT INTO test (name) VALUES ('Bob');", - 'SELECT * FROM unknown_table;', - null, /* result */ - QueryExecuteException::class, /* exception */ - ], - [ - "CREATE TABLE test ( - id INTEGER PRIMARY KEY, - name TEXT - ); - - INSERT INTO test (name) VALUES ('Alice'); - INSERT INTO test (name) VALUES ('Bob');", - 'SELECT * FROM test WHERE id = @1;', - null, /* result */ - null, /* exception */ - ], - [ - "CREATE TABLE test ( - id INTEGER PRIMARY KEY, - name TEXT - ); - - INSERT INTO test (name) VALUES ('Alice'); - INSERT INTO test (name) VALUES ('Bob');", - "SELECT * FROM test WHERE id = ':D)D)D))D)D)D)D)D;", - null, /* result */ - QueryExecuteException::class, /* exception */ - ], - [ - 'CREATE TABLE test ( - id INTEGER PRIMARY KEY, - name TEXT - ); - - ABCDEFG;', - 'SELECT * FROM test;', - null, /* result */ - SchemaExecuteException::class, /* exception */ - ], - [ - 'CREATE TABLE test ( - id INTEGER PRIMARY KEY, - name TEXT - ); - - INSERT INTO test VALUES (1, NULL);', - 'SELECT * FROM test;', - [ - ['id', 'name'], - ['1', 'NULL'], - ], /* result */ - null, /* exception */ - ], - [ - 'CREATE TABLE test ( - id INTEGER PRIMARY KEY, - name TEXT - ); - - INSERT INTO test VALUES (1, 1.23);', - 'SELECT * FROM test;', - [ - ['id', 'name'], - ['1', '1.23'], - ], /* result */ - null, /* exception */ - ], - [ - "CREATE TABLE test ( - id INTEGER PRIMARY KEY, - name TEXT - ); - - INSERT INTO test VALUES (1, x'68656c6c6f');", - 'SELECT * FROM test;', - [ - ['id', 'name'], - ['1', 'hello'], - ], /* result */ - null, /* exception */ - ], - [ - '', - 'SELECT 1;', - [ - ['1'], - ['1'], - ], /* result */ - null, /* exception */ - ], - [ - "CREATE TABLE records ( - RecordID INTEGER PRIMARY KEY, -- Assuming a unique identifier for each record - ClassNo varchar(5) NOT NULL, -- Stores the class number as a string - YMD DATE NOT NULL, -- Stores the date in 'YYYY-MM-DD' format - Leave INTEGER DEFAULT 0, -- Stores the leave count for personal leave - SickLeave INTEGER DEFAULT 0, -- Stores the leave count for sick leave - PublicLeave INTEGER DEFAULT 0, -- Stores the leave count for public leave - Absent INTEGER DEFAULT 0 -- Stores the count for absences -); - -INSERT INTO records (RecordID, ClassNo, YMD, Leave, SickLeave, PublicLeave, Absent) VALUES - (1, '101A', '2018-03-15', 2, 1, 0, 0), - (2, '101B', '2018-03-16', 0, 0, 1, 1), - (3, '102A', '2018-03-17', 1, 0, 2, 0), - (4, '101A', '2018-04-15', 0, 1, 0, 1), - (5, '102B', '2018-05-20', 3, 0, 0, 0), - (6, '101B', '2018-06-25', 0, 2, 0, 1), - (7, '101C', '2018-07-10', 1, 1, 1, 0), - (8, '103A', '2018-08-30', 0, 0, 3, 1), - (9, '101A', '2019-09-01', 2, 1, 0, 1), -- Different year for variety - (10, '102A', '2018-10-11', 0, 0, 1, 0);", - 'SELECT - LEFT(records.ClassNo, 3) AS 班級, - SUM(records.Leave) AS 事假總計, - SUM(records.SickLeave) AS 病假總計, - SUM(records.PublicLeave) AS 公假總計, - SUM(records.Absent) AS 曠課總計 -FROM - records -WHERE - YEAR(YMD) = 2018 -group BY - LEFT(records.ClassNo, 3) -', - [ - ['班級', '事假總計', '病假總計', '公假總計', '曠課總計'], - ['101', '3', '5', '2', '3'], - ['102', '4', '0', '3', '0'], - ['103', '0', '0', '3', '1'], - ], /* result */ - null, /* exception */ - ], - ]; - } - - /** - * @dataProvider hashProvider - */ - public function testHashStatement(string $leftStmt, string $rightStmt): void - { - $dbrunner = new DbRunner(); - - $leftHash = $dbrunner->hashStatement($leftStmt); - $rightHash = $dbrunner->hashStatement($rightStmt); - - self::assertEquals($leftHash, $rightHash); - } - - /** - * @dataProvider hashProvider - */ - public function testHashInvalidStatement(string $invalidStmt): void - { - $this->expectNotToPerformAssertions(); - - $dbrunner = new DbRunner(); - - // don't throw an exception - $dbrunner->hashStatement($invalidStmt); - } - - /** - * @dataProvider runQueryProvider - * - * @param ?array> $expect - * @param ?class-string<\Throwable> $exception - * - * @throws \Throwable - */ - public function testRunQuery(string $schema, string $query, ?array $expect, ?string $exception): void - { - $dbrunner = new DbRunner(); - - if (null !== $exception) { - $this->expectException($exception); - } elseif (null === $expect) { - $this->expectNotToPerformAssertions(); - } - - $result = $dbrunner->runQuery($schema, $query); - if (null === $expect) { - return; - } - - self::assertEquals($expect, $result->getResult()); - } - - public function testRunQueryCte(): void - { - $dbrunner = new DbRunner(5); - - $schema = "CREATE TABLE test ( - id INTEGER PRIMARY KEY, - name TEXT - ); - - INSERT INTO test (name) VALUES ('Alice'); - INSERT INTO test (name) VALUES ('Bob');"; - $query = 'WITH RECURSIVE cte (n) AS ( - SELECT 1 - UNION ALL - SELECT n + 1 FROM cte - ) - SELECT * FROM cte;'; - - $this->expectException(ResourceException::class); - $dbrunner->runQuery($schema, $query); - } - - public function testRunQueryBigPayload(): void - { - $dbrunner = new DbRunner(timeout: 1); - - $schema = ''; - $query = 'SELECT 1,2,3,4,5,6,randomblob(1000000000);'; - - $this->expectException(TimedOutException::class); - $dbrunner->runQuery($schema, $query); - } - - public function testRunQueryYear(): void - { - $dbrunner = new DbRunner(); - - $result = $dbrunner->runQuery('', 'SELECT year("2021-01-01")'); - self::assertEquals([['year("2021-01-01")'], ['2021']], $result->getResult()); - } - - public function testRunQueryMonth(): void - { - $dbrunner = new DbRunner(); - - $result = $dbrunner->runQuery('', 'SELECT month("2021-01-01")'); - self::assertEquals([['month("2021-01-01")'], ['1']], $result->getResult()); - } - - public function testRunQueryDay(): void - { - $dbrunner = new DbRunner(); - - $result = $dbrunner->runQuery('', 'SELECT day("2021-01-01")'); - self::assertEquals([['day("2021-01-01")'], ['1']], $result->getResult()); - } - - public function testRunQueryIf(): void - { - $dbrunner = new DbRunner(); - - $result = $dbrunner->runQuery('', 'SELECT if(1, 2, 3)'); - self::assertEquals([['if(1, 2, 3)'], ['2']], $result->getResult()); - - $result = $dbrunner->runQuery('', 'SELECT if(0, 2, 3)'); - self::assertEquals([['if(0, 2, 3)'], ['3']], $result->getResult()); - } - - public function testRunQueryLeft(): void - { - $dbrunner = new DbRunner(); - - $testcases = [ - 'left("abcdef", 3)' => 'abc', - 'left("1234567", 8)' => '1234567', - 'left("hello", 2)' => 'he', - 'left("hello", 0)' => '', - 'left("hello", 6)' => 'hello', - ]; - - foreach ($testcases as $query => $expected) { - $result = $dbrunner->runQuery('', 'SELECT '.$query); - self::assertEquals([[$query], [$expected]], $result->getResult()); - } - - $result = $dbrunner->runQuery('', 'SELECT left(c, 6) FROM (SELECT \'hello\' AS c)'); - self::assertEquals([['left(c, 6)'], ['hello']], $result->getResult()); - } - - public function testRunQuerySum(): void - { - $dbrunner = new DbRunner(); - - $result = $dbrunner->runQuery('', 'SELECT sum(1)'); - self::assertEquals([['sum(1)'], ['1']], $result->getResult()); - } - - public function testSchemaCache(): void - { - $dbrunner = new DbRunner(); - - $schema = 'CREATE TABLE test ( - id INTEGER PRIMARY KEY, - name BLOB - ); - - INSERT INTO test (name) VALUES (randomblob(1000000));'; - - $query = 'SELECT * FROM test;'; - - $firstResult = $dbrunner->runQuery($schema, $query); - $secondResult = $firstResult; - - // check if it always ran schema - // for an uncached case, it should take a lot of time - // for a cached case, it should be fast (~3s instead of ~6s) - for ($i = 0; $i < 50; ++$i) { - $secondResult = $dbrunner->runQuery($schema, $query); - } - - self::assertEquals($firstResult, $secondResult); - } -} diff --git a/tests/SqlRunner/SqlRunnerServiceTest.php b/tests/SqlRunner/SqlRunnerServiceTest.php new file mode 100644 index 0000000..524a51d --- /dev/null +++ b/tests/SqlRunner/SqlRunnerServiceTest.php @@ -0,0 +1,202 @@ +expectException(RunnerException::class); + $this->expectExceptionMessageMatches('/^CLIENT_ERROR: /'); + + $httpClient = self::createMock(HttpClientInterface::class); + $httpClient + ->expects(self::once()) + ->method('request') + ->willThrowException(new TransportException()) + ; + + $serializer = self::createMock(SerializerInterface::class); + $serializer + ->expects(self::never()) + ->method('deserialize') + ; + + $sqlRunnerService = new \App\Service\SqlRunnerService($httpClient, $serializer, ''); + $sqlRunnerService->runQuery(new \App\Entity\SqlRunnerDto\SqlRunnerRequest()); + } + + public function testRunQueryProtocolError(): void + { + $this->expectException(RunnerException::class); + $this->expectExceptionMessageMatches('/^PROTOCOL_ERROR: /'); + + $httpClient = self::createMock(HttpClientInterface::class); + $httpClient + ->expects(self::once()) + ->method('request') + ->willReturn(self::createMock(ResponseInterface::class)) + ; + + $serializer = self::createMock(SerializerInterface::class); + $serializer + ->expects(self::once()) + ->method('deserialize') + ->willThrowException(new \Symfony\Component\Serializer\Exception\NotNormalizableValueException()) + ; + + $sqlRunnerService = new \App\Service\SqlRunnerService($httpClient, $serializer, ''); + $sqlRunnerService->runQuery(new \App\Entity\SqlRunnerDto\SqlRunnerRequest()); + } + + public function testRunQueryRunnerException(): void + { + $this->expectException(RunnerException::class); + $this->expectExceptionMessage('INTERNAL_ERROR: Internal error'); + + $httpClient = self::createMock(HttpClientInterface::class); + $httpClient + ->expects(self::once()) + ->method('request') + ->willReturn(self::createMock(ResponseInterface::class)) + ; + + $serializer = self::createMock(SerializerInterface::class); + $serializer + ->expects(self::once()) + ->method('deserialize') + ->willReturn( + (new SqlRunnerResponse()) + ->setSuccess(false) + ->setCode('INTERNAL_ERROR') + ->setMessage('Internal error') + ) + ; + + $sqlRunnerService = new \App\Service\SqlRunnerService($httpClient, $serializer, ''); + $sqlRunnerService->runQuery(new \App\Entity\SqlRunnerDto\SqlRunnerRequest()); + } + + public function testRunQueryQueryException(): void + { + $this->expectException(\App\Exception\SqlRunner\QueryExecuteException::class); + $this->expectExceptionMessage('Query error'); + + $httpClient = self::createMock(HttpClientInterface::class); + $httpClient + ->expects(self::once()) + ->method('request') + ->willReturn(self::createMock(ResponseInterface::class)) + ; + + $serializer = self::createMock(SerializerInterface::class); + $serializer + ->expects(self::once()) + ->method('deserialize') + ->willReturn( + (new SqlRunnerResponse()) + ->setSuccess(false) + ->setCode('QUERY_ERROR') + ->setMessage('Query error') + ) + ; + + $sqlRunnerService = new \App\Service\SqlRunnerService($httpClient, $serializer, ''); + $sqlRunnerService->runQuery(new \App\Entity\SqlRunnerDto\SqlRunnerRequest()); + } + + public function testRunQuerySchemaException(): void + { + $this->expectException(\App\Exception\SqlRunner\SchemaExecuteException::class); + $this->expectExceptionMessage('Schema error'); + + $httpClient = self::createMock(HttpClientInterface::class); + $httpClient + ->expects(self::once()) + ->method('request') + ->willReturn(self::createMock(ResponseInterface::class)) + ; + + $serializer = self::createMock(SerializerInterface::class); + $serializer + ->expects(self::once()) + ->method('deserialize') + ->willReturn( + (new SqlRunnerResponse()) + ->setSuccess(false) + ->setCode('SCHEMA_ERROR') + ->setMessage('Schema error') + ) + ; + + $sqlRunnerService = new \App\Service\SqlRunnerService($httpClient, $serializer, ''); + $sqlRunnerService->runQuery(new \App\Entity\SqlRunnerDto\SqlRunnerRequest()); + } + + public function testRunQueryBadPayload(): void + { + $this->expectException(RunnerException::class); + $this->expectExceptionMessageMatches('/^BAD_PAYLOAD: /'); + + $httpClient = self::createMock(HttpClientInterface::class); + $httpClient + ->expects(self::once()) + ->method('request') + ->willReturn(self::createMock(ResponseInterface::class)) + ; + + $serializer = self::createMock(SerializerInterface::class); + $serializer + ->expects(self::once()) + ->method('deserialize') + ->willReturn( + (new SqlRunnerResponse()) + ->setSuccess(false) + ->setCode('BAD_PAYLOAD') + ->setMessage('Bad Payload') + ) + ; + + $sqlRunnerService = new \App\Service\SqlRunnerService($httpClient, $serializer, ''); + $sqlRunnerService->runQuery(new \App\Entity\SqlRunnerDto\SqlRunnerRequest()); + } + + public function testRunQuerySuccess(): void + { + $httpClient = self::createMock(HttpClientInterface::class); + $httpClient + ->expects(self::once()) + ->method('request') + ->willReturn(self::createMock(ResponseInterface::class)) + ; + + $result = (new \App\Entity\SqlRunnerDto\SqlRunnerResult()) + ->setColumns(['column1', 'column2']) + ->setRows([['row1', 'row2']]); + + $serializer = self::createMock(SerializerInterface::class); + $serializer + ->expects(self::once()) + ->method('deserialize') + ->willReturn( + (new SqlRunnerResponse()) + ->setSuccess(true) + ->setData($result) + ) + ; + + $sqlRunnerService = new \App\Service\SqlRunnerService($httpClient, $serializer, ''); + $result = $sqlRunnerService->runQuery(new \App\Entity\SqlRunnerDto\SqlRunnerRequest()); + self::assertEquals(['column1', 'column2'], $result->getColumns()); + } +} diff --git a/translations/messages.zh_TW.yaml b/translations/messages.zh_TW.yaml index 19ca4cc..8e29058 100644 --- a/translations/messages.zh_TW.yaml +++ b/translations/messages.zh_TW.yaml @@ -161,6 +161,10 @@ challenge: row-unmatched: 回傳列數和正確答案不一致(正確答案有 %expected% 列,你回答了 %actual% 列)。 errors: no-query-yet: 寫完查詢後按下「提交」來查看執行結果。 + unavailable: | + SQL Runner 服務發生故障,請稍後再試。 + 錯誤:%error% + schema-error: Schema 有問題,請回報給我們:%error% answer-query-failure: 正確答案也是個錯誤的 SQL 查詢:%error% user-query-error: 你的 SQL 查詢執行失敗:%error% user-query-failure: 你的 SQL 查詢不正確:%error%