From f9aa02ec1a4c24e80a206857d4f68198bb027bb4 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 11 Nov 2024 01:12:29 +0800 Subject: HALF WORK: 2024.12.19 Re-organize file structure. --- .gitignore | 10 +- .vscode/settings.json | 3 +- crupest-words.txt | 52 +- cspell.yaml | 2 +- docker/blog/Dockerfile | 9 + docker/blog/daemon.bash | 19 + docker/blog/install-hugo.bash | 22 + docker/blog/update.bash | 30 + docker/crupest-api/CrupestApi/.dockerignore | 2 - docker/crupest-api/CrupestApi/.gitignore | 4 - .../Crud/ColumnTypeInfoTest.cs | 39 - .../Crud/CrudIntegratedTest.cs | 200 -- .../Crud/CrudServiceTest.cs | 77 - .../Crud/SqlCompareHelper.cs | 85 - .../CrupestApi.Commons.Tests/Crud/TableInfoTest.cs | 35 - .../CrupestApi.Commons.Tests/Crud/TestEntity.cs | 23 - .../CrupestApi.Commons.Tests.csproj | 29 - .../CrupestApi/CrupestApi.Commons.Tests/Usings.cs | 1 - .../CrupestApi/CrupestApi.Commons/Config.cs | 23 - .../CrupestApi.Commons/Crud/ColumnInfo.cs | 236 --- .../CrupestApi.Commons/Crud/ColumnMetadata.cs | 188 -- .../CrupestApi.Commons/Crud/ColumnTypeInfo.cs | 218 -- .../CrupestApi.Commons/Crud/CrudService.cs | 132 -- .../Crud/CrudServiceCollectionExtensions.cs | 34 - .../Crud/CrudWebApplicationExtensions.cs | 101 - .../CrupestApi.Commons/Crud/DbConnectionFactory.cs | 75 - .../CrupestApi.Commons/Crud/DbNullValue.cs | 9 - .../CrupestApi.Commons/Crud/EntityJsonHelper.cs | 206 -- .../CrupestApi/CrupestApi.Commons/Crud/IClause.cs | 24 - .../CrupestApi.Commons/Crud/InsertClause.cs | 77 - .../Crud/Migrations/DatabaseMigrator.cs | 44 - .../Crud/Migrations/SqliteDatabaseMigrator.cs | 175 -- .../CrupestApi.Commons/Crud/OrderByClause.cs | 50 - .../CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs | 73 - .../CrupestApi/CrupestApi.Commons/Crud/README.md | 47 - .../CrupestApi.Commons/Crud/TableInfo.cs | 628 ------ .../CrupestApi.Commons/Crud/UpdateClause.cs | 77 - .../CrupestApi.Commons/Crud/UserException.cs | 15 - .../CrupestApi.Commons/Crud/WhereClause.cs | 182 -- .../CrupestApi.Commons/CrupestApi.Commons.csproj | 16 - .../CrupestApi.Commons/EntityNotExistException.cs | 8 - .../CrupestApi.Commons/HttpContextExtensions.cs | 113 -- .../CrupestApi.Commons/Secrets/ISecretService.cs | 8 - .../CrupestApi.Commons/Secrets/SecretInfo.cs | 48 - .../CrupestApi.Commons/Secrets/SecretService.cs | 48 - .../Secrets/SecretServiceCollectionExtensions.cs | 12 - .../CrupestApi.Commons/Secrets/SecretsConstants.cs | 6 - .../CrupestApi.Files/CrupestApi.Files.csproj | 20 - .../CrupestApi/CrupestApi.Files/FilesService.cs | 6 - .../CrupestApi.Secrets/CrupestApi.Secrets.csproj | 20 - .../CrupestApi.Secrets/SecretsExtensions.cs | 19 - .../CrupestApi.Todos/CrupestApi.Todos.csproj | 15 - .../CrupestApi.Todos/TodosConfiguration.cs | 14 - .../CrupestApi/CrupestApi.Todos/TodosService.cs | 163 -- .../TodosServiceCollectionExtensions.cs | 21 - .../TodosWebApplicationExtensions.cs | 32 - docker/crupest-api/CrupestApi/CrupestApi.sln | 46 - .../CrupestApi/CrupestApi/CrupestApi.csproj | 17 - .../crupest-api/CrupestApi/CrupestApi/Program.cs | 24 - .../CrupestApi/Properties/launchSettings.json | 15 - .../CrupestApi/CrupestApi/appsettings.json | 8 - docker/crupest-api/Dockerfile | 13 - docker/crupest-blog/Dockerfile | 9 - docker/crupest-blog/daemon.bash | 19 - docker/crupest-blog/install-hugo.bash | 22 - docker/crupest-blog/update.bash | 30 - docker/crupest-debian-dev/Dockerfile | 21 - .../bootstrap/apt-source/11/add-deb-src.bash | 14 - .../bootstrap/apt-source/11/get-domain.bash | 5 - .../bootstrap/apt-source/11/replace-domain.bash | 7 - .../bootstrap/apt-source/11/replace-http.bash | 7 - .../bootstrap/apt-source/12/add-deb-src.bash | 22 - .../bootstrap/apt-source/12/get-domain.bash | 6 - .../bootstrap/apt-source/12/replace-domain.bash | 7 - .../bootstrap/apt-source/12/replace-http.bash | 7 - .../bootstrap/apt-source/china-source.txt | 1 - .../bootstrap/apt-source/install-apt-https.bash | 8 - .../bootstrap/apt-source/setup.bash | 34 - .../bootstrap/bash/bash-completion.bash | 4 - .../bootstrap/bash/code-server.bash | 2 - .../crupest-debian-dev/bootstrap/bash/dquilt.bash | 4 - .../bootstrap/extra/setup-cmake.bash | 9 - .../bootstrap/extra/setup-dotnet.bash | 10 - .../bootstrap/extra/setup-llvm.bash | 26 - docker/crupest-debian-dev/bootstrap/func.bash | 19 - .../bootstrap/get-debian-version.bash | 13 - .../bootstrap/home-dot/devscripts | 1 - .../bootstrap/home-dot/quiltrc-dpkg | 13 - .../crupest-debian-dev/bootstrap/setup-base.bash | 25 - .../bootstrap/setup-code-server.bash | 28 - docker/crupest-debian-dev/bootstrap/setup-dev.bash | 18 - .../crupest-debian-dev/bootstrap/setup-user.bash | 20 - docker/crupest-debian-dev/bootstrap/setup.bash | 30 - .../bootstrap/start/code-server.bash | 18 - docker/crupest-debian-dev/bootstrap/wait.bash | 5 - docker/crupest-nginx/Dockerfile | 8 - docker/crupest-nginx/sites/www/.dockerignore | 2 - docker/crupest-nginx/sites/www/.gitignore | 26 - docker/crupest-nginx/sites/www/.proxyrc.json | 6 - docker/crupest-nginx/sites/www/avatar.png | Bin 12038 -> 0 bytes docker/crupest-nginx/sites/www/favicon.ico | Bin 15406 -> 0 bytes docker/crupest-nginx/sites/www/index.html | 95 - docker/crupest-nginx/sites/www/package.json | 17 - docker/crupest-nginx/sites/www/pnpm-lock.yaml | 2099 -------------------- docker/crupest-nginx/sites/www/src/main.ts | 97 - docker/crupest-nginx/sites/www/src/mock-todos.ts | 126 -- docker/crupest-nginx/sites/www/src/style.css | 185 -- docker/crupest-nginx/sites/www/src/todos.ts | 29 - docker/crupest-nginx/sites/www/tsconfig.json | 19 - docker/crupest-v2ray/Dockerfile | 6 - docker/debian-dev/Dockerfile | 21 + .../bootstrap/apt-source/11/add-deb-src.bash | 14 + .../bootstrap/apt-source/11/get-domain.bash | 5 + .../bootstrap/apt-source/11/replace-domain.bash | 7 + .../bootstrap/apt-source/11/replace-http.bash | 7 + .../bootstrap/apt-source/12/add-deb-src.bash | 22 + .../bootstrap/apt-source/12/get-domain.bash | 6 + .../bootstrap/apt-source/12/replace-domain.bash | 7 + .../bootstrap/apt-source/12/replace-http.bash | 7 + .../bootstrap/apt-source/china-source.txt | 1 + .../bootstrap/apt-source/install-apt-https.bash | 8 + docker/debian-dev/bootstrap/apt-source/setup.bash | 34 + .../debian-dev/bootstrap/bash/bash-completion.bash | 4 + docker/debian-dev/bootstrap/bash/code-server.bash | 2 + docker/debian-dev/bootstrap/bash/dquilt.bash | 4 + docker/debian-dev/bootstrap/extra/setup-cmake.bash | 9 + .../debian-dev/bootstrap/extra/setup-dotnet.bash | 10 + docker/debian-dev/bootstrap/extra/setup-llvm.bash | 26 + docker/debian-dev/bootstrap/func.bash | 19 + .../debian-dev/bootstrap/get-debian-version.bash | 13 + docker/debian-dev/bootstrap/home-dot/devscripts | 1 + docker/debian-dev/bootstrap/home-dot/quiltrc-dpkg | 13 + docker/debian-dev/bootstrap/setup-base.bash | 25 + docker/debian-dev/bootstrap/setup-code-server.bash | 28 + docker/debian-dev/bootstrap/setup-dev.bash | 18 + docker/debian-dev/bootstrap/setup-user.bash | 20 + docker/debian-dev/bootstrap/setup.bash | 30 + docker/debian-dev/bootstrap/start/code-server.bash | 18 + docker/debian-dev/bootstrap/wait.bash | 5 + docker/nginx/Dockerfile | 8 + docker/nginx/sites/www/.dockerignore | 2 + docker/nginx/sites/www/.gitignore | 26 + docker/nginx/sites/www/.proxyrc.json | 6 + docker/nginx/sites/www/avatar.png | Bin 0 -> 12038 bytes docker/nginx/sites/www/favicon.ico | Bin 0 -> 15406 bytes docker/nginx/sites/www/index.html | 95 + docker/nginx/sites/www/package.json | 17 + docker/nginx/sites/www/pnpm-lock.yaml | 2099 ++++++++++++++++++++ docker/nginx/sites/www/src/main.ts | 97 + docker/nginx/sites/www/src/mock-todos.ts | 126 ++ docker/nginx/sites/www/src/style.css | 185 ++ docker/nginx/sites/www/src/todos.ts | 29 + docker/nginx/sites/www/tsconfig.json | 19 + docker/v2ray/Dockerfile | 6 + .../docker/crupest-api/CrupestApi/.dockerignore | 2 + dropped/docker/crupest-api/CrupestApi/.gitignore | 4 + .../Crud/ColumnTypeInfoTest.cs | 39 + .../Crud/CrudIntegratedTest.cs | 200 ++ .../Crud/CrudServiceTest.cs | 77 + .../Crud/SqlCompareHelper.cs | 85 + .../CrupestApi.Commons.Tests/Crud/TableInfoTest.cs | 35 + .../CrupestApi.Commons.Tests/Crud/TestEntity.cs | 23 + .../CrupestApi.Commons.Tests.csproj | 29 + .../CrupestApi/CrupestApi.Commons.Tests/Usings.cs | 1 + .../CrupestApi/CrupestApi.Commons/Config.cs | 23 + .../CrupestApi.Commons/Crud/ColumnInfo.cs | 236 +++ .../CrupestApi.Commons/Crud/ColumnMetadata.cs | 188 ++ .../CrupestApi.Commons/Crud/ColumnTypeInfo.cs | 218 ++ .../CrupestApi.Commons/Crud/CrudService.cs | 132 ++ .../Crud/CrudServiceCollectionExtensions.cs | 34 + .../Crud/CrudWebApplicationExtensions.cs | 101 + .../CrupestApi.Commons/Crud/DbConnectionFactory.cs | 75 + .../CrupestApi.Commons/Crud/DbNullValue.cs | 9 + .../CrupestApi.Commons/Crud/EntityJsonHelper.cs | 206 ++ .../CrupestApi/CrupestApi.Commons/Crud/IClause.cs | 24 + .../CrupestApi.Commons/Crud/InsertClause.cs | 77 + .../Crud/Migrations/DatabaseMigrator.cs | 44 + .../Crud/Migrations/SqliteDatabaseMigrator.cs | 175 ++ .../CrupestApi.Commons/Crud/OrderByClause.cs | 50 + .../CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs | 73 + .../CrupestApi/CrupestApi.Commons/Crud/README.md | 47 + .../CrupestApi.Commons/Crud/TableInfo.cs | 628 ++++++ .../CrupestApi.Commons/Crud/UpdateClause.cs | 77 + .../CrupestApi.Commons/Crud/UserException.cs | 15 + .../CrupestApi.Commons/Crud/WhereClause.cs | 182 ++ .../CrupestApi.Commons/CrupestApi.Commons.csproj | 16 + .../CrupestApi.Commons/EntityNotExistException.cs | 8 + .../CrupestApi.Commons/HttpContextExtensions.cs | 113 ++ .../CrupestApi.Commons/Secrets/ISecretService.cs | 8 + .../CrupestApi.Commons/Secrets/SecretInfo.cs | 48 + .../CrupestApi.Commons/Secrets/SecretService.cs | 48 + .../Secrets/SecretServiceCollectionExtensions.cs | 12 + .../CrupestApi.Commons/Secrets/SecretsConstants.cs | 6 + .../CrupestApi.Files/CrupestApi.Files.csproj | 20 + .../CrupestApi/CrupestApi.Files/FilesService.cs | 6 + .../CrupestApi.Secrets/CrupestApi.Secrets.csproj | 20 + .../CrupestApi.Secrets/SecretsExtensions.cs | 19 + .../CrupestApi.Todos/CrupestApi.Todos.csproj | 15 + .../CrupestApi.Todos/TodosConfiguration.cs | 14 + .../CrupestApi/CrupestApi.Todos/TodosService.cs | 163 ++ .../TodosServiceCollectionExtensions.cs | 21 + .../TodosWebApplicationExtensions.cs | 32 + .../docker/crupest-api/CrupestApi/CrupestApi.sln | 46 + .../CrupestApi/CrupestApi/CrupestApi.csproj | 17 + .../crupest-api/CrupestApi/CrupestApi/Program.cs | 24 + .../CrupestApi/Properties/launchSettings.json | 15 + .../CrupestApi/CrupestApi/appsettings.json | 8 + dropped/docker/crupest-api/Dockerfile | 13 + dropped/template/crupest-api-config.json.template | 10 + dropped/template/docker-compose.yaml.template | 24 + dropped/template/nginx/timeline.conf.template | 21 + dropped/template/v2ray-client-config.json.template | 46 + template/crupest-api-config.json.template | 10 - template/docker-compose.yaml.template | 58 +- template/nginx/2fa.conf.template | 19 + template/nginx/cert-only.conf.template | 13 - template/nginx/code.conf.template | 22 + template/nginx/common/acme-challenge | 3 + template/nginx/common/https-redirect | 3 + template/nginx/common/proxy-common | 7 + template/nginx/git.conf.template | 22 + template/nginx/mail.conf.template | 27 + template/nginx/redirect.conf.template | 23 - template/nginx/reverse-proxy.conf.template | 32 - template/nginx/root.conf.template | 61 +- template/nginx/server.json | 39 - template/nginx/server.schema.json | 93 - template/nginx/server.ts | 36 - template/nginx/static-file.conf.template | 23 - template/nginx/timeline.conf.template | 7 + template/v2ray-client-config.json.template | 46 - template2/nginx/domain.conf.template | 19 - template2/nginx/global/client-max-body-size.conf | 1 - template2/nginx/global/forbid-unknown-domain.conf | 8 - template2/nginx/global/ssl.conf.template | 17 - template2/nginx/global/websocket.conf | 4 - template2/nginx/http/444.segment | 3 - template2/nginx/http/redirect-to-https.segment | 3 - template2/nginx/https/redirect.segment.template | 7 - .../nginx/https/reverse-proxy.segment.template | 10 - .../static-file.no-strip-prefix.segment.template | 3 - template2/nginx/https/static-file.segment.template | 3 - template2/nginx/server.schema.json | 96 - template2/nginx/server.ts | 66 - template2/server.json.template | 58 - tools/cru-py/.gitignore | 3 + 246 files changed, 7248 insertions(+), 7725 deletions(-) create mode 100644 docker/blog/Dockerfile create mode 100644 docker/blog/daemon.bash create mode 100644 docker/blog/install-hugo.bash create mode 100644 docker/blog/update.bash delete mode 100644 docker/crupest-api/CrupestApi/.dockerignore delete mode 100644 docker/crupest-api/CrupestApi/.gitignore delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/ColumnTypeInfoTest.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudIntegratedTest.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudServiceTest.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/SqlCompareHelper.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TableInfoTest.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TestEntity.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/CrupestApi.Commons.Tests.csproj delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Usings.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Config.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudServiceCollectionExtensions.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbNullValue.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/IClause.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/InsertClause.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/DatabaseMigrator.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/SqliteDatabaseMigrator.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/OrderByClause.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/README.md delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UpdateClause.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UserException.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/WhereClause.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/CrupestApi.Commons.csproj delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/EntityNotExistException.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/HttpContextExtensions.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/ISecretService.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretInfo.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretServiceCollectionExtensions.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretsConstants.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Files/CrupestApi.Files.csproj delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Files/FilesService.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Secrets/CrupestApi.Secrets.csproj delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretsExtensions.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Todos/CrupestApi.Todos.csproj delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosConfiguration.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosService.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosServiceCollectionExtensions.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosWebApplicationExtensions.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.sln delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi/CrupestApi.csproj delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi/Program.cs delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi/Properties/launchSettings.json delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi/appsettings.json delete mode 100644 docker/crupest-api/Dockerfile delete mode 100644 docker/crupest-blog/Dockerfile delete mode 100755 docker/crupest-blog/daemon.bash delete mode 100755 docker/crupest-blog/install-hugo.bash delete mode 100755 docker/crupest-blog/update.bash delete mode 100644 docker/crupest-debian-dev/Dockerfile delete mode 100755 docker/crupest-debian-dev/bootstrap/apt-source/11/add-deb-src.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/apt-source/11/get-domain.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/apt-source/11/replace-domain.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/apt-source/11/replace-http.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/apt-source/12/add-deb-src.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/apt-source/12/get-domain.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/apt-source/12/replace-domain.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/apt-source/12/replace-http.bash delete mode 100644 docker/crupest-debian-dev/bootstrap/apt-source/china-source.txt delete mode 100755 docker/crupest-debian-dev/bootstrap/apt-source/install-apt-https.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/apt-source/setup.bash delete mode 100644 docker/crupest-debian-dev/bootstrap/bash/bash-completion.bash delete mode 100644 docker/crupest-debian-dev/bootstrap/bash/code-server.bash delete mode 100644 docker/crupest-debian-dev/bootstrap/bash/dquilt.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/extra/setup-cmake.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/extra/setup-dotnet.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/extra/setup-llvm.bash delete mode 100644 docker/crupest-debian-dev/bootstrap/func.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/get-debian-version.bash delete mode 100644 docker/crupest-debian-dev/bootstrap/home-dot/devscripts delete mode 100644 docker/crupest-debian-dev/bootstrap/home-dot/quiltrc-dpkg delete mode 100755 docker/crupest-debian-dev/bootstrap/setup-base.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/setup-code-server.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/setup-dev.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/setup-user.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/setup.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/start/code-server.bash delete mode 100755 docker/crupest-debian-dev/bootstrap/wait.bash delete mode 100644 docker/crupest-nginx/Dockerfile delete mode 100644 docker/crupest-nginx/sites/www/.dockerignore delete mode 100644 docker/crupest-nginx/sites/www/.gitignore delete mode 100644 docker/crupest-nginx/sites/www/.proxyrc.json delete mode 100755 docker/crupest-nginx/sites/www/avatar.png delete mode 100644 docker/crupest-nginx/sites/www/favicon.ico delete mode 100644 docker/crupest-nginx/sites/www/index.html delete mode 100644 docker/crupest-nginx/sites/www/package.json delete mode 100644 docker/crupest-nginx/sites/www/pnpm-lock.yaml delete mode 100644 docker/crupest-nginx/sites/www/src/main.ts delete mode 100644 docker/crupest-nginx/sites/www/src/mock-todos.ts delete mode 100644 docker/crupest-nginx/sites/www/src/style.css delete mode 100644 docker/crupest-nginx/sites/www/src/todos.ts delete mode 100644 docker/crupest-nginx/sites/www/tsconfig.json delete mode 100644 docker/crupest-v2ray/Dockerfile create mode 100644 docker/debian-dev/Dockerfile create mode 100644 docker/debian-dev/bootstrap/apt-source/11/add-deb-src.bash create mode 100644 docker/debian-dev/bootstrap/apt-source/11/get-domain.bash create mode 100644 docker/debian-dev/bootstrap/apt-source/11/replace-domain.bash create mode 100644 docker/debian-dev/bootstrap/apt-source/11/replace-http.bash create mode 100644 docker/debian-dev/bootstrap/apt-source/12/add-deb-src.bash create mode 100644 docker/debian-dev/bootstrap/apt-source/12/get-domain.bash create mode 100644 docker/debian-dev/bootstrap/apt-source/12/replace-domain.bash create mode 100644 docker/debian-dev/bootstrap/apt-source/12/replace-http.bash create mode 100644 docker/debian-dev/bootstrap/apt-source/china-source.txt create mode 100644 docker/debian-dev/bootstrap/apt-source/install-apt-https.bash create mode 100644 docker/debian-dev/bootstrap/apt-source/setup.bash create mode 100644 docker/debian-dev/bootstrap/bash/bash-completion.bash create mode 100644 docker/debian-dev/bootstrap/bash/code-server.bash create mode 100644 docker/debian-dev/bootstrap/bash/dquilt.bash create mode 100644 docker/debian-dev/bootstrap/extra/setup-cmake.bash create mode 100644 docker/debian-dev/bootstrap/extra/setup-dotnet.bash create mode 100644 docker/debian-dev/bootstrap/extra/setup-llvm.bash create mode 100644 docker/debian-dev/bootstrap/func.bash create mode 100644 docker/debian-dev/bootstrap/get-debian-version.bash create mode 100644 docker/debian-dev/bootstrap/home-dot/devscripts create mode 100644 docker/debian-dev/bootstrap/home-dot/quiltrc-dpkg create mode 100644 docker/debian-dev/bootstrap/setup-base.bash create mode 100644 docker/debian-dev/bootstrap/setup-code-server.bash create mode 100644 docker/debian-dev/bootstrap/setup-dev.bash create mode 100644 docker/debian-dev/bootstrap/setup-user.bash create mode 100644 docker/debian-dev/bootstrap/setup.bash create mode 100644 docker/debian-dev/bootstrap/start/code-server.bash create mode 100644 docker/debian-dev/bootstrap/wait.bash create mode 100644 docker/nginx/Dockerfile create mode 100644 docker/nginx/sites/www/.dockerignore create mode 100644 docker/nginx/sites/www/.gitignore create mode 100644 docker/nginx/sites/www/.proxyrc.json create mode 100644 docker/nginx/sites/www/avatar.png create mode 100644 docker/nginx/sites/www/favicon.ico create mode 100644 docker/nginx/sites/www/index.html create mode 100644 docker/nginx/sites/www/package.json create mode 100644 docker/nginx/sites/www/pnpm-lock.yaml create mode 100644 docker/nginx/sites/www/src/main.ts create mode 100644 docker/nginx/sites/www/src/mock-todos.ts create mode 100644 docker/nginx/sites/www/src/style.css create mode 100644 docker/nginx/sites/www/src/todos.ts create mode 100644 docker/nginx/sites/www/tsconfig.json create mode 100644 docker/v2ray/Dockerfile create mode 100644 dropped/docker/crupest-api/CrupestApi/.dockerignore create mode 100644 dropped/docker/crupest-api/CrupestApi/.gitignore create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/ColumnTypeInfoTest.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudIntegratedTest.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudServiceTest.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/SqlCompareHelper.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TableInfoTest.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TestEntity.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/CrupestApi.Commons.Tests.csproj create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Usings.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Config.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudServiceCollectionExtensions.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbNullValue.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/IClause.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/InsertClause.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/DatabaseMigrator.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/SqliteDatabaseMigrator.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/OrderByClause.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/README.md create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UpdateClause.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UserException.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/WhereClause.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/CrupestApi.Commons.csproj create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/EntityNotExistException.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/HttpContextExtensions.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/ISecretService.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretInfo.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretServiceCollectionExtensions.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretsConstants.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Files/CrupestApi.Files.csproj create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Files/FilesService.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Secrets/CrupestApi.Secrets.csproj create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretsExtensions.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/CrupestApi.Todos.csproj create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosConfiguration.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosService.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosServiceCollectionExtensions.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosWebApplicationExtensions.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi.sln create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi/CrupestApi.csproj create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi/Program.cs create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi/Properties/launchSettings.json create mode 100644 dropped/docker/crupest-api/CrupestApi/CrupestApi/appsettings.json create mode 100644 dropped/docker/crupest-api/Dockerfile create mode 100644 dropped/template/crupest-api-config.json.template create mode 100644 dropped/template/docker-compose.yaml.template create mode 100644 dropped/template/nginx/timeline.conf.template create mode 100644 dropped/template/v2ray-client-config.json.template delete mode 100644 template/crupest-api-config.json.template create mode 100644 template/nginx/2fa.conf.template delete mode 100644 template/nginx/cert-only.conf.template create mode 100644 template/nginx/code.conf.template create mode 100644 template/nginx/common/acme-challenge create mode 100644 template/nginx/common/https-redirect create mode 100644 template/nginx/common/proxy-common create mode 100644 template/nginx/git.conf.template create mode 100644 template/nginx/mail.conf.template delete mode 100644 template/nginx/redirect.conf.template delete mode 100644 template/nginx/reverse-proxy.conf.template delete mode 100644 template/nginx/server.json delete mode 100644 template/nginx/server.schema.json delete mode 100644 template/nginx/server.ts delete mode 100644 template/nginx/static-file.conf.template create mode 100644 template/nginx/timeline.conf.template delete mode 100644 template/v2ray-client-config.json.template delete mode 100644 template2/nginx/domain.conf.template delete mode 100644 template2/nginx/global/client-max-body-size.conf delete mode 100644 template2/nginx/global/forbid-unknown-domain.conf delete mode 100644 template2/nginx/global/ssl.conf.template delete mode 100644 template2/nginx/global/websocket.conf delete mode 100644 template2/nginx/http/444.segment delete mode 100644 template2/nginx/http/redirect-to-https.segment delete mode 100644 template2/nginx/https/redirect.segment.template delete mode 100644 template2/nginx/https/reverse-proxy.segment.template delete mode 100644 template2/nginx/https/static-file.no-strip-prefix.segment.template delete mode 100644 template2/nginx/https/static-file.segment.template delete mode 100644 template2/nginx/server.schema.json delete mode 100644 template2/nginx/server.ts delete mode 100644 template2/server.json.template diff --git a/.gitignore b/.gitignore index 649c13b..e1349ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,13 @@ +/data +/log +/tmp +/backup +/generated + docker-compose.yaml mailserver.env crupest-api-config.json -data nginx-config -log -tmp -backup v2ray-config.json v2ray-client-config.json forgejo.app.ini.init diff --git a/.vscode/settings.json b/.vscode/settings.json index 457f44d..ff2a1bd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "python.analysis.typeCheckingMode": "basic" + "python.analysis.typeCheckingMode": "basic", + "dotnet.defaultSolution": "disable" } \ No newline at end of file diff --git a/crupest-words.txt b/crupest-words.txt index f5dd7dc..b0a9936 100644 --- a/crupest-words.txt +++ b/crupest-words.txt @@ -1,38 +1,34 @@ -crupest - -# software -forgejo -sypht -2fauth - +# debian # secret -vnext -vmess -confdir -geodata -geosite -geoip -userid - +# software # university -ustc - -# debian +2fauth aarch64 -nproc +buildpackage chroot +confdir cpio +createchroot +crupest +debhelper +debootstrap +devscripts +dquilt +forgejo +geodata +geoip +geosite +indep kmod - -buildpackage nocheck -indep -devscripts -debhelper +nproc +quiltrc +rspamd sbuild sbuildrc schroot -createchroot -debootstrap -quiltrc -dquilt +sypht +userid +ustc +vmess +vnext diff --git a/cspell.yaml b/cspell.yaml index 7a1151c..4cc35f7 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -1,6 +1,7 @@ version: "0.2" language: en dictionaries: + - crupest-words - en_US - softwareTerms - node @@ -16,7 +17,6 @@ dictionaries: dictionaryDefinitions: - name: crupest-words path: './crupest-words.txt' - addWords: true patterns: - name: libs diff --git a/docker/blog/Dockerfile b/docker/blog/Dockerfile new file mode 100644 index 0000000..7414d4e --- /dev/null +++ b/docker/blog/Dockerfile @@ -0,0 +1,9 @@ +FROM debian:latest +ARG CRUPEST_BLOG_UPDATE_INTERVAL=1d +COPY install-hugo.bash /install-hugo.bash +RUN /install-hugo.bash && rm /install-hugo.bash +ENV CRUPEST_BLOG_UPDATE_INTERVAL=${CRUPEST_BLOG_UPDATE_INTERVAL} +COPY daemon.bash update.bash /scripts/ +VOLUME [ "/public" ] +ENTRYPOINT ["tini", "--"] +CMD [ "/scripts/daemon.bash" ] diff --git a/docker/blog/daemon.bash b/docker/blog/daemon.bash new file mode 100644 index 0000000..561a80a --- /dev/null +++ b/docker/blog/daemon.bash @@ -0,0 +1,19 @@ +#! /usr/bin/env bash + +set -e + +# Check I'm root. +if [[ $EUID -ne 0 ]]; then + echo "This script must be run as root" 1>&2 + exit 1 +fi + +hugo version + +while true; do + /scripts/update.bash + + # sleep for CRUPEST_AUTO_BACKUP_INTERVAL + echo "Sleep for $CRUPEST_BLOG_UPDATE_INTERVAL for next build..." + sleep "$CRUPEST_BLOG_UPDATE_INTERVAL" +done diff --git a/docker/blog/install-hugo.bash b/docker/blog/install-hugo.bash new file mode 100644 index 0000000..a448138 --- /dev/null +++ b/docker/blog/install-hugo.bash @@ -0,0 +1,22 @@ +#! /usr/bin/env bash + +set -e + +apt-get update +apt-get install -y tini locales curl git +rm -rf /var/lib/apt/lists/* +localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 + +VERSION=$(curl -s https://api.github.com/repos/gohugoio/hugo/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') + +echo "The latest version of hugo is $VERSION." + +url="https://github.com/gohugoio/hugo/releases/download/v${VERSION}/hugo_extended_${VERSION}_linux-amd64.deb" + +echo "Download hugo from $url." + +curl -sSfOL "$url" +dpkg -i "hugo_extended_${VERSION}_linux-amd64.deb" +rm "hugo_extended_${VERSION}_linux-amd64.deb" + +echo "Hugo version: $(hugo version)." diff --git a/docker/blog/update.bash b/docker/blog/update.bash new file mode 100644 index 0000000..d4bcadc --- /dev/null +++ b/docker/blog/update.bash @@ -0,0 +1,30 @@ +#! /usr/bin/env bash + +set -e + +echo -e "\e[0;103m\e[K\e[1mBegin to build blog...\e[0m" +echo "Begin time: $(date +%Y-%m-%dT%H:%M:%SZ)" + +mkdir -p /public + +# check /blog directory exists +if [[ ! -d /blog ]]; then + echo "Directory /blog not found, clone blog repository..." + git clone https://github.com/crupest/blog.git /blog + cd /blog + git submodule update --init --recursive +else + echo "Directory /blog founded, update blog repository..." + cd /blog + git fetch -p + git reset --hard origin/master + git submodule update --init --recursive +fi + +# Now hugo it +echo "Run hugo to generate blog..." +hugo -d /public + +echo "Finish time: $(date +%Y-%m-%dT%H:%M:%SZ)" +echo -e "\e[0;102m\e[K\e[1mFinish build!\e[0m" + diff --git a/docker/crupest-api/CrupestApi/.dockerignore b/docker/crupest-api/CrupestApi/.dockerignore deleted file mode 100644 index f1c182d..0000000 --- a/docker/crupest-api/CrupestApi/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -*/obj -*/bin diff --git a/docker/crupest-api/CrupestApi/.gitignore b/docker/crupest-api/CrupestApi/.gitignore deleted file mode 100644 index 371ea59..0000000 --- a/docker/crupest-api/CrupestApi/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.vs -obj -bin -dev-config.json diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/ColumnTypeInfoTest.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/ColumnTypeInfoTest.cs deleted file mode 100644 index b9ec03e..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/ColumnTypeInfoTest.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Data; - -namespace CrupestApi.Commons.Crud.Tests; - -public class ColumnTypeInfoTest -{ - private ColumnTypeProvider _provider = new ColumnTypeProvider(); - - [Theory] - [InlineData(typeof(int), DbType.Int32, 123)] - [InlineData(typeof(long), DbType.Int64, 456)] - [InlineData(typeof(sbyte), DbType.SByte, 789)] - [InlineData(typeof(short), DbType.Int16, 101)] - [InlineData(typeof(float), DbType.Single, 1.0f)] - [InlineData(typeof(double), DbType.Double, 1.0)] - [InlineData(typeof(string), DbType.String, "Hello world!")] - [InlineData(typeof(byte[]), DbType.Binary, new byte[] { 1, 2, 3 })] - public void BasicColumnTypeTest(Type type, DbType dbType, object? value) - { - var typeInfo = _provider.Get(type); - Assert.True(typeInfo.IsSimple); - Assert.Equal(dbType, typeInfo.DbType); - Assert.Equal(value, typeInfo.ConvertFromDatabase(value)); - Assert.Equal(value, typeInfo.ConvertToDatabase(value)); - } - - [Fact] - public void DateTimeColumnTypeTest() - { - var dateTimeColumnTypeInfo = _provider.Get(typeof(DateTime)); - Assert.Equal(typeof(DateTime), dateTimeColumnTypeInfo.ClrType); - Assert.Equal(typeof(string), dateTimeColumnTypeInfo.DatabaseClrType); - - var dateTime = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); - var dateTimeString = "2000-01-01T00:00:00Z"; - Assert.Equal(dateTimeString, dateTimeColumnTypeInfo.ConvertToDatabase(dateTime)); - Assert.Equal(dateTime, dateTimeColumnTypeInfo.ConvertFromDatabase(dateTimeString)); - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudIntegratedTest.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudIntegratedTest.cs deleted file mode 100644 index bd07c70..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudIntegratedTest.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System.Net; -using System.Net.Http.Headers; -using CrupestApi.Commons.Secrets; -using Microsoft.AspNetCore.TestHost; - -namespace CrupestApi.Commons.Crud.Tests; - -public class CrudIntegratedTest : IAsyncLifetime -{ - private readonly WebApplication _app; - private HttpClient _httpClient = default!; - private HttpClient _authorizedHttpClient = default!; - private string _token = default!; - - public CrudIntegratedTest() - { - var builder = WebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); - builder.Services.AddSingleton(); - builder.Services.AddCrud(); - builder.WebHost.UseTestServer(); - _app = builder.Build(); - _app.UseCrudCore(); - _app.MapCrud("/test", "test-perm"); - } - - public async Task InitializeAsync() - { - await _app.StartAsync(); - _httpClient = _app.GetTestClient(); - - using (var scope = _app.Services.CreateScope()) - { - var secretService = (SecretService)scope.ServiceProvider.GetRequiredService(); - var key = secretService.Create(new SecretInfo - { - Key = "test-perm" - }); - _token = secretService.GetByKey(key).Secret; - } - - _authorizedHttpClient = _app.GetTestClient(); - _authorizedHttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token); - } - - public async Task DisposeAsync() - { - await _app.StopAsync(); - } - - - [Fact] - public async Task EmptyTest() - { - using var response = await _authorizedHttpClient.GetAsync("/test"); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var body = await response.Content.ReadFromJsonAsync>(); - Assert.NotNull(body); - Assert.Empty(body); - } - - [Fact] - public async Task CrudTest() - { - { - using var response = await _authorizedHttpClient.PostAsJsonAsync("/test", new TestEntity - { - Name = "test", - Age = 22 - }); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var body = await response.Content.ReadFromJsonAsync(); - Assert.NotNull(body); - Assert.Equal("test", body.Name); - Assert.Equal(22, body.Age); - Assert.Null(body.Height); - Assert.NotEmpty(body.Secret); - } - - { - using var response = await _authorizedHttpClient.GetAsync("/test"); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var body = await response.Content.ReadFromJsonAsync>(); - Assert.NotNull(body); - var entity = Assert.Single(body); - Assert.Equal("test", entity.Name); - Assert.Equal(22, entity.Age); - Assert.Null(entity.Height); - Assert.NotEmpty(entity.Secret); - } - - { - using var response = await _authorizedHttpClient.GetAsync("/test/test"); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var body = await response.Content.ReadFromJsonAsync(); - Assert.NotNull(body); - Assert.Equal("test", body.Name); - Assert.Equal(22, body.Age); - Assert.Null(body.Height); - Assert.NotEmpty(body.Secret); - } - - { - using var response = await _authorizedHttpClient.PatchAsJsonAsync("/test/test", new TestEntity - { - Name = "test-2", - Age = 23, - Height = 188.0f - }); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var body = await response.Content.ReadFromJsonAsync(); - Assert.NotNull(body); - Assert.Equal("test-2", body.Name); - Assert.Equal(23, body.Age); - Assert.Equal(188.0f, body.Height); - Assert.NotEmpty(body.Secret); - } - - { - using var response = await _authorizedHttpClient.GetAsync("/test/test-2"); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var body = await response.Content.ReadFromJsonAsync(); - Assert.NotNull(body); - Assert.Equal("test-2", body.Name); - Assert.Equal(23, body.Age); - Assert.Equal(188.0f, body.Height); - Assert.NotEmpty(body.Secret); - } - - { - using var response = await _authorizedHttpClient.DeleteAsync("/test/test-2"); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - { - using var response = await _authorizedHttpClient.GetAsync("/test"); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var body = await response.Content.ReadFromJsonAsync>(); - Assert.NotNull(body); - Assert.Empty(body); - } - } - - [Fact] - public async Task UnauthorizedTest() - { - { - using var response = await _httpClient.GetAsync("/test"); - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - { - using var response = await _httpClient.GetAsync("/test/test"); - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - { - using var response = await _httpClient.PostAsJsonAsync("/test", new TestEntity - { - Name = "test", - Age = 22 - }); - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - { - using var response = await _httpClient.PatchAsJsonAsync("/test/test", new TestEntity - { - Name = "test-2", - Age = 23, - Height = 188.0f - }); - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - { - using var response = await _httpClient.DeleteAsync("/test/test"); - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - } - - [Fact] - public async Task NotFoundTest() - { - { - using var response = await _authorizedHttpClient.GetAsync("/test/test"); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - { - using var response = await _authorizedHttpClient.PatchAsJsonAsync("/test/test", new TestEntity - { - Name = "test-2", - Age = 23, - Height = 188.0f - }); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudServiceTest.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudServiceTest.cs deleted file mode 100644 index ad0d34c..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudServiceTest.cs +++ /dev/null @@ -1,77 +0,0 @@ -using CrupestApi.Commons.Crud.Migrations; -using Microsoft.Extensions.Logging.Abstractions; - -namespace CrupestApi.Commons.Crud.Tests; - -public class CrudServiceTest -{ - private readonly SqliteMemoryConnectionFactory _memoryConnectionFactory = new SqliteMemoryConnectionFactory(); - - private readonly CrudService _crudService; - - public CrudServiceTest() - { - var columnTypeProvider = new ColumnTypeProvider(); - var tableInfoFactory = new TableInfoFactory(columnTypeProvider, NullLoggerFactory.Instance); - var dbConnectionFactory = new SqliteMemoryConnectionFactory(); - - _crudService = new CrudService( - tableInfoFactory, dbConnectionFactory, new SqliteDatabaseMigrator(), NullLoggerFactory.Instance); - } - - [Fact] - public void CrudTest() - { - var key = _crudService.Create(new TestEntity() - { - Name = "crupest", - Age = 18, - }); - - Assert.Equal("crupest", key); - - var entity = _crudService.GetByKey(key); - Assert.Equal("crupest", entity.Name); - Assert.Equal(18, entity.Age); - Assert.Null(entity.Height); - Assert.NotEmpty(entity.Secret); - - var list = _crudService.GetAll(); - entity = Assert.Single(list); - Assert.Equal("crupest", entity.Name); - Assert.Equal(18, entity.Age); - Assert.Null(entity.Height); - Assert.NotEmpty(entity.Secret); - - var count = _crudService.GetCount(); - Assert.Equal(1, count); - - _crudService.UpdateByKey(key, new TestEntity() - { - Name = "crupest2.0", - Age = 22, - Height = 180, - }); - - entity = _crudService.GetByKey("crupest2.0"); - Assert.Equal("crupest2.0", entity.Name); - Assert.Equal(22, entity.Age); - Assert.Equal(180, entity.Height); - Assert.NotEmpty(entity.Secret); - - _crudService.DeleteByKey("crupest2.0"); - - count = _crudService.GetCount(); - Assert.Equal(0, count); - } - - [Fact] - public void EntityNotExistTest() - { - Assert.Throws(() => _crudService.GetByKey("KeyNotExist")); - Assert.Throws(() => _crudService.UpdateByKey("KeyNotExist", new TestEntity - { - Name = "crupest" - })); - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/SqlCompareHelper.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/SqlCompareHelper.cs deleted file mode 100644 index 72b6218..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/SqlCompareHelper.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Text; - -namespace CrupestApi.Commons.Crud.Tests; - -public class SqlCompareHelper -{ - private static List SymbolTokens = new List() { '(', ')', ';' }; - - public static List SqlExtractWords(string? sql, bool toLower = true) - { - var result = new List(); - - if (string.IsNullOrEmpty(sql)) - { - return result; - } - - var current = 0; - - StringBuilder? wordBuilder = null; - - while (current < sql.Length) - { - if (char.IsWhiteSpace(sql[current])) - { - if (wordBuilder is not null) - { - result.Add(wordBuilder.ToString()); - wordBuilder = null; - } - } - else if (SymbolTokens.Contains(sql[current])) - { - if (wordBuilder is not null) - { - result.Add(wordBuilder.ToString()); - wordBuilder = null; - } - result.Add(sql[current].ToString()); - } - else - { - if (wordBuilder is not null) - { - wordBuilder.Append(sql[current]); - } - else - { - wordBuilder = new StringBuilder(); - wordBuilder.Append(sql[current]); - } - } - current++; - } - - if (wordBuilder is not null) - { - result.Add(wordBuilder.ToString()); - } - - if (toLower) - { - for (int i = 0; i < result.Count; i++) - { - result[i] = result[i].ToLower(); - } - } - - return result; - } - - public static bool SqlEqual(string left, string right) - { - return SqlExtractWords(left) == SqlExtractWords(right); - } - - [Fact] - public void TestSqlExtractWords() - { - var sql = "SELECT * FROM TableName WHERE (id = @abcd);"; - var words = SqlExtractWords(sql); - - Assert.Equal(new List { "select", "*", "from", "tablename", "where", "(", "id", "=", "@abcd", ")", ";" }, words); - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TableInfoTest.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TableInfoTest.cs deleted file mode 100644 index b0aa702..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TableInfoTest.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; - -namespace CrupestApi.Commons.Crud.Tests; - -public class TableInfoTest -{ - private static TableInfoFactory TableInfoFactory = new TableInfoFactory(new ColumnTypeProvider(), NullLoggerFactory.Instance); - - private TableInfo _tableInfo; - - public TableInfoTest() - { - _tableInfo = TableInfoFactory.Get(typeof(TestEntity)); - } - - [Fact] - public void TestColumnCount() - { - Assert.Equal(5, _tableInfo.Columns.Count); - Assert.Equal(4, _tableInfo.PropertyColumns.Count); - Assert.Equal(4, _tableInfo.ColumnProperties.Count); - Assert.Equal(1, _tableInfo.NonColumnProperties.Count); - } - - [Fact] - public void GenerateSelectSqlTest() - { - var (sql, parameters) = _tableInfo.GenerateSelectSql(null, WhereClause.Create().Eq("Name", "Hello")); - var parameterName = parameters.First().Name; - - // TODO: Is there a way to auto detect parameters? - SqlCompareHelper.SqlEqual($"SELECT * FROM TestEntity WHERE (Name = @{parameterName})", sql); - Assert.Equal("Hello", parameters.Get(parameterName)); - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TestEntity.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TestEntity.cs deleted file mode 100644 index c15334c..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TestEntity.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace CrupestApi.Commons.Crud.Tests; - -public class TestEntity -{ - [Column(ActAsKey = true, NotNull = true)] - public string Name { get; set; } = default!; - - [Column(NotNull = true)] - public int Age { get; set; } - - [Column] - public float? Height { get; set; } - - [Column(OnlyGenerated = true, NotNull = true, NoUpdate = true)] - public string Secret { get; set; } = default!; - - public static string SecretDefaultValueGenerator() - { - return "secret"; - } - - public string NonColumn { get; set; } = "Not A Column"; -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/CrupestApi.Commons.Tests.csproj b/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/CrupestApi.Commons.Tests.csproj deleted file mode 100644 index 0360ee1..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/CrupestApi.Commons.Tests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - net7.0 - enable - enable - - false - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - \ No newline at end of file diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Usings.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Usings.cs deleted file mode 100644 index 8c927eb..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; \ No newline at end of file diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Config.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Config.cs deleted file mode 100644 index 0ca3547..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Config.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace CrupestApi.Commons; - -public class CrupestApiConfig -{ - public string DataDir { get; set; } = string.Empty; -} - -public static class CrupestApiConfigExtensions -{ - public static IServiceCollection AddCrupestApiConfig(this IServiceCollection services) - { - services.AddOptions().BindConfiguration("CrupestApi"); - services.PostConfigure(config => - { - if (config.DataDir is null || config.DataDir.Length == 0) - { - config.DataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "crupest-api"); - } - }); - - return services; - } -} \ No newline at end of file diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs deleted file mode 100644 index e8d3c2e..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs +++ /dev/null @@ -1,236 +0,0 @@ -using System.Diagnostics; -using System.Reflection; -using System.Text; - -namespace CrupestApi.Commons.Crud; - -public class ColumnInfo -{ - private readonly AggregateColumnMetadata _metadata = new AggregateColumnMetadata(); - private ILogger _logger; - - /// - /// Initialize a column without corresponding property. - /// - public ColumnInfo(TableInfo table, IColumnMetadata metadata, Type clrType, IColumnTypeProvider typeProvider, ILoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - if (metadata is null) - throw new ArgumentException("You must specify metadata for non-property column."); - if (metadata.TryGetValue(ColumnMetadataKeys.ColumnName, out var columnName)) - _logger.LogInformation("Create column without property.", columnName); - else - throw new ArgumentException("You must specify name in metadata for non-property column."); - - Table = table; - _metadata.Add(metadata); - ColumnType = typeProvider.Get(clrType); - } - - /// - /// Initialize a column with corresponding property. - /// - public ColumnInfo(TableInfo table, PropertyInfo propertyInfo, IColumnTypeProvider typeProvider, ILoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - _logger.LogInformation("Create column with property {}.", propertyInfo.Name); - - Table = table; - PropertyInfo = propertyInfo; - ColumnType = typeProvider.Get(propertyInfo.PropertyType); - - var columnAttribute = propertyInfo.GetCustomAttribute(); - if (columnAttribute is not null) - { - _metadata.Add(columnAttribute); - } - } - - public TableInfo Table { get; } - - public Type EntityType => Table.EntityType; - - // If null, there is no corresponding property. - public PropertyInfo? PropertyInfo { get; } = null; - - public IColumnMetadata Metadata => _metadata; - - public IColumnTypeInfo ColumnType { get; } - - public bool IsPrimaryKey => Metadata.GetValueOrDefault(ColumnMetadataKeys.IsPrimaryKey) is true; - public bool IsAutoIncrement => IsPrimaryKey; - public bool IsNotNull => IsPrimaryKey || Metadata.GetValueOrDefault(ColumnMetadataKeys.NotNull) is true; - public bool IsOnlyGenerated => Metadata.GetValueOrDefault(ColumnMetadataKeys.OnlyGenerated) is true; - public bool IsNoUpdate => Metadata.GetValueOrDefault(ColumnMetadataKeys.NoUpdate) is true; - public object? DefaultValue => Metadata.GetValueOrDefault(ColumnMetadataKeys.DefaultValue); - /// - /// This only returns metadata value. It doesn't not fall back to primary column. If you want to get the real key column, go to table info. - /// - /// - /// - public bool IsSpecifiedAsKey => Metadata.GetValueOrDefault(ColumnMetadataKeys.ActAsKey) is true; - public ColumnIndexType Index => Metadata.GetValueOrDefault(ColumnMetadataKeys.Index) ?? ColumnIndexType.None; - - /// - /// Whether the column value can be generated, which means the column has a default value or a default value generator or is AUTOINCREMENT. - /// - public bool CanBeGenerated => DefaultValue is not null || DefaultValueGeneratorMethod is not null || IsAutoIncrement; - - /// - /// The real column name. Maybe set in metadata or just the property name. - /// - /// - public string ColumnName - { - get - { - object? value = Metadata.GetValueOrDefault(ColumnMetadataKeys.ColumnName); - Debug.Assert(value is null || value is string); - return ((string?)value ?? PropertyInfo?.Name) ?? throw new Exception("Failed to get column name."); - } - } - - public MethodInfo? DefaultValueGeneratorMethod - { - get - { - object? value = Metadata.GetValueOrDefault(ColumnMetadataKeys.DefaultValueGenerator); - Debug.Assert(value is null || value is string); - MethodInfo? result; - if (value is null) - { - string methodName = ColumnName + "DefaultValueGenerator"; - result = Table.EntityType.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static); - } - else - { - string methodName = (string)value; - result = Table.EntityType.GetMethod(methodName, BindingFlags.Static) ?? throw new Exception("The default value generator does not exist."); - } - - return result; - } - } - - public MethodInfo? ValidatorMethod - { - get - { - object? value = Metadata.GetValueOrDefault(ColumnMetadataKeys.DefaultValueGenerator); - Debug.Assert(value is null || value is string); - MethodInfo? result; - if (value is null) - { - string methodName = ColumnName + "Validator"; - result = Table.EntityType.GetMethod(methodName, BindingFlags.Static); - } - else - { - string methodName = (string)value; - result = Table.EntityType.GetMethod(methodName, BindingFlags.Static) ?? throw new Exception("The validator does not exist."); - } - - return result; - } - } - - public void InvokeValidator(object? value) - { - var method = this.ValidatorMethod; - if (method is null) - { - _logger.LogInformation("Try to invoke validator for column {} but it does not exist.", ColumnName); - return; - } - var parameters = method.GetParameters(); - if (parameters.Length == 0) - { - throw new Exception("The validator method must have at least one parameter."); - } - else if (parameters.Length == 1) - { - method.Invoke(null, new object?[] { value }); - } - else if (parameters.Length == 2) - { - if (parameters[0].ParameterType == typeof(ColumnInfo)) - method.Invoke(null, new object?[] { this, value }); - else if (parameters[1].ParameterType == typeof(ColumnInfo)) - method.Invoke(null, new object?[] { value, this }); - else - throw new Exception("The validator method must have a parameter of type ColumnInfo if it has 2 parameters."); - } - else - { - throw new Exception("The validator method can only have 1 or 2 parameters."); - } - } - - public object? InvokeDefaultValueGenerator() - { - var method = this.DefaultValueGeneratorMethod; - if (method is null) - { - _logger.LogInformation("Try to invoke default value generator for column {} but it does not exist.", ColumnName); - return null; - } - var parameters = method.GetParameters(); - if (parameters.Length == 0) - { - return method.Invoke(null, new object?[0]); - } - else if (parameters.Length == 1) - { - if (parameters[0].ParameterType != typeof(ColumnInfo)) - throw new Exception("The default value generator method can only have a parameter of type ColumnInfo."); - return method.Invoke(null, new object?[] { this }); - } - else - { - throw new Exception("The default value generator method can only have 0 or 1 parameter."); - } - } - - public object? GenerateDefaultValue() - { - if (DefaultValueGeneratorMethod is not null) - { - return InvokeDefaultValueGenerator(); - } - - if (Metadata.TryGetValue(ColumnMetadataKeys.DefaultValue, out object? value)) - { - return value; - } - else - { - return null; - } - } - - public string GenerateCreateTableColumnString(string? dbProviderId = null) - { - StringBuilder result = new StringBuilder(); - result.Append(ColumnName); - result.Append(' '); - result.Append(ColumnType.GetSqlTypeString(dbProviderId)); - if (IsPrimaryKey) - { - result.Append(' '); - result.Append("PRIMARY KEY"); - } - else if (IsNotNull) - { - result.Append(' '); - result.Append("NOT NULL"); - } - - if (IsAutoIncrement) - { - result.Append(' '); - result.Append("AUTOINCREMENT"); - } - - return result.ToString(); - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs deleted file mode 100644 index 7247ff1..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs +++ /dev/null @@ -1,188 +0,0 @@ -namespace CrupestApi.Commons.Crud; - -public static class ColumnMetadataKeys -{ - public const string ColumnName = nameof(ColumnAttribute.ColumnName); - public const string NotNull = nameof(ColumnAttribute.NotNull); - public const string IsPrimaryKey = nameof(ColumnAttribute.IsPrimaryKey); - public const string Index = nameof(ColumnAttribute.Index); - - /// - /// This will add hooks for string type column to coerce null to ""(empty string) when get or set. No effect on non-string type. - /// - public const string DefaultEmptyForString = nameof(ColumnAttribute.DefaultEmptyForString); - - /// - /// This indicates that you take care of generate this column value when create entity. User calling the api can not specify the value. - /// - public const string OnlyGenerated = nameof(ColumnAttribute.OnlyGenerated); - - /// - /// The default value generator method name in entity type. Default to null, aka, search for ColumnNameDefaultValueGenerator. - /// Generator has signature static void DefaultValueGenerator(ColumnInfo column) - /// - public const string DefaultValueGenerator = nameof(ColumnAttribute.DefaultValueGenerator); - - /// - /// The validator method name in entity type. Default to null, aka, the default validator. - /// Validator has signature static void Validator(ColumnInfo column, object value) - /// Value param is never null. If you want to mean NULL, it should be a . - /// - public const string Validator = nameof(ColumnAttribute.Validator); - - /// - /// The column can only be set when inserted, can't be changed in update. - /// - /// - public const string NoUpdate = nameof(ColumnAttribute.NoUpdate); - - /// - /// This column acts as key when get one entity for http get method in path. - /// - public const string ActAsKey = nameof(ColumnAttribute.ActAsKey); - - /// - /// The default value used for the column. - /// - public const string DefaultValue = nameof(ColumnAttribute.DefaultValue); -} - -public interface IColumnMetadata -{ - bool TryGetValue(string key, out object? value); - - object? GetValueOrDefault(string key) - { - if (TryGetValue(key, out var value)) - { - return value; - } - else - { - return null; - } - } - - T? GetValueOrDefault(string key) - { - return (T?)GetValueOrDefault(key); - } - - object? this[string key] - { - get - { - if (TryGetValue(key, out var value)) - { - return value; - } - else - { - throw new KeyNotFoundException("Key not found."); - } - } - } -} - -public enum ColumnIndexType -{ - None, - Unique, - NonUnique -} - -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] -public class ColumnAttribute : Attribute, IColumnMetadata -{ - // if null, use the property name. - public string? ColumnName { get; init; } - - // default false. - public bool NotNull { get; init; } - - // default false - public bool IsPrimaryKey { get; init; } - - // default None - public ColumnIndexType Index { get; init; } = ColumnIndexType.None; - - /// - public bool DefaultEmptyForString { get; init; } - - /// - public bool OnlyGenerated { get; init; } - - /// - public string? DefaultValueGenerator { get; init; } - - /// - public string? Validator { get; init; } - - /// - public bool NoUpdate { get; init; } - - /// - public bool ActAsKey { get; init; } - - public object? DefaultValue { get; init; } - - public bool TryGetValue(string key, out object? value) - { - var property = GetType().GetProperty(key); - if (property is null) - { - value = null; - return false; - } - value = property.GetValue(this); - return true; - } -} - -public class AggregateColumnMetadata : IColumnMetadata -{ - private IDictionary _own = new Dictionary(); - private IList _children = new List(); - - public void Add(string key, object? value) - { - _own[key] = value; - } - - public void Remove(string key) - { - _own.Remove(key); - } - - public void Add(IColumnMetadata child) - { - _children.Add(child); - } - - public void Remove(IColumnMetadata child) - { - _children.Remove(child); - } - - public bool TryGetValue(string key, out object? value) - { - if (_own.ContainsKey(key)) - { - value = _own[key]; - return true; - } - - bool found = false; - value = null; - foreach (var child in _children) - { - if (child.TryGetValue(key, out var tempValue)) - { - value = tempValue; - found = true; - } - } - - return found; - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs deleted file mode 100644 index 19eff52..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs +++ /dev/null @@ -1,218 +0,0 @@ -using System.Data; -using System.Diagnostics; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace CrupestApi.Commons.Crud; - -public interface IColumnTypeInfo -{ - public static IColumnTypeInfo BoolColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); - public static IColumnTypeInfo IntColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); - public static IColumnTypeInfo ShortColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); - public static IColumnTypeInfo SByteColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); - public static IColumnTypeInfo LongColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); - public static IColumnTypeInfo FloatColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); - public static IColumnTypeInfo DoubleColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); - public static IColumnTypeInfo StringColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); - public static IColumnTypeInfo BytesColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); - public static IColumnTypeInfo DateTimeColumnTypeInfo { get; } = new DateTimeColumnTypeInfo(); - - Type ClrType { get; } - Type DatabaseClrType { get; } - bool IsSimple { get { return ClrType == DatabaseClrType; } } - DbType DbType - { - get - { - if (DatabaseClrType == typeof(bool)) - { - return DbType.Boolean; - } - else if (DatabaseClrType == typeof(int)) - { - return DbType.Int32; - } - else if (DatabaseClrType == typeof(long)) - { - return DbType.Int64; - } - else if (DatabaseClrType == typeof(short)) - { - return DbType.Int16; - } - else if (DatabaseClrType == typeof(sbyte)) - { - return DbType.SByte; - } - else if (DatabaseClrType == typeof(double)) - { - return DbType.Double; - } - else if (DatabaseClrType == typeof(float)) - { - return DbType.Single; - } - else if (DatabaseClrType == typeof(string)) - { - return DbType.String; - } - else if (DatabaseClrType == typeof(byte[])) - { - return DbType.Binary; - } - else - { - throw new Exception("Can't deduce DbType."); - } - } - } - - string GetSqlTypeString(string? dbProviderId = null) - { - // Default implementation for SQLite - return DbType switch - { - DbType.String => "TEXT", - DbType.Boolean or DbType.Int16 or DbType.Int32 or DbType.Int64 => "INTEGER", - DbType.Single or DbType.Double => "REAL", - DbType.Binary => "BLOB", - _ => throw new Exception($"Unsupported DbType: {DbType}"), - }; - } - - JsonConverter? JsonConverter { get { return null; } } - - // You must override this method if ClrType != DatabaseClrType - object? ConvertFromDatabase(object? databaseValue) - { - Debug.Assert(IsSimple); - return databaseValue; - } - - // You must override this method if ClrType != DatabaseClrType - object? ConvertToDatabase(object? value) - { - Debug.Assert(IsSimple); - return value; - } -} - -public interface IColumnTypeProvider -{ - IReadOnlyList GetAll(); - IColumnTypeInfo Get(Type clrType); - - IList GetAllCustom() - { - return GetAll().Where(t => !t.IsSimple).ToList(); - } -} - -public class SimpleColumnTypeInfo : IColumnTypeInfo -{ - public Type ClrType => typeof(T); - public Type DatabaseClrType => typeof(T); -} - -public class DateTimeColumnTypeInfo : IColumnTypeInfo -{ - private JsonConverter _jsonConverter; - - public DateTimeColumnTypeInfo() - { - _jsonConverter = new DateTimeJsonConverter(this); - } - - public Type ClrType => typeof(DateTime); - public Type DatabaseClrType => typeof(string); - - public JsonConverter JsonConverter => _jsonConverter; - - public object? ConvertToDatabase(object? value) - { - if (value is null) return null; - Debug.Assert(value is DateTime); - return ((DateTime)value).ToUniversalTime().ToString("s") + "Z"; - } - - public object? ConvertFromDatabase(object? databaseValue) - { - if (databaseValue is null) return null; - Debug.Assert(databaseValue is string); - var databaseString = (string)databaseValue; - var dateTimeStyles = DateTimeStyles.None; - if (databaseString.Length > 0 && databaseString[^1] == 'Z') - { - databaseString = databaseString.Substring(0, databaseString.Length - 1); - dateTimeStyles = DateTimeStyles.AssumeUniversal & DateTimeStyles.AdjustToUniversal; - } - return DateTime.ParseExact(databaseString, "s", null, dateTimeStyles); - } -} - -public class DateTimeJsonConverter : JsonConverter -{ - private readonly DateTimeColumnTypeInfo _typeInfo; - - public DateTimeJsonConverter(DateTimeColumnTypeInfo typeInfo) - { - _typeInfo = typeInfo; - } - - public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var databaseValue = reader.GetString(); - return (DateTime)_typeInfo.ConvertFromDatabase(databaseValue)!; - } - - public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) - { - var databaseValue = _typeInfo.ConvertToDatabase(value); - writer.WriteStringValue((string)databaseValue!); - } -} - -public class ColumnTypeProvider : IColumnTypeProvider -{ - private Dictionary _typeMap = new Dictionary(); - - public ColumnTypeProvider() - { - _typeMap.Add(IColumnTypeInfo.BoolColumnTypeInfo.ClrType, IColumnTypeInfo.BoolColumnTypeInfo); - _typeMap.Add(IColumnTypeInfo.IntColumnTypeInfo.ClrType, IColumnTypeInfo.IntColumnTypeInfo); - _typeMap.Add(IColumnTypeInfo.ShortColumnTypeInfo.ClrType, IColumnTypeInfo.ShortColumnTypeInfo); - _typeMap.Add(IColumnTypeInfo.SByteColumnTypeInfo.ClrType, IColumnTypeInfo.SByteColumnTypeInfo); - _typeMap.Add(IColumnTypeInfo.LongColumnTypeInfo.ClrType, IColumnTypeInfo.LongColumnTypeInfo); - _typeMap.Add(IColumnTypeInfo.FloatColumnTypeInfo.ClrType, IColumnTypeInfo.FloatColumnTypeInfo); - _typeMap.Add(IColumnTypeInfo.DoubleColumnTypeInfo.ClrType, IColumnTypeInfo.DoubleColumnTypeInfo); - _typeMap.Add(IColumnTypeInfo.StringColumnTypeInfo.ClrType, IColumnTypeInfo.StringColumnTypeInfo); - _typeMap.Add(IColumnTypeInfo.BytesColumnTypeInfo.ClrType, IColumnTypeInfo.BytesColumnTypeInfo); - _typeMap.Add(IColumnTypeInfo.DateTimeColumnTypeInfo.ClrType, IColumnTypeInfo.DateTimeColumnTypeInfo); - } - - public IReadOnlyList GetAll() - { - return _typeMap.Values.ToList(); - } - - // This is thread-safe. - public IColumnTypeInfo Get(Type clrType) - { - if (_typeMap.TryGetValue(clrType, out var typeInfo)) - { - return typeInfo; - } - else - { - if (clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - clrType = clrType.GetGenericArguments()[0]; - return Get(clrType); - } - - throw new Exception($"Unsupported type: {clrType}"); - } - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs deleted file mode 100644 index 1e881d3..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Data; -using CrupestApi.Commons.Crud.Migrations; - -namespace CrupestApi.Commons.Crud; - -[Flags] -public enum UpdateBehavior -{ - None = 0, - SaveNull = 1 -} - -public class CrudService : IDisposable where TEntity : class -{ - protected readonly TableInfo _table; - protected readonly string? _connectionName; - protected readonly IDbConnection _dbConnection; - private readonly bool _shouldDisposeConnection; - private IDatabaseMigrator _migrator; - private readonly ILogger> _logger; - - public CrudService(ITableInfoFactory tableInfoFactory, IDbConnectionFactory dbConnectionFactory, IDatabaseMigrator migrator, ILoggerFactory loggerFactory) - { - _connectionName = GetConnectionName(); - _table = tableInfoFactory.Get(typeof(TEntity)); - _dbConnection = dbConnectionFactory.Get(_connectionName); - _shouldDisposeConnection = dbConnectionFactory.ShouldDisposeConnection; - _migrator = migrator; - _logger = loggerFactory.CreateLogger>(); - } - - protected virtual void EnsureDatabase() - { - if (_migrator.NeedMigrate(_dbConnection, _table)) - { - _logger.LogInformation($"Entity {_table.TableName} needs migration."); - _migrator.AutoMigrate(_dbConnection, _table); - } - } - - protected virtual string GetConnectionName() - { - return typeof(TEntity).Name; - } - - protected virtual void AfterMigrate(IDbConnection dbConnection, TableInfo tableInfo) - { - - } - - public void Dispose() - { - if (_shouldDisposeConnection) - _dbConnection.Dispose(); - } - - public List GetAll() - { - EnsureDatabase(); - var result = _table.Select(_dbConnection, null); - return result; - } - - public int GetCount() - { - EnsureDatabase(); - var result = _table.SelectCount(_dbConnection); - return result; - } - - public TEntity GetByKey(object key) - { - EnsureDatabase(); - var result = _table.Select(_dbConnection, null, WhereClause.Create().Eq(_table.KeyColumn.ColumnName, key)).SingleOrDefault(); - if (result is null) - { - throw new EntityNotExistException($"Required entity for key {key} not found."); - } - return result; - } - - public IInsertClause ConvertEntityToInsertClauses(TEntity entity) - { - var result = new InsertClause(); - foreach (var column in _table.PropertyColumns) - { - var value = column.PropertyInfo!.GetValue(entity); - result.Add(column.ColumnName, value); - } - return result; - } - - public object Create(TEntity entity) - { - EnsureDatabase(); - var insertClause = ConvertEntityToInsertClauses(entity); - _table.Insert(_dbConnection, insertClause, out var key); - return key; - } - - public IUpdateClause ConvertEntityToUpdateClauses(TEntity entity, UpdateBehavior behavior) - { - var result = UpdateClause.Create(); - var saveNull = behavior.HasFlag(UpdateBehavior.SaveNull); - foreach (var column in _table.PropertyColumns) - { - var value = column.PropertyInfo!.GetValue(entity); - if (!saveNull && value is null) continue; - result.Add(column.ColumnName, value); - } - return result; - } - - // Return new key. - public object UpdateByKey(object key, TEntity entity, UpdateBehavior behavior = UpdateBehavior.None) - { - EnsureDatabase(); - var affectedCount = _table.Update(_dbConnection, WhereClause.Create().Eq(_table.KeyColumn.ColumnName, key), - ConvertEntityToUpdateClauses(entity, behavior), out var newKey); - if (affectedCount == 0) - { - throw new EntityNotExistException($"Required entity for key {key} not found."); - } - return newKey ?? key; - } - - public bool DeleteByKey(object key) - { - EnsureDatabase(); - return _table.Delete(_dbConnection, WhereClause.Create().Eq(_table.KeyColumn.ColumnName, key)) == 1; - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudServiceCollectionExtensions.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudServiceCollectionExtensions.cs deleted file mode 100644 index a7e5193..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudServiceCollectionExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using CrupestApi.Commons.Crud.Migrations; -using CrupestApi.Commons.Secrets; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace CrupestApi.Commons.Crud; - -public static class CrudServiceCollectionExtensions -{ - public static IServiceCollection AddCrudCore(this IServiceCollection services) - { - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.AddSecrets(); - return services; - } - - public static IServiceCollection AddCrud(this IServiceCollection services) where TEntity : class where TCrudService : CrudService - { - AddCrudCore(services); - - services.TryAddScoped, TCrudService>(); - services.TryAddScoped>(); - - return services; - } - - public static IServiceCollection AddCrud(this IServiceCollection services) where TEntity : class - { - return services.AddCrud>(); - } - -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs deleted file mode 100644 index 8942979..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs +++ /dev/null @@ -1,101 +0,0 @@ -namespace CrupestApi.Commons.Crud; - -public static class CrudWebApplicationExtensions -{ - public static WebApplication UseCrudCore(this WebApplication app) - { - app.Use(async (context, next) => - { - try - { - await next(); - } - catch (EntityNotExistException) - { - await context.ResponseMessageAsync("Requested entity does not exist.", StatusCodes.Status404NotFound); - } - catch (UserException e) - { - await context.ResponseMessageAsync(e.Message); - } - }); - - return app; - } - - public static WebApplication MapCrud(this WebApplication app, string path, string? permission) where TEntity : class - { - app.MapGet(path, async (context) => - { - if (!context.RequirePermission(permission)) return; - var crudService = context.RequestServices.GetRequiredService>(); - var entityJsonHelper = context.RequestServices.GetRequiredService>(); - var allEntities = crudService.GetAll(); - await context.ResponseJsonAsync(allEntities.Select(e => entityJsonHelper.ConvertEntityToDictionary(e))); - }); - - app.MapGet(path + "/{key}", async (context) => - { - if (!context.RequirePermission(permission)) return; - var crudService = context.RequestServices.GetRequiredService>(); - var entityJsonHelper = context.RequestServices.GetRequiredService>(); - var key = context.Request.RouteValues["key"]?.ToString(); - if (key == null) - { - await context.ResponseMessageAsync("Please specify a key in path."); - return; - } - - var entity = crudService.GetByKey(key); - await context.ResponseJsonAsync(entityJsonHelper.ConvertEntityToDictionary(entity)); - }); - - app.MapPost(path, async (context) => - { - if (!context.RequirePermission(permission)) return; - var crudService = context.RequestServices.GetRequiredService>(); - var entityJsonHelper = context.RequestServices.GetRequiredService>(); - var jsonDocument = await context.Request.ReadJsonAsync(); - var key = crudService.Create(entityJsonHelper.ConvertJsonToEntityForInsert(jsonDocument.RootElement)); - await context.ResponseJsonAsync(entityJsonHelper.ConvertEntityToDictionary(crudService.GetByKey(key))); - }); - - app.MapPatch(path + "/{key}", async (context) => - { - if (!context.RequirePermission(permission)) return; - var key = context.Request.RouteValues["key"]?.ToString(); - var crudService = context.RequestServices.GetRequiredService>(); - var entityJsonHelper = context.RequestServices.GetRequiredService>(); - if (key == null) - { - await context.ResponseMessageAsync("Please specify a key in path."); - return; - } - - var jsonDocument = await context.Request.ReadJsonAsync(); - var entity = entityJsonHelper.ConvertJsonToEntityForUpdate(jsonDocument.RootElement, out var updateBehavior); - var newKey = crudService.UpdateByKey(key, entity, updateBehavior); - await context.ResponseJsonAsync(entityJsonHelper.ConvertEntityToDictionary(crudService.GetByKey(newKey))); - }); - - app.MapDelete(path + "/{key}", async (context) => - { - if (!context.RequirePermission(permission)) return; - var crudService = context.RequestServices.GetRequiredService>(); - var key = context.Request.RouteValues["key"]?.ToString(); - if (key == null) - { - await context.ResponseMessageAsync("Please specify a key in path."); - return; - } - - var deleted = crudService.DeleteByKey(key); - if (deleted) - await context.ResponseMessageAsync("Deleted.", StatusCodes.Status200OK); - else - await context.ResponseMessageAsync("Not exist.", StatusCodes.Status200OK); - }); - - return app; - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs deleted file mode 100644 index 701622c..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Data; -using Microsoft.Data.Sqlite; -using Microsoft.Extensions.Options; - -namespace CrupestApi.Commons.Crud; - -public interface IDbConnectionFactory -{ - IDbConnection Get(string? name = null); - bool ShouldDisposeConnection { get; } -} - -public class SqliteConnectionFactory : IDbConnectionFactory -{ - private readonly IOptionsMonitor _apiConfigMonitor; - - public SqliteConnectionFactory(IOptionsMonitor apiConfigMonitor) - { - _apiConfigMonitor = apiConfigMonitor; - } - - public IDbConnection Get(string? name = null) - { - var connectionString = new SqliteConnectionStringBuilder() - { - DataSource = Path.Combine(_apiConfigMonitor.CurrentValue.DataDir, $"{name ?? "crupest-api"}.db"), - Mode = SqliteOpenMode.ReadWriteCreate - }.ToString(); - - var connection = new SqliteConnection(connectionString); - connection.Open(); - return connection; - } - - public bool ShouldDisposeConnection => true; -} - -public class SqliteMemoryConnectionFactory : IDbConnectionFactory, IDisposable -{ - private readonly Dictionary _connections = new(); - - public IDbConnection Get(string? name = null) - { - name = name ?? "crupest-api"; - - if (_connections.TryGetValue(name, out var connection)) - { - return connection; - } - else - { - var connectionString = new SqliteConnectionStringBuilder() - { - DataSource = ":memory:", - Mode = SqliteOpenMode.ReadWriteCreate - }.ToString(); - - connection = new SqliteConnection(connectionString); - _connections.Add(name, connection); - connection.Open(); - return connection; - } - } - - public bool ShouldDisposeConnection => false; - - - public void Dispose() - { - foreach (var connection in _connections.Values) - { - connection.Dispose(); - } - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbNullValue.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbNullValue.cs deleted file mode 100644 index 5dc5a61..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbNullValue.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace CrupestApi.Commons.Crud; - -/// -/// This will always represent null value in database. -/// -public class DbNullValue -{ - public static DbNullValue Instance { get; } = new DbNullValue(); -} \ No newline at end of file diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs deleted file mode 100644 index cf3f178..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using Microsoft.Extensions.Options; - -namespace CrupestApi.Commons.Crud; - -/// -/// Contains all you need to do with json. -/// -public class EntityJsonHelper where TEntity : class -{ - private readonly TableInfo _table; - private readonly IOptionsMonitor _jsonSerializerOptions; - - public EntityJsonHelper(ITableInfoFactory tableInfoFactory, IOptionsMonitor jsonSerializerOptions) - { - _table = tableInfoFactory.Get(typeof(TEntity)); - _jsonSerializerOptions = jsonSerializerOptions; - } - - public Dictionary ConvertEntityToDictionary(TEntity entity, bool includeNonColumnProperties = false) - { - var result = new Dictionary(); - - foreach (var column in _table.PropertyColumns) - { - var value = column.PropertyInfo!.GetValue(entity); - var realValue = column.ColumnType.ConvertToDatabase(value); - result[column.ColumnName] = realValue; - } - - if (includeNonColumnProperties) - { - foreach (var propertyInfo in _table.NonColumnProperties) - { - var value = propertyInfo.GetValue(entity); - result[propertyInfo.Name] = value; - } - } - - return result; - } - - public string ConvertEntityToJson(TEntity entity, bool includeNonColumnProperties = false) - { - var dictionary = ConvertEntityToDictionary(entity, includeNonColumnProperties); - return JsonSerializer.Serialize(dictionary, _jsonSerializerOptions.CurrentValue); - } - - private object? ConvertJsonValue(JsonElement? optionalJsonElement, Type type, string propertyName) - { - if (optionalJsonElement is null) - { - return null; - } - - var jsonElement = optionalJsonElement.Value; - - if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) - { - return null; - } - - if (jsonElement.ValueKind is JsonValueKind.String) - { - if (type != typeof(string)) - { - throw new UserException($"Property {propertyName} must be a string."); - } - return jsonElement.GetString()!; - } - - if (jsonElement.ValueKind is JsonValueKind.True or JsonValueKind.False) - { - if (type != typeof(bool)) - { - throw new UserException($"Property {propertyName} must be a boolean."); - } - return jsonElement.GetBoolean(); - } - - if (jsonElement.ValueKind is JsonValueKind.Number) - { - try - { - return Convert.ChangeType(jsonElement.GetRawText(), type, CultureInfo.InvariantCulture); - } - catch (Exception) - { - throw new UserException($"Property {propertyName} must be a valid number."); - } - } - - throw new UserException($"Property {propertyName} is of wrong type."); - } - - public Dictionary ConvertJsonObjectToDictionary(JsonElement jsonElement) - { - var result = new Dictionary(); - - foreach (var property in jsonElement.EnumerateObject()) - { - result[property.Name.ToLower()] = property.Value; - } - - return result; - } - - public TEntity ConvertJsonToEntityForInsert(JsonElement jsonElement) - { - if (jsonElement.ValueKind is not JsonValueKind.Object) - throw new ArgumentException("The jsonElement must be an object."); - - var result = Activator.CreateInstance(); - - Dictionary jsonProperties = ConvertJsonObjectToDictionary(jsonElement); - - foreach (var column in _table.PropertyColumns) - { - var jsonPropertyValue = jsonProperties.GetValueOrDefault(column.ColumnName.ToLower()); - var value = ConvertJsonValue(jsonPropertyValue, column.ColumnType.DatabaseClrType, column.ColumnName); - if (column.IsOnlyGenerated && value is not null) - { - throw new UserException($"Property {column.ColumnName} is auto generated, you cannot set it."); - } - if (!column.CanBeGenerated && value is null && column.IsNotNull) - { - throw new UserException($"Property {column.ColumnName} can NOT be generated, you must set it."); - } - var realValue = column.ColumnType.ConvertFromDatabase(value); - column.PropertyInfo!.SetValue(result, realValue); - } - - return result; - } - - public TEntity ConvertJsonToEntityForInsert(string json) - { - var jsonElement = JsonSerializer.Deserialize(json, _jsonSerializerOptions.CurrentValue); - return ConvertJsonToEntityForInsert(jsonElement!); - } - - public TEntity ConvertJsonToEntityForUpdate(JsonElement jsonElement, out UpdateBehavior updateBehavior) - { - if (jsonElement.ValueKind is not JsonValueKind.Object) - throw new UserException("The jsonElement must be an object."); - - updateBehavior = UpdateBehavior.None; - - Dictionary jsonProperties = ConvertJsonObjectToDictionary(jsonElement); - - bool saveNull = false; - if (jsonProperties.TryGetValue("$saveNull".ToLower(), out var saveNullValue)) - { - if (saveNullValue.ValueKind is JsonValueKind.True) - { - updateBehavior |= UpdateBehavior.SaveNull; - saveNull = true; - } - else if (saveNullValue.ValueKind is JsonValueKind.False) - { - - } - else - { - throw new UserException("The $saveNull must be a boolean."); - } - } - - var result = Activator.CreateInstance(); - foreach (var column in _table.PropertyColumns) - { - if (jsonProperties.TryGetValue(column.ColumnName.ToLower(), out var jsonPropertyValue)) - { - if (jsonPropertyValue.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) - { - if ((column.IsOnlyGenerated || column.IsNoUpdate) && saveNull) - { - throw new UserException($"Property {column.ColumnName} is auto generated or not updatable, you cannot set it."); - } - - column.PropertyInfo!.SetValue(result, null); - } - else - { - if (column.IsOnlyGenerated || column.IsNoUpdate) - { - throw new UserException($"Property {column.ColumnName} is auto generated or not updatable, you cannot set it."); - } - - var value = ConvertJsonValue(jsonPropertyValue, column.ColumnType.DatabaseClrType, column.ColumnName); - var realValue = column.ColumnType.ConvertFromDatabase(value); - column.PropertyInfo!.SetValue(result, realValue); - } - } - } - - return result; - } - - public TEntity ConvertJsonToEntityForUpdate(string json, out UpdateBehavior updateBehavior) - { - var jsonElement = JsonSerializer.Deserialize(json, _jsonSerializerOptions.CurrentValue); - return ConvertJsonToEntityForUpdate(jsonElement!, out updateBehavior); - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/IClause.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/IClause.cs deleted file mode 100644 index 964a669..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/IClause.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Dapper; - -namespace CrupestApi.Commons.Crud; - -public interface IClause -{ - IEnumerable GetSubclauses() - { - return Enumerable.Empty(); - } - - IEnumerable GetRelatedColumns() - { - var subclauses = GetSubclauses(); - var result = new List(); - foreach (var subclause in subclauses) - { - var columns = subclause.GetRelatedColumns(); - if (columns is not null) - result.AddRange(columns); - } - return result; - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/InsertClause.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/InsertClause.cs deleted file mode 100644 index a880e66..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/InsertClause.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Text; - -namespace CrupestApi.Commons.Crud; - -public class InsertItem -{ - /// - /// Null means use default value. Use . - /// - public InsertItem(string columnName, object? value) - { - ColumnName = columnName; - Value = value; - } - - public string ColumnName { get; set; } - public object? Value { get; set; } -} - -public interface IInsertClause : IClause -{ - List Items { get; } - string GenerateColumnListSql(string? dbProviderId = null); - (string sql, ParamList parameters) GenerateValueListSql(string? dbProviderId = null); -} - -public class InsertClause : IInsertClause -{ - public List Items { get; } = new List(); - - public InsertClause(params InsertItem[] items) - { - Items.AddRange(items); - } - - public InsertClause Add(params InsertItem[] items) - { - Items.AddRange(items); - return this; - } - - public InsertClause Add(string column, object? value) - { - return Add(new InsertItem(column, value)); - } - - public static InsertClause Create(params InsertItem[] items) - { - return new InsertClause(items); - } - - public List GetRelatedColumns() - { - return Items.Select(i => i.ColumnName).ToList(); - } - - public string GenerateColumnListSql(string? dbProviderId = null) - { - return string.Join(", ", Items.Select(i => i.ColumnName)); - } - - public (string sql, ParamList parameters) GenerateValueListSql(string? dbProviderId = null) - { - var parameters = new ParamList(); - var sb = new StringBuilder(); - for (var i = 0; i < Items.Count; i++) - { - var item = Items[i]; - var parameterName = parameters.AddRandomNameParameter(item.Value, item.ColumnName); - sb.Append($"@{parameterName}"); - if (i != Items.Count - 1) - sb.Append(", "); - } - - return (sb.ToString(), parameters); - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/DatabaseMigrator.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/DatabaseMigrator.cs deleted file mode 100644 index f1ae616..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/DatabaseMigrator.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Data; - -namespace CrupestApi.Commons.Crud.Migrations; - -public class TableColumn -{ - public TableColumn(string name, string type, bool notNull, int primaryKey) - { - Name = name; - Type = type; - NotNull = notNull; - PrimaryKey = primaryKey; - } - - public string Name { get; set; } - public string Type { get; set; } - public bool NotNull { get; set; } - - /// - /// 0 if not primary key. 1-based index if in primary key. - /// - public int PrimaryKey { get; set; } -} - -public class Table -{ - public Table(string name) - { - Name = name; - } - - public string Name { get; set; } - public List Columns { get; set; } = new List(); -} - -public interface IDatabaseMigrator -{ - Table? GetTable(IDbConnection dbConnection, string tableName); - Table ConvertTableInfoToTable(TableInfo tableInfo); - string GenerateCreateTableColumnSqlSegment(TableColumn column); - string GenerateCreateTableSql(string tableName, IEnumerable columns); - bool NeedMigrate(IDbConnection dbConnection, TableInfo tableInfo); - void AutoMigrate(IDbConnection dbConnection, TableInfo tableInfo); -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/SqliteDatabaseMigrator.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/SqliteDatabaseMigrator.cs deleted file mode 100644 index 33310d6..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/SqliteDatabaseMigrator.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System.Data; -using System.Text; -using System.Text.Json; -using System.Text.RegularExpressions; -using Dapper; - -namespace CrupestApi.Commons.Crud.Migrations; - -public class SqliteDatabaseMigrator : IDatabaseMigrator -{ - private void CheckTableName(string name) - { - if (Regex.Match(name, @"^[_0-9a-zA-Z]+$").Success is false) - { - throw new ArgumentException("Fxxk, what have you passed as table name."); - } - } - - public Table? GetTable(IDbConnection dbConnection, string tableName) - { - var count = dbConnection.QuerySingle( - "SELECT count(*) FROM sqlite_schema WHERE type = 'table' AND name = @TableName;", - new { TableName = tableName }); - if (count == 0) - { - return null; - } - else if (count > 1) - { - throw new Exception($"More than 1 table has name {tableName}. What happened?"); - } - else - { - var table = new Table(tableName); - var queryColumns = dbConnection.Query($"PRAGMA table_info({tableName})"); - - foreach (var column in queryColumns) - { - var columnName = (string)column.name; - var columnType = (string)column.type; - var isNullable = Convert.ToBoolean(column.notnull); - var primaryKey = Convert.ToInt32(column.pk); - - table.Columns.Add(new TableColumn(columnName, columnType, isNullable, primaryKey)); - } - - return table; - } - } - - public Table ConvertTableInfoToTable(TableInfo tableInfo) - { - var table = new Table(tableInfo.TableName); - - foreach (var columnInfo in tableInfo.Columns) - { - table.Columns.Add(new TableColumn(columnInfo.ColumnName, columnInfo.ColumnType.GetSqlTypeString(), - columnInfo.IsNotNull, columnInfo.IsPrimaryKey ? 1 : 0)); - } - - return table; - } - - public string GenerateCreateTableColumnSqlSegment(TableColumn column) - { - StringBuilder result = new StringBuilder(); - result.Append(column.Name); - result.Append(' '); - result.Append(column.Type); - if (column.PrimaryKey is not 0) - { - result.Append(" PRIMARY KEY AUTOINCREMENT"); - } - else if (column.NotNull) - { - result.Append(" NOT NULL"); - } - - return result.ToString(); - } - - public string GenerateCreateTableSql(string tableName, IEnumerable columns) - { - CheckTableName(tableName); - - var sql = $@" -CREATE TABLE {tableName} ( - {string.Join(",\n ", columns.Select(GenerateCreateTableColumnSqlSegment))} -); - ".Trim(); - - return sql; - - } - - public void AutoMigrate(IDbConnection dbConnection, TableInfo tableInfo) - { - var tableName = tableInfo.TableName; - var databaseTable = GetTable(dbConnection, tableName); - var wantedTable = ConvertTableInfoToTable(tableInfo); - var databaseTableColumnNames = databaseTable is null ? new List() : databaseTable.Columns.Select(column => column.Name).ToList(); - var wantedTableColumnNames = wantedTable.Columns.Select(column => column.Name).ToList(); - - var notChangeColumns = wantedTableColumnNames.Where(column => databaseTableColumnNames.Contains(column)).ToList(); - var addColumns = wantedTableColumnNames.Where(column => !databaseTableColumnNames.Contains(column)).ToList(); - - if (databaseTable is not null && dbConnection.QuerySingle($"SELECT count(*) FROM {tableName}") > 0) - { - foreach (var columnName in addColumns) - { - var columnInfo = tableInfo.GetColumn(columnName); - if (!columnInfo.CanBeGenerated) - { - throw new Exception($"Column {columnName} cannot be generated. So we can't auto-migrate."); - } - } - } - - // We are sqlite, so it's a little bit difficult. - using var transaction = dbConnection.BeginTransaction(); - - if (databaseTable is not null) - { - var tempTableName = tableInfo.TableName + "_temp"; - dbConnection.Execute($"ALTER TABLE {tableName} RENAME TO {tempTableName}", new { TableName = tableName, tempTableName }); - - var createTableSql = GenerateCreateTableSql(tableName, wantedTable.Columns); - dbConnection.Execute(createTableSql); - - // Copy old data to new table. - var originalRows = dbConnection.Query($"SELECT * FROM {tempTableName}").Cast>().ToList(); - foreach (var originalRow in originalRows) - { - var parameters = new DynamicParameters(); - - foreach (var columnName in notChangeColumns) - { - parameters.Add(columnName, originalRow[columnName]); - } - - foreach (var columnName in addColumns) - { - parameters.Add(columnName, tableInfo.GetColumn(columnName).GenerateDefaultValue()); - } - - string columnSql = string.Join(", ", wantedTableColumnNames); - string valuesSql = string.Join(", ", wantedTableColumnNames.Select(c => "@" + c)); - - string sql = $"INSERT INTO {tableName} ({columnSql}) VALUES {valuesSql})"; - dbConnection.Execute(sql, parameters); - } - - // Finally drop old table - dbConnection.Execute($"DROP TABLE {tempTableName}"); - } - else - { - var createTableSql = GenerateCreateTableSql(tableName, wantedTable.Columns); - dbConnection.Execute(createTableSql); - } - - // Commit transaction. - transaction.Commit(); - } - - public bool NeedMigrate(IDbConnection dbConnection, TableInfo tableInfo) - { - var tableName = tableInfo.TableName; - var databaseTable = GetTable(dbConnection, tableName); - var wantedTable = ConvertTableInfoToTable(tableInfo); - var databaseTableColumns = databaseTable is null ? new HashSet() : new HashSet(databaseTable.Columns.Select(c => c.Name)); - var wantedTableColumns = new HashSet(wantedTable.Columns.Select(c => c.Name)); - return !databaseTableColumns.SetEquals(wantedTableColumns); - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/OrderByClause.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/OrderByClause.cs deleted file mode 100644 index 734d044..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/OrderByClause.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace CrupestApi.Commons.Crud; - -public class OrderByItem -{ - public OrderByItem(string columnName, bool isAscending) - { - ColumnName = columnName; - IsAscending = isAscending; - } - - public string ColumnName { get; } - public bool IsAscending { get; } - - public string GenerateSql() - { - return $"{ColumnName} {(IsAscending ? "ASC" : "DESC")}"; - } -} - -public interface IOrderByClause : IClause -{ - List Items { get; } - // Contains "ORDER BY" keyword! - string GenerateSql(string? dbProviderId = null); -} - -public class OrderByClause : IOrderByClause -{ - public List Items { get; } = new List(); - - public OrderByClause(params OrderByItem[] items) - { - Items.AddRange(items); - } - - public static OrderByClause Create(params OrderByItem[] items) - { - return new OrderByClause(items); - } - - public List GetRelatedColumns() - { - return Items.Select(x => x.ColumnName).ToList(); - } - - public string GenerateSql(string? dbProviderId = null) - { - return "ORDER BY " + string.Join(", ", Items.Select(i => i.GenerateSql())); - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs deleted file mode 100644 index 37d77ca..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Data; -using System.Diagnostics; - -namespace CrupestApi.Commons.Crud; - -/// -/// is an optional column name related to the param. You may use it to do some column related things. Like use a more accurate conversion. -/// -/// -/// If value is DbNullValue, it will be treated as null. -/// -public record ParamInfo(string Name, object? Value, string? ColumnName = null); - -public class ParamList : List -{ - private static Random random = new Random(); - private const string chars = "abcdefghijklmnopqrstuvwxyz"; - public static string GenerateRandomKey(int length) - { - lock (random) - { - var result = new string(Enumerable.Repeat(chars, length) - .Select(s => s[random.Next(s.Length)]).ToArray()); - return result; - } - } - - public string GenerateRandomParameterName() - { - var parameterName = GenerateRandomKey(10); - int retryTimes = 1; - while (ContainsKey(parameterName)) - { - retryTimes++; - Debug.Assert(retryTimes <= 100); - parameterName = GenerateRandomKey(10); - } - return parameterName; - } - - - public bool ContainsKey(string name) - { - return this.SingleOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) is not null; - } - - public T? Get(string key) - { - return (T?)this.SingleOrDefault(p => p.Name.Equals(key, StringComparison.OrdinalIgnoreCase))?.Value; - } - - public object? this[string key] - { - get - { - return this.SingleOrDefault(p => p.Name.Equals(key, StringComparison.OrdinalIgnoreCase)) ?? throw new KeyNotFoundException("Key not found."); - } - } - - public void Add(string name, object? value, string? columnName = null) - { - Add(new ParamInfo(name, value, columnName)); - } - - // Return the random name. - public string AddRandomNameParameter(object? value, string? columnName = null) - { - var parameterName = GenerateRandomParameterName(); - var param = new ParamInfo(parameterName, value, columnName); - Add(param); - return parameterName; - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/README.md b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/README.md deleted file mode 100644 index b008ea7..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# CRUD Technic Notes - -## Overview - -The ultimate CRUD scaffold finally comes. - -## Database Pipeline - -### Select - -1. Create select `what`, where clause, order clause, `Offset` and `Limit`. -2. Check clauses' related columns are valid. -3. Generate sql string and param list. -4. Convert param list to `Dapper` dynamic params with proper type conversion in `IColumnTypeInfo`. -5. Execute sql and get `dynamic`s. -6. (Optional) Convert `dynamic`s to `TEntity`s. - -### Insert - -1. Create insert clause. -2. Check clauses' related columns are valid. -3. Create a real empty insert clause. -4. For each column: - 1. If insert item exists and value is not null but the column `IsGenerated` is true, throw exception. - 2. If insert item does not exist or value is `null`, use default value generator to generate value. However, `DbNullValue` always means use `NULL` for that column. - 3. If value is `null` and the column `IsAutoIncrement` is true, skip to next column. - 4. Coerce null to `DbNullValue`. - 5. Run validator to validate the value. - 6. If value is `DbNullValue`, `IsNotNull` is true, throw exception. - 7. Add column and value to real insert clause. -5. Generate sql string and param list. -6. Convert param list to `Dapper` dynamic params with proper type conversion in `IColumnTypeInfo`. -7. Execute sql and return `KeyColumn` value. - -### Update - -1. Create update clause, where clause. -2. Check clauses' related columns are valid. Then generate sql string and param list. -3. Create a real empty update clause. -4. For each column: - 1. If update item exists and value is not null but the column `IsNoUpdate` is true, throw exception. - 2. Invoke validator to validate the value. - 3. If `IsNotNull` is true and value is `DbNullValue`, throw exception. - 4. Add column and value to real update clause. -5. Generate sql string and param list. -6. Convert param list to `Dapper` dynamic params with proper type conversion in `IColumnTypeInfo`. -7. Execute sql and return count of affected rows. diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs deleted file mode 100644 index 4a7ea95..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs +++ /dev/null @@ -1,628 +0,0 @@ -using System.Data; -using System.Diagnostics; -using System.Reflection; -using System.Text; -using Dapper; - -namespace CrupestApi.Commons.Crud; - -/// -/// Contains all you need to manipulate a table. -/// -public class TableInfo -{ - private readonly IColumnTypeProvider _columnTypeProvider; - private readonly Lazy> _lazyColumnNameList; - private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; - - public TableInfo(Type entityType, IColumnTypeProvider columnTypeProvider, ILoggerFactory loggerFactory) - : this(entityType.Name, entityType, columnTypeProvider, loggerFactory) - { - } - - public TableInfo(string tableName, Type entityType, IColumnTypeProvider columnTypeProvider, ILoggerFactory loggerFactory) - { - _loggerFactory = loggerFactory; - _logger = loggerFactory.CreateLogger(); - - _logger.LogInformation("Create TableInfo for entity type '{}'.", entityType.Name); - - _columnTypeProvider = columnTypeProvider; - - TableName = tableName; - EntityType = entityType; - - - var properties = entityType.GetProperties(); - _logger.LogInformation("Find following properties: {}", string.Join(", ", properties.Select(p => p.Name))); - - var columnInfos = new List(); - - bool hasId = false; - ColumnInfo? primaryKeyColumn = null; - ColumnInfo? keyColumn = null; - - List nonColumnProperties = new(); - - foreach (var property in properties) - { - _logger.LogInformation("Check property '{}'.", property.Name); - if (CheckPropertyIsColumn(property)) - { - _logger.LogInformation("{} is a column, create ColumnInfo for it.", property.Name); - var columnInfo = new ColumnInfo(this, property, _columnTypeProvider, _loggerFactory); - columnInfos.Add(columnInfo); - if (columnInfo.IsPrimaryKey) - { - _logger.LogInformation("Column {} is a primary key.", property.Name); - primaryKeyColumn = columnInfo; - } - if (columnInfo.ColumnName.Equals("id", StringComparison.OrdinalIgnoreCase)) - { - _logger.LogInformation("Column {} has name id.", property.Name); - hasId = true; - } - if (columnInfo.IsSpecifiedAsKey) - { - if (keyColumn is not null) - { - throw new Exception("Already exists a key column."); - } - _logger.LogInformation("Column {} is specified as key.", property.Name); - keyColumn = columnInfo; - } - } - else - { - _logger.LogInformation("{} is not a column.", property.Name); - nonColumnProperties.Add(property); - } - } - - if (primaryKeyColumn is null) - { - if (hasId) throw new Exception("A column named id already exists but is not primary key."); - _logger.LogInformation("No primary key column found, create one automatically."); - primaryKeyColumn = CreateAutoIdColumn(); - columnInfos.Add(primaryKeyColumn); - } - - if (keyColumn is null) - { - _logger.LogInformation("No key column is specified, will use primary key."); - keyColumn = primaryKeyColumn; - } - - Columns = columnInfos; - PrimaryKeyColumn = primaryKeyColumn; - KeyColumn = keyColumn; - NonColumnProperties = nonColumnProperties; - - _logger.LogInformation("Check table validity."); - CheckValidity(); - - _logger.LogInformation("TableInfo succeeded to create."); - - _lazyColumnNameList = new Lazy>(() => Columns.Select(c => c.ColumnName).ToList()); - } - - private ColumnInfo CreateAutoIdColumn() - { - return new ColumnInfo(this, - new ColumnAttribute - { - ColumnName = "Id", - NotNull = true, - IsPrimaryKey = true, - }, - typeof(long), _columnTypeProvider, _loggerFactory); - } - - public Type EntityType { get; } - public string TableName { get; } - public IReadOnlyList Columns { get; } - public IReadOnlyList PropertyColumns => Columns.Where(c => c.PropertyInfo is not null).ToList(); - public ColumnInfo PrimaryKeyColumn { get; } - /// - /// Maybe not the primary key. But acts as primary key. - /// - /// - public ColumnInfo KeyColumn { get; } - public IReadOnlyList ColumnProperties => PropertyColumns.Select(c => c.PropertyInfo!).ToList(); - public IReadOnlyList NonColumnProperties { get; } - public IReadOnlyList ColumnNameList => _lazyColumnNameList.Value; - - protected bool CheckPropertyIsColumn(PropertyInfo property) - { - var columnAttribute = property.GetCustomAttribute(); - if (columnAttribute is null) return false; - return true; - } - - public ColumnInfo GetColumn(string columnName) - { - foreach (var column in Columns) - { - if (column.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase)) - { - return column; - } - } - throw new KeyNotFoundException("No such column with given name."); - } - - public void CheckGeneratedColumnHasGenerator() - { - foreach (var column in Columns) - { - if (column.IsOnlyGenerated && column.DefaultValueGeneratorMethod is null) - { - throw new Exception($"Column '{column.ColumnName}' is generated but has no generator."); - } - } - } - - public void CheckValidity() - { - // Check if there is only one primary key. - bool hasPrimaryKey = false; - bool hasKey = false; - foreach (var column in Columns) - { - if (column.IsPrimaryKey) - { - if (hasPrimaryKey) throw new Exception("More than one columns are primary key."); - hasPrimaryKey = true; - } - - if (column.IsSpecifiedAsKey) - { - if (hasKey) throw new Exception("More than one columns are specified as key column."); - } - } - - if (!hasPrimaryKey) throw new Exception("No column is primary key."); - - // Check two columns have the same sql name. - HashSet sqlNameSet = new HashSet(); - - foreach (var column in Columns) - { - if (sqlNameSet.Contains(column.ColumnName)) - throw new Exception($"Two columns have the same sql name '{column.ColumnName}'."); - sqlNameSet.Add(column.ColumnName); - } - - CheckGeneratedColumnHasGenerator(); - } - - public string GenerateCreateIndexSql(string? dbProviderId = null) - { - var sb = new StringBuilder(); - - foreach (var column in Columns) - { - if (column.Index == ColumnIndexType.None) continue; - - sb.Append($"CREATE {(column.Index == ColumnIndexType.Unique ? "UNIQUE" : "")} INDEX {TableName}_{column.ColumnName}_index ON {TableName} ({column.ColumnName});\n"); - } - - return sb.ToString(); - } - - public string GenerateCreateTableSql(bool createIndex = true, string? dbProviderId = null) - { - var tableName = TableName; - var columnSql = string.Join(",\n", Columns.Select(c => c.GenerateCreateTableColumnString(dbProviderId))); - - var sql = $@" -CREATE TABLE {tableName}( - {columnSql} -); - "; - - if (createIndex) - { - sql += GenerateCreateIndexSql(dbProviderId); - } - - return sql; - } - - public void CheckColumnName(string columnName) - { - if (!ColumnNameList.Contains(columnName)) - { - throw new ArgumentException($"Column {columnName} is not in the table."); - } - } - - public void CheckRelatedColumns(IClause? clause) - { - if (clause is not null) - { - var relatedColumns = clause.GetRelatedColumns(); - foreach (var column in relatedColumns) - { - CheckColumnName(column); - } - } - } - - /// - /// If you call this manually, it's your duty to call hooks. - /// - /// - public (string sql, ParamList parameters) GenerateSelectSql(string? selectWhat, IWhereClause? whereClause, IOrderByClause? orderByClause = null, int? skip = null, int? limit = null, string? dbProviderId = null) - { - CheckRelatedColumns(whereClause); - CheckRelatedColumns(orderByClause); - - var parameters = new ParamList(); - - StringBuilder result = new StringBuilder() - .Append($"SELECT {selectWhat ?? "*"} FROM ") - .Append(TableName); - - if (whereClause is not null) - { - result.Append(" WHERE "); - var (whereSql, whereParameters) = whereClause.GenerateSql(dbProviderId); - parameters.AddRange(whereParameters); - result.Append(whereSql); - } - - if (orderByClause is not null) - { - result.Append(' '); - var orderBySql = orderByClause.GenerateSql(dbProviderId); - result.Append(orderBySql); - } - - if (limit is not null) - { - result.Append(" LIMIT @Limit"); - parameters.Add("Limit", limit.Value); - } - - if (skip is not null) - { - result.Append(" OFFSET @Skip"); - parameters.Add("Skip", skip.Value); - } - - result.Append(';'); - - return (result.ToString(), parameters); - } - - /// - /// If you call this manually, it's your duty to call hooks. - /// - /// - public (string sql, ParamList parameters) GenerateInsertSql(IInsertClause insertClause, string? dbProviderId = null) - { - CheckRelatedColumns(insertClause); - - var parameters = new ParamList(); - - var result = new StringBuilder() - .Append("INSERT INTO ") - .Append(TableName) - .Append(" (") - .Append(insertClause.GenerateColumnListSql(dbProviderId)) - .Append(") VALUES ("); - - var (valueSql, valueParameters) = insertClause.GenerateValueListSql(dbProviderId); - result.Append(valueSql).Append(");"); - - parameters.AddRange(valueParameters); - - return (result.ToString(), parameters); - } - - /// - /// If you call this manually, it's your duty to call hooks. - /// - /// - public (string sql, ParamList parameters) GenerateUpdateSql(IWhereClause? whereClause, IUpdateClause updateClause) - { - CheckRelatedColumns(whereClause); - CheckRelatedColumns(updateClause); - - var parameters = new ParamList(); - - StringBuilder sb = new StringBuilder("UPDATE "); - sb.Append(TableName); - sb.Append(" SET "); - var (updateSql, updateParameters) = updateClause.GenerateSql(); - sb.Append(updateSql); - parameters.AddRange(updateParameters); - if (whereClause is not null) - { - sb.Append(" WHERE "); - var (whereSql, whereParameters) = whereClause.GenerateSql(); - sb.Append(whereSql); - parameters.AddRange(whereParameters); - } - sb.Append(';'); - - return (sb.ToString(), parameters); - } - - /// - /// If you call this manually, it's your duty to call hooks. - /// - /// - public (string sql, ParamList parameters) GenerateDeleteSql(IWhereClause? whereClause) - { - CheckRelatedColumns(whereClause); - - var parameters = new ParamList(); - - StringBuilder sb = new StringBuilder("DELETE FROM "); - sb.Append(TableName); - if (whereClause is not null) - { - sb.Append(" WHERE "); - var (whereSql, whereParameters) = whereClause.GenerateSql(); - parameters.AddRange(whereParameters); - sb.Append(whereSql); - } - sb.Append(';'); - - return (sb.ToString(), parameters); - } - - private DynamicParameters ConvertParameters(ParamList parameters) - { - var result = new DynamicParameters(); - foreach (var param in parameters) - { - if (param.Value is null || param.Value is DbNullValue) - { - result.Add(param.Name, null); - continue; - } - - var columnName = param.ColumnName; - IColumnTypeInfo typeInfo; - if (columnName is not null) - { - typeInfo = GetColumn(columnName).ColumnType; - } - else - { - typeInfo = _columnTypeProvider.Get(param.Value.GetType()); - } - - result.Add(param.Name, typeInfo.ConvertToDatabase(param.Value), typeInfo.DbType); - } - return result; - } - - /// - /// ConvertParameters. Select. Call hooks. - /// - public virtual List SelectDynamic(IDbConnection dbConnection, string? what = null, IWhereClause? where = null, IOrderByClause? orderBy = null, int? skip = null, int? limit = null) - { - var (sql, parameters) = GenerateSelectSql(what, where, orderBy, skip, limit); - var queryResult = dbConnection.Query(sql, ConvertParameters(parameters)); - return queryResult.ToList(); - } - - public virtual int SelectCount(IDbConnection dbConnection, IWhereClause? where = null, IOrderByClause? orderBy = null, int? skip = null, int? limit = null) - { - var (sql, parameters) = GenerateSelectSql("COUNT(*)", where, orderBy, skip, limit); - var result = dbConnection.QuerySingle(sql, ConvertParameters(parameters)); - return result; - } - - public virtual TResult MapDynamicTo(dynamic d) - { - var dict = (IDictionary)d; - - var result = Activator.CreateInstance(); - Type resultType = typeof(TResult); - - foreach (var column in Columns) - { - var resultProperty = resultType.GetProperty(column.ColumnName); - if (dict.ContainsKey(column.ColumnName) && resultProperty is not null) - { - if (dict[column.ColumnName] is null) - { - resultProperty.SetValue(result, null); - continue; - } - object? value = Convert.ChangeType(dict[column.ColumnName], column.ColumnType.DatabaseClrType); - value = column.ColumnType.ConvertFromDatabase(value); - resultProperty.SetValue(result, value); - } - } - - return result; - } - - /// - /// Select and call hooks. - /// - public virtual List Select(IDbConnection dbConnection, string? what = null, IWhereClause? where = null, IOrderByClause? orderBy = null, int? skip = null, int? limit = null) - { - List queryResult = SelectDynamic(dbConnection, what, where, orderBy, skip, limit).ToList(); - - return queryResult.Select(MapDynamicTo).ToList(); - } - - public IInsertClause ConvertEntityToInsertClause(object entity) - { - Debug.Assert(EntityType.IsInstanceOfType(entity)); - var result = new InsertClause(); - foreach (var column in PropertyColumns) - { - var value = column.PropertyInfo!.GetValue(entity); - result.Add(column.ColumnName, value); - } - return result; - } - - /// - /// Insert a entity and call hooks. - /// - /// The key of insert entity. - public int Insert(IDbConnection dbConnection, IInsertClause insert, out object key) - { - object? finalKey = null; - - var realInsert = InsertClause.Create(); - - foreach (var column in Columns) - { - InsertItem? item = insert.Items.SingleOrDefault(i => i.ColumnName == column.ColumnName); - - var value = item?.Value; - - if (column.IsOnlyGenerated && value is not null) - { - throw new Exception($"The column '{column.ColumnName}' is auto generated. You can't specify it explicitly."); - } - - if (value is null) - { - value = column.GenerateDefaultValue(); - } - - if (value is null && column.IsAutoIncrement) - { - continue; - } - - if (value is null) - { - value = DbNullValue.Instance; - } - - column.InvokeValidator(value); - - InsertItem realInsertItem; - - if (value is DbNullValue) - { - if (column.IsNotNull) - { - throw new Exception($"Column '{column.ColumnName}' is not nullable. Please specify a non-null value."); - } - - realInsertItem = new InsertItem(column.ColumnName, null); - } - else - { - realInsertItem = new InsertItem(column.ColumnName, value); - } - - realInsert.Add(realInsertItem); - - if (realInsertItem.ColumnName == KeyColumn.ColumnName) - { - finalKey = realInsertItem.Value; - } - } - - if (finalKey is null) throw new Exception("No key???"); - key = finalKey; - - var (sql, parameters) = GenerateInsertSql(realInsert); - - var affectedRowCount = dbConnection.Execute(sql, ConvertParameters(parameters)); - - if (affectedRowCount != 1) - throw new Exception("Failed to insert."); - - return affectedRowCount; - } - - /// - /// Upgrade a entity and call hooks. - /// - /// The key of insert entity. - public virtual int Update(IDbConnection dbConnection, IWhereClause? where, IUpdateClause update, out object? newKey) - { - newKey = null; - - var realUpdate = UpdateClause.Create(); - - foreach (var column in Columns) - { - UpdateItem? item = update.Items.FirstOrDefault(i => i.ColumnName == column.ColumnName); - object? value = item?.Value; - - if (value is not null) - { - if (column.IsNoUpdate) - { - throw new Exception($"The column '{column.ColumnName}' can't be update."); - } - - column.InvokeValidator(value); - - realUpdate.Add(column.ColumnName, value); - - if (column.ColumnName == KeyColumn.ColumnName) - { - newKey = value; - } - } - } - - var (sql, parameters) = GenerateUpdateSql(where, realUpdate); - return dbConnection.Execute(sql, ConvertParameters(parameters)); - } - - public virtual int Delete(IDbConnection dbConnection, IWhereClause? where) - { - var (sql, parameters) = GenerateDeleteSql(where); - return dbConnection.Execute(sql, ConvertParameters(parameters)); - } -} - -public interface ITableInfoFactory -{ - TableInfo Get(Type type); -} - -public class TableInfoFactory : ITableInfoFactory -{ - private readonly Dictionary _cache = new Dictionary(); - private readonly IColumnTypeProvider _columnTypeProvider; - private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; - - public TableInfoFactory(IColumnTypeProvider columnTypeProvider, ILoggerFactory loggerFactory) - { - _columnTypeProvider = columnTypeProvider; - _loggerFactory = loggerFactory; - _logger = loggerFactory.CreateLogger(); - } - - // This is thread-safe. - public TableInfo Get(Type type) - { - lock (_cache) - { - if (_cache.TryGetValue(type, out var tableInfo)) - { - _logger.LogDebug("Table info of type '{}' is cached, return it.", type.Name); - return tableInfo; - } - else - { - _logger.LogDebug("Table info for type '{}' is not in cache, create it.", type.Name); - tableInfo = new TableInfo(type, _columnTypeProvider, _loggerFactory); - _logger.LogDebug("Table info for type '{}' is created, add it to cache.", type.Name); - _cache.Add(type, tableInfo); - return tableInfo; - } - } - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UpdateClause.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UpdateClause.cs deleted file mode 100644 index de5c6c3..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UpdateClause.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Text; - -namespace CrupestApi.Commons.Crud; - -public class UpdateItem -{ - public UpdateItem(string columnName, object? value) - { - ColumnName = columnName; - Value = value; - } - - public string ColumnName { get; set; } - public object? Value { get; set; } -} - -public interface IUpdateClause : IClause -{ - List Items { get; } - (string sql, ParamList parameters) GenerateSql(); -} - -public class UpdateClause : IUpdateClause -{ - public List Items { get; } = new List(); - - public UpdateClause(IEnumerable items) - { - Items.AddRange(items); - } - - public UpdateClause(params UpdateItem[] items) - { - Items.AddRange(items); - } - - public UpdateClause Add(params UpdateItem[] items) - { - Items.AddRange(items); - return this; - } - - public UpdateClause Add(string column, object? value) - { - return Add(new UpdateItem(column, value)); - } - - public static UpdateClause Create(params UpdateItem[] items) - { - return new UpdateClause(items); - } - - public List GetRelatedColumns() - { - return Items.Select(i => i.ColumnName).ToList(); - } - - public (string sql, ParamList parameters) GenerateSql() - { - var parameters = new ParamList(); - - StringBuilder result = new StringBuilder(); - - foreach (var item in Items) - { - if (result.Length > 0) - { - result.Append(", "); - } - - var parameterName = parameters.AddRandomNameParameter(item.Value, item.ColumnName); - result.Append($"{item.ColumnName} = @{parameterName}"); - } - - return (result.ToString(), parameters); - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UserException.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UserException.cs deleted file mode 100644 index 1a10b97..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UserException.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace CrupestApi.Commons.Crud; - -/// -/// This exception means the exception is caused by user and can be safely shown to user. -/// -[System.Serializable] -public class UserException : Exception -{ - public UserException() { } - public UserException(string message) : base(message) { } - public UserException(string message, System.Exception inner) : base(message, inner) { } - protected UserException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } -} \ No newline at end of file diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/WhereClause.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/WhereClause.cs deleted file mode 100644 index de69f2f..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/WhereClause.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System.Text; - -namespace CrupestApi.Commons.Crud; - -public interface IWhereClause : IClause -{ - // Does not contain "WHERE" keyword! - (string sql, ParamList parameters) GenerateSql(string? dbProviderId = null); -} - -public class CompositeWhereClause : IWhereClause -{ - public CompositeWhereClause(string concatOp, bool parenthesesSubclause, params IWhereClause[] subclauses) - { - ConcatOp = concatOp; - ParenthesesSubclause = parenthesesSubclause; - Subclauses = subclauses.ToList(); - } - - public string ConcatOp { get; } - public bool ParenthesesSubclause { get; } - public List Subclauses { get; } - - public CompositeWhereClause Eq(string column, object? value) - { - Subclauses.Add(SimpleCompareWhereClause.Eq(column, value)); - return this; - } - - public (string sql, ParamList parameters) GenerateSql(string? dbProviderId = null) - { - var parameters = new ParamList(); - var sql = new StringBuilder(); - var subclauses = GetSubclauses(); - if (subclauses is null) return ("", new()); - var first = true; - foreach (var subclause in Subclauses) - { - var (subSql, subParameters) = subclause.GenerateSql(dbProviderId); - if (subSql is null) continue; - if (first) - { - first = false; - } - else - { - sql.Append($" {ConcatOp} "); - } - if (ParenthesesSubclause) - { - sql.Append("("); - } - sql.Append(subSql); - if (ParenthesesSubclause) - { - sql.Append(")"); - } - parameters.AddRange(subParameters); - } - return (sql.ToString(), parameters); - } - - public object GetSubclauses() - { - return Subclauses; - } -} - -public class AndWhereClause : CompositeWhereClause -{ - public AndWhereClause(params IWhereClause[] clauses) - : this(true, clauses) - { - - } - - public AndWhereClause(bool parenthesesSubclause, params IWhereClause[] clauses) - : base("AND", parenthesesSubclause, clauses) - { - - } - - public static AndWhereClause Create(params IWhereClause[] clauses) - { - return new AndWhereClause(clauses); - } -} - -public class OrWhereClause : CompositeWhereClause -{ - public OrWhereClause(params IWhereClause[] clauses) - : this(true, clauses) - { - - } - - public OrWhereClause(bool parenthesesSubclause, params IWhereClause[] clauses) - : base("OR", parenthesesSubclause, clauses) - { - - } - - public static OrWhereClause Create(params IWhereClause[] clauses) - { - return new OrWhereClause(clauses); - } -} - -// It's simple because it only compare column and value but not expressions. -public class SimpleCompareWhereClause : IWhereClause -{ - public string Column { get; } - public string Operator { get; } - public object? Value { get; } - - public List GetRelatedColumns() - { - return new List { Column }; - } - - // It's user's responsibility to keep column safe, with proper escape. - public SimpleCompareWhereClause(string column, string op, object? value) - { - Column = column; - Operator = op; - Value = value; - } - - public static SimpleCompareWhereClause Create(string column, string op, object? value) - { - return new SimpleCompareWhereClause(column, op, value); - } - - public static SimpleCompareWhereClause Eq(string column, object? value) - { - return new SimpleCompareWhereClause(column, "=", value); - } - - public static SimpleCompareWhereClause Neq(string column, object? value) - { - return new SimpleCompareWhereClause(column, "<>", value); - } - - public static SimpleCompareWhereClause Gt(string column, object? value) - { - return new SimpleCompareWhereClause(column, ">", value); - } - - public static SimpleCompareWhereClause Gte(string column, object? value) - { - return new SimpleCompareWhereClause(column, ">=", value); - } - - public static SimpleCompareWhereClause Lt(string column, object? value) - { - return new SimpleCompareWhereClause(column, "<", value); - } - - public static SimpleCompareWhereClause Lte(string column, object? value) - { - return new SimpleCompareWhereClause(column, "<=", value); - } - - public (string sql, ParamList parameters) GenerateSql(string? dbProviderId = null) - { - var parameters = new ParamList(); - var parameterName = parameters.AddRandomNameParameter(Value, Column); - return ($"{Column} {Operator} @{parameterName}", parameters); - } -} - -public class WhereClause : AndWhereClause -{ - public WhereClause() - { - } - - public void Add(IWhereClause subclause) - { - Subclauses.Add(subclause); - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/CrupestApi.Commons.csproj b/docker/crupest-api/CrupestApi/CrupestApi.Commons/CrupestApi.Commons.csproj deleted file mode 100644 index 8e291fa..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/CrupestApi.Commons.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - net7.0 - library - enable - enable - false - - - - - - - - \ No newline at end of file diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/EntityNotExistException.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/EntityNotExistException.cs deleted file mode 100644 index 0e1f4f4..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/EntityNotExistException.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace CrupestApi.Commons; - -public class EntityNotExistException : Exception -{ - public EntityNotExistException() { } - public EntityNotExistException(string message) : base(message) { } - public EntityNotExistException(string message, Exception inner) : base(message, inner) { } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/HttpContextExtensions.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/HttpContextExtensions.cs deleted file mode 100644 index a0b2d89..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/HttpContextExtensions.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Text.Json; -using CrupestApi.Commons.Secrets; -using Microsoft.Extensions.Options; - -namespace CrupestApi.Commons; - -public delegate void HttpResponseAction(HttpResponse response); - -public class MessageBody -{ - public MessageBody(string message) - { - Message = message; - } - - public string Message { get; set; } -} - -public static class CrupestApiJsonExtensions -{ - public static IServiceCollection AddJsonOptions(this IServiceCollection services) - { - services.AddOptions(); - services.Configure(config => - { - config.AllowTrailingCommas = true; - config.PropertyNameCaseInsensitive = true; - config.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - }); - - return services; - } - - public static async Task ReadJsonAsync(this HttpRequest request) - { - var jsonOptions = request.HttpContext.RequestServices.GetRequiredService>(); - using var stream = request.Body; - var body = await JsonSerializer.DeserializeAsync(stream, jsonOptions.Value); - return body!; - } - - public static async Task WriteJsonAsync(this HttpResponse response, T bodyObject, int statusCode = 200, HttpResponseAction? beforeWriteBody = null, CancellationToken cancellationToken = default) - { - var jsonOptions = response.HttpContext.RequestServices.GetRequiredService>(); - byte[] json = JsonSerializer.SerializeToUtf8Bytes(bodyObject, jsonOptions.Value); - - var byteCount = json.Length; - - response.StatusCode = statusCode; - response.Headers.ContentType = "application/json; charset=utf-8"; - response.Headers.ContentLength = byteCount; - - if (beforeWriteBody is not null) - { - beforeWriteBody(response); - } - - await response.Body.WriteAsync(json, cancellationToken); - } - - public static async Task WriteMessageAsync(this HttpResponse response, string message, int statusCode = 400, HttpResponseAction? beforeWriteBody = null, CancellationToken cancellationToken = default) - { - await response.WriteJsonAsync(new MessageBody(message), statusCode: statusCode, beforeWriteBody, cancellationToken); - } - - public static Task ResponseJsonAsync(this HttpContext context, T bodyObject, int statusCode = 200, HttpResponseAction? beforeWriteBody = null, CancellationToken cancellationToken = default) - { - return context.Response.WriteJsonAsync(bodyObject, statusCode, beforeWriteBody, cancellationToken); - } - - public static Task ResponseMessageAsync(this HttpContext context, string message, int statusCode = 400, HttpResponseAction? beforeWriteBody = null, CancellationToken cancellationToken = default) - { - return context.Response.WriteMessageAsync(message, statusCode, beforeWriteBody, cancellationToken); - } - - public static string? GetToken(this HttpRequest request) - { - var token = request.Headers["Authorization"].ToString(); - if (token.StartsWith("Bearer ")) - { - token = token.Substring("Bearer ".Length); - return token; - } - - if (request.Query.TryGetValue("token", out var tokenValues)) - { - return tokenValues.Last(); - } - - return null; - } - - public static bool RequirePermission(this HttpContext context, string? permission) - { - if (permission is null) return true; - - var token = context.Request.GetToken(); - if (token is null) - { - context.ResponseMessageAsync("Unauthorized", 401); - return false; - } - - var secretService = context.RequestServices.GetRequiredService(); - var permissions = secretService.GetPermissions(token); - if (!permissions.Contains(permission)) - { - context.ResponseMessageAsync("Forbidden", 403); - return false; - } - return true; - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/ISecretService.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/ISecretService.cs deleted file mode 100644 index 83025f8..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/ISecretService.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace CrupestApi.Commons.Secrets; - -public interface ISecretService -{ - void CreateTestSecret(string key, string secret); - - List GetPermissions(string secret); -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretInfo.cs deleted file mode 100644 index c3a4de0..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretInfo.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -using CrupestApi.Commons.Crud; - -namespace CrupestApi.Commons.Secrets; - -public class SecretInfo -{ - [Column(NotNull = true)] - public string Key { get; set; } = default!; - [Column(NotNull = true, NoUpdate = true, ActAsKey = true)] - public string Secret { get; set; } = default!; - [Column(DefaultEmptyForString = true)] - public string Description { get; set; } = default!; - [Column(NotNull = false)] - public DateTime? ExpireTime { get; set; } - [Column(NotNull = true, DefaultValue = false)] - public bool Revoked { get; set; } - [Column(NotNull = true)] - public DateTime CreateTime { get; set; } - - private static RandomNumberGenerator RandomNumberGenerator = RandomNumberGenerator.Create(); - - private static string GenerateRandomKey(int length) - { - const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - var result = new StringBuilder(length); - lock (RandomNumberGenerator) - { - for (int i = 0; i < length; i++) - { - result.Append(chars[RandomNumberGenerator.GetInt32(chars.Length)]); - } - } - return result.ToString(); - } - - - public static string SecretDefaultValueGenerator() - { - return GenerateRandomKey(16); - } - - public static DateTime CreateTimeDefaultValueGenerator() - { - return DateTime.UtcNow; - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs deleted file mode 100644 index c693d8d..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Data; -using CrupestApi.Commons.Crud; -using CrupestApi.Commons.Crud.Migrations; - -namespace CrupestApi.Commons.Secrets; - -public class SecretService : CrudService, ISecretService -{ - private readonly ILogger _logger; - - public SecretService(ITableInfoFactory tableInfoFactory, IDbConnectionFactory dbConnectionFactory, IDatabaseMigrator migrator, ILoggerFactory loggerFactory) - : base(tableInfoFactory, dbConnectionFactory, migrator, loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - } - - protected override void AfterMigrate(IDbConnection connection, TableInfo table) - { - if (table.SelectCount(connection) == 0) - { - _logger.LogInformation("No secrets found, insert default secrets."); - using var transaction = connection.BeginTransaction(); - var insertClause = InsertClause.Create() - .Add(nameof(SecretInfo.Key), SecretsConstants.SecretManagementKey) - .Add(nameof(SecretInfo.Secret), "crupest") - .Add(nameof(SecretInfo.Description), "This is the init key. Please revoke it immediately after creating a new one."); - _table.Insert(connection, insertClause, out var _); - transaction.Commit(); - } - } - - public void CreateTestSecret(string key, string secret) - { - var connection = _dbConnection; - var insertClause = InsertClause.Create() - .Add(nameof(SecretInfo.Key), key) - .Add(nameof(SecretInfo.Secret), secret) - .Add(nameof(SecretInfo.Description), "Test secret."); - _table.Insert(connection, insertClause, out var _); - } - - public List GetPermissions(string secret) - { - var list = _table.Select(_dbConnection, - where: WhereClause.Create().Eq(nameof(SecretInfo.Secret), secret)); - return list.Select(x => x.Key).ToList(); - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretServiceCollectionExtensions.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretServiceCollectionExtensions.cs deleted file mode 100644 index a9c0e5f..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretServiceCollectionExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace CrupestApi.Commons.Secrets; - -public static class SecretServiceCollectionExtensions -{ - public static IServiceCollection AddSecrets(this IServiceCollection services) - { - services.TryAddScoped(); - return services; - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretsConstants.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretsConstants.cs deleted file mode 100644 index 207cc45..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretsConstants.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CrupestApi.Commons.Secrets; - -public static class SecretsConstants -{ - public const string SecretManagementKey = "crupest.secrets.management"; -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Files/CrupestApi.Files.csproj b/docker/crupest-api/CrupestApi/CrupestApi.Files/CrupestApi.Files.csproj deleted file mode 100644 index 2221809..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Files/CrupestApi.Files.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - net7.0 - library - enable - enable - false - - - diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Files/FilesService.cs b/docker/crupest-api/CrupestApi/CrupestApi.Files/FilesService.cs deleted file mode 100644 index c851a92..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Files/FilesService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CrupestApi.Files; - -public class FilesService -{ - -} \ No newline at end of file diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Secrets/CrupestApi.Secrets.csproj b/docker/crupest-api/CrupestApi/CrupestApi.Secrets/CrupestApi.Secrets.csproj deleted file mode 100644 index 70c83f3..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Secrets/CrupestApi.Secrets.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - net7.0 - library - enable - enable - false - - - diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretsExtensions.cs b/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretsExtensions.cs deleted file mode 100644 index e09887b..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretsExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using CrupestApi.Commons.Secrets; -using CrupestApi.Commons.Crud; - -namespace CrupestApi.Secrets; - -public static class SecretsExtensions -{ - public static IServiceCollection AddSecrets(this IServiceCollection services) - { - services.AddCrud(); - return services; - } - - public static WebApplication MapSecrets(this WebApplication webApplication, string path = "/api/secrets") - { - webApplication.MapCrud(path, SecretsConstants.SecretManagementKey); - return webApplication; - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Todos/CrupestApi.Todos.csproj b/docker/crupest-api/CrupestApi/CrupestApi.Todos/CrupestApi.Todos.csproj deleted file mode 100644 index 86460e3..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Todos/CrupestApi.Todos.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - net7.0 - library - enable - enable - false - - - diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosConfiguration.cs b/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosConfiguration.cs deleted file mode 100644 index e8160d2..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosConfiguration.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace CrupestApi.Todos; - -public class TodosConfiguration -{ - [Required] - public string Username { get; set; } = default!; - [Required] - public int ProjectNumber { get; set; } = default!; - [Required] - public string Token { get; set; } = default!; - public int Count { get; set; } -} \ No newline at end of file diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosService.cs b/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosService.cs deleted file mode 100644 index 5839086..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosService.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System.Net.Http.Headers; -using System.Net.Mime; -using System.Text; -using System.Text.Json; -using Microsoft.Extensions.Options; - -namespace CrupestApi.Todos; - -public class TodosItem -{ - public string Status { get; set; } = default!; - public string Title { get; set; } = default!; - public bool Closed { get; set; } - public string Color { get; set; } = default!; -} - -public class TodosService -{ - private readonly IOptionsSnapshot _options; - private readonly ILogger _logger; - - public TodosService(IOptionsSnapshot options, ILogger logger) - { - _options = options; - _logger = logger; - } - - private static string CreateGraphQLQuery(TodosConfiguration todoConfiguration) - { - return $$""" -{ - user(login: "{{todoConfiguration.Username}}") { - projectV2(number: {{todoConfiguration.ProjectNumber}}) { - items(last: {{todoConfiguration.Count}}) { - nodes { - fieldValueByName(name: "Status") { - ... on ProjectV2ItemFieldSingleSelectValue { - name - } - } - content { - __typename - ... on Issue { - title - closed - } - ... on PullRequest { - title - closed - } - ... on DraftIssue { - title - } - } - } - } - } - } -} -"""; - } - - - public async Task> GetTodosAsync() - { - var todoOptions = _options.Value; - if (todoOptions is null) - { - throw new Exception("Fail to get todos configuration."); - } - - _logger.LogInformation("Username: {}; ProjectNumber: {}; Count: {}", todoOptions.Username, todoOptions.ProjectNumber, todoOptions.Count); - _logger.LogInformation("Getting todos from GitHub GraphQL API..."); - - using var httpClient = new HttpClient(); - - using var requestContent = new StringContent(JsonSerializer.Serialize(new - { - query = CreateGraphQLQuery(todoOptions) - })); - requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Json, Encoding.UTF8.WebName); - - using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.github.com/graphql"); - request.Content = requestContent; - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", todoOptions.Token); - request.Headers.TryAddWithoutValidation("User-Agent", todoOptions.Username); - - using var response = await httpClient.SendAsync(request); - var responseBody = await response.Content.ReadAsStringAsync(); - - _logger.LogInformation("GitHub server returned status code: {}", response.StatusCode); - _logger.LogInformation("GitHub server returned body: {}", responseBody); - - if (response.IsSuccessStatusCode) - { - using var responseJson = JsonSerializer.Deserialize(responseBody); - if (responseJson is null) - { - throw new Exception("Fail to deserialize response body."); - } - - var nodes = responseJson.RootElement.GetProperty("data").GetProperty("user").GetProperty("projectV2").GetProperty("items").GetProperty("nodes").EnumerateArray(); - - var result = new List(); - - foreach (var node in nodes) - { - var content = node.GetProperty("content"); - var title = content.GetProperty("title").GetString(); - if (title is null) - { - throw new Exception("Fail to get title."); - } - - bool done = false; - - var statusField = node.GetProperty("fieldValueByName"); - if (statusField.ValueKind != JsonValueKind.Null) // if there is a "Status" field - { - var statusName = statusField.GetProperty("name").GetString(); - if (statusName is null) - { - throw new Exception("Fail to get status."); - } - - // if name is "Done", then it is closed, otherwise we check if the issue is closed - if (statusName.Equals("Done", StringComparison.OrdinalIgnoreCase)) - { - done = true; - } - } - - JsonElement closedElement; - // if item has a "closed" field, then it is a pull request or an issue, and we check if it is closed - if (content.TryGetProperty("closed", out closedElement) && closedElement.GetBoolean()) - { - done = true; - } - - // If item "Status" field is "Done' or item is a pull request or issue and it is closed, then it is done. - // Otherwise it is not closed. Like: - // 1. it is a draft issue with no "Status" field or "Status" field is not "Done" - // 2. it is a pull request or issue with no "Status" field or "Status" field is not "Done" and it is not closed - - result.Add(new TodosItem - { - Title = title, - Status = done ? "Done" : "Todo", - Closed = done, - Color = done ? "green" : "blue" - }); - } - - return result; - } - else - { - const string message = "Fail to get todos from GitHub."; - _logger.LogError(message); - throw new Exception(message); - } - } -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosServiceCollectionExtensions.cs b/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosServiceCollectionExtensions.cs deleted file mode 100644 index a49d55d..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosServiceCollectionExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace CrupestApi.Todos; - -public static class TodosServiceCollectionExtensions -{ - public static IServiceCollection AddTodos(this IServiceCollection services) - { - services.AddOptions().BindConfiguration("CrupestApi:Todos"); - services.PostConfigure(config => - { - if (config.Count == 0) - { - config.Count = 20; - } - }); - services.TryAddScoped(); - return services; - } -} - diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosWebApplicationExtensions.cs b/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosWebApplicationExtensions.cs deleted file mode 100644 index 0ff05a0..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosWebApplicationExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using CrupestApi.Commons; - -namespace CrupestApi.Todos; - -public static class TodosWebApplicationExtensions -{ - public static WebApplication MapTodos(this WebApplication app, string path) - { - if (app is null) - { - throw new ArgumentNullException(nameof(app)); - } - - app.MapGet(path, async (context) => - { - var todosService = context.RequestServices.GetRequiredService(); - - try - { - var todos = await todosService.GetTodosAsync(); - await context.Response.WriteJsonAsync(todos); - - } - catch (Exception e) - { - await context.Response.WriteMessageAsync(e.Message, statusCode: StatusCodes.Status503ServiceUnavailable); - } - }); - - return app; - } -} \ No newline at end of file diff --git a/docker/crupest-api/CrupestApi/CrupestApi.sln b/docker/crupest-api/CrupestApi/CrupestApi.sln deleted file mode 100644 index ebfd960..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.sln +++ /dev/null @@ -1,46 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrupestApi", "CrupestApi\CrupestApi.csproj", "{E30916BB-08F9-45F0-BC1A-69B66AE79913}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrupestApi.Todos", "CrupestApi.Todos\CrupestApi.Todos.csproj", "{BF9F5F71-AE65-4896-8E6F-FE0D4AD0E7D1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrupestApi.Secrets", "CrupestApi.Secrets\CrupestApi.Secrets.csproj", "{9A7CC9F9-70CB-408A-ADFC-5119C0BDB236}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrupestApi.Commons", "CrupestApi.Commons\CrupestApi.Commons.csproj", "{38083CCA-E56C-4D24-BAB6-EEC30E0F478F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrupestApi.Commons.Tests", "CrupestApi.Commons.Tests\CrupestApi.Commons.Tests.csproj", "{0D0304BF-6A18-444C-BAF4-6ABFF98A0F77}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {E30916BB-08F9-45F0-BC1A-69B66AE79913}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E30916BB-08F9-45F0-BC1A-69B66AE79913}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E30916BB-08F9-45F0-BC1A-69B66AE79913}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E30916BB-08F9-45F0-BC1A-69B66AE79913}.Release|Any CPU.Build.0 = Release|Any CPU - {BF9F5F71-AE65-4896-8E6F-FE0D4AD0E7D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BF9F5F71-AE65-4896-8E6F-FE0D4AD0E7D1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BF9F5F71-AE65-4896-8E6F-FE0D4AD0E7D1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BF9F5F71-AE65-4896-8E6F-FE0D4AD0E7D1}.Release|Any CPU.Build.0 = Release|Any CPU - {9A7CC9F9-70CB-408A-ADFC-5119C0BDB236}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9A7CC9F9-70CB-408A-ADFC-5119C0BDB236}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9A7CC9F9-70CB-408A-ADFC-5119C0BDB236}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9A7CC9F9-70CB-408A-ADFC-5119C0BDB236}.Release|Any CPU.Build.0 = Release|Any CPU - {38083CCA-E56C-4D24-BAB6-EEC30E0F478F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38083CCA-E56C-4D24-BAB6-EEC30E0F478F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {38083CCA-E56C-4D24-BAB6-EEC30E0F478F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38083CCA-E56C-4D24-BAB6-EEC30E0F478F}.Release|Any CPU.Build.0 = Release|Any CPU - {0D0304BF-6A18-444C-BAF4-6ABFF98A0F77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0D0304BF-6A18-444C-BAF4-6ABFF98A0F77}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0D0304BF-6A18-444C-BAF4-6ABFF98A0F77}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0D0304BF-6A18-444C-BAF4-6ABFF98A0F77}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/docker/crupest-api/CrupestApi/CrupestApi/CrupestApi.csproj b/docker/crupest-api/CrupestApi/CrupestApi/CrupestApi.csproj deleted file mode 100644 index 5954f00..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi/CrupestApi.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - net7.0 - enable - enable - false - - - \ No newline at end of file diff --git a/docker/crupest-api/CrupestApi/CrupestApi/Program.cs b/docker/crupest-api/CrupestApi/CrupestApi/Program.cs deleted file mode 100644 index 46648d9..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi/Program.cs +++ /dev/null @@ -1,24 +0,0 @@ -using CrupestApi.Commons; -using CrupestApi.Commons.Crud; -using CrupestApi.Secrets; -using CrupestApi.Todos; - -var builder = WebApplication.CreateBuilder(args); - -string configFilePath = Environment.GetEnvironmentVariable("CRUPEST_API_CONFIG_FILE") ?? "/crupest-api-config.json"; -builder.Configuration.AddJsonFile(configFilePath, optional: false, reloadOnChange: true); - -builder.Services.AddJsonOptions(); -builder.Services.AddCrupestApiConfig(); - -builder.Services.AddTodos(); -builder.Services.AddSecrets(); - -var app = builder.Build(); - -app.UseCrudCore(); -app.MapTodos("/api/todos"); -// TODO: It's not safe now! -// app.MapSecrets("/api/secrets"); - -app.Run(); diff --git a/docker/crupest-api/CrupestApi/CrupestApi/Properties/launchSettings.json b/docker/crupest-api/CrupestApi/CrupestApi/Properties/launchSettings.json deleted file mode 100644 index a4a5cbf..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi/Properties/launchSettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", - "profiles": { - "dev": { - "commandName": "Project", - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:5188", - "workingDirectory": ".", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "CRUPEST_API_CONFIG_FILE": "dev-config.json" - } - } - } -} \ No newline at end of file diff --git a/docker/crupest-api/CrupestApi/CrupestApi/appsettings.json b/docker/crupest-api/CrupestApi/CrupestApi/appsettings.json deleted file mode 100644 index 53753bd..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi/appsettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information" - } - }, - "AllowedHosts": "*" -} \ No newline at end of file diff --git a/docker/crupest-api/Dockerfile b/docker/crupest-api/Dockerfile deleted file mode 100644 index feb7522..0000000 --- a/docker/crupest-api/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS build -COPY CrupestApi /CrupestApi -WORKDIR /CrupestApi -RUN dotnet publish CrupestApi/CrupestApi.csproj --configuration Release --output ./publish -r linux-x64 - -FROM mcr.microsoft.com/dotnet/aspnet:7.0-alpine -ENV ASPNETCORE_URLS=http://0.0.0.0:5000 -ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true -COPY --from=build /CrupestApi/publish /CrupestApi -WORKDIR /CrupestApi -VOLUME [ "/crupest-api-config.json" ] -EXPOSE 5000 -ENTRYPOINT ["dotnet", "CrupestApi.dll"] diff --git a/docker/crupest-blog/Dockerfile b/docker/crupest-blog/Dockerfile deleted file mode 100644 index 7414d4e..0000000 --- a/docker/crupest-blog/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM debian:latest -ARG CRUPEST_BLOG_UPDATE_INTERVAL=1d -COPY install-hugo.bash /install-hugo.bash -RUN /install-hugo.bash && rm /install-hugo.bash -ENV CRUPEST_BLOG_UPDATE_INTERVAL=${CRUPEST_BLOG_UPDATE_INTERVAL} -COPY daemon.bash update.bash /scripts/ -VOLUME [ "/public" ] -ENTRYPOINT ["tini", "--"] -CMD [ "/scripts/daemon.bash" ] diff --git a/docker/crupest-blog/daemon.bash b/docker/crupest-blog/daemon.bash deleted file mode 100755 index 561a80a..0000000 --- a/docker/crupest-blog/daemon.bash +++ /dev/null @@ -1,19 +0,0 @@ -#! /usr/bin/env bash - -set -e - -# Check I'm root. -if [[ $EUID -ne 0 ]]; then - echo "This script must be run as root" 1>&2 - exit 1 -fi - -hugo version - -while true; do - /scripts/update.bash - - # sleep for CRUPEST_AUTO_BACKUP_INTERVAL - echo "Sleep for $CRUPEST_BLOG_UPDATE_INTERVAL for next build..." - sleep "$CRUPEST_BLOG_UPDATE_INTERVAL" -done diff --git a/docker/crupest-blog/install-hugo.bash b/docker/crupest-blog/install-hugo.bash deleted file mode 100755 index a448138..0000000 --- a/docker/crupest-blog/install-hugo.bash +++ /dev/null @@ -1,22 +0,0 @@ -#! /usr/bin/env bash - -set -e - -apt-get update -apt-get install -y tini locales curl git -rm -rf /var/lib/apt/lists/* -localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 - -VERSION=$(curl -s https://api.github.com/repos/gohugoio/hugo/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') - -echo "The latest version of hugo is $VERSION." - -url="https://github.com/gohugoio/hugo/releases/download/v${VERSION}/hugo_extended_${VERSION}_linux-amd64.deb" - -echo "Download hugo from $url." - -curl -sSfOL "$url" -dpkg -i "hugo_extended_${VERSION}_linux-amd64.deb" -rm "hugo_extended_${VERSION}_linux-amd64.deb" - -echo "Hugo version: $(hugo version)." diff --git a/docker/crupest-blog/update.bash b/docker/crupest-blog/update.bash deleted file mode 100755 index d4bcadc..0000000 --- a/docker/crupest-blog/update.bash +++ /dev/null @@ -1,30 +0,0 @@ -#! /usr/bin/env bash - -set -e - -echo -e "\e[0;103m\e[K\e[1mBegin to build blog...\e[0m" -echo "Begin time: $(date +%Y-%m-%dT%H:%M:%SZ)" - -mkdir -p /public - -# check /blog directory exists -if [[ ! -d /blog ]]; then - echo "Directory /blog not found, clone blog repository..." - git clone https://github.com/crupest/blog.git /blog - cd /blog - git submodule update --init --recursive -else - echo "Directory /blog founded, update blog repository..." - cd /blog - git fetch -p - git reset --hard origin/master - git submodule update --init --recursive -fi - -# Now hugo it -echo "Run hugo to generate blog..." -hugo -d /public - -echo "Finish time: $(date +%Y-%m-%dT%H:%M:%SZ)" -echo -e "\e[0;102m\e[K\e[1mFinish build!\e[0m" - diff --git a/docker/crupest-debian-dev/Dockerfile b/docker/crupest-debian-dev/Dockerfile deleted file mode 100644 index 95f0602..0000000 --- a/docker/crupest-debian-dev/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM debian:latest - -ARG USER=crupest -ARG IN_CHINA= -ARG CODE_SERVER=true - -ENV CRUPEST_DEBIAN_DEV_USER=${USER} -ENV CRUPEST_DEBIAN_DEV_IN_CHINA=${IN_CHINA} -ENV CRUPEST_DEBIAN_DEV_SETUP_CODE_SERVER=${CODE_SERVER} - -ADD bootstrap /bootstrap - -RUN /bootstrap/setup.bash -ENV LANG=en_US.utf8 - -USER ${USER} -WORKDIR /home/${USER} - -EXPOSE 8080 -VOLUME [ "/data", "/home/${USER}" ] -CMD [ "bash", "-l" ] diff --git a/docker/crupest-debian-dev/bootstrap/apt-source/11/add-deb-src.bash b/docker/crupest-debian-dev/bootstrap/apt-source/11/add-deb-src.bash deleted file mode 100755 index e134a00..0000000 --- a/docker/crupest-debian-dev/bootstrap/apt-source/11/add-deb-src.bash +++ /dev/null @@ -1,14 +0,0 @@ -#! /usr/bin/env bash - -set -e - -dir=$(dirname "$0") -domain=$("$dir/get-domain.bash") - -cat <> /etc/apt/sources.list - -deb-src https://$domain/debian/ bullseye main -deb-src https://$domain/debian-security/ bullseye-security main -deb-src https://$domain/debian-updates/ bullseye-updates main - -EOF diff --git a/docker/crupest-debian-dev/bootstrap/apt-source/11/get-domain.bash b/docker/crupest-debian-dev/bootstrap/apt-source/11/get-domain.bash deleted file mode 100755 index d44ea65..0000000 --- a/docker/crupest-debian-dev/bootstrap/apt-source/11/get-domain.bash +++ /dev/null @@ -1,5 +0,0 @@ -#! /usr/bin/env bash - -set -e - -sed "s|.*https\?://\([-_.a-zA-Z0-9]\+\)/.*|\\1|;q" /etc/apt/sources.list diff --git a/docker/crupest-debian-dev/bootstrap/apt-source/11/replace-domain.bash b/docker/crupest-debian-dev/bootstrap/apt-source/11/replace-domain.bash deleted file mode 100755 index 86e88dc..0000000 --- a/docker/crupest-debian-dev/bootstrap/apt-source/11/replace-domain.bash +++ /dev/null @@ -1,7 +0,0 @@ -#! /usr/bin/env bash - -set -e - -echo "Backup /etc/apt/sources.list to /etc/apt/sources.list.bak." -echo "Replace source domain in /etc/apt/sources.list to $1." -sed -i.bak "s|\(https\?://\)[-_.a-zA-Z0-9]\+/|\\1$1/|" /etc/apt/sources.list diff --git a/docker/crupest-debian-dev/bootstrap/apt-source/11/replace-http.bash b/docker/crupest-debian-dev/bootstrap/apt-source/11/replace-http.bash deleted file mode 100755 index fae082a..0000000 --- a/docker/crupest-debian-dev/bootstrap/apt-source/11/replace-http.bash +++ /dev/null @@ -1,7 +0,0 @@ -#! /usr/bin/env bash - -set -e - -echo "Backup /etc/apt/sources.list to /etc/apt/sources.list.bak." -echo "Replace http to https in /etc/apt/sources.list." -sed -i.bak 's/https\?/https/' /etc/apt/sources.list diff --git a/docker/crupest-debian-dev/bootstrap/apt-source/12/add-deb-src.bash b/docker/crupest-debian-dev/bootstrap/apt-source/12/add-deb-src.bash deleted file mode 100755 index cf741d6..0000000 --- a/docker/crupest-debian-dev/bootstrap/apt-source/12/add-deb-src.bash +++ /dev/null @@ -1,22 +0,0 @@ -#! /usr/bin/env bash - -set -e - -dir=$(dirname "$0") -domain=$("$dir/get-domain.bash") - -cat <> /etc/apt/sources.list.d/debian.sources - -Types: deb-src -URIs: https://$domain/debian -Suites: bookworm bookworm-updates -Components: main -Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg - -Types: deb-src -URIs: https://$domain/debian-security -Suites: bookworm-security -Components: main -Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg - -EOF \ No newline at end of file diff --git a/docker/crupest-debian-dev/bootstrap/apt-source/12/get-domain.bash b/docker/crupest-debian-dev/bootstrap/apt-source/12/get-domain.bash deleted file mode 100755 index a24538c..0000000 --- a/docker/crupest-debian-dev/bootstrap/apt-source/12/get-domain.bash +++ /dev/null @@ -1,6 +0,0 @@ -#! /usr/bin/env bash - -set -e - -grep -e 'URIs:' /etc/apt/sources.list.d/debian.sources | \ - sed -E 's|URIs:\s*https?://([-_.a-zA-Z0-9]+)/.*|\1|;q' diff --git a/docker/crupest-debian-dev/bootstrap/apt-source/12/replace-domain.bash b/docker/crupest-debian-dev/bootstrap/apt-source/12/replace-domain.bash deleted file mode 100755 index d55307c..0000000 --- a/docker/crupest-debian-dev/bootstrap/apt-source/12/replace-domain.bash +++ /dev/null @@ -1,7 +0,0 @@ -#! /usr/bin/env bash - -set -e - -echo "Backup /etc/apt/sources.list.d/debian.sources to /etc/apt/sources.list.d/debian.sources.bak." -echo "Replace source domain in /etc/apt/sources.list.d/debian.sources to $1." -sed -i.bak -E "s|(URIs:\\s*https?://)[-_.a-zA-Z0-9]+(/.*)|\\1$1\\2|" /etc/apt/sources.list.d/debian.sources diff --git a/docker/crupest-debian-dev/bootstrap/apt-source/12/replace-http.bash b/docker/crupest-debian-dev/bootstrap/apt-source/12/replace-http.bash deleted file mode 100755 index ed4391d..0000000 --- a/docker/crupest-debian-dev/bootstrap/apt-source/12/replace-http.bash +++ /dev/null @@ -1,7 +0,0 @@ -#! /usr/bin/env bash - -set -e - -echo "Backup /etc/apt/sources.list to /etc/apt/sources.list.d/debian.sources.bak." -echo "Replace http to https in /etc/apt/sources.list.d/debian.sources." -sed -i.bak -E "s|(URIs:\\s*)https?(://[-_.a-zA-Z0-9]+/.*)|\\1https\\2|" /etc/apt/sources.list.d/debian.sources diff --git a/docker/crupest-debian-dev/bootstrap/apt-source/china-source.txt b/docker/crupest-debian-dev/bootstrap/apt-source/china-source.txt deleted file mode 100644 index 4312686..0000000 --- a/docker/crupest-debian-dev/bootstrap/apt-source/china-source.txt +++ /dev/null @@ -1 +0,0 @@ -mirrors.tuna.tsinghua.edu.cn \ No newline at end of file diff --git a/docker/crupest-debian-dev/bootstrap/apt-source/install-apt-https.bash b/docker/crupest-debian-dev/bootstrap/apt-source/install-apt-https.bash deleted file mode 100755 index 70fb371..0000000 --- a/docker/crupest-debian-dev/bootstrap/apt-source/install-apt-https.bash +++ /dev/null @@ -1,8 +0,0 @@ -#! /usr/bin/env bash - -set -e - -echo "Install apt https transport." -apt-get update -apt-get install -y apt-utils -apt-get install -y apt-transport-https ca-certificates diff --git a/docker/crupest-debian-dev/bootstrap/apt-source/setup.bash b/docker/crupest-debian-dev/bootstrap/apt-source/setup.bash deleted file mode 100755 index cdf68af..0000000 --- a/docker/crupest-debian-dev/bootstrap/apt-source/setup.bash +++ /dev/null @@ -1,34 +0,0 @@ -#! /usr/bin/env bash - -set -e - -dir=/bootstrap/apt-source - -echo "Getting debian version..." -debian_version=$("$dir/../get-debian-version.bash") - -if [[ -z $debian_version ]]; then - echo "Debian version not found." - exit 1 -else - echo "Debian version: $debian_version" -fi - -if [[ $debian_version -ge 12 ]]; then - setup_dir=$dir/12 -else - setup_dir=$dir/11 -fi - -echo "Setting up apt source..." - -if [[ -n $CRUPEST_DEBIAN_DEV_IN_CHINA ]]; then - echo "In China, using China source..." - "$setup_dir/replace-domain.bash" "$(cat "$dir/china-source.txt")" -fi - -"$dir/install-apt-https.bash" -"$setup_dir/replace-http.bash" -"$setup_dir/add-deb-src.bash" - -echo "Setting up apt source done." diff --git a/docker/crupest-debian-dev/bootstrap/bash/bash-completion.bash b/docker/crupest-debian-dev/bootstrap/bash/bash-completion.bash deleted file mode 100644 index 75f8333..0000000 --- a/docker/crupest-debian-dev/bootstrap/bash/bash-completion.bash +++ /dev/null @@ -1,4 +0,0 @@ -if [ -f /etc/bash_completion ]; then - . /etc/bash_completion -fi - diff --git a/docker/crupest-debian-dev/bootstrap/bash/code-server.bash b/docker/crupest-debian-dev/bootstrap/bash/code-server.bash deleted file mode 100644 index 255c280..0000000 --- a/docker/crupest-debian-dev/bootstrap/bash/code-server.bash +++ /dev/null @@ -1,2 +0,0 @@ -mkdir -p ~/.local/share/code-server -/bootstrap/start/code-server.bash > ~/.local/share/code-server/log 2> ~/.local/share/code-server/error & diff --git a/docker/crupest-debian-dev/bootstrap/bash/dquilt.bash b/docker/crupest-debian-dev/bootstrap/bash/dquilt.bash deleted file mode 100644 index 96a4eb2..0000000 --- a/docker/crupest-debian-dev/bootstrap/bash/dquilt.bash +++ /dev/null @@ -1,4 +0,0 @@ -alias dquilt="quilt --quiltrc=${HOME}/.quiltrc-dpkg" -. /usr/share/bash-completion/completions/quilt -complete -F _quilt_completion $_quilt_complete_opt dquilt - diff --git a/docker/crupest-debian-dev/bootstrap/extra/setup-cmake.bash b/docker/crupest-debian-dev/bootstrap/extra/setup-cmake.bash deleted file mode 100755 index 76c1ae4..0000000 --- a/docker/crupest-debian-dev/bootstrap/extra/setup-cmake.bash +++ /dev/null @@ -1,9 +0,0 @@ -#! /usr/bin/env bash - -set -e - -CMAKE_VERSION=$(curl -s https://api.github.com/repos/Kitware/CMake/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') -wget -O cmake-installer.sh https://github.com/Kitware/CMake/releases/download/v"$CMAKE_VERSION"/cmake-"$CMAKE_VERSION"-linux-x86_64.sh -chmod +x cmake-installer.sh -./cmake-installer.sh --skip-license --prefix=/usr -rm cmake-installer.sh diff --git a/docker/crupest-debian-dev/bootstrap/extra/setup-dotnet.bash b/docker/crupest-debian-dev/bootstrap/extra/setup-dotnet.bash deleted file mode 100755 index 0ef7743..0000000 --- a/docker/crupest-debian-dev/bootstrap/extra/setup-dotnet.bash +++ /dev/null @@ -1,10 +0,0 @@ -#! /usr/bin/env bash - -set -e - -wget https://packages.microsoft.com/config/debian/11/packages-microsoft-prod.deb -O packages-microsoft-prod.deb -dpkg -i packages-microsoft-prod.deb -rm packages-microsoft-prod.deb - -apt-get update -apt-get install -y dotnet-sdk-7.0 diff --git a/docker/crupest-debian-dev/bootstrap/extra/setup-llvm.bash b/docker/crupest-debian-dev/bootstrap/extra/setup-llvm.bash deleted file mode 100755 index 48dde86..0000000 --- a/docker/crupest-debian-dev/bootstrap/extra/setup-llvm.bash +++ /dev/null @@ -1,26 +0,0 @@ -#! /usr/bin/env bash - -set -e - -LLVM_VERSION=18 - -. /bootstrap/func.bash - -if is_true "$CRUPEST_DEBIAN_DEV_IN_CHINA"; then - base_url=https://mirrors.tuna.tsinghua.edu.cn/llvm-apt -else - base_url=https://apt.llvm.org -fi - -wget "$base_url/llvm.sh" -chmod +x llvm.sh -./llvm.sh $LLVM_VERSION all -m "$base_url" -rm llvm.sh - -update-alternatives --install /usr/bin/clang clang /usr/bin/clang-$LLVM_VERSION 100 \ - --slave /usr/bin/clang++ clang++ /usr/bin/clang++-$LLVM_VERSION \ - --slave /usr/bin/clangd clangd /usr/bin/clangd-$LLVM_VERSION \ - --slave /usr/bin/clang-format clang-format /usr/bin/clang-format-$LLVM_VERSION \ - --slave /usr/bin/clang-tidy clang-tidy /usr/bin/clang-tidy-$LLVM_VERSION \ - --slave /usr/bin/lldb lldb /usr/bin/lldb-$LLVM_VERSION \ - --slave /usr/bin/lld lld /usr/bin/lld-$LLVM_VERSION diff --git a/docker/crupest-debian-dev/bootstrap/func.bash b/docker/crupest-debian-dev/bootstrap/func.bash deleted file mode 100644 index 7782035..0000000 --- a/docker/crupest-debian-dev/bootstrap/func.bash +++ /dev/null @@ -1,19 +0,0 @@ -is_true() { - if [[ "$1" =~ 1|on|true ]]; then - return 0 - else - return 1 - fi -} - -append-bash-profile() { - cat "/bootstrap/bash/$1" >> /home/$CRUPEST_DEBIAN_DEV_USER/.bash_profile -} - -append-bashrc() { - cat "/bootstrap/bash/$1" >> /home/$CRUPEST_DEBIAN_DEV_USER/.bashrc -} - -copy-home-dot-file() { - cp "/bootstrap/home-dot/$1" "/home/$CRUPEST_DEBIAN_DEV_USER/.$1" -} diff --git a/docker/crupest-debian-dev/bootstrap/get-debian-version.bash b/docker/crupest-debian-dev/bootstrap/get-debian-version.bash deleted file mode 100755 index 2cc10b9..0000000 --- a/docker/crupest-debian-dev/bootstrap/get-debian-version.bash +++ /dev/null @@ -1,13 +0,0 @@ -#! /usr/bin/env bash - -set -e - -if [ -f /etc/os-release ]; then - . /etc/os-release - if [ "$ID" = "debian" ]; then - echo "$VERSION_ID" - exit 0 - fi -fi - -exit 1 diff --git a/docker/crupest-debian-dev/bootstrap/home-dot/devscripts b/docker/crupest-debian-dev/bootstrap/home-dot/devscripts deleted file mode 100644 index a15b041..0000000 --- a/docker/crupest-debian-dev/bootstrap/home-dot/devscripts +++ /dev/null @@ -1 +0,0 @@ -export DGET_VERIFY=no \ No newline at end of file diff --git a/docker/crupest-debian-dev/bootstrap/home-dot/quiltrc-dpkg b/docker/crupest-debian-dev/bootstrap/home-dot/quiltrc-dpkg deleted file mode 100644 index e8fc3c5..0000000 --- a/docker/crupest-debian-dev/bootstrap/home-dot/quiltrc-dpkg +++ /dev/null @@ -1,13 +0,0 @@ -d=. -while [ ! -d $d/debian -a `readlink -e $d` != / ]; - do d=$d/..; done -if [ -d $d/debian ] && [ -z $QUILT_PATCHES ]; then - # if in Debian packaging tree with unset $QUILT_PATCHES - QUILT_PATCHES="debian/patches" - QUILT_PATCH_OPTS="--reject-format=unified" - QUILT_DIFF_ARGS="-p ab --no-timestamps --no-index --color=auto" - QUILT_REFRESH_ARGS="-p ab --no-timestamps --no-index" - QUILT_COLORS="diff_hdr=1;32:diff_add=1;34:diff_rem=1;31:diff_hunk=1;33:" - QUILT_COLORS="${QUILT_COLORS}diff_ctx=35:diff_cctx=33" - if ! [ -d $d/debian/patches ]; then mkdir $d/debian/patches; fi -fi diff --git a/docker/crupest-debian-dev/bootstrap/setup-base.bash b/docker/crupest-debian-dev/bootstrap/setup-base.bash deleted file mode 100755 index 31ded36..0000000 --- a/docker/crupest-debian-dev/bootstrap/setup-base.bash +++ /dev/null @@ -1,25 +0,0 @@ -#! /usr/bin/env bash - -set -e - -. /bootstrap/func.bash - -echo "Setting up basic system function..." - -echo "Installing basic packages..." -apt-get install -y apt-utils -apt-get install -y locales procps vim less man bash-completion software-properties-common rsync curl wget -echo "Installing basic packages done." - -echo "Setting up locale..." -localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 -echo "Setting up locale done." - -echo "Creating data dir..." -mkdir -p /data -chown $CRUPEST_DEBIAN_DEV_USER:$CRUPEST_DEBIAN_DEV_USER /data -echo "Creating data dir done." - -append-bashrc bash-completion.bash - -echo "Setting up basic system function done." diff --git a/docker/crupest-debian-dev/bootstrap/setup-code-server.bash b/docker/crupest-debian-dev/bootstrap/setup-code-server.bash deleted file mode 100755 index 34c9697..0000000 --- a/docker/crupest-debian-dev/bootstrap/setup-code-server.bash +++ /dev/null @@ -1,28 +0,0 @@ -#! /usr/bin/env bash - -set -e - -. /bootstrap/func.bash - -echo "Setting up code server..." - -echo "Get latest version of code-server..." -VERSION=$(curl -s https://api.github.com/repos/coder/code-server/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') -echo "Current latest version of code-server is $VERSION" - -echo "Downloading code-server..." -url="https://github.com/coder/code-server/releases/download/v${VERSION}/code-server_${VERSION}_amd64.deb" -curl -sSfOL "$url" -echo "Downloading code-server done." - -echo "Installing code-server..." -apt-get install -y "./code-server_${VERSION}_amd64.deb" -echo "Installing code-server done." - -echo "Cleaning up deb..." -rm "code-server_${VERSION}_amd64.deb" -echo "Cleaning up deb done." - -append-bash-profile code-server.bash - -echo "Setting up code server done." diff --git a/docker/crupest-debian-dev/bootstrap/setup-dev.bash b/docker/crupest-debian-dev/bootstrap/setup-dev.bash deleted file mode 100755 index 92deacb..0000000 --- a/docker/crupest-debian-dev/bootstrap/setup-dev.bash +++ /dev/null @@ -1,18 +0,0 @@ -#! /usr/bin/env bash - -set -e - -. /bootstrap/func.bash - -echo "Setting up dev function..." - -echo "Installing dev packages..." -apt-get install -y build-essential git devscripts debhelper quilt -apt-get build-dep -y linux -echo "Installing dev packages done." - -append-bashrc dquilt.bash -copy-home-dot-file devscripts -copy-home-dot-file quiltrc-dpkg - -echo "Setting up dev function done." diff --git a/docker/crupest-debian-dev/bootstrap/setup-user.bash b/docker/crupest-debian-dev/bootstrap/setup-user.bash deleted file mode 100755 index f74dcdb..0000000 --- a/docker/crupest-debian-dev/bootstrap/setup-user.bash +++ /dev/null @@ -1,20 +0,0 @@ -#! /usr/bin/env bash - -set -e - -echo "Setting up user..." - -echo "Installing sudo..." -apt-get install -y sudo -echo "Installing sudo done." - -echo "Setting up sudo..." -sed -i.bak 's|%sudo[[:space:]]\+ALL=(ALL:ALL)[[:space:]]\+ALL|%sudo ALL=(ALL:ALL) NOPASSWD: ALL|' /etc/sudoers -echo "Setting up sudo done." - -echo "Adding user $CRUPEST_DEBIAN_DEV_USER ..." -useradd -m -G sudo -s /usr/bin/bash "$CRUPEST_DEBIAN_DEV_USER" -echo "Adding user done." - -echo "Setting up user done." - diff --git a/docker/crupest-debian-dev/bootstrap/setup.bash b/docker/crupest-debian-dev/bootstrap/setup.bash deleted file mode 100755 index 09b8137..0000000 --- a/docker/crupest-debian-dev/bootstrap/setup.bash +++ /dev/null @@ -1,30 +0,0 @@ -#! /usr/bin/env bash - -set -e - -export DEBIAN_FRONTEND=noninteractive - -echo "Setting up crupest-debian-dev..." - -. /bootstrap/func.bash - -/bootstrap/apt-source/setup.bash - -echo "Updating apt source index..." -apt-get update -echo "Updating apt source index done." - -/bootstrap/setup-user.bash -/bootstrap/setup-base.bash -/bootstrap/setup-dev.bash - -if is_true "$CRUPEST_DEBIAN_DEV_SETUP_CODE_SERVER"; then - echo "CRUPEST_DEBIAN_DEV_SETUP_CODE_SERVER is true, setting up code-server..." - /bootstrap/setup-code-server.bash -fi - -echo "Cleaning up apt source index..." -rm -rf /var/lib/apt/lists/* -echo "Cleaning up apt source index done." - -echo "Setting up crupest-debian-dev done." diff --git a/docker/crupest-debian-dev/bootstrap/start/code-server.bash b/docker/crupest-debian-dev/bootstrap/start/code-server.bash deleted file mode 100755 index 7dfc0e9..0000000 --- a/docker/crupest-debian-dev/bootstrap/start/code-server.bash +++ /dev/null @@ -1,18 +0,0 @@ -#! /usr/bin/env bash - -export CODE_SERVER_CONFIG="/data/code-server-config.yaml" - -CODE_SERVER_PROGRAM=code-server -CODE_SERVER_PORT=8080 - -if which "$CODE_SERVER_PROGRAM" > /dev/null 2>&1; then - if ! pgrep -x "$CODE_SERVER_PROGRAM" > /dev/null 2>&1; then - echo "code-server is not running, starting..." - "$CODE_SERVER_PROGRAM" "--bind-addr" "0.0.0.0:$CODE_SERVER_PORT" - else - echo "code-server is already running." - fi -else - echo "code-server not found, skipping code-server setup." >&2 - exit 1 -fi diff --git a/docker/crupest-debian-dev/bootstrap/wait.bash b/docker/crupest-debian-dev/bootstrap/wait.bash deleted file mode 100755 index 501c706..0000000 --- a/docker/crupest-debian-dev/bootstrap/wait.bash +++ /dev/null @@ -1,5 +0,0 @@ -#! /usr/bin/env bash - -set -e - -tail -f /dev/null diff --git a/docker/crupest-nginx/Dockerfile b/docker/crupest-nginx/Dockerfile deleted file mode 100644 index 6d0400b..0000000 --- a/docker/crupest-nginx/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM node:lts AS build-www -RUN npm install -g pnpm -COPY sites/www /sites/www -WORKDIR /sites/www -RUN pnpm install --frozen-lockfile && pnpm run build - -FROM nginx:mainline -COPY --from=build-www /sites/www/dist /srv/www diff --git a/docker/crupest-nginx/sites/www/.dockerignore b/docker/crupest-nginx/sites/www/.dockerignore deleted file mode 100644 index de4d1f0..0000000 --- a/docker/crupest-nginx/sites/www/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/docker/crupest-nginx/sites/www/.gitignore b/docker/crupest-nginx/sites/www/.gitignore deleted file mode 100644 index 0b1e50b..0000000 --- a/docker/crupest-nginx/sites/www/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -.parcel-cache diff --git a/docker/crupest-nginx/sites/www/.proxyrc.json b/docker/crupest-nginx/sites/www/.proxyrc.json deleted file mode 100644 index 8fe13c0..0000000 --- a/docker/crupest-nginx/sites/www/.proxyrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "/api": { - "target": "http://localhost:5188", - "changeOrigin": true - } -} diff --git a/docker/crupest-nginx/sites/www/avatar.png b/docker/crupest-nginx/sites/www/avatar.png deleted file mode 100755 index d890d8d..0000000 Binary files a/docker/crupest-nginx/sites/www/avatar.png and /dev/null differ diff --git a/docker/crupest-nginx/sites/www/favicon.ico b/docker/crupest-nginx/sites/www/favicon.ico deleted file mode 100644 index 922a523..0000000 Binary files a/docker/crupest-nginx/sites/www/favicon.ico and /dev/null differ diff --git a/docker/crupest-nginx/sites/www/index.html b/docker/crupest-nginx/sites/www/index.html deleted file mode 100644 index 03485c5..0000000 --- a/docker/crupest-nginx/sites/www/index.html +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - crupest - - - -
-
- 🙃The world is full of pain, but we can fix it with love! -
-
- 😡The world is a piece of shit, so let's make it a little better! -
-
-
- -

Hello! This is crupest.

-
-

Welcome to my home page! I'm very glad to meet you here.

-

If you have something interesting to share with me, feel free to reach me via email at crupest@outlook.com or, of course, I@crupest.life. You can also fire an issue in - any repo on GitHub. Here is the link to my GitHub profile, https://github.com/crupest.

-

Currently this page is hosted on my own server and my own apex domain (crupest.life). You can also share this - website link, aka, https://crupest.life.

-
-
-

TODOs of me grabbed from my Github - project

-

Fetching...

-
    -
-
-
-

Friends of me:

- -
-
-

This is just the home page. I also run some other services on my server under this domain:

-

Public:

- -

Private:

- -

If you wish to deploy similar services like me, you are in the right place. Take a look at https://github.com/crupest/crupest and there is - all you need to start with. Also, contact me if you run into some problem.

-
- -
- - - - \ No newline at end of file diff --git a/docker/crupest-nginx/sites/www/package.json b/docker/crupest-nginx/sites/www/package.json deleted file mode 100644 index c42bb29..0000000 --- a/docker/crupest-nginx/sites/www/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "crupest-www", - "private": true, - "version": "0.1.0", - "source": "index.html", - "scripts": { - "start": "parcel", - "build": "tsc && parcel build" - }, - "devDependencies": { - "@tsconfig/recommended": "^1.0.7", - "@types/parcel-env": "^0.0.6", - "parcel": "^2.12.0", - "prettier": "^3.3.3", - "typescript": "^5.5.4" - } -} \ No newline at end of file diff --git a/docker/crupest-nginx/sites/www/pnpm-lock.yaml b/docker/crupest-nginx/sites/www/pnpm-lock.yaml deleted file mode 100644 index aa83a70..0000000 --- a/docker/crupest-nginx/sites/www/pnpm-lock.yaml +++ /dev/null @@ -1,2099 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - '@tsconfig/recommended': - specifier: ^1.0.7 - version: 1.0.7 - '@types/parcel-env': - specifier: ^0.0.6 - version: 0.0.6 - parcel: - specifier: ^2.12.0 - version: 2.12.0(@swc/helpers@0.5.12)(typescript@5.5.4) - prettier: - specifier: ^3.3.3 - version: 3.3.3 - typescript: - specifier: ^5.5.4 - version: 5.5.4 - -packages: - - '@babel/code-frame@7.24.7': - resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.24.7': - resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} - engines: {node: '>=6.9.0'} - - '@babel/highlight@7.24.7': - resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} - engines: {node: '>=6.9.0'} - - '@lezer/common@1.2.1': - resolution: {integrity: sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==} - - '@lezer/lr@1.4.2': - resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==} - - '@lmdb/lmdb-darwin-arm64@2.8.5': - resolution: {integrity: sha512-KPDeVScZgA1oq0CiPBcOa3kHIqU+pTOwRFDIhxvmf8CTNvqdZQYp5cCKW0bUk69VygB2PuTiINFWbY78aR2pQw==} - cpu: [arm64] - os: [darwin] - - '@lmdb/lmdb-darwin-x64@2.8.5': - resolution: {integrity: sha512-w/sLhN4T7MW1nB3R/U8WK5BgQLz904wh+/SmA2jD8NnF7BLLoUgflCNxOeSPOWp8geP6nP/+VjWzZVip7rZ1ug==} - cpu: [x64] - os: [darwin] - - '@lmdb/lmdb-linux-arm64@2.8.5': - resolution: {integrity: sha512-vtbZRHH5UDlL01TT5jB576Zox3+hdyogvpcbvVJlmU5PdL3c5V7cj1EODdh1CHPksRl+cws/58ugEHi8bcj4Ww==} - cpu: [arm64] - os: [linux] - - '@lmdb/lmdb-linux-arm@2.8.5': - resolution: {integrity: sha512-c0TGMbm2M55pwTDIfkDLB6BpIsgxV4PjYck2HiOX+cy/JWiBXz32lYbarPqejKs9Flm7YVAKSILUducU9g2RVg==} - cpu: [arm] - os: [linux] - - '@lmdb/lmdb-linux-x64@2.8.5': - resolution: {integrity: sha512-Xkc8IUx9aEhP0zvgeKy7IQ3ReX2N8N1L0WPcQwnZweWmOuKfwpS3GRIYqLtK5za/w3E60zhFfNdS+3pBZPytqQ==} - cpu: [x64] - os: [linux] - - '@lmdb/lmdb-win32-x64@2.8.5': - resolution: {integrity: sha512-4wvrf5BgnR8RpogHhtpCPJMKBmvyZPhhUtEwMJbXh0ni2BucpfF07jlmyM11zRqQ2XIq6PbC2j7W7UCCcm1rRQ==} - cpu: [x64] - os: [win32] - - '@mischnic/json-sourcemap@0.1.1': - resolution: {integrity: sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w==} - engines: {node: '>=12.0.0'} - - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} - cpu: [arm64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} - cpu: [x64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} - cpu: [arm64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} - cpu: [arm] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} - cpu: [x64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} - cpu: [x64] - os: [win32] - - '@parcel/bundler-default@2.12.0': - resolution: {integrity: sha512-3ybN74oYNMKyjD6V20c9Gerdbh7teeNvVMwIoHIQMzuIFT6IGX53PyOLlOKRLbjxMc0TMimQQxIt2eQqxR5LsA==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/cache@2.12.0': - resolution: {integrity: sha512-FX5ZpTEkxvq/yvWklRHDESVRz+c7sLTXgFuzz6uEnBcXV38j6dMSikflNpHA6q/L4GKkCqRywm9R6XQwhwIMyw==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@parcel/core': ^2.12.0 - - '@parcel/codeframe@2.12.0': - resolution: {integrity: sha512-v2VmneILFiHZJTxPiR7GEF1wey1/IXPdZMcUlNXBiPZyWDfcuNgGGVQkx/xW561rULLIvDPharOMdxz5oHOKQg==} - engines: {node: '>= 12.0.0'} - - '@parcel/compressor-raw@2.12.0': - resolution: {integrity: sha512-h41Q3X7ZAQ9wbQ2csP8QGrwepasLZdXiuEdpUryDce6rF9ZiHoJ97MRpdLxOhOPyASTw/xDgE1xyaPQr0Q3f5A==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/config-default@2.12.0': - resolution: {integrity: sha512-dPNe2n9eEsKRc1soWIY0yToMUPirPIa2QhxcCB3Z5RjpDGIXm0pds+BaiqY6uGLEEzsjhRO0ujd4v2Rmm0vuFg==} - peerDependencies: - '@parcel/core': ^2.12.0 - - '@parcel/core@2.12.0': - resolution: {integrity: sha512-s+6pwEj+GfKf7vqGUzN9iSEPueUssCCQrCBUlcAfKrJe0a22hTUCjewpB0I7lNrCIULt8dkndD+sMdOrXsRl6Q==} - engines: {node: '>= 12.0.0'} - - '@parcel/diagnostic@2.12.0': - resolution: {integrity: sha512-8f1NOsSFK+F4AwFCKynyIu9Kr/uWHC+SywAv4oS6Bv3Acig0gtwUjugk0C9UaB8ztBZiW5TQZhw+uPZn9T/lJA==} - engines: {node: '>= 12.0.0'} - - '@parcel/events@2.12.0': - resolution: {integrity: sha512-nmAAEIKLjW1kB2cUbCYSmZOGbnGj8wCzhqnK727zCCWaA25ogzAtt657GPOeFyqW77KyosU728Tl63Fc8hphIA==} - engines: {node: '>= 12.0.0'} - - '@parcel/fs@2.12.0': - resolution: {integrity: sha512-NnFkuvou1YBtPOhTdZr44WN7I60cGyly2wpHzqRl62yhObyi1KvW0SjwOMa0QGNcBOIzp4G0CapoZ93hD0RG5Q==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@parcel/core': ^2.12.0 - - '@parcel/graph@3.2.0': - resolution: {integrity: sha512-xlrmCPqy58D4Fg5umV7bpwDx5Vyt7MlnQPxW68vae5+BA4GSWetfZt+Cs5dtotMG2oCHzZxhIPt7YZ7NRyQzLA==} - engines: {node: '>= 12.0.0'} - - '@parcel/logger@2.12.0': - resolution: {integrity: sha512-cJ7Paqa7/9VJ7C+KwgJlwMqTQBOjjn71FbKk0G07hydUEBISU2aDfmc/52o60ErL9l+vXB26zTrIBanbxS8rVg==} - engines: {node: '>= 12.0.0'} - - '@parcel/markdown-ansi@2.12.0': - resolution: {integrity: sha512-WZz3rzL8k0H3WR4qTHX6Ic8DlEs17keO9gtD4MNGyMNQbqQEvQ61lWJaIH0nAtgEetu0SOITiVqdZrb8zx/M7w==} - engines: {node: '>= 12.0.0'} - - '@parcel/namer-default@2.12.0': - resolution: {integrity: sha512-9DNKPDHWgMnMtqqZIMiEj/R9PNWW16lpnlHjwK3ciRlMPgjPJ8+UNc255teZODhX0T17GOzPdGbU/O/xbxVPzA==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/node-resolver-core@3.3.0': - resolution: {integrity: sha512-rhPW9DYPEIqQBSlYzz3S0AjXxjN6Ub2yS6tzzsW/4S3Gpsgk/uEq4ZfxPvoPf/6TgZndVxmKwpmxaKtGMmf3cA==} - engines: {node: '>= 12.0.0'} - - '@parcel/optimizer-css@2.12.0': - resolution: {integrity: sha512-ifbcC97fRzpruTjaa8axIFeX4MjjSIlQfem3EJug3L2AVqQUXnM1XO8L0NaXGNLTW2qnh1ZjIJ7vXT/QhsphsA==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/optimizer-htmlnano@2.12.0': - resolution: {integrity: sha512-MfPMeCrT8FYiOrpFHVR+NcZQlXAptK2r4nGJjfT+ndPBhEEZp4yyL7n1y7HfX9geg5altc4WTb4Gug7rCoW8VQ==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/optimizer-image@2.12.0': - resolution: {integrity: sha512-bo1O7raeAIbRU5nmNVtx8divLW9Xqn0c57GVNGeAK4mygnQoqHqRZ0mR9uboh64pxv6ijXZHPhKvU9HEpjPjBQ==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - peerDependencies: - '@parcel/core': ^2.12.0 - - '@parcel/optimizer-svgo@2.12.0': - resolution: {integrity: sha512-Kyli+ZZXnoonnbeRQdoWwee9Bk2jm/49xvnfb+2OO8NN0d41lblBoRhOyFiScRnJrw7eVl1Xrz7NTkXCIO7XFQ==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/optimizer-swc@2.12.0': - resolution: {integrity: sha512-iBi6LZB3lm6WmbXfzi8J3DCVPmn4FN2lw7DGXxUXu7MouDPVWfTsM6U/5TkSHJRNRogZ2gqy5q9g34NPxHbJcw==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/package-manager@2.12.0': - resolution: {integrity: sha512-0nvAezcjPx9FT+hIL+LS1jb0aohwLZXct7jAh7i0MLMtehOi0z1Sau+QpgMlA9rfEZZ1LIeFdnZZwqSy7Ccspw==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@parcel/core': ^2.12.0 - - '@parcel/packager-css@2.12.0': - resolution: {integrity: sha512-j3a/ODciaNKD19IYdWJT+TP+tnhhn5koBGBWWtrKSu0UxWpnezIGZetit3eE+Y9+NTePalMkvpIlit2eDhvfJA==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/packager-html@2.12.0': - resolution: {integrity: sha512-PpvGB9hFFe+19NXGz2ApvPrkA9GwEqaDAninT+3pJD57OVBaxB8U+HN4a5LICKxjUppPPqmrLb6YPbD65IX4RA==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/packager-js@2.12.0': - resolution: {integrity: sha512-viMF+FszITRRr8+2iJyk+4ruGiL27Y6AF7hQ3xbJfzqnmbOhGFtLTQwuwhOLqN/mWR2VKdgbLpZSarWaO3yAMg==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/packager-raw@2.12.0': - resolution: {integrity: sha512-tJZqFbHqP24aq1F+OojFbQIc09P/u8HAW5xfndCrFnXpW4wTgM3p03P0xfw3gnNq+TtxHJ8c3UFE5LnXNNKhYA==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/packager-svg@2.12.0': - resolution: {integrity: sha512-ldaGiacGb2lLqcXas97k8JiZRbAnNREmcvoY2W2dvW4loVuDT9B9fU777mbV6zODpcgcHWsLL3lYbJ5Lt3y9cg==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/packager-wasm@2.12.0': - resolution: {integrity: sha512-fYqZzIqO9fGYveeImzF8ll6KRo2LrOXfD+2Y5U3BiX/wp9wv17dz50QLDQm9hmTcKGWxK4yWqKQh+Evp/fae7A==} - engines: {node: '>=12.0.0', parcel: ^2.12.0} - - '@parcel/plugin@2.12.0': - resolution: {integrity: sha512-nc/uRA8DiMoe4neBbzV6kDndh/58a4wQuGKw5oEoIwBCHUvE2W8ZFSu7ollSXUGRzfacTt4NdY8TwS73ScWZ+g==} - engines: {node: '>= 12.0.0'} - - '@parcel/profiler@2.12.0': - resolution: {integrity: sha512-q53fvl5LDcFYzMUtSusUBZSjQrKjMlLEBgKeQHFwkimwR1mgoseaDBDuNz0XvmzDzF1UelJ02TUKCGacU8W2qA==} - engines: {node: '>= 12.0.0'} - - '@parcel/reporter-cli@2.12.0': - resolution: {integrity: sha512-TqKsH4GVOLPSCanZ6tcTPj+rdVHERnt5y4bwTM82cajM21bCX1Ruwp8xOKU+03091oV2pv5ieB18pJyRF7IpIw==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/reporter-dev-server@2.12.0': - resolution: {integrity: sha512-tIcDqRvAPAttRlTV28dHcbWT5K2r/MBFks7nM4nrEDHWtnrCwimkDmZTc1kD8QOCCjGVwRHcQybpHvxfwol6GA==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/reporter-tracer@2.12.0': - resolution: {integrity: sha512-g8rlu9GxB8Ut/F8WGx4zidIPQ4pcYFjU9bZO+fyRIPrSUFH2bKijCnbZcr4ntqzDGx74hwD6cCG4DBoleq2UlQ==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/resolver-default@2.12.0': - resolution: {integrity: sha512-uuhbajTax37TwCxu7V98JtRLiT6hzE4VYSu5B7Qkauy14/WFt2dz6GOUXPgVsED569/hkxebPx3KCMtZW6cHHA==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/runtime-browser-hmr@2.12.0': - resolution: {integrity: sha512-4ZLp2FWyD32r0GlTulO3+jxgsA3oO1P1b5oO2IWuWilfhcJH5LTiazpL5YdusUjtNn9PGN6QLAWfxmzRIfM+Ow==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/runtime-js@2.12.0': - resolution: {integrity: sha512-sBerP32Z1crX5PfLNGDSXSdqzlllM++GVnVQVeM7DgMKS8JIFG3VLi28YkX+dYYGtPypm01JoIHCkvwiZEcQJg==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/runtime-react-refresh@2.12.0': - resolution: {integrity: sha512-SCHkcczJIDFTFdLTzrHTkQ0aTrX3xH6jrA4UsCBL6ji61+w+ohy4jEEe9qCgJVXhnJfGLE43HNXek+0MStX+Mw==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/runtime-service-worker@2.12.0': - resolution: {integrity: sha512-BXuMBsfiwpIEnssn+jqfC3jkgbS8oxeo3C7xhSQsuSv+AF2FwY3O3AO1c1RBskEW3XrBLNINOJujroNw80VTKA==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/rust@2.12.0': - resolution: {integrity: sha512-005cldMdFZFDPOjbDVEXcINQ3wT4vrxvSavRWI3Az0e3E18exO/x/mW9f648KtXugOXMAqCEqhFHcXECL9nmMw==} - engines: {node: '>= 12.0.0'} - - '@parcel/source-map@2.1.1': - resolution: {integrity: sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==} - engines: {node: ^12.18.3 || >=14} - - '@parcel/transformer-babel@2.12.0': - resolution: {integrity: sha512-zQaBfOnf/l8rPxYGnsk/ufh/0EuqvmnxafjBIpKZ//j6rGylw5JCqXSb1QvvAqRYruKeccxGv7+HrxpqKU6V4A==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/transformer-css@2.12.0': - resolution: {integrity: sha512-vXhOqoAlQGATYyQ433Z1DXKmiKmzOAUmKysbYH3FD+LKEKLMEl/pA14goqp00TW+A/EjtSKKyeMyHlMIIUqj4Q==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/transformer-html@2.12.0': - resolution: {integrity: sha512-5jW4dFFBlYBvIQk4nrH62rfA/G/KzVzEDa6S+Nne0xXhglLjkm64Ci9b/d4tKZfuGWUbpm2ASAq8skti/nfpXw==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/transformer-image@2.12.0': - resolution: {integrity: sha512-8hXrGm2IRII49R7lZ0RpmNk27EhcsH+uNKsvxuMpXPuEnWgC/ha/IrjaI29xCng1uGur74bJF43NUSQhR4aTdw==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - peerDependencies: - '@parcel/core': ^2.12.0 - - '@parcel/transformer-js@2.12.0': - resolution: {integrity: sha512-OSZpOu+FGDbC/xivu24v092D9w6EGytB3vidwbdiJ2FaPgfV7rxS0WIUjH4I0OcvHAcitArRXL0a3+HrNTdQQw==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - peerDependencies: - '@parcel/core': ^2.12.0 - - '@parcel/transformer-json@2.12.0': - resolution: {integrity: sha512-Utv64GLRCQILK5r0KFs4o7I41ixMPllwOLOhkdjJKvf1hZmN6WqfOmB1YLbWS/y5Zb/iB52DU2pWZm96vLFQZQ==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/transformer-postcss@2.12.0': - resolution: {integrity: sha512-FZqn+oUtiLfPOn67EZxPpBkfdFiTnF4iwiXPqvst3XI8H+iC+yNgzmtJkunOOuylpYY6NOU5jT8d7saqWSDv2Q==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/transformer-posthtml@2.12.0': - resolution: {integrity: sha512-z6Z7rav/pcaWdeD+2sDUcd0mmNZRUvtHaUGa50Y2mr+poxrKilpsnFMSiWBT+oOqPt7j71jzDvrdnAF4XkCljg==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/transformer-raw@2.12.0': - resolution: {integrity: sha512-Ht1fQvXxix0NncdnmnXZsa6hra20RXYh1VqhBYZLsDfkvGGFnXIgO03Jqn4Z8MkKoa0tiNbDhpKIeTjyclbBxQ==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/transformer-react-refresh-wrap@2.12.0': - resolution: {integrity: sha512-GE8gmP2AZtkpBIV5vSCVhewgOFRhqwdM5Q9jNPOY5PKcM3/Ff0qCqDiTzzGLhk0/VMBrdjssrfZkVx6S/lHdJw==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/transformer-svg@2.12.0': - resolution: {integrity: sha512-cZJqGRJ4JNdYcb+vj94J7PdOuTnwyy45dM9xqbIMH+HSiiIkfrMsdEwYft0GTyFTdsnf+hdHn3tau7Qa5hhX+A==} - engines: {node: '>= 12.0.0', parcel: ^2.12.0} - - '@parcel/types@2.12.0': - resolution: {integrity: sha512-8zAFiYNCwNTQcglIObyNwKfRYQK5ELlL13GuBOrSMxueUiI5ylgsGbTS1N7J3dAGZixHO8KhHGv5a71FILn9rQ==} - - '@parcel/utils@2.12.0': - resolution: {integrity: sha512-z1JhLuZ8QmDaYoEIuUCVZlhcFrS7LMfHrb2OCRui5SQFntRWBH2fNM6H/fXXUkT9SkxcuFP2DUA6/m4+Gkz72g==} - engines: {node: '>= 12.0.0'} - - '@parcel/watcher-android-arm64@2.4.1': - resolution: {integrity: sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [android] - - '@parcel/watcher-darwin-arm64@2.4.1': - resolution: {integrity: sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [darwin] - - '@parcel/watcher-darwin-x64@2.4.1': - resolution: {integrity: sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [darwin] - - '@parcel/watcher-freebsd-x64@2.4.1': - resolution: {integrity: sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [freebsd] - - '@parcel/watcher-linux-arm-glibc@2.4.1': - resolution: {integrity: sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - - '@parcel/watcher-linux-arm64-glibc@2.4.1': - resolution: {integrity: sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - - '@parcel/watcher-linux-arm64-musl@2.4.1': - resolution: {integrity: sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - - '@parcel/watcher-linux-x64-glibc@2.4.1': - resolution: {integrity: sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - - '@parcel/watcher-linux-x64-musl@2.4.1': - resolution: {integrity: sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - - '@parcel/watcher-win32-arm64@2.4.1': - resolution: {integrity: sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [win32] - - '@parcel/watcher-win32-ia32@2.4.1': - resolution: {integrity: sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.4.1': - resolution: {integrity: sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [win32] - - '@parcel/watcher@2.4.1': - resolution: {integrity: sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==} - engines: {node: '>= 10.0.0'} - - '@parcel/workers@2.12.0': - resolution: {integrity: sha512-zv5We5Jmb+ZWXlU6A+AufyjY4oZckkxsZ8J4dvyWL0W8IQvGO1JB4FGeryyttzQv3RM3OxcN/BpTGPiDG6keBw==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@parcel/core': ^2.12.0 - - '@swc/core-darwin-arm64@1.7.11': - resolution: {integrity: sha512-HRQv4qIeMBPThZ6Y/4yYW52rGsS6yrpusvuxLGyoFo45Y0y12/V2yXkOIA/0HIQyrqoUAxn1k4zQXpPaPNCmnw==} - engines: {node: '>=10'} - cpu: [arm64] - os: [darwin] - - '@swc/core-darwin-x64@1.7.11': - resolution: {integrity: sha512-vtMQj0F3oYwDu5yhO7SKDRg1XekRSi6/TbzHAbBXv+dBhlGGvcZZynT1H90EVFTv+7w7Sh+lOFvRv5Z4ZTcxow==} - engines: {node: '>=10'} - cpu: [x64] - os: [darwin] - - '@swc/core-linux-arm-gnueabihf@1.7.11': - resolution: {integrity: sha512-mHtzWKxhtyreI4CSxs+3+ENv8t/Qo35WFoYG66qHEgJz/Z2Lh6jv1E+MYgHdYwnpQHgHbdvAco7HsBu/Dt6xXw==} - engines: {node: '>=10'} - cpu: [arm] - os: [linux] - - '@swc/core-linux-arm64-gnu@1.7.11': - resolution: {integrity: sha512-FRwe/x0GfXSQjGP2lIk+NO0pUFS/lI/RorCLBPiK808EVE9JTbh9DKCc/4Bbb4jgScAjNkrFCUVObQYl3YKmpA==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - - '@swc/core-linux-arm64-musl@1.7.11': - resolution: {integrity: sha512-GY/rs0+GUq14Gbnza90KOrQd/9yHd5qQMii5jcSWcUCT5A8QTa8kiicsM2NxZeTJ69xlKmT7sLod5l99lki/2A==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - - '@swc/core-linux-x64-gnu@1.7.11': - resolution: {integrity: sha512-QDkGRwSPmp2RBOlSs503IUXlWYlny8DyznTT0QuK0ML2RpDFlXWU94K/EZhS0RBEUkMY/W51OacM8P8aS/dkCg==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - - '@swc/core-linux-x64-musl@1.7.11': - resolution: {integrity: sha512-SBEfKrXy6zQ6ksnyxw1FaCftrIH4fLfA81xNnKb7x/6iblv7Ko6H0aK3P5C86jyqF/82+ONl9C7ImGkUFQADig==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - - '@swc/core-win32-arm64-msvc@1.7.11': - resolution: {integrity: sha512-a2Y4xxEsLLYHJN7sMnw9+YQJDi3M1BxEr9hklfopPuGGnYLFNnx5CypH1l9ReijEfWjIAHNi7pq3m023lzW1Hg==} - engines: {node: '>=10'} - cpu: [arm64] - os: [win32] - - '@swc/core-win32-ia32-msvc@1.7.11': - resolution: {integrity: sha512-ZbZFMwZO+j8ulhegJ7EhJ/QVZPoQ5qc30ylJQSxizizTJaen71Q7/13lXWc6ksuCKvg6dUKrp/TPgoxOOtSrFA==} - engines: {node: '>=10'} - cpu: [ia32] - os: [win32] - - '@swc/core-win32-x64-msvc@1.7.11': - resolution: {integrity: sha512-IUohZedSJyDu/ReEBG/mqX6uG29uA7zZ9z6dIAF+p6eFxjXmh9MuHryyM+H8ebUyoq/Ad3rL+rUCksnuYNnI0w==} - engines: {node: '>=10'} - cpu: [x64] - os: [win32] - - '@swc/core@1.7.11': - resolution: {integrity: sha512-AB+qc45UrJrDfbhPKcUXk+9z/NmFfYYwJT6G7/iur0fCse9kXjx45gi40+u/O2zgarG/30/zV6E3ps8fUvjh7g==} - engines: {node: '>=10'} - peerDependencies: - '@swc/helpers': '*' - peerDependenciesMeta: - '@swc/helpers': - optional: true - - '@swc/counter@0.1.3': - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - - '@swc/helpers@0.5.12': - resolution: {integrity: sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g==} - - '@swc/types@0.1.12': - resolution: {integrity: sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==} - - '@trysound/sax@0.2.0': - resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} - engines: {node: '>=10.13.0'} - - '@tsconfig/recommended@1.0.7': - resolution: {integrity: sha512-xiNMgCuoy4mCL4JTywk9XFs5xpRUcKxtWEcMR6FNMtsgewYTIgIR+nvlP4A4iRCAzRsHMnPhvTRrzp4AGcRTEA==} - - '@types/parcel-env@0.0.6': - resolution: {integrity: sha512-HlPNbKhUe+0xVqzeqttl/gFC27dUZCjrUSI6MlfGoudjCtNeVOwm2kmDalkfXVWXsj4NJ4oa0PO3SdtOdGaGDg==} - - abortcontroller-polyfill@1.7.5: - resolution: {integrity: sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==} - - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - base-x@3.0.10: - resolution: {integrity: sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==} - - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.23.3: - resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - caniuse-lite@1.0.30001651: - resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==} - - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - chrome-trace-event@1.0.4: - resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} - engines: {node: '>=6.0'} - - clone@2.1.2: - resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} - engines: {node: '>=0.8'} - - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - - cosmiconfig@9.0.0: - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - - css-select@4.3.0: - resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} - - css-tree@1.1.3: - resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} - engines: {node: '>=8.0.0'} - - css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} - engines: {node: '>= 6'} - - csso@4.2.0: - resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} - engines: {node: '>=8.0.0'} - - detect-libc@1.0.3: - resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} - engines: {node: '>=0.10'} - hasBin: true - - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} - engines: {node: '>=8'} - - dom-serializer@1.4.1: - resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@4.3.1: - resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} - engines: {node: '>= 4'} - - domutils@2.8.0: - resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} - - dotenv-expand@5.1.0: - resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} - - dotenv@7.0.0: - resolution: {integrity: sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==} - engines: {node: '>=6'} - - electron-to-chromium@1.5.9: - resolution: {integrity: sha512-HfkT8ndXR0SEkU8gBQQM3rz035bpE/hxkZ1YIt4KJPEFES68HfIU6LzKukH0H794Lm83WJtkSAMfEToxCs15VA==} - - entities@2.2.0: - resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} - - entities@3.0.1: - resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} - engines: {node: '>=0.12'} - - env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - - escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} - engines: {node: '>=6'} - - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - get-port@4.2.0: - resolution: {integrity: sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==} - engines: {node: '>=6'} - - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - htmlnano@2.1.1: - resolution: {integrity: sha512-kAERyg/LuNZYmdqgCdYvugyLWNFAm8MWXpQMz1pLpetmCbFwoMxvkSoaAMlFrOC4OKTWI4KlZGT/RsNxg4ghOw==} - peerDependencies: - cssnano: ^7.0.0 - postcss: ^8.3.11 - purgecss: ^6.0.0 - relateurl: ^0.2.7 - srcset: 5.0.1 - svgo: ^3.0.2 - terser: ^5.10.0 - uncss: ^0.17.3 - peerDependenciesMeta: - cssnano: - optional: true - postcss: - optional: true - purgecss: - optional: true - relateurl: - optional: true - srcset: - optional: true - svgo: - optional: true - terser: - optional: true - uncss: - optional: true - - htmlparser2@7.2.0: - resolution: {integrity: sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==} - - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-json@2.0.1: - resolution: {integrity: sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - lightningcss-darwin-arm64@1.26.0: - resolution: {integrity: sha512-n4TIvHO1NY1ondKFYpL2ZX0bcC2y6yjXMD6JfyizgR8BCFNEeArINDzEaeqlfX9bXz73Bpz/Ow0nu+1qiDrBKg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.26.0: - resolution: {integrity: sha512-Rf9HuHIDi1R6/zgBkJh25SiJHF+dm9axUZW/0UoYCW1/8HV0gMI0blARhH4z+REmWiU1yYT/KyNF3h7tHyRXUg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.26.0: - resolution: {integrity: sha512-C/io7POAxp6sZxFSVGezjajMlCKQ8KSwISLLGRq8xLQpQMokYrUoqYEwmIX8mLmF6C/CZPk0gFmRSzd8biWM0g==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.26.0: - resolution: {integrity: sha512-Aag9kqXqkyPSW+dXMgyWk66C984Nay2pY8Nws+67gHlDzV3cWh7TvFlzuaTaVFMVqdDTzN484LSK3u39zFBnzg==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.26.0: - resolution: {integrity: sha512-iJmZM7fUyVjH+POtdiCtExG+67TtPUTer7K/5A8DIfmPfrmeGvzfRyBltGhQz13Wi15K1lf2cPYoRaRh6vcwNA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - - lightningcss-linux-arm64-musl@1.26.0: - resolution: {integrity: sha512-XxoEL++tTkyuvu+wq/QS8bwyTXZv2y5XYCMcWL45b8XwkiS8eEEEej9BkMGSRwxa5J4K+LDeIhLrS23CpQyfig==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - - lightningcss-linux-x64-gnu@1.26.0: - resolution: {integrity: sha512-1dkTfZQAYLj8MUSkd6L/+TWTG8V6Kfrzfa0T1fSlXCXQHrt1HC1/UepXHtKHDt/9yFwyoeayivxXAsApVxn6zA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - - lightningcss-linux-x64-musl@1.26.0: - resolution: {integrity: sha512-yX3Rk9m00JGCUzuUhFEojY+jf/6zHs3XU8S8Vk+FRbnr4St7cjyMXdNjuA2LjiT8e7j8xHRCH8hyZ4H/btRE4A==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - - lightningcss-win32-arm64-msvc@1.26.0: - resolution: {integrity: sha512-X/597/cFnCogy9VItj/+7Tgu5VLbAtDF7KZDPdSw0MaL6FL940th1y3HiOzFIlziVvAtbo0RB3NAae1Oofr+Tw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.26.0: - resolution: {integrity: sha512-pYS3EyGP3JRhfqEFYmfFDiZ9/pVNfy8jVIYtrx9TVNusVyDK3gpW1w/rbvroQ4bDJi7grdUtyrYU6V2xkY/bBw==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.26.0: - resolution: {integrity: sha512-a/XZ5hdgifrofQJUArr5AiJjx26SwMam3SJUSMjgebZbESZ96i+6Qsl8tLi0kaUsdMzBWXh9sN1Oe6hp2/dkQw==} - engines: {node: '>= 12.0.0'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - lmdb@2.8.5: - resolution: {integrity: sha512-9bMdFfc80S+vSldBmG3HOuLVHnxRdNTlpzR6QDnzqCQtCzGUEAGTzBKYMeIM+I/sU4oZfgbcbS7X7F65/z/oxQ==} - hasBin: true - - mdn-data@2.0.14: - resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} - - micromatch@4.0.7: - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} - engines: {node: '>=8.6'} - - msgpackr-extract@3.0.3: - resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} - hasBin: true - - msgpackr@1.11.0: - resolution: {integrity: sha512-I8qXuuALqJe5laEBYoFykChhSXLikZmUhccjGsPuSJ/7uPip2TJ7lwdIQwWSAi0jGZDXv4WOP8Qg65QZRuXxXw==} - - node-addon-api@6.1.0: - resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} - - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - - node-gyp-build-optional-packages@5.1.1: - resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} - hasBin: true - - node-gyp-build-optional-packages@5.2.2: - resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} - hasBin: true - - node-releases@2.0.18: - resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} - - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - - nullthrows@1.1.1: - resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} - - ordered-binary@1.5.1: - resolution: {integrity: sha512-5VyHfHY3cd0iza71JepYG50My+YUbrFtGoUz2ooEydPyPM7Aai/JW098juLr+RG6+rDJuzNNTsEQu2DZa1A41A==} - - parcel@2.12.0: - resolution: {integrity: sha512-W+gxAq7aQ9dJIg/XLKGcRT0cvnStFAQHPaI0pvD0U2l6IVLueUAm3nwN7lkY62zZNmlvNx6jNtE4wlbS+CyqSg==} - engines: {node: '>= 12.0.0'} - hasBin: true - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - - picocolors@1.0.1: - resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - - posthtml-parser@0.10.2: - resolution: {integrity: sha512-PId6zZ/2lyJi9LiKfe+i2xv57oEjJgWbsHGGANwos5AvdQp98i6AtamAl8gzSVFGfQ43Glb5D614cvZf012VKg==} - engines: {node: '>=12'} - - posthtml-parser@0.11.0: - resolution: {integrity: sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==} - engines: {node: '>=12'} - - posthtml-render@3.0.0: - resolution: {integrity: sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==} - engines: {node: '>=12'} - - posthtml@0.16.6: - resolution: {integrity: sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==} - engines: {node: '>=12.0.0'} - - prettier@3.3.3: - resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} - engines: {node: '>=14'} - hasBin: true - - react-error-overlay@6.0.9: - resolution: {integrity: sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==} - - react-refresh@0.9.0: - resolution: {integrity: sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==} - engines: {node: '>=0.10.0'} - - regenerator-runtime@0.13.11: - resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: '>=10'} - hasBin: true - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - srcset@4.0.0: - resolution: {integrity: sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==} - engines: {node: '>=12'} - - stable@0.1.8: - resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} - deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' - - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - svgo@2.8.0: - resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} - engines: {node: '>=10.13.0'} - hasBin: true - - term-size@2.2.1: - resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} - engines: {node: '>=8'} - - timsort@0.3.0: - resolution: {integrity: sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - tslib@2.6.3: - resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} - - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - - typescript@5.5.4: - resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} - engines: {node: '>=14.17'} - hasBin: true - - update-browserslist-db@1.1.0: - resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - utility-types@3.11.0: - resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} - engines: {node: '>= 4'} - - weak-lru-cache@1.2.2: - resolution: {integrity: sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==} - -snapshots: - - '@babel/code-frame@7.24.7': - dependencies: - '@babel/highlight': 7.24.7 - picocolors: 1.0.1 - - '@babel/helper-validator-identifier@7.24.7': {} - - '@babel/highlight@7.24.7': - dependencies: - '@babel/helper-validator-identifier': 7.24.7 - chalk: 2.4.2 - js-tokens: 4.0.0 - picocolors: 1.0.1 - - '@lezer/common@1.2.1': {} - - '@lezer/lr@1.4.2': - dependencies: - '@lezer/common': 1.2.1 - - '@lmdb/lmdb-darwin-arm64@2.8.5': - optional: true - - '@lmdb/lmdb-darwin-x64@2.8.5': - optional: true - - '@lmdb/lmdb-linux-arm64@2.8.5': - optional: true - - '@lmdb/lmdb-linux-arm@2.8.5': - optional: true - - '@lmdb/lmdb-linux-x64@2.8.5': - optional: true - - '@lmdb/lmdb-win32-x64@2.8.5': - optional: true - - '@mischnic/json-sourcemap@0.1.1': - dependencies: - '@lezer/common': 1.2.1 - '@lezer/lr': 1.4.2 - json5: 2.2.3 - - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - optional: true - - '@parcel/bundler-default@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/diagnostic': 2.12.0 - '@parcel/graph': 3.2.0 - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/rust': 2.12.0 - '@parcel/utils': 2.12.0 - nullthrows: 1.1.1 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/cache@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)': - dependencies: - '@parcel/core': 2.12.0(@swc/helpers@0.5.12) - '@parcel/fs': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/logger': 2.12.0 - '@parcel/utils': 2.12.0 - lmdb: 2.8.5 - transitivePeerDependencies: - - '@swc/helpers' - - '@parcel/codeframe@2.12.0': - dependencies: - chalk: 4.1.2 - - '@parcel/compressor-raw@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/config-default@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)(typescript@5.5.4)': - dependencies: - '@parcel/bundler-default': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/compressor-raw': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/core': 2.12.0(@swc/helpers@0.5.12) - '@parcel/namer-default': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/optimizer-css': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/optimizer-htmlnano': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(typescript@5.5.4) - '@parcel/optimizer-image': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/optimizer-svgo': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/optimizer-swc': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/packager-css': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/packager-html': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/packager-js': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/packager-raw': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/packager-svg': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/packager-wasm': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/reporter-dev-server': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/resolver-default': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/runtime-browser-hmr': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/runtime-js': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/runtime-react-refresh': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/runtime-service-worker': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/transformer-babel': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/transformer-css': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/transformer-html': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/transformer-image': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/transformer-js': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/transformer-json': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/transformer-postcss': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/transformer-posthtml': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/transformer-raw': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/transformer-react-refresh-wrap': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/transformer-svg': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - transitivePeerDependencies: - - '@swc/helpers' - - cssnano - - postcss - - purgecss - - relateurl - - srcset - - terser - - typescript - - uncss - - '@parcel/core@2.12.0(@swc/helpers@0.5.12)': - dependencies: - '@mischnic/json-sourcemap': 0.1.1 - '@parcel/cache': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/diagnostic': 2.12.0 - '@parcel/events': 2.12.0 - '@parcel/fs': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/graph': 3.2.0 - '@parcel/logger': 2.12.0 - '@parcel/package-manager': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/profiler': 2.12.0 - '@parcel/rust': 2.12.0 - '@parcel/source-map': 2.1.1 - '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/utils': 2.12.0 - '@parcel/workers': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - abortcontroller-polyfill: 1.7.5 - base-x: 3.0.10 - browserslist: 4.23.3 - clone: 2.1.2 - dotenv: 7.0.0 - dotenv-expand: 5.1.0 - json5: 2.2.3 - msgpackr: 1.11.0 - nullthrows: 1.1.1 - semver: 7.6.3 - transitivePeerDependencies: - - '@swc/helpers' - - '@parcel/diagnostic@2.12.0': - dependencies: - '@mischnic/json-sourcemap': 0.1.1 - nullthrows: 1.1.1 - - '@parcel/events@2.12.0': {} - - '@parcel/fs@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)': - dependencies: - '@parcel/core': 2.12.0(@swc/helpers@0.5.12) - '@parcel/rust': 2.12.0 - '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/utils': 2.12.0 - '@parcel/watcher': 2.4.1 - '@parcel/workers': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - transitivePeerDependencies: - - '@swc/helpers' - - '@parcel/graph@3.2.0': - dependencies: - nullthrows: 1.1.1 - - '@parcel/logger@2.12.0': - dependencies: - '@parcel/diagnostic': 2.12.0 - '@parcel/events': 2.12.0 - - '@parcel/markdown-ansi@2.12.0': - dependencies: - chalk: 4.1.2 - - '@parcel/namer-default@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/diagnostic': 2.12.0 - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - nullthrows: 1.1.1 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/node-resolver-core@3.3.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@mischnic/json-sourcemap': 0.1.1 - '@parcel/diagnostic': 2.12.0 - '@parcel/fs': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/rust': 2.12.0 - '@parcel/utils': 2.12.0 - nullthrows: 1.1.1 - semver: 7.6.3 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/optimizer-css@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/diagnostic': 2.12.0 - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/source-map': 2.1.1 - '@parcel/utils': 2.12.0 - browserslist: 4.23.3 - lightningcss: 1.26.0 - nullthrows: 1.1.1 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/optimizer-htmlnano@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(typescript@5.5.4)': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - htmlnano: 2.1.1(svgo@2.8.0)(typescript@5.5.4) - nullthrows: 1.1.1 - posthtml: 0.16.6 - svgo: 2.8.0 - transitivePeerDependencies: - - '@parcel/core' - - cssnano - - postcss - - purgecss - - relateurl - - srcset - - terser - - typescript - - uncss - - '@parcel/optimizer-image@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/core': 2.12.0(@swc/helpers@0.5.12) - '@parcel/diagnostic': 2.12.0 - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/rust': 2.12.0 - '@parcel/utils': 2.12.0 - '@parcel/workers': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - - '@parcel/optimizer-svgo@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/diagnostic': 2.12.0 - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/utils': 2.12.0 - svgo: 2.8.0 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/optimizer-swc@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)': - dependencies: - '@parcel/diagnostic': 2.12.0 - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/source-map': 2.1.1 - '@parcel/utils': 2.12.0 - '@swc/core': 1.7.11(@swc/helpers@0.5.12) - nullthrows: 1.1.1 - transitivePeerDependencies: - - '@parcel/core' - - '@swc/helpers' - - '@parcel/package-manager@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)': - dependencies: - '@parcel/core': 2.12.0(@swc/helpers@0.5.12) - '@parcel/diagnostic': 2.12.0 - '@parcel/fs': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/logger': 2.12.0 - '@parcel/node-resolver-core': 3.3.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/utils': 2.12.0 - '@parcel/workers': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@swc/core': 1.7.11(@swc/helpers@0.5.12) - semver: 7.6.3 - transitivePeerDependencies: - - '@swc/helpers' - - '@parcel/packager-css@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/diagnostic': 2.12.0 - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/source-map': 2.1.1 - '@parcel/utils': 2.12.0 - lightningcss: 1.26.0 - nullthrows: 1.1.1 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/packager-html@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/utils': 2.12.0 - nullthrows: 1.1.1 - posthtml: 0.16.6 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/packager-js@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/diagnostic': 2.12.0 - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/rust': 2.12.0 - '@parcel/source-map': 2.1.1 - '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/utils': 2.12.0 - globals: 13.24.0 - nullthrows: 1.1.1 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/packager-raw@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/packager-svg@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/utils': 2.12.0 - posthtml: 0.16.6 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/packager-wasm@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/plugin@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/profiler@2.12.0': - dependencies: - '@parcel/diagnostic': 2.12.0 - '@parcel/events': 2.12.0 - chrome-trace-event: 1.0.4 - - '@parcel/reporter-cli@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/utils': 2.12.0 - chalk: 4.1.2 - term-size: 2.2.1 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/reporter-dev-server@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/utils': 2.12.0 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/reporter-tracer@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/utils': 2.12.0 - chrome-trace-event: 1.0.4 - nullthrows: 1.1.1 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/resolver-default@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/node-resolver-core': 3.3.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/runtime-browser-hmr@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/utils': 2.12.0 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/runtime-js@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/diagnostic': 2.12.0 - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/utils': 2.12.0 - nullthrows: 1.1.1 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/runtime-react-refresh@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/utils': 2.12.0 - react-error-overlay: 6.0.9 - react-refresh: 0.9.0 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/runtime-service-worker@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/utils': 2.12.0 - nullthrows: 1.1.1 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/rust@2.12.0': {} - - '@parcel/source-map@2.1.1': - dependencies: - detect-libc: 1.0.3 - - '@parcel/transformer-babel@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/diagnostic': 2.12.0 - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/source-map': 2.1.1 - '@parcel/utils': 2.12.0 - browserslist: 4.23.3 - json5: 2.2.3 - nullthrows: 1.1.1 - semver: 7.6.3 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/transformer-css@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/diagnostic': 2.12.0 - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/source-map': 2.1.1 - '@parcel/utils': 2.12.0 - browserslist: 4.23.3 - lightningcss: 1.26.0 - nullthrows: 1.1.1 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/transformer-html@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/diagnostic': 2.12.0 - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/rust': 2.12.0 - nullthrows: 1.1.1 - posthtml: 0.16.6 - posthtml-parser: 0.10.2 - posthtml-render: 3.0.0 - semver: 7.6.3 - srcset: 4.0.0 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/transformer-image@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/core': 2.12.0(@swc/helpers@0.5.12) - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/utils': 2.12.0 - '@parcel/workers': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - nullthrows: 1.1.1 - - '@parcel/transformer-js@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/core': 2.12.0(@swc/helpers@0.5.12) - '@parcel/diagnostic': 2.12.0 - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/rust': 2.12.0 - '@parcel/source-map': 2.1.1 - '@parcel/utils': 2.12.0 - '@parcel/workers': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@swc/helpers': 0.5.12 - browserslist: 4.23.3 - nullthrows: 1.1.1 - regenerator-runtime: 0.13.11 - semver: 7.6.3 - - '@parcel/transformer-json@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - json5: 2.2.3 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/transformer-postcss@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/diagnostic': 2.12.0 - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/rust': 2.12.0 - '@parcel/utils': 2.12.0 - clone: 2.1.2 - nullthrows: 1.1.1 - postcss-value-parser: 4.2.0 - semver: 7.6.3 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/transformer-posthtml@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/utils': 2.12.0 - nullthrows: 1.1.1 - posthtml: 0.16.6 - posthtml-parser: 0.10.2 - posthtml-render: 3.0.0 - semver: 7.6.3 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/transformer-raw@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/transformer-react-refresh-wrap@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/utils': 2.12.0 - react-refresh: 0.9.0 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/transformer-svg@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/diagnostic': 2.12.0 - '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/rust': 2.12.0 - nullthrows: 1.1.1 - posthtml: 0.16.6 - posthtml-parser: 0.10.2 - posthtml-render: 3.0.0 - semver: 7.6.3 - transitivePeerDependencies: - - '@parcel/core' - - '@parcel/types@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)': - dependencies: - '@parcel/cache': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/diagnostic': 2.12.0 - '@parcel/fs': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/package-manager': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/source-map': 2.1.1 - '@parcel/workers': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - utility-types: 3.11.0 - transitivePeerDependencies: - - '@parcel/core' - - '@swc/helpers' - - '@parcel/utils@2.12.0': - dependencies: - '@parcel/codeframe': 2.12.0 - '@parcel/diagnostic': 2.12.0 - '@parcel/logger': 2.12.0 - '@parcel/markdown-ansi': 2.12.0 - '@parcel/rust': 2.12.0 - '@parcel/source-map': 2.1.1 - chalk: 4.1.2 - nullthrows: 1.1.1 - - '@parcel/watcher-android-arm64@2.4.1': - optional: true - - '@parcel/watcher-darwin-arm64@2.4.1': - optional: true - - '@parcel/watcher-darwin-x64@2.4.1': - optional: true - - '@parcel/watcher-freebsd-x64@2.4.1': - optional: true - - '@parcel/watcher-linux-arm-glibc@2.4.1': - optional: true - - '@parcel/watcher-linux-arm64-glibc@2.4.1': - optional: true - - '@parcel/watcher-linux-arm64-musl@2.4.1': - optional: true - - '@parcel/watcher-linux-x64-glibc@2.4.1': - optional: true - - '@parcel/watcher-linux-x64-musl@2.4.1': - optional: true - - '@parcel/watcher-win32-arm64@2.4.1': - optional: true - - '@parcel/watcher-win32-ia32@2.4.1': - optional: true - - '@parcel/watcher-win32-x64@2.4.1': - optional: true - - '@parcel/watcher@2.4.1': - dependencies: - detect-libc: 1.0.3 - is-glob: 4.0.3 - micromatch: 4.0.7 - node-addon-api: 7.1.1 - optionalDependencies: - '@parcel/watcher-android-arm64': 2.4.1 - '@parcel/watcher-darwin-arm64': 2.4.1 - '@parcel/watcher-darwin-x64': 2.4.1 - '@parcel/watcher-freebsd-x64': 2.4.1 - '@parcel/watcher-linux-arm-glibc': 2.4.1 - '@parcel/watcher-linux-arm64-glibc': 2.4.1 - '@parcel/watcher-linux-arm64-musl': 2.4.1 - '@parcel/watcher-linux-x64-glibc': 2.4.1 - '@parcel/watcher-linux-x64-musl': 2.4.1 - '@parcel/watcher-win32-arm64': 2.4.1 - '@parcel/watcher-win32-ia32': 2.4.1 - '@parcel/watcher-win32-x64': 2.4.1 - - '@parcel/workers@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': - dependencies: - '@parcel/core': 2.12.0(@swc/helpers@0.5.12) - '@parcel/diagnostic': 2.12.0 - '@parcel/logger': 2.12.0 - '@parcel/profiler': 2.12.0 - '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/utils': 2.12.0 - nullthrows: 1.1.1 - - '@swc/core-darwin-arm64@1.7.11': - optional: true - - '@swc/core-darwin-x64@1.7.11': - optional: true - - '@swc/core-linux-arm-gnueabihf@1.7.11': - optional: true - - '@swc/core-linux-arm64-gnu@1.7.11': - optional: true - - '@swc/core-linux-arm64-musl@1.7.11': - optional: true - - '@swc/core-linux-x64-gnu@1.7.11': - optional: true - - '@swc/core-linux-x64-musl@1.7.11': - optional: true - - '@swc/core-win32-arm64-msvc@1.7.11': - optional: true - - '@swc/core-win32-ia32-msvc@1.7.11': - optional: true - - '@swc/core-win32-x64-msvc@1.7.11': - optional: true - - '@swc/core@1.7.11(@swc/helpers@0.5.12)': - dependencies: - '@swc/counter': 0.1.3 - '@swc/types': 0.1.12 - optionalDependencies: - '@swc/core-darwin-arm64': 1.7.11 - '@swc/core-darwin-x64': 1.7.11 - '@swc/core-linux-arm-gnueabihf': 1.7.11 - '@swc/core-linux-arm64-gnu': 1.7.11 - '@swc/core-linux-arm64-musl': 1.7.11 - '@swc/core-linux-x64-gnu': 1.7.11 - '@swc/core-linux-x64-musl': 1.7.11 - '@swc/core-win32-arm64-msvc': 1.7.11 - '@swc/core-win32-ia32-msvc': 1.7.11 - '@swc/core-win32-x64-msvc': 1.7.11 - '@swc/helpers': 0.5.12 - - '@swc/counter@0.1.3': {} - - '@swc/helpers@0.5.12': - dependencies: - tslib: 2.6.3 - - '@swc/types@0.1.12': - dependencies: - '@swc/counter': 0.1.3 - - '@trysound/sax@0.2.0': {} - - '@tsconfig/recommended@1.0.7': {} - - '@types/parcel-env@0.0.6': {} - - abortcontroller-polyfill@1.7.5: {} - - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - argparse@2.0.1: {} - - base-x@3.0.10: - dependencies: - safe-buffer: 5.2.1 - - boolbase@1.0.0: {} - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.23.3: - dependencies: - caniuse-lite: 1.0.30001651 - electron-to-chromium: 1.5.9 - node-releases: 2.0.18 - update-browserslist-db: 1.1.0(browserslist@4.23.3) - - callsites@3.1.0: {} - - caniuse-lite@1.0.30001651: {} - - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - chrome-trace-event@1.0.4: {} - - clone@2.1.2: {} - - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.3: {} - - color-name@1.1.4: {} - - commander@7.2.0: {} - - cosmiconfig@9.0.0(typescript@5.5.4): - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - parse-json: 5.2.0 - optionalDependencies: - typescript: 5.5.4 - - css-select@4.3.0: - dependencies: - boolbase: 1.0.0 - css-what: 6.1.0 - domhandler: 4.3.1 - domutils: 2.8.0 - nth-check: 2.1.1 - - css-tree@1.1.3: - dependencies: - mdn-data: 2.0.14 - source-map: 0.6.1 - - css-what@6.1.0: {} - - csso@4.2.0: - dependencies: - css-tree: 1.1.3 - - detect-libc@1.0.3: {} - - detect-libc@2.0.3: {} - - dom-serializer@1.4.1: - dependencies: - domelementtype: 2.3.0 - domhandler: 4.3.1 - entities: 2.2.0 - - domelementtype@2.3.0: {} - - domhandler@4.3.1: - dependencies: - domelementtype: 2.3.0 - - domutils@2.8.0: - dependencies: - dom-serializer: 1.4.1 - domelementtype: 2.3.0 - domhandler: 4.3.1 - - dotenv-expand@5.1.0: {} - - dotenv@7.0.0: {} - - electron-to-chromium@1.5.9: {} - - entities@2.2.0: {} - - entities@3.0.1: {} - - env-paths@2.2.1: {} - - error-ex@1.3.2: - dependencies: - is-arrayish: 0.2.1 - - escalade@3.1.2: {} - - escape-string-regexp@1.0.5: {} - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - get-port@4.2.0: {} - - globals@13.24.0: - dependencies: - type-fest: 0.20.2 - - has-flag@3.0.0: {} - - has-flag@4.0.0: {} - - htmlnano@2.1.1(svgo@2.8.0)(typescript@5.5.4): - dependencies: - cosmiconfig: 9.0.0(typescript@5.5.4) - posthtml: 0.16.6 - timsort: 0.3.0 - optionalDependencies: - svgo: 2.8.0 - transitivePeerDependencies: - - typescript - - htmlparser2@7.2.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 4.3.1 - domutils: 2.8.0 - entities: 3.0.1 - - import-fresh@3.3.0: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - is-arrayish@0.2.1: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-json@2.0.1: {} - - is-number@7.0.0: {} - - js-tokens@4.0.0: {} - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - json-parse-even-better-errors@2.3.1: {} - - json5@2.2.3: {} - - lightningcss-darwin-arm64@1.26.0: - optional: true - - lightningcss-darwin-x64@1.26.0: - optional: true - - lightningcss-freebsd-x64@1.26.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.26.0: - optional: true - - lightningcss-linux-arm64-gnu@1.26.0: - optional: true - - lightningcss-linux-arm64-musl@1.26.0: - optional: true - - lightningcss-linux-x64-gnu@1.26.0: - optional: true - - lightningcss-linux-x64-musl@1.26.0: - optional: true - - lightningcss-win32-arm64-msvc@1.26.0: - optional: true - - lightningcss-win32-x64-msvc@1.26.0: - optional: true - - lightningcss@1.26.0: - dependencies: - detect-libc: 1.0.3 - optionalDependencies: - lightningcss-darwin-arm64: 1.26.0 - lightningcss-darwin-x64: 1.26.0 - lightningcss-freebsd-x64: 1.26.0 - lightningcss-linux-arm-gnueabihf: 1.26.0 - lightningcss-linux-arm64-gnu: 1.26.0 - lightningcss-linux-arm64-musl: 1.26.0 - lightningcss-linux-x64-gnu: 1.26.0 - lightningcss-linux-x64-musl: 1.26.0 - lightningcss-win32-arm64-msvc: 1.26.0 - lightningcss-win32-x64-msvc: 1.26.0 - - lines-and-columns@1.2.4: {} - - lmdb@2.8.5: - dependencies: - msgpackr: 1.11.0 - node-addon-api: 6.1.0 - node-gyp-build-optional-packages: 5.1.1 - ordered-binary: 1.5.1 - weak-lru-cache: 1.2.2 - optionalDependencies: - '@lmdb/lmdb-darwin-arm64': 2.8.5 - '@lmdb/lmdb-darwin-x64': 2.8.5 - '@lmdb/lmdb-linux-arm': 2.8.5 - '@lmdb/lmdb-linux-arm64': 2.8.5 - '@lmdb/lmdb-linux-x64': 2.8.5 - '@lmdb/lmdb-win32-x64': 2.8.5 - - mdn-data@2.0.14: {} - - micromatch@4.0.7: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - msgpackr-extract@3.0.3: - dependencies: - node-gyp-build-optional-packages: 5.2.2 - optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 - optional: true - - msgpackr@1.11.0: - optionalDependencies: - msgpackr-extract: 3.0.3 - - node-addon-api@6.1.0: {} - - node-addon-api@7.1.1: {} - - node-gyp-build-optional-packages@5.1.1: - dependencies: - detect-libc: 2.0.3 - - node-gyp-build-optional-packages@5.2.2: - dependencies: - detect-libc: 2.0.3 - optional: true - - node-releases@2.0.18: {} - - nth-check@2.1.1: - dependencies: - boolbase: 1.0.0 - - nullthrows@1.1.1: {} - - ordered-binary@1.5.1: {} - - parcel@2.12.0(@swc/helpers@0.5.12)(typescript@5.5.4): - dependencies: - '@parcel/config-default': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)(typescript@5.5.4) - '@parcel/core': 2.12.0(@swc/helpers@0.5.12) - '@parcel/diagnostic': 2.12.0 - '@parcel/events': 2.12.0 - '@parcel/fs': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/logger': 2.12.0 - '@parcel/package-manager': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) - '@parcel/reporter-cli': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/reporter-dev-server': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/reporter-tracer': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) - '@parcel/utils': 2.12.0 - chalk: 4.1.2 - commander: 7.2.0 - get-port: 4.2.0 - transitivePeerDependencies: - - '@swc/helpers' - - cssnano - - postcss - - purgecss - - relateurl - - srcset - - terser - - typescript - - uncss - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.24.7 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - - picocolors@1.0.1: {} - - picomatch@2.3.1: {} - - postcss-value-parser@4.2.0: {} - - posthtml-parser@0.10.2: - dependencies: - htmlparser2: 7.2.0 - - posthtml-parser@0.11.0: - dependencies: - htmlparser2: 7.2.0 - - posthtml-render@3.0.0: - dependencies: - is-json: 2.0.1 - - posthtml@0.16.6: - dependencies: - posthtml-parser: 0.11.0 - posthtml-render: 3.0.0 - - prettier@3.3.3: {} - - react-error-overlay@6.0.9: {} - - react-refresh@0.9.0: {} - - regenerator-runtime@0.13.11: {} - - resolve-from@4.0.0: {} - - safe-buffer@5.2.1: {} - - semver@7.6.3: {} - - source-map@0.6.1: {} - - srcset@4.0.0: {} - - stable@0.1.8: {} - - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - svgo@2.8.0: - dependencies: - '@trysound/sax': 0.2.0 - commander: 7.2.0 - css-select: 4.3.0 - css-tree: 1.1.3 - csso: 4.2.0 - picocolors: 1.0.1 - stable: 0.1.8 - - term-size@2.2.1: {} - - timsort@0.3.0: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - tslib@2.6.3: {} - - type-fest@0.20.2: {} - - typescript@5.5.4: {} - - update-browserslist-db@1.1.0(browserslist@4.23.3): - dependencies: - browserslist: 4.23.3 - escalade: 3.1.2 - picocolors: 1.0.1 - - utility-types@3.11.0: {} - - weak-lru-cache@1.2.2: {} diff --git a/docker/crupest-nginx/sites/www/src/main.ts b/docker/crupest-nginx/sites/www/src/main.ts deleted file mode 100644 index 2f09deb..0000000 --- a/docker/crupest-nginx/sites/www/src/main.ts +++ /dev/null @@ -1,97 +0,0 @@ -import "./style.css"; - -import { fetchTodos } from "./todos"; - -const happy = "happy" as const; -const angry = "angry" as const; -type Emotion = typeof happy | typeof angry; - -function emotionOpposite(emotion: Emotion): Emotion { - if (emotion === happy) { - return angry; - } else { - return happy; - } -} - -function emotionElement(emotion: Emotion): HTMLDivElement { - return document.querySelector(`.slogan.${emotion}`)!; -} - -function emotionElementHeight(emotion: Emotion): number { - return emotionElement(emotion).clientHeight; -} - -function updateBodyTopPadding(emotion: Emotion): void { - document.body.style.paddingTop = `${emotionElementHeight(emotion)}px`; -} - -const sloganEmotionKey = "sloganEmotion"; - -const savedEmotion = - (localStorage.getItem(sloganEmotionKey) as Emotion | null) ?? happy; -if (savedEmotion !== happy && savedEmotion !== angry) { - console.error(`Invalid saved emotion: ${savedEmotion}`); -} - -updateBodyTopPadding(savedEmotion); -// Then we add transition animation. -setTimeout(() => { - document.body.style.transition = "padding-top 1s"; -}); - -const sloganContainer = document.querySelector( - ".slogan-container", -) as HTMLDivElement; - -setTimeout(() => { - sloganContainer.dataset.sloganEmotion = savedEmotion; -}, 500); - -const sloganLoadedPromise = new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, 1500); -}); - -for (const emotion of [happy, angry]) { - emotionElement(emotion).addEventListener("click", () => { - const opposite = emotionOpposite(emotion); - localStorage.setItem(sloganEmotionKey, opposite); - sloganContainer.dataset.sloganEmotion = opposite; - updateBodyTopPadding(opposite); - }); -} - -async function loadTodos(syncWith: Promise): Promise { - const todoMessage = document.getElementById("todo-message")!; - const todoContainer = document.getElementById("todo-container")!; - - try { - const todosPromise = fetchTodos(); - await syncWith; // Let's wait this first. - const todos = await todosPromise; - todos.forEach((item, index) => { - const { status, title, closed } = item; - const li = document.createElement("li"); - li.dataset.status = closed ? "closed" : "open"; - li.style.animationDelay = `${index * 0.04}s`; - // The color from api server is kind of ugly at present. - // li.style.background = color; - const statusSpan = document.createElement("span"); - const titleSpan = document.createElement("span"); - statusSpan.textContent = status; - titleSpan.textContent = title; - li.appendChild(statusSpan); - li.append(" : "); - li.append(titleSpan); - todoContainer.appendChild(li); - }); - todoMessage.parentElement!.removeChild(todoMessage); - } catch (e) { - todoMessage.style.color = "red"; - todoMessage.textContent = (e as Error).message; - } -} - -loadTodos(sloganLoadedPromise); diff --git a/docker/crupest-nginx/sites/www/src/mock-todos.ts b/docker/crupest-nginx/sites/www/src/mock-todos.ts deleted file mode 100644 index aacb40e..0000000 --- a/docker/crupest-nginx/sites/www/src/mock-todos.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** Grabbed at Tue, 18 Jul 2023 15:30:05 GMT, used as mock data. 🍻 */ - -const todos = [ - { - status: "Done", - title: "All BLOCKed by graduate paper.", - closed: true, - color: "green", - }, - { - status: "Done", - title: "Slogan is not completely visible on phone.", - closed: true, - color: "green", - }, - { - status: "Todo", - title: "Users api.", - closed: false, - color: "blue", - }, - { - status: "Todo", - title: "Secrets api.", - closed: false, - color: "blue", - }, - { - status: "Todo", - title: "Refactor aio python scripts.", - closed: false, - color: "blue", - }, - { - status: "Todo", - title: "Nginx path redirection.", - closed: false, - color: "blue", - }, - { - status: "Todo", - title: "Make services optional.", - closed: false, - color: "blue", - }, - { - status: "Done", - title: "Optimize code-server.", - closed: true, - color: "green", - }, - { - status: "Todo", - title: "No more alpine.", - closed: false, - color: "blue", - }, - { - status: "Done", - title: "No netease music.", - closed: true, - color: "green", - }, - { - status: "Done", - title: "Draft issue status in www TODOs.", - closed: true, - color: "green", - }, - { - status: "Done", - title: "Re-bootstrap front end.", - closed: true, - color: "green", - }, - { - status: "Todo", - title: "Clean react imports for new jsx usage.", - closed: false, - color: "blue", - }, - { - status: "Done", - title: "i18next backend bug.", - closed: true, - color: "green", - }, - { - status: "Done", - title: "Organize buttons.", - closed: true, - color: "green", - }, - { - status: "Done", - title: "Fix dialog typo.", - closed: true, - color: "green", - }, - { - status: "Todo", - title: "Organize OperationDialog.", - closed: false, - color: "blue", - }, - { - status: "Todo", - title: "New palette api.", - closed: false, - color: "blue", - }, - { - status: "Todo", - title: "No Docker!!!", - closed: false, - color: "blue", - }, - { - status: "Done", - title: "Improve animation of slogan.", - closed: true, - color: "green", - }, -]; - -export default todos; diff --git a/docker/crupest-nginx/sites/www/src/style.css b/docker/crupest-nginx/sites/www/src/style.css deleted file mode 100644 index 1f9c9ed..0000000 --- a/docker/crupest-nginx/sites/www/src/style.css +++ /dev/null @@ -1,185 +0,0 @@ -html { - width: 100%; - line-height: 1.5; - font-family: ui-sans-serif; -} - -body { - width: 100%; - margin: 0; - box-sizing: border-box; -} - -a.mono { - font-family: ui-monospace; -} - -.h-note { - font-size: 0.6em; - color: gray; -} - -@keyframes article-enter { - from { - opacity: 0; - transform: translateY(100px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -:root { - --main-article-horizontal-padding: 1em; - --main-article-horizontal-margin-shrink: -1em; - - --im-me: deepskyblue; - --im-happy: dodgerblue; - --im-angry: orangered; - --im-good: hsl(120, 85%, 85%); - --im-active: hsl(20, 85%, 85%); -} - -@media (min-width: 576px) { - :root { - --main-article-horizontal-padding: 2em; - --main-article-horizontal-margin-shrink: -2em; - } -} - -#main-article { - padding: 0 var(--main-article-horizontal-padding); - animation: article-enter 1s; -} - -#title { - font-size: 2em; -} - -@keyframes title-name-enter { - from { - background-color: white; - } - - to { - background-color: var(--im-me); - } -} - -#title-name { - font-family: ui-monospace; - border-radius: 8px; - background-color: white; - animation: title-name-enter 3s 2s forwards; -} - -@keyframes avatar-enter { - from { - opacity: 0; - transform: translateX(calc(100% + var(--main-article-horizontal-padding))); - } - - to { - opacity: 1; - transform: translateX(0); - } -} - -#avatar { - float: right; - transform: translateX(calc(100% + var(--main-article-horizontal-padding))); - animation: avatar-enter 0.5s 1s forwards; -} - -.slogan-container { - width: 100%; - position: fixed; - z-index: 1; - top: 0; -} - -.slogan { - width: 100%; - padding: 0.5em 1em; - text-align: center; - box-sizing: border-box; - position: absolute; - transform: translateY(-100%); - transition: transform 1s; -} - -.slogan.happy { - background-color: var(--im-happy); -} - -.slogan.angry { - background-color: var(--im-angry); -} - -.slogan-container[data-slogan-emotion="happy"] .slogan.happy { - transform: translateY(0); -} - -.slogan-container[data-slogan-emotion="angry"] .slogan.angry { - transform: translateY(0); -} - -.slogan-text { - display: inline-block; - text-align: initial; - color: white; - font-size: 1.2em; -} - -#todo-container { - list-style: none; - margin-block: 0; - padding-inline: 0; -} - -@keyframes todo-enter { - from { - opacity: 0; - transform: translateX(-100%); - } - - to { - opacity: 1; - transform: translateX(0); - } -} - -#todo-container li { - margin: 0 var(--main-article-horizontal-margin-shrink); - padding: 0.25em 3em; - transform: translateX(-100%); - animation: todo-enter 1s forwards; -} - -#todo-container li[data-status="closed"] { - background-color: var(--im-good); -} - -#todo-container li[data-status="open"] { - background-color: var(--im-active); -} - -.friend-link { - display: inline-block; -} - -.friend-img { - display: block; - width: 80px; - height: 80px; - object-fit: cover; - border-radius: 50%; -} - -.friend-name { - display: block; - text-align: center; - font-size: 1.2em; -} diff --git a/docker/crupest-nginx/sites/www/src/todos.ts b/docker/crupest-nginx/sites/www/src/todos.ts deleted file mode 100644 index b69f524..0000000 --- a/docker/crupest-nginx/sites/www/src/todos.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface Todo { - status: string; - title: string; - closed: boolean; - color: string; -} - -export async function fetchTodos(): Promise { - console.log("Try to fetch TODOs from server."); - - if (process.env.NODE_ENV !== "production") { - console.log("YaY! We are developers. 🍻 Use mock TODOs. (After 2s)"); - // await new Promise((resolve) => setTimeout(resolve, 2000)); - return (await import("./mock-todos")).default; - } else { - const res = await fetch("/api/todos"); - const body: Todo[] = await res.json(); - - if (res.status !== 200) { - console.error( - `Failed to get TODOs. Status: ${res.status}. Body: ${body}`, - ); - throw new Error( - "Failed to fetch TODOs. (Maybe due to rate limit. Please try later.)", - ); - } - return body; - } -} diff --git a/docker/crupest-nginx/sites/www/tsconfig.json b/docker/crupest-nginx/sites/www/tsconfig.json deleted file mode 100644 index 9d1434c..0000000 --- a/docker/crupest-nginx/sites/www/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "@tsconfig/recommended/tsconfig.json", - "compilerOptions": { - "lib": [ - "ESNext", - "DOM", - "DOM.Iterable" - ], - "types": [ - "parcel-env" - ], - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true - } -} \ No newline at end of file diff --git a/docker/crupest-v2ray/Dockerfile b/docker/crupest-v2ray/Dockerfile deleted file mode 100644 index 003b545..0000000 --- a/docker/crupest-v2ray/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM alpine:edge - -RUN apk add --no-cache v2ray - -ENTRYPOINT [ "/usr/bin/v2ray" ] - diff --git a/docker/debian-dev/Dockerfile b/docker/debian-dev/Dockerfile new file mode 100644 index 0000000..95f0602 --- /dev/null +++ b/docker/debian-dev/Dockerfile @@ -0,0 +1,21 @@ +FROM debian:latest + +ARG USER=crupest +ARG IN_CHINA= +ARG CODE_SERVER=true + +ENV CRUPEST_DEBIAN_DEV_USER=${USER} +ENV CRUPEST_DEBIAN_DEV_IN_CHINA=${IN_CHINA} +ENV CRUPEST_DEBIAN_DEV_SETUP_CODE_SERVER=${CODE_SERVER} + +ADD bootstrap /bootstrap + +RUN /bootstrap/setup.bash +ENV LANG=en_US.utf8 + +USER ${USER} +WORKDIR /home/${USER} + +EXPOSE 8080 +VOLUME [ "/data", "/home/${USER}" ] +CMD [ "bash", "-l" ] diff --git a/docker/debian-dev/bootstrap/apt-source/11/add-deb-src.bash b/docker/debian-dev/bootstrap/apt-source/11/add-deb-src.bash new file mode 100644 index 0000000..e134a00 --- /dev/null +++ b/docker/debian-dev/bootstrap/apt-source/11/add-deb-src.bash @@ -0,0 +1,14 @@ +#! /usr/bin/env bash + +set -e + +dir=$(dirname "$0") +domain=$("$dir/get-domain.bash") + +cat <> /etc/apt/sources.list + +deb-src https://$domain/debian/ bullseye main +deb-src https://$domain/debian-security/ bullseye-security main +deb-src https://$domain/debian-updates/ bullseye-updates main + +EOF diff --git a/docker/debian-dev/bootstrap/apt-source/11/get-domain.bash b/docker/debian-dev/bootstrap/apt-source/11/get-domain.bash new file mode 100644 index 0000000..d44ea65 --- /dev/null +++ b/docker/debian-dev/bootstrap/apt-source/11/get-domain.bash @@ -0,0 +1,5 @@ +#! /usr/bin/env bash + +set -e + +sed "s|.*https\?://\([-_.a-zA-Z0-9]\+\)/.*|\\1|;q" /etc/apt/sources.list diff --git a/docker/debian-dev/bootstrap/apt-source/11/replace-domain.bash b/docker/debian-dev/bootstrap/apt-source/11/replace-domain.bash new file mode 100644 index 0000000..86e88dc --- /dev/null +++ b/docker/debian-dev/bootstrap/apt-source/11/replace-domain.bash @@ -0,0 +1,7 @@ +#! /usr/bin/env bash + +set -e + +echo "Backup /etc/apt/sources.list to /etc/apt/sources.list.bak." +echo "Replace source domain in /etc/apt/sources.list to $1." +sed -i.bak "s|\(https\?://\)[-_.a-zA-Z0-9]\+/|\\1$1/|" /etc/apt/sources.list diff --git a/docker/debian-dev/bootstrap/apt-source/11/replace-http.bash b/docker/debian-dev/bootstrap/apt-source/11/replace-http.bash new file mode 100644 index 0000000..fae082a --- /dev/null +++ b/docker/debian-dev/bootstrap/apt-source/11/replace-http.bash @@ -0,0 +1,7 @@ +#! /usr/bin/env bash + +set -e + +echo "Backup /etc/apt/sources.list to /etc/apt/sources.list.bak." +echo "Replace http to https in /etc/apt/sources.list." +sed -i.bak 's/https\?/https/' /etc/apt/sources.list diff --git a/docker/debian-dev/bootstrap/apt-source/12/add-deb-src.bash b/docker/debian-dev/bootstrap/apt-source/12/add-deb-src.bash new file mode 100644 index 0000000..cf741d6 --- /dev/null +++ b/docker/debian-dev/bootstrap/apt-source/12/add-deb-src.bash @@ -0,0 +1,22 @@ +#! /usr/bin/env bash + +set -e + +dir=$(dirname "$0") +domain=$("$dir/get-domain.bash") + +cat <> /etc/apt/sources.list.d/debian.sources + +Types: deb-src +URIs: https://$domain/debian +Suites: bookworm bookworm-updates +Components: main +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg + +Types: deb-src +URIs: https://$domain/debian-security +Suites: bookworm-security +Components: main +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg + +EOF \ No newline at end of file diff --git a/docker/debian-dev/bootstrap/apt-source/12/get-domain.bash b/docker/debian-dev/bootstrap/apt-source/12/get-domain.bash new file mode 100644 index 0000000..a24538c --- /dev/null +++ b/docker/debian-dev/bootstrap/apt-source/12/get-domain.bash @@ -0,0 +1,6 @@ +#! /usr/bin/env bash + +set -e + +grep -e 'URIs:' /etc/apt/sources.list.d/debian.sources | \ + sed -E 's|URIs:\s*https?://([-_.a-zA-Z0-9]+)/.*|\1|;q' diff --git a/docker/debian-dev/bootstrap/apt-source/12/replace-domain.bash b/docker/debian-dev/bootstrap/apt-source/12/replace-domain.bash new file mode 100644 index 0000000..d55307c --- /dev/null +++ b/docker/debian-dev/bootstrap/apt-source/12/replace-domain.bash @@ -0,0 +1,7 @@ +#! /usr/bin/env bash + +set -e + +echo "Backup /etc/apt/sources.list.d/debian.sources to /etc/apt/sources.list.d/debian.sources.bak." +echo "Replace source domain in /etc/apt/sources.list.d/debian.sources to $1." +sed -i.bak -E "s|(URIs:\\s*https?://)[-_.a-zA-Z0-9]+(/.*)|\\1$1\\2|" /etc/apt/sources.list.d/debian.sources diff --git a/docker/debian-dev/bootstrap/apt-source/12/replace-http.bash b/docker/debian-dev/bootstrap/apt-source/12/replace-http.bash new file mode 100644 index 0000000..ed4391d --- /dev/null +++ b/docker/debian-dev/bootstrap/apt-source/12/replace-http.bash @@ -0,0 +1,7 @@ +#! /usr/bin/env bash + +set -e + +echo "Backup /etc/apt/sources.list to /etc/apt/sources.list.d/debian.sources.bak." +echo "Replace http to https in /etc/apt/sources.list.d/debian.sources." +sed -i.bak -E "s|(URIs:\\s*)https?(://[-_.a-zA-Z0-9]+/.*)|\\1https\\2|" /etc/apt/sources.list.d/debian.sources diff --git a/docker/debian-dev/bootstrap/apt-source/china-source.txt b/docker/debian-dev/bootstrap/apt-source/china-source.txt new file mode 100644 index 0000000..4312686 --- /dev/null +++ b/docker/debian-dev/bootstrap/apt-source/china-source.txt @@ -0,0 +1 @@ +mirrors.tuna.tsinghua.edu.cn \ No newline at end of file diff --git a/docker/debian-dev/bootstrap/apt-source/install-apt-https.bash b/docker/debian-dev/bootstrap/apt-source/install-apt-https.bash new file mode 100644 index 0000000..70fb371 --- /dev/null +++ b/docker/debian-dev/bootstrap/apt-source/install-apt-https.bash @@ -0,0 +1,8 @@ +#! /usr/bin/env bash + +set -e + +echo "Install apt https transport." +apt-get update +apt-get install -y apt-utils +apt-get install -y apt-transport-https ca-certificates diff --git a/docker/debian-dev/bootstrap/apt-source/setup.bash b/docker/debian-dev/bootstrap/apt-source/setup.bash new file mode 100644 index 0000000..cdf68af --- /dev/null +++ b/docker/debian-dev/bootstrap/apt-source/setup.bash @@ -0,0 +1,34 @@ +#! /usr/bin/env bash + +set -e + +dir=/bootstrap/apt-source + +echo "Getting debian version..." +debian_version=$("$dir/../get-debian-version.bash") + +if [[ -z $debian_version ]]; then + echo "Debian version not found." + exit 1 +else + echo "Debian version: $debian_version" +fi + +if [[ $debian_version -ge 12 ]]; then + setup_dir=$dir/12 +else + setup_dir=$dir/11 +fi + +echo "Setting up apt source..." + +if [[ -n $CRUPEST_DEBIAN_DEV_IN_CHINA ]]; then + echo "In China, using China source..." + "$setup_dir/replace-domain.bash" "$(cat "$dir/china-source.txt")" +fi + +"$dir/install-apt-https.bash" +"$setup_dir/replace-http.bash" +"$setup_dir/add-deb-src.bash" + +echo "Setting up apt source done." diff --git a/docker/debian-dev/bootstrap/bash/bash-completion.bash b/docker/debian-dev/bootstrap/bash/bash-completion.bash new file mode 100644 index 0000000..75f8333 --- /dev/null +++ b/docker/debian-dev/bootstrap/bash/bash-completion.bash @@ -0,0 +1,4 @@ +if [ -f /etc/bash_completion ]; then + . /etc/bash_completion +fi + diff --git a/docker/debian-dev/bootstrap/bash/code-server.bash b/docker/debian-dev/bootstrap/bash/code-server.bash new file mode 100644 index 0000000..255c280 --- /dev/null +++ b/docker/debian-dev/bootstrap/bash/code-server.bash @@ -0,0 +1,2 @@ +mkdir -p ~/.local/share/code-server +/bootstrap/start/code-server.bash > ~/.local/share/code-server/log 2> ~/.local/share/code-server/error & diff --git a/docker/debian-dev/bootstrap/bash/dquilt.bash b/docker/debian-dev/bootstrap/bash/dquilt.bash new file mode 100644 index 0000000..96a4eb2 --- /dev/null +++ b/docker/debian-dev/bootstrap/bash/dquilt.bash @@ -0,0 +1,4 @@ +alias dquilt="quilt --quiltrc=${HOME}/.quiltrc-dpkg" +. /usr/share/bash-completion/completions/quilt +complete -F _quilt_completion $_quilt_complete_opt dquilt + diff --git a/docker/debian-dev/bootstrap/extra/setup-cmake.bash b/docker/debian-dev/bootstrap/extra/setup-cmake.bash new file mode 100644 index 0000000..76c1ae4 --- /dev/null +++ b/docker/debian-dev/bootstrap/extra/setup-cmake.bash @@ -0,0 +1,9 @@ +#! /usr/bin/env bash + +set -e + +CMAKE_VERSION=$(curl -s https://api.github.com/repos/Kitware/CMake/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') +wget -O cmake-installer.sh https://github.com/Kitware/CMake/releases/download/v"$CMAKE_VERSION"/cmake-"$CMAKE_VERSION"-linux-x86_64.sh +chmod +x cmake-installer.sh +./cmake-installer.sh --skip-license --prefix=/usr +rm cmake-installer.sh diff --git a/docker/debian-dev/bootstrap/extra/setup-dotnet.bash b/docker/debian-dev/bootstrap/extra/setup-dotnet.bash new file mode 100644 index 0000000..0ef7743 --- /dev/null +++ b/docker/debian-dev/bootstrap/extra/setup-dotnet.bash @@ -0,0 +1,10 @@ +#! /usr/bin/env bash + +set -e + +wget https://packages.microsoft.com/config/debian/11/packages-microsoft-prod.deb -O packages-microsoft-prod.deb +dpkg -i packages-microsoft-prod.deb +rm packages-microsoft-prod.deb + +apt-get update +apt-get install -y dotnet-sdk-7.0 diff --git a/docker/debian-dev/bootstrap/extra/setup-llvm.bash b/docker/debian-dev/bootstrap/extra/setup-llvm.bash new file mode 100644 index 0000000..48dde86 --- /dev/null +++ b/docker/debian-dev/bootstrap/extra/setup-llvm.bash @@ -0,0 +1,26 @@ +#! /usr/bin/env bash + +set -e + +LLVM_VERSION=18 + +. /bootstrap/func.bash + +if is_true "$CRUPEST_DEBIAN_DEV_IN_CHINA"; then + base_url=https://mirrors.tuna.tsinghua.edu.cn/llvm-apt +else + base_url=https://apt.llvm.org +fi + +wget "$base_url/llvm.sh" +chmod +x llvm.sh +./llvm.sh $LLVM_VERSION all -m "$base_url" +rm llvm.sh + +update-alternatives --install /usr/bin/clang clang /usr/bin/clang-$LLVM_VERSION 100 \ + --slave /usr/bin/clang++ clang++ /usr/bin/clang++-$LLVM_VERSION \ + --slave /usr/bin/clangd clangd /usr/bin/clangd-$LLVM_VERSION \ + --slave /usr/bin/clang-format clang-format /usr/bin/clang-format-$LLVM_VERSION \ + --slave /usr/bin/clang-tidy clang-tidy /usr/bin/clang-tidy-$LLVM_VERSION \ + --slave /usr/bin/lldb lldb /usr/bin/lldb-$LLVM_VERSION \ + --slave /usr/bin/lld lld /usr/bin/lld-$LLVM_VERSION diff --git a/docker/debian-dev/bootstrap/func.bash b/docker/debian-dev/bootstrap/func.bash new file mode 100644 index 0000000..7782035 --- /dev/null +++ b/docker/debian-dev/bootstrap/func.bash @@ -0,0 +1,19 @@ +is_true() { + if [[ "$1" =~ 1|on|true ]]; then + return 0 + else + return 1 + fi +} + +append-bash-profile() { + cat "/bootstrap/bash/$1" >> /home/$CRUPEST_DEBIAN_DEV_USER/.bash_profile +} + +append-bashrc() { + cat "/bootstrap/bash/$1" >> /home/$CRUPEST_DEBIAN_DEV_USER/.bashrc +} + +copy-home-dot-file() { + cp "/bootstrap/home-dot/$1" "/home/$CRUPEST_DEBIAN_DEV_USER/.$1" +} diff --git a/docker/debian-dev/bootstrap/get-debian-version.bash b/docker/debian-dev/bootstrap/get-debian-version.bash new file mode 100644 index 0000000..2cc10b9 --- /dev/null +++ b/docker/debian-dev/bootstrap/get-debian-version.bash @@ -0,0 +1,13 @@ +#! /usr/bin/env bash + +set -e + +if [ -f /etc/os-release ]; then + . /etc/os-release + if [ "$ID" = "debian" ]; then + echo "$VERSION_ID" + exit 0 + fi +fi + +exit 1 diff --git a/docker/debian-dev/bootstrap/home-dot/devscripts b/docker/debian-dev/bootstrap/home-dot/devscripts new file mode 100644 index 0000000..a15b041 --- /dev/null +++ b/docker/debian-dev/bootstrap/home-dot/devscripts @@ -0,0 +1 @@ +export DGET_VERIFY=no \ No newline at end of file diff --git a/docker/debian-dev/bootstrap/home-dot/quiltrc-dpkg b/docker/debian-dev/bootstrap/home-dot/quiltrc-dpkg new file mode 100644 index 0000000..e8fc3c5 --- /dev/null +++ b/docker/debian-dev/bootstrap/home-dot/quiltrc-dpkg @@ -0,0 +1,13 @@ +d=. +while [ ! -d $d/debian -a `readlink -e $d` != / ]; + do d=$d/..; done +if [ -d $d/debian ] && [ -z $QUILT_PATCHES ]; then + # if in Debian packaging tree with unset $QUILT_PATCHES + QUILT_PATCHES="debian/patches" + QUILT_PATCH_OPTS="--reject-format=unified" + QUILT_DIFF_ARGS="-p ab --no-timestamps --no-index --color=auto" + QUILT_REFRESH_ARGS="-p ab --no-timestamps --no-index" + QUILT_COLORS="diff_hdr=1;32:diff_add=1;34:diff_rem=1;31:diff_hunk=1;33:" + QUILT_COLORS="${QUILT_COLORS}diff_ctx=35:diff_cctx=33" + if ! [ -d $d/debian/patches ]; then mkdir $d/debian/patches; fi +fi diff --git a/docker/debian-dev/bootstrap/setup-base.bash b/docker/debian-dev/bootstrap/setup-base.bash new file mode 100644 index 0000000..31ded36 --- /dev/null +++ b/docker/debian-dev/bootstrap/setup-base.bash @@ -0,0 +1,25 @@ +#! /usr/bin/env bash + +set -e + +. /bootstrap/func.bash + +echo "Setting up basic system function..." + +echo "Installing basic packages..." +apt-get install -y apt-utils +apt-get install -y locales procps vim less man bash-completion software-properties-common rsync curl wget +echo "Installing basic packages done." + +echo "Setting up locale..." +localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 +echo "Setting up locale done." + +echo "Creating data dir..." +mkdir -p /data +chown $CRUPEST_DEBIAN_DEV_USER:$CRUPEST_DEBIAN_DEV_USER /data +echo "Creating data dir done." + +append-bashrc bash-completion.bash + +echo "Setting up basic system function done." diff --git a/docker/debian-dev/bootstrap/setup-code-server.bash b/docker/debian-dev/bootstrap/setup-code-server.bash new file mode 100644 index 0000000..34c9697 --- /dev/null +++ b/docker/debian-dev/bootstrap/setup-code-server.bash @@ -0,0 +1,28 @@ +#! /usr/bin/env bash + +set -e + +. /bootstrap/func.bash + +echo "Setting up code server..." + +echo "Get latest version of code-server..." +VERSION=$(curl -s https://api.github.com/repos/coder/code-server/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') +echo "Current latest version of code-server is $VERSION" + +echo "Downloading code-server..." +url="https://github.com/coder/code-server/releases/download/v${VERSION}/code-server_${VERSION}_amd64.deb" +curl -sSfOL "$url" +echo "Downloading code-server done." + +echo "Installing code-server..." +apt-get install -y "./code-server_${VERSION}_amd64.deb" +echo "Installing code-server done." + +echo "Cleaning up deb..." +rm "code-server_${VERSION}_amd64.deb" +echo "Cleaning up deb done." + +append-bash-profile code-server.bash + +echo "Setting up code server done." diff --git a/docker/debian-dev/bootstrap/setup-dev.bash b/docker/debian-dev/bootstrap/setup-dev.bash new file mode 100644 index 0000000..92deacb --- /dev/null +++ b/docker/debian-dev/bootstrap/setup-dev.bash @@ -0,0 +1,18 @@ +#! /usr/bin/env bash + +set -e + +. /bootstrap/func.bash + +echo "Setting up dev function..." + +echo "Installing dev packages..." +apt-get install -y build-essential git devscripts debhelper quilt +apt-get build-dep -y linux +echo "Installing dev packages done." + +append-bashrc dquilt.bash +copy-home-dot-file devscripts +copy-home-dot-file quiltrc-dpkg + +echo "Setting up dev function done." diff --git a/docker/debian-dev/bootstrap/setup-user.bash b/docker/debian-dev/bootstrap/setup-user.bash new file mode 100644 index 0000000..f74dcdb --- /dev/null +++ b/docker/debian-dev/bootstrap/setup-user.bash @@ -0,0 +1,20 @@ +#! /usr/bin/env bash + +set -e + +echo "Setting up user..." + +echo "Installing sudo..." +apt-get install -y sudo +echo "Installing sudo done." + +echo "Setting up sudo..." +sed -i.bak 's|%sudo[[:space:]]\+ALL=(ALL:ALL)[[:space:]]\+ALL|%sudo ALL=(ALL:ALL) NOPASSWD: ALL|' /etc/sudoers +echo "Setting up sudo done." + +echo "Adding user $CRUPEST_DEBIAN_DEV_USER ..." +useradd -m -G sudo -s /usr/bin/bash "$CRUPEST_DEBIAN_DEV_USER" +echo "Adding user done." + +echo "Setting up user done." + diff --git a/docker/debian-dev/bootstrap/setup.bash b/docker/debian-dev/bootstrap/setup.bash new file mode 100644 index 0000000..09b8137 --- /dev/null +++ b/docker/debian-dev/bootstrap/setup.bash @@ -0,0 +1,30 @@ +#! /usr/bin/env bash + +set -e + +export DEBIAN_FRONTEND=noninteractive + +echo "Setting up crupest-debian-dev..." + +. /bootstrap/func.bash + +/bootstrap/apt-source/setup.bash + +echo "Updating apt source index..." +apt-get update +echo "Updating apt source index done." + +/bootstrap/setup-user.bash +/bootstrap/setup-base.bash +/bootstrap/setup-dev.bash + +if is_true "$CRUPEST_DEBIAN_DEV_SETUP_CODE_SERVER"; then + echo "CRUPEST_DEBIAN_DEV_SETUP_CODE_SERVER is true, setting up code-server..." + /bootstrap/setup-code-server.bash +fi + +echo "Cleaning up apt source index..." +rm -rf /var/lib/apt/lists/* +echo "Cleaning up apt source index done." + +echo "Setting up crupest-debian-dev done." diff --git a/docker/debian-dev/bootstrap/start/code-server.bash b/docker/debian-dev/bootstrap/start/code-server.bash new file mode 100644 index 0000000..7dfc0e9 --- /dev/null +++ b/docker/debian-dev/bootstrap/start/code-server.bash @@ -0,0 +1,18 @@ +#! /usr/bin/env bash + +export CODE_SERVER_CONFIG="/data/code-server-config.yaml" + +CODE_SERVER_PROGRAM=code-server +CODE_SERVER_PORT=8080 + +if which "$CODE_SERVER_PROGRAM" > /dev/null 2>&1; then + if ! pgrep -x "$CODE_SERVER_PROGRAM" > /dev/null 2>&1; then + echo "code-server is not running, starting..." + "$CODE_SERVER_PROGRAM" "--bind-addr" "0.0.0.0:$CODE_SERVER_PORT" + else + echo "code-server is already running." + fi +else + echo "code-server not found, skipping code-server setup." >&2 + exit 1 +fi diff --git a/docker/debian-dev/bootstrap/wait.bash b/docker/debian-dev/bootstrap/wait.bash new file mode 100644 index 0000000..501c706 --- /dev/null +++ b/docker/debian-dev/bootstrap/wait.bash @@ -0,0 +1,5 @@ +#! /usr/bin/env bash + +set -e + +tail -f /dev/null diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile new file mode 100644 index 0000000..6d0400b --- /dev/null +++ b/docker/nginx/Dockerfile @@ -0,0 +1,8 @@ +FROM node:lts AS build-www +RUN npm install -g pnpm +COPY sites/www /sites/www +WORKDIR /sites/www +RUN pnpm install --frozen-lockfile && pnpm run build + +FROM nginx:mainline +COPY --from=build-www /sites/www/dist /srv/www diff --git a/docker/nginx/sites/www/.dockerignore b/docker/nginx/sites/www/.dockerignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/docker/nginx/sites/www/.dockerignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/docker/nginx/sites/www/.gitignore b/docker/nginx/sites/www/.gitignore new file mode 100644 index 0000000..0b1e50b --- /dev/null +++ b/docker/nginx/sites/www/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.parcel-cache diff --git a/docker/nginx/sites/www/.proxyrc.json b/docker/nginx/sites/www/.proxyrc.json new file mode 100644 index 0000000..8fe13c0 --- /dev/null +++ b/docker/nginx/sites/www/.proxyrc.json @@ -0,0 +1,6 @@ +{ + "/api": { + "target": "http://localhost:5188", + "changeOrigin": true + } +} diff --git a/docker/nginx/sites/www/avatar.png b/docker/nginx/sites/www/avatar.png new file mode 100644 index 0000000..d890d8d Binary files /dev/null and b/docker/nginx/sites/www/avatar.png differ diff --git a/docker/nginx/sites/www/favicon.ico b/docker/nginx/sites/www/favicon.ico new file mode 100644 index 0000000..922a523 Binary files /dev/null and b/docker/nginx/sites/www/favicon.ico differ diff --git a/docker/nginx/sites/www/index.html b/docker/nginx/sites/www/index.html new file mode 100644 index 0000000..03485c5 --- /dev/null +++ b/docker/nginx/sites/www/index.html @@ -0,0 +1,95 @@ + + + + + + + + + crupest + + + +
+
+ 🙃The world is full of pain, but we can fix it with love! +
+
+ 😡The world is a piece of shit, so let's make it a little better! +
+
+
+ +

Hello! This is crupest.

+
+

Welcome to my home page! I'm very glad to meet you here.

+

If you have something interesting to share with me, feel free to reach me via email at crupest@outlook.com or, of course, I@crupest.life. You can also fire an issue in + any repo on GitHub. Here is the link to my GitHub profile, https://github.com/crupest.

+

Currently this page is hosted on my own server and my own apex domain (crupest.life). You can also share this + website link, aka, https://crupest.life.

+
+
+

TODOs of me grabbed from my Github + project

+

Fetching...

+
    +
+
+
+

Friends of me:

+ +
+
+

This is just the home page. I also run some other services on my server under this domain:

+

Public:

+ +

Private:

+ +

If you wish to deploy similar services like me, you are in the right place. Take a look at https://github.com/crupest/crupest and there is + all you need to start with. Also, contact me if you run into some problem.

+
+ +
+ + + + \ No newline at end of file diff --git a/docker/nginx/sites/www/package.json b/docker/nginx/sites/www/package.json new file mode 100644 index 0000000..c42bb29 --- /dev/null +++ b/docker/nginx/sites/www/package.json @@ -0,0 +1,17 @@ +{ + "name": "crupest-www", + "private": true, + "version": "0.1.0", + "source": "index.html", + "scripts": { + "start": "parcel", + "build": "tsc && parcel build" + }, + "devDependencies": { + "@tsconfig/recommended": "^1.0.7", + "@types/parcel-env": "^0.0.6", + "parcel": "^2.12.0", + "prettier": "^3.3.3", + "typescript": "^5.5.4" + } +} \ No newline at end of file diff --git a/docker/nginx/sites/www/pnpm-lock.yaml b/docker/nginx/sites/www/pnpm-lock.yaml new file mode 100644 index 0000000..aa83a70 --- /dev/null +++ b/docker/nginx/sites/www/pnpm-lock.yaml @@ -0,0 +1,2099 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@tsconfig/recommended': + specifier: ^1.0.7 + version: 1.0.7 + '@types/parcel-env': + specifier: ^0.0.6 + version: 0.0.6 + parcel: + specifier: ^2.12.0 + version: 2.12.0(@swc/helpers@0.5.12)(typescript@5.5.4) + prettier: + specifier: ^3.3.3 + version: 3.3.3 + typescript: + specifier: ^5.5.4 + version: 5.5.4 + +packages: + + '@babel/code-frame@7.24.7': + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.24.7': + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} + + '@lezer/common@1.2.1': + resolution: {integrity: sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==} + + '@lezer/lr@1.4.2': + resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==} + + '@lmdb/lmdb-darwin-arm64@2.8.5': + resolution: {integrity: sha512-KPDeVScZgA1oq0CiPBcOa3kHIqU+pTOwRFDIhxvmf8CTNvqdZQYp5cCKW0bUk69VygB2PuTiINFWbY78aR2pQw==} + cpu: [arm64] + os: [darwin] + + '@lmdb/lmdb-darwin-x64@2.8.5': + resolution: {integrity: sha512-w/sLhN4T7MW1nB3R/U8WK5BgQLz904wh+/SmA2jD8NnF7BLLoUgflCNxOeSPOWp8geP6nP/+VjWzZVip7rZ1ug==} + cpu: [x64] + os: [darwin] + + '@lmdb/lmdb-linux-arm64@2.8.5': + resolution: {integrity: sha512-vtbZRHH5UDlL01TT5jB576Zox3+hdyogvpcbvVJlmU5PdL3c5V7cj1EODdh1CHPksRl+cws/58ugEHi8bcj4Ww==} + cpu: [arm64] + os: [linux] + + '@lmdb/lmdb-linux-arm@2.8.5': + resolution: {integrity: sha512-c0TGMbm2M55pwTDIfkDLB6BpIsgxV4PjYck2HiOX+cy/JWiBXz32lYbarPqejKs9Flm7YVAKSILUducU9g2RVg==} + cpu: [arm] + os: [linux] + + '@lmdb/lmdb-linux-x64@2.8.5': + resolution: {integrity: sha512-Xkc8IUx9aEhP0zvgeKy7IQ3ReX2N8N1L0WPcQwnZweWmOuKfwpS3GRIYqLtK5za/w3E60zhFfNdS+3pBZPytqQ==} + cpu: [x64] + os: [linux] + + '@lmdb/lmdb-win32-x64@2.8.5': + resolution: {integrity: sha512-4wvrf5BgnR8RpogHhtpCPJMKBmvyZPhhUtEwMJbXh0ni2BucpfF07jlmyM11zRqQ2XIq6PbC2j7W7UCCcm1rRQ==} + cpu: [x64] + os: [win32] + + '@mischnic/json-sourcemap@0.1.1': + resolution: {integrity: sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w==} + engines: {node: '>=12.0.0'} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + + '@parcel/bundler-default@2.12.0': + resolution: {integrity: sha512-3ybN74oYNMKyjD6V20c9Gerdbh7teeNvVMwIoHIQMzuIFT6IGX53PyOLlOKRLbjxMc0TMimQQxIt2eQqxR5LsA==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/cache@2.12.0': + resolution: {integrity: sha512-FX5ZpTEkxvq/yvWklRHDESVRz+c7sLTXgFuzz6uEnBcXV38j6dMSikflNpHA6q/L4GKkCqRywm9R6XQwhwIMyw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@parcel/core': ^2.12.0 + + '@parcel/codeframe@2.12.0': + resolution: {integrity: sha512-v2VmneILFiHZJTxPiR7GEF1wey1/IXPdZMcUlNXBiPZyWDfcuNgGGVQkx/xW561rULLIvDPharOMdxz5oHOKQg==} + engines: {node: '>= 12.0.0'} + + '@parcel/compressor-raw@2.12.0': + resolution: {integrity: sha512-h41Q3X7ZAQ9wbQ2csP8QGrwepasLZdXiuEdpUryDce6rF9ZiHoJ97MRpdLxOhOPyASTw/xDgE1xyaPQr0Q3f5A==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/config-default@2.12.0': + resolution: {integrity: sha512-dPNe2n9eEsKRc1soWIY0yToMUPirPIa2QhxcCB3Z5RjpDGIXm0pds+BaiqY6uGLEEzsjhRO0ujd4v2Rmm0vuFg==} + peerDependencies: + '@parcel/core': ^2.12.0 + + '@parcel/core@2.12.0': + resolution: {integrity: sha512-s+6pwEj+GfKf7vqGUzN9iSEPueUssCCQrCBUlcAfKrJe0a22hTUCjewpB0I7lNrCIULt8dkndD+sMdOrXsRl6Q==} + engines: {node: '>= 12.0.0'} + + '@parcel/diagnostic@2.12.0': + resolution: {integrity: sha512-8f1NOsSFK+F4AwFCKynyIu9Kr/uWHC+SywAv4oS6Bv3Acig0gtwUjugk0C9UaB8ztBZiW5TQZhw+uPZn9T/lJA==} + engines: {node: '>= 12.0.0'} + + '@parcel/events@2.12.0': + resolution: {integrity: sha512-nmAAEIKLjW1kB2cUbCYSmZOGbnGj8wCzhqnK727zCCWaA25ogzAtt657GPOeFyqW77KyosU728Tl63Fc8hphIA==} + engines: {node: '>= 12.0.0'} + + '@parcel/fs@2.12.0': + resolution: {integrity: sha512-NnFkuvou1YBtPOhTdZr44WN7I60cGyly2wpHzqRl62yhObyi1KvW0SjwOMa0QGNcBOIzp4G0CapoZ93hD0RG5Q==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@parcel/core': ^2.12.0 + + '@parcel/graph@3.2.0': + resolution: {integrity: sha512-xlrmCPqy58D4Fg5umV7bpwDx5Vyt7MlnQPxW68vae5+BA4GSWetfZt+Cs5dtotMG2oCHzZxhIPt7YZ7NRyQzLA==} + engines: {node: '>= 12.0.0'} + + '@parcel/logger@2.12.0': + resolution: {integrity: sha512-cJ7Paqa7/9VJ7C+KwgJlwMqTQBOjjn71FbKk0G07hydUEBISU2aDfmc/52o60ErL9l+vXB26zTrIBanbxS8rVg==} + engines: {node: '>= 12.0.0'} + + '@parcel/markdown-ansi@2.12.0': + resolution: {integrity: sha512-WZz3rzL8k0H3WR4qTHX6Ic8DlEs17keO9gtD4MNGyMNQbqQEvQ61lWJaIH0nAtgEetu0SOITiVqdZrb8zx/M7w==} + engines: {node: '>= 12.0.0'} + + '@parcel/namer-default@2.12.0': + resolution: {integrity: sha512-9DNKPDHWgMnMtqqZIMiEj/R9PNWW16lpnlHjwK3ciRlMPgjPJ8+UNc255teZODhX0T17GOzPdGbU/O/xbxVPzA==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/node-resolver-core@3.3.0': + resolution: {integrity: sha512-rhPW9DYPEIqQBSlYzz3S0AjXxjN6Ub2yS6tzzsW/4S3Gpsgk/uEq4ZfxPvoPf/6TgZndVxmKwpmxaKtGMmf3cA==} + engines: {node: '>= 12.0.0'} + + '@parcel/optimizer-css@2.12.0': + resolution: {integrity: sha512-ifbcC97fRzpruTjaa8axIFeX4MjjSIlQfem3EJug3L2AVqQUXnM1XO8L0NaXGNLTW2qnh1ZjIJ7vXT/QhsphsA==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/optimizer-htmlnano@2.12.0': + resolution: {integrity: sha512-MfPMeCrT8FYiOrpFHVR+NcZQlXAptK2r4nGJjfT+ndPBhEEZp4yyL7n1y7HfX9geg5altc4WTb4Gug7rCoW8VQ==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/optimizer-image@2.12.0': + resolution: {integrity: sha512-bo1O7raeAIbRU5nmNVtx8divLW9Xqn0c57GVNGeAK4mygnQoqHqRZ0mR9uboh64pxv6ijXZHPhKvU9HEpjPjBQ==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + peerDependencies: + '@parcel/core': ^2.12.0 + + '@parcel/optimizer-svgo@2.12.0': + resolution: {integrity: sha512-Kyli+ZZXnoonnbeRQdoWwee9Bk2jm/49xvnfb+2OO8NN0d41lblBoRhOyFiScRnJrw7eVl1Xrz7NTkXCIO7XFQ==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/optimizer-swc@2.12.0': + resolution: {integrity: sha512-iBi6LZB3lm6WmbXfzi8J3DCVPmn4FN2lw7DGXxUXu7MouDPVWfTsM6U/5TkSHJRNRogZ2gqy5q9g34NPxHbJcw==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/package-manager@2.12.0': + resolution: {integrity: sha512-0nvAezcjPx9FT+hIL+LS1jb0aohwLZXct7jAh7i0MLMtehOi0z1Sau+QpgMlA9rfEZZ1LIeFdnZZwqSy7Ccspw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@parcel/core': ^2.12.0 + + '@parcel/packager-css@2.12.0': + resolution: {integrity: sha512-j3a/ODciaNKD19IYdWJT+TP+tnhhn5koBGBWWtrKSu0UxWpnezIGZetit3eE+Y9+NTePalMkvpIlit2eDhvfJA==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/packager-html@2.12.0': + resolution: {integrity: sha512-PpvGB9hFFe+19NXGz2ApvPrkA9GwEqaDAninT+3pJD57OVBaxB8U+HN4a5LICKxjUppPPqmrLb6YPbD65IX4RA==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/packager-js@2.12.0': + resolution: {integrity: sha512-viMF+FszITRRr8+2iJyk+4ruGiL27Y6AF7hQ3xbJfzqnmbOhGFtLTQwuwhOLqN/mWR2VKdgbLpZSarWaO3yAMg==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/packager-raw@2.12.0': + resolution: {integrity: sha512-tJZqFbHqP24aq1F+OojFbQIc09P/u8HAW5xfndCrFnXpW4wTgM3p03P0xfw3gnNq+TtxHJ8c3UFE5LnXNNKhYA==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/packager-svg@2.12.0': + resolution: {integrity: sha512-ldaGiacGb2lLqcXas97k8JiZRbAnNREmcvoY2W2dvW4loVuDT9B9fU777mbV6zODpcgcHWsLL3lYbJ5Lt3y9cg==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/packager-wasm@2.12.0': + resolution: {integrity: sha512-fYqZzIqO9fGYveeImzF8ll6KRo2LrOXfD+2Y5U3BiX/wp9wv17dz50QLDQm9hmTcKGWxK4yWqKQh+Evp/fae7A==} + engines: {node: '>=12.0.0', parcel: ^2.12.0} + + '@parcel/plugin@2.12.0': + resolution: {integrity: sha512-nc/uRA8DiMoe4neBbzV6kDndh/58a4wQuGKw5oEoIwBCHUvE2W8ZFSu7ollSXUGRzfacTt4NdY8TwS73ScWZ+g==} + engines: {node: '>= 12.0.0'} + + '@parcel/profiler@2.12.0': + resolution: {integrity: sha512-q53fvl5LDcFYzMUtSusUBZSjQrKjMlLEBgKeQHFwkimwR1mgoseaDBDuNz0XvmzDzF1UelJ02TUKCGacU8W2qA==} + engines: {node: '>= 12.0.0'} + + '@parcel/reporter-cli@2.12.0': + resolution: {integrity: sha512-TqKsH4GVOLPSCanZ6tcTPj+rdVHERnt5y4bwTM82cajM21bCX1Ruwp8xOKU+03091oV2pv5ieB18pJyRF7IpIw==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/reporter-dev-server@2.12.0': + resolution: {integrity: sha512-tIcDqRvAPAttRlTV28dHcbWT5K2r/MBFks7nM4nrEDHWtnrCwimkDmZTc1kD8QOCCjGVwRHcQybpHvxfwol6GA==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/reporter-tracer@2.12.0': + resolution: {integrity: sha512-g8rlu9GxB8Ut/F8WGx4zidIPQ4pcYFjU9bZO+fyRIPrSUFH2bKijCnbZcr4ntqzDGx74hwD6cCG4DBoleq2UlQ==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/resolver-default@2.12.0': + resolution: {integrity: sha512-uuhbajTax37TwCxu7V98JtRLiT6hzE4VYSu5B7Qkauy14/WFt2dz6GOUXPgVsED569/hkxebPx3KCMtZW6cHHA==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/runtime-browser-hmr@2.12.0': + resolution: {integrity: sha512-4ZLp2FWyD32r0GlTulO3+jxgsA3oO1P1b5oO2IWuWilfhcJH5LTiazpL5YdusUjtNn9PGN6QLAWfxmzRIfM+Ow==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/runtime-js@2.12.0': + resolution: {integrity: sha512-sBerP32Z1crX5PfLNGDSXSdqzlllM++GVnVQVeM7DgMKS8JIFG3VLi28YkX+dYYGtPypm01JoIHCkvwiZEcQJg==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/runtime-react-refresh@2.12.0': + resolution: {integrity: sha512-SCHkcczJIDFTFdLTzrHTkQ0aTrX3xH6jrA4UsCBL6ji61+w+ohy4jEEe9qCgJVXhnJfGLE43HNXek+0MStX+Mw==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/runtime-service-worker@2.12.0': + resolution: {integrity: sha512-BXuMBsfiwpIEnssn+jqfC3jkgbS8oxeo3C7xhSQsuSv+AF2FwY3O3AO1c1RBskEW3XrBLNINOJujroNw80VTKA==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/rust@2.12.0': + resolution: {integrity: sha512-005cldMdFZFDPOjbDVEXcINQ3wT4vrxvSavRWI3Az0e3E18exO/x/mW9f648KtXugOXMAqCEqhFHcXECL9nmMw==} + engines: {node: '>= 12.0.0'} + + '@parcel/source-map@2.1.1': + resolution: {integrity: sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==} + engines: {node: ^12.18.3 || >=14} + + '@parcel/transformer-babel@2.12.0': + resolution: {integrity: sha512-zQaBfOnf/l8rPxYGnsk/ufh/0EuqvmnxafjBIpKZ//j6rGylw5JCqXSb1QvvAqRYruKeccxGv7+HrxpqKU6V4A==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/transformer-css@2.12.0': + resolution: {integrity: sha512-vXhOqoAlQGATYyQ433Z1DXKmiKmzOAUmKysbYH3FD+LKEKLMEl/pA14goqp00TW+A/EjtSKKyeMyHlMIIUqj4Q==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/transformer-html@2.12.0': + resolution: {integrity: sha512-5jW4dFFBlYBvIQk4nrH62rfA/G/KzVzEDa6S+Nne0xXhglLjkm64Ci9b/d4tKZfuGWUbpm2ASAq8skti/nfpXw==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/transformer-image@2.12.0': + resolution: {integrity: sha512-8hXrGm2IRII49R7lZ0RpmNk27EhcsH+uNKsvxuMpXPuEnWgC/ha/IrjaI29xCng1uGur74bJF43NUSQhR4aTdw==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + peerDependencies: + '@parcel/core': ^2.12.0 + + '@parcel/transformer-js@2.12.0': + resolution: {integrity: sha512-OSZpOu+FGDbC/xivu24v092D9w6EGytB3vidwbdiJ2FaPgfV7rxS0WIUjH4I0OcvHAcitArRXL0a3+HrNTdQQw==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + peerDependencies: + '@parcel/core': ^2.12.0 + + '@parcel/transformer-json@2.12.0': + resolution: {integrity: sha512-Utv64GLRCQILK5r0KFs4o7I41ixMPllwOLOhkdjJKvf1hZmN6WqfOmB1YLbWS/y5Zb/iB52DU2pWZm96vLFQZQ==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/transformer-postcss@2.12.0': + resolution: {integrity: sha512-FZqn+oUtiLfPOn67EZxPpBkfdFiTnF4iwiXPqvst3XI8H+iC+yNgzmtJkunOOuylpYY6NOU5jT8d7saqWSDv2Q==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/transformer-posthtml@2.12.0': + resolution: {integrity: sha512-z6Z7rav/pcaWdeD+2sDUcd0mmNZRUvtHaUGa50Y2mr+poxrKilpsnFMSiWBT+oOqPt7j71jzDvrdnAF4XkCljg==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/transformer-raw@2.12.0': + resolution: {integrity: sha512-Ht1fQvXxix0NncdnmnXZsa6hra20RXYh1VqhBYZLsDfkvGGFnXIgO03Jqn4Z8MkKoa0tiNbDhpKIeTjyclbBxQ==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/transformer-react-refresh-wrap@2.12.0': + resolution: {integrity: sha512-GE8gmP2AZtkpBIV5vSCVhewgOFRhqwdM5Q9jNPOY5PKcM3/Ff0qCqDiTzzGLhk0/VMBrdjssrfZkVx6S/lHdJw==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/transformer-svg@2.12.0': + resolution: {integrity: sha512-cZJqGRJ4JNdYcb+vj94J7PdOuTnwyy45dM9xqbIMH+HSiiIkfrMsdEwYft0GTyFTdsnf+hdHn3tau7Qa5hhX+A==} + engines: {node: '>= 12.0.0', parcel: ^2.12.0} + + '@parcel/types@2.12.0': + resolution: {integrity: sha512-8zAFiYNCwNTQcglIObyNwKfRYQK5ELlL13GuBOrSMxueUiI5ylgsGbTS1N7J3dAGZixHO8KhHGv5a71FILn9rQ==} + + '@parcel/utils@2.12.0': + resolution: {integrity: sha512-z1JhLuZ8QmDaYoEIuUCVZlhcFrS7LMfHrb2OCRui5SQFntRWBH2fNM6H/fXXUkT9SkxcuFP2DUA6/m4+Gkz72g==} + engines: {node: '>= 12.0.0'} + + '@parcel/watcher-android-arm64@2.4.1': + resolution: {integrity: sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.4.1': + resolution: {integrity: sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.4.1': + resolution: {integrity: sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.4.1': + resolution: {integrity: sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.4.1': + resolution: {integrity: sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.4.1': + resolution: {integrity: sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.4.1': + resolution: {integrity: sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.4.1': + resolution: {integrity: sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.4.1': + resolution: {integrity: sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.4.1': + resolution: {integrity: sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.4.1': + resolution: {integrity: sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.4.1': + resolution: {integrity: sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.4.1': + resolution: {integrity: sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==} + engines: {node: '>= 10.0.0'} + + '@parcel/workers@2.12.0': + resolution: {integrity: sha512-zv5We5Jmb+ZWXlU6A+AufyjY4oZckkxsZ8J4dvyWL0W8IQvGO1JB4FGeryyttzQv3RM3OxcN/BpTGPiDG6keBw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@parcel/core': ^2.12.0 + + '@swc/core-darwin-arm64@1.7.11': + resolution: {integrity: sha512-HRQv4qIeMBPThZ6Y/4yYW52rGsS6yrpusvuxLGyoFo45Y0y12/V2yXkOIA/0HIQyrqoUAxn1k4zQXpPaPNCmnw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.7.11': + resolution: {integrity: sha512-vtMQj0F3oYwDu5yhO7SKDRg1XekRSi6/TbzHAbBXv+dBhlGGvcZZynT1H90EVFTv+7w7Sh+lOFvRv5Z4ZTcxow==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.7.11': + resolution: {integrity: sha512-mHtzWKxhtyreI4CSxs+3+ENv8t/Qo35WFoYG66qHEgJz/Z2Lh6jv1E+MYgHdYwnpQHgHbdvAco7HsBu/Dt6xXw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.7.11': + resolution: {integrity: sha512-FRwe/x0GfXSQjGP2lIk+NO0pUFS/lI/RorCLBPiK808EVE9JTbh9DKCc/4Bbb4jgScAjNkrFCUVObQYl3YKmpA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.7.11': + resolution: {integrity: sha512-GY/rs0+GUq14Gbnza90KOrQd/9yHd5qQMii5jcSWcUCT5A8QTa8kiicsM2NxZeTJ69xlKmT7sLod5l99lki/2A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.7.11': + resolution: {integrity: sha512-QDkGRwSPmp2RBOlSs503IUXlWYlny8DyznTT0QuK0ML2RpDFlXWU94K/EZhS0RBEUkMY/W51OacM8P8aS/dkCg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.7.11': + resolution: {integrity: sha512-SBEfKrXy6zQ6ksnyxw1FaCftrIH4fLfA81xNnKb7x/6iblv7Ko6H0aK3P5C86jyqF/82+ONl9C7ImGkUFQADig==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.7.11': + resolution: {integrity: sha512-a2Y4xxEsLLYHJN7sMnw9+YQJDi3M1BxEr9hklfopPuGGnYLFNnx5CypH1l9ReijEfWjIAHNi7pq3m023lzW1Hg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.7.11': + resolution: {integrity: sha512-ZbZFMwZO+j8ulhegJ7EhJ/QVZPoQ5qc30ylJQSxizizTJaen71Q7/13lXWc6ksuCKvg6dUKrp/TPgoxOOtSrFA==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.7.11': + resolution: {integrity: sha512-IUohZedSJyDu/ReEBG/mqX6uG29uA7zZ9z6dIAF+p6eFxjXmh9MuHryyM+H8ebUyoq/Ad3rL+rUCksnuYNnI0w==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.7.11': + resolution: {integrity: sha512-AB+qc45UrJrDfbhPKcUXk+9z/NmFfYYwJT6G7/iur0fCse9kXjx45gi40+u/O2zgarG/30/zV6E3ps8fUvjh7g==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '*' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.12': + resolution: {integrity: sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g==} + + '@swc/types@0.1.12': + resolution: {integrity: sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==} + + '@trysound/sax@0.2.0': + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + + '@tsconfig/recommended@1.0.7': + resolution: {integrity: sha512-xiNMgCuoy4mCL4JTywk9XFs5xpRUcKxtWEcMR6FNMtsgewYTIgIR+nvlP4A4iRCAzRsHMnPhvTRrzp4AGcRTEA==} + + '@types/parcel-env@0.0.6': + resolution: {integrity: sha512-HlPNbKhUe+0xVqzeqttl/gFC27dUZCjrUSI6MlfGoudjCtNeVOwm2kmDalkfXVWXsj4NJ4oa0PO3SdtOdGaGDg==} + + abortcontroller-polyfill@1.7.5: + resolution: {integrity: sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + base-x@3.0.10: + resolution: {integrity: sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.23.3: + resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001651: + resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + + css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + csso@4.2.0: + resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} + engines: {node: '>=8.0.0'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + dotenv-expand@5.1.0: + resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} + + dotenv@7.0.0: + resolution: {integrity: sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==} + engines: {node: '>=6'} + + electron-to-chromium@1.5.9: + resolution: {integrity: sha512-HfkT8ndXR0SEkU8gBQQM3rz035bpE/hxkZ1YIt4KJPEFES68HfIU6LzKukH0H794Lm83WJtkSAMfEToxCs15VA==} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@3.0.1: + resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + get-port@4.2.0: + resolution: {integrity: sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==} + engines: {node: '>=6'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + htmlnano@2.1.1: + resolution: {integrity: sha512-kAERyg/LuNZYmdqgCdYvugyLWNFAm8MWXpQMz1pLpetmCbFwoMxvkSoaAMlFrOC4OKTWI4KlZGT/RsNxg4ghOw==} + peerDependencies: + cssnano: ^7.0.0 + postcss: ^8.3.11 + purgecss: ^6.0.0 + relateurl: ^0.2.7 + srcset: 5.0.1 + svgo: ^3.0.2 + terser: ^5.10.0 + uncss: ^0.17.3 + peerDependenciesMeta: + cssnano: + optional: true + postcss: + optional: true + purgecss: + optional: true + relateurl: + optional: true + srcset: + optional: true + svgo: + optional: true + terser: + optional: true + uncss: + optional: true + + htmlparser2@7.2.0: + resolution: {integrity: sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-json@2.0.1: + resolution: {integrity: sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lightningcss-darwin-arm64@1.26.0: + resolution: {integrity: sha512-n4TIvHO1NY1ondKFYpL2ZX0bcC2y6yjXMD6JfyizgR8BCFNEeArINDzEaeqlfX9bXz73Bpz/Ow0nu+1qiDrBKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.26.0: + resolution: {integrity: sha512-Rf9HuHIDi1R6/zgBkJh25SiJHF+dm9axUZW/0UoYCW1/8HV0gMI0blARhH4z+REmWiU1yYT/KyNF3h7tHyRXUg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.26.0: + resolution: {integrity: sha512-C/io7POAxp6sZxFSVGezjajMlCKQ8KSwISLLGRq8xLQpQMokYrUoqYEwmIX8mLmF6C/CZPk0gFmRSzd8biWM0g==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.26.0: + resolution: {integrity: sha512-Aag9kqXqkyPSW+dXMgyWk66C984Nay2pY8Nws+67gHlDzV3cWh7TvFlzuaTaVFMVqdDTzN484LSK3u39zFBnzg==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.26.0: + resolution: {integrity: sha512-iJmZM7fUyVjH+POtdiCtExG+67TtPUTer7K/5A8DIfmPfrmeGvzfRyBltGhQz13Wi15K1lf2cPYoRaRh6vcwNA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.26.0: + resolution: {integrity: sha512-XxoEL++tTkyuvu+wq/QS8bwyTXZv2y5XYCMcWL45b8XwkiS8eEEEej9BkMGSRwxa5J4K+LDeIhLrS23CpQyfig==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.26.0: + resolution: {integrity: sha512-1dkTfZQAYLj8MUSkd6L/+TWTG8V6Kfrzfa0T1fSlXCXQHrt1HC1/UepXHtKHDt/9yFwyoeayivxXAsApVxn6zA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.26.0: + resolution: {integrity: sha512-yX3Rk9m00JGCUzuUhFEojY+jf/6zHs3XU8S8Vk+FRbnr4St7cjyMXdNjuA2LjiT8e7j8xHRCH8hyZ4H/btRE4A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.26.0: + resolution: {integrity: sha512-X/597/cFnCogy9VItj/+7Tgu5VLbAtDF7KZDPdSw0MaL6FL940th1y3HiOzFIlziVvAtbo0RB3NAae1Oofr+Tw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.26.0: + resolution: {integrity: sha512-pYS3EyGP3JRhfqEFYmfFDiZ9/pVNfy8jVIYtrx9TVNusVyDK3gpW1w/rbvroQ4bDJi7grdUtyrYU6V2xkY/bBw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.26.0: + resolution: {integrity: sha512-a/XZ5hdgifrofQJUArr5AiJjx26SwMam3SJUSMjgebZbESZ96i+6Qsl8tLi0kaUsdMzBWXh9sN1Oe6hp2/dkQw==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lmdb@2.8.5: + resolution: {integrity: sha512-9bMdFfc80S+vSldBmG3HOuLVHnxRdNTlpzR6QDnzqCQtCzGUEAGTzBKYMeIM+I/sU4oZfgbcbS7X7F65/z/oxQ==} + hasBin: true + + mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + + micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.0: + resolution: {integrity: sha512-I8qXuuALqJe5laEBYoFykChhSXLikZmUhccjGsPuSJ/7uPip2TJ7lwdIQwWSAi0jGZDXv4WOP8Qg65QZRuXxXw==} + + node-addon-api@6.1.0: + resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nullthrows@1.1.1: + resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} + + ordered-binary@1.5.1: + resolution: {integrity: sha512-5VyHfHY3cd0iza71JepYG50My+YUbrFtGoUz2ooEydPyPM7Aai/JW098juLr+RG6+rDJuzNNTsEQu2DZa1A41A==} + + parcel@2.12.0: + resolution: {integrity: sha512-W+gxAq7aQ9dJIg/XLKGcRT0cvnStFAQHPaI0pvD0U2l6IVLueUAm3nwN7lkY62zZNmlvNx6jNtE4wlbS+CyqSg==} + engines: {node: '>= 12.0.0'} + hasBin: true + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + posthtml-parser@0.10.2: + resolution: {integrity: sha512-PId6zZ/2lyJi9LiKfe+i2xv57oEjJgWbsHGGANwos5AvdQp98i6AtamAl8gzSVFGfQ43Glb5D614cvZf012VKg==} + engines: {node: '>=12'} + + posthtml-parser@0.11.0: + resolution: {integrity: sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==} + engines: {node: '>=12'} + + posthtml-render@3.0.0: + resolution: {integrity: sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==} + engines: {node: '>=12'} + + posthtml@0.16.6: + resolution: {integrity: sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==} + engines: {node: '>=12.0.0'} + + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + engines: {node: '>=14'} + hasBin: true + + react-error-overlay@6.0.9: + resolution: {integrity: sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==} + + react-refresh@0.9.0: + resolution: {integrity: sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==} + engines: {node: '>=0.10.0'} + + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + srcset@4.0.0: + resolution: {integrity: sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==} + engines: {node: '>=12'} + + stable@0.1.8: + resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} + deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + svgo@2.8.0: + resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} + engines: {node: '>=10.13.0'} + hasBin: true + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + timsort@0.3.0: + resolution: {integrity: sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.1.0: + resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + + weak-lru-cache@1.2.2: + resolution: {integrity: sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==} + +snapshots: + + '@babel/code-frame@7.24.7': + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.1 + + '@babel/helper-validator-identifier@7.24.7': {} + + '@babel/highlight@7.24.7': + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 + + '@lezer/common@1.2.1': {} + + '@lezer/lr@1.4.2': + dependencies: + '@lezer/common': 1.2.1 + + '@lmdb/lmdb-darwin-arm64@2.8.5': + optional: true + + '@lmdb/lmdb-darwin-x64@2.8.5': + optional: true + + '@lmdb/lmdb-linux-arm64@2.8.5': + optional: true + + '@lmdb/lmdb-linux-arm@2.8.5': + optional: true + + '@lmdb/lmdb-linux-x64@2.8.5': + optional: true + + '@lmdb/lmdb-win32-x64@2.8.5': + optional: true + + '@mischnic/json-sourcemap@0.1.1': + dependencies: + '@lezer/common': 1.2.1 + '@lezer/lr': 1.4.2 + json5: 2.2.3 + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + + '@parcel/bundler-default@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/diagnostic': 2.12.0 + '@parcel/graph': 3.2.0 + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/rust': 2.12.0 + '@parcel/utils': 2.12.0 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/cache@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)': + dependencies: + '@parcel/core': 2.12.0(@swc/helpers@0.5.12) + '@parcel/fs': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/logger': 2.12.0 + '@parcel/utils': 2.12.0 + lmdb: 2.8.5 + transitivePeerDependencies: + - '@swc/helpers' + + '@parcel/codeframe@2.12.0': + dependencies: + chalk: 4.1.2 + + '@parcel/compressor-raw@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/config-default@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)(typescript@5.5.4)': + dependencies: + '@parcel/bundler-default': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/compressor-raw': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/core': 2.12.0(@swc/helpers@0.5.12) + '@parcel/namer-default': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/optimizer-css': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/optimizer-htmlnano': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(typescript@5.5.4) + '@parcel/optimizer-image': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/optimizer-svgo': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/optimizer-swc': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/packager-css': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/packager-html': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/packager-js': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/packager-raw': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/packager-svg': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/packager-wasm': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/reporter-dev-server': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/resolver-default': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/runtime-browser-hmr': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/runtime-js': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/runtime-react-refresh': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/runtime-service-worker': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/transformer-babel': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/transformer-css': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/transformer-html': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/transformer-image': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/transformer-js': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/transformer-json': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/transformer-postcss': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/transformer-posthtml': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/transformer-raw': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/transformer-react-refresh-wrap': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/transformer-svg': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + transitivePeerDependencies: + - '@swc/helpers' + - cssnano + - postcss + - purgecss + - relateurl + - srcset + - terser + - typescript + - uncss + + '@parcel/core@2.12.0(@swc/helpers@0.5.12)': + dependencies: + '@mischnic/json-sourcemap': 0.1.1 + '@parcel/cache': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/diagnostic': 2.12.0 + '@parcel/events': 2.12.0 + '@parcel/fs': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/graph': 3.2.0 + '@parcel/logger': 2.12.0 + '@parcel/package-manager': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/profiler': 2.12.0 + '@parcel/rust': 2.12.0 + '@parcel/source-map': 2.1.1 + '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/utils': 2.12.0 + '@parcel/workers': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + abortcontroller-polyfill: 1.7.5 + base-x: 3.0.10 + browserslist: 4.23.3 + clone: 2.1.2 + dotenv: 7.0.0 + dotenv-expand: 5.1.0 + json5: 2.2.3 + msgpackr: 1.11.0 + nullthrows: 1.1.1 + semver: 7.6.3 + transitivePeerDependencies: + - '@swc/helpers' + + '@parcel/diagnostic@2.12.0': + dependencies: + '@mischnic/json-sourcemap': 0.1.1 + nullthrows: 1.1.1 + + '@parcel/events@2.12.0': {} + + '@parcel/fs@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)': + dependencies: + '@parcel/core': 2.12.0(@swc/helpers@0.5.12) + '@parcel/rust': 2.12.0 + '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/utils': 2.12.0 + '@parcel/watcher': 2.4.1 + '@parcel/workers': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + transitivePeerDependencies: + - '@swc/helpers' + + '@parcel/graph@3.2.0': + dependencies: + nullthrows: 1.1.1 + + '@parcel/logger@2.12.0': + dependencies: + '@parcel/diagnostic': 2.12.0 + '@parcel/events': 2.12.0 + + '@parcel/markdown-ansi@2.12.0': + dependencies: + chalk: 4.1.2 + + '@parcel/namer-default@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/diagnostic': 2.12.0 + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/node-resolver-core@3.3.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@mischnic/json-sourcemap': 0.1.1 + '@parcel/diagnostic': 2.12.0 + '@parcel/fs': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/rust': 2.12.0 + '@parcel/utils': 2.12.0 + nullthrows: 1.1.1 + semver: 7.6.3 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/optimizer-css@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/diagnostic': 2.12.0 + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/source-map': 2.1.1 + '@parcel/utils': 2.12.0 + browserslist: 4.23.3 + lightningcss: 1.26.0 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/optimizer-htmlnano@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(typescript@5.5.4)': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + htmlnano: 2.1.1(svgo@2.8.0)(typescript@5.5.4) + nullthrows: 1.1.1 + posthtml: 0.16.6 + svgo: 2.8.0 + transitivePeerDependencies: + - '@parcel/core' + - cssnano + - postcss + - purgecss + - relateurl + - srcset + - terser + - typescript + - uncss + + '@parcel/optimizer-image@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/core': 2.12.0(@swc/helpers@0.5.12) + '@parcel/diagnostic': 2.12.0 + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/rust': 2.12.0 + '@parcel/utils': 2.12.0 + '@parcel/workers': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + + '@parcel/optimizer-svgo@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/diagnostic': 2.12.0 + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/utils': 2.12.0 + svgo: 2.8.0 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/optimizer-swc@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)': + dependencies: + '@parcel/diagnostic': 2.12.0 + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/source-map': 2.1.1 + '@parcel/utils': 2.12.0 + '@swc/core': 1.7.11(@swc/helpers@0.5.12) + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + - '@swc/helpers' + + '@parcel/package-manager@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)': + dependencies: + '@parcel/core': 2.12.0(@swc/helpers@0.5.12) + '@parcel/diagnostic': 2.12.0 + '@parcel/fs': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/logger': 2.12.0 + '@parcel/node-resolver-core': 3.3.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/utils': 2.12.0 + '@parcel/workers': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@swc/core': 1.7.11(@swc/helpers@0.5.12) + semver: 7.6.3 + transitivePeerDependencies: + - '@swc/helpers' + + '@parcel/packager-css@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/diagnostic': 2.12.0 + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/source-map': 2.1.1 + '@parcel/utils': 2.12.0 + lightningcss: 1.26.0 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/packager-html@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/utils': 2.12.0 + nullthrows: 1.1.1 + posthtml: 0.16.6 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/packager-js@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/diagnostic': 2.12.0 + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/rust': 2.12.0 + '@parcel/source-map': 2.1.1 + '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/utils': 2.12.0 + globals: 13.24.0 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/packager-raw@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/packager-svg@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/utils': 2.12.0 + posthtml: 0.16.6 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/packager-wasm@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/plugin@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/profiler@2.12.0': + dependencies: + '@parcel/diagnostic': 2.12.0 + '@parcel/events': 2.12.0 + chrome-trace-event: 1.0.4 + + '@parcel/reporter-cli@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/utils': 2.12.0 + chalk: 4.1.2 + term-size: 2.2.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/reporter-dev-server@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/utils': 2.12.0 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/reporter-tracer@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/utils': 2.12.0 + chrome-trace-event: 1.0.4 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/resolver-default@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/node-resolver-core': 3.3.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/runtime-browser-hmr@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/utils': 2.12.0 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/runtime-js@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/diagnostic': 2.12.0 + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/utils': 2.12.0 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/runtime-react-refresh@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/utils': 2.12.0 + react-error-overlay: 6.0.9 + react-refresh: 0.9.0 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/runtime-service-worker@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/utils': 2.12.0 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/rust@2.12.0': {} + + '@parcel/source-map@2.1.1': + dependencies: + detect-libc: 1.0.3 + + '@parcel/transformer-babel@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/diagnostic': 2.12.0 + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/source-map': 2.1.1 + '@parcel/utils': 2.12.0 + browserslist: 4.23.3 + json5: 2.2.3 + nullthrows: 1.1.1 + semver: 7.6.3 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-css@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/diagnostic': 2.12.0 + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/source-map': 2.1.1 + '@parcel/utils': 2.12.0 + browserslist: 4.23.3 + lightningcss: 1.26.0 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-html@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/diagnostic': 2.12.0 + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/rust': 2.12.0 + nullthrows: 1.1.1 + posthtml: 0.16.6 + posthtml-parser: 0.10.2 + posthtml-render: 3.0.0 + semver: 7.6.3 + srcset: 4.0.0 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-image@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/core': 2.12.0(@swc/helpers@0.5.12) + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/utils': 2.12.0 + '@parcel/workers': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + nullthrows: 1.1.1 + + '@parcel/transformer-js@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/core': 2.12.0(@swc/helpers@0.5.12) + '@parcel/diagnostic': 2.12.0 + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/rust': 2.12.0 + '@parcel/source-map': 2.1.1 + '@parcel/utils': 2.12.0 + '@parcel/workers': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@swc/helpers': 0.5.12 + browserslist: 4.23.3 + nullthrows: 1.1.1 + regenerator-runtime: 0.13.11 + semver: 7.6.3 + + '@parcel/transformer-json@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + json5: 2.2.3 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-postcss@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/diagnostic': 2.12.0 + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/rust': 2.12.0 + '@parcel/utils': 2.12.0 + clone: 2.1.2 + nullthrows: 1.1.1 + postcss-value-parser: 4.2.0 + semver: 7.6.3 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-posthtml@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/utils': 2.12.0 + nullthrows: 1.1.1 + posthtml: 0.16.6 + posthtml-parser: 0.10.2 + posthtml-render: 3.0.0 + semver: 7.6.3 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-raw@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-react-refresh-wrap@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/utils': 2.12.0 + react-refresh: 0.9.0 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/transformer-svg@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/diagnostic': 2.12.0 + '@parcel/plugin': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/rust': 2.12.0 + nullthrows: 1.1.1 + posthtml: 0.16.6 + posthtml-parser: 0.10.2 + posthtml-render: 3.0.0 + semver: 7.6.3 + transitivePeerDependencies: + - '@parcel/core' + + '@parcel/types@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)': + dependencies: + '@parcel/cache': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/diagnostic': 2.12.0 + '@parcel/fs': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/package-manager': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/source-map': 2.1.1 + '@parcel/workers': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + utility-types: 3.11.0 + transitivePeerDependencies: + - '@parcel/core' + - '@swc/helpers' + + '@parcel/utils@2.12.0': + dependencies: + '@parcel/codeframe': 2.12.0 + '@parcel/diagnostic': 2.12.0 + '@parcel/logger': 2.12.0 + '@parcel/markdown-ansi': 2.12.0 + '@parcel/rust': 2.12.0 + '@parcel/source-map': 2.1.1 + chalk: 4.1.2 + nullthrows: 1.1.1 + + '@parcel/watcher-android-arm64@2.4.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.4.1': + optional: true + + '@parcel/watcher-darwin-x64@2.4.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.4.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.4.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.4.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.4.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.4.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.4.1': + optional: true + + '@parcel/watcher-win32-arm64@2.4.1': + optional: true + + '@parcel/watcher-win32-ia32@2.4.1': + optional: true + + '@parcel/watcher-win32-x64@2.4.1': + optional: true + + '@parcel/watcher@2.4.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.7 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.4.1 + '@parcel/watcher-darwin-arm64': 2.4.1 + '@parcel/watcher-darwin-x64': 2.4.1 + '@parcel/watcher-freebsd-x64': 2.4.1 + '@parcel/watcher-linux-arm-glibc': 2.4.1 + '@parcel/watcher-linux-arm64-glibc': 2.4.1 + '@parcel/watcher-linux-arm64-musl': 2.4.1 + '@parcel/watcher-linux-x64-glibc': 2.4.1 + '@parcel/watcher-linux-x64-musl': 2.4.1 + '@parcel/watcher-win32-arm64': 2.4.1 + '@parcel/watcher-win32-ia32': 2.4.1 + '@parcel/watcher-win32-x64': 2.4.1 + + '@parcel/workers@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))': + dependencies: + '@parcel/core': 2.12.0(@swc/helpers@0.5.12) + '@parcel/diagnostic': 2.12.0 + '@parcel/logger': 2.12.0 + '@parcel/profiler': 2.12.0 + '@parcel/types': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/utils': 2.12.0 + nullthrows: 1.1.1 + + '@swc/core-darwin-arm64@1.7.11': + optional: true + + '@swc/core-darwin-x64@1.7.11': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.7.11': + optional: true + + '@swc/core-linux-arm64-gnu@1.7.11': + optional: true + + '@swc/core-linux-arm64-musl@1.7.11': + optional: true + + '@swc/core-linux-x64-gnu@1.7.11': + optional: true + + '@swc/core-linux-x64-musl@1.7.11': + optional: true + + '@swc/core-win32-arm64-msvc@1.7.11': + optional: true + + '@swc/core-win32-ia32-msvc@1.7.11': + optional: true + + '@swc/core-win32-x64-msvc@1.7.11': + optional: true + + '@swc/core@1.7.11(@swc/helpers@0.5.12)': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.12 + optionalDependencies: + '@swc/core-darwin-arm64': 1.7.11 + '@swc/core-darwin-x64': 1.7.11 + '@swc/core-linux-arm-gnueabihf': 1.7.11 + '@swc/core-linux-arm64-gnu': 1.7.11 + '@swc/core-linux-arm64-musl': 1.7.11 + '@swc/core-linux-x64-gnu': 1.7.11 + '@swc/core-linux-x64-musl': 1.7.11 + '@swc/core-win32-arm64-msvc': 1.7.11 + '@swc/core-win32-ia32-msvc': 1.7.11 + '@swc/core-win32-x64-msvc': 1.7.11 + '@swc/helpers': 0.5.12 + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.12': + dependencies: + tslib: 2.6.3 + + '@swc/types@0.1.12': + dependencies: + '@swc/counter': 0.1.3 + + '@trysound/sax@0.2.0': {} + + '@tsconfig/recommended@1.0.7': {} + + '@types/parcel-env@0.0.6': {} + + abortcontroller-polyfill@1.7.5: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + base-x@3.0.10: + dependencies: + safe-buffer: 5.2.1 + + boolbase@1.0.0: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.23.3: + dependencies: + caniuse-lite: 1.0.30001651 + electron-to-chromium: 1.5.9 + node-releases: 2.0.18 + update-browserslist-db: 1.1.0(browserslist@4.23.3) + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001651: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chrome-trace-event@1.0.4: {} + + clone@2.1.2: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + commander@7.2.0: {} + + cosmiconfig@9.0.0(typescript@5.5.4): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.5.4 + + css-select@4.3.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + + css-tree@1.1.3: + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + + css-what@6.1.0: {} + + csso@4.2.0: + dependencies: + css-tree: 1.1.3 + + detect-libc@1.0.3: {} + + detect-libc@2.0.3: {} + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + domelementtype@2.3.0: {} + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + dotenv-expand@5.1.0: {} + + dotenv@7.0.0: {} + + electron-to-chromium@1.5.9: {} + + entities@2.2.0: {} + + entities@3.0.1: {} + + env-paths@2.2.1: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + escalade@3.1.2: {} + + escape-string-regexp@1.0.5: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + get-port@4.2.0: {} + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + htmlnano@2.1.1(svgo@2.8.0)(typescript@5.5.4): + dependencies: + cosmiconfig: 9.0.0(typescript@5.5.4) + posthtml: 0.16.6 + timsort: 0.3.0 + optionalDependencies: + svgo: 2.8.0 + transitivePeerDependencies: + - typescript + + htmlparser2@7.2.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 3.0.1 + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + is-arrayish@0.2.1: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-json@2.0.1: {} + + is-number@7.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-parse-even-better-errors@2.3.1: {} + + json5@2.2.3: {} + + lightningcss-darwin-arm64@1.26.0: + optional: true + + lightningcss-darwin-x64@1.26.0: + optional: true + + lightningcss-freebsd-x64@1.26.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.26.0: + optional: true + + lightningcss-linux-arm64-gnu@1.26.0: + optional: true + + lightningcss-linux-arm64-musl@1.26.0: + optional: true + + lightningcss-linux-x64-gnu@1.26.0: + optional: true + + lightningcss-linux-x64-musl@1.26.0: + optional: true + + lightningcss-win32-arm64-msvc@1.26.0: + optional: true + + lightningcss-win32-x64-msvc@1.26.0: + optional: true + + lightningcss@1.26.0: + dependencies: + detect-libc: 1.0.3 + optionalDependencies: + lightningcss-darwin-arm64: 1.26.0 + lightningcss-darwin-x64: 1.26.0 + lightningcss-freebsd-x64: 1.26.0 + lightningcss-linux-arm-gnueabihf: 1.26.0 + lightningcss-linux-arm64-gnu: 1.26.0 + lightningcss-linux-arm64-musl: 1.26.0 + lightningcss-linux-x64-gnu: 1.26.0 + lightningcss-linux-x64-musl: 1.26.0 + lightningcss-win32-arm64-msvc: 1.26.0 + lightningcss-win32-x64-msvc: 1.26.0 + + lines-and-columns@1.2.4: {} + + lmdb@2.8.5: + dependencies: + msgpackr: 1.11.0 + node-addon-api: 6.1.0 + node-gyp-build-optional-packages: 5.1.1 + ordered-binary: 1.5.1 + weak-lru-cache: 1.2.2 + optionalDependencies: + '@lmdb/lmdb-darwin-arm64': 2.8.5 + '@lmdb/lmdb-darwin-x64': 2.8.5 + '@lmdb/lmdb-linux-arm': 2.8.5 + '@lmdb/lmdb-linux-arm64': 2.8.5 + '@lmdb/lmdb-linux-x64': 2.8.5 + '@lmdb/lmdb-win32-x64': 2.8.5 + + mdn-data@2.0.14: {} + + micromatch@4.0.7: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.0: + optionalDependencies: + msgpackr-extract: 3.0.3 + + node-addon-api@6.1.0: {} + + node-addon-api@7.1.1: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.0.3 + + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.0.3 + optional: true + + node-releases@2.0.18: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nullthrows@1.1.1: {} + + ordered-binary@1.5.1: {} + + parcel@2.12.0(@swc/helpers@0.5.12)(typescript@5.5.4): + dependencies: + '@parcel/config-default': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)(typescript@5.5.4) + '@parcel/core': 2.12.0(@swc/helpers@0.5.12) + '@parcel/diagnostic': 2.12.0 + '@parcel/events': 2.12.0 + '@parcel/fs': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/logger': 2.12.0 + '@parcel/package-manager': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12))(@swc/helpers@0.5.12) + '@parcel/reporter-cli': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/reporter-dev-server': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/reporter-tracer': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.12)) + '@parcel/utils': 2.12.0 + chalk: 4.1.2 + commander: 7.2.0 + get-port: 4.2.0 + transitivePeerDependencies: + - '@swc/helpers' + - cssnano + - postcss + - purgecss + - relateurl + - srcset + - terser + - typescript + - uncss + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.24.7 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + picocolors@1.0.1: {} + + picomatch@2.3.1: {} + + postcss-value-parser@4.2.0: {} + + posthtml-parser@0.10.2: + dependencies: + htmlparser2: 7.2.0 + + posthtml-parser@0.11.0: + dependencies: + htmlparser2: 7.2.0 + + posthtml-render@3.0.0: + dependencies: + is-json: 2.0.1 + + posthtml@0.16.6: + dependencies: + posthtml-parser: 0.11.0 + posthtml-render: 3.0.0 + + prettier@3.3.3: {} + + react-error-overlay@6.0.9: {} + + react-refresh@0.9.0: {} + + regenerator-runtime@0.13.11: {} + + resolve-from@4.0.0: {} + + safe-buffer@5.2.1: {} + + semver@7.6.3: {} + + source-map@0.6.1: {} + + srcset@4.0.0: {} + + stable@0.1.8: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + svgo@2.8.0: + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 4.3.0 + css-tree: 1.1.3 + csso: 4.2.0 + picocolors: 1.0.1 + stable: 0.1.8 + + term-size@2.2.1: {} + + timsort@0.3.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tslib@2.6.3: {} + + type-fest@0.20.2: {} + + typescript@5.5.4: {} + + update-browserslist-db@1.1.0(browserslist@4.23.3): + dependencies: + browserslist: 4.23.3 + escalade: 3.1.2 + picocolors: 1.0.1 + + utility-types@3.11.0: {} + + weak-lru-cache@1.2.2: {} diff --git a/docker/nginx/sites/www/src/main.ts b/docker/nginx/sites/www/src/main.ts new file mode 100644 index 0000000..2f09deb --- /dev/null +++ b/docker/nginx/sites/www/src/main.ts @@ -0,0 +1,97 @@ +import "./style.css"; + +import { fetchTodos } from "./todos"; + +const happy = "happy" as const; +const angry = "angry" as const; +type Emotion = typeof happy | typeof angry; + +function emotionOpposite(emotion: Emotion): Emotion { + if (emotion === happy) { + return angry; + } else { + return happy; + } +} + +function emotionElement(emotion: Emotion): HTMLDivElement { + return document.querySelector(`.slogan.${emotion}`)!; +} + +function emotionElementHeight(emotion: Emotion): number { + return emotionElement(emotion).clientHeight; +} + +function updateBodyTopPadding(emotion: Emotion): void { + document.body.style.paddingTop = `${emotionElementHeight(emotion)}px`; +} + +const sloganEmotionKey = "sloganEmotion"; + +const savedEmotion = + (localStorage.getItem(sloganEmotionKey) as Emotion | null) ?? happy; +if (savedEmotion !== happy && savedEmotion !== angry) { + console.error(`Invalid saved emotion: ${savedEmotion}`); +} + +updateBodyTopPadding(savedEmotion); +// Then we add transition animation. +setTimeout(() => { + document.body.style.transition = "padding-top 1s"; +}); + +const sloganContainer = document.querySelector( + ".slogan-container", +) as HTMLDivElement; + +setTimeout(() => { + sloganContainer.dataset.sloganEmotion = savedEmotion; +}, 500); + +const sloganLoadedPromise = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 1500); +}); + +for (const emotion of [happy, angry]) { + emotionElement(emotion).addEventListener("click", () => { + const opposite = emotionOpposite(emotion); + localStorage.setItem(sloganEmotionKey, opposite); + sloganContainer.dataset.sloganEmotion = opposite; + updateBodyTopPadding(opposite); + }); +} + +async function loadTodos(syncWith: Promise): Promise { + const todoMessage = document.getElementById("todo-message")!; + const todoContainer = document.getElementById("todo-container")!; + + try { + const todosPromise = fetchTodos(); + await syncWith; // Let's wait this first. + const todos = await todosPromise; + todos.forEach((item, index) => { + const { status, title, closed } = item; + const li = document.createElement("li"); + li.dataset.status = closed ? "closed" : "open"; + li.style.animationDelay = `${index * 0.04}s`; + // The color from api server is kind of ugly at present. + // li.style.background = color; + const statusSpan = document.createElement("span"); + const titleSpan = document.createElement("span"); + statusSpan.textContent = status; + titleSpan.textContent = title; + li.appendChild(statusSpan); + li.append(" : "); + li.append(titleSpan); + todoContainer.appendChild(li); + }); + todoMessage.parentElement!.removeChild(todoMessage); + } catch (e) { + todoMessage.style.color = "red"; + todoMessage.textContent = (e as Error).message; + } +} + +loadTodos(sloganLoadedPromise); diff --git a/docker/nginx/sites/www/src/mock-todos.ts b/docker/nginx/sites/www/src/mock-todos.ts new file mode 100644 index 0000000..aacb40e --- /dev/null +++ b/docker/nginx/sites/www/src/mock-todos.ts @@ -0,0 +1,126 @@ +/** Grabbed at Tue, 18 Jul 2023 15:30:05 GMT, used as mock data. 🍻 */ + +const todos = [ + { + status: "Done", + title: "All BLOCKed by graduate paper.", + closed: true, + color: "green", + }, + { + status: "Done", + title: "Slogan is not completely visible on phone.", + closed: true, + color: "green", + }, + { + status: "Todo", + title: "Users api.", + closed: false, + color: "blue", + }, + { + status: "Todo", + title: "Secrets api.", + closed: false, + color: "blue", + }, + { + status: "Todo", + title: "Refactor aio python scripts.", + closed: false, + color: "blue", + }, + { + status: "Todo", + title: "Nginx path redirection.", + closed: false, + color: "blue", + }, + { + status: "Todo", + title: "Make services optional.", + closed: false, + color: "blue", + }, + { + status: "Done", + title: "Optimize code-server.", + closed: true, + color: "green", + }, + { + status: "Todo", + title: "No more alpine.", + closed: false, + color: "blue", + }, + { + status: "Done", + title: "No netease music.", + closed: true, + color: "green", + }, + { + status: "Done", + title: "Draft issue status in www TODOs.", + closed: true, + color: "green", + }, + { + status: "Done", + title: "Re-bootstrap front end.", + closed: true, + color: "green", + }, + { + status: "Todo", + title: "Clean react imports for new jsx usage.", + closed: false, + color: "blue", + }, + { + status: "Done", + title: "i18next backend bug.", + closed: true, + color: "green", + }, + { + status: "Done", + title: "Organize buttons.", + closed: true, + color: "green", + }, + { + status: "Done", + title: "Fix dialog typo.", + closed: true, + color: "green", + }, + { + status: "Todo", + title: "Organize OperationDialog.", + closed: false, + color: "blue", + }, + { + status: "Todo", + title: "New palette api.", + closed: false, + color: "blue", + }, + { + status: "Todo", + title: "No Docker!!!", + closed: false, + color: "blue", + }, + { + status: "Done", + title: "Improve animation of slogan.", + closed: true, + color: "green", + }, +]; + +export default todos; diff --git a/docker/nginx/sites/www/src/style.css b/docker/nginx/sites/www/src/style.css new file mode 100644 index 0000000..1f9c9ed --- /dev/null +++ b/docker/nginx/sites/www/src/style.css @@ -0,0 +1,185 @@ +html { + width: 100%; + line-height: 1.5; + font-family: ui-sans-serif; +} + +body { + width: 100%; + margin: 0; + box-sizing: border-box; +} + +a.mono { + font-family: ui-monospace; +} + +.h-note { + font-size: 0.6em; + color: gray; +} + +@keyframes article-enter { + from { + opacity: 0; + transform: translateY(100px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +:root { + --main-article-horizontal-padding: 1em; + --main-article-horizontal-margin-shrink: -1em; + + --im-me: deepskyblue; + --im-happy: dodgerblue; + --im-angry: orangered; + --im-good: hsl(120, 85%, 85%); + --im-active: hsl(20, 85%, 85%); +} + +@media (min-width: 576px) { + :root { + --main-article-horizontal-padding: 2em; + --main-article-horizontal-margin-shrink: -2em; + } +} + +#main-article { + padding: 0 var(--main-article-horizontal-padding); + animation: article-enter 1s; +} + +#title { + font-size: 2em; +} + +@keyframes title-name-enter { + from { + background-color: white; + } + + to { + background-color: var(--im-me); + } +} + +#title-name { + font-family: ui-monospace; + border-radius: 8px; + background-color: white; + animation: title-name-enter 3s 2s forwards; +} + +@keyframes avatar-enter { + from { + opacity: 0; + transform: translateX(calc(100% + var(--main-article-horizontal-padding))); + } + + to { + opacity: 1; + transform: translateX(0); + } +} + +#avatar { + float: right; + transform: translateX(calc(100% + var(--main-article-horizontal-padding))); + animation: avatar-enter 0.5s 1s forwards; +} + +.slogan-container { + width: 100%; + position: fixed; + z-index: 1; + top: 0; +} + +.slogan { + width: 100%; + padding: 0.5em 1em; + text-align: center; + box-sizing: border-box; + position: absolute; + transform: translateY(-100%); + transition: transform 1s; +} + +.slogan.happy { + background-color: var(--im-happy); +} + +.slogan.angry { + background-color: var(--im-angry); +} + +.slogan-container[data-slogan-emotion="happy"] .slogan.happy { + transform: translateY(0); +} + +.slogan-container[data-slogan-emotion="angry"] .slogan.angry { + transform: translateY(0); +} + +.slogan-text { + display: inline-block; + text-align: initial; + color: white; + font-size: 1.2em; +} + +#todo-container { + list-style: none; + margin-block: 0; + padding-inline: 0; +} + +@keyframes todo-enter { + from { + opacity: 0; + transform: translateX(-100%); + } + + to { + opacity: 1; + transform: translateX(0); + } +} + +#todo-container li { + margin: 0 var(--main-article-horizontal-margin-shrink); + padding: 0.25em 3em; + transform: translateX(-100%); + animation: todo-enter 1s forwards; +} + +#todo-container li[data-status="closed"] { + background-color: var(--im-good); +} + +#todo-container li[data-status="open"] { + background-color: var(--im-active); +} + +.friend-link { + display: inline-block; +} + +.friend-img { + display: block; + width: 80px; + height: 80px; + object-fit: cover; + border-radius: 50%; +} + +.friend-name { + display: block; + text-align: center; + font-size: 1.2em; +} diff --git a/docker/nginx/sites/www/src/todos.ts b/docker/nginx/sites/www/src/todos.ts new file mode 100644 index 0000000..b69f524 --- /dev/null +++ b/docker/nginx/sites/www/src/todos.ts @@ -0,0 +1,29 @@ +export interface Todo { + status: string; + title: string; + closed: boolean; + color: string; +} + +export async function fetchTodos(): Promise { + console.log("Try to fetch TODOs from server."); + + if (process.env.NODE_ENV !== "production") { + console.log("YaY! We are developers. 🍻 Use mock TODOs. (After 2s)"); + // await new Promise((resolve) => setTimeout(resolve, 2000)); + return (await import("./mock-todos")).default; + } else { + const res = await fetch("/api/todos"); + const body: Todo[] = await res.json(); + + if (res.status !== 200) { + console.error( + `Failed to get TODOs. Status: ${res.status}. Body: ${body}`, + ); + throw new Error( + "Failed to fetch TODOs. (Maybe due to rate limit. Please try later.)", + ); + } + return body; + } +} diff --git a/docker/nginx/sites/www/tsconfig.json b/docker/nginx/sites/www/tsconfig.json new file mode 100644 index 0000000..9d1434c --- /dev/null +++ b/docker/nginx/sites/www/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/recommended/tsconfig.json", + "compilerOptions": { + "lib": [ + "ESNext", + "DOM", + "DOM.Iterable" + ], + "types": [ + "parcel-env" + ], + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true + } +} \ No newline at end of file diff --git a/docker/v2ray/Dockerfile b/docker/v2ray/Dockerfile new file mode 100644 index 0000000..003b545 --- /dev/null +++ b/docker/v2ray/Dockerfile @@ -0,0 +1,6 @@ +FROM alpine:edge + +RUN apk add --no-cache v2ray + +ENTRYPOINT [ "/usr/bin/v2ray" ] + diff --git a/dropped/docker/crupest-api/CrupestApi/.dockerignore b/dropped/docker/crupest-api/CrupestApi/.dockerignore new file mode 100644 index 0000000..f1c182d --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/.dockerignore @@ -0,0 +1,2 @@ +*/obj +*/bin diff --git a/dropped/docker/crupest-api/CrupestApi/.gitignore b/dropped/docker/crupest-api/CrupestApi/.gitignore new file mode 100644 index 0000000..371ea59 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/.gitignore @@ -0,0 +1,4 @@ +.vs +obj +bin +dev-config.json diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/ColumnTypeInfoTest.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/ColumnTypeInfoTest.cs new file mode 100644 index 0000000..b9ec03e --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/ColumnTypeInfoTest.cs @@ -0,0 +1,39 @@ +using System.Data; + +namespace CrupestApi.Commons.Crud.Tests; + +public class ColumnTypeInfoTest +{ + private ColumnTypeProvider _provider = new ColumnTypeProvider(); + + [Theory] + [InlineData(typeof(int), DbType.Int32, 123)] + [InlineData(typeof(long), DbType.Int64, 456)] + [InlineData(typeof(sbyte), DbType.SByte, 789)] + [InlineData(typeof(short), DbType.Int16, 101)] + [InlineData(typeof(float), DbType.Single, 1.0f)] + [InlineData(typeof(double), DbType.Double, 1.0)] + [InlineData(typeof(string), DbType.String, "Hello world!")] + [InlineData(typeof(byte[]), DbType.Binary, new byte[] { 1, 2, 3 })] + public void BasicColumnTypeTest(Type type, DbType dbType, object? value) + { + var typeInfo = _provider.Get(type); + Assert.True(typeInfo.IsSimple); + Assert.Equal(dbType, typeInfo.DbType); + Assert.Equal(value, typeInfo.ConvertFromDatabase(value)); + Assert.Equal(value, typeInfo.ConvertToDatabase(value)); + } + + [Fact] + public void DateTimeColumnTypeTest() + { + var dateTimeColumnTypeInfo = _provider.Get(typeof(DateTime)); + Assert.Equal(typeof(DateTime), dateTimeColumnTypeInfo.ClrType); + Assert.Equal(typeof(string), dateTimeColumnTypeInfo.DatabaseClrType); + + var dateTime = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var dateTimeString = "2000-01-01T00:00:00Z"; + Assert.Equal(dateTimeString, dateTimeColumnTypeInfo.ConvertToDatabase(dateTime)); + Assert.Equal(dateTime, dateTimeColumnTypeInfo.ConvertFromDatabase(dateTimeString)); + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudIntegratedTest.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudIntegratedTest.cs new file mode 100644 index 0000000..bd07c70 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudIntegratedTest.cs @@ -0,0 +1,200 @@ +using System.Net; +using System.Net.Http.Headers; +using CrupestApi.Commons.Secrets; +using Microsoft.AspNetCore.TestHost; + +namespace CrupestApi.Commons.Crud.Tests; + +public class CrudIntegratedTest : IAsyncLifetime +{ + private readonly WebApplication _app; + private HttpClient _httpClient = default!; + private HttpClient _authorizedHttpClient = default!; + private string _token = default!; + + public CrudIntegratedTest() + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + builder.Services.AddSingleton(); + builder.Services.AddCrud(); + builder.WebHost.UseTestServer(); + _app = builder.Build(); + _app.UseCrudCore(); + _app.MapCrud("/test", "test-perm"); + } + + public async Task InitializeAsync() + { + await _app.StartAsync(); + _httpClient = _app.GetTestClient(); + + using (var scope = _app.Services.CreateScope()) + { + var secretService = (SecretService)scope.ServiceProvider.GetRequiredService(); + var key = secretService.Create(new SecretInfo + { + Key = "test-perm" + }); + _token = secretService.GetByKey(key).Secret; + } + + _authorizedHttpClient = _app.GetTestClient(); + _authorizedHttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token); + } + + public async Task DisposeAsync() + { + await _app.StopAsync(); + } + + + [Fact] + public async Task EmptyTest() + { + using var response = await _authorizedHttpClient.GetAsync("/test"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync>(); + Assert.NotNull(body); + Assert.Empty(body); + } + + [Fact] + public async Task CrudTest() + { + { + using var response = await _authorizedHttpClient.PostAsJsonAsync("/test", new TestEntity + { + Name = "test", + Age = 22 + }); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(body); + Assert.Equal("test", body.Name); + Assert.Equal(22, body.Age); + Assert.Null(body.Height); + Assert.NotEmpty(body.Secret); + } + + { + using var response = await _authorizedHttpClient.GetAsync("/test"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync>(); + Assert.NotNull(body); + var entity = Assert.Single(body); + Assert.Equal("test", entity.Name); + Assert.Equal(22, entity.Age); + Assert.Null(entity.Height); + Assert.NotEmpty(entity.Secret); + } + + { + using var response = await _authorizedHttpClient.GetAsync("/test/test"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(body); + Assert.Equal("test", body.Name); + Assert.Equal(22, body.Age); + Assert.Null(body.Height); + Assert.NotEmpty(body.Secret); + } + + { + using var response = await _authorizedHttpClient.PatchAsJsonAsync("/test/test", new TestEntity + { + Name = "test-2", + Age = 23, + Height = 188.0f + }); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(body); + Assert.Equal("test-2", body.Name); + Assert.Equal(23, body.Age); + Assert.Equal(188.0f, body.Height); + Assert.NotEmpty(body.Secret); + } + + { + using var response = await _authorizedHttpClient.GetAsync("/test/test-2"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(body); + Assert.Equal("test-2", body.Name); + Assert.Equal(23, body.Age); + Assert.Equal(188.0f, body.Height); + Assert.NotEmpty(body.Secret); + } + + { + using var response = await _authorizedHttpClient.DeleteAsync("/test/test-2"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + { + using var response = await _authorizedHttpClient.GetAsync("/test"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync>(); + Assert.NotNull(body); + Assert.Empty(body); + } + } + + [Fact] + public async Task UnauthorizedTest() + { + { + using var response = await _httpClient.GetAsync("/test"); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + { + using var response = await _httpClient.GetAsync("/test/test"); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + { + using var response = await _httpClient.PostAsJsonAsync("/test", new TestEntity + { + Name = "test", + Age = 22 + }); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + { + using var response = await _httpClient.PatchAsJsonAsync("/test/test", new TestEntity + { + Name = "test-2", + Age = 23, + Height = 188.0f + }); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + { + using var response = await _httpClient.DeleteAsync("/test/test"); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + } + + [Fact] + public async Task NotFoundTest() + { + { + using var response = await _authorizedHttpClient.GetAsync("/test/test"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + { + using var response = await _authorizedHttpClient.PatchAsJsonAsync("/test/test", new TestEntity + { + Name = "test-2", + Age = 23, + Height = 188.0f + }); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudServiceTest.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudServiceTest.cs new file mode 100644 index 0000000..ad0d34c --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudServiceTest.cs @@ -0,0 +1,77 @@ +using CrupestApi.Commons.Crud.Migrations; +using Microsoft.Extensions.Logging.Abstractions; + +namespace CrupestApi.Commons.Crud.Tests; + +public class CrudServiceTest +{ + private readonly SqliteMemoryConnectionFactory _memoryConnectionFactory = new SqliteMemoryConnectionFactory(); + + private readonly CrudService _crudService; + + public CrudServiceTest() + { + var columnTypeProvider = new ColumnTypeProvider(); + var tableInfoFactory = new TableInfoFactory(columnTypeProvider, NullLoggerFactory.Instance); + var dbConnectionFactory = new SqliteMemoryConnectionFactory(); + + _crudService = new CrudService( + tableInfoFactory, dbConnectionFactory, new SqliteDatabaseMigrator(), NullLoggerFactory.Instance); + } + + [Fact] + public void CrudTest() + { + var key = _crudService.Create(new TestEntity() + { + Name = "crupest", + Age = 18, + }); + + Assert.Equal("crupest", key); + + var entity = _crudService.GetByKey(key); + Assert.Equal("crupest", entity.Name); + Assert.Equal(18, entity.Age); + Assert.Null(entity.Height); + Assert.NotEmpty(entity.Secret); + + var list = _crudService.GetAll(); + entity = Assert.Single(list); + Assert.Equal("crupest", entity.Name); + Assert.Equal(18, entity.Age); + Assert.Null(entity.Height); + Assert.NotEmpty(entity.Secret); + + var count = _crudService.GetCount(); + Assert.Equal(1, count); + + _crudService.UpdateByKey(key, new TestEntity() + { + Name = "crupest2.0", + Age = 22, + Height = 180, + }); + + entity = _crudService.GetByKey("crupest2.0"); + Assert.Equal("crupest2.0", entity.Name); + Assert.Equal(22, entity.Age); + Assert.Equal(180, entity.Height); + Assert.NotEmpty(entity.Secret); + + _crudService.DeleteByKey("crupest2.0"); + + count = _crudService.GetCount(); + Assert.Equal(0, count); + } + + [Fact] + public void EntityNotExistTest() + { + Assert.Throws(() => _crudService.GetByKey("KeyNotExist")); + Assert.Throws(() => _crudService.UpdateByKey("KeyNotExist", new TestEntity + { + Name = "crupest" + })); + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/SqlCompareHelper.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/SqlCompareHelper.cs new file mode 100644 index 0000000..72b6218 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/SqlCompareHelper.cs @@ -0,0 +1,85 @@ +using System.Text; + +namespace CrupestApi.Commons.Crud.Tests; + +public class SqlCompareHelper +{ + private static List SymbolTokens = new List() { '(', ')', ';' }; + + public static List SqlExtractWords(string? sql, bool toLower = true) + { + var result = new List(); + + if (string.IsNullOrEmpty(sql)) + { + return result; + } + + var current = 0; + + StringBuilder? wordBuilder = null; + + while (current < sql.Length) + { + if (char.IsWhiteSpace(sql[current])) + { + if (wordBuilder is not null) + { + result.Add(wordBuilder.ToString()); + wordBuilder = null; + } + } + else if (SymbolTokens.Contains(sql[current])) + { + if (wordBuilder is not null) + { + result.Add(wordBuilder.ToString()); + wordBuilder = null; + } + result.Add(sql[current].ToString()); + } + else + { + if (wordBuilder is not null) + { + wordBuilder.Append(sql[current]); + } + else + { + wordBuilder = new StringBuilder(); + wordBuilder.Append(sql[current]); + } + } + current++; + } + + if (wordBuilder is not null) + { + result.Add(wordBuilder.ToString()); + } + + if (toLower) + { + for (int i = 0; i < result.Count; i++) + { + result[i] = result[i].ToLower(); + } + } + + return result; + } + + public static bool SqlEqual(string left, string right) + { + return SqlExtractWords(left) == SqlExtractWords(right); + } + + [Fact] + public void TestSqlExtractWords() + { + var sql = "SELECT * FROM TableName WHERE (id = @abcd);"; + var words = SqlExtractWords(sql); + + Assert.Equal(new List { "select", "*", "from", "tablename", "where", "(", "id", "=", "@abcd", ")", ";" }, words); + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TableInfoTest.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TableInfoTest.cs new file mode 100644 index 0000000..b0aa702 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TableInfoTest.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Logging.Abstractions; + +namespace CrupestApi.Commons.Crud.Tests; + +public class TableInfoTest +{ + private static TableInfoFactory TableInfoFactory = new TableInfoFactory(new ColumnTypeProvider(), NullLoggerFactory.Instance); + + private TableInfo _tableInfo; + + public TableInfoTest() + { + _tableInfo = TableInfoFactory.Get(typeof(TestEntity)); + } + + [Fact] + public void TestColumnCount() + { + Assert.Equal(5, _tableInfo.Columns.Count); + Assert.Equal(4, _tableInfo.PropertyColumns.Count); + Assert.Equal(4, _tableInfo.ColumnProperties.Count); + Assert.Equal(1, _tableInfo.NonColumnProperties.Count); + } + + [Fact] + public void GenerateSelectSqlTest() + { + var (sql, parameters) = _tableInfo.GenerateSelectSql(null, WhereClause.Create().Eq("Name", "Hello")); + var parameterName = parameters.First().Name; + + // TODO: Is there a way to auto detect parameters? + SqlCompareHelper.SqlEqual($"SELECT * FROM TestEntity WHERE (Name = @{parameterName})", sql); + Assert.Equal("Hello", parameters.Get(parameterName)); + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TestEntity.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TestEntity.cs new file mode 100644 index 0000000..c15334c --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TestEntity.cs @@ -0,0 +1,23 @@ +namespace CrupestApi.Commons.Crud.Tests; + +public class TestEntity +{ + [Column(ActAsKey = true, NotNull = true)] + public string Name { get; set; } = default!; + + [Column(NotNull = true)] + public int Age { get; set; } + + [Column] + public float? Height { get; set; } + + [Column(OnlyGenerated = true, NotNull = true, NoUpdate = true)] + public string Secret { get; set; } = default!; + + public static string SecretDefaultValueGenerator() + { + return "secret"; + } + + public string NonColumn { get; set; } = "Not A Column"; +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/CrupestApi.Commons.Tests.csproj b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/CrupestApi.Commons.Tests.csproj new file mode 100644 index 0000000..0360ee1 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/CrupestApi.Commons.Tests.csproj @@ -0,0 +1,29 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + \ No newline at end of file diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Usings.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Config.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Config.cs new file mode 100644 index 0000000..0ca3547 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Config.cs @@ -0,0 +1,23 @@ +namespace CrupestApi.Commons; + +public class CrupestApiConfig +{ + public string DataDir { get; set; } = string.Empty; +} + +public static class CrupestApiConfigExtensions +{ + public static IServiceCollection AddCrupestApiConfig(this IServiceCollection services) + { + services.AddOptions().BindConfiguration("CrupestApi"); + services.PostConfigure(config => + { + if (config.DataDir is null || config.DataDir.Length == 0) + { + config.DataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "crupest-api"); + } + }); + + return services; + } +} \ No newline at end of file diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs new file mode 100644 index 0000000..e8d3c2e --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs @@ -0,0 +1,236 @@ +using System.Diagnostics; +using System.Reflection; +using System.Text; + +namespace CrupestApi.Commons.Crud; + +public class ColumnInfo +{ + private readonly AggregateColumnMetadata _metadata = new AggregateColumnMetadata(); + private ILogger _logger; + + /// + /// Initialize a column without corresponding property. + /// + public ColumnInfo(TableInfo table, IColumnMetadata metadata, Type clrType, IColumnTypeProvider typeProvider, ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + if (metadata is null) + throw new ArgumentException("You must specify metadata for non-property column."); + if (metadata.TryGetValue(ColumnMetadataKeys.ColumnName, out var columnName)) + _logger.LogInformation("Create column without property.", columnName); + else + throw new ArgumentException("You must specify name in metadata for non-property column."); + + Table = table; + _metadata.Add(metadata); + ColumnType = typeProvider.Get(clrType); + } + + /// + /// Initialize a column with corresponding property. + /// + public ColumnInfo(TableInfo table, PropertyInfo propertyInfo, IColumnTypeProvider typeProvider, ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + _logger.LogInformation("Create column with property {}.", propertyInfo.Name); + + Table = table; + PropertyInfo = propertyInfo; + ColumnType = typeProvider.Get(propertyInfo.PropertyType); + + var columnAttribute = propertyInfo.GetCustomAttribute(); + if (columnAttribute is not null) + { + _metadata.Add(columnAttribute); + } + } + + public TableInfo Table { get; } + + public Type EntityType => Table.EntityType; + + // If null, there is no corresponding property. + public PropertyInfo? PropertyInfo { get; } = null; + + public IColumnMetadata Metadata => _metadata; + + public IColumnTypeInfo ColumnType { get; } + + public bool IsPrimaryKey => Metadata.GetValueOrDefault(ColumnMetadataKeys.IsPrimaryKey) is true; + public bool IsAutoIncrement => IsPrimaryKey; + public bool IsNotNull => IsPrimaryKey || Metadata.GetValueOrDefault(ColumnMetadataKeys.NotNull) is true; + public bool IsOnlyGenerated => Metadata.GetValueOrDefault(ColumnMetadataKeys.OnlyGenerated) is true; + public bool IsNoUpdate => Metadata.GetValueOrDefault(ColumnMetadataKeys.NoUpdate) is true; + public object? DefaultValue => Metadata.GetValueOrDefault(ColumnMetadataKeys.DefaultValue); + /// + /// This only returns metadata value. It doesn't not fall back to primary column. If you want to get the real key column, go to table info. + /// + /// + /// + public bool IsSpecifiedAsKey => Metadata.GetValueOrDefault(ColumnMetadataKeys.ActAsKey) is true; + public ColumnIndexType Index => Metadata.GetValueOrDefault(ColumnMetadataKeys.Index) ?? ColumnIndexType.None; + + /// + /// Whether the column value can be generated, which means the column has a default value or a default value generator or is AUTOINCREMENT. + /// + public bool CanBeGenerated => DefaultValue is not null || DefaultValueGeneratorMethod is not null || IsAutoIncrement; + + /// + /// The real column name. Maybe set in metadata or just the property name. + /// + /// + public string ColumnName + { + get + { + object? value = Metadata.GetValueOrDefault(ColumnMetadataKeys.ColumnName); + Debug.Assert(value is null || value is string); + return ((string?)value ?? PropertyInfo?.Name) ?? throw new Exception("Failed to get column name."); + } + } + + public MethodInfo? DefaultValueGeneratorMethod + { + get + { + object? value = Metadata.GetValueOrDefault(ColumnMetadataKeys.DefaultValueGenerator); + Debug.Assert(value is null || value is string); + MethodInfo? result; + if (value is null) + { + string methodName = ColumnName + "DefaultValueGenerator"; + result = Table.EntityType.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static); + } + else + { + string methodName = (string)value; + result = Table.EntityType.GetMethod(methodName, BindingFlags.Static) ?? throw new Exception("The default value generator does not exist."); + } + + return result; + } + } + + public MethodInfo? ValidatorMethod + { + get + { + object? value = Metadata.GetValueOrDefault(ColumnMetadataKeys.DefaultValueGenerator); + Debug.Assert(value is null || value is string); + MethodInfo? result; + if (value is null) + { + string methodName = ColumnName + "Validator"; + result = Table.EntityType.GetMethod(methodName, BindingFlags.Static); + } + else + { + string methodName = (string)value; + result = Table.EntityType.GetMethod(methodName, BindingFlags.Static) ?? throw new Exception("The validator does not exist."); + } + + return result; + } + } + + public void InvokeValidator(object? value) + { + var method = this.ValidatorMethod; + if (method is null) + { + _logger.LogInformation("Try to invoke validator for column {} but it does not exist.", ColumnName); + return; + } + var parameters = method.GetParameters(); + if (parameters.Length == 0) + { + throw new Exception("The validator method must have at least one parameter."); + } + else if (parameters.Length == 1) + { + method.Invoke(null, new object?[] { value }); + } + else if (parameters.Length == 2) + { + if (parameters[0].ParameterType == typeof(ColumnInfo)) + method.Invoke(null, new object?[] { this, value }); + else if (parameters[1].ParameterType == typeof(ColumnInfo)) + method.Invoke(null, new object?[] { value, this }); + else + throw new Exception("The validator method must have a parameter of type ColumnInfo if it has 2 parameters."); + } + else + { + throw new Exception("The validator method can only have 1 or 2 parameters."); + } + } + + public object? InvokeDefaultValueGenerator() + { + var method = this.DefaultValueGeneratorMethod; + if (method is null) + { + _logger.LogInformation("Try to invoke default value generator for column {} but it does not exist.", ColumnName); + return null; + } + var parameters = method.GetParameters(); + if (parameters.Length == 0) + { + return method.Invoke(null, new object?[0]); + } + else if (parameters.Length == 1) + { + if (parameters[0].ParameterType != typeof(ColumnInfo)) + throw new Exception("The default value generator method can only have a parameter of type ColumnInfo."); + return method.Invoke(null, new object?[] { this }); + } + else + { + throw new Exception("The default value generator method can only have 0 or 1 parameter."); + } + } + + public object? GenerateDefaultValue() + { + if (DefaultValueGeneratorMethod is not null) + { + return InvokeDefaultValueGenerator(); + } + + if (Metadata.TryGetValue(ColumnMetadataKeys.DefaultValue, out object? value)) + { + return value; + } + else + { + return null; + } + } + + public string GenerateCreateTableColumnString(string? dbProviderId = null) + { + StringBuilder result = new StringBuilder(); + result.Append(ColumnName); + result.Append(' '); + result.Append(ColumnType.GetSqlTypeString(dbProviderId)); + if (IsPrimaryKey) + { + result.Append(' '); + result.Append("PRIMARY KEY"); + } + else if (IsNotNull) + { + result.Append(' '); + result.Append("NOT NULL"); + } + + if (IsAutoIncrement) + { + result.Append(' '); + result.Append("AUTOINCREMENT"); + } + + return result.ToString(); + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs new file mode 100644 index 0000000..7247ff1 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs @@ -0,0 +1,188 @@ +namespace CrupestApi.Commons.Crud; + +public static class ColumnMetadataKeys +{ + public const string ColumnName = nameof(ColumnAttribute.ColumnName); + public const string NotNull = nameof(ColumnAttribute.NotNull); + public const string IsPrimaryKey = nameof(ColumnAttribute.IsPrimaryKey); + public const string Index = nameof(ColumnAttribute.Index); + + /// + /// This will add hooks for string type column to coerce null to ""(empty string) when get or set. No effect on non-string type. + /// + public const string DefaultEmptyForString = nameof(ColumnAttribute.DefaultEmptyForString); + + /// + /// This indicates that you take care of generate this column value when create entity. User calling the api can not specify the value. + /// + public const string OnlyGenerated = nameof(ColumnAttribute.OnlyGenerated); + + /// + /// The default value generator method name in entity type. Default to null, aka, search for ColumnNameDefaultValueGenerator. + /// Generator has signature static void DefaultValueGenerator(ColumnInfo column) + /// + public const string DefaultValueGenerator = nameof(ColumnAttribute.DefaultValueGenerator); + + /// + /// The validator method name in entity type. Default to null, aka, the default validator. + /// Validator has signature static void Validator(ColumnInfo column, object value) + /// Value param is never null. If you want to mean NULL, it should be a . + /// + public const string Validator = nameof(ColumnAttribute.Validator); + + /// + /// The column can only be set when inserted, can't be changed in update. + /// + /// + public const string NoUpdate = nameof(ColumnAttribute.NoUpdate); + + /// + /// This column acts as key when get one entity for http get method in path. + /// + public const string ActAsKey = nameof(ColumnAttribute.ActAsKey); + + /// + /// The default value used for the column. + /// + public const string DefaultValue = nameof(ColumnAttribute.DefaultValue); +} + +public interface IColumnMetadata +{ + bool TryGetValue(string key, out object? value); + + object? GetValueOrDefault(string key) + { + if (TryGetValue(key, out var value)) + { + return value; + } + else + { + return null; + } + } + + T? GetValueOrDefault(string key) + { + return (T?)GetValueOrDefault(key); + } + + object? this[string key] + { + get + { + if (TryGetValue(key, out var value)) + { + return value; + } + else + { + throw new KeyNotFoundException("Key not found."); + } + } + } +} + +public enum ColumnIndexType +{ + None, + Unique, + NonUnique +} + +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public class ColumnAttribute : Attribute, IColumnMetadata +{ + // if null, use the property name. + public string? ColumnName { get; init; } + + // default false. + public bool NotNull { get; init; } + + // default false + public bool IsPrimaryKey { get; init; } + + // default None + public ColumnIndexType Index { get; init; } = ColumnIndexType.None; + + /// + public bool DefaultEmptyForString { get; init; } + + /// + public bool OnlyGenerated { get; init; } + + /// + public string? DefaultValueGenerator { get; init; } + + /// + public string? Validator { get; init; } + + /// + public bool NoUpdate { get; init; } + + /// + public bool ActAsKey { get; init; } + + public object? DefaultValue { get; init; } + + public bool TryGetValue(string key, out object? value) + { + var property = GetType().GetProperty(key); + if (property is null) + { + value = null; + return false; + } + value = property.GetValue(this); + return true; + } +} + +public class AggregateColumnMetadata : IColumnMetadata +{ + private IDictionary _own = new Dictionary(); + private IList _children = new List(); + + public void Add(string key, object? value) + { + _own[key] = value; + } + + public void Remove(string key) + { + _own.Remove(key); + } + + public void Add(IColumnMetadata child) + { + _children.Add(child); + } + + public void Remove(IColumnMetadata child) + { + _children.Remove(child); + } + + public bool TryGetValue(string key, out object? value) + { + if (_own.ContainsKey(key)) + { + value = _own[key]; + return true; + } + + bool found = false; + value = null; + foreach (var child in _children) + { + if (child.TryGetValue(key, out var tempValue)) + { + value = tempValue; + found = true; + } + } + + return found; + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs new file mode 100644 index 0000000..19eff52 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs @@ -0,0 +1,218 @@ +using System.Data; +using System.Diagnostics; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CrupestApi.Commons.Crud; + +public interface IColumnTypeInfo +{ + public static IColumnTypeInfo BoolColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); + public static IColumnTypeInfo IntColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); + public static IColumnTypeInfo ShortColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); + public static IColumnTypeInfo SByteColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); + public static IColumnTypeInfo LongColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); + public static IColumnTypeInfo FloatColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); + public static IColumnTypeInfo DoubleColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); + public static IColumnTypeInfo StringColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); + public static IColumnTypeInfo BytesColumnTypeInfo { get; } = new SimpleColumnTypeInfo(); + public static IColumnTypeInfo DateTimeColumnTypeInfo { get; } = new DateTimeColumnTypeInfo(); + + Type ClrType { get; } + Type DatabaseClrType { get; } + bool IsSimple { get { return ClrType == DatabaseClrType; } } + DbType DbType + { + get + { + if (DatabaseClrType == typeof(bool)) + { + return DbType.Boolean; + } + else if (DatabaseClrType == typeof(int)) + { + return DbType.Int32; + } + else if (DatabaseClrType == typeof(long)) + { + return DbType.Int64; + } + else if (DatabaseClrType == typeof(short)) + { + return DbType.Int16; + } + else if (DatabaseClrType == typeof(sbyte)) + { + return DbType.SByte; + } + else if (DatabaseClrType == typeof(double)) + { + return DbType.Double; + } + else if (DatabaseClrType == typeof(float)) + { + return DbType.Single; + } + else if (DatabaseClrType == typeof(string)) + { + return DbType.String; + } + else if (DatabaseClrType == typeof(byte[])) + { + return DbType.Binary; + } + else + { + throw new Exception("Can't deduce DbType."); + } + } + } + + string GetSqlTypeString(string? dbProviderId = null) + { + // Default implementation for SQLite + return DbType switch + { + DbType.String => "TEXT", + DbType.Boolean or DbType.Int16 or DbType.Int32 or DbType.Int64 => "INTEGER", + DbType.Single or DbType.Double => "REAL", + DbType.Binary => "BLOB", + _ => throw new Exception($"Unsupported DbType: {DbType}"), + }; + } + + JsonConverter? JsonConverter { get { return null; } } + + // You must override this method if ClrType != DatabaseClrType + object? ConvertFromDatabase(object? databaseValue) + { + Debug.Assert(IsSimple); + return databaseValue; + } + + // You must override this method if ClrType != DatabaseClrType + object? ConvertToDatabase(object? value) + { + Debug.Assert(IsSimple); + return value; + } +} + +public interface IColumnTypeProvider +{ + IReadOnlyList GetAll(); + IColumnTypeInfo Get(Type clrType); + + IList GetAllCustom() + { + return GetAll().Where(t => !t.IsSimple).ToList(); + } +} + +public class SimpleColumnTypeInfo : IColumnTypeInfo +{ + public Type ClrType => typeof(T); + public Type DatabaseClrType => typeof(T); +} + +public class DateTimeColumnTypeInfo : IColumnTypeInfo +{ + private JsonConverter _jsonConverter; + + public DateTimeColumnTypeInfo() + { + _jsonConverter = new DateTimeJsonConverter(this); + } + + public Type ClrType => typeof(DateTime); + public Type DatabaseClrType => typeof(string); + + public JsonConverter JsonConverter => _jsonConverter; + + public object? ConvertToDatabase(object? value) + { + if (value is null) return null; + Debug.Assert(value is DateTime); + return ((DateTime)value).ToUniversalTime().ToString("s") + "Z"; + } + + public object? ConvertFromDatabase(object? databaseValue) + { + if (databaseValue is null) return null; + Debug.Assert(databaseValue is string); + var databaseString = (string)databaseValue; + var dateTimeStyles = DateTimeStyles.None; + if (databaseString.Length > 0 && databaseString[^1] == 'Z') + { + databaseString = databaseString.Substring(0, databaseString.Length - 1); + dateTimeStyles = DateTimeStyles.AssumeUniversal & DateTimeStyles.AdjustToUniversal; + } + return DateTime.ParseExact(databaseString, "s", null, dateTimeStyles); + } +} + +public class DateTimeJsonConverter : JsonConverter +{ + private readonly DateTimeColumnTypeInfo _typeInfo; + + public DateTimeJsonConverter(DateTimeColumnTypeInfo typeInfo) + { + _typeInfo = typeInfo; + } + + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var databaseValue = reader.GetString(); + return (DateTime)_typeInfo.ConvertFromDatabase(databaseValue)!; + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + var databaseValue = _typeInfo.ConvertToDatabase(value); + writer.WriteStringValue((string)databaseValue!); + } +} + +public class ColumnTypeProvider : IColumnTypeProvider +{ + private Dictionary _typeMap = new Dictionary(); + + public ColumnTypeProvider() + { + _typeMap.Add(IColumnTypeInfo.BoolColumnTypeInfo.ClrType, IColumnTypeInfo.BoolColumnTypeInfo); + _typeMap.Add(IColumnTypeInfo.IntColumnTypeInfo.ClrType, IColumnTypeInfo.IntColumnTypeInfo); + _typeMap.Add(IColumnTypeInfo.ShortColumnTypeInfo.ClrType, IColumnTypeInfo.ShortColumnTypeInfo); + _typeMap.Add(IColumnTypeInfo.SByteColumnTypeInfo.ClrType, IColumnTypeInfo.SByteColumnTypeInfo); + _typeMap.Add(IColumnTypeInfo.LongColumnTypeInfo.ClrType, IColumnTypeInfo.LongColumnTypeInfo); + _typeMap.Add(IColumnTypeInfo.FloatColumnTypeInfo.ClrType, IColumnTypeInfo.FloatColumnTypeInfo); + _typeMap.Add(IColumnTypeInfo.DoubleColumnTypeInfo.ClrType, IColumnTypeInfo.DoubleColumnTypeInfo); + _typeMap.Add(IColumnTypeInfo.StringColumnTypeInfo.ClrType, IColumnTypeInfo.StringColumnTypeInfo); + _typeMap.Add(IColumnTypeInfo.BytesColumnTypeInfo.ClrType, IColumnTypeInfo.BytesColumnTypeInfo); + _typeMap.Add(IColumnTypeInfo.DateTimeColumnTypeInfo.ClrType, IColumnTypeInfo.DateTimeColumnTypeInfo); + } + + public IReadOnlyList GetAll() + { + return _typeMap.Values.ToList(); + } + + // This is thread-safe. + public IColumnTypeInfo Get(Type clrType) + { + if (_typeMap.TryGetValue(clrType, out var typeInfo)) + { + return typeInfo; + } + else + { + if (clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + clrType = clrType.GetGenericArguments()[0]; + return Get(clrType); + } + + throw new Exception($"Unsupported type: {clrType}"); + } + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs new file mode 100644 index 0000000..1e881d3 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs @@ -0,0 +1,132 @@ +using System.Data; +using CrupestApi.Commons.Crud.Migrations; + +namespace CrupestApi.Commons.Crud; + +[Flags] +public enum UpdateBehavior +{ + None = 0, + SaveNull = 1 +} + +public class CrudService : IDisposable where TEntity : class +{ + protected readonly TableInfo _table; + protected readonly string? _connectionName; + protected readonly IDbConnection _dbConnection; + private readonly bool _shouldDisposeConnection; + private IDatabaseMigrator _migrator; + private readonly ILogger> _logger; + + public CrudService(ITableInfoFactory tableInfoFactory, IDbConnectionFactory dbConnectionFactory, IDatabaseMigrator migrator, ILoggerFactory loggerFactory) + { + _connectionName = GetConnectionName(); + _table = tableInfoFactory.Get(typeof(TEntity)); + _dbConnection = dbConnectionFactory.Get(_connectionName); + _shouldDisposeConnection = dbConnectionFactory.ShouldDisposeConnection; + _migrator = migrator; + _logger = loggerFactory.CreateLogger>(); + } + + protected virtual void EnsureDatabase() + { + if (_migrator.NeedMigrate(_dbConnection, _table)) + { + _logger.LogInformation($"Entity {_table.TableName} needs migration."); + _migrator.AutoMigrate(_dbConnection, _table); + } + } + + protected virtual string GetConnectionName() + { + return typeof(TEntity).Name; + } + + protected virtual void AfterMigrate(IDbConnection dbConnection, TableInfo tableInfo) + { + + } + + public void Dispose() + { + if (_shouldDisposeConnection) + _dbConnection.Dispose(); + } + + public List GetAll() + { + EnsureDatabase(); + var result = _table.Select(_dbConnection, null); + return result; + } + + public int GetCount() + { + EnsureDatabase(); + var result = _table.SelectCount(_dbConnection); + return result; + } + + public TEntity GetByKey(object key) + { + EnsureDatabase(); + var result = _table.Select(_dbConnection, null, WhereClause.Create().Eq(_table.KeyColumn.ColumnName, key)).SingleOrDefault(); + if (result is null) + { + throw new EntityNotExistException($"Required entity for key {key} not found."); + } + return result; + } + + public IInsertClause ConvertEntityToInsertClauses(TEntity entity) + { + var result = new InsertClause(); + foreach (var column in _table.PropertyColumns) + { + var value = column.PropertyInfo!.GetValue(entity); + result.Add(column.ColumnName, value); + } + return result; + } + + public object Create(TEntity entity) + { + EnsureDatabase(); + var insertClause = ConvertEntityToInsertClauses(entity); + _table.Insert(_dbConnection, insertClause, out var key); + return key; + } + + public IUpdateClause ConvertEntityToUpdateClauses(TEntity entity, UpdateBehavior behavior) + { + var result = UpdateClause.Create(); + var saveNull = behavior.HasFlag(UpdateBehavior.SaveNull); + foreach (var column in _table.PropertyColumns) + { + var value = column.PropertyInfo!.GetValue(entity); + if (!saveNull && value is null) continue; + result.Add(column.ColumnName, value); + } + return result; + } + + // Return new key. + public object UpdateByKey(object key, TEntity entity, UpdateBehavior behavior = UpdateBehavior.None) + { + EnsureDatabase(); + var affectedCount = _table.Update(_dbConnection, WhereClause.Create().Eq(_table.KeyColumn.ColumnName, key), + ConvertEntityToUpdateClauses(entity, behavior), out var newKey); + if (affectedCount == 0) + { + throw new EntityNotExistException($"Required entity for key {key} not found."); + } + return newKey ?? key; + } + + public bool DeleteByKey(object key) + { + EnsureDatabase(); + return _table.Delete(_dbConnection, WhereClause.Create().Eq(_table.KeyColumn.ColumnName, key)) == 1; + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudServiceCollectionExtensions.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudServiceCollectionExtensions.cs new file mode 100644 index 0000000..a7e5193 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudServiceCollectionExtensions.cs @@ -0,0 +1,34 @@ +using CrupestApi.Commons.Crud.Migrations; +using CrupestApi.Commons.Secrets; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace CrupestApi.Commons.Crud; + +public static class CrudServiceCollectionExtensions +{ + public static IServiceCollection AddCrudCore(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.AddSecrets(); + return services; + } + + public static IServiceCollection AddCrud(this IServiceCollection services) where TEntity : class where TCrudService : CrudService + { + AddCrudCore(services); + + services.TryAddScoped, TCrudService>(); + services.TryAddScoped>(); + + return services; + } + + public static IServiceCollection AddCrud(this IServiceCollection services) where TEntity : class + { + return services.AddCrud>(); + } + +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs new file mode 100644 index 0000000..8942979 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs @@ -0,0 +1,101 @@ +namespace CrupestApi.Commons.Crud; + +public static class CrudWebApplicationExtensions +{ + public static WebApplication UseCrudCore(this WebApplication app) + { + app.Use(async (context, next) => + { + try + { + await next(); + } + catch (EntityNotExistException) + { + await context.ResponseMessageAsync("Requested entity does not exist.", StatusCodes.Status404NotFound); + } + catch (UserException e) + { + await context.ResponseMessageAsync(e.Message); + } + }); + + return app; + } + + public static WebApplication MapCrud(this WebApplication app, string path, string? permission) where TEntity : class + { + app.MapGet(path, async (context) => + { + if (!context.RequirePermission(permission)) return; + var crudService = context.RequestServices.GetRequiredService>(); + var entityJsonHelper = context.RequestServices.GetRequiredService>(); + var allEntities = crudService.GetAll(); + await context.ResponseJsonAsync(allEntities.Select(e => entityJsonHelper.ConvertEntityToDictionary(e))); + }); + + app.MapGet(path + "/{key}", async (context) => + { + if (!context.RequirePermission(permission)) return; + var crudService = context.RequestServices.GetRequiredService>(); + var entityJsonHelper = context.RequestServices.GetRequiredService>(); + var key = context.Request.RouteValues["key"]?.ToString(); + if (key == null) + { + await context.ResponseMessageAsync("Please specify a key in path."); + return; + } + + var entity = crudService.GetByKey(key); + await context.ResponseJsonAsync(entityJsonHelper.ConvertEntityToDictionary(entity)); + }); + + app.MapPost(path, async (context) => + { + if (!context.RequirePermission(permission)) return; + var crudService = context.RequestServices.GetRequiredService>(); + var entityJsonHelper = context.RequestServices.GetRequiredService>(); + var jsonDocument = await context.Request.ReadJsonAsync(); + var key = crudService.Create(entityJsonHelper.ConvertJsonToEntityForInsert(jsonDocument.RootElement)); + await context.ResponseJsonAsync(entityJsonHelper.ConvertEntityToDictionary(crudService.GetByKey(key))); + }); + + app.MapPatch(path + "/{key}", async (context) => + { + if (!context.RequirePermission(permission)) return; + var key = context.Request.RouteValues["key"]?.ToString(); + var crudService = context.RequestServices.GetRequiredService>(); + var entityJsonHelper = context.RequestServices.GetRequiredService>(); + if (key == null) + { + await context.ResponseMessageAsync("Please specify a key in path."); + return; + } + + var jsonDocument = await context.Request.ReadJsonAsync(); + var entity = entityJsonHelper.ConvertJsonToEntityForUpdate(jsonDocument.RootElement, out var updateBehavior); + var newKey = crudService.UpdateByKey(key, entity, updateBehavior); + await context.ResponseJsonAsync(entityJsonHelper.ConvertEntityToDictionary(crudService.GetByKey(newKey))); + }); + + app.MapDelete(path + "/{key}", async (context) => + { + if (!context.RequirePermission(permission)) return; + var crudService = context.RequestServices.GetRequiredService>(); + var key = context.Request.RouteValues["key"]?.ToString(); + if (key == null) + { + await context.ResponseMessageAsync("Please specify a key in path."); + return; + } + + var deleted = crudService.DeleteByKey(key); + if (deleted) + await context.ResponseMessageAsync("Deleted.", StatusCodes.Status200OK); + else + await context.ResponseMessageAsync("Not exist.", StatusCodes.Status200OK); + }); + + return app; + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs new file mode 100644 index 0000000..701622c --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs @@ -0,0 +1,75 @@ +using System.Data; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Options; + +namespace CrupestApi.Commons.Crud; + +public interface IDbConnectionFactory +{ + IDbConnection Get(string? name = null); + bool ShouldDisposeConnection { get; } +} + +public class SqliteConnectionFactory : IDbConnectionFactory +{ + private readonly IOptionsMonitor _apiConfigMonitor; + + public SqliteConnectionFactory(IOptionsMonitor apiConfigMonitor) + { + _apiConfigMonitor = apiConfigMonitor; + } + + public IDbConnection Get(string? name = null) + { + var connectionString = new SqliteConnectionStringBuilder() + { + DataSource = Path.Combine(_apiConfigMonitor.CurrentValue.DataDir, $"{name ?? "crupest-api"}.db"), + Mode = SqliteOpenMode.ReadWriteCreate + }.ToString(); + + var connection = new SqliteConnection(connectionString); + connection.Open(); + return connection; + } + + public bool ShouldDisposeConnection => true; +} + +public class SqliteMemoryConnectionFactory : IDbConnectionFactory, IDisposable +{ + private readonly Dictionary _connections = new(); + + public IDbConnection Get(string? name = null) + { + name = name ?? "crupest-api"; + + if (_connections.TryGetValue(name, out var connection)) + { + return connection; + } + else + { + var connectionString = new SqliteConnectionStringBuilder() + { + DataSource = ":memory:", + Mode = SqliteOpenMode.ReadWriteCreate + }.ToString(); + + connection = new SqliteConnection(connectionString); + _connections.Add(name, connection); + connection.Open(); + return connection; + } + } + + public bool ShouldDisposeConnection => false; + + + public void Dispose() + { + foreach (var connection in _connections.Values) + { + connection.Dispose(); + } + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbNullValue.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbNullValue.cs new file mode 100644 index 0000000..5dc5a61 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbNullValue.cs @@ -0,0 +1,9 @@ +namespace CrupestApi.Commons.Crud; + +/// +/// This will always represent null value in database. +/// +public class DbNullValue +{ + public static DbNullValue Instance { get; } = new DbNullValue(); +} \ No newline at end of file diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs new file mode 100644 index 0000000..cf3f178 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs @@ -0,0 +1,206 @@ +using System.Globalization; +using System.Text.Json; +using Microsoft.Extensions.Options; + +namespace CrupestApi.Commons.Crud; + +/// +/// Contains all you need to do with json. +/// +public class EntityJsonHelper where TEntity : class +{ + private readonly TableInfo _table; + private readonly IOptionsMonitor _jsonSerializerOptions; + + public EntityJsonHelper(ITableInfoFactory tableInfoFactory, IOptionsMonitor jsonSerializerOptions) + { + _table = tableInfoFactory.Get(typeof(TEntity)); + _jsonSerializerOptions = jsonSerializerOptions; + } + + public Dictionary ConvertEntityToDictionary(TEntity entity, bool includeNonColumnProperties = false) + { + var result = new Dictionary(); + + foreach (var column in _table.PropertyColumns) + { + var value = column.PropertyInfo!.GetValue(entity); + var realValue = column.ColumnType.ConvertToDatabase(value); + result[column.ColumnName] = realValue; + } + + if (includeNonColumnProperties) + { + foreach (var propertyInfo in _table.NonColumnProperties) + { + var value = propertyInfo.GetValue(entity); + result[propertyInfo.Name] = value; + } + } + + return result; + } + + public string ConvertEntityToJson(TEntity entity, bool includeNonColumnProperties = false) + { + var dictionary = ConvertEntityToDictionary(entity, includeNonColumnProperties); + return JsonSerializer.Serialize(dictionary, _jsonSerializerOptions.CurrentValue); + } + + private object? ConvertJsonValue(JsonElement? optionalJsonElement, Type type, string propertyName) + { + if (optionalJsonElement is null) + { + return null; + } + + var jsonElement = optionalJsonElement.Value; + + if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) + { + return null; + } + + if (jsonElement.ValueKind is JsonValueKind.String) + { + if (type != typeof(string)) + { + throw new UserException($"Property {propertyName} must be a string."); + } + return jsonElement.GetString()!; + } + + if (jsonElement.ValueKind is JsonValueKind.True or JsonValueKind.False) + { + if (type != typeof(bool)) + { + throw new UserException($"Property {propertyName} must be a boolean."); + } + return jsonElement.GetBoolean(); + } + + if (jsonElement.ValueKind is JsonValueKind.Number) + { + try + { + return Convert.ChangeType(jsonElement.GetRawText(), type, CultureInfo.InvariantCulture); + } + catch (Exception) + { + throw new UserException($"Property {propertyName} must be a valid number."); + } + } + + throw new UserException($"Property {propertyName} is of wrong type."); + } + + public Dictionary ConvertJsonObjectToDictionary(JsonElement jsonElement) + { + var result = new Dictionary(); + + foreach (var property in jsonElement.EnumerateObject()) + { + result[property.Name.ToLower()] = property.Value; + } + + return result; + } + + public TEntity ConvertJsonToEntityForInsert(JsonElement jsonElement) + { + if (jsonElement.ValueKind is not JsonValueKind.Object) + throw new ArgumentException("The jsonElement must be an object."); + + var result = Activator.CreateInstance(); + + Dictionary jsonProperties = ConvertJsonObjectToDictionary(jsonElement); + + foreach (var column in _table.PropertyColumns) + { + var jsonPropertyValue = jsonProperties.GetValueOrDefault(column.ColumnName.ToLower()); + var value = ConvertJsonValue(jsonPropertyValue, column.ColumnType.DatabaseClrType, column.ColumnName); + if (column.IsOnlyGenerated && value is not null) + { + throw new UserException($"Property {column.ColumnName} is auto generated, you cannot set it."); + } + if (!column.CanBeGenerated && value is null && column.IsNotNull) + { + throw new UserException($"Property {column.ColumnName} can NOT be generated, you must set it."); + } + var realValue = column.ColumnType.ConvertFromDatabase(value); + column.PropertyInfo!.SetValue(result, realValue); + } + + return result; + } + + public TEntity ConvertJsonToEntityForInsert(string json) + { + var jsonElement = JsonSerializer.Deserialize(json, _jsonSerializerOptions.CurrentValue); + return ConvertJsonToEntityForInsert(jsonElement!); + } + + public TEntity ConvertJsonToEntityForUpdate(JsonElement jsonElement, out UpdateBehavior updateBehavior) + { + if (jsonElement.ValueKind is not JsonValueKind.Object) + throw new UserException("The jsonElement must be an object."); + + updateBehavior = UpdateBehavior.None; + + Dictionary jsonProperties = ConvertJsonObjectToDictionary(jsonElement); + + bool saveNull = false; + if (jsonProperties.TryGetValue("$saveNull".ToLower(), out var saveNullValue)) + { + if (saveNullValue.ValueKind is JsonValueKind.True) + { + updateBehavior |= UpdateBehavior.SaveNull; + saveNull = true; + } + else if (saveNullValue.ValueKind is JsonValueKind.False) + { + + } + else + { + throw new UserException("The $saveNull must be a boolean."); + } + } + + var result = Activator.CreateInstance(); + foreach (var column in _table.PropertyColumns) + { + if (jsonProperties.TryGetValue(column.ColumnName.ToLower(), out var jsonPropertyValue)) + { + if (jsonPropertyValue.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) + { + if ((column.IsOnlyGenerated || column.IsNoUpdate) && saveNull) + { + throw new UserException($"Property {column.ColumnName} is auto generated or not updatable, you cannot set it."); + } + + column.PropertyInfo!.SetValue(result, null); + } + else + { + if (column.IsOnlyGenerated || column.IsNoUpdate) + { + throw new UserException($"Property {column.ColumnName} is auto generated or not updatable, you cannot set it."); + } + + var value = ConvertJsonValue(jsonPropertyValue, column.ColumnType.DatabaseClrType, column.ColumnName); + var realValue = column.ColumnType.ConvertFromDatabase(value); + column.PropertyInfo!.SetValue(result, realValue); + } + } + } + + return result; + } + + public TEntity ConvertJsonToEntityForUpdate(string json, out UpdateBehavior updateBehavior) + { + var jsonElement = JsonSerializer.Deserialize(json, _jsonSerializerOptions.CurrentValue); + return ConvertJsonToEntityForUpdate(jsonElement!, out updateBehavior); + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/IClause.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/IClause.cs new file mode 100644 index 0000000..964a669 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/IClause.cs @@ -0,0 +1,24 @@ +using Dapper; + +namespace CrupestApi.Commons.Crud; + +public interface IClause +{ + IEnumerable GetSubclauses() + { + return Enumerable.Empty(); + } + + IEnumerable GetRelatedColumns() + { + var subclauses = GetSubclauses(); + var result = new List(); + foreach (var subclause in subclauses) + { + var columns = subclause.GetRelatedColumns(); + if (columns is not null) + result.AddRange(columns); + } + return result; + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/InsertClause.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/InsertClause.cs new file mode 100644 index 0000000..a880e66 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/InsertClause.cs @@ -0,0 +1,77 @@ +using System.Text; + +namespace CrupestApi.Commons.Crud; + +public class InsertItem +{ + /// + /// Null means use default value. Use . + /// + public InsertItem(string columnName, object? value) + { + ColumnName = columnName; + Value = value; + } + + public string ColumnName { get; set; } + public object? Value { get; set; } +} + +public interface IInsertClause : IClause +{ + List Items { get; } + string GenerateColumnListSql(string? dbProviderId = null); + (string sql, ParamList parameters) GenerateValueListSql(string? dbProviderId = null); +} + +public class InsertClause : IInsertClause +{ + public List Items { get; } = new List(); + + public InsertClause(params InsertItem[] items) + { + Items.AddRange(items); + } + + public InsertClause Add(params InsertItem[] items) + { + Items.AddRange(items); + return this; + } + + public InsertClause Add(string column, object? value) + { + return Add(new InsertItem(column, value)); + } + + public static InsertClause Create(params InsertItem[] items) + { + return new InsertClause(items); + } + + public List GetRelatedColumns() + { + return Items.Select(i => i.ColumnName).ToList(); + } + + public string GenerateColumnListSql(string? dbProviderId = null) + { + return string.Join(", ", Items.Select(i => i.ColumnName)); + } + + public (string sql, ParamList parameters) GenerateValueListSql(string? dbProviderId = null) + { + var parameters = new ParamList(); + var sb = new StringBuilder(); + for (var i = 0; i < Items.Count; i++) + { + var item = Items[i]; + var parameterName = parameters.AddRandomNameParameter(item.Value, item.ColumnName); + sb.Append($"@{parameterName}"); + if (i != Items.Count - 1) + sb.Append(", "); + } + + return (sb.ToString(), parameters); + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/DatabaseMigrator.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/DatabaseMigrator.cs new file mode 100644 index 0000000..f1ae616 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/DatabaseMigrator.cs @@ -0,0 +1,44 @@ +using System.Data; + +namespace CrupestApi.Commons.Crud.Migrations; + +public class TableColumn +{ + public TableColumn(string name, string type, bool notNull, int primaryKey) + { + Name = name; + Type = type; + NotNull = notNull; + PrimaryKey = primaryKey; + } + + public string Name { get; set; } + public string Type { get; set; } + public bool NotNull { get; set; } + + /// + /// 0 if not primary key. 1-based index if in primary key. + /// + public int PrimaryKey { get; set; } +} + +public class Table +{ + public Table(string name) + { + Name = name; + } + + public string Name { get; set; } + public List Columns { get; set; } = new List(); +} + +public interface IDatabaseMigrator +{ + Table? GetTable(IDbConnection dbConnection, string tableName); + Table ConvertTableInfoToTable(TableInfo tableInfo); + string GenerateCreateTableColumnSqlSegment(TableColumn column); + string GenerateCreateTableSql(string tableName, IEnumerable columns); + bool NeedMigrate(IDbConnection dbConnection, TableInfo tableInfo); + void AutoMigrate(IDbConnection dbConnection, TableInfo tableInfo); +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/SqliteDatabaseMigrator.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/SqliteDatabaseMigrator.cs new file mode 100644 index 0000000..33310d6 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/SqliteDatabaseMigrator.cs @@ -0,0 +1,175 @@ +using System.Data; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using Dapper; + +namespace CrupestApi.Commons.Crud.Migrations; + +public class SqliteDatabaseMigrator : IDatabaseMigrator +{ + private void CheckTableName(string name) + { + if (Regex.Match(name, @"^[_0-9a-zA-Z]+$").Success is false) + { + throw new ArgumentException("Fxxk, what have you passed as table name."); + } + } + + public Table? GetTable(IDbConnection dbConnection, string tableName) + { + var count = dbConnection.QuerySingle( + "SELECT count(*) FROM sqlite_schema WHERE type = 'table' AND name = @TableName;", + new { TableName = tableName }); + if (count == 0) + { + return null; + } + else if (count > 1) + { + throw new Exception($"More than 1 table has name {tableName}. What happened?"); + } + else + { + var table = new Table(tableName); + var queryColumns = dbConnection.Query($"PRAGMA table_info({tableName})"); + + foreach (var column in queryColumns) + { + var columnName = (string)column.name; + var columnType = (string)column.type; + var isNullable = Convert.ToBoolean(column.notnull); + var primaryKey = Convert.ToInt32(column.pk); + + table.Columns.Add(new TableColumn(columnName, columnType, isNullable, primaryKey)); + } + + return table; + } + } + + public Table ConvertTableInfoToTable(TableInfo tableInfo) + { + var table = new Table(tableInfo.TableName); + + foreach (var columnInfo in tableInfo.Columns) + { + table.Columns.Add(new TableColumn(columnInfo.ColumnName, columnInfo.ColumnType.GetSqlTypeString(), + columnInfo.IsNotNull, columnInfo.IsPrimaryKey ? 1 : 0)); + } + + return table; + } + + public string GenerateCreateTableColumnSqlSegment(TableColumn column) + { + StringBuilder result = new StringBuilder(); + result.Append(column.Name); + result.Append(' '); + result.Append(column.Type); + if (column.PrimaryKey is not 0) + { + result.Append(" PRIMARY KEY AUTOINCREMENT"); + } + else if (column.NotNull) + { + result.Append(" NOT NULL"); + } + + return result.ToString(); + } + + public string GenerateCreateTableSql(string tableName, IEnumerable columns) + { + CheckTableName(tableName); + + var sql = $@" +CREATE TABLE {tableName} ( + {string.Join(",\n ", columns.Select(GenerateCreateTableColumnSqlSegment))} +); + ".Trim(); + + return sql; + + } + + public void AutoMigrate(IDbConnection dbConnection, TableInfo tableInfo) + { + var tableName = tableInfo.TableName; + var databaseTable = GetTable(dbConnection, tableName); + var wantedTable = ConvertTableInfoToTable(tableInfo); + var databaseTableColumnNames = databaseTable is null ? new List() : databaseTable.Columns.Select(column => column.Name).ToList(); + var wantedTableColumnNames = wantedTable.Columns.Select(column => column.Name).ToList(); + + var notChangeColumns = wantedTableColumnNames.Where(column => databaseTableColumnNames.Contains(column)).ToList(); + var addColumns = wantedTableColumnNames.Where(column => !databaseTableColumnNames.Contains(column)).ToList(); + + if (databaseTable is not null && dbConnection.QuerySingle($"SELECT count(*) FROM {tableName}") > 0) + { + foreach (var columnName in addColumns) + { + var columnInfo = tableInfo.GetColumn(columnName); + if (!columnInfo.CanBeGenerated) + { + throw new Exception($"Column {columnName} cannot be generated. So we can't auto-migrate."); + } + } + } + + // We are sqlite, so it's a little bit difficult. + using var transaction = dbConnection.BeginTransaction(); + + if (databaseTable is not null) + { + var tempTableName = tableInfo.TableName + "_temp"; + dbConnection.Execute($"ALTER TABLE {tableName} RENAME TO {tempTableName}", new { TableName = tableName, tempTableName }); + + var createTableSql = GenerateCreateTableSql(tableName, wantedTable.Columns); + dbConnection.Execute(createTableSql); + + // Copy old data to new table. + var originalRows = dbConnection.Query($"SELECT * FROM {tempTableName}").Cast>().ToList(); + foreach (var originalRow in originalRows) + { + var parameters = new DynamicParameters(); + + foreach (var columnName in notChangeColumns) + { + parameters.Add(columnName, originalRow[columnName]); + } + + foreach (var columnName in addColumns) + { + parameters.Add(columnName, tableInfo.GetColumn(columnName).GenerateDefaultValue()); + } + + string columnSql = string.Join(", ", wantedTableColumnNames); + string valuesSql = string.Join(", ", wantedTableColumnNames.Select(c => "@" + c)); + + string sql = $"INSERT INTO {tableName} ({columnSql}) VALUES {valuesSql})"; + dbConnection.Execute(sql, parameters); + } + + // Finally drop old table + dbConnection.Execute($"DROP TABLE {tempTableName}"); + } + else + { + var createTableSql = GenerateCreateTableSql(tableName, wantedTable.Columns); + dbConnection.Execute(createTableSql); + } + + // Commit transaction. + transaction.Commit(); + } + + public bool NeedMigrate(IDbConnection dbConnection, TableInfo tableInfo) + { + var tableName = tableInfo.TableName; + var databaseTable = GetTable(dbConnection, tableName); + var wantedTable = ConvertTableInfoToTable(tableInfo); + var databaseTableColumns = databaseTable is null ? new HashSet() : new HashSet(databaseTable.Columns.Select(c => c.Name)); + var wantedTableColumns = new HashSet(wantedTable.Columns.Select(c => c.Name)); + return !databaseTableColumns.SetEquals(wantedTableColumns); + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/OrderByClause.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/OrderByClause.cs new file mode 100644 index 0000000..734d044 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/OrderByClause.cs @@ -0,0 +1,50 @@ +namespace CrupestApi.Commons.Crud; + +public class OrderByItem +{ + public OrderByItem(string columnName, bool isAscending) + { + ColumnName = columnName; + IsAscending = isAscending; + } + + public string ColumnName { get; } + public bool IsAscending { get; } + + public string GenerateSql() + { + return $"{ColumnName} {(IsAscending ? "ASC" : "DESC")}"; + } +} + +public interface IOrderByClause : IClause +{ + List Items { get; } + // Contains "ORDER BY" keyword! + string GenerateSql(string? dbProviderId = null); +} + +public class OrderByClause : IOrderByClause +{ + public List Items { get; } = new List(); + + public OrderByClause(params OrderByItem[] items) + { + Items.AddRange(items); + } + + public static OrderByClause Create(params OrderByItem[] items) + { + return new OrderByClause(items); + } + + public List GetRelatedColumns() + { + return Items.Select(x => x.ColumnName).ToList(); + } + + public string GenerateSql(string? dbProviderId = null) + { + return "ORDER BY " + string.Join(", ", Items.Select(i => i.GenerateSql())); + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs new file mode 100644 index 0000000..37d77ca --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs @@ -0,0 +1,73 @@ +using System.Data; +using System.Diagnostics; + +namespace CrupestApi.Commons.Crud; + +/// +/// is an optional column name related to the param. You may use it to do some column related things. Like use a more accurate conversion. +/// +/// +/// If value is DbNullValue, it will be treated as null. +/// +public record ParamInfo(string Name, object? Value, string? ColumnName = null); + +public class ParamList : List +{ + private static Random random = new Random(); + private const string chars = "abcdefghijklmnopqrstuvwxyz"; + public static string GenerateRandomKey(int length) + { + lock (random) + { + var result = new string(Enumerable.Repeat(chars, length) + .Select(s => s[random.Next(s.Length)]).ToArray()); + return result; + } + } + + public string GenerateRandomParameterName() + { + var parameterName = GenerateRandomKey(10); + int retryTimes = 1; + while (ContainsKey(parameterName)) + { + retryTimes++; + Debug.Assert(retryTimes <= 100); + parameterName = GenerateRandomKey(10); + } + return parameterName; + } + + + public bool ContainsKey(string name) + { + return this.SingleOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) is not null; + } + + public T? Get(string key) + { + return (T?)this.SingleOrDefault(p => p.Name.Equals(key, StringComparison.OrdinalIgnoreCase))?.Value; + } + + public object? this[string key] + { + get + { + return this.SingleOrDefault(p => p.Name.Equals(key, StringComparison.OrdinalIgnoreCase)) ?? throw new KeyNotFoundException("Key not found."); + } + } + + public void Add(string name, object? value, string? columnName = null) + { + Add(new ParamInfo(name, value, columnName)); + } + + // Return the random name. + public string AddRandomNameParameter(object? value, string? columnName = null) + { + var parameterName = GenerateRandomParameterName(); + var param = new ParamInfo(parameterName, value, columnName); + Add(param); + return parameterName; + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/README.md b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/README.md new file mode 100644 index 0000000..b008ea7 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/README.md @@ -0,0 +1,47 @@ +# CRUD Technic Notes + +## Overview + +The ultimate CRUD scaffold finally comes. + +## Database Pipeline + +### Select + +1. Create select `what`, where clause, order clause, `Offset` and `Limit`. +2. Check clauses' related columns are valid. +3. Generate sql string and param list. +4. Convert param list to `Dapper` dynamic params with proper type conversion in `IColumnTypeInfo`. +5. Execute sql and get `dynamic`s. +6. (Optional) Convert `dynamic`s to `TEntity`s. + +### Insert + +1. Create insert clause. +2. Check clauses' related columns are valid. +3. Create a real empty insert clause. +4. For each column: + 1. If insert item exists and value is not null but the column `IsGenerated` is true, throw exception. + 2. If insert item does not exist or value is `null`, use default value generator to generate value. However, `DbNullValue` always means use `NULL` for that column. + 3. If value is `null` and the column `IsAutoIncrement` is true, skip to next column. + 4. Coerce null to `DbNullValue`. + 5. Run validator to validate the value. + 6. If value is `DbNullValue`, `IsNotNull` is true, throw exception. + 7. Add column and value to real insert clause. +5. Generate sql string and param list. +6. Convert param list to `Dapper` dynamic params with proper type conversion in `IColumnTypeInfo`. +7. Execute sql and return `KeyColumn` value. + +### Update + +1. Create update clause, where clause. +2. Check clauses' related columns are valid. Then generate sql string and param list. +3. Create a real empty update clause. +4. For each column: + 1. If update item exists and value is not null but the column `IsNoUpdate` is true, throw exception. + 2. Invoke validator to validate the value. + 3. If `IsNotNull` is true and value is `DbNullValue`, throw exception. + 4. Add column and value to real update clause. +5. Generate sql string and param list. +6. Convert param list to `Dapper` dynamic params with proper type conversion in `IColumnTypeInfo`. +7. Execute sql and return count of affected rows. diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs new file mode 100644 index 0000000..4a7ea95 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs @@ -0,0 +1,628 @@ +using System.Data; +using System.Diagnostics; +using System.Reflection; +using System.Text; +using Dapper; + +namespace CrupestApi.Commons.Crud; + +/// +/// Contains all you need to manipulate a table. +/// +public class TableInfo +{ + private readonly IColumnTypeProvider _columnTypeProvider; + private readonly Lazy> _lazyColumnNameList; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + + public TableInfo(Type entityType, IColumnTypeProvider columnTypeProvider, ILoggerFactory loggerFactory) + : this(entityType.Name, entityType, columnTypeProvider, loggerFactory) + { + } + + public TableInfo(string tableName, Type entityType, IColumnTypeProvider columnTypeProvider, ILoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(); + + _logger.LogInformation("Create TableInfo for entity type '{}'.", entityType.Name); + + _columnTypeProvider = columnTypeProvider; + + TableName = tableName; + EntityType = entityType; + + + var properties = entityType.GetProperties(); + _logger.LogInformation("Find following properties: {}", string.Join(", ", properties.Select(p => p.Name))); + + var columnInfos = new List(); + + bool hasId = false; + ColumnInfo? primaryKeyColumn = null; + ColumnInfo? keyColumn = null; + + List nonColumnProperties = new(); + + foreach (var property in properties) + { + _logger.LogInformation("Check property '{}'.", property.Name); + if (CheckPropertyIsColumn(property)) + { + _logger.LogInformation("{} is a column, create ColumnInfo for it.", property.Name); + var columnInfo = new ColumnInfo(this, property, _columnTypeProvider, _loggerFactory); + columnInfos.Add(columnInfo); + if (columnInfo.IsPrimaryKey) + { + _logger.LogInformation("Column {} is a primary key.", property.Name); + primaryKeyColumn = columnInfo; + } + if (columnInfo.ColumnName.Equals("id", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation("Column {} has name id.", property.Name); + hasId = true; + } + if (columnInfo.IsSpecifiedAsKey) + { + if (keyColumn is not null) + { + throw new Exception("Already exists a key column."); + } + _logger.LogInformation("Column {} is specified as key.", property.Name); + keyColumn = columnInfo; + } + } + else + { + _logger.LogInformation("{} is not a column.", property.Name); + nonColumnProperties.Add(property); + } + } + + if (primaryKeyColumn is null) + { + if (hasId) throw new Exception("A column named id already exists but is not primary key."); + _logger.LogInformation("No primary key column found, create one automatically."); + primaryKeyColumn = CreateAutoIdColumn(); + columnInfos.Add(primaryKeyColumn); + } + + if (keyColumn is null) + { + _logger.LogInformation("No key column is specified, will use primary key."); + keyColumn = primaryKeyColumn; + } + + Columns = columnInfos; + PrimaryKeyColumn = primaryKeyColumn; + KeyColumn = keyColumn; + NonColumnProperties = nonColumnProperties; + + _logger.LogInformation("Check table validity."); + CheckValidity(); + + _logger.LogInformation("TableInfo succeeded to create."); + + _lazyColumnNameList = new Lazy>(() => Columns.Select(c => c.ColumnName).ToList()); + } + + private ColumnInfo CreateAutoIdColumn() + { + return new ColumnInfo(this, + new ColumnAttribute + { + ColumnName = "Id", + NotNull = true, + IsPrimaryKey = true, + }, + typeof(long), _columnTypeProvider, _loggerFactory); + } + + public Type EntityType { get; } + public string TableName { get; } + public IReadOnlyList Columns { get; } + public IReadOnlyList PropertyColumns => Columns.Where(c => c.PropertyInfo is not null).ToList(); + public ColumnInfo PrimaryKeyColumn { get; } + /// + /// Maybe not the primary key. But acts as primary key. + /// + /// + public ColumnInfo KeyColumn { get; } + public IReadOnlyList ColumnProperties => PropertyColumns.Select(c => c.PropertyInfo!).ToList(); + public IReadOnlyList NonColumnProperties { get; } + public IReadOnlyList ColumnNameList => _lazyColumnNameList.Value; + + protected bool CheckPropertyIsColumn(PropertyInfo property) + { + var columnAttribute = property.GetCustomAttribute(); + if (columnAttribute is null) return false; + return true; + } + + public ColumnInfo GetColumn(string columnName) + { + foreach (var column in Columns) + { + if (column.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase)) + { + return column; + } + } + throw new KeyNotFoundException("No such column with given name."); + } + + public void CheckGeneratedColumnHasGenerator() + { + foreach (var column in Columns) + { + if (column.IsOnlyGenerated && column.DefaultValueGeneratorMethod is null) + { + throw new Exception($"Column '{column.ColumnName}' is generated but has no generator."); + } + } + } + + public void CheckValidity() + { + // Check if there is only one primary key. + bool hasPrimaryKey = false; + bool hasKey = false; + foreach (var column in Columns) + { + if (column.IsPrimaryKey) + { + if (hasPrimaryKey) throw new Exception("More than one columns are primary key."); + hasPrimaryKey = true; + } + + if (column.IsSpecifiedAsKey) + { + if (hasKey) throw new Exception("More than one columns are specified as key column."); + } + } + + if (!hasPrimaryKey) throw new Exception("No column is primary key."); + + // Check two columns have the same sql name. + HashSet sqlNameSet = new HashSet(); + + foreach (var column in Columns) + { + if (sqlNameSet.Contains(column.ColumnName)) + throw new Exception($"Two columns have the same sql name '{column.ColumnName}'."); + sqlNameSet.Add(column.ColumnName); + } + + CheckGeneratedColumnHasGenerator(); + } + + public string GenerateCreateIndexSql(string? dbProviderId = null) + { + var sb = new StringBuilder(); + + foreach (var column in Columns) + { + if (column.Index == ColumnIndexType.None) continue; + + sb.Append($"CREATE {(column.Index == ColumnIndexType.Unique ? "UNIQUE" : "")} INDEX {TableName}_{column.ColumnName}_index ON {TableName} ({column.ColumnName});\n"); + } + + return sb.ToString(); + } + + public string GenerateCreateTableSql(bool createIndex = true, string? dbProviderId = null) + { + var tableName = TableName; + var columnSql = string.Join(",\n", Columns.Select(c => c.GenerateCreateTableColumnString(dbProviderId))); + + var sql = $@" +CREATE TABLE {tableName}( + {columnSql} +); + "; + + if (createIndex) + { + sql += GenerateCreateIndexSql(dbProviderId); + } + + return sql; + } + + public void CheckColumnName(string columnName) + { + if (!ColumnNameList.Contains(columnName)) + { + throw new ArgumentException($"Column {columnName} is not in the table."); + } + } + + public void CheckRelatedColumns(IClause? clause) + { + if (clause is not null) + { + var relatedColumns = clause.GetRelatedColumns(); + foreach (var column in relatedColumns) + { + CheckColumnName(column); + } + } + } + + /// + /// If you call this manually, it's your duty to call hooks. + /// + /// + public (string sql, ParamList parameters) GenerateSelectSql(string? selectWhat, IWhereClause? whereClause, IOrderByClause? orderByClause = null, int? skip = null, int? limit = null, string? dbProviderId = null) + { + CheckRelatedColumns(whereClause); + CheckRelatedColumns(orderByClause); + + var parameters = new ParamList(); + + StringBuilder result = new StringBuilder() + .Append($"SELECT {selectWhat ?? "*"} FROM ") + .Append(TableName); + + if (whereClause is not null) + { + result.Append(" WHERE "); + var (whereSql, whereParameters) = whereClause.GenerateSql(dbProviderId); + parameters.AddRange(whereParameters); + result.Append(whereSql); + } + + if (orderByClause is not null) + { + result.Append(' '); + var orderBySql = orderByClause.GenerateSql(dbProviderId); + result.Append(orderBySql); + } + + if (limit is not null) + { + result.Append(" LIMIT @Limit"); + parameters.Add("Limit", limit.Value); + } + + if (skip is not null) + { + result.Append(" OFFSET @Skip"); + parameters.Add("Skip", skip.Value); + } + + result.Append(';'); + + return (result.ToString(), parameters); + } + + /// + /// If you call this manually, it's your duty to call hooks. + /// + /// + public (string sql, ParamList parameters) GenerateInsertSql(IInsertClause insertClause, string? dbProviderId = null) + { + CheckRelatedColumns(insertClause); + + var parameters = new ParamList(); + + var result = new StringBuilder() + .Append("INSERT INTO ") + .Append(TableName) + .Append(" (") + .Append(insertClause.GenerateColumnListSql(dbProviderId)) + .Append(") VALUES ("); + + var (valueSql, valueParameters) = insertClause.GenerateValueListSql(dbProviderId); + result.Append(valueSql).Append(");"); + + parameters.AddRange(valueParameters); + + return (result.ToString(), parameters); + } + + /// + /// If you call this manually, it's your duty to call hooks. + /// + /// + public (string sql, ParamList parameters) GenerateUpdateSql(IWhereClause? whereClause, IUpdateClause updateClause) + { + CheckRelatedColumns(whereClause); + CheckRelatedColumns(updateClause); + + var parameters = new ParamList(); + + StringBuilder sb = new StringBuilder("UPDATE "); + sb.Append(TableName); + sb.Append(" SET "); + var (updateSql, updateParameters) = updateClause.GenerateSql(); + sb.Append(updateSql); + parameters.AddRange(updateParameters); + if (whereClause is not null) + { + sb.Append(" WHERE "); + var (whereSql, whereParameters) = whereClause.GenerateSql(); + sb.Append(whereSql); + parameters.AddRange(whereParameters); + } + sb.Append(';'); + + return (sb.ToString(), parameters); + } + + /// + /// If you call this manually, it's your duty to call hooks. + /// + /// + public (string sql, ParamList parameters) GenerateDeleteSql(IWhereClause? whereClause) + { + CheckRelatedColumns(whereClause); + + var parameters = new ParamList(); + + StringBuilder sb = new StringBuilder("DELETE FROM "); + sb.Append(TableName); + if (whereClause is not null) + { + sb.Append(" WHERE "); + var (whereSql, whereParameters) = whereClause.GenerateSql(); + parameters.AddRange(whereParameters); + sb.Append(whereSql); + } + sb.Append(';'); + + return (sb.ToString(), parameters); + } + + private DynamicParameters ConvertParameters(ParamList parameters) + { + var result = new DynamicParameters(); + foreach (var param in parameters) + { + if (param.Value is null || param.Value is DbNullValue) + { + result.Add(param.Name, null); + continue; + } + + var columnName = param.ColumnName; + IColumnTypeInfo typeInfo; + if (columnName is not null) + { + typeInfo = GetColumn(columnName).ColumnType; + } + else + { + typeInfo = _columnTypeProvider.Get(param.Value.GetType()); + } + + result.Add(param.Name, typeInfo.ConvertToDatabase(param.Value), typeInfo.DbType); + } + return result; + } + + /// + /// ConvertParameters. Select. Call hooks. + /// + public virtual List SelectDynamic(IDbConnection dbConnection, string? what = null, IWhereClause? where = null, IOrderByClause? orderBy = null, int? skip = null, int? limit = null) + { + var (sql, parameters) = GenerateSelectSql(what, where, orderBy, skip, limit); + var queryResult = dbConnection.Query(sql, ConvertParameters(parameters)); + return queryResult.ToList(); + } + + public virtual int SelectCount(IDbConnection dbConnection, IWhereClause? where = null, IOrderByClause? orderBy = null, int? skip = null, int? limit = null) + { + var (sql, parameters) = GenerateSelectSql("COUNT(*)", where, orderBy, skip, limit); + var result = dbConnection.QuerySingle(sql, ConvertParameters(parameters)); + return result; + } + + public virtual TResult MapDynamicTo(dynamic d) + { + var dict = (IDictionary)d; + + var result = Activator.CreateInstance(); + Type resultType = typeof(TResult); + + foreach (var column in Columns) + { + var resultProperty = resultType.GetProperty(column.ColumnName); + if (dict.ContainsKey(column.ColumnName) && resultProperty is not null) + { + if (dict[column.ColumnName] is null) + { + resultProperty.SetValue(result, null); + continue; + } + object? value = Convert.ChangeType(dict[column.ColumnName], column.ColumnType.DatabaseClrType); + value = column.ColumnType.ConvertFromDatabase(value); + resultProperty.SetValue(result, value); + } + } + + return result; + } + + /// + /// Select and call hooks. + /// + public virtual List Select(IDbConnection dbConnection, string? what = null, IWhereClause? where = null, IOrderByClause? orderBy = null, int? skip = null, int? limit = null) + { + List queryResult = SelectDynamic(dbConnection, what, where, orderBy, skip, limit).ToList(); + + return queryResult.Select(MapDynamicTo).ToList(); + } + + public IInsertClause ConvertEntityToInsertClause(object entity) + { + Debug.Assert(EntityType.IsInstanceOfType(entity)); + var result = new InsertClause(); + foreach (var column in PropertyColumns) + { + var value = column.PropertyInfo!.GetValue(entity); + result.Add(column.ColumnName, value); + } + return result; + } + + /// + /// Insert a entity and call hooks. + /// + /// The key of insert entity. + public int Insert(IDbConnection dbConnection, IInsertClause insert, out object key) + { + object? finalKey = null; + + var realInsert = InsertClause.Create(); + + foreach (var column in Columns) + { + InsertItem? item = insert.Items.SingleOrDefault(i => i.ColumnName == column.ColumnName); + + var value = item?.Value; + + if (column.IsOnlyGenerated && value is not null) + { + throw new Exception($"The column '{column.ColumnName}' is auto generated. You can't specify it explicitly."); + } + + if (value is null) + { + value = column.GenerateDefaultValue(); + } + + if (value is null && column.IsAutoIncrement) + { + continue; + } + + if (value is null) + { + value = DbNullValue.Instance; + } + + column.InvokeValidator(value); + + InsertItem realInsertItem; + + if (value is DbNullValue) + { + if (column.IsNotNull) + { + throw new Exception($"Column '{column.ColumnName}' is not nullable. Please specify a non-null value."); + } + + realInsertItem = new InsertItem(column.ColumnName, null); + } + else + { + realInsertItem = new InsertItem(column.ColumnName, value); + } + + realInsert.Add(realInsertItem); + + if (realInsertItem.ColumnName == KeyColumn.ColumnName) + { + finalKey = realInsertItem.Value; + } + } + + if (finalKey is null) throw new Exception("No key???"); + key = finalKey; + + var (sql, parameters) = GenerateInsertSql(realInsert); + + var affectedRowCount = dbConnection.Execute(sql, ConvertParameters(parameters)); + + if (affectedRowCount != 1) + throw new Exception("Failed to insert."); + + return affectedRowCount; + } + + /// + /// Upgrade a entity and call hooks. + /// + /// The key of insert entity. + public virtual int Update(IDbConnection dbConnection, IWhereClause? where, IUpdateClause update, out object? newKey) + { + newKey = null; + + var realUpdate = UpdateClause.Create(); + + foreach (var column in Columns) + { + UpdateItem? item = update.Items.FirstOrDefault(i => i.ColumnName == column.ColumnName); + object? value = item?.Value; + + if (value is not null) + { + if (column.IsNoUpdate) + { + throw new Exception($"The column '{column.ColumnName}' can't be update."); + } + + column.InvokeValidator(value); + + realUpdate.Add(column.ColumnName, value); + + if (column.ColumnName == KeyColumn.ColumnName) + { + newKey = value; + } + } + } + + var (sql, parameters) = GenerateUpdateSql(where, realUpdate); + return dbConnection.Execute(sql, ConvertParameters(parameters)); + } + + public virtual int Delete(IDbConnection dbConnection, IWhereClause? where) + { + var (sql, parameters) = GenerateDeleteSql(where); + return dbConnection.Execute(sql, ConvertParameters(parameters)); + } +} + +public interface ITableInfoFactory +{ + TableInfo Get(Type type); +} + +public class TableInfoFactory : ITableInfoFactory +{ + private readonly Dictionary _cache = new Dictionary(); + private readonly IColumnTypeProvider _columnTypeProvider; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + + public TableInfoFactory(IColumnTypeProvider columnTypeProvider, ILoggerFactory loggerFactory) + { + _columnTypeProvider = columnTypeProvider; + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(); + } + + // This is thread-safe. + public TableInfo Get(Type type) + { + lock (_cache) + { + if (_cache.TryGetValue(type, out var tableInfo)) + { + _logger.LogDebug("Table info of type '{}' is cached, return it.", type.Name); + return tableInfo; + } + else + { + _logger.LogDebug("Table info for type '{}' is not in cache, create it.", type.Name); + tableInfo = new TableInfo(type, _columnTypeProvider, _loggerFactory); + _logger.LogDebug("Table info for type '{}' is created, add it to cache.", type.Name); + _cache.Add(type, tableInfo); + return tableInfo; + } + } + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UpdateClause.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UpdateClause.cs new file mode 100644 index 0000000..de5c6c3 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UpdateClause.cs @@ -0,0 +1,77 @@ +using System.Text; + +namespace CrupestApi.Commons.Crud; + +public class UpdateItem +{ + public UpdateItem(string columnName, object? value) + { + ColumnName = columnName; + Value = value; + } + + public string ColumnName { get; set; } + public object? Value { get; set; } +} + +public interface IUpdateClause : IClause +{ + List Items { get; } + (string sql, ParamList parameters) GenerateSql(); +} + +public class UpdateClause : IUpdateClause +{ + public List Items { get; } = new List(); + + public UpdateClause(IEnumerable items) + { + Items.AddRange(items); + } + + public UpdateClause(params UpdateItem[] items) + { + Items.AddRange(items); + } + + public UpdateClause Add(params UpdateItem[] items) + { + Items.AddRange(items); + return this; + } + + public UpdateClause Add(string column, object? value) + { + return Add(new UpdateItem(column, value)); + } + + public static UpdateClause Create(params UpdateItem[] items) + { + return new UpdateClause(items); + } + + public List GetRelatedColumns() + { + return Items.Select(i => i.ColumnName).ToList(); + } + + public (string sql, ParamList parameters) GenerateSql() + { + var parameters = new ParamList(); + + StringBuilder result = new StringBuilder(); + + foreach (var item in Items) + { + if (result.Length > 0) + { + result.Append(", "); + } + + var parameterName = parameters.AddRandomNameParameter(item.Value, item.ColumnName); + result.Append($"{item.ColumnName} = @{parameterName}"); + } + + return (result.ToString(), parameters); + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UserException.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UserException.cs new file mode 100644 index 0000000..1a10b97 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UserException.cs @@ -0,0 +1,15 @@ +namespace CrupestApi.Commons.Crud; + +/// +/// This exception means the exception is caused by user and can be safely shown to user. +/// +[System.Serializable] +public class UserException : Exception +{ + public UserException() { } + public UserException(string message) : base(message) { } + public UserException(string message, System.Exception inner) : base(message, inner) { } + protected UserException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } +} \ No newline at end of file diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/WhereClause.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/WhereClause.cs new file mode 100644 index 0000000..de69f2f --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/WhereClause.cs @@ -0,0 +1,182 @@ +using System.Text; + +namespace CrupestApi.Commons.Crud; + +public interface IWhereClause : IClause +{ + // Does not contain "WHERE" keyword! + (string sql, ParamList parameters) GenerateSql(string? dbProviderId = null); +} + +public class CompositeWhereClause : IWhereClause +{ + public CompositeWhereClause(string concatOp, bool parenthesesSubclause, params IWhereClause[] subclauses) + { + ConcatOp = concatOp; + ParenthesesSubclause = parenthesesSubclause; + Subclauses = subclauses.ToList(); + } + + public string ConcatOp { get; } + public bool ParenthesesSubclause { get; } + public List Subclauses { get; } + + public CompositeWhereClause Eq(string column, object? value) + { + Subclauses.Add(SimpleCompareWhereClause.Eq(column, value)); + return this; + } + + public (string sql, ParamList parameters) GenerateSql(string? dbProviderId = null) + { + var parameters = new ParamList(); + var sql = new StringBuilder(); + var subclauses = GetSubclauses(); + if (subclauses is null) return ("", new()); + var first = true; + foreach (var subclause in Subclauses) + { + var (subSql, subParameters) = subclause.GenerateSql(dbProviderId); + if (subSql is null) continue; + if (first) + { + first = false; + } + else + { + sql.Append($" {ConcatOp} "); + } + if (ParenthesesSubclause) + { + sql.Append("("); + } + sql.Append(subSql); + if (ParenthesesSubclause) + { + sql.Append(")"); + } + parameters.AddRange(subParameters); + } + return (sql.ToString(), parameters); + } + + public object GetSubclauses() + { + return Subclauses; + } +} + +public class AndWhereClause : CompositeWhereClause +{ + public AndWhereClause(params IWhereClause[] clauses) + : this(true, clauses) + { + + } + + public AndWhereClause(bool parenthesesSubclause, params IWhereClause[] clauses) + : base("AND", parenthesesSubclause, clauses) + { + + } + + public static AndWhereClause Create(params IWhereClause[] clauses) + { + return new AndWhereClause(clauses); + } +} + +public class OrWhereClause : CompositeWhereClause +{ + public OrWhereClause(params IWhereClause[] clauses) + : this(true, clauses) + { + + } + + public OrWhereClause(bool parenthesesSubclause, params IWhereClause[] clauses) + : base("OR", parenthesesSubclause, clauses) + { + + } + + public static OrWhereClause Create(params IWhereClause[] clauses) + { + return new OrWhereClause(clauses); + } +} + +// It's simple because it only compare column and value but not expressions. +public class SimpleCompareWhereClause : IWhereClause +{ + public string Column { get; } + public string Operator { get; } + public object? Value { get; } + + public List GetRelatedColumns() + { + return new List { Column }; + } + + // It's user's responsibility to keep column safe, with proper escape. + public SimpleCompareWhereClause(string column, string op, object? value) + { + Column = column; + Operator = op; + Value = value; + } + + public static SimpleCompareWhereClause Create(string column, string op, object? value) + { + return new SimpleCompareWhereClause(column, op, value); + } + + public static SimpleCompareWhereClause Eq(string column, object? value) + { + return new SimpleCompareWhereClause(column, "=", value); + } + + public static SimpleCompareWhereClause Neq(string column, object? value) + { + return new SimpleCompareWhereClause(column, "<>", value); + } + + public static SimpleCompareWhereClause Gt(string column, object? value) + { + return new SimpleCompareWhereClause(column, ">", value); + } + + public static SimpleCompareWhereClause Gte(string column, object? value) + { + return new SimpleCompareWhereClause(column, ">=", value); + } + + public static SimpleCompareWhereClause Lt(string column, object? value) + { + return new SimpleCompareWhereClause(column, "<", value); + } + + public static SimpleCompareWhereClause Lte(string column, object? value) + { + return new SimpleCompareWhereClause(column, "<=", value); + } + + public (string sql, ParamList parameters) GenerateSql(string? dbProviderId = null) + { + var parameters = new ParamList(); + var parameterName = parameters.AddRandomNameParameter(Value, Column); + return ($"{Column} {Operator} @{parameterName}", parameters); + } +} + +public class WhereClause : AndWhereClause +{ + public WhereClause() + { + } + + public void Add(IWhereClause subclause) + { + Subclauses.Add(subclause); + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/CrupestApi.Commons.csproj b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/CrupestApi.Commons.csproj new file mode 100644 index 0000000..8e291fa --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/CrupestApi.Commons.csproj @@ -0,0 +1,16 @@ + + + + net7.0 + library + enable + enable + false + + + + + + + + \ No newline at end of file diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/EntityNotExistException.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/EntityNotExistException.cs new file mode 100644 index 0000000..0e1f4f4 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/EntityNotExistException.cs @@ -0,0 +1,8 @@ +namespace CrupestApi.Commons; + +public class EntityNotExistException : Exception +{ + public EntityNotExistException() { } + public EntityNotExistException(string message) : base(message) { } + public EntityNotExistException(string message, Exception inner) : base(message, inner) { } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/HttpContextExtensions.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/HttpContextExtensions.cs new file mode 100644 index 0000000..a0b2d89 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/HttpContextExtensions.cs @@ -0,0 +1,113 @@ +using System.Text.Json; +using CrupestApi.Commons.Secrets; +using Microsoft.Extensions.Options; + +namespace CrupestApi.Commons; + +public delegate void HttpResponseAction(HttpResponse response); + +public class MessageBody +{ + public MessageBody(string message) + { + Message = message; + } + + public string Message { get; set; } +} + +public static class CrupestApiJsonExtensions +{ + public static IServiceCollection AddJsonOptions(this IServiceCollection services) + { + services.AddOptions(); + services.Configure(config => + { + config.AllowTrailingCommas = true; + config.PropertyNameCaseInsensitive = true; + config.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + }); + + return services; + } + + public static async Task ReadJsonAsync(this HttpRequest request) + { + var jsonOptions = request.HttpContext.RequestServices.GetRequiredService>(); + using var stream = request.Body; + var body = await JsonSerializer.DeserializeAsync(stream, jsonOptions.Value); + return body!; + } + + public static async Task WriteJsonAsync(this HttpResponse response, T bodyObject, int statusCode = 200, HttpResponseAction? beforeWriteBody = null, CancellationToken cancellationToken = default) + { + var jsonOptions = response.HttpContext.RequestServices.GetRequiredService>(); + byte[] json = JsonSerializer.SerializeToUtf8Bytes(bodyObject, jsonOptions.Value); + + var byteCount = json.Length; + + response.StatusCode = statusCode; + response.Headers.ContentType = "application/json; charset=utf-8"; + response.Headers.ContentLength = byteCount; + + if (beforeWriteBody is not null) + { + beforeWriteBody(response); + } + + await response.Body.WriteAsync(json, cancellationToken); + } + + public static async Task WriteMessageAsync(this HttpResponse response, string message, int statusCode = 400, HttpResponseAction? beforeWriteBody = null, CancellationToken cancellationToken = default) + { + await response.WriteJsonAsync(new MessageBody(message), statusCode: statusCode, beforeWriteBody, cancellationToken); + } + + public static Task ResponseJsonAsync(this HttpContext context, T bodyObject, int statusCode = 200, HttpResponseAction? beforeWriteBody = null, CancellationToken cancellationToken = default) + { + return context.Response.WriteJsonAsync(bodyObject, statusCode, beforeWriteBody, cancellationToken); + } + + public static Task ResponseMessageAsync(this HttpContext context, string message, int statusCode = 400, HttpResponseAction? beforeWriteBody = null, CancellationToken cancellationToken = default) + { + return context.Response.WriteMessageAsync(message, statusCode, beforeWriteBody, cancellationToken); + } + + public static string? GetToken(this HttpRequest request) + { + var token = request.Headers["Authorization"].ToString(); + if (token.StartsWith("Bearer ")) + { + token = token.Substring("Bearer ".Length); + return token; + } + + if (request.Query.TryGetValue("token", out var tokenValues)) + { + return tokenValues.Last(); + } + + return null; + } + + public static bool RequirePermission(this HttpContext context, string? permission) + { + if (permission is null) return true; + + var token = context.Request.GetToken(); + if (token is null) + { + context.ResponseMessageAsync("Unauthorized", 401); + return false; + } + + var secretService = context.RequestServices.GetRequiredService(); + var permissions = secretService.GetPermissions(token); + if (!permissions.Contains(permission)) + { + context.ResponseMessageAsync("Forbidden", 403); + return false; + } + return true; + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/ISecretService.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/ISecretService.cs new file mode 100644 index 0000000..83025f8 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/ISecretService.cs @@ -0,0 +1,8 @@ +namespace CrupestApi.Commons.Secrets; + +public interface ISecretService +{ + void CreateTestSecret(string key, string secret); + + List GetPermissions(string secret); +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretInfo.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretInfo.cs new file mode 100644 index 0000000..c3a4de0 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretInfo.cs @@ -0,0 +1,48 @@ +using System.Security.Cryptography; +using System.Text; +using CrupestApi.Commons.Crud; + +namespace CrupestApi.Commons.Secrets; + +public class SecretInfo +{ + [Column(NotNull = true)] + public string Key { get; set; } = default!; + [Column(NotNull = true, NoUpdate = true, ActAsKey = true)] + public string Secret { get; set; } = default!; + [Column(DefaultEmptyForString = true)] + public string Description { get; set; } = default!; + [Column(NotNull = false)] + public DateTime? ExpireTime { get; set; } + [Column(NotNull = true, DefaultValue = false)] + public bool Revoked { get; set; } + [Column(NotNull = true)] + public DateTime CreateTime { get; set; } + + private static RandomNumberGenerator RandomNumberGenerator = RandomNumberGenerator.Create(); + + private static string GenerateRandomKey(int length) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + var result = new StringBuilder(length); + lock (RandomNumberGenerator) + { + for (int i = 0; i < length; i++) + { + result.Append(chars[RandomNumberGenerator.GetInt32(chars.Length)]); + } + } + return result.ToString(); + } + + + public static string SecretDefaultValueGenerator() + { + return GenerateRandomKey(16); + } + + public static DateTime CreateTimeDefaultValueGenerator() + { + return DateTime.UtcNow; + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs new file mode 100644 index 0000000..c693d8d --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs @@ -0,0 +1,48 @@ +using System.Data; +using CrupestApi.Commons.Crud; +using CrupestApi.Commons.Crud.Migrations; + +namespace CrupestApi.Commons.Secrets; + +public class SecretService : CrudService, ISecretService +{ + private readonly ILogger _logger; + + public SecretService(ITableInfoFactory tableInfoFactory, IDbConnectionFactory dbConnectionFactory, IDatabaseMigrator migrator, ILoggerFactory loggerFactory) + : base(tableInfoFactory, dbConnectionFactory, migrator, loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + protected override void AfterMigrate(IDbConnection connection, TableInfo table) + { + if (table.SelectCount(connection) == 0) + { + _logger.LogInformation("No secrets found, insert default secrets."); + using var transaction = connection.BeginTransaction(); + var insertClause = InsertClause.Create() + .Add(nameof(SecretInfo.Key), SecretsConstants.SecretManagementKey) + .Add(nameof(SecretInfo.Secret), "crupest") + .Add(nameof(SecretInfo.Description), "This is the init key. Please revoke it immediately after creating a new one."); + _table.Insert(connection, insertClause, out var _); + transaction.Commit(); + } + } + + public void CreateTestSecret(string key, string secret) + { + var connection = _dbConnection; + var insertClause = InsertClause.Create() + .Add(nameof(SecretInfo.Key), key) + .Add(nameof(SecretInfo.Secret), secret) + .Add(nameof(SecretInfo.Description), "Test secret."); + _table.Insert(connection, insertClause, out var _); + } + + public List GetPermissions(string secret) + { + var list = _table.Select(_dbConnection, + where: WhereClause.Create().Eq(nameof(SecretInfo.Secret), secret)); + return list.Select(x => x.Key).ToList(); + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretServiceCollectionExtensions.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretServiceCollectionExtensions.cs new file mode 100644 index 0000000..a9c0e5f --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretServiceCollectionExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace CrupestApi.Commons.Secrets; + +public static class SecretServiceCollectionExtensions +{ + public static IServiceCollection AddSecrets(this IServiceCollection services) + { + services.TryAddScoped(); + return services; + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretsConstants.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretsConstants.cs new file mode 100644 index 0000000..207cc45 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretsConstants.cs @@ -0,0 +1,6 @@ +namespace CrupestApi.Commons.Secrets; + +public static class SecretsConstants +{ + public const string SecretManagementKey = "crupest.secrets.management"; +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Files/CrupestApi.Files.csproj b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Files/CrupestApi.Files.csproj new file mode 100644 index 0000000..2221809 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Files/CrupestApi.Files.csproj @@ -0,0 +1,20 @@ + + + + + + + + + + + + + net7.0 + library + enable + enable + false + + + diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Files/FilesService.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Files/FilesService.cs new file mode 100644 index 0000000..c851a92 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Files/FilesService.cs @@ -0,0 +1,6 @@ +namespace CrupestApi.Files; + +public class FilesService +{ + +} \ No newline at end of file diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Secrets/CrupestApi.Secrets.csproj b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Secrets/CrupestApi.Secrets.csproj new file mode 100644 index 0000000..70c83f3 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Secrets/CrupestApi.Secrets.csproj @@ -0,0 +1,20 @@ + + + + + + + + + + + + + net7.0 + library + enable + enable + false + + + diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretsExtensions.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretsExtensions.cs new file mode 100644 index 0000000..e09887b --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretsExtensions.cs @@ -0,0 +1,19 @@ +using CrupestApi.Commons.Secrets; +using CrupestApi.Commons.Crud; + +namespace CrupestApi.Secrets; + +public static class SecretsExtensions +{ + public static IServiceCollection AddSecrets(this IServiceCollection services) + { + services.AddCrud(); + return services; + } + + public static WebApplication MapSecrets(this WebApplication webApplication, string path = "/api/secrets") + { + webApplication.MapCrud(path, SecretsConstants.SecretManagementKey); + return webApplication; + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/CrupestApi.Todos.csproj b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/CrupestApi.Todos.csproj new file mode 100644 index 0000000..86460e3 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/CrupestApi.Todos.csproj @@ -0,0 +1,15 @@ + + + + + + + + net7.0 + library + enable + enable + false + + + diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosConfiguration.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosConfiguration.cs new file mode 100644 index 0000000..e8160d2 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosConfiguration.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace CrupestApi.Todos; + +public class TodosConfiguration +{ + [Required] + public string Username { get; set; } = default!; + [Required] + public int ProjectNumber { get; set; } = default!; + [Required] + public string Token { get; set; } = default!; + public int Count { get; set; } +} \ No newline at end of file diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosService.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosService.cs new file mode 100644 index 0000000..5839086 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosService.cs @@ -0,0 +1,163 @@ +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Options; + +namespace CrupestApi.Todos; + +public class TodosItem +{ + public string Status { get; set; } = default!; + public string Title { get; set; } = default!; + public bool Closed { get; set; } + public string Color { get; set; } = default!; +} + +public class TodosService +{ + private readonly IOptionsSnapshot _options; + private readonly ILogger _logger; + + public TodosService(IOptionsSnapshot options, ILogger logger) + { + _options = options; + _logger = logger; + } + + private static string CreateGraphQLQuery(TodosConfiguration todoConfiguration) + { + return $$""" +{ + user(login: "{{todoConfiguration.Username}}") { + projectV2(number: {{todoConfiguration.ProjectNumber}}) { + items(last: {{todoConfiguration.Count}}) { + nodes { + fieldValueByName(name: "Status") { + ... on ProjectV2ItemFieldSingleSelectValue { + name + } + } + content { + __typename + ... on Issue { + title + closed + } + ... on PullRequest { + title + closed + } + ... on DraftIssue { + title + } + } + } + } + } + } +} +"""; + } + + + public async Task> GetTodosAsync() + { + var todoOptions = _options.Value; + if (todoOptions is null) + { + throw new Exception("Fail to get todos configuration."); + } + + _logger.LogInformation("Username: {}; ProjectNumber: {}; Count: {}", todoOptions.Username, todoOptions.ProjectNumber, todoOptions.Count); + _logger.LogInformation("Getting todos from GitHub GraphQL API..."); + + using var httpClient = new HttpClient(); + + using var requestContent = new StringContent(JsonSerializer.Serialize(new + { + query = CreateGraphQLQuery(todoOptions) + })); + requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Json, Encoding.UTF8.WebName); + + using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.github.com/graphql"); + request.Content = requestContent; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", todoOptions.Token); + request.Headers.TryAddWithoutValidation("User-Agent", todoOptions.Username); + + using var response = await httpClient.SendAsync(request); + var responseBody = await response.Content.ReadAsStringAsync(); + + _logger.LogInformation("GitHub server returned status code: {}", response.StatusCode); + _logger.LogInformation("GitHub server returned body: {}", responseBody); + + if (response.IsSuccessStatusCode) + { + using var responseJson = JsonSerializer.Deserialize(responseBody); + if (responseJson is null) + { + throw new Exception("Fail to deserialize response body."); + } + + var nodes = responseJson.RootElement.GetProperty("data").GetProperty("user").GetProperty("projectV2").GetProperty("items").GetProperty("nodes").EnumerateArray(); + + var result = new List(); + + foreach (var node in nodes) + { + var content = node.GetProperty("content"); + var title = content.GetProperty("title").GetString(); + if (title is null) + { + throw new Exception("Fail to get title."); + } + + bool done = false; + + var statusField = node.GetProperty("fieldValueByName"); + if (statusField.ValueKind != JsonValueKind.Null) // if there is a "Status" field + { + var statusName = statusField.GetProperty("name").GetString(); + if (statusName is null) + { + throw new Exception("Fail to get status."); + } + + // if name is "Done", then it is closed, otherwise we check if the issue is closed + if (statusName.Equals("Done", StringComparison.OrdinalIgnoreCase)) + { + done = true; + } + } + + JsonElement closedElement; + // if item has a "closed" field, then it is a pull request or an issue, and we check if it is closed + if (content.TryGetProperty("closed", out closedElement) && closedElement.GetBoolean()) + { + done = true; + } + + // If item "Status" field is "Done' or item is a pull request or issue and it is closed, then it is done. + // Otherwise it is not closed. Like: + // 1. it is a draft issue with no "Status" field or "Status" field is not "Done" + // 2. it is a pull request or issue with no "Status" field or "Status" field is not "Done" and it is not closed + + result.Add(new TodosItem + { + Title = title, + Status = done ? "Done" : "Todo", + Closed = done, + Color = done ? "green" : "blue" + }); + } + + return result; + } + else + { + const string message = "Fail to get todos from GitHub."; + _logger.LogError(message); + throw new Exception(message); + } + } +} diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosServiceCollectionExtensions.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosServiceCollectionExtensions.cs new file mode 100644 index 0000000..a49d55d --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosServiceCollectionExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace CrupestApi.Todos; + +public static class TodosServiceCollectionExtensions +{ + public static IServiceCollection AddTodos(this IServiceCollection services) + { + services.AddOptions().BindConfiguration("CrupestApi:Todos"); + services.PostConfigure(config => + { + if (config.Count == 0) + { + config.Count = 20; + } + }); + services.TryAddScoped(); + return services; + } +} + diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosWebApplicationExtensions.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosWebApplicationExtensions.cs new file mode 100644 index 0000000..0ff05a0 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosWebApplicationExtensions.cs @@ -0,0 +1,32 @@ +using CrupestApi.Commons; + +namespace CrupestApi.Todos; + +public static class TodosWebApplicationExtensions +{ + public static WebApplication MapTodos(this WebApplication app, string path) + { + if (app is null) + { + throw new ArgumentNullException(nameof(app)); + } + + app.MapGet(path, async (context) => + { + var todosService = context.RequestServices.GetRequiredService(); + + try + { + var todos = await todosService.GetTodosAsync(); + await context.Response.WriteJsonAsync(todos); + + } + catch (Exception e) + { + await context.Response.WriteMessageAsync(e.Message, statusCode: StatusCodes.Status503ServiceUnavailable); + } + }); + + return app; + } +} \ No newline at end of file diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.sln b/dropped/docker/crupest-api/CrupestApi/CrupestApi.sln new file mode 100644 index 0000000..ebfd960 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.sln @@ -0,0 +1,46 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrupestApi", "CrupestApi\CrupestApi.csproj", "{E30916BB-08F9-45F0-BC1A-69B66AE79913}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrupestApi.Todos", "CrupestApi.Todos\CrupestApi.Todos.csproj", "{BF9F5F71-AE65-4896-8E6F-FE0D4AD0E7D1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrupestApi.Secrets", "CrupestApi.Secrets\CrupestApi.Secrets.csproj", "{9A7CC9F9-70CB-408A-ADFC-5119C0BDB236}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrupestApi.Commons", "CrupestApi.Commons\CrupestApi.Commons.csproj", "{38083CCA-E56C-4D24-BAB6-EEC30E0F478F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrupestApi.Commons.Tests", "CrupestApi.Commons.Tests\CrupestApi.Commons.Tests.csproj", "{0D0304BF-6A18-444C-BAF4-6ABFF98A0F77}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E30916BB-08F9-45F0-BC1A-69B66AE79913}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E30916BB-08F9-45F0-BC1A-69B66AE79913}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E30916BB-08F9-45F0-BC1A-69B66AE79913}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E30916BB-08F9-45F0-BC1A-69B66AE79913}.Release|Any CPU.Build.0 = Release|Any CPU + {BF9F5F71-AE65-4896-8E6F-FE0D4AD0E7D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF9F5F71-AE65-4896-8E6F-FE0D4AD0E7D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF9F5F71-AE65-4896-8E6F-FE0D4AD0E7D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF9F5F71-AE65-4896-8E6F-FE0D4AD0E7D1}.Release|Any CPU.Build.0 = Release|Any CPU + {9A7CC9F9-70CB-408A-ADFC-5119C0BDB236}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A7CC9F9-70CB-408A-ADFC-5119C0BDB236}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A7CC9F9-70CB-408A-ADFC-5119C0BDB236}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A7CC9F9-70CB-408A-ADFC-5119C0BDB236}.Release|Any CPU.Build.0 = Release|Any CPU + {38083CCA-E56C-4D24-BAB6-EEC30E0F478F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38083CCA-E56C-4D24-BAB6-EEC30E0F478F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38083CCA-E56C-4D24-BAB6-EEC30E0F478F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38083CCA-E56C-4D24-BAB6-EEC30E0F478F}.Release|Any CPU.Build.0 = Release|Any CPU + {0D0304BF-6A18-444C-BAF4-6ABFF98A0F77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D0304BF-6A18-444C-BAF4-6ABFF98A0F77}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D0304BF-6A18-444C-BAF4-6ABFF98A0F77}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D0304BF-6A18-444C-BAF4-6ABFF98A0F77}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi/CrupestApi.csproj b/dropped/docker/crupest-api/CrupestApi/CrupestApi/CrupestApi.csproj new file mode 100644 index 0000000..5954f00 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi/CrupestApi.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + net7.0 + enable + enable + false + + + \ No newline at end of file diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi/Program.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi/Program.cs new file mode 100644 index 0000000..46648d9 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi/Program.cs @@ -0,0 +1,24 @@ +using CrupestApi.Commons; +using CrupestApi.Commons.Crud; +using CrupestApi.Secrets; +using CrupestApi.Todos; + +var builder = WebApplication.CreateBuilder(args); + +string configFilePath = Environment.GetEnvironmentVariable("CRUPEST_API_CONFIG_FILE") ?? "/crupest-api-config.json"; +builder.Configuration.AddJsonFile(configFilePath, optional: false, reloadOnChange: true); + +builder.Services.AddJsonOptions(); +builder.Services.AddCrupestApiConfig(); + +builder.Services.AddTodos(); +builder.Services.AddSecrets(); + +var app = builder.Build(); + +app.UseCrudCore(); +app.MapTodos("/api/todos"); +// TODO: It's not safe now! +// app.MapSecrets("/api/secrets"); + +app.Run(); diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi/Properties/launchSettings.json b/dropped/docker/crupest-api/CrupestApi/CrupestApi/Properties/launchSettings.json new file mode 100644 index 0000000..a4a5cbf --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "dev": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5188", + "workingDirectory": ".", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "CRUPEST_API_CONFIG_FILE": "dev-config.json" + } + } + } +} \ No newline at end of file diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi/appsettings.json b/dropped/docker/crupest-api/CrupestApi/CrupestApi/appsettings.json new file mode 100644 index 0000000..53753bd --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/dropped/docker/crupest-api/Dockerfile b/dropped/docker/crupest-api/Dockerfile new file mode 100644 index 0000000..feb7522 --- /dev/null +++ b/dropped/docker/crupest-api/Dockerfile @@ -0,0 +1,13 @@ +FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS build +COPY CrupestApi /CrupestApi +WORKDIR /CrupestApi +RUN dotnet publish CrupestApi/CrupestApi.csproj --configuration Release --output ./publish -r linux-x64 + +FROM mcr.microsoft.com/dotnet/aspnet:7.0-alpine +ENV ASPNETCORE_URLS=http://0.0.0.0:5000 +ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true +COPY --from=build /CrupestApi/publish /CrupestApi +WORKDIR /CrupestApi +VOLUME [ "/crupest-api-config.json" ] +EXPOSE 5000 +ENTRYPOINT ["dotnet", "CrupestApi.dll"] diff --git a/dropped/template/crupest-api-config.json.template b/dropped/template/crupest-api-config.json.template new file mode 100644 index 0000000..65a7944 --- /dev/null +++ b/dropped/template/crupest-api-config.json.template @@ -0,0 +1,10 @@ +{ + "CrupestApi": { + "Todos": { + "Username": "$CRUPEST_GITHUB_USERNAME", + "ProjectNumber": "$CRUPEST_GITHUB_PROJECT_NUMBER", + "Token": "$CRUPEST_GITHUB_TOKEN", + "Count": "$CRUPEST_GITHUB_TODO_COUNT" + } + } +} diff --git a/dropped/template/docker-compose.yaml.template b/dropped/template/docker-compose.yaml.template new file mode 100644 index 0000000..73ff4e8 --- /dev/null +++ b/dropped/template/docker-compose.yaml.template @@ -0,0 +1,24 @@ +services: + + timeline: + image: crupest/timeline:latest + pull_policy: always + container_name: timeline + restart: on-failure:3 + environment: + - ASPNETCORE_FORWARDEDHEADERS_ENABLED=true + - TIMELINE_DisableAutoBackup=true + volumes: + - ./data/timeline:/root/timeline + + crupest-api: + pull_policy: build + build: + context: ./docker/crupest-api + dockerfile: Dockerfile + pull: true + tags: + - "crupest/crupest-api:latest" + container_name: crupest-api + volumes: + - "./crupest-api-config.json:/crupest-api-config.json:ro" diff --git a/dropped/template/nginx/timeline.conf.template b/dropped/template/nginx/timeline.conf.template new file mode 100644 index 0000000..551e0ae --- /dev/null +++ b/dropped/template/nginx/timeline.conf.template @@ -0,0 +1,21 @@ +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name timeline.${CRUPEST_DOMAIN}; + + location / { + include common/reverse-proxy; + proxy_pass http://timeline:5000/; + } + + client_max_body_size 5G; +} + +server { + listen 80; + listen [::]:80; + server_name timeline.${CRUPEST_DOMAIN}; + + include common/https-redirect; + include common/acme-challenge; +} diff --git a/dropped/template/v2ray-client-config.json.template b/dropped/template/v2ray-client-config.json.template new file mode 100644 index 0000000..0c99c6d --- /dev/null +++ b/dropped/template/v2ray-client-config.json.template @@ -0,0 +1,46 @@ +{ + "inbounds": [ + { + "port": 1080, + "listen": "127.0.0.1", + "protocol": "socks", + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls" + ] + }, + "settings": { + "auth": "noauth", + "udp": false + } + } + ], + "outbounds": [ + { + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "$CRUPEST_DOMAIN", + "port": 443, + "users": [ + { + "id": "$CRUPEST_V2RAY_TOKEN", + "alterId": 0 + } + ] + } + ] + }, + "streamSettings": { + "network": "ws", + "security": "tls", + "wsSettings": { + "path": "/_$CRUPEST_V2RAY_PATH" + } + } + } + ] +} \ No newline at end of file diff --git a/template/crupest-api-config.json.template b/template/crupest-api-config.json.template deleted file mode 100644 index 65a7944..0000000 --- a/template/crupest-api-config.json.template +++ /dev/null @@ -1,10 +0,0 @@ -{ - "CrupestApi": { - "Todos": { - "Username": "$CRUPEST_GITHUB_USERNAME", - "ProjectNumber": "$CRUPEST_GITHUB_PROJECT_NUMBER", - "Token": "$CRUPEST_GITHUB_TOKEN", - "Count": "$CRUPEST_GITHUB_TODO_COUNT" - } - } -} diff --git a/template/docker-compose.yaml.template b/template/docker-compose.yaml.template index d86aae6..cebdfb3 100644 --- a/template/docker-compose.yaml.template +++ b/template/docker-compose.yaml.template @@ -1,38 +1,27 @@ services: - timeline: - image: crupest/timeline:latest - pull_policy: always - container_name: timeline - restart: on-failure:3 - environment: - - ASPNETCORE_FORWARDEDHEADERS_ENABLED=true - - TIMELINE_DisableAutoBackup=true - volumes: - - ./data/timeline:/root/timeline - crupest-blog: + blog: pull_policy: build build: - context: ./docker/crupest-blog + context: ./docker/blog dockerfile: Dockerfile pull: true tags: - - "crupest/crupest-blog:latest" - container_name: crupest-blog - restart: on-failure:3 + - "crupest/blog:latest" + container_name: blog volumes: - "blog-public:/public" + restart: on-failure:3 nginx: pull_policy: build build: - context: ./docker/crupest-nginx + context: ./docker/nginx dockerfile: Dockerfile pull: true tags: - - "crupest/crupest-nginx:latest" + - "crupest/nginx:latest" container_name: nginx - restart: on-failure:3 ports: - "80:80" - "443:443" @@ -41,33 +30,22 @@ services: - "./nginx-config:/etc/nginx/conf.d:ro" - "./data/certbot/certs:/etc/letsencrypt:ro" - "./data/certbot/webroot:/srv/acme:ro" - - "blog-public:/srv/blog:ro" - - crupest-api: - pull_policy: build - build: - context: ./docker/crupest-api - dockerfile: Dockerfile - pull: true - tags: - - "crupest/crupest-api:latest" - container_name: crupest-api - volumes: - - "./crupest-api-config.json:/crupest-api-config.json:ro" + - "blog-public:/srv/www/blog:ro" + restart: on-failure:3 v2ray: pull_policy: build build: - context: ./docker/crupest-v2ray + context: ./docker/v2ray dockerfile: Dockerfile pull: true tags: - - "crupest/crupest-v2ray:latest" - command: [ "run", "-c", "/etc/v2fly/config.json" ] + - "crupest/v2ray:latest" container_name: v2ray - restart: on-failure:3 + command: [ "run", "-c", "/etc/v2fly/config.json" ] volumes: - "./v2ray-config.json:/etc/v2fly/config.json:ro" + restart: on-failure:3 auto-certbot: pull_policy: build @@ -148,7 +126,7 @@ services: debian-dev: pull_policy: build build: - context: ./docker/crupest-debian-dev + context: ./docker/debian-dev dockerfile: Dockerfile pull: true args: @@ -158,16 +136,15 @@ services: container_name: debian-dev init: true command: [ "/bootstrap/start/code-server.bash" ] - restart: on-failure:3 volumes: - ./data/debian-dev:/data - debian-dev-home:/home/crupest + restart: on-failure:3 forgejo: image: code.forgejo.org/forgejo/forgejo:9 pull_policy: always container_name: forgejo - restart: on-failure:3 environment: - USER_UID=1000 - USER_GID=1000 @@ -175,11 +152,11 @@ services: - ./data/forgejo:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro + restart: on-failure:3 roundcubemail: image: roundcube/roundcubemail:latest container_name: roundcubemail - restart: on-failure:3 volumes: - ./data/secret/gnupg:/gnupg - ./data/roundcube/www/html:/var/www/html @@ -193,6 +170,7 @@ services: - ROUNDCUBEMAIL_SMTP_PORT=465 - ROUNDCUBEMAIL_DB_TYPE=sqlite - ROUNDCUBEMAIL_PLUGINS=archive,enigma,jqueryui,newmail_notifier,show_additional_headers,userinfo,zipdownload + restart: on-failure:3 2fauth: image: 2fauth/2fauth @@ -218,4 +196,4 @@ services: volumes: blog-public: debian-dev-home: - roundcubemail-temp: \ No newline at end of file + roundcubemail-temp: diff --git a/template/nginx/2fa.conf.template b/template/nginx/2fa.conf.template new file mode 100644 index 0000000..aad66c1 --- /dev/null +++ b/template/nginx/2fa.conf.template @@ -0,0 +1,19 @@ +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name 2fa.${CRUPEST_DOMAIN}; + + location / { + include common/proxy-common; + proxy_pass http://2fauth:8000/; + } +} + +server { + listen 80; + listen [::]:80; + server_name 2fa.${CRUPEST_DOMAIN}; + + include common/https-redirect; + include common/acme-challenge; +} diff --git a/template/nginx/cert-only.conf.template b/template/nginx/cert-only.conf.template deleted file mode 100644 index 08daa8a..0000000 --- a/template/nginx/cert-only.conf.template +++ /dev/null @@ -1,13 +0,0 @@ -server { - listen 80; - listen [::]:80; - server_name ${CRUPEST_NGINX_SUBDOMAIN}.${CRUPEST_DOMAIN}; - - location / { - return 444; - } - - location /.well-known/acme-challenge { - root /srv/acme; - } -} diff --git a/template/nginx/code.conf.template b/template/nginx/code.conf.template new file mode 100644 index 0000000..a67500d --- /dev/null +++ b/template/nginx/code.conf.template @@ -0,0 +1,22 @@ +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name code.${CRUPEST_DOMAIN}; + + location / { + include common/proxy-common; + proxy_pass http://debian-dev:8080/; + } + + client_max_body_size 5G; +} + + +server { + listen 80; + listen [::]:80; + server_name code.${CRUPEST_DOMAIN}; + + include common/https-redirect; + include common/acme-challenge; +} diff --git a/template/nginx/common/acme-challenge b/template/nginx/common/acme-challenge new file mode 100644 index 0000000..26054b8 --- /dev/null +++ b/template/nginx/common/acme-challenge @@ -0,0 +1,3 @@ +location /.well-known/acme-challenge { + root /srv/acme; +} diff --git a/template/nginx/common/https-redirect b/template/nginx/common/https-redirect new file mode 100644 index 0000000..56d095d --- /dev/null +++ b/template/nginx/common/https-redirect @@ -0,0 +1,3 @@ +location / { + return 301 https://$host$request_uri; +} diff --git a/template/nginx/common/proxy-common b/template/nginx/common/proxy-common new file mode 100644 index 0000000..4193548 --- /dev/null +++ b/template/nginx/common/proxy-common @@ -0,0 +1,7 @@ +proxy_http_version 1.1; +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection $connection_upgrade; +proxy_set_header Host $host; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Real-IP $remote_addr; diff --git a/template/nginx/git.conf.template b/template/nginx/git.conf.template new file mode 100644 index 0000000..ea2a627 --- /dev/null +++ b/template/nginx/git.conf.template @@ -0,0 +1,22 @@ +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name git.${CRUPEST_DOMAIN}; + + location / { + include common/proxy-common; + proxy_pass http://forgejo:3000/; + } + + client_max_body_size 5G; +} + + +server { + listen 80; + listen [::]:80; + server_name git.${CRUPEST_DOMAIN}; + + include common/https-redirect; + include common/acme-challenge; +} diff --git a/template/nginx/mail.conf.template b/template/nginx/mail.conf.template new file mode 100644 index 0000000..ba2e44e --- /dev/null +++ b/template/nginx/mail.conf.template @@ -0,0 +1,27 @@ +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name mail.${CRUPEST_DOMAIN}; + + location / { + include common/proxy-common; + proxy_pass http://roundcubemail:80/; + } + + location /rspamd/ { + include common/proxy-common; + proxy_pass http://mailserver:11334/; + } + + client_max_body_size 5G; +} + + +server { + listen 80; + listen [::]:80; + server_name mail.${CRUPEST_DOMAIN}; + + include common/https-redirect; + include common/acme-challenge; +} diff --git a/template/nginx/redirect.conf.template b/template/nginx/redirect.conf.template deleted file mode 100644 index b3122e2..0000000 --- a/template/nginx/redirect.conf.template +++ /dev/null @@ -1,23 +0,0 @@ -server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name ${CRUPEST_NGINX_SUBDOMAIN}.${CRUPEST_DOMAIN}; - - location / { - return 301 ${CRUPEST_NGINX_URL}$request_uri; - } -} - -server { - listen 80; - listen [::]:80; - server_name ${CRUPEST_NGINX_SUBDOMAIN}.${CRUPEST_DOMAIN}; - - location / { - return 301 ${CRUPEST_NGINX_URL}$request_uri; - } - - location /.well-known/acme-challenge { - root /srv/acme; - } -} diff --git a/template/nginx/reverse-proxy.conf.template b/template/nginx/reverse-proxy.conf.template deleted file mode 100644 index 01442ab..0000000 --- a/template/nginx/reverse-proxy.conf.template +++ /dev/null @@ -1,32 +0,0 @@ -server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name ${CRUPEST_NGINX_SUBDOMAIN}.${CRUPEST_DOMAIN}; - - location / { - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass http://${CRUPEST_NGINX_UPSTREAM_SERVER}; - } - - client_max_body_size 5G; -} - -server { - listen 80; - listen [::]:80; - server_name ${CRUPEST_NGINX_SUBDOMAIN}.${CRUPEST_DOMAIN}; - - location / { - return 301 https://$host$request_uri; - } - - location /.well-known/acme-challenge { - root /srv/acme; - } -} diff --git a/template/nginx/root.conf.template b/template/nginx/root.conf.template index 21d144c..3f20cf1 100644 --- a/template/nginx/root.conf.template +++ b/template/nginx/root.conf.template @@ -13,27 +13,9 @@ server { } proxy_redirect off; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Real-IP $remote_addr; + include common/proxy-common; proxy_pass http://v2ray:10000; } - - location /api { - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass http://crupest-api:5000; - } - } server { @@ -41,43 +23,6 @@ server { listen [::]:80; server_name ${CRUPEST_DOMAIN}; - location / { - return 301 https://$host$request_uri; - } - - location /.well-known/acme-challenge { - root /srv/acme; - } -} - -# For mail temporarily -# TODO: Make subpath supported in aio. -server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name mail.${CRUPEST_DOMAIN}; - - location / { - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass http://roundcubemail:80/; - } - - location /rspamd/ { - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass http://mailserver:11334/; - } - - client_max_body_size 5G; + include common/https-redirect; + include common/acme-challenge; } diff --git a/template/nginx/server.json b/template/nginx/server.json deleted file mode 100644 index fa84030..0000000 --- a/template/nginx/server.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "./server.schema.json", - "sites": [ - { - "type": "reverse-proxy", - "subdomain": "timeline", - "upstream": "timeline:5000" - }, - { - "type": "reverse-proxy", - "subdomain": "code", - "upstream": "debian-dev:8080" - }, - { - "type": "reverse-proxy", - "subdomain": "git", - "upstream": "forgejo:3000" - }, - { - "type": "cert-only", - "subdomain": "mail" - }, - { - "type": "reverse-proxy", - "subdomain": "2fa", - "upstream": "2fauth:8000" - }, - { - "type": "static-file", - "subdomain": "blog", - "root": "/srv/blog" - }, - { - "type": "redirect", - "subdomain": "github", - "url": "https://github.com/crupest" - } - ] -} diff --git a/template/nginx/server.schema.json b/template/nginx/server.schema.json deleted file mode 100644 index c3bc7c0..0000000 --- a/template/nginx/server.schema.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "CertOnlySite": { - "properties": { - "subdomain": { - "type": "string" - }, - "type": { - "enum": [ - "cert-only" - ], - "type": "string" - } - }, - "type": "object" - }, - "RedirectSite": { - "properties": { - "subdomain": { - "type": "string" - }, - "type": { - "enum": [ - "redirect" - ], - "type": "string" - }, - "url": { - "type": "string" - } - }, - "type": "object" - }, - "ReverseProxySite": { - "properties": { - "subdomain": { - "type": "string" - }, - "type": { - "enum": [ - "reverse-proxy" - ], - "type": "string" - }, - "upstream": { - "type": "string" - } - }, - "type": "object" - }, - "StaticFileSite": { - "properties": { - "root": { - "type": "string" - }, - "subdomain": { - "type": "string" - }, - "type": { - "enum": [ - "static-file" - ], - "type": "string" - } - }, - "type": "object" - } - }, - "properties": { - "sites": { - "items": { - "anyOf": [ - { - "$ref": "#/definitions/ReverseProxySite" - }, - { - "$ref": "#/definitions/StaticFileSite" - }, - { - "$ref": "#/definitions/RedirectSite" - }, - { - "$ref": "#/definitions/CertOnlySite" - } - ] - }, - "type": "array" - } - }, - "type": "object" -} - diff --git a/template/nginx/server.ts b/template/nginx/server.ts deleted file mode 100644 index 368e5ff..0000000 --- a/template/nginx/server.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Used to generate json schema. - -export interface ReverseProxySite { - type: "reverse-proxy"; - subdomain: string; - upstream: string; -} - -export interface StaticFileSite { - type: "static-file"; - subdomain: string; - root: string; -} - -export interface RedirectSite { - type: "redirect"; - subdomain: string; - url: string; -} - -export interface CertOnlySite { - type: "cert-only"; - subdomain: string; -} - -export type Site = - | ReverseProxySite - | StaticFileSite - | RedirectSite - | CertOnlySite; - -export type Sites = Site[]; - -export interface Server { - sites: Sites; -} diff --git a/template/nginx/static-file.conf.template b/template/nginx/static-file.conf.template deleted file mode 100644 index 3022886..0000000 --- a/template/nginx/static-file.conf.template +++ /dev/null @@ -1,23 +0,0 @@ -server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name ${CRUPEST_NGINX_SUBDOMAIN}.${CRUPEST_DOMAIN}; - - location / { - root ${CRUPEST_NGINX_ROOT}; - } -} - -server { - listen 80; - listen [::]:80; - server_name ${CRUPEST_NGINX_SUBDOMAIN}.${CRUPEST_DOMAIN}; - - location / { - return 301 https://$host$request_uri; - } - - location /.well-known/acme-challenge { - root /srv/acme; - } -} diff --git a/template/nginx/timeline.conf.template b/template/nginx/timeline.conf.template new file mode 100644 index 0000000..db908e8 --- /dev/null +++ b/template/nginx/timeline.conf.template @@ -0,0 +1,7 @@ +server { + listen 80; + listen [::]:80; + server_name timeline.${CRUPEST_DOMAIN}; + + include common/acme-challenge; +} diff --git a/template/v2ray-client-config.json.template b/template/v2ray-client-config.json.template deleted file mode 100644 index 0c99c6d..0000000 --- a/template/v2ray-client-config.json.template +++ /dev/null @@ -1,46 +0,0 @@ -{ - "inbounds": [ - { - "port": 1080, - "listen": "127.0.0.1", - "protocol": "socks", - "sniffing": { - "enabled": true, - "destOverride": [ - "http", - "tls" - ] - }, - "settings": { - "auth": "noauth", - "udp": false - } - } - ], - "outbounds": [ - { - "protocol": "vmess", - "settings": { - "vnext": [ - { - "address": "$CRUPEST_DOMAIN", - "port": 443, - "users": [ - { - "id": "$CRUPEST_V2RAY_TOKEN", - "alterId": 0 - } - ] - } - ] - }, - "streamSettings": { - "network": "ws", - "security": "tls", - "wsSettings": { - "path": "/_$CRUPEST_V2RAY_PATH" - } - } - } - ] -} \ No newline at end of file diff --git a/template2/nginx/domain.conf.template b/template2/nginx/domain.conf.template deleted file mode 100644 index 7fa2d7a..0000000 --- a/template2/nginx/domain.conf.template +++ /dev/null @@ -1,19 +0,0 @@ -server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name ${DOMAIN}; - -${HTTPS_SEGMENT} -} - -server { - listen 80; - listen [::]:80; - server_name ${DOMAIN}; - -${HTTP_SEGMENT} - - location /.well-known/acme-challenge { - root /srv/acme; - } -} diff --git a/template2/nginx/global/client-max-body-size.conf b/template2/nginx/global/client-max-body-size.conf deleted file mode 100644 index a2b1c00..0000000 --- a/template2/nginx/global/client-max-body-size.conf +++ /dev/null @@ -1 +0,0 @@ -client_max_body_size 5G; diff --git a/template2/nginx/global/forbid-unknown-domain.conf b/template2/nginx/global/forbid-unknown-domain.conf deleted file mode 100644 index ae96393..0000000 --- a/template2/nginx/global/forbid-unknown-domain.conf +++ /dev/null @@ -1,8 +0,0 @@ -server { - listen 80 default_server; - listen [::]:80 default_server; - listen 443 ssl http2 default_server; - listen [::]:443 ssl http2 default_server; - - return 444; -} diff --git a/template2/nginx/global/ssl.conf.template b/template2/nginx/global/ssl.conf.template deleted file mode 100644 index ff70f5a..0000000 --- a/template2/nginx/global/ssl.conf.template +++ /dev/null @@ -1,17 +0,0 @@ -# This file contains important security parameters. If you modify this file -# manually, Certbot will be unable to automatically provide future security -# updates. Instead, Certbot will print and log an error message with a path to -# the up-to-date file that you will need to refer to when manually updating -# this file. Contents are based on https://ssl-config.mozilla.org - -ssl_certificate /etc/letsencrypt/live/${ROOT_DOMAIN}/fullchain.pem; -ssl_certificate_key /etc/letsencrypt/live/${ROOT_DOMAIN}/privkey.pem; - -ssl_session_cache shared:le_nginx_SSL:10m; -ssl_session_timeout 1440m; -ssl_session_tickets off; - -ssl_protocols TLSv1.2 TLSv1.3; -ssl_prefer_server_ciphers off; - -ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; diff --git a/template2/nginx/global/websocket.conf b/template2/nginx/global/websocket.conf deleted file mode 100644 index 32af4c3..0000000 --- a/template2/nginx/global/websocket.conf +++ /dev/null @@ -1,4 +0,0 @@ -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} diff --git a/template2/nginx/http/444.segment b/template2/nginx/http/444.segment deleted file mode 100644 index fe490d4..0000000 --- a/template2/nginx/http/444.segment +++ /dev/null @@ -1,3 +0,0 @@ -location / { - return 444; -} diff --git a/template2/nginx/http/redirect-to-https.segment b/template2/nginx/http/redirect-to-https.segment deleted file mode 100644 index 56d095d..0000000 --- a/template2/nginx/http/redirect-to-https.segment +++ /dev/null @@ -1,3 +0,0 @@ -location / { - return 301 https://$host$request_uri; -} diff --git a/template2/nginx/https/redirect.segment.template b/template2/nginx/https/redirect.segment.template deleted file mode 100644 index 028f617..0000000 --- a/template2/nginx/https/redirect.segment.template +++ /dev/null @@ -1,7 +0,0 @@ -location = ${PATH} { - return ${REDIRECT_CODE} ${REDIRECT_URL}; -} - -location ^${PATH}/(?.*)$ { - return ${REDIRECT_CODE} ${REDIRECT_URL}/$redirect_path; -} diff --git a/template2/nginx/https/reverse-proxy.segment.template b/template2/nginx/https/reverse-proxy.segment.template deleted file mode 100644 index 85a942e..0000000 --- a/template2/nginx/https/reverse-proxy.segment.template +++ /dev/null @@ -1,10 +0,0 @@ -location ${PATH}/ { - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass http://${UPSTREAM}; -} diff --git a/template2/nginx/https/static-file.no-strip-prefix.segment.template b/template2/nginx/https/static-file.no-strip-prefix.segment.template deleted file mode 100644 index 4e829ba..0000000 --- a/template2/nginx/https/static-file.no-strip-prefix.segment.template +++ /dev/null @@ -1,3 +0,0 @@ -location ${PATH}/ { - root ${ROOT}; -} diff --git a/template2/nginx/https/static-file.segment.template b/template2/nginx/https/static-file.segment.template deleted file mode 100644 index 683cad3..0000000 --- a/template2/nginx/https/static-file.segment.template +++ /dev/null @@ -1,3 +0,0 @@ -location ${PATH}/ { - alias ${ROOT}; -} diff --git a/template2/nginx/server.schema.json b/template2/nginx/server.schema.json deleted file mode 100644 index a19c131..0000000 --- a/template2/nginx/server.schema.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "RedirectService": { - "properties": { - "code": { - "type": "number" - }, - "path": { - "type": "string" - }, - "to": { - "type": "string" - }, - "type": { - "enum": [ - "redirect" - ], - "type": "string" - } - }, - "type": "object" - }, - "ReverseProxyService": { - "properties": { - "path": { - "type": "string" - }, - "type": { - "enum": [ - "reverse-proxy" - ], - "type": "string" - }, - "upstream": { - "type": "string" - } - }, - "type": "object" - }, - "StaticFileService": { - "properties": { - "no_strip_prefix": { - "type": "boolean" - }, - "path": { - "type": "string" - }, - "root": { - "type": "string" - }, - "type": { - "enum": [ - "static-file" - ], - "type": "string" - } - }, - "type": "object" - }, - "SubDomain": { - "properties": { - "name": { - "type": "string" - }, - "services": { - "items": { - "anyOf": [ - { - "$ref": "#/definitions/RedirectService" - }, - { - "$ref": "#/definitions/StaticFileService" - }, - { - "$ref": "#/definitions/ReverseProxyService" - } - ] - }, - "type": "array" - } - }, - "type": "object" - } - }, - "properties": { - "domains": { - "items": { - "$ref": "#/definitions/SubDomain" - }, - "type": "array" - } - }, - "type": "object" -} - diff --git a/template2/nginx/server.ts b/template2/nginx/server.ts deleted file mode 100644 index ffd64b7..0000000 --- a/template2/nginx/server.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Used to generate json schema. - -// path should start with "/", end without "/" and contain no special characters in regex. -// the special case is root path "/", which is allowed. - -// For example: -// Given -// path: /a/b -// to: http://c.com/d -// Then (no_strip_prefix is false) -// url: /a/b/c -// redirect to: http://c.com/d/c (/a/b is removed) -// Note: -// Contrary to reverse proxy, you would always want to strip the prefix path. -// Because there is no meaning to redirect to the new page with the original path. -// If you want a domain-only redirect, just specify the path as "/". -export interface RedirectService { - type: "redirect"; - path: string; // must be a path, should start with "/", end without "/" - to: string; // must be a url, should start with scheme (http:// or https://), end without "/" - code?: number; // default to 307 -} - -// For example: -// Given -// path: /a/b -// root: /e/f -// Then (no_strip_prefix is false) -// url: /a/b/c/d -// file path: /e/f/c/d (/a/b is removed) -// Or (no_strip_prefix is true) -// url: /a/b/c/d -// file path: /e/f/a/b/c/d -export interface StaticFileService { - type: "static-file"; - path: string; // must be a path, should start with "/", end without "/" - root: string; // must be a path (directory), should start with "/", end without "/" - no_strip_prefix?: boolean; // default to false. If true, the path prefix is not removed from the url when finding the file. -} - -// For example: -// Given -// path: /a/b -// upstream: another-server:1234 -// Then -// url: /a/b/c/d -// proxy to: another-server:1234/a/b/c/d -// Note: -// Contrary to redirect, you would always want to keep the prefix path. -// Because the upstream server will mess up the path handling if the prefix is not kept. -export interface ReverseProxyService { - type: "reverse-proxy"; - path: string; // must be a path, should start with "/", end without "/" - upstream: string; // should be a [host]:[port], like "localhost:1234" -} - -export type Service = RedirectService | StaticFileService | ReverseProxyService; - -export interface SubDomain { - name: string; // @ for root domain - services: Service[]; -} - -export interface Server { - domains: SubDomain[]; -} diff --git a/template2/server.json.template b/template2/server.json.template deleted file mode 100644 index 22f1251..0000000 --- a/template2/server.json.template +++ /dev/null @@ -1,58 +0,0 @@ -{ - "$schema": "./server.schema.json", - "domains": [ - { - "name": "@", - "services": [ - { - "type": "static-file", - "path": "/", - "root": "/srv/www" - }, - { - "type": "redirect", - "path": "/github", - "to": "https://github.com/crupest" - }, - { - "type": "reverse-proxy", - "path": "/_${V2RAY_PATH}", - "upstream": "crupest-v2ray:10000" - } - ] - }, - { - "name": "code", - "services": [ - { - "type": "reverse-proxy", - "path": "/", - "upstream": "code-server:8080" - } - ] - }, - { - "name": "timeline", - "services": [ - { - "type": "reverse-proxy", - "path": "/", - "upstream": "timeline:5000" - } - ] - }, - { - "name": "blog", - "services": [ - { - "type": "static-file", - "path": "/", - "root": "/srv/blog" - } - ] - }, - { - "name": "mail" - } - ] -} \ No newline at end of file diff --git a/tools/cru-py/.gitignore b/tools/cru-py/.gitignore index 259058c..d40cb9b 100644 --- a/tools/cru-py/.gitignore +++ b/tools/cru-py/.gitignore @@ -2,3 +2,6 @@ venv __pycache__ + +.continuerc.json +.continuerules -- cgit v1.2.3